9 Commits

Author SHA1 Message Date
21in7
09ae926f06 docs: 전략 post-mortem — 7전 7패 분석 및 다음 방향 제안
- 공통 실패 패턴 3가지 (IS/OOS 격차, 대칭성 실패, 필터 역효과)
- 근본 원인: 공개 시그널 방향 예측 패러다임 한계
- 다음 방향: 패러다임 변경(stat arb), 비용 절감(maker), 데이터 확장

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-04 09:30:10 +09:00
21in7
f53b8a5a0f research: MTF + BTC 추세 필터 백테스트 — FAIL, MTF 전략 최종 폐기
- 메인 가설(BTC 1h EMA50/200 ADX>20) OOS fees PF 0.90, 베이스라인(0.94)보다 악화
- 12개 sweep 조합 중 합격(fees PF>=1.2) 0개
- 761일 데이터로 전략 근본적 edge 부재 확인

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-04 09:24:45 +09:00
21in7
52d05f2ddd feat: 전략 리서치 스크립트 및 테스트 일괄 추가
- FR/OI 백테스트, LS ratio 백테스트 스크립트
- 펀딩/OI 분석, 거래 LS 분석 스크립트
- evaluate_oos 테스트 추가

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-04 09:03:06 +09:00
21in7
4a7b38ea43 results: 백테스트 및 주간 리포트 결과 일괄 추가
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-04 09:03:03 +09:00
21in7
cc0838e854 docs: 전략 리서치 결과 문서 일괄 추가
- FR+OI 백테스트 결과 (폐기)
- LS ratio 백테스트 설계/결과 (폐기)
- Binance 공개 API 리서치 (edge 없음)
- MTF 백테스트 및 OOS 중간 보고

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-04 09:03:02 +09:00
21in7
112f3e2377 models: DOGE/XRP/SOL 모델 및 학습 로그 업데이트
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-04 09:02:57 +09:00
21in7
960f997aac data: 최신 15m 캔들 데이터 및 거래 기록 업데이트
- DOGE/TRX/XRP/BTC/ETH 15m parquet 데이터 갱신
- MTF dry-run 거래 기록 추가
- XRP LS ratio 데이터 추가

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-04 09:02:56 +09:00
21in7
98476cc972 feat: MTF bot kill switch 및 비용 모델 추가
- src/config.py: COST_MODEL, COST_SCENARIOS 비용 모델 상수 추가
- src/mtf_bot.py: bps 기반 kill switch (Fast Kill + Slow Kill) 구현
- tests/test_mtf_bot.py: kill switch 테스트 케이스 추가

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-04 09:02:51 +09:00
21in7
29e307d7b2 fix: evaluate_oos 판정 로직을 fees_only PF 기준으로 수정하고 MTF OOS 최종 결과 문서화
- 판정 기준을 Raw PF → fees_only PF로 변경 (Raw PF는 비현실적)
- LONG/SHORT 대칭성 체크 추가 (양쪽 PF >= 0.8)
- MTF OOS 최종 결과: FAIL 폐기 (30건, fees_only PF 0.84, SHORT PF 0.56)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-04 09:01:40 +09:00
75 changed files with 131997 additions and 28 deletions

View File

@@ -65,7 +65,7 @@ bash scripts/deploy_model.sh --symbol XRPUSDT
4. `src/exchange.py` + `src/risk_manager.py` — Dynamic margin, MARKET orders with SL/TP, daily loss limit (5%), same-direction limit
5. `src/user_data_stream.py` + `src/notifier.py` — Real-time TP/SL detection via WebSocket, Discord webhooks
**Dual-layer kill switch** (per-symbol, in `src/bot.py`): Fast Kill (8 consecutive net losses) + Slow Kill (last 15 trades PF < 0.75). Trade history persisted to `data/trade_history/{symbol}.jsonl`. Blocks new entries only; existing SL/TP exits work normally. Manual reset via `RESET_KILL_SWITCH_{SYMBOL}=True` env var + restart.
**Dual-layer kill switch** (per-symbol, in `src/bot.py` and `src/mtf_bot.py`): Fast Kill (8 consecutive net losses) + Slow Kill (last 15 trades PF < 0.75). Trade history persisted to `data/trade_history/{symbol}.jsonl`. Blocks new entries only; existing SL/TP exits work normally. Manual reset via `RESET_KILL_SWITCH_{SYMBOL}=True` (main bot) or `RESET_KILL_SWITCH_MTF_{SYMBOL}=True` (MTF bot) env var + restart. MTF bot uses bps-based PnL for kill switch decisions.
**Parallel execution**: Per-symbol bots run independently via `asyncio.gather()`. Each bot's `user_data_stream` also runs in parallel.
@@ -154,4 +154,8 @@ All design documents and implementation plans are stored in `docs/plans/` with t
| 2026-03-30 | `ls-ratio-backtest` (design + result) | Edge 없음 확정, 폐기 |
| 2026-03-30 | `fr-oi-backtest` (result) | SHORT PF=1.88이나 대칭성 실패(Case2), 폐기 |
| 2026-03-30 | `public-api-research-closed` | Binance 공개 API 전수 테스트 완료, 단독 edge 없음 |
| 2026-03-30 | `mtf-pullback-bot` | MTF Pullback Bot 배포, 4월 OOS Dry-run 검증 진행 중 |
| 2026-03-30 | `mtf-pullback-bot` | MTF Pullback Bot **최종 폐기** (OOS+BTC필터 모두 실패) |
| 2026-04-21 | `mtf-oos-dryrun-result` | 중간 보고 — 24건 Raw PF 0.98 |
| 2026-05-04 | `mtf-oos-final-result` | **FAIL, 폐기** — 30건 fees_only PF 0.84, SHORT 대칭성 실패 |
| 2026-05-04 | `mtf-btc-filter` (design + result) | **FAIL, 최종 폐기** — BTC 필터 추가해도 OOS PF 0.90, 베이스라인보다 악화 |
| 2026-05-04 | `strategy-post-mortem` | 7전 7패 분석 — 공개 시그널 방향 예측 패러다임 한계, 다음 방향 제안 |

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,8 @@
{"symbol": "TESTUSDT", "side": "SHORT", "entry_price": 2.0, "entry_ts": "2026-04-03T08:48:46.103465+00:00", "exit_price": 2.015, "exit_ts": "2026-04-03T08:48:46.103523+00:00", "sl_price": 2.015, "tp_price": 1.977, "atr": 0.01, "pnl_bps": -75.0, "reason": "SL \ud788\ud2b8"}
{"symbol": "TESTUSDT", "side": "SHORT", "entry_price": 2.0, "entry_ts": "2026-04-03T08:48:46.103862+00:00", "exit_price": 2.015, "exit_ts": "2026-04-03T08:48:46.103922+00:00", "sl_price": 2.015, "tp_price": 1.977, "atr": 0.01, "pnl_bps": -75.0, "reason": "SL \ud788\ud2b8"}
{"symbol": "TESTUSDT", "side": "SHORT", "entry_price": 2.0, "entry_ts": "2026-04-03T08:49:26.310084+00:00", "exit_price": 2.015, "exit_ts": "2026-04-03T08:49:26.310144+00:00", "sl_price": 2.015, "tp_price": 1.977, "atr": 0.01, "pnl_bps": -75.0, "reason": "SL \ud788\ud2b8"}
{"symbol": "TESTUSDT", "side": "SHORT", "entry_price": 2.0, "entry_ts": "2026-04-03T08:49:26.310514+00:00", "exit_price": 2.015, "exit_ts": "2026-04-03T08:49:26.310577+00:00", "sl_price": 2.015, "tp_price": 1.977, "atr": 0.01, "pnl_bps": -75.0, "reason": "SL \ud788\ud2b8"}
{"symbol": "TESTUSDT", "side": "SHORT", "entry_price": 2.0, "entry_ts": "2026-04-17T01:57:57.242077+00:00", "exit_price": 2.015, "exit_ts": "2026-04-17T01:57:57.242137+00:00", "sl_price": 2.015, "tp_price": 1.977, "atr": 0.01, "pnl_bps": -75.0, "reason": "SL \ud788\ud2b8"}
{"symbol": "TESTUSDT", "side": "SHORT", "entry_price": 2.0, "entry_ts": "2026-04-17T01:57:57.242824+00:00", "exit_price": 2.015, "exit_ts": "2026-04-17T01:57:57.242904+00:00", "sl_price": 2.015, "tp_price": 1.977, "atr": 0.01, "pnl_bps": -75.0, "reason": "SL \ud788\ud2b8"}
{"symbol": "TESTUSDT", "side": "SHORT", "entry_price": 2.0, "entry_ts": "2026-04-17T01:59:56.588979+00:00", "exit_price": 2.015, "exit_ts": "2026-04-17T01:59:56.589032+00:00", "sl_price": 2.015, "tp_price": 1.977, "atr": 0.01, "pnl_bps": -75.0, "reason": "SL \ud788\ud2b8"}
{"symbol": "TESTUSDT", "side": "SHORT", "entry_price": 2.0, "entry_ts": "2026-04-17T01:59:56.589321+00:00", "exit_price": 2.015, "exit_ts": "2026-04-17T01:59:56.589381+00:00", "sl_price": 2.015, "tp_price": 1.977, "atr": 0.01, "pnl_bps": -75.0, "reason": "SL \ud788\ud2b8"}

View File

@@ -0,0 +1,6 @@
{"symbol": "XRPUSDT", "side": "LONG", "entry_price": 2.0, "entry_ts": "2026-03-31T02:09:25.326276+00:00", "exit_price": 2.01, "exit_ts": "2026-03-31T02:09:25.326333+00:00", "sl_price": 1.985, "tp_price": 2.023, "atr": 0.01, "pnl_bps": 50, "reason": "test"}
{"symbol": "XRPUSDT", "side": "LONG", "entry_price": 2.0, "entry_ts": "2026-03-31T02:10:01.522854+00:00", "exit_price": 2.01, "exit_ts": "2026-03-31T02:10:01.522915+00:00", "sl_price": 1.985, "tp_price": 2.023, "atr": 0.01, "pnl_bps": 50, "reason": "test"}
{"symbol": "XRPUSDT", "side": "LONG", "entry_price": 2.0, "entry_ts": "2026-04-03T08:48:46.101315+00:00", "exit_price": 2.01, "exit_ts": "2026-04-03T08:48:46.101362+00:00", "sl_price": 1.985, "tp_price": 2.023, "atr": 0.01, "pnl_bps": 50, "reason": "test"}
{"symbol": "XRPUSDT", "side": "LONG", "entry_price": 2.0, "entry_ts": "2026-04-03T08:49:26.307772+00:00", "exit_price": 2.01, "exit_ts": "2026-04-03T08:49:26.307825+00:00", "sl_price": 1.985, "tp_price": 2.023, "atr": 0.01, "pnl_bps": 50, "reason": "test"}
{"symbol": "XRPUSDT", "side": "LONG", "entry_price": 2.0, "entry_ts": "2026-04-17T01:57:57.239531+00:00", "exit_price": 2.01, "exit_ts": "2026-04-17T01:57:57.239584+00:00", "sl_price": 1.985, "tp_price": 2.023, "atr": 0.01, "pnl_bps": 50, "reason": "test"}
{"symbol": "XRPUSDT", "side": "LONG", "entry_price": 2.0, "entry_ts": "2026-04-17T01:59:56.586709+00:00", "exit_price": 2.01, "exit_ts": "2026-04-17T01:59:56.586758+00:00", "sl_price": 1.985, "tp_price": 2.023, "atr": 0.01, "pnl_bps": 50, "reason": "test"}

View File

@@ -0,0 +1,30 @@
{"symbol": "XRP/USDT:USDT", "side": "SHORT", "entry_price": 1.2924, "entry_ts": "2026-04-02T15:45:02.285284+00:00", "exit_price": 1.3011924698057984, "exit_ts": "2026-04-02T17:00:00.791551+00:00", "sl_price": 1.3011924698057984, "tp_price": 1.2789182129644425, "atr": 0.005861646537198913, "pnl_bps": -68.0, "reason": "SL \ud788\ud2b8 (1.3012)"}
{"symbol": "XRP/USDT:USDT", "side": "SHORT", "entry_price": 1.299, "entry_ts": "2026-04-02T18:15:02.275070+00:00", "exit_price": 1.3076847690550104, "exit_ts": "2026-04-02T20:30:00.977600+00:00", "sl_price": 1.3076847690550104, "tp_price": 1.2856833541156505, "atr": 0.005789846036673716, "pnl_bps": -66.9, "reason": "SL \ud788\ud2b8 (1.3077)"}
{"symbol": "XRP/USDT:USDT", "side": "SHORT", "entry_price": 1.3372, "entry_ts": "2026-04-06T07:30:02.964551+00:00", "exit_price": 1.343892780041389, "exit_ts": "2026-04-06T08:15:00.323145+00:00", "sl_price": 1.343892780041389, "tp_price": 1.3269377372698703, "atr": 0.004461853360925946, "pnl_bps": -50.1, "reason": "SL \ud788\ud2b8 (1.3439)"}
{"symbol": "XRP/USDT:USDT", "side": "SHORT", "entry_price": 1.338, "entry_ts": "2026-04-06T09:15:02.357616+00:00", "exit_price": 1.34426520318828, "exit_ts": "2026-04-06T09:30:00.575381+00:00", "sl_price": 1.34426520318828, "tp_price": 1.328393355111304, "atr": 0.004176802125519994, "pnl_bps": -46.8, "reason": "SL \ud788\ud2b8 (1.3443)"}
{"symbol": "XRP/USDT:USDT", "side": "SHORT", "entry_price": 1.3489, "entry_ts": "2026-04-06T11:45:02.281455+00:00", "exit_price": 1.337093964326515, "exit_ts": "2026-04-06T17:30:00.471399+00:00", "sl_price": 1.3565995884827076, "tp_price": 1.337093964326515, "atr": 0.005133058988471726, "pnl_bps": 87.5, "reason": "TP \ud788\ud2b8 (1.3371)"}
{"symbol": "XRP/USDT:USDT", "side": "SHORT", "entry_price": 1.3169, "entry_ts": "2026-04-07T05:15:02.864543+00:00", "exit_price": 1.3067678775397529, "exit_ts": "2026-04-07T11:15:00.824550+00:00", "sl_price": 1.323507905952335, "tp_price": 1.3067678775397529, "atr": 0.004405270634889999, "pnl_bps": 76.9, "reason": "TP \ud788\ud2b8 (1.3068)"}
{"symbol": "XRP/USDT:USDT", "side": "SHORT", "entry_price": 1.3025, "entry_ts": "2026-04-07T19:00:02.557934+00:00", "exit_price": 1.309204644912873, "exit_ts": "2026-04-07T20:15:00.598047+00:00", "sl_price": 1.309204644912873, "tp_price": 1.292219544466928, "atr": 0.004469763275248652, "pnl_bps": -51.5, "reason": "SL \ud788\ud2b8 (1.3092)"}
{"symbol": "XRP/USDT:USDT", "side": "LONG", "entry_price": 1.3794, "entry_ts": "2026-04-08T08:15:03.067336+00:00", "exit_price": 1.3681020223721523, "exit_ts": "2026-04-08T14:45:00.843687+00:00", "sl_price": 1.3681020223721523, "tp_price": 1.3967235656960328, "atr": 0.007531985085231735, "pnl_bps": -81.9, "reason": "SL \ud788\ud2b8 (1.3681)"}
{"symbol": "XRP/USDT:USDT", "side": "LONG", "entry_price": 1.3525, "entry_ts": "2026-04-08T22:15:02.356148+00:00", "exit_price": 1.342647301335702, "exit_ts": "2026-04-08T23:15:00.470216+00:00", "sl_price": 1.342647301335702, "tp_price": 1.3676074712852568, "atr": 0.006568465776198652, "pnl_bps": -72.8, "reason": "SL \ud788\ud2b8 (1.3426)"}
{"symbol": "XRP/USDT:USDT", "side": "LONG", "entry_price": 1.3324, "entry_ts": "2026-04-09T05:30:02.831310+00:00", "exit_price": 1.3242694197294704, "exit_ts": "2026-04-09T14:45:00.646118+00:00", "sl_price": 1.3242694197294704, "tp_price": 1.3448668897481453, "atr": 0.005420386847019691, "pnl_bps": -61.0, "reason": "SL \ud788\ud2b8 (1.3243)"}
{"symbol": "XRP/USDT:USDT", "side": "LONG", "entry_price": 1.33, "entry_ts": "2026-04-12T09:45:02.575343+00:00", "exit_price": 1.3234444290752214, "exit_ts": "2026-04-12T14:15:00.701105+00:00", "sl_price": 1.3234444290752214, "tp_price": 1.3400518754179942, "atr": 0.0043703806165191405, "pnl_bps": -49.3, "reason": "SL \ud788\ud2b8 (1.3234)"}
{"symbol": "XRP/USDT:USDT", "side": "LONG", "entry_price": 1.3314, "entry_ts": "2026-04-12T20:45:04.130138+00:00", "exit_price": 1.3260943015416145, "exit_ts": "2026-04-12T22:45:00.641365+00:00", "sl_price": 1.3260943015416145, "tp_price": 1.3395354043028576, "atr": 0.0035371323055903036, "pnl_bps": -39.9, "reason": "SL \ud788\ud2b8 (1.3261)"}
{"symbol": "XRP/USDT:USDT", "side": "LONG", "entry_price": 1.3662, "entry_ts": "2026-04-14T02:00:03.591231+00:00", "exit_price": 1.3826215436611005, "exit_ts": "2026-04-14T14:00:00.219774+00:00", "sl_price": 1.355490297612326, "tp_price": 1.3826215436611005, "atr": 0.007139801591782776, "pnl_bps": 120.2, "reason": "TP \ud788\ud2b8 (1.3826)"}
{"symbol": "XRP/USDT:USDT", "side": "LONG", "entry_price": 1.3773, "entry_ts": "2026-04-14T16:30:03.036264+00:00", "exit_price": 1.36621231930106, "exit_ts": "2026-04-14T18:30:00.959397+00:00", "sl_price": 1.36621231930106, "tp_price": 1.3943011104050413, "atr": 0.007391787132626665, "pnl_bps": -80.5, "reason": "SL \ud788\ud2b8 (1.3662)"}
{"symbol": "XRP/USDT:USDT", "side": "LONG", "entry_price": 1.3917, "entry_ts": "2026-04-16T00:15:02.437176+00:00", "exit_price": 1.404348303100205, "exit_ts": "2026-04-16T03:00:00.439541+00:00", "sl_price": 1.3834511066737791, "tp_price": 1.404348303100205, "atr": 0.00549926221748051, "pnl_bps": 90.9, "reason": "TP \ud788\ud2b8 (1.4043)"}
{"symbol": "XRP/USDT:USDT", "side": "LONG", "entry_price": 1.4039, "entry_ts": "2026-04-16T05:45:02.587650+00:00", "exit_price": 1.4150432459353304, "exit_ts": "2026-04-16T09:30:00.120795+00:00", "sl_price": 1.3966326656943495, "tp_price": 1.4150432459353304, "atr": 0.004844889537100242, "pnl_bps": 79.4, "reason": "TP \ud788\ud2b8 (1.4150)"}
{"symbol": "XRP/USDT:USDT", "side": "LONG", "entry_price": 1.4089, "entry_ts": "2026-04-16T12:15:03.065202+00:00", "exit_price": 1.4209071868858967, "exit_ts": "2026-04-16T13:45:00.389284+00:00", "sl_price": 1.4010692259439805, "tp_price": 1.4209071868858967, "atr": 0.005220516037346328, "pnl_bps": 85.2, "reason": "TP \ud788\ud2b8 (1.4209)"}
{"symbol": "XRP/USDT:USDT", "side": "LONG", "entry_price": 1.4199, "entry_ts": "2026-04-16T17:45:03.104428+00:00", "exit_price": 1.4339129204091046, "exit_ts": "2026-04-16T19:00:00.181964+00:00", "sl_price": 1.4107611388636274, "tp_price": 1.4339129204091046, "atr": 0.006092574090915036, "pnl_bps": 98.7, "reason": "TP \ud788\ud2b8 (1.4339)"}
{"symbol": "XRP/USDT:USDT", "side": "LONG", "entry_price": 1.4827, "entry_ts": "2026-04-17T19:30:03.006514+00:00", "exit_price": 1.4672241636153258, "exit_ts": "2026-04-18T04:30:00.884113+00:00", "sl_price": 1.4672241636153258, "tp_price": 1.5064296157898336, "atr": 0.010317224256449408, "pnl_bps": -104.4, "reason": "SL \ud788\ud2b8 (1.4672)"}
{"symbol": "XRP/USDT:USDT", "side": "LONG", "entry_price": 1.4303, "entry_ts": "2026-04-18T21:30:02.340862+00:00", "exit_price": 1.422038986984328, "exit_ts": "2026-04-19T06:30:00.566171+00:00", "sl_price": 1.422038986984328, "tp_price": 1.4429668866240302, "atr": 0.005507342010447945, "pnl_bps": -57.8, "reason": "SL \ud788\ud2b8 (1.4220)"}
{"symbol": "XRP/USDT:USDT", "side": "LONG", "entry_price": 1.4221, "entry_ts": "2026-04-19T11:00:02.406765+00:00", "exit_price": 1.4317857929446725, "exit_ts": "2026-04-19T13:30:00.147036+00:00", "sl_price": 1.415783178514344, "tp_price": 1.4317857929446725, "atr": 0.004211214323770682, "pnl_bps": 68.1, "reason": "TP \ud788\ud2b8 (1.4318)"}
{"symbol": "XRP/USDT:USDT", "side": "LONG", "entry_price": 1.4044, "entry_ts": "2026-04-20T00:45:02.394423+00:00", "exit_price": 1.3967874471566615, "exit_ts": "2026-04-20T06:00:00.011187+00:00", "sl_price": 1.3967874471566615, "tp_price": 1.4160725810264527, "atr": 0.005075035228892415, "pnl_bps": -54.2, "reason": "SL \ud788\ud2b8 (1.3968)"}
{"symbol": "XRP/USDT:USDT", "side": "LONG", "entry_price": 1.4053, "entry_ts": "2026-04-20T06:45:03.160723+00:00", "exit_price": 1.4157391122980558, "exit_ts": "2026-04-20T07:45:00.033371+00:00", "sl_price": 1.3984918832838766, "tp_price": 1.4157391122980558, "atr": 0.00453874447741562, "pnl_bps": 74.3, "reason": "TP \ud788\ud2b8 (1.4157)"}
{"symbol": "XRP/USDT:USDT", "side": "LONG", "entry_price": 1.414, "entry_ts": "2026-04-20T09:30:02.604341+00:00", "exit_price": 1.4264633526792339, "exit_ts": "2026-04-20T14:45:00.009801+00:00", "sl_price": 1.405871726513543, "tp_price": 1.4264633526792339, "atr": 0.005418848990971227, "pnl_bps": 88.1, "reason": "TP \ud788\ud2b8 (1.4265)"}
{"symbol": "XRP/USDT:USDT", "side": "LONG", "entry_price": 1.4143, "entry_ts": "2026-04-23T12:00:03.002778+00:00", "exit_price": 1.4234400599181054, "exit_ts": "2026-04-23T15:00:00.077208+00:00", "sl_price": 1.4083390913577571, "tp_price": 1.4234400599181054, "atr": 0.003973939094828441, "pnl_bps": 64.6, "reason": "TP \ud788\ud2b8 (1.4234)"}
{"symbol": "XRP/USDT:USDT", "side": "SHORT", "entry_price": 1.3924, "entry_ts": "2026-04-27T21:00:03.044720+00:00", "exit_price": 1.398856832668528, "exit_ts": "2026-04-28T00:00:00.585275+00:00", "sl_price": 1.398856832668528, "tp_price": 1.3824995232415904, "atr": 0.004304555112352007, "pnl_bps": -46.4, "reason": "SL \ud788\ud2b8 (1.3989)"}
{"symbol": "XRP/USDT:USDT", "side": "SHORT", "entry_price": 1.3955, "entry_ts": "2026-04-28T02:45:02.809371+00:00", "exit_price": 1.3859950974494593, "exit_ts": "2026-04-28T11:30:00.636915+00:00", "sl_price": 1.4016988494894829, "tp_price": 1.3859950974494593, "atr": 0.004132566326322003, "pnl_bps": 68.1, "reason": "TP \ud788\ud2b8 (1.3860)"}
{"symbol": "XRP/USDT:USDT", "side": "SHORT", "entry_price": 1.3791, "entry_ts": "2026-04-28T21:45:02.720338+00:00", "exit_price": 1.384214600169389, "exit_ts": "2026-04-29T03:45:00.543758+00:00", "sl_price": 1.384214600169389, "tp_price": 1.3712576130736036, "atr": 0.003409733446259308, "pnl_bps": -37.1, "reason": "SL \ud788\ud2b8 (1.3842)"}
{"symbol": "XRP/USDT:USDT", "side": "SHORT", "entry_price": 1.3906, "entry_ts": "2026-04-29T09:00:02.842299+00:00", "exit_price": 1.39511977056087, "exit_ts": "2026-04-29T09:45:00.260020+00:00", "sl_price": 1.39511977056087, "tp_price": 1.3836696851399997, "atr": 0.0030131803739132297, "pnl_bps": -32.5, "reason": "SL \ud788\ud2b8 (1.3951)"}
{"symbol": "XRP/USDT:USDT", "side": "SHORT", "entry_price": 1.395, "entry_ts": "2026-04-29T11:30:02.795097+00:00", "exit_price": 1.3865272285022556, "exit_ts": "2026-04-29T12:30:00.623489+00:00", "sl_price": 1.4005257205420072, "tp_price": 1.3865272285022556, "atr": 0.003683813694671445, "pnl_bps": 60.7, "reason": "TP \ud788\ud2b8 (1.3865)"}

View File

@@ -0,0 +1,15 @@
{"net_pnl": -24.8291, "reason": "MANUAL", "ts": "2026-03-22T15:15:35.180527+00:00"}
{"net_pnl": -34.2859, "reason": "MANUAL", "ts": "2026-03-22T15:20:06.214857+00:00"}
{"net_pnl": -158.9121, "reason": "MANUAL", "ts": "2026-03-22T16:00:48.207128+00:00"}
{"net_pnl": -350.7024, "reason": "MANUAL", "ts": "2026-03-23T03:53:54.960798+00:00"}
{"net_pnl": 9.89, "reason": "MANUAL", "ts": "2026-03-23T03:59:32.078260+00:00"}
{"net_pnl": -58.2073, "reason": "SL", "ts": "2026-03-23T04:01:58.094580+00:00"}
{"net_pnl": -20.956, "reason": "SL", "ts": "2026-03-23T04:08:30.143579+00:00"}
{"net_pnl": -12.4839, "reason": "SL", "ts": "2026-03-23T04:11:19.108516+00:00"}
{"net_pnl": -28.8319, "reason": "SL", "ts": "2026-03-23T04:15:50.164001+00:00"}
{"net_pnl": 120.289, "reason": "SL", "ts": "2026-03-23T04:42:02.062102+00:00"}
{"net_pnl": -20.8975, "reason": "TP", "ts": "2026-03-23T05:31:24.070622+00:00"}
{"net_pnl": -31.664, "reason": "MANUAL", "ts": "2026-03-23T06:01:18.223227+00:00"}
{"net_pnl": 31.2816, "reason": "MANUAL", "ts": "2026-03-23T06:30:11.651964+00:00"}
{"net_pnl": -78.5471, "reason": "MANUAL", "ts": "2026-03-23T07:05:18.387993+00:00"}
{"net_pnl": -1.2852, "reason": "SL", "ts": "2026-03-23T07:08:40.080152+00:00"}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,52 @@
# FR × OI 변화율 백테스트 결과: 폐기
**Date**: 2026-03-30
**Status**: REJECTED — 대칭성 실패 (Case 2), 시장 베타 의심
**Decision**: 설계 기준 준수, 폐기
## 상관분석 결과
| 피처 | → 4h | N |
|------|------|---|
| FR × OI변화율(1h) | r = -0.1734 | 448 |
| FR × OI변화율 | r = -0.0944 | 451 |
| OI 변화율 (1h) | r = +0.0916 | 448 |
## 백테스트 결과 (12개 조합)
| 조합 | 거래수 | PF | PnL(bps) |
|------|--------|-----|----------|
| SHORT 1h P75 | 36 | **1.88** | +304 |
| SHORT 1h P50 | 63 | 1.07 | +54 |
| SHORT 1h P25 | 80 | 1.16 | +163 |
| SHORT 4h P75 | 17 | 1.68 | +314 |
| SHORT 4h P50 | 23 | 1.56 | +327 |
| SHORT 4h P25 | 26 | 1.50 | +303 |
| LONG 전 조합 | — | **0.34~0.50** | 전부 손실 |
## 폐기 근거 (3단계 필터)
1. **필터 1 통과**: SHORT 1h P75 PF=1.88 > 1.5 ✓
2. **필터 2 경고**: 36건 (🟡 참고만, 50건 미달) ⚠️
3. **필터 3 실패**: LONG 전 조합 PF < 0.5 = Case 2 (한쪽만 성공) ✗
**Case 2 = 시장 베타/우연. 폐기.**
## 왜 SHORT만 작동했나
- 데이터 기간: 3/1~3/30 (29일)
- 이 기간 XRP: 하락 추세
- SHORT가 잘 먹힌 건 시장 방향과 일치 (beta)
- 상승장에서는 LONG이 작동하고 SHORT가 실패할 것
## 4월 재검증 (선택사항)
- 4/15: OI 29일 데이터로 동일 테스트 재실행
- 상승장 구간 포함 시 SHORT PF 유지되는지 확인
- 여전히 Case 2면 확정 폐기
## 교훈
- PF 1.88을 거절한 이유: 대칭성 없는 시그널은 시장 방향에 의존
- "한 방향만 작동"은 가장 흔한 과적합 패턴
- 설계 기준을 사전에 세우고 준수하는 것이 생존 전략

View File

@@ -0,0 +1,76 @@
# L/S Ratio 단독 백테스트 설계
**Date**: 2026-03-30
**Status**: Approved
**Goal**: L/S ratio의 독립적 edge 유무를 빠르게 판정
## 배경
- 기존 시그널(RSI+MACD+BB+EMA+StochRSI)은 PF 0.89, 수익성 부족 확정 (2026-03-29)
- L/S ratio 수집 중 (3/22~, 프로덕션 716행)
- 이전 분석에서 XRP top_acct_ls_ratio → 1h return 상관계수 +0.1158 (Momentum)
## Phase 1: Pure Edge Test
### 데이터
- L/S ratio: 프로덕션 수집 (716행, 3/22~3/30)
- Kline: Binance XRPUSDT 15m (같은 기간)
- 비교용: BTC/ETH L/S ratio
### 전략 로직
- 진입: `top_acct_ls_ratio` 백분위수 기반 임계값
- 보유: 고정 4캔들 (1시간)
- 청산: 1시간 후 종가
- 수수료: 0.04% × 2 = 0.08%
- 포지션: 1건씩만
### 테스트 조합 (6개)
| 임계값 | 방향 | 신호 |
|--------|------|------|
| 75th %ile | LONG | 모멘텀 강함 |
| 75th %ile | SHORT | 모멘텀 약함 |
| 50th %ile | LONG | 모멘텀 중간 |
| 50th %ile | SHORT | 모멘텀 중간 |
| 25th %ile | LONG | 모멘텀 약함 |
| 25th %ile | SHORT | 모멘텀 강함 |
### PF 계산
- PF = Σ(Gross Profit - 비용) / Σ(|Gross Loss| + 비용)
- 비용: 0.08% per trade
### 판정 기준 (3단계 필터)
**필터 1: 명확한 신호**
- PF > 1.5 → edge 있음
- PF < 0.5 → 실패
- 0.5~1.5 → 판단 보류
**필터 2: 거래수 신뢰도**
- < 20건 → 🔴 폐기
- 20~50건 → 🟡 참고만
- 50~100건 → 🟢 검토
- 100건+ → 🟢 우선
**필터 3: 대칭성**
- Case 1: LONG > 1.5 AND SHORT > 1.5 → 진정한 edge → Phase 2 진행
- Case 2: 한쪽만 성공 → 시장 베타/우연 → 폐기
- Case 3: 한쪽 강함 + 한쪽 애매 → 부분적 edge → 낮은 신뢰도로 Phase 2
## Phase 2: Bot Simulation (조건부)
Phase 1 필터 통과 시에만:
- L/S ratio로 RSI/MACD 완전 대체
- ATR SL 1.5x, TP 2.3x (기존 동일)
- 역신호 시 반대 포지션 재진입
## Phase 3: 4월 15일 재검증
- 4/1~4/15 데이터로 동일 로직 재실행
- 기준: PF > 1.15
- GO/STOP/HOLD 최종 판정
## 한계
- 8일 = ~700 시그널 포인트, 조합당 ~115개
- 극심한 과적합 위험 → 8일 결과는 "방향성 힌트"
- 최종 판정은 4월 15일 재검증 기반

View File

@@ -0,0 +1,43 @@
# L/S Ratio 백테스트 결과: Edge 없음
**Date**: 2026-03-30
**Status**: CLOSED — Edge 없음 확정, 폐기
**Decision**: Option B (결과 수용 + 방향 전환)
## 테스트 요약
- 데이터: 8일 (3/22~3/30), 716 candles, XRPUSDT top_acct_ls_ratio
- 방법: 6개 조합 (3 percentile × 2 direction), 1시간 보유, 수수료 0.08%
- 판정 기준: PF > 1.5 + 거래수 > 20 + 양방향 대칭
## 결과
| 조합 | 거래수 | 승률 | PF | PnL(bps) |
|------|--------|------|----|----------|
| P75 LONG (모멘텀) | 39 | 35.9% | 0.53 | -306 |
| P75 SHORT (역모멘텀) | 39 | 46.2% | 0.51 | -314 |
| P50 LONG | 76 | 34.2% | 0.38 | -1252 |
| P50 SHORT | 75 | 44.0% | 0.63 | -699 |
| P25 LONG (역모멘텀) | 41 | 41.5% | 0.86 | -161 |
| P25 SHORT (모멘텀) | 41 | 48.8% | 0.64 | -466 |
**6개 조합 전부 PF < 1.0. 최고값 0.86은 기존 시그널(0.89)보다 못함.**
## 판정 근거
1. 상관분석 r=0.1158 (약함) → 거래비용 커버 불가 예상 → 백테스트로 확인
2. Momentum도 Contrarian도 작동 안 함
3. 추가 조합 테스트는 과적합 위험 (8일 데이터에 20+ 조합 = 우연의 승자 필연)
## 다음 단계
- L/S ratio 단독 시그널 폐기
- collector는 유지 (4/15 재검증용 + 향후 복합 피처 가능성)
- 새 데이터 소스 탐색: Funding Rate, OI Velocity, Close Ratio, USDT Perp Inflows
- 4/15: L/S ratio 24일 재검증 (마지막 확인, 여전히 실패 시 확정 폐기)
## 교훈
- 약한 상관계수(r < 0.15)는 거래비용 후 수익 불가능 시사
- "혹시 다른 형태로는?" = curve-fitting의 시작
- 빠르게 실패하고 폐기하는 것이 생존 능력

View File

@@ -0,0 +1,40 @@
# Binance Public API Signal Research: 공식 종료
**Date**: 2026-03-30
**Status**: CLOSED — 단독 edge 가진 피처 없음
## 전수 테스트 결과
| # | 피처 | 상관계수 | 백테스트 Best PF | 판정 |
|---|------|---------|-----------------|------|
| 1 | RSI+MACD+BB+EMA+StochRSI | — | 0.89 | 폐기 (수익성 부족) |
| 2 | top_acct_ls_ratio | r=+0.116 (1h) | 0.86 | 폐기 (전 조합 < 1.0) |
| 3 | global_ls_ratio | r=+0.048 (1h) | — | 약함, 미실시 |
| 4 | top_pos_ls_ratio | r=+0.032 (1h) | — | 약함, 미실시 |
| 5 | Funding Rate | r=-0.054 (4h) | — | 약함, 미실시 |
| 6 | FR × OI변화율(1h) | r=-0.173 (4h) | SHORT 1.88 / LONG 0.50 | 폐기 (대칭성 실패) |
| 7 | Taker Buy/Sell Ratio | r=-0.079 (1h) | 0.93 | 폐기 (전 조합 < 1.0) |
| 8 | Liquidation (allForceOrders) | API 폐기됨 | — | 사용 불가 |
## 핵심 교훈
1. **r < 0.15는 거래비용(0.08%) 커버 불가** — 모든 피처에서 확인
2. **대칭성(LONG+SHORT) 없는 시그널은 시장 베타** — FR×OI에서 확인
3. **8일~29일 데이터는 "방향성 힌트"** — 최종 판정엔 더 긴 기간 필요
4. **PF 1.88도 거절할 수 있어야 함** — 설계 기준 사전 수립이 핵심
## 4월 15일 재검증
- crontab 등록 완료 (프로덕션 10.1.10.24)
- `scripts/revalidate_apr15.py` — L/S ratio + FR×OI 동시 재실행
- 추가 데이터(24일/29일)로 동일 테스트
- 여전히 실패 시 확정 폐기
## 다음 방향 (4월 이후)
Binance 공개 API 한정으로는 단독 edge 불가. 탐색 필요:
- 온체인 데이터 (whale wallet tracking, exchange inflow/outflow)
- 크로스 거래소 데이터 (OKX, Bybit OI/FR 차이)
- 소셜 센티먼트 (Fear & Greed, Twitter/X sentiment)
- 멀티피처 복합 시그널 (ML with L/S + FR + OI + Taker 조합)
- 다른 타임프레임 (1h, 4h candle 기반)

View File

@@ -0,0 +1,30 @@
【MTF 백테스트: 기존 vs 개선】
데이터:
├─ XRPUSDT kline (15m, 2026-02-01 ~ 현재)
├─ 타겟: 다음 4시간 수익률
└─ 기간: 2개월
Step 1: 기존 신호 백테스트
├─ 신호: 15분봉 RSI+MACD (메타필터 없음)
├─ SL: ATR 1.5x, TP: ATR 2.3x
├─ 결과 기대값: PF ≈ 0.89 (이미 알고 있는 값)
└─ 거래수: X건
Step 2: MTF 필터링 적용 백테스트
├─ Meta Filter 1: 1h EMA 50 vs EMA 200
├─ Meta Filter 2: 1h ADX > 20
├─ Trigger: 15분봉 RSI+MACD (필터 조건과 일치할 때만)
├─ SL: ATR 1.5x, TP: ATR 2.3x (동일)
├─ 결과 기대값: PF > 1.0 (목표)
└─ 거래수: Y건 (X의 40~60%)
Step 3: 비교 분석
├─ 기존: Trades X, PF 0.89, PnL A
├─ MTF: Trades Y, PF ?, PnL B
├─ 개선도: (PF_new - 0.89) / 0.89 = ?%
출력:
결과를 테이블 형식으로 정렬
│ 구분 │ Trades │ Win Rate │ PF │ PnL │ 개선율 │
└─────────────────────────────────────────

