Add launch readiness report
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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())
|
||||
@@ -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",
|
||||
|
||||
@@ -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"](
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user