Overall Statistics
Total Trades
4983
Average Win
0.16%
Average Loss
-0.12%
Compounding Annual Return
13.159%
Drawdown
22.100%
Expectancy
0.215
Net Profit
89.423%
Sharpe Ratio
0.772
Probabilistic Sharpe Ratio
28.639%
Loss Rate
48%
Win Rate
52%
Profit-Loss Ratio
1.35
Alpha
0.118
Beta
-0.058
Annual Standard Deviation
0.145
Annual Variance
0.021
Information Ratio
0.074
Tracking Error
0.193
Treynor Ratio
-1.942
Total Fees
$39920.97
import numpy as np
from collections import deque

class Starter(QCAlgorithm):
    def Initialize(self):
        # Environment setup.
        self.SetStartDate(2014, 11, 1)
        self.SetCash(1000000)
        self.SetBrokerageModel(AlphaStreamsBrokerageModel())
        self.SetExecution(ImmediateExecutionModel())
        self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
        self.UniverseSettings.Resolution = Resolution.Minute
        self.symbols = {}
        
        # Parameters.
        self.barResolution = Resolution.Hour # Use 1H tradebars.
        self.insightFrequency = self.TimeRules.At(10, 30) # Emit insights daily at 10:30am.
        self.warmupBars = 100 # Initialize with 100 historical tradebars.
        self.isLongOnly = True # Only use long positions.

        # Selection.           
        # self.SetUniverseSelection(LiquidETFUniverse()) # Or, self.AddEquities(['QQQ', 'QTEC'])
        self.AddEquities(['SPY', 'QQQ', 'QTEC'])
        self.SetBenchmark('SPY')
        
        # Main insight loop.
        self.Schedule.On(self.DateRules.EveryDay(), self.insightFrequency, self.PublishInsights)
                         
    def Logger(self, msg):
        self.Debug('[{}] {}'.format(self.Time, msg))
        
    def ActiveSymbols(self):
        # Returns a tuple list of active symbols, ready for trading.
        return [(str(x.Symbol), self.symbols[str(x.Symbol)]) for x in self.ActiveSecurities.Values \
                 if self.IsMarketOpen(x.Symbol) and \
                 str(x.Symbol) in self.symbols.keys() and \
                 self.Securities[x.Symbol].Price > 0 and \
                 self.Status != Status.Wait]

    def OnSecuritiesChanged(self, changes):
        symbolsAdded   = [x.Symbol for x in changes.AddedSecurities]
        symbolsRemoved = [x.Symbol for x in changes.RemovedSecurities]

        # Warm up new symbols.
        self.AddSymbols(symbolsAdded)

        # Discard removed symbols.
        self.RemoveSymbols(symbolsRemoved)
        
    def AddEquities(self, equities):
        # Adds a list of equities to the strategy.
        self.AddSymbols([self.AddEquity(equity).Symbol for equity in equities])

    def AddSymbols(self, symbols):
        # Warms up a list of symbols and adds them to the symbols dict.
        df = self.History(symbols, self.warmupBars, self.barResolution)
        for symbol in symbols:
            self.symbols[str(symbol)] = Symbol(str(symbol), symbol.ID, self)
            if str(symbol.ID) not in df.index.get_level_values(0): continue
            self.symbols[str(symbol)].HistoricalDataWarmup(df.loc[symbol], self)

    def RemoveSymbols(self, symbols):
        # Removes a list of symbols and unsubscribes their tradebar consolidators.
        for symbol in symbols:
            self.SubscriptionManager.RemoveConsolidator(symbol, self.symbols[symbol].bar)
            self.symbols[symbol].pop()
            
    def PublishInsights(self):
        # Emit insights from active symbols and update status.
        insights = []

        for symbol, symbolData in self.ActiveSymbols():
            status = symbolData.status
            duration = symbolData.exp_max - self.Time if symbolData.exp_max != None else Time.EndOfTimeTimeSpan

            isInvested = self.Portfolio[symbol].Invested
            isLong = self.Portfolio[symbol].IsLong
            isExpired = symbolData.exp_max != None and self.Time > symbolData.exp_max
            isClose = (status == Status.Sell and isLong) or (status == Status.Buy and not isLong) or status == Status.Close

            if not isInvested:
                if isExpired:
                    # Update the expired symbols from the portfolio environment.
                    symbolData.Reset(Status.Ready)
                    continue
                if status == Status.Buy:
                    # Push buy orders.
                    insights.append(Insight.Price(symbol, duration, InsightDirection.Up, symbolData.magnitude, None, None))
                    symbolData.status = Status.Bought
                    symbolData.orderAt = self.Time
                elif status == Status.Sell and not self.isLongOnly:
                    # Push sell orders.
                    insights.append(Insight.Price(symbol, duration, InsightDirection.Down, symbolData.magnitude, None, None))
                    symbolData.status = Status.Sold
                    symbolData.orderAt = self.Time
            else:
                # Updated expired (closed) positions and process close events for current holdings.
                if isExpired or isClose:
                    insights.append(Insight.Price(symbol, timedelta(1), InsightDirection.Flat))
                    symbolData.Reset(Status.Ready)

        self.EmitInsights(insights)

