# 꼬리잡기 백테스트 — 웹 vs 파라서치 정리 (2026-05-30)

> **목적:** `backtest_web` 꼬리 탭, `tail_param_search.py`, 실매(`TailCatchStrategy`) 결과가 어긋나던 원인·수정·운영 가이드를 한곳에 모음.

---

## 1. 아키텍처: SCAN vs TRIGGER

꼬리 전략은 **2단계**로 동작한다.

```
[SCAN] HTS/KIS 조건검색 (널넬하게)
  A) 1봉 등락률 -10% ~ -1.5%  (직전봉 종가 대비 현재봉 하락)
  B) 체결강도 85% ~ 400%
  C) 3봉전 대비 0봉 거래량 1.8~20배
  → ConditionSearchManager / KIS 조건식 `tail`
  → target_candidates_history (strategy_id=SHORT) 저장

[TRIGGER] tail_engine (엄격하게 — 매수 직전)
  망치 꼬리, 당일 회복률, 3분봉 회복 위치, (선택) RSI·MA20,
  고점추격·피뢰침, 시간대/쿨다운, (옵션) HTS A 재검사
```

| 단계 | 역할 | 백테에서 |
|------|------|----------|
| **SCAN** | 후보 종목·시각 좁히기 | `target_candidates_history` (저장 이력 ON) |
| **TRIGGER** | “지금 사도 되나?” 최종 검사 | `tail_engine._eval_tail_buy_at_index` |

**원칙 (`.cursorrules` SCAN vs TRIGGER 분리):**

- 무거운 필터·API 호출은 5분 스캔이 아니라 **매수 타점(TRIGGER)** 에 둔다.
- 저장 이력 ON이면 SCAN은 이미 HTS로 걸렀으므로, TRIGGER에서 **중복 필터**를 또 걸면 거래가 사라진다.

---

## 2. 공통 코드 경로 (2026-05-30 기준)

| 구성요소 | 파일 | 역할 |
|----------|------|------|
| 공통 로더 | `kis_trader/backtest/tail_backtest_common.py` | 유니버스·캔들·손익·`run_tail_backtest_web_aligned` |
| 엔진 | `kis_trader/engine/tail_engine.py` | 진입/청산, `get_tail_defaults_from_db`, `run_tail_backtest` |
| 파라서치 | `kis_trader/backtest/tail_param_search.py` | fast/coarse 그리드, `--apply` |
| 웹 API | `backtest_web.py` → `/api/backtest/tail` | 꼬리 탭 백테 |

웹·파라서치 모두:

1. `tbc.resolve_tail_universe()` — SHORT 저장 이력 (기본)
2. `tbc.load_tail_candles_by_code()` — ws_candles
3. `te.run_tail_backtest()` — 동일 엔진

---

## 3. 웹 123건 vs 파라서치 6건 — 원인과 수정

### 3.1 증상

| 조건 | 거래 수 | 손익 (대략) |
|------|---------|-------------|
| 파라서치 / DB base + 저장 이력 | **6건** | +60~69만 |
| 웹 (버그 시) 저장 이력 5772슬롯 | **123건** | +347만 |
| 저장 이력 OFF (전종목) | **133건+** | — |

### 3.2 근본 원인

웹 API가 폼 숫자만 `params`에 넣고, DB의 **TRIGGER 플래그**를 빠뜨렸다.

| 플래그 | DB (파라서치) | 웹 누락 시 엔진 기본값 | 영향 |
|--------|---------------|------------------------|------|
| `skip_hts_scan_dupes` | **False** | True (저장 이력 시 setdefault) | **거래 수 결정적** |
| `use_rsi_filter` | False | True | 소폭 |
| `use_daily_range_filter` | False | True | 이 기간 거의 없음 |
| `use_high_chase_filter` | False | True | 이 기간 거의 없음 |

**재현 (5/18~5/29, 저장 이력 ON):**

