| Overall Statistics |
|
Total Orders 7164 Average Win 0.30% Average Loss -0.25% Compounding Annual Return 160.615% Drawdown 10.100% Expectancy 0.180 Start Equity 1000000 End Equity 1637648.11 Net Profit 63.765% Sharpe Ratio 3.911 Sortino Ratio 7.332 Probabilistic Sharpe Ratio 96.374% Loss Rate 46% Win Rate 54% Profit-Loss Ratio 1.20 Alpha 0.731 Beta 1.144 Annual Standard Deviation 0.242 Annual Variance 0.059 Information Ratio 3.459 Tracking Error 0.219 Treynor Ratio 0.829 Total Fees $5573.49 Estimated Strategy Capacity $6500000.00 Lowest Capacity Asset BIOA R735QTJ8XC9X Portfolio Turnover 260.20% |
#region imports
from AlgorithmImports import *
#endregion
def CalculateTrendIndicators(self):
top_pct= 0.1
# Moving average calculation
moving_averages = {}
for symbol, prices in self.historical_data.items():
if len(prices) >= self.lookback:
moving_averages[symbol] = prices.mean()
top_symbols = sorted(moving_averages, key=moving_averages.get, reverse=True)[:int(len(moving_averages) * top_pct)]
# # Compounded Return Calculations
# moving_averages = {}
# compounded_returns = {}
# for symbol, prices in self.historical_data.items():
# if len(prices) >= self.lookback:
# daily_returns = prices.pct_change().dropna()
# compounded_return = (1 + daily_returns).prod() - 1
# compounded_returns[symbol] = compounded_return
# top_symbols = sorted(compounded_returns, key=compounded_returns.get, reverse=True)[:int(len(compounded_returns) * top_pct)]
return top_symbols#region imports
from AlgorithmImports import *
from pypfopt import BlackLittermanModel
import pandas as pd
import numpy as np
#endregion
def OptimizePortfolio(self, mu, S):
# Black-Litterman views (neutral in this case)
Q = pd.Series(index=mu.index, data=mu.values)
P = np.identity(len(mu.index))
# Optimize via Black-Litterman
bl = BlackLittermanModel(S,Q=Q, P=P, pi=mu)
bl_weights = bl.bl_weights()
# Normalize weights
total_weight = sum(bl_weights.values())
normalized_weights = {symbol: weight / total_weight for symbol, weight in bl_weights.items()}
return normalized_weights#region imports
from pypfopt import risk_models, expected_returns
from AlgorithmImports import *
#endregion
def CalculateRiskParameters(self, top_symbols):
# Get historical prices for selected symbols
selected_history = self.History(top_symbols, self.lookback, Resolution.Daily)['close'].unstack(level=0)
mu = expected_returns.mean_historical_return(selected_history)
S = risk_models.sample_cov(selected_history)
return mu, S#region imports
from AlgorithmImports import *
#endregion
def Execute_Trades(self, position_list):
# Place market orders
for symbol, weight in position_list.items():
self.SetHoldings(symbol, weight)
def Exit_Positions(self, position_list):
# Liquidate positions not in the target weights
for holding in self.Portfolio.Values:
if holding.Symbol not in position_list and holding.Invested:
self.Liquidate(holding.Symbol)# region imports
from AlgorithmImports import *
from Alpha_Models import CalculateTrendIndicators
from Risk_Models import CalculateRiskParameters
from Portfolio_Construction import OptimizePortfolio
from Trade_Execution import Execute_Trades, Exit_Positions
# endregion
class NCSU_Strategy_2024_Q3(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2023, 12, 1) # Set Start Date
self.SetEndDate(2024, 12, 31) # Set End Date
self.SetCash(1000000) # Set Strategy Cash
# ETF to get constituents from
self.etf = "SPY"
self.universe_settings.leverage = 2.0
self.AddEquity(self.etf, Resolution.Daily)
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverseSelection(ETFConstituentsUniverseSelectionModel(self.etf))
self.historical_data = {}
self.lookback = 252 # MAX lookback period for moving average calculation
self.rebalanceTime = None
self.rebalanced = False
self.Schedule.On(self.DateRules.every_day(self.etf), self.TimeRules.AfterMarketOpen(self.etf, 20), self.Rebalance)
def OnSecuritiesChanged(self, changes):
# Evaluate if performance is better by trading out of holdings dropped from ETF
for security in changes.AddedSecurities:
self.historical_data[security.Symbol] = self.History(security.Symbol, self.lookback, Resolution.Daily)['close']
for security in changes.RemovedSecurities:
if security.Symbol in self.historical_data:
del self.historical_data[security.Symbol]
if self.Portfolio[security.Symbol].Invested:
self.Liquidate(security.Symbol)
self.Debug(f"Liquidating {security.Symbol} as it is removed from the ETF")
def Rebalance(self):
# Check if today is the first day of the month and if we have already rebalanced
if self.Time.day == 1 and self.rebalanced == True:
self.Debug("Already rebalanced this month")
return
# Rebalancing logic
self.Debug(f"Rebalancing on {self.Time}")
# Alpha Model Output
sorted_symbols = CalculateTrendIndicators(self)
# Risk Model Output
mu, S = CalculateRiskParameters(self, top_symbols=sorted_symbols)
# Portfolio Construction Output
target_positions = OptimizePortfolio(self, mu=mu, S=S)
Exit_Positions(self, position_list=target_positions)
Execute_Trades(self, position_list=target_positions)
# Set rebalanced flag to True
self.rebalanced = True
def CheckPositions(self):
# If no positions are on, call Rebalance
if not self.Portfolio.Invested:
self.Debug("No positions on, calling Rebalance")
self.Rebalance()
# Reset rebalanced flag if it's not 1st of the Month
if self.Time.day != 1:
self.rebalanced = False