forked from chiki2bum2/SniperGold_ML
160 lines
5.5 KiB
Python
160 lines
5.5 KiB
Python
# -*- coding: utf-8 -*-
| |||
"""P3-S22.3 M30 REPAIR - COMMON shared infrastructure.
| |||
| |||
Disposable / research-only. This namespace is ADDITIVE: it does NOT modify the
| |||
committed research M30 construction, frozen gate caches, P3-S16/P3-S17/P3-S18/
| |||
P3-S20 artifacts, FEATURE_CONTRACT, labels, TP/SL/horizon, or production MQL5.
| |||
| |||
It imports ONLY frozen commits + frozen chain-parity machinery. The corrected
| |||
UTC-clock M30 construction and its independent oracle live in this namespace.
| |||
| |||
Usage: imported by s223_part*.py modules.
| |||
"""
| |||
import datetime as dt
| |||
import hashlib
| |||
import json
| |||
import os
| |||
import sys
| |||
| |||
import numpy as np
| |||
| |||
HERE = os.path.dirname(os.path.abspath(__file__))
| |||
OUT = os.path.join(HERE, "output")
| |||
REPO = os.path.normpath(os.path.join(HERE, "..", "..", ".."))
| |||
SETUP = os.path.normpath(os.path.join(HERE, "..", "setup_dataset"))
| |||
SMC = os.path.normpath(os.path.join(HERE, "..", "smc_semantic"))
| |||
P3 = os.path.normpath(os.path.join(HERE, ".."))
| |||
for p in (P3, SETUP, SMC):
| |||
if p not in sys.path:
| |||
sys.path.insert(0, p)
| |||
| |||
M15 = 900 # seconds
| |||
M30 = 1800
| |||
H4 = 14400
| |||
| |||
E2_MIN_BARS = 80 # frozen Context-agent minimum cache (AF_Defines.mqh)
| |||
| |||
# Frozen F3 windows (P3-S.17R.2 contracts)
| |||
W_SWEEP = 40
| |||
W_CHOCH = 40
| |||
W_SETUP = 40
| |||
W_M3 = 2
| |||
| |||
SCOPE_START = dt.datetime(2017, 1, 3, tzinfo=dt.timezone.utc)
| |||
SCOPE_END = dt.datetime(2026, 7, 21, 23, 45, tzinfo=dt.timezone.utc)
| |||
| |||
| |||
def _sha16(obj):
| |||
return hashlib.sha256(
| |||
json.dumps(obj, sort_keys=True, default=str).encode()).hexdigest()[:16]
| |||
| |||
| |||
def sha256_of(obj):
| |||
return hashlib.sha256(
| |||
json.dumps(obj, sort_keys=True, default=str).encode()).hexdigest()
| |||
| |||
| |||
def now_utc():
| |||
return dt.datetime.now(dt.timezone.utc).isoformat()
| |||
| |||
| |||
def load_all():
| |||
import spec_tests_vectorized_primitives as VPR
| |||
return VPR.load_all()
| |||
| |||
| |||
def load_gates():
| |||
import spec_tests_vectorized_primitives as VPR
| |||
return VPR.load_gates()
| |||
| |||
| |||
def scope_mask(t):
| |||
import spec_tests_vectorized_primitives as VPR
| |||
return VPR.scope_mask(t)
| |||
| |||
| |||
def committed_m30_gate():
| |||
g = load_gates()
| |||
return g[1] # index-pair resample research M30 gate (committed)
| |||
| |||
| |||
# --------------------------------------------------------------------------
| |||
# CORRECTED RESEARCH M30 CONSTRUCTION (UTC-clock buckets)
| |||
# --------------------------------------------------------------------------
| |||
def corrected_m30_from_m15(t15, o15, h15, l15, c15):
| |||
"""Build corrected research M30 bars from M15 by FIXED UTC 30-minute
| |||
buckets:
| |||
t30_open = floor(t15 / 1800) * 1800 (UTC epoch, seconds)
| |||
Aggregate open / high / low / close within [open, open+1800).
| |||
| |||
Deterministic chronological output. A bucket is emitted only when it
| |||
contains >= 1 M15 bar. No forward fill, no look-ahead. Returns arrays.
| |||
"""
| |||
n = len(t15)
| |||
if n == 0:
| |||
return (np.array([], dtype=np.int64),
| |||
np.array([], dtype=np.float64),
| |||
np.array([], dtype=np.float64),
| |||
np.array([], dtype=np.float64),
| |||
np.array([], dtype=np.float64))
| |||
opens = (t15.astype(np.int64) // M30) * M30
| |||
change = np.empty(n, dtype=bool)
| |||
change[0] = True
| |||
change[1:] = opens[1:] != opens[:-1]
| |||
start = np.flatnonzero(change)
| |||
end = np.append(start[1:], n)
| |||
t30 = opens[start]
| |||
o30 = o15[start]
| |||
c30 = c15[end - 1]
| |||
h30 = np.empty(len(start), dtype=np.float64)
| |||
l30 = np.empty(len(start), dtype=np.float64)
| |||
for k, (a, b) in enumerate(zip(start, end)):
| |||
seg = slice(a, b)
| |||
h30[k] = h15[seg].max()
| |||
l30[k] = l15[seg].min()
| |||
return t30, o30, h30, l30, c30
| |||
| |||
| |||
def as_of_m30_gate(t15, m30_dir_per_bucket, t30=None):
| |||
"""Map per-bucket Context-agent direction to a per-M15-bar gate series
| |||
using the FROZEN as-of rule: newest CLOSED M30 bar with close_time <= t+900.
| |||
| |||
close_time = bucket_open + 1800 ; decision close = t + 900.
| |||
Implemented with np.searchsorted over the ACTUAL closed closes (gap-aware):
| |||
for every gap, each M15 bar still as-ofs the most-recently closed M30 bar
| |||
that precedes its decision close. This is the exact rule used by the
| |||
validated P3-S22.2 oracle (which reproduced the committed dirs 0-mismatch).
| |||
Requires t15 and t30 (bucket opens) for the closes.
| |||
"""
| |||
t15 = np.asarray(t15, dtype=np.int64)
| |||
if t30 is None:
| |||
# derive bucket opens from t15 (must be contiguous gap-free input;
| |||
# for gapped feeds the caller MUST supply the real t30)
| |||
t30 = (t15 // M30) * M30
| |||
m30_close = np.asarray(t30, dtype=np.int64) + M30
| |||
dc = t15 + M15 # decision close
| |||
idx = np.searchsorted(m30_close, dc, side="right") - 1
| |||
n = len(t15)
| |||
gate = np.zeros(n, dtype=int)
| |||
valid = (idx >= 0) & (idx < len(m30_dir_per_bucket))
| |||
gate[valid] = np.asarray(m30_dir_per_bucket, dtype=int)[
| |||
np.clip(idx[valid], 0, len(m30_dir_per_bucket) - 1)]
| |||
return gate
| |||
| |||
| |||
def invariant_checkpoint(t15, t30):
| |||
"""Assert the explicit M30 invariant:
| |||
M30_open == floor(M15_open / 1800) * 1800 (UTC)
| |||
verified for every research M15 bar that begins a complete UTC M30 bucket.
| |||
"""
| |||
opens = (t15.astype(np.int64) // M30) * M30
| |||
change = np.empty(len(t15), dtype=bool)
| |||
change[0] = True
| |||
change[1:] = opens[1:] != opens[:-1]
| |||
start = np.flatnonzero(change)
| |||
mismatch = int((t30 != opens[start]).sum())
| |||
return {"n_buckets": int(len(t30)),
| |||
"n_start_m15_checked": int(len(start)),
| |||
"mismatch": mismatch,
| |||
"invariant_holds": bool(mismatch == 0),
| |||
"formula": "M30_open = floor(M15_open / 1800) * 1800 (UTC clock)"}
|