241 lines
11 KiB
Python
241 lines
11 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
|
|
from src.services.backtest import (
|
|
BacktestCostConfig,
|
|
BacktestQualityGate,
|
|
BacktestService,
|
|
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.ranges import DealingRangeService
|
|
from src.services.reports import IntradayReportService, PostTradeReviewService, PreMarketReportService
|
|
from src.services.sessions import SessionGenerator
|
|
from src.services.signals import MINIMAL_SIGNAL_EVIDENCE_KEYS, SignalService
|
|
from src.services.structure import MSSService
|
|
from src.services.sweeps import SweepService
|
|
from src.services.swings import SwingService
|
|
|
|
|
|
def build_default_runtime_dependencies(signal_required_evidence_keys: tuple[str, ...] = MINIMAL_SIGNAL_EVIDENCE_KEYS) -> dict:
|
|
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),
|
|
"execution_service": ExecutionService(),
|
|
"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_backtest_summaries": get_recent_backtest_summaries,
|
|
}
|
|
|
|
|
|
def run_closed_loop(
|
|
instrument_id: int = 1,
|
|
timeframe: str = "1m",
|
|
backtest_limit: int = 100,
|
|
run_execution: bool = True,
|
|
backtest_cost_config: Optional[BacktestCostConfig] = None,
|
|
backtest_quality_gate: Optional[BacktestQualityGate] = None,
|
|
signal_required_evidence_keys: tuple[str, ...] = MINIMAL_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)
|
|
|
|
session_written = session_generator.generate_for_date(instrument_id=instrument_id, timeframe=timeframe, session_date=now)
|
|
|
|
swing_service = deps["swing_service"]
|
|
swings = swing_service.detect_swings(instrument_id=instrument_id, timeframe=timeframe, left_bars=2, right_bars=2, limit=500)
|
|
swings_written = swing_service.persist_swings(instrument_id=instrument_id, timeframe=timeframe, candidates=swings)
|
|
|
|
range_service = deps["range_service"]
|
|
active_range = range_service.build_active_range(instrument_id=instrument_id, timeframe=timeframe)
|
|
ranges_written = range_service.persist_active_range(instrument_id=instrument_id, timeframe=timeframe, candidate=active_range)
|
|
|
|
liquidity_service = deps["liquidity_service"]
|
|
liquidity = liquidity_service.build_candidates(instrument_id=instrument_id, timeframe=timeframe)
|
|
liquidity_written = liquidity_service.persist_candidates(instrument_id=instrument_id, timeframe=timeframe, candidates=liquidity)
|
|
|
|
sweep_service = deps["sweep_service"]
|
|
sweeps = sweep_service.detect_sweeps(instrument_id=instrument_id, timeframe=timeframe)
|
|
sweeps_written = sweep_service.persist_sweeps(instrument_id=instrument_id, timeframe=timeframe, candidates=sweeps)
|
|
|
|
displacement_service = deps["displacement_service"]
|
|
displacements = displacement_service.detect_displacement(instrument_id=instrument_id, timeframe=timeframe, limit=200)
|
|
displacement_written = displacement_service.persist_displacement(instrument_id=instrument_id, timeframe=timeframe, candidates=displacements)
|
|
|
|
mss_service = deps["mss_service"]
|
|
mss_candidates = mss_service.detect_mss(instrument_id=instrument_id, timeframe=timeframe)
|
|
mss_written = mss_service.persist_mss(instrument_id=instrument_id, timeframe=timeframe, candidates=mss_candidates)
|
|
|
|
fvg_service = deps["fvg_service"]
|
|
fvgs = fvg_service.detect_fvg(instrument_id=instrument_id, timeframe=timeframe, limit=200)
|
|
fvg_written = fvg_service.persist_fvg(instrument_id=instrument_id, timeframe=timeframe, candidates=fvgs)
|
|
fvg_updated = fvg_service.update_existing_fvgs(instrument_id=instrument_id, timeframe=timeframe)
|
|
|
|
bias_service = deps["bias_service"]
|
|
bias = bias_service.build_bias(instrument_id=instrument_id, bias_tf=timeframe, setup_tf=timeframe, trigger_tf=timeframe)
|
|
bias_id = bias_service.persist_bias(instrument_id=instrument_id, bias_tf=timeframe, setup_tf=timeframe, trigger_tf=timeframe, candidate=bias)
|
|
|
|
signal_service = deps["signal_service"]
|
|
signal = signal_service.build_signal(instrument_id=instrument_id, setup_tf=timeframe, trigger_tf=timeframe)
|
|
signals_written = signal_service.persist_signal(instrument_id=instrument_id, setup_tf=timeframe, trigger_tf=timeframe, candidate=signal)
|
|
|
|
recent_signals = deps["get_recent_signals"](instrument_id=instrument_id, limit=1)
|
|
latest_signal = recent_signals[0] if recent_signals 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,
|
|
)
|
|
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,
|
|
"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,
|
|
"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,
|
|
},
|
|
"reports": {
|
|
"pre_market": pre_market_report,
|
|
"intraday": intraday_report,
|
|
"post_trade": post_trade_report,
|
|
},
|
|
"latest_signal": latest_signal,
|
|
"latest_backtest": latest_backtest,
|
|
"backtest_quality_gate": backtest_quality_gate_result,
|
|
}
|
|
|
|
|
|
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()
|