Engineering9 min read

Build in Public: A 0.16% fill exposed a 100% assumption

One limit order planned 143,342 units. Only 226 filled. The resulting trade was profitable and its recorded RR was mathematically defensible — but letting the position manager and the learning system treat it like a completed trade exposed a much deeper modeling error. This is how we separated trade intent, live execution state, and statistical evidence.

By

partial-fillexecutionrisk-managementposition-managementstatisticsRAGbuild-in-publicBybit

In brief

  • A position can exist before its entry lifecycle is finished. The first fill must not be treated as proof that the intended position has been built.
  • Planned risk, deployed risk and fill completeness answer different questions; collapsing them into one position row makes both automation and analytics unsafe.
  • While an entry order can still add exposure, active position management is frozen and released only by a terminal event from that exact root order.
  • Dust fills remain in cash PnL and equity, but are excluded from win rate, profit factor, loss streaks and RAG memory so execution accidents do not become strategy evidence.

One of our limit orders planned a position of 143,342 TRX. The market filled 226 TRX — 0.16% of the intended size — touched the order, moved away, and left almost the entire entry resting on the exchange.

The tiny position made $0.0636. Its recorded result was about +0.80R.

That number was mathematically defensible. It was also dangerous evidence.

The R multiple described the risk attached to the 226 units that actually traded. The original setup had planned roughly $50 of risk. Measured against that plan, the cash result represented about +0.0013R. Both numbers can be true because they answer different questions.

The real bug was not a division formula. It was that the system saw one positions row and silently concluded three things:

  1. the entry had finished,
  2. the position was ready for active management,
  3. the result was a complete observation of the strategy.

None of those conclusions followed from a 0.16% fill.

The first fill is not the end of an entry

On an exchange, a partially filled limit order creates a real position immediately. That is correct. There is exposure, unrealized PnL and a quantity that can be closed.

But another fact remains true at the same time: the unfilled part of the entry order may still be live.

planned entry: 143,342

               ├── filled now:       226
               └── still available: 143,116

If a position manager reacts to the 226-unit fragment — takes a partial profit, moves the stop to breakeven, or changes protection — the remaining 143,116 units can fill later into a state designed for a completely different exposure.

That is the subtle failure mode. A management decision can be locally correct for the current position and globally wrong for the order lifecycle that is still changing it.

Our original model did not represent that lifecycle. A position existed, so the management system treated it as settled.

One trade, three kinds of truth

The fix started by refusing to use one number as a proxy for everything else.

Layer Question Data
Intent What did the strategy decide to risk? planned size, planned risk
Execution What exposure exists right now? filled size, cash PnL, executed risk
Lifecycle Can the entry still change that exposure? exact entry order, terminal/open state
Evidence Is this a representative strategy sample? fill ratio, quality classification

These layers interact, but they are not interchangeable.

Planned risk is not cash PnL. Current size is not final size. A profitable fill is not automatically a useful training example. And the existence of a position is not proof that its entry order is finished.

This is the same general lesson that shaped our POI state machine: when several stages can be true at different times, implicit state eventually turns into contradictory behavior. The state has to be named.

The new invariant: manage only settled exposure

Every root entry order now persists its intended quantity and risk budget. When the exchange reports a new position, the WebSocket path correlates it back to that exact plan and derives:

fill_ratio = initial_size / planned_size
 
entry_order_open = (
    root_entry_is_not_terminal
    and fill_ratio < 0.999
)

If entry_order_open is true, active management is frozen.

That gate is applied at every path that can make a management decision: scheduled review, deterministic partials, opposite-POI logic, orphan handling, technical-analysis management and the news-aware manager. The exchange-native protection already attached to the position remains in place; the freeze prevents the agent from redesigning a position whose size is still unsettled.

The freeze ends when the root entry order reaches a terminal state: filled, cancelled, rejected, expired, invalidated or deactivated.

The important word is root. A take-profit fill is not the end of the entry lifecycle. Neither is a stop order update.

Exact order identity matters more than shared context

Our entry flow can retry a post-only order. The cancelled attempt and its replacement may share the same symbol, side, setup and POI. That makes contextual matching useful for finding a plan, but unsafe for changing lifecycle state.

Imagine this sequence:

entry A  ── partially fills ── cancelled
entry B  ── replacement for the same setup ── remains open

If the cancellation of A clears the freeze by POI or business key alone, it can accidentally unfreeze the position owned by B. The system now stores the exact orderLinkId that owns the position's plan. A terminal event can clear only the row pointing to that identifier.

