Close readonly analysis launch readiness

This commit is contained in:
Codex
2026-05-18 14:11:06 +08:00
parent b291e42ca2
commit d40d39af15
50 changed files with 10308 additions and 88 deletions
+131
View File
@@ -4,7 +4,11 @@ import unittest
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://user:pass@localhost:5432/ai_ict_test")
from src.api.alerts import (
_build_archive_bundle_manifest,
_build_archive_selection_provenance_summary,
_build_external_context_surface_history_summary,
_build_workflow_archive_artifact_bundle_summary,
_build_workflow_archive_compare_writeback_summary,
_build_workflow_archive_handoff_bundle_summary,
_build_workflow_archive_handoff_packet_starter,
_build_workflow_archive_replay_note_bridge_summary,
@@ -62,6 +66,133 @@ class AlertArchiveHelperTests(unittest.TestCase):
self.assertEqual(bundle["status"], "active")
self.assertEqual(bundle["day_group_count"], 1)
def test_workflow_compare_writeback_summary_persists_note_context(self) -> None:
result = _build_workflow_archive_compare_writeback_summary(
note={
"id": 21,
"note_scope": "workflow_compare",
"author": "local_operator",
"tags": ["workflow-compare", "session:LONDON"],
"created_ts": "2026-05-16T02:00:00+00:00",
},
replay_note_bridge_summary={
"note_title": "Workflow replay compare #9 vs #8",
"note_body": "compare body",
"compare_left_snapshot_id": 9,
"compare_right_snapshot_id": 8,
"changed_phase_count": 2,
},
compare={"summary": "compare summary"},
query_context={"profile_scope": "selected_profile", "left_id": 9, "right_id": 8},
)
self.assertEqual(result["status"], "persisted")
self.assertEqual(result["note_id"], 21)
self.assertEqual(result["note_title"], "Workflow replay compare #9 vs #8")
self.assertEqual(result["compare_left_snapshot_id"], 9)
self.assertEqual(result["changed_phase_count"], 2)
def test_archive_selection_provenance_summary_tracks_scope_filters_and_compare(self) -> None:
result = _build_archive_selection_provenance_summary(
archive={
"status": "active",
"profile_scope": "all_profiles",
"query_preset": "recent_7d",
"filters": {"query_text": "candidate", "created_after": "", "digest_mode": "high_value_only"},
"archive_baseline_state": {"digest_snapshot_id": 9},
"item_count": 5,
},
compare={"left": {"snapshot_id": 4}, "right": {"snapshot_id": 3}},
export_scope="selected_compare",
selection_kind="digest_archive",
)
self.assertEqual(result["status"], "active")
self.assertEqual(result["export_scope"], "selected_compare")
self.assertEqual(result["active_filter_count"], 2)
self.assertEqual(result["selected_snapshot_ids"], [4, 3])
self.assertEqual(result["baseline_snapshot_id"], 9)
def test_archive_bundle_manifest_normalizes_export_sections(self) -> None:
manifest = _build_archive_bundle_manifest(
archive={
"status": "active",
"profile_scope": "all_profiles",
"query_preset": "recent_7d",
"item_count": 5,
},
compare={"status": "active"},
generated_ts="2026-05-17T03:00:00+00:00",
export_kind="digest_archive_bundle",
export_scope="selected_compare",
selection_provenance_summary={
"active_filter_count": 2,
"selected_snapshot_ids": [4, 3],
"baseline_snapshot_id": 9,
},
structured_export_summary={"status": "active"},
)
self.assertEqual(manifest["normalization_status"], "normalized")
self.assertEqual(manifest["manifest_version"], "decision_assistant_archive_bundle_manifest_v1")
self.assertEqual(manifest["status"], "active")
self.assertIn("compare", manifest["included_sections"])
self.assertEqual(manifest["baseline_snapshot_id"], 9)
def test_external_context_surface_history_summary_includes_e30_resolution_follow_up_fields(self) -> None:
result = _build_external_context_surface_history_summary(
instrument_id=1,
profile_key="local_operator",
history=[
{
"surface_id": "okx_help_announcements_page",
"title": "okx_help_announcements_page",
"source_group": "exchange_context_surface",
"selection_kind": "target_url",
"target_url": "https://www.okx.com/help",
"selected_ts": "2026-05-16T00:00:00+00:00",
"revisit_acknowledged": True,
"revisit_acknowledged_by": "alice",
"revisit_acknowledged_ts": "2026-05-16T08:00:00+00:00",
},
{
"surface_id": "tradingview_public_chart_reference",
"title": "tradingview_public_chart_reference",
"source_group": "visual_reference_card",
"selection_kind": "target_url",
"target_url": "https://www.tradingview.com/",
"selected_ts": "2026-05-15T00:00:00+00:00",
"revisit_acknowledged": True,
"revisit_acknowledged_by": "bob",
"revisit_acknowledged_ts": "2026-05-16T07:30:00+00:00",
"revisit_ack_note": "checked",
},
{
"surface_id": "binance_public_chart_reference",
"title": "binance_public_chart_reference",
"source_group": "visual_reference_card",
"selection_kind": "target_url",
"target_url": "https://www.binance.com/en/trade/BTC_USDT?type=spot",
"selected_ts": "2026-05-14T00:00:00+00:00",
"revisit_acknowledged": True,
"revisit_acknowledged_by": "bob",
"revisit_acknowledged_ts": "2026-05-16T07:00:00+00:00",
"revisit_ack_note": "done",
},
],
)
resolution = result["resolution_filter_summary"]
self.assertIn("resolved_owner_drift_status", resolution)
self.assertIn("carry_forward_follow_up_lane_status", resolution)
self.assertEqual(resolution["resolved_owner_drift_status"], "drift_detected")
self.assertEqual(resolution["carry_forward_follow_up_lane_status"], "active")
self.assertEqual(resolution["resolved_owner_drift_review_status"], "review_required")
self.assertEqual(resolution["resolved_owner_drift_review_queue_status"], "active")
self.assertEqual(resolution["resolved_owner_drift_review_queue_aging_status"], "stale")
self.assertEqual(resolution["carry_forward_follow_up_lane_rollover_status"], "rollover_required")
self.assertEqual(resolution["carry_forward_follow_up_lane_rollover_handoff_status"], "handoff_required")
if __name__ == "__main__":
unittest.main()
+179 -2
View File
@@ -199,6 +199,70 @@ class ApiServerRoutingTests(unittest.TestCase):
self.assertEqual(post_status, 200)
self.assertEqual(post_payload["workflow_snapshot_id"], 34)
def test_routes_external_context_surface_history(self) -> None:
get_status, get_payload = route_request(
"/trading/external-context-surface-history",
{"instrument_id": ["1"], "profile_key": ["local_operator"]},
dependencies={
"trading_external_context_surface_history": lambda **kwargs: {
"status": "active",
"instrument_id": kwargs["instrument_id"],
"profile_key": kwargs["profile_key"],
"entry_count": 2,
}
},
)
post_status, post_payload = route_request(
"/trading/external-context-surface-history",
{},
method="POST",
body={
"instrument_id": 1,
"profile_key": "local_operator",
"surface_id": "binance_public_chart_reference",
"title": "binance_public_chart_reference",
"source_group": "visual_reference_card",
"selection_kind": "target_url",
"target_url": "https://www.binance.com/en/trade/BTC_USDT?type=spot",
},
dependencies={
"trading_external_context_surface_history_save": lambda **kwargs: {
"status": "saved",
"instrument_id": kwargs["instrument_id"],
"profile_key": kwargs["profile_key"],
"latest_selection": {"surface_id": kwargs["surface_id"]},
}
},
)
ack_status, ack_payload = route_request(
"/trading/external-context-surface-history/acknowledge",
{},
method="POST",
body={
"instrument_id": 1,
"profile_key": "local_operator",
"surface_id": "binance_public_chart_reference",
"selected_ts": "2026-05-12T02:05:30+00:00",
"note": "checked and cleared",
},
dependencies={
"trading_external_context_surface_history_ack": lambda **kwargs: {
"status": "saved",
"instrument_id": kwargs["instrument_id"],
"profile_key": kwargs["profile_key"],
"latest_acknowledgment": {"surface_id": kwargs["surface_id"], "revisit_ack_note": kwargs["note"]},
}
},
)
self.assertEqual(get_status, 200)
self.assertEqual(get_payload["entry_count"], 2)
self.assertEqual(post_status, 200)
self.assertEqual(post_payload["latest_selection"]["surface_id"], "binance_public_chart_reference")
self.assertEqual(ack_status, 200)
self.assertEqual(ack_payload["latest_acknowledgment"]["surface_id"], "binance_public_chart_reference")
self.assertEqual(ack_payload["latest_acknowledgment"]["revisit_ack_note"], "checked and cleared")
def test_routes_decision_assistant_archive_and_focus(self) -> None:
profiles_status, profiles_payload = route_request(
"/trading/alerts/digest-preferences/profiles",
@@ -224,6 +288,7 @@ class ApiServerRoutingTests(unittest.TestCase):
"filters": {"preferred_history_kind": kwargs["preferred_history_kind"], "query_text": kwargs["query_text"]},
"long_range_trend_summary": {"status": "active"},
"baseline_pin_summary": {"status": "active"},
"ranked_aggregation_summary": {"status": "active"},
}
},
)
@@ -234,6 +299,7 @@ class ApiServerRoutingTests(unittest.TestCase):
"trading_alert_operator_digest_history_export": lambda **kwargs: {
"status": "active",
"export_kind": "digest_archive_bundle",
"export_schema_version": "decision_assistant_archive_export_v4",
"profile_scope": kwargs["profile_scope"],
"query_preset": kwargs["query_preset"],
"query_text": kwargs["query_text"],
@@ -241,7 +307,10 @@ class ApiServerRoutingTests(unittest.TestCase):
"right_id": kwargs["right_id"],
"long_range_trend_summary": {"status": "active"},
"baseline_pin_summary": {"status": "active"},
"ranked_aggregation_summary": {"status": "active"},
"selection_provenance_summary": {"status": "active", "export_scope": "selected_compare"},
"structured_export_summary": {"status": "active"},
"bundle_manifest": {"status": "active", "manifest_version": "decision_assistant_archive_bundle_manifest_v1"},
}
},
)
@@ -271,28 +340,36 @@ class ApiServerRoutingTests(unittest.TestCase):
"filters": {"current_session": kwargs["current_session"], "next_phase": kwargs["next_phase"], "query_text": kwargs["query_text"]},
"long_range_trend_summary": {"status": "active"},
"baseline_pin_summary": {"status": "active"},
"ranked_aggregation_summary": {"status": "active"},
}
},
)
workflow_export_status, workflow_export_payload = route_request(
"/trading/workflow-archive/export",
{"instrument_id": ["1"], "profile_scope": ["all_profiles"], "query_preset": ["intraday_recent"], "query_text": ["handoff"], "left_id": ["9"], "right_id": ["8"]},
{"instrument_id": ["1"], "profile_scope": ["all_profiles"], "query_preset": ["intraday_recent"], "query_text": ["handoff"], "left_id": ["9"], "right_id": ["8"], "group_date": ["2026-05-16"], "session_group": ["LONDON"]},
dependencies={
"trading_workflow_archive_export": lambda **kwargs: {
"status": "active",
"export_kind": "workflow_archive_bundle",
"export_schema_version": "decision_assistant_archive_export_v4",
"profile_scope": kwargs["profile_scope"],
"query_preset": kwargs["query_preset"],
"query_text": kwargs["query_text"],
"left_id": kwargs["left_id"],
"right_id": kwargs["right_id"],
"group_date": kwargs["group_date"],
"session_group": kwargs["session_group"],
"long_range_trend_summary": {"status": "active"},
"baseline_pin_summary": {"status": "active"},
"ranked_aggregation_summary": {"status": "active"},
"handoff_bundle_summary": {"status": "active"},
"handoff_packet_starter": {"status": "active"},
"replay_note_bridge_summary": {"status": "active"},
"artifact_bundle_summary": {"status": "active"},
"group_export_summary": {"status": "active"},
"selection_provenance_summary": {"status": "active", "export_scope": "selected_compare"},
"structured_export_summary": {"status": "active"},
"bundle_manifest": {"status": "active", "manifest_version": "decision_assistant_archive_bundle_manifest_v1"},
}
},
)
@@ -310,6 +387,21 @@ class ApiServerRoutingTests(unittest.TestCase):
}
},
)
workflow_writeback_status, workflow_writeback_payload = route_request(
"/trading/workflow-archive/compare-writeback",
{},
method="POST",
body={"instrument_id": 1, "profile_key": "desk_a", "left_id": 9, "right_id": 8, "query_text": "handoff"},
dependencies={
"trading_workflow_archive_compare_writeback": lambda **kwargs: {
"status": "saved",
"profile_key": kwargs["profile_key"],
"left_id": kwargs["left_id"],
"right_id": kwargs["right_id"],
"compare_writeback_summary": {"status": "persisted", "note_title": "Workflow replay compare #9 vs #8"},
}
},
)
focus_get_status, focus_get_payload = route_request(
"/trading/assistant-focus",
{"instrument_id": ["1"], "profile_key": ["desk_a"]},
@@ -346,13 +438,18 @@ class ApiServerRoutingTests(unittest.TestCase):
self.assertEqual(digest_archive_payload["filters"]["preferred_history_kind"], "reminder")
self.assertEqual(digest_archive_payload["filters"]["query_text"], "candidate")
self.assertEqual(digest_archive_payload["long_range_trend_summary"]["status"], "active")
self.assertEqual(digest_archive_payload["ranked_aggregation_summary"]["status"], "active")
self.assertEqual(digest_export_status, 200)
self.assertEqual(digest_export_payload["export_kind"], "digest_archive_bundle")
self.assertEqual(digest_export_payload["export_schema_version"], "decision_assistant_archive_export_v4")
self.assertEqual(digest_export_payload["profile_scope"], "all_profiles")
self.assertEqual(digest_export_payload["query_preset"], "recent_7d")
self.assertEqual(digest_export_payload["left_id"], 4)
self.assertEqual(digest_export_payload["baseline_pin_summary"]["status"], "active")
self.assertEqual(digest_export_payload["ranked_aggregation_summary"]["status"], "active")
self.assertEqual(digest_export_payload["selection_provenance_summary"]["status"], "active")
self.assertEqual(digest_export_payload["structured_export_summary"]["status"], "active")
self.assertEqual(digest_export_payload["bundle_manifest"]["manifest_version"], "decision_assistant_archive_bundle_manifest_v1")
self.assertEqual(digest_compare_status, 200)
self.assertEqual(digest_compare_payload["left_id"], 4)
self.assertEqual(digest_compare_payload["profile_scope"], "all_profiles")
@@ -364,21 +461,32 @@ class ApiServerRoutingTests(unittest.TestCase):
self.assertEqual(workflow_archive_payload["filters"]["next_phase"], "intraday")
self.assertEqual(workflow_archive_payload["filters"]["query_text"], "handoff")
self.assertEqual(workflow_archive_payload["baseline_pin_summary"]["status"], "active")
self.assertEqual(workflow_archive_payload["ranked_aggregation_summary"]["status"], "active")
self.assertEqual(workflow_export_status, 200)
self.assertEqual(workflow_export_payload["export_kind"], "workflow_archive_bundle")
self.assertEqual(workflow_export_payload["export_schema_version"], "decision_assistant_archive_export_v4")
self.assertEqual(workflow_export_payload["profile_scope"], "all_profiles")
self.assertEqual(workflow_export_payload["query_preset"], "intraday_recent")
self.assertEqual(workflow_export_payload["group_date"], "2026-05-16")
self.assertEqual(workflow_export_payload["session_group"], "LONDON")
self.assertEqual(workflow_export_payload["long_range_trend_summary"]["status"], "active")
self.assertEqual(workflow_export_payload["ranked_aggregation_summary"]["status"], "active")
self.assertEqual(workflow_export_payload["handoff_bundle_summary"]["status"], "active")
self.assertEqual(workflow_export_payload["handoff_packet_starter"]["status"], "active")
self.assertEqual(workflow_export_payload["replay_note_bridge_summary"]["status"], "active")
self.assertEqual(workflow_export_payload["artifact_bundle_summary"]["status"], "active")
self.assertEqual(workflow_export_payload["group_export_summary"]["status"], "active")
self.assertEqual(workflow_export_payload["selection_provenance_summary"]["status"], "active")
self.assertEqual(workflow_export_payload["structured_export_summary"]["status"], "active")
self.assertEqual(workflow_export_payload["bundle_manifest"]["manifest_version"], "decision_assistant_archive_bundle_manifest_v1")
self.assertEqual(workflow_compare_status, 200)
self.assertEqual(workflow_compare_payload["profile_scope"], "all_profiles")
self.assertEqual(workflow_compare_payload["query_preset"], "intraday_recent")
self.assertEqual(workflow_compare_payload["right_id"], 8)
self.assertEqual(workflow_compare_payload["query_text"], "handoff")
self.assertEqual(workflow_writeback_status, 200)
self.assertEqual(workflow_writeback_payload["profile_key"], "desk_a")
self.assertEqual(workflow_writeback_payload["compare_writeback_summary"]["status"], "persisted")
self.assertEqual(focus_get_status, 200)
self.assertEqual(focus_get_payload["focus_candidate_id"], 18)
self.assertEqual(focus_post_status, 200)
@@ -634,6 +742,24 @@ class ApiServerRoutingTests(unittest.TestCase):
self.assertFalse(payload["ready_for_real_live"])
self.assertEqual(payload["timeframe"], "5m")
def test_routes_trading_okx_sandbox_preflight(self) -> None:
status, payload = route_request(
"/trading/okx-sandbox-preflight",
{"instrument_id": ["1"], "timeframe": ["5m"]},
dependencies={
"trading_okx_sandbox_preflight": lambda **kwargs: {
"status": "sandbox_preflight_ready",
"timeframe": kwargs["timeframe"],
"next_gate": "sandbox_submit_cancel_reconcile_smoke",
}
},
)
self.assertEqual(status, 200)
self.assertEqual(payload["status"], "sandbox_preflight_ready")
self.assertEqual(payload["timeframe"], "5m")
self.assertEqual(payload["next_gate"], "sandbox_submit_cancel_reconcile_smoke")
def test_routes_trading_launch_readiness(self) -> None:
status, payload = route_request(
"/trading/launch-readiness",
@@ -647,6 +773,56 @@ class ApiServerRoutingTests(unittest.TestCase):
self.assertFalse(payload["decision_support_ready"])
self.assertEqual(payload["timeframe"], "5m")
def test_routes_trading_shadow_paper_validation(self) -> None:
status, payload = route_request(
"/trading/shadow-paper-validation",
{"instrument_id": ["1"], "timeframe": ["5m"]},
dependencies={"trading_shadow_paper_validation": lambda **kwargs: {"status": "collecting_paper_samples", "timeframe": kwargs["timeframe"]}},
)
self.assertEqual(status, 200)
self.assertEqual(payload["status"], "collecting_paper_samples")
self.assertEqual(payload["timeframe"], "5m")
def test_routes_trading_shadow_paper_observation_record(self) -> None:
status, payload = route_request(
"/trading/shadow-paper/observe",
{},
method="POST",
body={
"instrument_id": 1,
"timeframe": "5m",
"session_code": "NY_AM",
"candidate_id": 7,
"paper_ticket_id": 11,
"paper_execution_id": 21,
"operator_agreement": "agree",
"operator_note": "useful paper sample",
"created_by": "alice",
},
dependencies={
"trading_shadow_paper_observation_record": lambda **kwargs: {
"status": "recorded",
"instrument_id": kwargs["instrument_id"],
"timeframe": kwargs["timeframe"],
"candidate_id": kwargs["candidate_id"],
"paper_ticket_id": kwargs["paper_ticket_id"],
"paper_execution_id": kwargs["paper_execution_id"],
"operator_agreement": kwargs["operator_agreement"],
"created_by": kwargs["created_by"],
}
},
)
self.assertEqual(status, 201)
self.assertEqual(payload["status"], "recorded")
self.assertEqual(payload["timeframe"], "5m")
self.assertEqual(payload["candidate_id"], 7)
self.assertEqual(payload["paper_ticket_id"], 11)
self.assertEqual(payload["paper_execution_id"], 21)
self.assertEqual(payload["operator_agreement"], "agree")
self.assertEqual(payload["created_by"], "alice")
def test_routes_trading_broker_state(self) -> None:
status, payload = route_request(
"/trading/broker-state",
@@ -694,7 +870,7 @@ class ApiServerRoutingTests(unittest.TestCase):
"/trading/candidates/4/build-ticket",
{},
method="POST",
body={"execution_mode": "semi_auto"},
body={"execution_mode": "semi_auto", "allow_shadow_paper": False},
dependencies={"trading_candidate_build_ticket": lambda **kwargs: {"ticket_id": 11, **kwargs}},
)
status_get, payload_get = route_request(
@@ -719,6 +895,7 @@ class ApiServerRoutingTests(unittest.TestCase):
self.assertEqual(status_build, 200)
self.assertEqual(payload_build["ticket_id"], 11)
self.assertFalse(payload_build["allow_shadow_paper"])
self.assertEqual(status_get, 200)
self.assertEqual(payload_get["status"], "awaiting_confirmation")
self.assertEqual(status_risk, 200)
+168
View File
@@ -155,6 +155,86 @@ class ApiStaticServerTests(unittest.TestCase):
"Handoff Bridge",
"Clear Workflow Bridge",
"provider health cards",
"provider health story",
"visual bridge story",
"surface history",
"surface history count",
"surface history recency",
"surface history cadence",
"surface cadence story",
"revisit queue count",
"cleared queue status",
"cleared queue story",
"resolution history count",
"latest acknowledged by",
"latest acknowledged note",
"surface revisit prompt",
"next revisit prompt",
"resolution review prompt",
"surface history story",
"Reopen Surface",
"Reopen Latest Surface",
"Reopen Revisit",
"acknowledged revisits",
"Acknowledge Revisit",
"Acknowledge With Note",
"Reopen Cleared Surface",
"resolution notes",
"resolution note gaps",
"note gap queue",
"note gap oldest",
"note gap avg age",
"resolution owners",
"owner spotlight",
"owner spotlight count",
"owner cadence",
"owner cadence avg gap",
"owner cadence story",
"owner trend",
"owner trend story",
"owner handoff",
"owner handoff story",
"owner handoff follow-up",
"owner handoff follow-up story",
"resolved owner drift",
"resolved owner drift story",
"resolved owner drift review",
"resolved owner drift review story",
"drift review queue",
"drift review queue story",
"drift review queue aging",
"drift review queue aging story",
"active resolution filter",
"All Resolved",
"With Resolution Note",
"Without Resolution Note",
"Add Resolution Note",
"Reopen Note Gap",
"owner spotlight prompt",
"priority lane status",
"priority lane count",
"priority lane story",
"priority lane carry-forward",
"priority lane carry-forward story",
"carry-forward aging",
"carry-forward aging story",
"follow-up lane status",
"follow-up lane count",
"follow-up lane story",
"follow-up lane rollover",
"follow-up lane rollover story",
"rollover handoff",
"rollover handoff story",
"priority lane prompt",
"rollover prompt",
"rollover handoff prompt",
"Open Priority Surface",
"Open Follow-Up Lane",
"Open Rollover Surface",
"Open Drift Review",
"Open Rollover Handoff",
"dominant surface",
"dominant surface story",
"Open Visual Bridge",
"Open Capture Surface",
"Open Screenshot Surface",
@@ -162,13 +242,85 @@ class ApiStaticServerTests(unittest.TestCase):
"Open Embed",
"Open Capture Target",
"Open Capture Embed",
"capture surface story",
"screenshot readiness",
"screenshot story",
"screenshot status",
"browser capture",
"export provenance",
"workflow export provenance",
"bundle manifest",
"manifest sections",
"workflow bundle manifest",
"workflow manifest sections",
"selection fingerprint",
"Read-only Live Analysis",
"read-only live analysis",
"ready_for_readonly_live_analysis",
"must_not_trade",
"No order submission",
"Not a buy/sell instruction",
"Execution remains disabled",
"live analysis assistant ready",
"readonly live health",
"broker/account context",
"latest decision packet timestamp",
"latest review packet timestamp",
"operator boundary",
"boundary summary",
"current model boundary",
"current model reason",
"live execution boundary",
"live execution reason",
"allowed operator actions",
"blocked operator actions",
"boundary next gate",
"boundary gate reason",
"decision support promotion",
"strict decision support ready",
"current decision support ready",
"promotion model",
"promotion classification",
"operator feedback samples",
"promotion criteria",
"failed promotion criteria",
"next promotion gate",
"promotion summary",
"shadow paper validation",
"shadow validation status",
"shadow observed days",
"shadow observation samples",
"shadow observation days",
"paper ticket samples",
"paper execution samples",
"operator observation feedback",
"Record Shadow Observation",
"Agree Shadow Observation",
"Disagree Shadow Observation",
"Build Shadow Paper",
"allow_shadow_paper",
"failed shadow criteria",
"next shadow gate",
"sample collection next actions",
"real live after shadow",
"shadow validation summary",
"shadow daily coverage",
"okx sandbox preflight",
"sandbox next gate",
"simulated trading header",
"submit dry-run",
"cancel dry-run",
"query order dry-run",
"query fills dry-run",
"idempotency key",
"error normalization",
"real live after sandbox",
"archive trend",
"archive trend story",
"archive baseline",
"archive baseline persistence",
"archive baseline delta",
"archive baseline delta story",
"Pin Baseline",
"Compare To Baseline",
"Save Baseline",
@@ -181,13 +333,29 @@ class ApiStaticServerTests(unittest.TestCase):
"workflow baseline",
"workflow trend story",
"workflow baseline persistence",
"archive operator overview",
"overview story",
"archive top profile",
"archive top kind",
"workflow top profile",
"workflow top session",
"workflow top phase",
"workflow baseline delta",
"workflow baseline delta story",
"digest baseline delta",
"digest baseline delta story",
"Profile Scope",
"Query Preset",
"Prefill Compare Replay Note",
"Persist Compare Replay Note",
"Export Handoff Starter",
"Export Artifact Bundle",
"Export Day Bundle",
"Export Session Bundle",
"Persist Bridge State",
"Prefill Saved Compare Note",
"bridge persistence",
"bridge saved compare note",
):
self.assertIn(expected, app_js)
@@ -43,6 +43,8 @@ class ArchiveLongRangeProjectionTests(unittest.TestCase):
self.assertEqual(baseline["latest_snapshot_id"], 5)
self.assertEqual(baseline["baseline_snapshot_id"], 1)
self.assertTrue(baseline["compare_ready"])
self.assertEqual(baseline["baseline_delta_status"], "changed")
self.assertIn("high-value delta", baseline["baseline_delta_story"])
def test_workflow_long_range_and_baseline_summary(self) -> None:
items = [
@@ -80,6 +82,8 @@ class ArchiveLongRangeProjectionTests(unittest.TestCase):
self.assertEqual(baseline["latest_snapshot_id"], 9)
self.assertEqual(baseline["baseline_snapshot_id"], 3)
self.assertTrue(baseline["suggested_compare_prompt"])
self.assertEqual(baseline["baseline_delta_status"], "changed")
self.assertIn("ready delta", baseline["baseline_delta_story"])
def test_empty_items_return_idle(self) -> None:
self.assertEqual(build_digest_archive_long_range_summary([])["status"], "idle")
@@ -0,0 +1,38 @@
import unittest
from src.services.operator.archive_retrieval_projection import (
build_digest_archive_ranked_aggregation,
build_workflow_archive_ranked_aggregation,
)
class ArchiveRetrievalProjectionTests(unittest.TestCase):
def test_digest_ranked_aggregation(self) -> None:
items = [
{"profile_key": "desk_b", "preferred_history_kind": "note"},
{"profile_key": "desk_a", "preferred_history_kind": "reminder"},
{"profile_key": "desk_a", "preferred_history_kind": "reminder"},
]
result = build_digest_archive_ranked_aggregation(items)
self.assertEqual(result["status"], "active")
self.assertEqual(result["top_profile"]["value"], "desk_a")
self.assertEqual(result["top_history_kind"]["value"], "reminder")
def test_workflow_ranked_aggregation(self) -> None:
items = [
{"profile_key": "desk_a", "current_session": "LONDON", "next_phase": "intraday", "next_artifact_kind": "intraday_digest"},
{"profile_key": "desk_a", "current_session": "LONDON", "next_phase": "intraday", "next_artifact_kind": "intraday_digest"},
{"profile_key": "desk_b", "current_session": "NY_AM", "next_phase": "post_trade", "next_artifact_kind": "post_trade_recap"},
]
result = build_workflow_archive_ranked_aggregation(items)
self.assertEqual(result["status"], "active")
self.assertEqual(result["top_profile"]["value"], "desk_a")
self.assertEqual(result["top_session"]["value"], "LONDON")
self.assertEqual(result["top_phase"]["value"], "intraday")
self.assertEqual(result["top_artifact"]["value"], "intraday_digest")
if __name__ == "__main__":
unittest.main()
@@ -18,7 +18,9 @@ OLD_TRAINING_DOC = ROOT / "docs" / "training_case_engine_m1_m6_product_readme.md
NONEXISTENT_VISUAL_PLAN_NAME = "AI_ICT_" + "视觉盯盘与主动告警增强方案_v1.md"
ACTIVE_TOP_LEVEL_DOCS = {
"AI_ICT_实盘辅助分析决策助手上线必做清单_2026-05-18_v1.md",
"AI_ICT_分析助手主线增强开发计划_2026-05-14_v1.md",
"AI_ICT_实盘辅助分析决策助手可用性评估与补齐方案_2026-05-17_v1.md",
"AI_ICT_全自动开发执行流程_v1.md",
"AI_ICT_实盘交易分析决策助手下一阶段开发方案_v1.md",
"AI_ICT_实盘交易分析系统开发计划_v1.md",
+114 -4
View File
@@ -15,6 +15,7 @@ class LaunchReadinessServiceTests(unittest.TestCase):
weekly_review_service=_WeeklyReviewService(),
trust_service=_TrustService(),
live_readiness_service=_LiveReadinessService(),
shadow_paper_validation_service=_ShadowPaperValidationService(),
)
with (
@@ -24,6 +25,11 @@ class LaunchReadinessServiceTests(unittest.TestCase):
payload = service.build_summary(instrument_id=1, timeframe="1m")
self.assertTrue(payload["analysis_only_ready"])
self.assertTrue(payload["live_analysis_assistant_ready"])
self.assertEqual(payload["live_analysis_assistant_summary"]["status"], "ready_for_readonly_live_analysis")
self.assertTrue(payload["live_analysis_assistant_summary"]["must_not_trade"])
self.assertFalse(payload["live_analysis_assistant_summary"]["execution_allowed"])
self.assertEqual(payload["readonly_live_health_summary"]["broker_account_context"], "available")
self.assertTrue(payload["assisted_trading_ready"])
self.assertTrue(payload["operator_soft_rollout_ready"])
self.assertEqual(payload["operator_soft_rollout_mode"], "execution_assist")
@@ -34,6 +40,20 @@ class LaunchReadinessServiceTests(unittest.TestCase):
self.assertIn("real_trade_sample_verified", payload["warning_reasons"])
self.assertIn("real_trade_sample_verified", payload["external_waiting_items"])
self.assertFalse(payload["real_live_ready"])
self.assertEqual(payload["decision_support_boundary_summary"]["status"], "execution_assist_available")
self.assertIn("review_market_context", payload["decision_support_boundary_summary"]["allowed_operator_actions"])
self.assertIn("submit_live_orders", payload["decision_support_boundary_summary"]["blocked_operator_actions"])
self.assertEqual(
payload["decision_support_promotion_summary"]["status"],
"ready_for_decision_support_non_execution",
)
self.assertTrue(payload["decision_support_promotion_summary"]["strict_decision_support_ready"])
self.assertEqual(
payload["decision_support_promotion_summary"]["execution_boundary_after_promotion"],
"execution_still_blocked",
)
self.assertEqual(payload["shadow_paper_validation_summary"]["status"], "collecting_paper_samples")
self.assertEqual(payload["shadow_paper_validation_summary"]["next_validation_gate"], "paper_ticket_sample_gate")
def test_build_summary_defers_real_trade_sample_gate_for_analysis_decision_phase(self) -> None:
service = LaunchReadinessService(
@@ -42,6 +62,7 @@ class LaunchReadinessServiceTests(unittest.TestCase):
weekly_review_service=_WeeklyReviewService(),
trust_service=_TrustService(),
live_readiness_service=_LiveReadinessService(),
shadow_paper_validation_service=_ShadowPaperValidationService(),
)
with (
@@ -57,6 +78,36 @@ class LaunchReadinessServiceTests(unittest.TestCase):
self.assertNotIn("real_trade_sample_verified", payload["warning_reasons"])
self.assertNotIn("real_trade_sample_verified", payload["external_waiting_items"])
self.assertNotIn("wait_for_and_verify_real_fill_sample", payload["next_actions"])
self.assertEqual(payload["decision_support_boundary_summary"]["live_execution_boundary"], "live_submit_blocked")
self.assertIn("decision_support_promotion_summary", payload)
self.assertIn("shadow_paper_validation_summary", payload)
def test_live_analysis_assistant_ready_when_account_context_unavailable(self) -> None:
service = LaunchReadinessService(
market_state_service=_MarketStateService(),
broker_read_model_service=_BrokerReadService(status="broker_state_unavailable"),
weekly_review_service=_WeeklyReviewService(),
trust_service=_TrustService(),
live_readiness_service=_LiveReadinessService(),
shadow_paper_validation_service=_ShadowPaperValidationService(),
)
with patch("src.services.readiness.launch_readiness_service.get_trading_runtime_service", return_value=_RuntimeService()):
payload = service.build_summary(instrument_id=1, timeframe="1m")
self.assertFalse(payload["analysis_only_ready"])
self.assertTrue(payload["live_analysis_assistant_ready"])
self.assertEqual(payload["product_operating_mode"], "readonly_live_analysis")
self.assertEqual(payload["readonly_account_context_status"], "unavailable")
self.assertIn("broker_state_ready", payload["blocking_reasons"])
self.assertIn("account_context_unavailable", payload["live_analysis_assistant_summary"]["warnings"])
self.assertEqual(payload["live_analysis_assistant_summary"]["blockers"], [])
self.assertEqual(payload["decision_support_boundary_summary"]["status"], "readonly_live_analysis_available")
self.assertIn("review_market_context", payload["decision_support_boundary_summary"]["allowed_operator_actions"])
self.assertIn("submit_live_orders", payload["live_analysis_assistant_summary"]["blocked_actions"])
self.assertFalse(payload["decision_support_ready"])
self.assertFalse(payload["execution_assist_ready"])
self.assertFalse(payload["real_live_ready"])
def test_build_summary_exposes_restricted_soft_rollout_when_only_blacklist_models_exist(self) -> None:
service = LaunchReadinessService(
@@ -65,6 +116,7 @@ class LaunchReadinessServiceTests(unittest.TestCase):
weekly_review_service=_WeeklyReviewService(),
trust_service=_BlacklistOnlyTrustService(),
live_readiness_service=_LiveReadinessService(),
shadow_paper_validation_service=_ShadowPaperValidationService(),
)
with patch("src.services.readiness.launch_readiness_service.get_trading_runtime_service", return_value=_RuntimeService()):
@@ -78,6 +130,17 @@ class LaunchReadinessServiceTests(unittest.TestCase):
self.assertIn("start_restricted_decision_support_soft_rollout", payload["next_actions"])
self.assertIn("execution_assist_remains_disabled", payload["operator_soft_rollout_boundary_notes"])
self.assertNotIn("trust_model_available", payload["warning_reasons"])
self.assertEqual(payload["decision_support_boundary_summary"]["status"], "restricted_assisted_analysis_available")
self.assertEqual(payload["decision_support_boundary_summary"]["current_model_boundary"], "observe_only_current_model")
self.assertEqual(
payload["decision_support_boundary_summary"]["next_boundary_gate"],
"promote_model_out_of_observe_only_or_blacklist",
)
self.assertEqual(payload["decision_support_promotion_summary"]["status"], "needs_trust_promotion")
self.assertIn(
"decision_support_trust_candidate",
payload["decision_support_promotion_summary"]["failed_criteria"],
)
class _MarketState:
@@ -100,25 +163,46 @@ class _MarketStateService:
class _BrokerState:
status = "broker_state_ok"
def __init__(self, status: str = "broker_state_ok") -> None:
self.status = status
def to_dict(self):
return {"status": "broker_state_ok"}
return {"status": self.status}
class _BrokerReadService:
def __init__(self, status: str = "broker_state_ok") -> None:
self.status = status
def fetch_snapshot(self, **kwargs):
return _BrokerState()
return _BrokerState(self.status)
class _WeeklyReviewService:
def build_summary(self, **kwargs):
return {"real_trade_count": 0}
return {
"real_trade_count": 0,
"entry_count": 5,
"framework_execution_rate": 0.8,
"risk_violation_count": 0,
}
class _TrustService:
def build_summary(self, **kwargs):
return {
"models": [
{
"model_code": "OSOK",
"classification": "whitelist",
"sample_size": 6,
"quality_gate_passed": True,
"expectancy_r": 0.4,
"framework_execution_rate": 0.8,
"operator_mode": "trade_inside_framework",
"reasons": ["backtest_quality_gate_passed"],
}
],
"whitelist": ["OSOK"],
"observe_list": [],
"blacklist": [],
@@ -132,6 +216,18 @@ class _TrustService:
class _BlacklistOnlyTrustService:
def build_summary(self, **kwargs):
return {
"models": [
{
"model_code": "SWEEP_MSS_FVG",
"classification": "blacklist",
"sample_size": 1,
"quality_gate_passed": False,
"expectancy_r": 0.0,
"framework_execution_rate": 0.2,
"operator_mode": "observe_only",
"reasons": ["backtest_quality_gate_failed"],
}
],
"whitelist": [],
"observe_list": [],
"blacklist": ["SWEEP_MSS_FVG"],
@@ -152,5 +248,19 @@ class _RuntimeService:
return {"scheduler_state": "running", "current_blockers": []}
class _ShadowPaperValidationService:
def build_summary(self, **kwargs):
return {
"status": "collecting_paper_samples",
"next_validation_gate": "paper_ticket_sample_gate",
"execution_boundary": "real_live_still_blocked",
"candidate_count": 3,
"recent_samples": {
"candidates": [{"created_ts": "2026-05-18T01:00:00+00:00"}],
"shadow_observations": [{"observed_ts": "2026-05-18T01:05:00+00:00"}],
},
}
if __name__ == "__main__":
unittest.main()
+13
View File
@@ -15,6 +15,7 @@ class LiveReadinessServiceTests(unittest.TestCase):
execution_repository=_ExecutionRepo(),
decision_packet_service=_DecisionPacketService(),
trust_service=_TrustService(),
okx_sandbox_preflight_service=_OKXSandboxPreflightService(),
)
with patch("src.services.broker.live_readiness_service.get_trading_runtime_service", return_value=_RuntimeService()):
@@ -22,6 +23,8 @@ class LiveReadinessServiceTests(unittest.TestCase):
self.assertFalse(payload["ready_for_real_live"])
self.assertIn("adapter_real_live_not_ready", payload["blocking_reasons"])
self.assertIn("okx_sandbox_preflight_summary", payload)
self.assertEqual(payload["okx_sandbox_preflight_summary"]["status"], "sandbox_preflight_ready")
self.assertEqual(payload["operator_confirmation_protocol"]["protocol_version"], "live-confirmation-v1")
self.assertEqual(payload["live_order_schema"]["schema_version"], "live-order-v1")
@@ -79,5 +82,15 @@ class _RuntimeService:
return {"scheduler_state": "running", "current_blockers": []}
class _OKXSandboxPreflightService:
def build_summary(self, **kwargs):
return {
"status": "sandbox_preflight_ready",
"dry_run_ready": True,
"next_gate": "sandbox_submit_cancel_reconcile_smoke",
"execution_boundary": "real_live_still_blocked",
}
if __name__ == "__main__":
unittest.main()
+36
View File
@@ -96,6 +96,42 @@ class MarketDataStreamServiceTests(unittest.TestCase):
self.assertTrue(gap_state["is_gap"])
self.assertEqual(gap_state["code"], DATA_GAP_CODE_DETECTED)
def test_stream_service_falls_back_to_direct_when_proxy_attempt_never_connects(self) -> None:
calls = []
service = None
class _FakeWebSocket:
def __init__(self, *args, **kwargs):
pass
def run_forever(self, **kwargs):
calls.append(dict(kwargs))
if len(calls) >= 2:
service._stop_event.set()
def close(self):
return None
service = MarketDataStreamService(
url="wss://example.test",
proxy_url="http://127.0.0.1:1080",
reconnect_backoff_seconds=1,
ws_factory=_FakeWebSocket,
)
service.subscribe(
instrument_id=1,
symbol="BTC-USDT",
inst_id="BTC-USDT-SWAP",
timeframe="1m",
)
service._run_loop()
self.assertEqual(len(calls), 2)
self.assertEqual(calls[0]["http_proxy_host"], "127.0.0.1")
self.assertEqual(calls[0]["http_proxy_port"], 1080)
self.assertNotIn("http_proxy_host", calls[1])
class EconomicCalendarProviderTests(unittest.TestCase):
def test_provider_marks_active_high_impact_event(self) -> None:
@@ -0,0 +1,219 @@
import os
import unittest
from io import BytesIO
from datetime import datetime, timezone
from urllib.error import HTTPError
from unittest.mock import patch
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://user:pass@localhost:5432/ai_ict_test")
from src.config.settings import settings
from src.services.broker.okx_sandbox_preflight_service import (
OKXSandboxPreflightService,
build_okx_client_order_id,
normalize_okx_order_error,
)
class OKXSandboxPreflightServiceTests(unittest.TestCase):
def test_build_summary_waits_for_sandbox_credentials_without_live_submit(self) -> None:
service = OKXSandboxPreflightService(api_key="", api_secret="", api_passphrase="")
with patch.object(settings, "live_trading_enabled", False):
payload = service.build_summary(
instrument_id=1,
timeframe="1m",
now=datetime(2026, 5, 17, 1, 2, 3, tzinfo=timezone.utc),
)
self.assertEqual(payload["status"], "waiting_for_sandbox_credentials")
self.assertFalse(payload["dry_run_ready"])
self.assertFalse(payload["sandbox_credentials_present"])
self.assertEqual(payload["sandbox_header"], {"x-simulated-trading": "1"})
self.assertEqual(payload["next_gate"], "okx_sandbox_credentials")
self.assertEqual(payload["execution_boundary"], "real_live_still_blocked")
self.assertIn("okx_sandbox_credentials_present", payload["failed_criteria"])
self.assertEqual(payload["signed_request_previews"][0]["operation"], "submit_order")
self.assertFalse(payload["signed_request_previews"][0]["signed"])
def test_build_summary_builds_signed_dry_run_request_previews(self) -> None:
service = OKXSandboxPreflightService(api_key="key", api_secret="secret", api_passphrase="pass")
with patch.object(settings, "live_trading_enabled", False):
payload = service.build_summary(
instrument_id=2,
timeframe="5m",
now=datetime(2026, 5, 17, 1, 2, 3, tzinfo=timezone.utc),
)
self.assertEqual(payload["status"], "sandbox_preflight_ready")
self.assertTrue(payload["dry_run_ready"])
self.assertTrue(payload["submit_dry_run_ready"])
self.assertTrue(payload["cancel_dry_run_ready"])
self.assertTrue(payload["query_order_dry_run_ready"])
self.assertTrue(payload["query_fills_dry_run_ready"])
self.assertEqual(payload["okx_inst_id"], "ETH-USDT-SWAP")
self.assertEqual(payload["next_gate"], "sandbox_submit_cancel_reconcile_smoke")
operations = {item["operation"]: item for item in payload["signed_request_previews"]}
self.assertIn("OK-ACCESS-SIGN", operations["submit_order"]["header_names"])
self.assertIn("x-simulated-trading", operations["cancel_order"]["header_names"])
self.assertTrue(operations["query_order"]["request_path"].startswith("/api/v5/trade/order?"))
self.assertEqual(payload["bracket_semantics"]["schema_version"], "bracket-v1")
self.assertIn("insufficient_balance", payload["error_normalization"])
def test_live_switch_enabled_keeps_preflight_in_operator_review(self) -> None:
service = OKXSandboxPreflightService(api_key="key", api_secret="secret", api_passphrase="pass")
with patch.object(settings, "live_trading_enabled", True):
payload = service.build_summary(
instrument_id=1,
timeframe="1m",
now=datetime(2026, 5, 17, 1, 2, 3, tzinfo=timezone.utc),
)
self.assertEqual(payload["status"], "live_switch_enabled_requires_operator_review")
self.assertIn("okx_live_submit_still_disabled", payload["failed_criteria"])
self.assertEqual(payload["execution_boundary"], "real_live_still_blocked")
def test_client_order_id_is_bounded_and_deterministic(self) -> None:
value = build_okx_client_order_id(
instrument_id=12,
timeframe="15m",
observed_ts=datetime(2026, 5, 17, 1, 2, 3, tzinfo=timezone.utc),
)
self.assertEqual(value, "AI1215M20260517010203")
self.assertLessEqual(len(value), 32)
def test_normalizes_okx_order_errors(self) -> None:
self.assertEqual(
normalize_okx_order_error(transport_error="connection refused")["category"],
"network_error",
)
self.assertEqual(
normalize_okx_order_error(payload={"code": "51008", "msg": "Insufficient balance"})["category"],
"insufficient_balance",
)
self.assertEqual(
normalize_okx_order_error(payload={"code": "51001", "msg": "Parameter error"})["category"],
"parameter_error",
)
self.assertEqual(
normalize_okx_order_error(payload={"code": "50120", "msg": "This API key doesn't have permission to use this function"})["category"],
"permission_error",
)
self.assertEqual(
normalize_okx_order_error(payload={"code": "51100", "msg": "Rejected"})["category"],
"exchange_rejected",
)
def test_isolated_smoke_requires_explicit_execute_flag(self) -> None:
service = OKXSandboxPreflightService(api_key="key", api_secret="secret", api_passphrase="pass")
calls = []
def opener(*args, **kwargs):
calls.append((args, kwargs))
raise AssertionError("opener should not be called when execute is false")
with patch.object(settings, "live_trading_enabled", False):
payload = service.run_isolated_smoke(
instrument_id=1,
timeframe="1m",
execute=False,
now=datetime(2026, 5, 17, 1, 2, 3, tzinfo=timezone.utc),
opener=opener,
)
self.assertEqual(payload["status"], "blocked")
self.assertEqual(payload["blockers"], ["okx_sandbox_smoke_execute_not_enabled"])
self.assertEqual(payload["next_gate"], "enable_explicit_sandbox_smoke_execute")
self.assertEqual(payload["steps"], [])
self.assertEqual(calls, [])
def test_isolated_smoke_passes_submit_query_cancel_fills_sequence(self) -> None:
service = OKXSandboxPreflightService(api_key="key", api_secret="secret", api_passphrase="pass")
seen_operations = []
def opener(request, timeout_seconds):
seen_operations.append((request.get_method(), request.selector, timeout_seconds))
if request.selector.startswith("/api/v5/trade/order?"):
return _FakeHTTPResponse({"code": "0", "msg": "", "data": [{"instId": "BTC-USDT-SWAP", "ordId": "123", "state": "live"}]})
if request.selector == "/api/v5/trade/order":
return _FakeHTTPResponse({"code": "0", "msg": "", "data": [{"ordId": "123", "clOrdId": "AI0101M20260517010203", "sCode": "0"}]})
if request.selector == "/api/v5/trade/cancel-order":
return _FakeHTTPResponse({"code": "0", "msg": "", "data": [{"ordId": "123", "clOrdId": "AI0101M20260517010203", "sCode": "0"}]})
if request.selector.startswith("/api/v5/trade/fills?"):
return _FakeHTTPResponse({"code": "0", "msg": "", "data": []})
raise AssertionError(f"unexpected request {request.get_method()} {request.selector}")
with patch.object(settings, "live_trading_enabled", False):
payload = service.run_isolated_smoke(
instrument_id=1,
timeframe="1m",
execute=True,
order_price="1",
order_size="1",
now=datetime(2026, 5, 17, 1, 2, 3, tzinfo=timezone.utc),
timeout_seconds=7,
opener=opener,
)
self.assertEqual(payload["status"], "passed")
self.assertEqual([item["operation"] for item in payload["steps"]], [
"submit_order",
"query_order_after_submit",
"cancel_order",
"query_order_after_cancel",
"query_fills",
])
self.assertEqual([item[2] for item in seen_operations], [7, 7, 7, 7, 7])
self.assertTrue(all(item["simulated_trading_header"] == "1" for item in payload["steps"]))
self.assertEqual(payload["execution_boundary"], "real_live_still_blocked")
self.assertEqual(payload["reconcile_check"]["status"], "ready")
def test_isolated_smoke_normalizes_http_error(self) -> None:
service = OKXSandboxPreflightService(api_key="key", api_secret="secret", api_passphrase="pass")
def opener(request, timeout_seconds):
raise HTTPError(
url=request.full_url,
code=400,
msg="Bad Request",
hdrs=None,
fp=BytesIO(b'{"code":"51001","msg":"Parameter error"}'),
)
with patch.object(settings, "live_trading_enabled", False):
payload = service.run_isolated_smoke(
instrument_id=1,
timeframe="1m",
execute=True,
now=datetime(2026, 5, 17, 1, 2, 3, tzinfo=timezone.utc),
opener=opener,
)
self.assertEqual(payload["status"], "blocked")
self.assertEqual(payload["blockers"], ["okx_sandbox_submit_failed"])
self.assertEqual(payload["steps"][0]["http_status"], 400)
self.assertEqual(payload["steps"][0]["normalized_error"]["category"], "parameter_error")
class _FakeHTTPResponse:
def __init__(self, payload: dict, status: int = 200) -> None:
self.payload = payload
self.status = status
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb) -> bool:
return False
def read(self) -> bytes:
import json
return json.dumps(self.payload).encode("utf-8")
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,86 @@
import contextlib
import io
import json
import tempfile
import unittest
from pathlib import Path
from unittest.mock import Mock, patch
from scripts import run_okx_sandbox_smoke
class OKXSandboxSmokeScriptTests(unittest.TestCase):
def test_main_writes_passed_json_artifact(self) -> None:
payload = {
"status": "passed",
"execute_requested": True,
"blockers": [],
"steps": [{"operation": "submit_order", "ok": True}],
}
service = Mock()
service.run_isolated_smoke.return_value = payload
output = io.StringIO()
with tempfile.TemporaryDirectory() as tmpdir:
artifact = Path(tmpdir) / "smoke.json"
with patch("scripts.run_okx_sandbox_smoke.OKXSandboxPreflightService", return_value=service):
with contextlib.redirect_stdout(output):
exit_code = run_okx_sandbox_smoke.main(
env={
"OKX_SANDBOX_SMOKE_OUTPUT_JSON": "1",
"OKX_SANDBOX_SMOKE_EXECUTE": "1",
"OKX_SANDBOX_SMOKE_INSTRUMENT_ID": "2",
"OKX_SANDBOX_SMOKE_TIMEFRAME": "5m",
"OKX_SANDBOX_SMOKE_PRICE": "1.2",
"OKX_SANDBOX_SMOKE_SIZE": "3",
"OKX_SANDBOX_SMOKE_SIDE": "sell",
"OKX_SANDBOX_SMOKE_ORDER_TYPE": "limit",
"OKX_SANDBOX_SMOKE_TD_MODE": "cross",
"OKX_SANDBOX_SMOKE_TIMEOUT_SECONDS": "4.5",
"OKX_SANDBOX_SMOKE_ARTIFACT_PATH": str(artifact),
}
)
self.assertEqual(exit_code, 0)
self.assertEqual(json.loads(output.getvalue())["status"], "passed")
self.assertEqual(json.loads(artifact.read_text(encoding="utf-8"))["status"], "passed")
service.run_isolated_smoke.assert_called_once_with(
instrument_id=2,
timeframe="5m",
execute=True,
order_price="1.2",
order_size="3",
order_side="sell",
order_type="limit",
td_mode="cross",
timeout_seconds=4.5,
)
def test_main_returns_nonzero_for_blocked_smoke(self) -> None:
payload = {
"status": "blocked",
"execute_requested": False,
"blockers": ["okx_sandbox_smoke_execute_not_enabled"],
"steps": [],
}
service = Mock()
service.run_isolated_smoke.return_value = payload
output = io.StringIO()
with tempfile.TemporaryDirectory() as tmpdir:
artifact = Path(tmpdir) / "smoke.json"
with patch("scripts.run_okx_sandbox_smoke.OKXSandboxPreflightService", return_value=service):
with contextlib.redirect_stdout(output):
exit_code = run_okx_sandbox_smoke.main(
env={
"OKX_SANDBOX_SMOKE_ARTIFACT_PATH": str(artifact),
}
)
self.assertEqual(exit_code, 2)
self.assertIn("okx_sandbox_smoke_execute_not_enabled", output.getvalue())
self.assertEqual(json.loads(artifact.read_text(encoding="utf-8"))["status"], "blocked")
if __name__ == "__main__":
unittest.main()
@@ -203,11 +203,18 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
)
self.assertEqual(summary["workflow_artifact_summary"]["workflow_archive_bridge_summary"]["status"], "active")
self.assertEqual(summary["workflow_artifact_summary"]["workflow_archive_bridge_persistence_summary"]["status"], "persisted")
self.assertEqual(
summary["workflow_artifact_summary"]["workflow_archive_bridge_persistence_summary"]["compare_writeback_summary"]["note_title"],
"Saved compare note",
)
self.assertIn("long_range_trend_summary", summary["workflow_artifact_summary"]["workflow_history_archive"])
self.assertIn("baseline_pin_summary", summary["workflow_artifact_summary"]["workflow_history_archive"])
self.assertIn("ranked_aggregation_summary", summary["workflow_artifact_summary"]["workflow_history_archive"])
self.assertEqual(summary["workflow_artifact_summary"]["workflow_history_archive"]["long_range_trend_summary"]["status"], "active")
self.assertEqual(summary["workflow_artifact_summary"]["workflow_history_archive"]["baseline_pin_summary"]["status"], "insufficient_history")
self.assertEqual(summary["workflow_artifact_summary"]["workflow_history_archive"]["baseline_pin_summary"]["persisted_baseline_snapshot_id"], 10)
self.assertIn("baseline_delta_story", summary["workflow_artifact_summary"]["workflow_history_archive"]["baseline_pin_summary"])
self.assertEqual(summary["workflow_artifact_summary"]["workflow_history_archive"]["ranked_aggregation_summary"]["status"], "active")
self.assertIn("daily_groups", summary["workflow_artifact_summary"]["workflow_history_archive"])
self.assertIn("session_groups", summary["workflow_artifact_summary"]["workflow_history_archive"])
self.assertIn("bridge_summary", summary["workflow_artifact_summary"]["workflow_history_archive"])
@@ -396,6 +403,8 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
self.assertIn("digest_history_archive", summary["digest_surface"])
self.assertIn("digest_history_archive_count", summary["digest_surface"])
self.assertIn("digest_archive_compare", summary["digest_surface"])
self.assertIn("archive_operator_overview", summary)
self.assertEqual(summary["archive_operator_overview"]["status"], "active")
self.assertIn("focus_related_reminders", summary["digest_surface"])
self.assertGreaterEqual(summary["digest_surface"]["focus_related_reminder_count"], 1)
self.assertIsNotNone(summary["digest_surface"]["next_focus_related_reminder"])
@@ -403,9 +412,14 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
self.assertIn("next_digest_history_archive_review", summary["digest_surface"])
self.assertIn("long_range_trend_summary", summary["digest_surface"]["digest_history_archive"])
self.assertIn("baseline_pin_summary", summary["digest_surface"]["digest_history_archive"])
self.assertIn("ranked_aggregation_summary", summary["digest_surface"]["digest_history_archive"])
self.assertEqual(summary["digest_surface"]["digest_history_archive"]["long_range_trend_summary"]["status"], "active")
self.assertEqual(summary["digest_surface"]["digest_history_archive"]["baseline_pin_summary"]["status"], "insufficient_history")
self.assertEqual(summary["digest_surface"]["digest_history_archive"]["baseline_pin_summary"]["persisted_baseline_snapshot_id"], 9)
self.assertIn("baseline_delta_story", summary["digest_surface"]["digest_history_archive"]["baseline_pin_summary"])
self.assertEqual(summary["digest_surface"]["digest_history_archive"]["ranked_aggregation_summary"]["status"], "active")
self.assertIn("digest_baseline_delta_story", summary["archive_operator_overview"])
self.assertIn("workflow_baseline_delta_story", summary["archive_operator_overview"])
self.assertIn("next_high_value_change", summary["digest_surface"])
self.assertIn("digest_review_back_actions", summary["digest_surface"])
self.assertIn("digest_review_back_action_count", summary["digest_surface"])
@@ -653,14 +667,67 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
self.assertGreaterEqual(summary["external_context_summary"]["provider_health_card_count"], 3)
self.assertIn("provider_health_summary", summary["external_context_summary"])
self.assertEqual(summary["external_context_summary"]["provider_health_summary"]["status"], "active")
self.assertIn("health_story", summary["external_context_summary"]["provider_health_summary"])
self.assertIn("visual_evidence_bridge_summary", summary["external_context_summary"])
self.assertEqual(summary["external_context_summary"]["visual_evidence_bridge_summary"]["status"], "active")
self.assertIn("bridge_story", summary["external_context_summary"]["visual_evidence_bridge_summary"])
self.assertIn("capture_surface_cards", summary["external_context_summary"])
self.assertGreaterEqual(summary["external_context_summary"]["capture_surface_card_count"], 3)
self.assertIn("capture_surface_summary", summary["external_context_summary"])
self.assertEqual(summary["external_context_summary"]["capture_surface_summary"]["status"], "active")
self.assertIn("capture_story", summary["external_context_summary"]["capture_surface_summary"])
self.assertIn("decision_support_boundary_summary", summary)
self.assertEqual(summary["decision_support_boundary_summary"]["label"], "Decision support available")
self.assertIn("operator_summary", summary["decision_support_boundary_summary"])
self.assertIn("live_analysis_assistant_summary", summary)
self.assertEqual(summary["live_analysis_assistant_summary"]["status"], "ready_for_readonly_live_analysis")
self.assertTrue(summary["evidence_summary"]["live_analysis_assistant_ready"])
self.assertTrue(summary["evidence_summary"]["must_not_trade"])
self.assertEqual(summary["readonly_live_health_summary"]["status"], "usable")
self.assertIn("decision_support_promotion_summary", summary)
self.assertEqual(
summary["decision_support_promotion_summary"]["status"],
"ready_for_decision_support_non_execution",
)
self.assertEqual(summary["evidence_summary"]["promotion_status"], "ready_for_decision_support_non_execution")
self.assertIn("shadow_paper_validation_summary", summary)
self.assertEqual(summary["shadow_paper_validation_summary"]["status"], "collecting_paper_samples")
self.assertEqual(summary["evidence_summary"]["shadow_paper_status"], "collecting_paper_samples")
self.assertEqual(summary["evidence_summary"]["next_shadow_paper_gate"], "paper_ticket_sample_gate")
self.assertIn("screenshot_readiness_summary", summary["external_context_summary"])
self.assertEqual(summary["external_context_summary"]["screenshot_readiness_summary"]["status"], "active")
self.assertIn("screenshot_story", summary["external_context_summary"]["screenshot_readiness_summary"])
self.assertIn("surface_selection_history_summary", summary["external_context_summary"])
self.assertEqual(summary["external_context_summary"]["surface_selection_history_summary"]["status"], "active")
self.assertEqual(summary["external_context_summary"]["surface_selection_history_summary"]["entry_count"], 1)
self.assertEqual(summary["external_context_summary"]["surface_selection_history_summary"]["dominant_surface_title"], "binance_public_chart_reference")
self.assertEqual(summary["external_context_summary"]["surface_selection_history_summary"]["dominant_source_group"], "visual_reference_card")
self.assertIn("recency_status", summary["external_context_summary"]["surface_selection_history_summary"])
self.assertIn("revisit_prompt", summary["external_context_summary"]["surface_selection_history_summary"])
self.assertIn("cadence_status", summary["external_context_summary"]["surface_selection_history_summary"])
self.assertIn("revisit_queue_count", summary["external_context_summary"]["surface_selection_history_summary"])
self.assertEqual(summary["external_context_summary"]["surface_selection_history_summary"]["acknowledged_count"], 1)
self.assertEqual(summary["external_context_summary"]["surface_selection_history_summary"]["resolution_history_count"], 1)
self.assertEqual(summary["external_context_summary"]["surface_selection_history_summary"]["cleared_queue_status"], "active")
self.assertEqual(summary["external_context_summary"]["surface_selection_history_summary"]["resolution_filter_summary"]["note_count"], 1)
self.assertEqual(summary["external_context_summary"]["surface_selection_history_summary"]["resolution_filter_summary"]["acknowledged_by_counts"]["local_operator"], 1)
self.assertIn("note_gap_queue_count", summary["external_context_summary"]["surface_selection_history_summary"]["resolution_filter_summary"])
self.assertIn("owner_spotlight", summary["external_context_summary"]["surface_selection_history_summary"]["resolution_filter_summary"])
self.assertIn("owner_cadence_status", summary["external_context_summary"]["surface_selection_history_summary"]["resolution_filter_summary"])
self.assertIn("note_gap_priority_lane_status", summary["external_context_summary"]["surface_selection_history_summary"]["resolution_filter_summary"])
self.assertIn("owner_trend_status", summary["external_context_summary"]["surface_selection_history_summary"]["resolution_filter_summary"])
self.assertIn("note_gap_priority_lane_story", summary["external_context_summary"]["surface_selection_history_summary"]["resolution_filter_summary"])
self.assertIn("owner_handoff_status", summary["external_context_summary"]["surface_selection_history_summary"]["resolution_filter_summary"])
self.assertIn("note_gap_priority_lane_carry_forward_status", summary["external_context_summary"]["surface_selection_history_summary"]["resolution_filter_summary"])
self.assertIn("owner_handoff_follow_up_status", summary["external_context_summary"]["surface_selection_history_summary"]["resolution_filter_summary"])
self.assertIn("note_gap_priority_lane_carry_forward_aging_status", summary["external_context_summary"]["surface_selection_history_summary"]["resolution_filter_summary"])
self.assertIn("resolved_owner_drift_status", summary["external_context_summary"]["surface_selection_history_summary"]["resolution_filter_summary"])
self.assertIn("carry_forward_follow_up_lane_status", summary["external_context_summary"]["surface_selection_history_summary"]["resolution_filter_summary"])
self.assertIn("resolved_owner_drift_review_status", summary["external_context_summary"]["surface_selection_history_summary"]["resolution_filter_summary"])
self.assertIn("resolved_owner_drift_review_queue_status", summary["external_context_summary"]["surface_selection_history_summary"]["resolution_filter_summary"])
self.assertIn("resolved_owner_drift_review_queue_aging_status", summary["external_context_summary"]["surface_selection_history_summary"]["resolution_filter_summary"])
self.assertIn("carry_forward_follow_up_lane_rollover_status", summary["external_context_summary"]["surface_selection_history_summary"]["resolution_filter_summary"])
self.assertIn("carry_forward_follow_up_lane_rollover_handoff_status", summary["external_context_summary"]["surface_selection_history_summary"]["resolution_filter_summary"])
self.assertIn("visual_reference_cards", summary["external_context_summary"])
self.assertGreaterEqual(summary["external_context_summary"]["visual_reference_card_count"], 3)
self.assertIsNotNone(summary["external_context_summary"]["visual_reference_next_prompt"])
@@ -775,11 +842,92 @@ class _LaunchReadinessService:
"instrument_id": instrument_id,
"timeframe": timeframe,
"analysis_only_ready": True,
"live_analysis_assistant_ready": True,
"decision_support_ready": True,
"blocking_reasons": [],
"warning_reasons": ["trust_observe_mode"],
"next_actions": ["review_priority_setup"],
"product_operating_mode": "decision_support_only",
"readonly_account_context_status": "available",
"readonly_live_health_summary": {
"status": "usable",
"api_health": "available",
"runtime_health": "available",
"market_freshness": "market_data_fresh",
"market_stream": "market_stream_connected",
"broker_account_context": "available",
"latest_decision_packet_timestamp": "2026-05-18T01:05:00+00:00",
"latest_review_packet_timestamp": "2026-05-18T01:05:00+00:00",
"current_candidate_count": 3,
"current_blocker": "",
"recommendation": "can_use_for_live_market_analysis",
},
"live_analysis_assistant_summary": {
"status": "ready_for_readonly_live_analysis",
"ready_for_readonly_live_analysis": True,
"must_not_trade": True,
"execution_allowed": False,
"launch_boundary": {
"mode_label": "Read-only Live Analysis",
"not_buy_sell_instruction": True,
"no_order_submission": True,
"execution_remains_disabled": True,
},
"health_summary": {
"status": "usable",
"market_freshness": "market_data_fresh",
"broker_account_context": "available",
},
"allowed_actions": ["review_market_context", "record_shadow_observation_or_operator_note"],
"blocked_actions": ["submit_live_orders", "live_execute"],
},
"decision_support_boundary_summary": {
"status": "decision_support_available",
"label": "Decision support available",
"operator_summary": "Decision support is available for human review, but execution assist and live submit remain gated.",
"current_model_boundary": "decision_support_candidate_available",
"current_model_reason": "decision_support_list_present",
"live_execution_boundary": "live_submit_blocked",
"live_execution_reason": "adapter_real_live_not_ready",
"allowed_operator_actions": ["review_market_context", "use_decision_support_for_manual_trade_review"],
"blocked_operator_actions": ["submit_live_orders", "enable_default_auto_trading"],
"next_boundary_gate": "execution_assist_trust_gate",
"next_boundary_gate_reason": "execution_candidate_list_missing",
},
"decision_support_promotion_summary": {
"status": "ready_for_decision_support_non_execution",
"strict_decision_support_ready": True,
"current_decision_support_ready": True,
"execution_assist_ready": False,
"execution_boundary_after_promotion": "execution_still_blocked",
"real_live_boundary_after_promotion": "real_live_still_blocked",
"candidate_model_code": "OSOK",
"candidate_classification": "observe",
"operator_review_sample_count": 5,
"passed_criteria_count": 8,
"required_criteria_count": 8,
"failed_criteria": [],
"next_promotion_gate": "decision_support_ready_can_be_true_without_execution",
"operator_summary": "Decision support can be marked ready for human review; execution assist and live submit remain blocked.",
},
"shadow_paper_validation_summary": {
"status": "collecting_paper_samples",
"observed_shadow_day_count": 3,
"window_days": 14,
"candidate_count": 6,
"paper_ticket_count": 1,
"paper_execution_count": 1,
"operator_feedback_count": 2,
"journal_entry_count": 1,
"risk_violation_count": 0,
"passed_criteria_count": 4,
"required_criteria_count": 8,
"failed_criteria": ["paper_ticket_sample_gate"],
"next_validation_gate": "paper_ticket_sample_gate",
"execution_boundary": "real_live_still_blocked",
"operator_summary": "Shadow/paper validation is collecting paper samples.",
"daily_coverage": [],
},
"market_state": {
"session": {"session_code": "LONDON", "status": "active"},
"freshness": {"code": "market_data_fresh"},
@@ -985,9 +1133,24 @@ class _DecisionAssistantRepo:
"handoff_packet_starter": {"headline": "Saved handoff starter"},
"replay_note_bridge_summary": {"note_title": "Saved replay note"},
"artifact_bundle_summary": {"starter_export_prompt": "Saved artifact bundle"},
"compare_writeback_summary": {"note_title": "Saved compare note", "status": "persisted"},
"query_context": {"profile_scope": "selected_profile", "query_preset": "intraday_recent"},
"updated_ts": "2026-05-12T02:04:30+00:00",
},
"external_context_surface_history": [
{
"surface_id": "binance_public_chart_reference",
"title": "binance_public_chart_reference",
"source_group": "visual_reference_card",
"selection_kind": "target_url",
"target_url": "https://www.binance.com/en/trade/BTC_USDT?type=spot",
"selected_ts": "2026-05-12T02:05:30+00:00",
"revisit_acknowledged": True,
"revisit_acknowledged_ts": "2026-05-12T02:15:30+00:00",
"revisit_acknowledged_by": "local_operator",
"revisit_ack_note": "checked and cleared",
}
],
},
"updated_ts": "2026-05-12T02:05:00+00:00",
}
@@ -0,0 +1,146 @@
import contextlib
import io
import json
import tempfile
import unittest
from pathlib import Path
from scripts import run_readonly_live_analysis_smoke
class ReadonlyLiveAnalysisSmokeScriptTests(unittest.TestCase):
def test_main_writes_passed_json_artifact_without_trade_operations(self) -> None:
output = io.StringIO()
with tempfile.TemporaryDirectory() as tmpdir:
artifact = Path(tmpdir) / "readonly-smoke.json"
opener = _FakeOpener(
{
"/health": {"app": "ai-ict", "database_configured": True},
"/trading/runtime-status": {"scheduler_state": "running", "current_blockers": []},
"/trading/launch-readiness?instrument_id=1&timeframe=1m": {
"live_analysis_assistant_ready": True,
"execution_assist_ready": False,
"real_live_ready": False,
"live_analysis_assistant_summary": {
"ready_for_readonly_live_analysis": True,
"must_not_trade": True,
},
},
"/trading/review-packet?instrument_id=1": {"status": "blocked", "action_recommendation": "observe_only"},
"/trading/shadow-paper-validation?instrument_id=1&timeframe=1m": {
"status": "collecting_paper_samples",
"execution_boundary": "real_live_still_blocked",
},
"/trading/research/weekly-review?instrument_id=1": {
"entry_count": 0,
"next_week_actions": ["prepare_shadow_paper_weekly_review"],
},
"/trading/candidates?instrument_id=1&limit=5": {"items": []},
"/static/app.js": (
"Read-only Live Analysis No order submission Not a buy/sell instruction "
"Execution remains disabled ready_for_readonly_live_analysis must_not_trade"
),
}
)
original_urlopen = run_readonly_live_analysis_smoke._urlopen_no_proxy
run_readonly_live_analysis_smoke._urlopen_no_proxy = opener
try:
with contextlib.redirect_stdout(output):
exit_code = run_readonly_live_analysis_smoke.main(
argv=[
"--base-url",
"http://example.test",
"--output-json",
"--artifact-path",
str(artifact),
],
env={},
)
finally:
run_readonly_live_analysis_smoke._urlopen_no_proxy = original_urlopen
artifact_payload = json.loads(artifact.read_text(encoding="utf-8"))
payload = json.loads(output.getvalue())
self.assertEqual(exit_code, 0)
self.assertEqual(payload["status"], "passed")
self.assertTrue(payload["ready_for_readonly_live_analysis"])
self.assertTrue(payload["must_not_trade"])
self.assertFalse(payload["submit_cancel_or_live_execution_called"])
self.assertFalse(payload["trade_permission_required"])
self.assertEqual(artifact_payload["status"], "passed")
def test_run_reports_blocker_when_launch_boundary_allows_execution(self) -> None:
payload = run_readonly_live_analysis_smoke.run_readonly_live_analysis_smoke(
base_url="http://example.test",
attempts=1,
sleep_seconds=0,
opener=_FakeOpener(
{
"/health": {"app": "ai-ict"},
"/trading/runtime-status": {"scheduler_state": "running"},
"/trading/launch-readiness?instrument_id=1&timeframe=1m": {
"live_analysis_assistant_ready": True,
"execution_assist_ready": True,
"real_live_ready": False,
"live_analysis_assistant_summary": {
"ready_for_readonly_live_analysis": True,
"must_not_trade": True,
},
},
"/trading/review-packet?instrument_id=1": {"status": "blocked"},
"/trading/shadow-paper-validation?instrument_id=1&timeframe=1m": {
"status": "collecting_paper_samples",
"execution_boundary": "real_live_still_blocked",
},
"/trading/research/weekly-review?instrument_id=1": {
"entry_count": 0,
"next_week_actions": ["prepare_shadow_paper_weekly_review"],
},
"/trading/candidates?instrument_id=1&limit=5": {"items": []},
"/static/app.js": (
"Read-only Live Analysis No order submission Not a buy/sell instruction "
"Execution remains disabled ready_for_readonly_live_analysis must_not_trade"
),
}
),
)
self.assertEqual(payload["status"], "blocked")
self.assertIn("launch_readiness", payload["blockers"])
class _FakeOpener:
def __init__(self, payloads: dict) -> None:
self.payloads = payloads
def __call__(self, request, timeout_seconds: float):
path = request.full_url.replace("http://example.test", "")
if path not in self.payloads:
raise RuntimeError(f"missing fake response for {path}")
payload = self.payloads[path]
if isinstance(payload, str):
raw = payload.encode("utf-8")
else:
raw = json.dumps(payload).encode("utf-8")
return _FakeResponse(raw)
class _FakeResponse:
status = 200
def __init__(self, raw: bytes) -> None:
self.raw = raw
def __enter__(self):
return self
def __exit__(self, exc_type, exc, traceback):
return False
def read(self):
return self.raw
if __name__ == "__main__":
unittest.main()
@@ -9,6 +9,7 @@ from tests.unit.test_trading_runtime_service import (
FakeMarketStateService,
FakeReconciliationService,
FakeResearchCouncilService,
FakeShadowPaperSampleService,
FakeSetupWatcherService,
FakeTradingPlanService,
)
@@ -25,6 +26,7 @@ class ResearchCouncilRuntimeIntegrationTests(unittest.TestCase):
decision_packet_service=FakeDecisionPacketService(),
alert_dispatcher_service=FakeAlertDispatcherService(),
research_council_service=research_council_service,
shadow_paper_sample_service=FakeShadowPaperSampleService(),
)
def test_council_task_runs_after_watch_before_decision_packet(self) -> None:
@@ -0,0 +1,75 @@
import contextlib
import io
import json
import unittest
from unittest.mock import Mock, patch
from scripts import run_shadow_paper_observation_backfill
class RunShadowPaperObservationBackfillTests(unittest.TestCase):
def test_main_outputs_json_and_defaults_to_dry_run(self) -> None:
service = Mock()
service.build_backfill_audit.return_value = {
"status": "dry_run",
"eligible_backfill_dates": ["2026-05-15"],
"created_observation_count": 0,
"projected_observed_shadow_day_count": 4,
"blocked_backfill_items": ["paper_ticket_backfill_not_allowed_without_executable_candidate"],
}
output = io.StringIO()
with patch("scripts.run_shadow_paper_observation_backfill.ShadowPaperObservationBackfillService", return_value=service):
with contextlib.redirect_stdout(output):
exit_code = run_shadow_paper_observation_backfill.main(
env={
"SHADOW_PAPER_BACKFILL_OUTPUT_JSON": "1",
"SHADOW_PAPER_BACKFILL_INSTRUMENT_ID": "2",
"SHADOW_PAPER_BACKFILL_TIMEFRAME": "5m",
"SHADOW_PAPER_BACKFILL_WINDOW_DAYS": "7",
}
)
self.assertEqual(exit_code, 0)
self.assertEqual(json.loads(output.getvalue())["status"], "dry_run")
service.build_backfill_audit.assert_called_once_with(
instrument_id=2,
timeframe="5m",
window_days=7,
apply=False,
created_by="shadow_paper_backfill_audit",
)
def test_main_honors_apply_flag(self) -> None:
service = Mock()
service.build_backfill_audit.return_value = {
"status": "applied",
"eligible_backfill_dates": ["2026-05-15"],
"created_observation_count": 1,
"projected_observed_shadow_day_count": 5,
"blocked_backfill_items": [],
}
output = io.StringIO()
with patch("scripts.run_shadow_paper_observation_backfill.ShadowPaperObservationBackfillService", return_value=service):
with contextlib.redirect_stdout(output):
exit_code = run_shadow_paper_observation_backfill.main(
env={
"SHADOW_PAPER_BACKFILL_APPLY": "1",
"SHADOW_PAPER_BACKFILL_CREATED_BY": "operator",
}
)
self.assertEqual(exit_code, 0)
self.assertIn("applied", output.getvalue())
service.build_backfill_audit.assert_called_once_with(
instrument_id=1,
timeframe="1m",
window_days=14,
apply=True,
created_by="operator",
)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,123 @@
import unittest
from datetime import date, datetime, timezone
from src.services.readiness.shadow_paper_observation_backfill_service import ShadowPaperObservationBackfillService
class ShadowPaperObservationBackfillServiceTests(unittest.TestCase):
def test_dry_run_finds_candidate_days_without_writing(self) -> None:
repository = _BackfillRepository()
service = ShadowPaperObservationBackfillService(repository=repository)
payload = service.build_backfill_audit(
instrument_id=1,
timeframe="1m",
now=datetime(2026, 5, 18, 12, 0, tzinfo=timezone.utc),
apply=False,
)
self.assertEqual(payload["status"], "dry_run")
self.assertEqual(payload["eligible_backfill_dates"], ["2026-05-15", "2026-05-16"])
self.assertEqual(payload["created_observation_count"], 0)
self.assertEqual(repository.insert_calls, [])
self.assertFalse(payload["paper_backfill_allowed"])
self.assertFalse(payload["operator_feedback_backfill_allowed"])
self.assertIn("paper_ticket_backfill_not_allowed_without_executable_candidate", payload["blocked_backfill_items"])
def test_apply_creates_only_shadow_observations(self) -> None:
repository = _BackfillRepository()
service = ShadowPaperObservationBackfillService(repository=repository)
payload = service.build_backfill_audit(
instrument_id=1,
timeframe="1m",
now=datetime(2026, 5, 18, 12, 0, tzinfo=timezone.utc),
apply=True,
created_by="test_backfill",
)
self.assertEqual(payload["status"], "applied")
self.assertEqual(payload["created_observation_count"], 2)
self.assertEqual([call["observation_date"].isoformat() for call in repository.insert_calls], ["2026-05-15", "2026-05-16"])
self.assertTrue(all(call["candidate"]["status"] == "blocked" for call in repository.insert_calls))
self.assertTrue(all(call["created_by"] == "test_backfill" for call in repository.insert_calls))
self.assertTrue(all(row["paper_ticket_id"] is None for row in payload["created_observations"]))
self.assertTrue(all(row["paper_execution_id"] is None for row in payload["created_observations"]))
self.assertTrue(all(row["operator_agreement"] == "pending" for row in payload["created_observations"]))
def test_no_candidate_days_has_no_eligible_backfill(self) -> None:
repository = _BackfillRepository(candidate_rows=[])
service = ShadowPaperObservationBackfillService(repository=repository)
payload = service.build_backfill_audit(
instrument_id=1,
timeframe="1m",
now=datetime(2026, 5, 18, 12, 0, tzinfo=timezone.utc),
apply=True,
)
self.assertEqual(payload["eligible_backfill_dates"], [])
self.assertEqual(payload["created_observation_count"], 0)
self.assertEqual(repository.insert_calls, [])
class _BackfillRepository:
def __init__(self, *, candidate_rows=None) -> None:
self.candidate_rows = candidate_rows
self.insert_calls = []
def fetch_candidate_day_summaries(self, **kwargs):
if self.candidate_rows is not None:
return self.candidate_rows
return [
{"day": date(2026, 5, 15), "status": "blocked", "count": 3},
{"day": date(2026, 5, 16), "status": "blocked", "count": 4},
{"day": date(2026, 5, 17), "status": "blocked", "count": 5},
]
def fetch_observation_day_summaries(self, **kwargs):
return [{"day": date(2026, 5, 17), "count": 1, "feedback_count": 1}]
def fetch_latest_candidate_for_day(self, **kwargs):
return {
"id": int(kwargs["day_start"].strftime("%d")),
"instrument_id": kwargs["instrument_id"],
"model_code": "SWEEP_MSS_FVG",
"status": "blocked",
"session_code": "ALL",
"created_ts": kwargs["day_start"],
"quality_score": 0.5,
}
def fetch_latest_decision_packet_for_day(self, **kwargs):
return {
"id": int(kwargs["day_start"].strftime("%d")) + 100,
"instrument_id": kwargs["instrument_id"],
"candidate_id": kwargs["candidate_id"],
"status": "blocked",
"action_recommendation": "stand_down",
"packet_ts": kwargs["day_start"],
}
def insert_shadow_observation(self, **kwargs):
self.insert_calls.append(kwargs)
return {
"id": len(self.insert_calls),
"instrument_id": kwargs["instrument_id"],
"observation_date": kwargs["observation_date"],
"timeframe": kwargs["timeframe"],
"session_code": kwargs["session_code"],
"candidate_id": kwargs["candidate"]["id"],
"decision_packet_id": kwargs["decision_packet"]["id"],
"paper_ticket_id": None,
"paper_execution_id": None,
"observation_status": "candidate_observed",
"system_recommendation": "stand_down_blocked_candidate",
"operator_agreement": "pending",
"reason_codes": ["shadow_observation_backfill"],
"observed_ts": kwargs["candidate"]["created_ts"],
}
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,106 @@
import unittest
from datetime import datetime, timezone
from src.services.readiness.shadow_paper_sample_collection_service import ShadowPaperSampleCollectionService
class ShadowPaperSampleCollectionServiceTests(unittest.TestCase):
def test_records_daily_observation_from_latest_candidate_and_packet(self) -> None:
now = datetime(2026, 5, 17, 12, 0, tzinfo=timezone.utc)
repository = _ObservationRepository()
service = ShadowPaperSampleCollectionService(repository=repository)
payload = service.record_daily_observation(
instrument_id=1,
timeframe="1m",
operator_agreement="agree",
operator_note="aligned with operator review",
created_by="alice",
runtime_status={"current_blockers": []},
now=now,
)
self.assertEqual(payload["status"], "recorded")
self.assertEqual(payload["observation_status"], "operator_feedback_recorded")
self.assertEqual(payload["system_recommendation"], "wait_for_operator_confirmation")
self.assertEqual(payload["operator_agreement"], "agree")
self.assertEqual(payload["candidate_id"], 7)
self.assertEqual(payload["decision_packet_id"], 9)
self.assertIn("keep_daily_shadow_observation_running", payload["next_collection_actions"])
self.assertIn("build_shadow_paper_ticket_for_latest_candidate", payload["next_collection_actions"])
self.assertEqual(repository.upsert_call["created_by"], "alice")
self.assertEqual(repository.upsert_call["reason_codes"][0], "operator_feedback_recorded")
def test_runtime_blocker_records_blocked_observation_without_sample_credit_action(self) -> None:
now = datetime(2026, 5, 17, 12, 0, tzinfo=timezone.utc)
service = ShadowPaperSampleCollectionService(repository=_ObservationRepository())
payload = service.record_daily_observation(
instrument_id=1,
runtime_status={"current_blockers": ["broker_sync_degraded:BTC-USDT"]},
now=now,
)
self.assertEqual(payload["observation_status"], "runtime_blocked")
self.assertEqual(payload["system_recommendation"], "pause_runtime_blocked")
self.assertEqual(payload["next_collection_actions"], ["clear_runtime_blockers_before_counting_shadow_day"])
def test_blocked_candidate_waits_before_paper_ticket_action(self) -> None:
now = datetime(2026, 5, 17, 12, 0, tzinfo=timezone.utc)
service = ShadowPaperSampleCollectionService(repository=_ObservationRepository(candidate_status="blocked"))
payload = service.record_daily_observation(
instrument_id=1,
runtime_status={"current_blockers": []},
now=now,
)
self.assertIn("wait_for_executable_candidate_before_paper_ticket", payload["next_collection_actions"])
self.assertNotIn("build_shadow_paper_ticket_for_latest_candidate", payload["next_collection_actions"])
class _ObservationRepository:
def __init__(self, *, candidate_status: str = "executable") -> None:
self.upsert_call = None
self.candidate_status = candidate_status
def fetch_latest_candidate(self, **kwargs):
return {
"id": 7,
"model_code": "SWEEP_MSS_FVG",
"status": self.candidate_status,
"session_code": "NY_AM",
"confirmation_state": {"reason_codes": ["mss_confirmed"]},
"created_ts": kwargs["since"],
}
def fetch_latest_decision_packet(self, **kwargs):
return {
"id": 9,
"status": "actionable",
"action_recommendation": "confirm_ticket",
"payload": {"decision": {"reason_codes": ["candidate_actionable"]}},
"packet_ts": kwargs["since"],
}
def upsert_daily_observation(self, **kwargs):
self.upsert_call = kwargs
return {
"id": 12,
"observation_date": kwargs["observation_date"],
"timeframe": kwargs["timeframe"],
"session_code": kwargs["session_code"],
"observation_status": kwargs["observation_status"],
"system_recommendation": kwargs["system_recommendation"],
"operator_agreement": kwargs["operator_agreement"],
"candidate_id": kwargs["candidate_id"],
"decision_packet_id": kwargs["decision_packet_id"],
"paper_ticket_id": kwargs["paper_ticket_id"],
"paper_execution_id": kwargs["paper_execution_id"],
"reason_codes": kwargs["reason_codes"],
"observed_ts": kwargs["observed_ts"],
}
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,146 @@
import unittest
from datetime import datetime, timedelta, timezone
from src.services.readiness.shadow_paper_validation_service import ShadowPaperValidationService
class ShadowPaperValidationServiceTests(unittest.TestCase):
def test_build_summary_collects_shadow_and_paper_validation_gates(self) -> None:
now = datetime(2026, 5, 17, 12, 0, tzinfo=timezone.utc)
service = ShadowPaperValidationService(repository=_SparseRepository(now))
summary = service.build_summary(
instrument_id=1,
timeframe="1m",
runtime_status={"current_blockers": []},
weekly_review={"entry_count": 1, "risk_violation_count": 0, "framework_execution_rate": 0.8},
now=now,
)
self.assertEqual(summary["status"], "collecting_shadow_candidates")
self.assertEqual(summary["candidate_count"], 3)
self.assertEqual(summary["paper_ticket_count"], 1)
self.assertEqual(summary["paper_execution_count"], 1)
self.assertEqual(summary["operator_feedback_count"], 1)
self.assertIn("shadow_candidate_day_coverage", summary["failed_criteria"])
self.assertEqual(summary["next_validation_gate"], "shadow_candidate_day_coverage")
self.assertEqual(summary["execution_boundary"], "real_live_still_blocked")
self.assertEqual(len(summary["daily_coverage"]), 14)
def test_build_summary_marks_ready_when_two_week_gates_are_met(self) -> None:
now = datetime(2026, 5, 17, 12, 0, tzinfo=timezone.utc)
service = ShadowPaperValidationService(repository=_ReadyRepository(now))
summary = service.build_summary(
instrument_id=1,
runtime_status={"current_blockers": []},
weekly_review={"entry_count": 5, "risk_violation_count": 0, "framework_execution_rate": 0.9},
now=now,
)
self.assertEqual(summary["status"], "ready_for_shadow_paper_review")
self.assertEqual(summary["failed_criteria"], [])
self.assertEqual(summary["next_validation_gate"], "shadow_paper_review_ready")
self.assertGreaterEqual(summary["observed_shadow_day_count"], 10)
self.assertGreaterEqual(summary["paper_execution_count"], 5)
def test_runtime_blocker_prevents_shadow_validation(self) -> None:
now = datetime(2026, 5, 17, 12, 0, tzinfo=timezone.utc)
service = ShadowPaperValidationService(repository=_ReadyRepository(now))
summary = service.build_summary(
instrument_id=1,
runtime_status={"current_blockers": ["market_stream_degraded"]},
weekly_review={"entry_count": 5, "risk_violation_count": 0},
now=now,
)
self.assertEqual(summary["status"], "blocked_by_runtime")
self.assertEqual(summary["next_validation_gate"], "runtime_clear_for_shadow")
def test_daily_observations_count_toward_shadow_coverage_and_operator_feedback(self) -> None:
now = datetime(2026, 5, 17, 12, 0, tzinfo=timezone.utc)
service = ShadowPaperValidationService(repository=_ObservationOnlyRepository(now))
summary = service.build_summary(
instrument_id=1,
runtime_status={"current_blockers": []},
weekly_review={"entry_count": 1, "risk_violation_count": 0},
now=now,
)
self.assertEqual(summary["shadow_observation_count"], 4)
self.assertEqual(summary["shadow_observation_day_count"], 4)
self.assertEqual(summary["observed_shadow_day_count"], 4)
self.assertEqual(summary["operator_observation_feedback_count"], 1)
self.assertEqual(summary["operator_feedback_count"], 1)
self.assertEqual(summary["daily_coverage"][-1]["shadow_observation_count"], 1)
self.assertEqual(summary["daily_coverage"][-1]["operator_feedback_count"], 1)
class _SparseRepository:
def __init__(self, now: datetime) -> None:
self.now = now
def fetch_recent_candidates(self, **kwargs):
return [_row(index=i, created_ts=self.now - timedelta(days=i)) for i in range(3)]
def fetch_recent_tickets(self, **kwargs):
return [_row(id=10, execution_mode="paper", created_ts=self.now - timedelta(days=1))]
def fetch_recent_executions(self, **kwargs):
return [_row(id=20, execution_mode="paper", opened_ts=self.now - timedelta(days=1))]
def fetch_recent_journal_entries(self, **kwargs):
return [_row(id=30, journal_ts=self.now - timedelta(days=1), journal_date=(self.now - timedelta(days=1)).date())]
def fetch_recent_operator_feedback(self, **kwargs):
return [_row(id=40, subject_type="alert", label="useful", created_ts=self.now - timedelta(days=1))]
class _ReadyRepository(_SparseRepository):
def fetch_recent_candidates(self, **kwargs):
return [_row(id=i, created_ts=self.now - timedelta(days=i % 10)) for i in range(12)]
def fetch_recent_tickets(self, **kwargs):
return [_row(id=100 + i, execution_mode="paper", created_ts=self.now - timedelta(days=i)) for i in range(5)]
def fetch_recent_executions(self, **kwargs):
return [_row(id=200 + i, execution_mode="paper", opened_ts=self.now - timedelta(days=i)) for i in range(5)]
def fetch_recent_operator_feedback(self, **kwargs):
return [_row(id=400 + i, subject_type="alert", label="useful", created_ts=self.now - timedelta(days=i)) for i in range(5)]
class _ObservationOnlyRepository(_SparseRepository):
def fetch_recent_candidates(self, **kwargs):
return []
def fetch_recent_tickets(self, **kwargs):
return []
def fetch_recent_executions(self, **kwargs):
return []
def fetch_recent_operator_feedback(self, **kwargs):
return []
def fetch_recent_observations(self, **kwargs):
return [
_row(
id=i,
observation_date=(self.now - timedelta(days=i)).date(),
observed_ts=self.now - timedelta(days=i),
observation_status="candidate_observed",
operator_agreement="agree" if i == 0 else "pending",
)
for i in range(4)
]
def _row(**kwargs):
return dict(kwargs)
if __name__ == "__main__":
unittest.main()
@@ -203,10 +203,60 @@ class TradePlanCandidateApiTests(unittest.TestCase):
with self.assertRaisesRegex(ValueError, "candidate_model_not_enabled_for_execution"):
build_ticket_from_candidate(candidate_id=77, execution_mode="paper")
def test_build_ticket_from_candidate_allows_explicit_shadow_paper_sample(self) -> None:
candidate_row = {
"id": 77,
"instrument_id": 1,
"trading_plan_id": 10,
"model_code": "SWEEP_MSS_FVG",
"status": "executable",
"entry": {"entry_type": "limit", "entry_low": 100.0, "entry_high": 100.2, "stop_loss": 99.0, "tp1": 101.0, "tp2": 102.0},
"confirmation_state": {},
"quality_score": 50.0,
"ai_explanation": "observe_only",
}
with unittest.mock.patch("src.api.trading_runtime.TradePlanCandidateRepository.fetch_candidate_by_id", return_value=candidate_row), unittest.mock.patch(
"src.api.trading_runtime.normalize_trade_plan_candidate_row",
return_value={"id": 77, "instrument_id": 1, "model_code": "SWEEP_MSS_FVG", "executable": True},
), unittest.mock.patch(
"src.api.trading_runtime.ModelTrustService.build_summary",
return_value={"models": [{"model_code": "SWEEP_MSS_FVG", "execution_allowed": False}]},
), unittest.mock.patch(
"src.api.trading_runtime.ensure_trade_signal_for_candidate",
return_value=88,
), unittest.mock.patch(
"src.api.trading_runtime.build_execution_service",
return_value=_ShadowPaperExecutionService(),
):
payload = build_ticket_from_candidate(candidate_id=77, execution_mode="paper", allow_shadow_paper=True)
self.assertEqual(payload["ticket_id"], 501)
self.assertEqual(payload["execution_mode"], "paper")
self.assertEqual(payload["shadow_paper_validation"]["status"], "paper_only_shadow_allowed")
self.assertFalse(payload["shadow_paper_validation"]["trust_execution_allowed"])
_DEFAULT = object()
class _ShadowPaperExecutionDecision:
def to_dict(self):
return {
"ticket_id": 501,
"status": "awaiting_confirmation",
"execution_mode": "paper",
"signal_id": 88,
"execution_id": None,
"reasons": [],
"ticket_snapshot": {},
}
class _ShadowPaperExecutionService:
def create_execution_ticket(self, **kwargs):
return _ShadowPaperExecutionDecision()
class _SetupWatcherStubRepository:
def __init__(
self,
@@ -194,6 +194,23 @@ class FakeResearchCouncilService:
}
class FakeShadowPaperSampleService:
def __init__(self):
self.calls = []
def record_daily_observation(self, **kwargs):
self.calls.append(kwargs)
return {
"status": "recorded",
"observation_id": 701,
"observation_date": kwargs["now"].date().isoformat(),
"observation_status": "candidate_observed",
"system_recommendation": "continue_shadow_tracking",
"operator_agreement": "pending",
"next_collection_actions": ["keep_daily_shadow_observation_running"],
}
class TradingRuntimeSchedulerTests(unittest.TestCase):
def test_run_cycle_records_successful_task_state(self) -> None:
scheduler = TradingRuntimeScheduler(
@@ -223,6 +240,7 @@ class TradingRuntimeSchedulerTests(unittest.TestCase):
),
visual_capture_retention_service=FakeVisualCaptureRetentionService(),
research_council_service=FakeResearchCouncilService(),
shadow_paper_sample_service=FakeShadowPaperSampleService(),
)
with patch("src.services.runtime.trading_runtime_service.settings.trading_runtime_symbols", "BTC-USDT"):
payload = scheduler.run_cycle()
@@ -239,6 +257,7 @@ class TradingRuntimeSchedulerTests(unittest.TestCase):
self.assertTrue(payload["tasks"]["decision_packet"]["details"]["visual_watch_reserved"])
self.assertEqual(payload["tasks"]["decision_packet"]["details"]["visual_trigger_policy_statuses"], ["triggered"])
self.assertEqual(payload["tasks"]["decision_packet"]["details"]["visual_triggered_alert_count"], 1)
self.assertEqual(payload["tasks"]["shadow_paper_observation"]["details"]["observation_status"], "candidate_observed")
self.assertEqual(payload["tasks"]["visual_watch"]["details"]["visual_reviews_created"], 1)
self.assertEqual(payload["tasks"]["visual_watch"]["details"]["last_visual_state"], "confirm")
self.assertEqual(payload["tasks"]["visual_watch"]["details"]["trigger_type_counts"]["candidate_actionable_watch"], 1)
@@ -264,6 +283,7 @@ class TradingRuntimeSchedulerTests(unittest.TestCase):
visual_review_service=FakeVisualReviewService(),
visual_capture_retention_service=FakeVisualCaptureRetentionService(),
research_council_service=FakeResearchCouncilService(),
shadow_paper_sample_service=FakeShadowPaperSampleService(),
)
with patch("src.services.runtime.trading_runtime_service.settings.trading_runtime_symbols", "BTC-USDT"):
payload = scheduler.run_cycle()
@@ -290,6 +310,7 @@ class TradingRuntimeSchedulerTests(unittest.TestCase):
visual_review_service=FakeVisualReviewService(),
visual_capture_retention_service=FakeVisualCaptureRetentionService(),
research_council_service=FakeResearchCouncilService(),
shadow_paper_sample_service=FakeShadowPaperSampleService(),
)
supervisor = TradingRuntimeSupervisor(scheduler=scheduler)
with patch("src.services.runtime.trading_runtime_service.settings.trading_runtime_symbols", "BTC-USDT"):
@@ -310,6 +331,7 @@ class TradingRuntimeSchedulerTests(unittest.TestCase):
visual_review_service=FakeVisualReviewService(),
visual_capture_retention_service=FakeVisualCaptureRetentionService(),
research_council_service=FakeResearchCouncilService(),
shadow_paper_sample_service=FakeShadowPaperSampleService(),
runtime_task_watchdog_service=RuntimeTaskWatchdogService(stale_multiplier=1, min_stuck_seconds=1),
)
stale_ts = (datetime.now(timezone.utc) - timedelta(seconds=30)).isoformat()