| Overall Statistics |
|
Total Trades 69 Average Win 8.44% Average Loss -2.45% Compounding Annual Return 16.620% Drawdown 23.200% Expectancy 1.349 Net Profit 183.166% Sharpe Ratio 0.832 Loss Rate 47% Win Rate 53% Profit-Loss Ratio 3.44 Alpha 0.163 Beta -0.255 Annual Standard Deviation 0.168 Annual Variance 0.028 Information Ratio 0.21 Tracking Error 0.231 Treynor Ratio -0.547 Total Fees $493.50 |
"""
Probabilistic Momentum
Ispired by:
Antonacci's Global Rotation: where best asset is selected on a monthly basis.
e.g. https://github.com/QuantConnect/Lean/blob/master/Algorithm.Python/ETFGlobalRotationAlgorithm.py
Improvement:
The reason for the periodic (monthly) rebalancing in Antonacci's algo is to avoid market whipsaw/noise.
A better and simple way is to have daily rebalancings, but filtering signal for statistical significance by using
the t-stat for the difference between the mean of two assets (standard statistics)
It is good for two uncorrelated assets like MDY and EDV (and self.pr_lvl=0.6), better than Antonacci's algo
but it needs further work for more assets: just naively increasing the prob level (self.pr_lvl) does not seem to cut it.
"""
import numpy as np
import pandas as pd
from datetime import datetime, timedelta
from scipy.stats import t # t.sf(x, dof), (sf: survival function - i.e. Pr(X>x) of t-dist with 1 tail)
class ProbabilisticMomentum(QCAlgorithm):
def __init__(self):
self.symbols = [
"MDY", # US S&P mid cap 400
# "IEV", # iShares S&P europe 350
# "EEM", # iShared MSCI emerging markets
# "ILF", # iShares S&P latin america
# "EPP", # iShared MSCI Pacific ex-Japan
"EDV", # Vanguard 25yr US treasuries
# "SHY" # Barclays short-datauration US treasuries
]
self.back_period = 21*3 + 1 # i.e. 3 months
# critical level for switching (for #2 assets = 0.6, higher for more assets...)
self.pr_lvl = 0.6
self.target = None
def Initialize(self):
self.SetCash(100000)
self.SetStartDate(2011,3,1)
self.SetEndDate(datetime.now().date() - timedelta(1))
self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage,
AccountType.Margin)
# register and replace 'tkr symbol' with 'tkr object'
for i, tkr in enumerate(self.symbols):
self.symbols[i] = self.AddEquity(tkr, Resolution.Daily).Symbol
self.Schedule.On(self.DateRules.EveryDay(self.symbols[0]),
self.TimeRules.AfterMarketOpen(self.symbols[0], 1), Action(self.rebalance))
def rebalance(self):
self.select()
if self.target is None: return
# check if already in
if self.Portfolio[self.target].Quantity != 0: return
# switch
self.Liquidate()
self.SetHoldings(self.target,1.)
self.Debug("self.Time: %s -- self.target: %s and self.prob: %s" %(str(self.Time), str(self.target), str(self.max_prob)) )
def select(self):
# get daily returns
self.price = self.History(self.symbols, self.back_period, Resolution.Daily)["close"].unstack(level=0) # .dropna(axis=1)
daily_rtrn = self.price.pct_change().dropna() # or: np.log(self.price / self.price.shift(1)).dropna()
_n = len(daily_rtrn.index); sqrt_n = np.sqrt(_n)
# reset
self.max_prob = -1.; target = None
# TODO: make it more Pythonic!
for i, tkr_1 in enumerate(self.symbols):
for j, tkr_2 in enumerate(self.symbols):
if i < j: # upper part matrix(tkr_1, tkr_2); (n^2 - n) / 2 operations
rtrn_diff = daily_rtrn[tkr_1.Value] - daily_rtrn[tkr_2.Value]
x = np.mean(rtrn_diff) / np.std(rtrn_diff) * np.sqrt(_n) # t_stat = avg(diff)/(std(diff)/sqrt(n))
if x > 0: # x>0 -> tkr_1 better than tkr_2
prob = (1 - t.sf(x, _n-1)) # T-dist cumulative probability: Prob(X<x)
if prob > self.max_prob:
self.max_prob = prob; target = tkr_1
else: # x<0 -> tkr_2 better than tkr_1 (reverse)
prob = (1 - t.sf(-x, _n-1)) # NB: -x
if prob > self.max_prob:
self.max_prob = prob; target = tkr_2 # NB: tkr_2
# check prob above min level
if self.max_prob > self.pr_lvl: self.target = target