252 lines
9.7 KiB
Python
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}%") |