quant-strategy-lab / backtester.py
Trae Assistant
Initial commit with enhanced features and localization
ef93c74
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