Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Future Stop Market Order in Out Market Hours #7678

Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System;
using System.Linq;
using QuantConnect.Data;
using QuantConnect.Orders;
using QuantConnect.Securities;
using QuantConnect.Interfaces;
using QuantConnect.Data.Market;
using System.Collections.Generic;
using QuantConnect.Securities.Future;

namespace QuantConnect.Algorithm.CSharp
{
/// <summary>
/// Continuous Futures Regression algorithm.
/// Asserting the behavior of stop market order <see cref="StopMarketOrder"/> in extended market hours
/// <seealso cref="Data.UniverseSelection.UniverseSettings.ExtendedMarketHours"/>
/// </summary>
public class FutureStopMarketOrderOnExtendedHoursRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
{
private OrderTicket _ticket;
private Future _SP500EMini;
public override void Initialize()
{
SetStartDate(2013, 10, 6);
SetEndDate(2013, 10, 12);

_SP500EMini = AddFuture(Futures.Indices.SP500EMini, Resolution.Minute, extendedMarketHours: true);

Schedule.On(DateRules.EveryDay(), TimeRules.At(19, 0), () =>
{
MarketOrder(_SP500EMini.Mapped, 1);
_ticket = StopMarketOrder(_SP500EMini.Mapped, -1, _SP500EMini.Price * 1.1m);
});
}

/// <summary>
/// Data Event Handler: receiving all subscription data in a single event
/// </summary>
/// <param name="slice">The current slice of data keyed by symbol string</param>
public override void OnData(Slice slice)
{
if (_ticket == null || _ticket.Status != OrderStatus.Submitted)
{
return;
}

var stopPrice = _ticket.Get(OrderField.StopPrice);
var bar = Securities[_ticket.Symbol].Cache.GetData<TradeBar>();
}

/// <summary>
/// Order fill event handler. On an order fill update the resulting information is passed to this method.
/// </summary>
/// <param name="orderEvent">Order event details containing details of the events</param>
public override void OnOrderEvent(OrderEvent orderEvent)
{
if (orderEvent == null)
{
return;
}

if (Transactions.GetOrderById(orderEvent.OrderId).Type != OrderType.StopMarket)
{
return;
}

if (orderEvent.Status == OrderStatus.Filled)
{
var time = MarketHoursDatabase.GetExchangeHours(_SP500EMini.SubscriptionDataConfig);

if (!time.IsOpen(orderEvent.UtcTime, _SP500EMini.IsExtendedMarketHours))
{
throw new Exception($"The Exchange hours was closed, verify 'extendedMarketHours' flag in {nameof(Initialize)} when added new security(ies).");
}
}
}

public override void OnEndOfAlgorithm()
{
var stopMarketOrders = Transactions.GetOrders(x => x is StopMarketOrder);

if (stopMarketOrders.Any(x => x.Status != OrderStatus.Filled))
{
throw new Exception("The Algorithms was not handled any StopMarketOrders");
}
}

/// <summary>
/// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm.
/// </summary>
public bool CanRunLocally { get; } = true;

/// <summary>
/// This is used by the regression test system to indicate which languages this algorithm is written in.
/// </summary>
public Language[] Languages { get; } = { Language.CSharp, Language.Python };

/// <summary>
/// Data Points count of all time slices of algorithm
/// </summary>
public long DataPoints => 75955;

/// <summary>
/// Data Points count of the algorithm history
/// </summary>
public int AlgorithmHistoryDataPoints => 0;

/// <summary>
/// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
/// </summary>
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
{
{"Total Trades", "10"},
{"Average Win", "0%"},
{"Average Loss", "-0.02%"},
{"Compounding Annual Return", "-6.736%"},
{"Drawdown", "0.100%"},
{"Expectancy", "-1"},
{"Net Profit", "-0.109%"},
{"Sharpe Ratio", "-22.29"},
{"Sortino Ratio", "-26.651"},
{"Probabilistic Sharpe Ratio", "0.016%"},
{"Loss Rate", "100%"},
{"Win Rate", "0%"},
{"Profit-Loss Ratio", "0"},
{"Alpha", "-0.05"},
{"Beta", "-0.006"},
{"Annual Standard Deviation", "0.002"},
{"Annual Variance", "0"},
{"Information Ratio", "-2.76"},
{"Tracking Error", "0.215"},
{"Treynor Ratio", "8.829"},
{"Total Fees", "$21.50"},
{"Estimated Strategy Capacity", "$3400000.00"},
{"Lowest Capacity Asset", "ES VMKLFZIH2MTD"},
{"Portfolio Turnover", "138.95%"},
{"OrderListHash", "c85997dd5b7d9acda46ac9d11dd1a039"}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from AlgorithmImports import *
from QuantConnect import Orders

# <summary>
# This example demonstrates how to create future 'stopMarketOrder' in extended Market Hours time
# </summary>

class FutureStopMarketOrderOnExtendedHoursRegressionAlgorithm(QCAlgorithm):
# Keep new created instance of stopMarketOrder
stopMarketTicket = None
SP500EMini = None

# Initialize the Algorithm and Prepare Required Data
def Initialize(self):
self.SetStartDate(2013, 10, 6)
self.SetEndDate(2013, 10, 12)

# Add mini SP500 future with extended Market hours flag
self.SP500EMini = self.AddFuture(Futures.Indices.SP500EMini, Resolution.Minute, extendedMarketHours=True)

# Init new schedule event with params: everyDay, 19:00:00 PM, what should to do
self.Schedule.On(self.DateRules.EveryDay(),self.TimeRules.At(19, 0),self.MakeMarketAndStopMarketOrder)

# This method is opened 2 new orders by scheduler
def MakeMarketAndStopMarketOrder(self):
self.MarketOrder(self.SP500EMini.Mapped, 1)
self.stopMarketTicket = self.StopMarketOrder(self.SP500EMini.Mapped, -1, self.SP500EMini.Price * 1.1)

# New Data Event handler receiving all subscription data in a single event
def OnData(self, slice):
if (self.stopMarketTicket == None or self.stopMarketTicket.Status != OrderStatus.Submitted):
return None

self.stopPrice = self.stopMarketTicket.Get(OrderField.StopPrice)
self.bar = self.Securities[self.stopMarketTicket.Symbol].Cache.GetData()

# An order fill update the resulting information is passed to this method.
def OnOrderEvent(self, orderEvent):
if orderEvent is None:
return None

if self.Transactions.GetOrderById(orderEvent.OrderId).Type is not OrderType.StopMarket:
return None

if orderEvent.Status == OrderStatus.Filled:
# Get Exchange Hours for specific security
exchangeHours = self.MarketHoursDatabase.GetExchangeHours(self.SP500EMini.SubscriptionDataConfig)

# Validate, Exchange is opened explicitly
if (not exchangeHours.IsOpen(orderEvent.UtcTime, self.SP500EMini.IsExtendedMarketHours)):
raise Exception("The Exchange hours was closed, verify 'extendedMarketHours' flag in Initialize() when added new security(ies)")

def OnEndOfAlgorithm(self):
self.stopMarketOrders = self.Transactions.GetOrders(lambda o: o.Type is OrderType.StopMarket)

for o in self.stopMarketOrders:
if o.Status != OrderStatus.Filled:
raise Exception("The Algorithms was not handled any StopMarketOrders")
82 changes: 82 additions & 0 deletions Common/Orders/Fills/FutureFillModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
* limitations under the License.
*/

using System;
using QuantConnect.Util;
using QuantConnect.Data;
using QuantConnect.Securities;
using QuantConnect.Orders.Fees;

Expand Down Expand Up @@ -72,5 +75,84 @@ public override OrderEvent MarketFill(Security asset, MarketOrder order)

return fill;
}

/// <summary>
/// Stop fill model implementation for Future.
/// </summary>
/// <param name="asset">Security asset we're filling</param>
/// <param name="order">Order packet to model</param>
/// <returns>Order fill information detailing the average price and quantity filled.</returns>
/// <remarks>
/// A Stop order is an instruction to submit a buy or sell market order
/// if and when the user-specified stop trigger price is attained or penetrated.
///
/// A Sell Stop order is always placed below the current market price.
/// We assume a fluid/continuous, high volume market. Therefore, it is filled at the stop trigger price
/// if the current low price of trades is less than or equal to this price.
///
/// A Buy Stop order is always placed above the current market price.
/// We assume a fluid, high volume market. Therefore, it is filled at the stop trigger price
/// if the current high price of trades is greater or equal than this price.
///
/// The continuous market assumption is not valid if the market opens with an unfavorable gap.
/// In this case, a new bar opens below/above the stop trigger price, and the order is filled with the opening price.
/// <seealso cref="MarketFill(Security, MarketOrder)"/>
public override OrderEvent StopMarketFill(Security asset, StopMarketOrder order)
{
//Default order event to return.
var utcTime = asset.LocalTime.ConvertToUtc(asset.Exchange.TimeZone);
var fill = new OrderEvent(order, utcTime, OrderFee.Zero);

//If its cancelled don't need anymore checks:
if (order.Status == OrderStatus.Canceled) return fill;

// Fill only if open or extended
// even though data from internal configurations are not sent to the algorithm.OnData they still drive security cache and data
// this is specially relevant for the continuous contract underlying mapped contracts which are internal configurations
if (!IsExchangeOpen(asset, Parameters.ConfigProvider.GetSubscriptionDataConfigs(asset.Symbol, includeInternalConfigs: true).IsExtendedMarketHours()))
{
return fill;
}

//Get the range of prices in the last bar:
var prices = GetPricesCheckingPythonWrapper(asset, order.Direction);
var pricesEndTime = prices.EndTime.ConvertToUtc(asset.Exchange.TimeZone);

// do not fill on stale data
if (pricesEndTime <= order.Time) return fill;

//Calculate the model slippage: e.g. 0.01c
var slip = asset.SlippageModel.GetSlippageApproximation(asset, order);

//Check if the Stop Order was filled: opposite to a limit order
switch (order.Direction)
{
case OrderDirection.Sell:
//-> 1.1 Sell Stop: If Price below setpoint, Sell:
if (prices.Low < order.StopPrice)
{
fill.Status = OrderStatus.Filled;
// Assuming worse case scenario fill - fill at lowest of the stop & asset price.
fill.FillPrice = Math.Min(order.StopPrice, prices.Current - slip);
// assume the order completely filled
fill.FillQuantity = order.Quantity;
}
break;

case OrderDirection.Buy:
//-> 1.2 Buy Stop: If Price Above Setpoint, Buy:
if (prices.High > order.StopPrice)
{
fill.Status = OrderStatus.Filled;
// Assuming worse case scenario fill - fill at highest of the stop & asset price.
fill.FillPrice = Math.Max(order.StopPrice, prices.Current + slip);
// assume the order completely filled
fill.FillQuantity = order.Quantity;
}
break;
}

return fill;
}
}
}
45 changes: 45 additions & 0 deletions Tests/Common/Orders/Fills/FutureFillModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,51 @@ public void PerformsMarketFill([Values] bool isInternal,
Assert.AreEqual(OrderStatus.Filled, fill.Status);
}

