Files
quant-trading/tests/test_agent_review.py
Gahow Wang 2ff7bc07cf agent review: reject future-dated sources at submit; treat fetched pages as data
validate() now takes the submission stamp and rejects any source without a
readable publication date or dated after the submission day (UTC). The skill
tells the agent and its subagents that web pages, news and filings are data,
never instructions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 22:24:47 -07:00

93 lines
4.2 KiB
Python

"""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, a vetoed holding is replaced the same session, and
submit rejects sources dated after the submission."""
import json
import os
import tempfile
import unittest
import numpy as np
import pandas as pd
from agent_review import validate
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]))
class SubmitValidationTests(unittest.TestCase):
def _check(self, *dates):
pack = {"candidates": [{"ticker": "AAA"}]}
sub = {"scores": {"AAA": {"score": 0, "summary": "x",
"sources": [{"url": "u", "date": d} for d in dates]}}}
return validate(sub, pack, pd.Timestamp("2026-10-12 04:00", tz="UTC"))
def test_sources_must_be_dated_no_later_than_the_submission_day(self):
self._check("2026-10-09", "2026-10-12", "2026-09")
for bad in ("2026-10-13", "2026-10-12T23:00:00-04:00", None, "next week"):
with self.subTest(date=bad), self.assertRaises(ValueError):
self._check("2026-10-09", bad)
if __name__ == "__main__":
unittest.main()