Overall Statistics
Total Trades
1086
Average Win
3.04%
Average Loss
-2.02%
Compounding Annual Return
37.041%
Drawdown
44.000%
Expectancy
0.839
Net Profit
4473.163%
Sharpe Ratio
1.008
Probabilistic Sharpe Ratio
31.200%
Loss Rate
27%
Win Rate
73%
Profit-Loss Ratio
1.50
Alpha
0
Beta
0
Annual Standard Deviation
0.297
Annual Variance
0.088
Information Ratio
1.008
Tracking Error
0.297
Treynor Ratio
0
Total Fees
$3086.12
Estimated Strategy Capacity
$380000.00
Lowest Capacity Asset
SPY 323H68DJAL1JA|SPY R735QTJ8XC9X
using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Brokerages;
using QuantConnect.Data;
using QuantConnect.Securities;
using QuantConnect.Securities.Equity;
using QuantConnect.Securities.Option;
using QuantConnect.Util;

namespace QuantConnect.Algorithm.CSharp {
internal class Protection {
  public Security Underlying { get; }
  public decimal Weight { get; }
  public decimal ProfitTarget { get; }
  public decimal PercentBelow { get; }
  private int MinimumDte { get; }
  private int MaximumDte { get; }
  public int ExerciseAtDte { get; }

  private Option? _contract;

  public Protection(Security underlying, decimal weight,
    decimal profitTarget = 1.3m, decimal percentBelow = 0.4m,
    int minimumDte = 270, int maximumDte = 420, int exerciseAtDte = 180) {
    Underlying = underlying;
    Weight = weight;
    ProfitTarget = profitTarget;
    PercentBelow = percentBelow;
    MinimumDte = minimumDte;
    MaximumDte = maximumDte;
    ExerciseAtDte = exerciseAtDte;
  }

  // Call this OnData at times to update options
  public void ManageContract(QCAlgorithm algo) {
    // if no contract, we try to find one 
    if (_contract == null) {
      var symbol = FindContract(algo);
      if (symbol != null) {
        _contract = algo.AddOptionContract(symbol, Resolution.Daily);
      }
    }
    // okay: now we know the contract is good...
    else {
      if (algo.Portfolio[_contract.Symbol].Invested) {
        var tooShort = (_contract.Symbol.ID.Date - algo.Time).Days < ExerciseAtDte;
        var targetHit = Underlying.Price < _contract.Symbol.ID.StrikePrice * ProfitTarget;

        if (tooShort || targetHit) {
          algo.Liquidate(_contract.Symbol);
          algo.RemoveSecurity(_contract.Symbol);
          _contract = null;
        }
      }
      else {
        algo.SetHoldings(_contract.Symbol, Weight);
      }
    }
  }

  public Symbol? FindContract(QCAlgorithm algo) {
    var targetStrike = Underlying.Price * (1 - PercentBelow) - Underlying.Price * (1 - PercentBelow) % 5m;

    return algo.OptionChainProvider
      .GetOptionContractList(Underlying.Symbol, algo.Time)
      .Where(c => {
        var dte = (c.ID.Date - algo.Time).Days;

        var isPut = c.ID.OptionRight == OptionRight.Put;
        var strikeOk = c.ID.StrikePrice == targetStrike;
        var dateOk = MinimumDte < dte && dte <= MaximumDte;

        return isPut && strikeOk && dateOk;
      })
      .OrderBy(p => (p.ID.Date, p.ID.StrikePrice))
      .Take(1)
      .FirstOrDefault();
  }
}

public class AllWeather : QCAlgorithm {
  private Protection _protection;
  private const decimal FreeCash = 0.02m;
  private const decimal EquityAllocation = 3m * 0.95m;
  private const decimal ProtectionAllocation = 0.01m;

  public bool ShouldRebalance { get; private set; } = true;
  public List<(Equity Security, decimal Target)> Targets { get; set; }

  public override void Initialize() {
    SetBrokerageModel(new InteractiveBrokersBrokerageModel());
    SetStartDate(2010, 1, 1);
    SetCash(100000);

    LoadWeights(new List<(string Symbol, decimal Target)> {
      ("SPY", 0.35m), // Equities
      ("XLK", 0.16m), // Tech
      ("TLT", 0.15m), // Fixed
      ("XLE", 0.09m), // Energy
      ("XLF", 0.08m), // Finance
      ("XLV", 0.07m), // Health
      ("GXC", 0.06m) // China
      // ("GBTC", 0.35m), // Crypto
      // ("DBP", 0.10m), // Metals
    });

    _protection = new Protection(Securities["SPY"], ProtectionAllocation);

    Schedule.On(
      DateRules.MonthStart(Targets[0].Security.Symbol),
      TimeRules.AfterMarketOpen(Targets[0].Security.Symbol, minutesAfterOpen: 10),
      delegate() { ShouldRebalance = true; }
    );
  }

  public override void OnData(Slice data) {
    if (ShouldRebalance) {
      var allValid = Targets.All(x => data.Bars.Keys.Contains(x.Security.Symbol));
      if (allValid) {
        var buyingPower = Portfolio.TotalPortfolioValue * (1 - FreeCash);
        var equityBuyingPower = buyingPower * EquityAllocation;

        Targets
          .Select(pair => {
            var (symbol, tgt) = pair;

            var close = data.Bars[symbol.Symbol].Close;
            var goal = (tgt * equityBuyingPower) / close;
            var current = Portfolio[symbol.Symbol].Quantity;
            var delta = (int)Math.Truncate(goal - current);

            return (symbol, close, current, goal, delta);
          })
          .Where(x => x.delta != 0)
          // Do the sells first then the buys
          .OrderBy(x => x.delta)
          .DoForEach(x => {
            Debug($"Rebal {x.symbol} @ ${x.close:F} from {x.current} to {x.goal}");
            MarketOrder(x.symbol, x.delta);
          });

        // We're done
        ShouldRebalance = false;
      }
    }

    _protection.ManageContract(this);
  }

  private void LoadWeights(List<(string Symbol, decimal Target)> Raw) {
    var total = Raw.Select(pair => pair.Target).Sum();
    Targets = Raw.Select(pair => {
      var (symbol, target) = pair;
      var security = AddEquity(symbol, Resolution.Daily);
      security.SetDataNormalizationMode(DataNormalizationMode.Raw);
      security.SetMarginModel(new PatternDayTradingMarginModel(4m, 4m));
      return (Security: security, Target: target / total);
    }).ToList();
  }
}
}