Overall Statistics
Total Trades
953
Average Win
1.90%
Average Loss
-1.34%
Compounding Annual Return
16.269%
Drawdown
15.400%
Expectancy
0.460
Net Profit
1634.269%
Sharpe Ratio
1.231
Probabilistic Sharpe Ratio
80.269%
Loss Rate
40%
Win Rate
60%
Profit-Loss Ratio
1.42
Alpha
0.101
Beta
0.178
Annual Standard Deviation
0.093
Annual Variance
0.009
Information Ratio
0.241
Tracking Error
0.155
Treynor Ratio
0.642
Total Fees
$176705.63
Estimated Strategy Capacity
$82000000.00
Lowest Capacity Asset
TLT SGNKIKYGE9NP
from AlgorithmImports import *
import numpy as np


class CryingVioletZebra(QCAlgorithm):

    def Initialize(self):
        
        # set up
        self.SetStartDate(2003, 2, 1)
        self.SetEndDate(2022, 1, 1)
        self.SetCash(1000000)
        self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin)

        # parameters
        self.sd_threshold = 0.004       # standard deviation threshold after which we sell
        self.predictions_sd_window = 3  # window size when calculating standard deviation of predictions
        self.safe_asset = "TLT"          # safe asset we buy, when we sell SPY. Chouces: (None, ticker: Inverse SPY: SH, SDS, SPXU, Bonds: TLT, VFSTX , money market FX: FXE)
        # self.thresold = 0.00          # ne koristimo, prvotno zamisljeno za predikcije
        #  self.sma_width = 1           # ne koristimo
        # self.close_window_length = 7  # ne koristimo

        # optimization parameters
        # self.sd_threshold = float(self.GetParameter("threshold-sd"))
        # self.predictions_sd_window = int(self.GetParameter("preds-window"))

        # data feeds
        self.UniverseSettings.Leverage = 4
        PRAData.set_algo(self)
        if self.LiveMode:
            self.spy = self.AddEquity("SPY", Resolution.Minute).Symbol
            self.symbol = self.AddData(PRAData, "PR", Resolution.Minute, TimeZones.Utc).Symbol
        else:
            equity = self.AddEquity("SPY", Resolution.Daily)
            equity.SetDataNormalizationMode(DataNormalizationMode.Raw)
            self.spy = equity.Symbol
            self.SetBenchmark(lambda dt: self.Securities[self.spy].Price)
            self.symbol = self.AddData(PRAData, "PR", Resolution.Daily).Symbol
            if self.safe_asset is not None: 
                self.AddEquity(self.safe_asset, Resolution.Daily)
            # self.pra_sma = self.SMA("PR", self.sma_width, Resolution.Daily)
        
        # init
        self.pred_window = RollingWindow[float](self.predictions_sd_window)
        self.std_window = RollingWindow[float](2)
        self.high_month = 0 
        
        # consolidator
        self.Consolidate("SPY", Calendar.Monthly, self.MonthBarHandler)
        
        # set and warmup rolling close
        # self.close_radf = RollingWindow[float](self.close_window_length)
        # self.volume = RollingWindow[float](8)
        # history = self.History(["SPY"], self.close_window_length, Resolution.Hour)
        # for index, ohlcv in history.loc["SPY"].iterrows():
        #     self.close_radf.Add(ohlcv["close"])
        #     self.volume.Add(ohlcv["volume"])
        
        # define margins
        # self.Securities["SPY"].SetLeverage(4)
        # self.Securities[self.safe_asset].SetLeverage(4)
 
    def MonthBarHandler(self, bar):
        self.high_month = bar.High
 
    def OnData(self, data):

        # extract indicator
        if self.LiveMode:
            # get SPY data
            spy = self.Securities[self.spy]
            o, h, l, c = spy.Open, spy.High, spy.Low, spy.Close
            if c == 0: 
                self.Log("SPY close price is 0.")    
                return
            # check if custom data is ready
            if not data.ContainsKey(self.symbol):
                # self.Log(f'There is no indicator data for the bar.')
                return
            # define radf value and spy close
            pr = data[self.symbol].Value
        else:
            # check if SPY and safe assets are ready
            if not data.Bars.ContainsKey(self.spy) and not data.Bars.ContainsKey(self.spy) or data[self.spy] is None:
                self.Log(f'SPY is not ready')
                return
            if self.safe_asset is not None:
                if not data.Bars.ContainsKey(self.safe_asset)  and not data.Bars.ContainsKey(self.safe_asset) or data[self.safe_asset] is None:
                    self.Log(f'TLT is not ready')
                    return
                
            # check if custom data is ready
            if not data.ContainsKey(self.symbol):
                self.Log(f'There is no indicator data for the bar.')
                return
            pr = data[self.symbol].Value
            ############ SMA ############
            # if not self.pra_sma.IsReady:
            #     self.Log(f'Radf agg SMA is not ready')
            #     return
            # pr = self.pra_sma.Current.Value
            ############ SMA ############
            
        # populate predictins window
        self.pred_window.Add(pr)
        if not self.pred_window.IsReady: return

        # predictions standard deviation
        preds_std = np.std(list(self.pred_window))
        self.std_window.Add(preds_std)
        if not self.pred_window.IsReady: return
        if not self.std_window.IsReady: return

        # growth rate of indicator
        indicator_gw = list(self.std_window)
        indicator_gw = indicator_gw[0] / indicator_gw[1] - 1
        self.Debug(f"Indicator growth rate: {indicator_gw}")

        # current return
        # close_diff = self.close_radf[0] - self.close_radf[self.close_window_length - 1]
        
        # plot indicator
        # self.Plot("Indicators", "PR", pr)
        # self.Plot("Indicators", "Threshold", self.thresold)
        self.Plot("Indicators", "Predictoins sd", preds_std)
        self.Plot("Indicators", "Threshold sd", self.sd_threshold)
        self.Plot("Indicators GR", "SD", indicator_gw)

        ## buy or sell all
        if not self.Portfolio["SPY"].Invested and preds_std <= self.sd_threshold: # pr > self.thresold
            if self.safe_asset is not None:
                if self.Portfolio[self.safe_asset].Invested:
                    self.Liquidate(self.safe_asset)
            self.SetHoldings("SPY", 1)
        elif self.Portfolio["SPY"].Invested and preds_std > self.sd_threshold:
            self.Liquidate("SPY")
            if self.safe_asset is not None:
                self.SetHoldings(self.safe_asset, 1)
        

