Keep frozen research conclusions and code; raw ledgers and secrets stay out of git. Co-authored-by: Cursor <cursoragent@cursor.com>
534 lines
22 KiB
Python
534 lines
22 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Economic Attribution v0.1
|
|
|
|
Hard Evidence Population only:
|
|
MATCHED = Local Fill ↔ Venue Trade dual evidence
|
|
|
|
Purpose:
|
|
Economic Attribution only.
|
|
No strategy modification.
|
|
No live execution.
|
|
No economic simulation.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import math
|
|
import sys
|
|
from collections import defaultdict
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
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[str, Any]] = []
|
|
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 _parse_fill_context(df: pd.DataFrame) -> pd.DataFrame:
|
|
if df.empty or "fill_context" not in df.columns:
|
|
return pd.DataFrame(columns=["fill_id"])
|
|
rows = []
|
|
for _, r in df.iterrows():
|
|
ctx = r.get("fill_context")
|
|
if not isinstance(ctx, dict):
|
|
continue
|
|
rows.append(
|
|
{
|
|
"fill_id": r.get("fill_id"),
|
|
"market_event_before_fill": ctx.get("market_event_before_fill"),
|
|
"trade_imbalance_5s": ctx.get("trade_imbalance_5s"),
|
|
"price_velocity_5s": ctx.get("price_velocity_5s"),
|
|
"fill_type": ctx.get("fill_type"),
|
|
}
|
|
)
|
|
return pd.DataFrame(rows)
|
|
|
|
|
|
def _fav_ret(side: pd.Series, fill: pd.Series, px: pd.Series) -> pd.Series:
|
|
raw = (pd.to_numeric(px, errors="coerce") - pd.to_numeric(fill, errors="coerce")) / pd.to_numeric(
|
|
fill, errors="coerce"
|
|
)
|
|
return pd.Series(np.where(side == "long", raw, -raw), index=side.index)
|
|
|
|
|
|
def _cluster_weight(frame: pd.DataFrame) -> pd.Series:
|
|
cnt = frame.groupby("event_cluster_id")["event_cluster_id"].transform("count")
|
|
return 1.0 / cnt.clip(lower=1)
|
|
|
|
|
|
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:.4f}%"
|
|
|
|
|
|
def _num(v: float | None, digits: int = 4) -> str:
|
|
if v is None or (isinstance(v, float) and (math.isnan(v) or math.isinf(v))):
|
|
return "n/a"
|
|
return f"{v:.{digits}f}"
|
|
|
|
|
|
def _mean(s: pd.Series) -> float | None:
|
|
s = pd.to_numeric(s, errors="coerce").dropna()
|
|
return None if s.empty else float(s.mean())
|
|
|
|
|
|
def _sum(s: pd.Series) -> float:
|
|
s = pd.to_numeric(s, errors="coerce").fillna(0.0)
|
|
return float(s.sum())
|
|
|
|
|
|
def _weighted_mean(v: pd.Series, w: pd.Series) -> float | None:
|
|
vv = pd.to_numeric(v, errors="coerce")
|
|
ww = pd.to_numeric(w, errors="coerce").fillna(0.0)
|
|
mask = vv.notna() & ww.notna()
|
|
vv = vv[mask]
|
|
ww = ww[mask]
|
|
if vv.empty or float(ww.sum()) == 0.0:
|
|
return None
|
|
return float((vv * ww).sum() / ww.sum())
|
|
|
|
|
|
def _prepare_paths(paths: pd.DataFrame) -> pd.DataFrame:
|
|
paths = paths.copy()
|
|
if "max_price" in paths.columns and "min_price" in paths.columns and "fill_price" in paths.columns:
|
|
rng = (pd.to_numeric(paths["max_price"], errors="coerce") - pd.to_numeric(paths["min_price"], errors="coerce")) / pd.to_numeric(
|
|
paths["fill_price"], errors="coerce"
|
|
)
|
|
med = float(rng.dropna().median()) if rng.notna().any() else 0.0
|
|
paths["vol_bucket"] = np.where(rng >= med, "high_vol", "low_vol")
|
|
if "price_velocity_5s" in paths.columns and pd.to_numeric(paths["price_velocity_5s"], errors="coerce").notna().any():
|
|
v = pd.to_numeric(paths["price_velocity_5s"], errors="coerce")
|
|
thr = float(v.abs().median()) * 0.5
|
|
paths["trend_bucket"] = np.where(v > thr, "trend_up", np.where(v < -thr, "trend_down", "range"))
|
|
if "spread" in paths.columns and "fill_price" in paths.columns and pd.to_numeric(paths["spread"], errors="coerce").notna().any():
|
|
sp = pd.to_numeric(paths["spread"], errors="coerce") / pd.to_numeric(paths["fill_price"], errors="coerce")
|
|
med = float(sp.dropna().median()) if sp.notna().any() else 0.0
|
|
paths["liq_bucket"] = np.where(sp <= med, "tight_spread", "wide_spread")
|
|
paths["toxicity_bucket"] = np.where(paths["path_type"].astype(str).str.startswith("C"), "toxic", "non_toxic")
|
|
return paths
|
|
|
|
|
|
def _inventory_metrics(matched: pd.DataFrame) -> dict[str, float | None]:
|
|
if matched.empty:
|
|
return {}
|
|
g = matched.sort_values("venue_time_ms").copy()
|
|
g["signed_qty"] = np.where(g["side"] == "long", g["qty"], -g["qty"])
|
|
g["net_btc"] = g["signed_qty"].cumsum()
|
|
g["abs_net_btc"] = g["net_btc"].abs()
|
|
times = pd.to_numeric(g["venue_time_ms"], errors="coerce").astype("float64") / 1000.0
|
|
dt = times.shift(-1) - times
|
|
dt = dt.fillna(0.0).clip(lower=0.0)
|
|
total_t = float(dt.sum())
|
|
tw_abs = float((g["abs_net_btc"] * dt).sum() / total_t) if total_t > 0 else None
|
|
tw_signed = float((g["net_btc"] * dt).sum() / total_t) if total_t > 0 else None
|
|
return {
|
|
"max_net_btc": float(g["net_btc"].max()),
|
|
"min_net_btc": float(g["net_btc"].min()),
|
|
"max_abs_net_btc": float(g["abs_net_btc"].max()),
|
|
"avg_abs_net_btc_per_fill": float(g["abs_net_btc"].mean()),
|
|
"time_weighted_abs_net_btc": tw_abs,
|
|
"time_weighted_signed_net_btc": tw_signed,
|
|
"long_qty": float(g.loc[g["signed_qty"] > 0, "signed_qty"].sum()),
|
|
"short_qty": float((-g.loc[g["signed_qty"] < 0, "signed_qty"]).sum()),
|
|
"turnover_btc": float(g["qty"].sum()),
|
|
}
|
|
|
|
|
|
def _bucket_table(paths: pd.DataFrame, bucket: str, title: str) -> list[dict[str, Any]]:
|
|
if bucket not in paths.columns or paths.empty:
|
|
return []
|
|
rows = []
|
|
for key, grp in paths.groupby(bucket):
|
|
notional = grp["notional_usdt"].sum()
|
|
clusters = grp["event_cluster_id"].nunique()
|
|
rows.append(
|
|
{
|
|
"dimension": title,
|
|
"bucket": str(key),
|
|
"fills": int(len(grp)),
|
|
"clusters": int(clusters),
|
|
"btc_qty": float(grp["qty"].sum()),
|
|
"notional_usdt": float(notional),
|
|
"fee_usdt": float(grp["commission_usdt"].sum()),
|
|
"fee_per_fill": float(grp["commission_usdt"].mean()) if len(grp) else None,
|
|
"fee_per_btc": float(grp["commission_usdt"].sum() / grp["qty"].sum()) if grp["qty"].sum() else None,
|
|
"markout_1s": _weighted_mean(grp["markout_1s"], grp["notional_usdt"]),
|
|
"markout_5s": _weighted_mean(grp["markout_5s"], grp["notional_usdt"]),
|
|
"markout_10s": _weighted_mean(grp["markout_10s"], grp["notional_usdt"]),
|
|
"markout_30s": _weighted_mean(grp["markout_30s"], grp["notional_usdt"]),
|
|
"markout_300s": _weighted_mean(grp["markout_300s"], grp["notional_usdt"]),
|
|
"gross_markout_30s_usdt": float(grp["gross_markout_30s_usdt"].sum()),
|
|
"realized_pnl_usdt": float(grp["realized_pnl_usdt"].sum()),
|
|
"net_attr_30s_usdt": float(grp["net_attr_30s_usdt"].sum()),
|
|
}
|
|
)
|
|
rows.sort(key=lambda x: (-x["fills"], x["bucket"]))
|
|
return rows
|
|
|
|
|
|
def _counterfactual(base: pd.DataFrame, exclude_col: str, exclude_values: set[str], label: str) -> dict[str, Any]:
|
|
kept = base[~base[exclude_col].astype(str).isin(exclude_values)].copy()
|
|
return {
|
|
"name": label,
|
|
"fills": int(len(kept)),
|
|
"clusters": int(kept["event_cluster_id"].nunique()) if not kept.empty else 0,
|
|
"btc_qty": float(kept["qty"].sum()) if not kept.empty else 0.0,
|
|
"fee_usdt": float(kept["commission_usdt"].sum()) if not kept.empty else 0.0,
|
|
"gross_markout_30s_usdt": float(kept["gross_markout_30s_usdt"].sum()) if not kept.empty else 0.0,
|
|
"realized_pnl_usdt": float(kept["realized_pnl_usdt"].sum()) if not kept.empty else 0.0,
|
|
"net_attr_30s_usdt": float(kept["net_attr_30s_usdt"].sum()) if not kept.empty else 0.0,
|
|
"markout_30s": _weighted_mean(kept["markout_30s"], kept["notional_usdt"]),
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description="Economic Attribution v0.1 (MATCHED only)")
|
|
ap.add_argument("--dir", default=str(ROOT / "logs" / "maker_edge"))
|
|
ap.add_argument("--out", default=str(ROOT / "logs" / "maker_edge" / "Economic_Attribution_v0_1.txt"))
|
|
ap.add_argument("--recon03", default=str(ROOT / "logs" / "maker_edge" / "RECONCILIATION_03.json"))
|
|
ap.add_argument("--account", default=str(ROOT / "logs" / "maker_edge" / "Account_Reconciliation.json"))
|
|
ap.add_argument("--venue-trades", default=str(ROOT / "logs" / "maker_edge" / "venue_trades.json"))
|
|
args = ap.parse_args()
|
|
|
|
log_dir = Path(args.dir)
|
|
df = _load_jsonl_df(log_dir)
|
|
fills = df[df["event"] == "fill"].copy()
|
|
paths = df[df["event"] == "fill_path"].copy()
|
|
inv = df[df["event"] == "inventory_tick"].copy()
|
|
|
|
venue_trades = json.loads(Path(args.venue_trades).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()
|
|
|
|
fc = _parse_fill_context(fills)
|
|
meta_cols = [
|
|
c
|
|
for c in [
|
|
"fill_id",
|
|
"side",
|
|
"fill_price",
|
|
"spread",
|
|
"spread_capture_pct",
|
|
"obi",
|
|
"trade_imbalance",
|
|
"bid_depth_5",
|
|
"ask_depth_5",
|
|
"book_age_ms",
|
|
"inventory",
|
|
"inventory_time",
|
|
"inventory_skew",
|
|
"pre_5s_deteriorated",
|
|
"mid",
|
|
"event_cluster_id",
|
|
"pair",
|
|
]
|
|
if c in fills.columns
|
|
]
|
|
meta = fills.drop_duplicates("fill_id")[meta_cols]
|
|
paths = paths.merge(meta, on="fill_id", how="left", suffixes=("", "_f"))
|
|
for col in ["event_cluster_id", "side", "fill_price", "spread_capture_pct", "mid"]:
|
|
alt = f"{col}_f"
|
|
if alt in paths.columns:
|
|
if col not in paths.columns:
|
|
paths[col] = paths[alt]
|
|
else:
|
|
paths[col] = paths[col].fillna(paths[alt])
|
|
if not fc.empty:
|
|
paths = paths.merge(fc, on="fill_id", how="left")
|
|
paths = _prepare_paths(paths)
|
|
|
|
venue = pd.DataFrame(venues_norm)
|
|
venue = venue[venue["venue_trade_id"].isin(matched_trade_ids)].copy()
|
|
venue = venue.rename(
|
|
columns={
|
|
"venue_trade_id": "trade_id_link",
|
|
"venue_order_id": "venue_order_id",
|
|
"qty": "qty",
|
|
"px": "venue_price",
|
|
"ts": "venue_ts",
|
|
}
|
|
)
|
|
raw_v = pd.DataFrame(venue_trades)
|
|
raw_v["trade_id_link"] = raw_v["id"].astype(str)
|
|
raw_v["venue_order_id"] = raw_v["orderId"].astype(str)
|
|
raw_v["commission_usdt"] = pd.to_numeric(raw_v["commission"], errors="coerce")
|
|
raw_v["realized_pnl_usdt"] = pd.to_numeric(raw_v["realizedPnl"], errors="coerce").fillna(0.0)
|
|
raw_v["venue_time_ms"] = pd.to_numeric(raw_v["time"], errors="coerce")
|
|
raw_v["qty"] = pd.to_numeric(raw_v["qty"], errors="coerce")
|
|
raw_v["venue_price"] = pd.to_numeric(raw_v["price"], errors="coerce")
|
|
raw_v["side"] = np.where(raw_v["buyer"].astype(bool), "long", "short")
|
|
raw_v = raw_v[raw_v["trade_id_link"].isin(matched_trade_ids)].copy()
|
|
|
|
matched_map = pd.DataFrame(
|
|
[
|
|
{
|
|
"fill_id": m["local"]["fill_id"],
|
|
"trade_id_link": m["venue"]["venue_trade_id"],
|
|
"venue_order_id": m["venue"]["venue_order_id"],
|
|
}
|
|
for m in recon["matched"]
|
|
]
|
|
)
|
|
|
|
paths = paths.merge(
|
|
matched_map.merge(
|
|
raw_v[
|
|
[
|
|
"trade_id_link",
|
|
"venue_order_id",
|
|
"commission_usdt",
|
|
"realized_pnl_usdt",
|
|
"venue_time_ms",
|
|
"qty",
|
|
"venue_price",
|
|
"side",
|
|
]
|
|
],
|
|
on=["trade_id_link", "venue_order_id"],
|
|
how="left",
|
|
),
|
|
on="fill_id",
|
|
how="left",
|
|
suffixes=("", "_venue"),
|
|
)
|
|
|
|
paths["fill_price"] = pd.to_numeric(paths["fill_price"], errors="coerce")
|
|
paths["qty"] = pd.to_numeric(paths["qty"], errors="coerce")
|
|
paths["notional_usdt"] = paths["fill_price"] * paths["qty"]
|
|
for sec, col in [(1, "after_1s_price"), (5, "after_5s_price"), (10, "after_10s_price"), (30, "after_30s_price"), (300, "after_5m_price")]:
|
|
paths[f"markout_{sec}s"] = _fav_ret(paths["side"], paths["fill_price"], paths[col])
|
|
paths["gross_markout_30s_usdt"] = paths["notional_usdt"] * paths["markout_30s"]
|
|
paths["net_attr_30s_usdt"] = (
|
|
paths["gross_markout_30s_usdt"]
|
|
- pd.to_numeric(paths["commission_usdt"], errors="coerce").fillna(0.0)
|
|
+ pd.to_numeric(paths["realized_pnl_usdt"], errors="coerce").fillna(0.0)
|
|
)
|
|
|
|
inventory_metrics = _inventory_metrics(
|
|
raw_v[
|
|
["venue_time_ms", "side", "qty", "commission_usdt", "realized_pnl_usdt", "venue_order_id", "trade_id_link"]
|
|
].copy()
|
|
)
|
|
|
|
n_matched_paths = len(paths)
|
|
n_matched_fills = len(fills)
|
|
n_matched_clusters = int(fills["event_cluster_id"].nunique()) if not fills.empty else 0
|
|
cluster_w = _cluster_weight(paths) if not paths.empty and "event_cluster_id" in paths.columns else pd.Series(dtype=float)
|
|
|
|
horizon_rows = []
|
|
for sec in (1, 5, 10, 30, 300):
|
|
col = f"markout_{sec}s"
|
|
valid = paths[col].notna()
|
|
sub = paths[valid]
|
|
w = sub["notional_usdt"]
|
|
horizon_rows.append(
|
|
{
|
|
"horizon": f"{sec}s",
|
|
"n": int(len(sub)),
|
|
"fill_w": _weighted_mean(sub[col], w),
|
|
"cluster_w": _weighted_mean(sub[col], _cluster_weight(sub) if not sub.empty else pd.Series(dtype=float)),
|
|
"gross_usdt": float((sub["notional_usdt"] * sub[col]).sum()) if not sub.empty else 0.0,
|
|
}
|
|
)
|
|
|
|
fee_total = float(paths["commission_usdt"].sum())
|
|
realized_total = float(paths["realized_pnl_usdt"].sum())
|
|
gross_30_total = float(paths["gross_markout_30s_usdt"].sum())
|
|
net_attr_30_total = float(paths["net_attr_30s_usdt"].sum())
|
|
total_qty = float(paths["qty"].sum())
|
|
total_notional = float(paths["notional_usdt"].sum())
|
|
|
|
bucket_rows: list[dict[str, Any]] = []
|
|
for col, title in [
|
|
("path_type", "PathType"),
|
|
("toxicity_bucket", "Toxicity"),
|
|
("vol_bucket", "Volatility"),
|
|
("liq_bucket", "Spread"),
|
|
("trend_bucket", "Trend"),
|
|
("market_event_before_fill", "FillContext"),
|
|
]:
|
|
bucket_rows.extend(_bucket_table(paths, col, title))
|
|
bucket_df = pd.DataFrame(bucket_rows)
|
|
|
|
negative_states: set[str] = set()
|
|
if not bucket_df.empty:
|
|
neg = bucket_df[(bucket_df["dimension"] != "PathType") & (bucket_df["markout_30s"] < 0)]
|
|
negative_states = set(neg["bucket"].astype(str))
|
|
|
|
counterfactuals = [
|
|
{
|
|
"name": "BASELINE",
|
|
"fills": int(len(paths)),
|
|
"clusters": int(paths["event_cluster_id"].nunique()) if not paths.empty else 0,
|
|
"btc_qty": total_qty,
|
|
"fee_usdt": fee_total,
|
|
"gross_markout_30s_usdt": gross_30_total,
|
|
"realized_pnl_usdt": realized_total,
|
|
"net_attr_30s_usdt": net_attr_30_total,
|
|
"markout_30s": _weighted_mean(paths["markout_30s"], paths["notional_usdt"]),
|
|
},
|
|
_counterfactual(paths, "path_type", {"C_toxic"}, "EXCLUDE_PATH_C"),
|
|
_counterfactual(paths, "toxicity_bucket", {"toxic"}, "EXCLUDE_TOXIC"),
|
|
_counterfactual(paths, "market_event_before_fill", negative_states, "EXCLUDE_NEGATIVE_STATE"),
|
|
]
|
|
|
|
account = json.loads(Path(args.account).read_text()) if Path(args.account).exists() else {}
|
|
recon03 = json.loads(Path(args.recon03).read_text()) if Path(args.recon03).exists() else {}
|
|
|
|
out_txt = Path(args.out)
|
|
out_json = out_txt.with_suffix(".json")
|
|
lines: list[str] = []
|
|
|
|
def p(s: str = "") -> None:
|
|
lines.append(s)
|
|
print(s)
|
|
|
|
p("=" * 72)
|
|
p("Economic Attribution v0.1")
|
|
p("=" * 72)
|
|
p("Experiment: MM_EDGE_EXP_001")
|
|
p("Population: MATCHED=3890")
|
|
p("Strategy: v0.1 FROZEN")
|
|
p("Execution: STOPPED")
|
|
p("Stage3: LOCKED")
|
|
p("Purpose: Economic Attribution only.")
|
|
p("No strategy modification. No live execution. No economic simulation.")
|
|
p()
|
|
p("Layer 1 — Hard Economic Evidence")
|
|
p("-" * 40)
|
|
p(f"Matched fills: {n_matched_fills}")
|
|
p(f"Matched paths: {n_matched_paths}")
|
|
p(f"Matched clusters: {n_matched_clusters}")
|
|
p(f"Fee total: {_num(fee_total, 6)} USDT")
|
|
p(f"Fee / fill: {_num(fee_total / max(n_matched_paths, 1), 6)} USDT")
|
|
p(f"Fee / BTC: {_num(fee_total / max(total_qty, 1e-12), 6)} USDT")
|
|
p(f"Fee / cluster: {_num(fee_total / max(n_matched_clusters, 1), 6)} USDT")
|
|
p(f"Realized component: {_num(realized_total, 6)} USDT")
|
|
p(f"Gross markout @30s: {_num(gross_30_total, 6)} USDT")
|
|
p(f"Net attributable @30s: {_num(net_attr_30_total, 6)} USDT")
|
|
p()
|
|
p("Markout by horizon (MATCHED only)")
|
|
p("-" * 40)
|
|
for row in horizon_rows:
|
|
p(
|
|
f"{row['horizon']:>5} n={row['n']:4d} fill-w={_pct(row['fill_w'])} "
|
|
f"cluster-w={_pct(row['cluster_w'])} gross={_num(row['gross_usdt'], 6)} USDT"
|
|
)
|
|
p()
|
|
p("Inventory carry / exposure")
|
|
p("-" * 40)
|
|
p(f"Max net BTC: {_num(inventory_metrics.get('max_net_btc'), 6)}")
|
|
p(f"Min net BTC: {_num(inventory_metrics.get('min_net_btc'), 6)}")
|
|
p(f"Max |net BTC|: {_num(inventory_metrics.get('max_abs_net_btc'), 6)}")
|
|
p(f"Average |net BTC|: {_num(inventory_metrics.get('avg_abs_net_btc_per_fill'), 6)}")
|
|
p(f"TW |net BTC|: {_num(inventory_metrics.get('time_weighted_abs_net_btc'), 6)}")
|
|
p(f"TW signed net BTC: {_num(inventory_metrics.get('time_weighted_signed_net_btc'), 6)}")
|
|
p(f"Long qty / Short qty: {_num(inventory_metrics.get('long_qty'), 6)} / {_num(inventory_metrics.get('short_qty'), 6)} BTC")
|
|
p(f"Inventory turnover: {_num(inventory_metrics.get('turnover_btc'), 6)} BTC")
|
|
p()
|
|
p("Slices (weighted by notional, MATCHED only)")
|
|
p("-" * 40)
|
|
for dim in ["PathType", "Toxicity", "Volatility", "Spread", "Trend", "FillContext"]:
|
|
sub = bucket_df[bucket_df["dimension"] == dim].copy()
|
|
if sub.empty:
|
|
continue
|
|
p(dim)
|
|
for _, r in sub.sort_values(["fills", "bucket"], ascending=[False, True]).iterrows():
|
|
p(
|
|
f" {r['bucket']}: n={int(r['fills'])} clusters={int(r['clusters'])} "
|
|
f"fee={_num(r['fee_usdt'], 4)} gross30={_num(r['gross_markout_30s_usdt'], 4)} "
|
|
f"realized={_num(r['realized_pnl_usdt'], 4)} net30={_num(r['net_attr_30s_usdt'], 4)} "
|
|
f"m30={_pct(r['markout_30s'])}"
|
|
)
|
|
p()
|
|
p("Layer 2 — Evidence Extension (excluded from core conclusion)")
|
|
p("-" * 40)
|
|
p(f"VENUE_CONFIRMED_NO_TRADE_HISTORY: {recon03.get('venue_confirmed_no_trade_history', 'n/a')}")
|
|
p(f"VENUE_PARTIAL_ORDER_CANCELED: {recon03.get('venue_partial_order_canceled', 'n/a')}")
|
|
p("These rows are order-confirmed, but not part of the Hard Evidence Population.")
|
|
p()
|
|
p("Layer 3 — Counterfactual Attribution (NOT backtest)")
|
|
p("-" * 40)
|
|
p("Observed vs Exclude-Bucket Attribution. These are contribution decompositions only.")
|
|
for row in counterfactuals:
|
|
p(
|
|
f"{row['name']}: fills={row['fills']} clusters={row['clusters']} "
|
|
f"fee={_num(row['fee_usdt'], 4)} gross30={_num(row['gross_markout_30s_usdt'], 4)} "
|
|
f"realized={_num(row['realized_pnl_usdt'], 4)} net30={_num(row['net_attr_30s_usdt'], 4)} "
|
|
f"m30={_pct(row['markout_30s'])}"
|
|
)
|
|
p()
|
|
p("Interpretation")
|
|
p("-" * 40)
|
|
p("Core conclusion is based on 3890 fully matched fills.")
|
|
p("Economic Attribution asks why MakerAlpha did not convert to money.")
|
|
p("It does NOT change quote logic, does NOT restart v0.1, and does NOT unlock Stage 3.")
|
|
p("=" * 72)
|
|
|
|
out_txt.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
sidecar = {
|
|
"experiment_id": "MM_EDGE_EXP_001",
|
|
"population": {
|
|
"name": "MATCHED",
|
|
"fills": n_matched_fills,
|
|
"paths": n_matched_paths,
|
|
"clusters": n_matched_clusters,
|
|
},
|
|
"strategy": "v0.1 FROZEN",
|
|
"execution": "STOPPED",
|
|
"stage3": "LOCKED",
|
|
"fee_total_usdt": fee_total,
|
|
"fee_per_fill_usdt": fee_total / max(n_matched_paths, 1),
|
|
"fee_per_btc_usdt": fee_total / max(total_qty, 1e-12),
|
|
"fee_per_cluster_usdt": fee_total / max(n_matched_clusters, 1),
|
|
"realized_component_usdt": realized_total,
|
|
"gross_markout_30s_usdt": gross_30_total,
|
|
"net_attr_30s_usdt": net_attr_30_total,
|
|
"markout_by_horizon": horizon_rows,
|
|
"inventory_metrics": inventory_metrics,
|
|
"bucket_rows": bucket_rows,
|
|
"counterfactuals": counterfactuals,
|
|
"recon03_extension": {
|
|
"venue_confirmed_no_trade_history": recon03.get("venue_confirmed_no_trade_history"),
|
|
"venue_partial_order_canceled": recon03.get("venue_partial_order_canceled"),
|
|
},
|
|
"account_recon_ref": account,
|
|
}
|
|
out_json.write_text(json.dumps(sidecar, indent=2) + "\n", encoding="utf-8")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|