View File

@@ -0,0 +1,40 @@
# MTF Pullback Bot OOS Dry-run 중간 보고
## 가설
MTF Pullback Bot의 멀티타임프레임 풀백 시그널이 XRPUSDT LONG/SHORT 양방향에서 수익을 낸다.
## 데이터
- 심볼: XRPUSDT
- 기간: 2026-04-02 ~ 2026-04-20 (19일)
- 거래 수: 24건 (LONG 17, SHORT 7) — 목표 50건 미달
- 테스트 마감: 2026-04-30
## 중간 결과
### 방향별 상세 (Raw, 수수료 미반영)
| 방향 | 거래 수 | 승률 | PF | CumPnL(bps) | 평균보유 |
|------|---------|------|----|-------------|---------|
| Total | 24 | 41.7% | 0.98 | -15.8 | 240m |
| LONG | 17 | 47.1% | 1.17 | +103.1 | 277m |
| SHORT | 7 | 28.6% | 0.58 | -118.9 | 150m |
### 비용 보정 시나리오
| 시나리오 | Total PF | Total CumPnL | LONG PF | SHORT PF |
|----------|----------|-------------|---------|----------|
| fees_only | 0.79 | -207.8 | 0.95 | 0.46 |
| realistic | 0.74 | -264.8 | 0.90 | 0.42 |
| pessimistic | 0.66 | -369.8 | 0.80 | 0.37 |
## 현재 상태
**판단 불가** — 거래 수 24건 < 50건 기준, 테스트 기간 잔여 9일 (4/30 마감)
### 우려 사항 (최종 판정 시 확인 필요)
- SHORT 승률 28.6%, PF 0.58 — 역 edge 가능성
- Raw PF 0.98 — 수수료 미반영으로도 손익분기 미달
- LONG은 Raw PF 1.17이나 수수료 반영 시 PF 0.95로 하락
## 결론
4/30 마감 후 50건 이상 누적 시 최종 판정 예정.

View File

@@ -0,0 +1,79 @@
# MTF + BTC 추세 필터 백테스트 설계
## 가설
BTC 추세 방향과 일치하는 MTF 풀백 시그널만 실행하면 비용 반영 후에도 PF > 1.2를 달성한다.
## 메인 가설 (사전 확정 — commitment device)
> 메인 가설은 sweep 결과를 보기 **전에** BTC 1h + EMA 50/200 + ADX > 20으로 확정한다.
> sweep 12개 결과에서 가장 좋은 조합으로 사후 변경하지 않는다.
> 4h/1d 결과는 robustness 참고용이며, 메인 가설이 OOS 실패 시
> "다른 조합이 됐으니 PASS"로 구제하지 않는다.
- **BTC 타임프레임**: 1h
- **BTC EMA**: fast=50, slow=200
- **BTC ADX 임계값**: 20
- **선택 근거**: XRP MTF bot의 1h 메타필터와 동일 기준 → 시그널 정합성 확보, 사후 정당화 차단
## 필터 로직
```
BTC_trend = (BTC EMA_fast > BTC EMA_slow) AND (BTC ADX > 20)
? (EMA_fast > EMA_slow ? "UP" : "DOWN")
: "NEUTRAL"
if BTC_trend == "UP": SHORT 차단, LONG만 허용
if BTC_trend == "DOWN": LONG 차단, SHORT만 허용
if BTC_trend == "NEUTRAL": 양방향 차단 (추세 불명확)
```
## Sweep 파라미터 (robustness check용)
| 파라미터 | 후보 | 설명 |
|---------|------|------|
| BTC 타임프레임 | 1h, 4h, 1d | BTC 추세 판단 주기 |
| BTC EMA fast | 20, 50 | 단기 EMA |
| BTC EMA slow | 100, 200 | 장기 EMA |
총 조합: 3 × 2 × 2 = 12개. ADX > 20은 전 조합 고정.
## 데이터
- XRP: `data/xrpusdt/combined_15m.parquet` (기존)
- BTC: `data/btcusdt/combined_15m.parquet` (fetch 필요)
- 기간: 최소 6개월 (XRP와 동일 기간)
- BTC 15m → 1h/4h/1d resample 후 EMA/ADX 계산
- merge: `merge_asof(direction="backward")` — look-ahead bias 방지
- fetch 후 XRP/BTC 첫/마지막 timestamp + bar 수 일치 검증 필수
## IS/OOS 분할
- 앞 70% IS, 뒤 30% OOS (단순 시간 분할)
- ML 없고 sweep 12개뿐이므로 walk-forward 불필요
## 합격 기준
| 기준 | 값 | 비고 |
|------|-----|------|
| 메인 가설 OOS fees_only PF | >= 1.2 | 실거래 마진 확보 |
| 메인 가설 OOS realistic PF | >= 1.0 | 슬리피지+펀딩 반영 후 흑자 |
| LONG/SHORT 양쪽 fees_only PF | >= 0.8 | 대칭성 |
| OOS 거래 수 | >= 50 | 통계적 유의성 |
| IS/OOS PF 격차 | < 30% | 과적합 방지 |
| 베이스라인 대비 OOS PF | 명확한 개선 | 절대값보다 차이 중요 |
| IS 거래 수 | >= 100 | 미달 시 조합 자동 제외 |
## 판정 흐름
1. 베이스라인(BTC 필터 없는 MTF) IS/OOS 결과 먼저 산출
2. 12개 조합 IS sweep, 모두 결과 저장 (IS 거래 수 < 100 자동 제외)
3. 메인 가설(1h/EMA50-200/ADX20) OOS 검증 → 합격 기준 통과 시 PASS
4. 나머지 11개도 OOS 검증 → robustness 보고서 (참고용)
5. 메인 가설 실패 시 → 전략 폐기 (다른 조합으로 구제 안 함)
## 산출물
- `scripts/mtf_btc_filter_backtest.py` — 백테스트 스크립트
- `docs/plans/2026-05-04-mtf-btc-filter-result.md` — 결과 문서
- 거래 수준 로그 (CSV) — entry/exit/BTC trend/PnL 포함, 사후 분석용

View File

@@ -0,0 +1,80 @@
# MTF + BTC 추세 필터 백테스트 결과
## 가설
BTC 추세 방향과 일치하는 MTF 풀백 시그널만 실행하면 비용 반영 후에도 PF > 1.2를 달성한다.
메인 가설 (사전 확정): BTC 1h + EMA 50/200 + ADX > 20
## 데이터
- 심볼: XRPUSDT
- 기간: 2024-04-02 ~ 2026-05-03 (761일, 73,123 bars)
- IS: 2024-04-02 ~ 2025-09-17 (70%)
- OOS: 2025-09-17 ~ 2026-05-03 (30%)
- BTC OHLCV: XRP parquet 내 상관 컬럼 활용 (NaN 0%)
## 베이스라인 (BTC 필터 없음)
| 구분 | IS PF(fees) | OOS PF(fees) | OOS 거래수 |
|------|------------|-------------|-----------|
| Total | 1.02 | 0.94 | 206 |
| LONG | 1.04 | 0.71 | 69 |
| SHORT | 1.01 | 1.06 | 137 |
베이스라인 자체가 OOS에서 적자 (fees PF 0.94).
## 메인 가설 결과 (BTC 1h EMA50/200 ADX>20)
| 구분 | IS PF(fees) | OOS PF(fees) | OOS PF(real) | OOS 거래수 |
|------|------------|-------------|-------------|-----------|
| Total | 1.12 | **0.90** | 0.88 | 158 |
| LONG | 1.24 | 0.81 | 0.78 | 56 |
| SHORT | 1.02 | 0.95 | 0.92 | 102 |
## 합격 기준 체크
| 기준 | 결과 | 판정 |
|------|------|------|
| OOS fees_only PF >= 1.2 | 0.90 | **FAIL** |
| OOS realistic PF >= 1.0 | 0.88 | **FAIL** |
| OOS 거래수 >= 50 | 158 | PASS |
| LONG/SHORT fees PF >= 0.8 | L:0.81 S:0.95 | PASS |
| IS/OOS PF 격차 < 30% | 19.6% | PASS |
| 베이스라인 대비 개선 | 0.90 vs 0.94 | **FAIL** |
## Sweep Robustness
| 조합 | IS N | IS FPF | OOS FPF | OOS rPF | 비고 |
|------|------|--------|---------|---------|------|
| BTC_1h_EMA20/100 | 327 | 1.19 | 0.84 | 0.81 | |
| BTC_1h_EMA20/200 | 333 | 1.16 | 0.86 | 0.84 | |
| BTC_1h_EMA50/100 | 335 | 1.14 | 0.83 | 0.80 | |
| **BTC_1h_EMA50/200** | **333** | **1.12** | **0.90** | **0.88** | **메인 ★** |
| BTC_4h_EMA20/100 | 310 | 1.00 | 1.04 | 1.01 | |
| BTC_4h_EMA20/200 | 269 | 0.95 | 1.09 | 1.06 | |
| BTC_4h_EMA50/100 | 271 | 0.91 | 1.07 | 1.04 | |
| BTC_4h_EMA50/200 | 231 | 0.91 | 1.01 | 0.98 | |
| BTC_1d_EMA20/100 | 151 | 1.10 | 1.17 | 1.13 | |
| BTC_1d_EMA20/200 | 94 | 1.41 | 1.28 | 1.25 | IS<100 SKIP |
| BTC_1d_EMA50/100 | 129 | 1.28 | 1.20 | 1.16 | |
| BTC_1d_EMA50/200 | 96 | 1.44 | 1.14 | 1.11 | IS<100 SKIP |
- 1h 조합: **전멸** (OOS fees PF 0.83~0.90)
- 4h 조합: 1.01~1.09 — BEP 수준, 1.2 미달
- 1d 조합: 1.17~1.28 — 겉보기 좋으나 IS 거래수 94~151로 과적합 위험 + 사전 합의에 의해 구제 불가
12개 중 fees PF >= 1.2 통과는 **1개** (BTC_1d_EMA20/100, 1.17로 미달 → 실제 0개).
## 핵심 발견
1. **BTC 필터가 성과를 악화시킴**: 메인 가설 OOS PF 0.90 < 베이스라인 0.94. 필터가 오히려 좋은 거래를 걸러냄
2. **IS/OOS 격차 패턴**: 1h 조합은 IS에서 과적합(1.12~1.19) → OOS 붕괴(0.83~0.90). 전형적 curve-fitting
3. **1d가 좋아보이는 함정**: IS 거래수가 94~151로 적어 통계적 유의성 부족. 사전 합의대로 구제 불가
4. **MTF Pullback 자체의 한계**: 베이스라인 OOS 0.94로 전략 자체에 edge 없음. 필터로 보완 불가능
## 결론
**FAIL** — MTF Pullback 전략 최종 폐기.
BTC 추세 필터(1h EMA50/200 ADX>20)는 OOS에서 베이스라인보다 오히려 악화. 12개 sweep 조합 중 합격 기준(fees PF >= 1.2)을 통과한 조합 0개. 베이스라인 자체도 OOS 적자(PF 0.94)로 전략의 근본적 edge 부재 확인.

View File

@@ -0,0 +1,48 @@
# MTF Pullback Bot OOS Dry-run 최종 결과
## 가설
MTF Pullback Bot의 멀티타임프레임 풀백 시그널이 XRPUSDT LONG/SHORT 양방향에서 수익을 낸다.
## 데이터
- 심볼: XRPUSDT
- 기간: 2026-04-02 ~ 2026-04-29 (28일)
- 거래 수: 30건 (LONG 18, SHORT 12)
- 실제 자금 투입: 0 (dry-run, 시그널+주문 기록만)
## 결과
### Raw (비용 미반영, 참고용)
| 방향 | 거래 수 | 승률 | PF | CumPnL(bps) | 평균보유 |
|------|---------|------|----|-------------|---------|
| Total | 30 | 43.3% | 1.06 | +61.6 | 237m |
| LONG | 18 | 50.0% | 1.28 | +167.7 | 272m |
| SHORT | 12 | 33.3% | 0.73 | -106.1 | 185m |
### 비용 보정 시나리오
| 시나리오 | Total PF | Total CumPnL | LONG PF | SHORT PF |
|----------|----------|-------------|---------|----------|
| fees_only | 0.84 | -178.4 | 1.04 | 0.56 |
| realistic | 0.79 | -250.4 | 0.98 | 0.52 |
| pessimistic | 0.69 | -382.4 | 0.87 | 0.45 |
### 4/21 중간 보고 대비 변화
| 지표 | 중간(24건) | 최종(30건) |
|------|-----------|-----------|
| Raw Total PF | 0.98 | 1.06 |
| fees_only Total PF | 0.79 | 0.84 |
| SHORT Raw PF | 0.58 | 0.73 |
| SHORT fees_only PF | 0.46 | 0.56 |
마지막 6건에서 소폭 개선됐으나 구조적 문제(SHORT 역 edge, 비용 흡수 불가) 불변.
## 결론
**FAIL** — 폐기 사유 2개 해당:
1. **OOS PF < 1.0 (비용 반영)**: fees_only 시나리오에서도 PF 0.84, 수수료를 이길 수 없음
2. **LONG/SHORT 대칭성 실패**: SHORT Raw PF 0.73 — 전략 자체에 SHORT edge 없음
Raw PF 1.06은 수수료(taker 왕복 8bps)를 흡수하기엔 마진이 너무 얇음. LONG만 분리 운영해도 fees_only PF 1.04로 거래 비용 감당 불가.

View File

@@ -0,0 +1,133 @@
# CoinTrader 전략 Post-Mortem (2026-05-04)
지금까지 시도한 모든 전략의 실패 원인을 분석하고, 다음 방향을 도출한다.
## 1. 사망자 명단
| # | 전략 | 기간 | IS PF | OOS PF(fees) | 사망 원인 |
|---|------|------|-------|-------------|----------|
| 1 | RSI+MACD+BB+EMA+StochRSI (기본) | 2026-03 | — | 0.89 | 수익성 부족 |
| 2 | ML Filter (LightGBM/ONNX) | 2026-03 | — | ML OFF > ML ON | 피처에 alpha 없음 |
| 3 | LS Ratio | 2026-03 | — | 0.86 (best) | r=0.12, 비용 커버 불가 |
| 4 | FR × OI | 2026-03 | — | SHORT 1.88 / LONG 0.50 | 대칭성 실패 |
| 5 | Taker Buy/Sell Ratio | 2026-03 | — | 0.93 | r=-0.08, 약함 |
| 6 | MTF Pullback (OOS dry-run) | 2026-04 | — | 0.84 (fees) | SHORT 역 edge |
| 7 | MTF Pullback + BTC 필터 | 2026-05 | 1.12 | 0.90 (fees) | 베이스라인보다 악화 |
**7전 7패.** 단 하나도 비용 반영 후 OOS PF > 1.0을 달성하지 못함.
## 2. 공통 실패 패턴
### 패턴 A: "IS에서 살고 OOS에서 죽는다"
| 전략 | IS fees PF | OOS fees PF | 격차 |
|------|-----------|-------------|------|
| 기본 시그널 | ~1.02 | 0.89 | -13% |
| MTF Pullback | 1.02 | 0.94 | -8% |
| MTF + BTC 1h | 1.12 | 0.90 | -20% |
IS에서 마진이 얇은 전략(PF 1.0~1.1)은 OOS에서 비용을 이기지 못한다. **IS PF < 1.3이면 OOS 통과 가능성 거의 0.**
### 패턴 B: "LONG/SHORT 중 한쪽만 작동"
| 전략 | 좋은 쪽 | 나쁜 쪽 | 진단 |
|------|---------|---------|------|
| FR × OI | SHORT PF 1.88 | LONG PF 0.50 | 시장 베타 |
| MTF OOS (4월) | LONG PF 1.28 | SHORT PF 0.73 | 상승장 편승 |
| MTF 백테스트 OOS | SHORT PF 1.06 | LONG PF 0.71 | 하락장 편승 |
한쪽만 좋은 건 "전략의 edge"가 아니라 **해당 기간의 시장 방향(beta)**을 타고 있을 뿐. 기간이 바뀌면 좋은 쪽과 나쁜 쪽이 뒤집힌다 (4월 OOS에서 LONG이 좋았고, 전체 백테스트 OOS에서는 SHORT가 좋았음).
### 패턴 C: "필터/피처를 추가할수록 악화"
| 레이어 | OOS fees PF |
|--------|-------------|
| 기본 시그널 | 0.89 |
| + ML Filter | 더 나빠짐 |
| MTF Pullback (베이스라인) | 0.94 |
| + BTC 추세 필터 | 0.90 (악화) |
필터를 추가하면 **좋은 거래도 같이 걸러진다**. 기본 시그널 자체에 edge가 없으면 필터로 구제 불가능. 오히려 거래 수 감소 → 통계적 불안정 → 악화.
## 3. 근본 원인 진단: Edge 부재 vs Regime Dependence?
**답: 둘 다. 그리고 이 둘은 연결되어 있다.**
1. **Edge 부재가 근본**: RSI/MACD/BB/EMA/StochRSI + 풀백 패턴은 너무 많은 참가자가 아는 시그널. 알파가 소진된 공공재. r < 0.15인 시그널은 수수료 8bps를 이길 수 없다.
2. **Regime dependence는 결과**: edge가 없는 전략은 시장 방향(beta)에 수익이 좌우된다. 상승장에선 LONG이, 하락장에선 SHORT가 좋아 보이지만 이건 전략의 alpha가 아니라 시장의 beta.
3. **검증 방법론은 정상 작동**: IS/OOS 분할, 비용 모델, 대칭성 체크, 사전 가설 확정 — 이 프레임워크가 7개 전략을 정확히 거부했다. 문제는 방법론이 아니라 **시그널 소스**.
## 4. "뭘 안 시도했나?" — 미탐색 영역 지도
### 이미 소진한 영역 (재시도 금지)
- Binance 공개 API 파생 피처 (전수 테스트 완료)
- 기술적 지표 조합 (RSI, MACD, BB, EMA, StochRSI, ADX)
- ML 피처 선택/앙상블 (LightGBM, ONNX — 피처 자체에 alpha 없음)
- 시간대 변경 (15m→1h→4h→1d sweep 완료)
- 상관 자산 필터 (BTC 추세 필터 검증 완료)
### 미탐색 영역
| 방향 | 구체적 아이디어 | 난이도 | 기대값 |
|------|---------------|--------|--------|
| **데이터 소스 변경** | 온체인 (whale wallet, exchange flow) | 높음 | 중 — 지연 있으나 독점 정보 가능성 |
| **데이터 소스 변경** | 크로스 거래소 (OKX/Bybit OI/FR 차이) | 중 | 중 — 차이 자체가 시그널 |
| **데이터 소스 변경** | 소셜/뉴스 센티먼트 | 높음 | 낮음 — 노이즈 많음, 지연 |
| **자산 변경** | BTC/ETH (유동성 높은 메이저) | 낮음 | 중 — 경쟁 치열하지만 유동성 풍부 |
| **자산 변경** | 저유동성 알트 (DYDX, APE 등) | 낮음 | 중 — 비효율성 클 수 있으나 슬리피지 |
| **패러다임 변경** | 통계적 차익 (pair trading, mean reversion) | 중 | 중상 — 방향 중립, beta 제거 |
| **패러다임 변경** | 마이크로 구조 (호가창 불균형, 체결 패턴) | 높음 | 높음 — 진입 장벽이 alpha 보호 |
| **패러다임 변경** | 변동성 전략 (vol expansion/compression) | 중 | 중 — 방향 아닌 "크기" 예측 |
| **실행 최적화** | 동일 시그널 + maker order (비용 절감) | 중 | 중 — 0.08%→0.04% 비용 반감 |
## 5. 다음 방향 제안
### 우선순위 1: 패러다임을 바꿔라
지금까지의 모든 전략은 **"방향 예측"** 패러다임:
- "XRP가 오를까 내릴까?" → LONG/SHORT
이 패러다임에서 공개 시그널로 alpha를 찾기는 극히 어렵다. 대안:
**(A) 통계적 차익 (Stat Arb / Pair Trading)**
- XRP/BTC 비율의 평균 회귀
- 여러 알트코인 간 상대 강도 기반 롱숏
- 장점: 방향 중립 → beta 제거, 대칭성 자동 충족
- 인프라: 현재 multi-symbol 아키텍처로 확장 가능
**(B) 변동성 전략**
- "방향"이 아니라 "크기"를 예측
- ATR 수축 → 확장 시 양방향 진입 (straddle-like)
- BB squeeze → breakout 방향은 예측하되, 비용 대비 마진이 큰 구간만
### 우선순위 2: 비용을 줄여라
현재 taker 왕복 8bps가 모든 전략의 적. 같은 시그널이라도:
- Entry를 limit order로 → 4bps 절감 (왕복 4bps)
- 이것만으로 MTF 베이스라인 OOS가 0.94 → ~1.02로 개선 가능
- 근본 해결은 아니지만, 마진이 얇은 전략을 BEP 위로 올릴 수 있음
### 우선순위 3: 데이터 소스를 넓혀라
Binance 공개 API는 소진. 외부 데이터가 필요:
- Glassnode/CryptoQuant (온체인)
- 크로스 거래소 OI/FR 차이
- 이 데이터는 접근 비용(유료 API)이 있으나 그만큼 경쟁자가 적음
## 6. 보존할 자산
7전 7패였지만 **인프라는 자산**:
| 자산 | 용도 |
|------|------|
| 비용 모델 (3 시나리오) | 어떤 전략이든 재사용 |
| IS/OOS 검증 프레임워크 | 자기기만 방지 |
| 사전 가설 확정 프로토콜 | sweep 과적합 방지 |
| multi-symbol 아키텍처 | pair trading 확장 가능 |
| kill switch (Fast/Slow) | 실전 리스크 관리 |
| 주간 리포트 파이프라인 | 모니터링 |
다음 전략이 뭐든, 이 검증 인프라 위에서 시작하면 실패를 빠르게 확인할 수 있다.

Binary file not shown.

Binary file not shown.

View File

@@ -98,5 +98,30 @@
"reg_lambda": 0.000157
},
"weight_scale": 1.783105
},
{
"date": "2026-03-21T19:04:07.386894",
"backend": "lgbm",
"auc": 0.9251,
"best_threshold": 0.3743,
"best_precision": 0.467,
"best_recall": 0.208,
"samples": 12172,
"features": 26,
"time_weight_decay": 2.0,
"model_path": "models/dogeusdt/lgbm_filter.pkl",
"tuned_params_path": null,
"lgbm_params": {
"n_estimators": 434,
"learning_rate": 0.123659,
"max_depth": 6,
"num_leaves": 14,
"min_child_samples": 10,
"subsample": 0.929062,
"colsample_bytree": 0.94633,
"reg_alpha": 0.573971,
"reg_lambda": 0.000157
},
"weight_scale": 1.783105
}
]

Binary file not shown.

View File

@@ -0,0 +1,27 @@
[
{
"date": "2026-03-21T19:04:06.511618",
"backend": "lgbm",
"auc": 0.9185,
"best_threshold": 0.4455,
"best_precision": 0.418,
"best_recall": 0.444,
"samples": 11772,
"features": 26,
"time_weight_decay": 2.0,
"model_path": "models/solusdt/lgbm_filter.pkl",
"tuned_params_path": null,
"lgbm_params": {
"n_estimators": 434,
"learning_rate": 0.123659,
"max_depth": 6,
"num_leaves": 14,
"min_child_samples": 10,
"subsample": 0.929062,
"colsample_bytree": 0.94633,
"reg_alpha": 0.573971,
"reg_lambda": 0.000157
},
"weight_scale": 1.783105
}
]

Binary file not shown.

Binary file not shown.

View File

@@ -98,5 +98,30 @@
"reg_lambda": 0.000157
},
"weight_scale": 1.783105
},
{
"date": "2026-03-21T19:04:05.287371",
"backend": "lgbm",
"auc": 0.9237,
"best_threshold": 0.5306,
"best_precision": 0.494,
"best_recall": 0.303,
"samples": 12156,
"features": 26,
"time_weight_decay": 2.0,
"model_path": "models/xrpusdt/lgbm_filter.pkl",
"tuned_params_path": null,
"lgbm_params": {
"n_estimators": 434,
"learning_rate": 0.123659,
"max_depth": 6,
"num_leaves": 14,
"min_child_samples": 10,
"subsample": 0.929062,
"colsample_bytree": 0.94633,
"reg_alpha": 0.573971,
"reg_lambda": 0.000157
},
"weight_scale": 1.783105
}
]

View File

@@ -0,0 +1,44 @@
{
"timestamp": "2026-03-21T19:58:41.732033",
"symbols": [
"DOGEUSDT"
],
"ml_off": {
"total_trades": 322,
"total_pnl": -1547.342,
"return_pct": -154.73,
"win_rate": 31.37,
"avg_win": 44.161,
"avg_loss": -27.1837,
"payoff_ratio": 1.62,
"max_consecutive_losses": 9,
"profit_factor": 0.74,
"max_drawdown_pct": 155.09,
"sharpe_ratio": -23.72,
"total_fees": 891.5776,
"close_reasons": {
"STOP_LOSS": 198,
"REVERSE_SIGNAL": 58,
"TAKE_PROFIT": 66
}
},
"ml_on": {
"total_trades": 20,
"total_pnl": -173.2651,
"return_pct": -17.33,
"win_rate": 35.0,
"avg_win": 42.0257,
"avg_loss": -35.9573,
"payoff_ratio": 1.17,
"max_consecutive_losses": 3,
"profit_factor": 0.63,
"max_drawdown_pct": 18.86,
"sharpe_ratio": -40.35,
"total_fees": 77.1422,
"close_reasons": {
"STOP_LOSS": 13,
"REVERSE_SIGNAL": 4,
"TAKE_PROFIT": 3
}
}
}

View File

@@ -0,0 +1,45 @@
{
"timestamp": "2026-03-21T20:03:34.650022",
"symbols": [
"DOGEUSDT"
],
"ml_off": {
"total_trades": 40,
"total_pnl": 161.0802,
"return_pct": 16.11,
"win_rate": 25.0,
"avg_win": 107.1017,
"avg_loss": -30.3312,
"payoff_ratio": 3.53,
"max_consecutive_losses": 8,
"profit_factor": 1.18,
"max_drawdown_pct": 22.19,
"sharpe_ratio": 7.08,
"total_fees": 150.4991,
"close_reasons": {
"STOP_LOSS": 25,
"REVERSE_SIGNAL": 8,
"END_OF_DATA": 1,
"TAKE_PROFIT": 6
}
},
"ml_on": {
"total_trades": 47,
"total_pnl": -285.0131,
"return_pct": -28.5,
"win_rate": 34.04,
"avg_win": 61.4296,
"avg_loss": -40.8996,
"payoff_ratio": 1.5,
"max_consecutive_losses": 7,
"profit_factor": 0.78,
"max_drawdown_pct": 40.93,
"sharpe_ratio": -21.03,
"total_fees": 176.3033,
"close_reasons": {
"STOP_LOSS": 30,
"REVERSE_SIGNAL": 6,
"TAKE_PROFIT": 11
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,188 @@
{
"mode": "walk_forward",
"config": {
"symbols": [
"DOGEUSDT"
],
"start": null,
"end": null,
"initial_balance": 1000.0,
"leverage": 10,
"fee_pct": 0.04,
"slippage_pct": 0.01,
"use_ml": true,
"ml_threshold": 0.55,
"max_daily_loss_pct": 0.05,
"max_positions": 3,
"max_same_direction": 2,
"margin_max_ratio": 0.5,
"margin_min_ratio": 0.2,
"margin_decay_rate": 0.0006,
"atr_sl_mult": 1.5,
"atr_tp_mult": 3.0,
"min_notional": 5.0,
"signal_threshold": 3,
"adx_threshold": 0,
"volume_multiplier": 1.5,
"train_months": 6,
"test_months": 1,
"time_weight_decay": 2.0,
"negative_ratio": 3
},
"summary": {
"total_trades": 2,
"total_pnl": -84.5014,
"return_pct": -8.45,
"win_rate": 0.0,
"avg_win": 0.0,
"avg_loss": -42.2507,
"payoff_ratio": 0.0,
"max_consecutive_losses": 2,
"profit_factor": 0.0,
"max_drawdown_pct": 4.28,
"sharpe_ratio": -5831.15,
"total_fees": 7.8265,
"close_reasons": {
"STOP_LOSS": 2
}
},
"folds": [
{
"fold": 1,
"train_period": "2025-03-05 ~ 2025-09-05",
"test_period": "2025-09-05 ~ 2025-10-05",
"summary": {
"total_trades": 2,
"total_pnl": -84.5014,
"return_pct": -8.45,
"win_rate": 0.0,
"avg_win": 0.0,
"avg_loss": -42.2507,
"payoff_ratio": 0.0,
"max_consecutive_losses": 2,
"profit_factor": 0.0,
"max_drawdown_pct": 4.28,
"sharpe_ratio": -5831.15,
"total_fees": 7.8265,
"close_reasons": {
"STOP_LOSS": 2
}
}
}
],
"trades": [
{
"symbol": "DOGEUSDT",
"side": "SHORT",
"entry_time": "2025-10-03 14:00:00",
"exit_time": "2025-10-03 14:30:00",
"entry_price": 0.253975,
"exit_price": 0.255986,
"quantity": 19685.0,
"sl": 0.255986,
"tp": 0.249952,
"gross_pnl": -39.591591,
"entry_fee": 1.999796,
"exit_fee": 2.015633,
"net_pnl": -43.60702,
"close_reason": "STOP_LOSS",
"ml_proba": 0.6215,
"indicators": {
"rsi": 39.31265367759862,
"macd_hist": -9.917203695834255e-06,
"atr": 0.001340837894829163,
"adx": 22.11118870853989
},
"fold": 1
},
{
"symbol": "DOGEUSDT",
"side": "LONG",
"entry_time": "2025-10-04 02:00:00",
"exit_time": "2025-10-04 03:00:00",
"entry_price": 0.254255,
"exit_price": 0.252284,
"quantity": 18809.6,
"sl": 0.252284,
"tp": 0.258198,
"gross_pnl": -37.083263,
"entry_fee": 1.912977,
"exit_fee": 1.898144,
"net_pnl": -40.894384,
"close_reason": "STOP_LOSS",
"ml_proba": 0.589,
"indicators": {
"rsi": 29.729826487088012,
"macd_hist": -0.0004551413936173264,
"atr": 0.0013143381729553857,
"adx": 17.29524723775582
},
"fold": 1
}
],
"validation": {
"overall": "FAIL",
"checks": [
{
"name": "exit_after_entry",
"passed": true,
"level": "FAIL",
"message": "모든 트레이드에서 청산 > 진입"
},
{
"name": "sl_tp_direction",
"passed": true,
"level": "FAIL",
"message": "SL/TP 방향 정합"
},
{
"name": "no_overlap",
"passed": true,
"level": "FAIL",
"message": "포지션 비중첩 확인"
},
{
"name": "positive_fees",
"passed": true,
"level": "FAIL",
"message": "수수료 양수 확인"
},
{
"name": "no_negative_balance",
"passed": true,
"level": "FAIL",
"message": "잔고 양수 유지"
},
{
"name": "win_rate_high",
"passed": true,
"level": "WARNING",
"message": "승률 정상 (0.0%)"
},
{
"name": "win_rate_low",
"passed": false,
"level": "WARNING",
"message": "승률 0.0% < 20% — 신호 로직 반전 의심"
},
{
"name": "mdd_nonzero",
"passed": true,
"level": "WARNING",
"message": "MDD 정상 (4.3%)"
},
{
"name": "trade_frequency",
"passed": false,
"level": "WARNING",
"message": "월 평균 2.0건 < 5건 — 신호 생성 부족"
},
{
"name": "profit_factor_high",
"passed": true,
"level": "WARNING",
"message": "PF 정상 (0.00)"
}
]
}
}

View File

@@ -0,0 +1,44 @@
{
"timestamp": "2026-03-21T19:58:41.166218",
"symbols": [
"SOLUSDT"
],
"ml_off": {
"total_trades": 346,
"total_pnl": -1287.9804,
"return_pct": -128.8,
"win_rate": 33.82,
"avg_win": 35.5174,
"avg_loss": -23.7708,
"payoff_ratio": 1.49,
"max_consecutive_losses": 11,
"profit_factor": 0.76,
"max_drawdown_pct": 176.42,
"sharpe_ratio": -20.59,
"total_fees": 940.6664,
"close_reasons": {
"STOP_LOSS": 187,
"TAKE_PROFIT": 68,
"REVERSE_SIGNAL": 91
}
},
"ml_on": {
"total_trades": 14,
"total_pnl": 80.7573,
"return_pct": 8.08,
"win_rate": 35.71,
"avg_win": 72.906,
"avg_loss": -31.5303,
"payoff_ratio": 2.31,
"max_consecutive_losses": 4,
"profit_factor": 1.28,
"max_drawdown_pct": 11.46,
"sharpe_ratio": 18.5,
"total_fees": 51.8491,
"close_reasons": {
"STOP_LOSS": 7,
"TAKE_PROFIT": 5,
"REVERSE_SIGNAL": 2
}
}
}

View File

@@ -0,0 +1,45 @@
{
"timestamp": "2026-03-21T20:03:32.481740",
"symbols": [
"SOLUSDT"
],
"ml_off": {
"total_trades": 31,
"total_pnl": -3218.5412,
"return_pct": -321.85,
"win_rate": 25.81,
"avg_win": 40.6328,
"avg_loss": -154.0697,
"payoff_ratio": 0.26,
"max_consecutive_losses": 7,
"profit_factor": 0.09,
"max_drawdown_pct": 333.12,
"sharpe_ratio": -39.57,
"total_fees": 112.8457,
"close_reasons": {
"STOP_LOSS": 21,
"TAKE_PROFIT": 5,
"REVERSE_SIGNAL": 4,
"END_OF_DATA": 1
}
},
"ml_on": {
"total_trades": 17,
"total_pnl": -488.3431,
"return_pct": -48.83,
"win_rate": 11.76,
"avg_win": 79.5151,
"avg_loss": -43.1582,
"payoff_ratio": 1.84,
"max_consecutive_losses": 8,
"profit_factor": 0.25,
"max_drawdown_pct": 45.94,
"sharpe_ratio": -123.73,
"total_fees": 61.1816,
"close_reasons": {
"STOP_LOSS": 14,
"REVERSE_SIGNAL": 1,
"TAKE_PROFIT": 2
}
}
}

View File

@@ -0,0 +1,81 @@
{
"mode": "walk_forward",
"config": {
"symbols": [
"SOLUSDT"
],
"start": null,
"end": null,
"initial_balance": 1000.0,
"leverage": 10,
"fee_pct": 0.04,
"slippage_pct": 0.01,
"use_ml": true,
"ml_threshold": 0.55,
"max_daily_loss_pct": 0.05,
"max_positions": 3,
"max_same_direction": 2,
"margin_max_ratio": 0.5,
"margin_min_ratio": 0.2,
"margin_decay_rate": 0.0006,
"atr_sl_mult": 1.5,
"atr_tp_mult": 3.0,
"min_notional": 5.0,
"signal_threshold": 3,
"adx_threshold": 0,
"volume_multiplier": 1.5,
"train_months": 6,
"test_months": 1,
"time_weight_decay": 2.0,
"negative_ratio": 3
},
"summary": {
"total_trades": 0,
"total_pnl": 0.0,
"return_pct": 0.0,
"win_rate": 0.0,
"avg_win": 0.0,
"avg_loss": 0.0,
"payoff_ratio": 0.0,
"max_consecutive_losses": 0,
"profit_factor": 0.0,
"max_drawdown_pct": 0.0,
"sharpe_ratio": 0.0,
"total_fees": 0.0,
"close_reasons": {}
},
"folds": [
{
"fold": 1,
"train_period": "2025-03-18 ~ 2025-09-18",
"test_period": "2025-09-18 ~ 2025-10-18",
"summary": {
"total_trades": 0,
"total_pnl": 0.0,
"return_pct": 0.0,
"win_rate": 0.0,
"avg_win": 0.0,
"avg_loss": 0.0,
"payoff_ratio": 0.0,
"max_consecutive_losses": 0,
"profit_factor": 0.0,
"max_drawdown_pct": 0.0,
"sharpe_ratio": 0.0,
"total_fees": 0.0,
"close_reasons": {}
}
}
],
"trades": [],
"validation": {
"overall": "PASS",
"checks": [
{
"name": "trade_count",
"passed": true,
"level": "FAIL",
"message": "트레이드 없음 (검증 스킵)"
}
]
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,549 @@
{
"config": {
"symbols": [
"XRPUSDT"
],
"start": null,
"end": null,
"initial_balance": 1000.0,
"leverage": 20,
"fee_pct": 0.04,
"slippage_pct": 0.01,
"use_ml": false,
"ml_threshold": 0.55,
"max_daily_loss_pct": 0.05,
"max_positions": 3,
"max_same_direction": 2,
"margin_max_ratio": 0.5,
"margin_min_ratio": 0.2,
"margin_decay_rate": 0.0006,
"atr_sl_mult": 1.5,
"atr_tp_mult": 4.0,
"min_notional": 5.0,
"signal_threshold": 3,
"adx_threshold": 25.0,
"volume_multiplier": 2.5
},
"summary": {
"total_trades": 19,
"total_pnl": -90.5608,
"return_pct": -9.06,
"win_rate": 31.58,
"avg_win": 187.773,
"avg_loss": -93.6307,
"payoff_ratio": 2.01,
"max_consecutive_losses": 8,
"profit_factor": 0.93,
"max_drawdown_pct": 39.26,
"sharpe_ratio": -6.38,
"total_fees": 130.2737,
"close_reasons": {
"STOP_LOSS": 13,
"TAKE_PROFIT": 6
}
},
"trades": [
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-03-26 13:45:00",
"exit_time": "2025-03-26 14:30:00",
"entry_price": 2.415342,
"exit_price": 2.399559,
"quantity": 4140.6,
"sl": 2.399559,
"tp": 2.457429,
"gross_pnl": -65.350891,
"entry_fee": 4.000385,
"exit_fee": 3.974245,
"net_pnl": -73.325521,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 26.92729946174303,
"macd_hist": -0.004175927274053651,
"atr": 0.010521967932285681,
"adx": 28.1934914013265
}
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-03-29 09:30:00",
"exit_time": "2025-03-29 13:00:00",
"entry_price": 2.091091,
"exit_price": 2.117184,
"quantity": 4431.1,
"sl": 2.117184,
"tp": 2.02151,
"gross_pnl": -115.619197,
"entry_fee": 3.706333,
"exit_fee": 3.752581,
"net_pnl": -123.07811,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 29.5299317993386,
"macd_hist": -0.0016272252554855211,
"atr": 0.017395108299016367,
"adx": 35.60495925685308
}
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-04-02 02:00:00",
"exit_time": "2025-04-02 05:45:00",
"entry_price": 2.090609,
"exit_price": 2.070463,
"quantity": 3844.2,
"sl": 2.070463,
"tp": 2.144332,
"gross_pnl": -77.446201,
"entry_fee": 3.214688,
"exit_fee": 3.183709,
"net_pnl": -83.844598,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 29.415272650543557,
"macd_hist": -0.0038881862305720294,
"atr": 0.013430831024751285,
"adx": 26.024817307471817
}
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-04-03 12:15:00",
"exit_time": "2025-04-03 14:45:00",
"entry_price": 2.016898,
"exit_price": 1.956939,
"quantity": 3568.3,
"sl": 2.039383,
"tp": 1.956939,
"gross_pnl": 213.952202,
"entry_fee": 2.878759,
"exit_fee": 2.793178,
"net_pnl": 208.280264,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 39.0031761939588,
"macd_hist": -0.00027555783529762453,
"atr": 0.01498978519243727,
"adx": 29.63100718549373
}
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-04-06 18:45:00",
"exit_time": "2025-04-06 20:30:00",
"entry_price": 1.994699,
"exit_price": 1.968556,
"quantity": 4653.0,
"sl": 1.968556,
"tp": 2.064416,
"gross_pnl": -121.647028,
"entry_fee": 3.712535,
"exit_fee": 3.663876,
"net_pnl": -129.023439,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 21.033027596614833,
"macd_hist": -0.006740564223060543,
"atr": 0.01742918952793266,
"adx": 42.14016267928252
}
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-04-08 12:00:00",
"exit_time": "2025-04-08 16:15:00",
"entry_price": 1.952805,
"exit_price": 1.879673,
"quantity": 4091.2,
"sl": 1.980229,
"tp": 1.879673,
"gross_pnl": 299.197837,
"entry_fee": 3.195726,
"exit_fee": 3.076047,
"net_pnl": 292.926065,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 73.41350187407278,
"macd_hist": 0.010090522420784207,
"atr": 0.01828301215814375,
"adx": 26.85765122231918
}
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-04-09 01:00:00",
"exit_time": "2025-04-09 01:45:00",
"entry_price": 1.737226,
"exit_price": 1.775195,
"quantity": 5591.5,
"sl": 1.775195,
"tp": 1.635978,
"gross_pnl": -212.299611,
"entry_fee": 3.88548,
"exit_fee": 3.9704,
"net_pnl": -220.155491,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 27.2339289074866,
"macd_hist": -0.002023427338529095,
"atr": 0.025312183439683184,
"adx": 47.62708053828556
}
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-04-12 13:00:00",
"exit_time": "2025-04-12 13:30:00",
"entry_price": 2.085709,
"exit_price": 2.123429,
"quantity": 4180.2,
"sl": 2.071563,
"tp": 2.123429,
"gross_pnl": 157.679154,
"entry_fee": 3.487472,
"exit_fee": 3.550543,
"net_pnl": 150.641139,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 71.08975614304354,
"macd_hist": 0.0004691893650785872,
"atr": 0.009430120184239851,
"adx": 41.039203834939215
}
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-04-22 13:15:00",
"exit_time": "2025-04-22 14:00:00",
"entry_price": 2.105411,
"exit_price": 2.137638,
"quantity": 4726.0,
"sl": 2.093325,
"tp": 2.137638,
"gross_pnl": 152.30925,
"entry_fee": 3.980068,
"exit_fee": 4.040992,
"net_pnl": 144.28819,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 57.841977820073645,
"macd_hist": 4.316762016507922e-06,
"atr": 0.008056985291244722,
"adx": 27.486954266795543
}
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-04-24 08:30:00",
"exit_time": "2025-04-24 13:30:00",
"entry_price": 2.129213,
"exit_price": 2.178907,
"quantity": 4383.8,
"sl": 2.110578,
"tp": 2.178907,
"gross_pnl": 217.849236,
"entry_fee": 3.733617,
"exit_fee": 3.820757,
"net_pnl": 210.294862,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 24.016950832949433,
"macd_hist": -0.003143540248224934,
"atr": 0.012423538716450883,
"adx": 45.63982334493204
}
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-04-26 07:30:00",
"exit_time": "2025-04-26 09:00:00",
"entry_price": 2.235676,
"exit_price": 2.198258,
"quantity": 3372.4,
"sl": 2.249708,
"tp": 2.198258,
"gross_pnl": 126.188433,
"entry_fee": 3.015838,
"exit_fee": 2.965363,
"net_pnl": 120.207233,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 76.71113426509163,
"macd_hist": 0.005320482189513192,
"atr": 0.009354497780232585,
"adx": 30.700551675430855
}
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-04-28 07:15:00",
"exit_time": "2025-04-28 07:45:00",
"entry_price": 2.314669,
"exit_price": 2.333953,
"quantity": 2608.7,
"sl": 2.333953,
"tp": 2.263244,
"gross_pnl": -50.306622,
"entry_fee": 2.41531,
"exit_fee": 2.435433,
"net_pnl": -55.157366,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 73.98848696551217,
"macd_hist": 0.0034538468677465604,
"atr": 0.012856115408039817,
"adx": 27.89133838050565
}
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-04-30 07:45:00",
"exit_time": "2025-04-30 12:30:00",
"entry_price": 2.219022,
"exit_price": 2.205478,
"quantity": 3051.6,
"sl": 2.205478,
"tp": 2.255138,
"gross_pnl": -41.329913,
"entry_fee": 2.708627,
"exit_fee": 2.692095,
"net_pnl": -46.730634,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 33.11711659050469,
"macd_hist": -0.0028131165153663005,
"atr": 0.00902912407526822,
"adx": 27.47479838290321
}
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-04-30 13:15:00",
"exit_time": "2025-04-30 13:30:00",
"entry_price": 2.173717,
"exit_price": 2.152862,
"quantity": 3374.3,
"sl": 2.152862,
"tp": 2.229333,
"gross_pnl": -70.373501,
"entry_fee": 2.93391,
"exit_fee": 2.90576,
"net_pnl": -76.213172,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 27.513370415814684,
"macd_hist": -0.006240527006868588,
"atr": 0.013903822313270735,
"adx": 31.23141348514888
}
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-05-05 01:30:00",
"exit_time": "2025-05-05 02:15:00",
"entry_price": 2.139986,
"exit_price": 2.153598,
"quantity": 3803.3,
"sl": 2.153598,
"tp": 2.103686,
"gross_pnl": -51.77209,
"entry_fee": 3.255603,
"exit_fee": 3.276312,
"net_pnl": -58.304005,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 30.413570034116045,
"macd_hist": -0.0010637135478183844,
"atr": 0.009074941875769175,
"adx": 26.27401092812825
}
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-05-17 01:15:00",
"exit_time": "2025-05-17 01:45:00",
"entry_price": 2.335133,
"exit_price": 2.31116,
"quantity": 3709.9,
"sl": 2.31116,
"tp": 2.399062,
"gross_pnl": -88.937959,
"entry_fee": 3.465245,
"exit_fee": 3.42967,
"net_pnl": -95.832873,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 28.536979724863077,
"macd_hist": -0.004501483267526404,
"atr": 0.01598209455229851,
"adx": 51.966384578102044
}
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-05-18 14:30:00",
"exit_time": "2025-05-18 15:45:00",
"entry_price": 2.427557,
"exit_price": 2.442135,
"quantity": 3848.5,
"sl": 2.442135,
"tp": 2.388682,
"gross_pnl": -56.104255,
"entry_fee": 3.736982,
"exit_fee": 3.759423,
"net_pnl": -63.60066,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 73.26480732687115,
"macd_hist": 0.0028537582643641994,
"atr": 0.009718809144697027,
"adx": 29.42506362068419
}
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-05-20 12:30:00",
"exit_time": "2025-05-20 17:00:00",
"entry_price": 2.330967,
"exit_price": 2.345796,
"quantity": 4149.8,
"sl": 2.345796,
"tp": 2.291423,
"gross_pnl": -61.53732,
"entry_fee": 3.869219,
"exit_fee": 3.893833,
"net_pnl": -69.300372,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 33.11852309537045,
"macd_hist": -0.0003174284393988123,
"atr": 0.009885989673296352,
"adx": 34.742890662078324
}
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-05-23 12:15:00",
"exit_time": "2025-05-23 22:45:00",
"entry_price": 2.340634,
"exit_price": 2.313574,
"quantity": 4240.1,
"sl": 2.313574,
"tp": 2.412795,
"gross_pnl": -114.738573,
"entry_fee": 3.969809,
"exit_fee": 3.923914,
"net_pnl": -122.632296,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 18.100631569972105,
"macd_hist": -0.011460760086984913,
"atr": 0.018040230723781962,
"adx": 33.73789035029933
}
}
],
"validation": {
"overall": "PASS",
"checks": [
{
"name": "exit_after_entry",
"passed": true,
"level": "FAIL",
"message": "모든 트레이드에서 청산 > 진입"
},
{
"name": "sl_tp_direction",
"passed": true,
"level": "FAIL",
"message": "SL/TP 방향 정합"
},
{
"name": "no_overlap",
"passed": true,
"level": "FAIL",
"message": "포지션 비중첩 확인"
},
{
"name": "positive_fees",
"passed": true,
"level": "FAIL",
"message": "수수료 양수 확인"
},
{
"name": "no_negative_balance",
"passed": true,
"level": "FAIL",
"message": "잔고 양수 유지"
},
{
"name": "win_rate_high",
"passed": true,
"level": "WARNING",
"message": "승률 정상 (31.6%)"
},
{
"name": "win_rate_low",
"passed": true,
"level": "WARNING",
"message": "승률 정상 (31.6%)"
},
{
"name": "mdd_nonzero",
"passed": true,
"level": "WARNING",
"message": "MDD 정상 (39.3%)"
},
{
"name": "trade_frequency",
"passed": true,
"level": "WARNING",
"message": "월 평균 10.0건"
},
{
"name": "profit_factor_high",
"passed": true,
"level": "WARNING",
"message": "PF 정상 (0.93)"
}
]
}
}

