# 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 ```python 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 ```yaml 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` 인자로 미래 데이터 누설 차단. ```python def scan_vwap_pullback_entries(candidates, bars_5m, cfg, as_of_bar_idx): # bars_5m[:as_of_bar_idx+1]만 사용 ``` ### 시뮬레이터 (`libs/tgtc/simulator.py`) ```python @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 최종 설정**: ```yaml 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 버그 확인: ```python # 버그: 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 계산 로직: ```python 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) 돌파를 진입 신호로 사용. ### 설계 ```python 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 기회. ### 설계 ```python 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 설정: ```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` ```python @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 버그 수정**: ```python 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 신호 추가**: ```python 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 신호 추가**: ```python 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 필드 추가**: ```python @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 계산**: ```python # 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**: ```python 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 검증 불가 | ### 핵심 판단 1. **TGTC universe (Yahoo day_gainers)는 방향성 alpha를 제공하지 않는다** - Long, HOD breakout, Fade short 모두 OOS-stable alpha 없음 - IS 과적합이 주된 문제: IS 조건에 맞는 변종이 많지만 OOS에서 재현 안 됨 2. **Quick TP는 IS 과적합 마스킹이다** - A7 (TP 2%)이 IS를 -17.4% → +13.73%로 올렸지만 OOS는 동일하게 음수 - TP가 IS 노이즈를 걸러주는 것처럼 보이지만 실제로는 OOS 패턴과 맞지 않음 3. **Score 함수가 예측력이 없다** - rank_persistence, rank_velocity, price_structure 등이 OOS에서 일관된 trade ranking을 제공 못함 - "좋은 종목 고르기"가 아닌 "운 좋은 종목 고르기"에 불과 4. **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 개념이 완전히 틀린 것은 아닐 수 있다. 개선 방향: 1. **데이터 품질**: Yahoo day_gainers는 real-time API지만 backtest를 위한 합성 재구성에는 한계. 실제 1분봉 데이터로 더 정밀한 재구성 필요. 2. **Universe 개선**: 단순 pct_change 상위가 아닌 catalyst-driven (8-K, 분기 실적) 갭 필터링 3. **1분봉 해상도**: 5분봉의 resolution 한계로 정밀한 pullback/reclaim 패턴 감지 어려움 4. **ML 기반 score**: Rule-based score 대신 통계적으로 검증된 특성 --- ## 부록 A: 테스트 자동화 인프라 ### 백테스트 실행 ```bash 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 검증 스크립트 ```python # /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 분석 ```python # /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 변종) ```yaml 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 변종) ```yaml 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 필터 개선 필요.*