Overall Statistics
Total Orders
70028
Average Win
0.02%
Average Loss
-0.02%
Compounding Annual Return
15.149%
Drawdown
41.400%
Expectancy
0.254
Start Equity
100000000
End Equity
813809136.46
Net Profit
713.809%
Sharpe Ratio
0.593
Sortino Ratio
0.647
Probabilistic Sharpe Ratio
5.407%
Loss Rate
36%
Win Rate
64%
Profit-Loss Ratio
0.95
Alpha
0.009
Beta
1.047
Annual Standard Deviation
0.172
Annual Variance
0.03
Information Ratio
0.148
Tracking Error
0.086
Treynor Ratio
0.097
Total Fees
$8449395.39
Estimated Strategy Capacity
$26000000.00
Lowest Capacity Asset
WMT R735QTJ8XC9X
Portfolio Turnover
6.68%
# 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_uncorrelated_assets)

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

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

        self._all_history = pd.DataFrame()

        sectors = [
            MorningstarSectorCode.BASIC_MATERIALS,
            MorningstarSectorCode.CONSUMER_CYCLICAL,
            MorningstarSectorCode.FINANCIAL_SERVICES,
            MorningstarSectorCode.REAL_ESTATE,
            MorningstarSectorCode.CONSUMER_DEFENSIVE,
            MorningstarSectorCode.HEALTHCARE,
            MorningstarSectorCode.UTILITIES,
            MorningstarSectorCode.COMMUNICATION_SERVICES,
            MorningstarSectorCode.ENERGY,
            MorningstarSectorCode.INDUSTRIALS,
            MorningstarSectorCode.TECHNOLOGY,
        ]
        self._sector = sectors[self.get_parameter('sector', 0)]
        self._universe_size = self.get_parameter('universe_size', 10)

    def _select_uncorrelated_assets(self, fundamentals):
        #return [Symbol.create('AMZN', SecurityType.EQUITY, Market.USA)]
        ## Select assets with sufficient history.
        selected = [f for f in fundamentals if f.symbol.id.date <= self.time - self._lookback_calendar_time]
        ## Select largest assets in the selected sector.
        selected = sorted(
            selected,#[f for f in selected if f.asset_classification.morningstar_sector_code == self._sector],
            key=lambda f: f.market_cap
        )[-self._universe_size:]
        return [f.symbol for f in selected]

    def on_securities_changed(self, changes):
        #self.insights.cancel([security.symbol for security in changes.removed_securities])
        for security in changes.removed_securities:
            if security.symbol not in self._all_history.columns:
                continue
            self._all_history.drop(security.symbol, axis=1, 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() if signal > 0])

    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