| Overall Statistics |
|
Total Trades 11 Average Win 3.61% Average Loss -7.06% Compounding Annual Return -40.482% Drawdown 37.000% Expectancy -0.568 Net Profit -34.717% Sharpe Ratio -0.925 Probabilistic Sharpe Ratio 1.782% Loss Rate 71% Win Rate 29% Profit-Loss Ratio 0.51 Alpha -0.281 Beta -0.225 Annual Standard Deviation 0.335 Annual Variance 0.112 Information Ratio -0.847 Tracking Error 0.517 Treynor Ratio 1.375 Total Fees $15.41 |
# 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.
from clr import AddReference
AddReference("QuantConnect.Common")
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Algorithm.Framework")
AddReference("QuantConnect.Indicators")
from QuantConnect import *
from QuantConnect.Indicators import *
from QuantConnect.Algorithm import *
from QuantConnect.Algorithm.Framework import *
from QuantConnect.Algorithm.Framework.Alphas import *
import pandas as pd
import numpy as np
from datetime import timedelta
from collections import deque
from sadf import get_sadf
class ExuberAlphaModel(AlphaModel):
def __init__(self, sadf_period, resolution=Resolution.Daily):
self.sadf_period = sadf_period
self.resolution = resolution
self.insightPeriod = Time.Multiply(Extensions.ToTimeSpan(resolution), sadf_period)
self.sadfDict = {}
self.SecData = {}
self.selected = {}
resolutionString = Extensions.GetEnumString(resolution, Resolution)
self.Name = '{}({},{})'.format(self.__class__.__name__, sadf_period, resolutionString)
def Update(self, algorithm, data):
insights = []
for symbol, sadf in self.sadfDict.items():
if sadf.Value <= 1:
insights.append(Insight.Price(symbol, self.insightPeriod, InsightDirection.Up))
if sadf.Value > 1:
insights.append(Insight.Price(symbol, self.insightPeriod, InsightDirection.Down))
algorithm.Plot("SADF", str(symbol), sadf.Value)
return insights
def OnSecuritiesChanged(self, algorithm, changes):
for security in changes.AddedSecurities:
self.sadfDict[security.Symbol] = SadfIndicator('sadf', self.sadf_period, algorithm, security)
for security in changes.RemovedSecurities:
symbol = security.Symbol
# if symbol in self.SecData:
# # Remove consolidator for removed securities
# algorithm.SubscriptionManager.RemoveConsolidator(symbol, self.SecData[symbol].consolidator)
# self.SecData.pop(symbol, None)
class SadfIndicator(PythonIndicator):
def __init__(self, name, period, algorithm, security):
self.period = period
self.Name = name
self.Time = datetime.min
self.Value = 0
# self.IsReady = False
self.queue = deque(maxlen=period)
self.queueTime = deque(maxlen=period)
self.queuePe = deque(maxlen=period)
self.CurrentReturn = 0
self.algorithm = algorithm
self.security = security
self.symbol = security.Symbol
# register indicator
algorithm.RegisterIndicator(self.symbol, self, Resolution.Daily)
# Initialize MOM indicator with historical data
history = algorithm.History(self.symbol, period + 1, Resolution.Daily)
if history.empty:
return
for time, row in history.loc[self.symbol].iterrows():
tb = TradeBar(time, self.symbol, row.open, row.high, row.low, row.close, row.volume)
self.Update(tb)
def sadf_last(self, close):
sadf_linear = get_sadf(
close,
min_length=50,
add_const=True,
model='linear',
# phi=0.5,
lags=1)
if len(sadf_linear) > 0:
last_value = sadf_linear.values[-1].item()
else:
last_value = 0
return last_value
def Update(self, input):
pe_ratio = self.security.Fundamentals.ValuationRatios.NormalizedPERatio
self.algorithm.Plot('Normalized PE', 'Ratio', pe_ratio)
self.queue.appendleft(input.Price)
self.queueTime.appendleft(input.EndTime)
self.queuePe.appendleft(pe_ratio)
self.Time = input.EndTime
if len(self.queue) >= self.period: # > ==> >=
close_ = pd.Series(self.queue, index=self.queueTime).rename('close').sort_index()
pe_ = pd.Series(self.queuePe, index=self.queueTime).rename('pe').sort_index()
self.CurrentReturn = close_.pct_change(periods=1)[-1]
self.PreviousReturn = close_.pct_change(periods=1)[-2]
self.Value = self.sadf_last(close_)
self.algorithm.Plot("SADF", "Value", self.Value)
self.ValuePe = self.sadf_last(close_)
count = len(self.queue)
# self.IsReady = count == self.queue.maxlen
return count == self.queue.maxlen# Copyright 2019, Hudson and Thames Quantitative Research
# All rights reserved
# Read more: https://github.com/hudson-and-thames/mlfinlab/blob/master/LICENSE.txt
"""
Explosiveness tests: SADF
"""
from typing import Union, Tuple
import pandas as pd
import numpy as np
# pylint: disable=invalid-name
def _get_sadf_at_t(X: pd.DataFrame, y: pd.DataFrame, min_length: int, model: str, phi: float) -> float:
"""
Advances in Financial Machine Learning, Snippet 17.2, page 258.
SADF's Inner Loop (get SADF value at t)
:param X: (pd.DataFrame) Lagged values, constants, trend coefficients
:param y: (pd.DataFrame) Y values (either y or y.diff())
:param min_length: (int) Minimum number of samples needed for estimation
:param model: (str) Either 'linear', 'quadratic', 'sm_poly_1', 'sm_poly_2', 'sm_exp', 'sm_power'
:param phi: (float) Coefficient to penalize large sample lengths when computing SMT, in [0, 1]
:return: (float) SADF statistics for y.index[-1]
"""
start_points, bsadf = range(0, y.shape[0] - min_length + 1), -np.inf
for start in start_points:
y_, X_ = y[start:], X[start:]
b_mean_, b_std_ = get_betas(X_, y_)
if not np.isnan(b_mean_[0]):
b_mean_, b_std_ = b_mean_[0, 0], b_std_[0, 0] ** 0.5
# TODO: Rewrite logic of this module to avoid division by zero
with np.errstate(invalid='ignore'):
all_adf = b_mean_ / b_std_
if model[:2] == 'sm':
all_adf = np.abs(all_adf) / (y.shape[0]**phi)
if all_adf > bsadf:
bsadf = all_adf
return bsadf
def _get_y_x(series: pd.Series, model: str, lags: Union[int, list],
add_const: bool) -> Tuple[pd.DataFrame, pd.DataFrame]:
"""
Advances in Financial Machine Learning, Snippet 17.2, page 258-259.
Preparing The Datasets
:param series: (pd.Series) Series to prepare for test statistics generation (for example log prices)
:param model: (str) Either 'linear', 'quadratic', 'sm_poly_1', 'sm_poly_2', 'sm_exp', 'sm_power'
:param lags: (int or list) Either number of lags to use or array of specified lags
:param add_const: (bool) Flag to add constant
:return: (pd.DataFrame, pd.DataFrame) Prepared y and X for SADF generation
"""
series = pd.DataFrame(series)
series_diff = series.diff().dropna()
x = _lag_df(series_diff, lags).dropna()
x['y_lagged'] = series.shift(1).loc[x.index] # add y_(t-1) column
y = series_diff.loc[x.index]
if add_const is True:
x['const'] = 1
if model == 'linear':
x['trend'] = np.arange(x.shape[0]) # Add t to the model (0, 1, 2, 3, 4, 5, .... t)
beta_column = 'y_lagged' # Column which is used to estimate test beta statistics
elif model == 'quadratic':
x['trend'] = np.arange(x.shape[0]) # Add t to the model (0, 1, 2, 3, 4, 5, .... t)
x['quad_trend'] = np.arange(x.shape[0]) ** 2 # Add t^2 to the model (0, 1, 4, 9, ....)
beta_column = 'y_lagged' # Column which is used to estimate test beta statistics
elif model == 'sm_poly_1':
y = series.loc[y.index]
x = pd.DataFrame(index=y.index)
x['const'] = 1
x['trend'] = np.arange(x.shape[0])
x['quad_trend'] = np.arange(x.shape[0]) ** 2
beta_column = 'quad_trend'
elif model == 'sm_poly_2':
y = np.log(series.loc[y.index])
x = pd.DataFrame(index=y.index)
x['const'] = 1
x['trend'] = np.arange(x.shape[0])
x['quad_trend'] = np.arange(x.shape[0]) ** 2
beta_column = 'quad_trend'
elif model == 'sm_exp':
y = np.log(series.loc[y.index])
x = pd.DataFrame(index=y.index)
x['const'] = 1
x['trend'] = np.arange(x.shape[0])
beta_column = 'trend'
elif model == 'sm_power':
y = np.log(series.loc[y.index])
x = pd.DataFrame(index=y.index)
x['const'] = 1
# TODO: Rewrite logic of this module to avoid division by zero
with np.errstate(divide='ignore'):
x['log_trend'] = np.log(np.arange(x.shape[0]))
beta_column = 'log_trend'
else:
raise ValueError('Unknown model')
# Move y_lagged column to the front for further extraction
columns = list(x.columns)
columns.insert(0, columns.pop(columns.index(beta_column)))
x = x[columns]
return x, y
def _lag_df(df: pd.DataFrame, lags: Union[int, list]) -> pd.DataFrame:
"""
Advances in Financial Machine Learning, Snipet 17.3, page 259.
Apply Lags to DataFrame
:param df: (int or list) Either number of lags to use or array of specified lags
:param lags: (int or list) Lag(s) to use
:return: (pd.DataFrame) Dataframe with lags
"""
df_lagged = pd.DataFrame()
if isinstance(lags, int):
lags = range(1, lags + 1)
else:
lags = [int(lag) for lag in lags]
for lag in lags:
temp_df = df.shift(lag).copy(deep=True)
temp_df.columns = [str(i) + '_' + str(lag) for i in temp_df.columns]
df_lagged = df_lagged.join(temp_df, how='outer')
return df_lagged
def get_betas(X: pd.DataFrame, y: pd.DataFrame) -> Tuple[np.array, np.array]:
"""
Advances in Financial Machine Learning, Snippet 17.4, page 259.
Fitting The ADF Specification (get beta estimate and estimate variance)
:param X: (pd.DataFrame) Features(factors)
:param y: (pd.DataFrame) Outcomes
:return: (np.array, np.array) Betas and variances of estimates
"""
xy = np.dot(X.T, y)
xx = np.dot(X.T, X)
try:
xx_inv = np.linalg.inv(xx)
except np.linalg.LinAlgError:
return [np.nan], [[np.nan, np.nan]]
b_mean = np.dot(xx_inv, xy)
err = y - np.dot(X, b_mean)
b_var = np.dot(err.T, err) / (X.shape[0] - X.shape[1]) * xx_inv
return b_mean, b_var
def _sadf_outer_loop(X: pd.DataFrame, y: pd.DataFrame, min_length: int, model: str, phi: float,
molecule: list) -> pd.Series:
"""
This function gets SADF for t times from molecule
:param X: (pd.DataFrame) Features(factors)
:param y: (pd.DataFrame) Outcomes
:param min_length: (int) Minimum number of observations
:param model: (str) Either 'linear', 'quadratic', 'sm_poly_1', 'sm_poly_2', 'sm_exp', 'sm_power'
:param phi: (float) Coefficient to penalize large sample lengths when computing SMT, in [0, 1]
:param molecule: (list) Indices to get SADF
:return: (pd.Series) SADF statistics
"""
sadf_series = pd.Series(index=molecule, dtype='float64')
for index in molecule:
X_subset = X.loc[:index].values
y_subset = y.loc[:index].values.reshape(-1, 1)
value = _get_sadf_at_t(X_subset, y_subset, min_length, model, phi)
sadf_series[index] = value
return sadf_series
def get_sadf(series: pd.Series, model: str, lags: Union[int, list], min_length: int, add_const: bool = False,
phi: float = 0, num_threads: int = 8, verbose: bool = True) -> pd.Series:
"""
Advances in Financial Machine Learning, p. 258-259.
Multithread implementation of SADF
SADF fits the ADF regression at each end point t with backwards expanding start points. For the estimation
of SADF(t), the right side of the window is fixed at t. SADF recursively expands the beginning of the sample
up to t - min_length, and returns the sup of this set.
When doing with sub- or super-martingale test, the variance of beta of a weak long-run bubble may be smaller than
one of a strong short-run bubble, hence biasing the method towards long-run bubbles. To correct for this bias,
ADF statistic in samples with large lengths can be penalized with the coefficient phi in [0, 1] such that:
ADF_penalized = ADF / (sample_length ^ phi)
:param series: (pd.Series) Series for which SADF statistics are generated
:param model: (str) Either 'linear', 'quadratic', 'sm_poly_1', 'sm_poly_2', 'sm_exp', 'sm_power'
:param lags: (int or list) Either number of lags to use or array of specified lags
:param min_length: (int) Minimum number of observations needed for estimation
:param add_const: (bool) Flag to add constant
:param phi: (float) Coefficient to penalize large sample lengths when computing SMT, in [0, 1]
:param num_threads: (int) Number of cores to use
:param verbose: (bool) Flag to report progress on asynch jobs
:return: (pd.Series) SADF statistics
"""
X, y = _get_y_x(series, model, lags, add_const)
molecule = y.index[min_length:y.shape[0]]
sadf_series = _sadf_outer_loop(X=X, y=y, min_length=min_length, model=model, phi=phi,
molecule=molecule)
return sadf_seriesimport pandas as pd
import numpy as np
from datetime import timedelta
from collections import deque
from sadf import get_sadf
from ExuberAlphaModel import ExuberAlphaModel
class DynamicTransdimensionalEngine(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2020, 1, 1)
self.SetCash(10000)
# universe
self.AddUniverseSelection(
FineFundamentalUniverseSelectionModel(self.SelectCoarse, self.SelectFine)
)
self.UniverseSettings.Resolution = Resolution.Daily
# Alpha
self.AddAlpha(ExuberAlphaModel(100, Resolution.Daily))
# Portfolio construction and execution
self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
self.SetExecution(ImmediateExecutionModel())
self.SetWarmUp(100)
def SelectCoarse(self, coarse):
tickers = ['T'] #, 'AMZN', 'IBM', 'SPY']
return [Symbol.Create(x, SecurityType.Equity, Market.USA) for x in tickers]
def SelectFine(self, fine):
return [f.Symbol for f in fine]