| Overall Statistics |
|
Total Orders 1480 Average Win 0.15% Average Loss -0.13% Compounding Annual Return 0.395% Drawdown 7.700% Expectancy 0.018 Start Equity 100000 End Equity 101490 Net Profit 1.490% Sharpe Ratio -1.678 Sortino Ratio -1.675 Probabilistic Sharpe Ratio 0.012% Loss Rate 53% Win Rate 47% Profit-Loss Ratio 1.18 Alpha -0.05 Beta 0.051 Annual Standard Deviation 0.027 Annual Variance 0.001 Information Ratio -0.854 Tracking Error 0.14 Treynor Ratio -0.908 Total Fees $0.00 Estimated Strategy Capacity $460000.00 Lowest Capacity Asset SPXW YZKQQ3P604A6|SPX 31 Portfolio Turnover 0.55% Drawdown Recovery 1065 |
#region imports
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Drawing;
using QuantConnect;
using QuantConnect.Algorithm.Framework;
using QuantConnect.Algorithm.Framework.Selection;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Algorithm.Framework.Portfolio.SignalExports;
using QuantConnect.Algorithm.Framework.Execution;
using QuantConnect.Algorithm.Framework.Risk;
using QuantConnect.Algorithm.Selection;
using QuantConnect.Api;
using QuantConnect.Parameters;
using QuantConnect.Benchmarks;
using QuantConnect.Brokerages;
using QuantConnect.Commands;
using QuantConnect.Configuration;
using QuantConnect.Util;
using QuantConnect.Interfaces;
using QuantConnect.Algorithm;
using QuantConnect.Indicators;
using QuantConnect.Data;
using QuantConnect.Data.Auxiliary;
using QuantConnect.Data.Consolidators;
using QuantConnect.Data.Custom;
using QuantConnect.Data.Custom.IconicTypes;
using QuantConnect.DataSource;
using QuantConnect.Data.Fundamental;
using QuantConnect.Data.Market;
using QuantConnect.Data.Shortable;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Notifications;
using QuantConnect.Orders;
using QuantConnect.Orders.Fees;
using QuantConnect.Orders.Fills;
using QuantConnect.Orders.OptionExercise;
using QuantConnect.Orders.Slippage;
using QuantConnect.Orders.TimeInForces;
using QuantConnect.Python;
using QuantConnect.Scheduling;
using QuantConnect.Securities;
using QuantConnect.Securities.Equity;
using QuantConnect.Securities.Future;
using QuantConnect.Securities.Option;
using QuantConnect.Securities.Positions;
using QuantConnect.Securities.Forex;
using QuantConnect.Securities.Crypto;
using QuantConnect.Securities.CryptoFuture;
using QuantConnect.Securities.IndexOption;
using QuantConnect.Securities.Interfaces;
using QuantConnect.Securities.Volatility;
using QuantConnect.Storage;
using QuantConnect.Statistics;
using QCAlgorithmFramework = QuantConnect.Algorithm.QCAlgorithm;
using QCAlgorithmFrameworkBridge = QuantConnect.Algorithm.QCAlgorithm;
using Calendar = QuantConnect.Data.Consolidators.Calendar;
using Futures = QuantConnect.Securities.Futures;
#endregion
namespace QuantConnect.Algorithm.CSharp
{
/// <summary>
/// SPX 0DTE long call strategy:
/// - 15-minute SPX bars, EMA(55)
/// - When the 15-min close crosses above the EMA55, buy 1 same-day-expiry (0DTE)
/// SPXW call with delta closest to 0.30
/// - Take profit +20% of premium, stop loss -20% of premium (checked each minute)
/// - Flatten at 15:55 ET if neither level was hit
/// </summary>
public class Newproject : QCAlgorithm
{
private Symbol _spx;
private Symbol _spxwCanonical;
private ExponentialMovingAverage _ema;
private decimal _prevClose;
private decimal _prevEma;
private bool _hasPrev;
private DateTime _pendingEntryUntil = DateTime.MinValue;
private Symbol _heldOption;
private decimal _entryPrice;
private static readonly TimeSpan LastEntryTime = new TimeSpan(15, 30, 0);
private static readonly TimeSpan FlattenTime = new TimeSpan(15, 55, 0);
private int _barCount;
private int _signalCount;
private int _attemptCount;
private int _chainLogCount;
public override void Initialize()
{
SetStartDate(2022, 5, 1);
SetEndDate(2026, 1, 31);
SetCash(100000);
var index = AddIndex("SPX", Resolution.Minute);
_spx = index.Symbol;
var option = AddIndexOption(_spx, "SPXW", Resolution.Minute);
// 0DTE calls only, strikes from 5 below to 30 above ATM (0.30-delta calls are OTM)
option.SetFilter(u => u.Expiration(0, 0).CallsOnly().Strikes(-5, 30));
option.PriceModel = OptionPriceModels.BlackScholes();
_spxwCanonical = option.Symbol;
_ema = new ExponentialMovingAverage(55);
Consolidate(_spx, TimeSpan.FromMinutes(15), OnFifteenMinuteBar);
SetWarmUp(TimeSpan.FromDays(10));
}
private void OnFifteenMinuteBar(TradeBar bar)
{
_barCount++;
_ema.Update(bar.EndTime, bar.Close);
var emaValue = _ema.Current.Value;
// NOTE: SPX index data is stamped in Chicago time; algorithm Time is New York.
// Use algorithm Time for all session-time logic.
if (_ema.IsReady && _hasPrev && !IsWarmingUp
&& _prevClose <= _prevEma && bar.Close > emaValue
&& !Portfolio.Invested
&& Time.TimeOfDay <= LastEntryTime)
{
// signal: cross above EMA55 -> try to enter on the next few minute bars
_pendingEntryUntil = Time.AddMinutes(5);
_signalCount++;
}
_prevClose = bar.Close;
_prevEma = emaValue;
_hasPrev = true;
}
public override void OnData(Slice slice)
{
if (IsWarmingUp) return;
// ---- manage open position ----
if (_heldOption != null)
{
if (!Portfolio[_heldOption].Invested)
{
_heldOption = null;
}
else
{
var price = Securities[_heldOption].Price;
if (price > 0 && _entryPrice > 0)
{
if (price >= _entryPrice * 1.20m)
{
Liquidate(_heldOption, "take profit +20%");
_heldOption = null;
return;
}
if (price <= _entryPrice * 0.80m)
{
Liquidate(_heldOption, "stop loss -20%");
_heldOption = null;
return;
}
}
if (Time.TimeOfDay >= FlattenTime)
{
Liquidate(_heldOption, "eod flatten before 0DTE expiry");
_heldOption = null;
}
return;
}
}
// ---- entry ----
if (Time >= _pendingEntryUntil) return;
if (!slice.OptionChains.TryGetValue(_spxwCanonical, out var chain))
{
_attemptCount++;
if (_attemptCount <= 20) Debug($"ATTEMPT {Time}: no option chain in slice");
return;
}
var todaysCalls = chain
.Where(c => c.Right == OptionRight.Call && c.Expiry.Date == Time.Date)
.Where(c => c.AskPrice > 0)
.ToList();
var candidate = todaysCalls
.Select(c => new { Contract = c, Delta = c.Greeks.Delta })
.Where(x => x.Delta > 0.05m && x.Delta < 0.95m)
.OrderBy(x => Math.Abs(x.Delta - 0.30m))
.FirstOrDefault();
if (candidate == null) return;
_pendingEntryUntil = DateTime.MinValue;
MarketOrder(candidate.Contract.Symbol, 1,
tag: $"entry: delta={candidate.Delta:F2} strike={candidate.Contract.Strike}");
}
public override void OnOrderEvent(OrderEvent orderEvent)
{
if (orderEvent.Status != OrderStatus.Filled) return;
if (orderEvent.Direction == OrderDirection.Buy
&& orderEvent.Symbol.SecurityType == SecurityType.IndexOption)
{
_heldOption = orderEvent.Symbol;
_entryPrice = orderEvent.FillPrice;
}
}
}
}