Skip to content

Commit

Permalink
Refactors Market Fill of Equity Fill Model (QuantConnect#5114)
Browse files Browse the repository at this point in the history
* Refactors Market Fill Model

Create `GetBidPrice` and `GetAskPrice` methods to request the most suitable price to fill market orders.

Fix unit tests to show that new implementation prevents using non-market data to fill the order.

* Addresses Peer Review …

Modifies EquityTickQuoteAdjustedModeRegressionAlgorithm to reflect changes in the market fill method. The previous implementation only looks for the last tick when it should look for the last tick of quote type, so current implementation is an improvement.
  • Loading branch information
AlexCatarino authored Jan 6, 2021
1 parent 33b58b0 commit c6d4888
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,11 @@ public override void OnData(Slice data)
{
{"Total Trades", "2"},
{"Average Win", "0%"},
{"Average Loss", "-0.01%"},
{"Compounding Annual Return", "-0.500%"},
{"Drawdown", "0.000%"},
{"Average Loss", "-0.12%"},
{"Compounding Annual Return", "-9.062%"},
{"Drawdown", "0.100%"},
{"Expectancy", "-1"},
{"Net Profit", "-0.006%"},
{"Net Profit", "-0.121%"},
{"Sharpe Ratio", "0"},
{"Probabilistic Sharpe Ratio", "0%"},
{"Loss Rate", "100%"},
Expand All @@ -103,12 +103,12 @@ public override void OnData(Slice data)
{"Tracking Error", "0.22"},
{"Treynor Ratio", "0"},
{"Total Fees", "$6.41"},
{"Fitness Score", "0.248"},
{"Fitness Score", "0.249"},
{"Kelly Criterion Estimate", "0"},
{"Kelly Criterion Probability Value", "0"},
{"Sortino Ratio", "79228162514264337593543950335"},
{"Return Over Maximum Drawdown", "-82.815"},
{"Portfolio Turnover", "0.497"},
{"Return Over Maximum Drawdown", "-79.031"},
{"Portfolio Turnover", "0.498"},
{"Total Insights Generated", "0"},
{"Total Insights Closed", "0"},
{"Total Insights Analysis Completed", "0"},
Expand All @@ -122,7 +122,7 @@ public override void OnData(Slice data)
{"Mean Population Magnitude", "0%"},
{"Rolling Averaged Population Direction", "0%"},
{"Rolling Averaged Population Magnitude", "0%"},
{"OrderListHash", "1213851303"}
{"OrderListHash", "-1760998125"}
};
}
}
167 changes: 154 additions & 13 deletions Common/Orders/Fills/EquityFillModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@
*/

using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Python;
using QuantConnect.Orders.Fees;
using QuantConnect.Securities;
using QuantConnect.Util;

