diff --git a/scripts/run_live_readiness.py b/scripts/run_live_readiness.py new file mode 100644 index 0000000..7a5bc96 --- /dev/null +++ b/scripts/run_live_readiness.py @@ -0,0 +1,37 @@ +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.broker import LiveBrokerService + + +def main(env=None) -> int: + env = os.environ if env is None else env + instrument_id = int(env.get("LIVE_READINESS_INSTRUMENT_ID", "1")) + timeframe = str(env.get("LIVE_READINESS_TIMEFRAME") or "1m") + output_json = _get_bool(env, "LIVE_READINESS_OUTPUT_JSON", False) + payload = LiveBrokerService().build_live_readiness(instrument_id=instrument_id, timeframe=timeframe) + if output_json: + print(json.dumps(payload, ensure_ascii=False, sort_keys=True)) + else: + print(f"Live readiness: {payload.get('ready_for_real_live')}") + print(f"Blocking reasons: {payload.get('blocking_reasons') or []}") + return 0 if payload.get("ready_for_real_live") 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()) diff --git a/src/api/__init__.py b/src/api/__init__.py index 2ba819c..522fbef 100644 --- a/src/api/__init__.py +++ b/src/api/__init__.py @@ -25,6 +25,7 @@ from src.api.trading_runtime import ( get_latest_trading_plan, get_trading_candidate_by_id, get_trading_market_state, + get_trading_live_readiness, get_trading_operator_notes, get_trading_plan_by_id, get_trading_review_packet, @@ -57,6 +58,7 @@ __all__ = [ "get_recent_trading_journal", "get_latest_trading_plan", "get_trading_market_state", + "get_trading_live_readiness", "get_trading_plan_by_id", "get_trading_runtime_status", "get_trading_trust_score", diff --git a/src/api/server.py b/src/api/server.py index 79c2fbd..8bd13cf 100644 --- a/src/api/server.py +++ b/src/api/server.py @@ -29,6 +29,7 @@ from src.api.trading_runtime import ( get_trading_operator_notes, get_trading_plan_by_id, get_trading_review_packet, + get_trading_live_readiness, get_trading_runtime_status, get_trading_session_view, get_trading_ticket_by_id, @@ -87,6 +88,7 @@ def build_api_dependencies() -> dict: "trading_operator_notes": get_trading_operator_notes, "trading_operator_note_create": create_trading_operator_note, "trading_review_packet": get_trading_review_packet, + "trading_live_readiness": get_trading_live_readiness, "trading_watch_run": run_trading_watch, "trading_candidate_by_id": get_trading_candidate_by_id, "trading_candidate_confirm_check": run_candidate_confirm_check, @@ -229,6 +231,10 @@ def route_request( if method == "GET" and path == "/trading/review-packet": instrument_id = _require_int(query, "instrument_id") return 200, deps["trading_review_packet"](instrument_id=instrument_id) + if method == "GET" and path == "/trading/live-readiness": + 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 == "POST" and path == "/trading/watch/run": instrument_id = _require_json_int(body or {}, "instrument_id") candidate = deps["trading_watch_run"]( diff --git a/src/api/trading_runtime.py b/src/api/trading_runtime.py index 00324f3..5d6f2d9 100644 --- a/src/api/trading_runtime.py +++ b/src/api/trading_runtime.py @@ -8,6 +8,7 @@ from src.repositories.signal_repository import SignalRepository from src.repositories.trade_plan_candidate_repository import TradePlanCandidateRepository from src.repositories.trading_plan_repository import TradingPlanRepository from src.services.broker import BrokerReadModelService, BROKER_STATE_OK +from src.services.broker import LiveBrokerService from src.services.decision import DecisionPacketService from src.services.execution import ExecutionService from src.services.journal import TradingJournalService, WeeklyReviewService @@ -209,51 +210,8 @@ def rerun_ticket_risk_check( def confirm_ticket_only(ticket_id: int, confirmed_by: str) -> Optional[dict]: service = build_execution_service() - ticket = service.repository.fetch_execution_ticket_by_id(ticket_id) - if not ticket: - return None - signal = service.repository.fetch_signal_for_execution(int(ticket["signal_id"])) - if not signal: - return None - risk_context = service.build_risk_context_for_signal(signal) - decision = service.prepare_execution( - signal_id=int(ticket["signal_id"]), - execution_mode=str(ticket["execution_mode"]), - risk_context=risk_context, - ) - confirmed_at = datetime.now(timezone.utc) - status = "confirmed" if decision and decision.allowed else "rejected" - snapshot = service.repository.fetch_execution_ticket_by_id(ticket_id).get("ticket_snapshot") or {} - new_snapshot = service.repository.fetch_execution_ticket_by_id(ticket_id).get("ticket_snapshot") or {} - if decision: - new_snapshot = { - **dict(snapshot), - "confirmation": { - "confirmed_by": confirmed_by if decision.allowed else None, - "confirmed_at": confirmed_at.isoformat() if decision.allowed else None, - }, - "decision": { - "allowed": decision.allowed, - "status": decision.status, - "reasons": list(decision.reasons), - "requires_confirmation": decision.requires_confirmation, - "risk_pct": decision.risk_pct, - "size": decision.size, - "entry_price": decision.entry_price, - "meta": dict(decision.meta), - }, - "status": status, - } - service.repository.update_execution_ticket( - ticket_id=ticket_id, - status=status, - ticket_snapshot=new_snapshot, - confirmed_by=confirmed_by if status == "confirmed" else None, - confirmed_at=confirmed_at if status == "confirmed" else None, - execution_id=ticket.get("execution_id"), - ) - updated = service.repository.fetch_execution_ticket_by_id(ticket_id) - return dict(updated) if updated else None + decision = service.confirm_execution_ticket(ticket_id, confirmed_by=confirmed_by) + return decision.to_dict() if decision else None def execute_ticket(ticket_id: int, *, confirmed_by: str, execution_mode_expected: str) -> Optional[dict]: @@ -426,6 +384,10 @@ def get_trading_trust_score(instrument_id: int) -> dict: return ModelTrustService().build_summary(instrument_id=instrument_id) +def get_trading_live_readiness(instrument_id: int, timeframe: str = "1m") -> dict: + return build_live_broker_service().build_live_readiness(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) @@ -465,6 +427,10 @@ def get_trading_review_packet(instrument_id: int) -> dict: return ReviewPacketService().build_packet(instrument_id=instrument_id) +def build_live_broker_service() -> LiveBrokerService: + return LiveBrokerService() + + def market_truth_degraded(market_state) -> bool: payload = market_state.to_dict() if hasattr(market_state, "to_dict") else dict(market_state or {}) freshness_code = str(((payload.get("freshness") or {}).get("code")) or "") diff --git a/src/services/broker/__init__.py b/src/services/broker/__init__.py index d755b89..a09acf0 100644 --- a/src/services/broker/__init__.py +++ b/src/services/broker/__init__.py @@ -6,7 +6,13 @@ from src.services.broker.broker_adapter import ( BrokerOperationResult, CancelAllRequest, KillSwitchRequest, + LiveOrderEnvelope, + LiveOrderLeg, ReconciliationRequest, + build_bracket_semantics, + build_live_order_envelope, + build_operator_confirmation_protocol, + build_reconciliation_semantics, ) from src.services.broker.broker_read_model_service import ( ALERT_UNKNOWN_OPEN_POSITION, @@ -22,6 +28,7 @@ from src.services.broker.live_broker_service import ( validate_live_dangerous_operation, validate_live_submit_request, ) +from src.services.broker.live_readiness_service import LiveReadinessService from src.services.broker.okx_order_adapter import OKXOrderAdapter __all__ = [ @@ -30,16 +37,23 @@ __all__ = [ "BrokerOperationResult", "CancelAllRequest", "KillSwitchRequest", + "LiveOrderEnvelope", + "LiveOrderLeg", "LiveBrokerService", + "LiveReadinessService", "LiveOperationDecision", "OKXOrderAdapter", "ReconciliationRequest", + "build_bracket_semantics", "BROKER_STATE_OK", "BROKER_STATE_UNAVAILABLE", "ALERT_UNKNOWN_OPEN_POSITION", "ALERT_UNKNOWN_PENDING_ORDER", "BrokerReadModelService", "build_broker_adapter", + "build_live_order_envelope", + "build_operator_confirmation_protocol", + "build_reconciliation_semantics", "validate_live_dangerous_operation", "validate_live_submit_request", ] diff --git a/src/services/broker/broker_adapter.py b/src/services/broker/broker_adapter.py index 6c0ef64..7885ebc 100644 --- a/src/services/broker/broker_adapter.py +++ b/src/services/broker/broker_adapter.py @@ -21,6 +21,38 @@ class BracketOrderRequest: return dict(asdict(self)) +@dataclass(frozen=True) +class LiveOrderLeg: + leg_type: str + side: str + price: float + trigger_price: Optional[float] + size: float + reduce_only: bool + + def to_dict(self) -> dict: + return dict(asdict(self)) + + +@dataclass(frozen=True) +class LiveOrderEnvelope: + schema_version: str + broker: str + instrument_id: int + execution_ticket_id: int + signal_id: int + side: str + size: float + entry_leg: dict + take_profit_legs: list[dict] + stop_loss_leg: dict + bracket_semantics: dict + reconciliation_semantics: dict + + def to_dict(self) -> dict: + return dict(asdict(self)) + + @dataclass(frozen=True) class ReconciliationRequest: instrument_id: Optional[int] @@ -86,3 +118,112 @@ class BrokerAdapter(ABC): @abstractmethod def activate_kill_switch(self, request: KillSwitchRequest) -> BrokerOperationResult: raise NotImplementedError + + +def build_live_order_envelope(request: BracketOrderRequest, *, broker: str) -> LiveOrderEnvelope: + entry_leg = LiveOrderLeg( + leg_type="entry", + side=request.side, + price=request.entry_price, + trigger_price=None, + size=request.size, + reduce_only=False, + ) + stop_side = "sell" if str(request.side).lower() == "buy" else "buy" + stop_loss_leg = LiveOrderLeg( + leg_type="stop_loss", + side=stop_side, + price=request.stop_loss, + trigger_price=request.stop_loss, + size=request.size, + reduce_only=True, + ) + tp1_size = round(float(request.size) * 0.5, 10) + tp2_size = max(float(request.size) - tp1_size, 0.0) + take_profit_legs = [ + LiveOrderLeg( + leg_type="take_profit_1", + side=stop_side, + price=request.take_profit_1, + trigger_price=request.take_profit_1, + size=tp1_size, + reduce_only=True, + ).to_dict(), + LiveOrderLeg( + leg_type="take_profit_2", + side=stop_side, + price=request.take_profit_2, + trigger_price=request.take_profit_2, + size=tp2_size, + reduce_only=True, + ).to_dict(), + ] + return LiveOrderEnvelope( + schema_version="live-order-v1", + broker=broker, + instrument_id=request.instrument_id, + execution_ticket_id=request.execution_ticket_id, + signal_id=request.signal_id, + side=request.side, + size=request.size, + entry_leg=entry_leg.to_dict(), + take_profit_legs=take_profit_legs, + stop_loss_leg=stop_loss_leg.to_dict(), + bracket_semantics=build_bracket_semantics(), + reconciliation_semantics=build_reconciliation_semantics(), + ) + + +def build_bracket_semantics() -> dict: + return { + "schema_version": "bracket-v1", + "oco_required": True, + "take_profit_policy": "split_tp1_tp2", + "stop_policy": "single_stop_reduce_only", + "states": [ + "not_submitted", + "submitted", + "live", + "tp1_partial", + "tp2_filled", + "stop_triggered", + "cancelled", + "rejected", + ], + } + + +def build_reconciliation_semantics() -> dict: + return { + "schema_version": "reconciliation-v1", + "states": [ + "not_reconciled", + "matched_fill", + "matched_fill_flat", + "position_only", + "imported_external_fill", + "placeholder_reconciliation", + ], + "required_keys": [ + "status", + "broker_order_ids", + "broker_fill_ids", + "position_detected", + "pending_order_detected", + ], + } + + +def build_operator_confirmation_protocol() -> dict: + return { + "protocol_version": "live-confirmation-v1", + "steps": [ + "review_live_readiness", + "confirm_market_truth_fresh", + "confirm_risk_governor_pass", + "confirm_broker_state_clean", + "confirm_order_schema_preview", + ], + "requires_human_identity": True, + "requires_explicit_live_confirmation": True, + } diff --git a/src/services/broker/live_broker_service.py b/src/services/broker/live_broker_service.py index 6dfa185..83026a9 100644 --- a/src/services/broker/live_broker_service.py +++ b/src/services/broker/live_broker_service.py @@ -10,8 +10,11 @@ from src.services.broker.broker_adapter import ( CancelAllRequest, KillSwitchRequest, ReconciliationRequest, + build_live_order_envelope, + build_operator_confirmation_protocol, ) from src.services.broker.okx_order_adapter import OKXOrderAdapter +from src.services.broker.live_readiness_service import LiveReadinessService @dataclass(frozen=True) @@ -40,6 +43,9 @@ class LiveBrokerService: def __init__(self, repository: Optional[ExecutionRepository] = None) -> None: self.repository = repository or ExecutionRepository() + def build_live_readiness(self, *, instrument_id: int, timeframe: str = "1m") -> dict: + return LiveReadinessService(execution_repository=self.repository).build_summary(instrument_id=instrument_id, timeframe=timeframe) + def submit_confirmed_ticket( self, ticket_id: int, @@ -128,6 +134,7 @@ class LiveBrokerService: oco_group_id=f"ticket:{ticket_id}", ticket_snapshot=snapshot, ) + envelope = build_live_order_envelope(request, broker=adapter_name) result = adapter.submit_bracket_order(request) return self._record_live_audit( adapter_name=adapter_name, @@ -139,7 +146,11 @@ class LiveBrokerService: signal_id=int(ticket["signal_id"]), instrument_id=int(signal["instrument_id"]), explicit_confirmation=explicit_live_confirmation, - payload=request.to_dict(), + payload={ + "request": request.to_dict(), + "live_order_envelope": envelope.to_dict(), + "operator_confirmation_protocol": build_operator_confirmation_protocol(), + }, result=result.to_dict(), reason_codes=result.reason_codes, ) @@ -379,6 +390,10 @@ def validate_live_submit_request( reason_codes.append("execution_ticket_not_confirmed") if not ticket.get("confirmed_by") or not ticket.get("confirmed_at"): reason_codes.append("execution_ticket_missing_confirmation_metadata") + confirmation = dict((ticket.get("ticket_snapshot") or {}).get("confirmation") or {}) + protocol = dict(confirmation.get("protocol") or {}) + if protocol.get("protocol_version") != build_operator_confirmation_protocol()["protocol_version"]: + reason_codes.append("operator_confirmation_protocol_missing") if not ticket.get("execution_id"): reason_codes.append("execution_ticket_missing_execution_record") return _dedupe(reason_codes) diff --git a/src/services/broker/live_readiness_service.py b/src/services/broker/live_readiness_service.py new file mode 100644 index 0000000..303ca01 --- /dev/null +++ b/src/services/broker/live_readiness_service.py @@ -0,0 +1,131 @@ +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.execution_repository import ExecutionRepository +from src.services.broker.broker_adapter import ( + build_bracket_semantics, + build_operator_confirmation_protocol, + build_reconciliation_semantics, +) +from src.services.broker.broker_read_model_service import BROKER_STATE_OK, BrokerReadModelService +from src.services.decision import DecisionPacketService +from src.services.market_state import MarketStateService +from src.services.runtime import get_trading_runtime_service +from src.services.trust import ModelTrustService + + +@dataclass(frozen=True) +class LiveReadinessCheck: + code: str + passed: bool + detail: str + + def to_dict(self) -> dict: + return dict(asdict(self)) + + +class LiveReadinessService: + def __init__( + self, + *, + broker_read_model_service: Optional[BrokerReadModelService] = None, + market_state_service: Optional[MarketStateService] = None, + execution_repository: Optional[ExecutionRepository] = None, + decision_packet_service: Optional[DecisionPacketService] = None, + trust_service: Optional[ModelTrustService] = None, + ) -> None: + self.broker_read_model_service = broker_read_model_service or BrokerReadModelService() + self.market_state_service = market_state_service or MarketStateService() + self.execution_repository = execution_repository or ExecutionRepository() + self.decision_packet_service = decision_packet_service or DecisionPacketService() + self.trust_service = trust_service or ModelTrustService() + + 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) + latest_ticket = self.execution_repository.fetch_recent_execution_ticket_rows(instrument_id=instrument_id, limit=1) + latest_audits = self.execution_repository.fetch_recent_live_operation_audits(instrument_id=instrument_id, limit=10) + decision_preview = self.decision_packet_service.build_preview(instrument_id=instrument_id) + protocol = build_operator_confirmation_protocol() + checklist = [ + LiveReadinessCheck( + code="live_trading_disabled_by_default", + passed=not bool(settings.live_trading_enabled), + detail="`LIVE_TRADING_ENABLED` should stay off unless an operator explicitly flips it.", + ), + LiveReadinessCheck( + code="broker_read_ready", + passed=broker_state.status == BROKER_STATE_OK, + detail=f"broker_state={broker_state.status}", + ), + LiveReadinessCheck( + code="market_truth_ready", + passed=market_truth_ready(market_state.to_dict()), + detail=f"freshness={market_state.freshness.code}, stream={market_state.stream_state.code}, gap={market_state.data_gap_state.code}", + ), + LiveReadinessCheck( + code="runtime_ready", + passed=runtime_status.get("scheduler_state") == "running" and not list(runtime_status.get("current_blockers") or []), + detail=f"runtime_blockers={runtime_status.get('current_blockers') or []}", + ), + LiveReadinessCheck( + code="decision_packet_ready", + passed=bool(decision_preview.get("action_recommendation")), + detail=f"decision_action={decision_preview.get('action_recommendation')}", + ), + LiveReadinessCheck( + code="trust_guard_ready", + passed=bool(trust_score.get("models")), + detail=f"whitelist={trust_score.get('whitelist') or []}, blacklist={trust_score.get('blacklist') or []}", + ), + LiveReadinessCheck( + code="operator_confirmation_protocol_ready", + passed=True, + detail=f"protocol_version={protocol['protocol_version']}", + ), + LiveReadinessCheck( + code="adapter_real_live_not_ready", + passed=False, + detail="OKX order adapter is still placeholder-only; real live submit remains intentionally blocked.", + ), + ] + blocking_reasons = [item.code for item in checklist if not item.passed] + return { + "generated_ts": now.isoformat(), + "instrument_id": instrument_id, + "timeframe": timeframe, + "ready_for_real_live": len(blocking_reasons) == 0, + "blocking_reasons": blocking_reasons, + "checklist": [item.to_dict() for item in checklist], + "operator_confirmation_protocol": protocol, + "live_order_schema": { + "schema_version": "live-order-v1", + "bracket_semantics": build_bracket_semantics(), + "reconciliation_semantics": build_reconciliation_semantics(), + }, + "market_state": market_state.to_dict(), + "broker_state": broker_state.to_dict(), + "runtime_status": runtime_status, + "trust_score": trust_score, + "decision_packet_preview": decision_preview, + "recent_live_audits": latest_audits, + "latest_ticket": latest_ticket[0] if latest_ticket else None, + } + + +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" diff --git a/src/services/broker/okx_order_adapter.py b/src/services/broker/okx_order_adapter.py index 16503dc..d0608df 100644 --- a/src/services/broker/okx_order_adapter.py +++ b/src/services/broker/okx_order_adapter.py @@ -8,6 +8,9 @@ from src.services.broker.broker_adapter import ( CancelAllRequest, KillSwitchRequest, ReconciliationRequest, + build_bracket_semantics, + build_live_order_envelope, + build_reconciliation_semantics, ) @@ -18,6 +21,7 @@ class OKXOrderAdapter(BrokerAdapter): self.base_url = base_url or settings.okx_order_base_url def submit_bracket_order(self, request: BracketOrderRequest) -> BrokerOperationResult: + envelope = build_live_order_envelope(request, broker=self.adapter_name) return BrokerOperationResult( ok=False, status="placeholder_not_implemented", @@ -28,6 +32,7 @@ class OKXOrderAdapter(BrokerAdapter): "base_url": self.base_url, "supports_bracket": True, "supports_oco": True, + "order_envelope": envelope.to_dict(), "request": request.to_dict(), }, ) @@ -43,6 +48,7 @@ class OKXOrderAdapter(BrokerAdapter): "base_url": self.base_url, "request": request.to_dict(), "supports_reconciliation": True, + "reconciliation_semantics": build_reconciliation_semantics(), }, ) @@ -57,6 +63,7 @@ class OKXOrderAdapter(BrokerAdapter): "base_url": self.base_url, "request": request.to_dict(), "supports_cancel_all": True, + "bracket_semantics": build_bracket_semantics(), }, ) diff --git a/src/services/execution/execution_service.py b/src/services/execution/execution_service.py index 9de464d..5f38a88 100644 --- a/src/services/execution/execution_service.py +++ b/src/services/execution/execution_service.py @@ -4,7 +4,13 @@ import math from typing import Optional from src.repositories.execution_repository import ExecutionRepository -from src.services.broker import BrokerReadModelService, BROKER_STATE_OK +from src.services.broker import ( + BROKER_STATE_OK, + BrokerReadModelService, + build_bracket_semantics, + build_operator_confirmation_protocol, + build_reconciliation_semantics, +) from src.services.backtest import ( BACKTEST_LIFECYCLE_INVALIDATED_BEFORE_ENTRY, BACKTEST_LIFECYCLE_MISSED, @@ -857,8 +863,8 @@ def evaluate_execution_decision( meta.setdefault("confirmed_by", None) meta.setdefault("confirmed_at", None) meta.setdefault("broker_order_ids", []) - meta.setdefault("bracket_state", {"status": "not_submitted", "oco_group_id": None}) - meta.setdefault("reconciliation_state", {"status": "not_reconciled"}) + meta.setdefault("bracket_state", {"status": "not_submitted", "oco_group_id": None, "semantics": build_bracket_semantics()}) + meta.setdefault("reconciliation_state", {"status": "not_reconciled", "semantics": build_reconciliation_semantics()}) allowed = len(reasons) == 0 requires_confirmation = execution_mode == "semi_auto" @@ -944,7 +950,10 @@ def build_execution_ticket_snapshot( snapshot["confirmation"] = { "confirmed_by": confirmed_by, "confirmed_at": confirmed_at.isoformat() if hasattr(confirmed_at, "isoformat") else confirmed_at, + "protocol": build_operator_confirmation_protocol(), + "explicit_live_confirmation_required": True, } + snapshot["operator_confirmation_protocol"] = build_operator_confirmation_protocol() return snapshot diff --git a/src/web/app.js b/src/web/app.js index 093942a..eb78b82 100644 --- a/src/web/app.js +++ b/src/web/app.js @@ -15,6 +15,7 @@ const marketStatePanel = document.getElementById('marketStatePanel'); const runtimeStatusPanel = document.getElementById('runtimeStatusPanel'); const decisionPacketPanel = document.getElementById('decisionPacketPanel'); const trustScorePanel = document.getElementById('trustScorePanel'); +const liveReadinessPanel = document.getElementById('liveReadinessPanel'); const sessionFocusPanel = document.getElementById('sessionFocusPanel'); const preMarketPlanPanel = document.getElementById('preMarketPlanPanel'); const tradeCandidatesPanel = document.getElementById('tradeCandidatesPanel'); @@ -412,6 +413,35 @@ function renderTrustScore(payload) { trustScorePanel.appendChild(grid); } +function renderLiveReadiness(payload) { + clearNode(liveReadinessPanel); + liveReadinessPanel.appendChild( + buildInfoGrid([ + ['ready for real live', String(Boolean(payload?.ready_for_real_live))], + ['blocking reasons', formatList(payload?.blocking_reasons || [])], + ['protocol version', payload?.operator_confirmation_protocol?.protocol_version || 'n/a'], + ['latest live audits', String((payload?.recent_live_audits || []).length)], + ['latest ticket', payload?.latest_ticket?.id || 'n/a'], + ]) + ); + const grid = createElement('div', 'cards'); + (payload?.checklist || []).forEach((item) => { + grid.appendChild( + buildItemCard( + item.code || 'check', + item.passed ? 'passed' : 'blocked', + [ + ['passed', String(Boolean(item.passed))], + ['detail', item.detail || 'n/a'], + ] + ) + ); + }); + if ((payload?.checklist || []).length) { + liveReadinessPanel.appendChild(grid); + } +} + function renderSessionFocus(payload) { clearNode(sessionFocusPanel); const items = payload?.items || []; @@ -909,6 +939,7 @@ function showErrorPanel(message) { runtimeStatusPanel, decisionPacketPanel, trustScorePanel, + liveReadinessPanel, sessionFocusPanel, preMarketPlanPanel, tradeCandidatesPanel, @@ -936,6 +967,7 @@ function renderDashboardData({ decisionPacket, decisionAlerts, trustScore, + liveReadiness, sessionView, tradeCandidates, executionTickets, @@ -952,6 +984,7 @@ function renderDashboardData({ renderRuntimeStatus(runtimeStatus); renderDecisionPacket(decisionPacket, decisionAlerts); renderTrustScore(trustScore); + renderLiveReadiness(liveReadiness); renderSessionFocus(sessionView); renderPreMarketPlan(tradingPlan); renderCandidates(tradeCandidates.items || []); @@ -971,7 +1004,7 @@ async function refreshDashboard() { const limit = limitInput.value || '20'; try { - const [health, candles, tradingPlan, marketState, runtimeStatus, decisionPacket, decisionAlerts, trustScore, sessionView, tradeCandidates, executionTickets, tradingExecutions, journalSummary, operatorNotes, reviewPacket, journalItems, weeklyReview] = await Promise.all([ + const [health, candles, tradingPlan, marketState, runtimeStatus, decisionPacket, decisionAlerts, trustScore, liveReadiness, 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), @@ -980,6 +1013,7 @@ async function refreshDashboard() { fetchJson(`/trading/decision-packet?${queryString({ instrument_id: instrumentId })}`).catch(() => ({ status: 'unavailable', action_recommendation: 'monitor' })), 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/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: [] })), @@ -1000,6 +1034,7 @@ async function refreshDashboard() { decisionPacket, decisionAlerts, trustScore, + liveReadiness, sessionView, tradeCandidates, executionTickets, diff --git a/src/web/index.html b/src/web/index.html index b387b94..b749a1e 100644 --- a/src/web/index.html +++ b/src/web/index.html @@ -92,6 +92,11 @@
+