Files
ai-exchange/src/main.py
T
2026-05-08 08:09:00 +08:00

426 lines
17 KiB
Python

from dataclasses import asdict, is_dataclass
from datetime import datetime
from typing import Optional
from src.api import get_health, get_recent_backtest_summaries, get_recent_signals, get_recent_trade_plan_candidates
from src.services.backtest import (
BACKTEST_MODE_ENTRY_THEN_OUTCOME,
BACKTEST_SUBJECT_PLAN_REPLAY,
BACKTEST_SUBJECT_SIGNAL,
BacktestCostConfig,
BacktestQualityGate,
BacktestReplayConfig,
BacktestService,
build_plan_replay_units,
candidate_row_is_executable,
evaluate_backtest_quality,
summarize_backtest_outcomes,
)
from src.services.bias import BiasService
from src.services.displacement import DisplacementService
from src.services.execution import AccountRiskState, ExecutionService
from src.services.fvg import FVGService
from src.services.liquidity import LiquidityPoolService
from src.services.market_state import MarketStateService, get_market_truth_supervisor
from src.services.ranges import DealingRangeService
from src.services.reports import IntradayReportService, PostTradeReviewService, PreMarketReportService
from src.services.sessions import SessionGenerator
from src.services.signals import STRICT_SIGNAL_EVIDENCE_KEYS, SignalService
from src.services.structure import MSSService
from src.services.sweeps import SweepService
from src.services.swings import SwingService
from src.services.trade_plan_candidates import SetupWatcherService
def build_default_runtime_dependencies(signal_required_evidence_keys: tuple[str, ...] = STRICT_SIGNAL_EVIDENCE_KEYS) -> dict:
market_state_service = MarketStateService(market_truth_supervisor=get_market_truth_supervisor())
return {
"session_generator": SessionGenerator(),
"swing_service": SwingService(),
"range_service": DealingRangeService(),
"liquidity_service": LiquidityPoolService(),
"sweep_service": SweepService(),
"displacement_service": DisplacementService(),
"mss_service": MSSService(),
"fvg_service": FVGService(),
"bias_service": BiasService(),
"signal_service": SignalService(required_evidence_keys=signal_required_evidence_keys),
"setup_watcher_service": SetupWatcherService(),
"execution_service": ExecutionService(market_state_service=market_state_service),
"backtest_service": BacktestService(),
"pre_market_report_service": PreMarketReportService(),
"intraday_report_service": IntradayReportService(),
"post_trade_review_service": PostTradeReviewService(),
"get_recent_signals": get_recent_signals,
"get_recent_trade_plan_candidates": get_recent_trade_plan_candidates,
"get_recent_backtest_summaries": get_recent_backtest_summaries,
}
def run_closed_loop(
instrument_id: int = 1,
timeframe: str = "1m",
bias_tf: Optional[str] = None,
setup_tf: Optional[str] = None,
trigger_tf: Optional[str] = None,
backtest_limit: int = 100,
run_execution: bool = True,
backtest_cost_config: Optional[BacktestCostConfig] = None,
backtest_quality_gate: Optional[BacktestQualityGate] = None,
backtest_mode: str = BACKTEST_MODE_ENTRY_THEN_OUTCOME,
backtest_replay_config: Optional[BacktestReplayConfig] = None,
signal_required_evidence_keys: tuple[str, ...] = STRICT_SIGNAL_EVIDENCE_KEYS,
execution_account_state: Optional[AccountRiskState] = None,
dependencies: Optional[dict] = None,
) -> dict:
deps = dependencies or build_default_runtime_dependencies(signal_required_evidence_keys=signal_required_evidence_keys)
session_generator = deps["session_generator"]
now = datetime.now(session_generator.timezone)
timeframe_set = {
"bias_tf": bias_tf or timeframe,
"setup_tf": setup_tf or timeframe,
"trigger_tf": trigger_tf or timeframe,
}
runtime_timeframes = tuple(dict.fromkeys(timeframe_set.values()))
session_written = session_generator.generate_for_date(
instrument_id=instrument_id,
timeframe=timeframe_set["trigger_tf"],
session_date=now,
)
swing_service = deps["swing_service"]
range_service = deps["range_service"]
liquidity_service = deps["liquidity_service"]
sweep_service = deps["sweep_service"]
displacement_service = deps["displacement_service"]
mss_service = deps["mss_service"]
fvg_service = deps["fvg_service"]
writes_by_timeframe = {}
active_range = None
sweeps = []
mss_candidates = []
fvgs = []
swings_written = 0
ranges_written = 0
liquidity_written = 0
sweeps_written = 0
displacement_written = 0
mss_written = 0
fvg_written = 0
fvg_updated = 0
for runtime_timeframe in runtime_timeframes:
swings = swing_service.detect_swings(
instrument_id=instrument_id,
timeframe=runtime_timeframe,
left_bars=2,
right_bars=2,
limit=500,
)
swings_persisted = swing_service.persist_swings(
instrument_id=instrument_id,
timeframe=runtime_timeframe,
candidates=swings,
)
range_candidate = range_service.build_active_range(instrument_id=instrument_id, timeframe=runtime_timeframe)
ranges_persisted = range_service.persist_active_range(
instrument_id=instrument_id,
timeframe=runtime_timeframe,
candidate=range_candidate,
)
liquidity = liquidity_service.build_candidates(instrument_id=instrument_id, timeframe=runtime_timeframe)
liquidity_persisted = liquidity_service.persist_candidates(
instrument_id=instrument_id,
timeframe=runtime_timeframe,
candidates=liquidity,
)
timeframe_sweeps = sweep_service.detect_sweeps(instrument_id=instrument_id, timeframe=runtime_timeframe)
sweeps_persisted_local = sweep_service.persist_sweeps(
instrument_id=instrument_id,
timeframe=runtime_timeframe,
candidates=timeframe_sweeps,
)
displacements = displacement_service.detect_displacement(
instrument_id=instrument_id,
timeframe=runtime_timeframe,
limit=200,
)
displacements_persisted = displacement_service.persist_displacement(
instrument_id=instrument_id,
timeframe=runtime_timeframe,
candidates=displacements,
)
timeframe_mss_candidates = mss_service.detect_mss(instrument_id=instrument_id, timeframe=runtime_timeframe)
mss_persisted = mss_service.persist_mss(
instrument_id=instrument_id,
timeframe=runtime_timeframe,
candidates=timeframe_mss_candidates,
)
timeframe_fvgs = fvg_service.detect_fvg(
instrument_id=instrument_id,
timeframe=runtime_timeframe,
limit=200,
)
fvg_persisted_local = fvg_service.persist_fvg(
instrument_id=instrument_id,
timeframe=runtime_timeframe,
candidates=timeframe_fvgs,
)
fvg_updated_local = fvg_service.update_existing_fvgs(
instrument_id=instrument_id,
timeframe=runtime_timeframe,
)
writes_by_timeframe[runtime_timeframe] = {
"swings": swings_persisted,
"ranges": ranges_persisted,
"liquidity_pools": liquidity_persisted,
"sweeps": sweeps_persisted_local,
"displacements": displacements_persisted,
"mss": mss_persisted,
"fvg_detected": fvg_persisted_local,
"fvg_updated": fvg_updated_local,
}
swings_written += swings_persisted
ranges_written += ranges_persisted
liquidity_written += liquidity_persisted
sweeps_written += sweeps_persisted_local
displacement_written += displacements_persisted
mss_written += mss_persisted
fvg_written += fvg_persisted_local
fvg_updated += fvg_updated_local
if runtime_timeframe == timeframe_set["bias_tf"]:
active_range = range_candidate
if runtime_timeframe == timeframe_set["trigger_tf"]:
sweeps = timeframe_sweeps
fvgs = timeframe_fvgs
if runtime_timeframe == timeframe_set["setup_tf"]:
mss_candidates = timeframe_mss_candidates
bias_service = deps["bias_service"]
bias = bias_service.build_bias(
instrument_id=instrument_id,
bias_tf=timeframe_set["bias_tf"],
setup_tf=timeframe_set["setup_tf"],
trigger_tf=timeframe_set["trigger_tf"],
)
bias_id = bias_service.persist_bias(
instrument_id=instrument_id,
bias_tf=timeframe_set["bias_tf"],
setup_tf=timeframe_set["setup_tf"],
trigger_tf=timeframe_set["trigger_tf"],
candidate=bias,
)
signal_service = deps["signal_service"]
signal = signal_service.build_signal(
instrument_id=instrument_id,
setup_tf=timeframe_set["setup_tf"],
trigger_tf=timeframe_set["trigger_tf"],
)
signals_written = signal_service.persist_signal(
instrument_id=instrument_id,
setup_tf=timeframe_set["setup_tf"],
trigger_tf=timeframe_set["trigger_tf"],
candidate=signal,
)
trade_plan_candidate = None
trade_plan_candidate_id = 0
setup_watcher_service = deps.get("setup_watcher_service")
if setup_watcher_service is not None:
trade_plan_candidate = setup_watcher_service.build_candidate(
instrument_id=instrument_id,
setup_tf=timeframe_set["setup_tf"],
trigger_tf=timeframe_set["trigger_tf"],
plan_date=now.date(),
)
trade_plan_candidate_id = setup_watcher_service.persist_candidate(trade_plan_candidate)
recent_signals = deps["get_recent_signals"](instrument_id=instrument_id, limit=1)
latest_signal = recent_signals[0] if recent_signals else None
recent_trade_plan_candidates = deps.get("get_recent_trade_plan_candidates", lambda instrument_id, limit: [])(
instrument_id=instrument_id,
limit=backtest_limit,
)
latest_trade_plan_candidate = recent_trade_plan_candidates[0] if recent_trade_plan_candidates else None
backtest_subject_type = select_closed_loop_backtest_subject_type(
recent_trade_plan_candidates=recent_trade_plan_candidates,
quality_gate=backtest_quality_gate,
)
backtest_trading_plan_id = (latest_trade_plan_candidate or {}).get("trading_plan_id") if backtest_subject_type == BACKTEST_SUBJECT_PLAN_REPLAY else None
backtest_candidate_model_code = (latest_trade_plan_candidate or {}).get("model_code") if backtest_subject_type == BACKTEST_SUBJECT_PLAN_REPLAY else None
execution_id = None
execution_record = None
if run_execution and latest_signal is not None:
execution_service = deps["execution_service"]
execution_id = execution_service.create_execution(
signal_id=latest_signal["id"],
execution_mode="paper",
account_state=execution_account_state,
)
execution_record = {
"status": "open" if execution_id is not None else "rejected",
"meta": {"risk_checks": [] if execution_id is not None else ["execution_creation_failed"]},
}
backtest_service = deps["backtest_service"]
backtest_run_id, outcomes = backtest_service.run_backtest(
instrument_id=instrument_id,
limit=backtest_limit,
cost_config=backtest_cost_config,
quality_gate=backtest_quality_gate,
subject_type=backtest_subject_type,
trading_plan_id=backtest_trading_plan_id,
candidate_model_code=backtest_candidate_model_code,
mode=backtest_mode,
replay_config=backtest_replay_config,
timeframe_set=timeframe_set,
)
backtest_summary = summarize_backtest_outcomes(outcomes)
backtest_quality_gate_result = evaluate_backtest_quality(backtest_summary, gate=backtest_quality_gate)
latest_outcome = _to_payload(outcomes[-1]) if outcomes else None
if execution_record is not None and latest_outcome is not None:
if execution_id is not None and hasattr(execution_service, "settle_latest_execution_for_signal"):
execution_record = execution_service.settle_latest_execution_for_signal(signal_id=latest_signal["id"], execution_mode="paper")
else:
execution_record = execution_service.persist_execution_settlement(
trade_execution={"id": execution_id, **execution_record},
backtest_result=latest_outcome,
)
if execution_record is None:
execution_record = {
"id": execution_id,
"status": "open" if execution_id is not None else "rejected",
"meta": {"lifecycle_state": None, "risk_checks": [] if execution_id is not None else ["execution_creation_failed"]},
}
recent_backtests = deps["get_recent_backtest_summaries"](instrument_id=instrument_id, limit=1)
latest_backtest = recent_backtests[0] if recent_backtests else None
pre_market_report = deps["pre_market_report_service"].build_report(
bias_snapshot=_to_payload(bias),
dealing_range=_to_payload(active_range),
current_price=_derive_current_price(signal),
session_code=(latest_signal or {}).get("session_code"),
)
intraday_report = deps["intraday_report_service"].build_report(
sweep_event=_to_payload(sweeps[-1]) if sweeps else None,
structure_event=_merge_structure_payload(mss_candidates[-1]) if mss_candidates else None,
fvg_zone=_to_payload(fvgs[-1]) if fvgs else None,
trade_signal=latest_signal,
)
post_trade_report = deps["post_trade_review_service"].build_report(
trade_execution=execution_record,
trade_signal=latest_signal,
backtest_result=latest_outcome,
)
return {
"health": get_health(),
"instrument_id": instrument_id,
"timeframe": timeframe,
"timeframe_set": timeframe_set,
"writes": {
"sessions": session_written,
"swings": swings_written,
"ranges": ranges_written,
"liquidity_pools": liquidity_written,
"sweeps": sweeps_written,
"displacements": displacement_written,
"mss": mss_written,
"fvg_detected": fvg_written,
"fvg_updated": fvg_updated,
"bias_snapshot_id": bias_id,
"signals": signals_written,
"trade_plan_candidate_id": trade_plan_candidate_id,
"trade_plan_candidates": 1 if trade_plan_candidate_id else 0,
"execution_id": execution_id,
"execution_status": (execution_record or {}).get("status"),
"execution_lifecycle_state": ((execution_record or {}).get("meta") or {}).get("lifecycle_state"),
"backtest_run_id": backtest_run_id,
},
"writes_by_timeframe": writes_by_timeframe,
"reports": {
"pre_market": pre_market_report,
"intraday": intraday_report,
"post_trade": post_trade_report,
},
"latest_signal": latest_signal,
"latest_trade_plan_candidate": latest_trade_plan_candidate,
"latest_backtest": latest_backtest,
"backtest_quality_gate": backtest_quality_gate_result,
}
def select_closed_loop_backtest_subject_type(
*,
recent_trade_plan_candidates: list[dict],
quality_gate: Optional[BacktestQualityGate],
) -> str:
if not recent_trade_plan_candidates:
return BACKTEST_SUBJECT_SIGNAL
replay_units = build_plan_replay_units(recent_trade_plan_candidates)
executable_units = sum(1 for unit in replay_units if unit.get("confirmed_ts") is not None)
gate = quality_gate or BacktestQualityGate()
if executable_units < gate.min_executed_signals:
return BACKTEST_SUBJECT_SIGNAL
if not any(candidate_row_is_executable(row) for row in recent_trade_plan_candidates):
return BACKTEST_SUBJECT_SIGNAL
return BACKTEST_SUBJECT_PLAN_REPLAY
def main() -> None:
health = get_health()
print("AI ICT scaffold ready")
print(f"Database URL configured: {health['database_configured']}")
print(f"Session timezone: {health['session_timezone']}")
def _to_payload(value):
if value is None:
return None
if isinstance(value, dict):
return value
if is_dataclass(value):
return asdict(value)
if hasattr(value, "__dict__"):
return {
key: val
for key, val in vars(value).items()
if not key.startswith("_")
}
return value
def _merge_structure_payload(candidate) -> Optional[dict]:
payload = _to_payload(candidate)
if payload is None:
return None
payload["event_type"] = "MSS"
return payload
def _derive_current_price(signal) -> Optional[float]:
payload = _to_payload(signal)
if payload is None or not isinstance(payload, dict):
return None
entry_low = float(payload.get("entry_low") or 0.0)
entry_high = float(payload.get("entry_high") or 0.0)
if entry_low and entry_high:
return (entry_low + entry_high) / 2
return entry_low or entry_high or None
if __name__ == "__main__":
main()