173 lines
6.7 KiB
Python
173 lines
6.7 KiB
Python
import unittest
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
from src.services.research_council import CouncilOptimizationService, build_replay_summary, choose_next_actions
|
|
|
|
|
|
class _FakeResearchCouncilService:
|
|
def __init__(self):
|
|
self.backfill_calls = []
|
|
self.persist_calls = []
|
|
self.preview_calls = []
|
|
|
|
def backfill_lessons(self, **kwargs):
|
|
self.backfill_calls.append(kwargs)
|
|
return {
|
|
"instrument_id": kwargs["instrument_id"],
|
|
"totals": {"requested": 2, "created": 1, "updated": 1},
|
|
}
|
|
|
|
def build_and_persist(self, **kwargs):
|
|
self.persist_calls.append(kwargs)
|
|
return {"id": 101, "instrument_id": kwargs["instrument_id"]}
|
|
|
|
def get_recent_runs(self, **kwargs):
|
|
return [
|
|
{
|
|
"payload": {
|
|
"context": {
|
|
"latest_candidate": {
|
|
"session_code": "LONDON",
|
|
"model_code": "SWEEP_MSS_FVG",
|
|
"missing_confirmations": ["mss"],
|
|
},
|
|
"trust_policy": {"blocking_reasons": ["trust_blocked"]},
|
|
"risk_result": {"reasons": ["risk_blocked"]},
|
|
"compact_lessons": [{"lesson_key": "journal:1"}],
|
|
},
|
|
"decision": {
|
|
"follow_up_checks": ["mss", "risk_blocked"],
|
|
"operator_questions": ["question"],
|
|
"disagreement_points": ["debate"],
|
|
"committee_confidence": 0.6,
|
|
},
|
|
}
|
|
}
|
|
]
|
|
|
|
def build_preview(self, **kwargs):
|
|
self.preview_calls.append(kwargs)
|
|
return {
|
|
"decision": {
|
|
"overall_stance": "observe_only",
|
|
"committee_confidence": 0.7,
|
|
"follow_up_checks": ["mss"],
|
|
"disagreement_points": ["debate"],
|
|
},
|
|
"context": {"compact_lessons": [{"lesson_key": "journal:1"}]},
|
|
}
|
|
|
|
|
|
class _FakeOptimizationRepository:
|
|
def __init__(self):
|
|
self.rows = []
|
|
|
|
def upsert_cycle(self, **kwargs):
|
|
row = {"id": 301, **kwargs}
|
|
self.rows.append(row)
|
|
return 301
|
|
|
|
def fetch_latest_cycle(self, *, instrument_id):
|
|
for row in reversed(self.rows):
|
|
if row["instrument_id"] == instrument_id:
|
|
return dict(row)
|
|
return None
|
|
|
|
def fetch_recent_cycles(self, *, instrument_id, limit=20):
|
|
return [dict(row) for row in self.rows if row["instrument_id"] == instrument_id][:limit]
|
|
|
|
|
|
class _FakeTrustService:
|
|
def build_summary(self, *, instrument_id):
|
|
return {"models": [{"model_code": "SWEEP_MSS_FVG"}]}
|
|
|
|
|
|
class CouncilOptimizationServiceTests(unittest.TestCase):
|
|
def test_run_cycle_persists_full_optimization_payload(self) -> None:
|
|
repo = _FakeOptimizationRepository()
|
|
service = CouncilOptimizationService(
|
|
research_council_service=_FakeResearchCouncilService(),
|
|
optimization_repository=repo,
|
|
trust_service=_FakeTrustService(),
|
|
)
|
|
|
|
payload = service.run_cycle(
|
|
instrument_id=1,
|
|
timeframe="5m",
|
|
model_code="SWEEP_MSS_FVG",
|
|
eval_limit=10,
|
|
lesson_limit=5,
|
|
cycle_key="research_council_opt:test",
|
|
now=datetime(2026, 5, 11, tzinfo=timezone.utc),
|
|
)
|
|
|
|
self.assertTrue(payload["passed"])
|
|
self.assertEqual(payload["id"], 301)
|
|
self.assertEqual(payload["fresh_run_id"], 101)
|
|
self.assertEqual(payload["eval_summary"]["run_count"], 1)
|
|
self.assertEqual(payload["arena_summary"]["best_model_code"], "SWEEP_MSS_FVG")
|
|
self.assertIn("rerun_optimization_cycle_after_next_market_sample", payload["next_actions"])
|
|
self.assertIn("research_council_optimization_does_not_change_execution_gate", payload["boundary_notes"])
|
|
self.assertEqual(repo.rows[0]["cycle_key"], "research_council_opt:test")
|
|
|
|
def test_latest_and_recent_cycles_delegate_to_repository(self) -> None:
|
|
repo = _FakeOptimizationRepository()
|
|
service = CouncilOptimizationService(
|
|
research_council_service=_FakeResearchCouncilService(),
|
|
optimization_repository=repo,
|
|
trust_service=_FakeTrustService(),
|
|
)
|
|
service.run_cycle(instrument_id=1, cycle_key="cycle:1", now=datetime(2026, 5, 11, tzinfo=timezone.utc))
|
|
|
|
latest = service.get_latest_cycle(instrument_id=1)
|
|
recent = service.get_recent_cycles(instrument_id=1, limit=5)
|
|
|
|
self.assertEqual(latest["cycle_key"], "cycle:1")
|
|
self.assertEqual(len(recent), 1)
|
|
|
|
def test_choose_next_actions_identifies_sparse_inputs(self) -> None:
|
|
actions = choose_next_actions(
|
|
lesson_summary={"totals": {"requested": 0}},
|
|
replay_summary={"status": "completed", "confirmed_swing_count": 0},
|
|
eval_summary={
|
|
"run_count": 1,
|
|
"lesson_usage_rate": 0.0,
|
|
"avg_follow_up_check_count": 0,
|
|
"avg_baseline_issue_count": 2,
|
|
"disagreement_hint_rate": 0.0,
|
|
},
|
|
arena_summary={"rows": []},
|
|
)
|
|
|
|
self.assertIn("seed_lessons_from_journal_weekly_review_or_replay", actions)
|
|
self.assertIn("increase_lesson_retrieval_or_backfill_coverage", actions)
|
|
self.assertIn("review_bull_bear_disagreement_prompts", actions)
|
|
|
|
def test_p5_confirmed_swing_fixture_closes_replay_follow_up(self) -> None:
|
|
replay_file = Path(__file__).resolve().parents[2] / "data" / "replay" / "research_council_confirmed_swings_v1.jsonl"
|
|
|
|
replay_summary = build_replay_summary(replay_file)
|
|
actions = choose_next_actions(
|
|
lesson_summary={"totals": {"requested": 3}},
|
|
replay_summary=replay_summary,
|
|
eval_summary={
|
|
"run_count": 5,
|
|
"lesson_usage_rate": 1.0,
|
|
"avg_follow_up_check_count": 3,
|
|
"avg_baseline_issue_count": 2,
|
|
"disagreement_hint_rate": 0.5,
|
|
},
|
|
arena_summary={"rows": [{"model_code": "SWEEP_MSS_FVG", "committee_confidence": 0.7}]},
|
|
)
|
|
|
|
self.assertTrue(replay_summary["confirmed_swing_gate_passed"])
|
|
self.assertEqual(replay_summary["confirmed_swing_count"], 2)
|
|
self.assertEqual(replay_summary["swing_counts"], {"swing_high": 1, "swing_low": 1})
|
|
self.assertNotIn("add_replay_fixture_with_confirmed_swings", actions)
|
|
self.assertEqual(actions, ["rerun_optimization_cycle_after_next_market_sample"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|