Overall Statistics
Total Orders
6090
Average Win
0.18%
Average Loss
-0.14%
Compounding Annual Return
17.526%
Drawdown
35.000%
Expectancy
0.368
Start Equity
1000000
End Equity
4761123.57
Net Profit
376.112%
Sharpe Ratio
0.652
Sortino Ratio
0.679
Probabilistic Sharpe Ratio
15.902%
Loss Rate
41%
Win Rate
59%
Profit-Loss Ratio
1.34
Alpha
0.028
Beta
1.056
Annual Standard Deviation
0.174
Annual Variance
0.03
Information Ratio
0.424
Tracking Error
0.077
Treynor Ratio
0.107
Total Fees
$22107.82
Estimated Strategy Capacity
$140000000.00
Lowest Capacity Asset
AMT RBASL7V8PIZP
Portfolio Turnover
3.82%
# region imports
from AlgorithmImports import *

from scipy import optimize
# endregion

class MaintainHistoricalDailyUniversePriceDataAlgorithm(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2015, 3, 20)
        self.set_cash(1_000_000)

        self.settings.automatic_indicator_warm_up = True
        self._spy = self.add_equity('SPY', Resolution.DAILY).symbol
        self.set_security_initializer(
            BrokerageModelSecurityInitializer(self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices))
        )
        # Add a universe of daily data.
        self.universe_settings.resolution = Resolution.HOUR
        self._universe = self.add_universe(
            self.universe.etf(self._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
                    )[-100:]
                ]
            )
        )

        # Define the lookback period.
        self._lookback = 252  # Trading days.

        # Create a Schedule Event to rebalance the portfolio.
        self.schedule.on(
            self.date_rules.month_start(self._spy),
            self.time_rules.after_market_open(self._spy, 31),
            self._rebalance
        )

        self.set_warm_up(timedelta(30))

    def on_securities_changed(self, changes):
        for security in changes.added_securities:
            security.factors = [
                CorrFactor(self, security.symbol, self._spy, self._lookback),
                ROCFactor(self, security.symbol, self._lookback),
                MarketCapFactor(self, security)
            ]

    def _rebalance(self):
        if self.is_warming_up or not self._universe.selected:
            return

        # Get raw factor values
        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
        raw_corr = factors_df.copy()
        # Calculate factor z-scores.
        factor_zscores = (factors_df - factors_df.mean()) / factors_df.std()

        # Run optimization
        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]
        result = optimize.minimize(
            lambda weights: -(np.dot(factor_zscores, weights) * trailing_return).sum(), # Maximize trailing return
            x0=np.array([1/num_factors] * num_factors), # Initial guess: Equal-weighted
            method='SLSQP',
            bounds=tuple((0, 1) for _ in range(num_factors)),
            constraints=({'type': 'eq', 'fun': lambda x: np.sum(x) - 1})  # The factor weights must sum to 1
        )
        factor_weights = result.x
        for i, w in enumerate(factor_weights):
            self.plot(f"Factor Weights", str(i), w)
        # Calculate the portfolio weights.
        portfolio_weights = np.dot(factor_zscores, factor_weights)
        # Make portfolio long-only
        portfolio_weights[portfolio_weights < 0] = 0
        # Make exposure 100%
        portfolio_weights[portfolio_weights > 0] /= portfolio_weights[portfolio_weights > 0].sum()
        # Rebalance the portfolio.
        self.set_holdings([PortfolioTarget(factor_zscores.index[i], portfolio_weights[i]) for i, symbol in enumerate(portfolio_weights)], True)


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


class MarketCapFactor:

    def __init__(self, algorithm, security):
        self._security = security

    @property
    def value(self):
        return self._security.fundamentals.market_cap