# -*- coding: utf-8 -*- """P3-S.17R.1 — ENGINE-2 GATE SERIES — faithful research port of the frozen runtime H4/M30/M15 gate producers. WHAT THIS MODULE IS ------------------- The F3 Candidate Setup layer (AF_Engine2_Setup.mqh) does NOT compute its own gate values: it CONSUMES per-M15-decision-bar semantic inputs h4 : H4 context STATE direction (+1/-1/0) m30 : M30 context STATE direction (+1/-1/0) m15 : M15 entry CONDITION direction (+1/-1/0) These inputs are produced by the frozen Engine-2 AGENTS (AF_Engine2_Agents.mqh): Narrative agent on the H4 slot (AF_E2_TF_S1 = PERIOD_H4) Context agent on the M30 slot (AF_E2_TF_S2 = PERIOD_M30) Entry agent on the M15 slot (AF_E2_TF_S3 = PERIOD_M15) Each agent is STATELESS per closed bar (P3-S.7 S-ST): its output is a pure function of the Engine-1 closed-bar cache ending at the decision bar, and its `dir` = +1 if fuzzy bias > AF_E2_DIR_TOL, -1 if bias < -AF_E2_DIR_TOL, else 0. This module is a FAITHFUL Python port of those frozen rules (same constants, same fuzzy membership functions, same rule weights, same tie-breaks, same cache capacities). It exists because NO runnable MQL5 file drives the agents or the F3 layer on the authorized historical scope (verified: the baseline EA AlgoForge_Backtest_Baseline.mq5 is the F1/feature path only; AFAgent* and AFSetupEngine are never invoked). The research side therefore reproduces the frozen E-agent rule directly, per P3-S.17R.1 §14 ("replicate that meaning, not merely the variable name") and §16 (port only what is needed; if a component is missing, document the missing contract — see AS-OF note below). AS-OF / TEMPORAL CONTRACT (P3-S.17R.1 §15; canonical §L; P3-S.7 S-T) -------------------------------------------------------------------- For an M15 decision bar with open time t (close time tc = t + 900 s): H4_asof(t) : newest CLOSED H4 bar with close_time <= tc M30_asof(t) : newest CLOSED M30 bar with close_time <= tc M15_asof(t) : the decision bar t itself (its own closed bar) The bound is the DECISION-BAR CLOSE time tc, mirroring the runtime Engine-1 cache reality: the EA processes the M15 bar t on the first tick at/after tc, and the HTF slot cache at that moment contains every HTF bar with close_time <= tc (the forming HTF bar is dropped by the closed-bar lock in AFEngine1MTF::Build). Consequently an HTF bar that closes EXACTLY at tc is visible to that decision ("exactly at HTF close" boundary). The canonical contract §L text states "close_time <= t" (open timestamp); the runtime behaviour is "close_time <= tc" (close timestamp). The two differ by exactly one M15 bar at HTF-close boundaries (M15 bars whose close coincides with an HTF close). This module reproduces the RUNTIME behaviour (the parity reference; §14/§15), and the boundary is pinned deterministically by GR-T11 (before / exactly at / after HTF close) and GR-T12 (no future HTF candle: an HTF bar with close_time > tc is NEVER visible). No future bar, no partial HTF candle, no hidden future state. SIMPLIFICATIONS (documented, contract-safe) ------------------------------------------- 1. Zone partial-fill flag is NOT computed in the agents: AF_FVGZoneState / AF_OBZoneState use mit_state = FULLY_MITIGATED + invalidated on FULL fill only; IsActive() = NOT full-filled. Partial fill never changes IsActive(), so the agent dir is a pure function of full-fill. (The F3 layer's zone `mit` field is produced by the PAR-suite zone ports, not by this module.) 2. Full-fill scans are answered with a sliding-window min/max over the closed cache ending at r (monotonic deque), which is EXACTLY equivalent to the MQL5 backward loop bounded by the decision bar (no look-ahead). 3. M30 bars are resampled from the M15 feed (2 x M15 per M30), same underlying ticks -> deterministic and identical to the runtime M30 OHLC. Research-only module. No MQL5, no FEATURE_CONTRACT, no model artifact. """ import collections import numpy as np # ---- frozen Engine-2 constants (AF_Defines.mqh) ---------------------------- E2_MIN_BARS = 80 # AF_E2_MIN_BARS: minimum cache bars before a valid signal E2_LOOKBACK_PD = 60 # AF_E2_LOOKBACK_PD: premium/discount & range window E2_LOOKBACK_AVG = 20 # AF_E2_LOOKBACK_AVG: avg-body window (OB strong move) E2_PIVOT_LOOKBACK = 200 # AF_E2_PIVOT_LOOKBACK: swing pivot search window E2_SWEEP_LOOKBACK = 8 # AF_E2_SWEEP_LOOKBACK: E2 sweep scan window E2_FVG_LOOKBACK = 40 # AF_E2_FVG_LOOKBACK: FVG search window (Context agent) E2_DIR_TOL = 0.05 # AF_E2_DIR_TOL: agent dir threshold on fuzzy bias E2_MAX_PIVOTS = 64 # AF_E2_MAX_PIVOTS: max stored pivots per side DISP_TH = 1.6 # displacement: body >= 1.6 x avg body (AF_DetectDisplacement) OB_MOVE_TH = 1.5 # AF_E3_MOVE_BODY: OB strong-move x avg body # Cache capacities used by the baseline EA registration (AlgoForge_Backtest_ # Baseline.mq5 OnInit): M15 = InpMaxBars = 700; H1/H4/D1 = 250. M30 is not # registered there; the F3 path would register it like the other HTF slots, # so the research port uses 250 for M30 (documented assumption, bounded by all # agent lookbacks <= 200). M15_MAXBARS = 700 HTF_MAXBARS = 250 # --------------------------------------------------------------------------- # Sliding-window min/max (monotonic deque) — exact equivalent of the bounded # backward fill scans in AF_FVGZoneState / AF_OBZoneState at decision bar r. # --------------------------------------------------------------------------- class _WindowMM: def __init__(self): self._qmin = collections.deque() self._qmax = collections.deque() self._r = -1 def push(self, vmin, vmax): self._r += 1 while self._qmin and self._qmin[-1][1] >= vmin: self._qmin.pop() self._qmin.append((self._r, vmin)) while self._qmax and self._qmax[-1][1] <= vmax: self._qmax.pop() self._qmax.append((self._r, vmax)) def query(self, a): """min over [a, r] and max over [a, r]; a <= r.""" while self._qmin and self._qmin[0][0] < a: self._qmin.popleft() while self._qmax and self._qmax[0][0] < a: self._qmax.popleft() mn = self._qmin[0][1] if self._qmin else None mx = self._qmax[0][1] if self._qmax else None return mn, mx # --------------------------------------------------------------------------- # Fuzzy membership functions (AF_Engine2_Agents.mqh) # --------------------------------------------------------------------------- def clamp01(v): return 0.0 if v < 0.0 else (1.0 if v > 1.0 else v) def mf_trap(x, a, b, c, d): if x <= a or x >= d: return 0.0 if x >= b and x <= c: return 1.0 lo = (x - a) / (b - a) if b > a else 1.0 hi = (d - x) / (d - c) if d > c else 1.0 return lo if x < b else hi def mf_tri(x, a, b, c): if x <= a or x >= c: return 0.0 if x == b: return 1.0 lo = (x - a) / (b - a) if b > a else 1.0 hi = (c - x) / (c - b) if c > b else 1.0 return lo if x < b else hi class _FuzzyEval: """AFFuzzyEval (Mamdani-light): weighted rule accumulation -> bias/dir.""" def __init__(self): self.buy_acc = 0.0 self.sell_acc = 0.0 self.w_tot = 0.0 def rule(self, buy_side, fire, weight): if fire <= 0.0 or weight <= 0.0: return self.w_tot += weight if buy_side: self.buy_acc += fire * weight else: self.sell_acc += fire * weight def finalize(self): denom = self.w_tot if self.w_tot > 0.0 else 1.0 buy = clamp01(self.buy_acc / denom) sell = clamp01(self.sell_acc / denom) bias = buy - sell d = 1 if bias > E2_DIR_TOL else (-1 if bias < -E2_DIR_TOL else 0) return {"buy": buy, "sell": sell, "bias": bias, "confidence": max(buy, sell), "dir": d} # --------------------------------------------------------------------------- # Per-TF closed-bar primitives (reversed-index semantics of Engine-1 cache) # All *_at(o,h,l,c, r, ...) functions evaluate AT closed chronological bar r # using only bars [max(0, r-maxbars+1), r] (closed-bar lock, no look-ahead). # --------------------------------------------------------------------------- def _atr(o, h, l, c, r, period=14, maxbars=HTF_MAXBARS): cnt = min(maxbars, r + 1) if cnt <= 0 or period < 1: return 0.0 n = min(period, cnt) s = 0.0 for k in range(n): b = r - k tr = h[b] - l[b] j = k + 1 if j < cnt: pc = c[r - j] t1 = abs(h[b] - pc) t2 = abs(l[b] - pc) tr = max(tr, t1, t2) s += tr return s / n def _avg_body(o, h, l, c, r, n=20, maxbars=HTF_MAXBARS): cnt = min(maxbars, r + 1) m = min(n, cnt) if m <= 0: return 0.0 s = 0.0 for k in range(m): b = r - k s += abs(c[b] - o[b]) return s / m def build_swing(o, h, l, c, r, lookback=E2_PIVOT_LOOKBACK, maxbars=HTF_MAXBARS): """AF_BuildSwing: fractal 2-left/2-right pivots on closed bars; returns (highs, lows) lists of (chrono_bar, price) NEWEST-first (reversed order), capped at AF_E2_MAX_PIVOTS per side.""" cnt = min(maxbars, r + 1) highs = [] lows = [] if cnt < 5: return highs, lows max_idx = min(cnt - 3, lookback if lookback > 0 else cnt) for k in range(2, max_idx + 1): b = r - k vh = h[b] if (vh > h[b + 1] and vh > h[b + 2] and vh > h[b - 1] and vh > h[b - 2]): if len(highs) < E2_MAX_PIVOTS: highs.append((b, float(vh))) vl = l[b] if (vl < l[b + 1] and vl < l[b + 2] and vl < l[b - 1] and vl < l[b - 2]): if len(lows) < E2_MAX_PIVOTS: lows.append((b, float(vl))) return highs, lows def trend_from_swing(highs, lows): """AF_TrendFromSwing: +1/-1/0 from the newest up-to-4 pivot sequence.""" up = dn = pairs = 0 n_h = min(4, len(highs)) n_l = min(4, len(lows)) for i in range(n_h - 1): if highs[i][1] > highs[i + 1][1]: up += 1 else: dn += 1 pairs += 1 for i in range(n_l - 1): if lows[i][1] > lows[i + 1][1]: up += 1 else: dn += 1 pairs += 1 if pairs <= 0: return 0, 0.0 clarity = float(max(up, dn)) / float(pairs) trend = 1 if up > dn else (-1 if dn > up else 0) return trend, clarity def detect_choch(o, h, l, c, r, highs, lows, prev_trend): """AF_DetectChoch: close break of the newest swing pivot, prior-trend gated.""" if len(highs) < 1 or len(lows) < 1: return 0 close = c[r] if prev_trend < 0 and close > highs[0][1]: return 1 if prev_trend > 0 and close < lows[0][1]: return -1 return 0 def detect_bos(o, h, l, c, r, highs, lows, trend): if len(highs) < 1 or len(lows) < 1: return 0 close = c[r] if trend > 0 and close > highs[0][1]: return 1 if trend < 0 and close < lows[0][1]: return -1 return 0 def detect_sweep_e2(o, h, l, c, r, highs, lows, lookback=E2_SWEEP_LOOKBACK, maxbars=HTF_MAXBARS): """AF_DetectSweep (Engine-2 agent version): level = min/max of the 2 newest lows/highs; newest-first scan for wick pen + same-bar close-back.""" if len(lows) < 1 or len(highs) < 1: return 0 cnt = min(maxbars, r + 1) n = min(lookback, cnt - 1) if n < 1: return 0 level_low = min(lows[0][1], lows[1][1]) if len(lows) >= 2 else lows[0][1] level_high = max(highs[0][1], highs[1][1]) if len(highs) >= 2 else highs[0][1] best_bull = -1 best_bear = -1 for k in range(n): b = r - k if best_bull < 0 and l[b] < level_low and c[b] > level_low: best_bull = k if best_bear < 0 and h[b] > level_high and c[b] < level_high: best_bear = k if best_bull >= 0 and (best_bear < 0 or best_bull <= best_bear): return 1 if best_bear >= 0: return -1 return 0 def detect_displacement(o, h, l, c, r, avg_n=E2_LOOKBACK_AVG, maxbars=HTF_MAXBARS): avg = _avg_body(o, h, l, c, r, avg_n, maxbars) if avg <= 0.0: return 0 body = abs(c[r] - o[r]) if body < DISP_TH * avg: return 0 return 1 if c[r] > o[r] else -1 def range_stat(o, h, l, c, r, lookback=E2_LOOKBACK_PD, maxbars=HTF_MAXBARS): """AF_RangeStat: (rmin, rmax, pos) over the newest `lookback` closed bars.""" cnt = min(maxbars, r + 1) n = min(lookback, cnt) if n < 2: return 0.0, 0.0, 0.5 rmin = min(l[r - k] for k in range(n)) rmax = max(h[r - k] for k in range(n)) if rmax > rmin: pos = clamp01((c[r] - rmin) / (rmax - rmin)) else: pos = 0.5 return rmin, rmax, pos def find_order_block(o, h, l, c, r, wm, avg_n=E2_LOOKBACK_AVG, maxbars=HTF_MAXBARS): """AF_FindOrderBlock: newest qualifying OB (reversed scan i=1..cnt-2). M at reversed i-1 (strong body >= 1.5 avg), B at reversed i (opposite color), zone = full range of B, active (not close-through full-filled). wm = _WindowMM over the CLOSED cache ending at r (for the fill query). Returns (dir, top, bot) or (0, 0.0, 0.0).""" cnt = min(maxbars, r + 1) if cnt < avg_n + 2: return 0, 0.0, 0.0 avg = _avg_body(o, h, l, c, r, avg_n, maxbars) if avg <= 0.0: return 0, 0.0, 0.0 for k in range(1, cnt - 1): b_m = r - k + 1 # M candle (newer, reversed idx k-1) b_b = r - k # B candle (older, reversed idx k) body_prev = abs(c[b_m] - o[b_m]) if body_prev < OB_MOVE_TH * avg: continue up_move = c[b_m] > o[b_m] d = 0 if up_move: if c[b_b] < o[b_b]: d = 1 else: if c[b_b] > o[b_b]: d = -1 if d == 0: continue top = h[b_b] bot = l[b_b] # full-fill (close-through): any closed bar newer than B beyond the zone mn, mx = wm.query(b_b + 1) if d > 0 and mn is not None and mn < bot: continue if d < 0 and mx is not None and mx > top: continue return d, float(top), float(bot) return 0, 0.0, 0.0 def find_fvg(o, h, l, c, r, wm, lookback=E2_FVG_LOOKBACK, maxbars=HTF_MAXBARS): """AF_FindFVG: newest eligible C3 (reversed i=0..), wick geometry, active (not wick full-filled). Returns (dir, top, bot) or (0,0,0).""" cnt = min(maxbars, r + 1) if cnt < 3: return 0, 0.0, 0.0 max_idx = min(cnt - 3, lookback if lookback > 0 else cnt) for k in range(max_idx + 1): b_c3 = r - k # C3 (newest of the three) b_c1 = r - k - 2 # C1 (oldest of the three) l0 = l[b_c3] h2 = h[b_c1] if l0 > h2: # bullish FVG: Low(C3) > High(C1) bot = h2 top = l0 mn, _ = wm.query(b_c3 + 1) if mn is not None and mn <= bot: continue # wick full-filled (bull: Low(j) <= bot) return 1, float(top), float(bot) h0 = h[b_c3] l2 = l[b_c1] if h0 < l2: # bearish FVG: High(C3) < Low(C1) bot = h0 top = l2 _, mx = wm.query(b_c3 + 1) if mx is not None and mx >= top: continue # wick full-filled (bear: High(j) >= top) return -1, float(top), float(bot) return 0, 0.0, 0.0 def pattern_pa(o, h, l, c, r): """AF_PatternPA: engulfing / pin bar / inside bar / momentum / close-pos.""" if r < 3: return 0, 0.0 o0, h0, l0, c0 = o[r], h[r], l[r], c[r] o1, h1, l1, c1 = o[r - 1], h[r - 1], l[r - 1], c[r - 1] b0 = c0 > o0 b1 = c1 > o1 body0 = abs(c0 - o0) rng0 = h0 - l0 best = 0 best_str = 0.0 if b0 and not b1 and c0 >= o1 and o0 <= c1: best, best_str = 1, 1.0 elif not b0 and b1 and c0 <= o1 and o0 >= c1: best, best_str = -1, 1.0 if rng0 > 0.0: lower_wick = min(o0, c0) - l0 upper_wick = h0 - max(o0, c0) if body0 > 0.0 and lower_wick > 2.0 * body0 and upper_wick < 0.5 * body0 \ and best_str < 0.85: best, best_str = 1, 0.85 if body0 > 0.0 and upper_wick > 2.0 * body0 and lower_wick < 0.5 * body0 \ and best_str < 0.85: best, best_str = -1, 0.85 if h0 <= h1 + 1e-12 and l0 >= l1 - 1e-12: if b1 and best_str < 0.6: best, best_str = 1, 0.6 if not b1 and best_str < 0.6: best, best_str = -1, 0.6 if b0 and b1 and best_str < 0.7: best, best_str = 1, 0.7 if not b0 and not b1 and best_str < 0.7: best, best_str = -1, 0.7 if rng0 > 0.0: pos = (c0 - l0) / rng0 if pos > 0.70 and best_str < 0.5: best, best_str = 1, 0.5 if pos < 0.30 and best_str < 0.5: best, best_str = -1, 0.5 return best, best_str # --------------------------------------------------------------------------- # AGENTS — per closed TF bar r (chronological). dir = gate state value. # --------------------------------------------------------------------------- def narrative_agent(o, h, l, c, r, maxbars=HTF_MAXBARS): """AFAgentNarrative.Compute -> dir. Returns dict(dir, bias, reason_summary).""" cnt = min(maxbars, r + 1) if cnt < E2_MIN_BARS: return {"dir": 0, "bias": 0.0, "reason": "data kurang"} highs, lows = build_swing(o, h, l, c, r, E2_PIVOT_LOOKBACK, maxbars) trend, clarity = trend_from_swing(highs, lows) choch = detect_choch(o, h, l, c, r, highs, lows, trend) bos = detect_bos(o, h, l, c, r, highs, lows, trend) sweep = detect_sweep_e2(o, h, l, c, r, highs, lows, E2_SWEEP_LOOKBACK, maxbars) _rmin, _rmax, pos = range_stat(o, h, l, c, r, E2_LOOKBACK_PD, maxbars) m_bull = (0.35 + 0.65 * clarity) if trend > 0 else 0.0 m_bear = (0.35 + 0.65 * clarity) if trend < 0 else 0.0 m_disc = mf_trap(pos, 0.0, 0.0, 0.30, 0.45) m_prem = mf_trap(pos, 0.55, 0.70, 1.0, 1.0) w_struct = 0.40 * (0.5 + 0.5 * clarity) w_liq = 0.35 w_zone = 0.25 * (1.5 - 0.5 * clarity) w_sum = w_struct + w_liq + w_zone w_struct /= w_sum w_liq /= w_sum w_zone /= w_sum fz = _FuzzyEval() fz.rule(True, m_bull, w_struct) fz.rule(False, m_bear, w_struct) fz.rule(True, 1.0 if sweep > 0 else 0.0, w_liq) fz.rule(False, 1.0 if sweep < 0 else 0.0, w_liq) fz.rule(True, m_disc, w_zone) fz.rule(False, m_prem, w_zone) if choch > 0: fz.rule(True, 0.8, w_struct * 0.5) if choch < 0: fz.rule(False, 0.8, w_struct * 0.5) if bos > 0 and trend > 0: fz.rule(True, 0.7, w_struct * 0.3) if bos < 0 and trend < 0: fz.rule(False, 0.7, w_struct * 0.3) out = fz.finalize() out["reason"] = "Narrative@" + ("H4" if maxbars == HTF_MAXBARS else "TF") return out def context_agent(o, h, l, c, r, maxbars=HTF_MAXBARS): """AFAgentContext.Compute -> dir (Context / M30 gate).""" cnt = min(maxbars, r + 1) if cnt < E2_MIN_BARS: return {"dir": 0, "bias": 0.0, "reason": "data kurang"} close = c[r] atr = _atr(o, h, l, c, r, 14, maxbars) if atr <= 0.0: return {"dir": 0, "bias": 0.0, "reason": "ATR 0"} wm = _WindowMM() for k in range(cnt): b = r - k wm.push(l[b], h[b]) ob_dir, ob_hi, ob_lo = find_order_block(o, h, l, c, r, wm, E2_LOOKBACK_AVG, maxbars) fv_dir, fv_hi, fv_lo = find_fvg(o, h, l, c, r, wm, E2_FVG_LOOKBACK, maxbars) highs, lows = build_swing(o, h, l, c, r, E2_PIVOT_LOOKBACK, maxbars) dist_sup = float("inf") dist_res = float("inf") for i in range(min(12, len(lows))): d = close - lows[i][1] if d >= 0.0 and d < dist_sup: dist_sup = d for i in range(min(12, len(highs))): d = highs[i][1] - close if d >= 0.0 and d < dist_res: dist_res = d m_at_sup = clamp01(1.0 - dist_sup / atr) if dist_sup < float("inf") else 0.0 m_at_res = clamp01(1.0 - dist_res / atr) if dist_res < float("inf") else 0.0 _rmin, _rmax, pos = range_stat(o, h, l, c, r, E2_LOOKBACK_PD, maxbars) m_disc = mf_trap(pos, 0.0, 0.0, 0.30, 0.45) m_prem = mf_trap(pos, 0.55, 0.70, 1.0, 1.0) m_in_ob_bull = m_in_ob_bear = 0.0 m_in_fv_bull = m_in_fv_bear = 0.0 if ob_dir > 0: m_in_ob_bull = mf_trap(close, ob_lo - 0.3 * atr, ob_lo, ob_hi, ob_hi + 0.3 * atr) if ob_dir < 0: m_in_ob_bear = mf_trap(close, ob_lo - 0.3 * atr, ob_lo, ob_hi, ob_hi + 0.3 * atr) if fv_dir > 0: m_in_fv_bull = mf_trap(close, fv_lo - 0.3 * atr, fv_lo, fv_hi, fv_hi + 0.3 * atr) if fv_dir < 0: m_in_fv_bear = mf_trap(close, fv_lo - 0.3 * atr, fv_lo, fv_hi, fv_hi + 0.3 * atr) vol = atr / close if close != 0.0 else 0.0 vol_factor = 0.70 if vol > 0.002 else 1.0 w_ob, w_fv, w_sr, w_pd = 0.30, 0.25, 0.25 * vol_factor, 0.20 * vol_factor w_sum2 = w_ob + w_fv + w_sr + w_pd w_ob /= w_sum2 w_fv /= w_sum2 w_sr /= w_sum2 w_pd /= w_sum2 fz = _FuzzyEval() fz.rule(True, m_in_ob_bull, w_ob) fz.rule(False, m_in_ob_bear, w_ob) fz.rule(True, m_in_fv_bull, w_fv) fz.rule(False, m_in_fv_bear, w_fv) fz.rule(True, m_at_sup, w_sr) fz.rule(False, m_at_res, w_sr) fz.rule(True, m_disc, w_pd) fz.rule(False, m_prem, w_pd) out = fz.finalize() out["reason"] = "Context@" + ("M30" if maxbars == HTF_MAXBARS else "TF") return out def entry_agent(o, h, l, c, r, maxbars=M15_MAXBARS): """AFAgentEntry.Compute -> dir (Entry / M15 entry condition).""" cnt = min(maxbars, r + 1) if cnt < E2_MIN_BARS: return {"dir": 0, "bias": 0.0, "reason": "data kurang"} highs, lows = build_swing(o, h, l, c, r, E2_PIVOT_LOOKBACK, maxbars) trend, _clarity = trend_from_swing(highs, lows) sweep = detect_sweep_e2(o, h, l, c, r, highs, lows, E2_SWEEP_LOOKBACK, maxbars) choch = detect_choch(o, h, l, c, r, highs, lows, trend) disp = detect_displacement(o, h, l, c, r, E2_LOOKBACK_AVG, maxbars) wm = _WindowMM() for k in range(cnt): b = r - k wm.push(l[b], h[b]) ob_dir, ob_hi, ob_lo = find_order_block(o, h, l, c, r, wm, E2_LOOKBACK_AVG, maxbars) fv_dir, fv_hi, fv_lo = find_fvg(o, h, l, c, r, wm, 20, maxbars) close = c[r] atr = _atr(o, h, l, c, r, 14, maxbars) if atr <= 0.0: return {"dir": 0, "bias": 0.0, "reason": "ATR 0"} m_sweep_bull = 1.0 if sweep > 0 else 0.0 m_sweep_bear = 1.0 if sweep < 0 else 0.0 m_choch_bull = 1.0 if choch > 0 else 0.0 m_choch_bear = 1.0 if choch < 0 else 0.0 m_disp_bull = 1.0 if disp > 0 else 0.0 m_disp_bear = 1.0 if disp < 0 else 0.0 m_zone_bull = m_zone_bear = 0.0 if ob_dir > 0: m_zone_bull = max(m_zone_bull, mf_trap(close, ob_lo - 0.3 * atr, ob_lo, ob_hi, ob_hi + 0.3 * atr)) if ob_dir < 0: m_zone_bear = max(m_zone_bear, mf_trap(close, ob_lo - 0.3 * atr, ob_lo, ob_hi, ob_hi + 0.3 * atr)) if fv_dir > 0: m_zone_bull = max(m_zone_bull, mf_trap(close, fv_lo - 0.3 * atr, fv_lo, fv_hi, fv_hi + 0.3 * atr)) if fv_dir < 0: m_zone_bear = max(m_zone_bear, mf_trap(close, fv_lo - 0.3 * atr, fv_lo, fv_hi, fv_hi + 0.3 * atr)) w_sweep, w_choch, w_disp, w_zone, w_setup = 0.25, 0.30, 0.20, 0.15, 0.30 if abs(disp) > 0: w_disp *= 1.3 w_sum3 = w_sweep + w_choch + w_disp + w_zone + w_setup w_sweep /= w_sum3 w_choch /= w_sum3 w_disp /= w_sum3 w_zone /= w_sum3 w_setup /= w_sum3 fz = _FuzzyEval() fz.rule(True, m_sweep_bull, w_sweep) fz.rule(False, m_sweep_bear, w_sweep) fz.rule(True, m_choch_bull, w_choch) fz.rule(False, m_choch_bear, w_choch) fz.rule(True, m_disp_bull, w_disp) fz.rule(False, m_disp_bear, w_disp) fz.rule(True, m_zone_bull, w_zone) fz.rule(False, m_zone_bear, w_zone) conf_bull = max(m_sweep_bull, m_choch_bull) conf_bear = max(m_sweep_bear, m_choch_bear) fz.rule(True, min(m_zone_bull, conf_bull), w_setup) fz.rule(False, min(m_zone_bear, conf_bear), w_setup) out = fz.finalize() out["reason"] = "Entry@M15" return out # --------------------------------------------------------------------------- # SERIES + AS-OF GATE WIRING # --------------------------------------------------------------------------- def agent_series(agent_fn, o, h, l, c, maxbars): """dir for every closed bar of a TF (agent evaluated AT that bar's close).""" n = len(c) out = np.zeros(n, dtype=int) for r in range(n): out[r] = agent_fn(o, h, l, c, r, maxbars)["dir"] return out def m30_from_m15(t15, o15, h15, l15, c15): """Resample M15 -> M30 (2 x M15 per M30). Returns (t30, o30, h30, l30, c30). t30 = M30 open time; close_time = t30 + 1800.""" n = len(t15) - (len(t15) % 2) t30 = t15[0:n:2] o30 = o15[0:n:2] h30 = np.maximum(h15[0:n:2], h15[1:n:2]) l30 = np.minimum(l15[0:n:2], l15[1:n:2]) c30 = c15[1:n:2] return t30, o30, h30, l30, c30 def as_of_index(htf_close, decision_close): """For each decision close time, the index of the last HTF bar with close_time <= decision_close (runtime Engine-1 cache as-of).""" return np.searchsorted(htf_close, decision_close, side="right") - 1 def gate_series(t15, o15, h15, l15, c15, h4, m30): """Build the full research H4/M30/M15 gate series aligned to M15 bars. h4 = (t4, o4, h4a, l4a, c4a) H4 OHLC (chronological) m30 = (t30, o30, h30a, l30a, c30a) M30 OHLC (chronological) For M15 decision bar r (open t15[r], close tc = t15[r]+900): h4_gate[r] = narrative dir of the newest H4 bar with close_time <= tc m30_gate[r] = context dir of the newest M30 bar with close_time <= tc m15_gate[r] = entry dir of the M15 bar r itself Returns dict with h4/m30/m15 int arrays + provenance metadata. """ t4, o4, h4a, l4a, c4a = h4 t30, o30, h30a, l30a, c30a = m30 # agent dir per closed TF bar h4_dir = agent_series(narrative_agent, o4, h4a, l4a, c4a, HTF_MAXBARS) m30_dir = agent_series(context_agent, o30, h30a, l30a, c30a, HTF_MAXBARS) m15_dir = agent_series(entry_agent, o15, h15, l15, c15, M15_MAXBARS) # runtime as-of: newest closed HTF bar with close_time <= decision close h4_close = t4 + 14400 m30_close = t30 + 1800 dc = t15 + 900 h4_idx = as_of_index(h4_close, dc) m30_idx = as_of_index(m30_close, dc) n = len(t15) h4_gate = np.zeros(n, dtype=int) m30_gate = np.zeros(n, dtype=int) m15_gate = m15_dir.copy() for r in range(n): i4 = int(h4_idx[r]) if i4 >= 0: h4_gate[r] = int(h4_dir[i4]) i30 = int(m30_idx[r]) if i30 >= 0: m30_gate[r] = int(m30_dir[i30]) return { "h4": h4_gate, "m30": m30_gate, "m15": m15_gate, "h4_dir_per_h4_bar": h4_dir, "m30_dir_per_m30_bar": m30_dir, "m15_dir_per_m15_bar": m15_dir, "as_of": "runtime Engine-1 cache: newest CLOSED HTF bar with " "close_time <= decision-bar close (t+900); M15 = decision bar", "constants": { "E2_MIN_BARS": E2_MIN_BARS, "E2_DIR_TOL": E2_DIR_TOL, "E2_PIVOT_LOOKBACK": E2_PIVOT_LOOKBACK, "E2_SWEEP_LOOKBACK": E2_SWEEP_LOOKBACK, "E2_FVG_LOOKBACK": E2_FVG_LOOKBACK, "E2_LOOKBACK_PD": E2_LOOKBACK_PD, "E2_LOOKBACK_AVG": E2_LOOKBACK_AVG, "M15_MAXBARS": M15_MAXBARS, "HTF_MAXBARS": HTF_MAXBARS, }, }