View File

@@ -0,0 +1,45 @@
{
"timestamp": "2026-03-21T19:58:40.087843",
"symbols": [
"XRPUSDT"
],
"ml_off": {
"total_trades": 362,
"total_pnl": -1076.1058,
"return_pct": -107.61,
"win_rate": 36.74,
"avg_win": 30.9612,
"avg_loss": -22.681,
"payoff_ratio": 1.37,
"max_consecutive_losses": 8,
"profit_factor": 0.79,
"max_drawdown_pct": 117.67,
"sharpe_ratio": -18.8,
"total_fees": 1094.0448,
"close_reasons": {
"STOP_LOSS": 202,
"TAKE_PROFIT": 74,
"REVERSE_SIGNAL": 84,
"END_OF_DATA": 2
}
},
"ml_on": {
"total_trades": 14,
"total_pnl": 2.0995,
"return_pct": 0.21,
"win_rate": 35.71,
"avg_win": 38.8329,
"avg_loss": -21.3406,
"payoff_ratio": 1.82,
"max_consecutive_losses": 6,
"profit_factor": 1.01,
"max_drawdown_pct": 11.88,
"sharpe_ratio": 0.74,
"total_fees": 54.5343,
"close_reasons": {
"STOP_LOSS": 7,
"REVERSE_SIGNAL": 4,
"TAKE_PROFIT": 3
}
}
}

View File

@@ -0,0 +1,44 @@
{
"timestamp": "2026-03-21T20:03:32.712401",
"symbols": [
"XRPUSDT"
],
"ml_off": {
"total_trades": 53,
"total_pnl": 121.6625,
"return_pct": 12.17,
"win_rate": 43.4,
"avg_win": 39.0003,
"avg_loss": -25.8448,
"payoff_ratio": 1.51,
"max_consecutive_losses": 8,
"profit_factor": 1.16,
"max_drawdown_pct": 25.63,
"sharpe_ratio": 11.94,
"total_fees": 181.3167,
"close_reasons": {
"STOP_LOSS": 30,
"TAKE_PROFIT": 15,
"REVERSE_SIGNAL": 8
}
},
"ml_on": {
"total_trades": 40,
"total_pnl": -256.2189,
"return_pct": -25.62,
"win_rate": 35.0,
"avg_win": 44.2545,
"avg_loss": -33.6839,
"payoff_ratio": 1.31,
"max_consecutive_losses": 11,
"profit_factor": 0.71,
"max_drawdown_pct": 36.87,
"sharpe_ratio": -27.75,
"total_fees": 151.8982,
"close_reasons": {
"STOP_LOSS": 21,
"REVERSE_SIGNAL": 10,
"TAKE_PROFIT": 9
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,835 @@
{
"mode": "walk_forward",
"config": {
"symbols": [
"XRPUSDT"
],
"start": null,
"end": null,
"initial_balance": 1000.0,
"leverage": 10,
"fee_pct": 0.04,
"slippage_pct": 0.01,
"use_ml": false,
"ml_threshold": 0.55,
"max_daily_loss_pct": 0.05,
"max_positions": 3,
"max_same_direction": 2,
"margin_max_ratio": 0.5,
"margin_min_ratio": 0.2,
"margin_decay_rate": 0.0006,
"atr_sl_mult": 2.0,
"atr_tp_mult": 2.0,
"min_notional": 5.0,
"signal_threshold": 3,
"adx_threshold": 25.0,
"volume_multiplier": 2.5,
"train_months": 3,
"test_months": 1,
"time_weight_decay": 2.0,
"negative_ratio": 5
},
"summary": {
"total_trades": 27,
"total_pnl": 217.0703,
"return_pct": 21.71,
"win_rate": 66.67,
"avg_win": 33.2223,
"avg_loss": -42.3256,
"payoff_ratio": 0.78,
"max_consecutive_losses": 3,
"profit_factor": 1.57,
"max_drawdown_pct": 11.99,
"sharpe_ratio": 33.32,
"total_fees": 102.7825,
"close_reasons": {
"STOP_LOSS": 9,
"TAKE_PROFIT": 18
}
},
"folds": [
{
"fold": 1,
"train_period": "2025-03-05 ~ 2025-06-05",
"test_period": "2025-06-05 ~ 2025-07-05",
"summary": {
"total_trades": 9,
"total_pnl": -54.288,
"return_pct": -5.43,
"win_rate": 44.44,
"avg_win": 40.6662,
"avg_loss": -43.3906,
"payoff_ratio": 0.94,
"max_consecutive_losses": 3,
"profit_factor": 0.75,
"max_drawdown_pct": 11.99,
"sharpe_ratio": -21.46,
"total_fees": 32.8699,
"close_reasons": {
"STOP_LOSS": 5,
"TAKE_PROFIT": 4
}
}
},
{
"fold": 2,
"train_period": "2025-06-05 ~ 2025-09-05",
"test_period": "2025-09-05 ~ 2025-10-05",
"summary": {
"total_trades": 10,
"total_pnl": 13.0734,
"return_pct": 1.31,
"win_rate": 60.0,
"avg_win": 29.5084,
"avg_loss": -40.9943,
"payoff_ratio": 0.72,
"max_consecutive_losses": 3,
"profit_factor": 1.08,
"max_drawdown_pct": 10.97,
"sharpe_ratio": 5.62,
"total_fees": 39.3346,
"close_reasons": {
"TAKE_PROFIT": 6,
"STOP_LOSS": 4
}
}
},
{
"fold": 3,
"train_period": "2025-09-05 ~ 2025-12-05",
"test_period": "2025-12-05 ~ 2026-01-05",
"summary": {
"total_trades": 8,
"total_pnl": 258.2849,
"return_pct": 25.83,
"win_rate": 100.0,
"avg_win": 32.2856,
"avg_loss": 0.0,
"payoff_ratio": "Infinity",
"max_consecutive_losses": 0,
"profit_factor": "Infinity",
"max_drawdown_pct": 0.0,
"sharpe_ratio": 502.51,
"total_fees": 30.578,
"close_reasons": {
"TAKE_PROFIT": 8
}
}
}
],
"trades": [
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-06-12 11:15:00",
"exit_time": "2025-06-12 13:00:00",
"entry_price": 2.223978,
"exit_price": 2.237955,
"quantity": 2248.0,
"sl": 2.237955,
"tp": 2.21,
"gross_pnl": -31.420573,
"entry_fee": 1.999801,
"exit_fee": 2.012369,
"net_pnl": -35.432743,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 26.97199737318929,
"macd_hist": -0.0007807103280135859,
"atr": 0.006988561711463904,
"adx": 43.4578914382015
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-06-12 20:30:00",
"exit_time": "2025-06-13 00:00:00",
"entry_price": 2.186419,
"exit_price": 2.163885,
"quantity": 2201.5,
"sl": 2.163885,
"tp": 2.208952,
"gross_pnl": -49.607437,
"entry_fee": 1.92536,
"exit_fee": 1.905517,
"net_pnl": -53.438315,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 26.41519543415668,
"macd_hist": -0.005495027011351469,
"atr": 0.011266735681191995,
"adx": 35.41443548413551
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-06-16 22:30:00",
"exit_time": "2025-06-16 23:45:00",
"entry_price": 2.260826,
"exit_price": 2.231631,
"quantity": 2006.6,
"sl": 2.231631,
"tp": 2.290021,
"gross_pnl": -58.582699,
"entry_fee": 1.814629,
"exit_fee": 1.791196,
"net_pnl": -62.188525,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 33.211506920436555,
"macd_hist": -0.007666291215691772,
"atr": 0.014597503086660083,
"adx": 41.77057022158849
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-06-17 15:00:00",
"exit_time": "2025-06-17 15:30:00",
"entry_price": 2.188781,
"exit_price": 2.164935,
"quantity": 1926.0,
"sl": 2.212627,
"tp": 2.164935,
"gross_pnl": 45.926824,
"entry_fee": 1.686237,
"exit_fee": 1.667866,
"net_pnl": 42.572721,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 35.98442517376965,
"macd_hist": -0.000473160901783975,
"atr": 0.011922851577807921,
"adx": 31.230008994240638
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-06-21 13:30:00",
"exit_time": "2025-06-21 15:15:00",
"entry_price": 2.119112,
"exit_price": 2.109684,
"quantity": 2086.2,
"sl": 2.109684,
"tp": 2.128539,
"gross_pnl": -19.667423,
"entry_fee": 1.768356,
"exit_fee": 1.76049,
"net_pnl": -23.196269,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 29.460371663394117,
"macd_hist": -0.002291006577745399,
"atr": 0.0047136955379463,
"adx": 26.139853452702802
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-06-21 21:15:00",
"exit_time": "2025-06-21 21:30:00",
"entry_price": 2.045995,
"exit_price": 2.018384,
"quantity": 2099.3,
"sl": 2.073607,
"tp": 2.018384,
"gross_pnl": 57.964484,
"entry_fee": 1.718063,
"exit_fee": 1.694877,
"net_pnl": 54.551543,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 24.112041978961905,
"macd_hist": -0.0015821538372272313,
"atr": 0.013805669484335523,
"adx": 47.020225174544926
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-06-24 05:30:00",
"exit_time": "2025-06-24 06:00:00",
"entry_price": 2.184818,
"exit_price": 2.207587,
"quantity": 2087.2,
"sl": 2.16205,
"tp": 2.207587,
"gross_pnl": 47.522393,
"entry_fee": 1.824061,
"exit_fee": 1.84307,
"net_pnl": 43.855261,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 73.02163697288638,
"macd_hist": 0.0005479493365071683,
"atr": 0.011384245075129916,
"adx": 47.36536786932758
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-07-01 13:00:00",
"exit_time": "2025-07-01 14:15:00",
"entry_price": 2.185781,
"exit_price": 2.203594,
"quantity": 2182.0,
"sl": 2.203594,
"tp": 2.167969,
"gross_pnl": -38.866074,
"entry_fee": 1.90775,
"exit_fee": 1.923296,
"net_pnl": -42.697121,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 34.25494593254047,
"macd_hist": -0.00014675405719808375,
"atr": 0.008906066514248343,
"adx": 38.40722178323835
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-07-04 06:00:00",
"exit_time": "2025-07-04 07:30:00",
"entry_price": 2.232877,
"exit_price": 2.220445,
"quantity": 2036.1,
"sl": 2.245309,
"tp": 2.220445,
"gross_pnl": 25.312421,
"entry_fee": 1.818544,
"exit_fee": 1.808419,
"net_pnl": 21.685458,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 31.442919224174045,
"macd_hist": -0.00029321477558042104,
"atr": 0.0062159081353788895,
"adx": 33.56850119916028
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-05 19:45:00",
"exit_time": "2025-09-05 21:00:00",
"entry_price": 2.863514,
"exit_price": 2.839468,
"quantity": 1745.9,
"sl": 2.887559,
"tp": 2.839468,
"gross_pnl": 41.980567,
"entry_fee": 1.999763,
"exit_fee": 1.982971,
"net_pnl": 37.997832,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 66.8667614741176,
"macd_hist": 0.005566660638547516,
"atr": 0.012022614930199287,
"adx": 25.794325095274626
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-09-07 07:30:00",
"exit_time": "2025-09-07 13:30:00",
"entry_price": 2.831483,
"exit_price": 2.839182,
"quantity": 1750.6,
"sl": 2.823784,
"tp": 2.839182,
"gross_pnl": 13.478415,
"entry_fee": 1.982718,
"exit_fee": 1.988109,
"net_pnl": 9.507588,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 70.72939012092385,
"macd_hist": 0.00013947818915852105,
"atr": 0.0038496557346203194,
"adx": 25.87514662158794
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-07 14:15:00",
"exit_time": "2025-09-07 17:45:00",
"entry_price": 2.888611,
"exit_price": 2.863658,
"quantity": 1711.8,
"sl": 2.913564,
"tp": 2.863658,
"gross_pnl": 42.714942,
"entry_fee": 1.97789,
"exit_fee": 1.960804,
"net_pnl": 38.776248,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 76.27147788789821,
"macd_hist": 0.006331113894477991,
"atr": 0.012476615713124774,
"adx": 29.135371839765913
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-09 07:00:00",
"exit_time": "2025-09-09 08:30:00",
"entry_price": 3.009099,
"exit_price": 3.032748,
"quantity": 1621.9,
"sl": 3.032748,
"tp": 2.98545,
"gross_pnl": -38.356788,
"entry_fee": 1.952183,
"exit_fee": 1.967526,
"net_pnl": -42.276497,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 73.38194905544675,
"macd_hist": 0.006160466326798137,
"atr": 0.011824646391069867,
"adx": 28.105801823891394
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-12 20:45:00",
"exit_time": "2025-09-12 21:30:00",
"entry_price": 3.121788,
"exit_price": 3.09291,
"quantity": 1587.4,
"sl": 3.150665,
"tp": 3.09291,
"gross_pnl": 45.839824,
"entry_fee": 1.98221,
"exit_fee": 1.963874,
"net_pnl": 41.893739,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 78.88995114530964,
"macd_hist": 0.004793783646225545,
"atr": 0.014438649390670705,
"adx": 33.848649235778474
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-14 11:15:00",
"exit_time": "2025-09-14 12:30:00",
"entry_price": 3.066993,
"exit_price": 3.050039,
"quantity": 1594.0,
"sl": 3.083947,
"tp": 3.050039,
"gross_pnl": 27.024629,
"entry_fee": 1.955515,
"exit_fee": 1.944705,
"net_pnl": 23.124409,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 31.419480021379645,
"macd_hist": -0.0003629091436070245,
"atr": 0.008476985221720718,
"adx": 31.882046477112183
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-20 14:00:00",
"exit_time": "2025-09-20 15:00:00",
"entry_price": 2.971603,
"exit_price": 2.984114,
"quantity": 1630.7,
"sl": 2.984114,
"tp": 2.959092,
"gross_pnl": -20.401941,
"entry_fee": 1.938317,
"exit_fee": 1.946478,
"net_pnl": -24.286736,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 36.09553473183674,
"macd_hist": -0.00018720159437711752,
"atr": 0.006255577606168083,
"adx": 28.84076206692945
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-09-24 19:45:00",
"exit_time": "2025-09-24 21:45:00",
"entry_price": 2.976198,
"exit_price": 2.948913,
"quantity": 1646.2,
"sl": 2.948913,
"tp": 3.003482,
"gross_pnl": -44.915196,
"entry_fee": 1.959767,
"exit_fee": 1.941801,
"net_pnl": -48.816763,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 77.25608496751632,
"macd_hist": 0.00019635721897620986,
"atr": 0.013642083686890932,
"adx": 66.36435142210216
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-25 12:15:00",
"exit_time": "2025-09-25 12:30:00",
"entry_price": 2.793221,
"exit_price": 2.81825,
"quantity": 1781.8,
"sl": 2.81825,
"tp": 2.768191,
"gross_pnl": -44.597807,
"entry_fee": 1.990784,
"exit_fee": 2.008623,
"net_pnl": -48.597214,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 27.256149561284357,
"macd_hist": -0.0005773233003328118,
"atr": 0.012514818420656685,
"adx": 36.706433983392714
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-09-28 22:00:00",
"exit_time": "2025-09-29 02:15:00",
"entry_price": 2.850785,
"exit_price": 2.868214,
"quantity": 1700.7,
"sl": 2.833356,
"tp": 2.868214,
"gross_pnl": 29.641341,
"entry_fee": 1.939332,
"exit_fee": 1.951189,
"net_pnl": 25.750821,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 69.94607598871028,
"macd_hist": 0.0002172037763433672,
"atr": 0.008714453238038499,
"adx": 45.96715774504039
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-12-10 10:30:00",
"exit_time": "2025-12-10 14:30:00",
"entry_price": 2.069993,
"exit_price": 2.056326,
"quantity": 2415.2,
"sl": 2.08366,
"tp": 2.056326,
"gross_pnl": 33.009596,
"entry_fee": 1.999779,
"exit_fee": 1.986575,
"net_pnl": 29.023242,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 37.038012691554805,
"macd_hist": -9.211636302669133e-05,
"atr": 0.0068337189344636114,
"adx": 28.943927763667713
},
"fold": 3
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-12-12 16:00:00",
"exit_time": "2025-12-12 18:30:00",
"entry_price": 1.988899,
"exit_price": 2.013001,
"quantity": 2498.4,
"sl": 1.964797,
"tp": 2.013001,
"gross_pnl": 60.216838,
"entry_fee": 1.987626,
"exit_fee": 2.011713,
"net_pnl": 56.2175,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 28.182013495720106,
"macd_hist": -0.00643391832048344,
"atr": 0.01205108033747697,
"adx": 30.769786891839054
},
"fold": 3
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-12-19 13:15:00",
"exit_time": "2025-12-19 13:45:00",
"entry_price": 1.878712,
"exit_price": 1.863781,
"quantity": 2596.8,
"sl": 1.893643,
"tp": 1.863781,
"gross_pnl": 38.773597,
"entry_fee": 1.951456,
"exit_fee": 1.935946,
"net_pnl": 34.886195,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 68.16547032772114,
"macd_hist": -4.5929936914913816e-05,
"atr": 0.007465649526915487,
"adx": 40.69667585881617
},
"fold": 3
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-12-25 23:30:00",
"exit_time": "2025-12-26 02:00:00",
"entry_price": 1.831783,
"exit_price": 1.844712,
"quantity": 2624.8,
"sl": 1.818854,
"tp": 1.844712,
"gross_pnl": 33.936201,
"entry_fee": 1.923226,
"exit_fee": 1.9368,
"net_pnl": 30.076175,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 18.688435627302994,
"macd_hist": -0.0034657628634239823,
"atr": 0.006464530874639477,
"adx": 30.228290924248867
},
"fold": 3
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2026-01-01 15:45:00",
"exit_time": "2026-01-01 17:15:00",
"entry_price": 1.861986,
"exit_price": 1.870892,
"quantity": 2543.8,
"sl": 1.853081,
"tp": 1.870892,
"gross_pnl": 22.653457,
"entry_fee": 1.894608,
"exit_fee": 1.90367,
"net_pnl": 18.855179,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 64.9515011671277,
"macd_hist": 8.017825296896758e-05,
"atr": 0.004452680396625609,
"adx": 29.061543249865803
},
"fold": 3
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2026-01-02 14:30:00",
"exit_time": "2026-01-02 14:45:00",
"entry_price": 1.906991,
"exit_price": 1.92129,
"quantity": 2458.8,
"sl": 1.892691,
"tp": 1.92129,
"gross_pnl": 35.159553,
"entry_fee": 1.875563,
"exit_fee": 1.889627,
"net_pnl": 31.394362,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 68.243618769103,
"macd_hist": 0.00021763021087121363,
"atr": 0.007149738287103824,
"adx": 34.75978288472445
},
"fold": 3
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2026-01-04 00:45:00",
"exit_time": "2026-01-04 01:30:00",
"entry_price": 2.041396,
"exit_price": 2.026932,
"quantity": 2251.8,
"sl": 2.05586,
"tp": 2.026932,
"gross_pnl": 32.569888,
"entry_fee": 1.838726,
"exit_fee": 1.825698,
"net_pnl": 28.905464,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 78.42031027026397,
"macd_hist": 0.002773445174521872,
"atr": 0.007231967338050755,
"adx": 25.06146716762975
},
"fold": 3
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2026-01-04 03:00:00",
"exit_time": "2026-01-04 03:45:00",
"entry_price": 2.038904,
"exit_price": 2.053633,
"quantity": 2209.5,
"sl": 2.024175,
"tp": 2.053633,
"gross_pnl": 32.54375,
"entry_fee": 1.801983,
"exit_fee": 1.815001,
"net_pnl": 28.926766,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 66.00101725582081,
"macd_hist": 4.503977849404806e-05,
"atr": 0.007364505506906315,
"adx": 30.991168735172053
},
"fold": 3
}
],
"validation": {
"overall": "FAIL",
"checks": [
{
"name": "exit_after_entry",
"passed": true,
"level": "FAIL",
"message": "모든 트레이드에서 청산 > 진입"
},
{
"name": "sl_tp_direction",
"passed": true,
"level": "FAIL",
"message": "SL/TP 방향 정합"
},
{
"name": "no_overlap",
"passed": true,
"level": "FAIL",
"message": "포지션 비중첩 확인"
},
{
"name": "positive_fees",
"passed": true,
"level": "FAIL",
"message": "수수료 양수 확인"
},
{
"name": "no_negative_balance",
"passed": true,
"level": "FAIL",
"message": "잔고 양수 유지"
},
{
"name": "win_rate_high",
"passed": true,
"level": "WARNING",
"message": "승률 정상 (66.7%)"
},
{
"name": "win_rate_low",
"passed": true,
"level": "WARNING",
"message": "승률 정상 (66.7%)"
},
{
"name": "mdd_nonzero",
"passed": true,
"level": "WARNING",
"message": "MDD 정상 (12.0%)"
},
{
"name": "trade_frequency",
"passed": false,
"level": "WARNING",
"message": "월 평균 4.0건 < 5건 — 신호 생성 부족"
},
{
"name": "profit_factor_high",
"passed": true,
"level": "WARNING",
"message": "PF 정상 (1.57)"
}
]
}
}

View File

@@ -0,0 +1,214 @@
{
"mode": "walk_forward",
"config": {
"symbols": [
"XRPUSDT"
],
"start": null,
"end": null,
"initial_balance": 1000.0,
"leverage": 10,
"fee_pct": 0.04,
"slippage_pct": 0.01,
"use_ml": true,
"ml_threshold": 0.55,
"max_daily_loss_pct": 0.05,
"max_positions": 3,
"max_same_direction": 2,
"margin_max_ratio": 0.5,
"margin_min_ratio": 0.2,
"margin_decay_rate": 0.0006,
"atr_sl_mult": 1.5,
"atr_tp_mult": 3.0,
"min_notional": 5.0,
"signal_threshold": 3,
"adx_threshold": 0,
"volume_multiplier": 1.5,
"train_months": 6,
"test_months": 1,
"time_weight_decay": 2.0,
"negative_ratio": 3
},
"summary": {
"total_trades": 3,
"total_pnl": -5.1514,
"return_pct": -0.52,
"win_rate": 33.33,
"avg_win": 66.3376,
"avg_loss": -35.7445,
"payoff_ratio": 1.86,
"max_consecutive_losses": 2,
"profit_factor": 0.93,
"max_drawdown_pct": 3.76,
"sharpe_ratio": -6.68,
"total_fees": 11.5694,
"close_reasons": {
"STOP_LOSS": 2,
"TAKE_PROFIT": 1
}
},
"folds": [
{
"fold": 1,
"train_period": "2025-03-05 ~ 2025-09-05",
"test_period": "2025-09-05 ~ 2025-10-05",
"summary": {
"total_trades": 3,
"total_pnl": -5.1514,
"return_pct": -0.52,
"win_rate": 33.33,
"avg_win": 66.3376,
"avg_loss": -35.7445,
"payoff_ratio": 1.86,
"max_consecutive_losses": 2,
"profit_factor": 0.93,
"max_drawdown_pct": 3.76,
"sharpe_ratio": -6.68,
"total_fees": 11.5694,
"close_reasons": {
"STOP_LOSS": 2,
"TAKE_PROFIT": 1
}
}
}
],
"trades": [
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-24 03:15:00",
"exit_time": "2025-09-24 04:30:00",
"entry_price": 2.811019,
"exit_price": 2.828555,
"quantity": 1778.5,
"sl": 2.828555,
"tp": 2.775947,
"gross_pnl": -31.18734,
"entry_fee": 1.999759,
"exit_fee": 2.012234,
"net_pnl": -35.199332,
"close_reason": "STOP_LOSS",
"ml_proba": 0.5782,
"indicators": {
"rsi": 35.41445012242145,
"macd_hist": -0.0005539721484064007,
"atr": 0.011690503070987813,
"adx": 22.830095368917625
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-25 12:15:00",
"exit_time": "2025-09-25 12:30:00",
"entry_price": 2.793221,
"exit_price": 2.811993,
"quantity": 1726.9,
"sl": 2.811993,
"tp": 2.755676,
"gross_pnl": -32.41776,
"entry_fee": 1.929445,
"exit_fee": 1.942412,
"net_pnl": -36.289617,
"close_reason": "STOP_LOSS",
"ml_proba": 0.724,
"indicators": {
"rsi": 27.256149561284357,
"macd_hist": -0.0005773233003328118,
"atr": 0.012514818420656685,
"adx": 36.706433983392714
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-26 09:30:00",
"exit_time": "2025-09-26 11:00:00",
"entry_price": 2.742026,
"exit_price": 2.700663,
"quantity": 1692.9,
"sl": 2.762707,
"tp": 2.700663,
"gross_pnl": 70.023157,
"entry_fee": 1.85679,
"exit_fee": 1.828781,
"net_pnl": 66.337586,
"close_reason": "TAKE_PROFIT",
"ml_proba": 0.8238,
"indicators": {
"rsi": 42.61073221775071,
"macd_hist": -0.0004772649982219266,
"atr": 0.013787614360953772,
"adx": 15.435644479956943
},
"fold": 1
}
],
"validation": {
"overall": "FAIL",
"checks": [
{
"name": "exit_after_entry",
"passed": true,
"level": "FAIL",
"message": "모든 트레이드에서 청산 > 진입"
},
{
"name": "sl_tp_direction",
"passed": true,
"level": "FAIL",
"message": "SL/TP 방향 정합"
},
{
"name": "no_overlap",
"passed": true,
"level": "FAIL",
"message": "포지션 비중첩 확인"
},
{
"name": "positive_fees",
"passed": true,
"level": "FAIL",
"message": "수수료 양수 확인"
},
{
"name": "no_negative_balance",
"passed": true,
"level": "FAIL",
"message": "잔고 양수 유지"
},
{
"name": "win_rate_high",
"passed": true,
"level": "WARNING",
"message": "승률 정상 (33.3%)"
},
{
"name": "win_rate_low",
"passed": true,
"level": "WARNING",
"message": "승률 정상 (33.3%)"
},
{
"name": "mdd_nonzero",
"passed": true,
"level": "WARNING",
"message": "MDD 정상 (3.8%)"
},
{
"name": "trade_frequency",
"passed": false,
"level": "WARNING",
"message": "월 평균 3.0건 < 5건 — 신호 생성 부족"
},
{
"name": "profit_factor_high",
"passed": true,
"level": "WARNING",
"message": "PF 정상 (0.93)"
}
]
}
}

View File

@@ -0,0 +1,382 @@
{
"mode": "walk_forward",
"config": {
"symbols": [
"XRPUSDT"
],
"start": null,
"end": null,
"initial_balance": 1000.0,
"leverage": 20,
"fee_pct": 0.04,
"slippage_pct": 0.01,
"use_ml": false,
"ml_threshold": 0.55,
"max_daily_loss_pct": 0.05,
"max_positions": 3,
"max_same_direction": 2,
"margin_max_ratio": 0.5,
"margin_min_ratio": 0.2,
"margin_decay_rate": 0.0006,
"atr_sl_mult": 1.5,
"atr_tp_mult": 4.0,
"min_notional": 5.0,
"signal_threshold": 3,
"adx_threshold": 25.0,
"volume_multiplier": 2.5,
"train_months": 6,
"test_months": 1,
"time_weight_decay": 2.0,
"negative_ratio": 3
},
"summary": {
"total_trades": 10,
"total_pnl": -71.4118,
"return_pct": -7.14,
"win_rate": 30.0,
"avg_win": 116.5629,
"avg_loss": -60.1572,
"payoff_ratio": 1.94,
"max_consecutive_losses": 4,
"profit_factor": 0.83,
"max_drawdown_pct": 28.16,
"sharpe_ratio": -15.9,
"total_fees": 75.9924,
"close_reasons": {
"TAKE_PROFIT": 3,
"STOP_LOSS": 7
}
},
"folds": [
{
"fold": 1,
"train_period": "2025-03-05 ~ 2025-09-05",
"test_period": "2025-09-05 ~ 2025-10-05",
"summary": {
"total_trades": 10,
"total_pnl": -71.4118,
"return_pct": -7.14,
"win_rate": 30.0,
"avg_win": 116.5629,
"avg_loss": -60.1572,
"payoff_ratio": 1.94,
"max_consecutive_losses": 4,
"profit_factor": 0.83,
"max_drawdown_pct": 28.16,
"sharpe_ratio": -15.9,
"total_fees": 75.9924,
"close_reasons": {
"TAKE_PROFIT": 3,
"STOP_LOSS": 7
}
}
}
],
"trades": [
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-05 19:45:00",
"exit_time": "2025-09-05 22:15:00",
"entry_price": 2.863514,
"exit_price": 2.815423,
"quantity": 3491.9,
"sl": 2.881548,
"tp": 2.815423,
"gross_pnl": 167.927076,
"entry_fee": 3.999641,
"exit_fee": 3.93247,
"net_pnl": 159.994965,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 66.8667614741176,
"macd_hist": 0.005566660638547516,
"atr": 0.012022614930199287,
"adx": 25.794325095274626
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-09-07 07:30:00",
"exit_time": "2025-09-07 10:15:00",
"entry_price": 2.831483,
"exit_price": 2.825709,
"quantity": 3310.6,
"sl": 2.825709,
"tp": 2.846882,
"gross_pnl": -19.117005,
"entry_fee": 3.749563,
"exit_fee": 3.741916,
"net_pnl": -26.608485,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 70.72939012092385,
"macd_hist": 0.00013947818915852105,
"atr": 0.0038496557346203194,
"adx": 25.87514662158794
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-07 14:15:00",
"exit_time": "2025-09-07 14:30:00",
"entry_price": 2.888611,
"exit_price": 2.907326,
"quantity": 3295.3,
"sl": 2.907326,
"tp": 2.838705,
"gross_pnl": -61.671288,
"entry_fee": 3.807536,
"exit_fee": 3.832205,
"net_pnl": -69.311028,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 76.27147788789821,
"macd_hist": 0.006331113894477991,
"atr": 0.012476615713124774,
"adx": 29.135371839765913
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-09 07:00:00",
"exit_time": "2025-09-09 07:15:00",
"entry_price": 3.009099,
"exit_price": 3.026836,
"quantity": 3264.0,
"sl": 3.026836,
"tp": 2.9618,
"gross_pnl": -57.893469,
"entry_fee": 3.92868,
"exit_fee": 3.951837,
"net_pnl": -65.773986,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 73.38194905544675,
"macd_hist": 0.006160466326798137,
"atr": 0.011824646391069867,
"adx": 28.105801823891394
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-12 20:45:00",
"exit_time": "2025-09-13 08:45:00",
"entry_price": 3.121788,
"exit_price": 3.143446,
"quantity": 3197.5,
"sl": 3.143446,
"tp": 3.064033,
"gross_pnl": -69.251372,
"entry_fee": 3.992767,
"exit_fee": 4.020467,
"net_pnl": -77.264606,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 78.88995114530964,
"macd_hist": 0.004793783646225545,
"atr": 0.014438649390670705,
"adx": 33.848649235778474
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-14 11:15:00",
"exit_time": "2025-09-14 13:45:00",
"entry_price": 3.066993,
"exit_price": 3.033085,
"quantity": 3002.8,
"sl": 3.079709,
"tp": 3.033085,
"gross_pnl": 101.818765,
"entry_fee": 3.683827,
"exit_fee": 3.643099,
"net_pnl": 94.491838,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 31.419480021379645,
"macd_hist": -0.0003629091436070245,
"atr": 0.008476985221720718,
"adx": 31.882046477112183
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-20 14:00:00",
"exit_time": "2025-09-20 14:45:00",
"entry_price": 2.971603,
"exit_price": 2.980986,
"quantity": 3353.4,
"sl": 2.980986,
"tp": 2.94658,
"gross_pnl": -31.466181,
"entry_fee": 3.985989,
"exit_fee": 3.998576,
"net_pnl": -39.450746,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 36.09553473183674,
"macd_hist": -0.00018720159437711752,
"atr": 0.006255577606168083,
"adx": 28.84076206692945
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-09-24 19:45:00",
"exit_time": "2025-09-24 20:30:00",
"entry_price": 2.976198,
"exit_price": 2.955734,
"quantity": 3279.9,
"sl": 2.955734,
"tp": 3.030766,
"gross_pnl": -67.117005,
"entry_fee": 3.904652,
"exit_fee": 3.877805,
"net_pnl": -74.899463,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 77.25608496751632,
"macd_hist": 0.00019635721897620986,
"atr": 0.013642083686890932,
"adx": 66.36435142210216
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-25 12:15:00",
"exit_time": "2025-09-25 12:30:00",
"entry_price": 2.793221,
"exit_price": 2.811993,
"quantity": 3226.0,
"sl": 2.811993,
"tp": 2.743161,
"gross_pnl": -60.559206,
"entry_fee": 3.604372,
"exit_fee": 3.628596,
"net_pnl": -67.792174,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 27.256149561284357,
"macd_hist": -0.0005773233003328118,
"atr": 0.012514818420656685,
"adx": 36.706433983392714
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-09-28 22:00:00",
"exit_time": "2025-09-29 07:15:00",
"entry_price": 2.850785,
"exit_price": 2.885643,
"quantity": 2923.6,
"sl": 2.837713,
"tp": 2.885643,
"gross_pnl": 101.910302,
"entry_fee": 3.333822,
"exit_fee": 3.374586,
"net_pnl": 95.201894,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 69.94607598871028,
"macd_hist": 0.0002172037763433672,
"atr": 0.008714453238038499,
"adx": 45.96715774504039
},
"fold": 1
}
],
"validation": {
"overall": "PASS",
"checks": [
{
"name": "exit_after_entry",
"passed": true,
"level": "FAIL",
"message": "모든 트레이드에서 청산 > 진입"
},
{
"name": "sl_tp_direction",
"passed": true,
"level": "FAIL",
"message": "SL/TP 방향 정합"
},
{
"name": "no_overlap",
"passed": true,
"level": "FAIL",
"message": "포지션 비중첩 확인"
},
{
"name": "positive_fees",
"passed": true,
"level": "FAIL",
"message": "수수료 양수 확인"
},
{
"name": "no_negative_balance",
"passed": true,
"level": "FAIL",
"message": "잔고 양수 유지"
},
{
"name": "win_rate_high",
"passed": true,
"level": "WARNING",
"message": "승률 정상 (30.0%)"
},
{
"name": "win_rate_low",
"passed": true,
"level": "WARNING",
"message": "승률 정상 (30.0%)"
},
{
"name": "mdd_nonzero",
"passed": true,
"level": "WARNING",
"message": "MDD 정상 (28.2%)"
},
{
"name": "trade_frequency",
"passed": true,
"level": "WARNING",
"message": "월 평균 10.0건"
},
{
"name": "profit_factor_high",
"passed": true,
"level": "WARNING",
"message": "PF 정상 (0.83)"
}
]
}
}

