Overall Statistics
Total Orders
2646
Average Win
0.07%
Average Loss
-0.07%
Compounding Annual Return
1.617%
Drawdown
18.300%
Expectancy
0.249
Start Equity
100000000
End Equity
126910221.94
Net Profit
26.910%
Sharpe Ratio
-0.095
Sortino Ratio
-0.049
Probabilistic Sharpe Ratio
0.040%
Loss Rate
36%
Win Rate
64%
Profit-Loss Ratio
0.94
Alpha
-0.021
Beta
0.181
Annual Standard Deviation
0.047
Annual Variance
0.002
Information Ratio
-0.759
Tracking Error
0.123
Treynor Ratio
-0.025
Total Fees
$384670.43
Estimated Strategy Capacity
$690000000.00
Lowest Capacity Asset
QQQ RIWIV7K5Z9LX
Portfolio Turnover
1.97%
# 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

        lookback_years = 12 # 12 years includes 3 non-leap years & 3 election cycles
        self._lookback = lookback_years * 252 
        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(AccumulativeInsightPortfolioConstructionModel(Resolution.DAILY, PortfolioBias.LONG, 1/self._period))

        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 expected return of entering at next market open.
        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()
        expected_return = expected_return_by_day.loc[(self.time + timedelta(1)).strftime('%m-%d')]
        
        # Calculate signal for this trade.
        std = expected_return_by_day.std()
        mean = expected_return_by_day.mean()
        long_threshold = mean + std
        short_threshold = mean - std
        signals = (
            (expected_return >= long_threshold).astype(int) # Long signals
            - (expected_return <= short_threshold).astype(int) # Short signals
        )

        # Rebalance.
        #self.set_holdings([PortfolioTarget(symbol, signal/len(signals)) for symbol, signal in signals.items()], True)
        self.emit_insights([Insight.price(symbol, timedelta(30), signal) for symbol, signal in signals.items()])