| Overall Statistics |
|
Total Orders 353 Average Win 4.16% Average Loss -2.29% Compounding Annual Return -0.535% Drawdown 30.200% Expectancy 0.008 Start Equity 100000 End Equity 97349.68 Net Profit -2.650% Sharpe Ratio -0.132 Sortino Ratio -0.162 Probabilistic Sharpe Ratio 0.528% Loss Rate 64% Win Rate 36% Profit-Loss Ratio 1.82 Alpha -0.017 Beta -0.032 Annual Standard Deviation 0.152 Annual Variance 0.023 Information Ratio -0.459 Tracking Error 0.236 Treynor Ratio 0.627 Total Fees $652.78 Estimated Strategy Capacity $49000000.00 Lowest Capacity Asset SPY R735QTJ8XC9X Portfolio Turnover 24.26% |
from AlgorithmImports import *
import random
from datetime import timedelta, datetime
class HookStrategyAlgorithm(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2020, 2, 21) # 4 years ago from current date
self.SetEndDate(2025, 2, 21) # Current date
self.SetCash(100000) # Starting capital
# Add your desired asset (example with SPY)
self.symbol = self.AddEquity("SPY", Resolution.Minute).Symbol
# Set trading parameters
self.lookback_period = 4 * 252 * 24 # 4 years of 15-minute bars (approximate)
self.risk_percentage = 0.02 # 2% risk per trade
self.reward_risk_ratio = 2 # 1:2 risk-reward ratio
# Initialize variables for position tracking
self.is_in_position = False
self.position_type = None
self.entry_price = 0
self.stop_loss = 0
self.target_price = 0
# Function parameters
self.lower_band = 0.85
self.upper_band = 0.98
self.pulse_length = 100
self.pulse_a = 0.0001
self.pulse_b = 0.01
self.pulse_c = 0.5
self.pulse_breakout_threshold = 2
# Consolidator for 15-minute bars
self.consolidator = self.consolidate(self.symbol, timedelta(minutes=15), self.OnDataConsolidated)
# Initialize dataframe for historical data
self.history_df = None
self.scheduled_history_pull = True
# Schedule the initial history pull
self.schedule.on(self.date_rules.every_day(self.symbol),
self.time_rules.after_market_open(self.symbol, 5),
self.PullHistoricalData)
def PullHistoricalData(self):
if self.scheduled_history_pull:
# Pull 4 years of 15-minute bar history
history = self.history(self.symbol, self.lookback_period, Resolution.Minute)
self.Debug(f"Fetched Historical data. Got {len(history)} bars.")
history = history.reset_index(level='symbol', drop=True)
history.index = pd.to_datetime(history.index)
if history.empty:
self.Debug("No historical data available yet. Will try again.")
return
# Convert to 15-minute bars
history_bars = history.resample('15T').agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'sum'
}).dropna()
# Create DataFrame with required format
df = history_bars[['open', 'high', 'low', 'close', 'volume']].copy()
df['time'] = history_bars.index
df['index'] = np.arange(len(df))
# Rename columns as specified
df.rename(columns={
'time': 'Time',
'open': 'Open',
'high': 'High',
'low': 'Low',
'close': 'Close',
'volume': 'Volume',
'index': 'index'
}, inplace=True)
self.history_df = df
self.scheduled_history_pull = False
self.Debug(f"Historical data processed. Got {len(self.history_df)} bars.")
def OnDataConsolidated(self, consolidated_bar: TradeBar) -> None:
"""Handler for consolidated 15-minute bars"""
if self.history_df is None:
return
# Append the new bar to our historical dataframe
new_bar = pd.DataFrame([{
'Time': consolidated_bar.Time,
'Open': consolidated_bar.Open,
'High': consolidated_bar.High,
'Low': consolidated_bar.Low,
'Close': consolidated_bar.Close,
'Volume': consolidated_bar.Volume
}])
self.history_df = pd.concat([self.history_df, new_bar], ignore_index=True)
#self.Debug(f"New bar added: {new_bar.iloc[0].to_dict()}")
# Process the strategy with the new bar
self.ProcessStrategy(consolidated_bar.Close, len(self.history_df) - 1)
# Check if we need to exit existing positions
self.CheckPositionExit(consolidated_bar.Close)
def ProcessStrategy(self, current_price, current_index):
"""Process strategy logic for each new 15-minute bar"""
if current_index < 5: # Need some bars to calculate indicators
return
# Check if we're already in a position
if self.is_in_position:
return
# Get pulse guardian signal
pulse_result = self.Pulse_Guardian_with_Quadratic_Volatility(
self.history_df,
length=self.pulse_length,
a=self.pulse_a,
b=self.pulse_b,
c=self.pulse_c,
breakout_threshold=self.pulse_breakout_threshold
)
#self.Debug(f"current breakout= {pulse_result}")
if not pulse_result:
return # No breakout signal
# Check for long signal
long_signal, long_stop_loss, *long_rest = self.Is_Pos_Hook2(
self.history_df,
current_price,
current_index,
self.lower_band,
self.upper_band
)
# Check for short signal
short_signal, short_stop_loss, *short_rest = self.Is_Neg_Hook2(
self.history_df,
current_price,
current_index,
self.lower_band,
self.upper_band
)
# Debug the results, printing long_rest and short_rest separately
self.Debug(f"Long signal result: {long_signal}, Stop Loss: {long_stop_loss}, Rest: {long_rest}")
self.Debug(f"Short signal result: {short_signal}, Stop Loss: {short_stop_loss}, Rest: {short_rest}")
# Process long signals
if long_signal in ['Type A hook. Buy!', 'Type B hook. Buy!']:
# Calculate position size based on risk
stop_distance = abs(current_price - long_stop_loss)
if stop_distance == 0:
self.Debug("Warning: Stop distance is zero, skipping trade")
return
risk_amount = self.portfolio.Cash * self.risk_percentage
shares_to_buy = int(risk_amount / stop_distance)
if shares_to_buy == 0:
self.Debug("Warning: Not enough capital to take position with current risk parameters")
return
# Calculate target based on risk:reward ratio
target_price = current_price + (stop_distance * self.reward_risk_ratio)
# Enter long position
self.market_order(self.symbol, shares_to_buy)
self.is_in_position = True
self.position_type = "LONG"
self.entry_price = current_price
self.stop_loss = long_stop_loss
self.target_price = target_price
# Log trade information
self.Debug(f"LONG ENTRY: Price=${current_price:.2f}, Stop=${long_stop_loss:.2f}, Target=${target_price:.2f}, Shares={shares_to_buy}")
# Process short signals
elif short_signal in ['Type A hook. Sell!', 'Type B hook. Sell!']:
# Calculate position size based on risk
stop_distance = abs(current_price - short_stop_loss)
if stop_distance == 0:
self.Debug("Warning: Stop distance is zero, skipping trade")
return
risk_amount = self.portfolio.Cash * self.risk_percentage
shares_to_short = int(risk_amount / stop_distance)
if shares_to_short == 0:
self.Debug("Warning: Not enough capital to take position with current risk parameters")
return
# Calculate target based on risk:reward ratio
target_price = current_price - (stop_distance * self.reward_risk_ratio)
# Enter short position
self.market_order(self.symbol, -shares_to_short)
self.is_in_position = True
self.position_type = "SHORT"
self.entry_price = current_price
self.stop_loss = short_stop_loss
self.target_price = target_price
# Log trade information
self.Debug(f"SHORT ENTRY: Price=${current_price:.2f}, Stop=${short_stop_loss:.2f}, Target=${target_price:.2f}, Shares={shares_to_short}")
def CheckPositionExit(self, current_price):
"""Check if we need to exit current position based on stop loss or target price"""
if not self.is_in_position:
return
if self.position_type == "LONG":
# Check if stop loss or target hit
if current_price <= self.stop_loss:
self.market_order(self.symbol, -self.portfolio[self.symbol].Quantity)
self.Debug(f"LONG EXIT - STOP LOSS: Entry=${self.entry_price:.2f}, Exit=${current_price:.2f}")
self.ResetPosition()
elif current_price >= self.target_price:
self.market_order(self.symbol, -self.portfolio[self.symbol].Quantity)
self.Debug(f"LONG EXIT - TARGET: Entry=${self.entry_price:.2f}, Exit=${current_price:.2f}")
self.ResetPosition()
elif self.position_type == "SHORT":
# Check if stop loss or target hit
if current_price >= self.stop_loss:
self.market_order(self.symbol, -self.portfolio[self.symbol].Quantity)
self.Debug(f"SHORT EXIT - STOP LOSS: Entry=${self.entry_price:.2f}, Exit=${current_price:.2f}")
self.ResetPosition()
elif current_price <= self.target_price:
self.market_order(self.symbol, -self.portfolio[self.symbol].Quantity)
self.Debug(f"SHORT EXIT - TARGET: Entry=${self.entry_price:.2f}, Exit=${current_price:.2f}")
self.ResetPosition()
def ResetPosition(self):
"""Reset position tracking variables"""
self.is_in_position = False
self.position_type = None
self.entry_price = 0
self.stop_loss = 0
self.target_price = 0
def Is_Pos_Hook2(self, df, Current_Closed_Price, Index_Current, Lower_band, Upper_band):
"""
Implementation of the Is_Pos_Hook2 function for long signals
Note: Placeholder implementation as the actual logic wasn't shared
"""
# Since the actual logic wasn't shared, we'll return placeholder values
# In your actual implementation, replace this with your proprietary logic
# Check the last few candles for a pattern (placeholder logic)
if Index_Current < 5:
return "No signal", Current_Closed_Price, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, Index_Current, Current_Closed_Price, Current_Closed_Price
# Placeholder for your proprietary logic
# Here we just use a simple example - replace with your actual logic
last_candles = df.iloc[Index_Current-5:Index_Current+1]
# Example placeholder logic (replace with your actual proprietary logic)
if (last_candles['Close'].iloc[-1] > last_candles['Close'].iloc[-2] and
last_candles['Close'].iloc[-2] > last_candles['Close'].iloc[-3] and
last_candles['Low'].iloc[-1] > last_candles['Low'].iloc[-3]):
signal = "Type A hook. Buy!"
stop_loss = last_candles['Low'].min() * 0.99 # Placeholder stop loss
elif (last_candles['Close'].iloc[-1] > last_candles['Open'].iloc[-1] and
last_candles['Close'].iloc[-2] < last_candles['Open'].iloc[-2]):
signal = "Type B hook. Buy!"
stop_loss = last_candles['Low'].min() * 0.99 # Placeholder stop loss
else:
signal = "No signal"
stop_loss = Current_Closed_Price * 0.95 # Placeholder
# Return placeholder values for all required outputs
return (signal, stop_loss, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
Index_Current, Current_Closed_Price, Current_Closed_Price)
def Is_Neg_Hook2(self, df, Current_Closed_Price, Index_Current, Lower_band, Upper_band):
"""
Implementation of the Is_Neg_Hook2 function for short signals
Note: Placeholder implementation as the actual logic wasn't shared
"""
# Since the actual logic wasn't shared, we'll return placeholder values
# In your actual implementation, replace this with your proprietary logic
# Check the last few candles for a pattern (placeholder logic)
if Index_Current < 5:
return "No signal", Current_Closed_Price, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, Index_Current, Current_Closed_Price, Current_Closed_Price
# Placeholder for your proprietary logic
# Here we just use a simple example - replace with your actual logic
last_candles = df.iloc[Index_Current-5:Index_Current+1]
# Example placeholder logic (replace with your actual proprietary logic)
if (last_candles['Close'].iloc[-1] < last_candles['Close'].iloc[-2] and
last_candles['Close'].iloc[-2] < last_candles['Close'].iloc[-3] and
last_candles['High'].iloc[-1] < last_candles['High'].iloc[-3]):
signal = "Type A hook. Sell!"
stop_loss = last_candles['High'].max() * 1.01 # Placeholder stop loss
elif (last_candles['Close'].iloc[-1] < last_candles['Open'].iloc[-1] and
last_candles['Close'].iloc[-2] > last_candles['Open'].iloc[-2]):
signal = "Type B hook. Sell!"
stop_loss = last_candles['High'].max() * 1.01 # Placeholder stop loss
else:
signal = "No signal"
stop_loss = Current_Closed_Price * 1.05 # Placeholder
# Return placeholder values for all required outputs
return (signal, stop_loss, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
Index_Current, Current_Closed_Price, Current_Closed_Price)
def Pulse_Guardian_with_Quadratic_Volatility(self, df, length=100, a=0.0001, b=0.01, c=0.5, breakout_threshold=2):
"""
Implementation of the Pulse_Guardian_with_Quadratic_Volatility function
Note: Placeholder implementation as the actual logic wasn't shared
"""
# Since the actual logic wasn't shared, we'll create a placeholder implementation
# In your actual implementation, replace this with your proprietary logic
return random.choice([True, False])