| Overall Statistics |
|
Total Trades 690 Average Win 0.03% Average Loss -0.02% Compounding Annual Return 45.151% Drawdown 1.200% Expectancy 0.314 Net Profit 3.110% Sharpe Ratio 3.755 Probabilistic Sharpe Ratio 82.145% Loss Rate 41% Win Rate 59% Profit-Loss Ratio 1.22 Alpha 0.29 Beta -0.759 Annual Standard Deviation 0.08 Annual Variance 0.006 Information Ratio 2.475 Tracking Error 0.127 Treynor Ratio -0.395 Total Fees $1911.61 |
import pandas as pd
import numpy as np
from collections import deque
class EmaCrossUniverseSelectionAlgorithm(QCAlgorithm):
def Initialize(self):
'''
Initialise the data and resolution required, as well as the cash and start-end dates for your algorithm.
All algorithms must initialized.
'''
self.SetStartDate(2017,6,1) #Set Start Date
self.SetEndDate(2017,6,30) #Set End Date
self.SetCash(1000000) #Set Strategy Cash
# ADJUST RESOLUTION TO CHANGE ORDER TYPES
self.UniverseSettings.Resolution = Resolution.Daily
self.UniverseSettings.Leverage = 2
self.coarse_count = 100
self.averages = {}
self.TOLERANCE = 0.025 # +/- target weight
# this add universe method accepts two parameters:
# - coarse selection function: accepts an IEnumerable<CoarseFundamental> and returns an IEnumerable<Symbol>
self.AddUniverse(self.CoarseSelectionFunction)
self.symbols = None
self.spy = self.AddEquity("SPY", Resolution.Minute).Symbol
self.ema_fast_window = 2
self.ema_slow_window = 10
self.LOOKBACK = 11 # 60
self.equity_arr = deque([0], maxlen=252)
#======================================================================
# schedule rebalance
# make buy list
self.Schedule.On(
#self.DateRules.EveryDay(self.spy),
#self.DateRules.MonthStart(self.spy),
self.DateRules.WeekStart(self.spy),
self.TimeRules.AfterMarketOpen(self.spy, 30),
Action(self.rebalance),
)
#======================================================================
# REF: https://www.quantconnect.com/docs/algorithm-reference/universes#Universes-Coarse-Universe-Selection
def CoarseSelectionFunction(self, coarse):
"""
This is the coarse selection function which filters the stocks.
This may be split between coarse and fine selection but does
not seem necessary at this point.
See REF: https://www.quantconnect.com/docs/algorithm-reference/universes#Universes-Fundamentals-Selection
"""
# -------------------------------------------------
# only trade stocks that have fundamental data
# price greater than $5 USD and
# Dollar Volume greater than $10MM USD
# Can be adjusted however you want, see the above reference.
filterCoarse = [x for x in coarse if x.HasFundamentalData and x.Price > 5 and x.DollarVolume > 10000000]
# -------------------------------------------------
# Add SymbolData to self.averages dict keyed by Symbol
# Call history for new symbols to warm up the SymbolData internals
tmp_symbols = [cf.Symbol for cf in filterCoarse if cf.Symbol not in self.averages]
# need more than longest indicator data to compute rolling
history = self.History(tmp_symbols, self.LOOKBACK, Resolution.Daily)
if not history.empty:
history = history.close.unstack(level=0)
for symbol in tmp_symbols:
if str(symbol) in history.columns:
self.averages[symbol] = SymbolData(self, history[symbol])
# Updates the SymbolData object with current EOD price
for cf in filterCoarse:
avg = self.averages.get(cf.Symbol, None)
if avg is not None:
avg.Update(cf) # add prices by passing the cf object
# --------------------------------------------------
# sort dict by mmi metric and convert to dataframe
mmiDict = {symbol:value.Metric for symbol, value in self.averages.items()}
# want to get the lowest values so remove the reveres=True flag
# sort and convert to df
sorted_by_metric = pd.DataFrame(
sorted(mmiDict.items(), key=lambda x: x[1]), columns=['symbol','metric']
).iloc[:self.coarse_count]
# we need the Symbol.Value to compare symbols
sorted_by_metric_symbols = [x.Value for x in sorted_by_metric["symbol"]]
#self.Log(f"[{self.Time}] sorted by metric | shape {sorted_by_metric.shape}:\n{sorted_by_metric}")
# --------------------------------------------------
# extract symbols that are in uptrend
#ema_uptrend = list(filter(lambda x: x.is_uptrend, self.averages.values()))
# self.Debug(f"[{self.Time}] uptrend syms: {len(ema_uptrend_sym)} |\n {ema_uptrend_sym}")
# --------------------------------------------------
# extract symbols from uptrending list
# This is the official symbol list. It contains the QC
# symbol object
#symbols = [x.symbol for x in ema_uptrend if x.symbol.Value in sorted_by_metric_symbols]
symbols = [x for x in sorted_by_metric["symbol"]]
self.Debug(f"[{self.Time}] number of selected symbols: {len(symbols)}")
#if len(symbols) > 0:
# self.Debug(f"{self.Time} type: {type(symbols)} |\n{symbols}")
# --------------------------------------------------
# assign symbols to global parameter so rebalancing
# can be handled in OnSecuritiesChanged() function
self.symbols = symbols
return symbols # we need to return only the symbol objects
#======================================================================
# scheduled rebalance
def update_portfolio_value(self):
"""
Update portfolio equity value tracked by self.equity_arr by appending portfolio value to list
"""
self.equity_arr.append(self.Portfolio.TotalPortfolioValue)
return
def check_current_weight(self, symbol):
"""
Check current symbol portfolio weight
:param symbol: str
:return current_weight: float
"""
P = self.Portfolio
current_weight = float(P[symbol].HoldingsValue) / float(P.TotalPortfolioValue)
return current_weight
def rebalance(self):
if self.symbols is None:
return
self.Debug(f"[{self.Time}] init rebalance")
# columns = []
prices_list = []
for k,v in self.averages.items():
tmp_hist = self.averages[k].Window
prices_list.append(tmp_hist)
self.Log(f"{self.Time} series list:\n{str(prices_list[0])}")
self.Log(f"{self.Time} series list:\n{str(prices_list)}")
prices = pd.concat(prices_list, axis=1).dropna()
returns = np.log(prices).diff().dropna()
self.Log(f"{self.Time} returns:\n{returns.head()}")
#self.update_prices() # update prices
self.update_portfolio_value() # update portfolio value
for sec in self.symbols:
# get current weights
current_weight = self.check_current_weight(sec)
target_weight = 1/len(self.symbols) # * returns[sec].iloc[-1]
# if current weights outside of tolerance send new orders
tol = self.TOLERANCE * target_weight
lower_bound = target_weight - tol
upper_bound = target_weight + tol
if (current_weight < lower_bound) or (current_weight > upper_bound):
self.SetHoldings(sec, target_weight)
return
#======================================================================
# this event fires whenever we have changes to our universe
def OnSecuritiesChanged(self, changes):
# liquidate removed securities
for security in changes.RemovedSecurities:
if security.Invested:
self.Liquidate(security.Symbol)
def OnData(self, data):
pass
#======================================================================
class SymbolData(object):
"""
This is the symbol data class. I have updated it to be able to
take advantage of what the MMI indicator is used for and that is
trending securities. In this example I implemented a simple
short term EMA crossover strategy, but it is easy enough to
change.
"""
def __init__(self, qccontext, history):
self.FastPeriod= qccontext.ema_fast_window #30
self.SlowPeriod= qccontext.ema_slow_window #90
#self.MainPeriod=90
self.Window = history
self.EndTime = history.index[-1]
# ema crossover attributes
self.is_uptrend = False
#self.ema_fast = ExponentialMovingAverage(8)
#self.ema_slow = ExponentialMovingAverage(21)
self.tolerance = 1.0 # QC example is 1.01
# other key attributes
self.dollar_volume = None
self.symbol = None
self.qc = qccontext
def Update(self, cf):
"""
Update the indicators for symbol.
This function updates the symbol and dollar volume attribute
which are needed for filtering, and also updates the history
dataframe and the EMA crossover indicators and attributes.
"""
# assign symbol attribute and dollar volume
self.symbol = cf.Symbol
self.dollar_volume = cf.DollarVolume
if self.EndTime >= cf.EndTime:
return
# Convert CoarseFundamental to pandas.Series
to_append = pd.Series([cf.AdjustedPrice], name=str(cf.Symbol),
index=pd.Index([cf.EndTime], name='time'))
self.Window.drop(self.Window.index[0], inplace=True)
self.Window = pd.concat([self.Window, to_append], ignore_index=True)
self.EndTime = cf.EndTime
"""# after warmup update ema indicators with current values to determine
# if they are uptrending
if self.ema_fast.Update(cf.EndTime, cf.AdjustedPrice) and self.ema_slow.Update(cf.EndTime, cf.AdjustedPrice):
fast = self.ema_fast.Current.Value
slow = self.ema_slow.Current.Value
# self.qc.Debug(f"[{self.qc.Time}] symbol: {cf.Symbol} | fast: {round(self.ema_fast.Current.Value,2)} | slow: {round(self.ema_slow.Current.Value,2)}")
self.is_uptrend = fast > slow * self.tolerance"""
@property
def Metric(self):
"""
This computes the MMI value for the security.
"""
# create returns to feed indicator
returns = self.Window.pct_change().dropna()
# compute the metric
x = returns.rolling(self.FastPeriod).mean().iloc[-1]
return x # should be single numeric