Overall Statistics
Total Trades
252
Average Win
1.44%
Average Loss
-1.85%
Compounding Annual Return
29.461%
Drawdown
28.600%
Expectancy
0.358
Net Profit
386.724%
Sharpe Ratio
1.14
Probabilistic Sharpe Ratio
53.788%
Loss Rate
23%
Win Rate
77%
Profit-Loss Ratio
0.77
Alpha
0
Beta
0
Annual Standard Deviation
0.19
Annual Variance
0.036
Information Ratio
1.14
Tracking Error
0.19
Treynor Ratio
0
Total Fees
$359.90
Estimated Strategy Capacity
$460000.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;
  private decimal _protectionValue;

  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 = symbol.SecurityType == SecurityType.Option ? $"{symbol.Underlying} PUT" : symbol.Value;
        Plot("Owned", name, holding.Quantity);
        Plot("Value", name, holding.HoldingsValue);
      }

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

  public override void OnOrderEvent(OrderEvent ev) {
    if (ev.Status == OrderStatus.Filled) {
      if (ev.Symbol.ID.SecurityType == SecurityType.Option) {
        _protectionValue -= ev.FillQuantity * ev.FillPrice * 100m; // hardcode this option multiplier
      }
    }
  }

  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 price = _contract.AskPrice * _contract.ContractMultiplier;
        var goal = (int)Math.Truncate(buyingPower / price);
        algo.Debug($"Pro {_contract.Symbol} @ ${price:F} to {goal} with {buyingPower}");
        algo.LimitOrder(_contract.Symbol, goal, price);
        // 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();
  }
}
}