"""Domain models for TGTC strategy.""" from __future__ import annotations from dataclasses import dataclass, field from typing import Any @dataclass class TGTCCollectionParams: start_et: str = "09:29:30" end_et: str = "10:00:00" interval_seconds: int = 20 @dataclass class TGTCFilterParams: min_price: float = 5.0 min_market_cap: float = 2_000_000_000.0 min_30m_dollar_volume: float = 10_000_000.0 min_avg_dollar_volume_20d: float = 50_000_000.0 # backtest surrogate for market_cap min_day_change_at_10: float = 0.04 max_day_change_at_10: float = 0.35 must_be_above_vwap: bool = True max_pullback_from_hod: float = 0.35 min_qqq_pct_change_at_10: float | None = None # 시장 레짐 floor (None=비활성) @dataclass class TGTCScoreWeights: rank_persistence: float = 0.30 rank_velocity: float = 0.20 price_structure: float = 0.25 volume_quality: float = 0.15 relative_strength_vs_qqq: float = 0.10 @dataclass class TGTCEntryParams: type: str = "vwap_pullback_reclaim" min_first3bar_gain_pct: float = 0.04 # 09:30~09:45 (3×5m bars) min cumulative gain pullback_volume_ratio_max: float = 0.70 stop_atr_multiple: float = 1.5 max_candidates_to_scan: int | None = None # None이면 기존 max_positions*3 유지 reclaim_volume_ratio_min: float | None = None # None=비활성. reclaim봉 volume >= pullback봉 volume * ratio no_entry_after_et: str | None = None # "12:00" 형식. None=비활성. 이 시각 이후 신규 진입 금지 no_entry_before_et: str | None = None # "11:00" 형식. None=비활성. 이 시각 이전 신규 진입 금지 fade_min_gain_pct: float | None = None # fade_short: first-3-bar gain threshold (default 0.08) @dataclass class TGTCExitParams: partial_at_1r: float = 0.33 stop_to_be_after_1r: bool = True min_trend_health_score: int = 4 eod_exit_et: str = "15:55" vwap_break_bars_to_exit: int = 2 # Stop exit mode: "conservative" (default, uses min(stop, bar_open) to model gap fills) # "optimistic" = assume stop always fills exactly at stop price (upper bound) # "moderate" = exit at stop * (1 - stop_slippage_bps/10000) — middle assumption stop_exit_mode: str = "conservative" stop_slippage_bps: float = 10.0 take_profit_pct: float | None = None # e.g., 0.015 → exit at +1.5% from entry disable_partial_at_1r: bool = False force_exit_after_minutes: int | None = None # time-stop: exit after N minutes in trade partial_levels: list | None = None # multi-level partial scale-out tp_requires_no_stop_touch: bool = False # if True, only fire quick_tp when bar.low > current_stop (avoids same-bar collision artifact) @dataclass class TGTCRiskParams: risk_per_trade_pct: float = 0.30 max_positions: int = 3 daily_loss_limit_pct: float = 1.0 initial_equity: float = 10000.0 @dataclass class TGTCStrategyParams: collection: TGTCCollectionParams = field(default_factory=TGTCCollectionParams) filters: TGTCFilterParams = field(default_factory=TGTCFilterParams) score_weights: TGTCScoreWeights = field(default_factory=TGTCScoreWeights) entry: TGTCEntryParams = field(default_factory=TGTCEntryParams) exit: TGTCExitParams = field(default_factory=TGTCExitParams) risk: TGTCRiskParams = field(default_factory=TGTCRiskParams) @dataclass class TGTCConfig: """Top-level config loaded from YAML.""" strategy_mode: str = "tgtc" tgtc_strategy: TGTCStrategyParams = field(default_factory=TGTCStrategyParams) # universe for backtest pre-screening universe: str = "broad" meta: dict[str, Any] = field(default_factory=dict) def load_tgtc_config(path: str) -> TGTCConfig: """Load a TGTC yaml config. Applies _meta, strategy_mode, tgtc_strategy fields.""" import yaml from pathlib import Path raw: dict[str, Any] = yaml.safe_load(Path(path).read_text()) or {} meta = raw.get("_meta", {}) tgtc_raw = raw.get("tgtc_strategy", {}) col_raw = tgtc_raw.get("collection", {}) flt_raw = tgtc_raw.get("filters", {}) sw_raw = tgtc_raw.get("score_weights", {}) ent_raw = tgtc_raw.get("entry", {}) ex_raw = tgtc_raw.get("exit", {}) rsk_raw = tgtc_raw.get("risk", {}) col = TGTCCollectionParams(**{k: v for k, v in col_raw.items() if k in TGTCCollectionParams.__dataclass_fields__}) flt = TGTCFilterParams(**{k: v for k, v in flt_raw.items() if k in TGTCFilterParams.__dataclass_fields__}) sw = TGTCScoreWeights(**{k: v for k, v in sw_raw.items() if k in TGTCScoreWeights.__dataclass_fields__}) ent = TGTCEntryParams(**{k: v for k, v in ent_raw.items() if k in TGTCEntryParams.__dataclass_fields__}) ex = TGTCExitParams(**{k: v for k, v in ex_raw.items() if k in TGTCExitParams.__dataclass_fields__}) rsk = TGTCRiskParams(**{k: v for k, v in rsk_raw.items() if k in TGTCRiskParams.__dataclass_fields__}) params = TGTCStrategyParams( collection=col, filters=flt, score_weights=sw, entry=ent, exit=ex, risk=rsk ) return TGTCConfig( strategy_mode=raw.get("strategy_mode", "tgtc"), tgtc_strategy=params, universe=raw.get("universe", "broad"), meta=meta, )