View File

@@ -0,0 +1,838 @@
{
"mode": "walk_forward",
"config": {
"symbols": [
"XRPUSDT"
],
"start": null,
"end": null,
"initial_balance": 1000.0,
"leverage": 20,
"fee_pct": 0.04,
"slippage_pct": 0.01,
"use_ml": false,
"ml_threshold": 0.55,
"max_daily_loss_pct": 0.05,
"max_positions": 3,
"max_same_direction": 2,
"margin_max_ratio": 0.5,
"margin_min_ratio": 0.2,
"margin_decay_rate": 0.0006,
"atr_sl_mult": 1.5,
"atr_tp_mult": 4.0,
"min_notional": 5.0,
"signal_threshold": 3,
"adx_threshold": 25.0,
"volume_multiplier": 2.5,
"train_months": 3,
"test_months": 1,
"time_weight_decay": 2.0,
"negative_ratio": 3
},
"summary": {
"total_trades": 27,
"total_pnl": 817.0717,
"return_pct": 81.71,
"win_rate": 44.44,
"avg_win": 140.6556,
"avg_loss": -58.053,
"payoff_ratio": 2.42,
"max_consecutive_losses": 4,
"profit_factor": 1.94,
"max_drawdown_pct": 20.23,
"sharpe_ratio": 51.77,
"total_fees": 195.3719,
"close_reasons": {
"STOP_LOSS": 15,
"TAKE_PROFIT": 11,
"REVERSE_SIGNAL": 1
}
},
"folds": [
{
"fold": 1,
"train_period": "2025-03-05 ~ 2025-06-05",
"test_period": "2025-06-05 ~ 2025-07-05",
"summary": {
"total_trades": 9,
"total_pnl": 454.4106,
"return_pct": 45.44,
"win_rate": 44.44,
"avg_win": 186.0558,
"avg_loss": -57.9625,
"payoff_ratio": 3.21,
"max_consecutive_losses": 2,
"profit_factor": 2.57,
"max_drawdown_pct": 8.26,
"sharpe_ratio": 73.09,
"total_fees": 60.8765,
"close_reasons": {
"STOP_LOSS": 5,
"TAKE_PROFIT": 4
}
}
},
{
"fold": 2,
"train_period": "2025-06-05 ~ 2025-09-05",
"test_period": "2025-09-05 ~ 2025-10-05",
"summary": {
"total_trades": 10,
"total_pnl": -71.4118,
"return_pct": -7.14,
"win_rate": 30.0,
"avg_win": 116.5629,
"avg_loss": -60.1572,
"payoff_ratio": 1.94,
"max_consecutive_losses": 4,
"profit_factor": 0.83,
"max_drawdown_pct": 28.16,
"sharpe_ratio": -15.9,
"total_fees": 75.9924,
"close_reasons": {
"TAKE_PROFIT": 3,
"STOP_LOSS": 7
}
}
},
{
"fold": 3,
"train_period": "2025-09-05 ~ 2025-12-05",
"test_period": "2025-12-05 ~ 2026-01-05",
"summary": {
"total_trades": 8,
"total_pnl": 434.073,
"return_pct": 43.41,
"win_rate": 62.5,
"avg_win": 118.7911,
"avg_loss": -53.2941,
"payoff_ratio": 2.23,
"max_consecutive_losses": 1,
"profit_factor": 3.71,
"max_drawdown_pct": 5.44,
"sharpe_ratio": 101.65,
"total_fees": 58.503,
"close_reasons": {
"STOP_LOSS": 3,
"TAKE_PROFIT": 4,
"REVERSE_SIGNAL": 1
}
}
}
],
"trades": [
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-06-12 11:15:00",
"exit_time": "2025-06-12 12:30:00",
"entry_price": 2.223978,
"exit_price": 2.23446,
"quantity": 4496.0,
"sl": 2.23446,
"tp": 2.196023,
"gross_pnl": -47.13086,
"entry_fee": 3.999601,
"exit_fee": 4.018454,
"net_pnl": -55.148915,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 26.97199737318929,
"macd_hist": -0.0007807103280135859,
"atr": 0.006988561711463904,
"adx": 43.4578914382015
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-06-13 00:00:00",
"exit_time": "2025-06-13 00:30:00",
"entry_price": 2.149685,
"exit_price": 2.095237,
"quantity": 4394.9,
"sl": 2.170103,
"tp": 2.095237,
"gross_pnl": 239.292043,
"entry_fee": 3.77906,
"exit_fee": 3.683343,
"net_pnl": 231.829639,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 26.809985061770455,
"macd_hist": -0.0014287229374708253,
"atr": 0.01361191626294587,
"adx": 45.994286262673526
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-06-16 22:30:00",
"exit_time": "2025-06-16 23:45:00",
"entry_price": 2.260826,
"exit_price": 2.23893,
"quantity": 4101.6,
"sl": 2.23893,
"tp": 2.319216,
"gross_pnl": -89.809678,
"entry_fee": 3.709202,
"exit_fee": 3.673278,
"net_pnl": -97.192157,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 33.211506920436555,
"macd_hist": -0.007666291215691772,
"atr": 0.014597503086660083,
"adx": 41.77057022158849
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-06-17 15:00:00",
"exit_time": "2025-06-17 17:00:00",
"entry_price": 2.188781,
"exit_price": 2.14109,
"quantity": 4461.0,
"sl": 2.206665,
"tp": 2.14109,
"gross_pnl": 212.751364,
"entry_fee": 3.905661,
"exit_fee": 3.82056,
"net_pnl": 205.025142,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 35.98442517376965,
"macd_hist": -0.000473160901783975,
"atr": 0.011922851577807921,
"adx": 31.230008994240638
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-06-21 13:30:00",
"exit_time": "2025-06-21 14:00:00",
"entry_price": 2.119112,
"exit_price": 2.112041,
"quantity": 3992.4,
"sl": 2.112041,
"tp": 2.137967,
"gross_pnl": -28.228437,
"entry_fee": 3.384137,
"exit_fee": 3.372846,
"net_pnl": -34.98542,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 29.460371663394117,
"macd_hist": -0.002291006577745399,
"atr": 0.0047136955379463,
"adx": 26.139853452702802
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-06-21 21:15:00",
"exit_time": "2025-06-21 21:30:00",
"entry_price": 2.045995,
"exit_price": 1.990773,
"quantity": 4278.1,
"sl": 2.066704,
"tp": 1.990773,
"gross_pnl": 236.248138,
"entry_fee": 3.501189,
"exit_fee": 3.40669,
"net_pnl": 229.340259,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 24.112041978961905,
"macd_hist": -0.0015821538372272313,
"atr": 0.013805669484335523,
"adx": 47.020225174544926
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-06-24 05:30:00",
"exit_time": "2025-06-24 08:00:00",
"entry_price": 2.184818,
"exit_price": 2.167742,
"quantity": 2879.5,
"sl": 2.167742,
"tp": 2.230355,
"gross_pnl": -49.171401,
"entry_fee": 2.516474,
"exit_fee": 2.496805,
"net_pnl": -54.18468,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 73.02163697288638,
"macd_hist": 0.0005479493365071683,
"atr": 0.011384245075129916,
"adx": 47.36536786932758
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-07-01 13:00:00",
"exit_time": "2025-07-01 14:00:00",
"entry_price": 2.185781,
"exit_price": 2.19914,
"quantity": 3196.0,
"sl": 2.19914,
"tp": 2.150157,
"gross_pnl": -42.695683,
"entry_fee": 2.794303,
"exit_fee": 2.811381,
"net_pnl": -48.301367,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 34.25494593254047,
"macd_hist": -0.00014675405719808375,
"atr": 0.008906066514248343,
"adx": 38.40722178323835
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-07-04 06:00:00",
"exit_time": "2025-07-04 08:15:00",
"entry_price": 2.232877,
"exit_price": 2.208013,
"quantity": 3379.7,
"sl": 2.242201,
"tp": 2.208013,
"gross_pnl": 84.031619,
"entry_fee": 3.018581,
"exit_fee": 2.984969,
"net_pnl": 78.028069,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 31.442919224174045,
"macd_hist": -0.00029321477558042104,
"atr": 0.0062159081353788895,
"adx": 33.56850119916028
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-05 19:45:00",
"exit_time": "2025-09-05 22:15:00",
"entry_price": 2.863514,
"exit_price": 2.815423,
"quantity": 3491.9,
"sl": 2.881548,
"tp": 2.815423,
"gross_pnl": 167.927076,
"entry_fee": 3.999641,
"exit_fee": 3.93247,
"net_pnl": 159.994965,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 66.8667614741176,
"macd_hist": 0.005566660638547516,
"atr": 0.012022614930199287,
"adx": 25.794325095274626
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-09-07 07:30:00",
"exit_time": "2025-09-07 10:15:00",
"entry_price": 2.831483,
"exit_price": 2.825709,
"quantity": 3310.6,
"sl": 2.825709,
"tp": 2.846882,
"gross_pnl": -19.117005,
"entry_fee": 3.749563,
"exit_fee": 3.741916,
"net_pnl": -26.608485,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 70.72939012092385,
"macd_hist": 0.00013947818915852105,
"atr": 0.0038496557346203194,
"adx": 25.87514662158794
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-07 14:15:00",
"exit_time": "2025-09-07 14:30:00",
"entry_price": 2.888611,
"exit_price": 2.907326,
"quantity": 3295.3,
"sl": 2.907326,
"tp": 2.838705,
"gross_pnl": -61.671288,
"entry_fee": 3.807536,
"exit_fee": 3.832205,
"net_pnl": -69.311028,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 76.27147788789821,
"macd_hist": 0.006331113894477991,
"atr": 0.012476615713124774,
"adx": 29.135371839765913
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-09 07:00:00",
"exit_time": "2025-09-09 07:15:00",
"entry_price": 3.009099,
"exit_price": 3.026836,
"quantity": 3264.0,
"sl": 3.026836,
"tp": 2.9618,
"gross_pnl": -57.893469,
"entry_fee": 3.92868,
"exit_fee": 3.951837,
"net_pnl": -65.773986,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 73.38194905544675,
"macd_hist": 0.006160466326798137,
"atr": 0.011824646391069867,
"adx": 28.105801823891394
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-12 20:45:00",
"exit_time": "2025-09-13 08:45:00",
"entry_price": 3.121788,
"exit_price": 3.143446,
"quantity": 3197.5,
"sl": 3.143446,
"tp": 3.064033,
"gross_pnl": -69.251372,
"entry_fee": 3.992767,
"exit_fee": 4.020467,
"net_pnl": -77.264606,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 78.88995114530964,
"macd_hist": 0.004793783646225545,
"atr": 0.014438649390670705,
"adx": 33.848649235778474
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-14 11:15:00",
"exit_time": "2025-09-14 13:45:00",
"entry_price": 3.066993,
"exit_price": 3.033085,
"quantity": 3002.8,
"sl": 3.079709,
"tp": 3.033085,
"gross_pnl": 101.818765,
"entry_fee": 3.683827,
"exit_fee": 3.643099,
"net_pnl": 94.491838,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 31.419480021379645,
"macd_hist": -0.0003629091436070245,
"atr": 0.008476985221720718,
"adx": 31.882046477112183
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-20 14:00:00",
"exit_time": "2025-09-20 14:45:00",
"entry_price": 2.971603,
"exit_price": 2.980986,
"quantity": 3353.4,
"sl": 2.980986,
"tp": 2.94658,
"gross_pnl": -31.466181,
"entry_fee": 3.985989,
"exit_fee": 3.998576,
"net_pnl": -39.450746,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 36.09553473183674,
"macd_hist": -0.00018720159437711752,
"atr": 0.006255577606168083,
"adx": 28.84076206692945
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-09-24 19:45:00",
"exit_time": "2025-09-24 20:30:00",
"entry_price": 2.976198,
"exit_price": 2.955734,
"quantity": 3279.9,
"sl": 2.955734,
"tp": 3.030766,
"gross_pnl": -67.117005,
"entry_fee": 3.904652,
"exit_fee": 3.877805,
"net_pnl": -74.899463,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 77.25608496751632,
"macd_hist": 0.00019635721897620986,
"atr": 0.013642083686890932,
"adx": 66.36435142210216
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-25 12:15:00",
"exit_time": "2025-09-25 12:30:00",
"entry_price": 2.793221,
"exit_price": 2.811993,
"quantity": 3226.0,
"sl": 2.811993,
"tp": 2.743161,
"gross_pnl": -60.559206,
"entry_fee": 3.604372,
"exit_fee": 3.628596,
"net_pnl": -67.792174,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 27.256149561284357,
"macd_hist": -0.0005773233003328118,
"atr": 0.012514818420656685,
"adx": 36.706433983392714
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-09-28 22:00:00",
"exit_time": "2025-09-29 07:15:00",
"entry_price": 2.850785,
"exit_price": 2.885643,
"quantity": 2923.6,
"sl": 2.837713,
"tp": 2.885643,
"gross_pnl": 101.910302,
"entry_fee": 3.333822,
"exit_fee": 3.374586,
"net_pnl": 95.201894,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 69.94607598871028,
"macd_hist": 0.0002172037763433672,
"atr": 0.008714453238038499,
"adx": 45.96715774504039
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-12-10 10:30:00",
"exit_time": "2025-12-10 18:30:00",
"entry_price": 2.069993,
"exit_price": 2.080244,
"quantity": 4830.5,
"sl": 2.080244,
"tp": 2.042658,
"gross_pnl": -49.515419,
"entry_fee": 3.99964,
"exit_fee": 4.019447,
"net_pnl": -57.534506,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 37.038012691554805,
"macd_hist": -9.211636302669133e-05,
"atr": 0.0068337189344636114,
"adx": 28.943927763667713
},
"fold": 3
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-12-12 16:00:00",
"exit_time": "2025-12-13 05:00:00",
"entry_price": 1.988899,
"exit_price": 2.037103,
"quantity": 4739.1,
"sl": 1.970822,
"tp": 2.037103,
"gross_pnl": 228.445099,
"entry_fee": 3.770236,
"exit_fee": 3.861614,
"net_pnl": 220.813249,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 28.182013495720106,
"macd_hist": -0.00643391832048344,
"atr": 0.01205108033747697,
"adx": 30.769786891839054
},
"fold": 3
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-12-19 13:15:00",
"exit_time": "2025-12-19 15:15:00",
"entry_price": 1.878712,
"exit_price": 1.889911,
"quantity": 4978.2,
"sl": 1.889911,
"tp": 1.84885,
"gross_pnl": -55.748245,
"entry_fee": 3.741042,
"exit_fee": 3.763341,
"net_pnl": -63.252628,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 68.16547032772114,
"macd_hist": -4.5929936914913816e-05,
"atr": 0.007465649526915487,
"adx": 40.69667585881617
},
"fold": 3
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-12-25 23:30:00",
"exit_time": "2025-12-26 02:15:00",
"entry_price": 1.831783,
"exit_price": 1.857641,
"quantity": 5284.9,
"sl": 1.822086,
"tp": 1.857641,
"gross_pnl": 136.657597,
"entry_fee": 3.872316,
"exit_fee": 3.926979,
"net_pnl": 128.858301,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 18.688435627302994,
"macd_hist": -0.0034657628634239823,
"atr": 0.006464530874639477,
"adx": 30.228290924248867
},
"fold": 3
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2026-01-01 15:45:00",
"exit_time": "2026-01-01 16:15:00",
"entry_price": 1.861986,
"exit_price": 1.855307,
"quantity": 4787.6,
"sl": 1.855307,
"tp": 1.879797,
"gross_pnl": -31.976479,
"entry_fee": 3.565778,
"exit_fee": 3.552987,
"net_pnl": -39.095244,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 64.9515011671277,
"macd_hist": 8.017825296896758e-05,
"atr": 0.004452680396625609,
"adx": 29.061543249865803
},
"fold": 3
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2026-01-02 14:30:00",
"exit_time": "2026-01-02 16:15:00",
"entry_price": 1.906991,
"exit_price": 1.93559,
"quantity": 4818.6,
"sl": 1.896266,
"tp": 1.93559,
"gross_pnl": 137.806916,
"entry_fee": 3.67561,
"exit_fee": 3.730733,
"net_pnl": 130.400573,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 68.243618769103,
"macd_hist": 0.00021763021087121363,
"atr": 0.007149738287103824,
"adx": 34.75978288472445
},
"fold": 3
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2026-01-04 00:45:00",
"exit_time": "2026-01-04 03:00:00",
"entry_price": 2.041396,
"exit_price": 2.038904,
"quantity": 3981.9,
"sl": 2.052244,
"tp": 2.012468,
"gross_pnl": 9.922775,
"entry_fee": 3.251454,
"exit_fee": 3.247485,
"net_pnl": 3.423837,
"close_reason": "REVERSE_SIGNAL",
"ml_proba": null,
"indicators": {
"rsi": 78.42031027026397,
"macd_hist": 0.002773445174521872,
"atr": 0.007231967338050755,
"adx": 25.06146716762975
},
"fold": 3
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2026-01-04 03:00:00",
"exit_time": "2026-01-04 05:15:00",
"entry_price": 2.038904,
"exit_price": 2.068362,
"quantity": 3971.2,
"sl": 2.027857,
"tp": 2.068362,
"gross_pnl": 116.983697,
"entry_fee": 3.238758,
"exit_fee": 3.285551,
"net_pnl": 110.459388,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 66.00101725582081,
"macd_hist": 4.503977849404806e-05,
"atr": 0.007364505506906315,
"adx": 30.991168735172053
},
"fold": 3
}
],
"validation": {
"overall": "FAIL",
"checks": [
{
"name": "exit_after_entry",
"passed": true,
"level": "FAIL",
"message": "모든 트레이드에서 청산 > 진입"
},
{
"name": "sl_tp_direction",
"passed": true,
"level": "FAIL",
"message": "SL/TP 방향 정합"
},
{
"name": "no_overlap",
"passed": true,
"level": "FAIL",
"message": "포지션 비중첩 확인"
},
{
"name": "positive_fees",
"passed": true,
"level": "FAIL",
"message": "수수료 양수 확인"
},
{
"name": "no_negative_balance",
"passed": true,
"level": "FAIL",
"message": "잔고 양수 유지"
},
{
"name": "win_rate_high",
"passed": true,
"level": "WARNING",
"message": "승률 정상 (44.4%)"
},
{
"name": "win_rate_low",
"passed": true,
"level": "WARNING",
"message": "승률 정상 (44.4%)"
},
{
"name": "mdd_nonzero",
"passed": true,
"level": "WARNING",
"message": "MDD 정상 (20.2%)"
},
{
"name": "trade_frequency",
"passed": false,
"level": "WARNING",
"message": "월 평균 4.0건 < 5건 — 신호 생성 부족"
},
{
"name": "profit_factor_high",
"passed": true,
"level": "WARNING",
"message": "PF 정상 (1.94)"
}
]
}
}

View File

@@ -0,0 +1,511 @@
{
"mode": "walk_forward",
"config": {
"symbols": [
"XRPUSDT"
],
"start": null,
"end": null,
"initial_balance": 1000.0,
"leverage": 10,
"fee_pct": 0.04,
"slippage_pct": 0.01,
"use_ml": false,
"ml_threshold": 0.55,
"max_daily_loss_pct": 0.05,
"max_positions": 3,
"max_same_direction": 2,
"margin_max_ratio": 0.5,
"margin_min_ratio": 0.2,
"margin_decay_rate": 0.0006,
"atr_sl_mult": 1.5,
"atr_tp_mult": 3.0,
"min_notional": 5.0,
"signal_threshold": 3,
"adx_threshold": 0,
"volume_multiplier": 1.5,
"train_months": 6,
"test_months": 1,
"time_weight_decay": 2.0,
"negative_ratio": 3
},
"summary": {
"total_trades": 15,
"total_pnl": -128.8207,
"return_pct": -12.88,
"win_rate": 26.67,
"avg_win": 38.6226,
"avg_loss": -25.7556,
"payoff_ratio": 1.5,
"max_consecutive_losses": 6,
"profit_factor": 0.55,
"max_drawdown_pct": 20.39,
"sharpe_ratio": -52.86,
"total_fees": 57.6972,
"close_reasons": {
"TAKE_PROFIT": 4,
"STOP_LOSS": 11
}
},
"folds": [
{
"fold": 1,
"train_period": "2025-03-05 ~ 2025-09-05",
"test_period": "2025-09-05 ~ 2025-10-05",
"test_start": "2025-09-05T15:00:00",
"test_end": "2025-10-05T15:00:00",
"summary": {
"total_trades": 15,
"total_pnl": -128.8207,
"return_pct": -12.88,
"win_rate": 26.67,
"avg_win": 38.6226,
"avg_loss": -25.7556,
"payoff_ratio": 1.5,
"max_consecutive_losses": 6,
"profit_factor": 0.55,
"max_drawdown_pct": 20.39,
"sharpe_ratio": -52.86,
"total_fees": 57.6972,
"close_reasons": {
"TAKE_PROFIT": 4,
"STOP_LOSS": 11
}
},
"market_context": {
"btc_return_pct": 11.2,
"eth_return_pct": 5.8,
"btc_avg_adx": 24.6,
"market_regime": "횡보",
"ls_ratio": null
}
}
],
"trades": [
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-05 19:45:00",
"exit_time": "2025-09-05 21:15:00",
"entry_price": 2.863514,
"exit_price": 2.827446,
"quantity": 1745.9,
"sl": 2.881548,
"tp": 2.827446,
"gross_pnl": 62.97085,
"entry_fee": 1.999763,
"exit_fee": 1.974575,
"net_pnl": 58.996512,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 66.8667614741176,
"macd_hist": 0.005566660638547516,
"atr": 0.012022614930199287,
"adx": 25.794325095274626
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-06 09:00:00",
"exit_time": "2025-09-06 13:15:00",
"entry_price": 2.806619,
"exit_price": 2.812828,
"quantity": 1752.9,
"sl": 2.812828,
"tp": 2.794201,
"gross_pnl": -10.883731,
"entry_fee": 1.967889,
"exit_fee": 1.972243,
"net_pnl": -14.823863,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 34.00610631074574,
"macd_hist": -0.0004704685694576242,
"atr": 0.00413932368453192,
"adx": 14.008193990926515
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-06 15:45:00",
"exit_time": "2025-09-06 22:00:00",
"entry_price": 2.80062,
"exit_price": 2.807842,
"quantity": 1765.2,
"sl": 2.807842,
"tp": 2.786175,
"gross_pnl": -12.748682,
"entry_fee": 1.977462,
"exit_fee": 1.982561,
"net_pnl": -16.708704,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 38.58702631504332,
"macd_hist": -0.0004103175941632724,
"atr": 0.004814820425995649,
"adx": 15.495502216732291
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-09-07 07:30:00",
"exit_time": "2025-09-07 10:15:00",
"entry_price": 2.831483,
"exit_price": 2.825709,
"quantity": 1754.7,
"sl": 2.825709,
"tp": 2.843032,
"gross_pnl": -10.132486,
"entry_fee": 1.987361,
"exit_fee": 1.983308,
"net_pnl": -14.103156,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 70.72939012092385,
"macd_hist": 0.00013947818915852105,
"atr": 0.0038496557346203194,
"adx": 25.87514662158794
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-09-07 13:30:00",
"exit_time": "2025-09-07 14:00:00",
"entry_price": 2.840884,
"exit_price": 2.853506,
"quantity": 1755.1,
"sl": 2.834573,
"tp": 2.853506,
"gross_pnl": 22.153405,
"entry_fee": 1.994414,
"exit_fee": 2.003276,
"net_pnl": 18.155715,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 70.99242896973038,
"macd_hist": 0.0004291197510571344,
"atr": 0.0042074344637298,
"adx": 18.351295376801428
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-07 14:15:00",
"exit_time": "2025-09-07 14:30:00",
"entry_price": 2.888611,
"exit_price": 2.907326,
"quantity": 1717.8,
"sl": 2.907326,
"tp": 2.851181,
"gross_pnl": -32.148496,
"entry_fee": 1.984822,
"exit_fee": 1.997682,
"net_pnl": -36.131,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 76.27147788789821,
"macd_hist": 0.006331113894477991,
"atr": 0.012476615713124774,
"adx": 29.135371839765913
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-09-07 22:45:00",
"exit_time": "2025-09-08 02:15:00",
"entry_price": 2.875788,
"exit_price": 2.862379,
"quantity": 1730.8,
"sl": 2.862379,
"tp": 2.902605,
"gross_pnl": -23.207724,
"entry_fee": 1.990965,
"exit_fee": 1.981682,
"net_pnl": -27.180371,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 55.067227780514635,
"macd_hist": 0.00013194571339722526,
"atr": 0.008939112576366447,
"adx": 14.580517290441753
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-09-08 07:15:00",
"exit_time": "2025-09-08 09:45:00",
"entry_price": 2.90089,
"exit_price": 2.932746,
"quantity": 1669.0,
"sl": 2.884962,
"tp": 2.932746,
"gross_pnl": 53.167202,
"entry_fee": 1.936634,
"exit_fee": 1.957901,
"net_pnl": 49.272667,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 60.12434238530789,
"macd_hist": 0.00041228306459878275,
"atr": 0.010618574376478408,
"adx": 8.300763892347662
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-09 07:00:00",
"exit_time": "2025-09-09 07:15:00",
"entry_price": 3.009099,
"exit_price": 3.026836,
"quantity": 1655.0,
"sl": 3.026836,
"tp": 2.973625,
"gross_pnl": -29.354685,
"entry_fee": 1.992024,
"exit_fee": 2.003765,
"net_pnl": -33.350474,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 73.38194905544675,
"macd_hist": 0.006160466326798137,
"atr": 0.011824646391069867,
"adx": 28.105801823891394
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-09 23:00:00",
"exit_time": "2025-09-10 02:00:00",
"entry_price": 2.943406,
"exit_price": 2.956284,
"quantity": 1671.6,
"sl": 2.956284,
"tp": 2.917649,
"gross_pnl": -21.527774,
"entry_fee": 1.968079,
"exit_fee": 1.97669,
"net_pnl": -25.472542,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 39.53536171049937,
"macd_hist": -1.946210228058031e-05,
"atr": 0.008585695753008377,
"adx": 20.785784979630154
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-09-11 08:30:00",
"exit_time": "2025-09-11 11:15:00",
"entry_price": 3.017402,
"exit_price": 3.006355,
"quantity": 1588.7,
"sl": 3.006355,
"tp": 3.039494,
"gross_pnl": -17.549278,
"entry_fee": 1.917498,
"exit_fee": 1.910479,
"net_pnl": -21.377255,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 65.07990381213337,
"macd_hist": 0.0004440826925332865,
"atr": 0.007364208872183876,
"adx": 14.825623048829055
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-12 15:00:00",
"exit_time": "2025-09-12 16:15:00",
"entry_price": 3.019198,
"exit_price": 3.037941,
"quantity": 1552.0,
"sl": 3.037941,
"tp": 2.981711,
"gross_pnl": -29.089679,
"entry_fee": 1.874318,
"exit_fee": 1.885954,
"net_pnl": -32.849951,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 38.155616440178264,
"macd_hist": -0.0008867617207142038,
"atr": 0.012495566579322376,
"adx": 19.09633552921856
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-12 20:45:00",
"exit_time": "2025-09-13 08:45:00",
"entry_price": 3.121788,
"exit_price": 3.143446,
"quantity": 1448.4,
"sl": 3.143446,
"tp": 3.078472,
"gross_pnl": -31.36941,
"entry_fee": 1.808639,
"exit_fee": 1.821187,
"net_pnl": -34.999235,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 78.88995114530964,
"macd_hist": 0.004793783646225545,
"atr": 0.014438649390670705,
"adx": 33.848649235778474
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-13 09:15:00",
"exit_time": "2025-09-13 09:45:00",
"entry_price": 3.161284,
"exit_price": 3.177886,
"quantity": 1375.0,
"sl": 3.177886,
"tp": 3.128079,
"gross_pnl": -22.828175,
"entry_fee": 1.738706,
"exit_fee": 1.747837,
"net_pnl": -26.314718,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 75.37053214953528,
"macd_hist": 0.00511253369858191,
"atr": 0.011068206025146562,
"adx": 36.52494916026817
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-14 00:45:00",
"exit_time": "2025-09-14 01:15:00",
"entry_price": 3.109189,
"exit_price": 3.086009,
"quantity": 1355.7,
"sl": 3.120779,
"tp": 3.086009,
"gross_pnl": 31.425221,
"entry_fee": 1.686051,
"exit_fee": 1.673481,
"net_pnl": 28.065689,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 38.7433537799598,
"macd_hist": -0.0002412627790731375,
"atr": 0.007726689972056756,
"adx": 19.663365885823197
},
"fold": 1
}
],
"validation": {
"overall": "PASS",
"checks": [
{
"name": "exit_after_entry",
"passed": true,
"level": "FAIL",
"message": "모든 트레이드에서 청산 > 진입"
},
{
"name": "sl_tp_direction",
"passed": true,
"level": "FAIL",
"message": "SL/TP 방향 정합"
},
{
"name": "no_overlap",
"passed": true,
"level": "FAIL",
"message": "포지션 비중첩 확인"
},
{
"name": "positive_fees",
"passed": true,
"level": "FAIL",
"message": "수수료 양수 확인"
},
{
"name": "no_negative_balance",
"passed": true,
"level": "FAIL",
"message": "잔고 양수 유지"
},
{
"name": "win_rate_high",
"passed": true,
"level": "WARNING",
"message": "승률 정상 (26.7%)"
},
{
"name": "win_rate_low",
"passed": true,
"level": "WARNING",
"message": "승률 정상 (26.7%)"
},
{
"name": "mdd_nonzero",
"passed": true,
"level": "WARNING",
"message": "MDD 정상 (20.4%)"
},
{
"name": "trade_frequency",
"passed": true,
"level": "WARNING",
"message": "월 평균 15.0건"
},
{
"name": "profit_factor_high",
"passed": true,
"level": "WARNING",
"message": "PF 정상 (0.55)"
}
]
}
}

View File

