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