SniperGold_ML/MQL5/Include/AlgoForge/AF_Engine2_Setup.mqh

563 lines
21 KiB
MQL5

//+------------------------------------------------------------------+
//| AF_Engine2_Setup.mqh |
//| Project : Algo Forge (refactor total) |
//| Sumber inspirasi: SniperGold SMC Pro+ (c) Waseem Shahrukh |
//| https://www.mql5.com/en/code/75466 |
//+------------------------------------------------------------------+
//| ENGINE 2 - F3 CANDIDATE SETUP LAYER (P3-S.13) |
//| |
//| TUJUAN: materialisasi entitas Candidate Setup (frozen contract |
//| docs/SNIPERGOLD_CANONICAL_SETUP_CONTRACT_v1.md): satu setup = |
//| satu identitas = satu lifecycle. LAYER INI TIDAK MENGHITUNG |
//| primitif sendiri; ia MENGONSUMSI input SEMANTIK per bar: |
//| F1 events : sweep/CHoCH {onset, dir} (P3-S.11) |
//| F2 zones : OB/FVG {type, formation, dir, mit, invalidated} |
//| (P3-S.12) |
//| H4/M30 : context STATE (as-of closed bar) |
//| M15 : entry CONDITION (direction-compatible) |
//| M3 : OPTIONAL micro-confirmation (no veto) |
//| dan MENGHASILKAN: AFCandidateSetup (setup_id, direction, state, |
//| referensi sweep/choch/zone, creation, M3, reason). |
//| |
//| KANONIK (frozen): |
//| H4 gate -> M30 gate -> fresh sweep EVENT -> CHoCH EVENT after |
//| sweep -> unmitigated OB|FVG ZONE -> M15 entry condition -> |
//| CANDIDATE_SETUP -> [optional M3 confirmation]. |
//| |
//| Lifecycle: NONE -> CONTEXT_VALID -> LIQUIDITY_TRIGGERED -> |
//| STRUCTURE_CONFIRMED -> ZONE_READY -> ENTRY_ARMED -> |
//| CANDIDATE_SETUP -> [M3_CONFIRMED] -> EXPIRED | INVALIDATED | |
//| CONSUMED. Tidak ada skip state; tidak ada reaktivasi; maksimum |
//| satu entry per setup (frozen §H). |
//| |
//| IDENTITAS (frozen §E): setup_id monotonic; uniqueness = (dir, |
//| sweep onset, choch onset, zone formation, creation). Satu rantai|
//| kausal = satu setup_id (dedup by registry, §G). |
//| |
//| SCORE SEPARATION (frozen §I): score TIDAK pernah menciptakan |
//| setup. Layer ini mengekspos setup_exists/setup_id/state; |
//| scoring boleh melekat setelahnya. |
//| |
//| W_setup / W_m3 = OPEN NUMERIC PARAMETERS; nilai di AF_Defines |
//| adalah PLACEHOLDER TEST BOUNDARY (bukan hasil optimasi). |
//| |
//| BAR SEMANTICS (parity with the F3 spec oracle): decisionBar dan |
//| event onsets adalah index bar MONOTONIK (bertambah seiring |
//| waktu, 0 = bar tertua). Konsumen Engine-1 (reversed index, |
//| 0 = bar tertutup terbaru) harus mengonversi: |
//| mono = totalClosed - 1 - reversed |
//| untuk memberi makan layer ini. |
//+------------------------------------------------------------------+
#ifndef AF_ENGINE2_SETUP_MQH
#define AF_ENGINE2_SETUP_MQH
#include "AF_Engine2_Agents.mqh"
//------------------------------------------------------------------
// Lifecycle state (frozen §F)
//------------------------------------------------------------------
enum AFSetupState
{
AF_SETUP_NONE = 0,
AF_SETUP_CONTEXT_VALID = 1,
AF_SETUP_LIQUIDITY_TRIGGERED= 2,
AF_SETUP_STRUCTURE_CONFIRMED= 3,
AF_SETUP_ZONE_READY = 4,
AF_SETUP_ENTRY_ARMED = 5,
AF_SETUP_CANDIDATE = 6, // CANDIDATE_SETUP (terminal formation)
AF_SETUP_M3_CONFIRMED = 7, // optional micro-confirmed
AF_SETUP_EXPIRED = 8, // terminal
AF_SETUP_INVALIDATED = 9, // terminal
AF_SETUP_CONSUMED = 10 // terminal (one entry emitted)
};
enum AFSetupZoneType
{
AF_SETUP_ZONE_NONE = 0,
AF_SETUP_ZONE_OB = 1,
AF_SETUP_ZONE_FVG = 2
};
//------------------------------------------------------------------
// Event input (F1 contract, P3-S.11) — monotonic onset
//------------------------------------------------------------------
struct AFSetupEvent
{
int onset; // M15 bar index (monotonic) of onset; -1 = none
int dir; // +1 / -1 / 0
};
//------------------------------------------------------------------
// Zone input (F2 contract, P3-S.12) — semantic projection of AFZoneState
//------------------------------------------------------------------
struct AFSetupZone
{
int type; // AF_SETUP_ZONE_OB / AF_SETUP_ZONE_FVG / NONE
int formation; // formation bar (monotonic); -1 = none
int dir; // +1 / -1 / 0
AFZoneMitState mit; // F2 mit_state
bool invalidated; // F2 terminal flag
bool IsActive() const
{
return (mit==AF_ZONE_UNMITIGATED || mit==AF_ZONE_PARTIALLY_FILLED)
&& !invalidated;
}
};
//------------------------------------------------------------------
// Semantic inputs for ONE decision bar (what the F3 layer consumes)
//------------------------------------------------------------------
struct AFSetupInputs
{
int h4; // H4 context STATE direction (+1/-1/0)
int m30; // M30 context STATE direction (+1/-1/0)
AFSetupEvent sweep; // sweep EVENT (onset + dir)
AFSetupEvent choch; // CHoCH EVENT (onset + dir)
AFSetupZone zone; // unmitigated OB|FVG ZONE
int m15; // M15 entry CONDITION direction (+1/-1/0)
int m3; // M3 micro-confirmation (+1/-1/0; 0 = absent)
bool entry; // entry signal bound to the valid setup this bar
double score; // informational (NEVER creates a setup, §I)
};
//------------------------------------------------------------------
// Candidate Setup entity (frozen §E)
//------------------------------------------------------------------
struct AFCandidateSetup
{
int setup_id; // monotonic, assigned at CANDIDATE_SETUP
int direction; // +1 bullish / -1 bearish
AFSetupState state; // lifecycle
// originating liquidity (sweep)
int sweep_onset;
int sweep_dir;
// structure event (choch)
int choch_onset;
int choch_dir;
// zone
AFSetupZoneType zone_type;
int zone_formation;
double zone_top;
double zone_bot;
int zone_dir;
AFZoneMitState zone_mit_at_creation;
// entry timeframe = M15 (fixed by construction)
int creation_bar;
// optional M3 confirmation
int m3_bar; // -1 = none
// provenance / lifecycle reason
string reason;
// one-entry bookkeeping (frozen §H)
bool entry_bound;
};
//------------------------------------------------------------------
// Per-bar outcome (what consumers read)
//------------------------------------------------------------------
struct AFSetupOutcome
{
int bar;
bool setup_exists; // CANDIDATE_SETUP / M3_CONFIRMED
int setup_id;
AFSetupState state;
int dir;
bool blocked; // gate-fail (OD-4)
string block_reason;
bool m3_confirmed;
bool entry_bound;
double score;
AFCandidateSetup entity; // full entity (valid when setup_exists)
};
//------------------------------------------------------------------
// F3 setup engine — STATEFUL runtime (frozen §F/§G/§H).
// Engine-2 detectors are stateless per-bar (P3-S.7 S-ST), but a
// Candidate Setup is by definition an ENTITY with identity + lifecycle
// that persists across bars (contract §A/§E/§F). This engine holds the
// cross-bar state: the current forming/formed setup, the identity
// registry (dedup), and the one-entry bookkeeping. It is driven once
// per CLOSED M15 bar with semantic inputs (consumed, never recomputed).
// The algorithm is a faithful port of the F3 spec oracle
// (spec_tests_candidate_setup_runtime.py, canonical_oracle).
//------------------------------------------------------------------
class AFSetupEngine
{
private:
AFCandidateSetup m_current; // forming/formed setup (id==0 = none)
AFCandidateSetup m_registry[]; // identity registry (dedup by chain key)
int m_nextId;
int m_entryBindings;
// windows (AF_Defines placeholders)
int m_wSweep;
int m_wChoch;
int m_wSetup;
int m_wM3;
void ResetEntity(AFCandidateSetup &s) const;
bool SameChainKey(const AFCandidateSetup &a,const AFCandidateSetup &b) const;
int RegistryHas(const AFCandidateSetup &s) const; // -1 = none
bool EventActive(const AFSetupEvent &ev,int r,int w) const;
void StartChain(const AFSetupInputs &inp); // NONE -> CONTEXT_VALID
void Advance(int r,const AFSetupInputs &inp); // forming stages
void PostFormation(int r,const AFSetupInputs &inp); // terminal/M3
public:
AFSetupEngine()
{
m_wSweep=40; // W_sweep (frozen §M)
m_wChoch=40; // W_choch (frozen §M)
m_wSetup=AF_F3_W_SETUP; // placeholder test boundary
m_wM3=AF_F3_W_M3; // placeholder test boundary
Reset();
}
void Reset()
{
m_nextId=1;
m_entryBindings=0;
ResetEntity(m_current);
ArrayResize(m_registry,0);
}
void SetWindows(int wSweep,int wChoch,int wSetup,int wM3)
{
m_wSweep=wSweep; m_wChoch=wChoch; m_wSetup=wSetup; m_wM3=wM3;
}
// Advance the runtime by ONE closed M15 decision bar.
// decisionBar : monotonic M15 bar index (see header bar semantics).
// inp : semantic inputs (consumed; not computed here).
// out : per-bar outcome (exposed to consumers/scoring).
void Step(int decisionBar,const AFSetupInputs &inp,AFSetupOutcome &out);
// diagnostics
int CreatedCount() const { return m_nextId-1; }
int EntryBindings() const { return m_entryBindings; }
};
//------------------------------------------------------------------
// Implementasi
//------------------------------------------------------------------
void AFSetupEngine::ResetEntity(AFCandidateSetup &s) const
{
s.setup_id=0;
s.direction=0;
s.state=AF_SETUP_NONE;
s.sweep_onset=-1; s.sweep_dir=0;
s.choch_onset=-1; s.choch_dir=0;
s.zone_type=AF_SETUP_ZONE_NONE; s.zone_formation=-1;
s.zone_top=0.0; s.zone_bot=0.0; s.zone_dir=0;
s.zone_mit_at_creation=AF_ZONE_UNMITIGATED;
s.creation_bar=-1;
s.m3_bar=-1;
s.reason="";
s.entry_bound=false;
}
bool AFSetupEngine::SameChainKey(const AFCandidateSetup &a,
const AFCandidateSetup &b) const
{
return (a.direction==b.direction
&& a.sweep_onset==b.sweep_onset
&& a.choch_onset==b.choch_onset
&& a.zone_formation==b.zone_formation);
}
int AFSetupEngine::RegistryHas(const AFCandidateSetup &s) const
{
for(int i=0;i<ArraySize(m_registry);i++)
if(m_registry[i].setup_id>0 && SameChainKey(m_registry[i],s))
return i;
return -1;
}
bool AFSetupEngine::EventActive(const AFSetupEvent &ev,int r,int w) const
{
if(ev.onset<0) return false;
return (ev.onset<=r && (r-ev.onset)<=w);
}
// NONE -> CONTEXT_VALID (H4+M30 direction-compatible, non-zero; OD-1)
void AFSetupEngine::StartChain(const AFSetupInputs &inp)
{
if(inp.h4==0 || inp.m30==0) return;
if(inp.h4!=inp.m30) return; // H4<->M30 conflict -> BLOCKED
ResetEntity(m_current);
m_current.direction=inp.h4;
m_current.state=AF_SETUP_CONTEXT_VALID;
m_current.reason="context valid";
}
// forming stage transitions (strict order; no skipping; lapsed stage
// discards the forming setup, frozen §F)
void AFSetupEngine::Advance(int r,const AFSetupInputs &inp)
{
if(m_current.setup_id>0) return; // already CANDIDATE/terminal
if(m_current.state==AF_SETUP_NONE) return;
int D=m_current.direction;
// ---- re-validate context gates at every decision bar (OD-1) ----
if(inp.h4!=D || inp.m30!=D)
{
ResetEntity(m_current);
m_current.reason="context break";
return;
}
// ---- re-validate ESTABLISHED legs at every decision bar (§F: a
// lapsed stage before CANDIDATE_SETUP discards the forming
// setup; e.g. a stale sweep at ZONE_READY -> no setup) ----
if(m_current.sweep_onset>=0)
{
if(inp.sweep.dir!=D || inp.sweep.onset!=m_current.sweep_onset ||
!EventActive(inp.sweep,r,m_wSweep))
{
ResetEntity(m_current);
m_current.reason="sweep lapsed";
return;
}
}
if(m_current.choch_onset>=0)
{
if(inp.choch.dir!=D || inp.choch.onset!=m_current.choch_onset ||
!EventActive(inp.choch,r,m_wChoch))
{
ResetEntity(m_current);
m_current.reason="choch lapsed";
return;
}
}
if(m_current.zone_formation>=0)
{
if(inp.zone.dir!=D || inp.zone.formation!=m_current.zone_formation ||
!inp.zone.IsActive())
{
ResetEntity(m_current);
m_current.reason="zone lapsed";
return;
}
}
// ---- CONTEXT_VALID -> LIQUIDITY_TRIGGERED (fresh sweep EVENT) ----
if(m_current.state==AF_SETUP_CONTEXT_VALID ||
m_current.state==AF_SETUP_LIQUIDITY_TRIGGERED)
{
bool sweepPresent=(inp.sweep.dir!=0 && inp.sweep.onset>=0);
if(!sweepPresent) { m_current.reason="waiting sweep"; return; }
if(inp.sweep.dir!=D)
{ ResetEntity(m_current); m_current.reason="sweep dir mismatch"; return; }
if(!EventActive(inp.sweep,r,m_wSweep))
{ ResetEntity(m_current); m_current.reason="stale sweep"; return; }
m_current.sweep_onset=inp.sweep.onset;
m_current.sweep_dir=inp.sweep.dir;
m_current.state=AF_SETUP_LIQUIDITY_TRIGGERED;
}
// ---- LIQUIDITY_TRIGGERED -> STRUCTURE_CONFIRMED (CHoCH after sweep) ----
if(m_current.state==AF_SETUP_LIQUIDITY_TRIGGERED ||
m_current.state==AF_SETUP_STRUCTURE_CONFIRMED)
{
bool chochPresent=(inp.choch.dir!=0 && inp.choch.onset>=0);
if(!chochPresent) { m_current.reason="waiting choch"; return; }
if(inp.choch.dir!=D)
{ ResetEntity(m_current); m_current.reason="choch dir mismatch"; return; }
// causal order: CHoCH AFTER sweep (monotonic onset)
if(inp.choch.onset < m_current.sweep_onset)
{ ResetEntity(m_current); m_current.reason="choch before sweep"; return; }
// chain bound: CHoCH within the sweep validity chain
if((inp.choch.onset-m_current.sweep_onset)>m_wSweep)
{ ResetEntity(m_current); m_current.reason="choch outside sweep window"; return; }
// freshness: CHoCH active at the decision bar
if(!EventActive(inp.choch,r,m_wChoch))
{ ResetEntity(m_current); m_current.reason="stale choch"; return; }
m_current.choch_onset=inp.choch.onset;
m_current.choch_dir=inp.choch.dir;
m_current.state=AF_SETUP_STRUCTURE_CONFIRMED;
}
// ---- STRUCTURE_CONFIRMED -> ZONE_READY (unmitigated OB|FVG) ----
if(m_current.state==AF_SETUP_STRUCTURE_CONFIRMED ||
m_current.state==AF_SETUP_ZONE_READY)
{
if(inp.zone.dir==0 || inp.zone.formation<0)
{ m_current.reason="waiting zone"; return; }
if(inp.zone.dir!=D)
{ ResetEntity(m_current); m_current.reason="zone dir mismatch"; return; }
if(!inp.zone.IsActive())
{ ResetEntity(m_current); m_current.reason="zone mitigated/invalidated"; return; }
m_current.zone_type=(AFSetupZoneType)inp.zone.type;
m_current.zone_formation=inp.zone.formation;
m_current.zone_top=0.0;
m_current.zone_bot=0.0;
m_current.zone_dir=inp.zone.dir;
m_current.zone_mit_at_creation=inp.zone.mit;
m_current.state=AF_SETUP_ZONE_READY;
}
// ---- ZONE_READY -> ENTRY_ARMED (M15 entry condition) ----
if(m_current.state==AF_SETUP_ZONE_READY ||
m_current.state==AF_SETUP_ENTRY_ARMED)
{
if(inp.m15==0) { m_current.reason="waiting m15 entry"; return; }
if(inp.m15!=D)
{ ResetEntity(m_current); m_current.reason="M15 conflict -> BLOCKED"; return; }
m_current.state=AF_SETUP_ENTRY_ARMED;
}
// ---- ENTRY_ARMED -> CANDIDATE_SETUP (chain complete; identity) ----
if(m_current.state==AF_SETUP_ENTRY_ARMED)
{
// dedup by construction (§G): same chain key -> no duplicate setup
if(RegistryHas(m_current)>=0)
{
ResetEntity(m_current);
m_current.reason="duplicate chain (dedup)";
return;
}
m_current.setup_id=m_nextId;
m_nextId++;
m_current.creation_bar=r;
m_current.state=AF_SETUP_CANDIDATE;
m_current.reason="candidate setup";
int n=ArraySize(m_registry);
ArrayResize(m_registry,n+1);
m_registry[n]=m_current;
}
}
// post-formation: optional M3 / expiry / invalidation / consumption
void AFSetupEngine::PostFormation(int r,const AFSetupInputs &inp)
{
if(m_current.setup_id<=0) return;
int D=m_current.direction;
int cb=m_current.creation_bar;
// one entry per setup (§H) — checked FIRST so consumption wins
if(inp.entry && !m_current.entry_bound)
{
m_current.entry_bound=true;
m_entryBindings++;
m_current.state=AF_SETUP_CONSUMED;
m_current.reason="consumed by entry";
return;
}
// zone mitigated after formation -> INVALIDATED (§F)
if(inp.zone.dir!=0 && !inp.zone.IsActive())
{
m_current.state=AF_SETUP_INVALIDATED;
m_current.reason="zone mitigated";
return;
}
// contrary CHoCH after formation -> INVALIDATED (§F)
if(inp.choch.dir==-D)
{
m_current.state=AF_SETUP_INVALIDATED;
m_current.reason="contrary choch";
return;
}
// context break after formation -> INVALIDATED (§F canonical)
if(inp.h4==-D || inp.m30==-D)
{
m_current.state=AF_SETUP_INVALIDATED;
m_current.reason="context break";
return;
}
// setup validity exceeded -> EXPIRED (§M W_setup)
if(cb>=0 && (r-cb)>m_wSetup)
{
m_current.state=AF_SETUP_EXPIRED;
m_current.reason="setup expired";
return;
}
// optional M3 confirmation (no veto, OD-4)
if(m_current.state==AF_SETUP_CANDIDATE)
{
if(inp.m3==D && (r-cb)<=m_wM3)
{
m_current.state=AF_SETUP_M3_CONFIRMED;
m_current.m3_bar=r;
m_current.reason="m3 confirmed";
}
else if(inp.m3==-D)
{
m_current.reason="m3 contrary (not confirmed)";
}
}
}
void AFSetupEngine::Step(int decisionBar,const AFSetupInputs &inp,
AFSetupOutcome &out)
{
out.bar=decisionBar;
out.score=inp.score;
out.blocked=false;
out.block_reason="";
// ---- gates (OD-1/OD-4): conflict -> BLOCKED ----
bool contextConflict=(inp.h4!=0 && inp.m30!=0 && inp.h4!=inp.m30);
bool chainConflict=false;
int D=0;
if(inp.sweep.dir!=0) D=inp.sweep.dir;
else if(inp.choch.dir!=0) D=inp.choch.dir;
else if(inp.zone.dir!=0) D=inp.zone.dir;
if(D!=0 && inp.m15!=0 && inp.m15!=D) chainConflict=true;
bool blocked=contextConflict || chainConflict;
if(blocked)
{
out.blocked=true;
out.block_reason=contextConflict? "H4_M30_CONFLICT" : "M15_CONFLICT";
}
// ---- forming / active handling (only when not blocked) ----
if(!blocked)
{
if(m_current.setup_id==0)
{
// no active setup: start a new chain if gates permit (OD-1)
StartChain(inp);
Advance(decisionBar,inp);
}
else if(m_current.state==AF_SETUP_CONSUMED ||
m_current.state==AF_SETUP_EXPIRED ||
m_current.state==AF_SETUP_INVALIDATED)
{
// terminal slot: free; a NEW chain may start on a later bar
ResetEntity(m_current);
StartChain(inp);
Advance(decisionBar,inp);
}
else
{
Advance(decisionBar,inp);
}
}
// ---- post-formation ALWAYS for an active setup (even if this bar
// is gate-blocked — context break invalidation still applies) ----
if(m_current.setup_id>0 &&
(m_current.state==AF_SETUP_CANDIDATE ||
m_current.state==AF_SETUP_M3_CONFIRMED))
{
PostFormation(decisionBar,inp);
}
// ---- expose outcome ----
out.dir=m_current.direction;
out.state=m_current.state;
out.setup_id=m_current.setup_id;
out.setup_exists=(m_current.setup_id>0 &&
(m_current.state==AF_SETUP_CANDIDATE ||
m_current.state==AF_SETUP_M3_CONFIRMED));
out.m3_confirmed=(m_current.state==AF_SETUP_M3_CONFIRMED);
out.entry_bound=m_current.entry_bound;
out.entity=m_current;
// ---- terminal -> slot free; a NEW chain may start next bar ----
if(m_current.setup_id>0 &&
(m_current.state==AF_SETUP_CONSUMED ||
m_current.state==AF_SETUP_EXPIRED ||
m_current.state==AF_SETUP_INVALIDATED))
{
ResetEntity(m_current);
}
}
#endif // AF_ENGINE2_SETUP_MQH