feat: agent-reviewed overnight_recovery_agent_top10; guard monitor hot-reload

overnight_recovery_agent_top10 (strategies/agent_review.py) re-ranks the
overnight_recovery top 50 with an LLM agent's daily scores:
final = quant percentile + 0.25 x score (-2..+2), vetoes for five event types,
top 10 on the base strategy's rebalance rows and hysteresis. Score files are
stamped by `agent_review.py submit` and steer a session only if stamped before
its 16:00 New York close; without one the strategy holds exactly what
overnight_recovery_top10 holds (checked on the live panel 2016-2026).
- agent_review.py: candidates / submit / evaluate; skill
  .claude/skills/agent-stock-review drives the daily research.
- US only, skipped by precompute (no history before its first score file);
  runs in the original book and in selected_2026_09 (from the book's start).
- .gitignore tracks .claude/skills/ and ignores data/agent/.

Monitor fix: hot-reload re-imports trader.py but not strategies/*. After the
2026-09-29 strategy commits the stale modules made every phase's panel refresh
fail ("Unknown signal 'low_risk'") and US books skipped 09-29 until a restart.
_reload_strategies now builds every factory and keeps the old registry when
any fails; CLAUDE.md says to restart the monitor after changing strategy code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 20:25:20 +08:00
parent ab3acfe4cb
commit 761725d2c8
9 changed files with 676 additions and 9 deletions

View File

@@ -42,6 +42,7 @@ import yfinance as yf
import data_manager
import universe_history
from strategies.base import BarStrategy
from strategies.agent_review import AgentReviewStrategy
from strategies.buy_and_hold import BuyAndHoldStrategy
from strategies.dual_momentum import DualMomentumStrategy
from strategies.factor_combo import FactorComboStrategy
@@ -129,7 +130,9 @@ LIVE_BOOKS: dict[str, dict] = {
# strategies from the 2026-09 PIT research, and the trend_rider ETF
# strategies (lost 7-12% in Apr-Sep, 10-year Sharpe 1.0-1.4): v7_vt24
# and v5_vt30 (best 10-year Sharpe of their version) and v7_vt36 (the
# V7+VT36 champion).
# V7+VT36 champion). overnight_recovery_agent_top10 was added on
# 2026-09-30 at the user's request; it starts with the book, and until
# its first agent score file it holds what overnight_recovery_top10 holds.
"strategies": {
"us": [
"fc_up_cap_quality_mom_biweekly", "fc_up_cap_quality_mom_weekly",
@@ -141,7 +144,7 @@ LIVE_BOOKS: dict[str, dict] = {
"fc_ma200_mom7m_rec126_monthly", "fc_ma200_mom7m_rec126_biweekly",
"fc_rec_mfilt_ma200_monthly", "fc_rec_mfilt_ma200_biweekly",
"hybrid_alpha_top10", "recovery_mom_top10",
"overnight_recovery_top10",
"overnight_recovery_top10", "overnight_recovery_agent_top10",
"trend_rider_v7_vt24", "trend_rider_v7_vt36", "trend_rider_v5_vt30",
],
"cn": [
@@ -301,6 +304,9 @@ STRATEGY_REGISTRY = {
"us_three_sleeve": lambda **kw: us_three_sleeve(),
# half overnight_recovery_top10, half QQQ 200-day trend with a 30% vol target (TQQQ + SHY)
"us_momentum_qqq_trend": lambda **kw: us_momentum_qqq_trend(),
# overnight_recovery's top 50, re-ranked with an LLM agent's daily scores
# (strategies/agent_review.py, agent_review.py); live from 2026-09-29, no backtest of its own
"overnight_recovery_agent_top10": lambda **kw: AgentReviewStrategy(),
# --- TrendRiderV6: stock-picking + V5 regime engine ---
"trend_rider_v6": lambda **kw: TrendRiderV6(),
"trend_rider_v6_top10": lambda **kw: TrendRiderV6(top_n=10),
@@ -334,6 +340,11 @@ MIXED_STRATEGY_EXTRA_TICKERS = {
"us_momentum_qqq_trend": MOMENTUM_QQQ_TREND_ETFS,
}
# Strategies steered by agent score files (data/agent/us/). US only; before
# their first score file they hold what their base strategy holds, so they
# have no backtest of their own and precompute_backtest.py skips them.
AGENT_REVIEW_STRATEGIES = {"overnight_recovery_agent_top10"}
# Registered strategies the live monitor does not run, with the reason.
LIVE_EXCLUDED_STRATEGIES = {
"buy_and_hold": "equal weight across ~500 names is not investable with whole "
@@ -359,7 +370,8 @@ def strategy_fields(strategy_name: str) -> tuple[str, ...]:
def strategy_supports_market(strategy_name: str, market: str) -> bool:
"""ETF-based strategies trade USD-listed ETFs, so they only run in the US book."""
base_name = strategy_name.removeprefix("sim_")
if base_name in ETF_STRATEGY_UNIVERSES or base_name in MIXED_STRATEGY_EXTRA_TICKERS:
if base_name in ETF_STRATEGY_UNIVERSES or base_name in MIXED_STRATEGY_EXTRA_TICKERS \
or base_name in AGENT_REVIEW_STRATEGIES:
return market == "us"
return True
@@ -1770,10 +1782,23 @@ def cmd_monitor(args):
return min(all_candidates, key=lambda x: x[0])
def _reload_strategies():
"""Re-read STRATEGY_REGISTRY to pick up new strategies without restart."""
"""Re-read STRATEGY_REGISTRY to pick up new strategies without restart.
Only trader.py is reloaded; strategies/* modules stay as first
imported. A registry whose entries need newer strategy code would
fail at every phase (2026-09-29: one stale signal name broke the panel
refresh for every book), so every factory is built first and the old
registry is kept if any fails; changed strategy code needs a restart.
"""
import importlib
import trader as _self_mod
importlib.reload(_self_mod)
for name, factory in _self_mod.STRATEGY_REGISTRY.items():
try:
factory()
except Exception as exc:
raise RuntimeError(f"{name} needs strategy code this process has not loaded "
f"({exc}); restart the monitor") from exc
current = list(_self_mod.DEFAULT_MONITOR_STRATEGIES)
return current, _self_mod.STRATEGY_REGISTRY