Overall Statistics
Total Orders
909
Average Win
1.71%
Average Loss
-1.27%
Compounding Annual Return
26.624%
Drawdown
36.600%
Expectancy
0.494
Start Equity
1000000
End Equity
14832409.69
Net Profit
1383.241%
Sharpe Ratio
0.807
Sortino Ratio
0.903
Probabilistic Sharpe Ratio
25.526%
Loss Rate
36%
Win Rate
64%
Profit-Loss Ratio
1.35
Alpha
0.111
Beta
0.867
Annual Standard Deviation
0.224
Annual Variance
0.05
Information Ratio
0.541
Tracking Error
0.186
Treynor Ratio
0.209
Total Fees
$37287.72
Estimated Strategy Capacity
$2900000000.00
Lowest Capacity Asset
HUM R735QTJ8XC9X
Portfolio Turnover
3.87%
Drawdown Recovery
623
# 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 optimization import SharpePortfolioOptimizer
# 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()
        # Add a universe of the top 100 most liquid Equities.
        self.universe_settings.asynchronous = True
        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:]]
        )
        # Rebalance on the last trading day of each month.
        self.schedule.on(self.date_rules.month_end("SPY"), self.time_rules.at(8, 0), self._rebalance)

    def on_securities_changed(self, changes):
        for security in changes.added_securities:
            # Data feed includes both 10-K and 10-Q filings' report and risk-factors similarity.
            security.brain_filings = self.add_data(BrainCompanyFilingLanguageMetricsAll, security.symbol, Resolution.DAILY).symbol
        for security in changes.removed_securities:
            self.remove_security(security.brain_filings)

    def _rebalance(self):
        if not self._universe.selected:
            return
        similarity_by_symbol = {}
        for symbol in self._universe.selected:
            security = self.securities[symbol]
            # Scan the past 6 months of 10-K/10-Q filings for similarity scores.
            history = list(self.history[BrainCompanyFilingLanguageMetricsAll](security.brain_filings, timedelta(30*6), Resolution.DAILY))
            # Prefer risk factors similarity scores, else the full report similarity scores.
            scores = [filing.risk_factors_statement_sentiment.similarity.all or filing.report_sentiment.similarity.all for filing in history[::-1]]
            scores = [score for score in scores if score is not None]
            if scores:
                similarity_by_symbol[symbol] = scores[0]
        # Rank by highest similarity.
        ranked = sorted(similarity_by_symbol.keys(), key=lambda symbol: similarity_by_symbol[symbol])
        # Run portfolio optimization on the selected long-only 10 asset portfolio.
        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[symbol].price]
        self.set_holdings(targets, True)
# region imports
from AlgorithmImports import *
from MaxSharpeRatioPortfolioOptimizer import MaxSharpeRatioPortfolioOptimizer
# endregion


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)}