|
|
"""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,
|
|
|
)
|