| 설정 | 거래 |
|------|------|
| 웹 params만 (플래그 없음) | 135건 |
| DB 플래그 전부 반영 | 6건 |
| `skip_hts=True` + 선택필터 ON (웹 버그와 유사) | **123건** |
| `skip_hts=True` only | 161건 |

### 3.3 수정 (backtest_web.py)

`tail_trigger_flags`를 DB `_def`에서 병합:

- `skip_hts_scan_dupes`, `use_intraday_drop`, `use_ma20_filter`
- `use_rsi_filter`, `use_daily_range_filter`, `use_high_chase_filter`
- `bar_chg_min_pct`, `bar_chg_max_pct`

응답 JSON `params`에도 동일 플래그 포함 (디버깅용).

**확인:** 재시작 후 응답에 `skip_hts_scan_dupes: false` 등이 보이고, 웹·파라서치 거래 수가 맞아야 함.

---

## 4. `TAIL_SKIP_HTS_SCAN_DUPES` / `skip_hts_scan_dupes` 상세

### 4.1 이름

| 이름 | 위치 |
|------|------|
| `TAIL_SKIP_HTS_SCAN_DUPES` | DB `env_config` |
| `skip_hts_scan_dupes` | Python `params` dict |

**같은 스위치** — TRIGGER에서 HTS SCAN 조건을 **또 검사할지** 여부.

### 4.2 HTS A만 해당 (B·C 아님)

| HTS | 내용 | TRIGGER 재검사 |
|-----|------|----------------|
| **A** | 직전봉 종가 대비 -10% ~ -1.5% | **`skip_hts`가 제어** |
| B | 체결강도 | ❌ (SCAN만) |
| C | 3봉 전 거래량 배수 | ❌ (SCAN만) |

코드 (`tail_engine.py`):

```python
if not skip_hts and i >= 1:
    bar_chg = (cl - prev_cl) / prev_cl * 100.0
    if bar_chg < bar_chg_min_pct or bar_chg > bar_chg_max_pct:
        return ("탈락-봉등락", ...)  # TAIL_BAR_CHG_MIN/MAX_PCT 기본 -10 ~ -1.5
```

| 값 | 의미 | 저장 이력 ON 시 거래 (5/18~5/29) |
|----|------|----------------------------------|
| **True** | HTS A **생략** (SCAN 신뢰) | ~123~161건 |
| **False** | HTS A **재검사** (이중 필터) | **~5~6건** |

### 4.3 `min_drop_rate` / 회복률과의 차이

| | HTS A | `min_recovery_ratio` 등 |
|--|-------|-------------------------|
| 비교 | **직전 3분봉 종가 → 현재 종가** | **당일 저점~고가 범위 내 회복** |
| 별도 축 | `TAIL_USE_INTRADAY_DROP=true` 시 **시가→저점 낙폭** (HTS A와 다름) | 항상 TRIGGER에서 검사 |

### 4.4 엔진 설계 의도

- **저장 이력 ON** → `skip_hts=True` 권장 (중복 필터 방지)
- **전종목 백테** (이력 OFF) → `skip_hts=False`로 HTS A를 TRIGGER에서 대신 검사

현재 DB는 `False` → 매우 보수적 (~0.5건/일).

---

## 5. 기타 TRIGGER 플래그 (그리드 **미포함**, DB 고정)

| env 키 | params 키 | True = | False = |
|--------|-----------|--------|---------|
| `TAIL_USE_RSI_FILTER` | `use_rsi_filter` | RSI≥임계값 탈락 (적음) | RSI 검사 안 함 |
| `TAIL_USE_DAILY_RANGE_FILTER` | `use_daily_range_filter` | 일중 변동폭 초과 탈락 | 검사 안 함 |
| `TAIL_USE_HIGH_CHASE_FILTER` | `use_high_chase_filter` | 고점 추격 탈락 | 검사 안 함 |
| `TAIL_USE_MA20_FILTER` | `use_ma20_filter` | MA20 필터 | 검사 안 함 |
| `TAIL_USE_INTRADAY_DROP` | `use_intraday_drop` | 시가→저점 낙폭 검사 | 검사 안 함 |

