Spaces:
Sleeping
Sleeping
| import pandas as pd | |
| import numpy as np | |
| import json | |
| from datetime import datetime, timedelta | |
| class Backtester: | |
| def __init__(self): | |
| pass | |
| def generate_mock_data(self, days=365, initial_price=100, volatility=0.02, drift=0.0005): | |
| """ | |
| Generate synthetic OHLCV data using Geometric Brownian Motion. | |
| """ | |
| np.random.seed(42) # For reproducibility in demo | |
| dates = pd.date_range(end=datetime.now(), periods=days).tolist() | |
| # Geometric Brownian Motion for Close prices | |
| returns = np.random.normal(loc=drift, scale=volatility, size=days) | |
| price_paths = initial_price * np.exp(np.cumsum(returns)) | |
| data = [] | |
| for i, date in enumerate(dates): | |
| close_price = price_paths[i] | |
| # Simulate High, Low, Open based on Close | |
| daily_vol = close_price * volatility | |
| open_price = close_price + np.random.uniform(-daily_vol/2, daily_vol/2) | |
| high_price = max(open_price, close_price) + np.random.uniform(0, daily_vol) | |
| low_price = min(open_price, close_price) - np.random.uniform(0, daily_vol) | |
| volume = int(np.random.uniform(1000, 50000)) | |
| data.append({ | |
| 'date': date.strftime('%Y-%m-%d'), | |
| 'open': round(open_price, 2), | |
| 'high': round(high_price, 2), | |
| 'low': round(low_price, 2), | |
| 'close': round(close_price, 2), | |
| 'volume': volume | |
| }) | |
| return pd.DataFrame(data) | |
| def run_strategy(self, short_window=20, long_window=50, initial_capital=10000, commission=0.001, custom_data=None): | |
| """ | |
| Run a simple Dual Moving Average Crossover strategy. | |
| """ | |
| if custom_data is not None: | |
| df = custom_data | |
| # Ensure required columns exist | |
| required_columns = ['close'] | |
| if not all(col in df.columns for col in required_columns): | |
| # Try to normalize columns if they are upper case | |
| df.columns = [c.lower() for c in df.columns] | |
| if not all(col in df.columns for col in required_columns): | |
| raise ValueError(f"Data must contain columns: {required_columns}") | |
| # Ensure we have a date column or index | |
| if 'date' in df.columns: | |
| df['date'] = pd.to_datetime(df['date']) | |
| df['date'] = df['date'].dt.strftime('%Y-%m-%d') | |
| else: | |
| # If no date, generate dummy dates | |
| dates = pd.date_range(end=datetime.now(), periods=len(df)).tolist() | |
| df['date'] = [d.strftime('%Y-%m-%d') for d in dates] | |
| else: | |
| df = self.generate_mock_data() | |
| # Calculate Indicators | |
| df['short_mavg'] = df['close'].rolling(window=short_window, min_periods=1).mean() | |
| df['long_mavg'] = df['close'].rolling(window=long_window, min_periods=1).mean() | |
| # Generate Signals | |
| df['signal'] = 0.0 | |
| # Calculate signal using numpy to avoid SettingWithCopyWarning | |
| signals = np.zeros(len(df)) | |
| if len(df) > short_window: | |
| signals[short_window:] = np.where( | |
| df['short_mavg'].iloc[short_window:] > df['long_mavg'].iloc[short_window:], | |
| 1.0, | |
| 0.0 | |
| ) | |
| df['signal'] = signals | |
| df['positions'] = df['signal'].diff() | |
| # Backtest Logic | |
| portfolio = pd.DataFrame(index=df.index) | |
| portfolio['holdings'] = df['signal'] * (initial_capital / df['close']) # Simplified: invest all capital | |
| # Adjust for fractional shares for simplicity in simulation or assume full investment | |
| # Better approach for simpler calculation: | |
| # Strategy: When signal is 1, hold stock. When 0, hold cash. | |
| df['pct_change'] = df['close'].pct_change() | |
| df['strategy_returns'] = df['signal'].shift(1) * df['pct_change'] | |
| # Commission cost on trades | |
| trades = df['positions'].abs() | |
| # Approximate commission impact (simplified) | |
| # If position changed, we paid commission | |
| # We need a more robust equity curve calculation | |
| equity = [initial_capital] | |
| position = 0 # 0: Cash, 1: Invested | |
| cash = initial_capital | |
| shares = 0 | |
| equity_curve = [] | |
| signals_log = [] | |
| for i, row in df.iterrows(): | |
| price = row['close'] | |
| signal = row['signal'] | |
| # Simple execution logic | |
| if signal == 1 and position == 0: | |
| # Buy | |
| shares = (cash * (1 - commission)) / price | |
| cash = 0 | |
| position = 1 | |
| signals_log.append({'date': row['date'], 'type': 'buy', 'price': price}) | |
| elif signal == 0 and position == 1: | |
| # Sell | |
| cash = shares * price * (1 - commission) | |
| shares = 0 | |
| position = 0 | |
| signals_log.append({'date': row['date'], 'type': 'sell', 'price': price}) | |
| # Update Daily Equity | |
| current_equity = cash + (shares * price) | |
| equity_curve.append(current_equity) | |
| df['equity'] = equity_curve | |
| # Metrics | |
| total_return = (df['equity'].iloc[-1] - initial_capital) / initial_capital | |
| df['daily_ret'] = df['equity'].pct_change() | |
| volatility = df['daily_ret'].std() * np.sqrt(252) | |
| sharpe_ratio = (df['daily_ret'].mean() / df['daily_ret'].std()) * np.sqrt(252) if df['daily_ret'].std() != 0 else 0 | |
| max_drawdown = (df['equity'] / df['equity'].cummax() - 1).min() | |
| win_rate = len(df[df['strategy_returns'] > 0]) / len(df[df['strategy_returns'] != 0]) if len(df[df['strategy_returns'] != 0]) > 0 else 0 | |
| # Prepare Data for Frontend | |
| # Ensure OHLC columns exist for charting | |
| for col in ['open', 'high', 'low']: | |
| if col not in df.columns: | |
| df[col] = df['close'] | |
| result_data = { | |
| 'dates': df['date'].tolist(), | |
| 'ohlc': df[['open', 'close', 'low', 'high']].values.tolist(), # ECharts candle format: [open, close, low, high] | |
| 'equity_curve': df['equity'].round(2).tolist(), | |
| 'short_mavg': df['short_mavg'].fillna(0).tolist(), | |
| 'long_mavg': df['long_mavg'].fillna(0).tolist(), | |
| 'signals': signals_log, | |
| 'metrics': { | |
| 'total_return': f"{total_return*100:.2f}%", | |
| 'final_equity': f"${df['equity'].iloc[-1]:.2f}", | |
| 'sharpe_ratio': f"{sharpe_ratio:.2f}", | |
| 'max_drawdown': f"{max_drawdown*100:.2f}%", | |
| 'volatility': f"{volatility*100:.2f}%" | |
| } | |
| } | |
| return result_data | |