Add real-time market truth layer
This commit is contained in:
@@ -48,6 +48,7 @@ from src.api.training import (
|
||||
submit_training_attempt,
|
||||
)
|
||||
from src.services.execution import ExecutionService
|
||||
from src.services.market_state import start_market_truth_runtime
|
||||
|
||||
|
||||
def build_api_dependencies() -> dict:
|
||||
@@ -409,6 +410,7 @@ class APIRequestHandler(BaseHTTPRequestHandler):
|
||||
|
||||
|
||||
def serve_api(host: str = "127.0.0.1", port: int = 8000) -> None:
|
||||
start_market_truth_runtime()
|
||||
server = ThreadingHTTPServer((host, port), APIRequestHandler)
|
||||
print(f"AI ICT API listening on http://{host}:{port}")
|
||||
server.serve_forever()
|
||||
|
||||
+57
-11
@@ -9,7 +9,7 @@ from src.repositories.trade_plan_candidate_repository import TradePlanCandidateR
|
||||
from src.repositories.trading_plan_repository import TradingPlanRepository
|
||||
from src.services.execution import ExecutionService
|
||||
from src.services.journal import TradingJournalService, WeeklyReviewService
|
||||
from src.services.market_state import MarketStateService
|
||||
from src.services.market_state import MarketStateService, get_market_truth_supervisor
|
||||
from src.services.trade_plan_candidates import SetupWatcherService
|
||||
from src.services.trading_plans import ACTIVE_PLAN_STATUS, TradingPlanService
|
||||
|
||||
@@ -68,14 +68,20 @@ def cancel_trading_plan(plan_id: int) -> Optional[dict]:
|
||||
|
||||
|
||||
def get_trading_market_state(instrument_id: int, timeframe: str = "1m") -> dict:
|
||||
service = MarketStateService()
|
||||
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,
|
||||
)
|
||||
return {
|
||||
"snapshot_id": snapshot_id,
|
||||
"market_state": market_state.to_dict(),
|
||||
"backtest_quality_gate": quality_gate,
|
||||
"recent_incidents": incidents,
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +91,17 @@ def run_trading_watch(
|
||||
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,
|
||||
@@ -135,7 +152,7 @@ def build_ticket_from_candidate(
|
||||
if not candidate.get("executable"):
|
||||
raise ValueError("candidate_not_executable")
|
||||
signal_id = ensure_trade_signal_for_candidate(candidate_row)
|
||||
ticket = ExecutionService().create_execution_ticket(
|
||||
ticket = build_execution_service().create_execution_ticket(
|
||||
signal_id=signal_id,
|
||||
execution_mode=execution_mode,
|
||||
account_equity=account_equity,
|
||||
@@ -145,12 +162,12 @@ def build_ticket_from_candidate(
|
||||
|
||||
|
||||
def get_trading_ticket_by_id(ticket_id: int) -> Optional[dict]:
|
||||
row = ExecutionService().repository.fetch_execution_ticket_by_id(ticket_id)
|
||||
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 ExecutionService().build_execution_dashboard_items(instrument_id=instrument_id, limit=limit)
|
||||
return build_execution_service().build_execution_dashboard_items(instrument_id=instrument_id, limit=limit)
|
||||
|
||||
|
||||
def rerun_ticket_risk_check(
|
||||
@@ -159,7 +176,7 @@ def rerun_ticket_risk_check(
|
||||
account_equity: float = 10000.0,
|
||||
risk_pct: float = 0.01,
|
||||
) -> Optional[dict]:
|
||||
service = ExecutionService()
|
||||
service = build_execution_service()
|
||||
ticket = service.repository.fetch_execution_ticket_by_id(ticket_id)
|
||||
if not ticket:
|
||||
return None
|
||||
@@ -183,7 +200,7 @@ def rerun_ticket_risk_check(
|
||||
|
||||
|
||||
def confirm_ticket_only(ticket_id: int, confirmed_by: str) -> Optional[dict]:
|
||||
service = ExecutionService()
|
||||
service = build_execution_service()
|
||||
ticket = service.repository.fetch_execution_ticket_by_id(ticket_id)
|
||||
if not ticket:
|
||||
return None
|
||||
@@ -239,13 +256,13 @@ def execute_ticket(ticket_id: int, *, confirmed_by: str, execution_mode_expected
|
||||
raise ValueError("ticket_execution_mode_mismatch")
|
||||
if ticket.get("status") != "confirmed":
|
||||
raise ValueError("execution_ticket_not_confirmed")
|
||||
service = ExecutionService()
|
||||
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 = ExecutionService()
|
||||
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"},
|
||||
@@ -253,11 +270,11 @@ def cancel_execution(execution_id: int) -> Optional[dict]:
|
||||
|
||||
|
||||
def close_execution(execution_id: int) -> Optional[dict]:
|
||||
return ExecutionService().close_execution_by_id(execution_id)
|
||||
return build_execution_service().close_execution_by_id(execution_id)
|
||||
|
||||
|
||||
def sync_execution(execution_id: int) -> Optional[dict]:
|
||||
return ExecutionService().settle_execution_by_id_from_latest_backtest(execution_id)
|
||||
return build_execution_service().settle_execution_by_id_from_latest_backtest(execution_id)
|
||||
|
||||
|
||||
def generate_trading_journal(execution_id: Optional[int] = None) -> dict:
|
||||
@@ -348,3 +365,32 @@ def ensure_trade_signal_for_candidate(candidate_row: dict) -> int:
|
||||
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 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"}]
|
||||
|
||||
Reference in New Issue
Block a user