Overall Statistics
Total Orders
666
Average Win
2.10%
Average Loss
-1.98%
Compounding Annual Return
667.070%
Drawdown
25.500%
Expectancy
0.249
Start Equity
1000000
End Equity
3432799.09
Net Profit
243.280%
Sharpe Ratio
5.675
Sortino Ratio
7.689
Probabilistic Sharpe Ratio
88.267%
Loss Rate
39%
Win Rate
61%
Profit-Loss Ratio
1.06
Alpha
3.357
Beta
4.41
Annual Standard Deviation
0.753
Annual Variance
0.567
Information Ratio
5.696
Tracking Error
0.714
Treynor Ratio
0.969
Total Fees
$3627.25
Estimated Strategy Capacity
$700000.00
Lowest Capacity Asset
BIOA R735QTJ8XC9X
Portfolio Turnover
94.86%
#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 = 20

        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.MonthStart(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:
            self.Debug("Already rebalanced this month")
            # Set rebalanced flag to True
            self.rebalanced = True
            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)
        
        # Reduce mu by transaction costs
        transaction_cost = 0.001  # Assumed transaction cost per trade
        for symbol in sorted_symbols:
            if symbol in mu:
                mu[symbol] -= transaction_cost
            else:
                self.Debug(f"Symbol {symbol} not found in mu")

        # 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)
       
    
    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 Day 1
        if self.Time.day != 1:
            self.rebalanced = False