Files
kis_bot/docs/BACKTEST_ALIGNMENT_FINAL.md
Hwang 61c72a8a4c feat(tests): 신규 키움 웹소켓 조건검색 및 실시간 조건검색 테스트 추가
변경 사항
----
- _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>
2026-07-06 01:27:00 +09:00

351 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 백테스트·파라서치·웹 정렬 — 최종 단계 코드 정리 (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) 정렬 최종 단계 기준.*