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