DEV Community

Timevolt
Timevolt

Posted on

Trading Systems: Don't Become the Redshirt of Wall Street

The Quest Begins (The "Why")

I still remember the first time I tried to spin up a tiny trading bot for fun. I was fresh out of a bootcamp, buzzing with excitement, and thought, “How hard can it be? I’ll just fetch prices, send orders, and watch the profits roll in!” I opened my favorite IDE, wrote a quick script that pulled ticker data from a public API, and fired off market orders whenever the price moved a tick. It felt like I’d just discovered a cheat code.

Then reality hit. After a few hours of running the bot on a testnet, I noticed my P&L was drifting weirdly. Sometimes I’d buy 1.00000001 BTC instead of exactly 1.0, and my profit calculations were off by fractions of a cent that added up over thousands of trades. Other times, two order‑update messages would arrive almost simultaneously, and my internal state would get corrupted—leading to duplicate fills or, worse, sending a sell order when I still had a long position. I felt like I’d stepped into a trap door and was tumbling down a dark shaft, wondering where I went wrong.

That moment sparked a quest: I wanted to uncover the common pitfalls that turn a promising trading system into a fragile house of cards. If you’ve ever felt your code behaving like a mischievous gremlin, you’re in the right place. Let’s slay those dragons together.

The Revelation (The Insight)

After digging through forums, reading exchange documentation, and (painfully) debugging my own logs, I realized two core issues kept showing up in almost every rookie trading system:

  1. Treating money like a floating‑point number – using float or double for prices, quantities, or P&L inevitably introduces rounding errors. In high‑frequency or high‑volume scenarios those tiny errors snowball into noticeable losses or even invalid order sizes that exchanges reject.

  2. Assuming single‑threaded safety in an asynchronous world – market data feeds, order acknowledgments, and fill notifications arrive concurrently. If you mutate shared state (like position counters or order maps) without proper synchronization, you open the door to race conditions that corrupt your bookkeeping and can lead to illegal orders.

The fix? Adopt a mindset of exact arithmetic and defensive concurrency. Use integer‑based representations for money (think “cents” or the smallest tick size the exchange offers) and guard mutable state with locks, queues, or actor‑style message passing. When you make those shifts, the system stops feeling like a ticking time bomb and starts behaving like a reliable trading engine.

Wielding the Power (Code & Examples)

Below are before‑and‑after snippets in Python that illustrate each mistake and how to correct them. I’ve kept the examples deliberately simple so you can drop them into a prototype and see the difference instantly.

Mistake #1: Floating‑Point Money

Before (the struggle):

# DON'T DO THIS – using float for BTC quantity and USD price
btc_quantity = 0.001          # looks fine, but it's a binary float
usd_price   = 27_345.67

# Calculate notional value
notional = btc_quantity * usd_price   # 27.345669999999998 ?!
print(f"Notional: {notional:.8f} USD")
Enter fullscreen mode Exit fullscreen mode

Running this prints something like 27.34566700 because the binary representation of 0.001 can’t be exact. Over thousands of trades the drift becomes measurable, and if you try to send an order with quantity 0.0010000000000000002 the exchange will reject it for invalid precision.

After (the victory):

# DO THIS – work in integer satoshis (or the exchange's minimal tick)
# 1 BTC = 100_000_000 satoshis
btc_quantity_sat = 100_000          # 0.001 BTC exactly
usd_price_cents  = 2_734_567        # $27,345.67 expressed in cents

# Notional in cents * satoshis → we keep two separate integer fields
notional_cents = btc_quantity_sat * usd_price_cents   # 273_456_700_000
# To display, convert back:
btc_display = btc_quantity_sat / 100_000_000
usd_display = usd_price_cents / 100
print(f"Notional: {notional_cents / 100:.2f} USD")   # 27,345.67 USD
Enter fullscreen mode Exit fullscreen mode

