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)