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:
76
tests/test_agent_review.py
Normal file
76
tests/test_agent_review.py
Normal 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()
|
||||
Reference in New Issue
Block a user