**`use_*_filter`:** True = 필터 **켜짐** → 기회 **↓**  
**`skip_hts`:** True = 검사 **생략** → 기회 **↑** (이름만 반대로 느껴짐)

---

## 6. `tail_param_search.py` fast 그리드 (2026-05-30)

### 6.1 fast 모드 축 (768조합, ~1~2분)

| 축 | 값 |
|----|-----|
| `min_drop_rate` | 2%, 3% |
| `min_recovery_ratio` | 40%, 45%, 50% |
| `tail_ratio_min` | 1.0, 1.5 |
| `max_rec_3m` | 85%, 90% |
| `shoulder_min_high` | 0.3%, 0.5% |
| `shoulder_cut_pct` | 0.2%, 0.3% |
| `stop_atr_mult` | 1.5, 2.0 |
| `target_atr_mult` | 1.5, 2.0 |
| `atr_tp_max_pct` | 0.8%, 1.0% |

**DB 고정 (그리드 밖):** `skip_hts_scan_dupes`, `use_rsi_filter`, RSI 기간, 쿨다운, `tail_pct_min` 등.

### 6.2 `--apply` 동작

- **그리드 축만** DB 반영 (전체 `apply_cfg` 덮어쓰기 방지)
- 진입·청산 env 키 예: `MIN_DROP_RATE`, `MIN_RECOVERY_RATIO_SHORT`, `TAIL_RATIO_MIN`, `MAX_RECOVERY_RATIO_3M`, `SHOULDER_*`, `STOP/TARGET_ATR_MULTIPLIER_TAIL`, `TAIL_ATR_TP_MAX_PCT`

### 6.3 최근 실행 결과 (2026-05-18 ~ 2026-05-29, 830~1530, 저장 이력)

| 실행 | 조합 | 1위 거래 | 1위 손익 | env_id |
|------|------|----------|----------|--------|
| fast 192 (진입+청산) | 192 | 6건 | +609,166 | 206 |
| fast 768 (+ tail_ratio, max_rec_3m) | 768 | 5건 | +690,252 | 207 |

**768조합 대부분 동률** — `skip_hts=False` 병목 때문에 낙폭·회복·청산축을 바꿔도 거래 수가 거의 안 변함.  
회복률 50%만 3건 등으로 더 줄어드는 예외 있음.

**1위 (env 207) 적용값:**

- 낙폭 2%, 회복 40%, 꼬리/몸통 1.0, 3분 회복 상한 90%
- 어깨 0.5%/0.2%, ATR 2.0/2.0, 익절 상한 1.0%

### 6.4 표준 실행 명령

```bash
python3 kis_trader/backtest/tail_param_search.py \
  --start 2026-05-18 --end 2026-05-29 \
  --time-start 830 --time-end 1530 \
  --min_trades 1 --apply
```

결과 JSON: `kis_trader/backtest/results/search_tail_fast_YYYYMMDD_HHMMSS.json`

---

## 7. RSI 14

- **고정 아님** — `env_config.RSI_PERIOD` (DB 기본 **14**)
- 꼬리 3분봉 + RSI(14) ≈ 42분 창 과열 필터
- 스캘핑 RSI(3)와 **별개**

---

## 8. `max_daily` 함정

- **종목당** 하루 N회 (계좌 전체 합산이 아님)
- 전종목 + 완화 진입 시 거래 수 폭증 가능

---

## 9. 가상 거래 내역 (최근 200건) — 정렬 수정

### 9.1 문제

- `all_trades[-200:]` — 종목 코드 순 append → **최근 200건이 아님**
- 프론트 `.reverse()`만으로는 **매도 시각 최신순** 불가

### 9.2 수정 (backtest_web.py)

**Python:** `_trades_recent_first(trades, 200)`