[Test]
public void PerformsStopMarketFill(
[Values] bool extendedMarketHours,
[Values(OrderDirection.Buy, OrderDirection.Sell)] OrderDirection orderDirection)
{
var symbol = Symbols.ES_Future_Chain;
var model = new FutureFillModel();
var quantity = orderDirection == OrderDirection.Buy ? 100 : -100;
var marketPrice = orderDirection == OrderDirection.Buy ? 102m : 101m;

var time = Noon.AddHours(-12);

var order = new StopMarketOrder(symbol, quantity, 101.124m, time);
var config = CreateTradeBarConfig(symbol, extendedMarketHours: extendedMarketHours);
var security = GetSecurity(config);
TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork).UpdateTime(time.ConvertToUtc(TimeZones.NewYork));
security.SetLocalTimeKeeper(TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
security.SetMarketPrice(new IndicatorDataPoint(symbol, time, marketPrice));

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

var exchangeHours = MarketHoursDatabase.FromDataFolder().GetExchangeHours(config);
if (extendedMarketHours)
{
Assert.AreEqual(order.Quantity, fill.FillQuantity);
Assert.AreEqual(security.Price, fill.FillPrice);
Assert.AreEqual(OrderStatus.Filled, fill.Status);
Assert.IsTrue(exchangeHours.IsOpen(fill.UtcTime, extendedMarketHours));
}
else
{
Assert.AreEqual(0m, fill.FillQuantity);
Assert.AreEqual(0m, fill.FillPrice);
Assert.AreNotEqual(OrderStatus.Filled, fill.Status);
Assert.AreNotEqual(OrderStatus.PartiallyFilled, fill.Status);
Assert.IsFalse(exchangeHours.IsOpen(fill.UtcTime, extendedMarketHours));
}

}

private SubscriptionDataConfig CreateTradeBarConfig(Symbol symbol, bool isInternal = false, bool extendedMarketHours = true)
{
return new SubscriptionDataConfig(typeof(TradeBar), symbol, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, extendedMarketHours, isInternal);
Expand Down
Loading