SniperGold_ML/ml/p3/p3_s222_discrepancy_impact/s222_common.py

134 lines
4.2 KiB
Python

# -*- coding: utf-8 -*-
"""P3-S22.2 COMMON — shared infrastructure for the discrepancy impact
assessment (ADJ-1 / ADJ-2).
Disposable / research-only. NO production modification. Reuses only the
FROZEN committed research inputs (caches, npz) and the frozen F3 oracle
(canonical_oracle / F3SetupEngine) as the semantic reference. The independent
oracles for the discrepancies live in this namespace and do NOT call the
MQL5 runtime.
Usage: imported by s222_partA_*.py / s222_partB_*.py / s222_*.py
"""
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"))
for p in (SETUP, SMC, os.path.normpath(os.path.join(HERE, ".."))):
if p not in sys.path:
sys.path.insert(0, p)
M15 = 900 # seconds
M30 = 1800
H4 = 14400
# F2 zone-state enumeration (frozen AF_Defines / AF_Engine2_Agents.mqh)
UNMITIGATED = 0
PARTIALLY = 1
FULLY = 2
# Frozen F3 windows (P3-S.17R.2)
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)
# ------------------------------------------------------------------
# Independent UTC-clock M30 aggregation from M15 bars (ADJ-2 oracle)
# ------------------------------------------------------------------
def utc_clock_m30(t15, o15, h15, l15, c15):
"""Aggregate M15 bars into fixed UTC 30-minute buckets purely by
wall-clock timestamp. An M30 bar covers [open, open+1800) with open a
multiple of 1800 s from the Unix epoch. Every M15 bar belongs to the
bucket whose interval contains its OPEN time.
Returns (t30, o30, h30, l30, c30) chronological."""
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 // M30) * M30 # UTC bucket open (int64)
# group starts where the bucket changes
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()
# drop any trailing bucket with no closed bar (should not happen for a
# complete feed; a bucket is always closed once its M15 bar is present)
return t30, o30, h30, l30, c30
def as_of_m30_utc_gate(t15, m30_dir_per_bucket):
"""Map per-bucket context dir to a per-M15-bar gate series using the
frozen as-of rule (newest CLOSED M30 bar with close_time <= t+900)."""
n = len(t15)
def bucket_id(t):
return int(((t + M15 - M30) // M30)) # newest closed M30 id at t+900
ids = np.array([bucket_id(int(x)) for x in t15], dtype=np.int64)
base = int(ids[0]) if n else 0
rel = ids - base
gate = np.zeros(n, dtype=int)
for i, ridx in enumerate(rel):
if 0 <= ridx < len(m30_dir_per_bucket):
gate[i] = int(m30_dir_per_bucket[ridx])
elif ridx < 0:
gate[i] = 0
else:
gate[i] = int(m30_dir_per_bucket[-1])
return gate