Files
ai-exchange/tests/unit/test_decision_packet_service.py
T
2026-05-08 14:39:44 +08:00

106 lines
4.4 KiB
Python

from datetime import datetime, timezone
import os
import unittest
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://user:pass@localhost:5432/ai_ict_test")
from src.services.decision.decision_packet_service import (
_select_candidate,
build_alert_candidates,
build_decision_packet_from_rows,
)
class DecisionPacketServiceTests(unittest.TestCase):
def test_build_packet_recommends_confirm_ticket_when_risk_passes(self) -> None:
packet = build_decision_packet_from_rows(
instrument_id=1,
observed_ts=datetime(2026, 5, 8, 1, 0, tzinfo=timezone.utc),
plan={
"id": 10,
"status": "active",
"narrative": "active:bullish",
"primary_draw": {"source": "liquidity_pool", "price_high": 101},
"bias": "bullish",
"invalidations": [{"code": "range_low_lost"}],
},
candidate={
"id": 20,
"trading_plan_id": 10,
"model_code": "OSOK_DAILY_RANGE_SWEEP_MSS_FVG_RETRACE",
"status": "executable",
"entry_low": 100,
"entry_high": 101,
"stop_loss": 99,
"tp1": 102,
"tp2": 104,
"quality_score": 0.8,
"confirmation_state": {"present_conditions": ["sweep", "mss", "fvg"], "reason_codes": ["confirmed"]},
"missing_confirmations": [],
"management_rules": [{"rule": "tp1_partial"}],
"entry": {"target_plan": {"tp1": {"source": "irl"}, "tp2": {"source": "erl"}}},
},
signal={"id": 30, "entry_type": "limit", "target_plan": {"tp1": {"source": "irl"}, "tp2": {"source": "erl"}}},
ticket={"id": 40, "status": "awaiting_confirmation"},
risk_result={"allowed": True, "status": "allowed", "reasons": []},
)
self.assertEqual(packet.status, "actionable")
self.assertEqual(packet.action_recommendation, "confirm_ticket")
self.assertEqual(packet.targets["tp1"]["source"], "irl")
self.assertIn("risk_governor_passed", packet.why_allowed)
def test_build_packet_explains_missing_confirmations(self) -> None:
packet = build_decision_packet_from_rows(
instrument_id=1,
observed_ts=datetime(2026, 5, 8, 1, 0, tzinfo=timezone.utc),
plan={"id": 10, "status": "active", "bias": "bearish"},
candidate={
"id": 20,
"trading_plan_id": 10,
"status": "blocked",
"missing_confirmations": ["mss_confirmed"],
"confirmation_state": {"rejected_conditions": ["fvg_missing"]},
},
signal={},
ticket={},
risk_result={"allowed": None, "status": "signal_missing", "reasons": ["signal_missing"]},
)
self.assertEqual(packet.status, "blocked")
self.assertEqual(packet.action_recommendation, "wait_for_confirmations")
self.assertIn("mss_confirmed", packet.why_blocked)
self.assertIn("fvg_missing", packet.why_blocked)
self.assertIn("signal_missing", packet.why_blocked)
def test_alert_candidates_are_stable_for_actionable_packet(self) -> None:
packet = build_decision_packet_from_rows(
instrument_id=1,
observed_ts=datetime(2026, 5, 8, 1, 0, tzinfo=timezone.utc),
plan={"id": 10, "status": "active"},
candidate={"id": 20, "trading_plan_id": 10, "status": "executable", "missing_confirmations": []},
signal={"id": 30},
ticket={"id": 40, "status": "awaiting_confirmation"},
risk_result={"allowed": True, "status": "allowed", "reasons": []},
)
alerts = build_alert_candidates(packet)
self.assertEqual(alerts[0]["alert_type"], "candidate_actionable")
self.assertIn("confirm_ticket", alerts[0]["alert_key"])
def test_select_candidate_prefers_active_plan_scope(self) -> None:
selected = _select_candidate(
[
{"id": 1, "trading_plan_id": 9, "model_code": "OTHER"},
{"id": 2, "trading_plan_id": 10, "model_code": "OSOK_DAILY_RANGE_SWEEP_MSS_FVG_RETRACE"},
],
plan_id=10,
model_code="OSOK_DAILY_RANGE_SWEEP_MSS_FVG_RETRACE",
)
self.assertEqual(selected["id"], 2)
if __name__ == "__main__":
unittest.main()