This sounds like a database detail. In practice, it is the difference between correlation and identity. Correlation helps us find likely relatives. Identity is what authorizes a state transition.

Why tiny fills corrupt statistics in a particular direction

A dust fill is not a random sample of the same trade at a smaller scale.

For a resting limit order to receive a tiny fill and then stop filling, price often has to tag the level and move away. When that move is in the intended direction, the record is unusually likely to look like a winner even though almost none of the planned risk was deployed.

If every closed row counts as one full trade, a few cents can contribute:

  • one win to win rate,
  • a high R multiple calculated on tiny executed risk,
  • another positive observation in profit factor,
  • a new success example for a retrieval system.

The cash is real. The statistical weight is not.

We now classify a position as a dust fill when the known initial fill is below 25% of its planned size. The threshold is an operational data-quality policy, not a claim about market microstructure. We chose it to separate the clear low-fill cluster visible in our incident audit from materially deployed trades, and we keep the threshold explicit so it can be challenged with more data.

Unknown plans are not classified as dust. Missing evidence cannot be turned into a confident label.

We did not rewrite the old R multiple

The tempting fix would be to replace every partial fill's realized_rr with cash PnL divided by planned risk.

We deliberately did not do that.

realized_rr already has an established meaning in our execution ledger: return relative to the risk actually present in the executed position. Reinterpreting the same column after the fact would make old and new records incomparable and quietly break consumers that trust its semantics.

Instead, we preserve both facts:

executed-capital R = cash result / deployed risk
planned-capital R  = cash result / planned risk

The first remains available for execution analysis. The second can be derived because planned risk is now stored. For strategy-quality aggregates, a dust fill is excluded rather than rewritten into a different kind of trade.

That produces a deliberately asymmetric accounting rule:

Consumer Dust fill treatment
Cash PnL included
Equity and drawdown included
Win rate and trade count excluded
Profit factor and average RR excluded
Loss-streak logic excluded
Trading Memory RAG excluded

The account must remember every cent. The strategy model should learn only from observations that actually tested a meaningful part of the plan.

RAG made the bug more consequential

Before trading memory, a malformed observation could distort a dashboard. Once historical outcomes can influence future reasoning, the same observation can become precedent.

Our Trading Memory RAG deliberately keeps outcomes outside the embedded narrative to prevent result leakage into semantic similarity. But retrieved memories still carry outcome metadata after retrieval. A 0.16% fill labelled as a successful +0.80R trade would therefore tell the agent that a setup worked when the intended position was never meaningfully tested.

The new rule is simple: dust fills never become entry-performance memories. They remain in the financial ledger and can still be inspected as execution incidents.

This is not deleting inconvenient results. Losses below the threshold are excluded by the same rule. The filter is based on execution completeness, not outcome.

Historical repair without guessing

Adding fields fixes new events. It does nothing for old positions whose plan was never stored beside them.

We built a backfill that searches the order history for the most credible root entry using user, symbol, side, business key and time. The time relationship matters because an order can be partially filled and later cancelled; its final status no longer advertises that it once created exposure.

The script is dry-run by default. It reports:

  • the proposed order-to-position match,
  • planned and executed quantities,
  • deployed versus planned risk,
  • the resulting fill ratio,
  • whether an open position should currently remain frozen,
  • records for which no defensible plan can be found.

Only an explicit apply mode writes the result. Ambiguous records stay unknown.

That constraint is part of the design, not an inconvenience. Backfilling false precision would merely replace one modeling error with another.

What this incident changed in our mental model

We had already learned to keep position-management triggers — session boundaries, bias changes, price alerts and user requests — behind a common decision layer. We wrote about that in user-defined position triggers.

This incident exposed a condition even earlier in the chain: before deciding why a position should be managed, the system has to know whether the position is ready to be managed at all.

The durable rule is now:

A position may be financially real while still being operationally incomplete.

That distinction is useful far beyond one exchange or one order type. It applies anywhere an asynchronous system turns intent into reality in pieces: payments, inventory reservations, distributed jobs, even document workflows. The first observable result is not necessarily the terminal state.

In automated trading, pretending otherwise creates two kinds of leverage. The obvious one is market exposure. The less obvious one is epistemic leverage: one tiny execution can be promoted into a full-strength belief about the strategy.

The first needs a lifecycle guard. The second needs honest statistics.

This bug gave us both.

ShareX / TwitterThreads
sc4mp avatar

Public pen name of LiquidMind's founder and builder. Writing first-hand engineering notes and transparent performance reviews from the system's internal ledger.

Stay Liquid

New posts on transparency, engineering, and the LiquidMind thesis — no noise.

Loading discussion…