You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

130 lines
5.1 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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