class Symbol:
    def __init__(self, symbol, symbolId, alg):
        # Setup.
        self.symbol = symbol
        self.id = symbolId
        self.Reset(Status.Wait)
        self.Initialize(alg)
        
        # Subscribe to tradebars.
        self.bar = TradeBarConsolidator(Extensions.ToTimeSpan(alg.barResolution))
        self.bar.DataConsolidated += lambda sender, tradebar: self.HandleBars(tradebar, alg)
        alg.SubscriptionManager.AddConsolidator(symbol, self.bar)

    def Initialize(self, alg):
        self.barQ = deque(maxlen=alg.warmupBars)
        self.rsi = alg.RSI(self.symbol, 30, alg.barResolution)

    def HandleBars(self, bar, alg, isWarmup=False):
        # Main event loop.
        self.rsi.Update(bar.Time, bar.Close)
        self.barQ.append(bar)
        
        if isWarmup or len(self.barQ) < self.barQ.maxlen: return
        
        rsiValue = self.rsi.Current.Value
        close = np.array([bar.Close for bar in self.barQ])
        stdDev = np.std(close)
        
        if self.status == Status.Ready:
            if rsiValue < 30:
                self.status = Status.Buy
                self.magnitude = stdDev / close[-1]
                self.exp_max = alg.Time + timedelta(weeks=4) # Close orders open longer than 4 weeks.
                self.exp_min = alg.Time + timedelta(weeks=1) # Keep orders open at least 1 week.
        elif self.status == Status.Bought:
            if self.exp_min != None and alg.Time < self.exp_min: return
            
            if rsiValue > 70:
                self.status = Status.Sell # Immediately close if RSI crosses upper limit.

    def HistoricalDataWarmup(self, history, alg):
        # Stream historical warm up data.
        for t, r in history.iterrows():
            o, h, l, c, v = r['open'], r['high'], r['low'], r['close'], r['volume']
            bar = TradeBar(t, self.symbol, o, h, l, c, v)
            self.HandleBars(bar, alg, isWarmup=True)
        self.status = Status.Ready
        
    def Reset(self, status):
        self.exp_max = None
        self.exp_min = None
        self.magnitude = None
        self.orderAt = None
        self.status = status
        
class Status(Enum):
    Wait   = 0 # Warming up or not ready for trading.
    Ready  = 1 # Ready to trade.
    Buy    = 2 # Signal a buy recommendation (close if sold.)
    Sell   = 3 # Signal a sell recommendation (close if bought.)
    Close  = 4 # Signal a close recommendation.
    Bought = 5 # Symbol holds a long position.
    Sold   = 6 # Symbol holds a short position.