# 꼬리잡기 백테스트 — 웹 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` — **코드 변경·실수 목록·다른 전략 적용 체크리스트 (최종)**