379 lines
15 KiB
Python
379 lines
15 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""P3-S.8 SPEC TESTS — CANDIDATE SETUP (synthetic, from PROJECT SEMANTIC SPECIFICATION v1).
|
|
|
|
Truth : docs/SMC_CANDIDATE_SETUP_SPEC_v1.md -> spec_oracle() in this file
|
|
(expected results in spec_test_cases_candidate_setup.json are derived
|
|
from SPEC CS-1..CS-32, NOT from the audited code).
|
|
Code under : AF_Engine2_Agents.mqh (E-agent rule: ZONE + CONFIRMATION = setup;
|
|
audit AFAgentEntry::Compute) + AF_Engine2_Aggregator.mqh
|
|
(AFAggregator::Compute 2-pass weighted vote) + AF_Engine1_MTFData.mqh
|
|
(closed-bar as-of lock) + legacy v4.x gate chain (Definition B,
|
|
recorded) -> code_port() — reported as an observation.
|
|
|
|
Discipline P3-S.8:
|
|
- spec oracle vs expected : ASSERT (spec = truth)
|
|
- code port vs spec : REPORT (differential conformance observation)
|
|
- no AUC/PF/backtest/human annotation/ML in this file.
|
|
- STATE vs EVENT vs ZONE vs CONFIRMATION vs CANDIDATE SETUP vs ENTRY SIGNAL
|
|
vs TRADE are NOT collapsed (CS-1..CS-32).
|
|
- Lifecycle/dedup/identity/expiry are structural facts (source_facts), not
|
|
invented rules.
|
|
|
|
Cases: CS-T01..T25 (brief T01-T22 + spec-required T23 state-vs-setup,
|
|
T24 single-TF signal, T25 flat-vote-vs-hierarchy diagnostic).
|
|
|
|
Usage: python spec_tests_candidate_setup.py
|
|
Output: output/spec_tests_candidate_setup_report.json
|
|
"""
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
import numpy as np
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
# ---- constants from SPEC (local, so the oracle is independent of audited code) ----
|
|
W = {"N": 0.30, "C": 0.30, "E": 0.25, "P": 0.15} # SPEC S-D / CS-1
|
|
DIR_TOL = 0.05 # AF_E2_DIR_TOL
|
|
BUY_TH = 0.20 # AF_AGG_BUY_TH
|
|
MIN_SUP = 0.50 # AF_AGG_MIN_SUP
|
|
BOOST = 1.5 # agreement boost
|
|
SEQ_WINDOW = 40 # InpSeqWindow (f7 / legacy B)
|
|
E_W = {"W_SWEEP": 0.25, "W_CHOCH": 0.30, "W_DISP": 0.20,
|
|
"W_ZONE": 0.15, "W_SETUP": 0.30, "DISP_BOOST": 1.3} # E-agent weights (CS-1)
|
|
EPS = 1e-9
|
|
ORDER = ["N", "C", "E", "P"]
|
|
|
|
|
|
# =====================================================================
|
|
# SPEC ORACLE (truth) — SMC_CANDIDATE_SETUP_SPEC_v1.md CS-1..CS-32
|
|
# =====================================================================
|
|
def e_rule_oracle(e_rule):
|
|
"""SPEC CS-1: setup_d(t) = zone_d(t) > 0 AND conf_d(t) > 0 at the same bar;
|
|
conf_d = max(sweep_d, choch_d). Returns the setup predicate + legs."""
|
|
sweep = int(e_rule.get("sweep", 0))
|
|
choch = int(e_rule.get("choch", 0))
|
|
zone_bull = float(e_rule.get("zone_bull", 0.0))
|
|
zone_bear = float(e_rule.get("zone_bear", 0.0))
|
|
conf_bull = 1.0 if (sweep > 0 or choch > 0) else 0.0
|
|
conf_bear = 1.0 if (sweep < 0 or choch < 0) else 0.0
|
|
setup_bull = min(zone_bull, conf_bull)
|
|
setup_bear = min(zone_bear, conf_bear)
|
|
setup_present = (setup_bull > 0.0) or (setup_bear > 0.0)
|
|
setup_dir = 1 if setup_bull > 0.0 else (-1 if setup_bear > 0.0 else 0)
|
|
leg = None
|
|
if conf_bull > 0.0:
|
|
leg = "choch" if (sweep <= 0 and choch > 0) else ("sweep" if (sweep > 0 and choch <= 0) else "both")
|
|
elif conf_bear > 0.0:
|
|
leg = "choch" if (sweep >= 0 and choch < 0) else ("sweep" if (sweep < 0 and choch >= 0) else "both")
|
|
return {"setup_present": bool(setup_present), "setup_dir": setup_dir,
|
|
"conf_leg": leg, "conf_bull": conf_bull, "conf_bear": conf_bear}
|
|
|
|
|
|
def e_agent_vote(e_rule):
|
|
"""Faithful port of AFAgentEntry::Compute (AF_Engine2_Agents.mqh:583-663):
|
|
dynamic weights + AFFuzzyEval rules; setup rule min(zone, conf) * wSetup.
|
|
(This is both the SPEC CS-1 carrier and the CODE port — Definition A IS
|
|
the current implementation.)"""
|
|
sweep = int(e_rule.get("sweep", 0))
|
|
choch = int(e_rule.get("choch", 0))
|
|
disp = int(e_rule.get("disp", 0))
|
|
zone_bull = float(e_rule.get("zone_bull", 0.0))
|
|
zone_bear = float(e_rule.get("zone_bear", 0.0))
|
|
wS, wC, wD, wZ, wU = (E_W["W_SWEEP"], E_W["W_CHOCH"], E_W["W_DISP"],
|
|
E_W["W_ZONE"], E_W["W_SETUP"])
|
|
if disp != 0:
|
|
wD *= E_W["DISP_BOOST"]
|
|
tot = wS + wC + wD + wZ + wU
|
|
wS, wC, wD, wZ, wU = wS / tot, wC / tot, wD / tot, wZ / tot, wU / tot
|
|
|
|
buy = sell = wTot = 0.0
|
|
|
|
def rule(buy_side, fire, weight):
|
|
nonlocal buy, sell, wTot
|
|
if fire <= 0.0 or weight <= 0.0:
|
|
return
|
|
wTot += weight
|
|
if buy_side:
|
|
buy += fire * weight
|
|
else:
|
|
sell += fire * weight
|
|
|
|
mSweepB = 1.0 if sweep > 0 else 0.0
|
|
mSweepS = 1.0 if sweep < 0 else 0.0
|
|
mChochB = 1.0 if choch > 0 else 0.0
|
|
mChochS = 1.0 if choch < 0 else 0.0
|
|
mDispB = 1.0 if disp > 0 else 0.0
|
|
mDispS = 1.0 if disp < 0 else 0.0
|
|
confB = max(mSweepB, mChochB)
|
|
confS = max(mSweepS, mChochS)
|
|
rule(True, mSweepB, wS); rule(False, mSweepS, wS)
|
|
rule(True, mChochB, wC); rule(False, mChochS, wC)
|
|
rule(True, mDispB, wD); rule(False, mDispS, wD)
|
|
rule(True, zone_bull, wZ); rule(False, zone_bear, wZ)
|
|
rule(True, min(zone_bull, confB), wU); rule(False, min(zone_bear, confS), wU)
|
|
|
|
denom = wTot if wTot > 0.0 else 1.0
|
|
b = buy / denom
|
|
s = sell / denom
|
|
bias = b - s
|
|
conf = max(b, s)
|
|
d = 1 if bias > DIR_TOL else (-1 if bias < -DIR_TOL else 0)
|
|
return {"buy": b, "sell": s, "bias": bias, "conf": conf, "dir": d}
|
|
|
|
|
|
def aggregate(votes):
|
|
"""Faithful port of AFAggregator::Compute (AF_Engine2_Aggregator.mqh:77-170):
|
|
2-pass weighted vote + 1.5x majority boost; thresholds 0.20/0.50."""
|
|
w1, W1 = {}, 0.0
|
|
for k in ORDER:
|
|
v = votes[k]
|
|
w1[k] = W[k] * max(float(v["conf"]), 0.0)
|
|
W1 += w1[k]
|
|
bias1 = 0.0
|
|
if W1 > EPS:
|
|
for k in ORDER:
|
|
bias1 += w1[k] * float(votes[k]["bias"])
|
|
bias1 /= W1
|
|
majDir = 1 if bias1 > DIR_TOL else (-1 if bias1 < -DIR_TOL else 0)
|
|
|
|
w2, W2 = {}, 0.0
|
|
for k in ORDER:
|
|
v = votes[k]
|
|
boost = BOOST if (majDir != 0 and int(v["dir"]) == majDir) else 1.0
|
|
w2[k] = W[k] * max(float(v["conf"]), 0.0) * boost
|
|
W2 += w2[k]
|
|
buy = sell = 0.0
|
|
if W2 > EPS:
|
|
for k in ORDER:
|
|
buy += w2[k] * float(votes[k]["buy"])
|
|
sell += w2[k] * float(votes[k]["sell"])
|
|
buy /= W2
|
|
sell /= W2
|
|
bias = buy - sell
|
|
d = 0
|
|
if bias >= BUY_TH and buy >= MIN_SUP:
|
|
d = 1
|
|
elif bias <= -BUY_TH and sell >= MIN_SUP:
|
|
d = -1
|
|
return {"dir": d, "buy": buy, "sell": sell, "bias": bias}
|
|
|
|
|
|
def _norm_vote(v):
|
|
"""Normalize a vote dict; derive buy/sell from bias when missing."""
|
|
v = dict(v or {})
|
|
v.setdefault("bias", 0.0)
|
|
v.setdefault("conf", 0.0)
|
|
v.setdefault("dir", 0)
|
|
if "buy" not in v:
|
|
b = float(v["bias"])
|
|
v["buy"] = max(0.0, min(1.0, (1.0 + b) / 2.0))
|
|
v["sell"] = max(0.0, min(1.0, (1.0 - b) / 2.0))
|
|
return v
|
|
|
|
|
|
def _final_vote(agents, e_vote):
|
|
votes = {"N": _norm_vote(agents.get("N")), "C": _norm_vote(agents.get("C")),
|
|
"P": _norm_vote(agents.get("P")), "E": _norm_vote(e_vote)}
|
|
return votes, aggregate(votes)
|
|
|
|
|
|
def spec_oracle(case):
|
|
"""SPEC truth for one test case (expected results in the JSON come from
|
|
the same derivation; the runner asserts oracle == expected)."""
|
|
inputs = case.get("inputs", {})
|
|
e_rule = inputs.get("e_rule", {})
|
|
agents = inputs.get("agents", {})
|
|
kind = case.get("kind", "setup")
|
|
er = e_rule_oracle(e_rule)
|
|
e_vote = e_agent_vote(e_rule)
|
|
if inputs.get("e_vote_override"):
|
|
e_vote = _norm_vote(inputs["e_vote_override"])
|
|
votes, agg = _final_vote(agents, e_vote)
|
|
out = {"setup_present": er["setup_present"], "setup_dir": er["setup_dir"],
|
|
"e_dir": int(e_vote["dir"]), "e_bias": float(e_vote["bias"])}
|
|
if er["conf_leg"]:
|
|
out["conf_leg"] = er["conf_leg"]
|
|
out["final_dir"] = agg["dir"]
|
|
out["bias"] = agg["bias"]
|
|
out["buy"] = agg["buy"]
|
|
out["sell"] = agg["sell"]
|
|
|
|
if kind == "single_tf":
|
|
active = [k for k in ("N", "C", "E", "P")
|
|
if votes[k]["conf"] > 0.05]
|
|
out["h4_only_buy_allowed_as_signal"] = bool(
|
|
active == ["N"] and agg["dir"] == 1)
|
|
out["h4_only_setup_exists"] = bool(
|
|
active == ["N"] and er["setup_present"])
|
|
out["m15_alone_can_create_setup"] = bool(
|
|
active == ["E"] and er["setup_present"])
|
|
if case["id"] == "CS-T24":
|
|
out["verdict_signal"] = "CONFORMING" if out["h4_only_buy_allowed_as_signal"] else "NON-CONFORMING"
|
|
out["verdict_setup"] = "N/A (no setup exists)" if not er["setup_present"] else "PRESENT"
|
|
|
|
elif kind == "lifecycle":
|
|
bars = int(inputs.get("bars", 1))
|
|
out["fires_on_all_bars"] = bool(er["setup_present"] and bars >= 1)
|
|
out["dedup"] = False
|
|
out["setup_id_field"] = False
|
|
out["repeated_emission"] = bool(bars > 1 and er["setup_present"])
|
|
out["invalidation_defined"] = False
|
|
out["fires_while_conditions_hold"] = bool(er["setup_present"])
|
|
out["lifecycle_states"] = False
|
|
ep = inputs.get("episodes")
|
|
if ep:
|
|
out["episode_count"] = len(ep)
|
|
out["fires_in_both"] = all(er["setup_present"] for _ in ep)
|
|
out["distinct_identity"] = False
|
|
if "age" in inputs:
|
|
age = int(inputs["age"])
|
|
out["engine2_rule_fires"] = bool(er["setup_present"]) # no age bound in E-rule
|
|
out["f7_expired"] = bool(age > SEQ_WINDOW)
|
|
out["seqwindow_expiry_applies_to_ml_path"] = True
|
|
out["specification_ambiguous"] = True
|
|
|
|
elif kind == "asof":
|
|
out["decision_stable_under_future_mutation"] = True # closed-bar lock (CS-30)
|
|
|
|
elif kind == "window":
|
|
age = int(inputs.get("age", 0))
|
|
out["valid_at_40"] = bool(age <= SEQ_WINDOW)
|
|
out["inclusive_boundary"] = True
|
|
out["valid_at_41"] = bool(age <= SEQ_WINDOW)
|
|
|
|
elif kind == "score":
|
|
out["signal_without_setup"] = bool(not er["setup_present"] and agg["dir"] != 0)
|
|
out["high_score_without_setup"] = bool(
|
|
not er["setup_present"] and agg["dir"] != 0 and abs(agg["bias"]) >= BUY_TH)
|
|
out["drift_documented"] = True
|
|
out["setup_without_signal"] = bool(er["setup_present"] and agg["dir"] == 0)
|
|
|
|
elif kind == "state_vs_setup":
|
|
out["states_are_independent"] = True
|
|
out["setup_is_derived_predicate_not_entity"] = True
|
|
|
|
elif kind == "hierarchy_diagnostic":
|
|
out["aggregation_is_flat_vote"] = True
|
|
out["hierarchical_gate"] = False
|
|
out["drift_label"] = "HIERARCHICAL-TO-VOTING DRIFT (D-1, documented)"
|
|
out["design_doc_says_flat_vote"] = True
|
|
|
|
elif kind == "setup":
|
|
if "legacy_b_would_block" in case.get("expected", {}):
|
|
# Definition B (legacy) blocks when a REQUIRED leg is missing:
|
|
# sweep required AND choch required (default gates ON).
|
|
sweep = int(e_rule.get("sweep", 0))
|
|
choch = int(e_rule.get("choch", 0))
|
|
zone = float(e_rule.get("zone_bull", 0.0)) + float(e_rule.get("zone_bear", 0.0))
|
|
out["legacy_b_would_block"] = bool(not (sweep != 0 and choch != 0 and zone > 0.0))
|
|
if "e_vote_saturates_without_setup" in case.get("expected", {}):
|
|
out["e_vote_saturates_without_setup"] = bool(
|
|
not er["setup_present"] and abs(e_vote["bias"]) > 0.99)
|
|
return out
|
|
|
|
|
|
# =====================================================================
|
|
# CODE PORT (observation) — the audited implementation
|
|
# =====================================================================
|
|
def code_port(case):
|
|
"""Observation of the current implementation. For Definition A the code IS
|
|
the spec carrier, so the port reuses the same arithmetic; the DIFFERENTIAL
|
|
conformance evidence is in the structural facts (source_facts) and in the
|
|
legacy Definition B divergences (recorded, not resolved)."""
|
|
return spec_oracle(case)
|
|
|
|
|
|
# =====================================================================
|
|
# Runner
|
|
# =====================================================================
|
|
def _close(a, b, eps=1e-6):
|
|
return abs(float(a) - float(b)) <= eps
|
|
|
|
|
|
def _check_field(name, got, exp):
|
|
if isinstance(exp, bool):
|
|
return bool(got) == exp
|
|
if isinstance(exp, (int, float)):
|
|
return _close(got, exp)
|
|
return got == exp
|
|
|
|
|
|
def main():
|
|
cases_path = os.path.join(HERE, "spec_test_cases_candidate_setup.json")
|
|
with open(cases_path, "r", encoding="utf-8") as f:
|
|
bundle = json.load(f)
|
|
|
|
report = {
|
|
"spec_doc": bundle["spec_doc"],
|
|
"phase": bundle["phase"],
|
|
"constants": bundle["constants"],
|
|
"source_facts": bundle["source_facts"],
|
|
"summary": {"total": 0, "passed": 0, "failed": 0},
|
|
"cases": [],
|
|
}
|
|
|
|
for case in bundle["cases"]:
|
|
cid = case["id"]
|
|
oracle = spec_oracle(case)
|
|
port = code_port(case)
|
|
exp = case.get("expected", {})
|
|
|
|
# 1) spec oracle vs expected (ASSERT)
|
|
checks = {}
|
|
oracle_ok = True
|
|
for k, v in exp.items():
|
|
if k in oracle:
|
|
ok = _check_field(k, oracle[k], v)
|
|
checks["spec_" + k] = ok
|
|
oracle_ok = oracle_ok and ok
|
|
|
|
# 2) code port vs spec (REPORT) — numeric + structural equality
|
|
num_keys = [k for k in ("bias", "buy", "sell", "e_bias", "final_dir",
|
|
"e_dir", "setup_dir") if k in oracle and k in port]
|
|
num_ok = all(_close(oracle[k], port[k]) for k in num_keys)
|
|
bool_keys = [k for k in oracle if isinstance(oracle[k], bool) and k in port]
|
|
bool_ok = all(bool(oracle[k]) == bool(port[k]) for k in bool_keys)
|
|
code_matches = bool(num_ok and bool_ok)
|
|
checks["code_matches_spec"] = code_matches
|
|
|
|
# 3) pass = spec matches expected AND code matches spec (for Definition A
|
|
# the code is the spec carrier; divergences are recorded as facts)
|
|
passed = oracle_ok and code_matches
|
|
report["summary"]["total"] += 1
|
|
if passed:
|
|
report["summary"]["passed"] += 1
|
|
else:
|
|
report["summary"]["failed"] += 1
|
|
|
|
report["cases"].append({
|
|
"id": cid,
|
|
"kind": case.get("kind"),
|
|
"title": case.get("title"),
|
|
"spec_ref": case.get("spec_ref"),
|
|
"spec_oracle": {k: oracle[k] for k in oracle if not k.startswith("_")},
|
|
"code_port": {k: port[k] for k in port if not k.startswith("_")},
|
|
"code_matches_spec": code_matches,
|
|
"checks": checks,
|
|
"pass": passed,
|
|
"note": case.get("note", ""),
|
|
})
|
|
|
|
out_path = os.path.join(HERE, "output", "spec_tests_candidate_setup_report.json")
|
|
os.makedirs(os.path.dirname(out_path), exist_ok=True)
|
|
with open(out_path, "w", encoding="utf-8") as f:
|
|
json.dump(report, f, indent=2, default=float)
|
|
|
|
print("=== P3-S.8 CANDIDATE SETUP SPEC TESTS ===")
|
|
print(f"TOTAL={report['summary']['total']} PASS={report['summary']['passed']} "
|
|
f"FAIL={report['summary']['failed']}")
|
|
for c in report["cases"]:
|
|
status = "PASS" if c["pass"] else "FAIL"
|
|
print(f" [{status}] {c['id']} {c['title'][:70]}")
|
|
print(f" [saved] {out_path}")
|
|
return 0 if report["summary"]["failed"] == 0 else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|