from datetime import datetime, timezone from typing import Optional from src.api.execution_tickets import get_recent_execution_tickets from src.api.trade_plan_candidates import get_recent_trade_plan_candidates, normalize_trade_plan_candidate_row from src.api.trading_plans import get_recent_trading_plans, normalize_trading_plan_row 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 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.runtime import get_trading_runtime_service from src.services.trade_plan_candidates import SetupWatcherService from src.services.trading_plans import ACTIVE_PLAN_STATUS, TradingPlanService from src.services.trust import ModelTrustService def generate_trading_plan( *, instrument_id: int, bias_tf: str = "1m", setup_tf: str = "1m", trigger_tf: str = "1m", activate: bool = False, ) -> dict: service = TradingPlanService() candidate = service.build_plan( instrument_id=instrument_id, bias_tf=bias_tf, setup_tf=setup_tf, trigger_tf=trigger_tf, ) plan_id = service.persist_plan(candidate) row = TradingPlanRepository().fetch_trading_plan_by_id(plan_id) or candidate.to_dict() payload = normalize_trading_plan_row(dict(row)) if activate and payload.get("status") == ACTIVE_PLAN_STATUS: updated = activate_trading_plan(plan_id) if updated is not None: payload = updated return payload def get_latest_trading_plan(instrument_id: int) -> Optional[dict]: row = TradingPlanRepository().fetch_latest_trading_plan(instrument_id) return normalize_trading_plan_row(row) if row else None def get_trading_plan_by_id(plan_id: int) -> Optional[dict]: row = TradingPlanRepository().fetch_trading_plan_by_id(plan_id) return normalize_trading_plan_row(row) if row else None def activate_trading_plan(plan_id: int) -> Optional[dict]: row = TradingPlanRepository().update_trading_plan_status( plan_id, status="active", activated_ts=datetime.now(timezone.utc), ) return normalize_trading_plan_row(row) if row else None def cancel_trading_plan(plan_id: int) -> Optional[dict]: row = TradingPlanRepository().update_trading_plan_status( plan_id, status="cancelled", activated_ts=None, ) return normalize_trading_plan_row(row) if row else None def get_trading_market_state(instrument_id: int, timeframe: str = "1m") -> dict: service = build_market_state_service() market_state = service.build_market_state(instrument_id=instrument_id, timeframe=timeframe) snapshot_id = service.persist_market_state_snapshot(market_state=market_state) quality_gate = service.repository.fetch_latest_backtest_quality_gate(instrument_id=instrument_id) incidents = service.repository.fetch_recent_market_truth_incidents( instrument_id=instrument_id, timeframe=timeframe, limit=10, ) broker_state = get_trading_broker_state(instrument_id=instrument_id) return { "snapshot_id": snapshot_id, "market_state": market_state.to_dict(), "backtest_quality_gate": quality_gate, "recent_incidents": incidents, "broker_state": broker_state, } def run_trading_watch( *, instrument_id: int, setup_tf: str = "1m", trigger_tf: str = "1m", ) -> Optional[dict]: market_state = build_market_state_service().build_market_state( instrument_id=instrument_id, timeframe=trigger_tf, ) if market_truth_degraded(market_state): return { "candidate_generated": False, "status": "market_truth_blocked", "market_state": market_state.to_dict(), "reason_codes": market_truth_reject_codes(market_state), } service = SetupWatcherService() candidate = service.build_candidate( instrument_id=instrument_id, setup_tf=setup_tf, trigger_tf=trigger_tf, ) candidate_id = service.persist_candidate(candidate) if candidate is None: return None if candidate_id: row = TradePlanCandidateRepository().fetch_candidate_by_id(candidate_id) if row: return normalize_trade_plan_candidate_row(row) return normalize_trade_plan_candidate_row(candidate.to_dict()) def get_trading_candidate_by_id(candidate_id: int) -> Optional[dict]: row = TradePlanCandidateRepository().fetch_candidate_by_id(candidate_id) return normalize_trade_plan_candidate_row(row) if row else None def run_candidate_confirm_check(candidate_id: int) -> Optional[dict]: candidate = get_trading_candidate_by_id(candidate_id) if not candidate: return None return { "candidate_id": candidate["id"], "status": candidate["status"], "executable": candidate.get("executable", False), "confirmation_state": candidate.get("confirmation_state") or {}, "missing_conditions": candidate.get("missing_conditions") or [], "rejected_conditions": candidate.get("rejected_conditions") or [], "reason_codes": candidate.get("reason_codes") or [], } def build_ticket_from_candidate( *, candidate_id: int, execution_mode: str = "semi_auto", account_equity: float = 10000.0, risk_pct: float = 0.01, ) -> Optional[dict]: candidate_row = TradePlanCandidateRepository().fetch_candidate_by_id(candidate_id) if not candidate_row: return None candidate = normalize_trade_plan_candidate_row(candidate_row) if not candidate.get("executable"): raise ValueError("candidate_not_executable") signal_id = ensure_trade_signal_for_candidate(candidate_row) ticket = build_execution_service().create_execution_ticket( signal_id=signal_id, execution_mode=execution_mode, account_equity=account_equity, risk_pct=risk_pct, ) return ticket.to_dict() if ticket else None def get_trading_ticket_by_id(ticket_id: int) -> Optional[dict]: row = build_execution_service().repository.fetch_execution_ticket_by_id(ticket_id) return dict(row) if row else None def get_trading_executions(instrument_id: int, limit: int = 20) -> list[dict]: return build_execution_service().build_execution_dashboard_items(instrument_id=instrument_id, limit=limit) def rerun_ticket_risk_check( *, ticket_id: int, account_equity: float = 10000.0, risk_pct: float = 0.01, ) -> 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"]), account_equity=account_equity, risk_pct=risk_pct, risk_context=risk_context, ) return { "ticket_id": ticket_id, "decision": decision.meta if decision else None, "allowed": decision.allowed if decision else False, "reasons": decision.reasons if decision else ["signal_not_found"], } def confirm_ticket_only(ticket_id: int, confirmed_by: str) -> Optional[dict]: service = build_execution_service() 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]: ticket = get_trading_ticket_by_id(ticket_id) if not ticket: return None if ticket.get("execution_mode") != execution_mode_expected: raise ValueError("ticket_execution_mode_mismatch") if ticket.get("status") != "confirmed": raise ValueError("execution_ticket_not_confirmed") service = build_execution_service() full = service.submit_execution_ticket(ticket_id) return full.to_dict() if full else None def cancel_execution(execution_id: int) -> Optional[dict]: service = build_execution_service() return service.persist_execution_observation( trade_execution=service.repository.fetch_execution_by_id(execution_id), observation={"source": "manual", "status": "cancelled", "close_reason": "cancelled"}, ) def close_execution(execution_id: int) -> Optional[dict]: return build_execution_service().close_execution_by_id(execution_id) def sync_execution(execution_id: int) -> Optional[dict]: return build_execution_service().settle_execution_by_id_from_latest_backtest(execution_id) def generate_trading_journal(execution_id: Optional[int] = None) -> dict: service = TradingJournalService() if execution_id is not None: journal_id = service.upsert_entry_for_execution(execution_id) return {"created": 1 if journal_id else 0, "execution_id": execution_id, "journal_id": journal_id} return service.backfill_missing_entries() def get_weekly_review(instrument_id: Optional[int] = None) -> dict: return WeeklyReviewService().build_summary(instrument_id=instrument_id) def ensure_trade_signal_for_candidate(candidate_row: dict) -> int: repository = SignalRepository() existing = repository.fetch_latest_trade_signal_for_candidate(int(candidate_row["id"])) if existing: return int(existing["id"]) entry = dict(candidate_row.get("entry") or {}) if not entry: raise ValueError("candidate_missing_entry") trading_plan = TradingPlanRepository().fetch_trading_plan_by_id(int(candidate_row["trading_plan_id"])) or {} allowed_sessions = list(trading_plan.get("allowed_sessions") or []) entry_low = float(candidate_row.get("entry_low") or entry.get("entry_low") or 0.0) entry_high = float(candidate_row.get("entry_high") or entry.get("entry_high") or 0.0) stop_loss = float(candidate_row.get("stop_loss") or entry.get("stop_loss") or 0.0) tp1 = float(candidate_row.get("tp1") or entry.get("tp1") or 0.0) tp2 = float(candidate_row.get("tp2") or entry.get("tp2") or 0.0) entry_price = (entry_low + entry_high) / 2 if entry_low and entry_high else (entry_low or entry_high) risk_distance = abs(entry_price - stop_loss) rr_tp1 = float(candidate_row.get("rr_tp1") or (abs(tp1 - entry_price) / risk_distance if risk_distance else 0.0)) rr_tp2 = float(candidate_row.get("rr_tp2") or (abs(tp2 - entry_price) / risk_distance if risk_distance else 0.0)) invalidation_codes = [ str(item.get("code")) for item in list(candidate_row.get("invalidations") or []) if item.get("code") ] target_plan = dict(entry.get("target_plan") or {}) quality_score = float(candidate_row.get("quality_score") or 0.0) ai_explanation = str(candidate_row.get("ai_explanation") or "") repository.insert_trade_signal( instrument_id=int(candidate_row["instrument_id"]), model_code=str(candidate_row.get("model_code") or "UNKNOWN"), side=str(candidate_row.get("side") or "buy"), status="awaiting_confirmation", trading_plan_id=int(candidate_row["trading_plan_id"]), candidate_id=int(candidate_row["id"]), bias_snapshot_id=None, entry_type=str(entry.get("entry_type") or "limit"), entry_low=entry_low, entry_high=entry_high, stop_loss=stop_loss, tp1=tp1, tp2=tp2, rr_tp1=rr_tp1, rr_tp2=rr_tp2, session_code=allowed_sessions[0] if allowed_sessions else None, created_ts=candidate_row.get("created_ts"), expires_ts=candidate_row.get("expires_ts"), invalidations=invalidation_codes, evidence={ **dict(candidate_row.get("evidence") or {}), "candidate_id": candidate_row.get("id"), "trading_plan_id": candidate_row.get("trading_plan_id"), }, primary_draw=dict((trading_plan or {}).get("primary_draw") or {}), target_plan=target_plan, risk_governor_decision={}, confirmation_quality=dict(candidate_row.get("confirmation_state") or {}), quality_score=quality_score, ai_explanation=ai_explanation, meta={ **dict(candidate_row.get("meta") or {}), "target_plan_required": True, "execution_permission": True, "structure_state": "aligned", "signal_timeframe_set": { "setup_tf": candidate_row.get("setup_timeframe"), "trigger_tf": candidate_row.get("trigger_timeframe"), }, "reason_codes": list((candidate_row.get("confirmation_state") or {}).get("reason_codes") or []), "rejected_conditions": list((candidate_row.get("confirmation_state") or {}).get("rejected_conditions") or []), }, ) created = repository.fetch_latest_trade_signal_for_candidate(int(candidate_row["id"])) if not created: raise ValueError("signal_not_created_from_candidate") return int(created["id"]) def build_market_state_service() -> MarketStateService: return MarketStateService(market_truth_supervisor=get_market_truth_supervisor()) def build_execution_service() -> ExecutionService: return ExecutionService(market_state_service=build_market_state_service()) def get_trading_broker_state(instrument_id: int) -> dict: execution_service = build_execution_service() items = execution_service.build_execution_dashboard_items(instrument_id=instrument_id, limit=100) known_order_ids = [] open_execution_count = 0 for item in items: if item.get("execution_status") == "open": open_execution_count += 1 known_order_ids.extend(list(item.get("broker_order_ids") or [])) snapshot = BrokerReadModelService().fetch_snapshot( instrument_id=instrument_id, known_execution_refs={ "broker_order_ids": known_order_ids, "open_execution_count": open_execution_count, }, ) return snapshot.to_dict() def get_trading_runtime_status() -> dict: return get_trading_runtime_service().status() def run_trading_runtime_once() -> dict: return get_trading_runtime_service().run_once() def get_trading_decision_packet(instrument_id: int) -> dict: return DecisionPacketService().build_preview(instrument_id=instrument_id) def get_trading_decision_alerts(instrument_id: int, limit: int = 20) -> list[dict]: return DecisionPacketService().fetch_recent_alerts(instrument_id=instrument_id, limit=limit) def run_trading_reconciliation(instrument_id: int) -> dict: return BrokerExecutionReconciliationService().reconcile_instrument(instrument_id=instrument_id) 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) def get_trading_operator_notes(instrument_id: int, limit: int = 20) -> list[dict]: return OperatorNoteService().get_recent_notes(instrument_id=instrument_id, limit=limit) def create_trading_operator_note( *, instrument_id: int, note_scope: str, author: str, body: str, session_code: Optional[str] = None, execution_id: Optional[int] = None, signal_id: Optional[int] = None, candidate_id: Optional[int] = None, decision_packet_id: Optional[int] = None, tags: Optional[list[str]] = None, ) -> dict: return OperatorNoteService().create_note( instrument_id=instrument_id, note_scope=note_scope, author=author, body=body, session_code=session_code, execution_id=execution_id, signal_id=signal_id, candidate_id=candidate_id, decision_packet_id=decision_packet_id, tags=tags, ) 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 "") 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 in {"market_data_missing", "market_data_stale"} or stream_code in { "market_stream_disconnected", "market_stream_stale", } or gap_code == "market_data_gap_detected" def market_truth_reject_codes(market_state) -> list[str]: payload = market_state.to_dict() if hasattr(market_state, "to_dict") else dict(market_state or {}) codes = [ str(((payload.get("freshness") or {}).get("code")) or ""), str(((payload.get("stream_state") or {}).get("code")) or ""), str(((payload.get("data_gap_state") or {}).get("code")) or ""), ] return [code for code in codes if code and code not in {"market_data_fresh", "market_stream_connected", "market_data_gap_clear"}]