| Overall Statistics |
|
Total Trades 1586 Average Win 1.43% Average Loss -1.20% Compounding Annual Return 40.730% Drawdown 37.800% Expectancy 0.423 Net Profit 4927.510% Sharpe Ratio 1.175 Probabilistic Sharpe Ratio 53.845% Loss Rate 35% Win Rate 65% Profit-Loss Ratio 1.19 Alpha 0 Beta 0 Annual Standard Deviation 0.265 Annual Variance 0.07 Information Ratio 1.175 Tracking Error 0.265 Treynor Ratio 0 Total Fees $161309.16 Estimated Strategy Capacity $33000000.00 Lowest Capacity Asset TLT SGNKIKYGE9NP |
import datetime
import pandas as pd
import numpy as np
def cal_volatility(df):
prices = np.array(df)
returns = (prices[1:]-prices[:-1])/prices[:-1]
volatility = np.std(returns, axis = 0)
return volatility
def inverse_volatility(df):
'''
caculate weight using inverse volatility
:param df: (DataFrame) datetime, asset_1, asset_2, ....
This dataframe has been bounded in a range of time
:return: (Series) weight of corresponding assets
'''
vol = cal_volatility(df)
inverse_vol = 1/vol
#weight for each asset
weights = pd.Series(inverse_vol/sum(inverse_vol), index = df.columns)
return weights
## import to run locally
# from AlgorithmImports import *
# from clr import AddReference
# AddReference("System")
# AddReference("QuantConnect.Algorithm")
# AddReference("QuantConnect.Common")
# from System import *
# from QuantConnect import *
# from QuantConnect.Algorithm import *
from risk_parity import risk_parity
from inverse_volatility import inverse_volatility
import constant
import random
random.seed(10)
class BareKnuckle(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2010, 5, 1) # Set Start Date
init_cash = 1000000
self.SetCash(init_cash) # Set Strategy Cash
#set maximum leverage level
self.leverage_level = 2 if self.GetParameter(constant.LEVERAGE) is None else int(self.GetParameter(constant.LEVERAGE))
if self.leverage_level > 1:
self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin) #change Brokerage >> get different fee
#max porfolio value (to calculate drawdown)
self.max_portfolio_value = init_cash
#param
stocks = self.GetParameter(constant.STOCK_NAME)
self.stocks = [
"SPY",
"QQQ",
"TLT"
] if stocks is None else str(stocks).split(constant.SPLIT_CHARACTER)
self.lookback_period = 42 if self.GetParameter(constant.RISK_LOOKBACK) is None else int(self.GetParameter(constant.RISK_LOOKBACK))
self.optimization = constant.RISK_PARITY if self.GetParameter(constant.OPTIMIZATION) is None else self.GetParameter(constant.OPTIMIZATION)
#add stock and set normalization mode (use raw price, dividents added directly to portfolio)
for stock in self.stocks:
equity = self.AddEquity(stock, Resolution.Daily)
self.Securities[stock].SetLeverage(self.leverage_level)
equity.SetDataNormalizationMode(DataNormalizationMode.Raw)
self.SetWarmUp(self.lookback_period) # Skip 30 beginning ticks
#choose random asset for scheduling
asset_for_scheduling = random.choice(self.stocks)
self.Schedule.On( # schedule reallocaton every monday
self.DateRules.Every(DayOfWeek.Monday),
self.TimeRules.AfterMarketOpen(asset_for_scheduling, 10),
self.Reallocate
)
# Chart - Master Container for the Chart:
stockPlot = Chart('Weight Plot')
# On the Trade Plotter Chart we want 3 series: trades and price:
[stockPlot.AddSeries(Series(stock, SeriesType.Line, 0)) for stock in self.stocks]
#schedule to add drawdown (monthly). Note: can choose any common etf/stock, not just SPY
self.Schedule.On(
self.DateRules.MonthStart(asset_for_scheduling),
self.TimeRules.AfterMarketOpen(asset_for_scheduling),
self.CalculateDrawdown
)
drawdownMontlyPlot = Chart('Monthly Drawdown')
drawdownMontlyPlot.AddSeries(Series('Drawdown', SeriesType.Line, 0))
def CalculateDrawdown(self):
current_portfolio_value = self.Portfolio.TotalPortfolioValue
#calculate drawdown
drawdown = round(min(0, current_portfolio_value - self.max_portfolio_value)/self.max_portfolio_value, 2) * 100
#plot new drawdown
self.Plot('Monthly Drawdown', 'Drawdown', drawdown)
# update max_portfolio_value
if self.max_portfolio_value < current_portfolio_value:
self.max_portfolio_value = current_portfolio_value
def Reallocate(self):
'''
Reallocate after a predefine x days/weeks
'''
#get historical open price of all stocks and calculate weight using risk parity
self.stocks_symbol = [self.Symbol(d) for d in self.stocks]
df = self.History(self.stocks_symbol, self.lookback_period)
df = df[constant.QC_OPEN_PRICE].unstack(level=0)
#calculate weight
if self.optimization == constant.INVERSE_VOLATILITY:
weight = inverse_volatility(df)
else:
weight = risk_parity(df)
weight[constant.VISUALIZATION_DATETIME] = str(self.Time)
self.Debug("weight after risk parity {}".format(weight.to_dict()))
#ploting weight chart
for symbol in self.stocks:
self.Plot('Weight Plot', symbol, weight[self.Symbol(symbol)])
#rebalance
portfolio_target = [PortfolioTarget(d, max(weight[d], 0) * self.leverage_level) for d in self.stocks]
self.SetHoldings(portfolio_target)
def OnData(self, data):
'''OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here.
Arguments:
data: Slice object keyed by symbol containing the stock data
'''
# debug: print the day that do not have data
for stock in self.stocks:
if not data.Bars.ContainsKey(stock):
self.Debug(self.Time)
self.Debug("{}, data doesn\'t contain {}".format(self.Time, stock))
return
# ---- Our constant SPLIT_CHARACTER=" " STOCK_NAME="stock" BOND_NAME="bond" RISK_LOOKBACK="rlb" MOMENTUM_LOOKBACK="mlb" THRES_LOW="tl" THRES_HIGH="th" ALPHA_MEDIUM="am" ALPHA_HIGH="ah" LEVERAGE="leverage" OPTIMIZATION="optimization" RISK_PARITY="rp" INVERSE_VOLATILITY="iv" # ---- QuantConnect Pararms QC_CLOSE_PRICE="close" QC_LOW_PRICE="low" QC_HIGH_PRICE="high" QC_OPEN_PRICE="open" # ---- Visualization VISUALIZATION_DATETIME="DateTime"
import datetime
import pandas as pd
import numpy as np
from scipy.optimize import minimize
TOLERANCE = 1e-10
#### EQUALLY WEIGHTED
def equal_weight(df,
portfolio_value):
weights = [1 / df.shape[1]] * df.shape[1]
# Convert the weights to a pandas Series
weights = pd.Series(weights, index=df.columns, name='weight')
return weights
#### RISK PARITY
def _allocation_risk(weights, covariances):
portfolio_risk = np.sqrt((weights * covariances * weights.T))[0, 0]
return portfolio_risk
def _assets_risk_contribution_to_allocation_risk(weights, covariances):
portfolio_risk = _allocation_risk(weights, covariances)
assets_risk_contribution = np.multiply(weights.T, covariances * weights.T) / portfolio_risk
return assets_risk_contribution
def _risk_budget_objective_error(weights, args):
covariances = args[0]
assets_risk_budget = args[1]
weights = np.matrix(weights)
portfolio_risk = _allocation_risk(weights, covariances)
assets_risk_contribution = _assets_risk_contribution_to_allocation_risk(weights, covariances)
assets_risk_target = np.asmatrix(np.multiply(portfolio_risk, assets_risk_budget))
error = sum(np.square(assets_risk_contribution - assets_risk_target.T))[0, 0]
return error
def _get_risk_parity_weights(covariances, assets_risk_budget, initial_weights):
constraints = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1.0},
{'type': 'ineq', 'fun': lambda x: x})
optimize_result = minimize(fun=_risk_budget_objective_error,
x0=initial_weights,
args=[covariances, assets_risk_budget],
method='SLSQP',
constraints=constraints,
tol=TOLERANCE,
options={'disp': False})
weights = optimize_result.x
return weights
def risk_parity(df):
"""
Execute risk parity measurement
:param df: (DataFrame) datetime, asset_1, asset_2, ....
This dataframe has been bounded in a range of time
:return: (Series) weight of corresponding assets
"""
covariances = 52.0 * df.asfreq('W-FRI').pct_change().iloc[1:, :].cov().values
assets_risk_budget = [1 / df.shape[1]] * df.shape[1]
init_weights = [1 / df.shape[1]] * df.shape[1]
weights = _get_risk_parity_weights(covariances, assets_risk_budget, init_weights)
weights = pd.Series(weights, index=df.columns, name='weight')
return weights