Now the quantity is exact, the price is exact, and the notional calculation stays integer‑precise. When you need to send the order to the exchange, you simply divide by the appropriate factor (or use the exchange’s prescribed integer field). No more sneaky rounding gremlins!

Mistake #2: Unsafe Shared State

Before (the struggle):

import threading

position = 0.0          # BTC position – a float (again, just for illustration)
order_map = {}          # client_order_id → order details

def handle_fill(msg):
    global position
    # Assume msg contains {'side': 'buy'/'sell', 'qty': float, 'client_order_id': str}
    qty = msg['qty']
    if msg['side'] == 'buy':
        position += qty
    else:
        position -= qty
    order_map.pop(msg['client_order_id'], None)   # remove filled order

# Simulated concurrent callbacks from two threads
t1 = threading.Thread(target=handle_fill, args=({'side':'buy','qty':0.005,'client_order_id':'A1'},))
t2 = threading.Thread(target=handle_fill, args=({'side':'sell','qty':0.003,'client_order_id':'A2'},))
t1.start(); t2.start()
t1.join(); t2.join()
print(f"Final position: {position}")
Enter fullscreen mode Exit fullscreen mode

If the two threads interleave at the wrong moment, you could end up adding quantities twice or removing the wrong order ID, leaving position corrupted and order_map in an inconsistent state. In a live system that could mean sending an order that exceeds your risk limits—or worse, submitting an order you don’t actually own.

After (the victory):

import threading

# Use a lock to protect all shared mutable state
_state_lock = threading.Lock()
position_sat = 0                     # integer satoshis
order_map = {}                       # client_order_id → dict

def handle_fill(msg):
    global position_sat
    with _state_lock:                # <-- critical section
        qty_sat = int(msg['qty'] * 100_000_000)   # convert to satoshis
        if msg['side'] == 'buy':
            position_sat += qty_sat
        else:
            position_sat -= qty_sat
        order_map.pop(msg['client_order_id'], None)

# Same simulated concurrent callbacks – now safe
t1 = threading.Thread(target=handle_fill, args=({'side':'buy','qty':0.005,'client_order_id':'A1'},))
t2 = threading.Thread(target=handle_fill, args=({'side':'sell','qty':0.003,'client_order_id':'A2'},))
t1.start(); t2.start()
t1.join(); t2.join()
print(f"Final position: {position_sat / 100_000_000:.8f} BTC")
Enter fullscreen mode Exit fullscreen mode

By guarding the updates with a simple threading.Lock, we guarantee that only one thread can modify position_sat and order_map at a time. The result? Deterministic bookkeeping, no phantom positions, and the confidence to scale to multiple threads or even async workers.

Why This New Power Matters

When you switch to exact integer money and protect your state with proper synchronization, the trading system stops feeling like a fragile prototype and starts behaving like a production‑grade engine. You’ll notice:

  • Order rejections drop dramatically because quantities and prices always conform to the exchange’s precision rules.
  • Profit‑and‑loss calculations become audit‑ready—you can reconcile every trade down to the smallest tick without chasing floating‑point ghosts.
  • Concurrency bugs become rare, letting you safely add more data feeds, strategy layers, or risk checks without worrying about hidden race conditions.

In short, you gain the reliability to focus on the fun part—designing and testing strategies—instead of firefighting subtle numerical or synchronization bugs.

Your Turn – A Quick Challenge

Grab a small market‑data WebSocket feed (many exchanges offer a free testnet). Write a mini‑bot that:

  1. Receives ticker updates.
  2. When the price crosses a simple moving average, sends a limit order using integer‑based quantity (e.g., satoshis or the exchange’s smallest lot).
  3. Updates an internal position map inside a lock (or an async‑safe queue if you’re using asyncio).

Run it for a few minutes on the testnet, watch the logs, and see how clean the bookkeeping feels. Then tweet me your results or drop a comment below—I’d love to hear how your quest went!

Happy coding, and may your orders always fill at the price you expect!

Top comments (0)