test_backtest.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  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_drawdown_series() -> list[Candle]:
  76. closes = [60.0] * 20 + [120.0, 75.0, 100.0] + [75.0] * 14
  77. opens = list(closes)
  78. opens[21] = 100.0
  79. opens[36] = 100.0
  80. candles = []
  81. for index, (open_price, close_price) in enumerate(zip(opens, closes)):
  82. high = max(open_price, close_price)
  83. low = min(open_price, close_price)
  84. candles.append(
  85. Candle(
  86. symbol="BTC-USDT-SWAP",
  87. ts=index,
  88. open=open_price,
  89. high=high,
  90. low=low,
  91. close=close_price,
  92. volume=1_000.0,
  93. )
  94. )
  95. return candles
  96. def test_simple_moving_average_requires_full_window():
  97. candles = [
  98. Candle(symbol="BTC-USDT-SWAP", ts=index, open=close, high=close, low=close, close=close, volume=1_000.0)
  99. for index, close in enumerate([10.0, 20.0, 30.0, 40.0])
  100. ]
  101. assert simple_moving_average(candles, 3) == [None, None, 20.0, 30.0]
  102. def test_backtest_rejects_invalid_leverage():
  103. candles = build_crossing_series()
  104. for leverage in (0, 4):
  105. try:
  106. run_backtest(candles=candles, leverage=leverage)
  107. except ValueError as exc:
  108. assert str(exc) == "leverage is invalid"
  109. else:
  110. raise AssertionError("expected ValueError")
  111. def test_backtest_runs_fixed_sma_crossover_series():
  112. candles = build_crossing_series()
  113. result = run_backtest(candles=candles, leverage=2)
  114. assert result.initial_equity == 10_000
  115. assert result.trade_count == 2
  116. assert result.trades[0].entry_price == candles[21].open
  117. assert result.trades[0].exit_price == candles[30].open
  118. assert result.trades[0].margin_used == 10_000
  119. assert result.trades[1].margin_used == result.trades[0].ending_equity
  120. assert result.ending_equity == 4_888.888888888889
  121. assert result.total_return == -0.5111111111111112
  122. assert "total_return" in result.to_dict()
  123. assert "max_drawdown" in result.to_dict()
  124. assert result.win_rate == 0.5
  125. def test_backtest_does_not_force_close_open_position_at_series_end():
  126. candles = build_open_position_series()
  127. result = run_backtest(candles=candles, leverage=2)
  128. assert result.trade_count == 0
  129. assert result.trades == []
  130. assert result.ending_equity == 0.0
  131. assert result.total_return == -1.0
  132. def test_backtest_tracks_open_trade_drawdown_from_candle_close():
  133. candles = build_drawdown_series()
  134. result = run_backtest(candles=candles, leverage=2)
  135. assert result.trade_count == 1
  136. assert result.trades[0].entry_price == candles[21].open
  137. assert result.trades[0].exit_price == candles[36].open
  138. assert result.max_drawdown == 0.5