A Sign Convention Is an Undocumented API
Is a negative price a credit or a debit? Multi-leg fills, signed quantities and mixed labels let a system report the wrong direction while every number looks plausible. Why you reconstruct from the legs, and how to test for it.
The number that was right and meant the opposite
Picture a fill report for a two-leg position. One line says the order was sold at a price of negative 0.70. A notification a minute earlier said the same family of order was bought at positive 1.00. A person reading quickly decides one of these is a typo.
Neither is. They are two correct reports written under a convention that says: for a combined instrument, the sign of the price carries whether money came in or went out, and the buy/sell word describes something else entirely — which side of the combined instrument you hold, not which way the cash moved. Read the word and ignore the sign, and you will compute the right magnitude with the wrong direction. Nothing crashes. The totals just quietly disagree with reality.
This is a small, boring failure, and it is exactly the kind that survives for months, because every individual value looks plausible.
Why conventions bite harder than bugs
A bug is code doing something other than what its author intended. A convention mismatch is code doing precisely what its author intended, against an input that means something different from what the author assumed. Unit tests written by the same author encode the same assumption, so they pass.
Typical places this hides, none specific to any one market or vendor:
Signed prices on combined instruments. Spreads and multi-leg orders are often quoted as one number. Whether a positive value is “I pay” or “I receive” is a decision somebody made once, and it differs between systems.
Signed quantities. Some feeds report a short position as a negative count; others report a positive count and a separate side field. Summing them naively either nets the book to zero or doubles it.
Commission and fee signs. A cost can appear as a positive charge, a negative cash flow, or a negative number inside a “realised result” field that already includes it. Subtract it twice and a small loss becomes a larger one.
Direction words attached to the wrong object. “Bought” can describe the order, the instrument, the account’s resulting exposure, or the cash leg. Four readers, four meanings.
Reconstruct from primitives
The most reliable defence is to stop trusting the summarised field and derive it from the lowest-level facts available. For a multi-leg fill, that means the individual legs: what was received on each, what was paid on each, and the arithmetic that combines them. Your own code computes the net; the vendor’s summary line becomes something you compare against, not something you consume.
This buys three things.
- Direction stops depending on a label. Credit or debit falls out of the leg arithmetic. The words “bought” and “sold” become display text.
- Disagreements become visible. If your reconstructed net and the vendor’s number differ beyond rounding, you have found either a convention you misunderstood or a data problem — both worth knowing about the same day.
- Per-leg detail comes for free. The summarised number rarely contains it, and you usually want it later for attribution.
A related caution: one order is not one price. A single order may execute as several partial fills at slightly different prices, so “the entry price” is really a size-weighted average, and anything a platform derives from an average cost is an estimate that can already blend in fees. Store the fills; compute the average yourself.
Persist at the moment of the event
Reconstruction only works if the primitives still exist. Many interfaces can tell you what happened in the current session and little or nothing about earlier ones, or return records with some fields zeroed once the order is complete. If your system needs fill details after a restart, the only dependable plan is to write them down when they occur, in your own storage, in your own explicit schema.
That schema is where you settle the convention for good. Pick one internal rule — for example, “cash received is positive, cash paid is negative, always, everywhere inside our code” — and convert at the boundary where outside data enters. Everything past that boundary speaks one dialect.
Testing a convention you can’t see
You can’t inspect a convention directly, but you can build fixtures that would expose a wrong assumption:
- Round-trip case. Open and close a hypothetical two-leg position with known amounts and assert the net result matches a hand calculation, including fees.
- Mirror case. Run the same scenario with the direction flipped. The result should flip sign, not magnitude. If it doesn’t, something is reading a label instead of a quantity.
- Partial-fill case. Split one order into several fills at different prices and assert the weighted average, not the last price.
- Zeroed-field case. Feed in a record with the usual details blanked and confirm the code fails loudly rather than computing from zeros.
- Reconciliation case. Compare your derived net with an independent summary on a schedule, and alert on a gap larger than rounding.
None of these require real data. Invented numbers are better, because you can compute the expected answer on paper first.
What this generalises to
A quick self-audit for any pipeline that ingests figures from somewhere else:
- For every signed number you read, can you state in one sentence what positive and negative mean — and where that sentence is written down?
- Does any logic infer direction from a text label instead of from quantities?
- Do you derive totals from components, or accept a vendor’s total on trust?
- If a summary field and its components disagree, does anything notice?
- Are the details you will need after a restart saved at the time of the event?
- Is there exactly one internal sign convention, applied at a single entry point?
- Would a mirrored scenario in your test suite actually fail if a sign were misread?
The aim isn’t to memorise every platform’s quirks. It’s to build code where a misread sign produces a loud mismatch on day one instead of a quiet error that compounds for months.