Compare commits
9 Commits
b6ba45f8de
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09ae926f06 | ||
|
|
f53b8a5a0f | ||
|
|
52d05f2ddd | ||
|
|
4a7b38ea43 | ||
|
|
cc0838e854 | ||
|
|
112f3e2377 | ||
|
|
960f997aac | ||
|
|
98476cc972 | ||
|
|
29e307d7b2 |
@@ -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
|
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
|
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.
|
**Parallel execution**: Per-symbol bots run independently via `asyncio.gather()`. Each bot's `user_data_stream` also runs in parallel.
|
||||||
|
|
||||||
@@ -154,4 +154,8 @@ All design documents and implementation plans are stored in `docs/plans/` with t
|
|||||||
| 2026-03-30 | `ls-ratio-backtest` (design + result) | Edge 없음 확정, 폐기 |
|
| 2026-03-30 | `ls-ratio-backtest` (design + result) | Edge 없음 확정, 폐기 |
|
||||||
| 2026-03-30 | `fr-oi-backtest` (result) | SHORT PF=1.88이나 대칭성 실패(Case2), 폐기 |
|
| 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 | `public-api-research-closed` | Binance 공개 API 전수 테스트 완료, 단독 edge 없음 |
|
||||||
| 2026-03-30 | `mtf-pullback-bot` | MTF Pullback Bot 배포, 4월 OOS Dry-run 검증 진행 중 |
|
| 2026-03-30 | `mtf-pullback-bot` | MTF Pullback Bot — **최종 폐기** (OOS+BTC필터 모두 실패) |
|
||||||
|
| 2026-04-21 | `mtf-oos-dryrun-result` | 중간 보고 — 24건 Raw PF 0.98 |
|
||||||
|
| 2026-05-04 | `mtf-oos-final-result` | **FAIL, 폐기** — 30건 fees_only PF 0.84, SHORT 대칭성 실패 |
|
||||||
|
| 2026-05-04 | `mtf-btc-filter` (design + result) | **FAIL, 최종 폐기** — BTC 필터 추가해도 OOS PF 0.90, 베이스라인보다 악화 |
|
||||||
|
| 2026-05-04 | `strategy-post-mortem` | 7전 7패 분석 — 공개 시그널 방향 예측 패러다임 한계, 다음 방향 제안 |
|
||||||
|
|||||||
BIN
data/btcusdt/ls_ratio_15m.parquet
Normal file
BIN
data/btcusdt/ls_ratio_15m.parquet
Normal file
Binary file not shown.
Binary file not shown.
BIN
data/ethusdt/ls_ratio_15m.parquet
Normal file
BIN
data/ethusdt/ls_ratio_15m.parquet
Normal file
Binary file not shown.
8
data/trade_history/mtf_testusdt.jsonl
Normal file
8
data/trade_history/mtf_testusdt.jsonl
Normal 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"}
|
||||||
6
data/trade_history/mtf_xrpusdt.jsonl
Normal file
6
data/trade_history/mtf_xrpusdt.jsonl
Normal 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"}
|
||||||
30
data/trade_history/mtf_xrpusdtusdt.jsonl
Normal file
30
data/trade_history/mtf_xrpusdtusdt.jsonl
Normal 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)"}
|
||||||
15
data/trade_history/testnet/xrpusdt.jsonl
Normal file
15
data/trade_history/testnet/xrpusdt.jsonl
Normal 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.
BIN
data/xrpusdt/combined_15m.parquet.bak
Normal file
BIN
data/xrpusdt/combined_15m.parquet.bak
Normal file
Binary file not shown.
BIN
data/xrpusdt/ls_ratio_15m.parquet
Normal file
BIN
data/xrpusdt/ls_ratio_15m.parquet
Normal file
Binary file not shown.
52
docs/plans/2026-03-30-fr-oi-backtest-result.md
Normal file
52
docs/plans/2026-03-30-fr-oi-backtest-result.md
Normal 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을 거절한 이유: 대칭성 없는 시그널은 시장 방향에 의존
|
||||||
|
- "한 방향만 작동"은 가장 흔한 과적합 패턴
|
||||||
|
- 설계 기준을 사전에 세우고 준수하는 것이 생존 전략
|
||||||
76
docs/plans/2026-03-30-ls-ratio-backtest-design.md
Normal file
76
docs/plans/2026-03-30-ls-ratio-backtest-design.md
Normal 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일 재검증 기반
|
||||||
43
docs/plans/2026-03-30-ls-ratio-backtest-result.md
Normal file
43
docs/plans/2026-03-30-ls-ratio-backtest-result.md
Normal 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의 시작
|
||||||
|
- 빠르게 실패하고 폐기하는 것이 생존 능력
|
||||||
40
docs/plans/2026-03-30-public-api-research-closed.md
Normal file
40
docs/plans/2026-03-30-public-api-research-closed.md
Normal 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 기반)
|
||||||
30
docs/plans/2026-04-01-mtf_backtest.md
Normal file
30
docs/plans/2026-04-01-mtf_backtest.md
Normal 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 │ 개선율 │
|
||||||
|
└─────────────────────────────────────────
|
||||||
40
docs/plans/2026-04-21-mtf-oos-dryrun-result.md
Normal file
40
docs/plans/2026-04-21-mtf-oos-dryrun-result.md
Normal 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건 이상 누적 시 최종 판정 예정.
|
||||||
79
docs/plans/2026-05-04-mtf-btc-filter-design.md
Normal file
79
docs/plans/2026-05-04-mtf-btc-filter-design.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# MTF + BTC 추세 필터 백테스트 설계
|
||||||
|
|
||||||
|
## 가설
|
||||||
|
|
||||||
|
BTC 추세 방향과 일치하는 MTF 풀백 시그널만 실행하면 비용 반영 후에도 PF > 1.2를 달성한다.
|
||||||
|
|
||||||
|
## 메인 가설 (사전 확정 — commitment device)
|
||||||
|
|
||||||
|
> 메인 가설은 sweep 결과를 보기 **전에** BTC 1h + EMA 50/200 + ADX > 20으로 확정한다.
|
||||||
|
> sweep 12개 결과에서 가장 좋은 조합으로 사후 변경하지 않는다.
|
||||||
|
> 4h/1d 결과는 robustness 참고용이며, 메인 가설이 OOS 실패 시
|
||||||
|
> "다른 조합이 됐으니 PASS"로 구제하지 않는다.
|
||||||
|
|
||||||
|
- **BTC 타임프레임**: 1h
|
||||||
|
- **BTC EMA**: fast=50, slow=200
|
||||||
|
- **BTC ADX 임계값**: 20
|
||||||
|
- **선택 근거**: XRP MTF bot의 1h 메타필터와 동일 기준 → 시그널 정합성 확보, 사후 정당화 차단
|
||||||
|
|
||||||
|
## 필터 로직
|
||||||
|
|
||||||
|
```
|
||||||
|
BTC_trend = (BTC EMA_fast > BTC EMA_slow) AND (BTC ADX > 20)
|
||||||
|
? (EMA_fast > EMA_slow ? "UP" : "DOWN")
|
||||||
|
: "NEUTRAL"
|
||||||
|
|
||||||
|
if BTC_trend == "UP": SHORT 차단, LONG만 허용
|
||||||
|
if BTC_trend == "DOWN": LONG 차단, SHORT만 허용
|
||||||
|
if BTC_trend == "NEUTRAL": 양방향 차단 (추세 불명확)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sweep 파라미터 (robustness check용)
|
||||||
|
|
||||||
|
| 파라미터 | 후보 | 설명 |
|
||||||
|
|---------|------|------|
|
||||||
|
| BTC 타임프레임 | 1h, 4h, 1d | BTC 추세 판단 주기 |
|
||||||
|
| BTC EMA fast | 20, 50 | 단기 EMA |
|
||||||
|
| BTC EMA slow | 100, 200 | 장기 EMA |
|
||||||
|
|
||||||
|
총 조합: 3 × 2 × 2 = 12개. ADX > 20은 전 조합 고정.
|
||||||
|
|
||||||
|
## 데이터
|
||||||
|
|
||||||
|
- XRP: `data/xrpusdt/combined_15m.parquet` (기존)
|
||||||
|
- BTC: `data/btcusdt/combined_15m.parquet` (fetch 필요)
|
||||||
|
- 기간: 최소 6개월 (XRP와 동일 기간)
|
||||||
|
- BTC 15m → 1h/4h/1d resample 후 EMA/ADX 계산
|
||||||
|
- merge: `merge_asof(direction="backward")` — look-ahead bias 방지
|
||||||
|
- fetch 후 XRP/BTC 첫/마지막 timestamp + bar 수 일치 검증 필수
|
||||||
|
|
||||||
|
## IS/OOS 분할
|
||||||
|
|
||||||
|
- 앞 70% IS, 뒤 30% OOS (단순 시간 분할)
|
||||||
|
- ML 없고 sweep 12개뿐이므로 walk-forward 불필요
|
||||||
|
|
||||||
|
## 합격 기준
|
||||||
|
|
||||||
|
| 기준 | 값 | 비고 |
|
||||||
|
|------|-----|------|
|
||||||
|
| 메인 가설 OOS fees_only PF | >= 1.2 | 실거래 마진 확보 |
|
||||||
|
| 메인 가설 OOS realistic PF | >= 1.0 | 슬리피지+펀딩 반영 후 흑자 |
|
||||||
|
| LONG/SHORT 양쪽 fees_only PF | >= 0.8 | 대칭성 |
|
||||||
|
| OOS 거래 수 | >= 50 | 통계적 유의성 |
|
||||||
|
| IS/OOS PF 격차 | < 30% | 과적합 방지 |
|
||||||
|
| 베이스라인 대비 OOS PF | 명확한 개선 | 절대값보다 차이 중요 |
|
||||||
|
| IS 거래 수 | >= 100 | 미달 시 조합 자동 제외 |
|
||||||
|
|
||||||
|
## 판정 흐름
|
||||||
|
|
||||||
|
1. 베이스라인(BTC 필터 없는 MTF) IS/OOS 결과 먼저 산출
|
||||||
|
2. 12개 조합 IS sweep, 모두 결과 저장 (IS 거래 수 < 100 자동 제외)
|
||||||
|
3. 메인 가설(1h/EMA50-200/ADX20) OOS 검증 → 합격 기준 통과 시 PASS
|
||||||
|
4. 나머지 11개도 OOS 검증 → robustness 보고서 (참고용)
|
||||||
|
5. 메인 가설 실패 시 → 전략 폐기 (다른 조합으로 구제 안 함)
|
||||||
|
|
||||||
|
## 산출물
|
||||||
|
|
||||||
|
- `scripts/mtf_btc_filter_backtest.py` — 백테스트 스크립트
|
||||||
|
- `docs/plans/2026-05-04-mtf-btc-filter-result.md` — 결과 문서
|
||||||
|
- 거래 수준 로그 (CSV) — entry/exit/BTC trend/PnL 포함, 사후 분석용
|
||||||
80
docs/plans/2026-05-04-mtf-btc-filter-result.md
Normal file
80
docs/plans/2026-05-04-mtf-btc-filter-result.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# MTF + BTC 추세 필터 백테스트 결과
|
||||||
|
|
||||||
|
## 가설
|
||||||
|
|
||||||
|
BTC 추세 방향과 일치하는 MTF 풀백 시그널만 실행하면 비용 반영 후에도 PF > 1.2를 달성한다.
|
||||||
|
|
||||||
|
메인 가설 (사전 확정): BTC 1h + EMA 50/200 + ADX > 20
|
||||||
|
|
||||||
|
## 데이터
|
||||||
|
|
||||||
|
- 심볼: XRPUSDT
|
||||||
|
- 기간: 2024-04-02 ~ 2026-05-03 (761일, 73,123 bars)
|
||||||
|
- IS: 2024-04-02 ~ 2025-09-17 (70%)
|
||||||
|
- OOS: 2025-09-17 ~ 2026-05-03 (30%)
|
||||||
|
- BTC OHLCV: XRP parquet 내 상관 컬럼 활용 (NaN 0%)
|
||||||
|
|
||||||
|
## 베이스라인 (BTC 필터 없음)
|
||||||
|
|
||||||
|
| 구분 | IS PF(fees) | OOS PF(fees) | OOS 거래수 |
|
||||||
|
|------|------------|-------------|-----------|
|
||||||
|
| Total | 1.02 | 0.94 | 206 |
|
||||||
|
| LONG | 1.04 | 0.71 | 69 |
|
||||||
|
| SHORT | 1.01 | 1.06 | 137 |
|
||||||
|
|
||||||
|
베이스라인 자체가 OOS에서 적자 (fees PF 0.94).
|
||||||
|
|
||||||
|
## 메인 가설 결과 (BTC 1h EMA50/200 ADX>20)
|
||||||
|
|
||||||
|
| 구분 | IS PF(fees) | OOS PF(fees) | OOS PF(real) | OOS 거래수 |
|
||||||
|
|------|------------|-------------|-------------|-----------|
|
||||||
|
| Total | 1.12 | **0.90** | 0.88 | 158 |
|
||||||
|
| LONG | 1.24 | 0.81 | 0.78 | 56 |
|
||||||
|
| SHORT | 1.02 | 0.95 | 0.92 | 102 |
|
||||||
|
|
||||||
|
## 합격 기준 체크
|
||||||
|
|
||||||
|
| 기준 | 결과 | 판정 |
|
||||||
|
|------|------|------|
|
||||||
|
| OOS fees_only PF >= 1.2 | 0.90 | **FAIL** |
|
||||||
|
| OOS realistic PF >= 1.0 | 0.88 | **FAIL** |
|
||||||
|
| OOS 거래수 >= 50 | 158 | PASS |
|
||||||
|
| LONG/SHORT fees PF >= 0.8 | L:0.81 S:0.95 | PASS |
|
||||||
|
| IS/OOS PF 격차 < 30% | 19.6% | PASS |
|
||||||
|
| 베이스라인 대비 개선 | 0.90 vs 0.94 | **FAIL** |
|
||||||
|
|
||||||
|
## Sweep Robustness
|
||||||
|
|
||||||
|
| 조합 | IS N | IS FPF | OOS FPF | OOS rPF | 비고 |
|
||||||
|
|------|------|--------|---------|---------|------|
|
||||||
|
| BTC_1h_EMA20/100 | 327 | 1.19 | 0.84 | 0.81 | |
|
||||||
|
| BTC_1h_EMA20/200 | 333 | 1.16 | 0.86 | 0.84 | |
|
||||||
|
| BTC_1h_EMA50/100 | 335 | 1.14 | 0.83 | 0.80 | |
|
||||||
|
| **BTC_1h_EMA50/200** | **333** | **1.12** | **0.90** | **0.88** | **메인 ★** |
|
||||||
|
| BTC_4h_EMA20/100 | 310 | 1.00 | 1.04 | 1.01 | |
|
||||||
|
| BTC_4h_EMA20/200 | 269 | 0.95 | 1.09 | 1.06 | |
|
||||||
|
| BTC_4h_EMA50/100 | 271 | 0.91 | 1.07 | 1.04 | |
|
||||||
|
| BTC_4h_EMA50/200 | 231 | 0.91 | 1.01 | 0.98 | |
|
||||||
|
| BTC_1d_EMA20/100 | 151 | 1.10 | 1.17 | 1.13 | |
|
||||||
|
| BTC_1d_EMA20/200 | 94 | 1.41 | 1.28 | 1.25 | IS<100 SKIP |
|
||||||
|
| BTC_1d_EMA50/100 | 129 | 1.28 | 1.20 | 1.16 | |
|
||||||
|
| BTC_1d_EMA50/200 | 96 | 1.44 | 1.14 | 1.11 | IS<100 SKIP |
|
||||||
|
|
||||||
|
- 1h 조합: **전멸** (OOS fees PF 0.83~0.90)
|
||||||
|
- 4h 조합: 1.01~1.09 — BEP 수준, 1.2 미달
|
||||||
|
- 1d 조합: 1.17~1.28 — 겉보기 좋으나 IS 거래수 94~151로 과적합 위험 + 사전 합의에 의해 구제 불가
|
||||||
|
|
||||||
|
12개 중 fees PF >= 1.2 통과는 **1개** (BTC_1d_EMA20/100, 1.17로 미달 → 실제 0개).
|
||||||
|
|
||||||
|
## 핵심 발견
|
||||||
|
|
||||||
|
1. **BTC 필터가 성과를 악화시킴**: 메인 가설 OOS PF 0.90 < 베이스라인 0.94. 필터가 오히려 좋은 거래를 걸러냄
|
||||||
|
2. **IS/OOS 격차 패턴**: 1h 조합은 IS에서 과적합(1.12~1.19) → OOS 붕괴(0.83~0.90). 전형적 curve-fitting
|
||||||
|
3. **1d가 좋아보이는 함정**: IS 거래수가 94~151로 적어 통계적 유의성 부족. 사전 합의대로 구제 불가
|
||||||
|
4. **MTF Pullback 자체의 한계**: 베이스라인 OOS 0.94로 전략 자체에 edge 없음. 필터로 보완 불가능
|
||||||
|
|
||||||
|
## 결론
|
||||||
|
|
||||||
|
**FAIL** — MTF Pullback 전략 최종 폐기.
|
||||||
|
|
||||||
|
BTC 추세 필터(1h EMA50/200 ADX>20)는 OOS에서 베이스라인보다 오히려 악화. 12개 sweep 조합 중 합격 기준(fees PF >= 1.2)을 통과한 조합 0개. 베이스라인 자체도 OOS 적자(PF 0.94)로 전략의 근본적 edge 부재 확인.
|
||||||
48
docs/plans/2026-05-04-mtf-oos-final-result.md
Normal file
48
docs/plans/2026-05-04-mtf-oos-final-result.md
Normal 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로 거래 비용 감당 불가.
|
||||||
133
docs/plans/2026-05-04-strategy-post-mortem.md
Normal file
133
docs/plans/2026-05-04-strategy-post-mortem.md
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
# CoinTrader 전략 Post-Mortem (2026-05-04)
|
||||||
|
|
||||||
|
지금까지 시도한 모든 전략의 실패 원인을 분석하고, 다음 방향을 도출한다.
|
||||||
|
|
||||||
|
## 1. 사망자 명단
|
||||||
|
|
||||||
|
| # | 전략 | 기간 | IS PF | OOS PF(fees) | 사망 원인 |
|
||||||
|
|---|------|------|-------|-------------|----------|
|
||||||
|
| 1 | RSI+MACD+BB+EMA+StochRSI (기본) | 2026-03 | — | 0.89 | 수익성 부족 |
|
||||||
|
| 2 | ML Filter (LightGBM/ONNX) | 2026-03 | — | ML OFF > ML ON | 피처에 alpha 없음 |
|
||||||
|
| 3 | LS Ratio | 2026-03 | — | 0.86 (best) | r=0.12, 비용 커버 불가 |
|
||||||
|
| 4 | FR × OI | 2026-03 | — | SHORT 1.88 / LONG 0.50 | 대칭성 실패 |
|
||||||
|
| 5 | Taker Buy/Sell Ratio | 2026-03 | — | 0.93 | r=-0.08, 약함 |
|
||||||
|
| 6 | MTF Pullback (OOS dry-run) | 2026-04 | — | 0.84 (fees) | SHORT 역 edge |
|
||||||
|
| 7 | MTF Pullback + BTC 필터 | 2026-05 | 1.12 | 0.90 (fees) | 베이스라인보다 악화 |
|
||||||
|
|
||||||
|
**7전 7패.** 단 하나도 비용 반영 후 OOS PF > 1.0을 달성하지 못함.
|
||||||
|
|
||||||
|
## 2. 공통 실패 패턴
|
||||||
|
|
||||||
|
### 패턴 A: "IS에서 살고 OOS에서 죽는다"
|
||||||
|
|
||||||
|
| 전략 | IS fees PF | OOS fees PF | 격차 |
|
||||||
|
|------|-----------|-------------|------|
|
||||||
|
| 기본 시그널 | ~1.02 | 0.89 | -13% |
|
||||||
|
| MTF Pullback | 1.02 | 0.94 | -8% |
|
||||||
|
| MTF + BTC 1h | 1.12 | 0.90 | -20% |
|
||||||
|
|
||||||
|
IS에서 마진이 얇은 전략(PF 1.0~1.1)은 OOS에서 비용을 이기지 못한다. **IS PF < 1.3이면 OOS 통과 가능성 거의 0.**
|
||||||
|
|
||||||
|
### 패턴 B: "LONG/SHORT 중 한쪽만 작동"
|
||||||
|
|
||||||
|
| 전략 | 좋은 쪽 | 나쁜 쪽 | 진단 |
|
||||||
|
|------|---------|---------|------|
|
||||||
|
| FR × OI | SHORT PF 1.88 | LONG PF 0.50 | 시장 베타 |
|
||||||
|
| MTF OOS (4월) | LONG PF 1.28 | SHORT PF 0.73 | 상승장 편승 |
|
||||||
|
| MTF 백테스트 OOS | SHORT PF 1.06 | LONG PF 0.71 | 하락장 편승 |
|
||||||
|
|
||||||
|
한쪽만 좋은 건 "전략의 edge"가 아니라 **해당 기간의 시장 방향(beta)**을 타고 있을 뿐. 기간이 바뀌면 좋은 쪽과 나쁜 쪽이 뒤집힌다 (4월 OOS에서 LONG이 좋았고, 전체 백테스트 OOS에서는 SHORT가 좋았음).
|
||||||
|
|
||||||
|
### 패턴 C: "필터/피처를 추가할수록 악화"
|
||||||
|
|
||||||
|
| 레이어 | OOS fees PF |
|
||||||
|
|--------|-------------|
|
||||||
|
| 기본 시그널 | 0.89 |
|
||||||
|
| + ML Filter | 더 나빠짐 |
|
||||||
|
| MTF Pullback (베이스라인) | 0.94 |
|
||||||
|
| + BTC 추세 필터 | 0.90 (악화) |
|
||||||
|
|
||||||
|
필터를 추가하면 **좋은 거래도 같이 걸러진다**. 기본 시그널 자체에 edge가 없으면 필터로 구제 불가능. 오히려 거래 수 감소 → 통계적 불안정 → 악화.
|
||||||
|
|
||||||
|
## 3. 근본 원인 진단: Edge 부재 vs Regime Dependence?
|
||||||
|
|
||||||
|
**답: 둘 다. 그리고 이 둘은 연결되어 있다.**
|
||||||
|
|
||||||
|
1. **Edge 부재가 근본**: RSI/MACD/BB/EMA/StochRSI + 풀백 패턴은 너무 많은 참가자가 아는 시그널. 알파가 소진된 공공재. r < 0.15인 시그널은 수수료 8bps를 이길 수 없다.
|
||||||
|
|
||||||
|
2. **Regime dependence는 결과**: edge가 없는 전략은 시장 방향(beta)에 수익이 좌우된다. 상승장에선 LONG이, 하락장에선 SHORT가 좋아 보이지만 이건 전략의 alpha가 아니라 시장의 beta.
|
||||||
|
|
||||||
|
3. **검증 방법론은 정상 작동**: IS/OOS 분할, 비용 모델, 대칭성 체크, 사전 가설 확정 — 이 프레임워크가 7개 전략을 정확히 거부했다. 문제는 방법론이 아니라 **시그널 소스**.
|
||||||
|
|
||||||
|
## 4. "뭘 안 시도했나?" — 미탐색 영역 지도
|
||||||
|
|
||||||
|
### 이미 소진한 영역 (재시도 금지)
|
||||||
|
|
||||||
|
- Binance 공개 API 파생 피처 (전수 테스트 완료)
|
||||||
|
- 기술적 지표 조합 (RSI, MACD, BB, EMA, StochRSI, ADX)
|
||||||
|
- ML 피처 선택/앙상블 (LightGBM, ONNX — 피처 자체에 alpha 없음)
|
||||||
|
- 시간대 변경 (15m→1h→4h→1d sweep 완료)
|
||||||
|
- 상관 자산 필터 (BTC 추세 필터 검증 완료)
|
||||||
|
|
||||||
|
### 미탐색 영역
|
||||||
|
|
||||||
|
| 방향 | 구체적 아이디어 | 난이도 | 기대값 |
|
||||||
|
|------|---------------|--------|--------|
|
||||||
|
| **데이터 소스 변경** | 온체인 (whale wallet, exchange flow) | 높음 | 중 — 지연 있으나 독점 정보 가능성 |
|
||||||
|
| **데이터 소스 변경** | 크로스 거래소 (OKX/Bybit OI/FR 차이) | 중 | 중 — 차이 자체가 시그널 |
|
||||||
|
| **데이터 소스 변경** | 소셜/뉴스 센티먼트 | 높음 | 낮음 — 노이즈 많음, 지연 |
|
||||||
|
| **자산 변경** | BTC/ETH (유동성 높은 메이저) | 낮음 | 중 — 경쟁 치열하지만 유동성 풍부 |
|
||||||
|
| **자산 변경** | 저유동성 알트 (DYDX, APE 등) | 낮음 | 중 — 비효율성 클 수 있으나 슬리피지 |
|
||||||
|
| **패러다임 변경** | 통계적 차익 (pair trading, mean reversion) | 중 | 중상 — 방향 중립, beta 제거 |
|
||||||
|
| **패러다임 변경** | 마이크로 구조 (호가창 불균형, 체결 패턴) | 높음 | 높음 — 진입 장벽이 alpha 보호 |
|
||||||
|
| **패러다임 변경** | 변동성 전략 (vol expansion/compression) | 중 | 중 — 방향 아닌 "크기" 예측 |
|
||||||
|
| **실행 최적화** | 동일 시그널 + maker order (비용 절감) | 중 | 중 — 0.08%→0.04% 비용 반감 |
|
||||||
|
|
||||||
|
## 5. 다음 방향 제안
|
||||||
|
|
||||||
|
### 우선순위 1: 패러다임을 바꿔라
|
||||||
|
|
||||||
|
지금까지의 모든 전략은 **"방향 예측"** 패러다임:
|
||||||
|
- "XRP가 오를까 내릴까?" → LONG/SHORT
|
||||||
|
|
||||||
|
이 패러다임에서 공개 시그널로 alpha를 찾기는 극히 어렵다. 대안:
|
||||||
|
|
||||||
|
**(A) 통계적 차익 (Stat Arb / Pair Trading)**
|
||||||
|
- XRP/BTC 비율의 평균 회귀
|
||||||
|
- 여러 알트코인 간 상대 강도 기반 롱숏
|
||||||
|
- 장점: 방향 중립 → beta 제거, 대칭성 자동 충족
|
||||||
|
- 인프라: 현재 multi-symbol 아키텍처로 확장 가능
|
||||||
|
|
||||||
|
**(B) 변동성 전략**
|
||||||
|
- "방향"이 아니라 "크기"를 예측
|
||||||
|
- ATR 수축 → 확장 시 양방향 진입 (straddle-like)
|
||||||
|
- BB squeeze → breakout 방향은 예측하되, 비용 대비 마진이 큰 구간만
|
||||||
|
|
||||||
|
### 우선순위 2: 비용을 줄여라
|
||||||
|
|
||||||
|
현재 taker 왕복 8bps가 모든 전략의 적. 같은 시그널이라도:
|
||||||
|
- Entry를 limit order로 → 4bps 절감 (왕복 4bps)
|
||||||
|
- 이것만으로 MTF 베이스라인 OOS가 0.94 → ~1.02로 개선 가능
|
||||||
|
- 근본 해결은 아니지만, 마진이 얇은 전략을 BEP 위로 올릴 수 있음
|
||||||
|
|
||||||
|
### 우선순위 3: 데이터 소스를 넓혀라
|
||||||
|
|
||||||
|
Binance 공개 API는 소진. 외부 데이터가 필요:
|
||||||
|
- Glassnode/CryptoQuant (온체인)
|
||||||
|
- 크로스 거래소 OI/FR 차이
|
||||||
|
- 이 데이터는 접근 비용(유료 API)이 있으나 그만큼 경쟁자가 적음
|
||||||
|
|
||||||
|
## 6. 보존할 자산
|
||||||
|
|
||||||
|
7전 7패였지만 **인프라는 자산**:
|
||||||
|
|
||||||
|
| 자산 | 용도 |
|
||||||
|
|------|------|
|
||||||
|
| 비용 모델 (3 시나리오) | 어떤 전략이든 재사용 |
|
||||||
|
| IS/OOS 검증 프레임워크 | 자기기만 방지 |
|
||||||
|
| 사전 가설 확정 프로토콜 | sweep 과적합 방지 |
|
||||||
|
| multi-symbol 아키텍처 | pair trading 확장 가능 |
|
||||||
|
| kill switch (Fast/Slow) | 실전 리스크 관리 |
|
||||||
|
| 주간 리포트 파이프라인 | 모니터링 |
|
||||||
|
|
||||||
|
다음 전략이 뭐든, 이 검증 인프라 위에서 시작하면 실패를 빠르게 확인할 수 있다.
|
||||||
Binary file not shown.
Binary file not shown.
@@ -98,5 +98,30 @@
|
|||||||
"reg_lambda": 0.000157
|
"reg_lambda": 0.000157
|
||||||
},
|
},
|
||||||
"weight_scale": 1.783105
|
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
BIN
models/solusdt/lgbm_filter.pkl
Normal file
BIN
models/solusdt/lgbm_filter.pkl
Normal file
Binary file not shown.
27
models/solusdt/training_log.json
Normal file
27
models/solusdt/training_log.json
Normal 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.
@@ -98,5 +98,30 @@
|
|||||||
"reg_lambda": 0.000157
|
"reg_lambda": 0.000157
|
||||||
},
|
},
|
||||||
"weight_scale": 1.783105
|
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
44
results/dogeusdt/ml_comparison_20260321_195841.json
Normal file
44
results/dogeusdt/ml_comparison_20260321_195841.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
results/dogeusdt/ml_comparison_20260321_200334.json
Normal file
45
results/dogeusdt/ml_comparison_20260321_200334.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1054
results/dogeusdt/wf_backtest_20260318_232112.json
Normal file
1054
results/dogeusdt/wf_backtest_20260318_232112.json
Normal file
File diff suppressed because it is too large
Load Diff
188
results/dogeusdt/wf_backtest_20260321_194418.json
Normal file
188
results/dogeusdt/wf_backtest_20260321_194418.json
Normal 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)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
44
results/solusdt/ml_comparison_20260321_195841.json
Normal file
44
results/solusdt/ml_comparison_20260321_195841.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
results/solusdt/ml_comparison_20260321_200332.json
Normal file
45
results/solusdt/ml_comparison_20260321_200332.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
81
results/solusdt/wf_backtest_20260321_194417.json
Normal file
81
results/solusdt/wf_backtest_20260321_194417.json
Normal 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": "트레이드 없음 (검증 스킵)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
13407
results/weekly/report_2026-03-22.html
Normal file
13407
results/weekly/report_2026-03-22.html
Normal file
File diff suppressed because it is too large
Load Diff
2235
results/weekly/report_2026-03-22.json
Normal file
2235
results/weekly/report_2026-03-22.json
Normal file
File diff suppressed because it is too large
Load Diff
13739
results/weekly/report_2026-03-29.html
Normal file
13739
results/weekly/report_2026-03-29.html
Normal file
File diff suppressed because it is too large
Load Diff
1398
results/weekly/report_2026-03-29.json
Normal file
1398
results/weekly/report_2026-03-29.json
Normal file
File diff suppressed because it is too large
Load Diff
13739
results/weekly/report_2026-04-05.html
Normal file
13739
results/weekly/report_2026-04-05.html
Normal file
File diff suppressed because it is too large
Load Diff
1401
results/weekly/report_2026-04-05.json
Normal file
1401
results/weekly/report_2026-04-05.json
Normal file
File diff suppressed because it is too large
Load Diff
13739
results/weekly/report_2026-04-12.html
Normal file
13739
results/weekly/report_2026-04-12.html
Normal file
File diff suppressed because it is too large
Load Diff
1401
results/weekly/report_2026-04-12.json
Normal file
1401
results/weekly/report_2026-04-12.json
Normal file
File diff suppressed because it is too large
Load Diff
13739
results/weekly/report_2026-04-19.html
Normal file
13739
results/weekly/report_2026-04-19.html
Normal file
File diff suppressed because it is too large
Load Diff
1401
results/weekly/report_2026-04-19.json
Normal file
1401
results/weekly/report_2026-04-19.json
Normal file
File diff suppressed because it is too large
Load Diff
13739
results/weekly/report_2026-04-26.html
Normal file
13739
results/weekly/report_2026-04-26.html
Normal file
File diff suppressed because it is too large
Load Diff
1401
results/weekly/report_2026-04-26.json
Normal file
1401
results/weekly/report_2026-04-26.json
Normal file
File diff suppressed because it is too large
Load Diff
13204
results/weekly/report_2026-05-03.html
Normal file
13204
results/weekly/report_2026-05-03.html
Normal file
File diff suppressed because it is too large
Load Diff
1761
results/weekly/report_2026-05-03.json
Normal file
1761
results/weekly/report_2026-05-03.json
Normal file
File diff suppressed because it is too large
Load Diff
549
results/xrpusdt/backtest_20260322_224712.json
Normal file
549
results/xrpusdt/backtest_20260322_224712.json
Normal 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)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
45
results/xrpusdt/ml_comparison_20260321_195840.json
Normal file
45
results/xrpusdt/ml_comparison_20260321_195840.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
results/xrpusdt/ml_comparison_20260321_200332.json
Normal file
44
results/xrpusdt/ml_comparison_20260321_200332.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4406
results/xrpusdt/mtf_btc_filter_trades.csv
Normal file
4406
results/xrpusdt/mtf_btc_filter_trades.csv
Normal file
File diff suppressed because it is too large
Load Diff
8243
results/xrpusdt/strategy_sweep_20260329_142440.json
Normal file
8243
results/xrpusdt/strategy_sweep_20260329_142440.json
Normal file
File diff suppressed because it is too large
Load Diff
835
results/xrpusdt/wf_backtest_20260318_232111.json
Normal file
835
results/xrpusdt/wf_backtest_20260318_232111.json
Normal 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)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
214
results/xrpusdt/wf_backtest_20260321_194417.json
Normal file
214
results/xrpusdt/wf_backtest_20260321_194417.json
Normal 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)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
382
results/xrpusdt/wf_backtest_20260322_224634.json
Normal file
382
results/xrpusdt/wf_backtest_20260322_224634.json
Normal 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)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
838
results/xrpusdt/wf_backtest_20260322_224727.json
Normal file
838
results/xrpusdt/wf_backtest_20260322_224727.json
Normal 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)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
511
results/xrpusdt/wf_backtest_20260322_231113.json
Normal file
511
results/xrpusdt/wf_backtest_20260322_231113.json
Normal 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)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
865
results/xrpusdt/wf_backtest_20260322_231737.json
Normal file
865
results/xrpusdt/wf_backtest_20260322_231737.json
Normal 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)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
537
results/xrpusdt/wf_backtest_20260322_231837.json
Normal file
537
results/xrpusdt/wf_backtest_20260322_231837.json
Normal 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)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
3062
results/xrpusdt/wf_backtest_20260322_232159.json
Normal file
3062
results/xrpusdt/wf_backtest_20260322_232159.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,10 +4,15 @@ MTF Pullback Bot — OOS Dry-run 평가 스크립트
|
|||||||
프로덕션 서버에서 JSONL 거래 기록을 가져와
|
프로덕션 서버에서 JSONL 거래 기록을 가져와
|
||||||
승률·PF·누적PnL·평균보유시간을 계산하고 LIVE 배포 판정을 출력한다.
|
승률·PF·누적PnL·평균보유시간을 계산하고 LIVE 배포 판정을 출력한다.
|
||||||
|
|
||||||
|
비용 모델(수수료·슬리피지·펀딩)을 사후보정으로 적용하여
|
||||||
|
fees_only / realistic / pessimistic 3개 시나리오 결과를 출력한다.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python scripts/evaluate_oos.py
|
python scripts/evaluate_oos.py
|
||||||
python scripts/evaluate_oos.py --symbol xrpusdt
|
python scripts/evaluate_oos.py --symbol xrpusdt
|
||||||
python scripts/evaluate_oos.py --local # 로컬 파일만 사용 (서버 fetch 스킵)
|
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
|
import argparse
|
||||||
@@ -17,6 +22,10 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pandas as pd
|
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"
|
PROD_HOST = "root@10.1.10.24"
|
||||||
REMOTE_DIR = "/root/cointrader/data/trade_history"
|
REMOTE_DIR = "/root/cointrader/data/trade_history"
|
||||||
@@ -67,20 +76,74 @@ def load_trades(path: Path) -> pd.DataFrame:
|
|||||||
return df
|
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이면 안전한 기본값 반환."""
|
"""핵심 지표 계산. 빈 DataFrame이면 안전한 기본값 반환."""
|
||||||
n = len(df)
|
n = len(df)
|
||||||
if n == 0:
|
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]
|
wins = df[df[pnl_col] > 0]
|
||||||
losses = df[df["pnl_bps"] < 0]
|
losses = df[df[pnl_col] < 0]
|
||||||
|
|
||||||
win_rate = len(wins) / n * 100
|
win_rate = len(wins) / n * 100
|
||||||
gross_profit = wins["pnl_bps"].sum() if len(wins) > 0 else 0.0
|
gross_profit = wins[pnl_col].sum() if len(wins) > 0 else 0.0
|
||||||
gross_loss = abs(losses["pnl_bps"].sum()) if len(losses) > 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")
|
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()
|
avg_dur = df["duration_min"].mean()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -88,28 +151,29 @@ def calc_metrics(df: pd.DataFrame) -> dict:
|
|||||||
"win_rate": round(win_rate, 1),
|
"win_rate": round(win_rate, 1),
|
||||||
"pf": round(pf, 2),
|
"pf": round(pf, 2),
|
||||||
"cum_pnl": round(cum_pnl, 1),
|
"cum_pnl": round(cum_pnl, 1),
|
||||||
|
"avg_pnl": round(avg_pnl, 2),
|
||||||
"avg_dur": round(avg_dur, 1),
|
"avg_dur": round(avg_dur, 1),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def print_report(df: pd.DataFrame):
|
def print_report(df: pd.DataFrame):
|
||||||
"""성적표 출력."""
|
"""성적표 출력 (raw, 비용 미반영)."""
|
||||||
total = calc_metrics(df)
|
total = calc_metrics(df)
|
||||||
longs = calc_metrics(df[df["side"] == "LONG"])
|
longs = calc_metrics(df[df["side"] == "LONG"])
|
||||||
shorts = calc_metrics(df[df["side"] == "SHORT"])
|
shorts = calc_metrics(df[df["side"] == "SHORT"])
|
||||||
|
|
||||||
header = f"{'':>10} {'Trades':>8} {'WinRate':>9} {'PF':>8} {'CumPnL':>10} {'AvgDur':>10}"
|
header = f"{'':>10} {'Trades':>8} {'WinRate':>9} {'PF':>8} {'CumPnL':>10} {'AvgDur':>10}"
|
||||||
sep = "─" * 60
|
sep = "\u2500" * 60
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print(sep)
|
print(sep)
|
||||||
print(" MTF Pullback Bot — OOS Dry-run 성적표")
|
print(" MTF Pullback Bot \u2014 OOS Dry-run \uc131\uc801\ud45c")
|
||||||
print(sep)
|
print(sep)
|
||||||
print(header)
|
print(header)
|
||||||
print(sep)
|
print(sep)
|
||||||
|
|
||||||
for label, m in [("Total", total), ("LONG", longs), ("SHORT", shorts)]:
|
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 "-"
|
dur_str = f"{m['avg_dur']:.0f}m" if m["trades"] > 0 else "-"
|
||||||
print(
|
print(
|
||||||
f"{label:>10} {m['trades']:>8d} {m['win_rate']:>8.1f}% {pf_str:>8} "
|
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"
|
dur = f"{row['duration_min']:.0f}m"
|
||||||
reason = row.get("reason", "")
|
reason = row.get("reason", "")
|
||||||
if len(reason) > 25:
|
if len(reason) > 25:
|
||||||
reason = reason[:25] + "…"
|
reason = reason[:25] + "\u2026"
|
||||||
print(
|
print(
|
||||||
f"{i+1:>3} {row['side']:>6} {row['entry_price']:>10.4f} {row['exit_price']:>10.4f} "
|
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}"
|
f"{row['pnl_bps']:>+10.1f} {dur:>8} {reason}"
|
||||||
)
|
)
|
||||||
print(sep)
|
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()
|
print()
|
||||||
if total["trades"] >= MIN_TRADES and total["pf"] >= MIN_PF:
|
if cost_total["trades"] >= MIN_TRADES and cost_total["pf"] >= MIN_PF and symmetry_ok:
|
||||||
print(f" [판정: 통과] 엣지가 증명되었습니다. LIVE 배포(자금 투입)를 권장합니다.")
|
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" (거래수 {total['trades']} >= {MIN_TRADES}, PF {total['pf']:.2f} >= {MIN_PF:.1f})")
|
print(f" (\uac70\ub798\uc218 {cost_total['trades']} >= {MIN_TRADES}, fees_only PF {cost_total['pf']:.2f} >= {MIN_PF:.1f})")
|
||||||
else:
|
else:
|
||||||
reasons = []
|
reasons = []
|
||||||
if total["trades"] < MIN_TRADES:
|
if cost_total["trades"] < MIN_TRADES:
|
||||||
reasons.append(f"거래수 {total['trades']} < {MIN_TRADES}")
|
reasons.append(f"\uac70\ub798\uc218 {cost_total['trades']} < {MIN_TRADES}")
|
||||||
if total["pf"] < MIN_PF:
|
if cost_total["pf"] < MIN_PF:
|
||||||
reasons.append(f"PF {total['pf']:.2f} < {MIN_PF:.1f}")
|
reasons.append(f"fees_only PF {cost_total['pf']:.2f} < {MIN_PF:.1f}")
|
||||||
print(f" [판정: 보류] 기준 미달. OOS 검증 실패로 실전 투입을 보류합니다.")
|
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(f" ({', '.join(reasons)})")
|
||||||
print()
|
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():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="MTF OOS Dry-run 평가")
|
parser = argparse.ArgumentParser(description="MTF OOS Dry-run \ud3c9\uac00")
|
||||||
parser.add_argument("--symbol", default="xrpusdt", help="심볼 (파일명 소문자, 기본: xrpusdt)")
|
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="로컬 파일만 사용 (서버 fetch 스킵)")
|
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()
|
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:
|
if args.local:
|
||||||
local_path = LOCAL_DIR / filename
|
local_path = LOCAL_DIR / filename
|
||||||
if not local_path.exists():
|
if not local_path.exists():
|
||||||
print(f"[Error] 로컬 파일 없음: {local_path}")
|
print(f"[Error] \ub85c\uceec \ud30c\uc77c \uc5c6\uc74c: {local_path}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
local_path = fetch_from_prod(filename)
|
local_path = fetch_from_prod(filename)
|
||||||
|
|
||||||
df = load_trades(local_path)
|
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)
|
print_report(df)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
364
scripts/fr_oi_backtest.py
Normal file
364
scripts/fr_oi_backtest.py
Normal 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())
|
||||||
312
scripts/funding_oi_analysis.py
Normal file
312
scripts/funding_oi_analysis.py
Normal 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())
|
||||||
485
scripts/ls_ratio_backtest.py
Normal file
485
scripts/ls_ratio_backtest.py
Normal 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())
|
||||||
601
scripts/mtf_btc_filter_backtest.py
Normal file
601
scripts/mtf_btc_filter_backtest.py
Normal file
@@ -0,0 +1,601 @@
|
|||||||
|
"""
|
||||||
|
MTF Pullback + BTC 추세 필터 백테스트
|
||||||
|
──────────────────────────────────────
|
||||||
|
기존 MTF Pullback 전략에 BTC 추세 필터를 추가하여 검증.
|
||||||
|
|
||||||
|
메인 가설 (사전 확정):
|
||||||
|
BTC 1h + EMA 50/200 + ADX > 20
|
||||||
|
sweep 결과와 무관하게 사후 변경하지 않음.
|
||||||
|
|
||||||
|
판정 흐름:
|
||||||
|
1. 베이스라인(필터 없음) IS/OOS 결과 산출
|
||||||
|
2. 12개 sweep IS, 메인 가설 OOS 검증
|
||||||
|
3. 나머지 11개 OOS robustness 체크
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/mtf_btc_filter_backtest.py
|
||||||
|
python scripts/mtf_btc_filter_backtest.py --symbol xrpusdt
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from itertools import product
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import pandas_ta as ta
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
from src.config import COST_MODEL, COST_SCENARIOS # noqa: E402
|
||||||
|
|
||||||
|
# ─── 설정 ──────────────────────────────────────────────────────────
|
||||||
|
SYMBOL = "xrpusdt"
|
||||||
|
DATA_PATH = Path(f"data/{SYMBOL}/combined_15m.parquet")
|
||||||
|
|
||||||
|
# XRP 1h 메타필터 (기존 MTF bot 설정 그대로)
|
||||||
|
MTF_EMA_FAST = 50
|
||||||
|
MTF_EMA_SLOW = 200
|
||||||
|
MTF_ADX_THRESHOLD = 20
|
||||||
|
|
||||||
|
# 15m Trigger
|
||||||
|
EMA_PULLBACK_LEN = 20
|
||||||
|
VOL_DRY_RATIO = 0.5
|
||||||
|
|
||||||
|
# SL/TP
|
||||||
|
ATR_SL_MULT = 1.5
|
||||||
|
ATR_TP_MULT = 2.3
|
||||||
|
|
||||||
|
# IS/OOS 분할
|
||||||
|
IS_RATIO = 0.7
|
||||||
|
|
||||||
|
# ─── Sweep 그리드 ─────────────────────────────────────────────────
|
||||||
|
SWEEP_GRID = {
|
||||||
|
"btc_tf": ["1h", "4h", "1D"],
|
||||||
|
"btc_ema_fast": [20, 50],
|
||||||
|
"btc_ema_slow": [100, 200],
|
||||||
|
}
|
||||||
|
|
||||||
|
# BTC ADX 임계값 — 전 조합 고정
|
||||||
|
BTC_ADX_THRESHOLD = 20
|
||||||
|
|
||||||
|
# 메인 가설 (사전 확정 — commitment device)
|
||||||
|
MAIN_HYPOTHESIS = {"btc_tf": "1h", "btc_ema_fast": 50, "btc_ema_slow": 200}
|
||||||
|
|
||||||
|
# IS 거래 수 최소 기준
|
||||||
|
MIN_IS_TRADES = 100
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Trade:
|
||||||
|
entry_time: pd.Timestamp
|
||||||
|
entry_price: float
|
||||||
|
side: str
|
||||||
|
sl: float
|
||||||
|
tp: float
|
||||||
|
btc_trend: str = ""
|
||||||
|
exit_time: pd.Timestamp | None = None
|
||||||
|
exit_price: float | None = None
|
||||||
|
pnl_bps: float | None = None
|
||||||
|
reason: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
def build_xrp_1h(df_15m: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""XRP 15m → 1h: EMA50, EMA200, ADX, ATR."""
|
||||||
|
df_1h = df_15m[["open", "high", "low", "close", "volume"]].resample("1h").agg(
|
||||||
|
{"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}
|
||||||
|
).dropna()
|
||||||
|
|
||||||
|
df_1h["ema50_1h"] = ta.ema(df_1h["close"], length=MTF_EMA_FAST)
|
||||||
|
df_1h["ema200_1h"] = ta.ema(df_1h["close"], length=MTF_EMA_SLOW)
|
||||||
|
adx_df = ta.adx(df_1h["high"], df_1h["low"], df_1h["close"], length=14)
|
||||||
|
df_1h["adx_1h"] = adx_df["ADX_14"]
|
||||||
|
df_1h["atr_1h"] = ta.atr(df_1h["high"], df_1h["low"], df_1h["close"], length=14)
|
||||||
|
|
||||||
|
return df_1h[["ema50_1h", "ema200_1h", "adx_1h", "atr_1h"]]
|
||||||
|
|
||||||
|
|
||||||
|
def build_btc_resampled(df_15m: pd.DataFrame, tf: str, ema_fast: int, ema_slow: int) -> pd.DataFrame:
|
||||||
|
"""BTC 15m → 지정 타임프레임: EMA + ADX."""
|
||||||
|
btc_cols = {"open_btc": "open", "high_btc": "high", "low_btc": "low",
|
||||||
|
"close_btc": "close", "volume_btc": "volume"}
|
||||||
|
df_btc = df_15m[list(btc_cols.keys())].rename(columns=btc_cols)
|
||||||
|
|
||||||
|
df_rs = df_btc.resample(tf).agg(
|
||||||
|
{"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}
|
||||||
|
).dropna()
|
||||||
|
|
||||||
|
df_rs[f"btc_ema_fast"] = ta.ema(df_rs["close"], length=ema_fast)
|
||||||
|
df_rs[f"btc_ema_slow"] = ta.ema(df_rs["close"], length=ema_slow)
|
||||||
|
adx_df = ta.adx(df_rs["high"], df_rs["low"], df_rs["close"], length=14)
|
||||||
|
df_rs["btc_adx"] = adx_df["ADX_14"]
|
||||||
|
|
||||||
|
return df_rs[["btc_ema_fast", "btc_ema_slow", "btc_adx"]]
|
||||||
|
|
||||||
|
|
||||||
|
def merge_higher_tf(df_15m: pd.DataFrame, df_htf: pd.DataFrame, tf: str) -> pd.DataFrame:
|
||||||
|
"""Look-ahead bias 방지 merge. 1h → +1h shift, 4h → +4h shift, 1d → +1d shift."""
|
||||||
|
shift_map = {"1h": pd.Timedelta(hours=1), "4h": pd.Timedelta(hours=4),
|
||||||
|
"1D": pd.Timedelta(days=1)}
|
||||||
|
df_shifted = df_htf.copy()
|
||||||
|
df_shifted.index = df_shifted.index + shift_map[tf]
|
||||||
|
|
||||||
|
df_15m_r = df_15m.reset_index()
|
||||||
|
df_htf_r = df_shifted.reset_index()
|
||||||
|
ts_col_15m = df_15m_r.columns[0]
|
||||||
|
ts_col_htf = df_htf_r.columns[0]
|
||||||
|
df_15m_r.rename(columns={ts_col_15m: "timestamp"}, inplace=True)
|
||||||
|
df_htf_r.rename(columns={ts_col_htf: "timestamp"}, inplace=True)
|
||||||
|
|
||||||
|
df_15m_r["timestamp"] = pd.to_datetime(df_15m_r["timestamp"]).astype("datetime64[us]")
|
||||||
|
df_htf_r["timestamp"] = pd.to_datetime(df_htf_r["timestamp"]).astype("datetime64[us]")
|
||||||
|
|
||||||
|
merged = pd.merge_asof(
|
||||||
|
df_15m_r.sort_values("timestamp"),
|
||||||
|
df_htf_r.sort_values("timestamp"),
|
||||||
|
on="timestamp",
|
||||||
|
direction="backward",
|
||||||
|
)
|
||||||
|
return merged.set_index("timestamp")
|
||||||
|
|
||||||
|
|
||||||
|
def get_xrp_meta(row) -> str:
|
||||||
|
"""XRP 1h 메타필터."""
|
||||||
|
ema50 = row.get("ema50_1h")
|
||||||
|
ema200 = row.get("ema200_1h")
|
||||||
|
adx = row.get("adx_1h")
|
||||||
|
if pd.isna(ema50) or pd.isna(ema200) or pd.isna(adx):
|
||||||
|
return "HOLD"
|
||||||
|
if adx < MTF_ADX_THRESHOLD:
|
||||||
|
return "HOLD"
|
||||||
|
return "LONG" if ema50 > ema200 else "SHORT"
|
||||||
|
|
||||||
|
|
||||||
|
def get_btc_trend(row) -> str:
|
||||||
|
"""BTC 추세 필터."""
|
||||||
|
ema_f = row.get("btc_ema_fast")
|
||||||
|
ema_s = row.get("btc_ema_slow")
|
||||||
|
adx = row.get("btc_adx")
|
||||||
|
if pd.isna(ema_f) or pd.isna(ema_s) or pd.isna(adx):
|
||||||
|
return "NEUTRAL"
|
||||||
|
if adx < BTC_ADX_THRESHOLD:
|
||||||
|
return "NEUTRAL"
|
||||||
|
return "UP" if ema_f > ema_s else "DOWN"
|
||||||
|
|
||||||
|
|
||||||
|
def run_backtest(df: pd.DataFrame, use_btc_filter: bool) -> list[Trade]:
|
||||||
|
"""MTF Pullback 백테스트 실행."""
|
||||||
|
trades: list[Trade] = []
|
||||||
|
in_trade = False
|
||||||
|
current_trade: Trade | None = None
|
||||||
|
pullback_ready = False
|
||||||
|
pullback_side = ""
|
||||||
|
|
||||||
|
for i in range(1, len(df)):
|
||||||
|
row = df.iloc[i]
|
||||||
|
|
||||||
|
# ── SL/TP 체크 ──
|
||||||
|
if in_trade and current_trade is not None:
|
||||||
|
hit_sl = hit_tp = False
|
||||||
|
if current_trade.side == "LONG":
|
||||||
|
hit_sl = row["low"] <= current_trade.sl
|
||||||
|
hit_tp = row["high"] >= current_trade.tp
|
||||||
|
else:
|
||||||
|
hit_sl = row["high"] >= current_trade.sl
|
||||||
|
hit_tp = row["low"] <= current_trade.tp
|
||||||
|
|
||||||
|
if hit_sl or hit_tp:
|
||||||
|
exit_price = current_trade.sl if hit_sl else current_trade.tp
|
||||||
|
if hit_sl and hit_tp:
|
||||||
|
exit_price = current_trade.sl # 보수적
|
||||||
|
|
||||||
|
if current_trade.side == "LONG":
|
||||||
|
raw_pnl = (exit_price - current_trade.entry_price) / current_trade.entry_price
|
||||||
|
else:
|
||||||
|
raw_pnl = (current_trade.entry_price - exit_price) / current_trade.entry_price
|
||||||
|
|
||||||
|
current_trade.exit_time = df.index[i]
|
||||||
|
current_trade.exit_price = exit_price
|
||||||
|
current_trade.pnl_bps = raw_pnl * 10000 # raw bps (비용 미반영)
|
||||||
|
current_trade.reason = "SL" if hit_sl else "TP"
|
||||||
|
trades.append(current_trade)
|
||||||
|
in_trade = False
|
||||||
|
current_trade = None
|
||||||
|
|
||||||
|
if in_trade:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# NaN 체크
|
||||||
|
if pd.isna(row.get("ema20")) or pd.isna(row.get("vol_ma20")) or pd.isna(row.get("atr_1h")):
|
||||||
|
pullback_ready = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ── XRP 1h Meta ──
|
||||||
|
meta = get_xrp_meta(row)
|
||||||
|
if meta == "HOLD":
|
||||||
|
pullback_ready = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ── BTC 추세 필터 ──
|
||||||
|
btc_trend = get_btc_trend(row) if use_btc_filter else "DISABLED"
|
||||||
|
|
||||||
|
if use_btc_filter:
|
||||||
|
# BTC UP → LONG만, BTC DOWN → SHORT만, NEUTRAL → 차단
|
||||||
|
if btc_trend == "UP" and meta != "LONG":
|
||||||
|
pullback_ready = False
|
||||||
|
continue
|
||||||
|
elif btc_trend == "DOWN" and meta != "SHORT":
|
||||||
|
pullback_ready = False
|
||||||
|
continue
|
||||||
|
elif btc_trend == "NEUTRAL":
|
||||||
|
pullback_ready = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ── Pullback 감지 → 재개 확인 ──
|
||||||
|
if pullback_ready and pullback_side == meta:
|
||||||
|
if pullback_side == "LONG" and row["close"] > row["ema20"]:
|
||||||
|
if i + 1 < len(df):
|
||||||
|
next_row = df.iloc[i + 1]
|
||||||
|
entry_price = next_row["open"]
|
||||||
|
atr = row["atr_1h"]
|
||||||
|
current_trade = Trade(
|
||||||
|
entry_time=df.index[i + 1], entry_price=entry_price,
|
||||||
|
side="LONG", sl=entry_price - atr * ATR_SL_MULT,
|
||||||
|
tp=entry_price + atr * ATR_TP_MULT, btc_trend=btc_trend,
|
||||||
|
)
|
||||||
|
in_trade = True
|
||||||
|
pullback_ready = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
elif pullback_side == "SHORT" and row["close"] < row["ema20"]:
|
||||||
|
if i + 1 < len(df):
|
||||||
|
next_row = df.iloc[i + 1]
|
||||||
|
entry_price = next_row["open"]
|
||||||
|
atr = row["atr_1h"]
|
||||||
|
current_trade = Trade(
|
||||||
|
entry_time=df.index[i + 1], entry_price=entry_price,
|
||||||
|
side="SHORT", sl=entry_price + atr * ATR_SL_MULT,
|
||||||
|
tp=entry_price - atr * ATR_TP_MULT, btc_trend=btc_trend,
|
||||||
|
)
|
||||||
|
in_trade = True
|
||||||
|
pullback_ready = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ── Pullback 감지 ──
|
||||||
|
vol_dry = row["volume"] < row["vol_ma20"] * VOL_DRY_RATIO
|
||||||
|
if meta == "LONG" and row["close"] < row["ema20"] and vol_dry:
|
||||||
|
pullback_ready = True
|
||||||
|
pullback_side = "LONG"
|
||||||
|
elif meta == "SHORT" and row["close"] > row["ema20"] and vol_dry:
|
||||||
|
pullback_ready = True
|
||||||
|
pullback_side = "SHORT"
|
||||||
|
elif meta != pullback_side:
|
||||||
|
pullback_ready = False
|
||||||
|
|
||||||
|
return trades
|
||||||
|
|
||||||
|
|
||||||
|
def apply_cost(trades: list[Trade], scenario_name: str) -> list[float]:
|
||||||
|
"""거래 리스트에 비용 시나리오 적용, adjusted pnl_bps 리스트 반환."""
|
||||||
|
scenario = COST_SCENARIOS[scenario_name]
|
||||||
|
fee_per_side = COST_MODEL["taker_fee_bps"] # 현재 전부 taker
|
||||||
|
fee_roundtrip = fee_per_side * 2
|
||||||
|
slippage_roundtrip = scenario["slippage_bps_per_side"] * 2
|
||||||
|
|
||||||
|
adjusted = []
|
||||||
|
for t in trades:
|
||||||
|
# 펀딩비: 보유 시간 중 8h 경계 교차 수
|
||||||
|
if t.entry_time is not None and t.exit_time is not None:
|
||||||
|
dur_h = (t.exit_time - t.entry_time).total_seconds() / 3600
|
||||||
|
funding_events = max(0, int(dur_h / 8))
|
||||||
|
else:
|
||||||
|
funding_events = 0
|
||||||
|
funding_cost = funding_events * scenario["funding_bps_per_8h"]
|
||||||
|
total_cost = fee_roundtrip + slippage_roundtrip + funding_cost
|
||||||
|
adjusted.append(t.pnl_bps - total_cost)
|
||||||
|
return adjusted
|
||||||
|
|
||||||
|
|
||||||
|
def calc_metrics(pnl_list: list[float]) -> dict:
|
||||||
|
"""pnl_bps 리스트로 메트릭 계산."""
|
||||||
|
if not pnl_list:
|
||||||
|
return {"trades": 0, "win_rate": 0.0, "pf": 0.0, "cum_pnl": 0.0, "avg_pnl": 0.0}
|
||||||
|
|
||||||
|
wins = [p for p in pnl_list if p > 0]
|
||||||
|
losses = [p for p in pnl_list if p <= 0]
|
||||||
|
gross_profit = sum(wins) if wins else 0
|
||||||
|
gross_loss = abs(sum(losses)) if losses else 0
|
||||||
|
pf = gross_profit / gross_loss if gross_loss > 0 else float("inf")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"trades": len(pnl_list),
|
||||||
|
"win_rate": round(len(wins) / len(pnl_list) * 100, 1),
|
||||||
|
"pf": round(pf, 2),
|
||||||
|
"cum_pnl": round(sum(pnl_list), 1),
|
||||||
|
"avg_pnl": round(sum(pnl_list) / len(pnl_list), 2),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def split_is_oos(trades: list[Trade], split_ts: pd.Timestamp):
|
||||||
|
"""IS/OOS 분할."""
|
||||||
|
is_trades = [t for t in trades if t.entry_time < split_ts]
|
||||||
|
oos_trades = [t for t in trades if t.entry_time >= split_ts]
|
||||||
|
return is_trades, oos_trades
|
||||||
|
|
||||||
|
|
||||||
|
def print_metrics_row(label: str, raw: dict, fees: dict, realistic: dict):
|
||||||
|
"""한 줄 메트릭 출력."""
|
||||||
|
print(f" {label:<8} {raw['trades']:>5} {raw['win_rate']:>5.1f}% "
|
||||||
|
f"{raw['pf']:>5.2f} {fees['pf']:>5.2f} {realistic['pf']:>5.2f} "
|
||||||
|
f"{raw['cum_pnl']:>+8.1f} {fees['cum_pnl']:>+8.1f}")
|
||||||
|
|
||||||
|
|
||||||
|
def print_section(title: str, trades: list[Trade]):
|
||||||
|
"""섹션별 메트릭 출력."""
|
||||||
|
if not trades:
|
||||||
|
print(f"\n [{title}] 거래 없음")
|
||||||
|
return
|
||||||
|
|
||||||
|
raw_all = [t.pnl_bps for t in trades]
|
||||||
|
fees_all = apply_cost(trades, "fees_only")
|
||||||
|
real_all = apply_cost(trades, "realistic")
|
||||||
|
|
||||||
|
long_t = [t for t in trades if t.side == "LONG"]
|
||||||
|
short_t = [t for t in trades if t.side == "SHORT"]
|
||||||
|
|
||||||
|
raw_l = [t.pnl_bps for t in long_t]
|
||||||
|
raw_s = [t.pnl_bps for t in short_t]
|
||||||
|
fees_l = apply_cost(long_t, "fees_only")
|
||||||
|
fees_s = apply_cost(short_t, "fees_only")
|
||||||
|
real_l = apply_cost(long_t, "realistic")
|
||||||
|
real_s = apply_cost(short_t, "realistic")
|
||||||
|
|
||||||
|
print(f"\n [{title}]")
|
||||||
|
print(f" {'':8} {'N':>5} {'WR':>6} {'RawPF':>5} {'FeePF':>5} {'RealPF':>5} {'RawPnL':>8} {'FeePnL':>8}")
|
||||||
|
print(f" {'-'*62}")
|
||||||
|
print_metrics_row("Total", calc_metrics(raw_all), calc_metrics(fees_all), calc_metrics(real_all))
|
||||||
|
print_metrics_row("LONG", calc_metrics(raw_l), calc_metrics(fees_l), calc_metrics(real_l))
|
||||||
|
print_metrics_row("SHORT", calc_metrics(raw_s), calc_metrics(fees_s), calc_metrics(real_s))
|
||||||
|
|
||||||
|
|
||||||
|
def save_trade_log(trades: list[Trade], filepath: Path, combo_label: str):
|
||||||
|
"""거래 수준 CSV 로그 저장."""
|
||||||
|
rows = []
|
||||||
|
for t in trades:
|
||||||
|
rows.append({
|
||||||
|
"combo": combo_label,
|
||||||
|
"entry_time": t.entry_time,
|
||||||
|
"exit_time": t.exit_time,
|
||||||
|
"side": t.side,
|
||||||
|
"entry_price": t.entry_price,
|
||||||
|
"exit_price": t.exit_price,
|
||||||
|
"pnl_bps": t.pnl_bps,
|
||||||
|
"reason": t.reason,
|
||||||
|
"btc_trend": t.btc_trend,
|
||||||
|
})
|
||||||
|
df = pd.DataFrame(rows)
|
||||||
|
mode = "a" if filepath.exists() else "w"
|
||||||
|
header = not filepath.exists()
|
||||||
|
df.to_csv(filepath, mode=mode, header=header, index=False)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
import argparse
|
||||||
|
parser = argparse.ArgumentParser(description="MTF + BTC 추세 필터 백테스트")
|
||||||
|
parser.add_argument("--symbol", default="xrpusdt")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
data_path = Path(f"data/{args.symbol}/combined_15m.parquet")
|
||||||
|
print("=" * 72)
|
||||||
|
print(" MTF Pullback + BTC 추세 필터 백테스트")
|
||||||
|
print(f" 메인 가설: BTC {MAIN_HYPOTHESIS['btc_tf']} EMA{MAIN_HYPOTHESIS['btc_ema_fast']}/{MAIN_HYPOTHESIS['btc_ema_slow']} ADX>{BTC_ADX_THRESHOLD}")
|
||||||
|
print("=" * 72)
|
||||||
|
|
||||||
|
# ── 데이터 로드 ──
|
||||||
|
df_raw = pd.read_parquet(data_path)
|
||||||
|
if df_raw.index.tz is not None:
|
||||||
|
df_raw.index = df_raw.index.tz_localize(None)
|
||||||
|
|
||||||
|
# EMA200 워밍업 (200h × 4 + 여유 = 1000 bars)
|
||||||
|
warmup_bars = 1000
|
||||||
|
df_full = df_raw.iloc[warmup_bars:].copy() if len(df_raw) > warmup_bars else df_raw.copy()
|
||||||
|
# 워밍업 포함 전체 데이터로 지표 계산
|
||||||
|
df_calc = df_raw.copy()
|
||||||
|
|
||||||
|
print(f"\n데이터: {len(df_raw)} bars total, 분석: {len(df_full)} bars")
|
||||||
|
print(f"기간: {df_full.index[0]} ~ {df_full.index[-1]}")
|
||||||
|
print(f"일수: {(df_full.index[-1] - df_full.index[0]).days}일")
|
||||||
|
|
||||||
|
# ── IS/OOS 분할 ──
|
||||||
|
split_idx = int(len(df_full) * IS_RATIO)
|
||||||
|
split_ts = df_full.index[split_idx]
|
||||||
|
print(f"IS/OOS 분할: IS ~{split_ts.date()} | OOS {split_ts.date()}~")
|
||||||
|
|
||||||
|
# ── XRP 15m 지표 ──
|
||||||
|
df_calc["ema20"] = ta.ema(df_calc["close"], length=EMA_PULLBACK_LEN)
|
||||||
|
df_calc["vol_ma20"] = ta.sma(df_calc["volume"], length=20)
|
||||||
|
|
||||||
|
# ── XRP 1h 지표 ──
|
||||||
|
df_1h = build_xrp_1h(df_calc)
|
||||||
|
df_merged_base = merge_higher_tf(df_calc, df_1h, "1h")
|
||||||
|
|
||||||
|
# 분석 기간만 슬라이스
|
||||||
|
df_analysis = df_merged_base[df_merged_base.index >= df_full.index[0]].copy()
|
||||||
|
|
||||||
|
# ── 1. 베이스라인 (BTC 필터 없음) ──
|
||||||
|
print("\n" + "=" * 72)
|
||||||
|
print(" BASELINE (BTC 필터 없음)")
|
||||||
|
print("=" * 72)
|
||||||
|
|
||||||
|
baseline_trades = run_backtest(df_analysis, use_btc_filter=False)
|
||||||
|
baseline_is, baseline_oos = split_is_oos(baseline_trades, split_ts)
|
||||||
|
|
||||||
|
print_section("IS (베이스라인)", baseline_is)
|
||||||
|
print_section("OOS (베이스라인)", baseline_oos)
|
||||||
|
|
||||||
|
# ── 2. Sweep ──
|
||||||
|
print("\n" + "=" * 72)
|
||||||
|
print(" SWEEP (12개 조합)")
|
||||||
|
print("=" * 72)
|
||||||
|
|
||||||
|
combos = list(product(
|
||||||
|
SWEEP_GRID["btc_tf"],
|
||||||
|
SWEEP_GRID["btc_ema_fast"],
|
||||||
|
SWEEP_GRID["btc_ema_slow"],
|
||||||
|
))
|
||||||
|
|
||||||
|
trade_log_path = Path(f"results/{args.symbol}/mtf_btc_filter_trades.csv")
|
||||||
|
trade_log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
if trade_log_path.exists():
|
||||||
|
trade_log_path.unlink()
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for btc_tf, ema_f, ema_s in combos:
|
||||||
|
if ema_f >= ema_s:
|
||||||
|
continue # fast >= slow는 무의미
|
||||||
|
|
||||||
|
label = f"BTC_{btc_tf}_EMA{ema_f}/{ema_s}"
|
||||||
|
is_main = (btc_tf == MAIN_HYPOTHESIS["btc_tf"] and
|
||||||
|
ema_f == MAIN_HYPOTHESIS["btc_ema_fast"] and
|
||||||
|
ema_s == MAIN_HYPOTHESIS["btc_ema_slow"])
|
||||||
|
|
||||||
|
# BTC 지표 계산 + merge
|
||||||
|
df_btc = build_btc_resampled(df_calc, btc_tf, ema_f, ema_s)
|
||||||
|
df_with_btc = merge_higher_tf(df_analysis, df_btc, btc_tf)
|
||||||
|
|
||||||
|
# 백테스트
|
||||||
|
trades = run_backtest(df_with_btc, use_btc_filter=True)
|
||||||
|
is_trades, oos_trades = split_is_oos(trades, split_ts)
|
||||||
|
|
||||||
|
# IS 거래 수 체크
|
||||||
|
if len(is_trades) < MIN_IS_TRADES:
|
||||||
|
status = "SKIP(IS<100)"
|
||||||
|
else:
|
||||||
|
status = "MAIN" if is_main else "sweep"
|
||||||
|
|
||||||
|
# 메트릭
|
||||||
|
is_raw = calc_metrics([t.pnl_bps for t in is_trades])
|
||||||
|
is_fees = calc_metrics(apply_cost(is_trades, "fees_only"))
|
||||||
|
oos_raw = calc_metrics([t.pnl_bps for t in oos_trades])
|
||||||
|
oos_fees = calc_metrics(apply_cost(oos_trades, "fees_only"))
|
||||||
|
oos_real = calc_metrics(apply_cost(oos_trades, "realistic"))
|
||||||
|
|
||||||
|
# LONG/SHORT 분리 (OOS)
|
||||||
|
oos_long = [t for t in oos_trades if t.side == "LONG"]
|
||||||
|
oos_short = [t for t in oos_trades if t.side == "SHORT"]
|
||||||
|
oos_fees_l = calc_metrics(apply_cost(oos_long, "fees_only"))
|
||||||
|
oos_fees_s = calc_metrics(apply_cost(oos_short, "fees_only"))
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
"label": label, "is_main": is_main, "status": status,
|
||||||
|
"is_trades": is_raw["trades"], "is_raw_pf": is_raw["pf"],
|
||||||
|
"is_fees_pf": is_fees["pf"],
|
||||||
|
"oos_trades": oos_raw["trades"], "oos_raw_pf": oos_raw["pf"],
|
||||||
|
"oos_fees_pf": oos_fees["pf"], "oos_real_pf": oos_real["pf"],
|
||||||
|
"oos_fees_pnl": oos_fees["cum_pnl"],
|
||||||
|
"oos_long_fees_pf": oos_fees_l["pf"], "oos_short_fees_pf": oos_fees_s["pf"],
|
||||||
|
"oos_long_n": oos_fees_l["trades"], "oos_short_n": oos_fees_s["trades"],
|
||||||
|
})
|
||||||
|
|
||||||
|
# 거래 로그 저장
|
||||||
|
save_trade_log(trades, trade_log_path, label)
|
||||||
|
|
||||||
|
# ── Sweep 결과 테이블 ──
|
||||||
|
print(f"\n {'Label':<22} {'St':>6} {'IS_N':>5} {'IS_FPF':>6} "
|
||||||
|
f"{'OOS_N':>5} {'OOS_RPF':>7} {'OOS_FPF':>7} {'OOS_rPF':>7} "
|
||||||
|
f"{'L_FPF':>6} {'S_FPF':>6}")
|
||||||
|
print(f" {'-'*92}")
|
||||||
|
|
||||||
|
for r in results:
|
||||||
|
marker = " ★" if r["is_main"] else ""
|
||||||
|
print(f" {r['label']:<22} {r['status']:>6} {r['is_trades']:>5} {r['is_fees_pf']:>6.2f} "
|
||||||
|
f"{r['oos_trades']:>5} {r['oos_raw_pf']:>7.2f} {r['oos_fees_pf']:>7.2f} {r['oos_real_pf']:>7.2f} "
|
||||||
|
f"{r['oos_long_fees_pf']:>6.2f} {r['oos_short_fees_pf']:>6.2f}{marker}")
|
||||||
|
|
||||||
|
# ── 3. 메인 가설 상세 결과 ──
|
||||||
|
main_result = next((r for r in results if r["is_main"]), None)
|
||||||
|
if main_result is None:
|
||||||
|
print("\n [ERROR] 메인 가설 결과 없음")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("\n" + "=" * 72)
|
||||||
|
print(f" 메인 가설 상세: {main_result['label']}")
|
||||||
|
print("=" * 72)
|
||||||
|
|
||||||
|
# 메인 가설 재실행하여 상세 출력
|
||||||
|
df_btc_main = build_btc_resampled(
|
||||||
|
df_calc, MAIN_HYPOTHESIS["btc_tf"],
|
||||||
|
MAIN_HYPOTHESIS["btc_ema_fast"], MAIN_HYPOTHESIS["btc_ema_slow"])
|
||||||
|
df_main = merge_higher_tf(df_analysis, df_btc_main, MAIN_HYPOTHESIS["btc_tf"])
|
||||||
|
main_trades = run_backtest(df_main, use_btc_filter=True)
|
||||||
|
main_is, main_oos = split_is_oos(main_trades, split_ts)
|
||||||
|
|
||||||
|
print_section("IS (메인 가설)", main_is)
|
||||||
|
print_section("OOS (메인 가설)", main_oos)
|
||||||
|
|
||||||
|
# ── 4. 판정 ──
|
||||||
|
print("\n" + "=" * 72)
|
||||||
|
print(" 판정")
|
||||||
|
print("=" * 72)
|
||||||
|
|
||||||
|
# 베이스라인 비교
|
||||||
|
bl_oos_fees = calc_metrics(apply_cost(baseline_oos, "fees_only"))
|
||||||
|
main_oos_fees = calc_metrics(apply_cost(main_oos, "fees_only"))
|
||||||
|
main_oos_real = calc_metrics(apply_cost(main_oos, "realistic"))
|
||||||
|
|
||||||
|
print(f"\n 베이스라인 OOS fees_only PF: {bl_oos_fees['pf']:.2f} ({bl_oos_fees['trades']}건)")
|
||||||
|
print(f" 메인 가설 OOS fees_only PF: {main_oos_fees['pf']:.2f} ({main_oos_fees['trades']}건)")
|
||||||
|
print(f" 메인 가설 OOS realistic PF: {main_oos_real['pf']:.2f}")
|
||||||
|
print(f" 개선폭: fees_only PF {main_oos_fees['pf'] - bl_oos_fees['pf']:+.2f}")
|
||||||
|
|
||||||
|
# 합격 기준 체크
|
||||||
|
checks = []
|
||||||
|
checks.append(("OOS fees_only PF >= 1.2", main_oos_fees["pf"] >= 1.2, f"{main_oos_fees['pf']:.2f}"))
|
||||||
|
checks.append(("OOS realistic PF >= 1.0", main_oos_real["pf"] >= 1.0, f"{main_oos_real['pf']:.2f}"))
|
||||||
|
checks.append(("OOS 거래수 >= 50", main_oos_fees["trades"] >= 50, f"{main_oos_fees['trades']}"))
|
||||||
|
|
||||||
|
# LONG/SHORT 대칭성
|
||||||
|
oos_long_t = [t for t in main_oos if t.side == "LONG"]
|
||||||
|
oos_short_t = [t for t in main_oos if t.side == "SHORT"]
|
||||||
|
l_pf = calc_metrics(apply_cost(oos_long_t, "fees_only"))["pf"]
|
||||||
|
s_pf = calc_metrics(apply_cost(oos_short_t, "fees_only"))["pf"]
|
||||||
|
checks.append(("LONG/SHORT fees PF >= 0.8", l_pf >= 0.8 and s_pf >= 0.8, f"L:{l_pf:.2f} S:{s_pf:.2f}"))
|
||||||
|
|
||||||
|
# IS/OOS 격차
|
||||||
|
main_is_fees = calc_metrics(apply_cost(main_is, "fees_only"))
|
||||||
|
if main_is_fees["pf"] > 0:
|
||||||
|
gap = abs(main_oos_fees["pf"] - main_is_fees["pf"]) / main_is_fees["pf"]
|
||||||
|
else:
|
||||||
|
gap = 1.0
|
||||||
|
checks.append(("IS/OOS PF 격차 < 30%", gap < 0.3, f"{gap*100:.1f}%"))
|
||||||
|
|
||||||
|
# 베이스라인 대비 개선
|
||||||
|
improvement = main_oos_fees["pf"] > bl_oos_fees["pf"]
|
||||||
|
checks.append(("베이스라인 대비 개선", improvement,
|
||||||
|
f"{main_oos_fees['pf']:.2f} vs {bl_oos_fees['pf']:.2f}"))
|
||||||
|
|
||||||
|
print(f"\n 합격 기준 체크:")
|
||||||
|
all_pass = True
|
||||||
|
for desc, passed, val in checks:
|
||||||
|
icon = "PASS" if passed else "FAIL"
|
||||||
|
print(f" [{icon}] {desc}: {val}")
|
||||||
|
if not passed:
|
||||||
|
all_pass = False
|
||||||
|
|
||||||
|
print()
|
||||||
|
if all_pass:
|
||||||
|
print(" ★ [최종 판정: PASS] BTC 추세 필터가 유효합니다.")
|
||||||
|
else:
|
||||||
|
print(" ✗ [최종 판정: FAIL] BTC 추세 필터로도 기준 미달. MTF 전략 폐기.")
|
||||||
|
|
||||||
|
# robustness 요약
|
||||||
|
passing_combos = [r for r in results if r["status"] != "SKIP(IS<100)" and r["oos_fees_pf"] >= 1.2]
|
||||||
|
print(f"\n Robustness: {len(passing_combos)}/{len(results)} 조합이 OOS fees_only PF >= 1.2")
|
||||||
|
|
||||||
|
print(f"\n 거래 로그: {trade_log_path}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
49
scripts/revalidate_apr15.py
Normal file
49
scripts/revalidate_apr15.py
Normal 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()
|
||||||
459
scripts/trade_ls_analysis.py
Normal file
459
scripts/trade_ls_analysis.py
Normal 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())
|
||||||
@@ -106,3 +106,21 @@ class Config:
|
|||||||
volume_multiplier=self.volume_multiplier,
|
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},
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
113
src/mtf_bot.py
113
src/mtf_bot.py
@@ -390,6 +390,21 @@ class TriggerStrategy:
|
|||||||
|
|
||||||
_MTF_TRADE_DIR = Path("data/trade_history")
|
_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:
|
class ExecutionManager:
|
||||||
"""
|
"""
|
||||||
@@ -408,6 +423,11 @@ class ExecutionManager:
|
|||||||
self._sl_price: Optional[float] = None
|
self._sl_price: Optional[float] = None
|
||||||
self._tp_price: Optional[float] = None
|
self._tp_price: Optional[float] = None
|
||||||
self._atr_at_entry: 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]:
|
def execute(self, signal: str, current_price: float, atr_value: Optional[float]) -> Optional[Dict]:
|
||||||
"""
|
"""
|
||||||
@@ -424,6 +444,10 @@ class ExecutionManager:
|
|||||||
if signal == "HOLD":
|
if signal == "HOLD":
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if self._killed:
|
||||||
|
logger.warning(f"[ExecutionManager] 킬스위치 발동 상태 — 신규 진입 차단 (신호={signal})")
|
||||||
|
return None
|
||||||
|
|
||||||
if self.current_position is not None:
|
if self.current_position is not None:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"[ExecutionManager] 포지션 중복 차단: "
|
f"[ExecutionManager] 포지션 중복 차단: "
|
||||||
@@ -503,6 +527,10 @@ class ExecutionManager:
|
|||||||
# JSONL에 기록
|
# JSONL에 기록
|
||||||
self._save_trade(reason, exit_price, pnl_bps)
|
self._save_trade(reason, exit_price, pnl_bps)
|
||||||
|
|
||||||
|
# ── 킬스위치: 거래 이력 추가 + 판정 ──
|
||||||
|
self._append_trade_history(pnl_bps)
|
||||||
|
self._check_kill_switch()
|
||||||
|
|
||||||
# ── 실주문 (프로덕션 전환 시 주석 해제) ──
|
# ── 실주문 (프로덕션 전환 시 주석 해제) ──
|
||||||
# if self.current_position == "LONG":
|
# if self.current_position == "LONG":
|
||||||
# await self.exchange.create_market_sell_order(symbol, amount)
|
# await self.exchange.create_market_sell_order(symbol, amount)
|
||||||
@@ -540,6 +568,91 @@ class ExecutionManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"[ExecutionManager] 거래 기록 저장 실패: {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:
|
def get_position_info(self) -> Dict:
|
||||||
"""현재 포지션 정보 반환."""
|
"""현재 포지션 정보 반환."""
|
||||||
return {
|
return {
|
||||||
|
|||||||
181
tests/test_evaluate_oos.py
Normal file
181
tests/test_evaluate_oos.py
Normal 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)
|
||||||
@@ -421,3 +421,107 @@ class TestExecutionManager:
|
|||||||
# TP/SL = 2.3/1.5 = 1.533...
|
# TP/SL = 2.3/1.5 = 1.533...
|
||||||
expected_rr = round(2.3 / 1.5, 2)
|
expected_rr = round(2.3 / 1.5, 2)
|
||||||
assert result["risk_reward"] == expected_rr
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user