Overall Statistics
Total Trades
1256
Average Win
1.44%
Average Loss
-1.17%
Compounding Annual Return
9.053%
Drawdown
6.000%
Expectancy
0.147
Net Profit
181.118%
Sharpe Ratio
0.463
Sortino Ratio
1.114
Probabilistic Sharpe Ratio
0.016%
Loss Rate
48%
Win Rate
52%
Profit-Loss Ratio
1.22
Alpha
0
Beta
0
Annual Standard Deviation
0.112
Annual Variance
0.013
Information Ratio
0.6
Tracking Error
0.112
Treynor Ratio
0
Total Fees
$17027.50
Estimated Strategy Capacity
$4600000.00
Lowest Capacity Asset
CZI UI5APSUZZHWL
Portfolio Turnover
1.45%
#region imports
from AlgorithmImports import *
#endregion

class UncorrelatedAssetsAlphaModel(AlphaModel):

    securities = []
    previous_insight_symbols = []
    symbols_to_liquidate  = []
    month = -1

    def __init__(self, portfolio_size, lookback_days):
        self.portfolio_size = portfolio_size
        self.lookback_days = lookback_days

    def Update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]:
        # Rebalance monthly when there is QuoteBar data available
        if self.month == algorithm.Time.month or data.QuoteBars.Count == 0:
            return []

        # Select the assets we can trade
        tradable_symbols = [security.Symbol for security in self.securities if security.Symbol in data.QuoteBars and security.Price > 0]

        # Fetch history on the tradable assets
        history = algorithm.History(tradable_symbols, self.lookback_days, Resolution.Daily, dataNormalizationMode=DataNormalizationMode.ScaledRaw)
        if history.empty: 
            return []

        self.month = algorithm.Time.month
    
        # Compute daily returns
        returns = history['close'].unstack(level=0).pct_change().iloc[1:]
    
        # Get correlation
        correlation = returns.corr()
        
        # Find the subset of assets with lowest absolute sum correlation
        selected = []
        for index, row in correlation.iteritems():
            corr_rank = row.abs().sum()
            selected.append((index, corr_rank))
        most_uncorrelated_symbols = [x[0] for x in sorted(selected, key = lambda x: x[1])[:self.portfolio_size]]
        
        # Create insights to long the assets with the most uncorrelated symbols
        insights = []
        expiry = self.securities[0].Exchange.Hours.GetNextMarketOpen(Expiry.EndOfMonth(algorithm.Time), extendedMarketHours=False) - timedelta(minutes=1)
        for symbol in most_uncorrelated_symbols:
            insights.append( Insight.Price(symbol, expiry, InsightDirection.Up) )

        return insights

    def OnSecuritiesChanged(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None:
        for security in changes.RemovedSecurities:
            if security in self.securities:
                self.securities.remove(security)
                
        self.securities.extend(changes.AddedSecurities)
                
# region imports
from AlgorithmImports import *
from alpha import UncorrelatedAssetsAlphaModel
from portfolio import ExtendedEqualWeightingPortfolioConstructionModel
from universe import MostLiquidUniverseSelectionModel

# endregion

class UncorrleatedAssetsAlgorithm(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2012, 1, 1)
        self.SetEndDate(2023, 12, 1)
        self.SetCash(1_000_000)

        self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin)

        self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.Adjusted
        # self.AddUniverseSelection(MostLiquidUniverseSelectionModel(self.GetParameter("universe_size", 100), self.UniverseSettings))
        self.yinn = self.AddEquity('YINN', Resolution.Daily)
        self.yang = self.AddEquity('YANG', Resolution.Daily)

        # self.AddAlpha(UncorrelatedAssetsAlphaModel(
        #     self.GetParameter("portfolio_size", 10),
        #     self.GetParameter("lookback_days", 252),
        # ))
        self.threshold = self.GetParameter('threshold', 0.2)

        # self.Settings.RebalancePortfolioOnSecurityChanges = True
        # self.SetPortfolioConstruction(ExtendedEqualWeightingPortfolioConstructionModel(lambda time: None))

        #self.AddRiskManagement(NullRiskManagementModel())

        #self.SetExecution(ImmediateExecutionModel()) 

    def OnData(self, slice: Slice):
        if not self.Portfolio.Invested:
            self.SetHoldings("YINN", -0.5)
            self.SetHoldings("YANG", -0.5)
        else:
            port_val = self.Portfolio.TotalHoldingsValue
            yinn_val = self.Portfolio['YINN'].HoldingsValue
            yang_val = self.Portfolio['YANG'].HoldingsValue
            log_exposure = (3)*yinn_val/port_val + (-3)*yang_val/port_val
            if self.Time.year==2016 and self.Time.month==3 and self.Time.day >= 15:
                self.Debug(f'{self.Time} port val: {port_val}, yinn_val: {yinn_val}, yang_val: {yang_val}\
                            exp: {log_exposure}, yang_price: {slice["YANG"].Close}, yinn_price: {slice["YINN"].Close}'  )

            if abs(log_exposure) > self.threshold:
                self.Debug('Rebalancing...')
                self.SetHoldings("YINN", -0.5)
                self.SetHoldings("YANG", -0.5)
        

#region imports
from AlgorithmImports import *
#endregion

class ExtendedEqualWeightingPortfolioConstructionModel(EqualWeightingPortfolioConstructionModel):

    target_collection = PortfolioTargetCollection()
    symbols_to_liquidate = []

    def CreateTargets(self, algorithm: QCAlgorithm, insights: List[Insight]) -> List[PortfolioTarget]:
        portfolio_targets = list(super().CreateTargets(algorithm, insights))
        self.target_collection.AddRange(portfolio_targets)

        # Flag securities as to-be-liquidated if they are no longer targeted
        if len(insights) > 0:
            targeted_symbols = [insight.Symbol for insight in insights]
            for target in self.target_collection:
                if target.Symbol not in targeted_symbols:
                    self.symbols_to_liquidate.append(target.Symbol)
        
        # Liquidate some securities that are flagged as to-be-liquidated
        liquidated_symbols = []
        for symbol in self.symbols_to_liquidate:
            security = algorithm.Securities[symbol]
            if security.IsDelisted:
                liquidated_symbols.append(symbol)

            elif security.Price > 0 and symbol in algorithm.CurrentSlice.QuoteBars:
                liquidated_symbols.append(symbol)
                portfolio_targets.append(PortfolioTarget.Percent(algorithm, symbol, 0))

        # Remove targets from the PortfolioTargetCollection if the security was delisted or liquidated
        for symbol in liquidated_symbols:
            self.target_collection.Remove(symbol)
            self.symbols_to_liquidate.remove(symbol)

        return portfolio_targets

        
#region imports
from AlgorithmImports import *
#endregion

class MostLiquidUniverseSelectionModel(CoarseFundamentalUniverseSelectionModel):
    def __init__(self, universe_size: int, universe_settings: UniverseSettings) -> None:
        self.universe_size = universe_size
        super().__init__(self.SelectCoarse, universe_settings)

    def SelectCoarse(self, coarse: List[CoarseFundamental]) -> List[Symbol]:
        # Select the most liquid stocks
        sorted_by_dollar_volume = sorted(coarse, key=lambda c: c.DollarVolume, reverse=True)
        return [c.Symbol for c in sorted_by_dollar_volume[:self.universe_size]]