/*
* 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.Collections.Generic;
using System.Linq;
using System.Threading;
using QuantConnect.Securities;
using static QuantConnect.StringExtensions;
namespace QuantConnect.Orders
{
///
/// Provides a single reference to an order for the algorithm to maintain. As the order gets
/// updated this ticket will also get updated
///
public sealed class OrderTicket
{
private readonly object _lock = new object();
private Order _order;
private OrderStatus? _orderStatusOverride;
private CancelOrderRequest _cancelRequest;
private FillState _fillState;
private List _orderEventsImpl;
private List _updateRequestsImpl;
private readonly SubmitOrderRequest _submitRequest;
private readonly ManualResetEvent _orderStatusClosedEvent;
private readonly ManualResetEvent _orderSetEvent;
// we pull this in to provide some behavior/simplicity to the ticket API
private readonly SecurityTransactionManager _transactionManager;
private List _orderEvents { get => _orderEventsImpl ??= new List(); }
private List _updateRequests { get => _updateRequestsImpl ??= new List(); }
///
/// Gets the order id of this ticket
///
public int OrderId
{
get { return _submitRequest.OrderId; }
}
///
/// Gets the current status of this order ticket
///
public OrderStatus Status
{
get
{
if (_orderStatusOverride.HasValue) return _orderStatusOverride.Value;
return _order == null ? OrderStatus.New : _order.Status;
}
}
///
/// Gets the symbol being ordered
///
public Symbol Symbol
{
get { return _submitRequest.Symbol; }
}
///
/// Gets the 's
///
public SecurityType SecurityType
{
get { return _submitRequest.SecurityType; }
}
///
/// Gets the number of units ordered
///
public decimal Quantity
{
get { return _order == null ? _submitRequest.Quantity : _order.Quantity; }
}
///
/// Gets the average fill price for this ticket. If no fills have been processed
/// then this will return a value of zero.
///
public decimal AverageFillPrice
{
get
{
return _fillState.AverageFillPrice;
}
}
///
/// Gets the total qantity filled for this ticket. If no fills have been processed
/// then this will return a value of zero.
///
public decimal QuantityFilled
{
get
{
return _fillState.QuantityFilled;
}
}
///
/// Gets the remaining quantity for this order ticket.
/// This is the difference between the total quantity ordered and the total quantity filled.
///
public decimal QuantityRemaining
{
get
{
var currentState = _fillState;
return Quantity - currentState.QuantityFilled;
}
}
///
/// Gets the time this order was last updated
///
public DateTime Time
{
get { return GetMostRecentOrderRequest().Time; }
}
///
/// Gets the type of order
///
public OrderType OrderType
{
get { return _submitRequest.OrderType; }
}
///
/// Gets the order's current tag
///
public string Tag
{
get { return _order == null ? _submitRequest.Tag : _order.Tag; }
}
///
/// Gets the that initiated this order
///
public SubmitOrderRequest SubmitRequest
{
get { return _submitRequest; }
}
///
/// Gets a list of containing an item for each
/// that was sent for this order id
///
public IReadOnlyList UpdateRequests
{
get
{
lock (_lock)
{
// Avoid creating the update requests list if not necessary
if (_updateRequestsImpl == null)
{
return Array.Empty();
}
return _updateRequestsImpl.ToList();
}
}
}
///
/// Gets the if this order was canceled. If this order
/// was not canceled, this will return null
///
public CancelOrderRequest CancelRequest
{
get
{
lock (_lock)
{
return _cancelRequest;
}
}
}
///
/// Gets a list of all order events for this ticket
///
public IReadOnlyList OrderEvents
{
get
{
lock (_lock)
{
return _orderEvents.ToList();
}
}
}
///
/// Gets a wait handle that can be used to wait until this order has filled
///
public WaitHandle OrderClosed
{
get { return _orderStatusClosedEvent; }
}
///
/// Returns true if the order has been set for this ticket
///
public bool HasOrder => _order != null;
///
/// Gets a wait handle that can be used to wait until the order has been set
///
public WaitHandle OrderSet => _orderSetEvent;
///
/// Initializes a new instance of the class
///
/// The transaction manager used for submitting updates and cancels for this ticket
/// The order request that initiated this order ticket
public OrderTicket(SecurityTransactionManager transactionManager, SubmitOrderRequest submitRequest)
{
_submitRequest = submitRequest;
_transactionManager = transactionManager;
_orderStatusClosedEvent = new ManualResetEvent(false);
_orderSetEvent = new ManualResetEvent(false);
_fillState = new FillState(0m, 0m);
}
///
/// Gets the specified field from the ticket
///
/// The order field to get
/// The value of the field
/// Field out of range
/// Field out of range for order type
public decimal Get(OrderField field)
{
return Get(field);
}
///
/// Gets the specified field from the ticket and tries to convert it to the specified type
///
/// The order field to get
/// The value of the field
/// Field out of range
/// Field out of range for order type
public T Get(OrderField field)
{
object fieldValue = null;
switch (field)
{
case OrderField.LimitPrice:
if (_submitRequest.OrderType == OrderType.ComboLimit)
{
fieldValue = AccessOrder(this, field, o => o.GroupOrderManager.LimitPrice, r => r.LimitPrice);
}
else if (_submitRequest.OrderType == OrderType.ComboLegLimit)
{
fieldValue = AccessOrder(this, field, o => o.LimitPrice, r => r.LimitPrice);
}
else if (_submitRequest.OrderType == OrderType.Limit)
{
fieldValue = AccessOrder(this, field, o => o.LimitPrice, r => r.LimitPrice);
}
else if (_submitRequest.OrderType == OrderType.StopLimit)
{
fieldValue = AccessOrder(this, field, o => o.LimitPrice, r => r.LimitPrice);
}
else if (_submitRequest.OrderType == OrderType.LimitIfTouched)
{
fieldValue = AccessOrder(this, field, o => o.LimitPrice, r => r.LimitPrice);
}
break;
case OrderField.StopPrice:
if (_submitRequest.OrderType == OrderType.StopLimit)
{
fieldValue = AccessOrder(this, field, o => o.StopPrice, r => r.StopPrice);
}
else if (_submitRequest.OrderType == OrderType.StopMarket)
{
fieldValue = AccessOrder(this, field, o => o.StopPrice, r => r.StopPrice);
}
else if (_submitRequest.OrderType == OrderType.TrailingStop)
{
fieldValue = AccessOrder(this, field, o => o.StopPrice, r => r.StopPrice);
}
break;
case OrderField.TriggerPrice:
fieldValue = AccessOrder(this, field, o => o.TriggerPrice, r => r.TriggerPrice);
break;
case OrderField.TrailingAmount:
fieldValue = AccessOrder(this, field, o => o.TrailingAmount, r => r.TrailingAmount);
break;
case OrderField.TrailingAsPercentage:
fieldValue = AccessOrder(this, field, o => o.TrailingAsPercentage, r => r.TrailingAsPercentage);
break;
default:
throw new ArgumentOutOfRangeException(nameof(field), field, null);
}
if (fieldValue == null)
{
throw new ArgumentException(Messages.OrderTicket.GetFieldError(this, field));
}
return (T)fieldValue;
}
///
/// Submits an with the to update
/// the ticket with data specified in
///
/// Defines what properties of the order should be updated
/// The from updating the order
public OrderResponse Update(UpdateOrderFields fields)
{
var ticket = _transactionManager.UpdateOrder(new UpdateOrderRequest(_transactionManager.UtcTime, SubmitRequest.OrderId, fields));
return ticket.UpdateRequests.Last().Response;
}
///
/// Submits an with the to update
/// the ticket with tag specified in
///
/// The new tag for this order ticket
/// from updating the order
public OrderResponse UpdateTag(string tag)
{
var fields = new UpdateOrderFields()
{
Tag = tag
};
return Update(fields);
}
///
/// Submits an with the to update
/// the ticket with quantity specified in and with tag specified in
///
/// The new quantity for this order ticket
/// The new tag for this order ticket
/// from updating the order
public OrderResponse UpdateQuantity(decimal quantity, string tag = null)
{
var fields = new UpdateOrderFields()
{
Quantity = quantity,
Tag = tag
};
return Update(fields);
}
///
/// Submits an with the to update
/// the ticker with limit price specified in and with tag specified in
///
/// The new limit price for this order ticket
/// The new tag for this order ticket
/// from updating the order
public OrderResponse UpdateLimitPrice(decimal limitPrice, string tag = null)
{
var fields = new UpdateOrderFields()
{
LimitPrice = limitPrice,
Tag = tag
};
return Update(fields);
}
///
/// Submits an with the to update
/// the ticker with stop price specified in and with tag specified in
///
/// The new stop price for this order ticket
/// The new tag for this order ticket
/// from updating the order
public OrderResponse UpdateStopPrice(decimal stopPrice, string tag = null)
{
var fields = new UpdateOrderFields()
{
StopPrice = stopPrice,
Tag = tag
};
return Update(fields);
}
///
/// Submits an with the to update
/// the ticker with trigger price specified in and with tag specified in
///
/// The new price which, when touched, will trigger the setting of a limit order.
/// The new tag for this order ticket
/// from updating the order
public OrderResponse UpdateTriggerPrice(decimal triggerPrice, string tag = null)
{
var fields = new UpdateOrderFields()
{
TriggerPrice = triggerPrice,
Tag = tag
};
return Update(fields);
}
///
/// Submits an with the to update
/// the ticker with stop trailing amount specified in and with tag specified in
///
/// The new trailing amount for this order ticket
/// The new tag for this order ticket
/// from updating the order
public OrderResponse UpdateStopTrailingAmount(decimal trailingAmount, string tag = null)
{
var fields = new UpdateOrderFields()
{
TrailingAmount = trailingAmount,
Tag = tag
};
return Update(fields);
}
///
/// Submits a new request to cancel this order
///
public OrderResponse Cancel(string tag = null)
{
var request = new CancelOrderRequest(_transactionManager.UtcTime, OrderId, tag);
lock (_lock)
{
// don't submit duplicate cancel requests, if the cancel request wasn't flagged as error
// this could happen when trying to cancel an order which status is still new and hasn't even been submitted to the brokerage
if (_cancelRequest != null && _cancelRequest.Status != OrderRequestStatus.Error)
{
return OrderResponse.Error(request, OrderResponseErrorCode.RequestCanceled,
Messages.OrderTicket.CancelRequestAlreadySubmitted(this));
}
}
var ticket = _transactionManager.ProcessRequest(request);
return ticket.CancelRequest.Response;
}
///
/// Gets the most recent for this ticket
///
/// The most recent for this ticket
public OrderResponse GetMostRecentOrderResponse()
{
return GetMostRecentOrderRequest().Response;
}
///
/// Gets the most recent for this ticket
///
/// The most recent for this ticket
public OrderRequest GetMostRecentOrderRequest()
{
lock (_lock)
{
if (_cancelRequest != null)
{
return _cancelRequest;
}
// Avoid creating the update requests list if not necessary
if (_updateRequestsImpl != null)
{
var lastUpdate = _updateRequestsImpl.LastOrDefault();
if (lastUpdate != null)
{
return lastUpdate;
}
}
}
return SubmitRequest;
}
///
/// Adds an order event to this ticket
///
/// The order event to be added
internal void AddOrderEvent(OrderEvent orderEvent)
{
lock (_lock)
{
_orderEvents.Add(orderEvent);
// Update the ticket and order
if (orderEvent.FillQuantity != 0)
{
var filledQuantity = _fillState.QuantityFilled;
var averageFillPrice = _fillState.AverageFillPrice;
if (_order.Type != OrderType.OptionExercise)
{
// keep running totals of quantity filled and the average fill price so we
// don't need to compute these on demand
filledQuantity += orderEvent.FillQuantity;
var quantityWeightedFillPrice = _orderEvents.Where(x => x.Status.IsFill())
.Aggregate(0m, (d, x) => d + x.AbsoluteFillQuantity * x.FillPrice);
averageFillPrice = quantityWeightedFillPrice / Math.Abs(filledQuantity);
_order.Price = averageFillPrice;
}
// For ITM option exercise orders we set the order price to the strike price.
// For OTM the fill price should be zero, which is the default for OptionExerciseOrders
else if (orderEvent.IsInTheMoney)
{
_order.Price = Symbol.ID.StrikePrice;
// We update the ticket only if the fill price is not zero (this fixes issue #2846 where average price
// is skewed by the removal of the option).
if (orderEvent.FillPrice != 0)
{
filledQuantity += orderEvent.FillQuantity;
averageFillPrice = _order.Price;
}
}
_fillState = new FillState(averageFillPrice, filledQuantity);
}
}
// fire the wait handle indicating this order is closed
if (orderEvent.Status.IsClosed())
{
_orderStatusClosedEvent.Set();
}
}
///
/// Updates the internal order object with the current state
///
/// The order
internal void SetOrder(Order order)
{
if (_order != null && _order.Id != order.Id)
{
throw new ArgumentException("Order id mismatch");
}
_order = order;
_orderSetEvent.Set();
}
///
/// Adds a new to this ticket.
///
/// The recently processed
internal void AddUpdateRequest(UpdateOrderRequest request)
{
if (request.OrderId != OrderId)
{
throw new ArgumentException("Received UpdateOrderRequest for incorrect order id.");
}
lock (_lock)
{
_updateRequests.Add(request);
}
}
///
/// Sets the for this ticket. This can only be performed once.
///
///
/// This method is thread safe.
///
/// The that canceled this ticket.
/// False if the the CancelRequest has already been set, true if this call set it
internal bool TrySetCancelRequest(CancelOrderRequest request)
{
if (request.OrderId != OrderId)
{
throw new ArgumentException("Received CancelOrderRequest for incorrect order id.");
}
lock (_lock)
{
// don't submit duplicate cancel requests, if the cancel request wasn't flagged as error
// this could happen when trying to cancel an order which status is still new and hasn't even been submitted to the brokerage
if (_cancelRequest != null && _cancelRequest.Status != OrderRequestStatus.Error)
{
return false;
}
_cancelRequest = request;
}
return true;
}
///
/// Creates a new that represents trying to cancel an order for which no ticket exists
///
public static OrderTicket InvalidCancelOrderId(SecurityTransactionManager transactionManager, CancelOrderRequest request)
{
var submit = new SubmitOrderRequest(OrderType.Market, SecurityType.Base, Symbol.Empty, 0, 0, 0, DateTime.MaxValue, request.Tag);
submit.SetResponse(OrderResponse.UnableToFindOrder(request));
submit.SetOrderId(request.OrderId);
var ticket = new OrderTicket(transactionManager, submit);
request.SetResponse(OrderResponse.UnableToFindOrder(request));
ticket.TrySetCancelRequest(request);
ticket._orderStatusOverride = OrderStatus.Invalid;
return ticket;
}
///
/// Creates a new that represents trying to update an order for which no ticket exists
///
public static OrderTicket InvalidUpdateOrderId(SecurityTransactionManager transactionManager, UpdateOrderRequest request)
{
var submit = new SubmitOrderRequest(OrderType.Market, SecurityType.Base, Symbol.Empty, 0, 0, 0, DateTime.MaxValue, request.Tag);
submit.SetResponse(OrderResponse.UnableToFindOrder(request));
submit.SetOrderId(request.OrderId);
var ticket = new OrderTicket(transactionManager, submit);
request.SetResponse(OrderResponse.UnableToFindOrder(request));
ticket.AddUpdateRequest(request);
ticket._orderStatusOverride = OrderStatus.Invalid;
return ticket;
}
///
/// Creates a new that represents trying to submit a new order that had errors embodied in the
///
public static OrderTicket InvalidSubmitRequest(SecurityTransactionManager transactionManager, SubmitOrderRequest request, OrderResponse response)
{
request.SetResponse(response);
return new OrderTicket(transactionManager, request) { _orderStatusOverride = OrderStatus.Invalid };
}
///
/// Returns a string that represents the current object.
///
///
/// A string that represents the current object.
///
/// 2
public override string ToString()
{
var requestCount = 1;
var responseCount = _submitRequest.Response == OrderResponse.Unprocessed ? 0 : 1;
lock (_lock)
{
// Avoid creating the update requests list if not necessary
if (_updateRequestsImpl != null)
{
requestCount += _updateRequestsImpl.Count;
responseCount += _updateRequestsImpl.Count(x => x.Response != OrderResponse.Unprocessed);
}
requestCount += _cancelRequest == null ? 0 : 1;
responseCount += _cancelRequest == null || _cancelRequest.Response == OrderResponse.Unprocessed ? 0 : 1;
}
return Messages.OrderTicket.ToString(this, _order, requestCount, responseCount);
}
///
/// This is provided for API backward compatibility and will resolve to the order ID, except during
/// an error, where it will return the integer value of the from
/// the most recent response
///
public static implicit operator int(OrderTicket ticket)
{
var response = ticket.GetMostRecentOrderResponse();
if (response != null && response.IsError)
{
return (int) response.ErrorCode;
}
return ticket.OrderId;
}
private static P AccessOrder(OrderTicket ticket, OrderField field, Func orderSelector, Func requestSelector)
where T : Order
{
var order = ticket._order;
if (order == null)
{
return requestSelector(ticket._submitRequest);
}
var typedOrder = order as T;
if (typedOrder != null)
{
return orderSelector(typedOrder);
}
throw new ArgumentException(Invariant($"Unable to access property {field} on order of type {order.Type}"));
}
///
/// Reference wrapper for decimal average fill price and quantity filled.
/// In order to update the average fill price and quantity filled, we create a new instance of this class
/// so we avoid potential race conditions when accessing these properties
/// (e.g. the decimals might be being updated and in a invalid state when being read)
///
private class FillState
{
public decimal AverageFillPrice { get; }
public decimal QuantityFilled { get; }
public FillState(decimal averageFillPrice, decimal quantityFilled)
{
AverageFillPrice = averageFillPrice;
QuantityFilled = quantityFilled;
}
}
}
}