Overall Statistics
Total Trades
67
Average Win
11.87%
Average Loss
-1.51%
Compounding Annual Return
26.052%
Drawdown
20.400%
Expectancy
6.265
Net Profit
1943.236%
Sharpe Ratio
1.662
Probabilistic Sharpe Ratio
95.317%
Loss Rate
18%
Win Rate
82%
Profit-Loss Ratio
7.88
Alpha
0.259
Beta
0.106
Annual Standard Deviation
0.168
Annual Variance
0.028
Information Ratio
0.34
Tracking Error
0.26
Treynor Ratio
2.631
Total Fees
$97.75
"""
Based on 'In & Out' strategy by Peter Guenther 4 Oct 2020
expanded/inspired by Tentor Testivis, Dan Whitnable (Quantopian), Vladimir, and Thomas Chang.

https://www.quantopian.com/posts/new-strategy-in-and-out
https://www.quantconnect.com/forum/discussion/9597/the-in-amp-out-strategy-continued-from-quantopian/p1
"""

# Import packages
import numpy as np
import pandas as pd
import scipy as sc


class InOut(QCAlgorithm):

    def Initialize(self):
        self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage)
        self.SetStartDate(2008, 1, 1)  # Set Start Date
        self.SetCash(5000)  # Set Strategy Cash
        self.UniverseSettings.Resolution = Resolution.Daily
        res = Resolution.Hour
        self.SetBenchmark("QQQ")
        # Feed-in constants
        self.INI_WAIT_DAYS = 15  # out for 3 trading weeks
        self.be_in = True
        self.last = True
        
        # Holdings
       ### 'Out' holdings and weights
        self.BND1 = self.AddEquity('TLT', res).Symbol #TLT; TMF for 3xlev
        self.BND2 = self.AddEquity('TMF', res).Symbol #IEF; TYD for 3xlev
        self.HLD_OUT = {self.BND1: 1}
        ### 'In' holdings and weights (static stock selection strategy)
        self.STKS = self.AddEquity('QQQ', res).Symbol #SPY or QQQ; TQQQ for 3xlev
        self.STKS2 = self.AddEquity('TQQQ', res).Symbol #SPY or QQQ; TQQQ for 3xlev
        self.HLD_IN = {self.STKS: 1}
        
        ### combined holdings dictionary
        self.wt = {**self.HLD_IN, **self.HLD_OUT}
        # Market and list of signals based on ETFs
        self.MRKT = self.AddEquity('SPY', res).Symbol  # market
        false = False
        true = True
        self.sig = [{"date": "2008-01-02", "be_in": false}, {"date": "2008-02-19", "be_in": true}, {"date": "2008-06-02", "be_in": false}, {"date": "2008-10-13", "be_in": true}, {"date": "2008-10-16", "be_in": false}, {"date": "2009-03-23", "be_in": true}, {"date": "2010-06-09", "be_in": false}, {"date": "2010-08-31", "be_in": true}, {"date": "2011-06-14", "be_in": false}, {"date": "2012-01-04", "be_in": true}, {"date": "2012-05-04", "be_in": false}, {"date": "2012-09-11", "be_in": true}, {"date": "2013-04-03", "be_in": false}, {"date": "2013-06-05", "be_in": true}, {"date": "2014-03-17", "be_in": false}, {"date": "2014-05-30", "be_in": true}, {"date": "2014-06-06", "be_in": false}, {"date": "2014-06-30", "be_in": true}, {"date": "2014-09-23", "be_in": false}, {"date": "2015-02-27", "be_in": true}, {"date": "2015-06-18", "be_in": false}, {"date": "2015-07-01", "be_in": true}, {"date": "2015-07-08", "be_in": false}, {"date": "2015-10-23", "be_in": true}, {"date": "2015-12-21", "be_in": false}, {"date": "2016-03-09", "be_in": true}, {"date": "2017-05-10", "be_in": false}, {"date": "2017-05-26", "be_in": true}, {"date": "2017-06-05", "be_in": false}, {"date": "2017-07-03", "be_in": true}, {"date": "2018-04-10", "be_in": false}, {"date": "2018-06-05", "be_in": true}, {"date": "2018-08-01", "be_in": false}, {"date": "2019-02-19", "be_in": true}, {"date": "2019-05-09", "be_in": false}, {"date": "2019-09-11", "be_in": true}, {"date": "2020-02-03", "be_in": false}, {"date": "2020-07-09", "be_in": true}]
        self.Schedule.On(
            self.DateRules.EveryDay(),
            self.TimeRules.AfterMarketOpen('SPY', 1),
            self.generate_signal
        )
        
        self.Schedule.On(
            self.DateRules.EveryDay(),
            self.TimeRules.AfterMarketOpen('SPY', 120),
            self.rebalance_when_out_of_the_market
        )


        self.Schedule.On(
            self.DateRules.WeekEnd(),
            self.TimeRules.AfterMarketOpen('SPY', 121),
            self.rebalance_when_in_the_market
        )

    def generate_signal(self):
        if(len(self.sig) > 0):
            currentDate = self.UtcTime.date()
            signalDate =  datetime.strptime(self.sig[0]["date"], '%Y-%m-%d').date()
               
            if currentDate == signalDate:
                self.be_in = self.sig[0]["be_in"]
                self.sig = self.sig[1:]

    def rebalance_when_out_of_the_market(self):
        # Swap to 'out' assets if applicable
        if not self.be_in:
            self.wt = {**dict.fromkeys(self.HLD_IN, 0), **self.HLD_OUT}
            # Only trade when changing from in to out
            for sec, weight in self.wt.items():
                cond1 = (self.Portfolio[sec].Quantity > 0) and (weight == 0)
                cond2 = (self.Portfolio[sec].Quantity == 0) and (weight > 0)
                if cond1 or cond2:
                    self.SetHoldings(sec, weight)

    def rebalance_when_in_the_market(self):
        # Swap to 'in' assets if applicable
        if self.be_in:
            self.wt = {**self.HLD_IN, **dict.fromkeys(self.HLD_OUT, 0)}
            # Only trade when changing from out to in
            for sec, weight in self.wt.items():
                cond1 = (self.Portfolio[sec].Quantity > 0) and (weight == 0)
                cond2 = (self.Portfolio[sec].Quantity == 0) and (weight > 0)
                if cond1 or cond2:
                    self.SetHoldings(sec, weight)