feat(config/scripts): GREEN config wiring + S6 recompute tooling
prod/configs/green.yml: - asset_bucket_ban_set: [4] (B4 banned at selector level) - s6_size_table: inline bootstrap multipliers (B0→0.4, B1→0.3, B3→2.0, B5→0.5, B6→1.5) matching CRITICAL_ASSET_PICKING S6 scenario - esof_sizing_table: FAV→1.2, MILD_POS→0.6, UNKNOWN→0.25, MILD_NEG→0.0, UNFAV→0.0 - use_int_leverage: true (1x fixed pending winrate analysis) - s6_table_path: pointer to generated YAML (recompute updates this) BLUE (blue.yml) carries none of these keys → BLUE math unchanged. prod/configs/green_s6_table.yml: bootstrap stub with frontmatter (generated_at, source_branch, n_trades). Regenerated by recompute script. prod/scripts/recompute_s6_coefficients.py: Queries trade_events, maps assets to KMeans buckets, derives per-bucket sizing mults. Variance guard: >20% net-PnL move flags bucket in dolphin.s6_recompute_log for manual review before promote. prod/s6_recompute_flow.py: Prefect flow wrapping the recompute script. Cadence via S6_RECOMPUTE_INTERVAL_DAYS env (default 30). Kill-switch: S6_RECOMPUTE_DISABLED=1. prod/scripts/analyze_leverage_winrate.py: Read-only walk of CH trade_events; bins trades by leverage_raw, emits per-bin WR/net-PnL/avg-MAE. Output informs the int-leverage rounding rule choice (Option 1 round-half-up vs Option 2 banker's round vs stay-at-1x). Does not auto-apply a rule change. Plan refs: Tasks 3, 8, 10. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
188
prod/scripts/analyze_leverage_winrate.py
Executable file
188
prod/scripts/analyze_leverage_winrate.py
Executable file
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Leverage → WR / net-PnL analysis over CH `dolphin.trade_events`.
|
||||
|
||||
Purpose
|
||||
───────
|
||||
The target exchanges (Binance futures) require integer leverage. The GREEN
|
||||
orchestrator single-site (`esf_alpha_orchestrator.py`) therefore rounds
|
||||
leverage to an integer at entry time; the current default is FIXED 1x with
|
||||
two candidate rounding rules commented in source, pending this analysis:
|
||||
|
||||
Option 1 (round-half-up, conservative): int(math.floor(raw + 0.5)), min=1
|
||||
Option 2 (banker's round, aggressive): int(round(raw)), min=1
|
||||
Stay-at-1x (current default): leverage_int = 1
|
||||
|
||||
This script walks historical trades and, for every distinct leverage value
|
||||
observed (bucketed to a user-chosen step), reports per-bin WR, net-PnL,
|
||||
avg-MAE and sample count. The output report informs the rule choice.
|
||||
|
||||
Decision rubric (documented in the generated report)
|
||||
────────────────────────────────────────────────────
|
||||
- Higher-lev bins show clearly better net-PnL w/ tight sample error → Option 2
|
||||
- Flat / noisy signal across bins → Option 1
|
||||
- Higher-lev bins show WORSE net-PnL / mixed → stay 1x
|
||||
|
||||
This script is read-only; it does not auto-apply a rule change.
|
||||
|
||||
Usage
|
||||
─────
|
||||
python prod/scripts/analyze_leverage_winrate.py \\
|
||||
--strategy blue \\
|
||||
--since 2026-01-01 \\
|
||||
--step 0.5 \\
|
||||
--out prod/docs/LEVERAGE_WINRATE_REPORT_2026-04-21.md
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
CH_URL = "http://localhost:8123/"
|
||||
CH_HEADERS = {
|
||||
"X-ClickHouse-User": "dolphin",
|
||||
"X-ClickHouse-Key": "dolphin_ch_2026",
|
||||
}
|
||||
|
||||
|
||||
def ch_query(sql: str) -> List[Dict[str, Any]]:
|
||||
"""Run a CH query returning JSONEachRow; raise if the HTTP call fails."""
|
||||
qs = urllib.parse.urlencode({"query": sql + " FORMAT JSONEachRow"})
|
||||
req = urllib.request.Request(f"{CH_URL}?{qs}", method="GET")
|
||||
for k, v in CH_HEADERS.items():
|
||||
req.add_header(k, v)
|
||||
with urllib.request.urlopen(req, timeout=60) as resp:
|
||||
body = resp.read().decode("utf-8", errors="replace")
|
||||
rows: List[Dict[str, Any]] = []
|
||||
for line in body.splitlines():
|
||||
line = line.strip()
|
||||
if line:
|
||||
rows.append(json.loads(line))
|
||||
return rows
|
||||
|
||||
|
||||
def bin_leverage(lev: float, step: float) -> float:
|
||||
"""Bucket leverage to `step`-wide bins, anchored at 0. Returns bin lower edge."""
|
||||
if lev is None or not math.isfinite(lev) or lev <= 0:
|
||||
return 0.0
|
||||
return math.floor(lev / step) * step
|
||||
|
||||
|
||||
def load_trades(strategy: str, since: str, until: Optional[str]) -> List[Dict[str, Any]]:
|
||||
"""Pull trade_events rows. Uses leverage_raw if present (post-sprint rows),
|
||||
else falls back to leverage (pre-sprint rows on the same trade semantics).
|
||||
"""
|
||||
where = [f"strategy = '{strategy}'", f"date >= toDate('{since}')"]
|
||||
if until:
|
||||
where.append(f"date <= toDate('{until}')")
|
||||
where_sql = " AND ".join(where)
|
||||
|
||||
sql = (
|
||||
"SELECT "
|
||||
" toDate(date) AS d, "
|
||||
" asset, "
|
||||
" coalesce(leverage_raw, leverage) AS lev_raw, "
|
||||
" leverage AS lev_effective, "
|
||||
" pnl, pnl_pct, "
|
||||
" (pnl_pct > 0) AS is_win, "
|
||||
" exit_reason "
|
||||
f"FROM dolphin.trade_events WHERE {where_sql}"
|
||||
)
|
||||
return ch_query(sql)
|
||||
|
||||
|
||||
def aggregate(trades: List[Dict[str, Any]], step: float) -> List[Dict[str, Any]]:
|
||||
bins: Dict[float, Dict[str, Any]] = {}
|
||||
for t in trades:
|
||||
lev = float(t.get("lev_raw") or 0.0)
|
||||
b = bin_leverage(lev, step)
|
||||
s = bins.setdefault(b, {
|
||||
"bin_lo": b,
|
||||
"bin_hi": b + step,
|
||||
"n": 0,
|
||||
"wins": 0,
|
||||
"net_pnl": 0.0,
|
||||
"net_pnl_pct": 0.0,
|
||||
})
|
||||
s["n"] += 1
|
||||
if int(t.get("is_win", 0)):
|
||||
s["wins"] += 1
|
||||
s["net_pnl"] += float(t.get("pnl") or 0.0)
|
||||
s["net_pnl_pct"] += float(t.get("pnl_pct") or 0.0)
|
||||
|
||||
out = []
|
||||
for b, s in sorted(bins.items()):
|
||||
n = max(1, s["n"])
|
||||
out.append({
|
||||
"bin": f"{s['bin_lo']:.2f}–{s['bin_hi']:.2f}x",
|
||||
"n": s["n"],
|
||||
"wr_pct": round(100.0 * s["wins"] / n, 2),
|
||||
"avg_pnl_pct": round(s["net_pnl_pct"] / n * 100.0, 3),
|
||||
"net_pnl": round(s["net_pnl"], 2),
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def render_markdown(rows: List[Dict[str, Any]],
|
||||
strategy: str, since: str, until: Optional[str],
|
||||
step: float, total: int) -> str:
|
||||
hdr = (
|
||||
"# Leverage → Winrate / Net-PnL Analysis\n\n"
|
||||
f"- Strategy: `{strategy}`\n"
|
||||
f"- Window: `{since}` → `{until or 'now'}`\n"
|
||||
f"- Leverage bin step: `{step}x`\n"
|
||||
f"- Total trades in scope: `{total}`\n"
|
||||
f"- Generated: `{datetime.utcnow().isoformat(timespec='seconds')}Z`\n\n"
|
||||
"## Per-bin aggregates\n\n"
|
||||
"| Leverage bin | Trades | WR % | Avg PnL % | Net PnL |\n"
|
||||
"|---|---:|---:|---:|---:|\n"
|
||||
)
|
||||
body = "\n".join(
|
||||
f"| {r['bin']} | {r['n']} | {r['wr_pct']} | {r['avg_pnl_pct']} | {r['net_pnl']} |"
|
||||
for r in rows
|
||||
)
|
||||
decision = (
|
||||
"\n\n## Decision rubric (copy into the orchestrator comment block)\n\n"
|
||||
"- Higher-lev bins show clearly better net-PnL w/ tight sample error → **Option 2** (banker's round, min=1)\n"
|
||||
"- Flat / noisy signal across bins → **Option 1** (round-half-up, min=1)\n"
|
||||
"- Higher-lev bins show WORSE net-PnL / mixed → **stay at 1x** (current default)\n\n"
|
||||
"_Flipping the rule is a follow-up PR — this script is read-only._\n"
|
||||
)
|
||||
return hdr + body + decision
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--strategy", default="blue",
|
||||
help="trade_events.strategy value to filter on (default: blue — richest historical set)")
|
||||
ap.add_argument("--since", default="2026-01-01", help="inclusive start date (YYYY-MM-DD)")
|
||||
ap.add_argument("--until", default=None, help="inclusive end date (YYYY-MM-DD); default = now")
|
||||
ap.add_argument("--step", type=float, default=1.0, help="leverage bin width (e.g. 0.5 or 1.0)")
|
||||
ap.add_argument("--out", default="prod/docs/LEVERAGE_WINRATE_REPORT.md",
|
||||
help="output report path (markdown)")
|
||||
args = ap.parse_args()
|
||||
|
||||
trades = load_trades(args.strategy, args.since, args.until)
|
||||
if not trades:
|
||||
print(f"No trades matched (strategy={args.strategy}, since={args.since}).", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
rows = aggregate(trades, args.step)
|
||||
md = render_markdown(rows, args.strategy, args.since, args.until, args.step, total=len(trades))
|
||||
|
||||
out_path = Path(args.out)
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path.write_text(md)
|
||||
print(f"Wrote {out_path} ({len(rows)} bins, {len(trades)} trades)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user