Overall Statistics
Total Orders
3116
Average Win
0.29%
Average Loss
-0.24%
Compounding Annual Return
16.747%
Drawdown
30.500%
Expectancy
0.604
Start Equity
100000000
End Equity
998318657.98
Net Profit
898.319%
Sharpe Ratio
0.709
Sortino Ratio
0.741
Probabilistic Sharpe Ratio
14.721%
Loss Rate
28%
Win Rate
72%
Profit-Loss Ratio
1.22
Alpha
0.016
Beta
1.055
Annual Standard Deviation
0.154
Annual Variance
0.024
Information Ratio
0.559
Tracking Error
0.037
Treynor Ratio
0.104
Total Fees
$2660326.24
Estimated Strategy Capacity
$970000000.00
Lowest Capacity Asset
QQQ RIWIV7K5Z9LX
Portfolio Turnover
5.50%
# region imports
from AlgorithmImports import *
# endregion


class SwimmingLightBrownGalago(QCAlgorithm):

    def initialize(self):

        self.set_start_date(2010, 1, 1)
        self.set_cash(100_000_000)

        self.settings.daily_precise_end_time = False
        self.settings.minimum_order_margin_portfolio_percentage = 0.01

        lookback_years = 12 # 12 years includes 3 non-leap years & 3 election cycles
        self._lookback = lookback_years * 252 
        self._lookback_calendar_time = timedelta(lookback_years*365)
        self._period = 21 # Trading days

        self.universe_settings.resolution = Resolution.DAILY
        self._universe = self.add_universe(self._select_assets)

        spy = Symbol.create('SPY', SecurityType.EQUITY, Market.USA)
        self.schedule.on(
            self.date_rules.every_day(spy),
            self.time_rules.midnight,
            self._rebalance
        )

        self.set_portfolio_construction(InsightWeightingPortfolioConstructionModel(portfolio_bias=PortfolioBias.LONG))

        self._all_history = pd.DataFrame()

    def _select_assets(self, fundamentals):
        return [
            Symbol.create(ticker, SecurityType.EQUITY, Market.USA)
            for ticker in ['SPY', 'QQQ']
        ]

    def on_securities_changed(self, changes):
        for security in changes.removed_securities:
            self._all_history.drop(security.symbol, inplace=True)

        history = self.history([security.symbol for security in changes.added_securities], self._lookback, Resolution.DAILY)
        if not history.empty:
            self._all_history = self._all_history.join(history.open.unstack(0), how='outer')

    def _rebalance(self):
        # Append new rows.
        self._all_history = pd.concat([
            self._all_history, 
            self.history(list(self._universe.selected), 1, Resolution.DAILY).open.unstack(0)
        ])
        # Drop rows with duplicate indices, then trim to lookback window size.
        self._all_history = self._all_history.loc[~self._all_history.index.duplicated(keep='last')].iloc[-self._lookback:]

        # Calculate signal for this trade.
        period_returns = self._all_history.dropna(axis=1).pct_change(self._period).shift(-self._period).dropna()
        expected_return_by_day = period_returns.groupby(period_returns.index.strftime('%m-%d')).mean()
        signals = expected_return_by_day.iloc[
            self._wrapped_indices(
                len(expected_return_by_day.index), 
                expected_return_by_day.index.get_loc((self.time + timedelta(1)).strftime('%m-%d')), 
                self._period
            )
        ].sum()

        # Rebalance.
        self.emit_insights([Insight.price(symbol, timedelta(30), InsightDirection.UP, weight=1000*signal) for symbol, signal in signals.items()])

    def _wrapped_indices(self, index_length, start_index, count):
        start_index = start_index % index_length
        indices = [(start_index + i) % index_length for i in range(count)]
        return indices