forked from chiki2bum2/SniperGold_ML
215 lines
9.8 KiB
Python
215 lines
9.8 KiB
Python
# -*- coding: utf-8 -*-
| |||
"""Liquidity Sweep EVENT — canonical research port (P3-S.2 / P3-S.11 frozen).
| |||
| |||
P3-S.17R.1 — Task A. This module REPLACES the legacy persistent-latch proxy
| |||
`smc_semantic_common.sweep_state` with a faithful *semantic EVENT* port of the
| |||
frozen runtime detector + the frozen F1 event contract. It exists so the
| |||
research-side Candidate Setup extractor can consume the SAME sweep semantics as
| |||
the frozen MQL5 runtime (AF_Engine2_Agents / AlgoForge_Backtest_Baseline.mq5
| |||
ProcessStructure + DetectLiquidityGrabs) instead of a state latch without expiry.
| |||
| |||
SOURCE OF TRUTH (frozen, unmodified):
| |||
- docs/SMC_LIQUIDITY_SWEEP_SPEC_v1.md (P3-S.2 project semantic spec)
| |||
- docs/SNIPERGOLD_CANONICAL_SETUP_CONTRACT_v1.md §K / §M (EVENT contract)
| |||
- docs/P3_S11_EVENT_CONTRACT_REPAIR.md (F1 EVENT {ts,dir,valid_until,
| |||
superseded}
| |||
- MQL5 runtime: AlgoForge_Backtest_Baseline.mq5
| |||
ProcessStructure(len=5,internal=true) (internal pivot emission)
| |||
DetectLiquidityGrabs(...) (sweep detection geometry)
| |||
g_feat[7] + AFEventSweep (P3-S.0 / F1) (event lifecycle/validity)
| |||
| |||
THE CONVENTION (what "Liquidity Sweep" means HERE — an EVENT, not a latch):
| |||
- A Liquidity Sweep is an EVENT with 4 fields: onset bar, direction, a bounded
| |||
validity window W_sweep, and a superseded flag.
| |||
- It is NOT "the latest sweep forever". Expiry = W_sweep bars after onset.
| |||
- Supersession: a NEWER onset replaces (refreshes) the active event.
| |||
| |||
DETECTION GEOMETRY (byte-locked to the runtime, NOT a new formula):
| |||
1. Internal swing pivot at bar p (fractal length 5 one-sided, i.e. 5 left + 5
| |||
right confirmations, strict inequality, equal bars do NOT cancel):
| |||
begin = max(startBar, 2*5); for i in [begin, total): p = i-5; p>=5
| |||
IsPivotHigh: idx-5>=0 && idx+5<total && all h[p]>h[p± k], k=1..5
| |||
IsPivotLow : idx-5>=0 && idx+5<total && all l[p]<l[p± k], k=1..5
| |||
2. For each confirmed internal pivot, scan b in (p, min(total-1, p+8)]:
| |||
buy-side swept : isHigh && high[b]>lvl && close[b]<lvl
| |||
-> dir=-1 (bearish intent)
| |||
sell-side swept: !isHigh && low[b]<lvl && close[b]>lvl
| |||
-> dir=+1 (bullish intent)
| |||
first matching bar per pivot sets an onset; `if b>cur_onset` (strictly
| |||
newer bar wins) matches the runtime tie-break. Later bars that satisfy
| |||
the condition in the same window are NOT new events for that pivot.
| |||
3. EVENT materialization (F1): each onset is an EVENT with
| |||
valid_until = onset + W_sweep (40); a newer onset supersedes the old.
| |||
| |||
DIRECTION (SPEC §5, not variable-name):
| |||
buy-side liquidity taken -> -1 (BEARISH); sell-side taken -> +1 (BULLISH).
| |||
| |||
CONSUMERS MUST apply the validity window: active iff onset <= r <= onset+W_sweep.
| |||
| |||
The legacy `smc_semantic_common.sweep_state` (a persistent latch marking every
| |||
bar >= onset as active) is NOT this module's semantics, and is NOT used by the
| |||
Candidate-Setup extractor path. It remains untouched inside the frozen P3-S.2
| |||
diagnostic suites which intentionally MEASURED that legacy state.
| |||
| |||
This file is research-only. No MQL5, no FEATURE_CONTRACT, no model artifact.
| |||
"""
| |||
import numpy as np
| |||
| |||
# Frozen constants (P3-S.2 §M / AF_Defines.mqh / AF_BT_*)
| |||
INTERNAL_LEN = 5 # internal pivot fractal one-sided length
| |||
GRAB_WINDOW = 8 # AF_E2_SWEEP_LOOKBACK / AF_BT_GRAB_WIN probe window
| |||
W_SWEEP = 40 # F1 sweep EVENT validity (M15 bars), InpSeqWindow origin
| |||
| |||
| |||
def _is_pivot_high(h, idx, length, total):
| |||
"""Mirror IsPivotHigh (strictly higher than 5 bars each side, equal cancels)."""
| |||
if idx - length < 0 or idx + length >= total:
| |||
return False
| |||
v = h[idx]
| |||
for k in range(1, length + 1):
| |||
if h[idx - k] > v or h[idx + k] > v:
| |||
return False
| |||
return True
| |||
| |||
| |||
def _is_pivot_low(l, idx, length, total):
| |||
"""Mirror IsPivotLow (strictly lower than 5 bars each side)."""
| |||
if idx - length < 0 or idx + length >= total:
| |||
return False
| |||
v = l[idx]
| |||
for k in range(1, length + 1):
| |||
if l[idx - k] < v or l[idx + k] < v:
| |||
return False
| |||
return True
| |||
| |||
| |||
def internal_pivots(h, l, start_bar=None, length=INTERNAL_LEN):
| |||
"""Confirmed internal swing pivots, monotonic chronological index.
| |||
| |||
Mirrors ProcessStructure(len=length, internal=true): begin = max(start_bar,
| |||
2*length); for i in [begin, n): p = i-length; if p>=length and IsPivot(p):
| |||
append (p, level, is_high).
| |||
A pivot at p is CONFIRMED once the p+length confirmation bar is available.
| |||
start_bar : the runtime warm-up / analysis-start bar. The runtime uses a
| |||
large cache (AF_BT_MIN_BARS / begin=100); default None -> 100 to match
| |||
the real-data runtime warm-up. Synthetic geometry tests may pass a
| |||
smaller start_bar (e.g. 0 -> begin = 2*length).
| |||
Returns list of (p, level: float, is_high: bool) in ascending p order.
| |||
"""
| |||
n = len(h)
| |||
begin = 100 if start_bar is None else start_bar
| |||
begin = max(begin, 2 * length)
| |||
pivots = []
| |||
for i in range(begin, n):
| |||
p = i - length
| |||
if p >= length:
| |||
if _is_pivot_high(h, p, length, n):
| |||
pivots.append((int(p), float(h[p]), True))
| |||
if _is_pivot_low(l, p, length, n):
| |||
pivots.append((int(p), float(l[p]), False))
| |||
return pivots
| |||
| |||
| |||
def sweep_scans(h, l, c, pivots, grab_window=GRAB_WINDOW, n_total=None,
| |||
length=INTERNAL_LEN, effective_window=True):
| |||
"""Raw sweep detection over a fixed full trace (DetectLiquidityGrabs +
| |||
the frozen SPEC's effective-window consequence, SMC_LIQUIDITY_SWEEP_SPEC §4).
| |||
| |||
For each confirmed internal pivot, the first bar b that satisfies
| |||
wick-penetration + same-bar close-back sets an onset. Per the frozen spec's
| |||
authoritative consequence note, the confirmation bars p+1 .. p+(length) would
| |||
cancel a pivot, so the mathematically possible sweep bars are
| |||
b in [p+length+1, min(total-1, p+grab_window)] (i.e. the effective window,
| |||
for length=5: b in [p+6, p+8]). When `effective_window` is False the raw EA
| |||
window (p+1 .. p+grab) is used for comparison.
| |||
| |||
Returns list of raw onsets {"onset","dir","level","pivot"} chronological
| |||
(dedup by onset bar, newest wins on a same-bar tie, per SPEC §12[3]).
| |||
"""
| |||
total = n_total if n_total is not None else len(c)
| |||
events = []
| |||
first_b_offset = (length + 1) if effective_window else 1
| |||
for (p, lvl, is_high) in pivots:
| |||
if p >= total:
| |||
continue
| |||
last = min(total - 1, p + grab_window)
| |||
first = p + first_b_offset
| |||
if first > last:
| |||
continue
| |||
for b in range(first, last + 1):
| |||
if b >= total:
| |||
break
| |||
if is_high and h[b] > lvl and c[b] < lvl:
| |||
events.append({"onset": int(b), "dir": -1,
| |||
"level": float(lvl), "pivot": int(p)})
| |||
break
| |||
if (not is_high) and l[b] < lvl and c[b] > lvl:
| |||
events.append({"onset": int(b), "dir": 1,
| |||
"level": float(lvl), "pivot": int(p)})
| |||
break
| |||
# EMISSION ORDER (byte-locked to the runtime): a sweep onset bar b is
| |||
# visible at decision r = b, so NEW events are the RECORD HIGHS of onsets
| |||
# in BAR order, not in pivot order. The runtime DetectLiquidityGrabs keeps
| |||
# g_swpBar only when b > g_swpBar (strict), and processes pivots ascending
| |||
# p, so a same-bar tie resolves to the EARLIEST pivot. Sorting by
| |||
# (onset, pivot) and taking strictly-increasing onsets reproduces the
| |||
# runtime's new-event sequence exactly (verified against the byte-locked
| |||
# sliding-window reference on the full authorized scope).
| |||
events.sort(key=lambda e: (e["onset"], e["pivot"]))
| |||
latest_onset = -1
| |||
out = []
| |||
for e in events:
| |||
if e["onset"] > latest_onset:
| |||
latest_onset = e["onset"]
| |||
out.append(e)
| |||
return out
| |||
| |||
| |||
def sweep_events(h, l, c, start_bar=None, w_sweep=W_SWEEP,
| |||
grab_window=GRAB_WINDOW, effective_window=True):
| |||
"""F1 sweep EVENT port: internal pivots + DetectLiquidityGrabs scan +
| |||
EVENT materialization with validity & supersession.
| |||
| |||
Returns a dict:
| |||
events : list of EVENT dicts in onset order
| |||
{"onset","dir","level","pivot","valid_until","superseded"}
| |||
active : np.int array y[r] = dir of the ACTIVE (latest, in-validity)
| |||
event at decision bar r, else 0. (per-bar consumption view)
| |||
legacy_latch_compare : demonstration that this is NOT the latch (each bar
| |||
only carries a dir within W_sweep of its own onset).
| |||
Callers needing the Candidate-Setup chain must consume `events` (or the
| |||
active[] view) and apply the validity window themselves (see the events'
| |||
valid_until).
| |||
"""
| |||
pivots = internal_pivots(h, l, start_bar=start_bar)
| |||
raw = sweep_scans(h, l, c, pivots, grab_window=grab_window,
| |||
effective_window=effective_window)
| |||
| |||
n = len(c)
| |||
active = np.zeros(n, dtype=int)
| |||
# per-bar active dir = newest onset visible & in-validity (SPEC §9 lifecycle)
| |||
events = []
| |||
for e in raw:
| |||
on, d = e["onset"], e["dir"]
| |||
upto = min(n - 1, on + w_sweep)
| |||
# mark this window (later onset overwrites -> supersession by construction)
| |||
active[on:upto + 1] = d
| |||
ev = dict(e)
| |||
ev["valid_until"] = upto
| |||
ev["superseded"] = False
| |||
events.append(ev)
| |||
| |||
# supersession flags: a later onset replaces an earlier one permanently
| |||
for i in range(len(events)):
| |||
for j in range(i + 1, len(events)):
| |||
if events[j]["onset"] > events[i]["onset"]:
| |||
events[i]["superseded"] = True
| |||
return {
| |||
"events": events,
| |||
"active": active,
| |||
"pivots": pivots,
| |||
"n_pivots": len(pivots),
| |||
"n_onsets": len(events),
| |||
"w_sweep": int(w_sweep),
| |||
"grab_window": int(grab_window),
| |||
}
|