Files
profitable-expert-advisor/polymarket/backtesting/engine.py
T
zhutoutoutousan 98a87a69ca Update
2026-02-13 08:03:25 +01:00

500 lines
19 KiB
Python

"""
Backtesting Engine for Polymarket
Simulates trading on historical market data.
"""
from typing import Dict, List, Optional, Any
from datetime import datetime, timedelta
import pandas as pd
import numpy as np
# Configure numpy to handle division by zero gracefully
np.seterr(divide='ignore', invalid='ignore')
from ..strategies.base_strategy import BaseStrategy, MarketSignal, Position
from ..api.gamma_client import GammaClient
from ..api.clob_client import ClobClient
import time
class BacktestEngine:
"""
Main backtesting engine for Polymarket strategies.
Simulates trading on historical data with realistic execution.
"""
def __init__(self,
strategy: BaseStrategy,
start_date: datetime,
end_date: datetime,
initial_balance: float = 1000.0):
"""
Initialize backtesting engine.
Args:
strategy: Strategy instance to backtest
start_date: Start date for backtesting
end_date: End date for backtesting
initial_balance: Starting USDC balance
"""
self.strategy = strategy
self.start_date = start_date
self.end_date = end_date
self.initial_balance = initial_balance
# Initialize API clients (for data fetching)
self.gamma_client = GammaClient()
self.clob_client = ClobClient()
# Backtest state
self.current_date = start_date
self.market_snapshots: List[Dict] = []
self.trades: List[Dict] = []
# Performance tracking
self.equity_curve: List[Dict] = []
self.daily_returns: List[float] = []
def fetch_historical_markets(self, tag_id: Optional[int] = None) -> List[Dict]:
"""
Fetch markets that were active during backtest period.
Note: Polymarket API may not provide full historical data.
This is a simplified implementation.
Args:
tag_id: Optional tag ID to filter markets
Returns:
List of market dictionaries
"""
# Get current active markets (as proxy for historical)
# In production, you'd need to store historical snapshots
events = self.gamma_client.get_events(
active=True,
closed=False,
limit=100,
tag_id=tag_id
)
markets = []
for event in events:
for market in event.get('markets', []):
markets.append({
'event': event,
'market': market,
'timestamp': datetime.now() # Would be historical in real implementation
})
return markets
def simulate_price_evolution(self,
initial_price: float,
days: int,
volatility: float = 0.05) -> List[float]:
"""
Simulate price evolution for backtesting.
In production, use actual historical price data.
Args:
initial_price: Starting price
days: Number of days to simulate
volatility: Daily volatility
Returns:
List of prices over time
"""
prices = [initial_price]
for _ in range(days):
# Random walk with mean reversion
change = np.random.normal(0, volatility)
new_price = prices[-1] + change
new_price = max(0.01, min(0.99, new_price)) # Bound between 0 and 1
prices.append(new_price)
return prices
def execute_signal(self,
signal: MarketSignal,
market_data: Dict,
timestamp: datetime) -> Optional[Dict]:
"""
Execute a trading signal.
Args:
signal: Trading signal from strategy
market_data: Current market data
timestamp: Current timestamp
Returns:
Trade dictionary or None if execution failed
"""
# Get market from market_data first (needed for prices)
market = market_data.get('market', {})
# Use token_id from signal if available, otherwise get from market
if signal.token_id:
token_id = signal.token_id
else:
token_ids = market.get('clobTokenIds', [])
if not token_ids:
return None
token_id = token_ids[0]
outcome = 'Yes' # Default outcome
# Get current price from market data
import json
prices = json.loads(market.get('outcomePrices', '[0.5, 0.5]'))
if signal.action == 'BUY':
current_price = float(prices[0]) # Yes price
else:
current_price = float(prices[0]) # Use Yes price for exit too
# Validate price
if current_price <= 0 or current_price >= 1:
return None # Invalid price
# Calculate position_size based on action
if signal.action == 'SELL':
if token_id not in self.strategy.positions:
return None # No position to close
# For SELL, position_size represents the value we'll get back
pos = self.strategy.positions[token_id]
# Validate position data
if not (pos.size > 0 and np.isfinite(pos.size) and
current_price > 0 and current_price < 1 and
np.isfinite(current_price)):
return None # Invalid position or price data
position_size = pos.size * current_price * signal.size # signal.size = 1.0 for full close
if not np.isfinite(position_size) or position_size <= 0:
return None
else:
# For BUY, calculate position size and check limits
# Validate balance
if not (np.isfinite(self.strategy.current_balance) and self.strategy.current_balance > 0):
return None
position_size = min(
signal.size * self.strategy.current_balance,
self.strategy.current_balance * self.strategy.max_position_size
)
# Validate position_size
if not (np.isfinite(position_size) and position_size > 0):
return None
# Check if can open position
if not self.strategy.can_open_position(position_size, token_id):
return None
# Ensure we have enough balance
if position_size > self.strategy.current_balance:
return None
# Execute trade
if signal.action == 'BUY':
# Buy tokens - safe division
if current_price > 0 and current_price < 1 and np.isfinite(current_price):
tokens_bought = position_size / current_price
# Validate tokens_bought
if not (np.isfinite(tokens_bought) and tokens_bought > 0):
return None
else:
return None # Invalid price, skip trade
# Validate balance before subtraction
if not (np.isfinite(self.strategy.current_balance) and
self.strategy.current_balance >= position_size):
return None
self.strategy.current_balance -= position_size
# Ensure balance is still finite
if not np.isfinite(self.strategy.current_balance):
self.strategy.current_balance = 0.0
return None
# Create position
position = Position(
token_id=token_id,
outcome=outcome,
size=tokens_bought,
entry_price=current_price,
entry_time=timestamp,
current_price=current_price,
unrealized_pnl=0.0
)
self.strategy.positions[token_id] = position
elif signal.action == 'SELL':
# Close existing position
if token_id in self.strategy.positions:
pos = self.strategy.positions[token_id]
# Validate position data
if not (np.isfinite(pos.size) and pos.size > 0 and
np.isfinite(pos.entry_price) and pos.entry_price > 0):
return None
# Close fraction of position (signal.size = 1.0 means close all)
close_size = pos.size * signal.size
if not (np.isfinite(close_size) and close_size > 0):
return None
exit_value = close_size * current_price
entry_cost = close_size * pos.entry_price
# Validate calculations
if not (np.isfinite(exit_value) and np.isfinite(entry_cost)):
return None
pnl = exit_value - entry_cost
if not np.isfinite(pnl):
pnl = 0.0
# Validate balance before addition
if not np.isfinite(self.strategy.current_balance):
self.strategy.current_balance = 0.0
self.strategy.current_balance += exit_value
# Ensure balance is still finite
if not np.isfinite(self.strategy.current_balance):
self.strategy.current_balance = 0.0
return None
self.strategy.total_trades += 1
if pnl > 0:
self.strategy.winning_trades += 1
self.strategy.total_profit += pnl if np.isfinite(pnl) else 0.0
else:
self.strategy.losing_trades += 1
self.strategy.total_loss += abs(pnl) if np.isfinite(pnl) else 0.0
# Update or remove position
if signal.size >= 1.0:
# Close entire position
pos.realized_pnl = pnl if np.isfinite(pnl) else 0.0
self.strategy.closed_positions.append(pos)
del self.strategy.positions[token_id]
else:
# Partial close
pos.size -= close_size
if not (np.isfinite(pos.size) and pos.size >= 0):
pos.size = 0.0
pos.realized_pnl += pnl if np.isfinite(pnl) else 0.0
if not np.isfinite(pos.realized_pnl):
pos.realized_pnl = 0.0
trade = {
'timestamp': timestamp,
'action': signal.action,
'token_id': token_id,
'outcome': outcome,
'price': current_price,
'size': position_size,
'reason': signal.reason,
'confidence': signal.confidence
}
self.trades.append(trade)
return trade
def run(self, markets: Optional[List[Dict]] = None) -> Dict[str, Any]:
"""
Run the backtest.
Args:
markets: Optional list of markets to backtest. If None, fetches markets.
Returns:
Dictionary with backtest results
"""
print(f"Starting backtest from {self.start_date} to {self.end_date}")
# Fetch markets if not provided
if markets is None:
markets = self.fetch_historical_markets()
if not markets:
raise ValueError("No markets found for backtesting")
print(f"Found {len(markets)} markets to backtest")
# Simulate time progression
current_date = self.start_date
day_count = 0
while current_date <= self.end_date:
# Update positions with current prices
for token_id, position in self.strategy.positions.items():
# Simulate price movement
# In production, use actual historical prices
price_change = np.random.normal(0, 0.02)
new_price = max(0.01, min(0.99, position.current_price + price_change))
self.strategy.update_position(token_id, new_price)
# Process each market
for market_snapshot in markets:
market_data = {
'event': market_snapshot['event'],
'market': market_snapshot['market'],
'timestamp': current_date
}
# Get current prices
market = market_snapshot['market']
import json
outcomes = json.loads(market.get('outcomes', '["Yes", "No"]'))
prices = json.loads(market.get('outcomePrices', '[0.5, 0.5]'))
market_data['prices'] = {
outcome: float(price)
for outcome, price in zip(outcomes, prices)
}
# Get strategy signal
signal = self.strategy.analyze_market(market_data)
if signal and signal.confidence >= self.strategy.min_confidence:
self.execute_signal(signal, market_data, current_date)
# Update equity curve
self.strategy.update_drawdown()
equity = self.strategy.calculate_equity()
self.equity_curve.append({
'date': current_date,
'equity': equity,
'balance': self.strategy.current_balance,
'unrealized_pnl': sum(pos.unrealized_pnl for pos in self.strategy.positions.values())
})
# Calculate daily return
if len(self.equity_curve) > 1:
prev_equity = self.equity_curve[-2]['equity']
daily_return = (equity - prev_equity) / prev_equity if prev_equity > 0 else 0.0
self.daily_returns.append(daily_return)
# Advance to next day
current_date += timedelta(days=1)
day_count += 1
if day_count % 10 == 0:
print(f"Progress: {day_count} days, Equity: ${equity:.2f}")
# Close all open positions at end
final_equity = self.strategy.calculate_equity()
for token_id, position in list(self.strategy.positions.items()):
# Assume final price is entry price (or use last known price)
exit_value = position.size * position.current_price
pnl = exit_value - (position.size * position.entry_price)
self.strategy.current_balance += exit_value
self.strategy.total_trades += 1
if pnl > 0:
self.strategy.winning_trades += 1
self.strategy.total_profit += pnl
else:
self.strategy.losing_trades += 1
self.strategy.total_loss += abs(pnl)
del self.strategy.positions[token_id]
# Calculate final metrics with safe division
if self.initial_balance > 0:
total_return = (final_equity - self.initial_balance) / self.initial_balance * 100
else:
total_return = 0.0
sharpe_ratio = self._calculate_sharpe_ratio()
# Safe win rate calculation
if self.strategy.total_trades > 0:
win_rate = (self.strategy.winning_trades / self.strategy.total_trades * 100)
else:
win_rate = 0.0
# Safe profit factor calculation
if abs(self.strategy.total_loss) > 1e-10:
profit_factor = abs(self.strategy.total_profit / self.strategy.total_loss)
else:
profit_factor = 0.0 if abs(self.strategy.total_profit) < 1e-10 else float('inf')
# Ensure all values are finite
total_return = total_return if np.isfinite(total_return) else 0.0
win_rate = win_rate if np.isfinite(win_rate) else 0.0
profit_factor = profit_factor if (np.isfinite(profit_factor) and profit_factor != float('inf')) else 0.0
sharpe_ratio = sharpe_ratio if np.isfinite(sharpe_ratio) else 0.0
max_dd = self.strategy.max_drawdown * 100 if np.isfinite(self.strategy.max_drawdown) else 0.0
results = {
'strategy': self.strategy.name,
'start_date': self.start_date,
'end_date': self.end_date,
'initial_balance': self.initial_balance,
'final_balance': self.strategy.current_balance,
'final_equity': final_equity if np.isfinite(final_equity) else self.initial_balance,
'total_return': total_return,
'total_trades': self.strategy.total_trades,
'winning_trades': self.strategy.winning_trades,
'losing_trades': self.strategy.losing_trades,
'win_rate': win_rate,
'total_profit': self.strategy.total_profit if np.isfinite(self.strategy.total_profit) else 0.0,
'total_loss': self.strategy.total_loss if np.isfinite(self.strategy.total_loss) else 0.0,
'net_profit': (self.strategy.total_profit + self.strategy.total_loss) if np.isfinite(self.strategy.total_profit + self.strategy.total_loss) else 0.0,
'profit_factor': profit_factor,
'max_drawdown': max_dd,
'sharpe_ratio': sharpe_ratio,
'trades': self.trades,
'equity_curve': self.equity_curve
}
return results
def _calculate_sharpe_ratio(self, risk_free_rate: float = 0.0) -> float:
"""Calculate Sharpe ratio from daily returns"""
if not self.daily_returns:
return 0.0
returns = np.array(self.daily_returns)
if len(returns) == 0:
return 0.0
excess_returns = returns - (risk_free_rate / 365) # Daily risk-free rate
std_dev = returns.std()
if std_dev == 0 or np.isnan(std_dev) or not np.isfinite(std_dev):
return 0.0
mean_return = excess_returns.mean()
if not np.isfinite(mean_return):
return 0.0
sharpe = np.sqrt(365) * mean_return / std_dev
return sharpe if np.isfinite(sharpe) else 0.0
def generate_report(self, output_file: Optional[str] = None) -> None:
"""Generate backtest report"""
results = {
'strategy': self.strategy.name,
'performance': self.strategy.get_performance_metrics()
}
print("\n" + "="*60)
print("BACKTEST RESULTS")
print("="*60)
print(f"Strategy: {results['strategy']}")
print(f"Period: {self.start_date.date()} to {self.end_date.date()}")
print(f"Initial Balance: ${self.initial_balance:.2f}")
print(f"Final Equity: ${self.strategy.equity:.2f}")
print(f"Total Return: {((self.strategy.equity - self.initial_balance) / self.initial_balance * 100):.2f}%")
print(f"Total Trades: {self.strategy.total_trades}")
print(f"Win Rate: {(self.strategy.winning_trades / self.strategy.total_trades * 100) if self.strategy.total_trades > 0 else 0:.2f}%")
print(f"Max Drawdown: {self.strategy.max_drawdown * 100:.2f}%")
print("="*60)