453 lines
18 KiB
Python
453 lines
18 KiB
Python
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"}]
|