forked from chiki2bum2/SniperGold_ML
728 lines
27 KiB
Python
728 lines
27 KiB
Python
# -*- 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,
|
|
},
|
|
}
|