Blog Post

Read more about Whale's Secret.

Trading Bot in C# — Part 4— Order Fills

The previous blog post covered how to place orders on the Binance Testnet. Additionally, we enhanced our trading bot to execute orders based on RSI signals. In this article, we will take a deeper dive into the topic of orders by showcasing how to monitor the individual fills of our trades.

How are Exchange Orders Processed Behind The Scenes?

If you are familiar with exchange order processing, you can skip this section.

We have previously discussed order books and noted that they generally offer some form of aggregated data regarding the bids and asks from other trading participants. However, how do exchanges handle order requests behind the scenes? The fundamental process for managing order requests by exchanges is as follows:

  1. Validate common order limits.
    Each exchange aims to create a level playing field where participants can trade while also generating profit for the exchange. To achieve this, various limits and rules are implemented (as seen in Binance’s filters). If an order request does not satisfy all exchange rules and limits, the exchange will reject the order request, preventing it from being added to the actual order book.
    For instance, a request to buy 1 satoshi (0.00000001 BTC) would be rejected by Binance and many other exchanges. Exchanges depend on fees for their revenue, and the profit from a 1-satoshi order is so minimal that there is no economic incentive to accommodate such small amounts.
  2. Specific order restrictions.
    Recently, European Union (EU) introduced a new policy regarding stable coins that requires greater transparency, and as a consequence Binance removed some stablecoins for European customers. So if you are a resident of the EU and try to place an order request with the BTC/USDT symbol, the request will be rejected. This example illustrates that exchange accounts may have different limitations, especially due to jurisdictions.
    Another example is that submitting an excessive number of bids or asks to an order book is not permitted, as this would create an unfair advantage over other participants and increase the operational costs for the exchange.
    Unfortunately, there are many reasons why your order request can be rejected and the requirements change in time. However, if your intentions are good, you are mostly fine.
  3. The order is accepted into the order book.
    After the order request is fully validated, the order is placed in the order book where the order waits until the order matching engine can pair the order with another order (or orders) to execute the order and thus produce a trade or a set of trades.
  4. The order is either filled or canceled.
    The lifetime of each order ends when the order is either fully filled (i.e. when the order matching engine finds a sufficient number of orders to pair it with to fully execute the order), or when the order is canceled by the person who placed it.

Order matching engine

The most exciting moment is when “The order is accepted into the order book.” which occurs after an order request is fully validated and it enters the order book. Let’s demonstrate it on a new market order to buy 0.02 BTC when the BTC/EUR order book is in this state:

The order matching engine takes your buy order and searches for asks with which your order can be matched with. The best ask offers 0.00001 BTC @ 75743.23 EUR, which is insufficient to fulfill the size of your market order — i.e. 0.02 BTC. The matching engine then finds another ask 0.01200 BTC @ 75752.28 EUR. Together, these two asks cover only 0.01201 out of 0.02 BTC needed. Therefore, the third-best ask (0.01170 BTC @ 75752.35 EUR) must also be considered.

In short, the first two asks are completely taken while the third one only partially because: 0.00001 + 0.01200 + 0.00799 = 0.02 BTC. Now how much will you pay for your 0.02 BTC? One can calculate it using weighted arithmetic mean and the result is about 75752.30 EUR.

By the way, did you notice the “trap” presented by the best ask? The idea is: Put a minuscule order before a real order in hopes of achieving a more favorable outcome. In this instance, there is a tiny order for 0.00001 BTC, and whoever decides to buy with a market order will consume this small order but more costly ones as well! While it is not always an actual trap, the effect remains the same for you — your trade might be executed for a worse average price than it appears at first sight.

Watching Order (Partial) Fills

As described, an order matching engine pairs orders together to generate trades. When an order is completely executed by matching it with another order, we refer to it as being filled. Each order execution smaller than the order size, we call a partial fill. However, it’s common for people to overlook this distinction.

