| Overall Statistics |
|
Total Trades 10001 Average Win 0.02% Average Loss -0.02% Compounding Annual Return 9.562% Drawdown 3.100% Expectancy 0.215 Net Profit 21.305% Sharpe Ratio 1.963 Loss Rate 44% Win Rate 56% Profit-Loss Ratio 1.17 Alpha 0.085 Beta -0.1 Annual Standard Deviation 0.038 Annual Variance 0.001 Information Ratio -0.157 Tracking Error 0.136 Treynor Ratio -0.753 Total Fees $0.00 |
import json
from datetime import datetime, timedelta
# Copies logic from https://www.quantconnect.com/tutorials/strategy-library/fundamental-factor-long-short-strategy
# Algo storing - https://www.quantconnect.com/docs/algorithm-framework/algorithm-scoring
class ChartMillBackTester(QCAlgorithm):
def Initialize(self):
self.SetCash(1000*100) # Are't we rich? :D
self.SetStartDate(2017, 1, 1)
self.SetEndDate(2019, 9, 28) # if not specified, the Backtesting EndDate would be today
self.MyPortfolio = {} # {:ticker => date_invested_on}
self.MaxHoldingPeriodInDays = 5
self.MaxQuanityToHoldPerTicker = 10
# # Long tickers
# self.LongScannerData = json.loads(self.Download("https://firebasestorage.googleapis.com/v0/b/red-zombies.appspot.com/o/chartmill_scans%2Flong_tickers_technical_breakout_2017-2019.json?alt=media"))
# self.Debug("[LongScannerData] : " + str(self.LongScannerData))
# # Short tickers
# self.ShortScannerData = json.loads(self.Download("https://firebasestorage.googleapis.com/v0/b/red-zombies.appspot.com/o/chartmill_scans%2Fshort_tickers_scanned.json?alt=media"))
# self.Debug("[ShortScannerData] : " + str(self.ShortScannerData))
#scannerDataUrl = "https://firebasestorage.googleapis.com/v0/b/red-zombies.appspot.com/o/chartmill_scans%2Flong_tickers_technical_breakout_2017-2019.json?alt=media"
#scannerDataUrl = "https://firebasestorage.googleapis.com/v0/b/red-zombies.appspot.com/o/chartmill_scans%2Fbull_bear_flags__2017-01-01__2019-09-28.json?alt=media"
scannerDataUrl = "https://firebasestorage.googleapis.com/v0/b/red-zombies.appspot.com/o/chartmill_scans%2Ftechnical_breakout__strong_downtrend__2017-01-01__2019-10-03_new.json?alt=media"
scannerData = json.loads(self.Download(scannerDataUrl)) # {:long_tickers => {}, :short_tickers => {}}
self.LongScannerData = scannerData["long_tickers"] # {:date => [ticker, ...], ...}
self.ShortScannerData = scannerData["short_tickers"] # {:date => [ticker, ...], ...}
self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
self.SetBenchmark("SPY")
self.UniverseSettings.Resolution = Resolution.Daily
#self.AddUniverse(self.ChatMillScannerData_CoarseSelectionFunction,self.ChatMillScannerData_FineSelectionFunction)
for date in self.LongScannerData:
try:
tickers = (self.LongScannerData[date] + self.ShortScannerData[date])
for ticker in tickers:
self.AddEquity(ticker, Resolution.Daily)
# Constant fee model - https://www.quantconnect.com/docs/algorithm-reference/reality-modelling#Reality-Modelling
self.Securities[ticker].FeeModel = ConstantFeeModel(0) # Let's assume for a sec that there's no tx fee! :)
except:
pass
# Schedule the rebalance function to execute at the begining of each day
# https://www.quantconnect.com/docs/algorithm-reference/scheduled-events
self.Schedule.On(self.DateRules.EveryDay("SPY"), self.TimeRules.AfterMarketOpen(self.spy,5), Action(self.LongShortStrategy))
# def ChatMillScannerData_CoarseSelectionFunction(self, coarse):
# #self.Debug("Inside ChatMillScannerData_CoarseSelectionFunction")
# return self.GetSymbolsFromChartMill()
# def ChatMillScannerData_FineSelectionFunction(self, fine):
# #self.Debug("Inside ChatMillScannerData_FineSelectionFunction")
# return self.GetSymbolsFromChartMill()
# Returns [Symbol, ...]
# longOrShort => "long" / "short"
def GetSymbolsFromChartMill(self, longOrShort):
date = str(self.Time).split(" ")[0]
symbols = []
symbolsForDate = []
try:
symbolsForDate = self.LongScannerData[date] if (longOrShort == "long") else self.ShortScannerData[date]
except:
self.Debug("Error in fetching symbols for date : " + date)
for ticker in symbolsForDate:
try:
symbols.append(Symbol.Create(ticker, SecurityType.Equity, Market.USA))
except:
self.Debug("Error in creating symbol : " + ticker)
#self.Debug(str(symbols))
return symbols
def OnData(self, data):
# This function is not needed as we rely on the self.Schedule function to trigger the algo
pass
# https://www.quantconnect.com/docs/algorithm-reference/securities-and-portfolio
# https://www.quantconnect.com/docs/key-concepts/security-identifiers
# Algo : if invested, liquidate after N days. Else, go long or short!
# TODO : if benchmark is below 90 ema, give more weightage to short. else, to longs
def LongShortStrategy(self):
#self.Debug("Inside Rebalance")
longSymbols = self.GetSymbolsFromChartMill("long")
shortSymbols = self.GetSymbolsFromChartMill("short")
# Go Long on long symbols
for symbol in longSymbols:
ticker = str(symbol)
try:
# If not bought the ticker already, buy it
if not self.Securities[ticker].Invested:
self.Order(ticker, self.MaxQuanityToHoldPerTicker)
# Update the ticker's last invested date in MyPortfolio
self.MyPortfolio[ticker] = self.Time
except Exception as error:
self.Debug("E/Long[" + ticker + "] ==> " + str(error))
# # Go Short on short symbols
for symbol in shortSymbols:
ticker = str(symbol)
try:
# If not bought the ticker already, buy it
if not self.Securities[ticker].Invested:
self.Order(ticker, -1 * self.MaxQuanityToHoldPerTicker)
# Update the ticker's last invested date in MyPortfolio
self.MyPortfolio[ticker] = self.Time
except Exception as error:
self.Debug("E/Short[" + ticker + "] ==> " + str(error))
# Liquidating
for ticker in list(self.MyPortfolio): # we need to use list() because we pop https://stackoverflow.com/a/11941855/440362
try:
# Buy the ticker and update it's holdings in MyPortfolio
today = self.Time
investedDate = self.MyPortfolio[ticker]
tickerHoldingPeriod = abs((today - investedDate).days)
if tickerHoldingPeriod > self.MaxHoldingPeriodInDays:
# liquidate the ticker and remove it from holding tracking
#self.Debug("Liquidating ticker : " + ticker)
self.Liquidate(ticker)
self.MyPortfolio.pop(ticker)
except Exception as error:
self.Debug("E/Liquidate[" + ticker + "] ==> " + str(error))
# Used to create some space at the bottom, ignore
def Foo(self):
pass