| Overall Statistics |
|
Total Orders 979 Average Win 1.88% Average Loss -1.48% Compounding Annual Return 14.489% Drawdown 41.500% Expectancy 0.246 Start Equity 1000000 End Equity 4691984.64 Net Profit 369.198% Sharpe Ratio 0.434 Sortino Ratio 0.472 Probabilistic Sharpe Ratio 2.369% Loss Rate 45% Win Rate 55% Profit-Loss Ratio 1.27 Alpha 0.023 Beta 1 Annual Standard Deviation 0.239 Annual Variance 0.057 Information Ratio 0.124 Tracking Error 0.189 Treynor Ratio 0.104 Total Fees $23653.52 Estimated Strategy Capacity $210000000.00 Lowest Capacity Asset UNP R735QTJ8XC9X Portfolio Turnover 4.42% Drawdown Recovery 799 |
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from AlgorithmImports import *
from scipy.optimize import minimize
### <summary>
### Provides an implementation of a portfolio optimizer that maximizes the portfolio Sharpe Ratio.
### The interval of weights in optimization method can be changed based on the long-short algorithm.
### The default model uses flat risk free rate and weight for an individual security range from -1 to 1.'''
### </summary>
class MaxSharpeRatioPortfolioOptimizer:
'''Provides an implementation of a portfolio optimizer that maximizes the portfolio Sharpe Ratio.
The interval of weights in optimization method can be changed based on the long-short algorithm.
The default model uses flat risk free rate and weight for an individual security range from -1 to 1.'''
def __init__(self,
minimum_weight = -1,
maximum_weight = 1,
risk_free_rate = 0):
'''Initialize the MaxSharpeRatioPortfolioOptimizer
Args:
minimum_weight(float): The lower bounds on portfolio weights
maximum_weight(float): The upper bounds on portfolio weights
risk_free_rate(float): The risk free rate'''
self.minimum_weight = minimum_weight
self.maximum_weight = maximum_weight
self.risk_free_rate = risk_free_rate
self.expected_returns = []
def optimize(self, historical_returns, expected_returns = None, covariance = None):
'''
Perform portfolio optimization for a provided matrix of historical returns and an array of expected returns
args:
historical_returns: Matrix of annualized historical returns where each column represents a security and each row returns for the given date/time (size: K x N).
expected_returns: Array of double with the portfolio annualized expected returns (size: K x 1).
covariance: Multi-dimensional array of double with the portfolio covariance of annualized returns (size: K x K).
Returns:
Array of double with the portfolio weights (size: K x 1)
'''
if covariance is None:
covariance = historical_returns.cov()
if expected_returns is None:
expected_returns = historical_returns.mean()
expected_returns = expected_returns - self.risk_free_rate
size = covariance.columns.size # K x 1
x0 = np.array(size * [1. / size])
# Direct Sharpe Ratio Maximization via SLSQP.
# The Charnes-Cooper substitution (min variance s.t. (µ-rf)^T w = 1) is only
# valid when NO per-weight inequality bounds exist. With bounds (e.g. long-only)
# the scaling variable kappa couples into the bound constraints, making the
# reformulation non-convex and incorrect. See: Tütüncü (2003) §5.2
# https://www.ie.bilkent.edu.tr/~mustafap/courses/OIF.pdf and
# https://quant.stackexchange.com/questions/18521/sharpe-maximization-under-quadratic-constraints
# SLSQP handles the fractional (non-linear) objective directly without substitution.
constraints = [
# Σw = 1
{'type': 'eq', 'fun': lambda weights: self.get_budget_constraint(weights)}]
opt = minimize(lambda weights: -expected_returns.dot(weights) / np.sqrt(self.portfolio_variance(weights, covariance)),
x0, # Initial guess
bounds = self.get_boundary_conditions(size), # Bounds for variables: lw ≤ w ≤ up
constraints = constraints, # Constraints definition
method='SLSQP') # Optimization method: Sequential Least SQuares Programming
return opt['x'] if opt['success'] else x0
def portfolio_variance(self, weights, covariance):
'''Computes the portfolio variance
Args:
weighs: Portfolio weights
covariance: Covariance matrix of historical returns'''
variance = np.dot(weights.T, np.dot(covariance, weights))
if variance == 0 and np.any(weights):
# variance can't be zero, with non zero weights
raise ValueError(f'MaxSharpeRatioPortfolioOptimizer.portfolio_variance: Volatility cannot be zero. Weights: {weights}')
return variance
def get_boundary_conditions(self, size):
'''Creates the boundary condition for the portfolio weights'''
return tuple((self.minimum_weight, self.maximum_weight) for x in range(size))
def get_budget_constraint(self, weights):
'''Defines a budget constraint: the sum of the weights equals unity'''
return np.sum(weights) - 1# region imports
from AlgorithmImports import *
from MaxSharpeRatioPortfolioOptimizer import MaxSharpeRatioPortfolioOptimizer
# endregion
class LazyPricesStrategy(QCAlgorithm):
def initialize(self):
self.set_start_date(2015, 1, 1)
self.set_end_date(2026, 6, 1)
self.set_cash(1_000_000)
self.settings.seed_initial_prices = True
self.settings.min_absolute_portfolio_target_percentage = 0.0
self._optimizer = SharpePortfolioOptimizer()
self.universe_settings.resolution = Resolution.DAILY
self._universe = self.add_universe(lambda fundamental:
[f.symbol for f in sorted([f for f in fundamental if f.has_fundamental_data], key=lambda f: f.dollar_volume)[-100:]]
)
def on_warmup_finished(self):
# Rebalance on the first trading day of each quarter at 8 AM.
time_rule = self.time_rules.at(8, 0)
self.schedule.on(self.date_rules.month_start("SPY"), time_rule, self._rebalance)
# Rebalance today too.
if self.live_mode:
self._rebalance()
else:
self.schedule.on(self.date_rules.today, time_rule, self._rebalance)
def on_securities_changed(self, changes):
for security in changes.added_securities:
security.brain_10K = self.add_data(BrainCompanyFilingLanguageMetrics10K, security.symbol, Resolution.DAILY).symbol
security.brain_10Q = self.add_data(BrainCompanyFilingLanguageMetricsAll, security.symbol, Resolution.DAILY).symbol
def _rebalance(self):
if self.is_warming_up or not self._universe.selected:
return
similarity_by_symbol = {}
for symbol in self._universe.selected:
security = self.securities[symbol]
# Fetch 10K and 10Q filings over 500 days and merge them sorted by date to find the most recent similarity score.
history_10k = list(self.history[BrainCompanyFilingLanguageMetrics10K](security.brain_10K, timedelta(days=500), Resolution.DAILY))
history_10q = list(self.history[BrainCompanyFilingLanguageMetricsAll](security.brain_10Q, timedelta(days=500), Resolution.DAILY))
score = None
# Search for the most recent valid similarity score, preferring risk factors statement over full report sentiment.
for point in sorted(history_10k + history_10q, key=lambda p: p.end_time):
if point.risk_factors_statement_sentiment.similarity.all is not None:
score = float(point.risk_factors_statement_sentiment.similarity.all)
elif point.report_sentiment.similarity.all is not None:
score = float(point.report_sentiment.similarity.all)
if score is not None:
break
if score is not None:
similarity_by_symbol[symbol] = score
# Rank by similarity ascending: low scores (textual divergence) signal shorts, high scores (convergence) signal longs.
ranked = sorted(similarity_by_symbol, key=similarity_by_symbol.get)
weight_by_symbol = self._optimizer.get_weights(self, ranked[-10:])
if not weight_by_symbol:
return
targets = [PortfolioTarget(symbol, weight) for symbol, weight in weight_by_symbol.items() if self.securities[security].price]
self.set_holdings(targets, True)
class SharpePortfolioOptimizer:
def __init__(self, period: int = 252):
self._period = period
self._optimizer = MaxSharpeRatioPortfolioOptimizer(0, 1)
def get_weights(self, algorithm: QCAlgorithm, symbols: list) -> dict:
history = algorithm.history(symbols, self._period + 1, Resolution.DAILY)
if history.empty:
return {}
returns = history["close"].unstack(level=0).pct_change().dropna()
if returns.empty or returns.shape[1] < 2:
return {}
# Maximize the portfolio Sharpe ratio using the long-only optimizer.
raw_weights = self._optimizer.optimize(returns)
return {symbol: float(raw_weights[i]) for i, symbol in enumerate(returns.columns)}