If the concept is not entirely clear, code is worth a thousand words:

ILiveMarketOrder order = await client.CreateMarketOrderAsync(SymbolPair.BTC_EUR,
    OrderSide.Sell, size: 0.00015m);
Print($"Order '{order.ExchangeOrderId}' was created.");

// Wait until the order is fully filled.
IReadOnlyList fills = await order.WaitForFillAsync();
Print($"Order '{order.ExchangeOrderId}' was fully filled.");

// Note that small orders are typically executed in just one fill.
foreach (FillData fill in fills)
    Print($"Fill: {fill}");

static void Print(string msg)
    => Console.WriteLine($"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss.fff}] {msg}");

And sample output is

[2025-04-04 07:14:57.249] Order 'BTC/EUR-187593' was created.
[2025-04-04 07:14:57.250] Waiting for fills.
[2025-04-04 07:14:57.345] Fill: [CumulativeSize=0.00015,CumulativeAveragePrice=75430.51,LastSize=0.00015,LastAveragePrice=75430.51,ImpliedFill=False,LastFee=0,LastFeeSymbol=`BTC`]
[2025-04-04 07:14:57.346] Order 'BTC/EUR-187593' was FILLED'.

We can see that the order was executed immediately in a single full fill. The average price was 75430.51 EUR. It is noteworthy that the cumulative price and the last size are the same. However, that’s not always the case as the following listing for a buy market order of 0.1 BTC illustrates:

[2025-04-14 14:30:09.722] Ready to trade!
[2025-04-14 14:30:10.103] Order 'BTC/EUR-1556875' was created.
[2025-04-14 14:30:10.218] Fill: [CumulativeSize=0.04708,CumulativeAveragePrice=74952.93,LastSize=0.04708,LastAveragePrice=74952.93,ImpliedFill=False,LastFee=0,LastFeeSymbol=`BTC`]
[2025-04-14 14:30:10.218] Fill: [CumulativeSize=0.1,CumulativeAveragePrice=74958.465

The FillData structure includes cumulative and last values for convenience. The last size indicates the most recent size reported by the exchange in the latest trade report for the order, while the cumulative size represents the total filled size of the order to date. The same applies to the last average price and cumulative average price. We believe it’s better to focus on actual trading strategy rather than on details like storing last fill sizes to compute cumulative sizes.

Finally, let’s take a look at monitoring order fills for limit orders. While market orders are typically filled instantly, limit orders can take a long time to be filled, and we need to adjust the code accordingly:

ILiveLimitOrder order = await client.CreateLimitOrderAsync(SymbolPair.BTC_EUR,
    OrderSide.Buy, size: 0.00015m, price: 75_000m);
Print($"Order '{order.ExchangeOrderId}' was created.");

// The number of partial fills an order is not known beforehand.
while (true)
{
    Print("Waiting for fills.");
    IReadOnlyList fills = await order.WaitForPartialFillOrCloseAsync();

    if (fills.Count > 0)
    {
        foreach (FillData fill in fills)
            Print($"Fill: {fill}");
    }

    // If the order is either filled or canceled, there won't be any new fills.
    if (order.OrderStatus.IsTerminated())
    {
        string status = order.OrderStatus.ToString().ToUpperInvariant();
        Print($"Order '{order.ExchangeOrderId}' was {status}'.");
        break;
    }
}

Note that ILiveOrder.WaitForFillAsync() is not a great fit for limit orders because:

  • Limit orders can be canceled, in which case the method would throw.
  • All (partial) fills would be returned by the method once the order is fully filled. However, consider an order that is filled 0.999 / 1.0 BTC immediately and the remaining size is executed after three days.

Conclusion

In this blog post, we outlined the process by which exchanges handle orders, starting from the moment an order request is submitted until the order is completed. We also demonstrated how to monitor individual partial fills. Next time, we will discuss monitoring of active orders and how to cancel orders.

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.