1025 lines
43 KiB
Python
1025 lines
43 KiB
Python
import contextlib
|
|
import io
|
|
import json
|
|
import os
|
|
import unittest
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
from sqlalchemy.exc import OperationalError
|
|
|
|
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://user:pass@localhost:5432/ai_ict_test")
|
|
|
|
from src.api import route_request
|
|
from scripts import run_execution, run_execution_settlement, run_offline_checks, run_pipeline
|
|
|
|
|
|
class ApiServerRoutingTests(unittest.TestCase):
|
|
def test_routes_trading_plan_generate(self) -> None:
|
|
status, payload = route_request(
|
|
"/trading/plans/generate",
|
|
{},
|
|
method="POST",
|
|
body={"instrument_id": 1, "activate": True},
|
|
dependencies={
|
|
"trading_plan_generate": lambda **kwargs: {"id": 1, "status": "active", **kwargs},
|
|
},
|
|
)
|
|
|
|
self.assertEqual(status, 200)
|
|
self.assertEqual(payload["instrument_id"], 1)
|
|
self.assertTrue(payload["activate"])
|
|
|
|
def test_routes_trading_plan_latest(self) -> None:
|
|
status, payload = route_request(
|
|
"/trading/plans/latest",
|
|
{"instrument_id": ["1"]},
|
|
dependencies={"trading_plan_latest": lambda **kwargs: {"id": 9, "status": "active"}},
|
|
)
|
|
|
|
self.assertEqual(status, 200)
|
|
self.assertEqual(payload["id"], 9)
|
|
|
|
def test_routes_unattended_harness(self) -> None:
|
|
status, payload = route_request(
|
|
"/trading/unattended-harness",
|
|
{"instrument_id": ["2"]},
|
|
dependencies={"unattended_harness": lambda **kwargs: {"status": "passed", "instrument_id": kwargs["instrument_id"]}},
|
|
)
|
|
|
|
self.assertEqual(status, 200)
|
|
self.assertEqual(payload["status"], "passed")
|
|
self.assertEqual(payload["instrument_id"], 2)
|
|
|
|
def test_routes_trading_market_state(self) -> None:
|
|
status, payload = route_request(
|
|
"/trading/market-state",
|
|
{"instrument_id": ["1"], "timeframe": ["5m"]},
|
|
dependencies={"trading_market_state": lambda **kwargs: {"snapshot_id": 1, "market_state": kwargs}},
|
|
)
|
|
|
|
self.assertEqual(status, 200)
|
|
self.assertEqual(payload["snapshot_id"], 1)
|
|
self.assertEqual(payload["market_state"]["timeframe"], "5m")
|
|
|
|
def test_routes_trading_decision_packet_and_alerts(self) -> None:
|
|
status_packet, payload_packet = route_request(
|
|
"/trading/decision-packet",
|
|
{"instrument_id": ["1"]},
|
|
dependencies={"trading_decision_packet": lambda **kwargs: {"status": "actionable", "action_recommendation": "confirm_ticket"}},
|
|
)
|
|
status_alerts, payload_alerts = route_request(
|
|
"/trading/alerts",
|
|
{"instrument_id": ["1"], "limit": ["2"]},
|
|
dependencies={"trading_decision_alerts": lambda **kwargs: [{"id": 1, "alert_type": "candidate_actionable"}]},
|
|
)
|
|
|
|
self.assertEqual(status_packet, 200)
|
|
self.assertEqual(payload_packet["action_recommendation"], "confirm_ticket")
|
|
self.assertEqual(status_alerts, 200)
|
|
self.assertEqual(payload_alerts["items"], [{"id": 1, "alert_type": "candidate_actionable"}])
|
|
|
|
def test_routes_trading_alert_quality_summary(self) -> None:
|
|
status, payload = route_request(
|
|
"/trading/alerts/quality-summary",
|
|
{"instrument_id": ["1"], "limit": ["25"], "visual_limit": ["15"]},
|
|
dependencies={
|
|
"trading_alert_quality_summary": lambda **kwargs: {
|
|
"instrument_id": kwargs["instrument_id"],
|
|
"recent_alert_count": kwargs["limit"],
|
|
"recent_visual_review_count": kwargs["visual_limit"],
|
|
}
|
|
},
|
|
)
|
|
|
|
self.assertEqual(status, 200)
|
|
self.assertEqual(payload["instrument_id"], 1)
|
|
self.assertEqual(payload["recent_alert_count"], 25)
|
|
self.assertEqual(payload["recent_visual_review_count"], 15)
|
|
|
|
def test_routes_trading_alert_operator_digest(self) -> None:
|
|
status, payload = route_request(
|
|
"/trading/alerts/operator-digest",
|
|
{"instrument_id": ["1"], "limit": ["25"], "visual_limit": ["15"], "top_limit": ["3"]},
|
|
dependencies={
|
|
"trading_alert_operator_digest": lambda **kwargs: {
|
|
"instrument_id": kwargs["instrument_id"],
|
|
"alert_limit": kwargs["limit"],
|
|
"visual_limit": kwargs["visual_limit"],
|
|
"top_limit": kwargs["top_limit"],
|
|
"review_queue": [{"queue_type": "failed_retry"}],
|
|
}
|
|
},
|
|
)
|
|
|
|
self.assertEqual(status, 200)
|
|
self.assertEqual(payload["instrument_id"], 1)
|
|
self.assertEqual(payload["alert_limit"], 25)
|
|
self.assertEqual(payload["visual_limit"], 15)
|
|
self.assertEqual(payload["top_limit"], 3)
|
|
self.assertEqual(payload["review_queue"][0]["queue_type"], "failed_retry")
|
|
|
|
def test_routes_trading_alert_digest_preferences(self) -> None:
|
|
get_status, get_payload = route_request(
|
|
"/trading/alerts/digest-preferences",
|
|
{"instrument_id": ["1"], "profile_key": ["local_operator"]},
|
|
dependencies={
|
|
"trading_alert_operator_digest_preferences": lambda **kwargs: {
|
|
"instrument_id": kwargs["instrument_id"],
|
|
"profile_key": kwargs["profile_key"],
|
|
"digest_mode": "high_value_only",
|
|
}
|
|
},
|
|
)
|
|
post_status, post_payload = route_request(
|
|
"/trading/alerts/digest-preferences",
|
|
{},
|
|
method="POST",
|
|
body={
|
|
"instrument_id": 1,
|
|
"profile_key": "local_operator",
|
|
"digest_mode": "risk_first",
|
|
"preferred_history_kind": "reminder",
|
|
"importance_threshold": 80,
|
|
},
|
|
dependencies={
|
|
"trading_alert_operator_digest_preferences_save": lambda **kwargs: {
|
|
"instrument_id": kwargs["instrument_id"],
|
|
"digest_mode": kwargs["digest_mode"],
|
|
"preferred_history_kind": kwargs["preferred_history_kind"],
|
|
"importance_threshold": kwargs["importance_threshold"],
|
|
"status": "saved",
|
|
}
|
|
},
|
|
)
|
|
|
|
self.assertEqual(get_status, 200)
|
|
self.assertEqual(get_payload["digest_mode"], "high_value_only")
|
|
self.assertEqual(post_status, 200)
|
|
self.assertEqual(post_payload["digest_mode"], "risk_first")
|
|
self.assertEqual(post_payload["importance_threshold"], 80)
|
|
|
|
def test_routes_maintenance_summary(self) -> None:
|
|
status, payload = route_request(
|
|
"/trading/maintenance-summary",
|
|
{"instrument_id": ["1"], "alert_limit": ["25"], "visual_limit": ["15"], "incident_limit": ["5"]},
|
|
dependencies={
|
|
"maintenance_summary": lambda **kwargs: {
|
|
"instrument_id": kwargs["instrument_id"],
|
|
"alert_limit": kwargs["alert_limit"],
|
|
"visual_limit": kwargs["visual_limit"],
|
|
"incident_limit": kwargs["incident_limit"],
|
|
"status": "healthy",
|
|
}
|
|
},
|
|
)
|
|
|
|
self.assertEqual(status, 200)
|
|
self.assertEqual(payload["instrument_id"], 1)
|
|
self.assertEqual(payload["alert_limit"], 25)
|
|
self.assertEqual(payload["visual_limit"], 15)
|
|
self.assertEqual(payload["incident_limit"], 5)
|
|
|
|
def test_routes_trading_alert_silent_window_status(self) -> None:
|
|
status, payload = route_request(
|
|
"/trading/alerts/silent-window-status",
|
|
{},
|
|
dependencies={"trading_alert_silent_window_status": lambda: {"active": True, "window_label": "00:00-06:00"}},
|
|
)
|
|
|
|
self.assertEqual(status, 200)
|
|
self.assertTrue(payload["active"])
|
|
self.assertEqual(payload["window_label"], "00:00-06:00")
|
|
|
|
def test_routes_trading_reconcile(self) -> None:
|
|
status, payload = route_request(
|
|
"/trading/reconcile",
|
|
{},
|
|
method="POST",
|
|
body={"instrument_id": 1},
|
|
dependencies={"trading_reconciliation_run": lambda **kwargs: {"status": "ok", "imported_execution_ids": [7]}},
|
|
)
|
|
|
|
self.assertEqual(status, 200)
|
|
self.assertEqual(payload["imported_execution_ids"], [7])
|
|
|
|
def test_routes_trading_trust_score(self) -> None:
|
|
status, payload = route_request(
|
|
"/trading/trust-score",
|
|
{"instrument_id": ["1"]},
|
|
dependencies={"trading_trust_score": lambda **kwargs: {"whitelist": ["OSOK"], "models": [{"model_code": "OSOK"}]}},
|
|
)
|
|
|
|
self.assertEqual(status, 200)
|
|
self.assertEqual(payload["whitelist"], ["OSOK"])
|
|
|
|
def test_routes_trading_session_view_operator_notes_and_review_packet(self) -> None:
|
|
session_status, session_payload = route_request(
|
|
"/trading/session-view",
|
|
{"instrument_id": ["1"], "limit": ["2"]},
|
|
dependencies={"trading_session_view": lambda **kwargs: {"items": [{"session_code": "LONDON"}]}},
|
|
)
|
|
notes_status, notes_payload = route_request(
|
|
"/trading/operator-notes",
|
|
{"instrument_id": ["1"], "limit": ["2"]},
|
|
dependencies={"trading_operator_notes": lambda **kwargs: [{"id": 1, "author": "alice"}]},
|
|
)
|
|
create_status, create_payload = route_request(
|
|
"/trading/operator-notes",
|
|
{},
|
|
method="POST",
|
|
body={"instrument_id": 1, "author": "alice", "body": "note"},
|
|
dependencies={"trading_operator_note_create": lambda **kwargs: {"id": 2, **kwargs}},
|
|
)
|
|
packet_status, packet_payload = route_request(
|
|
"/trading/review-packet",
|
|
{"instrument_id": ["1"]},
|
|
dependencies={"trading_review_packet": lambda **kwargs: {"instrument_id": 1, "generated_ts": "now"}},
|
|
)
|
|
|
|
self.assertEqual(session_status, 200)
|
|
self.assertEqual(session_payload["items"][0]["session_code"], "LONDON")
|
|
self.assertEqual(notes_status, 200)
|
|
self.assertEqual(notes_payload["items"][0]["author"], "alice")
|
|
self.assertEqual(create_status, 201)
|
|
self.assertEqual(create_payload["author"], "alice")
|
|
self.assertEqual(packet_status, 200)
|
|
self.assertEqual(packet_payload["instrument_id"], 1)
|
|
|
|
def test_routes_trading_research_council_run_and_queries(self) -> None:
|
|
status_run, payload_run = route_request(
|
|
"/trading/research-council/run",
|
|
{},
|
|
method="POST",
|
|
body={"instrument_id": 1, "timeframe": "5m", "model_code": "SWEEP_MSS_FVG"},
|
|
dependencies={"trading_research_council_run": lambda **kwargs: {"id": 41, **kwargs}},
|
|
)
|
|
status_latest, payload_latest = route_request(
|
|
"/trading/research-council/latest",
|
|
{"instrument_id": ["1"]},
|
|
dependencies={"trading_research_council_latest": lambda **kwargs: {"id": 41, "instrument_id": kwargs["instrument_id"]}},
|
|
)
|
|
status_runs, payload_runs = route_request(
|
|
"/trading/research-council/runs",
|
|
{"instrument_id": ["1"], "limit": ["2"]},
|
|
dependencies={"trading_research_council_runs": lambda **kwargs: [{"id": 41}, {"id": 40}]},
|
|
)
|
|
status_cycle, payload_cycle = route_request(
|
|
"/trading/research-council/optimization-cycle",
|
|
{},
|
|
method="POST",
|
|
body={"instrument_id": 1, "timeframe": "5m", "eval_limit": 5, "lesson_limit": 10},
|
|
dependencies={
|
|
"trading_research_council_optimization_run": lambda **kwargs: {
|
|
"id": 51,
|
|
"status": "completed",
|
|
**kwargs,
|
|
}
|
|
},
|
|
)
|
|
status_latest_cycle, payload_latest_cycle = route_request(
|
|
"/trading/research-council/optimization-cycle/latest",
|
|
{"instrument_id": ["1"]},
|
|
dependencies={"trading_research_council_optimization_latest": lambda **kwargs: {"id": 51, "instrument_id": kwargs["instrument_id"]}},
|
|
)
|
|
status_cycles, payload_cycles = route_request(
|
|
"/trading/research-council/optimization-cycles",
|
|
{"instrument_id": ["1"], "limit": ["2"]},
|
|
dependencies={"trading_research_council_optimization_cycles": lambda **kwargs: [{"id": 51}]},
|
|
)
|
|
status_review, payload_review = route_request(
|
|
"/trading/research-council/optimization-review",
|
|
{"instrument_id": ["1"], "limit": ["2"], "stale_after_hours": ["12"]},
|
|
dependencies={
|
|
"trading_research_council_optimization_review": lambda **kwargs: {
|
|
"instrument_id": kwargs["instrument_id"],
|
|
"limit": kwargs["limit"],
|
|
"stale_after_hours": kwargs["stale_after_hours"],
|
|
"health_state": "healthy",
|
|
}
|
|
},
|
|
)
|
|
status_tick, payload_tick = route_request(
|
|
"/trading/research-council/governance-tick",
|
|
{},
|
|
method="POST",
|
|
body={"instrument_id": 1, "limit": 2, "stale_after_hours": 12, "force_cycle": False},
|
|
dependencies={
|
|
"trading_research_council_governance_tick": lambda **kwargs: {
|
|
"instrument_id": kwargs["instrument_id"],
|
|
"limit": kwargs["limit"],
|
|
"stale_after_hours": kwargs["stale_after_hours"],
|
|
"force_cycle": kwargs["force_cycle"],
|
|
"status": "no_op_healthy",
|
|
}
|
|
},
|
|
)
|
|
|
|
self.assertEqual(status_run, 200)
|
|
self.assertEqual(payload_run["id"], 41)
|
|
self.assertEqual(payload_run["timeframe"], "5m")
|
|
self.assertEqual(status_latest, 200)
|
|
self.assertEqual(payload_latest["instrument_id"], 1)
|
|
self.assertEqual(status_runs, 200)
|
|
self.assertEqual(len(payload_runs["items"]), 2)
|
|
self.assertEqual(status_cycle, 200)
|
|
self.assertEqual(payload_cycle["id"], 51)
|
|
self.assertEqual(payload_cycle["eval_limit"], 5)
|
|
self.assertEqual(payload_cycle["lesson_limit"], 10)
|
|
self.assertEqual(status_latest_cycle, 200)
|
|
self.assertEqual(payload_latest_cycle["instrument_id"], 1)
|
|
self.assertEqual(status_cycles, 200)
|
|
self.assertEqual(payload_cycles["items"], [{"id": 51}])
|
|
self.assertEqual(status_review, 200)
|
|
self.assertEqual(payload_review["health_state"], "healthy")
|
|
self.assertEqual(payload_review["limit"], 2)
|
|
self.assertEqual(payload_review["stale_after_hours"], 12.0)
|
|
self.assertEqual(status_tick, 200)
|
|
self.assertEqual(payload_tick["status"], "no_op_healthy")
|
|
self.assertFalse(payload_tick["force_cycle"])
|
|
|
|
def test_routes_trading_live_readiness(self) -> None:
|
|
status, payload = route_request(
|
|
"/trading/live-readiness",
|
|
{"instrument_id": ["1"], "timeframe": ["5m"]},
|
|
dependencies={"trading_live_readiness": lambda **kwargs: {"ready_for_real_live": False, "timeframe": kwargs["timeframe"]}},
|
|
)
|
|
|
|
self.assertEqual(status, 200)
|
|
self.assertFalse(payload["ready_for_real_live"])
|
|
self.assertEqual(payload["timeframe"], "5m")
|
|
|
|
def test_routes_trading_launch_readiness(self) -> None:
|
|
status, payload = route_request(
|
|
"/trading/launch-readiness",
|
|
{"instrument_id": ["1"], "timeframe": ["5m"]},
|
|
dependencies={"trading_launch_readiness": lambda **kwargs: {"analysis_only_ready": True, "decision_support_ready": False, "real_trade_loop_ready": False, "assisted_trading_ready": True, "timeframe": kwargs["timeframe"]}},
|
|
)
|
|
|
|
self.assertEqual(status, 200)
|
|
self.assertTrue(payload["assisted_trading_ready"])
|
|
self.assertTrue(payload["analysis_only_ready"])
|
|
self.assertFalse(payload["decision_support_ready"])
|
|
self.assertEqual(payload["timeframe"], "5m")
|
|
|
|
def test_routes_trading_broker_state(self) -> None:
|
|
status, payload = route_request(
|
|
"/trading/broker-state",
|
|
{"instrument_id": ["1"]},
|
|
dependencies={"broker_state": lambda **kwargs: {"status": "broker_state_ok", **kwargs}},
|
|
)
|
|
|
|
self.assertEqual(status, 200)
|
|
self.assertEqual(payload["status"], "broker_state_ok")
|
|
self.assertEqual(payload["instrument_id"], 1)
|
|
|
|
def test_routes_trading_watch_run(self) -> None:
|
|
status, payload = route_request(
|
|
"/trading/watch/run",
|
|
{},
|
|
method="POST",
|
|
body={"instrument_id": 1, "setup_tf": "15m", "trigger_tf": "5m"},
|
|
dependencies={"trading_watch_run": lambda **kwargs: {"id": 4, "status": "executable", **kwargs}},
|
|
)
|
|
|
|
self.assertEqual(status, 200)
|
|
self.assertEqual(payload["setup_tf"], "15m")
|
|
|
|
def test_routes_trading_candidate_detail_and_confirm_check(self) -> None:
|
|
status_detail, payload_detail = route_request(
|
|
"/trading/candidates/4",
|
|
{},
|
|
dependencies={"trading_candidate_by_id": lambda candidate_id: {"id": candidate_id, "status": "executable"}},
|
|
)
|
|
status_check, payload_check = route_request(
|
|
"/trading/candidates/4/confirm-check",
|
|
{},
|
|
method="POST",
|
|
body={},
|
|
dependencies={"trading_candidate_confirm_check": lambda candidate_id: {"candidate_id": candidate_id, "executable": True}},
|
|
)
|
|
|
|
self.assertEqual(status_detail, 200)
|
|
self.assertEqual(payload_detail["id"], 4)
|
|
self.assertEqual(status_check, 200)
|
|
self.assertTrue(payload_check["executable"])
|
|
|
|
def test_routes_trading_ticket_flow(self) -> None:
|
|
status_build, payload_build = route_request(
|
|
"/trading/candidates/4/build-ticket",
|
|
{},
|
|
method="POST",
|
|
body={"execution_mode": "semi_auto"},
|
|
dependencies={"trading_candidate_build_ticket": lambda **kwargs: {"ticket_id": 11, **kwargs}},
|
|
)
|
|
status_get, payload_get = route_request(
|
|
"/trading/tickets/11",
|
|
{},
|
|
dependencies={"trading_ticket_by_id": lambda ticket_id: {"id": ticket_id, "status": "awaiting_confirmation"}},
|
|
)
|
|
status_risk, payload_risk = route_request(
|
|
"/trading/tickets/11/risk-check",
|
|
{},
|
|
method="POST",
|
|
body={},
|
|
dependencies={"trading_ticket_risk_check": lambda *args, **kwargs: {"ticket_id": 11, "allowed": False}},
|
|
)
|
|
status_confirm, payload_confirm = route_request(
|
|
"/trading/tickets/11/confirm",
|
|
{},
|
|
method="POST",
|
|
body={"confirmed_by": "alice"},
|
|
dependencies={"trading_ticket_confirm": lambda *args, **kwargs: {"id": 11, "status": "confirmed"}},
|
|
)
|
|
|
|
self.assertEqual(status_build, 200)
|
|
self.assertEqual(payload_build["ticket_id"], 11)
|
|
self.assertEqual(status_get, 200)
|
|
self.assertEqual(payload_get["status"], "awaiting_confirmation")
|
|
self.assertEqual(status_risk, 200)
|
|
self.assertFalse(payload_risk["allowed"])
|
|
self.assertEqual(status_confirm, 200)
|
|
self.assertEqual(payload_confirm["status"], "confirmed")
|
|
|
|
def test_routes_trading_execute_cancel_sync_and_weekly_review(self) -> None:
|
|
status_paper, payload_paper = route_request(
|
|
"/trading/tickets/11/paper-execute",
|
|
{},
|
|
method="POST",
|
|
body={"confirmed_by": "alice"},
|
|
dependencies={"trading_ticket_execute": lambda *args, **kwargs: {"status": "open", "mode": kwargs["execution_mode_expected"]}},
|
|
)
|
|
status_semi, payload_semi = route_request(
|
|
"/trading/tickets/11/semi-auto-submit",
|
|
{},
|
|
method="POST",
|
|
body={"confirmed_by": "alice"},
|
|
dependencies={"trading_ticket_execute": lambda *args, **kwargs: {"status": "pending", "mode": kwargs["execution_mode_expected"]}},
|
|
)
|
|
status_cancel, payload_cancel = route_request(
|
|
"/trading/executions/7/cancel",
|
|
{},
|
|
method="POST",
|
|
body={},
|
|
dependencies={"trading_execution_cancel": lambda execution_id: {"id": execution_id, "status": "cancelled"}},
|
|
)
|
|
status_close, payload_close = route_request(
|
|
"/trading/executions/7/close",
|
|
{},
|
|
method="POST",
|
|
body={},
|
|
dependencies={"trading_execution_close": lambda execution_id: {"id": execution_id, "status": "closed"}},
|
|
)
|
|
status_sync, payload_sync = route_request(
|
|
"/trading/executions/7/sync",
|
|
{},
|
|
method="POST",
|
|
body={},
|
|
dependencies={"trading_execution_sync": lambda execution_id: {"id": execution_id, "status": "closed"}},
|
|
)
|
|
status_weekly, payload_weekly = route_request(
|
|
"/trading/research/weekly-review",
|
|
{"instrument_id": ["1"]},
|
|
dependencies={"trading_weekly_review": lambda **kwargs: {"entry_count": 2, "next_week_actions": ["review_risk_process"]}},
|
|
)
|
|
|
|
self.assertEqual(status_paper, 200)
|
|
self.assertEqual(payload_paper["mode"], "paper")
|
|
self.assertEqual(status_semi, 200)
|
|
self.assertEqual(payload_semi["mode"], "semi_auto")
|
|
self.assertEqual(status_cancel, 200)
|
|
self.assertEqual(payload_cancel["status"], "cancelled")
|
|
self.assertEqual(status_close, 200)
|
|
self.assertEqual(payload_close["status"], "closed")
|
|
self.assertEqual(status_sync, 200)
|
|
self.assertEqual(payload_sync["status"], "closed")
|
|
self.assertEqual(status_weekly, 200)
|
|
self.assertEqual(payload_weekly["entry_count"], 2)
|
|
|
|
def test_routes_trading_executions_query(self) -> None:
|
|
status, payload = route_request(
|
|
"/trading/executions",
|
|
{"instrument_id": ["1"], "limit": ["2"]},
|
|
dependencies={"trading_executions": lambda **kwargs: [{"execution_id": 1, "execution_status": "open"}]},
|
|
)
|
|
|
|
self.assertEqual(status, 200)
|
|
self.assertEqual(payload["items"], [{"execution_id": 1, "execution_status": "open"}])
|
|
|
|
def test_routes_trading_journal_generate(self) -> None:
|
|
status, payload = route_request(
|
|
"/trading/journal/generate",
|
|
{},
|
|
method="POST",
|
|
body={"execution_id": 7},
|
|
dependencies={"trading_journal_generate": lambda **kwargs: {"created": 1, **kwargs}},
|
|
)
|
|
|
|
self.assertEqual(status, 200)
|
|
self.assertEqual(payload["execution_id"], 7)
|
|
|
|
def test_routes_health(self) -> None:
|
|
status, payload = route_request(
|
|
"/health",
|
|
{},
|
|
dependencies={
|
|
"health": lambda: {"ok": True},
|
|
"candles": lambda **kwargs: [],
|
|
"sessions": lambda **kwargs: [],
|
|
"signals": lambda **kwargs: [],
|
|
"backtests": lambda **kwargs: [],
|
|
},
|
|
)
|
|
|
|
self.assertEqual(status, 200)
|
|
self.assertEqual(payload, {"ok": True})
|
|
|
|
def test_routes_trading_plans_query(self) -> None:
|
|
status, payload = route_request(
|
|
"/trading/plans",
|
|
{"instrument_id": ["1"], "limit": ["2"]},
|
|
dependencies={
|
|
"trading_plans": lambda **kwargs: [{"id": 1}, {"id": 2}],
|
|
},
|
|
)
|
|
|
|
self.assertEqual(status, 200)
|
|
self.assertEqual(payload["items"], [{"id": 1}, {"id": 2}])
|
|
|
|
def test_routes_trading_candidates_query(self) -> None:
|
|
status, payload = route_request(
|
|
"/trading/candidates",
|
|
{"instrument_id": ["1"], "limit": ["2"]},
|
|
dependencies={
|
|
"trade_plan_candidates": lambda **kwargs: [{"id": 9}],
|
|
},
|
|
)
|
|
|
|
self.assertEqual(status, 200)
|
|
self.assertEqual(payload["items"], [{"id": 9}])
|
|
|
|
def test_routes_trading_executions_query(self) -> None:
|
|
status, payload = route_request(
|
|
"/trading/executions",
|
|
{"instrument_id": ["1"], "limit": ["2"]},
|
|
dependencies={
|
|
"trading_executions": lambda **kwargs: [{"execution_id": 1, "audit_status": "ok"}],
|
|
},
|
|
)
|
|
|
|
self.assertEqual(status, 200)
|
|
self.assertEqual(payload["items"], [{"execution_id": 1, "audit_status": "ok"}])
|
|
|
|
def test_routes_trading_tickets_query(self) -> None:
|
|
status, payload = route_request(
|
|
"/trading/tickets",
|
|
{"signal_id": ["1"], "limit": ["2"]},
|
|
dependencies={
|
|
"execution_tickets": lambda **kwargs: [{"id": 1, "status": "awaiting_confirmation"}],
|
|
},
|
|
)
|
|
|
|
self.assertEqual(status, 200)
|
|
self.assertEqual(payload["items"], [{"id": 1, "status": "awaiting_confirmation"}])
|
|
|
|
def test_routes_live_operation_audits_query(self) -> None:
|
|
status, payload = route_request(
|
|
"/live-operation-audits",
|
|
{"execution_ticket_id": ["1"], "limit": ["2"]},
|
|
dependencies={
|
|
"live_operation_audits": lambda **kwargs: [{"id": 1, "status": "rejected"}],
|
|
},
|
|
)
|
|
|
|
self.assertEqual(status, 200)
|
|
self.assertEqual(payload["items"], [{"id": 1, "status": "rejected"}])
|
|
|
|
def test_routes_trading_journal_query(self) -> None:
|
|
status, payload = route_request(
|
|
"/trading/journal",
|
|
{"instrument_id": ["1"], "limit": ["2"]},
|
|
dependencies={
|
|
"trading_journal": lambda **kwargs: [{"id": 2, "framework_execution": True}],
|
|
},
|
|
)
|
|
|
|
self.assertEqual(status, 200)
|
|
self.assertEqual(payload["items"], [{"id": 2, "framework_execution": True}])
|
|
|
|
def test_routes_trading_journal_summary_query(self) -> None:
|
|
status, payload = route_request(
|
|
"/trading/journal/summary",
|
|
{"instrument_id": ["1"]},
|
|
dependencies={
|
|
"trading_journal_summary": lambda **kwargs: {"entry_count": 4, "next_week_actions": ["review_model_context"]},
|
|
},
|
|
)
|
|
|
|
self.assertEqual(status, 200)
|
|
self.assertEqual(payload["entry_count"], 4)
|
|
|
|
def test_routes_health(self) -> None:
|
|
status, payload = route_request(
|
|
"/health",
|
|
{},
|
|
dependencies={
|
|
"health": lambda: {"ok": True},
|
|
"candles": lambda **kwargs: [],
|
|
"sessions": lambda **kwargs: [],
|
|
"signals": lambda **kwargs: [],
|
|
"backtests": lambda **kwargs: [],
|
|
"executions": lambda **kwargs: [],
|
|
},
|
|
)
|
|
|
|
self.assertEqual(status, 200)
|
|
self.assertEqual(payload, {"ok": True})
|
|
|
|
def test_routes_trading_plans_query_duplicate_guard(self) -> None:
|
|
status, payload = route_request(
|
|
"/trading/plans",
|
|
{"instrument_id": ["1"], "limit": ["2"]},
|
|
dependencies={
|
|
"trading_plans": lambda **kwargs: [{"id": 1}, {"id": 2}],
|
|
},
|
|
)
|
|
|
|
self.assertEqual(status, 200)
|
|
self.assertEqual(payload["items"], [{"id": 1}, {"id": 2}])
|
|
|
|
def test_rejects_missing_required_query_param(self) -> None:
|
|
with self.assertRaises(ValueError):
|
|
route_request(
|
|
"/trading/plans",
|
|
{},
|
|
dependencies={
|
|
"trading_plans": lambda **kwargs: [],
|
|
},
|
|
)
|
|
|
|
def test_returns_not_found_for_unknown_route(self) -> None:
|
|
status, payload = route_request(
|
|
"/unknown",
|
|
{},
|
|
dependencies={
|
|
"health": lambda: {"ok": True},
|
|
"candles": lambda **kwargs: [],
|
|
"sessions": lambda **kwargs: [],
|
|
"signals": lambda **kwargs: [],
|
|
"backtests": lambda **kwargs: [],
|
|
"executions": lambda **kwargs: [],
|
|
},
|
|
)
|
|
|
|
self.assertEqual(status, 404)
|
|
self.assertEqual(payload["error"], "not_found")
|
|
|
|
|
|
class ScriptFallbackTests(unittest.TestCase):
|
|
|
|
def test_rejects_missing_required_query_param(self) -> None:
|
|
with self.assertRaises(ValueError):
|
|
route_request(
|
|
"/trading/plans",
|
|
{},
|
|
dependencies={
|
|
"trading_plans": lambda **kwargs: [],
|
|
},
|
|
)
|
|
|
|
def test_returns_not_found_for_unknown_route(self) -> None:
|
|
status, payload = route_request(
|
|
"/unknown",
|
|
{},
|
|
dependencies={
|
|
"health": lambda: {"ok": True},
|
|
"candles": lambda **kwargs: [],
|
|
"sessions": lambda **kwargs: [],
|
|
"signals": lambda **kwargs: [],
|
|
"backtests": lambda **kwargs: [],
|
|
},
|
|
)
|
|
|
|
self.assertEqual(status, 404)
|
|
self.assertEqual(payload["error"], "not_found")
|
|
|
|
|
|
class ScriptFallbackTests(unittest.TestCase):
|
|
def test_run_execution_reports_database_unavailable(self) -> None:
|
|
output = io.StringIO()
|
|
with patch("scripts.run_execution.ExecutionService.prepare_execution", side_effect=OperationalError("stmt", {}, Exception())), contextlib.redirect_stdout(output):
|
|
exit_code = run_execution.main()
|
|
|
|
self.assertEqual(exit_code, 2)
|
|
self.assertIn("Database unavailable for execution sample: OperationalError", output.getvalue())
|
|
|
|
def test_run_execution_reports_database_unavailable_json(self) -> None:
|
|
output = io.StringIO()
|
|
with patch("scripts.run_execution.ExecutionService.prepare_execution", side_effect=OperationalError("stmt", {}, Exception())), contextlib.redirect_stdout(output):
|
|
exit_code = run_execution.main(env={"EXECUTION_OUTPUT_JSON": "1"})
|
|
|
|
payload = json.loads(output.getvalue())
|
|
self.assertEqual(exit_code, 2)
|
|
self.assertFalse(payload["passed"])
|
|
self.assertEqual(payload["error_type"], "OperationalError")
|
|
self.assertEqual(payload["execution_mode"], "paper")
|
|
|
|
def test_run_execution_builds_account_risk_state_from_env(self) -> None:
|
|
state = run_execution.build_account_risk_state_from_env(
|
|
{
|
|
"EXECUTION_KILL_SWITCH": "true",
|
|
"EXECUTION_DAILY_PNL_PCT": "-0.01",
|
|
"EXECUTION_CONSECUTIVE_LOSSES": "2",
|
|
"EXECUTION_OPEN_POSITIONS": "1",
|
|
"EXECUTION_SAME_SIDE_OPEN_POSITIONS": "1",
|
|
"EXECUTION_MAX_DAILY_LOSS_PCT": "0.015",
|
|
"EXECUTION_MAX_CONSECUTIVE_LOSSES": "4",
|
|
"EXECUTION_MAX_OPEN_POSITIONS": "2",
|
|
"EXECUTION_MAX_SAME_SIDE_POSITIONS": "2",
|
|
}
|
|
)
|
|
|
|
self.assertTrue(state.kill_switch)
|
|
self.assertEqual(state.daily_pnl_pct, -0.01)
|
|
self.assertEqual(state.consecutive_losses, 2)
|
|
self.assertEqual(state.max_daily_loss_pct, 0.015)
|
|
self.assertEqual(state.max_consecutive_losses, 4)
|
|
|
|
def test_run_execution_empty_env_ignores_ambient_risk_state(self) -> None:
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"EXECUTION_KILL_SWITCH": "true",
|
|
"EXECUTION_CONSECUTIVE_LOSSES": "9",
|
|
"EXECUTION_OPEN_POSITIONS": "7",
|
|
},
|
|
clear=False,
|
|
):
|
|
state = run_execution.build_account_risk_state_from_env({})
|
|
|
|
self.assertFalse(state.kill_switch)
|
|
self.assertEqual(state.consecutive_losses, 0)
|
|
self.assertEqual(state.open_positions, 0)
|
|
|
|
def test_run_execution_settlement_reports_database_unavailable(self) -> None:
|
|
output = io.StringIO()
|
|
with patch("scripts.run_execution_settlement.ExecutionService.settle_latest_execution_for_signal", side_effect=OperationalError("stmt", {}, Exception())), contextlib.redirect_stdout(output):
|
|
exit_code = run_execution_settlement.main()
|
|
|
|
self.assertEqual(exit_code, 2)
|
|
self.assertIn("Database unavailable for execution settlement sample: OperationalError", output.getvalue())
|
|
|
|
def test_run_execution_settlement_reports_database_unavailable_json(self) -> None:
|
|
output = io.StringIO()
|
|
with patch("scripts.run_execution_settlement.ExecutionService.settle_latest_execution_for_signal", side_effect=OperationalError("stmt", {}, Exception())), contextlib.redirect_stdout(output):
|
|
exit_code = run_execution_settlement.main(env={"EXECUTION_SETTLEMENT_OUTPUT_JSON": "1"})
|
|
|
|
payload = json.loads(output.getvalue())
|
|
self.assertEqual(exit_code, 2)
|
|
self.assertFalse(payload["passed"])
|
|
self.assertEqual(payload["error_type"], "OperationalError")
|
|
self.assertEqual(payload["execution_mode"], "paper")
|
|
|
|
def test_run_pipeline_reports_database_unavailable(self) -> None:
|
|
output = io.StringIO()
|
|
with patch("scripts.run_pipeline.run_closed_loop", side_effect=OperationalError("stmt", {}, Exception())), contextlib.redirect_stdout(output):
|
|
exit_code = run_pipeline.main()
|
|
|
|
self.assertEqual(exit_code, 2)
|
|
self.assertIn("Database unavailable for pipeline sample: OperationalError", output.getvalue())
|
|
|
|
def test_run_pipeline_reports_database_unavailable_json(self) -> None:
|
|
output = io.StringIO()
|
|
with patch("scripts.run_pipeline.run_closed_loop", side_effect=OperationalError("stmt", {}, Exception())), contextlib.redirect_stdout(output):
|
|
exit_code = run_pipeline.main(env={"PIPELINE_OUTPUT_JSON": "1"})
|
|
|
|
payload = json.loads(output.getvalue())
|
|
self.assertEqual(exit_code, 2)
|
|
self.assertFalse(payload["passed"])
|
|
self.assertEqual(payload["error_type"], "OperationalError")
|
|
|
|
def test_run_pipeline_passes_backtest_cost_config(self) -> None:
|
|
captured = {}
|
|
|
|
def fake_run_closed_loop(**kwargs):
|
|
captured.update(kwargs)
|
|
return {"ok": True}
|
|
|
|
output = io.StringIO()
|
|
env = {
|
|
"BACKTEST_FEE_RATE": "0.0005",
|
|
"BACKTEST_SLIPPAGE_RATE": "0.0007",
|
|
"BACKTEST_FIXED_COST_R": "0.05",
|
|
}
|
|
with patch("scripts.run_pipeline.run_closed_loop", side_effect=fake_run_closed_loop), patch.dict(os.environ, env, clear=False), contextlib.redirect_stdout(output):
|
|
run_pipeline.main()
|
|
|
|
cost_config = captured["backtest_cost_config"]
|
|
self.assertEqual(cost_config.fee_rate, 0.0005)
|
|
self.assertEqual(cost_config.slippage_rate, 0.0007)
|
|
self.assertEqual(cost_config.fixed_cost_r, 0.05)
|
|
|
|
def test_run_pipeline_passes_backtest_quality_gate_config(self) -> None:
|
|
captured = {}
|
|
|
|
def fake_run_closed_loop(**kwargs):
|
|
captured.update(kwargs)
|
|
return {"ok": True, "backtest_quality_gate": {"passed": True}}
|
|
|
|
output = io.StringIO()
|
|
env = {
|
|
"BACKTEST_MIN_TOTAL_SIGNALS": "50",
|
|
"BACKTEST_MIN_EXECUTED_SIGNALS": "20",
|
|
"BACKTEST_MIN_EXPECTANCY_R": "0.1",
|
|
"BACKTEST_MIN_PROFIT_FACTOR_R": "1.5",
|
|
"BACKTEST_MAX_DRAWDOWN_R": "6.0",
|
|
"BACKTEST_MAX_CONSECUTIVE_LOSSES": "3",
|
|
}
|
|
with patch("scripts.run_pipeline.run_closed_loop", side_effect=fake_run_closed_loop), patch.dict(os.environ, env, clear=False), contextlib.redirect_stdout(output):
|
|
run_pipeline.main()
|
|
|
|
quality_gate = captured["backtest_quality_gate"]
|
|
self.assertEqual(quality_gate.min_total_signals, 50)
|
|
self.assertEqual(quality_gate.min_executed_signals, 20)
|
|
self.assertEqual(quality_gate.min_expectancy_r, 0.1)
|
|
self.assertEqual(quality_gate.min_profit_factor_r, 1.5)
|
|
self.assertEqual(quality_gate.max_drawdown_r, 6.0)
|
|
self.assertEqual(quality_gate.max_consecutive_losses, 3)
|
|
|
|
def test_run_pipeline_passes_backtest_replay_mode_and_config(self) -> None:
|
|
captured = {}
|
|
|
|
def fake_run_closed_loop(**kwargs):
|
|
captured.update(kwargs)
|
|
return {"ok": True, "backtest_quality_gate": {"passed": True}}
|
|
|
|
output = io.StringIO()
|
|
env = {
|
|
"BACKTEST_MODE": "replay_no_lookahead",
|
|
"BACKTEST_REPLAY_LEFT_BARS": "1",
|
|
"BACKTEST_REPLAY_RIGHT_BARS": "3",
|
|
}
|
|
with patch("scripts.run_pipeline.run_closed_loop", side_effect=fake_run_closed_loop), patch.dict(os.environ, env, clear=False), contextlib.redirect_stdout(output):
|
|
run_pipeline.main()
|
|
|
|
self.assertEqual(captured["backtest_mode"], "replay_no_lookahead")
|
|
self.assertEqual(captured["backtest_replay_config"].left_bars, 1)
|
|
self.assertEqual(captured["backtest_replay_config"].right_bars, 3)
|
|
|
|
def test_run_pipeline_passes_signal_evidence_mode(self) -> None:
|
|
captured = {}
|
|
|
|
def fake_run_closed_loop(**kwargs):
|
|
captured.update(kwargs)
|
|
return {"ok": True, "backtest_quality_gate": {"passed": True}}
|
|
|
|
output = io.StringIO()
|
|
with patch("scripts.run_pipeline.run_closed_loop", side_effect=fake_run_closed_loop), patch.dict(os.environ, {"SIGNAL_EVIDENCE_MODE": "strict"}, clear=False), contextlib.redirect_stdout(output):
|
|
run_pipeline.main()
|
|
|
|
self.assertIn("sweep_id", captured["signal_required_evidence_keys"])
|
|
self.assertIn("liquidity_target_id", captured["signal_required_evidence_keys"])
|
|
|
|
def test_run_pipeline_passes_execution_account_state(self) -> None:
|
|
captured = {}
|
|
|
|
def fake_run_closed_loop(**kwargs):
|
|
captured.update(kwargs)
|
|
return {"ok": True}
|
|
|
|
output = io.StringIO()
|
|
env = {
|
|
"EXECUTION_DAILY_PNL_PCT": "-0.01",
|
|
"EXECUTION_CONSECUTIVE_LOSSES": "2",
|
|
}
|
|
with patch("scripts.run_pipeline.run_closed_loop", side_effect=fake_run_closed_loop), patch.dict(os.environ, env, clear=False), contextlib.redirect_stdout(output):
|
|
run_pipeline.main()
|
|
|
|
account_state = captured["execution_account_state"]
|
|
self.assertEqual(account_state.daily_pnl_pct, -0.01)
|
|
self.assertEqual(account_state.consecutive_losses, 2)
|
|
|
|
def test_run_pipeline_blocks_when_required_quality_gate_fails(self) -> None:
|
|
output = io.StringIO()
|
|
result = {"backtest_quality_gate": {"passed": False, "reasons": ["sample_size_too_small"]}}
|
|
|
|
with patch("scripts.run_pipeline.run_closed_loop", return_value=result), patch.dict(os.environ, {"BACKTEST_REQUIRE_QUALITY_GATE": "1"}, clear=False), contextlib.redirect_stdout(output):
|
|
exit_code = run_pipeline.main()
|
|
|
|
self.assertEqual(exit_code, 2)
|
|
self.assertIn('"backtest_quality_gate"', output.getvalue())
|
|
|
|
|
|
class OfflineChecksScriptTests(unittest.TestCase):
|
|
def test_build_offline_check_commands_adds_replay_file_when_configured(self) -> None:
|
|
commands = run_offline_checks.build_check_commands(env={"OFFLINE_REPLAY_FILE": "sample.jsonl"})
|
|
|
|
self.assertEqual(commands[-1][0], "replay_file")
|
|
self.assertEqual(commands[-1][1][-1], "sample.jsonl")
|
|
|
|
def test_run_offline_checks_returns_zero_when_all_checks_pass(self) -> None:
|
|
calls = []
|
|
|
|
def fake_runner(command, **kwargs):
|
|
calls.append(command)
|
|
return SimpleNamespace(returncode=0, stdout="ok\n", stderr="")
|
|
|
|
output = io.StringIO()
|
|
commands = [("unit_tests", ["python", "-m", "unittest"]), ("dashboard_js_syntax", ["node", "--check", "src/web/app.js"])]
|
|
|
|
with contextlib.redirect_stdout(output):
|
|
exit_code = run_offline_checks.run_checks(commands=commands, runner=fake_runner, node_available=True, env={})
|
|
|
|
self.assertEqual(exit_code, 0)
|
|
self.assertEqual(len(calls), 2)
|
|
self.assertIn("Offline check passed: dashboard_js_syntax", output.getvalue())
|
|
|
|
def test_run_offline_checks_can_include_optional_replay_file(self) -> None:
|
|
calls = []
|
|
|
|
def fake_runner(command, **kwargs):
|
|
calls.append(command)
|
|
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
|
|
|
output = io.StringIO()
|
|
|
|
with contextlib.redirect_stdout(output):
|
|
exit_code = run_offline_checks.run_checks(runner=fake_runner, node_available=True, env={"OFFLINE_REPLAY_FILE": "sample.csv"})
|
|
|
|
self.assertEqual(exit_code, 0)
|
|
self.assertEqual(calls[-1][-1], "sample.csv")
|
|
self.assertIn("Offline check passed: replay_file", output.getvalue())
|
|
|
|
def test_run_offline_checks_stops_on_failure(self) -> None:
|
|
calls = []
|
|
|
|
def fake_runner(command, **kwargs):
|
|
calls.append(command)
|
|
return SimpleNamespace(returncode=7, stdout="", stderr="failed\n")
|
|
|
|
output = io.StringIO()
|
|
|
|
with contextlib.redirect_stdout(output):
|
|
exit_code = run_offline_checks.run_checks(commands=[("unit_tests", ["python", "-m", "unittest"])], runner=fake_runner, env={})
|
|
|
|
self.assertEqual(exit_code, 7)
|
|
self.assertEqual(len(calls), 1)
|
|
self.assertIn("Offline check failed: unit_tests exit_code=7", output.getvalue())
|
|
|
|
def test_run_offline_checks_skips_node_when_unavailable(self) -> None:
|
|
calls = []
|
|
|
|
def fake_runner(command, **kwargs):
|
|
calls.append(command)
|
|
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
|
|
|
output = io.StringIO()
|
|
|
|
with contextlib.redirect_stdout(output):
|
|
exit_code = run_offline_checks.run_checks(commands=[("dashboard_js_syntax", ["node", "--check", "src/web/app.js"])], runner=fake_runner, node_available=False, env={})
|
|
|
|
self.assertEqual(exit_code, 0)
|
|
self.assertEqual(calls, [])
|
|
self.assertIn("Offline check skipped: dashboard_js_syntax", output.getvalue())
|
|
|
|
def test_run_offline_checks_can_emit_json_summary(self) -> None:
|
|
def fake_runner(command, **kwargs):
|
|
return SimpleNamespace(returncode=0, stdout="ok\n", stderr="")
|
|
|
|
output = io.StringIO()
|
|
|
|
with contextlib.redirect_stdout(output):
|
|
exit_code = run_offline_checks.run_checks(
|
|
commands=[("unit_tests", ["python", "-m", "unittest"])],
|
|
runner=fake_runner,
|
|
env={"OFFLINE_CHECKS_OUTPUT_JSON": "1"},
|
|
)
|
|
|
|
summary = json.loads(output.getvalue())
|
|
self.assertEqual(exit_code, 0)
|
|
self.assertTrue(summary["passed"])
|
|
self.assertEqual(summary["checks"][0]["name"], "unit_tests")
|
|
self.assertEqual(summary["checks"][0]["status"], "passed")
|
|
|
|
def test_run_offline_checks_passes_merged_env_to_subprocesses(self) -> None:
|
|
seen_env = {}
|
|
|
|
def fake_runner(command, **kwargs):
|
|
seen_env.update(kwargs["env"])
|
|
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
|
|
|
with contextlib.redirect_stdout(io.StringIO()):
|
|
exit_code = run_offline_checks.run_checks(
|
|
commands=[("unit_tests", ["python", "-m", "unittest"])],
|
|
runner=fake_runner,
|
|
env={"CUSTOM_OFFLINE_FLAG": "1"},
|
|
)
|
|
|
|
self.assertEqual(exit_code, 0)
|
|
self.assertEqual(seen_env["CUSTOM_OFFLINE_FLAG"], "1")
|
|
self.assertIn("PATH", seen_env)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|