| Overall Statistics |
|
Total Orders 422 Average Win 3.00% Average Loss -1.67% Compounding Annual Return 31.652% Drawdown 35.500% Expectancy 0.837 Start Equity 10000000 End Equity 153685556.41 Net Profit 1436.856% Sharpe Ratio 0.979 Sortino Ratio 1.113 Probabilistic Sharpe Ratio 44.871% Loss Rate 34% Win Rate 66% Profit-Loss Ratio 1.80 Alpha 0.121 Beta 1.192 Annual Standard Deviation 0.222 Annual Variance 0.049 Information Ratio 0.971 Tracking Error 0.14 Treynor Ratio 0.183 Total Fees $274664.68 Estimated Strategy Capacity $3500000000.00 Lowest Capacity Asset FB V6OIPNZEM8V9 Portfolio Turnover 3.65% |
# region imports
from AlgorithmImports import *
# endregion
class MarketCapFactor:
def __init__(self, security):
self._security = security
@property
def value(self):
return self._security.fundamentals.market_cap
class SortinoFactor:
def __init__(self, algorithm, symbol, lookback):
self._sortino = algorithm.sortino(symbol, lookback, resolution=Resolution.DAILY)
@property
def value(self):
return self._sortino.current.value
class KERFactor:
def __init__(self, algorithm, symbol, lookback):
self._ker = algorithm.ker(symbol, lookback, resolution=Resolution.DAILY)
@property
def value(self):
return self._ker.current.value
class FCFYieldFactor:
"""Free Cash Flow Yield factor"""
def __init__(self, security):
self._security = security
@property
def value(self):
try:
fundamentals = self._security.fundamentals
return fundamentals.valuation_ratios.FCFYield
except:
return np.nan
class CorrFactor:
def __init__(self, algorithm, symbol, reference, lookback):
self._c = algorithm.c(symbol, reference, lookback, correlation_type=CorrelationType.Pearson, resolution=Resolution.DAILY)
@property
def value(self):
return 1 - abs(self._c.current.value)
class ROCFactor:
def __init__(self, algorithm, symbol, lookback):
self._roc = algorithm.roc(symbol, lookback, resolution=Resolution.DAILY)
@property
def value(self):
return self._roc.current.value# region imports
from AlgorithmImports import *
from scipy import optimize
from scipy.optimize import Bounds
from factors import *
# endregion
class FactorWeightOptimizationAlgorithm(QCAlgorithm):
def initialize(self):
self.set_start_date(2014, 12, 31)
self.set_cash(10_000_000)
self.settings.automatic_indicator_warm_up = True
spy = Symbol.create('SPY', SecurityType.EQUITY, Market.USA)
# Add a universe of hourly data.
self.universe_settings.resolution = Resolution.HOUR
universe_size = self.get_parameter('universe_size', 5)
self._universe = self.add_universe(self.universe.etf(spy, universe_filter_func=lambda constituents: [c.symbol for c in sorted([c for c in constituents if c.weight], key=lambda c: c.weight)[-universe_size:]]))
self._lookback = self.get_parameter('lookback', 21) # Set a 21-day trading lookback.
# Create a Schedule Event to rebalance the portfolio.
self.schedule.on(self.date_rules.month_start(spy), self.time_rules.after_market_open(spy, 31), self._rebalance)
def on_securities_changed(self, changes):
for security in changes.added_securities: # Create factors for assets that enter the universe.
# security.factors = [MarketCapFactor(security), SortinoFactor(self, security.symbol, self._lookback)]
security.factors = [FCFYieldFactor(security), KERFactor(self, security.symbol, self._lookback)]
def _rebalance(self):
# Get raw factor values of the universe constituents.
factors_df = pd.DataFrame()
for symbol in self._universe.selected:
for i, factors in enumerate(self.securities[symbol].factors):
factors_df.loc[symbol, i] = factors.value
# Calculate the factor z-scores.
factor_zscores = (factors_df - factors_df.mean()) / factors_df.std()
# Run an optimization to find optimal factor weights. Objective: Maximize trailing return. Initial guess: Equal-weighted.
trailing_return = self.history(list(self._universe.selected), self._lookback, Resolution.DAILY).close.unstack(0).pct_change(self._lookback-1).iloc[-1]
num_factors = factors_df.shape[1]
factor_weights = optimize.minimize(lambda weights: -(np.dot(factor_zscores, weights) * trailing_return).sum(), x0=np.array([1.0/num_factors] * num_factors), method='Nelder-Mead', bounds=Bounds([0] * num_factors, [1] * num_factors), options={'maxiter': 10}).x
# Calculate the portfolio weights. Ensure the portfolio is long-only with 100% exposure, then rebalance the portfolio.
portfolio_weights = (factor_zscores * factor_weights).sum(axis=1)
portfolio_weights = portfolio_weights[portfolio_weights > 0]
self.set_holdings([PortfolioTarget(symbol, weight/portfolio_weights.sum()) for symbol, weight in portfolio_weights.items()], True)