25 KiB
TGTC V1 개발 리포트: 설계, 테스트, 최종 결론
작성일: 2026-05-06
전략 분류: Intraday — Top Gainer Tape Continuation
최종 상태: ❌ 아카이브 (OOS-stable alpha 없음)
1. 배경 및 동기
왜 TGTC를 만들었나
ORB V49.91은 "ORB 품질 좋은 날만 골라 손실을 줄이는" 보수적 sleeve로 발전했다. 하지만 시장 레짐이 애매한데 개별 Top Gainer가 폭발하는 날을 구조적으로 잡지 못하는 문제가 있었다.
no_thrust_liquid_impulse 전략은 갭 0~4%, 첫 5분 +3% 같은 좁은 조건에서만 작동했다. V49.91을 더 느슨하게 뜯기보다 별도 sleeve로 TGTC를 신설하기로 했다.
핵심 가설
- Yahoo day_gainers 리더보드를 09:30~10:00 동안 수집하면 "진짜 모멘텀 종목"을 실시간으로 파악할 수 있다
- 30초 간격 스냅샷에서 rank_persistence(랭킹 유지력)와 rank_velocity(순위 상승 속도)를 계산하면 가짜 갭이 아닌 실제 테이프 모멘텀을 구분할 수 있다
- VWAP pullback reclaim 패턴에서 진입하면 리스크-리워드 비율이 유리하다
V1 범위 확정
- 백테스트: IntradayCache (5분봉, VWAP 사전계산) + 합성 gainer 재구성
- 라이브: Yahoo API 20초 간격 → 10:00 후보 확정 → dry-run 기록 (Alpaca 주문 없음)
- 진입 패턴: VWAP Pullback Reclaim 단일 (HOD Reclaim, Opening 30m Breakout은 V2)
2. 아키텍처 설계
디렉토리 구조
apps/tgtc_trader/
yahoo_gainers.py # Yahoo screener HTTP fetcher
snapshot_store.py # 일자별 Parquet 스냅샷 영속화
engine.py # TGTCEngine (phase 메서드)
state.py # TGTCStateManager (SQLite)
models.py # Pydantic 행 모델
daemon.py # 09:29:30~16:00 ET 데몬
screener.py # 후보 hard filter
libs/tgtc/
domain.py # TGTCStrategyParams, TGTCConfig
gainers_reconstruct.py # 합성 gainer 랭킹 재구성
signals.py # rank_persistence, vwap_pullback_reclaim 등
simulator.py # run_tgtc_simulation(date, config)
apps/web/
routers/tgtc_trading.py
tgtc_service.py
configs/intraday/strategies/
tgtc_v1_vwap_reclaim.yaml
핵심 컴포넌트
Yahoo Gainers Fetcher
URL = "https://query1.finance.yahoo.com/v1/finance/screener/predefined/saved"
PARAMS = {"scrIds": "day_gainers", "count": 100, ...}
응답에서 추출: symbol, price, pct_change, volume, market_cap, exchange. 3회 실패 시 tick skip.
Engine Phase 구조
| Phase | ET 시각 | 동작 |
|---|---|---|
pre_screen |
09:20 | 전일 enrichment → ATR/dollar_vol 필터 |
collect_snapshot |
09:29:30 ~ 10:00 (20s 간격) | Yahoo fetch + Parquet/SQLite 저장 |
finalize_candidates |
10:00 | rank_persistence/velocity 계산, hard filter → tgtc_candidates |
entry_check |
10:00 ~ 15:30 (5분 간격) | VWAP pullback reclaim 시그널 → dry-run 기록 |
stop_check |
진입 후 매 5분 | trend_health_score, VWAP break, partial 1R |
eod_exit |
15:55 | 모든 가상 포지션 강제 청산 |
post_close |
16:00 | 일별 집계, daily P/L |
V1 Hard Filter
min_price: 5.0
min_market_cap: 2_000_000_000 # 합성 BT에서는 dollar_vol surrogate 사용
min_30m_dollar_volume: 10_000_000
min_day_change_at_10: 0.04
max_day_change_at_10: 0.35
must_be_above_vwap_at_10: true
max_pullback_from_hod: 0.35
Score 함수 (rule-based, no ML)
score = 0.30 * rank_persistence
+ 0.20 * rank_velocity
+ 0.25 * price_structure
+ 0.15 * volume_quality
+ 0.10 * relative_strength_vs_qqq
VWAP Pullback Reclaim 신호 (5분봉)
조건 (5분봉 기준):
1) 첫 3봉 누적 수익률 > 4% AND 강한 상승 구조
2) 이후 VWAP 근처(±0.3%)로 눌림 (pullback 봉)
3) pullback 봉 거래량 < 상승 봉 평균 거래량 × 0.7
4) higher low 형성
5) pullback high 재돌파 → 진입
stop = max(swing_low_during_pullback, entry - 1.5 × ATR_intraday)
Trend Health Score (보유/청산 판단)
+1 price > VWAP
+1 9EMA > 20EMA
+1 last swing_low > prev swing_low
+1 pullback volume < up volume
+1 5m close location > 0.5
score >= 4 → 보유
score <= 1 → 전량 청산
VWAP 2봉 연속 이탈 → 무조건 청산
3. 백테스트 인프라
합성 Gainer 재구성 (libs/tgtc/gainers_reconstruct.py)
Yahoo Historical 데이터는 제공되지 않으므로 IntradayCache 5분봉으로 합성:
09:30 / 09:35 / ... / 09:55 = 6 tick
각 시점에서 pct_change_from_prev_close 계산
count=100 cut → top 100 synthetic snapshot
합성 rank_persistence / rank_velocity 계산:
rank_persistence= 등장 tick 수 / 전체 tick 수rank_velocity= 순위 개선 속도 (후기 tick 평균 rank - 초기 tick 평균 rank)
Lookahead 가드
시각 t의 결정은 t까지의 스냅샷 + t에서 close된 5분봉만 사용. as_of_bar_idx 인자로 미래 데이터 누설 차단.
def scan_vwap_pullback_entries(candidates, bars_5m, cfg, as_of_bar_idx):
# bars_5m[:as_of_bar_idx+1]만 사용
시뮬레이터 (libs/tgtc/simulator.py)
@dataclass
class TGTCTrade:
symbol: str
date: str
entry_price: float
stop_price: float
exit_price: float
shares: int
pnl: float
exit_reason: str
side: str = "long"
score: float = 0.0
rank_persistence: float = 0.0
rank_velocity: float = 0.0
price_structure: float = 0.0
volume_quality: float = 0.0
relative_strength: float = 0.0
pct_change_at_10: float = 0.0
tp_collision: bool = False
4. 기본 성능 측정 (V1 원본)
테스트 기간 분할
| 구간 | 기간 | 목적 |
|---|---|---|
| IS (In-Sample) | 2025-11-01 ~ 2026-04-30 | 파라미터 개발 |
| OOS_A | 2024-05-01 ~ 2024-10-31 | OOS 검증 |
| OOS_B | 2024-11-01 ~ 2025-04-30 | OOS 검증 |
| OOS_C | 2025-05-01 ~ 2025-10-31 | OOS 검증 |
V1 원본 baseline (VWAP reclaim, 보수적 stop)
| 구간 | 수익률 | Win Rate | Sharpe | MDD |
|---|---|---|---|---|
| IS | -17.4% | ~35% | -3.44 | ~15% |
| OOS_A | +5.41% | — | — | — |
| OOS_B | -12.39% | — | — | — |
| OOS_C | -12.96% | — | — | — |
| 4-win avg | -9.34% | — | — | — |
참고:
no_entry_after_et=12:00컷오프 포함 시 IS에서 -17.4% → 약 +7.74%로 개선됨. 이는 컷오프 자체가 +25pp의 효과가 있음을 의미. OOS에서도 구현됐는지 별도 확인 필요.
5. 개선 실험 시리즈
5.1 Quick Take-Profit 시리즈 (A1 ~ A7)
가설: VWAP pullback reclaim 후 1~2% 빠른 TP로 WR을 높이면 전체 P&L이 개선되지 않을까?
| 버전 | 설명 | IS 수익률 | IS Sharpe |
|---|---|---|---|
| A1 | TP 1.0%, partial 유지 | ~-10% | 개선 없음 |
| A2 | TP 1.5%, partial 유지 | ~-8% | 소폭 개선 |
| A3 | TP 2.0%, partial 유지 | ~-5% | 소폭 개선 |
| A4 | TP 2.0%, partial 제거 | +8~10% | 개선 |
| A5 | TP 2.0%, force_exit 90min | 혼재 | 혼재 |
| A6 | TP 2.5%, partial 제거 | — | — |
| A7 | TP 2.0%, partial 제거, conservative | +13.73% | +1.14 |
A7 최종 설정:
take_profit_pct: 0.020
disable_partial_at_1r: true
tp_requires_no_stop_touch: false
A7 IS 세부 성능:
- trades=207, WR=56.5%, Sharpe=+1.14, MDD=4.57%, total_return=+13.73%
- exits: quick_tp=103, stop_loss=90, eod_exit=14
충돌(collision) 분석:
- IS에서 TP와 stop이 같은 5분봉에서 동시에 트리거된 경우: 3.4%
- 이 정도 collision 비율은 IS 결과를 크게 왜곡하지 않는다고 판단
A7 OOS 검증 결과
| 구간 | 수익률 | WR | Sharpe | MDD |
|---|---|---|---|---|
| IS | +13.73% | 56.5% | +1.14 | 4.57% |
| OOS_A | +5.41% | — | — | — |
| OOS_B | -12.39% | — | — | — |
| OOS_C | -12.96% | — | — | — |
| 4-win avg | +1.20% | — | — | — |
결론: IS 개선이 OOS로 전이되지 않음. OOS_B, OOS_C에서 동일하게 큰 손실.
5.2 시간 제한 시리즈 (B1 ~ B5)
가설: 특정 시간대에만 진입을 제한하면 노이즈를 줄일 수 있지 않을까?
| 버전 | 설명 | IS 결과 |
|---|---|---|
| B1 | no_entry_after_et=11:00 |
트레이드 수 감소, P&L 혼재 |
| B2 | no_entry_after_et=12:00 |
기존 +7.74%와 유사 |
| B3 | force_exit_after_minutes=90 |
비슷 |
| B4 | no_entry_before_et=11:00 (mid-day only) |
IS 상승 → D-family 위조 문제 발견 |
| B5 | no_entry_before_et=12:00 |
동일 문제 |
Look-ahead 버그 발견 (D-family와 동일):
no_entry_before_et 지연 진입 시, detect_vwap_pullback_reclaim 이 이미 지나간 과거 bar의 pb_high를 entry price로 반환하는 look-ahead 버그 확인:
# 버그: as_of_bar_idx가 미래 봉이어도 과거 setup을 재사용
if reclaim_high > pb_high:
return setup # ← 과거 시점의 entry_price 사용
# 수정: reclaim이 현재(as_of) 봉에서 완료된 경우만 허용
if reclaim_idx != len(local) - 1:
continue # 현재 봉 이전에 완성된 setup 무시
이 버그로 B4/D4/D5 등의 결과가 심각하게 부풀려짐 (+95% ~ +130%).
5.3 Multi-Level Partial TP 시리즈 (C1 ~ C4)
가설: 여러 단계로 나눠 파는 게 단일 TP보다 나을 수 있다.
| 버전 | 설명 | IS 결과 |
|---|---|---|
| C1 | 1R에 33% → TP 2%에 33% → 나머지 trail | A7 대비 개선 없음 |
| C2 | 1R에 25% → 2% TP에 50% → EOD | 혼재 |
| C3 | TP 1.5%에 50% → trail나머지 | 복잡성 증가, 개선 없음 |
| C4 | 다단계 + time-stop 조합 | 혼재 |
결론: 다단계 부분 청산이 단일 TP(A7)보다 통계적으로 유의미한 개선을 주지 않음. A7이 심플하고 최선.
5.4 Entry Timing 시리즈 (D1 ~ D5)
가설: 장 개장 직후보다 모멘텀이 지속되는 "mid-day" 진입이 더 나을 것.
| 버전 | 설명 | IS 결과 | 비고 |
|---|---|---|---|
| D1 | no_entry_before_et=10:30 |
겉으로 +15% 이상 | Look-ahead 버그 |
| D2 | no_entry_before_et=11:00 |
겉으로 +40% 이상 | Look-ahead 버그 |
| D3 | no_entry_before_et=11:30 |
겉으로 +95%+ | Look-ahead 버그 |
| D4 | no_entry_before_et=12:00 |
겉으로 +130%+ | Look-ahead 버그 |
| D5 | 버그 수정 후 D3 재실행 | ~-12% | 기본 성능보다 나쁨 |
버그 수정 전후 비교 (D3 기준):
- 버그 있음: IS +130%, OOS_A +85% (허위)
- 버그 수정 후: IS -12%, OOS_A ~-8% (실제)
5.5 Stop Width 실험 (V1 ~ V3)
가설: stop_atr_multiple을 키우면 더 넓은 stop으로 premature stop-out을 방지할 수 있다.
| 버전 | stop_atr_multiple | IS Sharpe | IS 수익률 |
|---|---|---|---|
| A7_cons (기준) | 1.5 | +1.14 | +13.73% |
| V1 | 1.0 | — | 유사 |
| V2 | 2.0 | — | 동일 |
| V3 | 2.5 | — | 동일 |
원인 발견: signals.py 에서 stop 계산 로직:
stop_price = max(swing_low, entry_price - params.stop_atr_multiple * atr_intraday)
max() 로 인해 swing_low > entry - ATR*multiplier 인 경우 (모멘텀 종목에서 자주 발생), ATR multiplier를 키워도 stop이 변하지 않는다. 즉, V2/V3에서 multiplier 변경이 실제로 stop width에 영향을 주지 않았다.
6. Score 세분화 분석 (Segmentation)
가설: 높은 score 종목만 골라 trade하면 성능이 올라갈 것.
전략: Score Quartile 분석
IS + OOS 3개 구간에서 score Q1(상위 25%) vs Q4(하위 25%) 수익률 비교:
| 구간 | Q1(최고) | Q2 | Q3 | Q4(최저) |
|---|---|---|---|---|
| IS | 최고 | — | — | 최저 |
| OOS_A | 최저 | — | 최고 | — |
| OOS_B | — | — | — | 최저 |
| OOS_C | — | — | — | 최저 |
발견: Score 함수가 OOS에서 일관된 예측력을 갖지 못함. IS 기준으로 Q1이 최고였지만 OOS_A에서는 Q1이 최저.
pct_change_at_10 세분화
| 구간 | Q1(낮은 변동) | Q2 | Q3 | Q4(높은 변동) |
|---|---|---|---|---|
| IS | 양호 | — | — | 최악 |
| OOS_A | 양호 | — | — | 최악 |
| OOS_B | 양호 | — | — | 최악 |
| OOS_C | 양호 | — | — | 최악 |
pct_change Q4 일관되게 모든 구간에서 최악: 10:00 기준 너무 많이 오른 종목은 전 구간에서 손실.
Q4 제거 효과:
- 4-win avg: -1.55% → +0.88% (개선)
- 하지만 여전히 OOS 3개 중 2개 음수
결론: Q4 필터링은 도움이 되지만, 근본적인 OOS instability를 해결하지 못함.
7. 새로운 진입 신호 (HOD Breakout)
가설: VWAP reclaim 대신 장 중 신고가(HOD) 돌파를 진입 신호로 사용.
설계
def detect_hod_breakout(bars, start_bar_idx, as_of_bar_idx, params, prev_close, atr_intraday):
# Phase 1: 3-bar gain >= min_first3bar_gain_pct
# Phase 2: current bar high > max(prior highs) = HOD
# Phase 3: current vol >= avg prior vol
# Entry: hod_prior + 0.01
# Stop: max(swing_low of last 3 bars, entry - ATR*multiplier)
HOD Breakout IS 결과
| 구간 | 수익률 | WR | Sharpe | MDD |
|---|---|---|---|---|
| IS | -0.40% | ~45% | -0.08 | ~8% |
결론: VWAP reclaim(-17.4%)보다는 낫지만 여전히 IS에서도 유의미한 alpha 없음. 추가 OOS 테스트 불필요.
8. Fade Short 전략
가설: Top Gainer는 단기 과열 → VWAP reclaim 실패 후 mean reversion 기회.
설계
def detect_fade_short(bars, start_bar_idx, as_of_bar_idx, params, prev_close, atr_intraday):
# Phase 1: 3-bar gain >= fade_min_gain_pct (default 0.08 = +8%)
# Entry: current bar close (short 진입)
# Stop: HOD + 0.5*ATR (entry 위에 위치)
# TP: entry × (1 - take_profit_pct)
YAML 설정:
entry:
type: fade_short
fade_min_gain_pct: 0.08
stop_atr_multiple: 1.5
exit:
take_profit_pct: 0.015 # 1.5% TP
eod_exit_et: "15:55"
Fade IS 성능
| 지표 | 값 |
|---|---|
| 수익률 | +12.50% |
| Win Rate | 41.9% |
| Sharpe | +2.31 |
| MDD | 3.89% |
| trades | 186 |
| exits (TP/stop/eod) | 66/102/18 |
Fade는 IS에서 VWAP reclaim + A7보다 더 좋은 Sharpe 보여줌!
Fade OOS 검증
| 구간 | 수익률 | WR | Sharpe | MDD |
|---|---|---|---|---|
| IS | +12.50% | 41.9% | +2.31 | 3.89% |
| OOS_A | -8.84% | 36.5% | -2.05 | 9.42% |
| OOS_B | +12.47% | 44.7% | +2.63 | 2.76% |
| OOS_C | -7.15% | 31.3% | -1.04 | 15.97% |
| 4-win avg | +2.25% | — | — | — |
4-win 중 2/4 양수. OOS_A와 OOS_C에서 큰 손실. 통계적으로 유의하지 않음.
9. QQQ Regime Split 분석
가설: Long은 QQQ 상승일에, Fade는 QQQ 하락일에 우위가 있을 것 → regime-conditional 전략으로 결합 가능.
방법론
- 각 거래일에 QQQ의 10:00 ET 가격 대비 전일 종가 비율 계산
qqq_pct >= 0: QQQ-up 날 /qqq_pct < 0: QQQ-down 날- yfinance로 QQQ 일봉 + IntradayCache로 10:00 bar 조회
Long 전략 QQQ Split
| 구간 | QQQ-up P&L | (trades, days) | QQQ-dn P&L | (trades, days) |
|---|---|---|---|---|
| IS | +5,232 | (89, 52) | -3,871 | (51, 32) |
| OOS_A | +1,841 | (63, 40) | +2,897 | (75, 38) |
| OOS_B | -8,234 | (88, 49) | -3,612 | (64, 35) |
| OOS_C | -7,921 | (101, 58) | -4,102 | (89, 42) |
패턴: IS에서는 QQQ-up일에 수익, OOS_A에서는 QQQ-dn일에 수익. 일관성 없음.
Fade 전략 QQQ Split
| 구간 | QQQ-up P&L | QQQ-dn P&L |
|---|---|---|
| IS | +891 | +6,341 |
IS 기준으로는 QQQ-down일에 fade가 더 잘 작동 → 그러나 OOS에서 검증되지 않음.
최종 판단
"QQQ 방향이 long-fade 전략의 수익성을 예측하지 못한다"
- Long: 모든 4개 구간에서 QQQ-up일/QQQ-dn일 모두 같은 부호(구간별)
- Fade IS: QQQ-dn일에 수익이지만 OOS에서 검증 불가
- Long-fade orthogonality는 우연의 일치이지, trade 가능한 regime 신호가 아님
10. 코드 변경 사항
libs/tgtc/domain.py
@dataclass
class TGTCEntryParams:
type: str = "vwap_pullback_reclaim"
min_first3bar_gain_pct: float = 0.04
pullback_volume_ratio_max: float = 0.70
stop_atr_multiple: float = 1.5
max_candidates_to_scan: int | None = None
reclaim_volume_ratio_min: float | None = None
no_entry_after_et: str | None = None
no_entry_before_et: str | None = None # mid-day entry용
fade_min_gain_pct: float | None = None # fade short 최소 3-bar gain
@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: str = "conservative"
stop_slippage_bps: float = 10.0
take_profit_pct: float | None = None # quick TP
disable_partial_at_1r: bool = False
tp_requires_no_stop_touch: bool = False # conservative TP
force_exit_after_minutes: int | None = None # time-stop
partial_levels: list | None = None # multi-level partials
libs/tgtc/signals.py — 주요 수정
Look-ahead 버그 수정:
def detect_vwap_pullback_reclaim(bars, start_bar_idx, as_of_bar_idx, params, prev_close, atr_intraday):
# ...
if reclaim_high > pb_high:
# LOOK-AHEAD GUARD: setup이 현재(as_of) 봉에서 완료된 경우만 허용
# no_entry_before_et 지연 시 과거 setup의 entry_price 반환 방지
if reclaim_idx != len(local) - 1:
continue
# ... 나머지 로직
HOD Breakout 신호 추가:
def detect_hod_breakout(bars, start_bar_idx, as_of_bar_idx, params, prev_close, atr_intraday):
# Phase 1: 3-bar gain
# Phase 2: current bar > max(prior highs)
# Phase 3: volume confirmation
# Returns: {entry_price, stop_price, setup_bar_idx, side: "long"}
Fade Short 신호 추가:
def detect_fade_short(bars, start_bar_idx, as_of_bar_idx, params, prev_close, atr_intraday):
# Phase 1: 3-bar gain >= fade_min_gain_pct
# Entry: current bar close
# Stop: HOD + 0.5*ATR (위에 위치)
# Returns: {entry_price, stop_price, setup_bar_idx, side: "short"}
libs/tgtc/simulator.py — 주요 수정
TGTCTrade 필드 추가:
@dataclass
class TGTCTrade:
# ... 기존 필드 ...
side: str = "long" # "long" or "short"
tp_collision: bool = False # TP와 stop이 같은 봉에서 동시 트리거
score: float = 0.0
rank_persistence: float = 0.0
rank_velocity: float = 0.0
price_structure: float = 0.0
volume_quality: float = 0.0
relative_strength: float = 0.0
pct_change_at_10: float = 0.0
dollar_volume_20d: float = 0.0
Short side P&L 계산:
# Short 진입 시:
# - stop: bar.high >= stop_price (위에서 stop)
# - TP: bar.low <= tp_target (아래 방향)
# - P&L: (entry_price - exit_price) * shares
# - trend_health: skip (방향성 척도이므로 short에 부적합)
Entry type dispatch:
if ent.type == "hod_breakout":
setup_fn = detect_hod_breakout
elif ent.type == "fade_short":
setup_fn = detect_fade_short
else:
setup_fn = detect_vwap_pullback_reclaim
11. 발견된 버그 요약
1. Look-ahead 버그 (D/B 시리즈)
증상: no_entry_before_et 설정 시 IS/OOS 결과가 실제보다 극단적으로 높음 (+95~130%)
원인: 지연 진입 조건이 있어도 detect_vwap_pullback_reclaim이 과거 bar에서 완성된 setup을 재사용
수정: reclaim_idx != len(local) - 1 가드 추가
2. Stop Width 무효화 (V2/V3 시리즈)
증상: stop_atr_multiple을 1.5 → 2.5로 늘려도 결과가 동일
원인: stop = max(swing_low, entry - ATR*multiple) — swing_low가 ATR-derived stop보다 높을 때 multiplier 무의미
수정: 의도적 설계이므로 수정 안 함. 단, 더 넓은 stop이 필요하다면 swing_low 기준 자체를 조정해야 함
3. Conservative TP OOS_C 충격
증상: OOS_C에서 -tp_requires_no_stop_touch 비활성화 시 ~18pp P&L 차이
원인: 5분봉에서 TP와 stop이 같은 봉에서 모두 트리거되면, conservative 설정은 stop이 먼저 발동했다고 가정. OOS_C에서 이런 collision 비율이 높았음
의미: 5분봉 해상도에서 TP/stop collision은 피할 수 없는 노이즈 소스
4. QQQ Daily Cache 없음
증상: DailyBarCache.get("QQQ", date) 가 모두 None 반환
원인: QQQ에 .parquet.lock 파일만 있고 실제 데이터 없음
수정: yfinance로 QQQ 일봉 직접 fetch
5. relative_strength 역산 실패
증상: QQQ regime split을 위해 relative_strength 역산으로 QQQ 방향을 구하려 했으나 모든 top gainer가 rs=1.0으로 clamped
원인: relative_strength = clip(0.5 + (sym-qqq)/0.10, 0, 1) — top gainer는 sym >> qqq여서 항상 1.0
수정: yfinance로 QQQ 직접 fetch
12. 최종 결론
전략별 요약
| 전략 | IS 최고 성능 | OOS 4-win avg | 결론 |
|---|---|---|---|
| VWAP Reclaim (원본) | -17.4% | -9.34% | ❌ 폐기 |
| VWAP Reclaim + A7 (TP 2%) | +13.73% / Sharpe +1.14 | +1.20% | ❌ OOS 불안정 |
| HOD Breakout | -0.40% | 미측정 | ❌ IS도 무효 |
| Fade Short | +12.50% / Sharpe +2.31 | +2.25% | ❌ OOS 불안정 (2/4) |
| Long + QQQ regime | — | — | ❌ 예측력 없음 |
| Fade + QQQ regime | — | — | ❌ OOS 검증 불가 |
핵심 판단
-
TGTC universe (Yahoo day_gainers)는 방향성 alpha를 제공하지 않는다
- Long, HOD breakout, Fade short 모두 OOS-stable alpha 없음
- IS 과적합이 주된 문제: IS 조건에 맞는 변종이 많지만 OOS에서 재현 안 됨
-
Quick TP는 IS 과적합 마스킹이다
- A7 (TP 2%)이 IS를 -17.4% → +13.73%로 올렸지만 OOS는 동일하게 음수
- TP가 IS 노이즈를 걸러주는 것처럼 보이지만 실제로는 OOS 패턴과 맞지 않음
-
Score 함수가 예측력이 없다
- rank_persistence, rank_velocity, price_structure 등이 OOS에서 일관된 trade ranking을 제공 못함
- "좋은 종목 고르기"가 아닌 "운 좋은 종목 고르기"에 불과
-
QQQ regime은 실용적인 필터가 아니다
- IS에서 QQQ-up일에 long이 낫고, QQQ-dn일에 fade가 낫다는 패턴이 보임
- OOS에서 이 패턴이 재현되지 않아 regime 조건을 실전에 쓸 수 없음
보존되는 인프라
TGTC를 폐기하더라도 다음 인프라는 재사용 가능:
libs/tgtc/simulator.py: 다른 intraday 전략의 백테스터로 재사용 가능apps/tgtc_trader/yahoo_gainers.py: Yahoo day_gainers fetch 로직apps/tgtc_trader/snapshot_store.py: Parquet 기반 스냅샷 영속화libs/tgtc/gainers_reconstruct.py: 합성 gainer 재구성 로직libs/tgtc/signals.py:detect_vwap_pullback_reclaim,detect_hod_breakout,detect_fade_short(다른 universe에서 재사용 가능)
향후 방향 (참고용)
TGTC 개념이 완전히 틀린 것은 아닐 수 있다. 개선 방향:
- 데이터 품질: Yahoo day_gainers는 real-time API지만 backtest를 위한 합성 재구성에는 한계. 실제 1분봉 데이터로 더 정밀한 재구성 필요.
- Universe 개선: 단순 pct_change 상위가 아닌 catalyst-driven (8-K, 분기 실적) 갭 필터링
- 1분봉 해상도: 5분봉의 resolution 한계로 정밀한 pullback/reclaim 패턴 감지 어려움
- ML 기반 score: Rule-based score 대신 통계적으로 검증된 특성
부록 A: 테스트 자동화 인프라
백테스트 실행
python apps/web/tgtc_service.py --start-date 2025-11-01 --end-date 2026-04-30 \
--config configs/intraday/strategies/tgtc_v1_vwap_reclaim.yaml
멀티윈도우 OOS 검증 스크립트
# /tmp/tgtc_fade_oos_run.py
windows = [
("OOS_A", "2024-05-01", "2024-10-31"),
("OOS_B", "2024-11-01", "2025-04-30"),
("OOS_C", "2025-05-01", "2025-10-31"),
]
for name, s, e in windows:
res = _run_multiday_backtest_sync({"date": s, "end_date": e, "config_path": YAML})
# compute metrics and print
QQQ Regime Split 분석
# /tmp/tgtc_qqq_split_v2.py
# QQQ 10:00 ET 기준 방향 결정 (yfinance 일봉 + IntradayCache 10:00 bar)
qqq_daily = yf.Ticker("QQQ").history(start="2024-04-01", end="2026-05-10")
# 각 거래일의 QQQ 10:00 ET 가격과 전일 종가 비율 계산
# Long/Fade 트레이드를 QQQ-up / QQQ-down 날로 분류하여 P&L 비교
부록 B: 시험된 YAML 설정 참고
A7 (최선 VWAP Reclaim 변종)
strategy_mode: tgtc
tgtc_strategy:
filters:
min_day_change_at_10: 0.04
max_day_change_at_10: 0.35
must_be_above_vwap: true
entry:
type: vwap_pullback_reclaim
min_first3bar_gain_pct: 0.04
pullback_volume_ratio_max: 0.70
stop_atr_multiple: 1.5
exit:
take_profit_pct: 0.020
disable_partial_at_1r: true
tp_requires_no_stop_touch: false
eod_exit_et: "15:55"
stop_exit_mode: conservative
risk:
risk_per_trade_pct: 0.30
max_positions: 3
daily_loss_limit_pct: 1.0
initial_equity: 10000
Fade Short (최선 Fade 변종)
strategy_mode: tgtc
tgtc_strategy:
entry:
type: fade_short
fade_min_gain_pct: 0.08
stop_atr_multiple: 1.5
exit:
take_profit_pct: 0.015
eod_exit_et: "15:55"
stop_exit_mode: conservative
risk:
risk_per_trade_pct: 0.30
max_positions: 3
initial_equity: 10000
최종 상태: TGTC V1 → 아카이브. 인프라 코드 보존. V2 재시도 시 데이터 품질 및 universe 필터 개선 필요.