Add non-core allocator v2 and Form4 freshness presets

main
I Luk Kim 4 months ago
parent e38c314a09
commit 5cb2b9fcb8

File diff suppressed because it is too large Load Diff

@ -39,6 +39,7 @@ def run_backtest_session_sync(
form4_sleeve_preset: str | None = None, form4_sleeve_preset: str | None = None,
ownership_sleeve_preset: str | None = None, ownership_sleeve_preset: str | None = None,
risk_off_alpha_sleeve_preset: str | None = None, risk_off_alpha_sleeve_preset: str | None = None,
non_core_allocator_v2_mode: str | None = None,
snapshot_id_override: str | None = None, snapshot_id_override: str | None = None,
fixed_capital_sizing: bool = False, fixed_capital_sizing: bool = False,
) -> dict[str, Any]: ) -> dict[str, Any]:
@ -59,6 +60,15 @@ def run_backtest_session_sync(
manifest = load_manifest(config_path) manifest = load_manifest(config_path)
config = resolve_config(manifest, snapshot_id_override=snapshot_id_override) config = resolve_config(manifest, snapshot_id_override=snapshot_id_override)
# Auto-refresh snapshot if stale (covers web direct_runner path).
# Uses _last_market_closed_date() as the reference so the snapshot updates
# once market closes today (same criteria as price bar extension).
import asyncio as _asyncio
from apps.backtester.run import _last_market_closed_date
_snap_id = config.canonical_snapshot_id or config.dataset_snapshot_id
if _snapshot_needs_refresh(_snap_id, _last_market_closed_date()):
_asyncio.run(_refresh_snapshot(_snap_id, universe_profile=None))
# Apply parking preset override (CLI --parking option) # Apply parking preset override (CLI --parking option)
if parking_preset: if parking_preset:
config.risk.cash_parking_preset = parking_preset config.risk.cash_parking_preset = parking_preset
@ -75,6 +85,9 @@ def run_backtest_session_sync(
if risk_off_alpha_sleeve_preset: if risk_off_alpha_sleeve_preset:
config.risk_off_alpha_sleeve_preset = risk_off_alpha_sleeve_preset config.risk_off_alpha_sleeve_preset = risk_off_alpha_sleeve_preset
config.apply_risk_off_alpha_sleeve_preset() config.apply_risk_off_alpha_sleeve_preset()
if non_core_allocator_v2_mode:
config.non_core_allocator_v2.enabled = True
config.non_core_allocator_v2.mode = non_core_allocator_v2_mode
if fixed_capital_sizing: if fixed_capital_sizing:
config.risk.fixed_capital_sizing = True config.risk.fixed_capital_sizing = True
@ -276,12 +289,13 @@ def _snapshot_has_required_coverage(
snapshot_id: str, snapshot_id: str,
end_date: dt.date, end_date: dt.date,
snapshot_dir: str | None = None, snapshot_dir: str | None = None,
grace_days: int = 3, grace_days: int = 1,
) -> bool: ) -> bool:
"""Return True when an existing snapshot already covers the requested date. """Return True when an existing snapshot already covers the requested date.
grace_days: max allowed gap between snapshot's latest event and end_date. grace_days: max allowed gap between snapshot's latest event and end_date.
Default 3 (covers weekends/holidays). Previous default was 14. Default 1 (covers weekends/holidays with 1-day lag). Paired with the
.last_refresh marker this ensures at most one refresh per calendar day.
""" """
snapshot_path = _resolve_snapshot_path(snapshot_id, snapshot_dir=snapshot_dir) snapshot_path = _resolve_snapshot_path(snapshot_id, snapshot_dir=snapshot_dir)
if snapshot_path is None: if snapshot_path is None:
@ -431,6 +445,8 @@ def run_backtest(
idle_alpha_preset: str | None = None, idle_alpha_preset: str | None = None,
form4_sleeve_preset: str | None = None, form4_sleeve_preset: str | None = None,
ownership_sleeve_preset: str | None = None, ownership_sleeve_preset: str | None = None,
risk_off_alpha_sleeve_preset: str | None = None,
non_core_allocator_v2_mode: str | None = None,
snapshot_id_override: str | None = None, snapshot_id_override: str | None = None,
auto_refresh: bool = True, auto_refresh: bool = True,
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
@ -503,6 +519,8 @@ def run_backtest(
idle_alpha_preset=idle_alpha_preset, idle_alpha_preset=idle_alpha_preset,
form4_sleeve_preset=form4_sleeve_preset, form4_sleeve_preset=form4_sleeve_preset,
ownership_sleeve_preset=ownership_sleeve_preset, ownership_sleeve_preset=ownership_sleeve_preset,
risk_off_alpha_sleeve_preset=risk_off_alpha_sleeve_preset,
non_core_allocator_v2_mode=non_core_allocator_v2_mode,
snapshot_id_override=snapshot_id_override, snapshot_id_override=snapshot_id_override,
) )

@ -498,6 +498,8 @@ def cmd_backtest(args: argparse.Namespace) -> None:
except ValueError as exc: except ValueError as exc:
_console.print(f"[red]ERROR: Invalid date: {exc}[/]") _console.print(f"[red]ERROR: Invalid date: {exc}[/]")
sys.exit(1) sys.exit(1)
if getattr(args, "non_core_allocator_v2", False) and not getattr(args, "non_core_allocator_v2_mode", None):
args.non_core_allocator_v2_mode = "shadow"
oracle_url = os.environ.get("ORACLE_URL") or os.environ.get("STOCK_ORACLE_URL", "http://localhost:8000") oracle_url = os.environ.get("ORACLE_URL") or os.environ.get("STOCK_ORACLE_URL", "http://localhost:8000")
db_dsn = os.environ.get("DB_DSN") or os.environ.get("POSTGRES_DSN", "") db_dsn = os.environ.get("DB_DSN") or os.environ.get("POSTGRES_DSN", "")
@ -525,6 +527,8 @@ def cmd_backtest(args: argparse.Namespace) -> None:
idle_alpha_preset=getattr(args, 'idle_alpha', None), idle_alpha_preset=getattr(args, 'idle_alpha', None),
form4_sleeve_preset=getattr(args, 'form4_sleeve', None), form4_sleeve_preset=getattr(args, 'form4_sleeve', None),
ownership_sleeve_preset=getattr(args, 'ownership_sleeve', None), ownership_sleeve_preset=getattr(args, 'ownership_sleeve', None),
risk_off_alpha_sleeve_preset=getattr(args, 'risk_off_sleeve', None),
non_core_allocator_v2_mode=getattr(args, 'non_core_allocator_v2_mode', None),
snapshot_id_override=getattr(args, 'snapshot_id', None), snapshot_id_override=getattr(args, 'snapshot_id', None),
auto_refresh=not getattr(args, 'no_refresh', False), auto_refresh=not getattr(args, 'no_refresh', False),
) )
@ -684,6 +688,12 @@ def main() -> None:
help="Apply Form 4 sleeve preset (e.g. reserve_form4_cluster_patient). Overrides config.") help="Apply Form 4 sleeve preset (e.g. reserve_form4_cluster_patient). Overrides config.")
p.add_argument("--ownership-sleeve", default=None, metavar="PRESET", p.add_argument("--ownership-sleeve", default=None, metavar="PRESET",
help="Apply 13D/13G ownership sleeve preset (e.g. ownership_13d_raise_reserve_plus_strict). Overrides config.") help="Apply 13D/13G ownership sleeve preset (e.g. ownership_13d_raise_reserve_plus_strict). Overrides config.")
p.add_argument("--risk-off-sleeve", default=None, metavar="PRESET",
help="Apply risk-off alpha sleeve preset. Overrides config.")
p.add_argument("--non-core-allocator-v2", action="store_true", default=False,
help="Enable non-core allocator v2.")
p.add_argument("--non-core-allocator-v2-mode", default=None, choices=["shadow", "live"],
help="Run non-core allocator v2 in shadow or live mode.")
p.add_argument("--snapshot-id", default=None, metavar="SNAPSHOT_ID", p.add_argument("--snapshot-id", default=None, metavar="SNAPSHOT_ID",
help="Override dataset_snapshot_id (e.g. midlarge-liquid-long-v1-oot-2020-2021)") help="Override dataset_snapshot_id (e.g. midlarge-liquid-long-v1-oot-2020-2021)")
p.add_argument("--no-refresh", action="store_true", default=False, p.add_argument("--no-refresh", action="store_true", default=False,

@ -127,6 +127,8 @@ class PaperTradingEngine:
) )
# Lookback entry: fired once per daemon session on the first run_next_open call # Lookback entry: fired once per daemon session on the first run_next_open call
self._lookback_injected: bool = False self._lookback_injected: bool = False
# Overlay shock brake cooldown (in-memory, session-scoped)
self._parking_brake_cooldown_remaining: int = 0
def _get_candidate_capital_bucket_id(self, candidate: Candidate) -> str | None: def _get_candidate_capital_bucket_id(self, candidate: Candidate) -> str | None:
return candidate.engine_capital_bucket_id return candidate.engine_capital_bucket_id
@ -1181,17 +1183,185 @@ class PaperTradingEngine:
return actual_sym return actual_sym
def _parking_check_shock_brake(self, today: dt.date) -> bool:
"""Check QQQ/SMH acceleration signals for fast overlay exit.
Returns True if any brake signal fires.
"""
import math
risk = self._config.risk
rv_ratio = risk.cash_parking_overlay_shock_brake_rv_ratio
dd5_pct = risk.cash_parking_overlay_shock_brake_dd5_pct
sma_cross = risk.cash_parking_overlay_shock_brake_sma_cross
lookback = 55 # enough for vol_20 + buffer
start = today - dt.timedelta(days=lookback * 2)
qqq_bars_dict = self._broker.get_bars_as_dict(["QQQ"], start, today)
qqq_bars = qqq_bars_dict.get("QQQ", {})
if len(qqq_bars) < 22:
return False
sorted_dates = sorted(qqq_bars.keys())
qqq_closes = [qqq_bars[d]["close"] for d in sorted_dates]
n = len(qqq_closes)
def _vol(lb: int) -> float | None:
if n < lb + 1:
return None
log_rets = []
for i in range(n - lb, n):
if qqq_closes[i - 1] > 0:
log_rets.append(math.log(qqq_closes[i] / qqq_closes[i - 1]))
if not log_rets:
return None
mean_r = sum(log_rets) / len(log_rets)
var_r = sum((r - mean_r) ** 2 for r in log_rets) / len(log_rets)
return math.sqrt(var_r * 252)
# Signal 1: vol acceleration
vol5 = _vol(5)
vol20 = _vol(20)
if vol5 is not None and vol20 is not None and vol20 > 0:
if vol5 / vol20 > rv_ratio:
return True
# Signal 2: trend break + sector weakness
if sma_cross and n >= 11:
qqq_close_now = qqq_closes[-1]
qqq_sma10 = sum(qqq_closes[-10:]) / 10
if qqq_close_now < qqq_sma10:
# Check SMH mom_5
smh_bars_dict = self._broker.get_bars_as_dict(["SMH"], start, today)
smh_bars = smh_bars_dict.get("SMH", {})
if len(smh_bars) >= 7:
smh_sorted = sorted(smh_bars.keys())
smh_closes = [smh_bars[d]["close"] for d in smh_sorted]
if len(smh_closes) >= 6 and smh_closes[-6] > 0:
smh_mom5 = (smh_closes[-1] - smh_closes[-6]) / smh_closes[-6]
if smh_mom5 < 0:
return True
# Signal 3: sharp 5-day drawdown
if n >= 6:
qqq_high5 = max(qqq_closes[-5:])
qqq_close_now = qqq_closes[-1]
if qqq_high5 > 0 and (qqq_high5 - qqq_close_now) / qqq_high5 > dd5_pct:
return True
# Signal 4: near-SMA buffer — exit when QQQ within sma_buffer% ABOVE SMA10 (pre-emptive)
sma_buffer = getattr(risk, "cash_parking_overlay_shock_brake_sma_buffer", 0.0)
if sma_buffer > 0 and n >= 11:
qqq_close_now = qqq_closes[-1]
qqq_sma10 = sum(qqq_closes[-10:]) / 10
if qqq_sma10 > 0:
sma_gap = (qqq_close_now - qqq_sma10) / qqq_sma10
if 0 < sma_gap < sma_buffer:
return True
return False
def _parking_check_and_sell(self, session_id: str, today: dt.date) -> bool: def _parking_check_and_sell(self, session_id: str, today: dt.date) -> bool:
"""Check gate signal, apply confirmation, trailing stop. Returns True if sold.""" """Check gate signal, apply confirmation, trailing stop. Returns True if sold."""
import math
parking_st = self._state.get_parking_state(session_id) parking_st = self._state.get_parking_state(session_id)
if parking_st is None: if parking_st is None:
return False return False
current_sym = parking_st["symbol"].lower() current_sym = parking_st["symbol"].lower()
risk = self._config.risk
target = self._parking_evaluate_gate(today) target = self._parking_evaluate_gate(today)
# --- Decrement overlay brake cooldown ---
brake_cooldown = int(parking_st.get("overlay_brake_cooldown", 0))
if brake_cooldown > 0:
brake_cooldown -= 1
self._state.update_parking_gate_state(session_id, overlay_brake_cooldown=brake_cooldown)
# --- Shock brake check ---
overlay_sym = (risk.cash_parking_low_vol_overlay_symbol or "").lower()
if (
overlay_sym
and risk.cash_parking_overlay_shock_brake_enabled
and current_sym == overlay_sym
):
brake_fired = self._parking_check_shock_brake(today)
if brake_fired:
logger.info("parking_shock_brake", symbol=current_sym, date=str(today))
# Execute sell immediately (bypass confirmation)
sym = parking_st["symbol"]
qty = parking_st["qty"]
try:
if qty > 0:
self._broker.close_position(sym, qty=qty)
import time
time.sleep(1)
self._state.close_parking_state(session_id)
bars_now = self._broker.get_latest_bars([sym])
exit_price = bars_now[sym].close if sym in bars_now else parking_st["avg_price"]
self._state.record_trade(
session_id=session_id,
symbol=sym,
engine_id=None,
capital_bucket_id=None,
entry_date=parking_st["entry_date"],
exit_date=today.isoformat(),
entry_price=parking_st["avg_price"],
exit_price=exit_price,
exit_reason="PARKING_SHOCK_BRAKE",
shares=qty,
net_pnl=(exit_price - parking_st["avg_price"]) * qty,
r_multiple=0.0,
holding_days=(today - dt.date.fromisoformat(parking_st["entry_date"])).days,
)
except Exception as e:
logger.warning("parking_shock_brake_sell_failed", error=str(e))
# Set cooldown for next active parking state (will be created on re-buy)
# We store cooldown on a session-level attribute for now
self._parking_brake_cooldown_remaining = risk.cash_parking_overlay_shock_brake_cooldown_days
return True
# --- Dwell cap check ---
overlay_hold_days = int(parking_st.get("overlay_hold_days", 0))
if overlay_sym and current_sym == overlay_sym:
overlay_hold_days += 1
self._state.update_parking_gate_state(session_id, overlay_hold_days=overlay_hold_days)
max_hold = risk.cash_parking_overlay_max_hold_days
if max_hold > 0 and overlay_hold_days >= max_hold:
logger.info("parking_dwell_cap", symbol=current_sym, hold_days=overlay_hold_days)
sym = parking_st["symbol"]
qty = parking_st["qty"]
try:
if qty > 0:
self._broker.close_position(sym, qty=qty)
import time
time.sleep(1)
self._state.close_parking_state(session_id)
bars_now = self._broker.get_latest_bars([sym])
exit_price = bars_now[sym].close if sym in bars_now else parking_st["avg_price"]
self._state.record_trade(
session_id=session_id,
symbol=sym,
engine_id=None,
capital_bucket_id=None,
entry_date=parking_st["entry_date"],
exit_date=today.isoformat(),
entry_price=parking_st["avg_price"],
exit_price=exit_price,
exit_reason="PARKING_DWELL_CAP",
shares=qty,
net_pnl=(exit_price - parking_st["avg_price"]) * qty,
r_multiple=0.0,
holding_days=(today - dt.date.fromisoformat(parking_st["entry_date"])).days,
)
except Exception as e:
logger.warning("parking_dwell_cap_sell_failed", error=str(e))
return True
# --- Trailing stop check --- # --- Trailing stop check ---
stop_pct = self._config.risk.cash_parking_stop_pct stop_pct = risk.cash_parking_stop_pct
if stop_pct > 0 and current_sym != "sgov": if stop_pct > 0 and current_sym != "sgov":
peak = parking_st.get("peak_price", 0) or parking_st["avg_price"] peak = parking_st.get("peak_price", 0) or parking_st["avg_price"]
bars = self._broker.get_latest_bars([parking_st["symbol"]]) bars = self._broker.get_latest_bars([parking_st["symbol"]])
@ -1412,7 +1582,19 @@ class PaperTradingEngine:
# --- New parking position --- # --- New parking position ---
target = self._parking_evaluate_gate(today) target = self._parking_evaluate_gate(today)
# SGOV/QQQ/SPY/QQQM: buy through broker # Overlay brake cooldown: suppress TQQQ overlay re-entry during cooldown
overlay_sym_cfg = (risk.cash_parking_low_vol_overlay_symbol or "").lower()
if (
overlay_sym_cfg
and target.lower() == overlay_sym_cfg
and self._parking_brake_cooldown_remaining > 0
):
self._parking_brake_cooldown_remaining -= 1
# Fall back to base park symbol
target = risk.cash_parking_symbol
logger.info("parking_overlay_cooldown_active", remaining=self._parking_brake_cooldown_remaining)
# SGOV/QQQ/SPY/QQQM/TQQQ: buy through broker
sym = target.upper() sym = target.upper()
session_equity, session_cash = self._session_cash(session_id) session_equity, session_cash = self._session_cash(session_id)
reserve = session_equity * risk.cash_parking_reserve_pct reserve = session_equity * risk.cash_parking_reserve_pct

