book: scaffold + ch00 (execution trail as spine) — evidence exp 8-31, round 3
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
"""Risk-limit spec shared by backtest and live executor.
|
||||
|
||||
One JSON spec is consulted by BOTH ``rd_backtest`` (as a strategy filter
|
||||
overlay) and ``rd_strategy_targets`` (as pre-gate + sizing caps), so a limit
|
||||
that holds in backtest holds in live — the round's ``strategy_snapshot``
|
||||
stores the exact spec used.
|
||||
|
||||
Supported keys (all optional, all pct are 0-100):
|
||||
liquidity_floor_adv : min avg daily dollar volume (USD) per symbol.
|
||||
Names below it are filtered out of the tradable set.
|
||||
size_cap_pct : max notional per name as % of account equity.
|
||||
concentration_cap_pct: max total deployed as % of account equity.
|
||||
drawdown_pause_pct : if equity drawdown from peak exceeds this, new buys
|
||||
are paused (executor gate; not expressible in a
|
||||
one-shot qlib backtest and therefore documented).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import pandas as pd
|
||||
|
||||
|
||||
def parse_limits(spec: Optional[str]) -> Dict[str, float]:
|
||||
"""Parse a risk_limits JSON string into a flat float map (empty = no limits)."""
|
||||
if not spec or not str(spec).strip():
|
||||
return {}
|
||||
if isinstance(spec, dict):
|
||||
raw = spec
|
||||
else:
|
||||
raw = json.loads(str(spec))
|
||||
out: Dict[str, float] = {}
|
||||
for k in ("liquidity_floor_adv", "size_cap_pct", "concentration_cap_pct", "drawdown_pause_pct"):
|
||||
v = raw.get(k)
|
||||
if v is not None and str(v) != "":
|
||||
out[k] = float(v)
|
||||
return out
|
||||
|
||||
|
||||
def dollar_adv(
|
||||
symbols: List[str],
|
||||
lake_root: str = "",
|
||||
market: str = "US",
|
||||
asof: Optional[str] = None,
|
||||
lookback: int = 20,
|
||||
) -> Dict[str, float]:
|
||||
"""Average daily dollar volume per symbol over the ``lookback`` sessions
|
||||
ending at ``asof`` (inclusive), read straight from lake 1d bars. Symbols
|
||||
with no lake data map to 0.0 (treated as illiquid)."""
|
||||
from tac_qlib.data.config import LakeConfig, resolve_lake_root
|
||||
|
||||
cfg = LakeConfig(resolve_lake_root(lake_root or None), market)
|
||||
asof_ts = pd.Timestamp(asof) if asof else pd.Timestamp.utcnow()
|
||||
out: Dict[str, float] = {}
|
||||
for sym in sorted({str(s).upper() for s in symbols}):
|
||||
p = cfg.bar_path("1d", sym)
|
||||
if not p.exists():
|
||||
out[sym] = 0.0
|
||||
continue
|
||||
try:
|
||||
df = pd.read_parquet(p)
|
||||
except Exception:
|
||||
out[sym] = 0.0
|
||||
continue
|
||||
if not len(df):
|
||||
out[sym] = 0.0
|
||||
continue
|
||||
tcol = df["t"] if "t" in df.columns else df["date"]
|
||||
ts = pd.to_datetime(tcol)
|
||||
df = df.assign(_t=ts).sort_values("_t")
|
||||
df = df[df["_t"] <= asof_ts]
|
||||
if not len(df):
|
||||
out[sym] = 0.0
|
||||
continue
|
||||
df = df.tail(lookback)
|
||||
px = df["vw"] if "vw" in df.columns else df["c"]
|
||||
out[sym] = float((df["v"] * px).mean()) if len(df) else 0.0
|
||||
return out
|
||||
|
||||
|
||||
def apply_to_ranking(
|
||||
ranking: pd.Series,
|
||||
adv: Dict[str, float],
|
||||
limits: Dict[str, float],
|
||||
account: float,
|
||||
risk_degree: float,
|
||||
topk: int,
|
||||
) -> Tuple[pd.Series, Dict[str, Any]]:
|
||||
"""Executor-side overlay on the ranked signal (``pd.Series`` symbol -> score).
|
||||
|
||||
Returns (filtered_ranking, applied) where ``filtered_ranking`` has
|
||||
illiquid names removed and ``applied`` records what the limits did (audit
|
||||
trail). Per-name notional and total caps are reported but not folded into
|
||||
the ranking — the caller sizes targets and can read ``applied`` to cap.
|
||||
"""
|
||||
applied: Dict[str, Any] = {"notes": [], "dropped_liquidity": []}
|
||||
filtered = ranking
|
||||
floor = limits.get("liquidity_floor_adv")
|
||||
if floor:
|
||||
dropped = [s for s in filtered.index if adv.get(str(s).upper(), 0.0) < floor]
|
||||
if dropped:
|
||||
filtered = filtered.drop(index=[s for s in dropped if s in filtered.index])
|
||||
applied["dropped_liquidity"] = [str(s) for s in dropped]
|
||||
applied["notes"].append(f"liquidity floor ${floor:,.0f} ADV dropped {len(dropped)}")
|
||||
per_name = account * risk_degree / max(topk, 1)
|
||||
size_cap = limits.get("size_cap_pct")
|
||||
if size_cap:
|
||||
cap = account * size_cap / 100.0
|
||||
applied["size_cap_notional"] = round(cap, 2)
|
||||
if per_name > cap:
|
||||
applied["per_name_capped_from"] = round(per_name, 2)
|
||||
per_name = cap
|
||||
applied["notes"].append(f"size cap {size_cap:g}% cut per-name notional to ${cap:,.2f}")
|
||||
applied["per_name_notional"] = round(per_name, 2)
|
||||
n_buys = min(topk, max(len(filtered), 0))
|
||||
conc = limits.get("concentration_cap_pct")
|
||||
if conc:
|
||||
conc_cap = account * conc / 100.0
|
||||
applied["concentration_cap_notional"] = round(conc_cap, 2)
|
||||
total = per_name * max(n_buys, 1)
|
||||
if total > conc_cap:
|
||||
applied["total_capped_from"] = round(total, 2)
|
||||
applied["notes"].append(f"concentration cap {conc:g}% cut total to ${conc_cap:,.2f}")
|
||||
per_name = conc_cap / max(n_buys, 1)
|
||||
applied["per_name_capped_from"] = applied.get("per_name_capped_from") or round(total / max(n_buys, 1), 2)
|
||||
applied["per_name_notional"] = round(per_name, 2)
|
||||
applied["total_notional"] = round(min(total, conc_cap), 2)
|
||||
else:
|
||||
applied["total_notional"] = round(per_name * n_buys, 2)
|
||||
return filtered, applied
|
||||
|
||||
|
||||
def drawdown_pause(equity: float, peak_equity: float, limits: Dict[str, float]) -> Tuple[bool, Optional[str]]:
|
||||
"""Executor gate: True when drawdown from peak exceeds drawdown_pause_pct."""
|
||||
pct = limits.get("drawdown_pause_pct")
|
||||
if not pct or not peak_equity or not equity:
|
||||
return False, None
|
||||
dd = (peak_equity - equity) / peak_equity * 100.0
|
||||
if dd >= pct:
|
||||
return True, f"drawdown {dd:.1f}% >= pause {pct:g}% (peak ${peak_equity:,.2f}, equity ${equity:,.2f})"
|
||||
return False, None
|
||||
Reference in New Issue
Block a user