@@ -0,0 +1,865 @@
{
"mode": "walk_forward",
"config": {
"symbols": [
"XRPUSDT"
],
"start": null,
"end": null,
"initial_balance": 1000.0,
"leverage": 20,
"fee_pct": 0.04,
"slippage_pct": 0.01,
"use_ml": false,
"ml_threshold": 0.55,
"max_daily_loss_pct": 0.05,
"max_positions": 3,
"max_same_direction": 2,
"margin_max_ratio": 0.5,
"margin_min_ratio": 0.2,
"margin_decay_rate": 0.0006,
"atr_sl_mult": 1.5,
"atr_tp_mult": 4.0,
"min_notional": 5.0,
"signal_threshold": 3,
"adx_threshold": 25.0,
"volume_multiplier": 2.5,
"train_months": 3,
"test_months": 1,
"time_weight_decay": 2.0,
"negative_ratio": 3
},
"summary": {
"total_trades": 27,
"total_pnl": 817.0717,
"return_pct": 81.71,
"win_rate": 44.44,
"avg_win": 140.6556,
"avg_loss": -58.053,
"payoff_ratio": 2.42,
"max_consecutive_losses": 4,
"profit_factor": 1.94,
"max_drawdown_pct": 20.23,
"sharpe_ratio": 51.77,
"total_fees": 195.3719,
"close_reasons": {
"STOP_LOSS": 15,
"TAKE_PROFIT": 11,
"REVERSE_SIGNAL": 1
}
},
"folds": [
{
"fold": 1,
"train_period": "2025-03-05 ~ 2025-06-05",
"test_period": "2025-06-05 ~ 2025-07-05",
"test_start": "2025-06-05T15:00:00",
"test_end": "2025-07-05T15:00:00",
"summary": {
"total_trades": 9,
"total_pnl": 454.4106,
"return_pct": 45.44,
"win_rate": 44.44,
"avg_win": 186.0558,
"avg_loss": -57.9625,
"payoff_ratio": 3.21,
"max_consecutive_losses": 2,
"profit_factor": 2.57,
"max_drawdown_pct": 8.26,
"sharpe_ratio": 73.09,
"total_fees": 60.8765,
"close_reasons": {
"STOP_LOSS": 5,
"TAKE_PROFIT": 4
}
},
"market_context": {
"btc_return_pct": 3.7,
"eth_return_pct": -2.5,
"btc_avg_adx": 24.9,
"market_regime": "횡보",
"ls_ratio": null
}
},
{
"fold": 2,
"train_period": "2025-06-05 ~ 2025-09-05",
"test_period": "2025-09-05 ~ 2025-10-05",
"test_start": "2025-09-05T15:00:00",
"test_end": "2025-10-05T15:00:00",
"summary": {
"total_trades": 10,
"total_pnl": -71.4118,
"return_pct": -7.14,
"win_rate": 30.0,
"avg_win": 116.5629,
"avg_loss": -60.1572,
"payoff_ratio": 1.94,
"max_consecutive_losses": 4,
"profit_factor": 0.83,
"max_drawdown_pct": 28.16,
"sharpe_ratio": -15.9,
"total_fees": 75.9924,
"close_reasons": {
"TAKE_PROFIT": 3,
"STOP_LOSS": 7
}
},
"market_context": {
"btc_return_pct": 11.2,
"eth_return_pct": 5.8,
"btc_avg_adx": 24.6,
"market_regime": "횡보",
"ls_ratio": null
}
},
{
"fold": 3,
"train_period": "2025-09-05 ~ 2025-12-05",
"test_period": "2025-12-05 ~ 2026-01-05",
"test_start": "2025-12-05T15:00:00",
"test_end": "2026-01-05T15:00:00",
"summary": {
"total_trades": 8,
"total_pnl": 434.073,
"return_pct": 43.41,
"win_rate": 62.5,
"avg_win": 118.7911,
"avg_loss": -53.2941,
"payoff_ratio": 2.23,
"max_consecutive_losses": 1,
"profit_factor": 3.71,
"max_drawdown_pct": 5.44,
"sharpe_ratio": 101.65,
"total_fees": 58.503,
"close_reasons": {
"STOP_LOSS": 3,
"TAKE_PROFIT": 4,
"REVERSE_SIGNAL": 1
}
},
"market_context": {
"btc_return_pct": 2.6,
"eth_return_pct": 0.8,
"btc_avg_adx": 25.5,
"market_regime": "상승 추세",
"ls_ratio": null
}
}
],
"trades": [
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-06-12 11:15:00",
"exit_time": "2025-06-12 12:30:00",
"entry_price": 2.223978,
"exit_price": 2.23446,
"quantity": 4496.0,
"sl": 2.23446,
"tp": 2.196023,
"gross_pnl": -47.13086,
"entry_fee": 3.999601,
"exit_fee": 4.018454,
"net_pnl": -55.148915,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 26.97199737318929,
"macd_hist": -0.0007807103280135859,
"atr": 0.006988561711463904,
"adx": 43.4578914382015
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-06-13 00:00:00",
"exit_time": "2025-06-13 00:30:00",
"entry_price": 2.149685,
"exit_price": 2.095237,
"quantity": 4394.9,
"sl": 2.170103,
"tp": 2.095237,
"gross_pnl": 239.292043,
"entry_fee": 3.77906,
"exit_fee": 3.683343,
"net_pnl": 231.829639,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 26.809985061770455,
"macd_hist": -0.0014287229374708253,
"atr": 0.01361191626294587,
"adx": 45.994286262673526
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-06-16 22:30:00",
"exit_time": "2025-06-16 23:45:00",
"entry_price": 2.260826,
"exit_price": 2.23893,
"quantity": 4101.6,
"sl": 2.23893,
"tp": 2.319216,
"gross_pnl": -89.809678,
"entry_fee": 3.709202,
"exit_fee": 3.673278,
"net_pnl": -97.192157,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 33.211506920436555,
"macd_hist": -0.007666291215691772,
"atr": 0.014597503086660083,
"adx": 41.77057022158849
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-06-17 15:00:00",
"exit_time": "2025-06-17 17:00:00",
"entry_price": 2.188781,
"exit_price": 2.14109,
"quantity": 4461.0,
"sl": 2.206665,
"tp": 2.14109,
"gross_pnl": 212.751364,
"entry_fee": 3.905661,
"exit_fee": 3.82056,
"net_pnl": 205.025142,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 35.98442517376965,
"macd_hist": -0.000473160901783975,
"atr": 0.011922851577807921,
"adx": 31.230008994240638
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-06-21 13:30:00",
"exit_time": "2025-06-21 14:00:00",
"entry_price": 2.119112,
"exit_price": 2.112041,
"quantity": 3992.4,
"sl": 2.112041,
"tp": 2.137967,
"gross_pnl": -28.228437,
"entry_fee": 3.384137,
"exit_fee": 3.372846,
"net_pnl": -34.98542,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 29.460371663394117,
"macd_hist": -0.002291006577745399,
"atr": 0.0047136955379463,
"adx": 26.139853452702802
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-06-21 21:15:00",
"exit_time": "2025-06-21 21:30:00",
"entry_price": 2.045995,
"exit_price": 1.990773,
"quantity": 4278.1,
"sl": 2.066704,
"tp": 1.990773,
"gross_pnl": 236.248138,
"entry_fee": 3.501189,
"exit_fee": 3.40669,
"net_pnl": 229.340259,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 24.112041978961905,
"macd_hist": -0.0015821538372272313,
"atr": 0.013805669484335523,
"adx": 47.020225174544926
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-06-24 05:30:00",
"exit_time": "2025-06-24 08:00:00",
"entry_price": 2.184818,
"exit_price": 2.167742,
"quantity": 2879.5,
"sl": 2.167742,
"tp": 2.230355,
"gross_pnl": -49.171401,
"entry_fee": 2.516474,
"exit_fee": 2.496805,
"net_pnl": -54.18468,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 73.02163697288638,
"macd_hist": 0.0005479493365071683,
"atr": 0.011384245075129916,
"adx": 47.36536786932758
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-07-01 13:00:00",
"exit_time": "2025-07-01 14:00:00",
"entry_price": 2.185781,
"exit_price": 2.19914,
"quantity": 3196.0,
"sl": 2.19914,
"tp": 2.150157,
"gross_pnl": -42.695683,
"entry_fee": 2.794303,
"exit_fee": 2.811381,
"net_pnl": -48.301367,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 34.25494593254047,
"macd_hist": -0.00014675405719808375,
"atr": 0.008906066514248343,
"adx": 38.40722178323835
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-07-04 06:00:00",
"exit_time": "2025-07-04 08:15:00",
"entry_price": 2.232877,
"exit_price": 2.208013,
"quantity": 3379.7,
"sl": 2.242201,
"tp": 2.208013,
"gross_pnl": 84.031619,
"entry_fee": 3.018581,
"exit_fee": 2.984969,
"net_pnl": 78.028069,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 31.442919224174045,
"macd_hist": -0.00029321477558042104,
"atr": 0.0062159081353788895,
"adx": 33.56850119916028
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-05 19:45:00",
"exit_time": "2025-09-05 22:15:00",
"entry_price": 2.863514,
"exit_price": 2.815423,
"quantity": 3491.9,
"sl": 2.881548,
"tp": 2.815423,
"gross_pnl": 167.927076,
"entry_fee": 3.999641,
"exit_fee": 3.93247,
"net_pnl": 159.994965,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 66.8667614741176,
"macd_hist": 0.005566660638547516,
"atr": 0.012022614930199287,
"adx": 25.794325095274626
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-09-07 07:30:00",
"exit_time": "2025-09-07 10:15:00",
"entry_price": 2.831483,
"exit_price": 2.825709,
"quantity": 3310.6,
"sl": 2.825709,
"tp": 2.846882,
"gross_pnl": -19.117005,
"entry_fee": 3.749563,
"exit_fee": 3.741916,
"net_pnl": -26.608485,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 70.72939012092385,
"macd_hist": 0.00013947818915852105,
"atr": 0.0038496557346203194,
"adx": 25.87514662158794
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-07 14:15:00",
"exit_time": "2025-09-07 14:30:00",
"entry_price": 2.888611,
"exit_price": 2.907326,
"quantity": 3295.3,
"sl": 2.907326,
"tp": 2.838705,
"gross_pnl": -61.671288,
"entry_fee": 3.807536,
"exit_fee": 3.832205,
"net_pnl": -69.311028,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 76.27147788789821,
"macd_hist": 0.006331113894477991,
"atr": 0.012476615713124774,
"adx": 29.135371839765913
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-09 07:00:00",
"exit_time": "2025-09-09 07:15:00",
"entry_price": 3.009099,
"exit_price": 3.026836,
"quantity": 3264.0,
"sl": 3.026836,
"tp": 2.9618,
"gross_pnl": -57.893469,
"entry_fee": 3.92868,
"exit_fee": 3.951837,
"net_pnl": -65.773986,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 73.38194905544675,
"macd_hist": 0.006160466326798137,
"atr": 0.011824646391069867,
"adx": 28.105801823891394
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-12 20:45:00",
"exit_time": "2025-09-13 08:45:00",
"entry_price": 3.121788,
"exit_price": 3.143446,
"quantity": 3197.5,
"sl": 3.143446,
"tp": 3.064033,
"gross_pnl": -69.251372,
"entry_fee": 3.992767,
"exit_fee": 4.020467,
"net_pnl": -77.264606,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 78.88995114530964,
"macd_hist": 0.004793783646225545,
"atr": 0.014438649390670705,
"adx": 33.848649235778474
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-14 11:15:00",
"exit_time": "2025-09-14 13:45:00",
"entry_price": 3.066993,
"exit_price": 3.033085,
"quantity": 3002.8,
"sl": 3.079709,
"tp": 3.033085,
"gross_pnl": 101.818765,
"entry_fee": 3.683827,
"exit_fee": 3.643099,
"net_pnl": 94.491838,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 31.419480021379645,
"macd_hist": -0.0003629091436070245,
"atr": 0.008476985221720718,
"adx": 31.882046477112183
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-20 14:00:00",
"exit_time": "2025-09-20 14:45:00",
"entry_price": 2.971603,
"exit_price": 2.980986,
"quantity": 3353.4,
"sl": 2.980986,
"tp": 2.94658,
"gross_pnl": -31.466181,
"entry_fee": 3.985989,
"exit_fee": 3.998576,
"net_pnl": -39.450746,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 36.09553473183674,
"macd_hist": -0.00018720159437711752,
"atr": 0.006255577606168083,
"adx": 28.84076206692945
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-09-24 19:45:00",
"exit_time": "2025-09-24 20:30:00",
"entry_price": 2.976198,
"exit_price": 2.955734,
"quantity": 3279.9,
"sl": 2.955734,
"tp": 3.030766,
"gross_pnl": -67.117005,
"entry_fee": 3.904652,
"exit_fee": 3.877805,
"net_pnl": -74.899463,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 77.25608496751632,
"macd_hist": 0.00019635721897620986,
"atr": 0.013642083686890932,
"adx": 66.36435142210216
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-25 12:15:00",
"exit_time": "2025-09-25 12:30:00",
"entry_price": 2.793221,
"exit_price": 2.811993,
"quantity": 3226.0,
"sl": 2.811993,
"tp": 2.743161,
"gross_pnl": -60.559206,
"entry_fee": 3.604372,
"exit_fee": 3.628596,
"net_pnl": -67.792174,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 27.256149561284357,
"macd_hist": -0.0005773233003328118,
"atr": 0.012514818420656685,
"adx": 36.706433983392714
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-09-28 22:00:00",
"exit_time": "2025-09-29 07:15:00",
"entry_price": 2.850785,
"exit_price": 2.885643,
"quantity": 2923.6,
"sl": 2.837713,
"tp": 2.885643,
"gross_pnl": 101.910302,
"entry_fee": 3.333822,
"exit_fee": 3.374586,
"net_pnl": 95.201894,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 69.94607598871028,
"macd_hist": 0.0002172037763433672,
"atr": 0.008714453238038499,
"adx": 45.96715774504039
},
"fold": 2
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-12-10 10:30:00",
"exit_time": "2025-12-10 18:30:00",
"entry_price": 2.069993,
"exit_price": 2.080244,
"quantity": 4830.5,
"sl": 2.080244,
"tp": 2.042658,
"gross_pnl": -49.515419,
"entry_fee": 3.99964,
"exit_fee": 4.019447,
"net_pnl": -57.534506,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 37.038012691554805,
"macd_hist": -9.211636302669133e-05,
"atr": 0.0068337189344636114,
"adx": 28.943927763667713
},
"fold": 3
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-12-12 16:00:00",
"exit_time": "2025-12-13 05:00:00",
"entry_price": 1.988899,
"exit_price": 2.037103,
"quantity": 4739.1,
"sl": 1.970822,
"tp": 2.037103,
"gross_pnl": 228.445099,
"entry_fee": 3.770236,
"exit_fee": 3.861614,
"net_pnl": 220.813249,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 28.182013495720106,
"macd_hist": -0.00643391832048344,
"atr": 0.01205108033747697,
"adx": 30.769786891839054
},
"fold": 3
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-12-19 13:15:00",
"exit_time": "2025-12-19 15:15:00",
"entry_price": 1.878712,
"exit_price": 1.889911,
"quantity": 4978.2,
"sl": 1.889911,
"tp": 1.84885,
"gross_pnl": -55.748245,
"entry_fee": 3.741042,
"exit_fee": 3.763341,
"net_pnl": -63.252628,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 68.16547032772114,
"macd_hist": -4.5929936914913816e-05,
"atr": 0.007465649526915487,
"adx": 40.69667585881617
},
"fold": 3
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-12-25 23:30:00",
"exit_time": "2025-12-26 02:15:00",
"entry_price": 1.831783,
"exit_price": 1.857641,
"quantity": 5284.9,
"sl": 1.822086,
"tp": 1.857641,
"gross_pnl": 136.657597,
"entry_fee": 3.872316,
"exit_fee": 3.926979,
"net_pnl": 128.858301,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 18.688435627302994,
"macd_hist": -0.0034657628634239823,
"atr": 0.006464530874639477,
"adx": 30.228290924248867
},
"fold": 3
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2026-01-01 15:45:00",
"exit_time": "2026-01-01 16:15:00",
"entry_price": 1.861986,
"exit_price": 1.855307,
"quantity": 4787.6,
"sl": 1.855307,
"tp": 1.879797,
"gross_pnl": -31.976479,
"entry_fee": 3.565778,
"exit_fee": 3.552987,
"net_pnl": -39.095244,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 64.9515011671277,
"macd_hist": 8.017825296896758e-05,
"atr": 0.004452680396625609,
"adx": 29.061543249865803
},
"fold": 3
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2026-01-02 14:30:00",
"exit_time": "2026-01-02 16:15:00",
"entry_price": 1.906991,
"exit_price": 1.93559,
"quantity": 4818.6,
"sl": 1.896266,
"tp": 1.93559,
"gross_pnl": 137.806916,
"entry_fee": 3.67561,
"exit_fee": 3.730733,
"net_pnl": 130.400573,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 68.243618769103,
"macd_hist": 0.00021763021087121363,
"atr": 0.007149738287103824,
"adx": 34.75978288472445
},
"fold": 3
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2026-01-04 00:45:00",
"exit_time": "2026-01-04 03:00:00",
"entry_price": 2.041396,
"exit_price": 2.038904,
"quantity": 3981.9,
"sl": 2.052244,
"tp": 2.012468,
"gross_pnl": 9.922775,
"entry_fee": 3.251454,
"exit_fee": 3.247485,
"net_pnl": 3.423837,
"close_reason": "REVERSE_SIGNAL",
"ml_proba": null,
"indicators": {
"rsi": 78.42031027026397,
"macd_hist": 0.002773445174521872,
"atr": 0.007231967338050755,
"adx": 25.06146716762975
},
"fold": 3
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2026-01-04 03:00:00",
"exit_time": "2026-01-04 05:15:00",
"entry_price": 2.038904,
"exit_price": 2.068362,
"quantity": 3971.2,
"sl": 2.027857,
"tp": 2.068362,
"gross_pnl": 116.983697,
"entry_fee": 3.238758,
"exit_fee": 3.285551,
"net_pnl": 110.459388,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 66.00101725582081,
"macd_hist": 4.503977849404806e-05,
"atr": 0.007364505506906315,
"adx": 30.991168735172053
},
"fold": 3
}
],
"validation": {
"overall": "FAIL",
"checks": [
{
"name": "exit_after_entry",
"passed": true,
"level": "FAIL",
"message": "모든 트레이드에서 청산 > 진입"
},
{
"name": "sl_tp_direction",
"passed": true,
"level": "FAIL",
"message": "SL/TP 방향 정합"
},
{
"name": "no_overlap",
"passed": true,
"level": "FAIL",
"message": "포지션 비중첩 확인"
},
{
"name": "positive_fees",
"passed": true,
"level": "FAIL",
"message": "수수료 양수 확인"
},
{
"name": "no_negative_balance",
"passed": true,
"level": "FAIL",
"message": "잔고 양수 유지"
},
{
"name": "win_rate_high",
"passed": true,
"level": "WARNING",
"message": "승률 정상 (44.4%)"
},
{
"name": "win_rate_low",
"passed": true,
"level": "WARNING",
"message": "승률 정상 (44.4%)"
},
{
"name": "mdd_nonzero",
"passed": true,
"level": "WARNING",
"message": "MDD 정상 (20.2%)"
},
{
"name": "trade_frequency",
"passed": false,
"level": "WARNING",
"message": "월 평균 4.0건 < 5건 — 신호 생성 부족"
},
{
"name": "profit_factor_high",
"passed": true,
"level": "WARNING",
"message": "PF 정상 (1.94)"
}
]
}
}

View File

