Trading Bot in C# — Part 5— Order Cancelling
In the previous blog post, we covered monitoring of order (partial) fills. In this article, we will expand our toolbox by discussing how to cancel orders and outlining the common issues one may face in the process.
Introduction
In trading, there is only a limited number of actions one can take. Actually, one typically just ask an exchange to create an order, or to cancel an existing order. So, when do people decide to cancel orders?
- When a trading opportunity is missed, it makes sense to cancel an existing order and create a new one when another opportunity arises. A good example of this is a missed arbitrage opportunity, which typically exists only for a brief period of time before being taken advantage of by other traders.
- People who execute orders manually often cancel orders that were placed due to fat-finger errors — that is when one mistypes the price or order size before submitting the order request.
- … and of course when you feel like you are rich enough. :-)
Note that some exchanges attempt to help people avoid unwanted mistakes by allowing people to trade only in a band of prices, meaning you can buy and sell an asset but with a price that is, e.g., up to 10% higher or lower than the last price. This avoids you to buy bitcoin for $950,000 instead of intended $95,000. However, it can limit you, for instance, in profiting from a serious market crash.
While it is possible to cancel any type of order, in practice, canceling, e.g., market orders can be challenging since such orders can get filled right away. Therefore, this blog post will focus solely on cancelling of limit orders.
API for Order Cancelling
In line with the reasons for canceling an order, we can demonstrate the API by placing a limit order and then canceling it after two seconds:
// Make sure the price is greater than the current market price.
Print("Place a limit sell order to sell 0.00015 BTC @ 120,000 EUR.");
ILiveLimitOrder order = await client.CreateLimitOrderAsync(SymbolPair.BTC_EUR,
OrderSide.Sell, price: 120_000m, size: 0.00015m);
Print("Wait 2 seconds.");
await Task.Delay(2_000);
Print($"Cancel the order '{order.ExchangeOrderId}'.");
try
{
await client.CancelOrderAsync(order); // <--- Actually cancels the order.
}
catch (NotFoundException e)
{
Print($"[!] {nameof(NotFoundException)}: {e.Message}");
}
Print($"Attempt to cancel the order '{order.ExchangeOrderId}' again.");
try
{
await client.CancelOrderAsync(order);
throw new SanityCheckException("No, no, no. This exception should not be thrown.");
}
catch (NotFoundException e)
{
Print($"[!] {nameof(NotFoundException)}: {e.Message}");
}
The sample output is as follows:
[2025-04-04 10:24:35.701] Ready to trade!
[2025-04-04 10:24:35.705] Place a limit sell order to sell 0.00015 BTC @ 120,000 EUR.
[2025-04-04 10:24:36.548] Wait 2 seconds.
[2025-04-04 10:24:38.549] Cancel the order 'BTC/EUR-205382'.
[2025-04-04 10:24:38.876] Attempt to cancel the order 'BTC/EUR-205382' again.
[2025-04-04 10:24:38.935] [!] NotFoundException: Order 'BTC/EUR-205382'
You may be curious as to why the code listing consistently includes a try-catch
block when calling ITradeApiClient.CancelOrderAsync(ILiveOrder order, /* ... */).
The explanation is quite simple: in trading, factors like network delays and exchange processing times introduce latencies. Consequently, you may try to cancel an order only to discover that it has already been filled in the meantime. This situation occurs frequently in practice, making it essential to account for it.
Fortunately, we considered this scenario, so our app won’t crash, right? Unfortunately, that’s it’s not so easy. Another frequent source of business bugs arises from neglecting to check whether a canceled order was partially filled or not. The following sample demonstrates how to do it:
Print($"Cancel the order '{order.ExchangeOrderId}'.");
try
{
await client.CancelOrderAsync(order);
}
catch (NotFoundException e)
{
Print($"[!] {nameof(NotFoundException)}: {e.Message}");
}
// At this stage, the possible outcomes for the order are:
// * The order is canceled.
// * The order is canceled and partially filled
// * The order is fully filled.
decimal filledSize = order.LatestFillData.CumulativeSize;
if (filledSize > 0)
Print($"The order '{order.ExchangeOrderId}' was partially filled {filledSize}.");
Always check the filled size of your orders!
Existing Orders
There are situations where you might need to cancel an order that was placed prior to launching your trading application. In these instances, you first need to obtain an instance of ILiveLimitOrder
. The ITradeApiClient
interface includes a method for retrieving all open orders:
IReadOnlyList<ILiveOrder> orders = await client.GetOpenOrdersAsync(OrderFilterOptions.AllOrders);
Print($"There are {orders.Count} open order(s).");
// Now we can, for example, print when the orders were created, or we can
// cancel the orders one by one, etc.
foreach (ILiveOrder o in orders)
Print($"Order '{o.ExchangeOrderId}' was created at {o.CreateTime} UTC.");
Please note that ScriptApiLib utilizes Coordinated Universal Time (UTC) for reporting dates and times to eliminate any confusion caused by time zones.
You might be reluctant to call client.GetOpenOrdersAsync(..)
multiple times in order to manage your API rate limits effectively, but there’s no need for that concern. The method is designed to be efficient, as ScriptApiLib locally stores open orders and monitors any changes. Therefore, you can call the method as often as you wish without affecting your ability to create new orders, etc.
Bulk Order Cancellation
Cancelling orders one by one is not efficient if you want to cancel all orders quickly. Naturally, ITradeApiClient
provides a method for this exact use case — CancelAllOrdersAsync
.
await client.CancelAllOrdersAsync();
// The behavior can be verified in the following manner:
IReadOnlyList orders = await client.GetOpenOrdersAsync(OrderFilterOptions.AllOrders);
Print($"There are {orders.Count} open order(s).");
Create an Order, Cancel an Order, and …?
In fact, there is a third action that exchanges frequently provide for order management via their APIs: the ability to modify an existing order.
It’s important to note that while exchanges typically provide some form of API for order modifications, these capabilities are often limited. The reason for this is order execution performance and fairness. Exchanges aim to deliver peak performance for their users, which involves minimizing latency in order processing. Consequently, it can be understood that the more fixed or immutable the orders are, the easier and quicker it becomes for the matching engine to maintain faster processing.
It is worth looking at the problem from a different angle: modifying an existing order can be effectively simulated by canceling the current order and placing a new one. This may seem unusual, but for instance, the KuCoin exchange utilizes this method by providing a single API endpoint that internally performs exactly this — cancels the existing order and creates a new one. The advantage for users is that this approach reduces network latency compared to making two separate API requests.
Is it fair to modify an existing order? For instance, allowing increases in order sizes may not be fair. This is because order matching engines typically operate on a “first come, first served” basis. A trader aiming to have their orders at the top of the book might place numerous small orders and later inflate them as necessary, potentially securing better execution prices than they would achieve by canceling and re-entering orders.
Conclusion
In this blog post, we described how orders can be canceled using Whale’s Secret ScriptApiLib API. Moreover, we demonstrated API for listing currently open orders. Next time, we will discuss reconnecting strategies and their behavior.
Disclaimer: The information provided in this blog post should not be considered financial advice. Always conduct your own research and consider your personal circumstances before acting on any financial information.