arbintel / src /backtest /engine.py
AJAY KASU
Add root app.py for Streamlit GUI and dependencies
77fd2f6
import logging
import pandas as pd
import numpy as np
import plotly.graph_objects as go
from typing import Dict, Any
logger = logging.getLogger(__name__)
class BacktestEngine:
def __init__(self, initial_capital: float = 10000.0):
self.initial_capital = initial_capital
self.capital = initial_capital
self.positions = 0.0 # simplified sizing (e.g. holding 100 shares of YES)
self.trades = []
self.equity_curve = []
def load_data(self, data: pd.DataFrame):
"""
Expects a DataFrame with ['timestamp', 'bid', 'ask', 'mid', 'volume']
and external features like ['sentiment'] if testing the momentum strategy.
"""
self.df = data.sort_values("timestamp").reset_index(drop=True)
# Assuming slippage model: market orders execute at worst of bid/ask + slippage
self.slippage_bps = 5
def run_macrostem_simulation(self, strategy_func):
"""
Iterate through tick/candle data, evaluating the strategy function.
strategy_func takes a dict row and returns a trade action dict or None.
"""
logger.info(f"Starting backtest with {len(self.df)} ticks. Capital: ${self.capital}")
for idx, row in self.df.iterrows():
# 1. Update Equity based on current holdings marked to market (mid price)
mtm_value = self.capital + (self.positions * row['mid'])
self.equity_curve.append({'timestamp': row['timestamp'], 'equity': mtm_value})
# 2. Evaluate Strategy Signal
signal = strategy_func(row)
if not signal:
continue
action = signal.get("action")
# Execute Trade Simulation
if action == "BUY_YES":
# Buy 100 shares at ask price + slippage
execute_price = row['ask'] + (row['ask'] * self.slippage_bps / 10000)
cost = execute_price * 100
if self.capital >= cost:
self.capital -= cost
self.positions += 100
self.trades.append({
"timestamp": row['timestamp'], "action": action,
"price": execute_price, "size": 100, "pnl": 0
})
elif action == "SELL_YES" and self.positions > 0:
# Close position at bid price - slippage
execute_price = row['bid'] - (row['bid'] * self.slippage_bps / 10000)
revenue = execute_price * self.positions
pnl = revenue - sum([t['price']*t['size'] for t in self.trades if t['action']=='BUY_YES']) # Naive PnL tracking for demo
self.capital += revenue
self.trades.append({
"timestamp": row['timestamp'], "action": action,
"price": execute_price, "size": self.positions, "pnl": pnl
})
self.positions = 0.0
# Final MTM
final_equity = self.capital + (self.positions * self.df.iloc[-1]['mid'])
logger.info(f"Backtest Complete. Final Equity: ${final_equity:.2f}")
def calculate_metrics(self) -> Dict[str, Any]:
"""Calculate Sharpe, Sortino, Max Drawdown, etc."""
if not self.equity_curve:
return {}
equity_df = pd.DataFrame(self.equity_curve)
equity_df.set_index('timestamp', inplace=True)
# Calculate returns
returns = equity_df['equity'].pct_change().dropna()
# Annualization factor (assuming hourly data for example, 252*24 approx 6048)
# Using a generic 252 * 252 for Crypto/Prediction markets pseudo-continuous
annualization = 365 * 24
mean_ret = returns.mean() * annualization
std_ret = returns.std() * np.sqrt(annualization)
sharpe = mean_ret / std_ret if std_ret != 0 else 0
# Max Drawdown
cum_ret = (1 + returns).cumprod()
rolling_max = cum_ret.cummax()
drawdowns = (cum_ret - rolling_max) / rolling_max
max_dd = drawdowns.min()
# Win Rate
winning_trades = len([t for t in self.trades if t.get('pnl', 0) > 0])
total_closed = len([t for t in self.trades if t['action'] == 'SELL_YES'])
win_rate = winning_trades / total_closed if total_closed > 0 else 0
return {
"Total Return (%)": ((equity_df['equity'].iloc[-1] / self.initial_capital) - 1) * 100,
"Sharpe Ratio": sharpe,
"Max Drawdown (%)": max_dd * 100,
"Win Rate (%)": win_rate * 100,
"Total Trades": len(self.trades)
}
def generate_report(self):
"""Optional: Output Plotly charts or console summary."""
metrics = self.calculate_metrics()
logger.info("=== Backtest Performance Report ===")
for k, v in metrics.items():
logger.info(f"{k}: {v:.2f}")