Files
siloqy/adaptive_exit/calibrate_v7_long_from_journal.py

316 lines
12 KiB
Python
Raw Normal View History

2026-05-08 19:54:13 +02:00
"""Calibrate AlphaExitEngineV7 thresholds for synthetic LONG EFSM paths.
This script replays BLUE V7 decision journal price paths with side inverted to
LONG. It follows the original V7 SHORT calibration pattern:
1. Reconstruct per-trade path from V7 journal rows.
2. Compute the natural end-of-path LONG outcome.
3. Replay AlphaExitEngineV7 on the same path using side=LONG.
4. Sweep configurable threshold surfaces.
5. Compare the first V7 EXIT against the natural end outcome.
The output is a calibration proxy for EFSM FLIP_LONG trades, not proof from
actual exchange-filled LONG trades.
"""
from __future__ import annotations
import argparse
import base64
import csv
import json
import logging
import math
import sys
import urllib.request
from collections import defaultdict
from dataclasses import asdict
from pathlib import Path
from statistics import fmean
from typing import Any
ROOT = Path("/mnt/dolphinng5_predict")
sys.path.insert(0, str(ROOT / "nautilus_dolphin"))
sys.path.insert(1, str(ROOT))
from nautilus_dolphin.nautilus.alpha_exit_v7_engine import AlphaExitEngineV7, AlphaExitV7Config # noqa: E402
CH_URL = "http://localhost:8123/?database=dolphin"
AUTH = "Basic " + base64.b64encode(b"dolphin:dolphin_ch_2026").decode()
FEE_PCT = 0.0004
logging.getLogger("nautilus_dolphin.nautilus.alpha_exit_v7_engine").setLevel(logging.ERROR)
def _query(sql: str) -> str:
req = urllib.request.Request(CH_URL, data=sql.encode(), headers={"Authorization": AUTH})
return urllib.request.urlopen(req, timeout=60).read().decode()
def load_v7_rows(limit_trades: int = 0) -> list[dict[str, Any]]:
trade_filter = ""
if limit_trades > 0:
trade_filter = (
"AND trade_id IN ("
"SELECT trade_id FROM ("
"SELECT trade_id, max(ts) AS mx FROM v7_decision_events "
"WHERE strategy='blue' AND side='SHORT' GROUP BY trade_id ORDER BY mx DESC "
f"LIMIT {int(limit_trades)}"
"))"
)
sql = f"""
SELECT
ts, trade_id, asset, entry_price, current_price, quantity, leverage,
bar_idx, decision_seq, bars_held, ob_imbalance,
exf_funding, exf_dvol, exf_fear_greed, exf_taker
FROM v7_decision_events
WHERE strategy='blue' AND side='SHORT' {trade_filter}
ORDER BY trade_id ASC, decision_seq ASC, ts ASC
FORMAT CSVWithNames
"""
text = _query(sql)
rows: list[dict[str, Any]] = []
for r in csv.DictReader(text.splitlines()):
rows.append({
"ts": r["ts"],
"trade_id": r["trade_id"],
"asset": r["asset"],
"entry_price": float(r["entry_price"] or 0.0),
"current_price": float(r["current_price"] or 0.0),
"quantity": float(r["quantity"] or 0.0),
"leverage": float(r["leverage"] or 0.0),
"bar_idx": int(float(r["bar_idx"] or 0)),
"decision_seq": int(float(r["decision_seq"] or 0)),
"bars_held": int(float(r["bars_held"] or 0)),
"ob_imbalance": float(r["ob_imbalance"] or 0.0),
"exf_funding": float(r["exf_funding"] or 0.0),
"exf_dvol": float(r["exf_dvol"] or 0.0),
"exf_fear_greed": float(r["exf_fear_greed"] or 0.0),
"exf_taker": float(r["exf_taker"] or 0.0),
})
return rows
def group_paths(rows: list[dict[str, Any]]) -> list[list[dict[str, Any]]]:
grouped: dict[str, list[dict[str, Any]]] = defaultdict(list)
for row in rows:
grouped[row["trade_id"]].append(row)
paths = []
for path in grouped.values():
path.sort(key=lambda r: (r["decision_seq"], r["bar_idx"], r["ts"]))
clean = [r for r in path if r["entry_price"] > 0 and r["current_price"] > 0]
if len(clean) >= 2:
paths.append(clean)
paths.sort(key=lambda p: p[-1]["ts"])
return paths
def natural_long_return(path: list[dict[str, Any]]) -> float:
entry = path[0]["entry_price"]
last = path[-1]["current_price"]
return (last - entry) / entry - FEE_PCT if entry > 0 else 0.0
def pnl_dollars(path: list[dict[str, Any]], ret: float) -> float:
notional = abs(path[0]["entry_price"] * path[0]["quantity"])
return notional * ret
def replay_path(path: list[dict[str, Any]], cfg: AlphaExitV7Config) -> dict[str, Any]:
engine = AlphaExitEngineV7(
bar_duration_sec=11.0,
bounce_model_path="/tmp/nonexistent-bounce-model.pkl",
config=cfg,
)
ctx = engine.make_context(entry_price=path[0]["entry_price"], entry_bar=path[0]["bar_idx"], side=0)
first_exit = None
decisions = []
for row in path:
if hasattr(ctx, "set_exf"):
ctx.set_exf(
funding=row["exf_funding"],
dvol=row["exf_dvol"],
fear_greed=row["exf_fear_greed"],
taker=row["exf_taker"],
)
dec = engine.evaluate(
ctx,
current_price=row["current_price"],
current_bar=row["bar_idx"],
ob_imbalance=row["ob_imbalance"],
asset=row["asset"],
)
decisions.append(dec)
if first_exit is None and dec["action"] == "EXIT":
first_exit = (row, dec)
break
nat_ret = natural_long_return(path)
if first_exit is None:
exit_ret = nat_ret
exit_row = path[-1]
exit_dec = decisions[-1]
exited = False
else:
exit_row, exit_dec = first_exit
exit_ret = (exit_row["current_price"] - path[0]["entry_price"]) / path[0]["entry_price"] - FEE_PCT
exited = True
return {
"trade_id": path[0]["trade_id"],
"asset": path[0]["asset"],
"n_rows": len(path),
"natural_ret": nat_ret,
"natural_pnl": pnl_dollars(path, nat_ret),
"exit_ret": exit_ret,
"exit_pnl": pnl_dollars(path, exit_ret),
"delta_pnl": pnl_dollars(path, exit_ret) - pnl_dollars(path, nat_ret),
"exited": exited,
"exit_action": exit_dec.get("action"),
"exit_reason": exit_dec.get("reason") or "",
"exit_pressure": float(exit_dec.get("exit_pressure", 0.0) or 0.0),
"exit_bars_held": int(exit_dec.get("bars_held", 0) or 0),
"exit_mae": float(exit_dec.get("mae", 0.0) or 0.0),
"exit_mfe": float(exit_dec.get("mfe", 0.0) or 0.0),
"exit_mae_risk": float(exit_dec.get("mae_risk", 0.0) or 0.0),
"exit_mfe_risk": float(exit_dec.get("mfe_risk", 0.0) or 0.0),
}
def equity_stats(vals: list[float]) -> dict[str, float]:
eq = 1.0
peak = 1.0
dd = 0.0
for r in vals:
eq *= max(0.0, 1.0 + r)
peak = max(peak, eq)
dd = max(dd, (peak - eq) / peak if peak else 0.0)
return {
"n": len(vals),
"wr": sum(1 for r in vals if r > 0) / len(vals) if vals else 0.0,
"mean": fmean(vals) if vals else 0.0,
"compound": eq - 1.0,
"max_dd": dd,
}
def summarize(results: list[dict[str, Any]], cfg: AlphaExitV7Config, name: str) -> dict[str, Any]:
natural_rets = [r["natural_ret"] for r in results]
exit_rets = [r["exit_ret"] for r in results]
deltas = [r["delta_pnl"] for r in results]
return {
"name": name,
"config": asdict(cfg),
"n": len(results),
"exits": sum(1 for r in results if r["exited"]),
"exit_rate": sum(1 for r in results if r["exited"]) / len(results) if results else 0.0,
"natural": {
**equity_stats(natural_rets),
"pnl": sum(r["natural_pnl"] for r in results),
},
"v7": {
**equity_stats(exit_rets),
"pnl": sum(r["exit_pnl"] for r in results),
},
"delta_pnl": sum(deltas),
"positive_delta_trades": sum(1 for d in deltas if d > 0),
"negative_delta_trades": sum(1 for d in deltas if d < 0),
"avg_exit_pressure": fmean([r["exit_pressure"] for r in results if r["exited"]]) if any(r["exited"] for r in results) else 0.0,
"reasons": dict(sorted({
reason: sum(1 for r in results if r["exit_reason"] == reason)
for reason in {r["exit_reason"] for r in results}
}.items())),
}
def candidate_configs() -> list[tuple[str, AlphaExitV7Config]]:
out = [("short_default", AlphaExitV7Config())]
for threshold in [1.4, 1.7, 2.0, 2.35, 2.69, 3.0]:
out.append((f"exit_p{threshold}", AlphaExitV7Config(exit_pressure_threshold=threshold)))
for tier_scale in [0.5, 0.75, 1.0, 1.25, 1.5]:
out.append((
f"mae_scale_{tier_scale}",
AlphaExitV7Config(
mae_tier1_k=3.5 * tier_scale,
mae_tier2_k=7.0 * tier_scale,
mae_tier3_k=12.0 * tier_scale,
mae_tier1_floor=0.005 * tier_scale,
mae_tier2_floor=0.012 * tier_scale,
mae_tier3_floor=0.025 * tier_scale,
),
))
for mfe_scale in [0.5, 0.75, 1.25, 1.5]:
out.append((
f"mfe_risk_scale_{mfe_scale}",
AlphaExitV7Config(
mfe_convexity_exit_risk=1.5 * mfe_scale,
mfe_convexity_soft_risk=0.3 * mfe_scale,
mfe_accel_risk=0.2 * mfe_scale,
),
))
for late_start in [0.3, 0.45, 0.6, 0.75]:
out.append((f"late_start_{late_start}", AlphaExitV7Config(mae_late_start_frac=late_start)))
for threshold in [1.7, 2.0, 2.35]:
for mae_scale in [0.5, 0.75, 1.25]:
out.append((
f"combo_p{threshold}_mae{mae_scale}",
AlphaExitV7Config(
exit_pressure_threshold=threshold,
mae_tier1_k=3.5 * mae_scale,
mae_tier2_k=7.0 * mae_scale,
mae_tier3_k=12.0 * mae_scale,
mae_tier1_floor=0.005 * mae_scale,
mae_tier2_floor=0.012 * mae_scale,
mae_tier3_floor=0.025 * mae_scale,
),
))
return out
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--limit-trades", type=int, default=0)
parser.add_argument("--out", default="/tmp/v7_long_calibration.json")
args = parser.parse_args()
rows = load_v7_rows(limit_trades=args.limit_trades)
paths = group_paths(rows)
summaries = []
for name, cfg in candidate_configs():
results = [replay_path(path, cfg) for path in paths]
summaries.append(summarize(results, cfg, name))
summaries.sort(key=lambda r: (r["delta_pnl"], r["v7"]["pnl"], -r["exits"]), reverse=True)
payload = {
"method": "Synthetic LONG replay of BLUE SHORT V7 decision journal paths; bounce model disabled.",
"input": {
"rows": len(rows),
"paths": len(paths),
"limit_trades": args.limit_trades,
},
"top_by_delta": summaries[:20],
"all": summaries,
}
Path(args.out).write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
print(json.dumps({
"input": payload["input"],
"top_by_delta": [
{
"name": s["name"],
"n": s["n"],
"exits": s["exits"],
"exit_rate": s["exit_rate"],
"natural_pnl": s["natural"]["pnl"],
"v7_pnl": s["v7"]["pnl"],
"delta_pnl": s["delta_pnl"],
"natural_compound": s["natural"]["compound"],
"v7_compound": s["v7"]["compound"],
"v7_dd": s["v7"]["max_dd"],
"reasons": s["reasons"],
}
for s in summaries[:12]
],
}, indent=2, sort_keys=True))
if __name__ == "__main__":
main()