Table of Contents

Orders

A strategy sends, changes and cancels orders with OpenOrderAsync, ModifyOrderAsync and CancelOrderAsync. The synchronous OpenOrder, ModifyOrder and CancelOrder are obsolete: they start the asynchronous method and return at once, and an error is only written to the strategy log.

Every call is an instruction to the trading connection, not a result: an order can be filled completely, partially, with deviations or rejected. The methods work only while the strategy is started; in any other state the call returns without sending anything. OnCalculate is also called while the history is being calculated, before the strategy is started, so check CanProcess before sending an order: by default it returns true only for the last bar of a started strategy.

An order

An Order carries the portfolio and the instrument of the strategy, the direction, the type and the volume (QuantityToFill); a limit order has a Price, a stop order a TriggerPrice, a stop-limit order both.

// A market buy order of 1 lot
private Order MarketBuy() => new()
{
    Portfolio = Portfolio,
    Security = Security,
    Direction = OrderDirections.Buy,
    Type = OrderTypes.Market,
    QuantityToFill = 1,
};
// A limit sell order at 100, 1 lot
private Order LimitSell() => new()
{
    Portfolio = Portfolio,
    Security = Security,
    Direction = OrderDirections.Sell,
    Type = OrderTypes.Limit,
    Price = 100,
    QuantityToFill = 1,
};
// A stop sell order triggered at 100, 1 lot
private Order StopSell() => new()
{
    Portfolio = Portfolio,
    Security = Security,
    Direction = OrderDirections.Sell,
    Type = OrderTypes.Stop,
    TriggerPrice = 100,
    QuantityToFill = 1,
};
// A stop-limit sell order: triggered at 100, executed at 95 or better, 1 lot
private Order StopLimitSell() => new()
{
    Portfolio = Portfolio,
    Security = Security,
    Direction = OrderDirections.Sell,
    Type = OrderTypes.StopLimit,
    TriggerPrice = 100,
    Price = 95,
    QuantityToFill = 1,
};

Placing an order

OnCalculate and the other handlers are not asynchronous, so start the operation without waiting for it (_ = ...) and handle its errors inside the started method. If the trading connection rejects the request, OpenOrderAsync throws: catch the exception. Without an exception, the order may still have been sent or not: when the strategy is not started, trading is blocked or the volume does not pass the check, the task completes without sending anything, and the order keeps the OrderStatus.None status. So check order.Status() after the await:

private async Task OpenPositionAsync(Order order)
{
    try
    {
        await OpenOrderAsync(order);

        // No exception: the request was accepted, check what became of the order.
        switch (order.Status())
        {
            case OrderStatus.None:
                // the order was not sent (for example, the strategy is not started or trading is blocked).
                break;
            case OrderStatus.Placed:
                // the order is placed.
                break;
            case OrderStatus.Filled:
                // the order is filled.
                break;
            case OrderStatus.PartlyFilled:
                // the order is partially filled; order.Unfilled is the remaining volume.
                break;
        }
    }
    catch (Exception ex)
    {
        // The trading connection rejected the request.
        this.LogInfo($"Open position error: {ex.Message}.");
    }
}

The later life of a placed order (a fill, a partial fill, a cancellation) arrives through the handlers OnOrderChanged, OnOrderRegisterFailed, OnOrderModifyFailed and OnOrderCancelFailed. They receive all orders of the chart instrument and portfolio, including the orders placed by hand or by other strategies, so always check that the order is the one your strategy sent. A rejected order gets the Failed State, and Status() returns OrderStatus.None for it, the same as for an order that has not been accepted yet, so check the State to tell them apart.

protected override void OnOrderRegisterFailed(Order order, string message)
{
    if (order == _order)
    {
        // order placement error, see message for details.
    }
}

protected override void OnOrderChanged(Order order)
{
    // The handler receives all orders of the chart instrument and portfolio,
    // including orders placed manually, so check that this is our order.
    if (order != _order)
        return;

    if (order.State == OrderStates.Failed)
    {
        // the order was rejected (its Status() is OrderStatus.None).
        return;
    }

    switch (order.Status())
    {
        case OrderStatus.None:
            // the order has not been accepted yet (wait for the next calls).
            break;
        case OrderStatus.Placed:
            // the order is placed.
            break;
        case OrderStatus.Filled:
            // the order is filled.
            break;
        case OrderStatus.PartlyFilled:
            // the order is partially filled; order.Unfilled is the remaining volume.
            break;
        case OrderStatus.Canceled:
            // the order is canceled (it may have been partially filled before that).
            break;
    }
}

