Overall Statistics
Total Trades
252
Average Win
1.63%
Average Loss
-0.79%
Compounding Annual Return
34.051%
Drawdown
27.500%
Expectancy
1.253
Net Profit
502.579%
Sharpe Ratio
1.309
Probabilistic Sharpe Ratio
69.421%
Loss Rate
26%
Win Rate
74%
Profit-Loss Ratio
2.05
Alpha
0
Beta
0
Annual Standard Deviation
0.188
Annual Variance
0.035
Information Ratio
1.309
Tracking Error
0.188
Treynor Ratio
0
Total Fees
$380.34
Estimated Strategy Capacity
$450000.00
Lowest Capacity Asset
SPY 323H68DJAL1JA|SPY R735QTJ8XC9X
using System;
using System.Collections.Generic;
using System.Linq;
using QLNet;
using QuantConnect.Brokerages;
using QuantConnect.Data;
using QuantConnect.Orders;
using QuantConnect.Securities;
using QuantConnect.Securities.Equity;
using QuantConnect.Util;
using Option = QuantConnect.Securities.Option.Option;

namespace QuantConnect.Algorithm.CSharp {
public class AllWeather : QCAlgorithm {
  private Protection _protection;

  public const decimal FreeCash = 0.02m;
  private const decimal EquityAllocation = 2m * 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(2016, 1, 1);
    SetCash(100000);

    SetSecurityInitializer(sec => {
      sec.SetDataNormalizationMode(DataNormalizationMode.Raw);
      sec.SetBuyingPowerModel(new PatternDayTradingMarginModel(2m, 4m)); // this is just true
    });

    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; }
    );

    Schedule.On(DateRules.EveryDay(), TimeRules.Midnight, () => {
      foreach (var (symbol, holding) in Portfolio) {
        string name;
        if (symbol.SecurityType == SecurityType.Option) {
          name = $"{symbol.Underlying} PUT";
        }
        else {
          name = symbol.Value;
        }
        Plot("Owned", name, holding.Quantity);
        Plot("Value", name, holding.HoldingsValue);
      }

      Plot("Balances", "Cash", Portfolio.Cash);
      Plot("Balances", "Value", Portfolio.TotalHoldingsValue);
    });
  }

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

        Targets
          .Select(pair => {
            var (sec, tgt) = pair;
            var delta = (int)Math.Truncate(tgt * buyingPower / sec.Close) - sec.Holdings.Quantity;
            return (Sec: sec, Delta: delta);
          })
          .Where(x => x.Delta != 0)
          // Do the sells first then the buys
          .OrderBy(x => x.Delta)
          .DoForEach(x => {
            Debug($"Rebal {x.Sec.Symbol} @ ${x.Sec.Close:F} from {x.Sec.Holdings.Quantity} we {x.Delta}");
            MarketOrder(x.Sec, x.Delta);
          });

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

    _protection.ManageContract(this);
  }

  /// Normalize the list of weights and add all symbols.
  private void LoadWeights(List<(string Symbol, decimal Target)> raw) {
    var total = raw.Select(pair => pair.Target).Sum();

    Targets = raw.Select(pair => (
      Security: AddEquity(pair.Symbol, Resolution.Daily),
      Target: pair.Target / total
    )).ToList();
  }
}

internal class Protection {
  public Security Underlying { get; }
  public decimal Allocation { 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 allocation,
    decimal profitTarget = 1.3m, decimal percentBelow = 0.4m,
    int minimumDte = 270, int maximumDte = 420, int exerciseAtDte = 180) {
    Underlying = underlying;
    Allocation = allocation;
    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);
          algo.Debug($"Pro liquidate {_contract.Holdings.Quantity} {_contract.Symbol} @ {_contract.BidPrice}");
          _contract = null;
        }
      }
      else {
        // var buyingPower = Allocation * algo.Portfolio.TotalPortfolioValue * (1 - AllWeather.FreeCash);
        // var close = _contract.AskPrice * _contract.ContractMultiplier;
        // var goal = (int)Math.Truncate(buyingPower / close);
        // algo.Debug($"Pro {_contract.Symbol} @ ${close:F} to {goal} with {buyingPower}");
        // algo.LimitOrder(_contract.Symbol, goal, close);
        algo.SetHoldings(_contract.Symbol, Allocation);
      }
    }
  }

  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();
  }
}
}