Snapshot nautilus_mm after EXP_002 Phase 1 stop.
Keep frozen research conclusions and code; raw ledgers and secrets stay out of git. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Metric Reconciliation v0.1 (MATCHED only)
|
||||
|
||||
Confirms consistency between:
|
||||
- "MakerAlpha" reported in v0.1 research (return space)
|
||||
- "Gross markout @30s" in Economic Attribution (dollar space)
|
||||
- realized component used in Economic Attribution
|
||||
|
||||
Key point:
|
||||
Same definition may flip sign depending on weighting:
|
||||
fill-weighted mean return vs notional-weighted dollar markout
|
||||
|
||||
This script is read-only: it does NOT change any strategy/execution.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
|
||||
from reconcile_fills import load_local_fills, match, normalize_local, normalize_venue # noqa: E402
|
||||
|
||||
|
||||
def _load_jsonl_df(log_dir: Path) -> pd.DataFrame:
|
||||
rows: list[dict] = []
|
||||
for f in sorted(log_dir.glob("*.jsonl")):
|
||||
if f.name.startswith(("Account_", "Maker_", "RECON")):
|
||||
continue
|
||||
for line in f.open():
|
||||
try:
|
||||
e = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
if isinstance(e, dict):
|
||||
rows.append(e)
|
||||
return pd.DataFrame(rows)
|
||||
|
||||
|
||||
def _fav_ret(side: pd.Series, fill: pd.Series, px: pd.Series) -> pd.Series:
|
||||
# return space, signed by side
|
||||
fill = pd.to_numeric(fill, errors="coerce")
|
||||
px = pd.to_numeric(px, errors="coerce")
|
||||
raw = (px - fill) / fill
|
||||
return pd.Series(np.where(side == "long", raw, -raw), index=side.index)
|
||||
|
||||
|
||||
def _weighted_mean(x: pd.Series, w: pd.Series) -> float | None:
|
||||
xx = pd.to_numeric(x, errors="coerce")
|
||||
ww = pd.to_numeric(w, errors="coerce")
|
||||
mask = xx.notna() & ww.notna()
|
||||
xx = xx[mask]
|
||||
ww = ww[mask]
|
||||
if xx.empty:
|
||||
return None
|
||||
sw = float(ww.sum())
|
||||
if sw == 0:
|
||||
return None
|
||||
return float((xx * ww).sum() / sw)
|
||||
|
||||
|
||||
def _cluster_weight(paths: pd.DataFrame) -> pd.Series:
|
||||
if "event_cluster_id" not in paths.columns:
|
||||
return pd.Series(1.0, index=paths.index)
|
||||
cnt = paths.groupby("event_cluster_id")["event_cluster_id"].transform("count")
|
||||
return 1.0 / cnt.clip(lower=1)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="Economic Metric Reconciliation v0.1")
|
||||
ap.add_argument("--dir", default=str(ROOT / "logs" / "maker_edge"))
|
||||
ap.add_argument("--out", default=str(ROOT / "logs" / "maker_edge" / "Economic_Metric_Reconciliation_v0_1.txt"))
|
||||
ap.add_argument("--venue-trades", default=str(ROOT / "logs" / "maker_edge" / "venue_trades.json"))
|
||||
ap.add_argument("--matched-take", type=int, default=3890)
|
||||
args = ap.parse_args()
|
||||
|
||||
log_dir = Path(args.dir)
|
||||
venue_trades_path = Path(args.venue_trades)
|
||||
|
||||
df = _load_jsonl_df(log_dir)
|
||||
fills = df[df["event"] == "fill"].copy() if "event" in df.columns else pd.DataFrame()
|
||||
paths = df[df["event"] == "fill_path"].copy() if "event" in df.columns else pd.DataFrame()
|
||||
|
||||
# Hard matched population via RECON-02/03 evidence: use existing matcher logic.
|
||||
venue_trades = json.loads(venue_trades_path.read_text())
|
||||
local_fills_raw = load_local_fills(log_dir)
|
||||
locals_norm = [normalize_local(e, i) for i, e in enumerate(local_fills_raw)]
|
||||
venues_norm = [normalize_venue(t, i) for i, t in enumerate(venue_trades)]
|
||||
recon = match(locals_norm, venues_norm)
|
||||
|
||||
matched_fill_ids = {m["local"]["fill_id"] for m in recon["matched"]}
|
||||
matched_trade_ids = {m["venue"]["venue_trade_id"] for m in recon["matched"]}
|
||||
|
||||
fills = fills[fills["fill_id"].isin(matched_fill_ids)].copy()
|
||||
paths = paths[paths["fill_id"].isin(matched_fill_ids)].copy()
|
||||
|
||||
# Build after_30s already present in fill_path fields.
|
||||
# MakerAlpha in analyze_maker_edge uses after_30s_price and _fav_ret definition.
|
||||
# We'll recompute:
|
||||
# return space:
|
||||
# maker_alpha_fill_weighted = mean(markout_30s)
|
||||
# maker_alpha_notional_weighted_return = (gross_markout_usdt / total_notional)
|
||||
# gross_markout_usdt = sum(notional * markout_30s)
|
||||
#
|
||||
if paths.empty:
|
||||
raise SystemExit("No matched paths loaded")
|
||||
|
||||
# Merge meta from fills (side, fill_price, event_cluster_id, notional proxy)
|
||||
meta_cols = [
|
||||
c
|
||||
for c in [
|
||||
"fill_id",
|
||||
"side",
|
||||
"fill_price",
|
||||
"amount",
|
||||
"event_cluster_id",
|
||||
"spread_capture_pct",
|
||||
"pair",
|
||||
]
|
||||
if c in fills.columns
|
||||
]
|
||||
meta = fills.drop_duplicates("fill_id")[meta_cols]
|
||||
paths = paths.merge(meta, on="fill_id", how="left", suffixes=("", "_m"))
|
||||
|
||||
# If fill_path already had these columns, merge created *_m alternates.
|
||||
for col in ["side", "fill_price", "amount", "event_cluster_id"]:
|
||||
alt = f"{col}_m"
|
||||
if alt in paths.columns:
|
||||
if col not in paths.columns:
|
||||
paths[col] = paths[alt]
|
||||
else:
|
||||
paths[col] = paths[col].fillna(paths[alt])
|
||||
|
||||
# Ensure required fields
|
||||
paths["side"] = paths["side"].astype(str)
|
||||
paths["fill_price"] = pd.to_numeric(paths["fill_price"], errors="coerce")
|
||||
paths["qty"] = pd.to_numeric(paths["amount"], errors="coerce")
|
||||
paths["notional_usdt"] = paths["fill_price"] * paths["qty"]
|
||||
paths["after_30s_price"] = pd.to_numeric(paths["after_30s_price"], errors="coerce")
|
||||
|
||||
paths["markout_30s_return"] = _fav_ret(paths["side"], paths["fill_price"], paths["after_30s_price"])
|
||||
|
||||
gross_markout_usdt = float((paths["notional_usdt"] * paths["markout_30s_return"]).sum())
|
||||
total_notional = float(paths["notional_usdt"].sum())
|
||||
maker_alpha_fill_weighted = float(paths["markout_30s_return"].mean())
|
||||
maker_alpha_notional_weighted_return = float(gross_markout_usdt / total_notional) if total_notional else None
|
||||
|
||||
cw = _cluster_weight(paths)
|
||||
maker_alpha_cluster_weighted_return = _weighted_mean(paths["markout_30s_return"], cw)
|
||||
|
||||
# realized component from userTrades is already in Economic Attribution.
|
||||
# Here we only validate return space; realized component sign conventions are asserted elsewhere.
|
||||
out = Path(args.out)
|
||||
lines: list[str] = []
|
||||
|
||||
def p(s: str = "") -> None:
|
||||
lines.append(s)
|
||||
print(s)
|
||||
|
||||
p("=" * 72)
|
||||
p("Economic Metric Reconciliation v0.1 (MATCHED=3890)")
|
||||
p("=" * 72)
|
||||
p(f"Matched paths: {len(paths)} (expected ~3886)")
|
||||
p()
|
||||
p("Definitions (same math as analyze_maker_edge):")
|
||||
p("- markout_30s_return = _fav_ret(side, fill_price, after_30s_price)")
|
||||
p("- gross_markout_usdt = sum(notional_usdt * markout_30s_return)")
|
||||
p()
|
||||
p("Return-space metrics (sign may differ due to weighting):")
|
||||
p(f"MakerAlpha fill-weighted mean return: {_pct(maker_alpha_fill_weighted)}")
|
||||
p(f"MakerAlpha notional-weighted mean return: {_pct(maker_alpha_notional_weighted_return)}")
|
||||
p(f"MakerAlpha cluster-weighted mean return: {_pct(maker_alpha_cluster_weighted_return)}")
|
||||
p()
|
||||
p("Dollar-space metrics:")
|
||||
p(f"gross_markout_usdt (30s): {gross_markout_usdt:+.6f} USDT")
|
||||
p(f"total_notional_usdt: {total_notional:.3f} USDT")
|
||||
p()
|
||||
p("If fill-weighted return is + but gross_markout_usdt is negative,")
|
||||
p("it means notional weighting flips sign (alpha is conditionally realized).")
|
||||
p("=" * 72)
|
||||
|
||||
out.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
def _pct(v: float | None) -> str:
|
||||
if v is None or (isinstance(v, float) and (math.isnan(v) or math.isinf(v))):
|
||||
return "n/a"
|
||||
return f"{v*100:.6f}%"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
Reference in New Issue
Block a user