backtest.py 2.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. from okx_codex_trader.models import BacktestResult, BacktestTrade, Candle
  2. from okx_codex_trader.strategy import simple_moving_average
  3. def run_backtest(candles: list[Candle], leverage: int) -> BacktestResult:
  4. if leverage is True or leverage is False or not isinstance(leverage, int) or not 1 <= leverage <= 3:
  5. raise ValueError("leverage is invalid")
  6. fast = simple_moving_average(candles, 10)
  7. slow = simple_moving_average(candles, 20)
  8. initial_equity = 10_000.0
  9. equity = initial_equity
  10. trades: list[BacktestTrade] = []
  11. wins = 0
  12. peak_equity = initial_equity
  13. max_drawdown = 0.0
  14. position: dict[str, float | str] | None = None
  15. for index in range(len(candles) - 1):
  16. if fast[index] is None or slow[index] is None:
  17. continue
  18. signal: str | None = None
  19. if fast[index - 1] is not None and slow[index - 1] is not None:
  20. if fast[index - 1] <= slow[index - 1] and fast[index] > slow[index]:
  21. signal = "long"
  22. elif fast[index - 1] >= slow[index - 1] and fast[index] < slow[index]:
  23. signal = "short"
  24. if signal is None:
  25. continue
  26. execution_price = candles[index + 1].open
  27. if position is not None and position["direction"] != signal:
  28. entry_price = float(position["entry_price"])
  29. margin_used = float(position["margin_used"])
  30. if position["direction"] == "long":
  31. price_return = (execution_price - entry_price) / entry_price
  32. else:
  33. price_return = (entry_price - execution_price) / entry_price
  34. ending_equity = margin_used + (margin_used * leverage * price_return)
  35. trades.append(
  36. BacktestTrade(
  37. direction=str(position["direction"]),
  38. entry_price=entry_price,
  39. exit_price=execution_price,
  40. margin_used=margin_used,
  41. ending_equity=ending_equity,
  42. )
  43. )
  44. equity = ending_equity
  45. if ending_equity > float(position["margin_used"]):
  46. wins += 1
  47. if equity > peak_equity:
  48. peak_equity = equity
  49. drawdown = (peak_equity - equity) / peak_equity
  50. if drawdown > max_drawdown:
  51. max_drawdown = drawdown
  52. position = None
  53. if position is None:
  54. position = {
  55. "direction": signal,
  56. "entry_price": execution_price,
  57. "margin_used": equity,
  58. }
  59. trade_count = len(trades)
  60. win_rate = wins / trade_count if trade_count else 0.0
  61. return BacktestResult(
  62. initial_equity=initial_equity,
  63. ending_equity=equity,
  64. total_return=(equity - initial_equity) / initial_equity,
  65. max_drawdown=max_drawdown,
  66. win_rate=win_rate,
  67. trade_count=trade_count,
  68. trades=trades,
  69. )