| Overall Statistics |
|
Total Trades 96 Average Win 0.48% Average Loss -0.09% Compounding Annual Return 9.423% Drawdown 0.900% Expectancy 0.576 Net Profit 2.540% Sharpe Ratio 1.815 Probabilistic Sharpe Ratio 71.167% Loss Rate 75% Win Rate 25% Profit-Loss Ratio 5.31 Alpha 0.028 Beta 0.116 Annual Standard Deviation 0.028 Annual Variance 0.001 Information Ratio -1.79 Tracking Error 0.082 Treynor Ratio 0.438 Total Fees $240.00 Estimated Strategy Capacity $1400000.00 Lowest Capacity Asset SPY XQ55MD94802U|SPY R735QTJ8XC9X |
using System;
using QuantConnect.Data;
using QuantConnect.Securities;
using QuantConnect.Securities.Option;
using QuantConnect.Securities.Option.StrategyMatcher;
namespace QuantConnect {
public class SpyATMSpread: QCAlgorithm {
const int INITIAL_CAPITAL = 50000; //baseline capital
const int NUM_CONTRACTS = 10; //total number of contracts per position
private decimal lastBuyPrice = 0; //price of last transaction buy side
private decimal lastSellPrice = 0; //price of last transaction sell side
private decimal posPrice = 0; //purchase price of position
private decimal PortfolioVal = 0;
private decimal OpenPrice = 0; //open price of day, used later
private decimal FirstCandleHighPrice = 0;
private decimal FirstCandleLowPrice = 0;
private decimal SecondCandleClosePrice = 0;
private Symbol spySymbol;
private List<OptionContract> Contracts; //contracts updated each min
public override void Initialize() {
SetStartDate(2021,4, 5); //start date
SetEndDate(2021, 7, 15); //end date
SetCash(INITIAL_CAPITAL);
SetWarmUp(TimeSpan.FromDays(30), Resolution.Minute);
Option option = (Option) null;
option = AddOption("SPY", Resolution.Minute); //add spy option to universe
option.SetFilter(universe => from symbol in universe //filter out to 3 days of options
.WeeklysOnly()
.Strikes(-20,20)
.Expiration(TimeSpan.Zero, TimeSpan.FromDays(3))
select symbol);
spySymbol = option.Symbol;
option.PriceModel = OptionPriceModels.BjerksundStensland(); //
var optionMuliplier = option.ContractMultiplier;
Schedule.On( DateRules.Every(DayOfWeek.Monday, DayOfWeek.Wednesday, DayOfWeek.Friday), TimeRules.At(10,02),
() => ExecuteTrade(spySymbol,0, NUM_CONTRACTS, "") );
Schedule.On( DateRules.EveryDay(), TimeRules.At(9,30), () => PortfolioVal = Portfolio.TotalPortfolioValue);
//Schedule.On( DateRules.MonthEnd(), TimeRules.At(9,00), ListHoldings );
//Schedule.On( DateRules.EveryDay(), TimeRules.At(9,30), () => Debug("Time: " + Time + "Total Value: " + Portfolio.TotalPortfolioValue));
//Schedule.On( DateRules.EveryDay(), TimeRules.At(13,58), () => Debug("Time: " + Time + "Total Value: " + Portfolio.TotalPortfolioValue));
//Schedule.On( DateRules.EveryDay(), TimeRules.At(14,02), () => Debug("Time: " + Time + "Total Value: " + Portfolio.TotalPortfolioValue));
}
public override void OnData(Slice slice) {
OptionChain chain;
CaptureProfitLoss();
if ( slice.OptionChains.TryGetValue(spySymbol, out chain) ) {
if (chain.Count() == 0)
return;
//available contracts to build spread
Contracts = chain
.OrderBy(c => c.Expiry)
.ToList();
//Debug("PutContractsSize: " + put_Contracts.Count());
if (Contracts.Count == 0)
return;
} else {
return;
}
//record first 15 min high and low.
if (Time.Hour == 9 && Time.Minute == 31) {
FirstCandleLowPrice = Securities[spySymbol.Underlying].Price;
FirstCandleHighPrice = FirstCandleLowPrice;
OpenPrice = FirstCandleLowPrice;
//Debug("Open Price: " + OpenPrice + " Time: " + Time);
//record second candle high and low
} else if (Time.Hour == 9 && Time.Minute > 30 && Time.Minute < 45) {
if ( FirstCandleHighPrice < Securities[spySymbol.Underlying].Price) {
FirstCandleHighPrice = Securities[spySymbol.Underlying].Price;
} else if ( FirstCandleLowPrice > Securities[spySymbol.Underlying].Price) {
FirstCandleLowPrice = Securities[spySymbol.Underlying].Price;
}
//Debug("First Candle: " + FirstCandleClosePrice + " Time: " + Time);
//close price of second candle
} else if (Time.Hour == 10 && Time.Minute == 0) {
SecondCandleClosePrice = Securities[spySymbol.Underlying].Price;
//Debug("Second Candle: " + SecondCandleClosePrice + " Time: " + Time);
} else if ( Time.Hour == 11 ) {
FirstCandleHighPrice = 0;
SecondCandleClosePrice = 0;
}
if(IsMarketOpen(spySymbol)) {
if(Time.Hour == 15 && Time.Minute == 30) {
Liquidate(null, "End of Day Close");
}
}
}
public override void OnOrderEvent(OrderEvent orderEvent){
var order = Transactions.GetOrderById(orderEvent.OrderId);
var quan = orderEvent.FillQuantity;
var fillPrice = orderEvent.FillPrice;
var sym = orderEvent.Symbol;
//detect order purchase pricing
if (orderEvent.Status != OrderStatus.Filled){
//Debug("Order failed: " + sym + " Time: " + Time + " Quan: " + orderEvent.Quantity + " Portfolio: " + Portfolio.TotalPortfolioValue);
return;
} else {
if (orderEvent.Direction == OrderDirection.Buy) {
lastBuyPrice = fillPrice;
} else if (orderEvent.Direction == OrderDirection.Sell) {
lastSellPrice = fillPrice;
}
//ListHoldings();
//Debug("Order failed: " + sym + " Time: " + Time + " Quan: " + orderEvent.Quantity + " UnderlyingPrice: " + Securities[sym.Underlying].Price + " Portfolio: " + Portfolio.TotalPortfolioValue);
}
}
public void ExecuteTrade(Symbol sym, int duration, int quan, string tag) {
decimal CurPrice = Securities[ spySymbol.Underlying ].Price;
//use linq to create possible order
if (Contracts != null && Contracts.Count != 0 ) {
var spread = (from short_p in Contracts
join short_c in Contracts on short_p.UnderlyingSymbol equals short_c.UnderlyingSymbol
join long_p in Contracts on short_p.UnderlyingSymbol equals long_p.UnderlyingSymbol
join long_c in Contracts on short_p.UnderlyingSymbol equals long_c.UnderlyingSymbol
where short_p.Expiry == short_c.Expiry &&
short_p.Expiry == long_p.Expiry &&
long_p.Expiry == long_c.Expiry &&
(short_p.Expiry.Date-Time.Date).Days == duration &&
short_p.Strike == Math.Floor(CurPrice) && //just out of the money puts
short_c.Strike == Math.Ceiling(CurPrice) && //just out of the money calls
long_p.Strike == short_p.Strike - 5 && //5 point wide spread
long_c.Strike == short_c.Strike + 5 && //5 point wide spread
short_p.Right == OptionRight.Put &&
long_p.Right == OptionRight.Put &&
long_c.Right == OptionRight.Call &&
short_c.Right == OptionRight.Call
orderby Math.Abs((short_p.Expiry.Date-Time.Date).Days - duration),
Math.Abs(short_p.Strike - CurPrice)
select new Tuple<OptionContract, OptionContract, OptionContract, OptionContract>( short_p, long_p, short_c, long_c ) )
.FirstOrDefault();
//place an order if high low criteria met
if ( IsMarketOpen(sym) ) {
if (spread != null) {
if (spread.Item1 != null && spread.Item2 != null && spread.Item3 != null && spread.Item4 != null) {
//build order for put spread, bullish
if (SecondCandleClosePrice > FirstCandleHighPrice) {
Order(spread.Item1.Symbol, -quan, false, ("Price " + CurPrice + " Low: " + FirstCandleLowPrice + " High: " + FirstCandleHighPrice) );
Order(spread.Item2.Symbol, quan );
posPrice = lastSellPrice - lastBuyPrice;
}
//build order for call spread, bearish
else if (SecondCandleClosePrice < FirstCandleLowPrice ) {
Order(spread.Item3.Symbol, -quan, false, ("Price " + CurPrice + " Low: " + FirstCandleLowPrice + " High: " + FirstCandleHighPrice) );
Order(spread.Item4.Symbol, quan );
posPrice = lastSellPrice - lastBuyPrice;
}
} else {
Debug("No Contracts in field");
}
Debug("Position Price: " + posPrice);
}
}
}
}
public void CaptureProfitLoss(){
Dictionary<Symbol, SecurityHolding> option_invested = Portfolio
.Where(x => x.Value.Invested == true &&
x.Value.Type == SecurityType.Option )
.OrderBy(x => x.Key.ID.StrikePrice)
.Reverse().ToDictionary();
decimal value = 0;
decimal profit = 0;
decimal credit = 0;
decimal cost = 0;
decimal pandl = 0;
//check if we have open positions
if (option_invested.Count > 0) {
//iterate through option holdings
foreach(var pos in option_invested) {
pandl += Portfolio[pos.Key].TotalCloseProfit();
if (Portfolio[pos.Key].IsShort) {
value += Portfolio[pos.Key].Price;
cost += Portfolio[pos.Key].AveragePrice;
} else if (Portfolio[pos.Key].IsLong) {
value -= Portfolio[pos.Key].Price;
cost -= Portfolio[pos.Key].AveragePrice;
}
//profit += Portfolio[pos.Key].UnrealizedProfit;
//cost += Portfolio[pos.Key].HoldingsCost;
//value += Portfolio[pos.Key].TotalCloseProfit();
}
//close for profit at 50%
if ( cost >= value * 2M ){
//Debug("Take Profit " + Time);
Liquidate(null, "Take Profit");
} // close for loss at ~70 loss
else if ( pandl <= -70M ) {
//Debug("Stop loss: " + Time);
Liquidate(null, "Stop Loss" );
}
//Debug("Position Price: " + posPrice + " Value: " + value);
}
}
//Are you holding any options of symbol
public bool isInvested(Symbol sym) {
Dictionary<Symbol, SecurityHolding> option_invested = Portfolio
.Where(x => x.Value.Symbol.Underlying == sym.Underlying &&
x.Value.Type == SecurityType.Option &&
x.Value.Invested == true)
.ToDictionary();
if (option_invested.Count() > 0)
return true;
return false;
}
public bool isMarketOpenDay(Symbol sym, int d) {
var hours = Securities[sym.Canonical].Exchange.Hours;
var todayClose = hours.GetNextMarketClose(Time.Date, false);
if (hours.IsDateOpen(Time.Date.AddDays(d))){
var tomorrowOpen = hours.GetNextMarketOpen(Time.Date.AddDays(d), false);
var tomorrowClose = hours.GetNextMarketClose(Time.Date.AddDays(d), false);
return true;
}
return false;
}
public void ListHoldings() {
Dictionary<Symbol, SecurityHolding> option_invested = Portfolio
.Where(x => x.Value.Invested == true )
.OrderBy(x => x.Key)
.Reverse().ToDictionary();
foreach(KeyValuePair<Symbol, SecurityHolding> entry in option_invested) {
Log(entry.Value.Symbol + " Quan: " + entry.Value.Quantity + " Margin: " + Portfolio.MarginRemaining + " Time: " + Time);
}
Log("____________");
}
public decimal MidPrice(OptionContract s, OptionContract l) {
return ((s.AskPrice - l.AskPrice) + (s.BidPrice - l.BidPrice)) / 2;
}
}
}