forked from chiki2bum2/SniperGold_ML
160 lines
No EOL
5.5 KiB
Python
160 lines
No EOL
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)"} |