Add launch readiness report

This commit is contained in:
Codex
2026-05-08 19:11:00 +08:00
parent afe34a2b7b
commit 81258db489
14 changed files with 364 additions and 2 deletions
+2
View File
@@ -39,6 +39,8 @@ NEWS_PROVIDER_TRADINGECONOMICS_API_KEY=guest:guest
NEWS_EVENT_PRE_WINDOW_MINUTES=15
NEWS_EVENT_POST_WINDOW_MINUTES=15
NEWS_EVENT_HIGH_IMPACT_ONLY=1
LAUNCH_REQUIRE_REAL_TRADE_SAMPLE=1
LIVE_SUBMIT_STRATEGY=blocked_until_verified
TRADING_RUNTIME_ENABLED=1
TRADING_RUNTIME_SYMBOLS=BTC-USDT,ETH-USDT
TRADING_RUNTIME_BIAS_TIMEFRAME=1m
+38
View File
@@ -0,0 +1,38 @@
from __future__ import annotations
from pathlib import Path
import json
import os
import sys
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from src.services.readiness import LaunchReadinessService
def main(env=None) -> int:
env = os.environ if env is None else env
instrument_id = int(env.get("LAUNCH_READINESS_INSTRUMENT_ID", "1"))
timeframe = str(env.get("LAUNCH_READINESS_TIMEFRAME") or "1m")
output_json = _get_bool(env, "LAUNCH_READINESS_OUTPUT_JSON", False)
payload = LaunchReadinessService().build_summary(instrument_id=instrument_id, timeframe=timeframe)
if output_json:
print(json.dumps(payload, ensure_ascii=False, sort_keys=True))
else:
print(f"Assisted trading ready: {payload.get('assisted_trading_ready')}")
print(f"Real live ready: {payload.get('real_live_ready')}")
print(f"Blocking reasons: {payload.get('blocking_reasons') or []}")
return 0 if payload.get("assisted_trading_ready") else 2
def _get_bool(env, key: str, default: bool = False) -> bool:
value = env.get(key)
if value is None or value == "":
return default
return str(value).strip().lower() in {"1", "true", "yes", "on"}
if __name__ == "__main__":
raise SystemExit(main())
+2
View File
@@ -26,6 +26,7 @@ from src.api.trading_runtime import (
get_trading_candidate_by_id,
get_trading_market_state,
get_trading_live_readiness,
get_trading_launch_readiness,
get_trading_operator_notes,
get_trading_plan_by_id,
get_trading_review_packet,
@@ -59,6 +60,7 @@ __all__ = [
"get_latest_trading_plan",
"get_trading_market_state",
"get_trading_live_readiness",
"get_trading_launch_readiness",
"get_trading_plan_by_id",
"get_trading_runtime_status",
"get_trading_trust_score",
+6
View File
@@ -30,6 +30,7 @@ from src.api.trading_runtime import (
get_trading_plan_by_id,
get_trading_review_packet,
get_trading_live_readiness,
get_trading_launch_readiness,
get_trading_runtime_status,
get_trading_session_view,
get_trading_ticket_by_id,
@@ -89,6 +90,7 @@ def build_api_dependencies() -> dict:
"trading_operator_note_create": create_trading_operator_note,
"trading_review_packet": get_trading_review_packet,
"trading_live_readiness": get_trading_live_readiness,
"trading_launch_readiness": get_trading_launch_readiness,
"trading_watch_run": run_trading_watch,
"trading_candidate_by_id": get_trading_candidate_by_id,
"trading_candidate_confirm_check": run_candidate_confirm_check,
@@ -235,6 +237,10 @@ def route_request(
instrument_id = _require_int(query, "instrument_id")
timeframe = _get_single(query, "timeframe", "1m")
return 200, deps["trading_live_readiness"](instrument_id=instrument_id, timeframe=timeframe)
if method == "GET" and path == "/trading/launch-readiness":
instrument_id = _require_int(query, "instrument_id")
timeframe = _get_single(query, "timeframe", "1m")
return 200, deps["trading_launch_readiness"](instrument_id=instrument_id, timeframe=timeframe)
if method == "POST" and path == "/trading/watch/run":
instrument_id = _require_json_int(body or {}, "instrument_id")
candidate = deps["trading_watch_run"](
+5
View File
@@ -15,6 +15,7 @@ from src.services.journal import TradingJournalService, WeeklyReviewService
from src.services.market_state import MarketStateService, get_market_truth_supervisor
from src.services.operator import OperatorNoteService, ReviewPacketService, SessionFocusService
from src.services.reconciliation import BrokerExecutionReconciliationService
from src.services.readiness import LaunchReadinessService
from src.services.runtime import get_trading_runtime_service
from src.services.trade_plan_candidates import SetupWatcherService
from src.services.trading_plans import ACTIVE_PLAN_STATUS, TradingPlanService
@@ -388,6 +389,10 @@ def get_trading_live_readiness(instrument_id: int, timeframe: str = "1m") -> dic
return build_live_broker_service().build_live_readiness(instrument_id=instrument_id, timeframe=timeframe)
def get_trading_launch_readiness(instrument_id: int, timeframe: str = "1m") -> dict:
return LaunchReadinessService().build_summary(instrument_id=instrument_id, timeframe=timeframe)
def get_trading_session_view(instrument_id: int, limit: int = 12) -> dict:
return SessionFocusService().build_summary(instrument_id=instrument_id, limit=limit)
+2
View File
@@ -43,6 +43,8 @@ class Settings(BaseSettings):
news_event_pre_window_minutes: int = 15
news_event_post_window_minutes: int = 15
news_event_high_impact_only: bool = True
launch_require_real_trade_sample: bool = True
live_submit_strategy: str = "blocked_until_verified"
trading_runtime_enabled: bool = True
trading_runtime_symbols: str = "BTC-USDT,ETH-USDT"
trading_runtime_bias_timeframe: str = "1m"
+3
View File
@@ -0,0 +1,3 @@
from src.services.readiness.launch_readiness_service import LaunchReadinessService
__all__ = ["LaunchReadinessService"]
@@ -0,0 +1,153 @@
from __future__ import annotations
from dataclasses import asdict, dataclass
from datetime import datetime, timezone
from typing import Optional
from src.config.settings import settings
from src.repositories.trading_journal_repository import default_week_range
from src.services.broker import BROKER_STATE_OK, BrokerReadModelService, LiveReadinessService
from src.services.journal import WeeklyReviewService
from src.services.market_state import MarketStateService, get_market_truth_supervisor
from src.services.runtime import get_trading_runtime_service
from src.services.trust import ModelTrustService
@dataclass(frozen=True)
class LaunchReadinessCheck:
code: str
passed: bool
severity: str
detail: str
def to_dict(self) -> dict:
return dict(asdict(self))
class LaunchReadinessService:
def __init__(
self,
*,
market_state_service: Optional[MarketStateService] = None,
broker_read_model_service: Optional[BrokerReadModelService] = None,
weekly_review_service: Optional[WeeklyReviewService] = None,
trust_service: Optional[ModelTrustService] = None,
live_readiness_service: Optional[LiveReadinessService] = None,
) -> None:
self.market_state_service = market_state_service or MarketStateService(market_truth_supervisor=get_market_truth_supervisor())
self.broker_read_model_service = broker_read_model_service or BrokerReadModelService()
self.weekly_review_service = weekly_review_service or WeeklyReviewService()
self.trust_service = trust_service or ModelTrustService()
self.live_readiness_service = live_readiness_service or LiveReadinessService()
def build_summary(self, *, instrument_id: int, timeframe: str = "1m") -> dict:
now = datetime.now(timezone.utc)
market_state = self.market_state_service.build_market_state(instrument_id=instrument_id, timeframe=timeframe)
broker_state = self.broker_read_model_service.fetch_snapshot(
instrument_id=instrument_id,
now=now,
known_execution_refs={"broker_order_ids": [], "open_execution_count": 0},
)
runtime_status = get_trading_runtime_service().status()
trust_score = self.trust_service.build_summary(instrument_id=instrument_id)
week_start, week_end = default_week_range()
weekly_review = self.weekly_review_service.build_summary(
week_start=week_start,
week_end=week_end,
instrument_id=instrument_id,
)
live_readiness = self.live_readiness_service.build_summary(instrument_id=instrument_id, timeframe=timeframe)
checklist = [
LaunchReadinessCheck(
code="market_truth_ready",
passed=_market_truth_ready(market_state.to_dict()),
severity="blocker",
detail=f"freshness={market_state.freshness.code}, stream={market_state.stream_state.code}, gap={market_state.data_gap_state.code}",
),
LaunchReadinessCheck(
code="news_provider_usable",
passed=str(market_state.news_window.freshness_code or "") in {"news_provider_fresh", "news_provider_cached"},
severity="blocker",
detail=f"provider={market_state.news_window.provider_name}, freshness={market_state.news_window.freshness_code}, error={market_state.news_window.error}",
),
LaunchReadinessCheck(
code="broker_state_ready",
passed=broker_state.status == BROKER_STATE_OK,
severity="blocker",
detail=f"broker_state={broker_state.status}",
),
LaunchReadinessCheck(
code="runtime_ready",
passed=runtime_status.get("scheduler_state") == "running" and not list(runtime_status.get("current_blockers") or []),
severity="blocker",
detail=f"runtime_blockers={runtime_status.get('current_blockers') or []}",
),
LaunchReadinessCheck(
code="trust_model_available",
passed=bool(trust_score.get("whitelist") or trust_score.get("observe_list")),
severity="blocker",
detail=f"whitelist={trust_score.get('whitelist') or []}, observe={trust_score.get('observe_list') or []}, blacklist={trust_score.get('blacklist') or []}",
),
LaunchReadinessCheck(
code="real_trade_sample_verified",
passed=(weekly_review.get("real_trade_count") or 0) > 0 if settings.launch_require_real_trade_sample else True,
severity="blocker",
detail=f"real_trade_count={weekly_review.get('real_trade_count') or 0}",
),
LaunchReadinessCheck(
code="live_submit_strategy_defined",
passed=bool(str(settings.live_submit_strategy or '').strip()),
severity="warning",
detail=f"live_submit_strategy={settings.live_submit_strategy}",
),
LaunchReadinessCheck(
code="real_live_path_ready",
passed=bool(live_readiness.get("ready_for_real_live")),
severity="warning",
detail=f"blocking_reasons={live_readiness.get('blocking_reasons') or []}",
),
]
blockers = [item.code for item in checklist if not item.passed and item.severity == "blocker"]
warnings = [item.code for item in checklist if not item.passed and item.severity == "warning"]
next_actions = _build_next_actions(blockers=blockers, warnings=warnings)
return {
"generated_ts": now.isoformat(),
"instrument_id": instrument_id,
"timeframe": timeframe,
"assisted_trading_ready": not blockers,
"real_live_ready": bool(live_readiness.get("ready_for_real_live")),
"blocking_reasons": blockers,
"warning_reasons": warnings,
"checklist": [item.to_dict() for item in checklist],
"next_actions": next_actions,
"market_state": market_state.to_dict(),
"broker_state": broker_state.to_dict(),
"runtime_status": runtime_status,
"trust_score": trust_score,
"weekly_review": weekly_review,
"live_readiness": live_readiness,
}
def _market_truth_ready(payload: dict) -> bool:
freshness_code = str(((payload.get("freshness") or {}).get("code")) or "")
stream_code = str(((payload.get("stream_state") or {}).get("code")) or "")
gap_code = str(((payload.get("data_gap_state") or {}).get("code")) or "")
return freshness_code == "market_data_fresh" and stream_code == "market_stream_connected" and gap_code == "market_data_gap_clear"
def _build_next_actions(*, blockers: list[str], warnings: list[str]) -> list[str]:
actions = []
if "news_provider_usable" in blockers:
actions.append("stabilize_news_event_source")
if "real_trade_sample_verified" in blockers:
actions.append("wait_for_and_verify_real_fill_sample")
if "trust_model_available" in blockers:
actions.append("improve_model_quality_or_collect_more_samples")
if "real_live_path_ready" in warnings:
actions.append("decide_live_submit_strategy")
if not actions:
actions.append("launch_assisted_trading_soft_rollout")
return actions[:4]
+37 -1
View File
@@ -16,6 +16,7 @@ const runtimeStatusPanel = document.getElementById('runtimeStatusPanel');
const decisionPacketPanel = document.getElementById('decisionPacketPanel');
const trustScorePanel = document.getElementById('trustScorePanel');
const liveReadinessPanel = document.getElementById('liveReadinessPanel');
const launchReadinessPanel = document.getElementById('launchReadinessPanel');
const sessionFocusPanel = document.getElementById('sessionFocusPanel');
const preMarketPlanPanel = document.getElementById('preMarketPlanPanel');
const tradeCandidatesPanel = document.getElementById('tradeCandidatesPanel');
@@ -442,6 +443,36 @@ function renderLiveReadiness(payload) {
}
}
function renderLaunchReadiness(payload) {
clearNode(launchReadinessPanel);
launchReadinessPanel.appendChild(
buildInfoGrid([
['assisted trading ready', String(Boolean(payload?.assisted_trading_ready))],
['real live ready', String(Boolean(payload?.real_live_ready))],
['blocking reasons', formatList(payload?.blocking_reasons || [])],
['warning reasons', formatList(payload?.warning_reasons || [])],
['next actions', formatList(payload?.next_actions || [])],
])
);
const grid = createElement('div', 'cards');
(payload?.checklist || []).forEach((item) => {
grid.appendChild(
buildItemCard(
item.code || 'check',
item.passed ? 'passed' : item.severity || 'warning',
[
['passed', String(Boolean(item.passed))],
['severity', item.severity || 'n/a'],
['detail', item.detail || 'n/a'],
]
)
);
});
if ((payload?.checklist || []).length) {
launchReadinessPanel.appendChild(grid);
}
}
function renderSessionFocus(payload) {
clearNode(sessionFocusPanel);
const items = payload?.items || [];
@@ -940,6 +971,7 @@ function showErrorPanel(message) {
decisionPacketPanel,
trustScorePanel,
liveReadinessPanel,
launchReadinessPanel,
sessionFocusPanel,
preMarketPlanPanel,
tradeCandidatesPanel,
@@ -968,6 +1000,7 @@ function renderDashboardData({
decisionAlerts,
trustScore,
liveReadiness,
launchReadiness,
sessionView,
tradeCandidates,
executionTickets,
@@ -985,6 +1018,7 @@ function renderDashboardData({
renderDecisionPacket(decisionPacket, decisionAlerts);
renderTrustScore(trustScore);
renderLiveReadiness(liveReadiness);
renderLaunchReadiness(launchReadiness);
renderSessionFocus(sessionView);
renderPreMarketPlan(tradingPlan);
renderCandidates(tradeCandidates.items || []);
@@ -1004,7 +1038,7 @@ async function refreshDashboard() {
const limit = limitInput.value || '20';
try {
const [health, candles, tradingPlan, marketState, runtimeStatus, decisionPacket, decisionAlerts, trustScore, liveReadiness, sessionView, tradeCandidates, executionTickets, tradingExecutions, journalSummary, operatorNotes, reviewPacket, journalItems, weeklyReview] = await Promise.all([
const [health, candles, tradingPlan, marketState, runtimeStatus, decisionPacket, decisionAlerts, trustScore, liveReadiness, launchReadiness, sessionView, tradeCandidates, executionTickets, tradingExecutions, journalSummary, operatorNotes, reviewPacket, journalItems, weeklyReview] = await Promise.all([
fetchJson('/health'),
fetchJson(`/candles?${queryString({ instrument_id: instrumentId, timeframe, limit })}`),
fetchJson(`/trading/plans/latest?${queryString({ instrument_id: instrumentId })}`).catch(() => null),
@@ -1014,6 +1048,7 @@ async function refreshDashboard() {
fetchJson(`/trading/alerts?${queryString({ instrument_id: instrumentId, limit: 10 })}`).catch(() => ({ items: [] })),
fetchJson(`/trading/trust-score?${queryString({ instrument_id: instrumentId })}`).catch(() => ({ models: [], whitelist: [], observe_list: [], blacklist: [], next_actions: [] })),
fetchJson(`/trading/live-readiness?${queryString({ instrument_id: instrumentId, timeframe })}`).catch(() => ({ ready_for_real_live: false, blocking_reasons: ['live_readiness_unavailable'], checklist: [] })),
fetchJson(`/trading/launch-readiness?${queryString({ instrument_id: instrumentId, timeframe })}`).catch(() => ({ assisted_trading_ready: false, real_live_ready: false, blocking_reasons: ['launch_readiness_unavailable'], warning_reasons: [], checklist: [], next_actions: [] })),
fetchJson(`/trading/session-view?${queryString({ instrument_id: instrumentId, limit: 12 })}`).catch(() => ({ items: [] })),
fetchJson(`/trading/candidates?${queryString({ instrument_id: instrumentId, limit })}`).catch(() => ({ items: [] })),
fetchJson(`/trading/tickets?${queryString({ instrument_id: instrumentId, limit })}`).catch(() => ({ items: [] })),
@@ -1035,6 +1070,7 @@ async function refreshDashboard() {
decisionAlerts,
trustScore,
liveReadiness,
launchReadiness,
sessionView,
tradeCandidates,
executionTickets,
+5
View File
@@ -97,6 +97,11 @@
<div id="liveReadinessPanel" class="detail-panel"></div>
</section>
<section class="panel panel-wide">
<h2>Launch Readiness</h2>
<div id="launchReadinessPanel" class="detail-panel"></div>
</section>
<section class="panel panel-wide">
<h2>Session Focus</h2>
<div id="sessionFocusPanel" class="detail-panel"></div>
+11
View File
@@ -134,6 +134,17 @@ class ApiServerRoutingTests(unittest.TestCase):
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: {"assisted_trading_ready": False, "timeframe": kwargs["timeframe"]}},
)
self.assertEqual(status, 200)
self.assertFalse(payload["assisted_trading_ready"])
self.assertEqual(payload["timeframe"], "5m")
def test_routes_trading_broker_state(self) -> None:
status, payload = route_request(
"/trading/broker-state",
+6 -1
View File
@@ -17,6 +17,11 @@ TRADING_APP_JS = ROOT / "src" / "web" / "app.js"
TRAINING_HTML = ROOT / "src" / "web" / "training.html"
TRAINING_JS = ROOT / "src" / "web" / "training.js"
OLD_TRAINING_DOC = ROOT / "docs" / "training_case_engine_m1_m6_product_readme.md"
OPTIONAL_DORMANT_DOCS = {
"AI_ICT_盯盘告警开发任务清单_v1.md",
"AI_ICT_盯盘告警技术设计稿_v1.md",
"AI_ICT_盯盘告警架构图_v1.md",
}
class DocumentationConsistencyTests(unittest.TestCase):
@@ -34,7 +39,7 @@ class DocumentationConsistencyTests(unittest.TestCase):
"AI_ICT_实盘辅助决策系统开发方案_v2.md",
"ICT交易全书_出版终校版.md",
"training_console_public_surface_v1.md",
},
} | OPTIONAL_DORMANT_DOCS,
)
self.assertTrue(TRAINING_PACKAGE_SMOKE_DOC.is_file())
self.assertFalse(OLD_TRAINING_DOC.exists())
@@ -0,0 +1,80 @@
import os
import unittest
from unittest.mock import patch
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://user:pass@localhost:5432/ai_ict_test")
from src.services.readiness import LaunchReadinessService
class LaunchReadinessServiceTests(unittest.TestCase):
def test_build_summary_reports_assisted_not_ready_without_real_trade_sample(self) -> None:
service = LaunchReadinessService(
market_state_service=_MarketStateService(),
broker_read_model_service=_BrokerReadService(),
weekly_review_service=_WeeklyReviewService(),
trust_service=_TrustService(),
live_readiness_service=_LiveReadinessService(),
)
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["assisted_trading_ready"])
self.assertIn("real_trade_sample_verified", payload["blocking_reasons"])
self.assertFalse(payload["real_live_ready"])
class _MarketState:
freshness = type("Freshness", (), {"code": "market_data_fresh"})()
stream_state = type("Stream", (), {"code": "market_stream_connected"})()
data_gap_state = type("Gap", (), {"code": "market_data_gap_clear"})()
news_window = type("News", (), {"provider_name": "cached", "freshness_code": "news_provider_cached", "error": None})()
def to_dict(self):
return {
"freshness": {"code": "market_data_fresh"},
"stream_state": {"code": "market_stream_connected"},
"data_gap_state": {"code": "market_data_gap_clear"},
}
class _MarketStateService:
def build_market_state(self, **kwargs):
return _MarketState()
class _BrokerState:
status = "broker_state_ok"
def to_dict(self):
return {"status": "broker_state_ok"}
class _BrokerReadService:
def fetch_snapshot(self, **kwargs):
return _BrokerState()
class _WeeklyReviewService:
def build_summary(self, **kwargs):
return {"real_trade_count": 0}
class _TrustService:
def build_summary(self, **kwargs):
return {"whitelist": ["OSOK"], "observe_list": [], "blacklist": []}
class _LiveReadinessService:
def build_summary(self, **kwargs):
return {"ready_for_real_live": False, "blocking_reasons": ["adapter_real_live_not_ready"]}
class _RuntimeService:
def status(self):
return {"scheduler_state": "running", "current_blockers": []}
if __name__ == "__main__":
unittest.main()
+14
View File
@@ -23,6 +23,7 @@ from scripts import (
run_mss,
run_ranges,
run_decision_packet,
run_launch_readiness,
run_review_packet,
run_session_generator,
run_live_readiness,
@@ -184,6 +185,19 @@ class RuntimeScriptJsonTests(unittest.TestCase):
self.assertEqual(payload["instrument_id"], 2)
self.assertEqual(payload["timeframe"], "5m")
def test_run_launch_readiness_outputs_json(self) -> None:
service = type("Service", (), {"build_summary": lambda self, instrument_id, timeframe: {"instrument_id": instrument_id, "timeframe": timeframe, "assisted_trading_ready": False, "real_live_ready": False, "blocking_reasons": ["real_trade_sample_verified"]}})()
output = io.StringIO()
with patch("scripts.run_launch_readiness.LaunchReadinessService", return_value=service), contextlib.redirect_stdout(output):
exit_code = run_launch_readiness.main(
env={"LAUNCH_READINESS_OUTPUT_JSON": "1", "LAUNCH_READINESS_INSTRUMENT_ID": "2", "LAUNCH_READINESS_TIMEFRAME": "5m"}
)
payload = json.loads(output.getvalue())
self.assertEqual(exit_code, 2)
self.assertEqual(payload["instrument_id"], 2)
self.assertEqual(payload["timeframe"], "5m")
if __name__ == "__main__":
unittest.main()