from html import escape from pathlib import Path import numpy as np import pandas as pd from backtesting import Strategy from backtesting.lib import FractionalBacktest, crossover from okx_codex_trader.models import Candle def render_report_html( *, symbol: str, bar: str, leverage: int, stats: dict[str, object], trades: list[dict[str, object]], plot_filename: str, ) -> str: stat_cards = "".join( f"""
{escape(str(label))}
{escape(str(value))}
""" for label, value in stats.items() ) trade_rows = "".join( f""" {escape(str(trade["side"]))} {escape(str(trade["entry_time"]))} {escape(str(trade["exit_time"]))} {escape(str(trade["entry_price"]))} {escape(str(trade["exit_price"]))} {escape(str(trade["pnl"]))} {escape(str(trade["return_pct"]))} """ for trade in trades ) return f""" {escape(symbol)} Backtest Report
Journal-first backtest report

{escape(symbol)}

Bar: {escape(bar)} ยท Leverage: {escape(str(leverage))}x
{stat_cards}

Trade Journal

{trade_rows}
Side Entry Time Exit Time Entry Exit PnL Return %

Chart

""" def generate_backtest_report( *, candles: list[Candle], leverage: int, output_file: Path, symbol: str, bar: str, ) -> dict[str, object]: output_file.parent.mkdir(parents=True, exist_ok=True) plot_file = output_file.with_name(output_file.stem + ".plot.html") frame = pd.DataFrame( { "Open": [candle.open for candle in candles], "High": [candle.high for candle in candles], "Low": [candle.low for candle in candles], "Close": [candle.close for candle in candles], "Volume": [candle.volume for candle in candles], }, index=pd.to_datetime([candle.ts for candle in candles], unit="ms", utc=True), ) class SmaCross(Strategy): n1 = 10 n2 = 20 def init(self): close = self.data.Close.s self.sma1 = self.I( lambda values: np.array(pd.Series(values).rolling(self.n1).mean().to_list(), dtype=float), close, ) self.sma2 = self.I( lambda values: np.array(pd.Series(values).rolling(self.n2).mean().to_list(), dtype=float), close, ) def next(self): if crossover(self.sma1, self.sma2): self.position.close() self.buy() elif crossover(self.sma2, self.sma1): self.position.close() self.sell() backtest = FractionalBacktest( frame, SmaCross, cash=10_000, commission=0.0, margin=1 / leverage, trade_on_close=False, exclusive_orders=True, hedging=False, finalize_trades=False, ) stats = backtest.run() backtest.plot(filename=str(plot_file), open_browser=False) summary = { "Return [%]": round(float(stats["Return [%]"]), 2), "Win Rate [%]": round(float(stats["Win Rate [%]"]), 2), "Max. Drawdown [%]": round(float(stats["Max. Drawdown [%]"]), 2), "# Trades": int(stats["# Trades"]), } trades_frame = stats["_trades"] trades = [ { "side": "Long" if float(row["Size"]) > 0 else "Short", "entry_time": row["EntryTime"].strftime("%Y-%m-%d %H:%M"), "exit_time": row["ExitTime"].strftime("%Y-%m-%d %H:%M"), "entry_price": round(float(row["EntryPrice"]), 4), "exit_price": round(float(row["ExitPrice"]), 4), "pnl": round(float(row["PnL"]), 4), "return_pct": round(float(row["ReturnPct"]) * 100, 2), } for _, row in trades_frame.iterrows() ] output_file.write_text( render_report_html( symbol=symbol, bar=bar, leverage=leverage, stats=summary, trades=trades, plot_filename=plot_file.name, ) ) return { "report_file": str(output_file), "plot_file": str(plot_file), "trade_count": int(stats["# Trades"]), "total_return": round(float(stats["Return [%]"]) / 100, 6), }