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