변경 사항 ---- - _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>
13 KiB
백테스트·파라서치·웹 정렬 — 최종 단계 코드 정리 (2026-05-30)
대상: 꼬리(SHORT) 정렬 작업에서 여러 번 잘못 수정한 뒤 도달한 최종 패턴.
목적: 모멘텀·돌파·스캘핑 등 다른 전략에 같은 실수 없이 적용하기 위한 체크리스트.
관련 문서: 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 |
꼬리 구현:
# 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 그리드 (최종)
"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 고정)
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 플래그
백테 호출 (웹과 동일)
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 반영)
# 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 이중 필터 붕괴.
수정:
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 모듈
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, 전략 공통):
def _trade_exit_sort_key(t): # exit_time / sell_time / sell_date ...
def _trades_recent_first(trades, limit=200): # 매도 시각 내림차순 상위 N
수정 (JS, 모든 가상 거래 테이블):
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 검증 명령 (꼬리)
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 공통 패턴 (복사 순서)
{strategy}_backtest_common.py생성 (§2.1 함수 세트)param_search_{strategy}.py에서tbc.run_*_web_aligned호출backtest_web.pyAPI route:_get_*_defaults_for_backtest()→ 엔진get_*_defaults_from_db{strategy}_trigger_flagsdict (bool 플래그 전부 merge)tbc로더 +_trades_recent_first
--applyenv_map 그리드 키만 매핑 테이블 작성- 문서 + 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)
# 그리드 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 거래 수 다를 때:
backtest_web재시작했는가?- 응답 JSON:
skip_hts_scan_dupes,universe_source,time_start_hm,timeframe - CLI:
get_tail_defaults_from_db()출력 vs 웹paramsdiff - 저장 이력 ON/OFF 동일한가?
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) 정렬 최종 단계 기준.