- `exit_time` / `sell_time` / `sell_date` 기준 **내림차순** 후 200건
- 적용: 실거래, 스캘핑, 꼬리, 돌파 API

**JavaScript:** `tradesNewestFirst(trades)`

- 모든 “가상 거래 내역 (최근 200건)” 테이블에 적용
- **맨 위 = 가장 최근 매도**

---

## 10. 웹 백테스트 체크리스트 (파라서치와 맞출 때)

| 항목 | 맞춰야 할 값 |
|------|--------------|
| 기간 | `--start` / `--end` 동일 |
| TF | 3분 (ws_candles) |
| 매수 시간 | 830~1530 (웹 `tl_ts` / `tl_te`) |
| 저장 이력 | ON/OFF 동일 (`universe=history|all`) |
| TRIGGER 플래그 | 응답 JSON `skip_hts_scan_dupes` 등 확인 |
| 서버 | `backtest_web.py` 수정 후 **재시작** |

**경고 문구 (summary.universe_warning):**

- 저장 이력 ON인데 거래 > 25건 → 파라미터/플래그 불일치 의심
- 수정 후에는 6건 전후가 정상 (`skip_hts=False` 기준)

---

## 11. 거래 빈도 목표 (주 5~10건) 시 권장

`skip_hts`만 토글하면 **6건 ↔ 123건 cliff** — 중간값 없음.

| 방향 | 조치 |
|------|------|
| **A (권장)** | 저장 이력 ON + `TAIL_SKIP_HTS_SCAN_DUPES=true` 고정, 진입 수치(`min_drop`, `recovery`, `tail_ratio`)로 조절 |
| **B** | `skip_hts=False` 유지 + 진입 수치 완화 (coarse/full) |
| **C** | fast 그리드에 `skip_hts` 축 추가 → 768×2=1536조합 (모드 선택용) |

---

## 12. 관련 env 키 (config_schema)

```
TAIL_SKIP_HTS_SCAN_DUPES
TAIL_USE_INTRADAY_DROP
TAIL_USE_MA20_FILTER
TAIL_USE_RSI_FILTER
TAIL_USE_DAILY_RANGE_FILTER
TAIL_USE_HIGH_CHASE_FILTER
TAIL_BAR_CHG_MIN_PCT / TAIL_BAR_CHG_MAX_PCT
MIN_DROP_RATE / MIN_RECOVERY_RATIO_SHORT
TAIL_RATIO_MIN / MAX_RECOVERY_RATIO_3M
SHOULDER_MIN_HIGH_PCT / SHOULDER_CUT_PCT
STOP_ATR_MULTIPLIER_TAIL / TARGET_ATR_MULTIPLIER_TAIL
TAIL_ATR_TP_MAX_PCT
```

---

## 13. 변경 이력 요약

| 날짜 | 변경 |
|------|------|
| 2026-05-30 | `tail_backtest_common.py` — 웹·파라서치 공통 로더 |
| 2026-05-30 | 웹 API TRIGGER 플래그 DB 병합 (123 vs 6건 버그) |
| 2026-05-30 | fast 그리드: 진입(`min_drop`, `recovery`, `tail_ratio`, `max_rec_3m`) + 청산, 768조합 |
| 2026-05-30 | 가상 거래 내역 매도 시각 최신순 정렬 |

---

## 14. 참고 파일

- `kis_trader/engine/tail_engine.py` — SCAN/TRIGGER 주석, `_eval_tail_buy_at_index`
- `kis_trader/backtest/tail_param_search.py` — `PARAM_GRIDS["fast"]`
- `backtest_web.py` — `/api/backtest/tail`, `_trades_recent_first`, `tradesNewestFirst()`
- `docs/TAIL_BACKTEST_WEB_VS_PARAM_SEARCH.md` — 꼬리 도메인·skip_hts·어깨컷 상세
- `docs/BACKTEST_ALIGNMENT_FINAL.md` — **코드 변경·실수 목록·다른 전략 적용 체크리스트 (최종)**
