변경 사항 ---- - _test_kiwoom_condition_list.py: 키움 웹소켓 조건검색 '목록조회' 기능을 단독으로 테스트하는 스크립트 추가 - _test_kiwoom_condition_realtime.py: 'momentum' 조건식을 실시간으로 등록하고 초기 매칭 종목 리스트 및 실시간 편입/이탈을 수신하는 테스트 스크립트 추가 - _verify_columnar_bitid.py, _verify_shared_e2e_breakout.py, _verify_shared_e2e.py: 공유 메모리 및 dict 간의 데이터 일관성을 검증하는 테스트 추가 영향 ---- - 신규 테스트 스크립트 추가로 키움 웹소켓 API의 기능 검증 및 안정성을 높임 - 기존 기능에 대한 영향 없음 Co-authored-by: Cursor <cursoragent@cursor.com>
351 lines
13 KiB
Markdown
351 lines
13 KiB
Markdown
# 백테스트·파라서치·웹 정렬 — 최종 단계 코드 정리 (2026-05-30)
|
||
|
||
> **대상:** 꼬리(SHORT) 정렬 작업에서 여러 번 잘못 수정한 뒤 도달한 **최종 패턴**.
|
||
> **목적:** 모멘텀·돌파·스캘핑 등 **다른 전략**에 같은 실수 없이 적용하기 위한 체크리스트.
|
||
|
||
관련 문서: [TAIL_BACKTEST_WEB_VS_PARAM_SEARCH.md](./TAIL_BACKTEST_WEB_VS_PARAM_SEARCH.md) (꼬리 도메인 상세)
|
||
|
||
---
|
||
|
||
## 0. 최종 아키텍처 (한 줄)
|
||
|
||
```
|
||
단일 엔진 (tail_engine / scalping_engine / breakout)
|
||
↑
|
||
공통 백테 로더 (*_backtest_common.py) ← 웹 API + param_search 둘 다
|
||
↑
|
||
get_*_defaults_from_db() ← env 단일 소스
|
||
```
|
||
|
||
**금지:** 웹 API만 별도 루프·별도 손익식·폼 숫자만 params에 넣기.
|
||
|
||
---
|
||
|
||
## 1. 신규/수정 파일 요약
|
||
|
||
| 파일 | 상태 | 역할 |
|
||
|------|------|------|
|
||
| `kis_trader/backtest/tail_backtest_common.py` | **신규** | 유니버스·캔들·손익·`run_tail_backtest_web_aligned` |
|
||
| `kis_trader/backtest/tail_param_search.py` | **수정** | fast 그리드 확장, `tbc` 경유, `--apply` 그리드만 반영 |
|
||
| `kis_trader/engine/tail_engine.py` | 기존 | `get_tail_defaults_from_db`, `run_tail_backtest`, V4 청산 |
|
||
| `backtest_web.py` | **수정** | TRIGGER 플래그 병합, 거래 정렬, 꼬리 API → `tbc` |
|
||
|
||
---
|
||
|
||
## 2. 코드 변경 상세
|
||
|
||
### 2.1 `tail_backtest_common.py` (다른 전략 템플릿)
|
||
|
||
**해야 할 것:**
|
||
|
||
| 함수 | 책임 |
|
||
|------|------|
|
||
| `date_keys(start, end)` | `YYYY-MM-DD` → `candle_time` 키 |
|
||
| `resolve_*_universe(...)` | `target_candidates_history` / 전종목 — **웹과 동일 쿼리** |
|
||
| `load_*_candles_by_code(...)` | `ws_candles` SQL — **웹과 byte 단위 동일** |
|
||
| `attach_*_trade_pnl(...)` | 수수료·세금·보유분 — **웹 jsonify와 동일식** |
|
||
| `run_*_backtest_web_aligned(...)` | `engine.run_*_backtest` 1회 + 손익 부착 |
|
||
| `fee_and_slot_from_env_row(...)` | `FEE_RATE_PCT`, `SLOT_MONEY_DEFAULT` |
|
||
|
||
**꼬리 구현:**
|
||
|
||
```python
|
||
# kis_trader/backtest/tail_backtest_common.py
|
||
trades = te.run_tail_backtest(candles_by_code, engine_params, universe_by_slot=...)
|
||
attach_tail_trade_pnl(trades, slot_money=..., fee_rate=..., sell_tax=...)
|
||
```
|
||
|
||
**다른 전략 적용 시:** `momentum_backtest_common.py`, `breakout_backtest_common.py` 등 **전략당 1파일**, 엔진 import만 교체.
|
||
|
||
---
|
||
|
||
### 2.2 `tail_param_search.py`
|
||
|
||
#### fast 그리드 (최종)
|
||
|
||
```python
|
||
"fast": {
|
||
"min_drop_rate": [0.02, 0.03],
|
||
"min_recovery_ratio": [0.4, 0.45, 0.5],
|
||
"tail_ratio_min": [1.0, 1.5],
|
||
"max_rec_3m": [0.85, 0.9],
|
||
"shoulder_min_high": [0.003, 0.005],
|
||
"shoulder_cut_pct": [0.002, 0.003],
|
||
"stop_atr_mult": [1.5, 2.0],
|
||
"target_atr_mult": [1.5, 2.0],
|
||
"atr_tp_max_pct": [0.8, 1.0],
|
||
}
|
||
# FAST_MAX_COMBOS = 768 (2×3×2×2×2×2×2×2×2)
|
||
```
|
||
|
||
#### base_params (그리드 **밖**, DB 고정)
|
||
|
||
```python
|
||
base_params = te.get_tail_defaults_from_db(db)
|
||
# + time_start_hm / time_end_hm CLI
|
||
# + scan_interval_min, force_eod_exit=False
|
||
```
|
||
|
||
**그리드에 넣지 않은 것 (거래 수·정렬에 치명적):**
|
||
|
||
- `skip_hts_scan_dupes`, `use_rsi_filter`, `use_daily_range_filter`, …
|
||
- `TAIL_SKIP_HTS_SCAN_DUPES` 등 env 플래그
|
||
|
||
#### 백테 호출 (웹과 동일)
|
||
|
||
```python
|
||
trades = tbc.run_tail_backtest_web_aligned(
|
||
candles_by_code, test_params, universe_by_slot,
|
||
slot_money=slot_money, fee_rate=fee_rate, sell_tax=sell_tax,
|
||
)
|
||
```
|
||
|
||
#### `--apply` (최종 — **그리드 축만** DB 반영)
|
||
|
||
```python
|
||
# apply_from_json — target["params"] 우선, apply_cfg 전체 덮어쓰기 금지
|
||
p = dict(target.get("params") or {})
|
||
env_map = { ... min_drop_rate → MIN_DROP_RATE, shoulder_* → SHOULDER_* ... }
|
||
snap.update(env_map) # latest snapshot 위에 merge
|
||
```
|
||
|
||
**과거 실수:** `apply_cfg` 전체를 DB에 저장 → 진입 완화값(예: rec 0.25)까지 실매에 박힘.
|
||
|
||
---
|
||
|
||
### 2.3 `backtest_web.py` — `/api/backtest/tail`
|
||
|
||
#### (A) TRIGGER 플래그 DB 병합 — **123건 vs 6건 버그 수정**
|
||
|
||
**문제:** 폼 숫자만 `params`에 넣음 → 엔진 `_eval_*` 기본값(True) 적용 → SCAN/TRIGGER 이중 필터 붕괴.
|
||
|
||
**수정:**
|
||
|
||
```python
|
||
tail_trigger_flags = {
|
||
"skip_hts_scan_dupes": _tail_bool_arg(request, "skip_hts_scan_dupes", _def.get(...)),
|
||
"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": ...,
|
||
}
|
||
params = { ...폼 숫자..., **tail_trigger_flags }
|
||
```
|
||
|
||
헬퍼: `_tail_bool_arg(request, key, def_val)` — 미전달 시 `_def`(= `get_tail_defaults_from_db`).
|
||
|
||
**응답 JSON `params`에도 `**tail_trigger_flags` 포함** → 웹·파라서치 diff 디버깅용.
|
||
|
||
#### (B) 엔진 경로 + common 모듈
|
||
|
||
```python
|
||
universe_by_slot, universe_source, universe_history_slots, _scan_iv = tbc.resolve_tail_universe(...)
|
||
candles_by_code, _, _ = tbc.load_tail_candles_by_code(...)
|
||
all_trades = tbc.run_tail_backtest_web_aligned(...)
|
||
tail_trades_out = _trades_recent_first(all_trades, 200)
|
||
```
|
||
|
||
**레거시 `else` 분기 (`use_engine=0`)** — 유니버스·V4 청산 미적용. **기본 `use_engine=1` 유지.**
|
||
|
||
#### (C) 가상 거래 200건 정렬
|
||
|
||
**문제:** `all_trades[-200:]` — 종목 코드 순 append → “최근 200건” 아님.
|
||
|
||
**수정 (Python, 전략 공통):**
|
||
|
||
```python
|
||
def _trade_exit_sort_key(t): # exit_time / sell_time / sell_date ...
|
||
def _trades_recent_first(trades, limit=200): # 매도 시각 내림차순 상위 N
|
||
```
|
||
|
||
**수정 (JS, 모든 가상 거래 테이블):**
|
||
|
||
```javascript
|
||
function tradesNewestFirst(trades) { ... }
|
||
// .reverse() 대신 명시적 정렬 — API가 최신순이어도 프론트에서 재정렬
|
||
```
|
||
|
||
적용 API: 실거래 분석, 스캘핑, 꼬리, 돌파, 홀딩, Updow, 모멘텀.
|
||
|
||
#### (D) universe_warning (요약)
|
||
|
||
- 저장 이력 요청했는데 `universe_source=all` → 이력 없음 경고
|
||
- 저장 이력 ON + 거래 > 25건 → 플래그/파라미터 불일치 의심 (수정 전 123건 케이스)
|
||
|
||
---
|
||
|
||
## 3. 잘못 수정했던 것 — 다시 하지 말 것
|
||
|
||
| # | 잘못된 접근 | 왜 틀렸나 | 최종 |
|
||
|---|-------------|-----------|------|
|
||
| 1 | 웹만 레거시 루프 유지 | 유니버스·엔진 V4와 불일치 | `tbc` + `use_engine=1` |
|
||
| 2 | 파라서치 `--apply`가 `apply_cfg` 전체 저장 | base 진입값까지 DB 오염 | **`params`(그리드 축)만 env_map** |
|
||
| 3 | fast 그리드에 진입 완화 하드코딩 (`FAST_BASE_OVERRIDES`) | 웹·실매 env와 불일치 | **제거**, DB base 고정 |
|
||
| 4 | 웹 params에 TRIGGER 플래그 생략 | `skip_hts` 등 엔진 default → 123건 | **`tail_trigger_flags` merge** |
|
||
| 5 | `skip_hts`를 그리드에 넣어 768×2 탐색 | 모드 선택이지 미세 <20>uning 아님 | **env 고정** 또는 정책 결정 후 고정 |
|
||
| 6 | `trades[-200:]` + `.reverse()` | 종목 순서 기준, 최신 아님 | **`_trades_recent_first`** |
|
||
| 7 | UI `sl_pct`/`tp_pct`만 맞추면 된다 | V4 청산은 어깨+ATR, UI %는 거의 무관 | **어깨·ATR 축 탐색** |
|
||
| 8 | 저장 이력 ON인데 거래 수만 보고 “버그” | `skip_hts=False`면 6건이 **정상** | 플래그·응답 JSON 확인 |
|
||
| 9 | backtest_web 수정 후 **서버 미재시작** | 구 코드로 123건 재현 | **재시작 필수** |
|
||
|
||
---
|
||
|
||
## 4. 조심해야 할 것 (최종 체크리스트)
|
||
|
||
### 4.1 웹 API ↔ param_search ↔ 실매 3-way
|
||
|
||
- [ ] `get_*_defaults_from_db()` 단일 소스
|
||
- [ ] 웹 `params` = 폼 숫자 + **DB bool/플래그 전부** (엔진 default에 맡기지 않음)
|
||
- [ ] 유니버스: `strategy_id`, `history|all`, `scan_interval_min` 동일
|
||
- [ ] `time_start_hm` / `time_end_hm` / `timeframe` 동일
|
||
- [ ] `slot_money`, `fee_rate`, `sell_tax` 동일 소스 (`fee_and_slot_from_env_row`)
|
||
- [ ] `force_eod_exit` 의도 확인 (기본 OFF → 며칠 보유 가능)
|
||
|
||
### 4.2 SCAN vs TRIGGER
|
||
|
||
- [ ] 저장 이력 ON → `skip_hts_scan_dupes=True` **권장** (HTS A 이중 필터 방지)
|
||
- [ ] DB `TAIL_SKIP_HTS_SCAN_DUPES=False` → 거래 **극소**(6건) — 정책인지 버그인지 명시
|
||
- [ ] 무거운 필터는 **param_search 그리드** 또는 **TRIGGER**, SCAN(5분)에는 넣지 않음
|
||
|
||
### 4.3 param_search `--apply`
|
||
|
||
- [ ] **그리드 keys → env_map** 화이트리스트만
|
||
- [ ] `base_params` 전체 저장 금지
|
||
- [ ] 총손익 ≤ 0 결과 apply 스kip (기존 유지)
|
||
|
||
### 4.4 청산 파라미터 해석
|
||
|
||
- [ ] UI **손절 3% / 익절 5%** ≠ 실제 1순위 청산 (어깨컷 V4)
|
||
- [ ] fast/coarse 그리드: **shoulder_*, stop/target_atr_mult, atr_*_pct**
|
||
- [ ] `sl_pct`는 주로 **포지션 사이징** (`static_sl_pct`)
|
||
|
||
### 4.5 UI/표시
|
||
|
||
- [ ] `trades` = `_trades_recent_first(..., 200)` — 매도 시각 기준
|
||
- [ ] 프론트 `tradesNewestFirst()` — `.reverse()`만 쓰지 않음
|
||
- [ ] 응답 `params`에 플래그 노출 — 재현용
|
||
|
||
### 4.6 검증 명령 (꼬리)
|
||
|
||
```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
|
||
```
|
||
|
||
웹 동일 조건 실행 후:
|
||
|
||
- `total_trades` 일치 (±0)
|
||
- `params.skip_hts_scan_dupes` 일치
|
||
- `universe_source` / `universe_history_slots` 일치
|
||
|
||
---
|
||
|
||
## 5. 다른 전략 적용 로드맵
|
||
|
||
### 5.1 공통 패턴 (복사 순서)
|
||
|
||
1. `{strategy}_backtest_common.py` 생성 (§2.1 함수 세트)
|
||
2. `param_search_{strategy}.py`에서 `tbc.run_*_web_aligned` 호출
|
||
3. `backtest_web.py` API route:
|
||
- `_get_*_defaults_for_backtest()` → 엔진 `get_*_defaults_from_db`
|
||
- `{strategy}_trigger_flags` dict (bool 플래그 **전부** merge)
|
||
- `tbc` 로더 + `_trades_recent_first`
|
||
4. `--apply` env_map **그리드 키만** 매핑 테이블 작성
|
||
5. 문서 + 3-way 검증 1회
|
||
|
||
### 5.2 전략별 참고
|
||
|
||
| 전략 | 엔진 | strategy_id | 저장 이력 | 플래그 예시 |
|
||
|------|------|-------------|-----------|-------------|
|
||
| 꼬리 SHORT | `tail_engine` | `SHORT` | ✅ 기본 | `skip_hts_scan_dupes`, `use_rsi_filter` |
|
||
| 모멘텀 | `scalping_engine` | (조건식 id) | 확인 필요 | `MOMENTUM_SKIP_HTS_SCAN_DUPES` |
|
||
| 돌파 | `breakout` | `BREAKOUT` | `_resolve_backtest_universe` | HTS H 등 |
|
||
| 스캘핑 | `scalping_engine` | — | optional | `use_defense_filters`, `use_macd_cross` |
|
||
|
||
**모멘텀/스캘핑:** 이미 `get_scalping_defaults_from_db()` 사용 중 — **웹 API에 bool 플래그 merge 여부**만 재점검.
|
||
|
||
**돌파:** `api/backtest/breakout` — `_trades_recent_first` 적용됨, common 모듈 분리는 **미완** → 꼬리 패턴 이식 후보.
|
||
|
||
### 5.3 env_map 템플릿 (꼬리 `--apply`)
|
||
|
||
```python
|
||
# 그리드 params key → env_config column
|
||
PARAM_TO_ENV = {
|
||
"min_drop_rate": "MIN_DROP_RATE",
|
||
"min_recovery_ratio": "MIN_RECOVERY_RATIO_SHORT",
|
||
"max_rec_3m": "MAX_RECOVERY_RATIO_3M",
|
||
"tail_ratio_min": "TAIL_RATIO_MIN",
|
||
"shoulder_min_high": "SHOULDER_MIN_HIGH_PCT",
|
||
"shoulder_cut_pct": "SHOULDER_CUT_PCT",
|
||
"stop_atr_mult": "STOP_ATR_MULTIPLIER_TAIL",
|
||
"target_atr_mult": "TARGET_ATR_MULTIPLIER_TAIL",
|
||
"atr_tp_max_pct": "TAIL_ATR_TP_MAX_PCT",
|
||
# ... 그리드에 없는 키는 넣지 않음
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 청산(V4) — 전략 공통 개념 (꼬리)
|
||
|
||
```
|
||
1순위 어깨컷 (shoulder_min_high + shoulder_cut_pct) — 변형 트레일링
|
||
2순위 ATR 익절 (target)
|
||
3순위 ATR 손절 (stop)
|
||
4순위 금액손실컷 (max_loss_krw, 어깨 미발동 시)
|
||
5순위 장마감 (force_eod_exit=True 일 때만)
|
||
```
|
||
|
||
- UI **tp_pct 5%** 는 2순위까지 **거의 도달 전** 1순위에서 대부분 청산.
|
||
- **EOD OFF** → 며칠 보유 + 어깨 → 소룩스형 +20% (백테만 해당 가능).
|
||
|
||
---
|
||
|
||
## 7. 변경 이력 (코드 기준)
|
||
|
||
| 일자 | 파일 | 변경 |
|
||
|------|------|------|
|
||
| 2026-05-30 | `tail_backtest_common.py` | 신규 — 웹·파라서치 단일 진입 |
|
||
| 2026-05-30 | `backtest_web.py` | `tail_trigger_flags`, `_trades_recent_first`, `tradesNewestFirst` |
|
||
| 2026-05-30 | `tail_param_search.py` | fast 768조합, `tbc` 경유, apply 그리드만 |
|
||
| 2026-05-30 | `docs/TAIL_BACKTEST_WEB_VS_PARAM_SEARCH.md` | 꼬리 도메인·skip_hts 상세 |
|
||
|
||
---
|
||
|
||
## 8. 빠른 디버깅
|
||
|
||
**웹 vs CLI 거래 수 다를 때:**
|
||
|
||
1. `backtest_web` 재시작했는가?
|
||
2. 응답 JSON: `skip_hts_scan_dupes`, `universe_source`, `time_start_hm`, `timeframe`
|
||
3. CLI: `get_tail_defaults_from_db()` 출력 vs 웹 `params` diff
|
||
4. 저장 이력 ON/OFF 동일한가?
|
||
5. `force_eod_exit` 동일한가?
|
||
|
||
**거래 표 순서 이상할 때:**
|
||
|
||
- API가 `_trades_recent_first` 쓰는지
|
||
- 프론트가 `tradesNewestFirst` 쓰는지 (`.reverse()` 단독 X)
|
||
|
||
---
|
||
|
||
## 9. 관련 파일 경로
|
||
|
||
```
|
||
kis_trader/backtest/tail_backtest_common.py
|
||
kis_trader/backtest/tail_param_search.py
|
||
kis_trader/engine/tail_engine.py
|
||
backtest_web.py # /api/backtest/tail, 유틸 함수
|
||
docs/TAIL_BACKTEST_WEB_VS_PARAM_SEARCH.md
|
||
docs/BACKTEST_ALIGNMENT_FINAL.md # 본 문서
|
||
```
|
||
|
||
---
|
||
|
||
*마지막 갱신: 2026-05-30 — 꼬리(SHORT) 정렬 최종 단계 기준.*
|