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)