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

@@ -0,0 +1,76 @@
"""Contracts of the agent-reviewed strategy: without score files it holds
exactly what its base strategy holds, a scores file only steers sessions it
was stamped before, and a vetoed holding is replaced the same session."""
import json
import os
import tempfile
import unittest
import numpy as np
import pandas as pd
from strategies.agent_review import AgentReviewStrategy, session_deadline
from strategies.robust_momentum import RobustMomentumStrategy
from tests.test_robust_strategies import _bars
def _write(score_dir: str, as_of, created_at, scores: dict) -> None:
rows = {t: {"score": s, "veto": s == "veto", "summary": "x"} for t, s in scores.items()}
for v in rows.values():
if v["veto"]:
v["score"] = 0
path = os.path.join(score_dir, f"scores_{pd.Timestamp(as_of).date()}_{len(os.listdir(score_dir)):06d}.json")
with open(path, "w") as fh:
json.dump({"as_of": str(pd.Timestamp(as_of).date()), "created_at": pd.Timestamp(created_at).isoformat(),
"scores": rows}, fh)
class AgentReviewTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.dir = self.tmp.name
self.bars = _bars(n_days=420, n_assets=40)
self.kw = dict(top_n=5, pool=20, rebal_freq=5, buffer=0.5, warmup=260)
def tearDown(self):
self.tmp.cleanup()
def _agent(self):
return AgentReviewStrategy(score_dir=self.dir, **self.kw).generate_signals(self.bars)
def test_without_score_files_it_holds_what_the_base_strategy_holds(self):
base = RobustMomentumStrategy("overnight_recovery", top_n=5, rebal_freq=5, buffer=0.5,
warmup=260).generate_signals(self.bars)
pd.testing.assert_frame_equal(self._agent(), base, atol=1e-12)
def test_scores_stamped_after_the_session_close_do_not_steer_it(self):
dates = self.bars["close"].index
k = 260 + 5 * 20 # a rebalance row; its weights trade at dates[k + 1]
before = self._agent()
worst = before.columns[before.iloc[k + 1] == 0][0]
late = session_deadline(dates[k + 1]) + pd.Timedelta(minutes=1)
_write(self.dir, dates[k], late, {worst: 2, **{t: -2 for t in before.columns[before.iloc[k + 1] > 0]}})
pd.testing.assert_frame_equal(self._agent(), before, atol=1e-12)
_write(self.dir, dates[k], session_deadline(dates[k + 1]) - pd.Timedelta(hours=1),
{t: -2 for t in before.columns[before.iloc[k + 1] > 0]})
after = self._agent()
pd.testing.assert_frame_equal(after.iloc[:k + 1], before.iloc[:k + 1], atol=1e-12)
self.assertNotEqual(set(after.columns[after.iloc[k + 1] > 0]), set(before.columns[before.iloc[k + 1] > 0]))
def test_a_vetoed_holding_is_replaced_between_rebalances(self):
dates = self.bars["close"].index
k = 260 + 5 * 20 + 2 # not a rebalance row
before = self._agent()
held = list(before.columns[before.iloc[k + 1] > 0])
_write(self.dir, dates[k], session_deadline(dates[k + 1]) - pd.Timedelta(hours=2), {held[0]: "veto"})
after = self._agent()
now = set(after.columns[after.iloc[k + 1] > 0])
self.assertNotIn(held[0], now)
self.assertEqual(len(now), len(held))
self.assertAlmostEqual(float(after.iloc[k + 1].sum()), 1.0, places=9)
self.assertTrue(np.allclose(after.iloc[:k + 1], before.iloc[:k + 1]))
if __name__ == "__main__":
unittest.main()