The Quest Begins (The "Why")
I still remember the first time I tried to turn a simple Python script into a live trading bot. I’d spent weeks back‑testing a moving‑average crossover on historic data, felt like Neo staring at the green code rain, and thought, “I’ve got this.” I hooked the script up to a test‑net exchange, hit run, and watched my account balance swing like a pendulum in a hurricane. Within minutes I’d placed duplicate orders, suffered horrendous slippage, and ended up with a negative cash balance that made my boss raise an eyebrow.
That nightmare taught me something vital: building a trading system isn’t just about writing a clever algorithm; it’s about surviving the chaos of real‑time markets. The dragons we face aren’t mythical beasts—they’re latency spikes, rounding errors, and race conditions that can turn a profitable strategy into a costly lesson.
The Revelation (The Insight)
After that humbling experience, I dug into the guts of a few production‑grade trading libraries and talked to veterans who’d survived multiple market crashes. The biggest “aha!” moment came when I realized two simple, yet deadly, mistakes kept popping up in beginner codebases:
Using floating‑point numbers for money and prices.
A price like0.1can’t be represented exactly in binary floating point, so repeated additions/subtractions drift away from the true value. In a high‑frequency system that drift compounds into mis‑priced orders, missed fills, or even regulatory violations.Assuming order IDs are unique without handling concurrency.
When you fire off orders from multiple threads or async tasks, it’s easy to generate the same client‑order ID twice. Exchanges reject duplicate IDs, leaving you hanging, or worse, they accept them and you end up with unintended position size.
Fixing these issues felt like discovering the cheat code that lets Neo see the Matrix for what it really is—structured, predictable, and beatable if you know the rules.
Wielding the Power (Code & Examples)
Below are the “before” snippets that caused me grief, followed by the “after” versions that keep the system sane. I’ll keep the examples in Python because it’s quick to read, but the concepts translate to any language.
Mistake #1 – Floating‑Point Money
Before (the trap):
# DON'T DO THIS
def calculate_position(size: float, price: float) -> float:
return size * price # size in BTC, price in USDT
# Example usage
btc_amount = 0.1
usdt_price = 30000.0
notional = calculate_position(btc_amount, usdt_price)
print(notional) # 2999.9999999999995 ???
The result is almost 3000, but not exactly. When you later compare notional against a risk limit (if notional > MAX_NOTIONAL:), you might slip through the cracks or trigger a false alarm.
After (the victory):
from decimal import Decimal, getcontext
# Set enough precision for crypto prices (8 decimal places is common)
getcontext().prec = 28
def calculate_position(size: Decimal, price: Decimal) -> Decimal:
return size * price # exact decimal arithmetic
# Example usage
btc_amount = Decimal('0.1')
usdt_price = Decimal('30000.0')
notional = calculate_position(btc_amount, usdt_price)
print(notional) # 3000.0 – exactly what we expect
Using Decimal (or a dedicated fixed‑point library) guarantees that money‑related math stays precise, no matter how many times you add, subtract, or scale values.
Mistake #2 – Duplicate Order IDs in Concurrent Code
Before (the trap):
import asyncio
import time
order_counter = 0 # shared across all coroutines
async def send_order(exchange, symbol, side, qty):
global order_counter
order_counter += 1 # ← not atomic!
client_order_id = f"order_{order_counter}"
await exchange.create_order(
symbol=symbol,
type='market',
side=side,
amount=qty,
params={'newClientOrderId': client_order_id}
)
print(f"Sent {client_order_id}")
async def main():
exchange = await init_exchange() # pretend async CCXT wrapper
tasks = [send_order(exchange, 'BTC/USDT', 'buy', 0.01) for _ in range(5)]
await asyncio.gather(*tasks)
asyncio.run(main())
If two coroutines increment order_counter at the same microsecond, they can end up with the same ID, causing the exchange to reject the second order—or, in some flawed implementations, to accept it and double your position.
After (the victory):
import asyncio
import uuid # universally unique identifier, thread‑safe
async def send_order(exchange, symbol, side, qty):
client_order_id = str(uuid.uuid4()) # guaranteed unique
await exchange.create_order(
symbol=symbol,
type='market',
side=side,
amount=qty,
params={'newClientOrderId': client_order_id}
)
print(f"Sent {client_order_id}")
async def main():
exchange = await init_exchange()
tasks = [send_order(exchange, 'BTC/USDT', 'buy', 0.01) for _ in range(5)]
await asyncio.gather(*tasks)
asyncio.run(main())
By delegating uniqueness to uuid.uuid4(), we remove the need for a shared counter and eliminate the race condition entirely. The exchange sees a fresh ID every time, and we can safely fire off hundreds of orders per second without worrying about duplicates.
Quick Bonus – Handling Latency Gracefully
While not a “mistake” per se, many newbies block the main thread waiting for an exchange response. Here’s a tiny pattern that keeps the bot responsive:
import aiohttp
import asyncio
async def fetch_ticker(session, symbol):
async with session.get(f'https://api.example.com/ticker?symbol={symbol}') as resp:
return await resp.json()
async def price_loop():
async with aiohttp.ClientSession() as sess:
while True:
ticker = await fetch_ticker(sess, 'BTC/USDT')
print(f"Mid price: {(ticker['bid'] + ticker['ask']) / 2}")
await asyncio.sleep(0.05) # 20 Hz non‑blocking poll
asyncio.run(price_loop())
Using asyncio (or similar event‑loop frameworks) lets the bot react to market moves while still handling network jitter, reconnects, and other housekeeping chores.
Why This New Power Matters
Now that I’ve swapped floating‑point for Decimal and replaced fragile counters with UUIDs, my bots behave like well‑trained agents in the Matrix: they see the exact numbers, they never step on each other’s toes, and they can scale to dozens of strategies running in parallel without a hitch.
The payoff?
- Accurate P&L tracking – no mysterious pennies disappearing.
- Fewer rejected orders – exchanges love unique IDs, and we love not getting throttled.
- Confidence to iterate – when the foundation is solid, you can spend time sharpening the signal, not patching bugs.
In short, you get to spend your energy on the fun part—researching alpha—while the infrastructure quietly does its job.
Your Turn: The Challenge
I dare you to take a simple trading script you’ve got lying around (or write a fresh one in 10 minutes) and apply these two fixes:
- Replace every float that represents price, size, or notional with
Decimal. - Swap any manual order‑ID generation for a UUID (or your exchange’s built‑in client‑order ID generator).
Run it against a test‑net or a paper‑trading endpoint for a few minutes. Observe how the logs become cleaner, how the order IDs are all unique, and how your P&L calculations line up with the exchange’s reported values.
When you see that smooth, error‑free stream of trades, drop a comment below with your before/after snippet or just shout “I’ve seen the code!”—I’d love to hear how the quest went for you.
Happy hunting, and may your orders always be filled at the price you expect! 🚀
Top comments (0)