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.
fithia2/docs/tgtc_v1_development_report.md

773 lines
25 KiB
Markdown

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.

# 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 필터 개선 필요.*