The complete strategy:

using System;
using System.Threading.Tasks;

using ATAS.DataFeedsCore;
using ATAS.Strategies.Chart;
using Utils.Common.Logging;

namespace ATAS.Docs.Samples.ChartStrategies;

public class PlaceOrderStrategy : ChartStrategy
{
    private Order? _order;

    protected override void OnCalculate(int bar, decimal value)
    {
        // CanProcess is true only for the last bar of a started strategy,
        // so nothing is sent while the history is being calculated.
        if (!CanProcess(bar) || _order != null)
            return;

        _order = new Order
        {
            Portfolio = Portfolio,
            Security = Security,
            Direction = OrderDirections.Buy,
            Type = OrderTypes.Limit,
            Price = 100,
            QuantityToFill = 1,
        };

        // OnCalculate cannot be awaited: start the operation without waiting for it.
        // OpenPositionAsync handles its own errors.
        _ = OpenPositionAsync(_order);
    }

    private async Task OpenPositionAsync(Order order)
    {
        try
        {
            await OpenOrderAsync(order);

            // No exception: the request was accepted, check what became of the order.
            switch (order.Status())
            {
                case OrderStatus.None:
                    // the order was not sent (for example, the strategy is not started or trading is blocked).
                    break;
                case OrderStatus.Placed:
                    // the order is placed.
                    break;
                case OrderStatus.Filled:
                    // the order is filled.
                    break;
                case OrderStatus.PartlyFilled:
                    // the order is partially filled; order.Unfilled is the remaining volume.
                    break;
            }
        }
        catch (Exception ex)
        {
            // The trading connection rejected the request.
            this.LogInfo($"Open position error: {ex.Message}.");
        }
    }

    protected override void OnOrderRegisterFailed(Order order, string message)
    {
        if (order == _order)
        {
            // order placement error, see message for details.
        }
    }

    protected override void OnOrderChanged(Order order)
    {
        // The handler receives all orders of the chart instrument and portfolio,
        // including orders placed manually, so check that this is our order.
        if (order != _order)
            return;

        if (order.State == OrderStates.Failed)
        {
            // the order was rejected (its Status() is OrderStatus.None).
            return;
        }

        switch (order.Status())
        {
            case OrderStatus.None:
                // the order has not been accepted yet (wait for the next calls).
                break;
            case OrderStatus.Placed:
                // the order is placed.
                break;
            case OrderStatus.Filled:
                // the order is filled.
                break;
            case OrderStatus.PartlyFilled:
                // the order is partially filled; order.Unfilled is the remaining volume.
                break;
            case OrderStatus.Canceled:
                // the order is canceled (it may have been partially filled before that).
                break;
        }
    }
}

Changing an order

To change a placed order, clone it, change the price or the volume of the copy and pass the old and the new order to ModifyOrderAsync. When the task completes without an exception, the new order has replaced the old one: from then on, keep the new one. Check that the order is OrderStates.Active before sending the change, and send it once: the strategy below changes the price of its limit order when a new bar starts.

using System;
using System.Threading.Tasks;

using ATAS.DataFeedsCore;
using ATAS.Strategies.Chart;
using Utils.Common.Logging;

namespace ATAS.Docs.Samples.ChartStrategies;

public class ModifyOrderStrategy : ChartStrategy
{
    private Order? _order;
    private int _orderBar;
    private bool _modifySent;

    protected override void OnCalculate(int bar, decimal value)
    {
        if (!CanProcess(bar))
            return;

        if (_order == null)
        {
            // Here we receive a signal and place a limit order.
            _order = new Order
            {
                Portfolio = Portfolio,
                Security = Security,
                Direction = OrderDirections.Buy,
                Type = OrderTypes.Limit,
                Price = 100,
                QuantityToFill = 1,
            };

            _orderBar = bar;
            _ = PlaceOrderAsync(_order);
            return;
        }

        // The condition to change the price has been met (here: a new bar has started).
        // The modification is sent only once.
        if (!_modifySent && bar > _orderBar && _order.State == OrderStates.Active)
        {
            _modifySent = true;
            _ = ChangeOrderAsync(_order);
        }
    }

    private async Task PlaceOrderAsync(Order order)
    {
        try
        {
            await OpenOrderAsync(order);
        }
        catch (Exception ex)
        {
            this.LogError("Open order error.", ex);
        }
    }

