Checkpoint BLUE V7 long overlay work
This commit is contained in:
315
adaptive_exit/calibrate_v7_long_from_journal.py
Normal file
315
adaptive_exit/calibrate_v7_long_from_journal.py
Normal file
@@ -0,0 +1,315 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user