Spaces:
Running
Running
| from __future__ import annotations | |
| from typing import Any, Dict, Tuple, Type | |
| import backtrader as bt | |
| DEFAULT_CASH = 100000 | |
| DEFAULT_COMMISSION = 0 | |
| def create_cerebro(cash: float = DEFAULT_CASH, commission: float = DEFAULT_COMMISSION) -> bt.Cerebro: | |
| """Create and configure Backtrader Cerebro engine with analyzers.""" | |
| cerebro = bt.Cerebro() | |
| cerebro.broker.setcash(cash) | |
| cerebro.broker.setcommission(commission=commission) | |
| cerebro.addanalyzer(bt.analyzers.PyFolio, _name="pyfolio") | |
| cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name="trades") | |
| return cerebro | |
| def run_backtest( | |
| ohlc_data, | |
| strategy_class: Type[bt.Strategy], | |
| strategy_name: str, | |
| **strategy_params: Any, | |
| ) -> Tuple[Dict[str, Any], bt.Cerebro]: | |
| """Run a backtest with given strategy; return metrics and the Cerebro instance.""" | |
| cerebro = create_cerebro() | |
| cerebro.addstrategy(strategy_class, **strategy_params) | |
| data_params = { | |
| "dataname": ohlc_data.set_index("Date"), | |
| "datetime": None, | |
| "open": "Open", | |
| "high": "High", | |
| "low": "Low", | |
| "close": "Close", | |
| "volume": "Volume", | |
| "openinterest": None, | |
| } | |
| data = bt.feeds.PandasData(**data_params) | |
| cerebro.adddata(data) | |
| results = cerebro.run() | |
| final_value = cerebro.broker.getvalue() | |
| strat = results[0] | |
| pyfolio_analyzer = strat.analyzers.pyfolio | |
| pf_items = pyfolio_analyzer.get_pf_items() | |
| # Some versions may return 3 or 4 items; we only need returns | |
| returns_series = pf_items[0] | |
| cumulative_return = (returns_series + 1).prod() - 1 | |
| cumulative_return_pct = cumulative_return * 100 | |
| # Manual metrics | |
| annual_volatility = returns_series.std() * (252 ** 0.5) * 100 | |
| running_max = (1 + returns_series).cumprod().cummax() | |
| drawdown = (1 + returns_series).cumprod() / running_max - 1 | |
| max_drawdown = abs(drawdown.min()) * 100 | |
| excess_returns = returns_series - 0.02 / 252 # assume 2% risk-free | |
| sharpe_ratio = ( | |
| excess_returns.mean() / returns_series.std() * (252 ** 0.5) | |
| if returns_series.std() != 0 | |
| else 0 | |
| ) | |
| trades = strat.analyzers.trades.get_analysis() | |
| total_closed_trades = trades.get("total", {}).get("closed", 0) if isinstance(trades.get("total", {}), dict) else 0 | |
| total_trades = total_closed_trades if total_closed_trades > 0 else getattr(strat, "order_count", 0) | |
| results_dict: Dict[str, Any] = { | |
| "Final Value": final_value, | |
| "Cumulative Return [%]": cumulative_return_pct, | |
| "Annual Volatility [%]": annual_volatility, | |
| "Max Drawdown [%]": max_drawdown, | |
| "Sharpe Ratio": sharpe_ratio, | |
| "Total Trades": total_trades, | |
| "Strategy": strategy_name, | |
| } | |
| return results_dict, cerebro |