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=180)).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 # ENHANCED POSITION MANAGEMENT - Better risk control for longer periods 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: # MORE STRICT ENTRY - Only in very strong trends if (bars['close'].iloc[i] > bars['ema_trend'].iloc[i] * 1.02 and # 2% above major trend bars['ema_fast'].iloc[i] > bars['ema_slow'].iloc[i] * 1.01): # Fast EMA clearly above slow 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 with better risk management 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 elif profit_pct > 0.10: # 10%+ profit: 15% trailing stop trailing_stop_pct = 0.85 else: # < 10% profit: 12% trailing stop (tighter for early losses) trailing_stop_pct = 0.88 trailing_stop_price = highest_price_since_entry * trailing_stop_pct # IMPROVED EXIT CONDITIONS - Better loss control exit_conditions = ( bars['sell_signal'].iloc[i] or current_price <= trailing_stop_price or # Tighter time-based stops to prevent long drawdowns (bars_since_entry >= 168 and profit_pct < -0.10) or # 7 days & -10% (not 28 days & -30%) (bars_since_entry >= 336 and profit_pct < -0.05) or # 14 days & -5% (bars['close'].iloc[i] < bars['ema_trend'].iloc[i] * 0.85) # 15% below major trend (not 25%) ) 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}%")