class PRAData(PythonData):
    
    algo = None
    
    @staticmethod
    def set_algo(algo):
        PRAData.algo = algo
        
    def GetSource(self, config, date, isLive):
        if isLive:
            url = "https://contentiobatch.blob.core.windows.net/qc-live/pra.csv?sp=r&st=2021-09-29T14:42:22Z&se=2025-04-28T22:42:22Z&sv=2020-08-04&sr=b&sig=gJCmQj%2FU6%2FwL2pPEytx6nwtfbWAoKXDLYQ%2FmgeieGGM%3D"
            return SubscriptionDataSource(url, SubscriptionTransportMedium.RemoteFile) 
        else:
            url = "https://contentiobatch.blob.core.windows.net/qc-backtest/systemic_risk.csv"
            return SubscriptionDataSource(url, SubscriptionTransportMedium.RemoteFile) 
        
    def Reader(self, config, line, date, isLive):
        # If first character is not digit, pass
        if not (line.strip() and line[0].isdigit()): return None

        if isLive:
            data = line.split(';') 
            index = PRAData()
            index.Symbol = config.Symbol
            index.EndTime = datetime.utcnow()
            index.Time = datetime.utcnow()
            index.Value = int(data[1])

        else:
            data = line.split(',')
            index = PRAData()
            index.Symbol = config.Symbol
            index.Time = datetime.strptime(data[0], '%Y-%m-%d') #+ timedelta(hours=1) # Make sure we only get this data AFTER trading day - don't want forward bias.
            index.Value = float(data[1])      # pr_below_dummy_176

        return index