/*
* 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.Concurrent;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Data.Market;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Interfaces;
using QuantConnect.Logging;
using QuantConnect.Orders;
using QuantConnect.Orders.Fills;
using QuantConnect.Orders.Fees;
using QuantConnect.Securities;
using QuantConnect.Securities.Option;
using QuantConnect.Util;
namespace QuantConnect.Brokerages.Backtesting
{
///
/// Represents a brokerage to be used during backtesting. This is intended to be only be used with the BacktestingTransactionHandler
///
[BrokerageFactory(typeof(BacktestingBrokerageFactory))]
public class BacktestingBrokerage : Brokerage
{
// flag used to indicate whether or not we need to scan for
// fills, this is purely a performance concern is ConcurrentDictionary.IsEmpty
// is not exactly the fastest operation and Scan gets called at least twice per
// time loop
private bool _needsScan;
private DateTime _nextOptionAssignmentTime;
private readonly ConcurrentDictionary _pending;
private readonly object _needsScanLock = new object();
private readonly HashSet _pendingOptionAssignments = new HashSet();
///
/// This is the algorithm under test
///
protected IAlgorithm Algorithm { get; init; }
///
/// Creates a new BacktestingBrokerage for the specified algorithm
///
/// The algorithm instance
public BacktestingBrokerage(IAlgorithm algorithm)
: this(algorithm, "Backtesting Brokerage")
{
}
///
/// Creates a new BacktestingBrokerage for the specified algorithm
///
/// The algorithm instance
/// The name of the brokerage
protected BacktestingBrokerage(IAlgorithm algorithm, string name)
: base(name)
{
Algorithm = algorithm;
_pending = new ConcurrentDictionary();
}
///
/// Gets the connection status
///
///
/// The BacktestingBrokerage is always connected
///
public override bool IsConnected => true;
///
/// Gets all open orders on the account
///
/// The open orders returned from IB
public override List GetOpenOrders()
{
return Algorithm.Transactions.GetOpenOrders().ToList();
}
///
/// Gets all holdings for the account
///
/// The current holdings from the account
public override List GetAccountHoldings()
{
// grab everything from the portfolio with a non-zero absolute quantity
return (from kvp in Algorithm.Portfolio.Securities.OrderBy(x => x.Value.Symbol)
where kvp.Value.Holdings.AbsoluteQuantity > 0
select new Holding(kvp.Value)).ToList();
}
///
/// Gets the current cash balance for each currency held in the brokerage account
///
/// The current cash balance for each currency available for trading
public override List GetCashBalance()
{
return Algorithm.Portfolio.CashBook.Select(x => new CashAmount(x.Value.Amount, x.Value.Symbol)).ToList();
}
///
/// Places a new order and assigns a new broker ID to the order
///
/// The order to be placed
/// True if the request for a new order has been placed, false otherwise
public override bool PlaceOrder(Order order)
{
if (Algorithm.LiveMode)
{
Log.Trace("BacktestingBrokerage.PlaceOrder(): Type: " + order.Type + " Symbol: " + order.Symbol.Value + " Quantity: " + order.Quantity);
}
if (order.Status == OrderStatus.New)
{
lock (_needsScanLock)
{
_needsScan = true;
SetPendingOrder(order);
}
AddBrokerageOrderId(order);
// fire off the event that says this order has been submitted
var submitted = new OrderEvent(order,
Algorithm.UtcTime,
OrderFee.Zero)
{ Status = OrderStatus.Submitted };
OnOrderEvent(submitted);
return true;
}
return false;
}
///
/// Updates the order with the same ID
///
/// The new order information
/// True if the request was made for the order to be updated, false otherwise
public override bool UpdateOrder(Order order)
{
if (Algorithm.LiveMode)
{
Log.Trace("BacktestingBrokerage.UpdateOrder(): Symbol: " + order.Symbol.Value + " Quantity: " + order.Quantity + " Status: " + order.Status);
}
lock (_needsScanLock)
{
Order pending;
if (!_pending.TryGetValue(order.Id, out pending))
{
// can't update something that isn't there
return false;
}
_needsScan = true;
SetPendingOrder(order);
}
AddBrokerageOrderId(order);
// fire off the event that says this order has been updated
var updated = new OrderEvent(order,
Algorithm.UtcTime,
OrderFee.Zero)
{
Status = OrderStatus.UpdateSubmitted
};
OnOrderEvent(updated);
return true;
}
///
/// Cancels the order with the specified ID
///
/// The order to cancel
/// True if the request was made for the order to be canceled, false otherwise
public override bool CancelOrder(Order order)
{
if (Algorithm.LiveMode)
{
Log.Trace("BacktestingBrokerage.CancelOrder(): Symbol: " + order.Symbol.Value + " Quantity: " + order.Quantity);
}
if (!order.TryGetGroupOrders(TryGetOrder, out var orders))
{
return false;
}
var result = true;
foreach (var orderInGroup in orders)
{
lock (_needsScanLock)
{
if (!_pending.TryRemove(orderInGroup.Id, out var _))
{
// can't cancel something that isn't there,
// let's continue just in case some other order of the group has to be cancelled
result = false;
}
}
AddBrokerageOrderId(orderInGroup);
// fire off the event that says this order has been canceled
var canceled = new OrderEvent(orderInGroup,
Algorithm.UtcTime,
OrderFee.Zero)
{ Status = OrderStatus.Canceled };
OnOrderEvent(canceled);
}
return result;
}
///
/// Scans all the outstanding orders and applies the algorithm model fills to generate the order events
///
public virtual void Scan()
{
ProcessAssignmentOrders();
lock (_needsScanLock)
{
// there's usually nothing in here
if (!_needsScan)
{
return;
}
var stillNeedsScan = false;
// process each pending order to produce fills/fire events
foreach (var kvp in _pending.OrderBySafe(x => x.Key))
{
var order = kvp.Value;
if (order == null)
{
Log.Error("BacktestingBrokerage.Scan(): Null pending order found: " + kvp.Key);
_pending.TryRemove(kvp.Key, out order);
continue;
}
if (order.Status.IsClosed())
{
// this should never actually happen as we always remove closed orders as they happen
_pending.TryRemove(order.Id, out var _);
continue;
}
// all order fills are processed on the next bar (except for market orders)
if (order.Time == Algorithm.UtcTime && order.Type != OrderType.Market && order.Type != OrderType.ComboMarket && order.Type != OrderType.OptionExercise)
{
stillNeedsScan = true;
continue;
}
if (!order.TryGetGroupOrders(TryGetOrder, out var orders))
{
// an Order of the group is missing
stillNeedsScan = true;
continue;
}
if(!orders.TryGetGroupOrdersSecurities(Algorithm.Portfolio, out var securities))
{
Log.Error($"BacktestingBrokerage.Scan(): Unable to process orders: [{string.Join(",", orders.Select(o => o.Id))}] The security no longer exists. UtcTime: {Algorithm.UtcTime}");
// invalidate the order in the algorithm before removing
RemoveOrders(orders, OrderStatus.Invalid);
continue;
}
if (!TryOrderPreChecks(securities, out stillNeedsScan))
{
continue;
}
// verify sure we have enough cash to perform the fill
HasSufficientBuyingPowerForOrderResult hasSufficientBuyingPowerResult;
try
{
hasSufficientBuyingPowerResult = Algorithm.Portfolio.HasSufficientBuyingPowerForOrder(orders);
}
catch (Exception err)
{
// if we threw an error just mark it as invalid and remove the order from our pending list
RemoveOrders(orders, OrderStatus.Invalid, err.Message);
Log.Error(err);
Algorithm.Error($"Order Error: ids: [{string.Join(",", orders.Select(o => o.Id))}], Error executing margin models: {err.Message}");
continue;
}
var fills = new List();
//Before we check this queued order make sure we have buying power:
if (hasSufficientBuyingPowerResult.IsSufficient)
{
//Model:
var security = securities[order];
var model = security.FillModel;
//Based on the order type: refresh its model to get fill price and quantity
try
{
if (order.Type == OrderType.OptionExercise)
{
var option = (Option)security;
fills.AddRange(option.OptionExerciseModel.OptionExercise(option, order as OptionExerciseOrder));
}
else
{
var context = new FillModelParameters(
security,
order,
Algorithm.SubscriptionManager.SubscriptionDataConfigService,
Algorithm.Settings.StalePriceTimeSpan,
securities,
OnOrderUpdated);
// check if the fill should be emitted
var fill = model.Fill(context);
if (fill.All(x => order.TimeInForce.IsFillValid(security, order, x)))
{
fills.AddRange(fill);
}
}
// invoke fee models for completely filled order events
foreach (var fill in fills)
{
if (fill.Status == OrderStatus.Filled)
{
// this check is provided for backwards compatibility of older user-defined fill models
// that may be performing fee computation inside the fill model w/out invoking the fee model
// TODO : This check can be removed in April, 2019 -- a 6-month window to upgrade (also, suspect small % of users, if any are impacted)
if (fill.OrderFee.Value.Amount == 0m)
{
// It could be the case the order is a combo order, then it contains legs with different quantities and security types.
// Therefore, we need to compute the fees based on the specific leg order and security
var legKVP = securities.Where(x => x.Key.Id == fill.OrderId).Single();
fill.OrderFee = legKVP.Value.FeeModel.GetOrderFee(new OrderFeeParameters(legKVP.Value, legKVP.Key));
}
}
}
}
catch (Exception err)
{
Log.Error(err);
Algorithm.Error($"Order Error: id: {order.Id}, Transaction model failed to fill for order type: {order.Type} with error: {err.Message}");
}
}
else if (order.Status == OrderStatus.CancelPending)
{
// the pending CancelOrderRequest will be handled during the next transaction handler run
continue;
}
else
{
// invalidate the order in the algorithm before removing
var message = securities.GetErrorMessage(hasSufficientBuyingPowerResult);
RemoveOrders(orders, OrderStatus.Invalid, message);
Algorithm.Error(message);
continue;
}
if (fills.Count == 0)
{
continue;
}
List fillEvents = new(orders.Count);
List> positionAssignments = new(orders.Count);
foreach (var targetOrder in orders)
{
var orderFills = fills.Where(f => f.OrderId == targetOrder.Id);
foreach (var fill in orderFills)
{
// change in status or a new fill
if (targetOrder.Status != fill.Status || fill.FillQuantity != 0)
{
// we update the order status so we do not re process it if we re enter
// because of the call to OnOrderEvent.
// Note: this is done by the transaction handler but we have a clone of the order
targetOrder.Status = fill.Status;
fillEvents.Add(fill);
}
if (fill.IsAssignment)
{
positionAssignments.Add(Tuple.Create(targetOrder, fill));
}
}
}
OnOrderEvents(fillEvents);
foreach (var assignment in positionAssignments)
{
assignment.Item2.Message = assignment.Item1.Tag;
OnOptionPositionAssigned(assignment.Item2);
}
if (fills.All(x => x.Status.IsClosed()))
{
foreach (var o in orders)
{
_pending.TryRemove(o.Id, out var _);
}
}
else
{
stillNeedsScan = true;
}
}
// if we didn't fill then we need to continue to scan or
// if there are still pending orders
_needsScan = stillNeedsScan || !_pending.IsEmpty;
}
}
///
/// Invokes the event with the given order updates.
///
private void OnOrderUpdated(Order order)
{
switch (order.Type)
{
case OrderType.TrailingStop:
OnOrderUpdated(new OrderUpdateEvent { OrderId = order.Id, TrailingStopPrice = ((TrailingStopOrder)order).StopPrice });
break;
case OrderType.StopLimit:
OnOrderUpdated(new OrderUpdateEvent { OrderId = order.Id, StopTriggered = ((StopLimitOrder)order).StopTriggered });
break;
}
}
///
/// Helper method to drive option assignment models
///
private void ProcessAssignmentOrders()
{
if (Algorithm.UtcTime >= _nextOptionAssignmentTime)
{
_nextOptionAssignmentTime = Algorithm.UtcTime.RoundDown(Time.OneHour) + Time.OneHour;
foreach (var security in Algorithm.Securities.Values
.Where(security => security.Symbol.SecurityType.IsOption() && security.Holdings.IsShort)
.OrderBy(security => security.Symbol.ID.Symbol))
{
var option = (Option)security;
var result = option.OptionAssignmentModel.GetAssignment(new OptionAssignmentParameters(option));
if (result != null && result.Quantity != 0)
{
if (!_pendingOptionAssignments.Add(option.Symbol))
{
throw new InvalidOperationException($"Duplicate option exercise order request for symbol {option.Symbol}. Please contact support");
}
OnOptionNotification(new OptionNotificationEventArgs(option.Symbol, 0, result.Tag));
}
}
}
}
///
/// Event invocator for the OrderFilled event
///
/// The list of order events
protected override void OnOrderEvents(List orderEvents)
{
for (int i = 0; i < orderEvents.Count; i++)
{
_pendingOptionAssignments.Remove(orderEvents[i].Symbol);
}
base.OnOrderEvents(orderEvents);
}
///
/// The BacktestingBrokerage is always connected. This is a no-op.
///
public override void Connect()
{
//NOP
}
///
/// The BacktestingBrokerage is always connected. This is a no-op.
///
public override void Disconnect()
{
//NOP
}
///
/// Sets the pending order as a clone to prevent object reference nastiness
///
/// The order to be added to the pending orders dictionary
///
private void SetPendingOrder(Order order)
{
_pending[order.Id] = order;
}
///
/// Process delistings
///
/// Delistings to process
public void ProcessDelistings(Delistings delistings)
{
// Process our delistings, important to do options first because of possibility of having future options contracts
// and underlying future delisting at the same time.
foreach (var delisting in delistings?.Values.OrderBy(x => !x.Symbol.SecurityType.IsOption()))
{
Log.Debug($"BacktestingBrokerage.ProcessDelistings(): Delisting {delisting.Type}: {delisting.Symbol.Value}, UtcTime: {Algorithm.UtcTime}, DelistingTime: {delisting.Time}");
if (delisting.Type == DelistingType.Warning)
{
// We do nothing with warnings
continue;
}
var security = Algorithm.Securities[delisting.Symbol];
if (security.Symbol.SecurityType.IsOption())
{
// Process the option delisting
OnOptionNotification(new OptionNotificationEventArgs(delisting.Symbol, 0));
}
else
{
// Any other type of delisting
OnDelistingNotification(new DelistingNotificationEventArgs(delisting.Symbol));
}
// the subscription are getting removed from the data feed because they end
// remove security from all universes
foreach (var ukvp in Algorithm.UniverseManager)
{
var universe = ukvp.Value;
if (universe.ContainsMember(security.Symbol))
{
var userUniverse = universe as UserDefinedUniverse;
if (userUniverse != null)
{
userUniverse.Remove(security.Symbol);
}
else
{
universe.RemoveMember(Algorithm.UtcTime, security);
}
}
}
if (!Algorithm.IsWarmingUp)
{
// Cancel any other orders
var cancelledOrders = Algorithm.Transactions.CancelOpenOrders(delisting.Symbol);
foreach (var cancelledOrder in cancelledOrders)
{
Log.Trace("AlgorithmManager.Run(): " + cancelledOrder);
}
}
}
}
private void RemoveOrders(List orders, OrderStatus orderStatus, string message = "")
{
var orderEvents = new List(orders.Count);
for (var i = 0; i < orders.Count; i++)
{
var order = orders[i];
orderEvents.Add(new OrderEvent(order, Algorithm.UtcTime, OrderFee.Zero, message) { Status = orderStatus });
_pending.TryRemove(order.Id, out var _);
}
OnOrderEvents(orderEvents);
}
private bool TryOrderPreChecks(Dictionary ordersSecurities, out bool stillNeedsScan)
{
var result = true;
stillNeedsScan = false;
var removedOrdersIds = new HashSet();
foreach (var kvp in ordersSecurities)
{
var order = kvp.Key;
var security = kvp.Value;
if (order.Type == OrderType.MarketOnOpen)
{
// This is a performance improvement:
// Since MOO should never fill on the same bar or on stale data (see FillModel)
// the order can remain unfilled for multiple 'scans', so we want to avoid
// margin and portfolio calculations since they are expensive
var currentBar = security.GetLastData();
var localOrderTime = order.Time.ConvertFromUtc(security.Exchange.TimeZone);
if (currentBar == null || localOrderTime >= currentBar.EndTime)
{
stillNeedsScan = true;
result = false;
break;
}
}
// check if the time in force handler allows fills
if (order.TimeInForce.IsOrderExpired(security, order))
{
// We remove all orders in the combo
RemoveOrders(ordersSecurities.Select(kvp => kvp.Key).ToList(), OrderStatus.Canceled, "The order has expired.");
result = false;
break;
}
// check if we would actually be able to fill this
if (!Algorithm.BrokerageModel.CanExecuteOrder(security, order))
{
result = false;
break;
}
}
return result;
}
private Order TryGetOrder(int orderId)
{
_pending.TryGetValue(orderId, out var order);
return order;
}
private static void AddBrokerageOrderId(Order order)
{
var orderId = order.Id.ToStringInvariant();
if (!order.BrokerId.Contains(orderId))
{
order.BrokerId.Add(orderId);
}
}
}
}