/*
* 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 NUnit.Framework;
using QuantConnect.Algorithm.CSharp;
using QuantConnect.Brokerages.Backtesting;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Lean.Engine.TransactionHandlers;
using QuantConnect.Orders;
using QuantConnect.Orders.Fees;
using QuantConnect.Orders.Fills;
using QuantConnect.Securities;
using QuantConnect.Tests.Engine;
namespace QuantConnect.Tests.Common.Orders.Fills
{
[TestFixture, Ignore("TODO: fix me")]
public class PartialMarketFillModelTests
{
[Test]
public void CreatesSpecificNumberOfFills()
{
Security security;
MarketOrder order;
OrderTicket ticket;
PartialMarketFillModel model;
BasicTemplateAlgorithm algorithm;
var referenceTimeUtc = InitializeTest(out algorithm, out security, out model, out order, out ticket);
algorithm.SetDateTime(referenceTimeUtc.AddSeconds(1));
var fill1 = model.MarketFill(security, order);
ticket.AddOrderEvent(fill1);
Assert.AreEqual(order.Quantity / 2, fill1.FillQuantity);
Assert.AreEqual(OrderStatus.PartiallyFilled, fill1.Status);
algorithm.SetDateTime(referenceTimeUtc.AddSeconds(2));
var fill2 = model.MarketFill(security, order);
ticket.AddOrderEvent(fill2);
Assert.AreEqual(order.Quantity / 2, fill2.FillQuantity);
Assert.AreEqual(OrderStatus.Filled, fill2.Status);
}
[Test]
public void RequiresAdvancingTime()
{
Security security;
MarketOrder order;
OrderTicket ticket;
PartialMarketFillModel model;
BasicTemplateAlgorithm algorithm;
var referenceTimeUtc = InitializeTest(out algorithm, out security, out model, out order, out ticket);
var fill1 = model.MarketFill(security, order);
ticket.AddOrderEvent(fill1);
Assert.AreEqual(order.Quantity / 2, fill1.FillQuantity);
Assert.AreEqual(OrderStatus.PartiallyFilled, fill1.Status);
var fill2 = model.MarketFill(security, order);
ticket.AddOrderEvent(fill2);
Assert.AreEqual(0, fill2.FillQuantity);
Assert.AreEqual(OrderStatus.None, fill2.Status);
algorithm.SetDateTime(referenceTimeUtc.AddSeconds(1));
var fill3 = model.MarketFill(security, order);
ticket.AddOrderEvent(fill3);
Assert.AreEqual(order.Quantity / 2, fill3.FillQuantity);
Assert.AreEqual(OrderStatus.Filled, fill3.Status);
}
private static DateTime InitializeTest(out BasicTemplateAlgorithm algorithm, out Security security, out PartialMarketFillModel model, out MarketOrder order, out OrderTicket ticket)
{
var referenceTimeNY = new DateTime(2015, 12, 21, 13, 0, 0);
var referenceTimeUtc = referenceTimeNY.ConvertToUtc(TimeZones.NewYork);
algorithm = new BasicTemplateAlgorithm();
algorithm.SetDateTime(referenceTimeUtc);
var transactionHandler = new BacktestingTransactionHandler();
# pragma warning disable CA2000
var backtestingBrokerage = new BacktestingBrokerage(algorithm);
#pragma warning restore CA2000
transactionHandler.Initialize(algorithm, backtestingBrokerage, new TestResultHandler(Console.WriteLine));
algorithm.Transactions.SetOrderProcessor(transactionHandler);
var config = new SubscriptionDataConfig(typeof(TradeBar), Symbols.SPY, Resolution.Second, TimeZones.NewYork, TimeZones.NewYork, false, false, false);
security = new Security(
SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork),
config,
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
);
model = new PartialMarketFillModel(algorithm.Transactions, 2);
algorithm.Securities.Add(security);
algorithm.Securities[Symbols.SPY].FillModel = model;
security.SetMarketPrice(new Tick { Symbol = Symbols.SPY, Value = 100 });
algorithm.SetFinishedWarmingUp();
order = new MarketOrder(Symbols.SPY, 100, referenceTimeUtc) { Id = 1 };
var request = new SubmitOrderRequest(OrderType.Market, security.Type, security.Symbol, order.Quantity, 0, 0, algorithm.UtcTime, null);
ticket = algorithm.Transactions.ProcessRequest(request);
return referenceTimeUtc;
}
// Provides a test implementation that forces partial market order fills
///
/// Provides an implementation of that creates a specific
/// number of partial fills for marke orders only. All other order types reuse the
/// behavior. This model will emit one partial fill
/// per time step.
/// NOTE: If the desired number of fills is very large, then a few more fills may be issued
/// due to rounding errors. This model does not hold internal state regarding orders/previous
/// fills.
///
public class PartialMarketFillModel : ImmediateFillModel
{
private readonly decimal _percent;
private readonly IOrderProvider _orderProvider;
///
/// Initializes a new instance of the class
///
///
/// // split market orders into two fills
/// Securities["SPY"].FillModel = new PartialMarketFillModel(Transactions, 2);
///
/// The order provider used for getting order tickets
///
public PartialMarketFillModel(IOrderProvider orderProvider, int numberOfFills = 1)
{
_orderProvider = orderProvider;
_percent = 1m / numberOfFills;
}
///
/// Performs partial market fills once per time step
///
/// The security being ordered
/// The order
/// The order fill
public override OrderEvent MarketFill(Security asset, MarketOrder order)
{
var currentUtcTime = asset.LocalTime.ConvertToUtc(asset.Exchange.TimeZone);
var ticket = _orderProvider.GetOrderTickets(x => x.OrderId == order.Id).FirstOrDefault();
if (ticket == null)
{
// if we can't find the ticket issue empty fills
return new OrderEvent(order, currentUtcTime, OrderFee.Zero);
}
// make sure some time has passed
var lastOrderEvent = ticket.OrderEvents.LastOrDefault();
var increment = TimeSpan.FromTicks(Math.Max(asset.Resolution.ToTimeSpan().Ticks, 1));
if (lastOrderEvent != null && currentUtcTime - lastOrderEvent.UtcTime < increment)
{
// wait a minute between fills
return new OrderEvent(order, currentUtcTime, OrderFee.Zero);
}
var remaining = (int)(ticket.Quantity - ticket.QuantityFilled);
var fill = base.MarketFill(asset, order);
var filledThisTime = Math.Min(remaining, (int)(_percent * order.Quantity));
fill.FillQuantity = filledThisTime;
// only mark it as filled if there is zero quantity remaining
fill.Status = remaining == filledThisTime
? OrderStatus.Filled
: OrderStatus.PartiallyFilled;
return fill;
}
}
}
}