Overall Statistics
Total Trades
10137
Average Win
0.07%
Average Loss
-0.07%
Compounding Annual Return
-1.113%
Drawdown
7.500%
Expectancy
-0.009
Net Profit
-3.849%
Sharpe Ratio
-1.535
Probabilistic Sharpe Ratio
0.034%
Loss Rate
50%
Win Rate
50%
Profit-Loss Ratio
1.00
Alpha
-0.024
Beta
0.007
Annual Standard Deviation
0.015
Annual Variance
0
Information Ratio
-0.728
Tracking Error
0.152
Treynor Ratio
-3.185
Total Fees
$0.00
Estimated Strategy Capacity
$2800000.00
Lowest Capacity Asset
EWI R735QTJ8XC9X
Portfolio Turnover
62.40%
# region imports
from AlgorithmImports import *
from statsmodels.tsa.vector_ar.vecm import VECM
# endregion

class CreativeRedBadger(QCAlgorithm):
    tickers = ['SPY','QQQ','IWM','DIA','EZU', 'EWJ','FXI','EWT','EWG','EWC','EWI','EWW','INDA']

    length = 10
    sigma = 1.5
    res = Resolution.Hour # Daily, Hour, Minute

    time_exit_bars = 10 # Same as length? (HL?)
    extra_edge = 0.05

    # ---------------------------------------------------

    wt_arr = None # Numpy array.
    wt_vec = []
    wts_by_symbol = {}

    series_ask_terms_by_symbol = {}
    series_bid_terms_by_symbol = {}

    position = 0
    bar_ct = 0

    def Initialize(self):
        self.SetStartDate(2020, 4, 19)
        # self.SetEndDate(2022,5, 20)
        self.SetCash(100000)

        self.BuyBasis = RollingWindow[float](self.length)
        self.SellBasis = RollingWindow[float](self.length)
        self.Basis = RollingWindow[float](self.length)

        self.bb = BollingerBands(self.length, self.sigma)

        symbols = []
        for t in self.tickers:
            try:
                symbol = self.AddEquity(t, self.res).Symbol
                symbols.append(symbol)
            except:
                pass 

        self.SetSecurityInitializer(lambda x: x.SetFeeModel(ConstantFeeModel(0)))  
        # # 0 fees, for now.
        # for security in self.Securities:
        #     security.SetFeeModel(ConstantFeeModel(0))

        # Only run this weekly?
        self.Schedule.On(self.DateRules.WeekStart(0),
                 self.TimeRules.AfterMarketOpen("SPY", -10),
                 self.BeforeMarketOpen)

        

    def BeforeMarketOpen(self):
        ## TODO: when we are dealing with quoting, and bid/ask spreads -- use this.
        # quote_bars_df = self.History(QuoteBar, self.Securities.Keys, 5, Resolution.Minute)
        df = self.History(self.Securities.Keys, 10000, self.res) # IF fails, use DAILY, or HOURLY
        closes = df.unstack(level=0).close
        closes.dropna(inplace=True, how='any')
        self.Log(f'Closes -- head: {closes.head()}')

        closes = closes[[i for i in self.Securities.Keys]] # Addit, for order safety.

        vecm = VECM(closes, deterministic='n', k_ar_diff=1)
        vecm_fit = vecm.fit()

        norm_wts = vecm_fit.beta / np.abs(vecm_fit.beta).sum()

        self.wt_vec = norm_wts[:,0] # (n,) vec
        self.wt_arr = norm_wts


        # THIS might have broken something... this was using Securities.Keys
        for n, k in enumerate(self.Securities.Keys): # Ensure columns line up w this order.
            self.wts_by_symbol[k] = self.wt_vec[n]

        self.Log(f'Weights: {self.wts_by_symbol.items()}')
        self.basis = np.dot(closes, vecm_fit.beta)


        # Valid...
        # self.long_targets = [PortfolioTarget(self.s1, per_symbol), PortfolioTarget(self.s2, -per_symbol)]
        # self.short_targets = [PortfolioTarget(self.s1, -per_symbol), PortfolioTarget(self.s2, per_symbol)]
        # self.flat_targets = [PortfolioTarget(self.s1, 0.0), PortfolioTarget(self.s2, 0.0)]


        self.long_targets = []
        self.flat_targets = []
        self.short_targets = []
        for symbol, wt in self.wts_by_symbol.items():
            print(type(symbol), symbol)
            # self.Log(f'type: {type(symbol)}, symbol: {symbol} -- wt: {wt}, type: {type(wt)}')

            pft = PortfolioTarget(symbol, wt)
            self.long_targets.append(pft)

            pft = PortfolioTarget(symbol, 0.0)
            self.flat_targets.append(pft)

            pft = PortfolioTarget(symbol, -1.0 * wt)
            self.short_targets.append(pft)

        # WARMUP? 
        basis = pd.DataFrame(self.basis[:,0], index=closes.index, columns=['Series'])
        for dt, row in basis.iterrows():
            # self.Log(f'dt: {dt}, Basis: {row}, {row.Series}')
            b = row.Series
            self.bb.Update(dt, b)
            self.Basis.Add(b)


    def OnSecuritiesChanged(self, changes):
        for security in changes.AddedSecurities:
            security.SetFeeModel(ConstantFeeModel(0.0))

    def OnData(self, data: Slice):

        # Check data valid
        # data = data.QuoteBars
        data = data.Bars
        if not data.ContainsKey('SPY'):
            self.Log('No Data -- return out')
            return 

        # Check weights valid (Model Fit)
        if len(self.wt_vec) == 0: return 


        # # How do we price this out, to quote them iteratively? 
        # # i.e. pull a wrt b-f, etc.
        # # Price each 'term', maybe?
        curr_basis = 0
        for symbol, wt in self.wts_by_symbol.items():
            
            try:
                # If wt is positive, we are BUYING this, so price it wrt ASK (for pos spread)
                buy_term = data[symbol].Ask.Close * wt 
                sell_term = data[symbol].Bid.Close * wt
            except:
                buy_term = data[symbol].Close 
                sell_term = buy_term 

            # Assuming this is a buy, our bid term would be pricing the series as if buying
            # thus, for bid, we are pricing at ask of positive weighted symbols (BUY Spread)
            #       for ask, we are pricing at bid of negative weighted symbols.(SELL Spread)
            term = buy_term if wt > 0 else sell_term 
            self.series_bid_terms_by_symbol[symbol] = buy_term if wt > 0 else buy_term 
            self.series_ask_terms_by_symbol[symbol] = sell_term if wt > 0 else sell_term 
            curr_basis += term
        
        self.Basis.Add(curr_basis)
        self.bb.Update(self.Time, curr_basis)

        # # This is a bit muddled, but it should work.
        # if not self.bb.IsReady: return 

        # ## TODO: price out each leg, wrt the rest...
        # # Prototype elsewhere, then bring into this.

        upper, lower, mid = self.bb.UpperBand.Current.Value, self.bb.LowerBand.Current.Value, self.bb.MiddleBand.Current.Value
        self.Log(f'Basis BB: \
                    Upper: {self.bb.UpperBand.Current.Value},\
                    Lower: {self.bb.LowerBand.Current.Value}\
                    Middle: {self.bb.MiddleBand.Current.Value}')

        
        curr = curr_basis
        if self.position == 0:
            # could sum the bid_terms for bid basis. 
            if curr < lower - self.extra_edge:
                self.position = 1
                self.SetHoldings(self.short_targets, False, f"LE -- Series: {curr}")
            
            if curr > upper + self.extra_edge:
                self.position = -1
                self.SetHoldings(self.long_targets, False, f"SE -- Series: {curr}")


        # Increment bar count
        if self.position != 0:
            self.bar_ct += 1
            if self.bar_ct > self.time_exit_bars:
                self.Liquidate()
        
        opnl = self.Portfolio.TotalUnrealisedProfit
        # Long -- Looking to exit.
        if self.position > 0:
            if curr > mid:
                self.position = 0
                self.bar_ct = 0
                self.SetHoldings(self.flat_targets, False, f"LX -- Series: {curr}, PNL: {opnl}")
                return 
            

        # Short -- Looking to exit.
        if self.position < 0:
            if curr < mid:
                self.position = 0
                self.bar_ct = 0
                self.SetHoldings(self.flat_targets, False, f"SX -- Series: {curr}, PNL: {opnl}")
                return