namespace QuantConnect.Orders.Fills
{
Expand Down Expand Up @@ -116,17 +118,8 @@ public virtual OrderEvent MarketFill(Security asset, MarketOrder order)
// make sure the exchange is open/normal market hours before filling
if (!IsExchangeOpen(asset, false)) return fill;

var prices = GetPricesCheckingPythonWrapper(asset, order.Direction);
var pricesEndTimeUtc = prices.EndTime.ConvertToUtc(asset.Exchange.TimeZone);

// if the order is filled on stale (fill-forward) data, set a warning message on the order event
if (pricesEndTimeUtc.Add(Parameters.StalePriceTimeSpan) < order.Time)
{
fill.Message = $"Warning: fill at stale price ({prices.EndTime.ToStringInvariant()} {asset.Exchange.TimeZone})";
}

//Order [fill]price for a market order model is the current security price
fill.FillPrice = prices.Current;
// Define the last bid or ask time to set stale prices message
var endTime = DateTime.MinValue;
fill.Status = OrderStatus.Filled;

//Calculate the model slippage: e.g. 0.01c
Expand All @@ -136,13 +129,23 @@ public virtual OrderEvent MarketFill(Security asset, MarketOrder order)
switch (order.Direction)
{
case OrderDirection.Buy:
fill.FillPrice += slip;
//Order [fill]price for a buy market order model is the current security ask price
fill.FillPrice = GetAskPrice(asset, out endTime) + slip;
break;
case OrderDirection.Sell:
fill.FillPrice -= slip;
//Order [fill]price for a buy market order model is the current security bid price
fill.FillPrice = GetBidPrice(asset, out endTime) - slip;
break;
}

var endTimeUtc = endTime.ConvertToUtc(asset.Exchange.TimeZone);

// if the order is filled on stale (fill-forward) data, set a warning message on the order event
if (endTimeUtc.Add(Parameters.StalePriceTimeSpan) < order.Time)
{
fill.Message = $"Warning: fill at stale price ({endTime.ToStringInvariant()} {asset.Exchange.TimeZone})";
}

// assume the order completely filled
fill.FillQuantity = order.Quantity;

Expand Down Expand Up @@ -460,6 +463,144 @@ public virtual OrderEvent MarketOnCloseFill(Security asset, MarketOnCloseOrder o
return fill;
}

/// <summary>
/// Get data types the Security is subscribed to
/// </summary>
/// <param name="asset">Security which has subscribed data types</param>
private HashSet<Type> GetSubscribedTypes(Security asset)
{
var subscribedTypes = Parameters
.ConfigProvider
.GetSubscriptionDataConfigs(asset.Symbol)
.ToHashSet(x => x.Type);

if (subscribedTypes.Count == 0)
{
throw new InvalidOperationException($"Cannot perform fill for {asset.Symbol} because no data subscription were found.");
}

return subscribedTypes;
}

/// <summary>
/// Get current ask price for subscribed data
/// This method will try to get the most recent ask price data, so it will try to get tick quote first, then quote bar.
/// If no quote, tick or bar, is available (e.g. hourly data), use trade data with preference to tick data.
/// </summary>
/// <param name="asset">Security which has subscribed data types</param>
/// <param name="endTime">Timestamp of the most recent data type</param>
private decimal GetAskPrice(Security asset, out DateTime endTime)
{
var subscribedTypes = GetSubscribedTypes(asset);

List<Tick> ticks = null;
var isTickSubscribed = subscribedTypes.Contains(typeof(Tick));

if (isTickSubscribed)
{
ticks = asset.Cache.GetAll<Tick>().ToList();

var quote = ticks.LastOrDefault(x => x.TickType == TickType.Quote && x.AskPrice > 0);
if (quote != null)
{
endTime = quote.EndTime;
return quote.AskPrice;
}
}

if (subscribedTypes.Contains(typeof(QuoteBar)))
{
var quoteBar = asset.Cache.GetData<QuoteBar>();
if (quoteBar != null)
{
endTime = quoteBar.EndTime;
return quoteBar.Ask?.Close ?? quoteBar.Close;
}
}

if (isTickSubscribed)
{
var trade = ticks.LastOrDefault(x => x.TickType == TickType.Trade && x.Price > 0);
if (trade != null)
{
endTime = trade.EndTime;
return trade.Price;
}
}

if (subscribedTypes.Contains(typeof(TradeBar)))
{
var tradeBar = asset.Cache.GetData<TradeBar>();
if (tradeBar != null)
{
endTime = tradeBar.EndTime;
return tradeBar.Close;
}
}

throw new InvalidOperationException($"Cannot get ask price to perform fill for {asset.Symbol} because no market data subscription were found.");
}

/// <summary>
/// Get current bid price for subscribed data
/// This method will try to get the most recent bid price data, so it will try to get tick quote first, then quote bar.
/// If no quote, tick or bar, is available (e.g. hourly data), use trade data with preference to tick data.
/// </summary>
/// <param name="asset">Security which has subscribed data types</param>
/// <param name="endTime">Timestamp of the most recent data type</param>
private decimal GetBidPrice(Security asset, out DateTime endTime)
{
var subscribedTypes = GetSubscribedTypes(asset);

List<Tick> ticks = null;
var isTickSubscribed = subscribedTypes.Contains(typeof(Tick));

if (isTickSubscribed)
{
ticks = asset.Cache.GetAll<Tick>().ToList();

var quote = ticks.LastOrDefault(x => x.TickType == TickType.Quote && x.BidPrice > 0);
if (quote != null)
{
endTime = quote.EndTime;
return quote.BidPrice;
}
}

if (subscribedTypes.Contains(typeof(QuoteBar)))
{
var quoteBar = asset.Cache.GetData<QuoteBar>();
if (quoteBar != null)
{
endTime = quoteBar.EndTime;
return quoteBar.Bid?.Close ?? quoteBar.Close;
}
}

if (isTickSubscribed)
{
var trade = ticks.LastOrDefault(x => x.TickType == TickType.Trade && x.Price > 0);
if (trade != null)
{
endTime = trade.EndTime;
return trade.Price;
}
}

if (subscribedTypes.Contains(typeof(TradeBar)))
{
var tradeBar = asset.Cache.GetData<TradeBar>();
if (tradeBar != null)
{
endTime = tradeBar.EndTime;
return tradeBar.Close;
}
}

throw new InvalidOperationException($"Cannot get bid price to perform fill for {asset.Symbol} because no market data subscription were found.");
}


/// <summary>
/// This is required due to a limitation in PythonNet to resolved
/// overriden methods. <see cref="GetPrices"/>
Expand Down
49 changes: 39 additions & 10 deletions Tests/Common/Orders/Fills/EquityFillModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public void PerformsMarketFillBuy()
{
var model = new EquityFillModel();
var order = new MarketOrder(Symbols.SPY, 100, Noon);
var config = CreateTradeBarConfig(Symbols.SPY);
var config = CreateQuoteBarConfig(Symbols.SPY);
var security = new Security(
SecurityExchangeHoursTests.CreateUsEquitySecurityExchangeHours(),
config,
Expand All @@ -52,13 +52,23 @@ public void PerformsMarketFillBuy()
security.SetLocalTimeKeeper(TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 101.123m));

var fill = model.Fill(new FillModelParameters(
var parameters = new FillModelParameters(
security,
order,
new MockSubscriptionDataConfigProvider(config),
Time.OneHour)).OrderEvent;
Time.OneHour);

// IndicatorDataPoint is not market data
Assert.Throws<InvalidOperationException>(() => model.Fill(parameters),
$"Cannot get ask price to perform fill for {security.Symbol} because no market data subscription were found.");

var bidBar = new Bar(101.123m, 101.123m, 101.123m, 101.123m);
var askBar = new Bar(101.234m, 101.234m, 101.234m, 101.234m);
security.SetMarketPrice(new QuoteBar(Noon, Symbols.SPY, bidBar, 0, askBar, 0));

var fill = model.Fill(parameters).OrderEvent;
Assert.AreEqual(order.Quantity, fill.FillQuantity);
Assert.AreEqual(security.Price, fill.FillPrice);
Assert.AreEqual(askBar.Close, fill.FillPrice);
Assert.AreEqual(OrderStatus.Filled, fill.Status);
}

Expand All @@ -67,7 +77,7 @@ public void PerformsMarketFillSell()
{
var model = new EquityFillModel();
var order = new MarketOrder(Symbols.SPY, -100, Noon);
var config = CreateTradeBarConfig(Symbols.SPY);
var config = CreateQuoteBarConfig(Symbols.SPY);
var security = new Security(
SecurityExchangeHoursTests.CreateUsEquitySecurityExchangeHours(),
config,
Expand All @@ -80,13 +90,23 @@ public void PerformsMarketFillSell()
security.SetLocalTimeKeeper(TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 101.123m));

var fill = model.Fill(new FillModelParameters(
var parameters = new FillModelParameters(
security,
order,
new MockSubscriptionDataConfigProvider(config),
Time.OneHour)).OrderEvent;
Time.OneHour);

// IndicatorDataPoint is not market data
Assert.Throws<InvalidOperationException>(() => model.Fill(parameters),
$"Cannot get bid price to perform fill for {security.Symbol} because no market data subscription were found.");

var bidBar = new Bar(101.123m, 101.123m, 101.123m, 101.123m);
var askBar = new Bar(101.234m, 101.234m, 101.234m, 101.234m);
security.SetMarketPrice(new QuoteBar(Noon, Symbols.SPY, bidBar, 0, askBar, 0));

var fill = model.Fill(parameters).OrderEvent;
Assert.AreEqual(order.Quantity, fill.FillQuantity);
Assert.AreEqual(security.Price, fill.FillPrice);
Assert.AreEqual(bidBar.Close, fill.FillPrice);
Assert.AreEqual(OrderStatus.Filled, fill.Status);
}

Expand Down Expand Up @@ -735,7 +755,7 @@ public void MarketOrderFillWithStalePriceHasWarningMessage()
{
var model = new EquityFillModel();
var order = new MarketOrder(Symbols.SPY, -100, Noon.ConvertToUtc(TimeZones.NewYork).AddMinutes(61));
var config = CreateTradeBarConfig(Symbols.SPY);
var config = CreateTickConfig(Symbols.SPY);
var security = new Security(
SecurityExchangeHoursTests.CreateUsEquitySecurityExchangeHours(),
config,
Expand All @@ -746,7 +766,7 @@ public void MarketOrderFillWithStalePriceHasWarningMessage()
new SecurityCache()
);
security.SetLocalTimeKeeper(TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 101.123m));
security.SetMarketPrice(new Tick(Noon, Symbols.SPY, 101.123m, 101.456m));

var fill = model.Fill(new FillModelParameters(
security,
Expand Down Expand Up @@ -804,6 +824,15 @@ public void PriceReturnsQuoteBarsIfPresent(OrderDirection orderDirection, decima
Assert.AreEqual(expected, result.Close);
}

private SubscriptionDataConfig CreateTickConfig(Symbol symbol)
{
return new SubscriptionDataConfig(typeof(Tick), symbol, Resolution.Tick, TimeZones.NewYork, TimeZones.NewYork, true, true, false);
}

private SubscriptionDataConfig CreateQuoteBarConfig(Symbol symbol)
{
return new SubscriptionDataConfig(typeof(QuoteBar), symbol, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, true, false);
}

private SubscriptionDataConfig CreateTradeBarConfig(Symbol symbol)
{
Expand Down

0 comments on commit c6d4888

Please sign in to comment.