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>
This commit is contained in:
2026-07-06 01:27:00 +09:00
parent d8ba01afa4
commit 61c72a8a4c
171 changed files with 176914 additions and 7329 deletions

View File

@@ -0,0 +1,350 @@
# 백테스트·파라서치·웹 정렬 — 최종 단계 코드 정리 (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) 정렬 최종 단계 기준.*

View File

@@ -32,7 +32,22 @@
---
## 3. 매수 시 캔들 조회 — 세 가지 분기 (DB / 메모리 / 키움·KIS REST)
## 3. 체결 틱 (ws_ticks, C안 — TickRecorder)
| 구분 | 내용 |
|------|------|
| **모듈** | `kis_trader/ws/tick_recorder.py` |
| **트랙 1** | 종목별 deque 링버퍼 (`WS_TICK_BUFFER_MAX_PER_CODE`) — `WSManager.get_recent_ticks()` |
| **트랙 2** | 제한 큐 → `TickDBWriter` 스레드 → `ws_ticks` 배치 INSERT |
| **대상 종목** | `WS_TICK_RECORD_SCOPE=candidates`(기본): 후보+영구 / `subscribed`: 후보+보유+영구 |
| **해외 확장** | `market`, `exchange`, `currency`, `session` 컬럼 (국내 기본 KR/KRX/KRW) |
| **env** | `WS_TICK_SAVE_ENABLED`, `WS_TICK_DB_BATCH_SIZE`, `WS_TICK_DB_FLUSH_SEC`, `WS_TICK_KEEP_DAYS` 등 |
journalctl 파싱이 아니라 **WS H0STCNT0 / 키움 0B** 와 동일 틱 스트림을 사용한다.
---
## 4. 매수 시 캔들 조회 — 세 가지 분기 (DB / 메모리 / 키움·KIS REST)
꼬리잡기 매수 신호(`check_buy_signal_tail_catch`)에서 3분봉을 가져오는 순서:
@@ -50,7 +65,7 @@
---
## 4. “캔들 넣고 바로 매매” — 현재 동작
## 5. “캔들 넣고 바로 매매” — 현재 동작
- 봉이 **확정되는 순간** 이미 **RAM(_confirmed)** 에 들어가므로,
매수 루프는 **DB 쓰기 완료를 기다리지 않고** 바로 그 데이터로 신호 판단.

View File

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