Overall Statistics
Total Orders
427
Average Win
0.27%
Average Loss
-0.25%
Compounding Annual Return
1.573%
Drawdown
7.900%
Expectancy
0.147
Start Equity
100000
End Equity
108121.41
Net Profit
8.121%
Sharpe Ratio
-0.707
Sortino Ratio
-0.757
Probabilistic Sharpe Ratio
3.045%
Loss Rate
45%
Win Rate
55%
Profit-Loss Ratio
1.07
Alpha
-0.028
Beta
0.023
Annual Standard Deviation
0.038
Annual Variance
0.001
Information Ratio
-0.593
Tracking Error
0.143
Treynor Ratio
-1.201
Total Fees
$830.69
Estimated Strategy Capacity
$100000.00
Lowest Capacity Asset
CLY UIB7ED20G0V9
Portfolio Turnover
5.13%
Drawdown Recovery
861
# region imports
from AlgorithmImports import *
from datetime import timedelta
import math
# endregion


class DeterminedLightBrownAlpaca(QCAlgorithm):

    def initialize(self):
        self.set_cash(100_000)
        self.set_start_date(self.end_date - timedelta(5*365))
        self.settings.automatic_indicator_warm_up = True
        self.settings.seed_initial_prices = True
        self._portfolio_size = 4
        # Add the safety hedge asset.
        self._safety_asset = self.add_equity("SHY", Resolution.DAILY)
        self._ranked_assets = []
        # Define the tickers for fixed income and credit-focused securities.
        tickers = ["AGZ", "BIL", "HYG", "IEF", "IEI", "IGIB", "IGLB",
                   "JNK", "LQD", "MBB", "MINT", "TBF", "TBX", "TIP", "TLT"]
        for ticker in tickers:
            asset = self.add_equity(ticker, Resolution.DAILY)
            # Attach indicators for each asset.
            asset.return_a = self.momp(asset, 20)
            asset.return_b = self.momp(asset, 63)
            # Create a 6-day SMA updating monthly.
            asset.trend_sma = SimpleMovingAverage(6)
            self.register_indicator(asset, asset.trend_sma, TradeBarConsolidator(CalendarType.MONTHLY))
            # Create a 20-day log return annualized volatility indicator.
            annualized_volatility = IndicatorExtensions.of(StandardDeviation(20), self.logr(asset, 20))
            asset.volatility = IndicatorExtensions.times(annualized_volatility, math.sqrt(252))
            self._ranked_assets.append(asset)
        self.set_warm_up(timedelta(365))
        self._rebalance_counter = 0

    def on_warmup_finished(self):
        rebalance_time_rule = self.time_rules.at(8, 0)
        # Add a Scheduled event to rebalance the portfolio bi-weekly.
        self.schedule.on(
            self.date_rules.week_start("SPY"),
            rebalance_time_rule,
            self._bi_weekly
        )
        # Rebalance the portfolio today too.
        if self.live_mode:
            self._rebalance()
        else:
            self.schedule.on(self.date_rules.today, rebalance_time_rule, self._rebalance)

    def _bi_weekly(self):
        self._rebalance_counter += 1
        if self._rebalance_counter % 2 == 0:
            self._rebalance()

    def _rebalance(self):
        ready_assets = [asset for asset in self._ranked_assets if asset.volatility.is_ready and asset.trend_sma.is_ready]
        if not ready_assets:
            return
        # Rank securities by: short-term and medium term momentum as well as low volatility.
        asset_count = len(ready_assets)
        for asset in ready_assets:
            asset.rank_score = 0.0
        for rank, asset in enumerate(sorted(ready_assets, key=lambda security: security.return_a)):
            asset.rank_score += (asset_count - rank - 1) * 0.2
        for rank, asset in enumerate(sorted(ready_assets, key=lambda security: security.return_b)):
            asset.rank_score += (asset_count - rank - 1) * 0.5
        for rank, asset in enumerate(sorted(ready_assets, key=lambda security: security.volatility)):
            asset.rank_score += rank * 0.3
        top_assets = sorted(ready_assets, key=lambda security: security.rank_score)[:self._portfolio_size]
        # Select the top-ranked assets with their price above monthly SMA.
        trend_assets = [asset for asset in top_assets if asset.close > asset.trend_sma.current.value]
        # Add the safety asset to portfolio if fewer assets pass the filter.
        target_assets = trend_assets + ([self._safety_asset] if len(trend_assets) < len(top_assets) else [])
        # Exit the safety asset when at least one top asset passes the trend filter.
        if trend_assets and self._safety_asset.holdings.invested:
            self.liquidate(self._safety_asset)
        # Maintain the safety asset if no trend-filtered assets were selected.
        if self._safety_asset.holdings.invested:
            return
        # Create a equal-weighted portfolio.
        portfolio_targets = [PortfolioTarget(asset, 1 / self._portfolio_size) for asset in target_assets]
        self.set_holdings(portfolio_targets, True)