@@ -0,0 +1,537 @@
{
"mode": "walk_forward",
"config": {
"symbols": [
"XRPUSDT"
],
"start": null,
"end": null,
"initial_balance": 1000.0,
"leverage": 10,
"fee_pct": 0.04,
"slippage_pct": 0.01,
"use_ml": false,
"ml_threshold": 0.55,
"max_daily_loss_pct": 0.05,
"max_positions": 3,
"max_same_direction": 2,
"margin_max_ratio": 0.5,
"margin_min_ratio": 0.2,
"margin_decay_rate": 0.0006,
"atr_sl_mult": 1.5,
"atr_tp_mult": 4.0,
"min_notional": 5.0,
"signal_threshold": 3,
"adx_threshold": 25.0,
"volume_multiplier": 1.5,
"train_months": 6,
"test_months": 1,
"time_weight_decay": 2.0,
"negative_ratio": 3
},
"summary": {
"total_trades": 16,
"total_pnl": -130.1928,
"return_pct": -13.02,
"win_rate": 25.0,
"avg_win": 65.5037,
"avg_loss": -32.684,
"payoff_ratio": 2.0,
"max_consecutive_losses": 5,
"profit_factor": 0.67,
"max_drawdown_pct": 19.46,
"sharpe_ratio": -34.26,
"total_fees": 61.4728,
"close_reasons": {
"TAKE_PROFIT": 4,
"STOP_LOSS": 11,
"REVERSE_SIGNAL": 1
}
},
"folds": [
{
"fold": 1,
"train_period": "2025-03-05 ~ 2025-09-05",
"test_period": "2025-09-05 ~ 2025-10-05",
"test_start": "2025-09-05T15:00:00",
"test_end": "2025-10-05T15:00:00",
"summary": {
"total_trades": 16,
"total_pnl": -130.1928,
"return_pct": -13.02,
"win_rate": 25.0,
"avg_win": 65.5037,
"avg_loss": -32.684,
"payoff_ratio": 2.0,
"max_consecutive_losses": 5,
"profit_factor": 0.67,
"max_drawdown_pct": 19.46,
"sharpe_ratio": -34.26,
"total_fees": 61.4728,
"close_reasons": {
"TAKE_PROFIT": 4,
"STOP_LOSS": 11,
"REVERSE_SIGNAL": 1
}
},
"market_context": {
"btc_return_pct": 11.2,
"eth_return_pct": 5.8,
"btc_avg_adx": 24.6,
"market_regime": "횡보",
"ls_ratio": null
}
}
],
"trades": [
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-05 19:45:00",
"exit_time": "2025-09-05 22:15:00",
"entry_price": 2.863514,
"exit_price": 2.815423,
"quantity": 1745.9,
"sl": 2.881548,
"tp": 2.815423,
"gross_pnl": 83.961134,
"entry_fee": 1.999763,
"exit_fee": 1.966179,
"net_pnl": 79.995191,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 66.8667614741176,
"macd_hist": 0.005566660638547516,
"atr": 0.012022614930199287,
"adx": 25.794325095274626
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-09-07 07:30:00",
"exit_time": "2025-09-07 10:15:00",
"entry_price": 2.831483,
"exit_price": 2.825709,
"quantity": 1724.2,
"sl": 2.825709,
"tp": 2.846882,
"gross_pnl": -9.956365,
"entry_fee": 1.952817,
"exit_fee": 1.948835,
"net_pnl": -13.858017,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 70.72939012092385,
"macd_hist": 0.00013947818915852105,
"atr": 0.0038496557346203194,
"adx": 25.87514662158794
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-07 14:15:00",
"exit_time": "2025-09-07 14:30:00",
"entry_price": 2.888611,
"exit_price": 2.907326,
"quantity": 1698.8,
"sl": 2.907326,
"tp": 2.838705,
"gross_pnl": -31.792912,
"entry_fee": 1.962869,
"exit_fee": 1.975586,
"net_pnl": -35.731367,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 76.27147788789821,
"macd_hist": 0.006331113894477991,
"atr": 0.012476615713124774,
"adx": 29.135371839765913
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-09 07:00:00",
"exit_time": "2025-09-09 07:15:00",
"entry_price": 3.009099,
"exit_price": 3.026836,
"quantity": 1649.5,
"sl": 3.026836,
"tp": 2.9618,
"gross_pnl": -29.257131,
"entry_fee": 1.985404,
"exit_fee": 1.997106,
"net_pnl": -33.239641,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 73.38194905544675,
"macd_hist": 0.006160466326798137,
"atr": 0.011824646391069867,
"adx": 28.105801823891394
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-12 20:45:00",
"exit_time": "2025-09-13 08:45:00",
"entry_price": 3.121788,
"exit_price": 3.143446,
"quantity": 1596.9,
"sl": 3.143446,
"tp": 3.064033,
"gross_pnl": -34.585619,
"entry_fee": 1.994073,
"exit_fee": 2.007907,
"net_pnl": -38.587599,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 78.88995114530964,
"macd_hist": 0.004793783646225545,
"atr": 0.014438649390670705,
"adx": 33.848649235778474
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-13 09:15:00",
"exit_time": "2025-09-13 09:45:00",
"entry_price": 3.161284,
"exit_price": 3.177886,
"quantity": 1516.0,
"sl": 3.177886,
"tp": 3.117011,
"gross_pnl": -25.169101,
"entry_fee": 1.917003,
"exit_fee": 1.92707,
"net_pnl": -29.013173,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 75.37053214953528,
"macd_hist": 0.00511253369858191,
"atr": 0.011068206025146562,
"adx": 36.52494916026817
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-14 11:15:00",
"exit_time": "2025-09-14 13:45:00",
"entry_price": 3.066993,
"exit_price": 3.033085,
"quantity": 1515.3,
"sl": 3.079709,
"tp": 3.033085,
"gross_pnl": 51.380703,
"entry_fee": 1.858966,
"exit_fee": 1.838414,
"net_pnl": 47.683323,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 31.419480021379645,
"macd_hist": -0.0003629091436070245,
"atr": 0.008476985221720718,
"adx": 31.882046477112183
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-14 23:15:00",
"exit_time": "2025-09-15 01:30:00",
"entry_price": 3.030497,
"exit_price": 3.044966,
"quantity": 1612.2,
"sl": 3.044966,
"tp": 2.991913,
"gross_pnl": -23.326699,
"entry_fee": 1.954307,
"exit_fee": 1.963638,
"net_pnl": -27.244643,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 41.86565415846166,
"macd_hist": -6.680085954976589e-05,
"atr": 0.009645907843496216,
"adx": 26.821083958033373
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-19 20:00:00",
"exit_time": "2025-09-20 06:30:00",
"entry_price": 2.988001,
"exit_price": 3.002469,
"quantity": 1589.5,
"sl": 3.002469,
"tp": 2.949422,
"gross_pnl": -22.995886,
"entry_fee": 1.899771,
"exit_fee": 1.908969,
"net_pnl": -26.804627,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 36.43471826247567,
"macd_hist": -0.00020348228213156426,
"atr": 0.009644913974796715,
"adx": 38.358109484089894
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-20 14:00:00",
"exit_time": "2025-09-20 14:45:00",
"entry_price": 2.971603,
"exit_price": 2.980986,
"quantity": 1553.2,
"sl": 2.980986,
"tp": 2.94658,
"gross_pnl": -14.574245,
"entry_fee": 1.846197,
"exit_fee": 1.852027,
"net_pnl": -18.272469,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 36.09553473183674,
"macd_hist": -0.00018720159437711752,
"atr": 0.006255577606168083,
"adx": 28.84076206692945
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-09-24 14:15:00",
"exit_time": "2025-09-24 15:30:00",
"entry_price": 2.894089,
"exit_price": 2.929027,
"quantity": 1563.6,
"sl": 2.880988,
"tp": 2.929027,
"gross_pnl": 54.628771,
"entry_fee": 1.810079,
"exit_fee": 1.831931,
"net_pnl": 50.986761,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 66.07824522998891,
"macd_hist": 0.0002837479638076925,
"atr": 0.008734454267839082,
"adx": 33.216876327535196
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-09-24 19:45:00",
"exit_time": "2025-09-24 20:00:00",
"entry_price": 2.976198,
"exit_price": 2.963004,
"quantity": 1606.1,
"sl": 2.955734,
"tp": 3.030766,
"gross_pnl": -21.190755,
"entry_fee": 1.912028,
"exit_fee": 1.903552,
"net_pnl": -25.006335,
"close_reason": "REVERSE_SIGNAL",
"ml_proba": null,
"indicators": {
"rsi": 77.25608496751632,
"macd_hist": 0.00019635721897620986,
"atr": 0.013642083686890932,
"adx": 66.36435142210216
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-24 20:00:00",
"exit_time": "2025-09-25 01:00:00",
"entry_price": 2.963004,
"exit_price": 2.90759,
"quantity": 1570.7,
"sl": 2.983784,
"tp": 2.90759,
"gross_pnl": 87.037912,
"entry_fee": 1.861596,
"exit_fee": 1.826781,
"net_pnl": 83.349535,
"close_reason": "TAKE_PROFIT",
"ml_proba": null,
"indicators": {
"rsi": 66.65458359619365,
"macd_hist": -7.427449332575442e-05,
"atr": 0.013853363423541577,
"adx": 67.76721468449082
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "SHORT",
"entry_time": "2025-09-25 12:15:00",
"exit_time": "2025-09-25 12:30:00",
"entry_price": 2.793221,
"exit_price": 2.811993,
"quantity": 1784.3,
"sl": 2.811993,
"tp": 2.743161,
"gross_pnl": -33.495286,
"entry_fee": 1.993577,
"exit_fee": 2.006976,
"net_pnl": -37.495839,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 27.256149561284357,
"macd_hist": -0.0005773233003328118,
"atr": 0.012514818420656685,
"adx": 36.706433983392714
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-09-25 17:15:00",
"exit_time": "2025-09-25 17:30:00",
"entry_price": 2.769777,
"exit_price": 2.742807,
"quantity": 1763.4,
"sl": 2.742807,
"tp": 2.841698,
"gross_pnl": -47.559642,
"entry_fee": 1.95369,
"exit_fee": 1.934666,
"net_pnl": -51.447998,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 31.610683788190443,
"macd_hist": -0.006544986366080717,
"atr": 0.01798028133337769,
"adx": 26.44006928288656
},
"fold": 1
},
{
"symbol": "XRPUSDT",
"side": "LONG",
"entry_time": "2025-09-25 17:45:00",
"exit_time": "2025-09-26 11:00:00",
"entry_price": 2.736174,
"exit_price": 2.705528,
"quantity": 1691.1,
"sl": 2.705528,
"tp": 2.817895,
"gross_pnl": -51.824893,
"entry_fee": 1.850857,
"exit_fee": 1.830127,
"net_pnl": -55.505877,
"close_reason": "STOP_LOSS",
"ml_proba": null,
"indicators": {
"rsi": 25.044837785333744,
"macd_hist": -0.0090419871390266,
"atr": 0.020430446659902204,
"adx": 30.966856582892532
},
"fold": 1
}
],
"validation": {
"overall": "PASS",
"checks": [
{
"name": "exit_after_entry",
"passed": true,
"level": "FAIL",
"message": "모든 트레이드에서 청산 > 진입"
},
{
"name": "sl_tp_direction",
"passed": true,
"level": "FAIL",
"message": "SL/TP 방향 정합"
},
{
"name": "no_overlap",
"passed": true,
"level": "FAIL",
"message": "포지션 비중첩 확인"
},
{
"name": "positive_fees",
"passed": true,
"level": "FAIL",
"message": "수수료 양수 확인"
},
{
"name": "no_negative_balance",
"passed": true,
"level": "FAIL",
"message": "잔고 양수 유지"
},
{
"name": "win_rate_high",
"passed": true,
"level": "WARNING",
"message": "승률 정상 (25.0%)"
},
{
"name": "win_rate_low",
"passed": true,
"level": "WARNING",
"message": "승률 정상 (25.0%)"
},
{
"name": "mdd_nonzero",
"passed": true,
"level": "WARNING",
"message": "MDD 정상 (19.5%)"
},
{
"name": "trade_frequency",
"passed": true,
"level": "WARNING",
"message": "월 평균 16.0건"
},
{
"name": "profit_factor_high",
"passed": true,
"level": "WARNING",
"message": "PF 정상 (0.67)"
}
]
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,10 +4,15 @@ MTF Pullback Bot — OOS Dry-run 평가 스크립트
프로덕션 서버에서 JSONL 거래 기록을 가져와
승률·PF·누적PnL·평균보유시간을 계산하고 LIVE 배포 판정을 출력한다.
비용 모델(수수료·슬리피지·펀딩)을 사후보정으로 적용하여
fees_only / realistic / pessimistic 3개 시나리오 결과를 출력한다.
Usage:
python scripts/evaluate_oos.py
python scripts/evaluate_oos.py --symbol xrpusdt
python scripts/evaluate_oos.py --local # 로컬 파일만 사용 (서버 fetch 스킵)
python scripts/evaluate_oos.py --local --scenario all
python scripts/evaluate_oos.py --local --scenario fees_only
"""
import argparse
@@ -17,6 +22,10 @@ from pathlib import Path
import pandas as pd
# ── 비용 모델 import ─────────────────────────────────────────────
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from src.config import COST_MODEL, COST_SCENARIOS # noqa: E402
# ── 설정 ──────────────────────────────────────────────────────────
PROD_HOST = "root@10.1.10.24"
REMOTE_DIR = "/root/cointrader/data/trade_history"
@@ -67,20 +76,74 @@ def load_trades(path: Path) -> pd.DataFrame:
return df
def calc_metrics(df: pd.DataFrame) -> dict:
def count_funding_events(entry_ts, exit_ts) -> int:
"""
Binance USDⓈ-M Futures 펀딩 스냅샷 시각(00/08/16 UTC)이
[entry_ts, exit_ts] 구간에 몇 번 포함되는지 카운트.
"""
start = entry_ts.ceil("h")
end = exit_ts.floor("h")
if start > end:
return 0
hours = pd.date_range(start, end, freq="1h", inclusive="both")
return sum(1 for h in hours if h.hour % 8 == 0)
def _get_fee_bps(order_type: str) -> float:
"""주문 타입에 따른 수수료 bps 반환."""
if order_type == "taker":
return COST_MODEL["taker_fee_bps"]
return COST_MODEL["maker_fee_bps"]
def calc_trade_cost(row, scenario: dict) -> float:
"""개별 거래의 총 비용(bps)을 계산."""
# 1) Fee: entry + exit
entry_fee = _get_fee_bps(COST_MODEL["entry_order_type"])
# exit order type: SL 히트면 sl_order_type, TP 히트면 tp_order_type
reason = row.get("reason", "")
if "SL" in reason:
exit_fee = _get_fee_bps(COST_MODEL["sl_order_type"])
else:
exit_fee = _get_fee_bps(COST_MODEL["tp_order_type"])
fee = entry_fee + exit_fee
# 2) Slippage: 왕복
slippage = scenario["slippage_bps_per_side"] * 2
# 3) Funding: 경계 교차 카운트
funding_count = count_funding_events(row["entry_ts"], row["exit_ts"])
funding = funding_count * scenario["funding_bps_per_8h"]
return fee + slippage + funding
def apply_cost_model(df: pd.DataFrame, scenario_name: str) -> pd.DataFrame:
"""DataFrame에 비용을 적용하여 adjusted_pnl_bps 컬럼 추가."""
scenario = COST_SCENARIOS[scenario_name]
result = df.copy()
result["cost_bps"] = result.apply(lambda row: calc_trade_cost(row, scenario), axis=1)
result["adjusted_pnl_bps"] = result["pnl_bps"] - result["cost_bps"]
return result
def calc_metrics(df: pd.DataFrame, pnl_col: str = "pnl_bps") -> dict:
"""핵심 지표 계산. 빈 DataFrame이면 안전한 기본값 반환."""
n = len(df)
if n == 0:
return {"trades": 0, "win_rate": 0.0, "pf": 0.0, "cum_pnl": 0.0, "avg_dur": 0.0}
return {"trades": 0, "win_rate": 0.0, "pf": 0.0, "cum_pnl": 0.0, "avg_pnl": 0.0, "avg_dur": 0.0}
wins = df[df["pnl_bps"] > 0]
losses = df[df["pnl_bps"] < 0]
wins = df[df[pnl_col] > 0]
losses = df[df[pnl_col] < 0]
win_rate = len(wins) / n * 100
gross_profit = wins["pnl_bps"].sum() if len(wins) > 0 else 0.0
gross_loss = abs(losses["pnl_bps"].sum()) if len(losses) > 0 else 0.0
gross_profit = wins[pnl_col].sum() if len(wins) > 0 else 0.0
gross_loss = abs(losses[pnl_col].sum()) if len(losses) > 0 else 0.0
pf = gross_profit / gross_loss if gross_loss > 0 else float("inf")
cum_pnl = df["pnl_bps"].sum()
cum_pnl = df[pnl_col].sum()
avg_pnl = cum_pnl / n
avg_dur = df["duration_min"].mean()
return {
@@ -88,28 +151,29 @@ def calc_metrics(df: pd.DataFrame) -> dict:
"win_rate": round(win_rate, 1),
"pf": round(pf, 2),
"cum_pnl": round(cum_pnl, 1),
"avg_pnl": round(avg_pnl, 2),
"avg_dur": round(avg_dur, 1),
}
def print_report(df: pd.DataFrame):
"""성적표 출력."""
"""성적표 출력 (raw, 비용 미반영)."""
total = calc_metrics(df)
longs = calc_metrics(df[df["side"] == "LONG"])
shorts = calc_metrics(df[df["side"] == "SHORT"])
header = f"{'':>10} {'Trades':>8} {'WinRate':>9} {'PF':>8} {'CumPnL':>10} {'AvgDur':>10}"
sep = "" * 60
sep = "\u2500" * 60
print()
print(sep)
print(" MTF Pullback Bot OOS Dry-run 성적표")
print(" MTF Pullback Bot \u2014 OOS Dry-run \uc131\uc801\ud45c")
print(sep)
print(header)
print(sep)
for label, m in [("Total", total), ("LONG", longs), ("SHORT", shorts)]:
pf_str = f"{m['pf']:.2f}" if m["pf"] != float("inf") else ""
pf_str = f"{m['pf']:.2f}" if m["pf"] != float("inf") else "\u221e"
dur_str = f"{m['avg_dur']:.0f}m" if m["trades"] > 0 else "-"
print(
f"{label:>10} {m['trades']:>8d} {m['win_rate']:>8.1f}% {pf_str:>8} "
@@ -128,46 +192,168 @@ def print_report(df: pd.DataFrame):
dur = f"{row['duration_min']:.0f}m"
reason = row.get("reason", "")
if len(reason) > 25:
reason = reason[:25] + ""
reason = reason[:25] + "\u2026"
print(
f"{i+1:>3} {row['side']:>6} {row['entry_price']:>10.4f} {row['exit_price']:>10.4f} "
f"{row['pnl_bps']:>+10.1f} {dur:>8} {reason}"
)
print(sep)
# ── 최종 판정 ──
# ── 최종 판정 (비용 반영 기준) ──
# Raw PF는 비현실적 — fees_only 기준으로 판정
cost_df = apply_cost_model(df, "fees_only")
cost_total = calc_metrics(cost_df, pnl_col="adjusted_pnl_bps")
cost_long = calc_metrics(cost_df[cost_df["side"] == "LONG"], pnl_col="adjusted_pnl_bps")
cost_short = calc_metrics(cost_df[cost_df["side"] == "SHORT"], pnl_col="adjusted_pnl_bps")
# 대칭성 체크: LONG/SHORT 양쪽 모두 PF >= 0.8 이상이어야 함
symmetry_ok = True
if cost_long["trades"] >= 5 and cost_short["trades"] >= 5:
symmetry_ok = cost_long["pf"] >= 0.8 and cost_short["pf"] >= 0.8
print()
if total["trades"] >= MIN_TRADES and total["pf"] >= MIN_PF:
print(f" [판정: 통과] 엣지가 증명되었습니다. LIVE 배포(자금 투입)를 권장합니다.")
print(f" (거래수 {total['trades']} >= {MIN_TRADES}, PF {total['pf']:.2f} >= {MIN_PF:.1f})")
if cost_total["trades"] >= MIN_TRADES and cost_total["pf"] >= MIN_PF and symmetry_ok:
print(f" [\ud310\uc815: \ud1b5\uacfc] \uc5e3\uc9c0\uac00 \uc99d\uba85\ub418\uc5c8\uc2b5\ub2c8\ub2e4. LIVE \ubc30\ud3ec(\uc790\uae08 \ud22c\uc785)\ub97c \uad8c\uc7a5\ud569\ub2c8\ub2e4.")
print(f" (\uac70\ub798\uc218 {cost_total['trades']} >= {MIN_TRADES}, fees_only PF {cost_total['pf']:.2f} >= {MIN_PF:.1f})")
else:
reasons = []
if total["trades"] < MIN_TRADES:
reasons.append(f"거래수 {total['trades']} < {MIN_TRADES}")
if total["pf"] < MIN_PF:
reasons.append(f"PF {total['pf']:.2f} < {MIN_PF:.1f}")
print(f" [판정: 보류] 기준 미달. OOS 검증 실패로 실전 투입을 보류합니다.")
if cost_total["trades"] < MIN_TRADES:
reasons.append(f"\uac70\ub798\uc218 {cost_total['trades']} < {MIN_TRADES}")
if cost_total["pf"] < MIN_PF:
reasons.append(f"fees_only PF {cost_total['pf']:.2f} < {MIN_PF:.1f}")
if not symmetry_ok:
reasons.append(f"LONG/SHORT \ube44\ub300\uce6d (L:{cost_long['pf']:.2f} / S:{cost_short['pf']:.2f})")
print(f" [\ud310\uc815: \uc2e4\ud328] OOS \uac80\uc99d \uc2e4\ud328. \uc2e4\uc804 \ud22c\uc785 \ubd88\uac00.")
print(f" ({', '.join(reasons)})")
print()
def print_cost_report(df: pd.DataFrame, scenario_names: list[str]):
"""비용 보정 시나리오별 성적표 출력."""
sep = "\u2500" * 61
# 시나리오별 데이터 준비
scenario_dfs = {}
for name in scenario_names:
scenario_dfs[name] = apply_cost_model(df, name)
print()
print(sep)
print(" MTF Pullback Bot \u2014 OOS Cost-Adjusted Results")
print(sep)
# 헤더
header = f"{'Scenario:':>16}"
for name in scenario_names:
header += f" {name:>14}"
print(header)
print(sep)
# Total / LONG / SHORT 각각
for section_label, filter_fn in [
("Total", lambda d: d),
("LONG", lambda d: d[d["side"] == "LONG"]),
("SHORT", lambda d: d[d["side"] == "SHORT"]),
]:
print(section_label)
# 각 시나리오에 대해 metrics 계산
metrics_list = []
for name in scenario_names:
sdf = filter_fn(scenario_dfs[name])
m = calc_metrics(sdf, pnl_col="adjusted_pnl_bps")
metrics_list.append(m)
# Trades
line = f"{'Trades:':>16}"
for m in metrics_list:
line += f" {m['trades']:>14d}"
print(line)
# WinRate
line = f"{'WinRate:':>16}"
for m in metrics_list:
line += f" {m['win_rate']:>13.1f}%"
print(line)
# PF
line = f"{'PF:':>16}"
for m in metrics_list:
pf_str = f"{m['pf']:.2f}" if m["pf"] != float("inf") else "\u221e"
line += f" {pf_str:>14}"
print(line)
# CumPnL(bps)
line = f"{'CumPnL(bps):':>16}"
for m in metrics_list:
line += f" {m['cum_pnl']:>+14.1f}"
print(line)
# AvgPnL(bps)
line = f"{'AvgPnL(bps):':>16}"
for m in metrics_list:
line += f" {m['avg_pnl']:>+14.2f}"
print(line)
# AvgDur
line = f"{'AvgDur:':>16}"
for m in metrics_list:
dur_str = f"{m['avg_dur']:.0f}m" if m["trades"] > 0 else "-"
line += f" {dur_str:>14}"
print(line)
print(sep)
# Raw 참고
raw_total = calc_metrics(df)
print(f"Raw (\ube44\uc6a9 \ubbf8\ubc18\uc601, \ucc38\uace0\uc6a9):")
pf_str = f"{raw_total['pf']:.2f}" if raw_total["pf"] != float("inf") else "\u221e"
print(f" Total PF: {pf_str}, CumPnL: {raw_total['cum_pnl']:+.1f} bps")
print(sep)
print()
def main():
parser = argparse.ArgumentParser(description="MTF OOS Dry-run 평가")
parser.add_argument("--symbol", default="xrpusdt", help="심볼 (파일명 소문자, 기본: xrpusdt)")
parser.add_argument("--local", action="store_true", help="로컬 파일만 사용 (서버 fetch 스킵)")
parser = argparse.ArgumentParser(description="MTF OOS Dry-run \ud3c9\uac00")
parser.add_argument("--symbol", default="xrpusdt", help="\uc2ec\ubcfc (\ud30c\uc77c\uba85 \uc18c\ubb38\uc790, \uae30\ubcf8: xrpusdt)")
parser.add_argument("--local", action="store_true", help="\ub85c\uceec \ud30c\uc77c\ub9cc \uc0ac\uc6a9 (\uc11c\ubc84 fetch \uc2a4\ud0b5)")
parser.add_argument(
"--scenario",
choices=["fees_only", "realistic", "pessimistic", "all"],
default="all",
help="\ube44\uc6a9 \ubcf4\uc815 \uc2dc\ub098\ub9ac\uc624 (\uae30\ubcf8: all)",
)
args = parser.parse_args()
filename = f"mtf_{args.symbol}.jsonl"
# MTF bot은 ccxt 심볼(XRP/USDT:USDT)에서 /,:를 제거하여 파일명 생성
# → mtf_xrpusdtusdt.jsonl (심볼 인자 xrpusdt → xrpusdtusdt 변환)
raw = args.symbol.lower()
if not raw.endswith("usdt"):
raw = raw + "usdt"
# xrpusdt → xrpusdtusdt (ccxt 포맷 XRP/USDT:USDT 의 슬래시·콜론 제거 결과)
if raw.endswith("usdt") and not raw.endswith("usdtusdt"):
raw = raw + "usdt"
filename = f"mtf_{raw}.jsonl"
if args.local:
local_path = LOCAL_DIR / filename
if not local_path.exists():
print(f"[Error] 로컬 파일 없음: {local_path}")
print(f"[Error] \ub85c\uceec \ud30c\uc77c \uc5c6\uc74c: {local_path}")
sys.exit(1)
else:
local_path = fetch_from_prod(filename)
df = load_trades(local_path)
# 비용 보정 리포트 출력
if args.scenario == "all":
scenario_names = ["fees_only", "realistic", "pessimistic"]
else:
scenario_names = [args.scenario]
print_cost_report(df, scenario_names)
# raw 리포트도 하단에 유지
print_report(df)

364
scripts/fr_oi_backtest.py Normal file
View File

@@ -0,0 +1,364 @@
"""
FR × OI 변화율 백테스트 — Phase 1: 12개 조합
신호: FR × OI변화율(1h) = funding_rate × oi_pct_change_4
- SHORT: 피처 >= threshold (롱 스퀴즈 전조)
- LONG: 피처 <= threshold (숏 스퀴즈 전조)
- 보유: 1h(4캔들) / 4h(16캔들)
Usage: python scripts/fr_oi_backtest.py
"""
import asyncio
import aiohttp
import pandas as pd
import numpy as np
from datetime import datetime, timedelta, timezone
from pathlib import Path
BASE = "https://fapi.binance.com"
SYMBOL = "XRPUSDT"
DATA_DIR = Path("data/xrpusdt")
FEE_RATE = 0.0004
async def fetch_oi_history(session, symbol, start_ms, end_ms):
all_data = []
current = start_ms
calls = 0
while current < end_ms:
params = {"symbol": symbol, "period": "15m", "startTime": current, "endTime": end_ms, "limit": 500}
async with session.get(f"{BASE}/futures/data/openInterestHist", params=params) as resp:
data = await resp.json()
if not data or not isinstance(data, list):
break
all_data.extend(data)
last_ts = int(data[-1]["timestamp"])
if last_ts <= current:
break
current = last_ts + 1
calls += 1
if calls % 50 == 0:
await asyncio.sleep(5)
else:
await asyncio.sleep(0.1)
if not all_data:
return pd.DataFrame()
df = pd.DataFrame(all_data)
df["timestamp"] = pd.to_datetime(df["timestamp"].astype(int), unit="ms", utc=True)
df["oi_value"] = df["sumOpenInterestValue"].astype(float)
return df[["timestamp", "oi_value"]].drop_duplicates("timestamp").sort_values("timestamp")
async def fetch_funding_rate(session, symbol, start_ms, end_ms):
all_data = []
current = start_ms
while current < end_ms:
params = {"symbol": symbol, "startTime": current, "endTime": end_ms, "limit": 1000}
async with session.get(f"{BASE}/fapi/v1/fundingRate", params=params) as resp:
data = await resp.json()
if not data or not isinstance(data, list):
break
all_data.extend(data)
last_ts = int(data[-1]["fundingTime"])
if last_ts <= current:
break
current = last_ts + 1
await asyncio.sleep(0.1)
if not all_data:
return pd.DataFrame()
df = pd.DataFrame(all_data)
df["timestamp"] = pd.to_datetime(df["fundingTime"].astype(int), unit="ms", utc=True)
df["funding_rate"] = df["fundingRate"].astype(float)
return df[["timestamp", "funding_rate"]].drop_duplicates("timestamp").sort_values("timestamp")
def run_backtest(df, feature_col, percentile, direction, hold_bars):
threshold = df[feature_col].quantile(percentile / 100)
trades = []
i = 0
while i < len(df) - hold_bars - 1:
val = df.iloc[i][feature_col]
if pd.isna(val):
i += 1
continue
trigger = False
if direction == "SHORT" and val >= threshold:
trigger = True
elif direction == "LONG" and val <= threshold:
trigger = True
if trigger:
entry_idx = i + 1
exit_idx = i + 1 + hold_bars - 1
if exit_idx >= len(df):
break
entry_price = df.iloc[entry_idx]["open"]
exit_price = df.iloc[exit_idx]["close"]
if direction == "LONG":
gross_return = (exit_price / entry_price) - 1
else:
gross_return = (entry_price / exit_price) - 1
fee = FEE_RATE * 2
net_return = gross_return - fee
trades.append({
"entry_time": df.iloc[entry_idx]["timestamp"],
"exit_time": df.iloc[exit_idx]["timestamp"],
"entry_price": entry_price,
"exit_price": exit_price,
"feature_val": val,
"gross_return_bps": gross_return * 10000,
"net_return_bps": net_return * 10000,
})
i = exit_idx + 1 # 포지션 종료 후 다음
else:
i += 1
if not trades:
return None
tdf = pd.DataFrame(trades)
wins = tdf[tdf["net_return_bps"] > 0]["net_return_bps"]
losses = tdf[tdf["net_return_bps"] <= 0]["net_return_bps"]
gross_profit = wins.sum() if len(wins) > 0 else 0
gross_loss = abs(losses.sum()) if len(losses) > 0 else 0
pf = gross_profit / gross_loss if gross_loss > 0 else float("inf") if gross_profit > 0 else 0
cum_pnl = tdf["net_return_bps"].cumsum()
max_dd = (cum_pnl - cum_pnl.cummax()).min()
return {
"trades": len(tdf),
"wins": len(wins),
"losses": len(losses),
"win_rate": len(wins) / len(tdf) * 100,
"pf": pf,
"total_pnl_bps": tdf["net_return_bps"].sum(),
"avg_pnl_bps": tdf["net_return_bps"].mean(),
"max_dd_bps": max_dd,
"threshold": threshold,
"df_trades": tdf,
}
def confidence(n):
if n < 20:
return "🔴", "폐기"
elif n < 50:
return "🟡", "참고"
else:
return "🟢", "검토"
async def main():
print("=" * 80)
print(" FR × OI 변화율 백테스트 — Phase 1: 12개 조합")
print("=" * 80)
# 데이터 수집
print("\n[1] 데이터 수집")
df_kline = pd.read_parquet(DATA_DIR / "combined_15m.parquet")
end_dt = datetime.now(timezone.utc)
oi_start_dt = end_dt - timedelta(days=29)
oi_start_ms = int(oi_start_dt.replace(microsecond=0, second=0).timestamp()) * 1000
fr_start_ms = oi_start_ms
end_ms = int(end_dt.replace(microsecond=0, second=0).timestamp()) * 1000
async with aiohttp.ClientSession() as session:
print(" OI 수집...")
oi_df = await fetch_oi_history(session, SYMBOL, oi_start_ms, end_ms)
print(f" OI: {len(oi_df)} rows")
print(" FR 수집...")
fr_df = await fetch_funding_rate(session, SYMBOL, fr_start_ms, end_ms)
print(f" FR: {len(fr_df)} rows")
# 병합
print("\n[2] 데이터 병합")
df = df_kline.loc[oi_start_dt:].copy().reset_index()
print(f" Kline (29일): {len(df)} rows")
# OI 병합
df = pd.merge_asof(df.sort_values("timestamp"), oi_df.sort_values("timestamp"),
on="timestamp", direction="nearest", tolerance=pd.Timedelta(minutes=20))
df["oi_pct_change_4"] = df["oi_value"].pct_change(4)
# FR 병합 (forward fill)
df = pd.merge_asof(df.sort_values("timestamp"), fr_df.rename(columns={"funding_rate": "fr_api"}).sort_values("timestamp"),
on="timestamp", direction="backward")
# 핵심 피처: FR × OI변화율(1h)
df["fr_x_oi_1h"] = df["fr_api"] * df["oi_pct_change_4"]
valid = df.dropna(subset=["fr_x_oi_1h"])
print(f" 유효 데이터: {len(valid)} rows")
print(f" fr_x_oi_1h: mean={valid['fr_x_oi_1h'].mean():.8f}, std={valid['fr_x_oi_1h'].std():.8f}")
for p in [25, 50, 75]:
v = valid["fr_x_oi_1h"].quantile(p / 100)
print(f" P{p}: {v:.8f}")
# 12개 조합 백테스트
print("\n[3] 12개 조합 백테스트")
print("=" * 80)
combos = []
for hold_label, hold_bars in [("1h", 4), ("4h", 16)]:
for direction in ["SHORT", "LONG"]:
for pct in [75, 50, 25]:
desc_dir = "롱스퀴즈" if direction == "SHORT" else "숏스퀴즈"
combos.append({
"hold_label": hold_label,
"hold_bars": hold_bars,
"direction": direction,
"percentile": pct,
"desc": f"{direction} {hold_label} P{pct} ({desc_dir})",
})
results = []
for c in combos:
r = run_backtest(valid.reset_index(drop=True), "fr_x_oi_1h",
c["percentile"], c["direction"], c["hold_bars"])
if r:
r.update(c)
else:
r = {**c, "trades": 0, "wins": 0, "losses": 0, "win_rate": 0,
"pf": 0, "total_pnl_bps": 0, "avg_pnl_bps": 0, "max_dd_bps": 0, "threshold": 0}
results.append(r)
# 결과 테이블
print(f"\n{'ID':>3} {'조합':<28} {'거래수':>6} {'승률':>7} {'PF':>7} {'PnL(bps)':>10} {'MaxDD':>10} {'신뢰도'}")
print("-" * 90)
for i, r in enumerate(results, 1):
emoji, label = confidence(r["trades"])
pf_str = f"{r['pf']:.2f}" if r["pf"] != float("inf") else "INF"
print(f"{i:>3} {r['desc']:<28} {r['trades']:>6} {r['win_rate']:>6.1f}% {pf_str:>7} "
f"{r['total_pnl_bps']:>+10.1f} {r['max_dd_bps']:>10.1f} {emoji} {label}")
# 대칭성 검증
print("\n" + "=" * 80)
print(" [대칭성 검증]")
print("=" * 80)
for hold_label in ["1h", "4h"]:
shorts = [r for r in results if r["hold_label"] == hold_label and r["direction"] == "SHORT" and r["trades"] > 0]
longs = [r for r in results if r["hold_label"] == hold_label and r["direction"] == "LONG" and r["trades"] > 0]
best_short = max(shorts, key=lambda x: x["pf"]) if shorts else None
best_long = max(longs, key=lambda x: x["pf"]) if longs else None
print(f"\n [{hold_label} 보유]")
if best_short:
print(f" Best SHORT: {best_short['desc']} — PF={best_short['pf']:.2f}, {best_short['trades']}")
if best_long:
print(f" Best LONG: {best_long['desc']} — PF={best_long['pf']:.2f}, {best_long['trades']}")
if best_short and best_long:
s_pf = best_short["pf"]
l_pf = best_long["pf"]
if s_pf > 1.5 and l_pf > 1.5:
print(f" → Case 1: 양방향 생존 ✓ Phase 2 후보")
elif (s_pf > 1.5 and l_pf < 0.5) or (l_pf > 1.5 and s_pf < 0.5):
print(f" → Case 2: 한쪽만 성공 ✗ 시장 베타/우연")
elif s_pf > 1.5 or l_pf > 1.5:
print(f" → Case 3: 부분적 edge ~ 낮은 신뢰도")
elif s_pf > 1.0 and l_pf > 1.0:
print(f" → 양쪽 PF > 1.0이나 < 1.5 — 약한 edge")
else:
print(f" → 양쪽 모두 약함")
# 보유시간 비교
print("\n" + "=" * 80)
print(" [보유시간 비교]")
print("=" * 80)
for direction in ["SHORT", "LONG"]:
r_1h = [r for r in results if r["hold_label"] == "1h" and r["direction"] == direction and r["trades"] > 0]
r_4h = [r for r in results if r["hold_label"] == "4h" and r["direction"] == direction and r["trades"] > 0]
best_1h = max(r_1h, key=lambda x: x["pf"]) if r_1h else None
best_4h = max(r_4h, key=lambda x: x["pf"]) if r_4h else None
print(f"\n [{direction}]")
if best_1h:
print(f" 1h Best: PF={best_1h['pf']:.2f} ({best_1h['desc']}, {best_1h['trades']}건)")
if best_4h:
print(f" 4h Best: PF={best_4h['pf']:.2f} ({best_4h['desc']}, {best_4h['trades']}건)")
if best_1h and best_4h:
if best_4h["pf"] > best_1h["pf"]:
print(f" → 4h가 더 강함 (상관분석 r=-0.1734과 일치)")
else:
print(f" → 1h가 더 강함 (주의: 상관분석은 4h 기준)")
# 최종 판정
print("\n" + "=" * 80)
print(" [최종 판정]")
print("=" * 80)
# Phase 2 후보 찾기
phase2 = []
for hold_label in ["4h", "1h"]:
shorts = [r for r in results if r["hold_label"] == hold_label and r["direction"] == "SHORT" and r["trades"] >= 20]
longs = [r for r in results if r["hold_label"] == hold_label and r["direction"] == "LONG" and r["trades"] >= 20]
best_s = max(shorts, key=lambda x: x["pf"]) if shorts else None
best_l = max(longs, key=lambda x: x["pf"]) if longs else None
if best_s and best_l:
if best_s["pf"] > 1.5 and best_l["pf"] > 1.5:
phase2.append(("Case1", hold_label, best_s, best_l))
elif best_s["pf"] > 1.5 or best_l["pf"] > 1.5:
phase2.append(("Case3", hold_label, best_s, best_l))
if phase2:
print(f"\n 🟢 Phase 2 후보 발견!")
for case, hl, bs, bl in phase2:
print(f" [{case}] {hl}: SHORT PF={bs['pf']:.2f}({bs['trades']}건), "
f"LONG PF={bl['pf']:.2f}({bl['trades']}건)")
print(f"\n → Phase 2 (Bot Simulation) 진행 권장")
print(f" → 단, 29일 OI 데이터 + 448행 제한 감안")
else:
all_pf = [(r["desc"], r["pf"], r["trades"]) for r in results if r["trades"] > 0]
all_pf.sort(key=lambda x: x[1], reverse=True)
best = all_pf[0] if all_pf else ("N/A", 0, 0)
above_1 = [r for r in results if r["pf"] > 1.0 and r["trades"] >= 20]
if above_1:
print(f"\n 🟡 PF > 1.0 조합 존재 ({len(above_1)}개), 단 < 1.5")
for r in sorted(above_1, key=lambda x: x["pf"], reverse=True):
emoji, _ = confidence(r["trades"])
print(f" {r['desc']}: PF={r['pf']:.2f}, {r['trades']}{emoji}")
print(f"\n → 약한 edge. 4월 데이터 축적 후 재검증 권장.")
else:
print(f"\n 🔴 PF > 1.0 조합 없음 (20건 이상)")
print(f" Best: {best[0]} (PF={best[1]:.2f}, {best[2]}건)")
print(f"\n → FR × OI 시그널도 비용 후 edge 없음")
# Best 조합 상세
valid_results = [r for r in results if r["trades"] > 10 and "df_trades" in r]
if valid_results:
best_r = max(valid_results, key=lambda x: x["pf"])
print(f"\n[참고] Best 조합 상세: {best_r['desc']}")
print("-" * 60)
tdf = best_r["df_trades"]
print(f" 기간: {tdf['entry_time'].min()} ~ {tdf['exit_time'].max()}")
print(f" 평균 피처값: {tdf['feature_val'].mean():.8f}")
w = tdf[tdf["net_return_bps"] > 0]
l = tdf[tdf["net_return_bps"] <= 0]
if len(w) > 0:
print(f" 수익 거래 평균: {w['net_return_bps'].mean():.1f} bps ({len(w)}건)")
if len(l) > 0:
print(f" 손실 거래 평균: {l['net_return_bps'].mean():.1f} bps ({len(l)}건)")
print("\n" + "=" * 80)
print(" 분석 완료.")
print("=" * 80)
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,312 @@
"""
Funding Rate + OI 변화율 상관분석
기존 combined_15m.parquet에 funding_rate 2년치 있음.
OI는 Binance API에서 2개월치 수집 후 병합.
상관분석 → r 값으로 edge 판정.
Usage: python scripts/funding_oi_analysis.py
"""
import asyncio
import aiohttp
import pandas as pd
import numpy as np
from datetime import datetime, timedelta, timezone
from pathlib import Path
import time
BASE = "https://fapi.binance.com"
SYMBOL = "XRPUSDT"
DATA_DIR = Path("data/xrpusdt")
FEE_RATE = 0.0004 # 0.04% per side
async def fetch_oi_history(session, symbol, start_ms, end_ms):
"""Binance Open Interest Statistics (15m) 수집"""
all_data = []
current = start_ms
calls = 0
while current < end_ms:
params = {
"symbol": symbol,
"period": "15m",
"startTime": current,
"endTime": end_ms,
"limit": 500,
}
async with session.get(f"{BASE}/futures/data/openInterestHist", params=params) as resp:
data = await resp.json()
if not data or not isinstance(data, list):
break
all_data.extend(data)
last_ts = int(data[-1]["timestamp"])
if last_ts <= current:
break
current = last_ts + 1
calls += 1
# Rate limit: ~10 weight per call, 1200/min limit
if calls % 50 == 0:
print(f" ... {len(all_data)} rows fetched, sleeping 5s for rate limit")
await asyncio.sleep(5)
else:
await asyncio.sleep(0.1)
if not all_data:
return pd.DataFrame()
df = pd.DataFrame(all_data)
df["timestamp"] = pd.to_datetime(df["timestamp"].astype(int), unit="ms", utc=True)
df["sumOpenInterest"] = df["sumOpenInterest"].astype(float)
df["sumOpenInterestValue"] = df["sumOpenInterestValue"].astype(float)
return df[["timestamp", "sumOpenInterest", "sumOpenInterestValue"]].drop_duplicates("timestamp").sort_values("timestamp")
async def fetch_funding_rate_history(session, symbol, start_ms, end_ms):
"""Binance Funding Rate History 수집 (8시간 간격)"""
all_data = []
current = start_ms
while current < end_ms:
params = {
"symbol": symbol,
"startTime": current,
"endTime": end_ms,
"limit": 1000,
}
async with session.get(f"{BASE}/fapi/v1/fundingRate", params=params) as resp:
data = await resp.json()
if not data or not isinstance(data, list):
break
all_data.extend(data)
last_ts = int(data[-1]["fundingTime"])
if last_ts <= current:
break
current = last_ts + 1
await asyncio.sleep(0.1)
if not all_data:
return pd.DataFrame()
df = pd.DataFrame(all_data)
df["timestamp"] = pd.to_datetime(df["fundingTime"].astype(int), unit="ms", utc=True)
df["funding_rate_api"] = df["fundingRate"].astype(float)
return df[["timestamp", "funding_rate_api"]].drop_duplicates("timestamp").sort_values("timestamp")
async def main():
print("=" * 80)
print(" Funding Rate + OI 변화율 상관분석")
print("=" * 80)
# Step 1: 데이터 수집
print("\n[Step 1] 데이터 수집")
# 기존 kline 로드
kline_path = DATA_DIR / "combined_15m.parquet"
df = pd.read_parquet(kline_path)
print(f" 기존 kline: {len(df)} rows ({df.index.min()} ~ {df.index.max()})")
# 기간 설정: OI는 30일 제한, FR은 무제한
end_dt = datetime.now(timezone.utc)
oi_start_dt = end_dt - timedelta(days=29) # OI: 30일 제한
fr_start_dt = end_dt - timedelta(days=60) # FR: 60일
kline_start_dt = fr_start_dt # kline도 60일
# Clean timestamps (no microseconds)
oi_start_ms = int(oi_start_dt.replace(microsecond=0, second=0).timestamp()) * 1000
fr_start_ms = int(fr_start_dt.replace(microsecond=0, second=0).timestamp()) * 1000
end_ms = int(end_dt.replace(microsecond=0, second=0).timestamp()) * 1000
print(f" OI 수집 기간: {oi_start_dt.date()} ~ {end_dt.date()} (29일)")
print(f" FR 수집 기간: {fr_start_dt.date()} ~ {end_dt.date()} (60일)")
async with aiohttp.ClientSession() as session:
print(" OI 수집 중...")
oi_df = await fetch_oi_history(session, SYMBOL, oi_start_ms, end_ms)
print(f" OI: {len(oi_df)} rows")
print(" Funding Rate 수집 중...")
fr_df = await fetch_funding_rate_history(session, SYMBOL, fr_start_ms, end_ms)
print(f" Funding Rate: {len(fr_df)} rows")
# Step 2: 병합
print("\n[Step 2] 데이터 병합")
# 2개월 kline 슬라이스
df_2m = df.loc[kline_start_dt:].copy()
print(f" 2개월 kline: {len(df_2m)} rows")
# OI 병합 (merge_asof)
df_2m = df_2m.reset_index()
if not oi_df.empty:
df_2m = pd.merge_asof(
df_2m.sort_values("timestamp"),
oi_df.sort_values("timestamp"),
on="timestamp",
direction="nearest",
tolerance=pd.Timedelta(minutes=20),
)
# OI 변화율 계산
df_2m["oi"] = df_2m["sumOpenInterestValue"]
df_2m["oi_pct_change"] = df_2m["oi"].pct_change()
df_2m["oi_pct_change_4"] = df_2m["oi"].pct_change(4) # 1시간 변화율
print(f" OI 매칭: {df_2m['oi'].notna().sum()} rows")
# Funding Rate 병합 (8h → 15m forward fill)
if not fr_df.empty:
df_2m = pd.merge_asof(
df_2m.sort_values("timestamp"),
fr_df.sort_values("timestamp"),
on="timestamp",
direction="backward", # 가장 최근 funding rate 사용
)
# Funding rate 변화율
df_2m["fr"] = df_2m["funding_rate_api"]
df_2m["fr_change"] = df_2m["fr"].diff()
print(f" Funding Rate 매칭: {df_2m['fr'].notna().sum()} rows")
# 기존 funding_rate 컬럼도 활용
df_2m["fr_existing"] = df_2m["funding_rate"]
df_2m["fr_existing_change"] = df_2m["fr_existing"].diff()
# 미래 수익률 계산
df_2m["next_1h_return"] = df_2m["close"].shift(-4) / df_2m["close"] - 1
df_2m["next_4h_return"] = df_2m["close"].shift(-16) / df_2m["close"] - 1
df_2m["next_15m_return"] = df_2m["close"].shift(-1) / df_2m["close"] - 1
# 복합 피처
if "oi_pct_change" in df_2m.columns and "fr" in df_2m.columns:
df_2m["fr_x_oi"] = df_2m["fr"] * df_2m["oi_pct_change"] # 펀딩비 × OI변화율
df_2m["fr_x_oi_4"] = df_2m["fr"] * df_2m["oi_pct_change_4"]
df_2m = df_2m.set_index("timestamp")
# OI velocity (변화율의 변화율)
if "oi_pct_change" in df_2m.columns:
df_2m["oi_velocity"] = df_2m["oi_pct_change"].diff()
df_2m["oi_acceleration"] = df_2m["oi_velocity"].diff()
print(f"\n 최종 데이터셋: {len(df_2m)} rows, {len(df_2m.columns)} columns")
# Step 3: 상관분석
print("\n[Step 3] 상관분석")
print("=" * 80)
features = [
("fr_existing", "Funding Rate (기존)"),
("fr_existing_change", "ΔFunding Rate"),
("fr", "Funding Rate (API)"),
("fr_change", "ΔFunding Rate (API)"),
("oi_pct_change", "OI 변화율 (15m)"),
("oi_pct_change_4", "OI 변화율 (1h)"),
("oi_velocity", "OI Velocity"),
("oi_acceleration", "OI Acceleration"),
("fr_x_oi", "FR × OI변화율"),
("fr_x_oi_4", "FR × OI변화율(1h)"),
]
targets = [
("next_15m_return", "다음 15m"),
("next_1h_return", "다음 1h"),
("next_4h_return", "다음 4h"),
]
print(f"\n{'피처':<25} {'→15m':>8} {'→1h':>8} {'→4h':>8} {'N':>7}")
print("-" * 60)
strong_signals = []
for feat_col, feat_name in features:
if feat_col not in df_2m.columns:
continue
corrs = []
n = 0
for tgt_col, _ in targets:
valid = df_2m[[feat_col, tgt_col]].dropna()
n = len(valid)
if n > 50:
r = valid[feat_col].corr(valid[tgt_col])
corrs.append(r)
else:
corrs.append(float("nan"))
r_strs = [f"{r:>+8.4f}" if not np.isnan(r) else f"{'N/A':>8}" for r in corrs]
print(f"{feat_name:<25} {''.join(r_strs)} {n:>7}")
# 강한 시그널 체크 (|r| > 0.05)
for r, (tgt_col, tgt_name) in zip(corrs, targets):
if not np.isnan(r) and abs(r) > 0.05:
strong_signals.append((feat_name, tgt_name, r, n))
# Quintile 분석 (강한 시그널에 대해)
print("\n" + "=" * 80)
print(" [Quintile 분석] |r| > 0.05 피처")
print("=" * 80)
for feat_col, feat_name in features:
if feat_col not in df_2m.columns:
continue
for tgt_col, tgt_name in targets:
valid = df_2m[[feat_col, tgt_col]].dropna()
if len(valid) < 100:
continue
r = valid[feat_col].corr(valid[tgt_col])
if abs(r) < 0.05:
continue
print(f"\n {feat_name}{tgt_name} (r={r:+.4f}, n={len(valid)})")
print(f" {'Quintile':<12} {'mean_feat':>12} {'return_bps':>12} {'win_rate':>10} {'count':>7}")
print(" " + "-" * 55)
try:
valid["q"] = pd.qcut(valid[feat_col], 5, labels=["Q1", "Q2", "Q3", "Q4", "Q5"], duplicates="drop")
except ValueError:
continue
for q in valid["q"].cat.categories:
grp = valid[valid["q"] == q]
if len(grp) == 0:
continue
mr = grp[feat_col].mean()
ret = grp[tgt_col].mean() * 10000
wr = (grp[tgt_col] > 0).mean() * 100
print(f" {q:<12} {mr:>12.6f} {ret:>+12.2f} {wr:>9.1f}% {len(grp):>7}")
# 판정
print("\n" + "=" * 80)
print(" [최종 판정]")
print("=" * 80)
if strong_signals:
print(f"\n |r| > 0.05 시그널: {len(strong_signals)}")
for feat, tgt, r, n in sorted(strong_signals, key=lambda x: abs(x[2]), reverse=True):
marker = "🟢" if abs(r) > 0.15 else "🟡" if abs(r) > 0.10 else ""
print(f" {marker} {feat}{tgt}: r={r:+.4f} (n={n})")
best_r = max(abs(r) for _, _, r, _ in strong_signals)
if best_r > 0.15:
print(f"\n ✅ r > 0.15 시그널 발견! 백테스트 진행 가치 있음")
elif best_r > 0.10:
print(f"\n 🟡 r = 0.10~0.15. L/S ratio(0.1158)과 비슷한 수준.")
print(f" 단, 2개월 데이터(8일 대비 7.5배)이므로 신뢰도 높음.")
print(f" 백테스트로 비용 후 PF 확인 필요.")
else:
print(f"\n ⚠️ 최대 |r| = {best_r:.4f}. 약한 시그널.")
print(f" 비용(0.08%) 커버 가능성 낮음.")
else:
print("\n 🔴 |r| > 0.05 시그널 없음. Edge 없음.")
print("\n" + "=" * 80)
print(" 분석 완료.")
print("=" * 80)
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,485 @@
"""
L/S Ratio 단독 백테스트 — Phase 1: Pure Edge Test
6개 조합 (3 임계값 × 2 방향) 스윕, 3단계 필터 판정.
데이터: 프로덕션 수집 L/S ratio + Binance kline (같은 기간).
Usage: python scripts/ls_ratio_backtest.py
"""
import asyncio
import aiohttp
import pandas as pd
import numpy as np
from datetime import timezone
from pathlib import Path
BASE = "https://fapi.binance.com"
DATA_DIR = Path("data")
SYMBOL = "XRPUSDT"
FEE_RATE = 0.0004 # 0.04% per side
HOLD_BARS = 4 # 4 candles = 1 hour
async def fetch_klines(session, symbol, start_ms, end_ms):
"""Binance kline 데이터 가져오기"""
all_klines = []
current = start_ms
while current < end_ms:
params = {
"symbol": symbol, "interval": "15m",
"startTime": current, "endTime": end_ms, "limit": 1500,
}
async with session.get(f"{BASE}/fapi/v1/klines", params=params) as resp:
data = await resp.json()
if not data:
break
all_klines.extend(data)
current = data[-1][0] + 1
return all_klines
def load_ls_ratio(symbol):
"""프로덕션 수집 L/S ratio 로드"""
path = DATA_DIR / symbol.lower() / "ls_ratio_15m.parquet"
if not path.exists():
raise FileNotFoundError(f"{path} not found. Sync from production first.")
df = pd.read_parquet(path)
df["timestamp"] = pd.to_datetime(df["timestamp"], utc=True)
return df.sort_values("timestamp").reset_index(drop=True)
def build_dataset(klines_raw, ls_df):
"""Kline + L/S ratio 조인"""
df = pd.DataFrame(klines_raw, columns=[
"open_time", "open", "high", "low", "close", "volume",
"close_time", "quote_vol", "trades", "taker_buy_vol",
"taker_buy_quote_vol", "ignore",
])
df["timestamp"] = pd.to_datetime(df["open_time"], unit="ms", utc=True)
for c in ["open", "high", "low", "close", "volume"]:
df[c] = df[c].astype(float)
# L/S ratio 조인 (가장 가까운 타임스탬프)
df = df.sort_values("timestamp").reset_index(drop=True)
merged = pd.merge_asof(
df, ls_df, on="timestamp", direction="nearest",
tolerance=pd.Timedelta(minutes=20),
)
return merged
def run_backtest(df, percentile, direction, hold_bars=HOLD_BARS):
"""
단일 조합 백테스트 실행.
- percentile: L/S ratio 임계값 (0~100)
- direction: "LONG" or "SHORT"
- hold_bars: 보유 캔들 수
LONG 진입: ratio >= threshold (Momentum)
SHORT 진입: ratio <= threshold (Momentum)
"""
threshold = df["top_acct_ls_ratio"].quantile(percentile / 100)
trades = []
i = 0
while i < len(df) - hold_bars:
ratio = df.iloc[i]["top_acct_ls_ratio"]
if pd.isna(ratio):
i += 1
continue
# 시그널 체크
if direction == "LONG" and ratio >= threshold:
entry_price = df.iloc[i + 1]["open"] # 다음 캔들 시가 진입
exit_price = df.iloc[i + 1 + hold_bars - 1]["close"] # hold_bars 후 종가
gross_return = (exit_price / entry_price) - 1
fee = FEE_RATE * 2 # 진입 + 청산
net_return = gross_return - fee
trades.append({
"entry_time": df.iloc[i + 1]["timestamp"],
"exit_time": df.iloc[i + 1 + hold_bars - 1]["timestamp"],
"entry_price": entry_price,
"exit_price": exit_price,
"entry_ls_ratio": ratio,
"gross_return_bps": gross_return * 10000,
"net_return_bps": net_return * 10000,
"fee_bps": fee * 10000,
})
i += 1 + hold_bars # 포지션 종료 후 다음 캔들부터
elif direction == "SHORT" and ratio <= threshold:
entry_price = df.iloc[i + 1]["open"]
exit_price = df.iloc[i + 1 + hold_bars - 1]["close"]
gross_return = (entry_price / exit_price) - 1 # SHORT: 반대
fee = FEE_RATE * 2
net_return = gross_return - fee
trades.append({
"entry_time": df.iloc[i + 1]["timestamp"],
"exit_time": df.iloc[i + 1 + hold_bars - 1]["timestamp"],
"entry_price": entry_price,
"exit_price": exit_price,
"entry_ls_ratio": ratio,
"gross_return_bps": gross_return * 10000,
"net_return_bps": net_return * 10000,
"fee_bps": fee * 10000,
})
i += 1 + hold_bars
else:
i += 1
if not trades:
return None
df_trades = pd.DataFrame(trades)
# PF 계산: Σ(net profit) / Σ(|net loss|)
wins = df_trades[df_trades["net_return_bps"] > 0]["net_return_bps"]
losses = df_trades[df_trades["net_return_bps"] <= 0]["net_return_bps"]
gross_profit = wins.sum() if len(wins) > 0 else 0
gross_loss = abs(losses.sum()) if len(losses) > 0 else 0
pf = gross_profit / gross_loss if gross_loss > 0 else float("inf") if gross_profit > 0 else 0
# Max Drawdown (cumulative bps)
cum_pnl = df_trades["net_return_bps"].cumsum()
running_max = cum_pnl.cummax()
drawdown = cum_pnl - running_max
max_dd = drawdown.min()
return {
"trades": len(df_trades),
"wins": len(wins),
"losses": len(losses),
"win_rate": len(wins) / len(df_trades) * 100,
"pf": pf,
"total_pnl_bps": df_trades["net_return_bps"].sum(),
"avg_pnl_bps": df_trades["net_return_bps"].mean(),
"max_dd_bps": max_dd,
"threshold": threshold,
"df_trades": df_trades,
}
def confidence_emoji(n_trades):
if n_trades < 20:
return "🔴"
elif n_trades < 50:
return "🟡"
elif n_trades < 100:
return "🟢"
else:
return "🟢"
def confidence_label(n_trades):
if n_trades < 20:
return "폐기(과적합)"
elif n_trades < 50:
return "낮음(참고만)"
elif n_trades < 100:
return "보통(검토)"
else:
return "높음(우선)"
async def main():
print("=" * 80)
print(" L/S Ratio 단독 백테스트 — Phase 1: Pure Edge Test")
print("=" * 80)
# 1. 데이터 로드
print("\n[1] 데이터 로드")
ls_df = load_ls_ratio(SYMBOL)
print(f" L/S ratio: {len(ls_df)} rows ({ls_df['timestamp'].min()} ~ {ls_df['timestamp'].max()})")
start_ms = int(ls_df["timestamp"].min().timestamp() * 1000)
end_ms = int(ls_df["timestamp"].max().timestamp() * 1000)
async with aiohttp.ClientSession() as session:
klines = await fetch_klines(session, SYMBOL, start_ms, end_ms)
print(f" Klines: {len(klines)} rows")
df = build_dataset(klines, ls_df)
valid = df.dropna(subset=["top_acct_ls_ratio"])
print(f" 조인 결과: {len(df)} rows (L/S 매칭: {len(valid)})")
print(f" top_acct_ls_ratio: mean={valid['top_acct_ls_ratio'].mean():.4f}, "
f"std={valid['top_acct_ls_ratio'].std():.4f}")
# 백분위수 표시
for p in [25, 50, 75]:
v = valid["top_acct_ls_ratio"].quantile(p / 100)
print(f" P{p}: {v:.4f}")
# 2. 6개 조합 백테스트
print("\n[2] 6개 조합 백테스트 실행")
print("-" * 80)
combinations = [
(75, "LONG", "모멘텀 강함: ratio ≥ P75 → LONG"),
(75, "SHORT", "역모멘텀: ratio ≥ P75 → SHORT"),
(50, "LONG", "모멘텀 중간: ratio ≥ P50 → LONG"),
(50, "SHORT", "역모멘텀 중간: ratio ≤ P50 → SHORT"),
(25, "LONG", "역모멘텀 약: ratio ≤ P25 → LONG"),
(25, "SHORT", "모멘텀 강함: ratio ≤ P25 → SHORT"),
]
results = []
for pct, direction, desc in combinations:
# LONG: ratio >= threshold, SHORT: ratio <= threshold
# 25th percentile LONG = ratio가 낮을 때 LONG (Contrarian)
# 실제 로직:
# 75 LONG = ratio >= P75 (상위 25% 롱비율 높을 때 롱) = Momentum
# 75 SHORT = ratio >= P75 (상위 25% 롱비율 높을 때 숏) = Contrarian
# 25 SHORT = ratio <= P25 (하위 25% 롱비율 낮을 때 숏) = Momentum
# 25 LONG = ratio <= P25 (하위 25% 롱비율 낮을 때 롱) = Contrarian
# 방향 보정: 25th에서 LONG은 "ratio <= P25일 때 LONG" (Contrarian)
if pct == 25 and direction == "LONG":
# 특수 케이스: 낮은 ratio에서 LONG (Contrarian)
result = run_backtest_contrarian(df, pct, "LONG")
elif pct == 25 and direction == "SHORT":
# ratio <= P25일 때 SHORT (Momentum)
result = run_backtest(df, pct, "SHORT")
elif pct == 75 and direction == "SHORT":
# ratio >= P75일 때 SHORT (Contrarian)
result = run_backtest_contrarian(df, pct, "SHORT")
else:
result = run_backtest(df, pct, direction)
if result:
result["percentile"] = pct
result["direction"] = direction
result["description"] = desc
results.append(result)
else:
results.append({
"percentile": pct, "direction": direction,
"description": desc, "trades": 0, "pf": 0,
"win_rate": 0, "total_pnl_bps": 0, "max_dd_bps": 0,
"threshold": 0, "wins": 0, "losses": 0, "avg_pnl_bps": 0,
})
# 3. 결과 테이블
print("\n[3] 결과 테이블")
print("=" * 80)
print(f"{'조합':<35} {'거래수':>6} {'승률':>7} {'PF':>7} {'PnL(bps)':>10} {'MaxDD':>10} {'신뢰도':<15}")
print("-" * 80)
for r in results:
emoji = confidence_emoji(r["trades"])
label = confidence_label(r["trades"])
pf_str = f"{r['pf']:.2f}" if r["pf"] != float("inf") else "INF"
print(f"{r['description']:<35} {r['trades']:>6} {r['win_rate']:>6.1f}% {pf_str:>7} "
f"{r['total_pnl_bps']:>+10.1f} {r['max_dd_bps']:>10.1f} {emoji} {label}")
# 4. 필터 1: PF 판정
print("\n[4] 필터 1: PF 판정")
print("-" * 80)
strong = [r for r in results if r["pf"] > 1.5 and r["trades"] > 0]
weak = [r for r in results if 0.5 <= r["pf"] <= 1.5 and r["trades"] > 0]
failed = [r for r in results if r["pf"] < 0.5 and r["trades"] > 0]
print(f" PF > 1.5 (명확한 edge): {len(strong)}개 조합")
for r in strong:
print(f"{r['description']} (PF={r['pf']:.2f}, trades={r['trades']})")
print(f" 0.5 ≤ PF ≤ 1.5 (보류): {len(weak)}개 조합")
for r in weak:
print(f" ~ {r['description']} (PF={r['pf']:.2f}, trades={r['trades']})")
print(f" PF < 0.5 (실패): {len(failed)}개 조합")
for r in failed:
print(f"{r['description']} (PF={r['pf']:.2f}, trades={r['trades']})")
# 5. 필터 2: 거래수 신뢰도
print("\n[5] 필터 2: 거래수 신뢰도 (필터 1 통과 조합)")
print("-" * 80)
filter2_passed = [r for r in strong if r["trades"] >= 20]
filter2_ref = [r for r in strong if r["trades"] < 20]
if filter2_passed:
for r in filter2_passed:
print(f"{r['description']}{r['trades']}건 ({confidence_label(r['trades'])})")
else:
print(" ⚠️ PF > 1.5 조합 중 거래수 20건 이상인 것 없음")
if filter2_ref:
for r in filter2_ref:
print(f" 🔴 {r['description']}{r['trades']}건 (폐기: 과적합)")
# 6. 필터 3: 대칭성 판정
print("\n[6] 필터 3: 대칭성 판정")
print("-" * 80)
# 같은 percentile에서 LONG/SHORT 양쪽 확인
for pct in [75, 50, 25]:
long_r = next((r for r in results if r["percentile"] == pct and r["direction"] == "LONG"), None)
short_r = next((r for r in results if r["percentile"] == pct and r["direction"] == "SHORT"), None)
if not long_r or not short_r:
continue
l_pf = long_r["pf"]
s_pf = short_r["pf"]
if l_pf > 1.5 and s_pf > 1.5:
verdict = "Case 1: 양방향 생존 → ✓ Phase 2 후보"
elif (l_pf > 1.5 and s_pf < 0.5) or (s_pf > 1.5 and l_pf < 0.5):
verdict = "Case 2: 한쪽만 성공 → ✗ 시장 베타/우연 (폐기)"
elif l_pf > 1.5 or s_pf > 1.5:
verdict = "Case 3: 부분적 edge → ~ 낮은 신뢰도"
else:
verdict = "양쪽 모두 약함 → 해당 없음"
print(f" P{pct}: LONG PF={l_pf:.2f}, SHORT PF={s_pf:.2f}")
print(f"{verdict}")
# 7. 최종 판정
print("\n" + "=" * 80)
print(" [최종 판정]")
print("=" * 80)
# Phase 2 후보 찾기
phase2_candidates = []
for pct in [75, 50, 25]:
long_r = next((r for r in results if r["percentile"] == pct and r["direction"] == "LONG"), None)
short_r = next((r for r in results if r["percentile"] == pct and r["direction"] == "SHORT"), None)
if not long_r or not short_r:
continue
# Case 1: 양방향 PF > 1.5
if long_r["pf"] > 1.5 and short_r["pf"] > 1.5:
if long_r["trades"] >= 20 and short_r["trades"] >= 20:
phase2_candidates.append(("Case1", pct, long_r, short_r))
# Case 3: 한쪽만 PF > 1.5
elif long_r["pf"] > 1.5 and long_r["trades"] >= 20:
phase2_candidates.append(("Case3-LONG", pct, long_r, short_r))
elif short_r["pf"] > 1.5 and short_r["trades"] >= 20:
phase2_candidates.append(("Case3-SHORT", pct, long_r, short_r))
if phase2_candidates:
print("\n 🟢 Phase 2 진행 후보 발견!")
for case, pct, lr, sr in phase2_candidates:
print(f" [{case}] P{pct}: LONG PF={lr['pf']:.2f}({lr['trades']}건), "
f"SHORT PF={sr['pf']:.2f}({sr['trades']}건)")
print("\n → Phase 2 (Bot Simulation) 진행 권장")
print(" → 단, 8일 데이터이므로 4월 15일 재검증 필수")
else:
# 모든 조합 중 최고 PF
best = max(results, key=lambda r: r["pf"] if r["trades"] > 0 else 0)
if best["pf"] > 1.0:
print(f"\n 🟡 필터 미통과이나 PF > 1.0 조합 존재")
print(f" Best: {best['description']} (PF={best['pf']:.2f}, {best['trades']}건)")
print(f"\n → 데이터 부족. 4월 15일까지 수집 후 재검증")
else:
print(f"\n 🔴 PF > 1.0 조합 없음")
print(f" Best: {best['description']} (PF={best['pf']:.2f}, {best['trades']}건)")
print(f"\n → L/S ratio 단독 시그널로는 edge 없음")
print(f" → 다른 데이터 소스 탐색 권장")
# 8. 추가: 전 구간 상세 (best 조합)
best = max(results, key=lambda r: r["pf"] if r["trades"] > 10 else 0)
if "df_trades" in best and best["trades"] > 0:
print(f"\n[참고] Best 조합 상세: {best['description']}")
print("-" * 60)
tdf = best["df_trades"]
print(f" 거래 기간: {tdf['entry_time'].min()} ~ {tdf['exit_time'].max()}")
print(f" 평균 진입 L/S ratio: {tdf['entry_ls_ratio'].mean():.4f}")
print(f" 수익 거래 평균: {tdf[tdf['net_return_bps']>0]['net_return_bps'].mean():.1f} bps")
if len(tdf[tdf['net_return_bps'] <= 0]) > 0:
print(f" 손실 거래 평균: {tdf[tdf['net_return_bps']<=0]['net_return_bps'].mean():.1f} bps")
print(f" 최대 연승: ", end="")
streaks = []
streak = 0
for _, row in tdf.iterrows():
if row["net_return_bps"] > 0:
streak += 1
else:
if streak > 0:
streaks.append(streak)
streak = 0
if streak > 0:
streaks.append(streak)
print(f"{max(streaks) if streaks else 0}연승")
print("\n" + "=" * 80)
print(" 분석 완료. 결과를 바탕으로 의사결정하세요.")
print("=" * 80)
def run_backtest_contrarian(df, percentile, direction, hold_bars=HOLD_BARS):
"""
Contrarian 방향 백테스트.
- P25 + LONG: ratio <= P25일 때 LONG (낮은 ratio에서 롱)
- P75 + SHORT: ratio >= P75일 때 SHORT (높은 ratio에서 숏)
"""
threshold = df["top_acct_ls_ratio"].quantile(percentile / 100)
trades = []
i = 0
while i < len(df) - hold_bars:
ratio = df.iloc[i]["top_acct_ls_ratio"]
if pd.isna(ratio):
i += 1
continue
trigger = False
if direction == "LONG" and ratio <= threshold:
trigger = True
elif direction == "SHORT" and ratio >= threshold:
trigger = True
if trigger:
entry_price = df.iloc[i + 1]["open"]
exit_price = df.iloc[i + 1 + hold_bars - 1]["close"]
if direction == "LONG":
gross_return = (exit_price / entry_price) - 1
else:
gross_return = (entry_price / exit_price) - 1
fee = FEE_RATE * 2
net_return = gross_return - fee
trades.append({
"entry_time": df.iloc[i + 1]["timestamp"],
"exit_time": df.iloc[i + 1 + hold_bars - 1]["timestamp"],
"entry_price": entry_price,
"exit_price": exit_price,
"entry_ls_ratio": ratio,
"gross_return_bps": gross_return * 10000,
"net_return_bps": net_return * 10000,
"fee_bps": fee * 10000,
})
i += 1 + hold_bars
else:
i += 1
if not trades:
return None
df_trades = pd.DataFrame(trades)
wins = df_trades[df_trades["net_return_bps"] > 0]["net_return_bps"]
losses = df_trades[df_trades["net_return_bps"] <= 0]["net_return_bps"]
gross_profit = wins.sum() if len(wins) > 0 else 0
gross_loss = abs(losses.sum()) if len(losses) > 0 else 0
pf = gross_profit / gross_loss if gross_loss > 0 else float("inf") if gross_profit > 0 else 0
cum_pnl = df_trades["net_return_bps"].cumsum()
running_max = cum_pnl.cummax()
max_dd = (cum_pnl - running_max).min()
return {
"trades": len(df_trades),
"wins": len(wins),
"losses": len(losses),
"win_rate": len(wins) / len(df_trades) * 100,
"pf": pf,
"total_pnl_bps": df_trades["net_return_bps"].sum(),
"avg_pnl_bps": df_trades["net_return_bps"].mean(),
"max_dd_bps": max_dd,
"threshold": threshold,
"df_trades": df_trades,
}
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,601 @@
"""
MTF Pullback + BTC 추세 필터 백테스트
──────────────────────────────────────
기존 MTF Pullback 전략에 BTC 추세 필터를 추가하여 검증.
메인 가설 (사전 확정):
BTC 1h + EMA 50/200 + ADX > 20
sweep 결과와 무관하게 사후 변경하지 않음.
판정 흐름:
1. 베이스라인(필터 없음) IS/OOS 결과 산출
2. 12개 sweep IS, 메인 가설 OOS 검증
3. 나머지 11개 OOS robustness 체크
Usage:
python scripts/mtf_btc_filter_backtest.py
python scripts/mtf_btc_filter_backtest.py --symbol xrpusdt
"""
import sys
from pathlib import Path
from dataclasses import dataclass, field
from itertools import product
import pandas as pd
import pandas_ta as ta
import numpy as np
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from src.config import COST_MODEL, COST_SCENARIOS # noqa: E402
# ─── 설정 ──────────────────────────────────────────────────────────
SYMBOL = "xrpusdt"
DATA_PATH = Path(f"data/{SYMBOL}/combined_15m.parquet")
# XRP 1h 메타필터 (기존 MTF bot 설정 그대로)
MTF_EMA_FAST = 50
MTF_EMA_SLOW = 200
MTF_ADX_THRESHOLD = 20
# 15m Trigger
EMA_PULLBACK_LEN = 20
VOL_DRY_RATIO = 0.5
# SL/TP
ATR_SL_MULT = 1.5
ATR_TP_MULT = 2.3
# IS/OOS 분할
IS_RATIO = 0.7
# ─── Sweep 그리드 ─────────────────────────────────────────────────
SWEEP_GRID = {
"btc_tf": ["1h", "4h", "1D"],
"btc_ema_fast": [20, 50],
"btc_ema_slow": [100, 200],
}
# BTC ADX 임계값 — 전 조합 고정
BTC_ADX_THRESHOLD = 20
# 메인 가설 (사전 확정 — commitment device)
MAIN_HYPOTHESIS = {"btc_tf": "1h", "btc_ema_fast": 50, "btc_ema_slow": 200}
# IS 거래 수 최소 기준
MIN_IS_TRADES = 100
@dataclass
class Trade:
entry_time: pd.Timestamp
entry_price: float
side: str
sl: float
tp: float
btc_trend: str = ""
exit_time: pd.Timestamp | None = None
exit_price: float | None = None
pnl_bps: float | None = None
reason: str = ""
def build_xrp_1h(df_15m: pd.DataFrame) -> pd.DataFrame:
"""XRP 15m → 1h: EMA50, EMA200, ADX, ATR."""
df_1h = df_15m[["open", "high", "low", "close", "volume"]].resample("1h").agg(
{"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}
).dropna()
df_1h["ema50_1h"] = ta.ema(df_1h["close"], length=MTF_EMA_FAST)
df_1h["ema200_1h"] = ta.ema(df_1h["close"], length=MTF_EMA_SLOW)
adx_df = ta.adx(df_1h["high"], df_1h["low"], df_1h["close"], length=14)
df_1h["adx_1h"] = adx_df["ADX_14"]
df_1h["atr_1h"] = ta.atr(df_1h["high"], df_1h["low"], df_1h["close"], length=14)
return df_1h[["ema50_1h", "ema200_1h", "adx_1h", "atr_1h"]]
def build_btc_resampled(df_15m: pd.DataFrame, tf: str, ema_fast: int, ema_slow: int) -> pd.DataFrame:
"""BTC 15m → 지정 타임프레임: EMA + ADX."""
btc_cols = {"open_btc": "open", "high_btc": "high", "low_btc": "low",
"close_btc": "close", "volume_btc": "volume"}
df_btc = df_15m[list(btc_cols.keys())].rename(columns=btc_cols)
df_rs = df_btc.resample(tf).agg(
{"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}
).dropna()
df_rs[f"btc_ema_fast"] = ta.ema(df_rs["close"], length=ema_fast)
df_rs[f"btc_ema_slow"] = ta.ema(df_rs["close"], length=ema_slow)
adx_df = ta.adx(df_rs["high"], df_rs["low"], df_rs["close"], length=14)
df_rs["btc_adx"] = adx_df["ADX_14"]
return df_rs[["btc_ema_fast", "btc_ema_slow", "btc_adx"]]
def merge_higher_tf(df_15m: pd.DataFrame, df_htf: pd.DataFrame, tf: str) -> pd.DataFrame:
"""Look-ahead bias 방지 merge. 1h → +1h shift, 4h → +4h shift, 1d → +1d shift."""
shift_map = {"1h": pd.Timedelta(hours=1), "4h": pd.Timedelta(hours=4),
"1D": pd.Timedelta(days=1)}
df_shifted = df_htf.copy()
df_shifted.index = df_shifted.index + shift_map[tf]
df_15m_r = df_15m.reset_index()
df_htf_r = df_shifted.reset_index()
ts_col_15m = df_15m_r.columns[0]
ts_col_htf = df_htf_r.columns[0]
df_15m_r.rename(columns={ts_col_15m: "timestamp"}, inplace=True)
df_htf_r.rename(columns={ts_col_htf: "timestamp"}, inplace=True)
df_15m_r["timestamp"] = pd.to_datetime(df_15m_r["timestamp"]).astype("datetime64[us]")
df_htf_r["timestamp"] = pd.to_datetime(df_htf_r["timestamp"]).astype("datetime64[us]")
merged = pd.merge_asof(
df_15m_r.sort_values("timestamp"),
df_htf_r.sort_values("timestamp"),
on="timestamp",
direction="backward",
)
return merged.set_index("timestamp")
def get_xrp_meta(row) -> str:
"""XRP 1h 메타필터."""
ema50 = row.get("ema50_1h")
ema200 = row.get("ema200_1h")
adx = row.get("adx_1h")
if pd.isna(ema50) or pd.isna(ema200) or pd.isna(adx):
return "HOLD"
if adx < MTF_ADX_THRESHOLD:
return "HOLD"
return "LONG" if ema50 > ema200 else "SHORT"
def get_btc_trend(row) -> str:
"""BTC 추세 필터."""
ema_f = row.get("btc_ema_fast")
ema_s = row.get("btc_ema_slow")
adx = row.get("btc_adx")
if pd.isna(ema_f) or pd.isna(ema_s) or pd.isna(adx):
return "NEUTRAL"
if adx < BTC_ADX_THRESHOLD:
return "NEUTRAL"
return "UP" if ema_f > ema_s else "DOWN"
def run_backtest(df: pd.DataFrame, use_btc_filter: bool) -> list[Trade]:
"""MTF Pullback 백테스트 실행."""
trades: list[Trade] = []
in_trade = False
current_trade: Trade | None = None
pullback_ready = False
pullback_side = ""
for i in range(1, len(df)):
row = df.iloc[i]
# ── SL/TP 체크 ──
if in_trade and current_trade is not None:
hit_sl = hit_tp = False
if current_trade.side == "LONG":
hit_sl = row["low"] <= current_trade.sl
hit_tp = row["high"] >= current_trade.tp
else:
hit_sl = row["high"] >= current_trade.sl
hit_tp = row["low"] <= current_trade.tp
if hit_sl or hit_tp:
exit_price = current_trade.sl if hit_sl else current_trade.tp
if hit_sl and hit_tp:
exit_price = current_trade.sl # 보수적
if current_trade.side == "LONG":
raw_pnl = (exit_price - current_trade.entry_price) / current_trade.entry_price
else:
raw_pnl = (current_trade.entry_price - exit_price) / current_trade.entry_price
current_trade.exit_time = df.index[i]
current_trade.exit_price = exit_price
current_trade.pnl_bps = raw_pnl * 10000 # raw bps (비용 미반영)
current_trade.reason = "SL" if hit_sl else "TP"
trades.append(current_trade)
in_trade = False
current_trade = None
if in_trade:
continue
# NaN 체크
if pd.isna(row.get("ema20")) or pd.isna(row.get("vol_ma20")) or pd.isna(row.get("atr_1h")):
pullback_ready = False
continue
# ── XRP 1h Meta ──
meta = get_xrp_meta(row)
if meta == "HOLD":
pullback_ready = False
continue
# ── BTC 추세 필터 ──
btc_trend = get_btc_trend(row) if use_btc_filter else "DISABLED"
if use_btc_filter:
# BTC UP → LONG만, BTC DOWN → SHORT만, NEUTRAL → 차단
if btc_trend == "UP" and meta != "LONG":
pullback_ready = False
continue
elif btc_trend == "DOWN" and meta != "SHORT":
pullback_ready = False
continue
elif btc_trend == "NEUTRAL":
pullback_ready = False
continue
# ── Pullback 감지 → 재개 확인 ──
if pullback_ready and pullback_side == meta:
if pullback_side == "LONG" and row["close"] > row["ema20"]:
if i + 1 < len(df):
next_row = df.iloc[i + 1]
entry_price = next_row["open"]
atr = row["atr_1h"]
current_trade = Trade(
entry_time=df.index[i + 1], entry_price=entry_price,
side="LONG", sl=entry_price - atr * ATR_SL_MULT,
tp=entry_price + atr * ATR_TP_MULT, btc_trend=btc_trend,
)
in_trade = True
pullback_ready = False
continue
elif pullback_side == "SHORT" and row["close"] < row["ema20"]:
if i + 1 < len(df):
next_row = df.iloc[i + 1]
entry_price = next_row["open"]
atr = row["atr_1h"]
current_trade = Trade(
entry_time=df.index[i + 1], entry_price=entry_price,
side="SHORT", sl=entry_price + atr * ATR_SL_MULT,
tp=entry_price - atr * ATR_TP_MULT, btc_trend=btc_trend,
)
in_trade = True
pullback_ready = False
continue
# ── Pullback 감지 ──
vol_dry = row["volume"] < row["vol_ma20"] * VOL_DRY_RATIO
if meta == "LONG" and row["close"] < row["ema20"] and vol_dry:
pullback_ready = True
pullback_side = "LONG"
elif meta == "SHORT" and row["close"] > row["ema20"] and vol_dry:
pullback_ready = True
pullback_side = "SHORT"
elif meta != pullback_side:
pullback_ready = False
return trades
def apply_cost(trades: list[Trade], scenario_name: str) -> list[float]:
"""거래 리스트에 비용 시나리오 적용, adjusted pnl_bps 리스트 반환."""
scenario = COST_SCENARIOS[scenario_name]
fee_per_side = COST_MODEL["taker_fee_bps"] # 현재 전부 taker
fee_roundtrip = fee_per_side * 2
slippage_roundtrip = scenario["slippage_bps_per_side"] * 2
adjusted = []
for t in trades:
# 펀딩비: 보유 시간 중 8h 경계 교차 수
if t.entry_time is not None and t.exit_time is not None:
dur_h = (t.exit_time - t.entry_time).total_seconds() / 3600
funding_events = max(0, int(dur_h / 8))
else:
funding_events = 0
funding_cost = funding_events * scenario["funding_bps_per_8h"]
total_cost = fee_roundtrip + slippage_roundtrip + funding_cost
adjusted.append(t.pnl_bps - total_cost)
return adjusted
def calc_metrics(pnl_list: list[float]) -> dict:
"""pnl_bps 리스트로 메트릭 계산."""
if not pnl_list:
return {"trades": 0, "win_rate": 0.0, "pf": 0.0, "cum_pnl": 0.0, "avg_pnl": 0.0}
wins = [p for p in pnl_list if p > 0]
losses = [p for p in pnl_list if p <= 0]
gross_profit = sum(wins) if wins else 0
gross_loss = abs(sum(losses)) if losses else 0
pf = gross_profit / gross_loss if gross_loss > 0 else float("inf")
return {
"trades": len(pnl_list),
"win_rate": round(len(wins) / len(pnl_list) * 100, 1),
"pf": round(pf, 2),
"cum_pnl": round(sum(pnl_list), 1),
"avg_pnl": round(sum(pnl_list) / len(pnl_list), 2),
}
def split_is_oos(trades: list[Trade], split_ts: pd.Timestamp):
"""IS/OOS 분할."""
is_trades = [t for t in trades if t.entry_time < split_ts]
oos_trades = [t for t in trades if t.entry_time >= split_ts]
return is_trades, oos_trades
def print_metrics_row(label: str, raw: dict, fees: dict, realistic: dict):
"""한 줄 메트릭 출력."""
print(f" {label:<8} {raw['trades']:>5} {raw['win_rate']:>5.1f}% "
f"{raw['pf']:>5.2f} {fees['pf']:>5.2f} {realistic['pf']:>5.2f} "
f"{raw['cum_pnl']:>+8.1f} {fees['cum_pnl']:>+8.1f}")
def print_section(title: str, trades: list[Trade]):
"""섹션별 메트릭 출력."""
if not trades:
print(f"\n [{title}] 거래 없음")
return
raw_all = [t.pnl_bps for t in trades]
fees_all = apply_cost(trades, "fees_only")
real_all = apply_cost(trades, "realistic")
long_t = [t for t in trades if t.side == "LONG"]
short_t = [t for t in trades if t.side == "SHORT"]
raw_l = [t.pnl_bps for t in long_t]
raw_s = [t.pnl_bps for t in short_t]
fees_l = apply_cost(long_t, "fees_only")
fees_s = apply_cost(short_t, "fees_only")
real_l = apply_cost(long_t, "realistic")
real_s = apply_cost(short_t, "realistic")
print(f"\n [{title}]")
print(f" {'':8} {'N':>5} {'WR':>6} {'RawPF':>5} {'FeePF':>5} {'RealPF':>5} {'RawPnL':>8} {'FeePnL':>8}")
print(f" {'-'*62}")
print_metrics_row("Total", calc_metrics(raw_all), calc_metrics(fees_all), calc_metrics(real_all))
print_metrics_row("LONG", calc_metrics(raw_l), calc_metrics(fees_l), calc_metrics(real_l))
print_metrics_row("SHORT", calc_metrics(raw_s), calc_metrics(fees_s), calc_metrics(real_s))
def save_trade_log(trades: list[Trade], filepath: Path, combo_label: str):
"""거래 수준 CSV 로그 저장."""
rows = []
for t in trades:
rows.append({
"combo": combo_label,
"entry_time": t.entry_time,
"exit_time": t.exit_time,
"side": t.side,
"entry_price": t.entry_price,
"exit_price": t.exit_price,
"pnl_bps": t.pnl_bps,
"reason": t.reason,
"btc_trend": t.btc_trend,
})
df = pd.DataFrame(rows)
mode = "a" if filepath.exists() else "w"
header = not filepath.exists()
df.to_csv(filepath, mode=mode, header=header, index=False)
def main():
import argparse
parser = argparse.ArgumentParser(description="MTF + BTC 추세 필터 백테스트")
parser.add_argument("--symbol", default="xrpusdt")
args = parser.parse_args()
data_path = Path(f"data/{args.symbol}/combined_15m.parquet")
print("=" * 72)
print(" MTF Pullback + BTC 추세 필터 백테스트")
print(f" 메인 가설: BTC {MAIN_HYPOTHESIS['btc_tf']} EMA{MAIN_HYPOTHESIS['btc_ema_fast']}/{MAIN_HYPOTHESIS['btc_ema_slow']} ADX>{BTC_ADX_THRESHOLD}")
print("=" * 72)
# ── 데이터 로드 ──
df_raw = pd.read_parquet(data_path)
if df_raw.index.tz is not None:
df_raw.index = df_raw.index.tz_localize(None)
# EMA200 워밍업 (200h × 4 + 여유 = 1000 bars)
warmup_bars = 1000
df_full = df_raw.iloc[warmup_bars:].copy() if len(df_raw) > warmup_bars else df_raw.copy()
# 워밍업 포함 전체 데이터로 지표 계산
df_calc = df_raw.copy()
print(f"\n데이터: {len(df_raw)} bars total, 분석: {len(df_full)} bars")
print(f"기간: {df_full.index[0]} ~ {df_full.index[-1]}")
print(f"일수: {(df_full.index[-1] - df_full.index[0]).days}")
# ── IS/OOS 분할 ──
split_idx = int(len(df_full) * IS_RATIO)
split_ts = df_full.index[split_idx]
print(f"IS/OOS 분할: IS ~{split_ts.date()} | OOS {split_ts.date()}~")
# ── XRP 15m 지표 ──
df_calc["ema20"] = ta.ema(df_calc["close"], length=EMA_PULLBACK_LEN)
df_calc["vol_ma20"] = ta.sma(df_calc["volume"], length=20)
# ── XRP 1h 지표 ──
df_1h = build_xrp_1h(df_calc)
df_merged_base = merge_higher_tf(df_calc, df_1h, "1h")
# 분석 기간만 슬라이스
df_analysis = df_merged_base[df_merged_base.index >= df_full.index[0]].copy()
# ── 1. 베이스라인 (BTC 필터 없음) ──
print("\n" + "=" * 72)
print(" BASELINE (BTC 필터 없음)")
print("=" * 72)
baseline_trades = run_backtest(df_analysis, use_btc_filter=False)
baseline_is, baseline_oos = split_is_oos(baseline_trades, split_ts)
print_section("IS (베이스라인)", baseline_is)
print_section("OOS (베이스라인)", baseline_oos)
# ── 2. Sweep ──
print("\n" + "=" * 72)
print(" SWEEP (12개 조합)")
print("=" * 72)
combos = list(product(
SWEEP_GRID["btc_tf"],
SWEEP_GRID["btc_ema_fast"],
SWEEP_GRID["btc_ema_slow"],
))
trade_log_path = Path(f"results/{args.symbol}/mtf_btc_filter_trades.csv")
trade_log_path.parent.mkdir(parents=True, exist_ok=True)
if trade_log_path.exists():
trade_log_path.unlink()
results = []
for btc_tf, ema_f, ema_s in combos:
if ema_f >= ema_s:
continue # fast >= slow는 무의미
label = f"BTC_{btc_tf}_EMA{ema_f}/{ema_s}"
is_main = (btc_tf == MAIN_HYPOTHESIS["btc_tf"] and
ema_f == MAIN_HYPOTHESIS["btc_ema_fast"] and
ema_s == MAIN_HYPOTHESIS["btc_ema_slow"])
# BTC 지표 계산 + merge
df_btc = build_btc_resampled(df_calc, btc_tf, ema_f, ema_s)
df_with_btc = merge_higher_tf(df_analysis, df_btc, btc_tf)
# 백테스트
trades = run_backtest(df_with_btc, use_btc_filter=True)
is_trades, oos_trades = split_is_oos(trades, split_ts)
# IS 거래 수 체크
if len(is_trades) < MIN_IS_TRADES:
status = "SKIP(IS<100)"
else:
status = "MAIN" if is_main else "sweep"
# 메트릭
is_raw = calc_metrics([t.pnl_bps for t in is_trades])
is_fees = calc_metrics(apply_cost(is_trades, "fees_only"))
oos_raw = calc_metrics([t.pnl_bps for t in oos_trades])
oos_fees = calc_metrics(apply_cost(oos_trades, "fees_only"))
oos_real = calc_metrics(apply_cost(oos_trades, "realistic"))
# LONG/SHORT 분리 (OOS)
oos_long = [t for t in oos_trades if t.side == "LONG"]
oos_short = [t for t in oos_trades if t.side == "SHORT"]
oos_fees_l = calc_metrics(apply_cost(oos_long, "fees_only"))
oos_fees_s = calc_metrics(apply_cost(oos_short, "fees_only"))
results.append({
"label": label, "is_main": is_main, "status": status,
"is_trades": is_raw["trades"], "is_raw_pf": is_raw["pf"],
"is_fees_pf": is_fees["pf"],
"oos_trades": oos_raw["trades"], "oos_raw_pf": oos_raw["pf"],
"oos_fees_pf": oos_fees["pf"], "oos_real_pf": oos_real["pf"],
"oos_fees_pnl": oos_fees["cum_pnl"],
"oos_long_fees_pf": oos_fees_l["pf"], "oos_short_fees_pf": oos_fees_s["pf"],
"oos_long_n": oos_fees_l["trades"], "oos_short_n": oos_fees_s["trades"],
})
# 거래 로그 저장
save_trade_log(trades, trade_log_path, label)
# ── Sweep 결과 테이블 ──
print(f"\n {'Label':<22} {'St':>6} {'IS_N':>5} {'IS_FPF':>6} "
f"{'OOS_N':>5} {'OOS_RPF':>7} {'OOS_FPF':>7} {'OOS_rPF':>7} "
f"{'L_FPF':>6} {'S_FPF':>6}")
print(f" {'-'*92}")
for r in results:
marker = "" if r["is_main"] else ""
print(f" {r['label']:<22} {r['status']:>6} {r['is_trades']:>5} {r['is_fees_pf']:>6.2f} "
f"{r['oos_trades']:>5} {r['oos_raw_pf']:>7.2f} {r['oos_fees_pf']:>7.2f} {r['oos_real_pf']:>7.2f} "
f"{r['oos_long_fees_pf']:>6.2f} {r['oos_short_fees_pf']:>6.2f}{marker}")
# ── 3. 메인 가설 상세 결과 ──
main_result = next((r for r in results if r["is_main"]), None)
if main_result is None:
print("\n [ERROR] 메인 가설 결과 없음")
return
print("\n" + "=" * 72)
print(f" 메인 가설 상세: {main_result['label']}")
print("=" * 72)
# 메인 가설 재실행하여 상세 출력
df_btc_main = build_btc_resampled(
df_calc, MAIN_HYPOTHESIS["btc_tf"],
MAIN_HYPOTHESIS["btc_ema_fast"], MAIN_HYPOTHESIS["btc_ema_slow"])
df_main = merge_higher_tf(df_analysis, df_btc_main, MAIN_HYPOTHESIS["btc_tf"])
main_trades = run_backtest(df_main, use_btc_filter=True)
main_is, main_oos = split_is_oos(main_trades, split_ts)
print_section("IS (메인 가설)", main_is)
print_section("OOS (메인 가설)", main_oos)
# ── 4. 판정 ──
print("\n" + "=" * 72)
print(" 판정")
print("=" * 72)
# 베이스라인 비교
bl_oos_fees = calc_metrics(apply_cost(baseline_oos, "fees_only"))
main_oos_fees = calc_metrics(apply_cost(main_oos, "fees_only"))
main_oos_real = calc_metrics(apply_cost(main_oos, "realistic"))
print(f"\n 베이스라인 OOS fees_only PF: {bl_oos_fees['pf']:.2f} ({bl_oos_fees['trades']}건)")
print(f" 메인 가설 OOS fees_only PF: {main_oos_fees['pf']:.2f} ({main_oos_fees['trades']}건)")
print(f" 메인 가설 OOS realistic PF: {main_oos_real['pf']:.2f}")
print(f" 개선폭: fees_only PF {main_oos_fees['pf'] - bl_oos_fees['pf']:+.2f}")
# 합격 기준 체크
checks = []
checks.append(("OOS fees_only PF >= 1.2", main_oos_fees["pf"] >= 1.2, f"{main_oos_fees['pf']:.2f}"))
checks.append(("OOS realistic PF >= 1.0", main_oos_real["pf"] >= 1.0, f"{main_oos_real['pf']:.2f}"))
checks.append(("OOS 거래수 >= 50", main_oos_fees["trades"] >= 50, f"{main_oos_fees['trades']}"))
# LONG/SHORT 대칭성
oos_long_t = [t for t in main_oos if t.side == "LONG"]
oos_short_t = [t for t in main_oos if t.side == "SHORT"]
l_pf = calc_metrics(apply_cost(oos_long_t, "fees_only"))["pf"]
s_pf = calc_metrics(apply_cost(oos_short_t, "fees_only"))["pf"]
checks.append(("LONG/SHORT fees PF >= 0.8", l_pf >= 0.8 and s_pf >= 0.8, f"L:{l_pf:.2f} S:{s_pf:.2f}"))
# IS/OOS 격차
main_is_fees = calc_metrics(apply_cost(main_is, "fees_only"))
if main_is_fees["pf"] > 0:
gap = abs(main_oos_fees["pf"] - main_is_fees["pf"]) / main_is_fees["pf"]
else:
gap = 1.0
checks.append(("IS/OOS PF 격차 < 30%", gap < 0.3, f"{gap*100:.1f}%"))
# 베이스라인 대비 개선
improvement = main_oos_fees["pf"] > bl_oos_fees["pf"]
checks.append(("베이스라인 대비 개선", improvement,
f"{main_oos_fees['pf']:.2f} vs {bl_oos_fees['pf']:.2f}"))
print(f"\n 합격 기준 체크:")
all_pass = True
for desc, passed, val in checks:
icon = "PASS" if passed else "FAIL"
print(f" [{icon}] {desc}: {val}")
if not passed:
all_pass = False
print()
if all_pass:
print(" ★ [최종 판정: PASS] BTC 추세 필터가 유효합니다.")
else:
print(" ✗ [최종 판정: FAIL] BTC 추세 필터로도 기준 미달. MTF 전략 폐기.")
# robustness 요약
passing_combos = [r for r in results if r["status"] != "SKIP(IS<100)" and r["oos_fees_pf"] >= 1.2]
print(f"\n Robustness: {len(passing_combos)}/{len(results)} 조합이 OOS fees_only PF >= 1.2")
print(f"\n 거래 로그: {trade_log_path}")
print()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,49 @@
"""
4월 15일 재검증 스크립트 — L/S ratio + FR×OI 동시 재실행
crontab: 0 10 15 4 * cd /root/cointrader && /root/cointrader/.venv/bin/python scripts/revalidate_apr15.py
재검증 대상:
1. L/S ratio (top_acct_ls_ratio) — 24일 데이터로 6개 조합
2. FR × OI변화율(1h) — 29일 데이터로 12개 조합
3. 대칭성 재판정
Usage: python scripts/revalidate_apr15.py
"""
import subprocess
import sys
from datetime import datetime, timezone
def main():
now = datetime.now(timezone.utc)
print("=" * 80)
print(f" 4월 재검증 실행 — {now.strftime('%Y-%m-%d %H:%M UTC')}")
print("=" * 80)
print("\n[1/2] L/S ratio 백테스트 재실행")
print("-" * 40)
r1 = subprocess.run(
[sys.executable, "scripts/ls_ratio_backtest.py"],
capture_output=False,
)
print("\n\n[2/2] FR × OI 백테스트 재실행")
print("-" * 40)
r2 = subprocess.run(
[sys.executable, "scripts/fr_oi_backtest.py"],
capture_output=False,
)
print("\n" + "=" * 80)
print(" 재검증 완료")
print("=" * 80)
print(f"\n L/S ratio: {'성공' if r1.returncode == 0 else '실패'}")
print(f" FR × OI: {'성공' if r2.returncode == 0 else '실패'}")
print(f"\n 판정 기준:")
print(f" - L/S ratio: PF > 1.0인 조합 있으면 재검토")
print(f" - FR × OI: SHORT+LONG 모두 PF > 1.0이면 대칭성 통과")
print(f" - 둘 다 실패 시 확정 폐기")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,459 @@
"""
Trade History + L/S Ratio 종합 분석
- 봇 대시보드 API에서 거래 기록 로드
- Binance API에서 L/S ratio (30일) 로드 + 로컬 parquet 병합
- 진입/청산 시점 L/S ratio 매칭
- 수익/손실 거래별 L/S 분포 분석
- L/S 임계값 필터링 시뮬레이션
Usage: python scripts/trade_ls_analysis.py [--api URL]
"""
import asyncio
import aiohttp
import pandas as pd
import numpy as np
from datetime import datetime, timedelta, timezone
from pathlib import Path
import argparse
import json
BASE = "https://fapi.binance.com"
DASHBOARD_API = "http://10.1.10.24:8080/api/trades"
DATA_DIR = Path("data")
SYMBOLS_FOR_LS = ["XRPUSDT", "BTCUSDT", "ETHUSDT"]
async def fetch_json(session, url, params=None):
async with session.get(url, params=params) as resp:
return await resp.json()
async def fetch_ls_ratios_from_api(session, symbol, start_ms, end_ms):
"""Binance API에서 L/S ratio 전체 기간 가져오기 (페이징)"""
all_top_acct = []
all_global = []
for endpoint, target in [
(f"{BASE}/futures/data/topLongShortAccountRatio", all_top_acct),
(f"{BASE}/futures/data/globalLongShortAccountRatio", all_global),
]:
current = start_ms
while current < end_ms:
params = {
"symbol": symbol,
"period": "15m",
"startTime": current,
"endTime": end_ms,
"limit": 500,
}
data = await fetch_json(session, endpoint, params)
if not data or not isinstance(data, list):
break
target.extend(data)
last_ts = int(data[-1]["timestamp"])
if last_ts <= current:
break
current = last_ts + 1
def to_df(data, col_name):
if not data:
return pd.DataFrame()
df = pd.DataFrame(data)
df["timestamp"] = pd.to_datetime(df["timestamp"].astype(int), unit="ms", utc=True)
df[col_name] = df["longShortRatio"].astype(float)
return df[["timestamp", col_name]].drop_duplicates("timestamp")
df_top = to_df(all_top_acct, "top_acct_ls_ratio")
df_global = to_df(all_global, "global_ls_ratio")
if df_top.empty and df_global.empty:
return pd.DataFrame()
if df_top.empty:
return df_global
if df_global.empty:
return df_top
return df_top.merge(df_global, on="timestamp", how="outer").sort_values("timestamp")
def load_local_ls_ratio(symbol):
"""로컬 parquet에서 L/S ratio 로드"""
path = DATA_DIR / symbol.lower() / "ls_ratio_15m.parquet"
if not path.exists():
return pd.DataFrame()
df = pd.read_parquet(path)
if "timestamp" in df.columns:
df["timestamp"] = pd.to_datetime(df["timestamp"], utc=True)
return df
def find_nearest_ls(ls_df, target_time, max_gap_minutes=30):
"""타겟 시간에 가장 가까운 L/S ratio 찾기"""
if ls_df.empty:
return None, None, None
target = pd.Timestamp(target_time, tz="UTC")
diffs = (ls_df["timestamp"] - target).abs()
idx = diffs.idxmin()
gap = diffs[idx]
if gap > pd.Timedelta(minutes=max_gap_minutes):
return None, None, gap
row = ls_df.loc[idx]
return row.get("top_acct_ls_ratio"), row.get("global_ls_ratio"), gap
def classify_signal(trade):
"""진입 신호 분류"""
rsi = trade.get("rsi", 0)
macd = trade.get("macd_hist", 0)
direction = trade["direction"]
signals = []
if direction == "LONG":
if rsi and rsi > 65:
signals.append("RSI과매수진입")
elif rsi and rsi < 35:
signals.append("RSI역방향")
if macd and macd > 0:
signals.append("MACD+")
elif macd and macd < 0:
signals.append("MACD역방향")
else: # SHORT
if rsi and rsi < 35:
signals.append("RSI과매도진입")
elif rsi and rsi > 65:
signals.append("RSI역방향")
if macd and macd < 0:
signals.append("MACD-")
elif macd and macd > 0:
signals.append("MACD역방향")
return ", ".join(signals) if signals else "복합신호"
def classify_close_reason(trade):
"""청산 이유 분류"""
reason = trade["close_reason"]
if reason == "TP":
return "TP(익절)"
elif reason == "SYNC":
return "SL(손절)"
elif reason == "MANUAL":
# MANUAL인데 SL가격과 exit가격이 같으면 SL
sl = trade.get("sl")
exit_p = trade.get("exit_price")
if sl and exit_p and abs(float(sl) - float(exit_p)) < 0.0001:
return "SL(손절)"
# 역방향 시그널로 청산
extra = trade.get("extra", "{}")
if isinstance(extra, str):
try:
extra = json.loads(extra)
except json.JSONDecodeError:
extra = {}
if extra.get("recovery"):
return "신호반전"
return "SL(손절)" # 대부분 MANUAL은 SL 히트
return reason
async def main():
parser = argparse.ArgumentParser()
parser.add_argument("--api", default=DASHBOARD_API)
args = parser.parse_args()
print("=" * 80)
print(" Trade History + L/S Ratio 종합 분석")
print("=" * 80)
# 1. 거래 데이터 로드
async with aiohttp.ClientSession() as session:
trade_data = await fetch_json(session, args.api)
trades = trade_data["trades"]
print(f"\n📊 거래 데이터: {len(trades)}건 로드")
# 2. L/S ratio 데이터 로드 (API + local)
# 가장 오래된 거래 기준으로 시작 시간 설정
earliest = min(t["entry_time"] for t in trades)
start_dt = pd.Timestamp(earliest, tz="UTC") - timedelta(hours=1)
end_dt = datetime.now(timezone.utc)
start_ms = int(start_dt.timestamp() * 1000)
end_ms = int(end_dt.timestamp() * 1000)
print(f"📡 Binance API에서 L/S ratio 로딩 ({start_dt.date()} ~ {end_dt.date()})...")
ls_data = {}
for sym in SYMBOLS_FOR_LS:
api_df = await fetch_ls_ratios_from_api(session, sym, start_ms, end_ms)
local_df = load_local_ls_ratio(sym)
if not api_df.empty and not local_df.empty:
combined = pd.concat([api_df, local_df]).drop_duplicates("timestamp").sort_values("timestamp")
elif not api_df.empty:
combined = api_df
elif not local_df.empty:
combined = local_df
else:
combined = pd.DataFrame()
ls_data[sym] = combined.reset_index(drop=True)
print(f" {sym}: {len(ls_data[sym])} rows ({ls_data[sym]['timestamp'].min()} ~ {ls_data[sym]['timestamp'].max()})" if not combined.empty else f" {sym}: no data")
# 3. 거래별 L/S ratio 매칭
print("\n" + "=" * 80)
print(" 1. 거래 기록 + L/S Ratio 매칭")
print("=" * 80)
enriched = []
for t in trades:
sym = t["symbol"]
# XRP 거래에는 XRP L/S, 다른 심볼도 XRP L/S 참조 (크로스 분석)
ls_sym = ls_data.get(sym, pd.DataFrame())
ls_xrp = ls_data.get("XRPUSDT", pd.DataFrame())
ls_btc = ls_data.get("BTCUSDT", pd.DataFrame())
entry_top, entry_global, _ = find_nearest_ls(ls_sym if not ls_sym.empty else ls_xrp, t["entry_time"])
exit_top, exit_global, _ = find_nearest_ls(ls_sym if not ls_sym.empty else ls_xrp, t["exit_time"])
# BTC L/S for cross-reference
btc_entry_top, btc_entry_global, _ = find_nearest_ls(ls_btc, t["entry_time"])
enriched.append({
"id": t["id"],
"symbol": sym,
"direction": t["direction"],
"entry_time": t["entry_time"],
"exit_time": t["exit_time"],
"signal": classify_signal(t),
"close_reason": classify_close_reason(t),
"rsi": t.get("rsi"),
"macd_hist": t.get("macd_hist"),
"entry_top_acct_ls": entry_top,
"entry_global_ls": entry_global,
"exit_top_acct_ls": exit_top,
"exit_global_ls": exit_global,
"ls_change_top": (exit_top - entry_top) if entry_top and exit_top else None,
"ls_change_global": (exit_global - entry_global) if entry_global and exit_global else None,
"btc_entry_top_ls": btc_entry_top,
"btc_entry_global_ls": btc_entry_global,
"net_pnl": t["net_pnl"],
"is_win": t["net_pnl"] > 0,
"entry_price": t["entry_price"],
"exit_price": t["exit_price"],
})
df = pd.DataFrame(enriched)
# 거래 기록 테이블 출력
print(f"\n{'ID':>3} {'심볼':<10} {'방향':<5} {'진입시간':<20} {'진입신호':<16} "
f"{'진입L/S':>7} {'청산L/S':>7} {'ΔL/S':>7} {'청산이유':<10} {'PnL':>8}")
print("-" * 120)
for _, r in df.iterrows():
entry_ls = f"{r['entry_top_acct_ls']:.3f}" if pd.notna(r['entry_top_acct_ls']) else "N/A"
exit_ls = f"{r['exit_top_acct_ls']:.3f}" if pd.notna(r['exit_top_acct_ls']) else "N/A"
delta_ls = f"{r['ls_change_top']:+.3f}" if pd.notna(r['ls_change_top']) else "N/A"
pnl_str = f"{r['net_pnl']:+.4f}"
print(f"{r['id']:>3} {r['symbol']:<10} {r['direction']:<5} {r['entry_time']:<20} {r['signal']:<16} "
f"{entry_ls:>7} {exit_ls:>7} {delta_ls:>7} {r['close_reason']:<10} {pnl_str:>8}")
# 4. 수익 거래 vs 손실 거래 L/S 비교
print("\n" + "=" * 80)
print(" 2. 수익 거래 vs 손실 거래: L/S Ratio 비교")
print("=" * 80)
has_ls = df.dropna(subset=["entry_top_acct_ls"])
if len(has_ls) > 0:
wins = has_ls[has_ls["is_win"]]
losses = has_ls[~has_ls["is_win"]]
print(f"\n L/S ratio 매칭된 거래: {len(has_ls)}건 (수익: {len(wins)}, 손실: {len(losses)})")
print(f"\n {'지표':<30} {'수익 거래':>12} {'손실 거래':>12} {'차이':>10}")
print(" " + "-" * 70)
for col, label in [
("entry_top_acct_ls", "진입 시 top_acct L/S"),
("entry_global_ls", "진입 시 global L/S"),
("exit_top_acct_ls", "청산 시 top_acct L/S"),
("exit_global_ls", "청산 시 global L/S"),
("ls_change_top", "진입→청산 ΔL/S (top)"),
("ls_change_global", "진입→청산 ΔL/S (global)"),
("btc_entry_top_ls", "BTC 진입 시 top_acct L/S"),
]:
w_vals = wins[col].dropna()
l_vals = losses[col].dropna()
if len(w_vals) > 0 and len(l_vals) > 0:
w_mean = w_vals.mean()
l_mean = l_vals.mean()
diff = w_mean - l_mean
print(f" {label:<30} {w_mean:>12.4f} {l_mean:>12.4f} {diff:>+10.4f}")
else:
w_str = f"{w_vals.mean():.4f}" if len(w_vals) > 0 else "N/A"
l_str = f"{l_vals.mean():.4f}" if len(l_vals) > 0 else "N/A"
print(f" {label:<30} {w_str:>12} {l_str:>12} {'N/A':>10}")
else:
print("\n ⚠️ L/S ratio 매칭 가능한 거래가 없습니다")
# 5. 진입 시점 L/S와 거래 결과의 상관계수
print("\n" + "=" * 80)
print(" 3. L/S Ratio ↔ PnL 상관계수")
print("=" * 80)
for col, label in [
("entry_top_acct_ls", "진입 top_acct L/S"),
("entry_global_ls", "진입 global L/S"),
("btc_entry_top_ls", "BTC 진입 top_acct L/S"),
("ls_change_top", "ΔL/S (top)"),
]:
valid = df.dropna(subset=[col, "net_pnl"])
if len(valid) >= 3:
corr = valid[col].corr(valid["net_pnl"])
print(f" {label:<30} r = {corr:>+.4f} (n={len(valid)})")
else:
print(f" {label:<30} 데이터 부족 (n={len(valid)})")
# 6. 방향별 분석 (LONG 진입 시 L/S 높으면? SHORT 진입 시 낮으면?)
print("\n" + "=" * 80)
print(" 4. 방향별 L/S Ratio 분석")
print("=" * 80)
for direction in ["LONG", "SHORT"]:
subset = has_ls[has_ls["direction"] == direction]
if len(subset) == 0:
continue
print(f"\n [{direction}] ({len(subset)}건)")
wins_d = subset[subset["is_win"]]
losses_d = subset[~subset["is_win"]]
print(f" 수익: {len(wins_d)}건, 손실: {len(losses_d)}")
if len(subset) > 0:
for col in ["entry_top_acct_ls", "entry_global_ls"]:
vals = subset[col].dropna()
if len(vals) > 0:
w = wins_d[col].dropna()
l = losses_d[col].dropna()
w_str = f"{w.mean():.4f}" if len(w) > 0 else "N/A"
l_str = f"{l.mean():.4f}" if len(l) > 0 else "N/A"
print(f" {col}: 수익평균={w_str}, 손실평균={l_str}")
# 7. 청산 이유별 L/S ratio 분포
print("\n" + "=" * 80)
print(" 5. 청산 이유별 L/S Ratio 분포")
print("=" * 80)
for reason in df["close_reason"].unique():
subset = has_ls[has_ls["close_reason"] == reason]
if len(subset) == 0:
continue
print(f"\n [{reason}] ({len(subset)}건)")
for col, label in [("entry_top_acct_ls", "진입 L/S"), ("exit_top_acct_ls", "청산 L/S"), ("ls_change_top", "ΔL/S")]:
vals = subset[col].dropna()
if len(vals) > 0:
print(f" {label}: mean={vals.mean():.4f}, std={vals.std():.4f}, min={vals.min():.4f}, max={vals.max():.4f}")
# 8. L/S 임계값 필터링 시뮬레이션
print("\n" + "=" * 80)
print(" 6. L/S 임계값 필터링 시뮬레이션")
print(" '만약 L/S 조건으로 진입을 필터링했다면?'")
print("=" * 80)
if len(has_ls) > 0:
# 시뮬레이션 1: top_acct L/S ratio 기준 필터
print("\n [A] top_acct_ls_ratio 임계값별 (LONG 진입 시 ratio > threshold)")
print(f" {'Threshold':>10} {'통과':>5} {'차단':>5} {'통과 PnL':>10} {'차단 PnL':>10} {'통과 승률':>10} {'원본 승률':>10}")
print(" " + "-" * 70)
longs = has_ls[has_ls["direction"] == "LONG"]
shorts = has_ls[has_ls["direction"] == "SHORT"]
all_wr = has_ls["is_win"].mean() * 100 if len(has_ls) > 0 else 0
if len(longs) > 0:
ls_vals = longs["entry_top_acct_ls"].dropna()
if len(ls_vals) > 0:
for pct in [0.25, 0.50, 0.75]:
threshold = ls_vals.quantile(pct)
passed = longs[longs["entry_top_acct_ls"] >= threshold]
blocked = longs[longs["entry_top_acct_ls"] < threshold]
p_pnl = passed["net_pnl"].sum()
b_pnl = blocked["net_pnl"].sum()
p_wr = passed["is_win"].mean() * 100 if len(passed) > 0 else 0
print(f" {threshold:>10.4f} {len(passed):>5} {len(blocked):>5} "
f"{p_pnl:>+10.4f} {b_pnl:>+10.4f} {p_wr:>9.1f}% {all_wr:>9.1f}%")
# 시뮬레이션 2: SHORT 진입 시 ratio < threshold
print(f"\n [B] top_acct_ls_ratio 임계값별 (SHORT 진입 시 ratio < threshold)")
print(f" {'Threshold':>10} {'통과':>5} {'차단':>5} {'통과 PnL':>10} {'차단 PnL':>10} {'통과 승률':>10}")
print(" " + "-" * 70)
if len(shorts) > 0:
ls_vals = shorts["entry_top_acct_ls"].dropna()
if len(ls_vals) > 0:
for pct in [0.75, 0.50, 0.25]:
threshold = ls_vals.quantile(pct)
passed = shorts[shorts["entry_top_acct_ls"] <= threshold]
blocked = shorts[shorts["entry_top_acct_ls"] > threshold]
p_pnl = passed["net_pnl"].sum()
b_pnl = blocked["net_pnl"].sum()
p_wr = passed["is_win"].mean() * 100 if len(passed) > 0 else 0
print(f" {threshold:>10.4f} {len(passed):>5} {len(blocked):>5} "
f"{p_pnl:>+10.4f} {b_pnl:>+10.4f} {p_wr:>9.1f}%")
# 시뮬레이션 3: Momentum 전략 - L/S 방향과 같은 방향만 진입
print(f"\n [C] Momentum 필터: L/S ratio > 중앙값이면 LONG만, < 중앙값이면 SHORT만")
if len(has_ls) > 0:
median_ls = has_ls["entry_top_acct_ls"].median()
momentum_filter = has_ls.apply(
lambda r: (r["direction"] == "LONG" and r["entry_top_acct_ls"] >= median_ls) or
(r["direction"] == "SHORT" and r["entry_top_acct_ls"] < median_ls),
axis=1
)
passed = has_ls[momentum_filter]
blocked = has_ls[~momentum_filter]
print(f" 중앙값: {median_ls:.4f}")
print(f" 통과: {len(passed)}건, PnL합계: {passed['net_pnl'].sum():+.4f}, "
f"승률: {passed['is_win'].mean()*100:.1f}%")
print(f" 차단: {len(blocked)}건, PnL합계: {blocked['net_pnl'].sum():+.4f}, "
f"승률: {blocked['is_win'].mean()*100:.1f}%")
# 시뮬레이션 4: Contrarian 전략 - L/S 반대 방향만 진입
print(f"\n [D] Contrarian 필터: L/S ratio > 중앙값이면 SHORT만, < 중앙값이면 LONG만")
if len(has_ls) > 0:
contrarian_filter = has_ls.apply(
lambda r: (r["direction"] == "SHORT" and r["entry_top_acct_ls"] >= median_ls) or
(r["direction"] == "LONG" and r["entry_top_acct_ls"] < median_ls),
axis=1
)
passed = has_ls[contrarian_filter]
blocked = has_ls[~contrarian_filter]
print(f" 통과: {len(passed)}건, PnL합계: {passed['net_pnl'].sum():+.4f}, "
f"승률: {passed['is_win'].mean()*100:.1f}%")
print(f" 차단: {len(blocked)}건, PnL합계: {blocked['net_pnl'].sum():+.4f}, "
f"승률: {blocked['is_win'].mean()*100:.1f}%")
# 9. 전체 L/S ratio 시계열 + 거래 오버레이 요약
print("\n" + "=" * 80)
print(" 7. 전체 요약")
print("=" * 80)
total_trades = len(df)
ls_matched = len(has_ls)
total_pnl = df["net_pnl"].sum()
win_rate = df["is_win"].mean() * 100
print(f"\n 전체 거래: {total_trades}건 (L/S 매칭: {ls_matched}건)")
print(f" 총 PnL: {total_pnl:+.4f} USDT")
print(f" 승률: {win_rate:.1f}% ({df['is_win'].sum()}/{total_trades})")
if len(has_ls) > 0:
print(f"\n L/S 매칭 거래 통계:")
print(f" 진입 top_acct L/S 범위: {has_ls['entry_top_acct_ls'].min():.4f} ~ {has_ls['entry_top_acct_ls'].max():.4f}")
print(f" 진입 global L/S 범위: {has_ls['entry_global_ls'].min():.4f} ~ {has_ls['entry_global_ls'].max():.4f}")
print(f"\n ⚠️ 주의: 거래 {total_trades}건은 통계적 유의성이 부족합니다.")
print(f" 현재 결과는 탐색적 분석이며, 최소 50건 이상의 거래가 필요합니다.")
print(f" L/S ratio 데이터는 계속 축적 중이므로 4월 말 재분석을 권장합니다.")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -106,3 +106,21 @@ class Config:
volume_multiplier=self.volume_multiplier,
))
# ── OOS 사후보정용 비용 모델 ──────────────────────────────────────
COST_MODEL = {
"taker_fee_bps": 4.0, # Binance USDⓈ-M Futures VIP 0
"maker_fee_bps": 2.0, # 향후 limit TP 도입 대비
# MTF bot 주문 타입 (현재 전부 MARKET = taker)
"entry_order_type": "taker",
"sl_order_type": "taker",
"tp_order_type": "taker",
}
# 3개 프리셋 시나리오 (확장 금지, 이 셋으로 고정)
COST_SCENARIOS = {
"fees_only": {"slippage_bps_per_side": 0.0, "funding_bps_per_8h": 0.0},
"realistic": {"slippage_bps_per_side": 1.0, "funding_bps_per_8h": 1.0},
"pessimistic": {"slippage_bps_per_side": 3.0, "funding_bps_per_8h": 2.0},
}

View File

@@ -390,6 +390,21 @@ class TriggerStrategy:
_MTF_TRADE_DIR = Path("data/trade_history")
# ── 킬스위치 상수 (bot.py와 동일 기준) ──
_FAST_KILL_STREAK = 8 # 연속 손실 N회 → 즉시 중단
_SLOW_KILL_WINDOW = 15 # 최근 N거래 PF 산출
_SLOW_KILL_PF_THRESHOLD = 0.75 # PF < 이 값이면 중단
def _tail_lines(path: Path, n: int) -> list[str]:
"""파일의 마지막 n줄을 읽는다."""
try:
with open(path) as f:
lines = f.readlines()
return lines[-n:]
except Exception:
return []
class ExecutionManager:
"""
@@ -408,6 +423,11 @@ class ExecutionManager:
self._sl_price: Optional[float] = None
self._tp_price: Optional[float] = None
self._atr_at_entry: Optional[float] = None
# ── 킬스위치 ──
self._killed: bool = False
self._trade_history: list[dict] = []
self._restore_trade_history()
self._restore_kill_switch()
def execute(self, signal: str, current_price: float, atr_value: Optional[float]) -> Optional[Dict]:
"""
@@ -424,6 +444,10 @@ class ExecutionManager:
if signal == "HOLD":
return None
if self._killed:
logger.warning(f"[ExecutionManager] 킬스위치 발동 상태 — 신규 진입 차단 (신호={signal})")
return None
if self.current_position is not None:
logger.debug(
f"[ExecutionManager] 포지션 중복 차단: "
@@ -503,6 +527,10 @@ class ExecutionManager:
# JSONL에 기록
self._save_trade(reason, exit_price, pnl_bps)
# ── 킬스위치: 거래 이력 추가 + 판정 ──
self._append_trade_history(pnl_bps)
self._check_kill_switch()
# ── 실주문 (프로덕션 전환 시 주석 해제) ──
# if self.current_position == "LONG":
# await self.exchange.create_market_sell_order(symbol, amount)
@@ -540,6 +568,91 @@ class ExecutionManager:
except Exception as e:
logger.warning(f"[ExecutionManager] 거래 기록 저장 실패: {e}")
# ── 킬스위치 ──────────────────────────────────────────────────────
def _trade_history_path(self) -> Path:
_MTF_TRADE_DIR.mkdir(parents=True, exist_ok=True)
return _MTF_TRADE_DIR / f"mtf_{self.symbol.replace('/', '').replace(':', '').lower()}.jsonl"
def _restore_trade_history(self) -> None:
"""부팅 시 JSONL에서 최근 N건의 pnl_bps를 복원한다."""
path = self._trade_history_path()
if not path.exists():
return
try:
tail_n = max(_FAST_KILL_STREAK, _SLOW_KILL_WINDOW)
lines = _tail_lines(path, tail_n)
for line in lines:
line = line.strip()
if line:
record = json.loads(line)
self._trade_history.append({"pnl_bps": record.get("pnl_bps", 0.0)})
logger.info(
f"[ExecutionManager] 거래 이력 복원: {len(self._trade_history)}"
)
except Exception as e:
logger.warning(f"[ExecutionManager] 거래 이력 복원 실패: {e}")
def _restore_kill_switch(self) -> None:
"""부팅 시 리셋 플래그 확인 후, 이력 기반으로 킬스위치 소급 검증."""
reset_key = f"RESET_KILL_SWITCH_MTF_{self.symbol.replace('/', '').replace(':', '').upper()}"
if os.environ.get(reset_key, "").lower() == "true":
logger.info(f"[ExecutionManager] 킬스위치 수동 해제 감지 ({reset_key}=True)")
self._killed = False
return
if self._check_kill_switch(silent=True):
logger.warning("[ExecutionManager] 부팅 시 킬스위치 조건 충족 — 신규 진입 차단")
def _append_trade_history(self, pnl_bps: float) -> None:
"""킬스위치 판단용 거래 이력에 추가한다."""
self._trade_history.append({"pnl_bps": round(pnl_bps, 1)})
max_window = max(_FAST_KILL_STREAK, _SLOW_KILL_WINDOW)
if len(self._trade_history) > max_window * 2:
self._trade_history = self._trade_history[-max_window:]
def _check_kill_switch(self, silent: bool = False) -> bool:
"""킬스위치 조건을 검사하고, 발동 시 True를 반환한다.
Fast Kill: 최근 8연속 손실 (pnl_bps < 0)
Slow Kill: 최근 15거래 PF < 0.75
"""
trades = self._trade_history
if not trades:
return False
# Fast Kill
if len(trades) >= _FAST_KILL_STREAK:
recent = trades[-_FAST_KILL_STREAK:]
if all(t["pnl_bps"] < 0 for t in recent):
reason = f"Fast Kill ({_FAST_KILL_STREAK}연속 손실)"
self._trigger_kill_switch(reason, silent)
return True
# Slow Kill
if len(trades) >= _SLOW_KILL_WINDOW:
recent = trades[-_SLOW_KILL_WINDOW:]
gross_profit = sum(t["pnl_bps"] for t in recent if t["pnl_bps"] > 0)
gross_loss = abs(sum(t["pnl_bps"] for t in recent if t["pnl_bps"] < 0))
if gross_loss > 0:
pf = gross_profit / gross_loss
if pf < _SLOW_KILL_PF_THRESHOLD:
reason = f"Slow Kill (최근 {_SLOW_KILL_WINDOW}거래 PF={pf:.2f})"
self._trigger_kill_switch(reason, silent)
return True
return False
def _trigger_kill_switch(self, reason: str, silent: bool = False) -> None:
"""킬스위치 발동: 상태 변경 + 로그."""
self._killed = True
msg = (
f"[MTF KILL SWITCH] {self.symbol} 신규 진입 중단\n"
f"사유: {reason}\n"
f"기존 포지션 SL/TP는 정상 작동합니다.\n"
f"해제: RESET_KILL_SWITCH_MTF_{self.symbol.replace('/', '').replace(':', '').upper()}=True 후 재시작"
)
logger.error(msg)
def get_position_info(self) -> Dict:
"""현재 포지션 정보 반환."""
return {

181
tests/test_evaluate_oos.py Normal file
View File

@@ -0,0 +1,181 @@
"""
evaluate_oos.py 비용 모델 단위 테스트
"""
import sys
from pathlib import Path
import pandas as pd
import pytest
# 프로젝트 루트를 path에 추가
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from scripts.evaluate_oos import (
apply_cost_model,
calc_metrics,
calc_trade_cost,
count_funding_events,
)
from src.config import COST_MODEL, COST_SCENARIOS
# ── count_funding_events 테스트 ──────────────────────────────────
def test_count_funding_events_no_crossing():
"""진입 01:00 UTC -> 청산 05:00 UTC, 펀딩 경계(00/08/16) 미포함 -> count == 0."""
entry = pd.Timestamp("2026-04-10 01:00:00+00:00")
exit_ = pd.Timestamp("2026-04-10 05:00:00+00:00")
assert count_funding_events(entry, exit_) == 0
def test_count_funding_events_single_crossing():
"""진입 06:00 UTC -> 청산 10:00 UTC, 08:00 포함 -> count == 1."""
entry = pd.Timestamp("2026-04-10 06:00:00+00:00")
exit_ = pd.Timestamp("2026-04-10 10:00:00+00:00")
assert count_funding_events(entry, exit_) == 1
def test_count_funding_events_multiple_crossings():
"""12시간 보유: 02:00 -> 14:00, 08:00 포함 -> count == 1."""
entry = pd.Timestamp("2026-04-10 02:00:00+00:00")
exit_ = pd.Timestamp("2026-04-10 14:00:00+00:00")
assert count_funding_events(entry, exit_) == 1
# 22:00 -> 10:00 (다음날), 00:00 + 08:00 포함 -> count == 2
entry2 = pd.Timestamp("2026-04-10 22:00:00+00:00")
exit2 = pd.Timestamp("2026-04-11 10:00:00+00:00")
assert count_funding_events(entry2, exit2) == 2
def test_count_funding_events_short_trade_no_overcounting():
"""75분 거래, 경계 미포함 -> count == 0."""
# 18:15 -> 19:30, 펀딩 경계 없음
entry = pd.Timestamp("2026-04-10 18:15:00+00:00")
exit_ = pd.Timestamp("2026-04-10 19:30:00+00:00")
assert count_funding_events(entry, exit_) == 0
def test_count_funding_events_exact_boundary():
"""정확히 경계에서 진입/청산하는 경우."""
# entry=08:00, exit=16:00 -> ceil(08:00)=08:00, floor(16:00)=16:00
# hours: 08, 09, ..., 16 -> 08:00(yes), 16:00(yes) -> count == 2
entry = pd.Timestamp("2026-04-10 08:00:00+00:00")
exit_ = pd.Timestamp("2026-04-10 16:00:00+00:00")
assert count_funding_events(entry, exit_) == 2
# ── 비용 계산 테스트 ─────────────────────────────────────────────
def test_cost_calculation_taker_roundtrip():
"""진입 taker + SL taker, slippage 0, funding 0 -> 8 bps."""
row = pd.Series({
"entry_ts": pd.Timestamp("2026-04-10 01:00:00+00:00"),
"exit_ts": pd.Timestamp("2026-04-10 02:00:00+00:00"),
"pnl_bps": -50.0,
"reason": "SL 히트 (1.3012)",
"side": "SHORT",
})
scenario = COST_SCENARIOS["fees_only"]
cost = calc_trade_cost(row, scenario)
assert cost == 8.0 # taker(4) + taker(4) + 0 + 0
def test_cost_calculation_tp_exit():
"""TP 히트 시에도 현재 설정에서는 taker -> 8 bps."""
row = pd.Series({
"entry_ts": pd.Timestamp("2026-04-10 01:00:00+00:00"),
"exit_ts": pd.Timestamp("2026-04-10 02:00:00+00:00"),
"pnl_bps": 80.0,
"reason": "TP 히트 (1.3826)",
"side": "LONG",
})
scenario = COST_SCENARIOS["fees_only"]
cost = calc_trade_cost(row, scenario)
assert cost == 8.0
def test_cost_with_slippage_and_funding():
"""realistic 시나리오: fee 8 + slippage 2 + funding 1 = 11 bps."""
# 진입 15:45, 청산 17:00 -> funding event at 16:00 -> count=1
row = pd.Series({
"entry_ts": pd.Timestamp("2026-04-02 15:45:00+00:00"),
"exit_ts": pd.Timestamp("2026-04-02 17:00:00+00:00"),
"pnl_bps": -68.0,
"reason": "SL 히트 (1.3012)",
"side": "SHORT",
})
scenario = COST_SCENARIOS["realistic"]
cost = calc_trade_cost(row, scenario)
# fee=8, slippage=1*2=2, funding=1*1=1 -> total=11
assert cost == 11.0
def test_adjusted_pnl_matches_manual():
"""첫 번째 거래(Trade #0)에 대해 수작업 계산값과 일치 확인."""
# Trade #0: SHORT, entry 15:45 UTC, exit 17:00 UTC, pnl_bps=-68.0, SL 히트
# fees_only: cost=8 (fee only, funding event at 16:00 but funding_bps=0) -> adjusted=-76.0
# realistic: cost=8+2+1=11 -> adjusted=-79.0
# pessimistic: cost=8+6+2=16 -> adjusted=-84.0
row = pd.Series({
"entry_ts": pd.Timestamp("2026-04-02 15:45:02.285284+00:00"),
"exit_ts": pd.Timestamp("2026-04-02 17:00:00.791551+00:00"),
"pnl_bps": -68.0,
"reason": "SL 히트 (1.3012)",
"side": "SHORT",
})
for scenario_name, expected_adj in [
("fees_only", -76.0),
("realistic", -79.0),
("pessimistic", -84.0),
]:
scenario = COST_SCENARIOS[scenario_name]
cost = calc_trade_cost(row, scenario)
adjusted = row["pnl_bps"] - cost
assert adjusted == expected_adj, f"{scenario_name}: {adjusted} != {expected_adj}"
# ── 회귀 테스트 ──────────────────────────────────────────────────
def test_regression_fees_only_cum_pnl():
"""18건 전체를 fees_only로 돌렸을 때 CumPnL == -173.9 bps (+-0.5 bps 허용)."""
jsonl_path = Path("data/trade_history/mtf_xrpusdtusdt.jsonl")
if not jsonl_path.exists():
pytest.skip("로컬 jsonl 파일 없음")
df = pd.read_json(jsonl_path, lines=True)
df["entry_ts"] = pd.to_datetime(df["entry_ts"], utc=True)
df["exit_ts"] = pd.to_datetime(df["exit_ts"], utc=True)
df["duration_min"] = (df["exit_ts"] - df["entry_ts"]).dt.total_seconds() / 60
result = apply_cost_model(df, "fees_only")
metrics = calc_metrics(result, pnl_col="adjusted_pnl_bps")
assert metrics["trades"] == 18
assert abs(metrics["cum_pnl"] - (-173.9)) <= 0.5, f"CumPnL={metrics['cum_pnl']}, expected -173.9"
# ── calc_metrics 테스트 ──────────────────────────────────────────
def test_calc_metrics_empty():
"""빈 DataFrame -> 안전한 기본값."""
df = pd.DataFrame(columns=["pnl_bps", "duration_min"])
m = calc_metrics(df)
assert m["trades"] == 0
assert m["pf"] == 0.0
def test_calc_metrics_with_avg_pnl():
"""avg_pnl 필드가 정확히 계산되는지 확인."""
df = pd.DataFrame({
"pnl_bps": [10.0, -5.0, 20.0],
"duration_min": [60.0, 30.0, 90.0],
})
m = calc_metrics(df)
assert m["trades"] == 3
assert m["avg_pnl"] == pytest.approx(25.0 / 3, abs=0.01)

View File

@@ -421,3 +421,107 @@ class TestExecutionManager:
# TP/SL = 2.3/1.5 = 1.533...
expected_rr = round(2.3 / 1.5, 2)
assert result["risk_reward"] == expected_rr
# ═══════════════════════════════════════════════════════════════════
# Test 5: ExecutionManager 킬스위치
# ═══════════════════════════════════════════════════════════════════
class TestExecutionManagerKillSwitch:
"""ExecutionManager 킬스위치 (Fast Kill / Slow Kill) 테스트."""
def _make_em(self) -> ExecutionManager:
"""JSONL 복원 없이 깨끗한 ExecutionManager 생성."""
em = ExecutionManager.__new__(ExecutionManager)
em.symbol = "TESTUSDT"
em.current_position = None
em._entry_price = None
em._entry_ts = None
em._sl_price = None
em._tp_price = None
em._atr_at_entry = None
em._killed = False
em._trade_history = []
return em
def test_fast_kill_triggers_after_8_consecutive_losses(self):
"""8연속 손실 시 Fast Kill 발동."""
em = self._make_em()
for _ in range(8):
em._append_trade_history(-50.0)
assert em._check_kill_switch() is True
assert em._killed is True
def test_fast_kill_not_triggered_with_7_losses(self):
"""7연속 손실은 Fast Kill 미발동."""
em = self._make_em()
for _ in range(7):
em._append_trade_history(-50.0)
assert em._check_kill_switch() is False
assert em._killed is False
def test_fast_kill_broken_by_single_win(self):
"""연속 손실 중 1회 수익이 있으면 Fast Kill 미발동."""
em = self._make_em()
for _ in range(4):
em._append_trade_history(-50.0)
em._append_trade_history(10.0) # 중간에 수익
for _ in range(3):
em._append_trade_history(-50.0)
assert em._check_kill_switch() is False
def test_slow_kill_triggers_when_pf_below_threshold(self):
"""최근 15거래 PF < 0.75 시 Slow Kill 발동."""
em = self._make_em()
# 12패 (-100 bps each) + 3승 (+50 bps each)
# gross_profit=150, gross_loss=1200, PF=0.125
for _ in range(12):
em._append_trade_history(-100.0)
for _ in range(3):
em._append_trade_history(50.0)
assert em._check_kill_switch() is True
assert em._killed is True
def test_slow_kill_not_triggered_when_pf_above_threshold(self):
"""PF > 0.75면 Slow Kill 미발동."""
em = self._make_em()
# 7패 (-100 each) + 8승 (+100 each)
# gross_profit=800, gross_loss=700, PF=1.14
for _ in range(7):
em._append_trade_history(-100.0)
for _ in range(8):
em._append_trade_history(100.0)
assert em._check_kill_switch() is False
def test_killed_state_blocks_new_entry(self):
"""킬스위치 발동 후 신규 진입이 차단된다."""
em = self._make_em()
em._killed = True
result = em.execute("EXECUTE_LONG", 2.0, 0.01)
assert result is None
def test_killed_state_allows_existing_sl_tp(self):
"""킬스위치 발동 후에도 기존 포지션의 청산은 정상 동작."""
em = self._make_em()
# 먼저 포지션 진입
em.execute("EXECUTE_SHORT", 2.0, 0.01)
# 킬스위치 발동
em._killed = True
# 청산은 정상 동작해야 함
em.close_position("SL 히트", exit_price=2.015, pnl_bps=-75.0)
assert em.current_position is None
def test_kill_switch_integrated_with_close(self):
"""close_position 호출 시 자동으로 킬스위치 판정이 실행된다."""
em = self._make_em()
# 7번 손실 기록
for _ in range(7):
em._append_trade_history(-50.0)
# 포지션 진입 후 8번째 손실로 청산
em.execute("EXECUTE_SHORT", 2.0, 0.01)
em.close_position("SL 히트", exit_price=2.015, pnl_bps=-75.0)
# 8연패 → Fast Kill 발동
assert em._killed is True
# 다음 진입 차단
assert em.execute("EXECUTE_LONG", 1.95, 0.01) is None