trading-algo/backtesting_short.py
Gal Podlipnik 69462cf3e0 first
2025-07-17 02:30:21 +02:00

252 lines
9.7 KiB
Python

import alpaca_trade_api as tradeapi
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
# Use past dates for backtesting
end_date = datetime.now().strftime('%Y-%m-%d')
start_date = (datetime.now() - timedelta(days=90)).strftime('%Y-%m-%d')
api = tradeapi.REST('PKXR08ET6CSGV5QFS89E', '5ILlNGVM7WfPwJk8kPRlHhQwDO022H19RWBOkwgd', base_url='https://data.alpaca.markets')
# Get historical data
bars = api.get_crypto_bars('ETH/USD', tradeapi.TimeFrame.Hour, start_date, end_date).df
bars.index = pd.to_datetime(bars.index)
# SIMPLIFIED TREND-FOLLOWING INDICATORS - Focus on what works
bars['ema_fast'] = bars['close'].ewm(span=12).mean() # 12 hours - responsive but not noisy
bars['ema_slow'] = bars['close'].ewm(span=30).mean() # 30 hours - trend filter
bars['ema_trend'] = bars['close'].ewm(span=80).mean() # 80 hours - major trend
# RSI for momentum filtering
def calculate_rsi(prices, window=14):
delta = prices.diff()
gain = (delta.where(delta > 0, 0)).rolling(window=window).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window=window).mean()
rs = gain / loss
rsi = 100 - (100 / (1 + rs))
return rsi
bars['rsi'] = calculate_rsi(bars['close'], 14)
# MACD for momentum confirmation
bars['ema_12'] = bars['close'].ewm(span=12).mean()
bars['ema_26'] = bars['close'].ewm(span=26).mean()
bars['macd'] = bars['ema_12'] - bars['ema_26']
bars['macd_signal'] = bars['macd'].ewm(span=9).mean()
# Price momentum
bars['price_momentum_6h'] = bars['close'].pct_change(periods=6)
# Volume confirmation (relaxed)
bars['volume_ma'] = bars['volume'].rolling(window=20).mean()
bars['volume_ok'] = bars['volume'] > bars['volume_ma'] * 0.7 # Very relaxed
# CONSERVATIVE BUY CONDITIONS - Back to what worked!
buy_conditions = (
# BASIC TREND REQUIREMENT - More strict
(bars['close'] > bars['ema_trend']) & # Must be above major trend (no tolerance)
# ENTRY TRIGGERS - More selective
(
# EMA bullish alignment with momentum
((bars['ema_fast'] > bars['ema_slow']) &
(bars['close'] > bars['ema_fast']) &
(bars['rsi'] > 45) & (bars['rsi'] < 75)) | # Balanced RSI
# MACD bullish with strong momentum
((bars['macd'] > bars['macd_signal']) &
(bars['price_momentum_6h'] > 0.02) & # 2% momentum required
(bars['rsi'] > 40) & (bars['rsi'] < 70)) | # Not overbought
# Strong breakout with volume
((bars['close'] > bars['ema_fast']) &
(bars['close'] > bars['ema_slow']) &
(bars['close'] > bars['ema_trend']) &
(bars['volume_ok']) &
(bars['price_momentum_6h'] > 0.015)) # Strong momentum
)
)
# ULTRA-CONSERVATIVE SELL CONDITIONS - Let winners run much longer!
sell_conditions = (
# ONLY MAJOR TREND BREAKS - Much more restrictive
(
# Confirmed EMA death cross AND major breakdown
((bars['ema_fast'] < bars['ema_slow']) &
(bars['close'] < bars['ema_trend'] * 0.90) & # 10% below trend (not 8%)
(bars['rsi'] < 35)) | # Very weak momentum (not 40)
# Sustained breakdown below major trend - more severe
((bars['close'] < bars['ema_trend'] * 0.88) & # 12% below major trend (not 8%)
(bars['price_momentum_6h'] < -0.04)) | # Stronger decline (not -0.03)
# Extreme overbought with strong reversal signs
((bars['rsi'] > 85) & # Higher threshold (not 80)
(bars['rsi'].shift(1) > bars['rsi']) & # RSI declining
(bars['rsi'].shift(2) > bars['rsi'].shift(1)) & # For 2 periods
(bars['rsi'].shift(3) > bars['rsi'].shift(2)) & # For 3 periods
(bars['macd'] < bars['macd_signal']) & # MACD bearish
(bars['price_momentum_6h'] < -0.02)) # Plus price weakness
)
)
# Apply signals with LONGER cooldown - Back to original
bars['buy_signal_raw'] = buy_conditions
bars['sell_signal_raw'] = sell_conditions
bars['buy_signal'] = False
bars['sell_signal'] = False
last_signal_idx = -50
COOLDOWN_HOURS = 24 # Back to 24-hour cooldown for quality trades
for i in range(len(bars)):
if i - last_signal_idx >= COOLDOWN_HOURS:
if bars['buy_signal_raw'].iloc[i]:
bars.iloc[i, bars.columns.get_loc('buy_signal')] = True
last_signal_idx = i
elif bars['sell_signal_raw'].iloc[i]:
bars.iloc[i, bars.columns.get_loc('sell_signal')] = True
last_signal_idx = i
# POSITION MANAGEMENT - Conservative entry + WIDER trailing stops
bars['position'] = 0
position = 0
entry_price = None
highest_price_since_entry = None
bars_since_entry = 0
for i in range(1, len(bars)):
current_price = bars['close'].iloc[i]
if bars['buy_signal'].iloc[i] and position == 0:
# CONSERVATIVE ENTRY - Back to original
if bars['close'].iloc[i] > bars['ema_trend'].iloc[i]: # Must be above trend
position = 1
entry_price = current_price
highest_price_since_entry = current_price
bars_since_entry = 0
elif position == 1:
bars_since_entry += 1
# Update highest price
if current_price > highest_price_since_entry:
highest_price_since_entry = current_price
profit_pct = (current_price / entry_price - 1)
# CRYPTO-SCALE TRAILING STOPS - Let big winners truly run!
if profit_pct > 2.00: # 200%+ profit: 40% trailing stop
trailing_stop_pct = 0.60
elif profit_pct > 1.50: # 150%+ profit: 35% trailing stop
trailing_stop_pct = 0.65
elif profit_pct > 1.00: # 100%+ profit: 30% trailing stop
trailing_stop_pct = 0.70
elif profit_pct > 0.75: # 75%+ profit: 25% trailing stop
trailing_stop_pct = 0.75
elif profit_pct > 0.50: # 50%+ profit: 22% trailing stop
trailing_stop_pct = 0.78
elif profit_pct > 0.25: # 25%+ profit: 20% trailing stop
trailing_stop_pct = 0.80
else: # < 25% profit: 18% trailing stop
trailing_stop_pct = 0.82
trailing_stop_price = highest_price_since_entry * trailing_stop_pct
# MINIMAL EXIT CONDITIONS - Only major trend breaks
exit_conditions = (
bars['sell_signal'].iloc[i] or
current_price <= trailing_stop_price or
# Emergency stops - very relaxed
(bars_since_entry >= 672 and profit_pct < -0.30) or # 28 days & -30%
(bars['close'].iloc[i] < bars['ema_trend'].iloc[i] * 0.75) # 25% below major trend
)
if exit_conditions:
position = 0
entry_price = None
highest_price_since_entry = None
bars_since_entry = 0
bars.iloc[i, bars.columns.get_loc('position')] = position
# Calculate returns with lower costs (fewer trades)
bars['market_return'] = bars['close'].pct_change()
bars['strategy_return'] = bars['position'].shift(1) * bars['market_return']
trade_cost = 0.0005 # Slightly lower for fewer, larger trades
bars['position_change'] = bars['position'].diff().abs()
bars['costs'] = bars['position_change'] * trade_cost
bars['strategy_return'] = bars['strategy_return'] - bars['costs']
# Cumulative returns
bars['cum_market'] = (1 + bars['market_return']).cumprod() - 1
bars['cum_strategy'] = (1 + bars['strategy_return']).cumprod() - 1
# Performance metrics
total_return = bars['cum_strategy'].iloc[-1] * 100
market_return_pct = bars['cum_market'].iloc[-1] * 100
# Count trades
position_changes = bars['position'].diff()
buys = (position_changes == 1).sum()
sells = (position_changes == -1).sum()
# Calculate trade returns
trade_returns = []
entry_price = None
for i in range(len(bars)):
if position_changes.iloc[i] == 1:
entry_price = bars['close'].iloc[i]
elif position_changes.iloc[i] == -1 and entry_price is not None:
exit_price = bars['close'].iloc[i]
trade_return = (exit_price / entry_price - 1) - (2 * trade_cost)
trade_returns.append(trade_return)
entry_price = None
if len(trade_returns) > 0:
trade_returns = np.array(trade_returns)
win_rate = (trade_returns > 0).mean() * 100
avg_win = trade_returns[trade_returns > 0].mean() * 100 if (trade_returns > 0).any() else 0
avg_loss = trade_returns[trade_returns < 0].mean() * 100 if (trade_returns < 0).any() else 0
max_win = trade_returns.max() * 100
max_loss = trade_returns.min() * 100
total_wins = trade_returns[trade_returns > 0].sum()
total_losses = abs(trade_returns[trade_returns < 0].sum())
profit_factor = total_wins / total_losses if total_losses > 0 else float('inf')
else:
win_rate = avg_win = avg_loss = max_win = max_loss = profit_factor = 0
# Sharpe ratio
if bars['strategy_return'].std() > 0:
sharpe = bars['strategy_return'].mean() / bars['strategy_return'].std() * np.sqrt(365 * 24)
else:
sharpe = 0
# Max drawdown
running_max = bars['cum_strategy'].cummax()
drawdown = (bars['cum_strategy'] - running_max)
max_drawdown = drawdown.min() * 100
print("===== TREND-FOLLOWING STRATEGY =====")
print(f"Strategy Return: {total_return:.2f}%")
print(f"Market Return: {market_return_pct:.2f}%")
print(f"Outperformance: {total_return - market_return_pct:.2f}%")
print(f"Sharpe Ratio: {sharpe:.2f}")
print(f"Max Drawdown: {max_drawdown:.2f}%")
print(f"Total Trades: {len(trade_returns)}")
print(f"Buy Signals: {buys}")
print(f"Sell Signals: {sells}")
print(f"Win Rate: {win_rate:.2f}%")
print(f"Profit Factor: {profit_factor:.2f}")
print(f"Avg Win: {avg_win:.2f}%")
print(f"Avg Loss: {avg_loss:.2f}%")
print(f"Max Win: {max_win:.2f}%")
print(f"Max Loss: {max_loss:.2f}%")
# Calculate time in market
time_in_market = (bars['position'] > 0).mean() * 100
print(f"Time in Market: {time_in_market:.1f}%")