test_backtest.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. from okx_codex_trader.backtest import run_backtest
  2. from okx_codex_trader.models import Candle
  3. from okx_codex_trader.strategy import simple_moving_average
  4. def build_crossing_series() -> list[Candle]:
  5. closes = [
  6. 60.0,
  7. 60.0,
  8. 60.0,
  9. 60.0,
  10. 60.0,
  11. 60.0,
  12. 60.0,
  13. 60.0,
  14. 60.0,
  15. 60.0,
  16. 60.0,
  17. 60.0,
  18. 60.0,
  19. 60.0,
  20. 60.0,
  21. 60.0,
  22. 60.0,
  23. 60.0,
  24. 60.0,
  25. 60.0,
  26. 140.0,
  27. 50.0,
  28. 50.0,
  29. 50.0,
  30. 50.0,
  31. 50.0,
  32. 50.0,
  33. 50.0,
  34. 50.0,
  35. 50.0,
  36. 60.0,
  37. 60.0,
  38. 60.0,
  39. 60.0,
  40. 60.0,
  41. 60.0,
  42. 60.0,
  43. 60.0,
  44. 60.0,
  45. 60.0,
  46. 60.0,
  47. 60.0,
  48. 60.0,
  49. 60.0,
  50. 60.0,
  51. ]
  52. opens = list(closes)
  53. opens[21] = 100.0
  54. opens[30] = 90.0
  55. opens[40] = 80.0
  56. candles = []
  57. for index, (open_price, close_price) in enumerate(zip(opens, closes)):
  58. high = max(open_price, close_price)
  59. low = min(open_price, close_price)
  60. candles.append(
  61. Candle(
  62. symbol="BTC-USDT-SWAP",
  63. ts=index,
  64. open=open_price,
  65. high=high,
  66. low=low,
  67. close=close_price,
  68. volume=1_000.0,
  69. )
  70. )
  71. return candles
  72. def build_open_position_series() -> list[Candle]:
  73. candles = build_crossing_series()[:29]
  74. return candles
  75. def build_tail_drawdown_series() -> list[Candle]:
  76. candles = build_crossing_series()[:22]
  77. return candles
  78. def build_drawdown_series() -> list[Candle]:
  79. closes = [60.0] * 20 + [120.0, 75.0, 100.0] + [75.0] * 14
  80. opens = list(closes)
  81. opens[21] = 100.0
  82. opens[36] = 100.0
  83. candles = []
  84. for index, (open_price, close_price) in enumerate(zip(opens, closes)):
  85. high = max(open_price, close_price)
  86. low = min(open_price, close_price)
  87. candles.append(
  88. Candle(
  89. symbol="BTC-USDT-SWAP",
  90. ts=index,
  91. open=open_price,
  92. high=high,
  93. low=low,
  94. close=close_price,
  95. volume=1_000.0,
  96. )
  97. )
  98. return candles
  99. def test_simple_moving_average_requires_full_window():
  100. candles = [
  101. Candle(symbol="BTC-USDT-SWAP", ts=index, open=close, high=close, low=close, close=close, volume=1_000.0)
  102. for index, close in enumerate([10.0, 20.0, 30.0, 40.0])
  103. ]
  104. assert simple_moving_average(candles, 3) == [None, None, 20.0, 30.0]
  105. def test_backtest_rejects_invalid_leverage():
  106. candles = build_crossing_series()
  107. for leverage in (0, 4):
  108. try:
  109. run_backtest(candles=candles, leverage=leverage)
  110. except ValueError as exc:
  111. assert str(exc) == "leverage is invalid"
  112. else:
  113. raise AssertionError("expected ValueError")
  114. def test_backtest_runs_fixed_sma_crossover_series():
  115. candles = build_crossing_series()
  116. result = run_backtest(candles=candles, leverage=2)
  117. assert result.initial_equity == 10_000
  118. assert result.trade_count == 2
  119. assert result.trades[0].entry_price == candles[21].open
  120. assert result.trades[0].exit_price == candles[30].open
  121. assert result.trades[0].margin_used == 10_000
  122. assert result.trades[1].margin_used == result.trades[0].ending_equity
  123. assert result.ending_equity == 4_888.888888888889
  124. assert result.total_return == -0.5111111111111112
  125. assert "total_return" in result.to_dict()
  126. assert "max_drawdown" in result.to_dict()
  127. assert result.win_rate == 0.5
  128. def test_backtest_does_not_force_close_open_position_at_series_end():
  129. candles = build_open_position_series()
  130. result = run_backtest(candles=candles, leverage=2)
  131. assert result.trade_count == 0
  132. assert result.trades == []
  133. assert result.ending_equity == 0.0
  134. assert result.total_return == -1.0
  135. def test_backtest_tracks_open_trade_drawdown_from_candle_close():
  136. candles = build_drawdown_series()
  137. result = run_backtest(candles=candles, leverage=2)
  138. assert result.trade_count == 1
  139. assert result.trades[0].entry_price == candles[21].open
  140. assert result.trades[0].exit_price == candles[36].open
  141. assert result.max_drawdown == 0.5
  142. def test_backtest_tracks_tail_drawdown_for_final_open_position():
  143. candles = build_tail_drawdown_series()
  144. result = run_backtest(candles=candles, leverage=2)
  145. assert result.trade_count == 0
  146. assert result.trades == []
  147. assert result.ending_equity == 0.0
  148. assert result.total_return == -1.0
  149. assert result.max_drawdown == 1.0