test_bbmr_report.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. import pytest
  2. from okx_codex_trader import bbmr_report
  3. from okx_codex_trader.bbmr_report import (
  4. BBMRConfig,
  5. generate_bbmr_sampled_report,
  6. run_bbmr_segment,
  7. )
  8. from okx_codex_trader.models import Candle
  9. from okx_codex_trader.sampled_report import SegmentResult
  10. def build_candles_from_closes(closes: list[float]) -> list[Candle]:
  11. candles: list[Candle] = []
  12. for index, close in enumerate(closes):
  13. candles.append(
  14. Candle(
  15. symbol="BTC-USDT-SWAP",
  16. ts=index * 60_000,
  17. open=close,
  18. high=close + 1.0,
  19. low=close - 1.0,
  20. close=close,
  21. volume=1_000.0 + index,
  22. )
  23. )
  24. return candles
  25. def build_linear_candles(count: int) -> list[Candle]:
  26. return build_candles_from_closes([100.0 + index for index in range(count)])
  27. def test_run_bbmr_segment_can_enter_on_final_bar_from_prior_signal():
  28. config = BBMRConfig(band_length=2, std_multiplier=0.1, bandwidth_lookback=1, stop_loss_pct=0.005)
  29. candles = build_candles_from_closes([100.0, 110.0, 106.0, 96.0])
  30. result = run_bbmr_segment(candles=candles, leverage=2, warmup_bars=2, config=config)
  31. assert isinstance(result, SegmentResult)
  32. assert result.trade_count == 1
  33. assert result.trades[0]["exit_price"] == pytest.approx(95.52)
  34. assert result.entries == [{"ts": 180_000, "price": 96.0, "side": "long"}]
  35. assert result.open_position is None
  36. def test_run_bbmr_segment_stop_loss_takes_precedence_and_no_reverse_entry():
  37. config = BBMRConfig(band_length=2, std_multiplier=0.1, bandwidth_lookback=1, stop_loss_pct=0.01)
  38. candles = [
  39. Candle(symbol="BTC-USDT-SWAP", ts=0, open=100.0, high=101.0, low=99.0, close=100.0, volume=1000.0),
  40. Candle(symbol="BTC-USDT-SWAP", ts=60_000, open=110.0, high=111.0, low=109.0, close=110.0, volume=1001.0),
  41. Candle(symbol="BTC-USDT-SWAP", ts=120_000, open=104.0, high=105.0, low=103.0, close=104.0, volume=1002.0),
  42. Candle(symbol="BTC-USDT-SWAP", ts=180_000, open=105.0, high=105.2, low=103.7, close=103.8, volume=1003.0),
  43. Candle(symbol="BTC-USDT-SWAP", ts=240_000, open=106.0, high=109.0, low=103.5, close=108.0, volume=1004.0),
  44. Candle(symbol="BTC-USDT-SWAP", ts=300_000, open=107.0, high=108.0, low=106.0, close=107.0, volume=1005.0),
  45. ]
  46. result = run_bbmr_segment(candles=candles, leverage=2, warmup_bars=2, config=config)
  47. assert result.trade_count == 1
  48. assert len(result.trades) == 1
  49. assert result.trades[0]["side"] == "Long"
  50. assert result.trades[0]["exit_price"] == pytest.approx(103.95)
  51. assert result.open_position is None
  52. def test_run_bbmr_segment_marks_open_position_to_market_but_keeps_journal_realized_only():
  53. config = BBMRConfig(band_length=2, std_multiplier=0.1, bandwidth_lookback=1, stop_loss_pct=0.2)
  54. candles = build_candles_from_closes([100.0, 110.0, 104.0, 103.0, 102.0])
  55. result = run_bbmr_segment(candles=candles, leverage=2, warmup_bars=2, config=config)
  56. assert result.trade_count == 0
  57. assert result.trades == []
  58. assert isinstance(result, SegmentResult)
  59. assert result.total_return == pytest.approx((9805.825242718447 - 10_000.0) / 10_000.0)
  60. assert result.open_position is not None
  61. def test_generate_bbmr_sampled_report_rejects_insufficient_history_pool(tmp_path):
  62. candles = build_linear_candles(1_000)
  63. with pytest.raises(ValueError, match="history pool is too small"):
  64. generate_bbmr_sampled_report(
  65. candles=candles,
  66. leverage=2,
  67. output_file=tmp_path / "bbmr.html",
  68. symbol="BTC-USDT-SWAP",
  69. bar="3m",
  70. segments=8,
  71. window_size=300,
  72. )
  73. def test_generate_bbmr_sampled_report_uses_shared_shell(monkeypatch, tmp_path):
  74. candles = build_linear_candles(500)
  75. output_file = tmp_path / "bbmr.html"
  76. recorded: dict[str, object] = {}
  77. sentinel = {"report_file": str(output_file), "segment_count": 2, "window_size": 50, "aggregate_trade_count": 3, "average_return": 0.25}
  78. def fake_generate_sampled_report(**kwargs):
  79. recorded.update(kwargs)
  80. return sentinel
  81. monkeypatch.setattr(bbmr_report, "generate_sampled_report", fake_generate_sampled_report)
  82. result = generate_bbmr_sampled_report(
  83. candles=candles,
  84. leverage=3,
  85. output_file=output_file,
  86. symbol="BTC-USDT-SWAP",
  87. bar="3m",
  88. segments=2,
  89. window_size=50,
  90. )
  91. assert result == sentinel
  92. assert recorded["candles"] == candles
  93. assert recorded["leverage"] == 3
  94. assert recorded["output_file"] == output_file
  95. assert recorded["symbol"] == "BTC-USDT-SWAP"
  96. assert recorded["bar"] == "3m"
  97. assert recorded["segments"] == 2
  98. assert recorded["window_size"] == 50
  99. assert recorded["report_title"] == "BBMR Sampled Report"
  100. assert recorded["strategy_label"] == "BBMR"
  101. assert recorded["strategy_description"] == (
  102. "Bollinger Band mean reversion, bandwidth filter against previous 50 completed values, "
  103. "close-based return-to-middle exits at next open, intrabar 0.5% stop-loss."
  104. )
  105. assert recorded["strategy_params"] == {
  106. "band_length": 20,
  107. "std_multiplier": 2.0,
  108. "bandwidth_lookback": 50,
  109. "stop_loss_pct": 0.005,
  110. }
  111. assert recorded["run_segment"] is run_bbmr_segment