@ -114,7 +114,9 @@ CREATE TABLE IF NOT EXISTS parking_state (
pending_target TEXT NOT NULL DEFAULT '', pending_target TEXT NOT NULL DEFAULT '',
pending_days INTEGER NOT NULL DEFAULT 0, pending_days INTEGER NOT NULL DEFAULT 0,
sgov_entry_value REAL NOT NULL DEFAULT 0, sgov_entry_value REAL NOT NULL DEFAULT 0,
sold_today INTEGER NOT NULL DEFAULT 0 sold_today INTEGER NOT NULL DEFAULT 0,
overlay_brake_cooldown INTEGER NOT NULL DEFAULT 0,
overlay_hold_days INTEGER NOT NULL DEFAULT 0
); );
""" """
@ -159,6 +161,8 @@ def create_schema(db_path: str | Path) -> None:
("pending_days", "INTEGER", "0"), ("pending_days", "INTEGER", "0"),
("sgov_entry_value", "REAL", "0"), ("sgov_entry_value", "REAL", "0"),
("sold_today", "INTEGER", "0"), ("sold_today", "INTEGER", "0"),
("overlay_brake_cooldown", "INTEGER", "0"),
("overlay_hold_days", "INTEGER", "0"),
]: ]:
try: try:
conn.execute(f"ALTER TABLE parking_state ADD COLUMN {col} {dtype} NOT NULL DEFAULT {default}") conn.execute(f"ALTER TABLE parking_state ADD COLUMN {col} {dtype} NOT NULL DEFAULT {default}")

@ -492,8 +492,9 @@ class StateManager:
self, session_id: str, **kwargs: Any, self, session_id: str, **kwargs: Any,
) -> None: ) -> None:
"""Update gate hysteresis fields. Accepts: gate_in_sgov, committed_target, """Update gate hysteresis fields. Accepts: gate_in_sgov, committed_target,
pending_target, pending_days, sold_today.""" pending_target, pending_days, sold_today, overlay_brake_cooldown, overlay_hold_days."""
allowed = {"gate_in_sgov", "committed_target", "pending_target", "pending_days", "sold_today"} allowed = {"gate_in_sgov", "committed_target", "pending_target", "pending_days", "sold_today",
"overlay_brake_cooldown", "overlay_hold_days"}
updates, params = [], [] updates, params = [], []
for k, v in kwargs.items(): for k, v in kwargs.items():
if k in allowed: if k in allowed:

@ -7,7 +7,7 @@ Usage:
--manifests configs/experiments/return_max_long_v7.70.json \\ --manifests configs/experiments/return_max_long_v7.70.json \\
configs/experiments/return_max_long_v18.250.json \\ configs/experiments/return_max_long_v18.250.json \\
--parking qqqm_low_dd \\ --parking qqqm_low_dd \\
--idle-alpha micro_event_alpha_plus \\ --idle-alpha micro_event_alpha_plus_event_plus \\
--output-root /tmp/composite_eval --output-root /tmp/composite_eval
Compares composite CW, Sharpe, DD, idle fraction, and amplification factor. Compares composite CW, Sharpe, DD, idle fraction, and amplification factor.
@ -39,8 +39,8 @@ def parse_args() -> argparse.Namespace:
help="Cash parking preset name (default: qqqm_low_dd).", help="Cash parking preset name (default: qqqm_low_dd).",
) )
parser.add_argument( parser.add_argument(
"--idle-alpha", default="micro_event_alpha_plus", "--idle-alpha", default="micro_event_alpha_plus_event_plus",
help="Idle alpha sleeve preset name (default: micro_event_alpha_plus).", help="Idle alpha sleeve preset name (default: micro_event_alpha_plus_event_plus).",
) )
parser.add_argument( parser.add_argument(
"--no-presets", action="store_true", "--no-presets", action="store_true",

@ -10,19 +10,19 @@ Usage:
python -m apps.tools.run_engine_ablation \\ python -m apps.tools.run_engine_ablation \\
--manifest configs/experiments/return_max_long_v7.70.json \\ --manifest configs/experiments/return_max_long_v7.70.json \\
--parking qqqm_low_dd \\ --parking qqqm_low_dd \\
--idle-alpha micro_event_alpha_plus \\ --idle-alpha micro_event_alpha_plus_event_plus \\
--output-root /tmp/ablation_v7.70 --output-root /tmp/ablation_v7.70
# Greedy pruning (iteratively removes worst composite-contributor) # Greedy pruning (iteratively removes worst composite-contributor)
python -m apps.tools.run_engine_ablation \\ python -m apps.tools.run_engine_ablation \\
--manifest configs/experiments/return_max_long_v7.70.json \\ --manifest configs/experiments/return_max_long_v7.70.json \\
--parking qqqm_low_dd --idle-alpha micro_event_alpha_plus \\ --parking qqqm_low_dd --idle-alpha micro_event_alpha_plus_event_plus \\
--greedy-prune --output-root /tmp/ablation_v7.70 --greedy-prune --output-root /tmp/ablation_v7.70
# Risk budget sweep (test 0.5x/1.0x/1.5x for each engine's risk budget) # Risk budget sweep (test 0.5x/1.0x/1.5x for each engine's risk budget)
python -m apps.tools.run_engine_ablation \\ python -m apps.tools.run_engine_ablation \\
--manifest configs/experiments/return_max_long_v7.70.json \\ --manifest configs/experiments/return_max_long_v7.70.json \\
--parking qqqm_low_dd --idle-alpha micro_event_alpha_plus \\ --parking qqqm_low_dd --idle-alpha micro_event_alpha_plus_event_plus \\
--budget-sweep --output-root /tmp/ablation_v7.70 --budget-sweep --output-root /tmp/ablation_v7.70
""" """
from __future__ import annotations from __future__ import annotations
@ -46,7 +46,7 @@ def parse_args() -> argparse.Namespace:
) )
parser.add_argument("--manifest", required=True, help="Base experiment manifest path.") parser.add_argument("--manifest", required=True, help="Base experiment manifest path.")
parser.add_argument("--parking", default="qqqm_low_dd", help="Cash parking preset.") parser.add_argument("--parking", default="qqqm_low_dd", help="Cash parking preset.")
parser.add_argument("--idle-alpha", default="micro_event_alpha_plus", help="IA sleeve preset.") parser.add_argument("--idle-alpha", default="micro_event_alpha_plus_event_plus", help="IA sleeve preset.")
parser.add_argument("--snapshot-dir", default=DEFAULT_SNAPSHOT_DIR) parser.add_argument("--snapshot-dir", default=DEFAULT_SNAPSHOT_DIR)
parser.add_argument("--output-root", required=True, help="Root directory for run artifacts.") parser.add_argument("--output-root", required=True, help="Root directory for run artifacts.")
parser.add_argument("--split", default="all", help="Backtest split (default: 'all' = full period merging train+valid+test).") parser.add_argument("--split", default="all", help="Backtest split (default: 'all' = full period merging train+valid+test).")

@ -1,7 +1,7 @@
"""Direct backtest subprocess runner. """Direct backtest subprocess runner.
Invoked by the web server as a subprocess: Invoked by the web server as a subprocess:
python -m apps.web.direct_runner TASK_ID CONFIG_PATH CAPITAL START_DATE END_DATE RESULT_FILE [--parking PRESET] [--idle-alpha PRESET] [--form4-sleeve PRESET] [--ownership-sleeve PRESET] [--risk-off-sleeve PRESET] python -m apps.web.direct_runner TASK_ID CONFIG_PATH CAPITAL START_DATE END_DATE RESULT_FILE [--parking PRESET] [--idle-alpha PRESET] [--form4-sleeve PRESET] [--ownership-sleeve PRESET] [--risk-off-sleeve PRESET] [--non-core-allocator-v2-mode MODE]
Runs run_backtest_session_sync() and saves the result JSON to RESULT_FILE. Runs run_backtest_session_sync() and saves the result JSON to RESULT_FILE.
Exits 0 on success, non-zero on failure. Exits 0 on success, non-zero on failure.
@ -23,7 +23,7 @@ def _json_default(obj):
def main(): def main():
if len(sys.argv) < 7: if len(sys.argv) < 7:
print( print(
"Usage: direct_runner TASK_ID CONFIG_PATH CAPITAL START_DATE END_DATE RESULT_FILE [--parking PRESET] [--idle-alpha PRESET] [--form4-sleeve PRESET] [--ownership-sleeve PRESET] [--risk-off-sleeve PRESET]", "Usage: direct_runner TASK_ID CONFIG_PATH CAPITAL START_DATE END_DATE RESULT_FILE [--parking PRESET] [--idle-alpha PRESET] [--form4-sleeve PRESET] [--ownership-sleeve PRESET] [--risk-off-sleeve PRESET] [--non-core-allocator-v2-mode MODE]",
file=sys.stderr, file=sys.stderr,
) )
sys.exit(2) sys.exit(2)
@ -41,6 +41,7 @@ def main():
form4_sleeve_preset = None form4_sleeve_preset = None
ownership_sleeve_preset = None ownership_sleeve_preset = None
risk_off_alpha_sleeve_preset = None risk_off_alpha_sleeve_preset = None
non_core_allocator_v2_mode = None
snapshot_id_override = None snapshot_id_override = None
fixed_capital = False fixed_capital = False
remaining = sys.argv[7:] remaining = sys.argv[7:]
@ -61,6 +62,9 @@ def main():
elif remaining[i] == "--risk-off-sleeve" and i + 1 < len(remaining): elif remaining[i] == "--risk-off-sleeve" and i + 1 < len(remaining):
risk_off_alpha_sleeve_preset = remaining[i + 1] risk_off_alpha_sleeve_preset = remaining[i + 1]
i += 2 i += 2
elif remaining[i] == "--non-core-allocator-v2-mode" and i + 1 < len(remaining):
non_core_allocator_v2_mode = remaining[i + 1]
i += 2
elif remaining[i] == "--snapshot-id" and i + 1 < len(remaining): elif remaining[i] == "--snapshot-id" and i + 1 < len(remaining):
snapshot_id_override = remaining[i + 1] snapshot_id_override = remaining[i + 1]
i += 2 i += 2
@ -76,6 +80,7 @@ def main():
(f" · form4={form4_sleeve_preset}" if form4_sleeve_preset else "") + (f" · form4={form4_sleeve_preset}" if form4_sleeve_preset else "") +
(f" · ownership={ownership_sleeve_preset}" if ownership_sleeve_preset else "") + (f" · ownership={ownership_sleeve_preset}" if ownership_sleeve_preset else "") +
(f" · risk_off={risk_off_alpha_sleeve_preset}" if risk_off_alpha_sleeve_preset else "") + (f" · risk_off={risk_off_alpha_sleeve_preset}" if risk_off_alpha_sleeve_preset else "") +
(f" · ncav2={non_core_allocator_v2_mode}" if non_core_allocator_v2_mode else "") +
(f" · snapshot={snapshot_id_override}" if snapshot_id_override else "") + (f" · snapshot={snapshot_id_override}" if snapshot_id_override else "") +
(" · fixed_capital" if fixed_capital else "")) (" · fixed_capital" if fixed_capital else ""))
sys.stdout.flush() sys.stdout.flush()
@ -93,6 +98,7 @@ def main():
form4_sleeve_preset=form4_sleeve_preset, form4_sleeve_preset=form4_sleeve_preset,
ownership_sleeve_preset=ownership_sleeve_preset, ownership_sleeve_preset=ownership_sleeve_preset,
risk_off_alpha_sleeve_preset=risk_off_alpha_sleeve_preset, risk_off_alpha_sleeve_preset=risk_off_alpha_sleeve_preset,
non_core_allocator_v2_mode=non_core_allocator_v2_mode,
snapshot_id_override=snapshot_id_override, snapshot_id_override=snapshot_id_override,
fixed_capital_sizing=fixed_capital, fixed_capital_sizing=fixed_capital,
) )

