test_explore_ultrashort.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. import importlib.util
  2. import sys
  3. from pathlib import Path
  4. import pytest
  5. from okx_codex_trader.models import Candle
  6. from okx_codex_trader.sampled_report import SegmentResult
  7. def load_explore_module():
  8. path = Path(__file__).resolve().parents[1] / "scripts" / "explore_ultrashort.py"
  9. spec = importlib.util.spec_from_file_location("explore_ultrashort", path)
  10. assert spec is not None
  11. module = importlib.util.module_from_spec(spec)
  12. assert spec.loader is not None
  13. sys.modules[spec.name] = module
  14. spec.loader.exec_module(module)
  15. return module
  16. def build_candles(count: int) -> list[Candle]:
  17. return [
  18. Candle(
  19. symbol="BTC-USDT-SWAP",
  20. ts=index * 60_000,
  21. open=100.0 + index,
  22. high=101.0 + index,
  23. low=99.0 + index,
  24. close=100.0 + index,
  25. volume=1_000.0 + index,
  26. )
  27. for index in range(count)
  28. ]
  29. def build_result(
  30. total_return: float,
  31. trade_count: int,
  32. win_rate: float,
  33. max_drawdown: float,
  34. trade_returns: list[float] | None = None,
  35. equity_curve: list[dict[str, float | int]] | None = None,
  36. ) -> SegmentResult:
  37. return SegmentResult(
  38. trade_count=trade_count,
  39. total_return=total_return,
  40. win_rate=win_rate,
  41. max_drawdown=max_drawdown,
  42. trades=[
  43. {
  44. "side": "Long",
  45. "entry_time": "2026-04-01 00:00",
  46. "exit_time": "2026-04-01 00:15",
  47. "entry_price": 100.0,
  48. "exit_price": 100.0 * (1.0 + trade_return),
  49. "pnl": trade_return * 10_000.0,
  50. "return_pct": trade_return * 100.0,
  51. }
  52. for trade_return in (trade_returns or [])
  53. ],
  54. open_position=None,
  55. candles=[],
  56. equity_curve=equity_curve or [],
  57. entries=[],
  58. exits=[],
  59. )
  60. def test_evaluate_candidate_all_windows_uses_complete_non_overlapping_windows():
  61. module = load_explore_module()
  62. returns = iter([0.01, -0.02, 0.03])
  63. calls: list[dict[str, object]] = []
  64. def run_segment(*, candles, leverage, warmup_bars):
  65. calls.append(
  66. {
  67. "first_ts": candles[0].ts,
  68. "count": len(candles),
  69. "leverage": leverage,
  70. "warmup_bars": warmup_bars,
  71. }
  72. )
  73. return build_result(
  74. next(returns),
  75. trade_count=2,
  76. win_rate=0.5,
  77. max_drawdown=0.04,
  78. trade_returns=[0.02, -0.01],
  79. )
  80. metrics = module.evaluate_candidate_all_windows(
  81. candidate=module.Candidate("test-candidate", warmup_bars=2, run=run_segment),
  82. candles=build_candles(17),
  83. window_size=5,
  84. leverage=3,
  85. )
  86. assert calls == [
  87. {"first_ts": 0, "count": 7, "leverage": 3, "warmup_bars": 2},
  88. {"first_ts": 300_000, "count": 7, "leverage": 3, "warmup_bars": 2},
  89. {"first_ts": 600_000, "count": 7, "leverage": 3, "warmup_bars": 2},
  90. ]
  91. assert metrics["sample_count"] == 3
  92. assert metrics["avg_return"] == pytest.approx(0.0066666667)
  93. assert metrics["median_return"] == pytest.approx(0.01)
  94. assert metrics["positive_window_rate"] == pytest.approx(2 / 3)
  95. assert metrics["worst_return"] == pytest.approx(-0.02)
  96. assert metrics["p10_return"] == pytest.approx(-0.014)
  97. assert metrics["p90_return"] == pytest.approx(0.026)
  98. assert metrics["best_return"] == pytest.approx(0.03)
  99. assert metrics["trades"] == 6
  100. assert metrics["avg_trades_per_window"] == pytest.approx(2.0)
  101. assert metrics["win_rate"] == pytest.approx(0.5)
  102. assert metrics["trade_win_rate"] == pytest.approx(0.5)
  103. assert metrics["avg_trade_return"] == pytest.approx(0.005)
  104. assert metrics["avg_win_return"] == pytest.approx(0.02)
  105. assert metrics["avg_loss_return_abs"] == pytest.approx(0.01)
  106. assert metrics["payoff_ratio"] == pytest.approx(2.0)
  107. assert metrics["profit_factor"] == pytest.approx(2.0)
  108. assert metrics["expectancy_per_trade"] == pytest.approx(0.005)
  109. assert metrics["max_drawdown"] == pytest.approx(0.04)
  110. assert metrics["return_drawdown_ratio"] == pytest.approx(0.0066666667 / 0.04)
  111. assert metrics["ci95_low"] < metrics["avg_return"] < metrics["ci95_high"]
  112. def test_evaluate_candidate_window_rows_keeps_window_timestamps():
  113. module = load_explore_module()
  114. def run_segment(*, candles, leverage, warmup_bars):
  115. return build_result(
  116. 0.01,
  117. trade_count=1,
  118. win_rate=1.0,
  119. max_drawdown=0.02,
  120. trade_returns=[0.01],
  121. )
  122. rows = module.evaluate_candidate_window_rows(
  123. candidate=module.Candidate("test-candidate", warmup_bars=2, run=run_segment),
  124. candles=build_candles(12),
  125. window_size=5,
  126. leverage=3,
  127. )
  128. assert rows[0]["window_start_ts"] == 120_000
  129. assert rows[0]["window_end_ts"] == 360_000
  130. assert rows[1]["window_start_ts"] == 420_000
  131. assert rows[1]["window_end_ts"] == 660_000
  132. assert module.summarize_window_rows(rows)["sample_count"] == 2
  133. def test_sort_robust_results_prioritizes_confidence_interval_lower_bound():
  134. module = load_explore_module()
  135. pandas = pytest.importorskip("pandas")
  136. frame = pandas.DataFrame(
  137. [
  138. {"name": "higher-average", "avg_return": 0.10, "ci95_low": -0.01},
  139. {"name": "supported", "avg_return": 0.03, "ci95_low": 0.01},
  140. {"name": "tie-breaker", "avg_return": 0.04, "ci95_low": 0.01},
  141. ]
  142. )
  143. sorted_frame = module.sort_robust_results(frame)
  144. assert sorted_frame["name"].tolist() == ["tie-breaker", "supported", "higher-average"]
  145. def test_add_cost_metrics_and_sort_cost_results():
  146. module = load_explore_module()
  147. pandas = pytest.importorskip("pandas")
  148. frame = pandas.DataFrame(
  149. [
  150. {"name": "active", "avg_return": 0.01, "ci95_low": 0.004, "ci95_high": 0.016, "avg_trades_per_window": 10.0},
  151. {"name": "selective", "avg_return": 0.006, "ci95_low": 0.003, "ci95_high": 0.009, "avg_trades_per_window": 2.0},
  152. ]
  153. )
  154. with_cost = module.add_cost_metrics(frame, 0.0012)
  155. sorted_frame = module.sort_cost_results(with_cost)
  156. assert with_cost.loc[0, "net_avg_return"] == pytest.approx(-0.002)
  157. assert with_cost.loc[0, "net_ci95_low"] == pytest.approx(-0.008)
  158. assert with_cost.loc[0, "breakeven_roundtrip_cost_on_margin"] == pytest.approx(0.001)
  159. assert with_cost.loc[1, "net_avg_return"] == pytest.approx(0.0036)
  160. assert with_cost.loc[1, "net_ci95_low"] == pytest.approx(0.0006)
  161. assert sorted_frame["name"].tolist() == ["selective", "active"]
  162. def test_summarize_periods_applies_trade_cost_to_period_average():
  163. module = load_explore_module()
  164. pandas = pytest.importorskip("pandas")
  165. frame = pandas.DataFrame(
  166. [
  167. {"window_end_ts": 0, "total_return": 0.01, "trade_count": 2, "win_rate": 0.5, "max_drawdown": 0.01},
  168. {"window_end_ts": 86_400_000, "total_return": -0.02, "trade_count": 1, "win_rate": 0.0, "max_drawdown": 0.03},
  169. ]
  170. )
  171. monthly = module.summarize_periods(frame, "M", 0.001)
  172. assert monthly.loc[0, "period"] == "1970-01"
  173. assert monthly.loc[0, "window_count"] == 2
  174. assert monthly.loc[0, "avg_return"] == pytest.approx(-0.005)
  175. assert monthly.loc[0, "positive_window_rate"] == pytest.approx(0.5)
  176. assert monthly.loc[0, "trades"] == 3
  177. assert monthly.loc[0, "net_avg_return"] == pytest.approx(-0.0065)
  178. def test_summarize_cost_adjusted_trade_equity_periods_compounds_net_trade_returns():
  179. module = load_explore_module()
  180. result = build_result(
  181. 0.0,
  182. trade_count=2,
  183. win_rate=0.5,
  184. max_drawdown=0.0,
  185. trade_returns=[0.01, -0.02],
  186. equity_curve=[{"ts": 1_775_001_600_000, "equity": 10_000.0, "close": 100.0}],
  187. )
  188. monthly = module.summarize_cost_adjusted_trade_equity_periods(result, "M", 0.001)
  189. assert monthly.loc[0, "period"] == "2026-04"
  190. assert monthly.loc[0, "trades"] == 2
  191. assert monthly.loc[0, "end_equity"] == pytest.approx(10_000.0 * 1.009 * 0.979)
  192. def test_add_market_regime_columns_adds_net_return_and_buckets():
  193. module = load_explore_module()
  194. rows = [
  195. {"window_start_ts": 300 * 60_000, "window_end_ts": 359 * 60_000, "total_return": 0.02, "trade_count": 2, "win_rate": 0.5, "max_drawdown": 0.01, "trades": []},
  196. {"window_start_ts": 360 * 60_000, "window_end_ts": 419 * 60_000, "total_return": -0.01, "trade_count": 1, "win_rate": 0.0, "max_drawdown": 0.02, "trades": []},
  197. {"window_start_ts": 420 * 60_000, "window_end_ts": 479 * 60_000, "total_return": 0.03, "trade_count": 3, "win_rate": 1.0, "max_drawdown": 0.01, "trades": []},
  198. ]
  199. frame = module.add_market_regime_columns(build_candles(500), rows, 0.001)
  200. assert frame.loc[0, "net_return"] == pytest.approx(0.018)
  201. assert set(frame["market_return_bucket"].astype(str)) <= {"down", "flat", "up"}
  202. assert "realized_vol" in frame
  203. assert "ma240_distance" in frame
  204. def test_annualized_metrics_from_cost_adjusted_equity():
  205. module = load_explore_module()
  206. pandas = pytest.importorskip("pandas")
  207. frame = pandas.DataFrame(
  208. [
  209. {"ts": pandas.Timestamp("2025-01-01", tz="UTC"), "equity": 100.0},
  210. {"ts": pandas.Timestamp("2025-01-02", tz="UTC"), "equity": 120.0},
  211. {"ts": pandas.Timestamp("2025-01-03", tz="UTC"), "equity": 90.0},
  212. {"ts": pandas.Timestamp("2026-01-01", tz="UTC"), "equity": 110.0},
  213. ]
  214. )
  215. metrics = module.annualized_metrics_from_equity(frame, 1_735_689_600_000, 1_767_225_600_000)
  216. assert metrics["net_total_return"] == pytest.approx(0.10)
  217. assert metrics["net_annualized_return"] == pytest.approx(0.10)
  218. assert metrics["net_max_drawdown"] == pytest.approx(0.25)
  219. assert metrics["net_calmar"] == pytest.approx(0.4)
  220. assert "net_sharpe_daily" in metrics
  221. def test_recent_horizon_metrics_use_equity_at_cutoff():
  222. module = load_explore_module()
  223. pandas = pytest.importorskip("pandas")
  224. frame = pandas.DataFrame(
  225. [
  226. {"ts": pandas.Timestamp("2024-01-01 00:00", tz="UTC"), "equity": 100.0},
  227. {"ts": pandas.Timestamp("2025-06-01 00:00", tz="UTC"), "equity": 120.0},
  228. {"ts": pandas.Timestamp("2025-12-01 00:00", tz="UTC"), "equity": 90.0},
  229. {"ts": pandas.Timestamp("2026-04-01 00:00", tz="UTC"), "equity": 110.0},
  230. ]
  231. )
  232. rows = module.recent_horizon_metrics_from_equity(
  233. frame,
  234. int(pandas.Timestamp("2026-04-01 00:00", tz="UTC").timestamp() * 1000),
  235. (("1y", pandas.DateOffset(years=1)), ("3m", pandas.DateOffset(months=3))),
  236. )
  237. assert rows.loc[0, "horizon"] == "1y"
  238. assert rows.loc[0, "horizon_start"] == "2025-04-01 00:00"
  239. assert rows.loc[0, "net_total_return"] == pytest.approx(110.0 / 100.0 - 1.0)
  240. assert rows.loc[1, "horizon"] == "3m"
  241. assert rows.loc[1, "horizon_start"] == "2026-01-01 00:00"
  242. assert rows.loc[1, "net_total_return"] == pytest.approx(110.0 / 90.0 - 1.0)
  243. def test_build_ma_cross_candidate_names_and_warmup():
  244. module = load_explore_module()
  245. candidate = module.build_ma_cross_candidate(20, 80, "long")
  246. assert candidate.name == "ma-cross-long-f20-s80"
  247. assert candidate.warmup_bars == 80
  248. def test_build_rsi2_long_guarded_candidate_names_and_warmup():
  249. module = load_explore_module()
  250. candidate = module.build_rsi2_long_guarded_candidate(240, 2.0, 55.0, 0.008, 96)
  251. assert candidate.name == "rsi2-long-guarded-t240-l2.0-x55.0-sl0.008-mh96"
  252. assert candidate.warmup_bars == 240
  253. def test_build_trend_rsi_bb_long_candidate_names_and_warmup():
  254. module = load_explore_module()
  255. candidate = module.build_trend_rsi_bb_long_candidate(240, 20, 2.5, 3.0, 45.0, 0.005)
  256. assert candidate.name == "trend-rsi-bb-long-t240-b20-m2.5-r3.0-x45.0-sl0.005"
  257. assert candidate.warmup_bars == 240
  258. def test_build_regime_hybrid_candidate_names_and_warmup():
  259. module = load_explore_module()
  260. candidate = module.build_regime_hybrid_candidate(240, 240, 0.015, 2.0, 55.0, 2.5, 0.008)
  261. assert candidate.name == "regime-hybrid-t240-r240-n0.015-l2.0-x55.0-m2.5-sl0.008"
  262. assert candidate.warmup_bars == 240
  263. def test_align_pair_candles_keeps_shared_timestamps():
  264. module = load_explore_module()
  265. left = build_candles(4)
  266. right = [
  267. Candle(symbol="BTC-USDT-SWAP", ts=60_000, open=1.0, high=1.0, low=1.0, close=1.0, volume=1.0),
  268. Candle(symbol="BTC-USDT-SWAP", ts=180_000, open=3.0, high=3.0, low=3.0, close=3.0, volume=1.0),
  269. ]
  270. left_aligned, right_aligned = module.align_pair_candles(left, right)
  271. assert [candle.ts for candle in left_aligned] == [60_000, 180_000]
  272. assert [candle.ts for candle in right_aligned] == [60_000, 180_000]
  273. def test_build_eth_btc_rsi_filter_candidate_names_and_warmup():
  274. module = load_explore_module()
  275. candidate = module.build_eth_btc_rsi_filter_candidate(50, 3.0, 55.0, 240, 96, 0.01)
  276. assert candidate.name == "eth-btc-rsi-filter-et50-l3.0-x55.0-bt240-bm96-br0.01"
  277. assert candidate.warmup_bars == 240
  278. def test_build_eth_btc_shock_filter_candidate_names_and_warmup():
  279. module = load_explore_module()
  280. candidate = module.build_eth_btc_shock_filter_candidate(50, 3.0, 55.0, 480, 240, 0.0, 240, 0.006, 0.05)
  281. assert candidate.name == "eth-btc-shock-filter-et50-l3.0-x55.0-bt480-bm240-br0.0-sw240-sv0.006-sd0.05"
  282. assert candidate.warmup_bars == 480
  283. def test_build_eth_btc_ratio_pullback_candidate_names_and_warmup():
  284. module = load_explore_module()
  285. candidate = module.build_eth_btc_ratio_pullback_candidate(480, 96, 0.01, 48, 2.0, 5.0, 0.008)
  286. assert candidate.name == "eth-btc-ratio-pullback-bt480-bm96-br0.01-rl48-rs2.0-rr5.0-sl0.008"
  287. assert candidate.warmup_bars == 480
  288. def test_build_btc_lead_eth_lag_candidate_names_and_warmup():
  289. module = load_explore_module()
  290. candidate = module.build_btc_lead_eth_lag_candidate(16, 0.018, 0.012, 32, 0.006, 0.012)
  291. assert candidate.name == "btc-lead-eth-lag-lb16-br0.018-gap0.012-mh32-sl0.006-tp0.012"
  292. assert candidate.warmup_bars == 16
  293. def test_btc_lead_eth_lag_enters_after_btc_outperformance():
  294. module = load_explore_module()
  295. eth = build_candles(8)
  296. btc = build_candles(8)
  297. btc[3] = Candle(symbol="BTC-USDT-SWAP", ts=180_000, open=100.0, high=100.0, low=100.0, close=103.0, volume=1.0)
  298. btc[4] = Candle(symbol="BTC-USDT-SWAP", ts=240_000, open=103.0, high=103.0, low=103.0, close=103.0, volume=1.0)
  299. eth[3] = Candle(symbol="ETH-USDT-SWAP", ts=180_000, open=100.0, high=100.0, low=100.0, close=100.0, volume=1.0)
  300. eth[4] = Candle(symbol="ETH-USDT-SWAP", ts=240_000, open=100.0, high=102.0, low=100.0, close=101.0, volume=1.0)
  301. result = module.run_btc_lead_eth_lag_segment(
  302. eth_candles=eth,
  303. btc_candles=btc,
  304. leverage=3,
  305. warmup_bars=3,
  306. lead_lookback=3,
  307. btc_return_threshold=0.02,
  308. lag_gap=0.02,
  309. max_hold_bars=3,
  310. stop_loss_pct=0.01,
  311. take_profit_pct=0.01,
  312. )
  313. assert result.trade_count == 1
  314. assert result.trades[0]["side"] == "Long"
  315. assert result.total_return > 0.0
  316. def test_get_candles_cached_saves_exhausted_history_and_updates_latest(tmp_path):
  317. module = load_explore_module()
  318. class Client:
  319. def __init__(self):
  320. self.limits: list[int] = []
  321. def get_candles(self, symbol, bar, limit):
  322. self.limits.append(limit)
  323. if len(self.limits) == 1:
  324. return build_candles(3)
  325. return [
  326. Candle(
  327. symbol=symbol,
  328. ts=3 * 60_000,
  329. open=103.0,
  330. high=104.0,
  331. low=102.0,
  332. close=103.0,
  333. volume=1_003.0,
  334. )
  335. ]
  336. client = Client()
  337. first = module.get_candles_cached(client, "BTC-USDT-SWAP", "15m", 500, tmp_path)
  338. second = module.get_candles_cached(client, "BTC-USDT-SWAP", "15m", 500, tmp_path)
  339. assert [candle.ts for candle in first] == [0, 60_000, 120_000]
  340. assert [candle.ts for candle in second] == [0, 60_000, 120_000, 180_000]
  341. assert client.limits == [500, 300]
  342. assert (tmp_path / "BTC-USDT-SWAP" / "15m.csv").exists()
  343. assert module.load_cached_candles(tmp_path, "BTC-USDT-SWAP", "15m")[1] is True
  344. def test_get_candles_cached_bridges_stale_cache_gap(tmp_path):
  345. module = load_explore_module()
  346. module.save_cached_candles(
  347. tmp_path,
  348. "BTC-USDT-SWAP",
  349. "3m",
  350. [
  351. Candle(symbol="BTC-USDT-SWAP", ts=0, open=100.0, high=101.0, low=99.0, close=100.0, volume=1.0),
  352. Candle(symbol="BTC-USDT-SWAP", ts=180_000, open=101.0, high=102.0, low=100.0, close=101.0, volume=1.0),
  353. ],
  354. history_exhausted=True,
  355. )
  356. class Client:
  357. def __init__(self):
  358. self.limits: list[int] = []
  359. def get_candles(self, symbol, bar, limit):
  360. self.limits.append(limit)
  361. if limit == 300:
  362. return [
  363. Candle(symbol=symbol, ts=720_000, open=104.0, high=105.0, low=103.0, close=104.0, volume=1.0),
  364. Candle(symbol=symbol, ts=900_000, open=105.0, high=106.0, low=104.0, close=105.0, volume=1.0),
  365. ]
  366. return [
  367. Candle(symbol=symbol, ts=360_000, open=102.0, high=103.0, low=101.0, close=102.0, volume=1.0),
  368. Candle(symbol=symbol, ts=540_000, open=103.0, high=104.0, low=102.0, close=103.0, volume=1.0),
  369. Candle(symbol=symbol, ts=720_000, open=104.0, high=105.0, low=103.0, close=104.0, volume=1.0),
  370. Candle(symbol=symbol, ts=900_000, open=105.0, high=106.0, low=104.0, close=105.0, volume=1.0),
  371. ]
  372. candles = module.get_candles_cached(Client(), "BTC-USDT-SWAP", "3m", 10, tmp_path)
  373. assert [candle.ts for candle in candles] == [0, 180_000, 360_000, 540_000, 720_000, 900_000]
  374. def test_get_candles_cached_repairs_existing_internal_gap(tmp_path):
  375. module = load_explore_module()
  376. module.save_cached_candles(
  377. tmp_path,
  378. "BTC-USDT-SWAP",
  379. "3m",
  380. [
  381. Candle(symbol="BTC-USDT-SWAP", ts=0, open=100.0, high=101.0, low=99.0, close=100.0, volume=1.0),
  382. Candle(symbol="BTC-USDT-SWAP", ts=180_000, open=101.0, high=102.0, low=100.0, close=101.0, volume=1.0),
  383. Candle(symbol="BTC-USDT-SWAP", ts=720_000, open=104.0, high=105.0, low=103.0, close=104.0, volume=1.0),
  384. Candle(symbol="BTC-USDT-SWAP", ts=900_000, open=105.0, high=106.0, low=104.0, close=105.0, volume=1.0),
  385. ],
  386. history_exhausted=True,
  387. )
  388. class Client:
  389. def get_candles(self, symbol, bar, limit):
  390. if limit == 300:
  391. return [
  392. Candle(symbol=symbol, ts=720_000, open=104.0, high=105.0, low=103.0, close=104.0, volume=1.0),
  393. Candle(symbol=symbol, ts=900_000, open=105.0, high=106.0, low=104.0, close=105.0, volume=1.0),
  394. ]
  395. return [
  396. Candle(symbol=symbol, ts=360_000, open=102.0, high=103.0, low=101.0, close=102.0, volume=1.0),
  397. Candle(symbol=symbol, ts=540_000, open=103.0, high=104.0, low=102.0, close=103.0, volume=1.0),
  398. Candle(symbol=symbol, ts=720_000, open=104.0, high=105.0, low=103.0, close=104.0, volume=1.0),
  399. Candle(symbol=symbol, ts=900_000, open=105.0, high=106.0, low=104.0, close=105.0, volume=1.0),
  400. ]
  401. candles = module.get_candles_cached(Client(), "BTC-USDT-SWAP", "3m", 10, tmp_path)
  402. assert [candle.ts for candle in candles] == [0, 180_000, 360_000, 540_000, 720_000, 900_000]
  403. def test_history_bars_for_years_counts_minute_bars():
  404. module = load_explore_module()
  405. assert module.history_bars_for_years("15m", 10.0) == 350_400
  406. assert module.history_bars_for_years("3m", 10.0) == 1_752_000
  407. def test_build_strategy_timeframe_candidates_uses_fixed_strategy_set():
  408. module = load_explore_module()
  409. candidates = module.build_strategy_timeframe_candidates()
  410. assert [candidate.name for candidate in candidates] == [
  411. "bbmr-default",
  412. "bbsb-default",
  413. "donchian-e12-x6-s0.008",
  414. "rsi2-t50-l3.0-s97.0",
  415. "ema-pullback-f13-s34-b0.006",
  416. "range-momo-l10-tp0.006-sl0.004",
  417. "vwap-revert-w72-z2.0-sl0.006",
  418. ]