| Overall Statistics |
|
Total Orders 124 Average Win 1.17% Average Loss -0.52% Compounding Annual Return 15.071% Drawdown 35.900% Expectancy 1.105 Start Equity 10000 End Equity 34316.11 Net Profit 243.161% Sharpe Ratio 0.514 Sortino Ratio 0.536 Probabilistic Sharpe Ratio 7.739% Loss Rate 35% Win Rate 65% Profit-Loss Ratio 2.25 Alpha 0.008 Beta 1.05 Annual Standard Deviation 0.195 Annual Variance 0.038 Information Ratio 0.11 Tracking Error 0.117 Treynor Ratio 0.096 Total Fees $113.00 Estimated Strategy Capacity $1600000.00 Lowest Capacity Asset CCNE RF2OA4CJ9N51 Portfolio Turnover 0.07% |
from AlgorithmImports import *
class FactorInvestingStrategy(QCAlgorithm):
def Initialize(self):
# Set start and end dates
self.SetStartDate(2016, 1, 1) # Adjusted start date to ensure data availability
self.SetEndDate(2024, 10, 10)
self.SetCash(10000) # Starting cash
# Set resolution and universe
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.CoarseSelectionFunction)
self.lastRebalanceTime = datetime.min
self.rebalanceInterval = timedelta(weeks=4) # Rebalance every 4 weeks
# Dictionaries to store data for trailing stops
self.highestPrices = {}
self.stopOrderTickets = {}
# List to store selected symbols
self.long_symbols = []
# Warm up the algorithm to gather historical data
self.SetWarmUp(timedelta(days=365)) # Warm up for 1 year
# Flag to indicate warm-up completion
self.warmupComplete = False
def CoarseSelectionFunction(self, coarse):
if (self.Time - self.lastRebalanceTime) < self.rebalanceInterval:
return Universe.Unchanged
# Filter for stocks with fundamental data, price > $5, and sufficient volume
filtered = [c.Symbol for c in coarse if c.HasFundamentalData and c.Price > 5 and c.DollarVolume > 1e6]
return filtered
def OnSecuritiesChanged(self, changes):
if self.IsWarmingUp or not self.warmupComplete:
return # Skip processing during warm-up or before initial rebalancing
self.lastRebalanceTime = self.Time
self.ScheduleRebalance()
def ScheduleRebalance(self):
self.SelectSymbols()
self.SetHoldingsForSymbols()
def SelectSymbols(self):
value_scores = {}
momentum_scores = {}
quality_scores = {}
for security in self.ActiveSecurities.Values:
# Ensure security has data before processing
if not security.HasData:
continue
# Calculate individual factor scores
value_score = self.CalculateValueScore(security)
momentum_score = self.CalculateMomentumScore(security)
quality_score = self.CalculateQualityScore(security)
symbol = security.Symbol
if value_score is not None:
value_scores[symbol] = value_score
if momentum_score is not None:
momentum_scores[symbol] = momentum_score
if quality_score is not None:
quality_scores[symbol] = quality_score
# Standardize scores
value_scores_std = self.StandardizeScores(value_scores)
momentum_scores_std = self.StandardizeScores(momentum_scores)
quality_scores_std = self.StandardizeScores(quality_scores)
# Combine standardized scores
combined_scores = {}
for symbol in value_scores_std.keys():
if symbol in momentum_scores_std and symbol in quality_scores_std:
combined_score = (
1/3 * value_scores_std[symbol] +
1/3 * momentum_scores_std[symbol] +
1/3 * quality_scores_std[symbol]
)
combined_scores[symbol] = combined_score
# Rank stocks based on combined score
ranked_symbols = [symbol for symbol, score in
sorted(combined_scores.items(), key=lambda x: x[1], reverse=True)]
# Select top N stocks
self.long_symbols = ranked_symbols[:50]
def CalculateValueScore(self, security):
pe_ratio = security.Fundamentals.ValuationRatios.PERatio
if pe_ratio and pe_ratio > 0:
return 1 / pe_ratio # Earnings yield
else:
return None
def CalculateMomentumScore(self, security):
symbol = security.Symbol
lookback_period = 252
history = self.History(symbol, lookback_period, Resolution.Daily)
min_history_length = 60 # Minimum required days
if history.empty or len(history['close']) < min_history_length:
return None
price_start = history['close'][0]
current_price = history['close'][-1]
return (current_price / price_start) - 1
def CalculateQualityScore(self, security):
fundamentals = security.Fundamentals
# Return on Equity (ROE)
roe = fundamentals.OperationRatios.ROE.Value
if roe is None:
return None
# Debt-to-Equity Ratio
debt_to_equity = fundamentals.OperationRatios.TotalDebtEquityRatio.Value
if debt_to_equity is None or debt_to_equity <= 0:
return None
# For Quality, higher ROE and lower Debt-to-Equity is better
# We'll use the ratio of ROE to Debt-to-Equity
return roe / debt_to_equity
def StandardizeScores(self, scores_dict):
# Remove None values
valid_scores = {k: v for k, v in scores_dict.items() if v is not None}
if not valid_scores:
return {}
mean = sum(valid_scores.values()) / len(valid_scores)
variance = sum((v - mean) ** 2 for v in valid_scores.values()) / len(valid_scores)
std_dev = variance ** 0.5 if variance > 0 else 1
standardized_scores = {k: (v - mean) / std_dev for k, v in valid_scores.items()}
return standardized_scores
def SetHoldingsForSymbols(self):
if self.IsWarmingUp:
return # Skip order operations during warm-up
if not self.long_symbols:
self.Debug("No symbols selected for investment at this time.")
return # Avoid division by zero
# Liquidate positions not in the new selection
for symbol in list(self.Portfolio.Keys):
if symbol not in self.long_symbols:
self.Liquidate(symbol)
self.highestPrices.pop(symbol, None)
stop_ticket = self.stopOrderTickets.pop(symbol, None)
if stop_ticket:
self.Transactions.CancelOrder(stop_ticket.OrderId)
# Set holdings for selected symbols
weight = 1 / len(self.long_symbols)
for symbol in self.long_symbols:
if not self.Portfolio[symbol].Invested:
self.SetHoldings(symbol, weight)
# Initialize highest price for trailing stop
self.highestPrices[symbol] = self.Securities[symbol].Price
# Place initial stop order
quantity = self.Portfolio[symbol].Quantity
stop_price = self.Securities[symbol].Price * 0.95 # 5% below current price
stop_ticket = self.StopMarketOrder(symbol, -quantity, stop_price)
self.stopOrderTickets[symbol] = stop_ticket
def OnData(self, data):
if self.IsWarmingUp:
return # Skip processing during warm-up
if not self.warmupComplete:
self.Debug("Warm-up finished. Performing initial symbol selection and rebalancing.")
self.warmupComplete = True
self.ScheduleRebalance()
return
# Update highest prices and adjust stop orders
for symbol in list(self.Portfolio.Keys):
if symbol in data and data[symbol]:
price = data[symbol].Close
if symbol in self.highestPrices:
if price > self.highestPrices[symbol]:
self.highestPrices[symbol] = price
# Update stop price
new_stop_price = price * 0.95 # 5% below highest price
stop_ticket = self.stopOrderTickets.get(symbol, None)
if stop_ticket and stop_ticket.Status == OrderStatus.Submitted:
update_fields = UpdateOrderFields()
update_fields.StopPrice = new_stop_price
self.Transactions.UpdateOrder(stop_ticket.OrderId, update_fields)
else:
# Initialize highest price if not set
self.highestPrices[symbol] = price
def OnOrderEvent(self, orderEvent):
if orderEvent.Status == OrderStatus.Filled:
symbol = orderEvent.Symbol
# If a stop order was filled, remove from tracking
if orderEvent.Direction == OrderDirection.Sell:
self.highestPrices.pop(symbol, None)
self.stopOrderTickets.pop(symbol, None)
if symbol in self.long_symbols:
self.long_symbols.remove(symbol)