Overall Statistics
Total Orders
276
Average Win
4.87%
Average Loss
-2.26%
Compounding Annual Return
20.422%
Drawdown
39.000%
Expectancy
0.763
Start Equity
1000000
End Equity
8354772.10
Net Profit
735.477%
Sharpe Ratio
0.625
Sortino Ratio
0.668
Probabilistic Sharpe Ratio
10.107%
Loss Rate
44%
Win Rate
56%
Profit-Loss Ratio
2.16
Alpha
0.066
Beta
0.909
Annual Standard Deviation
0.223
Annual Variance
0.05
Information Ratio
0.329
Tracking Error
0.18
Treynor Ratio
0.153
Total Fees
$26787.25
Estimated Strategy Capacity
$130000000.00
Lowest Capacity Asset
GS RKEOGCOG6RFP
Portfolio Turnover
2.57%
Drawdown Recovery
370
# 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
        self._optimizer = SharpePortfolioOptimizer()
        self._fundamental = []
        # Select the universe monthly to match the rebalance cadence.
        self.universe_settings.resolution = Resolution.DAILY
        self.universe_settings.schedule.on(self.date_rules.month_end("SPY"))
        # Collect the 100 most liquid Equities, then rank them by Brain filing textual similarity.
        self.add_universe(self._fundamental_filter)
        self._universe = self.add_universe(BrainCompanyFilingLanguageMetricsUniverseAll, self._select_assets)
        self.schedule.on(self.date_rules.month_end("SPY"), self.time_rules.at(8, 0), self._rebalance)

    def _fundamental_filter(self, fundamental):
        # Store the 100 most liquid Equities; the Brain filing universe ranks within this set.
        self._fundamental = [f.symbol for f in sorted([f for f in fundamental if f.has_fundamental_data], key=lambda f: f.dollar_volume)[-100:]]
        return Universe.UNCHANGED

    def _select_assets(self, filings):
        # Map each liquid filer to its similarity score, preferring risk factors over the full report.
        similarity_by_symbol = {
            f.symbol: f.risk_factors_statement_sentiment.similarity.all or f.report_sentiment.similarity.all
            for f in filings if f.symbol in self._fundamental
        }
        # Rank by similarity and keep the long-only top 10 most textually stable filers.
        return sorted([s for s in similarity_by_symbol if similarity_by_symbol[s] is not None], key=lambda symbol: similarity_by_symbol[symbol])[-10:]

    def _rebalance(self):
        if not self._universe.selected:
            return
        # Run portfolio optimization on the selected long-only 10 asset portfolio.
        weight_by_symbol = self._optimizer.get_weights(self, list(self._universe.selected))
        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)}