Compare commits

...

7 Commits

Author SHA1 Message Date
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
70 changed files with 126695 additions and 27 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.
@@ -155,3 +155,5 @@ All design documents and implementation plans are stored in `docs/plans/` with t
| 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-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 대칭성 실패 |

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,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로 거래 비용 감당 불가.

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

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,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