| Overall Statistics |
|
Total Orders 290 Average Win 3.52% Average Loss -1.92% Compounding Annual Return 36.325% Drawdown 34.400% Expectancy 0.825 Start Equity 10000 End Equity 77038.99 Net Profit 670.390% Sharpe Ratio 1.026 Sortino Ratio 1.202 Probabilistic Sharpe Ratio 49.494% Loss Rate 36% Win Rate 64% Profit-Loss Ratio 1.83 Alpha 0.156 Beta 1.198 Annual Standard Deviation 0.245 Annual Variance 0.06 Information Ratio 1.146 Tracking Error 0.15 Treynor Ratio 0.21 Total Fees $303.07 Estimated Strategy Capacity $2900000000.00 Lowest Capacity Asset AMZN R735QTJ8XC9X Portfolio Turnover 3.74% |
##########################################################################
# Factor Definitions for Factor Optimization Algorithm
# ---------------------------------------------
# This file defines the factors used in the KER and FCF Yield strategy.
# Each factor is implemented as a class with standardized interface:
# - Each has an __init__ method to set up the factor
# - Each has a value property that returns the current factor value
#
# ................................................................
# Copyright(c) Quantish.io - Granted to the public domain
##########################################################################
# region imports
from AlgorithmImports import *
# endregion
class MarketCapFactor:
"""
## Market Capitalization Factor
## ----------------------------
## Measures the size of a company based on its market capitalization.
## Higher values indicate larger companies.
"""
def __init__(self, security):
self._security = security
@property
def value(self):
return self._security.fundamentals.market_cap
class SortinoFactor:
"""
## Sortino Ratio Factor
## -------------------
## Measures risk-adjusted return using only downside deviation.
## Higher values indicate better risk-adjusted performance.
"""
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:
"""
## Kaufman Efficiency Ratio Factor
## ------------------------------
## Measures the efficiency/smoothness of price movement.
## Values range from 0 to 1:
## - Values closer to 1 indicate strong trend efficiency
## - Values closer to 0 indicate noisy, choppy price action
## Used to identify securities with strong directional price movement.
"""
def __init__(self, algorithm, symbol, lookback):
## Initialize KER indicator with specified lookback period
self._ker = algorithm.ker(symbol, lookback, resolution=Resolution.DAILY)
@property
def value(self):
## Return current KER value
return self._ker.current.value
class FCFYieldFactor:
"""
## Free Cash Flow Yield Factor
## --------------------------
## Measures a company's ability to generate cash relative to its market value.
## FCF Yield = Free Cash Flow / Market Capitalization
## Higher values indicate potentially undervalued companies with strong cash generation.
## Used to identify companies with solid fundamental value.
"""
def __init__(self, security):
self._security = security
@property
def value(self):
try:
## Access fundamental data from the security
fundamentals = self._security.fundamentals
## Return the FCF Yield value from fundamentals
return fundamentals.valuation_ratios.FCFYield
except:
## Return NaN if fundamental data is unavailable
return np.nan
class CorrFactor:
"""
## Correlation Factor
## -----------------
## Measures the inverse of absolute correlation with a reference asset.
## Higher values indicate lower correlation (more diversification benefit).
"""
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 minus the absolute correlation value
## This converts high correlations to low values and vice versa
return 1 - abs(self._c.current.value)
class ROCFactor:
"""
## Rate of Change Factor
## --------------------
## Measures the percentage change in price over a specified lookback period.
## Higher values indicate stronger positive momentum.
"""
def __init__(self, algorithm, symbol, lookback):
self._roc = algorithm.roc(symbol, lookback, resolution=Resolution.DAILY)
@property
def value(self):
return self._roc.current.value
##########################################################################
# KER and FCF Yield Factor Optimization Algorithm
# ---------------------------------------------
# FOR EDUCATIONAL PURPOSES ONLY. DO NOT DEPLOY.
#
# Strategy:
# ---------
# This strategy targets the top 5 weighted stocks in the S&P 500 ETF (SPY),
# evaluating them using two complementary metrics:
# - Kaufman Efficiency Ratio (KER): Identifies stocks exhibiting smooth,
# directional price movement
# - Free Cash Flow Yield (FCF Yield): Highlights companies generating
# strong cash flows relative to their market value
#
# Live Optimization:
# -----------------
# Uses Nelder-Mead method to optimize factor weights, maximizing the
# correlation between combined factor scores and recent price performance.
# This direct search method is effective for this low-dimensional optimization
# problem without requiring derivatives.
#
# Rebalancing:
# ------------
# Monthly rebalancing incorporates both technical efficiency and fundamental
# strength, with position weights optimized based on recent performance trends.
# This approach combines momentum and value factors while maintaining focus on
# the market's most liquid, large-cap names.
#
# ................................................................
# Copyright(c) Quantish.io - Granted to the public domain
##########################################################################
# region imports
from AlgorithmImports import *
from scipy import optimize
from scipy.optimize import Bounds
from factors import *
# endregion
class FactorWeightOptimizationAlgorithm(QCAlgorithm):
def initialize(self):
## Set up basic algorithm parameters
self.set_start_date(2018, 1, 1)
self.set_end_date(2024, 8, 1)
self.set_cash(10_000)
self.settings.automatic_indicator_warm_up = True
## Create SPY symbol reference
spy = Symbol.create('SPY', SecurityType.EQUITY, Market.USA)
## Set up universe to hourly resolution
self.universe_settings.resolution = Resolution.HOUR
## Get universe size from parameters, default to top 5 constituents
universe_size = self.get_parameter('universe_size', 5)
## Create universe of top weighted stocks from SPY constituents
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:]]))
## Set lookback period for factor calculations
self._lookback = self.get_parameter('lookback', 21) # Set a 21-day trading lookback.
## Schedule monthly rebalancing after market open
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):
## Assign factors to newly added securities
for security in changes.added_securities:
# Create KER factor (technical) and FCF Yield factor (fundamental)
security.factors = [FCFYieldFactor(security), KERFactor(self, security.symbol, self._lookback)]
def _rebalance(self):
## Collect raw factor values for each security in our universe
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
## Normalize factor values to z-scores for cross-sectional comparison
factor_zscores = (factors_df - factors_df.mean()) / factors_df.std()
## Get trailing returns for optimization objective
trailing_return = self.history(list(self._universe.selected), self._lookback, Resolution.DAILY).close.unstack(0).pct_change(self._lookback-1).iloc[-1]
## Determine number of factors to optimize
num_factors = factors_df.shape[1]
## Optimize factor weights to maximize correlation with trailing returns
## The objective function is negated because we're minimizing, but want to maximize returns
## Initial weights are equal across factors (1/num_factors)
## Bounds ensure weights are between 0 and 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 combined portfolio scores using optimized factor weights
portfolio_weights = (factor_zscores * factor_weights).sum(axis=1)
## Only include positive-scored securities (long-only portfolio)
portfolio_weights = portfolio_weights[portfolio_weights > 0]
## Rebalance the portfolio with normalized weights to ensure 100% exposure
self.set_holdings([PortfolioTarget(symbol, weight/portfolio_weights.sum()) for symbol, weight in portfolio_weights.items()], True)