| Overall Statistics |
|
Total Orders 515 Average Win 0.30% Average Loss -0.15% Compounding Annual Return 3.724% Drawdown 18.100% Expectancy 1.042 Start Equity 100000 End Equity 172072.65 Net Profit 72.073% Sharpe Ratio 0.19 Sortino Ratio 0.181 Probabilistic Sharpe Ratio 1.012% Loss Rate 32% Win Rate 68% Profit-Loss Ratio 2.01 Alpha -0.009 Beta 0.224 Annual Standard Deviation 0.054 Annual Variance 0.003 Information Ratio -0.647 Tracking Error 0.119 Treynor Ratio 0.046 Total Fees $108.73 Estimated Strategy Capacity $0 Lowest Capacity Asset FAMA_FRENCH_5_MARKET_EQ.QuantpediaFamaFrenchEquity 2S Portfolio Turnover 0.31% |
from AlgorithmImports import *
class LastDateHandler():
_last_update_date:Dict[Symbol, datetime.date] = {}
@staticmethod
def get_last_update_date() -> Dict[Symbol, datetime.date]:
return LastDateHandler._last_update_date
# Quantpedia data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaFamaFrench(PythonData):
def GetSource(self, config, date, isLiveMode):
return SubscriptionDataSource(f'data.quantpedia.com/backtesting_data/equity/fama_french/{config.Symbol.Value.lower()}.csv', SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
def Reader(self, config, line, date, isLiveMode):
data = QuantpediaFamaFrench()
data.Symbol = config.Symbol
if not line[0].isdigit():
return None
split = line.split(';')
data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1)
data['market'] = float(split[1])
data['size'] = float(split[2])
data['value'] = float(split[3])
data['profitability'] = float(split[4])
data['investment'] = float(split[5])
if config.Symbol not in LastDateHandler._last_update_date:
LastDateHandler._last_update_date[config.Symbol] = datetime(1,1,1).date()
if data.Time.date() > LastDateHandler._last_update_date[config.Symbol]:
LastDateHandler._last_update_date[config.Symbol] = data.Time.date()
return data
class QuantpediaFamaFrenchEquity(PythonData):
def GetSource(self, config, date, isLiveMode):
return SubscriptionDataSource(f'data.quantpedia.com/backtesting_data/equity/fama_french/{config.Symbol.Value.lower()}.csv', SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
def Reader(self, config, line, date, isLiveMode):
data = QuantpediaFamaFrenchEquity()
data.Symbol = config.Symbol
if not line[0].isdigit():
return None
split = line.split(';')
data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1)
data.Value = float(split[1])
if config.Symbol.Value not in LastDateHandler._last_update_date:
LastDateHandler._last_update_date[config.Symbol.Value] = datetime(1,1,1).date()
if data.Time.date() > LastDateHandler._last_update_date[config.Symbol.Value]:
LastDateHandler._last_update_date[config.Symbol.Value] = data.Time.date()
return data
# custom fee model
class CustomFeeModel:
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))# https://quantpedia.com/strategies/mean-variance-factor-timing/
#
# The investment universe consists of all AMEX, NYSE, and NASDAQ-listed U.S. stocks. The data come from Kenneth French’s website. Create factor portfolios based on five factors:
# size, value, momentum, investment, and profitability.
# Using the Markowitz model, construct a long-short efficient portfolio maximizing the Sharpe ratio. Each month run out-of-sample estimation using previous 60-month data.
#
# QC Implementation changes:
#region imports
from AlgorithmImports import *
from scipy.optimize import minimize
import data_tools
#endregion
class MeanVarianceFactorTiming(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(100000)
self.period:int = 60 * 21
# warm up fama french values for idiosyncratic volatility
self.SetWarmup(self.period, Resolution.Daily)
self.data:dict = {}
self.fama_french_symbol:Symbol = self.AddData(data_tools.QuantpediaFamaFrench, 'fama_french_5_factor', Resolution.Daily).Symbol
self.ff_factor_names:list[str] = ['market', 'size', 'value', 'profitability', 'investment']
# ff performance data
self.fama_french_data:dict = { ff_factor_name : RollingWindow[float](self.period) for ff_factor_name in self.ff_factor_names }
# ff traded symbols
for factor_name in self.ff_factor_names:
data:Security = self.AddData(data_tools.QuantpediaFamaFrenchEquity, f'fama_french_5_{factor_name}_eq', Resolution.Daily)
data.SetLeverage(3)
data.SetFeeModel(data_tools.CustomFeeModel())
self.recent_month:int = -1
self.settings.minimum_order_margin_portfolio_percentage = 0.
def OnData(self, data):
# Check if custom data is still coming.
if any(
[
self.securities[x].get_last_data() and self.time.date() > data_tools.LastDateHandler.get_last_update_date()[x]
for x in [(f'fama_french_5_{factor_name}_eq').upper() for factor_name in self.ff_factor_names] + [self.fama_french_symbol]
]
):
self.liquidate()
return
# update fama french values on daily basis
if self.fama_french_symbol in data and data[self.fama_french_symbol]:
for ff_factor_name in self.ff_factor_names:
self.fama_french_data[ff_factor_name].Add(data[self.fama_french_symbol].GetProperty(ff_factor_name))
if self.recent_month == self.Time.month:
return
self.recent_month = self.Time.month
# optimization
if all(x[1].IsReady for x in self.fama_french_data.items()):
perf_df:pd.DataFrame = pd.DataFrame(columns=self.ff_factor_names)
for ff_factor_name in self.ff_factor_names:
perf_df[ff_factor_name] = np.array([x for x in self.fama_french_data[ff_factor_name]][::-1])
opt, weights = self.optimization_method(perf_df)
for ff_factor_symbol, w in weights.items():
traded_symbol:str = f'fama_french_5_{ff_factor_symbol}_eq'
if abs(w) > 0.001:
self.SetHoldings(traded_symbol, w)
else:
self.Liquidate(traded_symbol)
def optimization_method(self, returns:pd.DataFrame):
'''Maximize sharpe ratio method'''
# objective function
fun = lambda weights: - np.sum(returns.mean() * weights) * 252 / np.sqrt(np.dot(weights.T, np.dot(returns.cov() * 252, weights)))
# Constraint #1: The weights can be negative, which means investors can short a security.
constraints = [{'type': 'eq', 'fun': lambda w: 1 - np.sum(w)}]
size = returns.columns.size
x0 = np.array(size * [1. / size])
# bounds = tuple((self.minimum_weight, self.maximum_weight) for x in range(size))
bounds = tuple((0, 1) for x in range(size))
opt = minimize(fun, # Objective function
x0, # Initial guess
method='SLSQP', # Optimization method: Sequential Least SQuares Programming
bounds = bounds, # Bounds for variables
constraints = constraints) # Constraints definition
return opt, pd.Series(opt['x'], index = returns.columns)