    private async Task ChangeOrderAsync(Order order)
    {
        var newOrder = order.Clone();
        newOrder.Price = 200;

        try
        {
            await ModifyOrderAsync(order, newOrder);

            // The change was successful: from now on, work with the new order.
            _order = newOrder;
        }
        catch (Exception ex)
        {
            // The order could not be changed.
            this.LogError("Modify order error.", ex);
        }
    }
}

Cancelling an order

CancelOrderAsync asks the trading connection to cancel the order; the task throws when the cancellation is refused. Check that the order is OrderStates.Active before cancelling it and remember which orders you have already asked to cancel: a second cancel of the same order is not sent, an error is written to the log instead.

using System;
using System.Threading.Tasks;

using ATAS.DataFeedsCore;
using ATAS.Strategies.Chart;
using Utils.Common.Logging;

namespace ATAS.Docs.Samples.ChartStrategies;

public class CancelOrderStrategy : ChartStrategy
{
    private Order? _order;
    private int _orderBar;
    private bool _cancelSent;

    protected override void OnCalculate(int bar, decimal value)
    {
        if (!CanProcess(bar))
            return;

        if (_order == null)
        {
            // Here we receive a signal and place a limit order.
            _order = new Order
            {
                Portfolio = Portfolio,
                Security = Security,
                Direction = OrderDirections.Buy,
                Type = OrderTypes.Limit,
                Price = 100,
                QuantityToFill = 1,
            };

            _orderBar = bar;
            _ = PlaceOrderAsync(_order);
            return;
        }

        // The condition to cancel the order has been met (here: a new bar has started).
        // The cancellation is sent only once.
        if (!_cancelSent && bar > _orderBar && _order.State == OrderStates.Active)
        {
            _cancelSent = true;
            _ = TryCancelOrderAsync(_order);
        }
    }

    private async Task PlaceOrderAsync(Order order)
    {
        try
        {
            await OpenOrderAsync(order);
        }
        catch (Exception ex)
        {
            this.LogError("Open order error.", ex);
        }
    }

    private async Task TryCancelOrderAsync(Order order)
    {
        try
        {
            await CancelOrderAsync(order);

            // The order was cancelled successfully.
        }
        catch (Exception ex)
        {
            // The order could not be cancelled.
            this.LogError("Cancel order error.", ex);
        }
    }
}

Points to consider

Order statuses and the position arrive asynchronously, as the responses come from the exchange. The usual mistakes:

  • A trigger to cancel an order fires, the strategy sends the cancel; before the response arrives, the trigger fires again and the strategy sends another cancel for the same order. The second one is ignored with an error in the log. Remember which orders you have asked to cancel instead of relying on the trigger.
  • A trigger to open a position fires, the strategy sees CurrentPosition == 0 and sends a market order; before the fill is reported and the position updated, the trigger fires again and a second order goes out, doubling the risk.
  • A trigger to close a position fires, the strategy sends an order of the volume of CurrentPosition; before the position is updated, the trigger fires again and a second closing order reverses the position instead of closing it.

Keep a flag or the order object for every pending operation, as the examples above do, and clear it in the handlers.

Order options

Some connectors support additional order options. They are set on the order before it is sent, through the TradingManager of the strategy: GetSecurityTradingOptions() returns what the connector supports for the instrument, CreateExtendedOptions(order.Type) creates the option set for the order type, and the set implements an interface per option. Where the connector does not support an option, the cast fails and the order is sent without it.

Reduce only ensures that the order only reduces an existing position and never increases the exposure.

private void TrySetReduceOnly(Order order)
{
    var flags = TradingManager?
        .GetSecurityTradingOptions()?
        .CreateExtendedOptions(order.Type);

    if (flags is not IOrderOptionReduceOnly reduceOnly)
        return;

    reduceOnly.ReduceOnly = true;

    order.ExtendedOptions = flags;
}

Post only makes the order a maker order: if it would execute immediately as a taker order, the exchange cancels it instead.

private void TrySetPostOnly(Order order)
{
    var flags = TradingManager?
        .GetSecurityTradingOptions()?
        .CreateExtendedOptions(order.Type);

    if (flags is not IOrderOptionPostOnly postOnly)
        return;

    postOnly.PostOnly = true;

    order.ExtendedOptions = flags;
}

Close on trigger marks an order that closes the position when its trigger condition is met.

private void TrySetCloseOnTrigger(Order order)
{
    var flags = TradingManager?
        .GetSecurityTradingOptions()?
        .CreateExtendedOptions(order.Type);

    if (flags is not IOrderOptionCloseOnTrigger closeOnTrigger)
        return;

    closeOnTrigger.CloseOnTrigger = true;

    order.ExtendedOptions = flags;
}