106 lines
4.4 KiB
Python
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()
|