Overall Statistics |
Total Trades 26 Average Win 0.33% Average Loss -0.09% Compounding Annual Return 98.213% Drawdown 0.800% Expectancy 0.326 Net Profit 1.447% Sharpe Ratio 6.28 Loss Rate 71% Win Rate 29% Profit-Loss Ratio 3.55 Alpha -0.3 Beta 1.528 Annual Standard Deviation 0.097 Annual Variance 0.009 Information Ratio 0.213 Tracking Error 0.065 Treynor Ratio 0.398 Total Fees $77.53 |
# # Compare Second/Minute resampled data vs Consolidated data # # Sets up a helper class SymbolData that allows different resample rates per symbol # self.assets holds a list of SymbolData objects to support multiple symbols # Has optional delegates for OnDataConsolidated and OnSmaUpdated # Call pandas.resample when OnDataConsolidated fires # Uses a simple moving average indicator to Trade # converted to use 5 minute sma bars # # Cleans up import * which is discouraged in python # # You can view the QCAlgorithm base class on Github: # https://github.com/QuantConnect/Lean/tree/master/Algorithm # # LICENSE is Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 # # https://github.com/QuantConnect/Lean/blob/dc27c4e3d655bd40da1f9d8b9b8ef850a2f10ee0/ # Algorithm.Python/RollingWindowAlgorithm.py # # Modifications copyright (C) 2017 John Glossner # QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. # Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Is entire universe a public method of QCAlgorithm??? # https://www.quantconnect.com/lean/documentation/topic101.html from QuantConnect import SecurityType, DataNormalizationMode, SymbolCache, Symbol, Chart from QuantConnect.Algorithm import QCAlgorithm from QuantConnect.Data import SubscriptionManager from QuantConnect.Data.Market import TradeBar from QuantConnect.Data.Consolidators import TradeBarConsolidator from QuantConnect.Indicators import SimpleMovingAverage import numpy as np import pandas as pd from datetime import datetime, timedelta from decimal import Decimal class ResampleAlgorithm(QCAlgorithm): def Initialize(self): self.verbose = True #For debugging # Backtest setup. Ignored in live trading self.SetCash(100000) self.startDate = self.SetStartDate(2017,7,7) self.endDate = self.SetEndDate(2017,7,14) #self.endDate = self.SetEndDate(2017,10,2) # Add assets with 5 minute consolidation # self.symbols = [SymbolData(self, 'FB', resolution=Resolution.Minute, mode=DataNormalizationMode.Adjusted, # consolidated_window_length=10, consolidation_period=timedelta(minutes=5), # sma_length=10)] # Can have multiple symbols with different parameters # FB=5min Consolidator SPY=10min Consolidator self.symbols = [SymbolData(self, 'FB', resolution=Resolution.Minute, mode=DataNormalizationMode.Adjusted, consolidated_window_length=10, consolidation_period=timedelta(minutes=5), sma_length=10), SymbolData(self, 'SPY', resolution=Resolution.Minute, mode=DataNormalizationMode.Adjusted, consolidated_window_length=15, consolidation_period=timedelta(minutes=10), sma_length=20)] #Schedule trading every 5 minutes self.Schedule.On(self.DateRules.EveryDay(), \ self.TimeRules.Every(timedelta(minutes=5)), \ Action(self.Trade)) return def OnData(self, data): '''OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here.''' pass def Resample(self, symbol_name, bars=3, frequency='5T'): ''' Get historical bars and resample them to a higher frequency Uses pandas.resample Consolidators in QC use closed=left label=right QC 1min bars appear to use closed=left label=left (based on resampling 1sec data) To get QC 1m bars to match Quantopian 1min bars resample QC 1sec data with closed=right label=left To get Quantopian data to match my broker's 5min data resample Q 1min data with closed=right label=left To get QC 5min data to match my broker's 5min data resample QC 1min data with closed=left label=left Note: The algorithm doesn't always work for open. It seems to work for close. I didn't check high/low/volumme ''' try: hist = self.History( [symbol_name], bars*5 ) #use list to get back a pandas datafram self.pretty_print_df(hist, -6) how = { 'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last', 'volume': 'sum'} df = hist.resample(frequency, closed='right', label='left',level=1).agg(how).dropna() self.Log('after resample') self.pretty_print_df(df, -6) return except Exception as e: self.Debug('@@@@@@@@@@@@@@@@@@@ EXCEPTION: Resample() @@@@@@@@@@@@@@@@@@@@@') self.Debug('Exception is %s' %e) #Invalid resample parameter return None def Trade(self): ''' Determine if trade should be placed If current SMA > previous SMA: buy If current SMA < previous SMA: sell ''' for symbol in self.symbols: if not (symbol.consolidated_window_bars.IsReady and symbol.sma_window.IsReady): continue if not self.Securities[symbol.symbol_name].Exchange.ExchangeOpen: continue current_bar = symbol.consolidated_window_bars[0] previous_bar = symbol.consolidated_window_bars[1] #list is appended current_sma = symbol.sma_window[0] previous_sma = symbol.sma_window[symbol.sma_window.Count-1] #Update plots self.Plot(symbol.symbol_name + " Chart", "SMA", current_sma.Value); if self.Portfolio[symbol.symbol_name].IsLong: self.Plot(symbol.symbol_name + " Chart", "LongShort", Decimal(100.00)); if self.Portfolio[symbol.symbol_name].IsShort: self.Plot(symbol.symbol_name + " Chart", "LongShort", Decimal(-100.00)); #Trade if current_sma.Value > previous_sma.Value: if self.Portfolio[symbol.symbol_name].IsShort or not self.Portfolio[symbol.symbol_name].Invested: self.SetHoldings(symbol.symbol_name, 1.0/len(self.symbols), liquidateExistingHoldings = True) if self.verbose: self.Log(' TRADE LONG ' + symbol.symbol_name + '\n' \ + ' prev bar time= ' + str(previous_bar.Time) \ + ' prev price= ' + '{0:.3f}'.format(previous_bar.Close) \ + ' curr bar time= ' + str(current_bar.Time) \ + ' curr price= ' + '{0:.3f}'.format(current_bar.Close) ) self.Log( ' prev sma time= ' + str(previous_sma.Time) \ + ' past sma= ' + '{0:.3f}'.format(previous_sma.Value) \ + ' curr sma time= ' + str(current_sma.Time) \ + ' curr sma= ' + '{0:.3f}'.format(current_sma.Value) ) elif current_sma.Value < previous_sma.Value: if self.Portfolio[symbol.symbol_name].IsLong or not self.Portfolio[symbol.symbol_name].Invested: self.SetHoldings(symbol.symbol_name, -1.0/len(self.symbols), liquidateExistingHoldings = True) if self.verbose: self.Log(' TRADE SHORT ' + symbol.symbol_name + '\n' \ + ' prev bar time= ' + str(previous_bar.Time) \ + ' prev price= ' + '{0:.3f}'.format(previous_bar.Close) \ + ' curr bar time= ' + str(current_bar.Time) \ + ' curr price= ' + '{0:.3f}'.format(current_bar.Close) ) self.Log( ' prev sma time= ' + str(previous_sma.Time) \ + ' past sma= ' + '{0:.3f}'.format(previous_sma.Value) \ + ' curr sma time= ' + str(current_sma.Time) \ + ' curr sma= ' + '{0:.3f}'.format(current_sma.Value) ) def pretty_print_df(self, df, length=5): ''' Because what comes out of self.Log( str(df.tail())) looks like crap ''' if not len(df): return self.Log( ' '.join( df.columns.values )) if length > 0: if len(df) > length: count = 0 for index, row in df.iterrows(): if count > length: return formatted_row = ["%.2f" % member for member in row] self.Log( str(index) + ' ' + ' '.join(str(e) for e in formatted_row) ) count += 1 else: for index, row in df.iterrows(): formatted_row = ["%.2f" % member for member in row] self.Log( str(index) + ' ' + ' '.join(str(e) for e in formatted_row) ) if length < 0: count = abs(length) if len(df) > count: for index, row in df[::-1].iterrows(): #Reverse view into df if count ==0: return formatted_row = ["%.2f" % member for member in row] self.Log( str(index) + ' ' + ' '.join(str(e) for e in formatted_row) ) count -= 1 else: for index, row in df[::-1].iterrows(): #Reverse view into df if count ==0: return formatted_row = ["%.2f" % member for member in row] self.Log( str(index) + ' ' + ' '.join(str(e) for e in formatted_row) ) class SymbolData(object): ''' Packages Symbols to allow list like operations SymbolData should only be called from a class that inherits from QCAlgorithm ''' def __init__(self, qca, symbol_name, resolution = Resolution.Minute, mode = DataNormalizationMode.Adjusted, #Raw, Adjusted, SplitAdjusted, TotalReturn security_type=SecurityType.Equity, consolidated_window_length = None, consolidation_period = timedelta(minutes=5), sma_length = None ): self.qca = qca #QCAlgorithm reference #Add equity to original qca self.symbol_name = symbol_name self.security_type = security_type self.symbol_object = qca.AddEquity(symbol_name, resolution) #QCAlgorithm public method self.symbol_object.SetDataNormalizationMode(mode) #Set up plots # Chart - Master Container for the Chart: stockPlot = Chart(symbol_name + " Chart") # On the Trade Plotter Chart we want 3 series: trades and price: stockPlot.AddSeries(Series("LongShort", SeriesType.Line, 0)) stockPlot.AddSeries(Series("Price", SeriesType.Line, 0)) stockPlot.AddSeries(Series("SMA", SeriesType.Line, 0)) qca.AddChart(stockPlot) #Set up consolidated tradebar window to keep historical data #Only setup if a window length given if consolidated_window_length: consolidator = TradeBarConsolidator(consolidation_period) consolidator.DataConsolidated += self.OnDataConsolidated qca.SubscriptionManager.AddConsolidator(symbol_name, consolidator) self.consolidation_period = consolidation_period self.consolidated_window_bars = RollingWindow[TradeBar](consolidated_window_length) #Creates an indicator and add to a rolling window when it is updated #Only setup if sma_length given if sma_length: #Why we need a symbol and not the string name is a mystery symbol = SymbolCache.GetSymbol(symbol_name) ind_name = qca.CreateIndicatorName(symbol, "SMA" + str(sma_length), resolution ) #inherited from QCAlgorithm self.sma = SimpleMovingAverage( ind_name, sma_length) sma_consolidator = TradeBarConsolidator(consolidation_period) #Need a different delegate called qca.SubscriptionManager.AddConsolidator(symbol_name, sma_consolidator) qca.RegisterIndicator(symbol_name, self.sma, sma_consolidator) self.sma.Updated += self.OnSmaUpdated #Called when sma updated self.sma_window = RollingWindow[IndicatorDataPoint](sma_length) #store the last N sma values return def OnDataConsolidated(self, sender, bar): ''' delegate to update consolidate bars rolling window ''' if self.qca.verbose: self.qca.Log(' CONSOLIDATOR ' + self.symbol_name + ' ' + str(self.qca.Time) \ + ' open= ' + '{0:.3f}'.format(bar.Open) \ + ' close= ' + '{0:.3f}'.format(bar.Close) \ + ' high= ' + '{0:.3f}'.format(bar.High) \ + ' low= ' + '{0:.3f}'.format(bar.Low) \ ) # Add TradeBar in rollling window self.consolidated_window_bars.Add(bar) self.qca.Resample(self.symbol_name) return def OnSmaUpdated(self, sender, updated): ''' Delegate to update SMA rolling window ''' if self.qca.verbose: self.qca.Log(' SMA ' + self.symbol_name + ' ' + str(self.qca.Time) \ + ' updated sma= ' + str(updated) + ' type= ' + str( type(updated) ) \ + ' self.sma= ' + str(self.sma) + ' type self.sma= ' + str(type(self.sma))) self.sma_window.Add(updated) return