You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
184 lines
6.7 KiB
Python
184 lines
6.7 KiB
Python
"""
|
|
V28 Stage A: Signal sanity probe.
|
|
Does the existing sparse attention cache contain usable information about V23 trade outcomes?
|
|
|
|
Usage:
|
|
python scripts/v28_probe/wiki_signal_probe.py
|
|
|
|
Gate (promote to Stage B):
|
|
spread (WR_has_signal - WR_no_signal) > 3pp
|
|
AND avg PnL on has_signal trades > 0
|
|
AND has_signal trade count >= 20
|
|
"""
|
|
|
|
import gzip
|
|
import json
|
|
import os
|
|
|
|
import numpy as np
|
|
|
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
RUNS_DIR = os.path.join(BASE_DIR, "runs", "intraday_orb")
|
|
ATTENTION_DIR = os.path.join(BASE_DIR, "data", "cache", "orb_attention")
|
|
|
|
V23_200D = os.path.join(RUNS_DIR, "intraday_20260419_195924_f325f91a.json")
|
|
V23_400D = os.path.join(RUNS_DIR, "intraday_20260419_213436_27e7c928.json")
|
|
|
|
# Loaded once per run
|
|
_attention_cache: dict[str, dict] = {}
|
|
|
|
|
|
def load_attention(ticker: str) -> dict:
|
|
if ticker in _attention_cache:
|
|
return _attention_cache[ticker]
|
|
path = os.path.join(ATTENTION_DIR, f"{ticker}.json.gz")
|
|
if not os.path.exists(path):
|
|
_attention_cache[ticker] = {}
|
|
return {}
|
|
try:
|
|
with gzip.open(path) as f:
|
|
data = json.load(f)
|
|
days = data.get("days", {})
|
|
except Exception:
|
|
days = {}
|
|
_attention_cache[ticker] = days
|
|
return days
|
|
|
|
|
|
def has_signal(entry: dict, wiki_thresh: float, article_thresh: int) -> bool:
|
|
wiki = entry.get("attention_wiki_spike_10d") or 0.0
|
|
articles = entry.get("attention_article_count_3d") or 0
|
|
return wiki >= wiki_thresh or articles >= article_thresh
|
|
|
|
|
|
def analyze_trades(trades: list, label: str, wiki_thresh: float = 2.0, article_thresh: int = 1) -> dict:
|
|
signal_trades = []
|
|
no_signal_trades = []
|
|
missing = 0
|
|
|
|
for trade in trades:
|
|
ticker = trade["ticker"]
|
|
date = trade["date"]
|
|
pnl = trade["pnl"]
|
|
win = pnl > 0
|
|
|
|
days = load_attention(ticker)
|
|
if date not in days:
|
|
missing += 1
|
|
no_signal_trades.append({"pnl": pnl, "win": win, "ticker": ticker, "date": date})
|
|
elif has_signal(days[date], wiki_thresh, article_thresh):
|
|
signal_trades.append({
|
|
"pnl": pnl, "win": win, "ticker": ticker, "date": date,
|
|
"wiki": days[date].get("attention_wiki_spike_10d", 0),
|
|
"articles": days[date].get("attention_article_count_3d", 0),
|
|
})
|
|
else:
|
|
no_signal_trades.append({"pnl": pnl, "win": win, "ticker": ticker, "date": date})
|
|
|
|
def stats(bucket):
|
|
if not bucket:
|
|
return {"count": 0, "wr": 0.0, "avg_pnl": 0.0, "total_pnl": 0.0}
|
|
wins = sum(1 for t in bucket if t["win"])
|
|
return {
|
|
"count": len(bucket),
|
|
"wr": wins / len(bucket),
|
|
"avg_pnl": float(np.mean([t["pnl"] for t in bucket])),
|
|
"total_pnl": sum(t["pnl"] for t in bucket),
|
|
}
|
|
|
|
s = stats(signal_trades)
|
|
n = stats(no_signal_trades)
|
|
spread = s["wr"] - n["wr"]
|
|
|
|
print(f"\n{'='*60}")
|
|
print(f" {label} [wiki>={wiki_thresh}, articles>={article_thresh}]")
|
|
print(f"{'='*60}")
|
|
print(f" Total trades: {len(trades)} (cache miss: {missing})")
|
|
print(f" HAS SIGNAL: count={s['count']} WR={s['wr']:.1%} avg_pnl=${s['avg_pnl']:.2f}")
|
|
print(f" NO SIGNAL: count={n['count']} WR={n['wr']:.1%} avg_pnl=${n['avg_pnl']:.2f}")
|
|
print(f" WR spread: {spread:+.1%}")
|
|
|
|
gate_pass = spread > 0.03 and s["avg_pnl"] > 0 and s["count"] >= 20
|
|
print(f"\n Gate: spread>{0.03:.0%}:{spread*100:.1f}pp avg_pnl>0:${s['avg_pnl']:.2f} count>=20:{s['count']}")
|
|
print(f" => {'PROMOTE to Stage B' if gate_pass else 'FAIL'}")
|
|
|
|
return {
|
|
"label": label, "signal": s, "no_signal": n, "spread": spread,
|
|
"gate_pass": gate_pass, "signal_trades": signal_trades,
|
|
}
|
|
|
|
|
|
def analyze_2024_losing_days(run_400d: dict):
|
|
"""For each V23 2024 losing day, what % of cached tickers had wiki signal?"""
|
|
print(f"\n{'='*60}")
|
|
print(" 2024 LOSING DAY ANALYSIS (400d run)")
|
|
print(f"{'='*60}")
|
|
|
|
losing_2024 = [
|
|
d for d in run_400d.get("daily_summary", [])
|
|
if d["date"].startswith("2024") and d["daily_pnl"] < 0 and d["trades"] > 0
|
|
]
|
|
print(f" V23 2024 losing days (with trades): {len(losing_2024)}")
|
|
|
|
tickers = [f.replace(".json.gz", "") for f in os.listdir(ATTENTION_DIR) if f.endswith(".json.gz")][:200]
|
|
|
|
rows = []
|
|
for day in losing_2024[:8]:
|
|
date = day["date"]
|
|
checked = with_signal = 0
|
|
for ticker in tickers:
|
|
d = load_attention(ticker)
|
|
if date in d:
|
|
checked += 1
|
|
if has_signal(d[date], 2.0, 1):
|
|
with_signal += 1
|
|
pct = (with_signal / checked * 100) if checked else 0
|
|
rows.append((date, day["daily_pnl"], checked, with_signal, pct))
|
|
print(f" {date}: V23 PnL=${day['daily_pnl']:+.0f} checked={checked} signal={with_signal} ({pct:.0f}%)")
|
|
|
|
if rows:
|
|
print(f"\n Avg signal coverage on V23 losing days: {np.mean([r[4] for r in rows]):.0f}%")
|
|
|
|
|
|
def main():
|
|
print("V28 Stage A: Wiki signal sanity probe")
|
|
|
|
_attention_cache.clear()
|
|
|
|
with open(V23_200D) as f:
|
|
run_200d = json.load(f)
|
|
with open(V23_400D) as f:
|
|
run_400d = json.load(f)
|
|
|
|
r200 = analyze_trades(run_200d["trades"], "V23 200d", wiki_thresh=2.0)
|
|
r400 = analyze_trades(run_400d["trades"], "V23 400d", wiki_thresh=2.0)
|
|
|
|
print()
|
|
r200_low = analyze_trades(run_200d["trades"], "V23 200d [sensitivity: wiki>=1.5]", wiki_thresh=1.5)
|
|
r400_low = analyze_trades(run_400d["trades"], "V23 400d [sensitivity: wiki>=1.5]", wiki_thresh=1.5)
|
|
|
|
analyze_2024_losing_days(run_400d)
|
|
|
|
# Final verdict
|
|
any_pass = r200["gate_pass"] or r400["gate_pass"] or r200_low["gate_pass"] or r400_low["gate_pass"]
|
|
print(f"\n\n{'='*60}")
|
|
print(" FINAL VERDICT")
|
|
print(f"{'='*60}")
|
|
print(f" 200d (thresh=2.0): {'PASS' if r200['gate_pass'] else 'FAIL'}")
|
|
print(f" 400d (thresh=2.0): {'PASS' if r400['gate_pass'] else 'FAIL'}")
|
|
print(f" 200d (thresh=1.5): {'PASS' if r200_low['gate_pass'] else 'FAIL'}")
|
|
print(f" 400d (thresh=1.5): {'PASS' if r400_low['gate_pass'] else 'FAIL'}")
|
|
print(f"\n Decision: {'PROMOTE to Stage B' if any_pass else 'ABORT V28 — signal is noise in existing cache'}")
|
|
|
|
if any_pass:
|
|
best = max([r200, r400, r200_low, r400_low], key=lambda r: r["spread"])
|
|
print(f"\n Best: {best['label']} spread={best['spread']:+.1%}")
|
|
top = sorted(best["signal_trades"], key=lambda x: x["pnl"], reverse=True)[:10]
|
|
print(" Top signal trades:")
|
|
for t in top:
|
|
print(f" {t['date']} {t['ticker']:6s} wiki={t['wiki']:.2f} articles={t['articles']} pnl=${t['pnl']:+.0f}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|