@ -354,6 +354,7 @@ class BacktestRequest(BaseModel):
form4_sleeve: str | None = None # Form 4 residual-cash sleeve preset name form4_sleeve: str | None = None # Form 4 residual-cash sleeve preset name
ownership_sleeve: str | None = None # 13D/13G ownership sleeve preset name ownership_sleeve: str | None = None # 13D/13G ownership sleeve preset name
risk_off_sleeve: str | None = None # risk-off alpha sleeve preset name risk_off_sleeve: str | None = None # risk-off alpha sleeve preset name
non_core_allocator_v2_mode: str | None = None # shadow|live
snapshot_id: str | None = None # override dataset_snapshot_id (e.g. for OOT periods) snapshot_id: str | None = None # override dataset_snapshot_id (e.g. for OOT periods)
fixed_capital: bool = False # non-compounding: size positions using initial capital fixed_capital: bool = False # non-compounding: size positions using initial capital
@ -370,6 +371,7 @@ class BatchBacktestRequest(BaseModel):
form4_sleeve: str | None = None form4_sleeve: str | None = None
ownership_sleeve: str | None = None ownership_sleeve: str | None = None
risk_off_sleeve: str | None = None risk_off_sleeve: str | None = None
non_core_allocator_v2_mode: str | None = None
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -389,6 +391,7 @@ def _make_task(
form4_sleeve: str | None = None, form4_sleeve: str | None = None,
ownership_sleeve: str | None = None, ownership_sleeve: str | None = None,
risk_off_sleeve: str | None = None, risk_off_sleeve: str | None = None,
non_core_allocator_v2_mode: str | None = None,
snapshot_id: str | None = None, snapshot_id: str | None = None,
fixed_capital: bool = False, fixed_capital: bool = False,
) -> dict[str, Any]: ) -> dict[str, Any]:
@ -415,6 +418,7 @@ def _make_task(
"form4_sleeve": form4_sleeve, "form4_sleeve": form4_sleeve,
"ownership_sleeve": ownership_sleeve, "ownership_sleeve": ownership_sleeve,
"risk_off_sleeve": risk_off_sleeve, "risk_off_sleeve": risk_off_sleeve,
"non_core_allocator_v2_mode": non_core_allocator_v2_mode,
"snapshot_id": snapshot_id, "snapshot_id": snapshot_id,
} }
@ -535,6 +539,7 @@ def _build_cmd(
form4_sleeve: str | None = None, form4_sleeve: str | None = None,
ownership_sleeve: str | None = None, ownership_sleeve: str | None = None,
risk_off_sleeve: str | None = None, risk_off_sleeve: str | None = None,
non_core_allocator_v2_mode: str | None = None,
snapshot_id: str | None = None, snapshot_id: str | None = None,
) -> list[str]: ) -> list[str]:
"""Build the paper backtest CLI command.""" """Build the paper backtest CLI command."""
@ -563,6 +568,8 @@ def _build_cmd(
cmd += ["--ownership-sleeve", ownership_sleeve] cmd += ["--ownership-sleeve", ownership_sleeve]
if risk_off_sleeve: if risk_off_sleeve:
cmd += ["--risk-off-sleeve", risk_off_sleeve] cmd += ["--risk-off-sleeve", risk_off_sleeve]
if non_core_allocator_v2_mode:
cmd += ["--non-core-allocator-v2", "--non-core-allocator-v2-mode", non_core_allocator_v2_mode]
if snapshot_id: if snapshot_id:
cmd += ["--snapshot-id", snapshot_id] cmd += ["--snapshot-id", snapshot_id]
return cmd return cmd
@ -601,6 +608,7 @@ def _launch_backtest(req: BacktestRequest) -> dict[str, Any]:
eff_form4 = req.form4_sleeve or baked.get("form4_sleeve") eff_form4 = req.form4_sleeve or baked.get("form4_sleeve")
eff_ownership = req.ownership_sleeve or baked.get("ownership_sleeve") eff_ownership = req.ownership_sleeve or baked.get("ownership_sleeve")
eff_risk_off = req.risk_off_sleeve or baked.get("risk_off_sleeve") eff_risk_off = req.risk_off_sleeve or baked.get("risk_off_sleeve")
eff_non_core_allocator_v2_mode = req.non_core_allocator_v2_mode
if eff_parking and eff_parking not in PARKING_PRESETS: if eff_parking and eff_parking not in PARKING_PRESETS:
raise HTTPException(status_code=400, detail=f"Unknown parking preset: {eff_parking}") raise HTTPException(status_code=400, detail=f"Unknown parking preset: {eff_parking}")
@ -612,6 +620,8 @@ def _launch_backtest(req: BacktestRequest) -> dict[str, Any]:
raise HTTPException(status_code=400, detail=f"Unknown ownership sleeve preset: {eff_ownership}") raise HTTPException(status_code=400, detail=f"Unknown ownership sleeve preset: {eff_ownership}")
if eff_risk_off and eff_risk_off not in RISK_OFF_ALPHA_SLEEVE_PRESETS: if eff_risk_off and eff_risk_off not in RISK_OFF_ALPHA_SLEEVE_PRESETS:
raise HTTPException(status_code=400, detail=f"Unknown risk-off sleeve preset: {eff_risk_off}") raise HTTPException(status_code=400, detail=f"Unknown risk-off sleeve preset: {eff_risk_off}")
if eff_non_core_allocator_v2_mode and eff_non_core_allocator_v2_mode not in {"shadow", "live"}:
raise HTTPException(status_code=400, detail=f"Unknown non-core allocator v2 mode: {eff_non_core_allocator_v2_mode}")
task = _make_task( task = _make_task(
req.experiment_name, req.experiment_name,
@ -625,6 +635,7 @@ def _launch_backtest(req: BacktestRequest) -> dict[str, Any]:
form4_sleeve=eff_form4, form4_sleeve=eff_form4,
ownership_sleeve=eff_ownership, ownership_sleeve=eff_ownership,
risk_off_sleeve=eff_risk_off, risk_off_sleeve=eff_risk_off,
non_core_allocator_v2_mode=eff_non_core_allocator_v2_mode,
snapshot_id=req.snapshot_id, snapshot_id=req.snapshot_id,
) )
task_id = task["task_id"] task_id = task["task_id"]
@ -648,6 +659,7 @@ def _launch_backtest(req: BacktestRequest) -> dict[str, Any]:
"form4_sleeve": eff_form4, "form4_sleeve": eff_form4,
"ownership_sleeve": eff_ownership, "ownership_sleeve": eff_ownership,
"risk_off_sleeve": eff_risk_off, "risk_off_sleeve": eff_risk_off,
"non_core_allocator_v2_mode": eff_non_core_allocator_v2_mode,
"created_at": task["created_at"], "created_at": task["created_at"],
}, indent=2)) }, indent=2))
@ -664,6 +676,7 @@ def _launch_backtest(req: BacktestRequest) -> dict[str, Any]:
eff_form4, eff_form4,
eff_ownership, eff_ownership,
eff_risk_off, eff_risk_off,
eff_non_core_allocator_v2_mode,
req.snapshot_id, req.snapshot_id,
) )
@ -701,6 +714,7 @@ def _launch_backtest_multi(
form4_sleeve: str | None = None, form4_sleeve: str | None = None,
ownership_sleeve: str | None = None, ownership_sleeve: str | None = None,
risk_off_sleeve: str | None = None, risk_off_sleeve: str | None = None,
non_core_allocator_v2_mode: str | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Launch ONE subprocess with multiple --config flags for batch experiments.""" """Launch ONE subprocess with multiple --config flags for batch experiments."""
project_root = get_project_root() project_root = get_project_root()
@ -723,6 +737,8 @@ def _launch_backtest_multi(
raise HTTPException(status_code=400, detail=f"Unknown ownership sleeve preset: {ownership_sleeve}") raise HTTPException(status_code=400, detail=f"Unknown ownership sleeve preset: {ownership_sleeve}")
if risk_off_sleeve and risk_off_sleeve not in RISK_OFF_ALPHA_SLEEVE_PRESETS: if risk_off_sleeve and risk_off_sleeve not in RISK_OFF_ALPHA_SLEEVE_PRESETS:
raise HTTPException(status_code=400, detail=f"Unknown risk-off sleeve preset: {risk_off_sleeve}") raise HTTPException(status_code=400, detail=f"Unknown risk-off sleeve preset: {risk_off_sleeve}")
if non_core_allocator_v2_mode and non_core_allocator_v2_mode not in {"shadow", "live"}:
raise HTTPException(status_code=400, detail=f"Unknown non-core allocator v2 mode: {non_core_allocator_v2_mode}")
display_name = ", ".join(names) display_name = ", ".join(names)
task = _make_task( task = _make_task(
@ -737,6 +753,7 @@ def _launch_backtest_multi(
form4_sleeve=form4_sleeve, form4_sleeve=form4_sleeve,
ownership_sleeve=ownership_sleeve, ownership_sleeve=ownership_sleeve,
risk_off_sleeve=risk_off_sleeve, risk_off_sleeve=risk_off_sleeve,
non_core_allocator_v2_mode=non_core_allocator_v2_mode,
) )
task_id = task["task_id"] task_id = task["task_id"]
@ -760,6 +777,7 @@ def _launch_backtest_multi(
"form4_sleeve": form4_sleeve, "form4_sleeve": form4_sleeve,
"ownership_sleeve": ownership_sleeve, "ownership_sleeve": ownership_sleeve,
"risk_off_sleeve": risk_off_sleeve, "risk_off_sleeve": risk_off_sleeve,
"non_core_allocator_v2_mode": non_core_allocator_v2_mode,
"created_at": created_at, "created_at": created_at,
}, indent=2)) }, indent=2))
@ -776,6 +794,7 @@ def _launch_backtest_multi(
form4_sleeve, form4_sleeve,
ownership_sleeve, ownership_sleeve,
risk_off_sleeve, risk_off_sleeve,
non_core_allocator_v2_mode,
) )
with open(log_file, "wb") as log_fp: with open(log_file, "wb") as log_fp:
@ -868,6 +887,7 @@ def _launch_direct_backtest(req: BacktestRequest) -> dict[str, Any]:
eff_form4 = req.form4_sleeve or baked.get("form4_sleeve") eff_form4 = req.form4_sleeve or baked.get("form4_sleeve")
eff_ownership = req.ownership_sleeve or baked.get("ownership_sleeve") eff_ownership = req.ownership_sleeve or baked.get("ownership_sleeve")
eff_risk_off = req.risk_off_sleeve or baked.get("risk_off_sleeve") eff_risk_off = req.risk_off_sleeve or baked.get("risk_off_sleeve")
eff_non_core_allocator_v2_mode = req.non_core_allocator_v2_mode
task = _make_task( task = _make_task(
req.experiment_name, req.capital, req.start, req.end, req.year, req.no_trades, req.experiment_name, req.capital, req.start, req.end, req.year, req.no_trades,
@ -877,6 +897,7 @@ def _launch_direct_backtest(req: BacktestRequest) -> dict[str, Any]:
form4_sleeve=eff_form4, form4_sleeve=eff_form4,
ownership_sleeve=eff_ownership, ownership_sleeve=eff_ownership,
risk_off_sleeve=eff_risk_off, risk_off_sleeve=eff_risk_off,
non_core_allocator_v2_mode=eff_non_core_allocator_v2_mode,
snapshot_id=req.snapshot_id, snapshot_id=req.snapshot_id,
fixed_capital=req.fixed_capital, fixed_capital=req.fixed_capital,
) )
@ -905,6 +926,8 @@ def _launch_direct_backtest(req: BacktestRequest) -> dict[str, Any]:
cmd += ["--ownership-sleeve", eff_ownership] cmd += ["--ownership-sleeve", eff_ownership]
if eff_risk_off: if eff_risk_off:
cmd += ["--risk-off-sleeve", eff_risk_off] cmd += ["--risk-off-sleeve", eff_risk_off]
if eff_non_core_allocator_v2_mode:
cmd += ["--non-core-allocator-v2-mode", eff_non_core_allocator_v2_mode]
if req.snapshot_id: if req.snapshot_id:
cmd += ["--snapshot-id", req.snapshot_id] cmd += ["--snapshot-id", req.snapshot_id]
if req.fixed_capital: if req.fixed_capital:
@ -965,6 +988,7 @@ def submit_batch(req: BatchBacktestRequest) -> dict[str, Any]:
form4_sleeve=req.form4_sleeve, form4_sleeve=req.form4_sleeve,
ownership_sleeve=req.ownership_sleeve, ownership_sleeve=req.ownership_sleeve,
risk_off_sleeve=req.risk_off_sleeve, risk_off_sleeve=req.risk_off_sleeve,
non_core_allocator_v2_mode=req.non_core_allocator_v2_mode,
) )
task = _launch_backtest(single) task = _launch_backtest(single)
return {"tasks": [task]} return {"tasks": [task]}
@ -981,6 +1005,7 @@ def submit_batch(req: BatchBacktestRequest) -> dict[str, Any]:
form4_sleeve=req.form4_sleeve, form4_sleeve=req.form4_sleeve,
ownership_sleeve=req.ownership_sleeve, ownership_sleeve=req.ownership_sleeve,
risk_off_sleeve=req.risk_off_sleeve, risk_off_sleeve=req.risk_off_sleeve,
non_core_allocator_v2_mode=req.non_core_allocator_v2_mode,
) )
return {"tasks": [task]} return {"tasks": [task]}
@ -1200,37 +1225,42 @@ def list_snapshots() -> dict[str, Any]:
@router.get("/parking-presets") @router.get("/parking-presets")
def get_parking_presets() -> dict[str, Any]: def get_parking_presets() -> dict[str, Any]:
"""Return all named parking presets grouped by category.""" """Return curated parking presets for public UI selection."""
_GROUP_PREFIX = { curated_groups: dict[str, list[str]] = {
"vol_": "Volatility Only", "Simple / Baseline": [
"vm_": "Vol + Momentum", "sgov",
"ve_": "Entropy", "qqq_no_gate",
"vme_": "Vol + Momentum + Entropy", "qqqm_low_dd",
"vv_": "VRP", "qqqm_low_dd_risk25",
"vmv_": "Vol + Momentum + VRP", ],
"vt_": "Temperature", "GLD Defensive": [
"vh_": "Hurst", "qqqm_low_dd_gld",
"vmh_": "Vol + Momentum + Hurst", ],
"vmeh_": "Multi-Signal", "TQQQ Overlay": [
"composite_": "Composite", "qqqm_low_dd_tqqq_calm",
"dd": "Drawdown", "qqqm_low_dd_tqqq_calm_v2",
"vd_": "Vol + Drawdown", "qqqm_low_dd_tqqq_calm_v2_gld",
],
"Aggressive Frontier": [
"qqqm_low_dd_tqqq_active",
"qqqm_low_dd_tqqq_active_v2",
"qqqm_low_dd_tqqq_active_v2_gld",
],
"TQQQ Shock Brake": [
"qqqm_low_dd_tqqq_active_v2_gld_brake",
"qqqm_low_dd_tqqq_active_v2_gld_brake_blend",
"qqqm_low_dd_tqqq_active_v2_gld_brake_dwell",
"qqqm_low_dd_tqqq_active_v2_gld_brake_v2",
],
} }
_STANDALONE = {"sgov": "Simple / Baseline", "qqq_no_gate": "Simple / Baseline"}
groups: dict[str, list[str]] = {} groups = {
for name in PARKING_PRESETS: group: [name for name in names if name in PARKING_PRESETS]
if name in _STANDALONE: for group, names in curated_groups.items()
group = _STANDALONE[name] }
else: groups = {group: names for group, names in groups.items() if names}
group = "Other" presets = [name for names in groups.values() for name in names]
for prefix, label in _GROUP_PREFIX.items(): return {"presets": presets, "groups": groups}
if name.startswith(prefix):
group = label
break
groups.setdefault(group, []).append(name)
return {"presets": list(PARKING_PRESETS.keys()), "groups": groups}
@router.get("/idle-alpha-presets") @router.get("/idle-alpha-presets")

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -5,8 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Fithia2</title> <title>Fithia2</title>
<script type="module" crossorigin src="/assets/index-CEy65bTm.js"></script> <script type="module" crossorigin src="/assets/index-aO-xepTy.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BryHz-pc.css"> <link rel="stylesheet" crossorigin href="/assets/index-CbyS0Od7.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

@ -212,7 +212,9 @@ export interface BacktestRequest {
form4_sleeve?: string | null; // Form 4 residual-cash sleeve preset name form4_sleeve?: string | null; // Form 4 residual-cash sleeve preset name
ownership_sleeve?: string | null; // 13D/13G ownership sleeve preset name ownership_sleeve?: string | null; // 13D/13G ownership sleeve preset name
risk_off_sleeve?: string | null; // risk-off alpha sleeve preset name risk_off_sleeve?: string | null; // risk-off alpha sleeve preset name
non_core_allocator_v2_mode?: 'shadow' | 'live' | null;
snapshot_id?: string | null; // override dataset snapshot snapshot_id?: string | null; // override dataset snapshot
fixed_capital?: boolean;
} }
export interface BacktestLastParams { export interface BacktestLastParams {
@ -244,6 +246,7 @@ export interface BacktestTask {
form4_sleeve?: string | null; form4_sleeve?: string | null;
ownership_sleeve?: string | null; ownership_sleeve?: string | null;
risk_off_sleeve?: string | null; risk_off_sleeve?: string | null;
non_core_allocator_v2_mode?: 'shadow' | 'live' | null;
snapshot_id?: string | null; snapshot_id?: string | null;
result_summary?: { result_summary?: {
return_pct: number | null; return_pct: number | null;

@ -756,6 +756,7 @@ export function BacktestPage() {
)) ))
: null} : null}
</select> </select>
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>

@ -1,10 +1,10 @@
{ {
"experiment_name": "return_max_long_v17.1_idle_alpha_extended", "experiment_name": "return_max_long_v17.1_idle_alpha_extended",
"dataset_snapshot_id": "midlarge-liquid-long-v1_bucketfix_full_audit_canonical", "dataset_snapshot_id": "midlarge-liquid-long-v1_bucketfix_full_audit_canonical",
"description": "v17.1 champion + qqqm_low_dd parking + micro_event_alpha_plus idle sleeve.", "description": "v17.1 champion + qqqm_low_dd parking + micro_event_alpha_plus_event_plus idle sleeve.",
"base_config": "configs/backtest/return_max_long_v1.json", "base_config": "configs/backtest/return_max_long_v1.json",
"overrides": { "overrides": {
"idle_alpha_sleeve_preset": "micro_event_alpha_plus", "idle_alpha_sleeve_preset": "micro_event_alpha_plus_event_plus",
"signal": { "signal": {
"scoring_model": "return_max_long_v13e", "scoring_model": "return_max_long_v13e",
"score_threshold": 0.45, "score_threshold": 0.45,

@ -55,7 +55,7 @@
} }
}, },
"idle_alpha_sleeve_preset": "micro_event_alpha_plus_event_plus", "idle_alpha_sleeve_preset": "micro_event_alpha_plus_event_plus",
"form4_capture_sleeve_preset": "reserve_form4_cluster_plus_high", "form4_capture_sleeve_preset": "reserve_form4_cluster_plus",
"ownership_capture_sleeve_preset": "ownership_13d_raise_reserve_ultra_balanced_purpose_plus_cooldown90_r95" "ownership_capture_sleeve_preset": "ownership_13d_raise_reserve_ultra_balanced_purpose_plus_cooldown90_r95"
}, },
"strategy_engines": [ "strategy_engines": [

@ -58,7 +58,7 @@
"idle_alpha_sleeve_preset": "micro_event_alpha_plus_event_plus", "idle_alpha_sleeve_preset": "micro_event_alpha_plus_event_plus",
"form4_capture_sleeve_preset": "reserve_form4_cluster_plus", "form4_capture_sleeve_preset": "reserve_form4_cluster_plus",
"ownership_capture_sleeve_preset": "ownership_13d_raise_reserve_ultra_balanced_purpose_plus_cooldown90_r95", "ownership_capture_sleeve_preset": "ownership_13d_raise_reserve_ultra_balanced_purpose_plus_cooldown90_r95",
"risk_off_alpha_sleeve_preset": "risk_off_alpha_gld_crisis60" "risk_off_alpha_sleeve_preset": "risk_off_alpha_gld_crisis65_heavy"
}, },
"strategy_engines": [ "strategy_engines": [
{ {
@ -583,5 +583,6 @@
"changelog": "Composed from return_max_long_v7.119 with sleeves", "changelog": "Composed from return_max_long_v7.119 with sleeves",
"parent": "return_max_long_v7.119", "parent": "return_max_long_v7.119",
"id": 1152, "id": 1152,
"performance_summary": null "performance_summary": null,
"has_journal_entry": true
} }

@ -0,0 +1,613 @@
# Current Best Non-Core Sleeve Stack
작성 기준일: 2026-04-07
이 문서는 현재 `core` 전략 위에 얹어 쓰는 비-core sleeve 5개와, 그 조합 중 현재 가장 높은 수익률을 만들고 있는 스택을 정리한 내부 운영 문서다.
이 문서에서 말하는 `최고 수익 조합`은 아래 조합이다.
- Parking: `qqqm_low_dd_tqqq_active_v2_gld`
- Idle Alpha: `micro_event_alpha_plus_event_plus`
- Form 4: `reserve_form4_cluster_plus`
- Ownership 13D: `ownership_13d_raise_reserve_ultra_balanced_purpose_plus_cooldown90_r95`
- Risk-Off Alpha: `risk_off_alpha_gld_crisis65_heavy`
중요한 점:
- 이 문서는 `최고 수익 조합` 문서다.
- `가장 단순한 조합`, `가장 robust한 조합`, `가장 낮은 DD 조합` 문서가 아니다.
- 지금 스택은 레이어가 많고 상호작용이 강하다.
- 따라서 각 슬리브를 개별적으로 평가한 결과보다, 실제로는 `합친 뒤 최종 결과`가 더 중요하다.
## 1. 현재 최고 수익 조합
개념적으로는 아래와 같은 6-layer 구조다.
1. `Core`
2. `Idle Alpha`
3. `Form 4`
4. `Ownership 13D`
5. `Risk-Off Alpha`
6. `Cash Parking`
실제 일일 실행 순서는 대략 다음과 같다.
1. `Core` 엔진 진입/청산
2. `Idle Alpha` 진입
3. `Form 4` 진입
4. `Ownership 13D` 진입
5. `Risk-Off Alpha` 진입
6. 남은 현금을 `Cash Parking`에 배치
`Cash Parking`은 선행 알파가 아니라, 모든 진입이 끝난 뒤 남은 현금의 마지막 sink다.
## 2. 왜 이 조합이 현재 최고 수익인가
이 조합이 세게 나오는 이유는 각 슬리브의 역할이 비교적 다르기 때문이다.
- `Parking`이 가장 많은 idle cash를 실제로 굴린다.
- `Idle Alpha`는 core가 안 먹는 micro-event PEAD 계열을 줍는다.
- `Form 4`는 내부자 매수 클러스터를 짧게 먹는다.
- `Ownership 13D`는 저빈도지만 한 건당 임팩트가 큰 소유구조 변화를 먹는다.
- `Risk-Off Alpha`는 parking이 `SGOV` 쪽으로 기울 때 위기 국면의 `GLD`를 별도 reserve로 크게 실어준다.
즉 이 스택은:
- 평시에는 `QQQM/TQQQ` 쪽 parking 알파를 크게 먹고
- stress 구간에서는 `GLD`
- event 구간에서는 `Idle Alpha / Form 4 / 13D`
를 동시에 가져가도록 설계돼 있다.
## 3. 현재 최고 수익 스택 예시 설정
```json
{
"risk": {
"cash_parking_preset": "qqqm_low_dd_tqqq_active_v2_gld"
},
"idle_alpha_sleeve_preset": "micro_event_alpha_plus_event_plus",
"form4_capture_sleeve_preset": "reserve_form4_cluster_plus",
"ownership_capture_sleeve_preset": "ownership_13d_raise_reserve_ultra_balanced_purpose_plus_cooldown90_r95",
"risk_off_alpha_sleeve_preset": "risk_off_alpha_gld_crisis65_heavy"
}
```
CLI 예시는 아래와 같다.
```bash
python -m apps.backtester.run \
--manifest /Users/yirugi/mycloud/personal/workspace/fithia2/configs/experiments/return_max_long_v7.119.json \
--split train \
--parking qqqm_low_dd_tqqq_active_v2_gld \
--idle-alpha micro_event_alpha_plus_event_plus \
--form4-sleeve reserve_form4_cluster_plus \
--ownership-sleeve ownership_13d_raise_reserve_ultra_balanced_purpose_plus_cooldown90_r95 \
--risk-off-sleeve risk_off_alpha_gld_crisis65_heavy
```
## 4. Sleeve 1: Cash Parking
Preset:
- `qqqm_low_dd_tqqq_active_v2_gld`
핵심 역할:
- 아무도 안 쓰는 현금을 그냥 놀리지 않고, 가능하면 `QQQM`
- 아주 저변동 구간이면 `TQQQ`
- risk-off defensive 구간이면 `GLD`
- 아무것도 안 맞으면 최종적으로 `SGOV`
즉 이 preset은 `QQQM` 기반 parking에 `TQQQ overlay``GLD defensive relay`를 합친 형태다.
### 4.1. 기본 파라미터
- base parking symbol: `QQQM`
- gate mode: `volatility`
- volatility lookback: `20`
- volatility threshold: `0.35`
- temperature threshold: `1.2`
- entropy lookback: `20`
- entropy threshold: `1.45`
- trend requirement: `True`
- trend mode: `momentum`
- trend period: `20`
- trend re-entry pct: `0.001`
- autocorr threshold: `0.0`
- top-up max peak drawdown pct: `0.02`
- reserve pct: `0.0`
### 4.2. 이 preset이 실제로 하는 일
평시:
- `QQQM`을 기본 parking risk asset으로 쓴다.
매우 저변동 구간:
- `TQQQ` overlay를 허용한다.
defensive relay 구간:
- 원래 gate가 `SGOV`를 가리키는 상황에서도 `GLD`가 충분히 강하면 `GLD`를 쓴다.
### 4.3. TQQQ overlay 조건
- `cash_parking_low_vol_overlay_symbol = "tqqq"`
- `vol_threshold = 0.22`
- `temperature_max = 1.05`
- `entropy_max = 1.45`
즉 이 preset은 `TQQQ`를 항상 쓰는 게 아니라, 아주 차분한 구간에만 overlay로 쓴다.
### 4.4. GLD relay 조건
- `cash_parking_defensive_symbol = "gld"`
- defensive relay enabled
- trigger mode = `always`
- relay risk score max = `100.0`
- defensive momentum min = `0.05`
의미는 이렇다.
- parking gate가 원래 `SGOV`로 가려는 상황이어도
- `GLD` 20일 모멘텀이 충분히 강하면
- defensive asset을 `GLD`로 대체할 수 있다.
### 4.5. 이 preset이 높은 수익을 만드는 이유
- `qqqm_low_dd`의 기본 low-DD 성격을 유지하면서
- calm regime에서는 `TQQQ`
- crisis defensive regime에서는 `GLD`
를 같이 쓴다.
`QQQM -> TQQQ -> GLD -> SGOV`로 이어지는 다층 parking이다.
### 4.6. 한계
- 설정이 복잡하다.
- 실제 성능의 상당 부분이 이 parking preset 하나에 집중된다.
- 다른 sleeve를 잘 만들어도, parking이 너무 세면 비교가 왜곡되기 쉽다.
## 5. Sleeve 2: Idle Alpha
Preset:
- `micro_event_alpha_plus_event_plus`
핵심 역할:
- `Core`가 안 먹는 low-frequency post-allocation micro-event 알파를 뒤에 붙인다.
- 그냥 엔진을 추가하는 게 아니라, `post_allocation_idle_only` 엔진으로 따로 붙는다.
- 그리고 synthetic breadth engine 하나를 더 넣는다.
### 5.1. 엔진 구성
이 preset은 아래 5개 엔진으로 구성된다.
1. `next_open_long_material_contract_mixed_micro_postmarket`
2. `next_open_long_guidance_mixed_micro_postmarket`
3. `next_open_long_earnings_unknown_inline_postmarket_strict`
4. `next_open_long_bullish_raised_strong`
5. `idle_macro_breadth_smh_postalloc`
앞의 4개는 micro event snapshot 엔진이고, 마지막 1개는 synthetic breadth engine이다.
### 5.2. 각 엔진의 의도
`next_open_long_material_contract_mixed_micro_postmarket`
- `material_contract`
- small/mid-ish universe
- post-market
- next-open long
- 보수적인 gap cap
- max hold `12`
`next_open_long_guidance_mixed_micro_postmarket`
- `guidance_update`
- mixed direction parse에서 under-owned micro setup
- post-market
- next-open long
- max hold `12`
`next_open_long_earnings_unknown_inline_postmarket_strict`
- `earnings_release`
- direction `unknown`
- guidance `inline_or_maintained`
- post-market
- strict filter
- max hold `12`
`next_open_long_bullish_raised_strong`
- `earnings_release`
- bullish
- guidance `raised`
- 강한 reaction day
- 높은 close location
- 더 긴 max hold `20`
`idle_macro_breadth_smh_postalloc`
- synthetic breadth engine
- trade symbol은 고정 `SMH`
- breadth symbols는 `QQQ`, `XLK`, `SMH`
- breadth count `2` 이상
- max hold `2`
### 5.3. allocator 설정
이 preset은 엔진만 붙이는 게 아니라 `IdleAlphaConfig`도 같이 조정한다.
- dynamic allocator enabled
- cash ratio low/high: `0.04 / 0.16`
- cash scale low/high: `0.8 / 1.025`
- crowded primary candidate count: `6`
- crowded primary unique sector count: `4`
- crowded scale: `0.79`
- synthetic scale multiplier: `1.03`
- snapshot scale multiplier: `1.02`
- synthetic reentry cooldown days: `2`
- min scale: `0.6`
- max scale: `1.045`
즉 cash가 많고 core slate가 덜 혼잡한 날에만 idle alpha를 조금 더 키운다.
### 5.4. 이 preset의 핵심 특징
- core와 같은 loop에서 무작정 싸우는 방식이 아니다.
- `selection_priority = -1/-2`
- `post_allocation_idle_only = True`
즉 core allocation 이후 남은 자본으로만 진입한다.
### 5.5. 장점
- core와의 충돌을 줄인다.
- 일반적인 PEAD와 다른 micro-event 알파를 추가한다.
- breadth engine이 따로 있어 `SMH` 계열 확장도 먹을 수 있다.
### 5.6. 한계
- 엔진 수가 많아 설명과 attribution이 복잡하다.
- 실제로는 synthetic breadth engine이 존재감이 클 수 있다.
- 사용자 체감상 `SMH`가 자주 보일 수 있다.
## 6. Sleeve 3: Form 4
Preset:
- `reserve_form4_cluster_plus`
핵심 역할:
- 내부자 매수 클러스터를 `next_open`으로 짧게 먹는 reserve sleeve다.
### 6.1. 입력 데이터
- `pit_events_path = data/reference/form4_daily_events_pit.parquet`
즉 point-in-time Form 4 event cache를 사용한다.
### 6.2. 기본 파라미터
- reserve pct: `0.06`
- min owner count: `2`
- min total value: `5,000,000`
- min purchase pct: `0.0`
- max lag days: `None`
- hold days: `20`
- max positions: `6`
- max new per day: `2`
### 6.3. 선택 로직
Form 4 후보는:
- 전 거래일 이후부터 어제까지 filing된 event를 본다.
- 오늘이 해당 filing의 `first trading day after filing`인 경우만 진입한다.
- `owner_count >= 2`
- `total_value >= 5M`
- `weighted_purchase_pct >= min_purchase_pct`
- `event_day_count >= min_event_day_count`
- `max_lag_days` 조건 통과
- 이미 열려 있는 symbol은 제외
정렬 우선순위는:
1. `owner_count`
2. `event_day_count`
3. `weighted_purchase_pct`
4. `total_value`
즉 단순히 dollar value만 보지 않고, `몇 명이 동시에 샀는가`를 가장 강하게 본다.
### 6.4. 진입/청산 구조
- entry timing: `next_open`
- max holding days: `20`
- 기본적으로 early failure 룰은 살아 있다.
### 6.5. 현재 중요한 구현상 특성
현재 Form 4 sleeve는:
- `equity_est * reserve_pct`로 예산을 계산하지만
- 실제 집행 예산은 `min(reserve_budget, self._cash)`다.
즉 현재 구현은:
- `Ownership`
- `Risk-Off`
- `Dividend`
처럼 parked cash를 일부 풀어서 reserve를 채우지 않는다.
이 말은 곧:
- `Form 4`는 개념상 reserve sleeve지만
- 실제로는 `그 시점에 free cash가 남아 있을 때만` 강하게 작동한다.
이 특성은 성능에 큰 영향을 준다.
### 6.6. 현재 평가
- 알파는 완전히 무효가 아니다.
- 하지만 다른 sleeve 대비 일관성은 가장 약한 편이다.
- 자금을 더 열어준다고 항상 좋아지지 않았고, 실제로 추가 진입분 질이 나빠지는 구간이 있었다.
`Form 4`는 현재 스택에서 필요는 하지만, 가장 신중히 다뤄야 하는 sleeve다.
## 7. Sleeve 4: Ownership 13D
Preset:
- `ownership_13d_raise_reserve_ultra_balanced_purpose_plus_cooldown90_r95`
핵심 역할:
- 저빈도지만 임팩트가 큰 `13D amendment` 기반 소유구조 변화를 크게 탄다.
### 7.1. 입력 데이터
- `pit_events_path = data/reference/ownership_13d13g_events_pit.parquet`
즉 13D/13G point-in-time event cache를 사용한다.
### 7.2. 기본 파라미터
- reserve pct: `0.95`
- form groups: `["13D"]`
- min percent owned: `5.0`
- min percent delta points: `2.0`
- exclude_housekeeping_purpose: `True`
- exclude_structural_exchange_purpose: `True`
- require_amendment: `True`
- require_initial: `False`
- require_activist: `False`
- require_13g_to_13d_transition: `False`
- symbol_cooldown_days_after_loss: `90`
- hold_days: `30`
- min_avg_dollar_volume: `10,000,000`
- max_positions: `6`
- max_new_per_day: `2`
- min_cash_ratio_for_overlay: `0.25`
- max_idle_deploy_pct: `0.0`
### 7.3. 이 preset 이름의 의미
`ultra_balanced_purpose_plus_cooldown90_r95`
의 의미는 대략 아래와 같다.
- `ultra_balanced`: reserve를 크게 쓴다
- `purpose_plus`: housekeeping / structural exchange 목적 filing을 뺀다
- `cooldown90`: 같은 symbol에서 loss 뒤 90일 cooldown
- `r95`: reserve pct `95%`
### 7.4. 선택 로직
후보는:
- `13D` 그룹만
- amendment만
- percent owned `>= 5%`
- delta `>= 2.0%p`
- housekeeping purpose 제외
- structural exchange purpose 제외
- loss cooldown 90일 통과
- `ADV20 >= 10M`
그리고 symbol별로 가장 좋은 event 하나만 남긴다.
정렬 키는:
1. `ownership_strength_score`
2. `activist_flag`
3. `percent_delta_points`
4. `percent_owned`
즉 핵심은 `delta 확대폭``strength score`다.
### 7.5. 자본 사용 방식
Ownership sleeve는 reserve budget을 계산한 뒤:
- cash가 부족하면 parked cash를 일부 풀 수 있다.
- 따라서 Form 4보다 실제 집행력이 더 강하다.
이 슬리브는 현재 스택에서 가장 `capital-hungry`한 sleeve 중 하나다.
### 7.6. 왜 강한가
- low-frequency
- high-conviction
- purpose filter로 잡음 제거
- loss cooldown으로 같은 symbol 재진입 실패를 줄임
즉 횟수는 적지만, 한 건당 임팩트가 크다.
### 7.7. 한계
- 이벤트 수가 적다.
- reserve가 커서 parking과 직접 경쟁한다.
- 목적문구 파싱 품질에 민감하다.
## 8. Sleeve 5: Risk-Off Alpha
Preset:
- `risk_off_alpha_gld_crisis65_heavy`
핵심 역할:
- parking regime가 이미 `SGOV` 쪽일 때, 그 stress 구간에서 `GLD`를 별도 reserve sleeve로 크게 실어준다.
즉 이 sleeve는 평시 알파가 아니라 `위기 구간 전용 알파`다.
### 8.1. 기본 파라미터
- enabled: `True`
- reserve pct: `0.70`
- symbols: `["gld"]`
- momentum lookback days: `20`
- min symbol momentum: `0.05`
- min consecutive sgov days: `3`
- min parking risk score: `65.0`
- rotation momentum gap: `0.02`
- max holding days: `0`
- min cash ratio for overlay: `0.0`
- max idle deploy pct: `0.0`
### 8.2. 진입 조건
아래를 모두 만족해야 한다.
- preview parking target이 `SGOV`
- `SGOV` streak가 최소 `3일`
- parking risk score가 `65` 이상
- `GLD` 20일 모멘텀이 `5%` 이상
이 조건을 통과하면 reserve `70%``GLD`에 태운다.
### 8.3. symbol 선택 방식
현재 preset은 symbols가 `["gld"]` 하나라 회전 대상이 사실상 없다.
그래도 로직상으로는:
- 후보 symbol들의 momentum을 비교
- 현재 들고 있는 symbol과 새 best symbol의 차이가 `rotation_momentum_gap`보다 크지 않으면 유지
하는 구조다.
### 8.4. 자본 사용 방식
Risk-Off Alpha는:
- reserve budget 기반
- cash가 부족하면 parked cash를 일부 풀 수 있다.
즉 이 슬리브는 parking 위에 얹는 별도 crisis reserve layer다.
### 8.5. 왜 `crisis65_heavy`인가
`crisis60`보다 느리지만 더 정제된 진입이고, `balanced_refined`보다 reserve가 커서 full stack에서 더 세게 나왔다.
핵심은:
- 너무 이른 `GLD` 진입은 불필요
- 하지만 실제 위기 구간에선 reserve를 크게 태우는 게 유리
라는 점이다.
## 9. 5개 sleeve의 상호작용
현재 스택의 핵심은 각 슬리브가 서로 완전히 독립이 아니라는 점이다.
### 9.1. capital interaction
- `Idle Alpha`는 core 이후의 후순위 엔진 집합이다.
- `Form 4`는 free cash 기반 reserve sleeve다.
- `Ownership`는 reserve sleeve이며 parking cash release가 가능하다.
- `Risk-Off Alpha`도 reserve sleeve이며 parking cash release가 가능하다.
- `Parking`은 최종 sink다.
즉 자본 경쟁은 주로 아래 축에서 일어난다.
- `Ownership` vs `Risk-Off` vs `Parking`
- `Form 4` vs available raw cash
### 9.2. practical impact
- parking이 너무 세면 나머지 sleeve 비교가 왜곡된다.
- ownership reserve가 너무 크면 parking 수익이 줄 수 있다.
- risk-off alpha가 너무 세면 `GLD` 노출이 과도해질 수 있다.
그래서 이 스택은 단순 합이 아니라 `capital routing system`에 가깝다.
## 10. 왜 지금은 이 구조를 core에 합치지 않는가
현재는 core에 전부 섞기보다 sleeve를 유지하는 편이 낫다.
이유는 아래와 같다.
- 데이터 소스가 다르다.
- 운용 목적이 다르다.
- attribution이 필요하다.
- 개별 on/off가 가능해야 한다.
- parking/ownership/risk-off는 `현금 routing` 문제까지 포함한다.
즉 지금 단계에서는:
- `core alpha`
- `idle capital utilization`
을 분리해 두는 게 더 낫다.
## 11. 현재 스택에서 빠진 것
현재 최고 수익 스택에는 아래가 없다.
- `Dividend Capture`
이유는 간단하다.
- point-in-time infra는 만들었지만
- 현재까지는 robust하게 additive하지 않았다.
즉 현재 public surface에는 관련 기반이 일부 남아 있어도, 최고 수익 스택에는 포함하지 않는다.
## 12. 운영 메모
이 스택을 쓸 때는 아래를 같이 기억해야 한다.
- `최고 수익 스택``가장 단순한 스택`은 다르다.
- `최고 수익 스택``가장 설명하기 쉬운 스택`도 다르다.
- 현재 가장 큰 기여자는 대체로 parking 쪽이다.
- `Form 4`는 현재 가장 불안정한 축이다.
- `Ownership`는 가장 저빈도지만 가장 의미 있는 reserve sleeve다.
- `Risk-Off Alpha`는 위기 국면에서만 존재감이 커진다.
## 13. 추천 해석
현재 최고 수익 조합은 아래처럼 이해하면 된다.
- `Parking`이 엔진이다.
- `Idle Alpha`가 micro-event 확장이다.
- `Form 4`가 insider cluster 확장이다.
- `Ownership 13D`가 ownership shock 확장이다.
- `Risk-Off Alpha`가 crisis GLD 확장이다.
즉 이 스택은 `core + 다섯 개의 별도 알파/자본-routing 레이어`다.
현재 기준에서 최고 수익 조합을 한 줄로 쓰면:
`core + qqqm_low_dd_tqqq_active_v2_gld + micro_event_alpha_plus_event_plus + reserve_form4_cluster_plus + ownership_13d_raise_reserve_ultra_balanced_purpose_plus_cooldown90_r95 + risk_off_alpha_gld_crisis65_heavy`
이 문서의 목적은 이 조합을 기억하기 쉽게 적는 게 아니라, 왜 이 조합이 이렇게 생겼는지와 각 레이어가 무엇을 담당하는지를 한 번에 이해하게 만드는 데 있다.

@ -0,0 +1,790 @@
# return_max_long_v7.119_composed_gld Explained
작성 기준일: 2026-04-07
이 문서는 `return_max_long_v7.119_composed_gld` 전략을 고등학생도 이해할 수 있게 최대한 쉽게, 하지만 실제 운영에 필요한 내용은 빠뜨리지 않고 정리한 문서다.
이 전략은 단순히 "좋은 실적 발표 주식을 사는 전략"이 아니다.
정확히는:
- `v7.119 core strategy`
- `cash parking`
- `idle alpha`
- `Form 4`
- `13D ownership`
- `risk-off alpha`
를 한 번에 합친 `복합 운용 스택`이다.
즉 이 전략은 "한 개의 엔진"이 아니라, `하나의 코어 전략 + 다섯 개의 보조 레이어`라고 이해하는 게 맞다.
## 1. 한 줄 요약
이 전략은 이렇게 생각하면 된다.
- 좋은 기업 공시가 나오면 core가 먼저 산다.
- core가 안 산 좋은 기회는 `Idle Alpha`가 줍는다.
- 내부자들이 같이 산 종목은 `Form 4`가 줍는다.
- 큰 투자자가 지분을 확 늘린 종목은 `13D`가 줍는다.
- 시장이 위험해지면 `Risk-Off Alpha``GLD`를 산다.
- 남는 현금은 `Cash Parking``QQQM / TQQQ / GLD / SGOV` 중 하나에 둔다.
즉 이 전략은 `기업 이벤트`, `내부자 거래`, `소유구조 변화`, `위기 방어`, `현금 운용`을 동시에 다루는 전략이다.
## 2. 현재 최고 수익 조합
현재 이 전략에서 최고 수익 조합으로 보고 있는 구성은 아래다.
- Parking: `qqqm_low_dd_tqqq_active_v2_gld`
- Idle Alpha: `micro_event_alpha_plus_event_plus`
- Form 4: `reserve_form4_cluster_plus`
- Ownership 13D: `ownership_13d_raise_reserve_ultra_balanced_purpose_plus_cooldown90_r95`
- Risk-Off Alpha: `risk_off_alpha_gld_crisis65_heavy`
`return_max_long_v7.119_composed_gld`는 사실상 아래 조합을 전제로 읽는 게 맞다.
```json
{
"risk": {
"cash_parking_preset": "qqqm_low_dd_tqqq_active_v2_gld"
},
"idle_alpha_sleeve_preset": "micro_event_alpha_plus_event_plus",
"form4_capture_sleeve_preset": "reserve_form4_cluster_plus",
"ownership_capture_sleeve_preset": "ownership_13d_raise_reserve_ultra_balanced_purpose_plus_cooldown90_r95",
"risk_off_alpha_sleeve_preset": "risk_off_alpha_gld_crisis65_heavy"
}
```
## 3. 이 전략은 무엇을 노리나
가장 중심이 되는 아이디어는 `좋은 소식 뒤의 관성`이다.
예를 들어:
- 회사가 실적을 발표했는데 생각보다 훨씬 좋다
- 장 마감 후 공시가 나왔다
- 다음날 사람들이 "와, 이 회사 진짜 좋네" 하고 천천히 더 산다
그러면 발표 당일 반응이 끝이 아니라, 그 뒤 며칠에서 몇 주 동안 주가가 더 움직일 수 있다.
이 전략은 그 움직임을 먹는 전략이다.
그런데 여기서 한 단계 더 나아간다.
단순히 실적 발표만 보는 게 아니라:
- 실적 발표
- 가이던스 발표
- 큰 계약
- 기타 중요 공시
- 내부자 매수
- 13D 지분 확대
- 위기 구간 gold
까지 한 묶음으로 본다.
즉 "좋은 기업 이벤트" 전반을 먹는 구조다.
## 4. 이 전략은 하나의 전략이 아니라 두 덩어리다
이 전략은 크게 두 덩어리로 나뉜다.
### 4.1. Core
Core는 `기업 이벤트 알파`다.
- 좋은 실적
- 좋은 가이던스
- 좋은 공시
가 나왔을 때 종목을 직접 사는 부분이다.
### 4.2. Non-Core Sleeves
Core 외의 5개 sleeve는:
- 현금을 놀리지 않기 위해
- core가 못 먹는 기회를 따로 잡기 위해
- 위기 구간에 따로 방어/알파를 만들기 위해
존재한다.
즉 core가 "메인 축"이고, sleeves는 "남는 기회와 남는 자본을 더 뽑아먹는 장치"다.
## 5. 데이터셋과 대상 종목
이 전략은 아래 스냅샷을 쓴다.
- `midlarge-liquid-long-v1_bucketfix_full_audit_canonical`
이 이름을 쉽게 풀면:
- `midlarge`: 너무 작은 잡주가 아니라 어느 정도 큰 회사들
- `liquid`: 거래가 충분히 많이 되는 회사들
- `long`: 매수만 하는 전략
즉 거래가 잘 되고, 너무 허접하지 않은 미국 중대형 주식이 대상이다.
## 6. 이 전략은 어디서 시작됐나
이 실험 이름만 보면 복잡하지만, 실제 계보는 단순하다.
- `v7.70`
- `v7.110`
- `v7.113`
- `v7.114`
- `v7.115`
- `v7.116`
- `v7.117`
- `v7.118`
- `v7.119`
즉 오래 쌓아 온 `v7 계열`의 최신 조합 버전이다.
`return_max_long_v7.119_composed_gld`는:
- `v7.119` core를 베이스로 두고
- 그 위에 GLD 기반 parking/sleeves까지 붙인 `composed` 버전이다.
## 7. core 전략의 큰 그림
이 전략의 core는 "좋은 이벤트가 나온 뒤, 시장이 너무 늦게 반응하거나 딱 적당히 반응한 종목을 사서 며칠 보유하는" 구조다.
여기서 중요한 포인트는:
- 아무 이벤트나 사지 않는다
- 반응이 너무 약해도 안 산다
- 반응이 너무 과열돼도 안 산다
- 어떤 이벤트냐에 따라 다른 엔진이 산다
즉 "실적이 좋다" 한 줄로 끝나는 전략이 아니라, 상황별로 다른 12개의 코어 엔진이 존재한다.
## 8. 코어 전략의 공통 기본 설정
manifest에서 핵심 공통 설정은 이렇다.
### 8.1. 점수 기준
- scoring model: `return_max_long_v13e`
- 기본 score threshold: `0.45`
- 하루 최대 후보 수: `18`
- A-tier threshold: `0.58`
쉽게 말하면:
- 각 공시에 대해 AI/규칙이 점수를 매긴다
- 너무 낮은 점수는 버린다
- 높은 점수는 더 좋은 후보로 본다
### 8.2. 리스크 기본값
- 기본 per-trade risk: `0.069`
- A-tier per-trade risk: `0.0759`
- max daily new risk: `1.5`
- max positions: `24`
- max positions per sector: `5`
- max position value pct: `1.5`
- max ADV fraction: `0.2`
이 숫자들은 단순히 "많이 사자"가 아니라, 종목 하나와 하루 전체에 들어가는 위험을 나누는 규칙이다.
### 8.3. 실행 기본값
- A-tier target 1R: `3.5`
- non-A-tier target 1R: `2.25`
- non-A-tier partial take: `20%`
- trailing warmup days: `7`
- max holding days: `12`
- early failure no-progress days: `1`
- early failure no-progress r: `0.15`
- lookback entry enabled: `true`
쉽게 말하면:
- 이익이 나면 일부 익절할 수 있고
- 너무 오래 끌지 않고
- 들어갔는데 바로 안 움직이면 빨리 자른다
## 9. 코어의 12개 엔진
이 전략의 core는 12개 엔진으로 구성된다.
각 엔진은 "어떤 종류의 공시를, 어떤 반응 패턴에서, 언제 살지"를 따로 정한다.
### 9.1. 엔진 1: `next_open_long_unknown_material_patient`
무슨 상황인가:
- `material_contract`
- 방향은 완전히 명확하지 않음
- post-market 공시
- 다음날 시가에 진입
왜 쓰나:
- 큰 계약은 좋은 뉴스일 수 있지만, 시장이 첫날 완전히 이해하지 못하는 경우가 있다
- 너무 과열되기 전, 다음날 시가에 들어가려는 엔진이다
핵심 조건:
- reaction day return: `0 ~ 15%`
- close location: `0.5 ~ 0.85`
- gap: `0 ~ 2%`
- volume ratio: `0.8 ~ 2`
- 시총: `5B ~ 10B`
한마디로:
- "좋은 계약 같긴 한데 아직 시장이 난리 난 상태는 아닌 중형 종목"
### 9.2. 엔진 2: `reaction_close_long_core`
이 전략의 중심 엔진이다.
무슨 상황인가:
- `earnings_release`, `guidance_update`
- same-day
- 발표 당일 종가에 진입
핵심 조건:
- reaction day return: `0 ~ 18%`
- close location: `0.45+`
- volume ratio: `1+`
- score threshold override: `0.42`
한마디로:
- "좋은 실적/가이던스가 나왔고, 당일 주가가 괜찮게 반응했다면 종가에 산다"
이 엔진이 전략의 메인 몸통이다.
### 9.3. 엔진 3: `reaction_close_long_residual_lowclose_gap_d3`
무슨 상황인가:
- 실적/가이던스는 좋음
- 당일 상승은 꽤 있는데
- 종가 위치는 살짝 낮다
- gap은 비교적 크다
핵심 조건:
- reaction day return: `3 ~ 20%`
- close location: `0.35 ~ 0.45`
- gap: `5 ~ 10%`
- volume ratio: `1.25+`
한마디로:
- "좋은 뉴스인데, 장중 흔들리면서 종가가 살짝 아래에 끝난 gap 종목"
### 9.4. 엔진 4: `reaction_close_long_residual_smallcap_gap`
무슨 상황인가:
- 실적/가이던스
- gap up이 있음
- close location은 0.45 이상
- 상대적으로 작은 쪽 종목
핵심 조건:
- reaction day return: `3 ~ 20%`
- gap: `5 ~ 10%`
- volume ratio: `1.25+`
- max market cap: `10B`
한마디로:
- "작은 쪽 종목에서 나온 gap-up 좋은 발표"
### 9.5. 엔진 5: `reaction_close_long_extreme_orderly`
무슨 상황인가:
- 실적/가이던스 발표 후
- 당일 반응이 매우 강함
- 그런데 모양이 질서정연하다
핵심 조건:
- reaction day return: `20 ~ 40%`
- close location: `0.83+`
- gap: `0 ~ 15%`
- volume ratio: `6+`
- score threshold override: `0.65`
한마디로:
- "엄청 세게 오른 초강한 승자"
이 엔진은 폭발적으로 오른 종목 중에서도 가장 좋은 것만 잡는다.
### 9.6. 엔진 6: `next_open_long_unknown_inline_hivol`
무슨 상황인가:
- 실적 발표
- 방향은 unknown
- guidance는 inline
- 당일 거래량이 많음
- 다음날 시가 진입
핵심 조건:
- reaction day return: `0 ~ 6%`
- close location: `0.74 ~ 0.86`
- gap: `0 ~ 4%`
- volume ratio: `2.1 ~ 2.8`
한마디로:
- "방향 해석은 애매하지만 시장이 진지하게 보고 있는 실적 종목"
### 9.7. 엔진 7: `next_open_long_guidance_unknown_orderly`
무슨 상황인가:
- guidance update
- 방향은 unknown
- not provided 쪽
- 다음날 시가 진입
핵심 조건:
- reaction day return: `-3 ~ 5%`
- close location: `0.9+`
- gap: `-2 ~ 3%`
- volume ratio: `0.8+`
- min market cap: `9B`
한마디로:
- "가이던스 문장은 애매한데, 시장이 고점 근처로 마감시킨 큰 종목"
### 9.8. 엔진 8: `next_open_long_earnings_mixed_inline_orderly`
무슨 상황인가:
- 실적 발표
- direction mixed
- guidance inline
- 크게 흔들리지 않음
- 다음날 시가 진입
핵심 조건:
- reaction day return: `-3 ~ 6%`
- close location: `0.5+`
- gap: `-2 ~ 5%`
- volume ratio: `1+`
- min market cap: `8B`
한마디로:
- "엄청 좋다고 확신하긴 어렵지만, 생각보다 버틸 만한 실적 종목"
### 9.9. 엔진 9: `next_open_long_unknown_material_orderly`
무슨 상황인가:
- `material_contract`
- unknown direction
- orderly reaction
- 다음날 시가 진입
핵심 조건:
- reaction day return: `-2 ~ 6%`
- close location: `0.5 ~ 0.8`
- gap: `-2 ~ 2%`
- volume ratio: `0.9 ~ 1.4`
- 시총: `10B ~ 100B`
한마디로:
- "대형 계약인데 시장이 담담하게 받아들인 큰 회사"
### 9.10. 엔진 10: `next_open_long_other_material_mixed_orderly`
무슨 상황인가:
- `other_material_event`
- mixed direction
- 다음날 시가 진입
핵심 조건:
- reaction day return: `-4 ~ 6%`
- close location: `0.45+`
- gap: `-3 ~ 5%`
- volume ratio: `1+`
- min market cap: `6B`
한마디로:
- "기타 중요 공시인데 시장이 적당히 긍정적으로 받아들인 경우"
### 9.11. 엔진 11: `next_open_long_unknown_ome_orderly`
무슨 상황인가:
- `other_material_event`
- unknown direction
- 다음날 시가 진입
핵심 조건:
- reaction day return: `0 ~ 12%`
- close location: `0.55+`
- volume ratio: `1+`
- min market cap: `4B`
한마디로:
- "기타 공시인데 아직 해석은 애매하지만 시장 반응은 괜찮은 경우"
### 9.12. 엔진 12: `next_open_long_bullish_raised_recovery_broad_oneoff`
무슨 상황인가:
- 실적 발표
- bullish
- guidance raised
- 그런데 첫날 너무 깔끔하게 끝나진 않음
- recovery 가능성을 노림
핵심 조건:
- reaction day return: `0 ~ 10%`
- close location: `0.3 ~ 0.75`
- gap: `1 ~ 12%`
- volume ratio: `1.3 ~ 5`
- min market cap: `8B`
한마디로:
- "좋은 실적인데 시장 반응이 애매해서, 다음날부터 회복할 수 있는 종목"
## 10. 코어를 아주 쉽게 요약하면
12개 엔진은 복잡해 보이지만, 실제로는 네 부류다.
1. `당일 바로 사는 강한 실적형`
2. `다음날 시가에 사는 애매하지만 좋아 보이는 실적형`
3. `계약/기타 공시형`
4. `폭발적 초강자/회복형`
즉 core는 "좋은 기업 이벤트 뒤의 관성"을 여러 모습으로 나눠 잡는 시스템이다.
## 11. non-core 5개 sleeve는 무엇을 하나
이 전략의 핵심은 core만이 아니다.
오히려 지금 최고 수익 버전은 아래 5개 sleeve가 함께 있어야 완성된다.
### 11.1. Parking: `qqqm_low_dd_tqqq_active_v2_gld`
역할:
- 남는 현금을 놀리지 않고 굴린다
어떻게 굴리나:
- 기본은 `QQQM`
- 아주 calm하면 `TQQQ`
- defensive 상황에서 gold가 강하면 `GLD`
- 최후에는 `SGOV`
쉽게 말하면:
- "현금을 그냥 현금으로 두지 않고, 상황에 따라 가장 유리한 주차장에 둔다"
### 11.2. Idle Alpha: `micro_event_alpha_plus_event_plus`
역할:
- core가 안 사는 micro-event 기회를 뒤에서 줍는다
구성:
- 4개 micro snapshot engine
- 1개 `SMH` breadth engine
쉽게 말하면:
- "메인 전략이 안 잡은 작은 기회를 추가로 먹는다"
### 11.3. Form 4: `reserve_form4_cluster_plus`
역할:
- 내부자 여러 명이 같이 큰 금액을 산 종목을 잡는다
쉽게 말하면:
- "회사 안 사람들이 직접 돈 넣어 산 종목을 따라간다"
핵심 기준:
- 2명 이상
- 총 500만 달러 이상
- hold 20일
- reserve 6%
### 11.4. Ownership 13D: `ownership_13d_raise_reserve_ultra_balanced_purpose_plus_cooldown90_r95`
역할:
- 큰 투자자가 지분을 확 늘린 종목을 잡는다
쉽게 말하면:
- "큰손이 회사 지분을 많이 늘렸다면, 그 이유가 있을 가능성이 높다"
핵심 기준:
- 13D amendment
- owned 5% 이상
- delta 2.0%p 이상
- housekeeping / structural exchange 제거
- 손실 뒤 90일 cooldown
- reserve 95%
이건 현재 sleeve들 중 가장 무거운 reserve를 쓰는 축 중 하나다.
### 11.5. Risk-Off Alpha: `risk_off_alpha_gld_crisis65_heavy`
역할:
- 시장이 위험해서 parking이 `SGOV` 쪽으로 기울 때, 별도 reserve로 `GLD`를 크게 산다
쉽게 말하면:
- "시장 분위기가 무서울 때 gold를 따로 크게 실어서 방어 겸 알파를 노린다"
핵심 기준:
- parking target이 `SGOV`
- `SGOV` streak 3일 이상
- parking risk score 65 이상
- `GLD` 20일 모멘텀 5% 이상
- reserve 70%
## 12. 하루 동안 실제로 무슨 일이 일어나나
이 전략의 하루를 아주 단순하게 말하면 다음과 같다.
### 단계 1. core가 먼저 본다
오늘 들어올 만한 기업 이벤트 후보를 찾고, core 엔진 12개 중 맞는 엔진으로 진입한다.
### 단계 2. idle alpha가 본다
core가 다 돈 뒤에 남는 기회를 본다.
### 단계 3. Form 4가 본다
최근 filing된 내부자 매수 이벤트를 보고, reserve 안에서 진입한다.
### 단계 4. Ownership 13D가 본다
최근 filing된 13D 이벤트를 보고, 조건 맞는 종목을 진입한다.
### 단계 5. Risk-Off Alpha가 본다
만약 지금 market regime가 충분히 위험하고 gold가 강하면 `GLD`를 reserve로 산다.
### 단계 6. 마지막에 parking
그날 아무도 안 쓴 현금이 남으면, parking preset 규칙대로 `QQQM / TQQQ / GLD / SGOV` 중 하나에 둔다.
즉 순서가 중요하다.
- sleeves가 먼저
- parking은 맨 마지막
이다.
## 13. 왜 이 전략이 세게 나오나
이 전략이 강한 이유는 core 하나가 미친 듯이 좋아서만은 아니다.
실제로는:
- core가 기업 이벤트를 먹고
- parking이 idle cash를 매우 적극적으로 굴리고
- risk-off alpha가 stress 구간 `GLD`
- ownership가 저빈도 고충격 이벤트
- idle alpha가 core 바깥의 추가 기회
를 같이 담당한다.
즉 이 전략은 "좋은 이벤트 주식 전략"이라기보다:
- 좋은 이벤트를 사고
- 남는 돈도 계속 굴리고
- 위기 때도 방어 자산으로 추가 알파를 만들고
- 내부자/소유구조 변화도 별도로 먹는
`멀티 엔진 자본 운용 시스템`이다.
## 14. 이 전략의 장점
### 14.1. 한 가지 시장 패턴에만 의존하지 않는다
실적 발표만 보는 전략은 아니다.
- earnings
- guidance
- contract
- other material
- insider
- ownership
- gold crisis overlay
를 동시에 본다.
### 14.2. idle cash를 거의 쉬게 두지 않는다
보통 전략은 기회가 없을 때 현금이 그냥 놀기 쉽다.
이 전략은 그렇지 않다.
- parking이 있고
- risk-off alpha가 있고
- 별도 sleeve들이 있다
그래서 현금이 계속 일하려고 한다.
### 14.3. 시장 상태에 따라 돈 가는 곳이 바뀐다
calm:
- `TQQQ`
normal:
- `QQQM`
stress:
- `GLD`
- 심하면 `SGOV`
즉 한 가지 자산만 고집하지 않는다.
## 15. 이 전략의 단점
### 15.1. 매우 복잡하다
이 전략은:
- core 엔진 12개
- non-core sleeve 5개
를 동시에 쓴다.
그래서 설명하기 어렵고, 원인분석도 어렵다.
### 15.2. 성능의 상당 부분이 parking에 묻힐 수 있다
이 전략은 core 성능만 보는 게 아니다.
오히려:
- parking
- risk-off alpha
가 강하게 돌면, 성과의 상당 부분이 거기서 나온다.
즉 "이 전략이 core가 좋아서 오른 건가, 아니면 cash routing이 좋아서 오른 건가"를 항상 따져야 한다.
### 15.3. sleeve 간 자본 경쟁이 있다
예를 들어:
- ownership가 reserve를 많이 쓰면
- parking으로 갈 돈이 줄고
- risk-off alpha와도 경쟁한다
즉 각 부품이 독립적이지 않다.
## 16. 이 전략을 볼 때 가장 중요한 오해 하나
이 전략을 보면 많은 사람이 이렇게 생각하기 쉽다.
- "아, 좋은 실적 종목을 사는 전략이구나"
그건 절반만 맞다.
정확히는:
- core는 좋은 실적/공시를 사고
- sleeves는 좋은 현금 활용 기회를 산다
이다.
즉 이 전략은 단순한 stock picking 전략이 아니라, `stock picking + capital routing + crisis overlay` 전략이다.
## 17. `composed_gld`라는 이름의 뜻
이 이름은 세 부분으로 나눠 읽으면 쉽다.
### `return_max_long`
- 기본 철학은 long-only
- 좋은 이벤트 이후의 상승 관성을 최대한 크게 먹는 것
### `v7.119`
- v7 계열의 119번째 주요 변형
### `composed_gld`
- core 하나만 있는 게 아니라 여러 레이어를 합친 composed 버전
- 그 composed 버전에서 `GLD`가 매우 중요한 역할을 한다
## 18. 이 전략을 정말 쉽게 비유하면
이 전략은 하나의 스포츠 팀 같다고 보면 된다.
- `core`는 주전 선수
- `idle alpha`는 벤치 득점원
- `form4`는 내부 정보에 가까운 보조 스카우트
- `ownership 13d`는 대형 투자자 흐름을 읽는 분석가
- `risk-off alpha`는 위기 때 투입되는 수비 specialist
- `parking`은 남는 체력을 쉬게 두지 않고 계속 굴리는 체력 코치
즉 한 명의 슈퍼스타가 아니라, 역할이 다른 여러 선수가 같이 뛰는 팀이다.
## 19. 이 전략을 운영할 때 기억해야 할 것
1. 이 전략은 `core 단독 전략`이 아니다.
2. 현재 최고 수익은 `non-core sleeves`까지 포함한 전체 조합에서 나온다.
3. `parking``risk-off alpha`의 영향력이 매우 크다.
4. `Form 4`는 존재하지만, 가장 안정적인 축은 아니다.
5. `Ownership 13D`는 저빈도지만 임팩트가 크다.
6. 이 전략의 성능은 "좋은 종목 선정"만으로 설명되지 않는다.
## 20. 최종 요약
`return_max_long_v7.119_composed_gld`는:
- 좋은 기업 이벤트를 core가 사고
- core가 못 산 작은 기회를 idle alpha가 보완하고
- 내부자 매수를 Form 4가 잡고
- 큰손 지분 확대를 13D가 잡고
- 위기 구간 gold를 risk-off alpha가 잡고
- 남는 현금은 parking이 계속 굴리는
`복합 이벤트-주도 알파 시스템`이다.
한마디로 가장 쉽게 말하면:
`좋은 뉴스가 나온 주식을 사고, 남는 돈도 쉬지 않게 계속 굴리도록 만든 전략`
이다.

@ -38,7 +38,7 @@ def _resolve_trade_sleeve(trade: FilledTrade, candidate_map: dict[str, Any] | No
if candidate is not None: if candidate is not None:
features = getattr(candidate, "features", {}) or {} features = getattr(candidate, "features", {}) or {}
sleeve = str(features.get("trade_sleeve") or "").strip().lower() sleeve = str(features.get("trade_sleeve") or "").strip().lower()
if sleeve in {"core", "idle_alpha", "ownership", "parking"}: if sleeve in {"core", "idle_alpha", "form4", "ownership", "risk_off_alpha", "parking"}:
return sleeve return sleeve
return "core" return "core"
@ -283,6 +283,7 @@ def write_position_timeline(
"symbol": p.plan.candidate.symbol, "symbol": p.plan.candidate.symbol,
"source_symbol": p.plan.candidate.source_symbol, "source_symbol": p.plan.candidate.source_symbol,
"event_date": p.plan.event_date.isoformat() if p.plan.event_date else None, "event_date": p.plan.event_date.isoformat() if p.plan.event_date else None,
"trade_sleeve": str((p.plan.candidate.features or {}).get("trade_sleeve") or "core"),
"timing_class": p.plan.timing_class, "timing_class": p.plan.timing_class,
"engine_id": p.plan.engine_id, "engine_id": p.plan.engine_id,
"entry_timing_policy": p.plan.entry_timing_policy, "entry_timing_policy": p.plan.entry_timing_policy,
@ -522,7 +523,9 @@ def write_sleeve_decomposition(
sleeve_data: dict[str, dict[str, Any]] = { sleeve_data: dict[str, dict[str, Any]] = {
"core": {"trade_count": 0, "wins": 0, "net_pnl": 0.0}, "core": {"trade_count": 0, "wins": 0, "net_pnl": 0.0},
"idle_alpha": {"trade_count": 0, "wins": 0, "net_pnl": 0.0}, "idle_alpha": {"trade_count": 0, "wins": 0, "net_pnl": 0.0},
"form4": {"trade_count": 0, "wins": 0, "net_pnl": 0.0},
"ownership": {"trade_count": 0, "wins": 0, "net_pnl": 0.0}, "ownership": {"trade_count": 0, "wins": 0, "net_pnl": 0.0},
"risk_off_alpha": {"trade_count": 0, "wins": 0, "net_pnl": 0.0},
"parking": {"trade_count": 0, "wins": 0, "net_pnl": 0.0}, "parking": {"trade_count": 0, "wins": 0, "net_pnl": 0.0},
} }
for t in trades: for t in trades:
@ -598,6 +601,252 @@ def write_sleeve_decomposition(
return out return out
def write_non_core_allocator_shadow_candidates(
run_dir: Path,
rows: list[dict[str, Any]],
) -> Path | None:
"""Write artifacts/non_core_allocator_shadow_candidates.parquet."""
if not rows:
return None
out = run_dir / "artifacts" / "non_core_allocator_shadow_candidates.parquet"
_write_parquet(rows, out)
return out
def write_non_core_allocator_shadow_summary(
run_dir: Path,
rows: list[dict[str, Any]],
) -> Path | None:
"""Write metrics/non_core_allocator_shadow.json."""
if not rows:
return None
per_sleeve: dict[str, dict[str, Any]] = defaultdict(
lambda: {
"candidate_count": 0,
"complete_count": 0,
"shadow_selected_count": 0,
"live_selected_count": 0,
"disagreement_count": 0,
"marginal_score_sum": 0.0,
"shadow_selected_cash": 0.0,
}
)
parking_distribution: dict[str, int] = defaultdict(int)
decision_distribution: dict[str, int] = defaultdict(int)
complete_total = 0
disagreement_total = 0
for row in rows:
sleeve = str(row.get("allocator_v2_family") or "unknown")
d = per_sleeve[sleeve]
d["candidate_count"] += 1
complete = bool(row.get("allocator_v2_complete"))
if complete:
d["complete_count"] += 1
complete_total += 1
marginal_score = float(row.get("allocator_v2_marginal_score") or 0.0)
d["marginal_score_sum"] += marginal_score
shadow_selected = bool(row.get("allocator_v2_shadow_selected"))
live_selected = bool(row.get("allocator_v2_live_selected"))
if shadow_selected:
d["shadow_selected_count"] += 1
d["shadow_selected_cash"] += float(row.get("allocator_v2_requested_cash_est") or 0.0)
if live_selected:
d["live_selected_count"] += 1
if bool(row.get("allocator_v2_live_vs_shadow_disagree")):
d["disagreement_count"] += 1
disagreement_total += 1
parking_symbol = str(row.get("allocator_v2_parking_symbol") or "").strip().lower()
if parking_symbol:
parking_distribution[parking_symbol] += 1
decision = str(row.get("allocator_v2_shadow_decision") or "unknown")
decision_distribution[decision] += 1
total_candidates = len(rows)
summary = {
"candidate_count": total_candidates,
"complete_count": complete_total,
"complete_pct": round(complete_total / total_candidates * 100.0, 2) if total_candidates else None,
"live_vs_shadow_disagreement_count": disagreement_total,
"parking_symbol_distribution": dict(sorted(parking_distribution.items())),
"shadow_decision_distribution": dict(sorted(decision_distribution.items())),
"sleeves": {},
}
for sleeve, d in sorted(per_sleeve.items()):
count = int(d["candidate_count"])
summary["sleeves"][sleeve] = {
"candidate_count": count,
"complete_count": int(d["complete_count"]),
"complete_pct": round(int(d["complete_count"]) / count * 100.0, 2) if count else None,
"shadow_selected_count": int(d["shadow_selected_count"]),
"live_selected_count": int(d["live_selected_count"]),
"disagreement_count": int(d["disagreement_count"]),
"avg_marginal_score": round(float(d["marginal_score_sum"]) / count, 6) if count else None,
"shadow_selected_cash": round(float(d["shadow_selected_cash"]), 4),
}
out = run_dir / "metrics" / "non_core_allocator_shadow.json"
out.write_text(json.dumps(summary, indent=2))
return out
def write_non_core_allocator_shadow_report(
run_dir: Path,
rows: list[dict[str, Any]],
) -> Path | None:
"""Write notes/non_core_allocator_shadow_report.md."""
if not rows:
return None
summary_path = write_non_core_allocator_shadow_summary(run_dir, rows)
summary: dict[str, Any] = {}
if summary_path and summary_path.exists():
summary = json.loads(summary_path.read_text())
def _fmt_pct(value: Any) -> str:
if value is None:
return "n/a"
return f"{float(value):.2f}%"
def _fmt_num(value: Any, digits: int = 2) -> str:
if value is None:
return "n/a"
return f"{float(value):.{digits}f}"
def _fmt_cash(value: Any) -> str:
if value is None:
return "n/a"
return f"${float(value):,.2f}"
lines: list[str] = [
"# Non-Core Allocator Shadow Report",
"",
"## Summary",
f"- Candidates: {summary.get('candidate_count', len(rows))}",
f"- Complete scoring: {_fmt_pct(summary.get('complete_pct'))}",
f"- Live vs shadow disagreements: {summary.get('live_vs_shadow_disagreement_count', 0)}",
"",
"## Parking Context",
]
parking_distribution = summary.get("parking_symbol_distribution", {}) or {}
if parking_distribution:
for symbol, count in sorted(parking_distribution.items()):
lines.append(f"- `{symbol}`: {count}")
else:
lines.append("- No parking benchmark observations recorded.")
lines.extend(["", "## Sleeve Summary"])
sleeve_summary = summary.get("sleeves", {}) or {}
if sleeve_summary:
lines.extend(
[
"",
"| Sleeve | Candidates | Complete % | Shadow Selected | Live Selected | Disagreements | Avg Score | Shadow Cash |",
"| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
]
)
for sleeve, data in sorted(sleeve_summary.items()):
lines.append(
"| {sleeve} | {candidate_count} | {complete_pct} | {shadow_selected_count} | "
"{live_selected_count} | {disagreement_count} | {avg_score} | {shadow_cash} |".format(
sleeve=sleeve,
candidate_count=int(data.get("candidate_count", 0)),
complete_pct=_fmt_pct(data.get("complete_pct")),
shadow_selected_count=int(data.get("shadow_selected_count", 0)),
live_selected_count=int(data.get("live_selected_count", 0)),
disagreement_count=int(data.get("disagreement_count", 0)),
avg_score=_fmt_num(data.get("avg_marginal_score"), 4),
shadow_cash=_fmt_cash(data.get("shadow_selected_cash")),
)
)
else:
lines.append("")
lines.append("- No per-sleeve summary available.")
def _candidate_sort_key(row: dict[str, Any]) -> tuple[float, float, str, str]:
return (
-float(row.get("allocator_v2_marginal_score") or 0.0),
-float(row.get("allocator_v2_requested_cash_est") or 0.0),
str(row.get("date") or ""),
str(row.get("symbol") or ""),
)
shadow_only = sorted(
[
row for row in rows
if bool(row.get("allocator_v2_shadow_selected"))
and not bool(row.get("allocator_v2_live_selected"))
],
key=_candidate_sort_key,
)
live_only = sorted(
[
row for row in rows
if bool(row.get("allocator_v2_live_selected"))
and not bool(row.get("allocator_v2_shadow_selected"))
],
key=_candidate_sort_key,
)
blocked_budget = sorted(
[row for row in rows if str(row.get("allocator_v2_shadow_decision") or "") == "blocked_by_budget"],
key=_candidate_sort_key,
)
blocked_better = sorted(
[row for row in rows if str(row.get("allocator_v2_shadow_decision") or "") == "blocked_by_better_opportunity"],
key=_candidate_sort_key,
)
def _append_candidate_section(title: str, candidate_rows: list[dict[str, Any]], *, limit: int = 15) -> None:
lines.extend(["", f"## {title}"])
if not candidate_rows:
lines.append("")
lines.append("- None.")
return
lines.extend(
[
"",
"| Date | Sleeve | Symbol | Engine | Score | Cash Est | Parking | Decision |",
"| --- | --- | --- | --- | ---: | ---: | --- | --- |",
]
)
for row in candidate_rows[:limit]:
lines.append(
"| {date} | {sleeve} | `{symbol}` | `{engine}` | {score} | {cash} | `{parking}` | `{decision}` |".format(
date=str(row.get("date") or ""),
sleeve=str(row.get("allocator_v2_family") or ""),
symbol=str(row.get("symbol") or ""),
engine=str(row.get("engine_id") or ""),
score=_fmt_num(row.get("allocator_v2_marginal_score"), 4),
cash=_fmt_cash(row.get("allocator_v2_requested_cash_est")),
parking=str(row.get("allocator_v2_parking_symbol") or ""),
decision=str(row.get("allocator_v2_shadow_decision") or ""),
)
)
_append_candidate_section("Top Shadow-Selected But Live-Skipped", shadow_only)
_append_candidate_section("Top Live-Selected But Shadow-Skipped", live_only)
_append_candidate_section("Top Budget-Blocked Candidates", blocked_budget, limit=10)
_append_candidate_section("Top Better-Opportunity Blocks", blocked_better, limit=10)
disagreement_by_date: dict[str, int] = defaultdict(int)
for row in rows:
if bool(row.get("allocator_v2_live_vs_shadow_disagree")):
disagreement_by_date[str(row.get("date") or "")] += 1
lines.extend(["", "## Disagreement Hotspots"])
if disagreement_by_date:
lines.extend(["", "| Date | Disagreements |", "| --- | ---: |"])
for date, count in sorted(disagreement_by_date.items(), key=lambda item: (-item[1], item[0]))[:15]:
lines.append(f"| {date} | {count} |")
else:
lines.append("")
lines.append("- No disagreement dates.")
out = run_dir / "notes" / "non_core_allocator_shadow_report.md"
out.write_text("\n".join(lines) + "\n")
return out
def write_all_artifacts( def write_all_artifacts(
run_dir: Path, run_dir: Path,
run_id: str, run_id: str,
@ -616,6 +865,7 @@ def write_all_artifacts(
total_orders_rejected: int, total_orders_rejected: int,
split_name: str | None = None, split_name: str | None = None,
per_engine_metrics: dict[str, dict[str, Any]] | None = None, per_engine_metrics: dict[str, dict[str, Any]] | None = None,
non_core_allocator_shadow_rows: list[dict[str, Any]] | None = None,
) -> dict[str, str]: ) -> dict[str, str]:
"""Write all output files. Returns mapping of artifact_name → file_path.""" """Write all output files. Returns mapping of artifact_name → file_path."""
from libs.backtest.manifests import save_manifest, save_resolved_config from libs.backtest.manifests import save_manifest, save_resolved_config
@ -674,6 +924,16 @@ def write_all_artifacts(
paths["sleeve_decomposition"] = str( paths["sleeve_decomposition"] = str(
write_sleeve_decomposition(run_dir, trades, equity_curve, candidate_map, config) write_sleeve_decomposition(run_dir, trades, equity_curve, candidate_map, config)
) )
if non_core_allocator_shadow_rows:
p = write_non_core_allocator_shadow_candidates(run_dir, non_core_allocator_shadow_rows)
if p:
paths["non_core_allocator_shadow_candidates"] = str(p)
p = write_non_core_allocator_shadow_summary(run_dir, non_core_allocator_shadow_rows)
if p:
paths["non_core_allocator_shadow"] = str(p)
p = write_non_core_allocator_shadow_report(run_dir, non_core_allocator_shadow_rows)
if p:
paths["non_core_allocator_shadow_report"] = str(p)
# Plots (no-op) # Plots (no-op)
paths["plots_dir"] = str(write_plots(run_dir, config.reporting.generate_plots)) paths["plots_dir"] = str(write_plots(run_dir, config.reporting.generate_plots))

File diff suppressed because it is too large Load Diff

@ -0,0 +1,127 @@
"""Helpers for shadow-only non-core allocator v2 diagnostics."""
from __future__ import annotations
import math
def compute_native_rank_pct(rank_index: int, total: int) -> float:
if total <= 1:
return 1.0
clamped_index = max(0, min(rank_index, total - 1))
return max(0.0, 1.0 - (clamped_index / float(total - 1)))
def normalize_hold_days_est(hold_days_est: int | float | None) -> float:
hold_days = max(0.0, float(hold_days_est or 0.0))
return min(hold_days, 30.0) / 30.0
def normalize_liquidity_penalty(
requested_cash_est: int | float | None,
avg_dollar_volume: int | float | None,
) -> float:
requested_cash = max(0.0, float(requested_cash_est or 0.0))
adv = max(0.0, float(avg_dollar_volume or 0.0))
denom = max(adv * 0.02, 1.0)
return min(requested_cash / denom, 1.0)
def classify_parking_class(parking_symbol: str | None) -> str:
normalized = str(parking_symbol or "").strip().lower()
if normalized == "sgov":
return "cash"
if normalized == "gld":
return "gold"
if normalized in {"qqqm", "qqq", "tqqq", "spy"}:
return "equity_beta"
return "cash"
def classify_non_core_overlap_class(
family: str,
*,
trade_symbol_mode: str | None = None,
engine_id: str | None = None,
symbol: str | None = None,
) -> str:
normalized_family = str(family or "").strip().lower()
normalized_engine = str(engine_id or "").strip().lower()
normalized_symbol = str(symbol or "").strip().lower()
normalized_mode = str(trade_symbol_mode or "").strip().lower()
if normalized_family == "risk_off_alpha" and normalized_symbol == "gld":
return "gold"
if normalized_family == "idle_alpha" and (
normalized_engine == "idle_macro_breadth_smh_postalloc"
or normalized_mode == "sector_etf"
or normalized_symbol == "smh"
):
return "equity_beta_high"
if normalized_family in {"idle_alpha", "form4", "ownership"}:
return "equity_beta_med"
if normalized_family == "risk_off_alpha":
return "gold" if normalized_symbol == "gld" else "cash"
return "cash"
def compute_overlap_penalty(candidate_class: str, parking_class: str) -> float:
candidate = str(candidate_class or "").strip().lower()
parking = str(parking_class or "").strip().lower()
if parking == "cash" or candidate == "cash":
return 0.0
if candidate == "gold" and parking == "gold":
return 1.0
if candidate == "equity_beta_high" and parking == "equity_beta":
return 1.0
if candidate == "equity_beta_med" and parking == "equity_beta":
return 0.6
return 0.0
def normalize_parking_proxy(
parking_symbol: str | None,
*,
parking_momentum_20: int | float | None,
hold_days_est: int | float | None,
sgov_annual_rate: int | float | None,
) -> float:
normalized_symbol = str(parking_symbol or "").strip().lower()
hold_days = max(0.0, float(hold_days_est or 0.0))
if normalized_symbol == "sgov":
annual_rate = max(0.0, float(sgov_annual_rate or 0.0))
return min(((annual_rate * hold_days / 252.0) / 0.02), 1.0)
momentum = max(0.0, float(parking_momentum_20 or 0.0))
scaled_momentum = momentum * (min(hold_days, 20.0) / 20.0)
return min(scaled_momentum / 0.10, 1.0)
def compute_marginal_score(
*,
native_rank_pct: int | float | None,
hold_norm: int | float | None,
liquidity_penalty_norm: int | float | None,
overlap_penalty_norm: int | float | None,
parking_proxy_norm: int | float | None,
native_strength: int | float = 1.0,
hold_penalty: int | float = 0.20,
liquidity_penalty: int | float = 0.25,
overlap_penalty: int | float = 0.20,
parking_opportunity_penalty: int | float = 0.35,
) -> float:
values = (
float(native_rank_pct or 0.0),
float(hold_norm or 0.0),
float(liquidity_penalty_norm or 0.0),
float(overlap_penalty_norm or 0.0),
float(parking_proxy_norm or 0.0),
)
if any(math.isnan(value) or math.isinf(value) for value in values):
return 0.0
return (
float(native_strength) * float(native_rank_pct or 0.0)
- float(hold_penalty) * float(hold_norm or 0.0)
- float(liquidity_penalty) * float(liquidity_penalty_norm or 0.0)
- float(overlap_penalty) * float(overlap_penalty_norm or 0.0)
- float(parking_opportunity_penalty) * float(parking_proxy_norm or 0.0)
)

@ -80,7 +80,7 @@ _MACRO_TRADE_BAR_PREFIXES: tuple[tuple[str, str], ...] = (
("MERIX", "merix"), ("MERIX", "merix"),
) )
_PRICE_FEATURE_WARMUP_DAYS = 120 _PRICE_FEATURE_WARMUP_DAYS = 120
_RUNTIME_CACHE_VERSION = 11 _RUNTIME_CACHE_VERSION = 12
_RUNTIME_CACHE_WAIT_TIMEOUT_SECONDS = 1800.0 _RUNTIME_CACHE_WAIT_TIMEOUT_SECONDS = 1800.0
_RUNTIME_CACHE_WAIT_LOG_INTERVAL_SECONDS = 5.0 _RUNTIME_CACHE_WAIT_LOG_INTERVAL_SECONDS = 5.0
@ -1889,9 +1889,9 @@ class SnapshotStore:
result: dict[dt.date, dict[str, Any]] = {} result: dict[dt.date, dict[str, Any]] = {}
# Lookback periods for rolling high / momentum / volatility # Lookback periods for rolling high / momentum / volatility
rolling_high_periods = (20, 50, 100) rolling_high_periods = (5, 20, 50, 100)
momentum_periods = (5, 10, 20, 50) momentum_periods = (5, 10, 20, 50)
vol_periods = (15, 20, 30, 50) vol_periods = (5, 15, 20, 30, 50)
efficiency_periods = (10, 20) efficiency_periods = (10, 20)
downside_vol_periods = (10, 20) downside_vol_periods = (10, 20)
ulcer_periods = (10, 20) ulcer_periods = (10, 20)

@ -0,0 +1,189 @@
from __future__ import annotations
import datetime as dt
from zoneinfo import ZoneInfo
import pytest
from libs.backtest.artifacts import (
_resolve_trade_sleeve,
create_run_directory,
write_non_core_allocator_shadow_report,
)
from libs.backtest.domain import BacktestConfig, Candidate, ExitReason, FilledTrade
from libs.backtest.non_core_allocator import (
classify_non_core_overlap_class,
classify_parking_class,
compute_marginal_score,
compute_native_rank_pct,
compute_overlap_penalty,
normalize_hold_days_est,
normalize_liquidity_penalty,
normalize_parking_proxy,
)
_UTC = ZoneInfo("UTC")
_NOW = dt.datetime(2026, 1, 5, 21, 0, tzinfo=_UTC)
_TODAY = dt.date(2026, 1, 5)
_TOMORROW = dt.date(2026, 1, 6)
def _make_candidate(**kwargs) -> Candidate:
defaults = dict(
event_id="EVT::TEST",
symbol="AAPL",
score=0.8,
sector="Technology",
event_type="earnings",
event_timestamp=_NOW,
filing_time_bucket="post_market",
reaction_date=_TODAY,
execution_date=_TOMORROW,
entry_price_est=100.0,
avg_dollar_volume=5_000_000.0,
score_bucket="high",
features={},
)
defaults.update(kwargs)
return Candidate(**defaults)
def _make_trade(**kwargs) -> FilledTrade:
defaults = dict(
trade_id="t1",
position_id="p1",
event_id="EVT::TEST",
symbol="AAPL",
entry_date=_TODAY,
exit_date=_TOMORROW,
entry_price=100.0,
exit_price=101.0,
exit_reason=ExitReason.TARGET,
shares=10,
commission=0.1,
slippage_bps=10.0,
gross_pnl=10.0,
net_pnl=9.9,
pnl_pct=0.01,
r_multiple=1.0,
holding_days=1,
)
defaults.update(kwargs)
return FilledTrade(**defaults)
def test_non_core_allocator_config_defaults():
cfg = BacktestConfig(strategy_name="test", dataset_snapshot_id="snap_001")
assert cfg.non_core_allocator_v2.enabled is False
assert cfg.non_core_allocator_v2.mode == "shadow"
assert cfg.non_core_allocator_v2.scope == "non_core"
assert cfg.non_core_allocator_v2.benchmark_mode == "current_effective_parking"
assert cfg.non_core_allocator_v2.weights.parking_opportunity_penalty == pytest.approx(0.35)
def test_trade_sleeve_resolves_form4_and_risk_off():
form4_trade = _make_trade(trade_id="f4")
risk_trade = _make_trade(trade_id="ro")
candidate_map = {
"f4": _make_candidate(features={"trade_sleeve": "form4"}),
"ro": _make_candidate(features={"trade_sleeve": "risk_off_alpha"}),
}
assert _resolve_trade_sleeve(form4_trade, candidate_map) == "form4"
assert _resolve_trade_sleeve(risk_trade, candidate_map) == "risk_off_alpha"
def test_parking_and_overlap_classification():
assert classify_parking_class("SGOV") == "cash"
assert classify_parking_class("GLD") == "gold"
assert classify_parking_class("QQQM") == "equity_beta"
assert classify_non_core_overlap_class("idle_alpha", engine_id="idle_macro_breadth_smh_postalloc") == "equity_beta_high"
assert classify_non_core_overlap_class("form4", symbol="AAPL") == "equity_beta_med"
assert classify_non_core_overlap_class("risk_off_alpha", symbol="GLD") == "gold"
def test_rank_hold_liquidity_and_parking_proxy_normalization():
assert compute_native_rank_pct(0, 1) == pytest.approx(1.0)
assert compute_native_rank_pct(1, 3) == pytest.approx(0.5)
assert normalize_hold_days_est(45) == pytest.approx(1.0)
assert normalize_hold_days_est(15) == pytest.approx(0.5)
assert normalize_liquidity_penalty(50_000, 5_000_000) == pytest.approx(0.5)
assert normalize_parking_proxy("sgov", parking_momentum_20=0.0, hold_days_est=20, sgov_annual_rate=0.05) == pytest.approx((0.05 * 20 / 252) / 0.02)
assert normalize_parking_proxy("gld", parking_momentum_20=0.12, hold_days_est=20, sgov_annual_rate=0.05) == pytest.approx(1.0)
def test_overlap_penalty_matrix():
assert compute_overlap_penalty("gold", "gold") == pytest.approx(1.0)
assert compute_overlap_penalty("equity_beta_high", "equity_beta") == pytest.approx(1.0)
assert compute_overlap_penalty("equity_beta_med", "equity_beta") == pytest.approx(0.6)
assert compute_overlap_penalty("equity_beta_med", "cash") == pytest.approx(0.0)
def test_marginal_score_formula_matches_plan():
score = compute_marginal_score(
native_rank_pct=1.0,
hold_norm=0.5,
liquidity_penalty_norm=0.25,
overlap_penalty_norm=0.6,
parking_proxy_norm=0.4,
)
expected = 1.0 - 0.20 * 0.5 - 0.25 * 0.25 - 0.20 * 0.6 - 0.35 * 0.4
assert score == pytest.approx(expected)
def test_shadow_report_writer_outputs_human_readable_report(tmp_path):
run_dir = create_run_directory(tmp_path, "allocator-shadow-test")
rows = [
{
"date": "2026-01-05",
"engine_id": "idle_macro_breadth_smh_postalloc",
"event_id": "EVT::IDLE",
"symbol": "SMH",
"allocator_v2_family": "idle_alpha",
"allocator_v2_marginal_score": 0.91,
"allocator_v2_requested_cash_est": 125000.0,
"allocator_v2_parking_symbol": "sgov",
"allocator_v2_shadow_selected": True,
"allocator_v2_live_selected": True,
"allocator_v2_live_vs_shadow_disagree": False,
"allocator_v2_complete": True,
"allocator_v2_shadow_decision": "selected_shadow_v2",
},
{
"date": "2026-01-06",
"engine_id": "idle_form4_capture",
"event_id": "EVT::F4",
"symbol": "MSFT",
"allocator_v2_family": "form4",
"allocator_v2_marginal_score": 0.72,
"allocator_v2_requested_cash_est": 80000.0,
"allocator_v2_parking_symbol": "qqqm",
"allocator_v2_shadow_selected": True,
"allocator_v2_live_selected": False,
"allocator_v2_live_vs_shadow_disagree": True,
"allocator_v2_complete": True,
"allocator_v2_shadow_decision": "selected_shadow_v2",
},
{
"date": "2026-01-06",
"engine_id": "idle_form4_capture",
"event_id": "EVT::F4B",
"symbol": "NVDA",
"allocator_v2_family": "form4",
"allocator_v2_marginal_score": 0.55,
"allocator_v2_requested_cash_est": 65000.0,
"allocator_v2_parking_symbol": "qqqm",
"allocator_v2_shadow_selected": False,
"allocator_v2_live_selected": False,
"allocator_v2_live_vs_shadow_disagree": False,
"allocator_v2_complete": True,
"allocator_v2_shadow_decision": "blocked_by_better_opportunity",
},
]
report_path = write_non_core_allocator_shadow_report(run_dir, rows)
assert report_path is not None
text = report_path.read_text()
assert "# Non-Core Allocator Shadow Report" in text
assert "## Sleeve Summary" in text
assert "## Top Shadow-Selected But Live-Skipped" in text
assert "## Top Better-Opportunity Blocks" in text
assert "`MSFT`" in text
Loading…
Cancel
Save