TimevoltThe Quest Begins (The "Why") Honestly, I still remember the first time I tried to build a...
Honestly, I still remember the first time I tried to build a tiny crypto‑trading bot. I was pumped, thinking I’d cracked the code to passive income while I slept. I wired up a quick Python script that pulled ticker data from an exchange, decided when to buy or sell based on a simple moving‑average crossover, and fired off market orders. The first few runs looked promising—my notebook showed a neat upward curve, and I felt like a superhero who’d just discovered a secret lair.
Then reality hit. After a few hours, the bot started placing orders that were way off the price I expected. My balance dwindled, and the logs showed weird rounding errors: a 0.001 BTC order turned into 0.0009999999999 BTC, and the exchange rejected it for being below the minimum size. I spent three hours staring at floating‑point numbers, wondering why my “simple” math was betraying me. It felt like dodging barrels in Donkey Kong—every jump seemed fine until I missed the timing and fell into a pit.
That frustration sparked a quest: I needed to understand the hidden traps that turn a promising trading system into a money‑leaking monster.
The biggest eye‑opener? Money is not a float.
When you represent currency with IEEE‑754 floating‑point numbers, you inherit binary rounding errors that are harmless for graphics or physics but deadly for accounting. A tiny epsilon can accumulate across thousands of trades, turning a profitable strategy into a loss‑maker, or worse, cause order rejections because the exchange enforces strict precision rules.
The second revelation was about state and concurrency. Trading systems are inherently event‑driven: market data streams, order updates, and exchange responses arrive asynchronously. If you store shared state in a plain dict or list and update it from multiple callbacks without synchronization, you get race conditions—orders duplicated, positions mis‑calculated, and your risk limits blown sky‑high.
Once I grasped these two ideas, the rest started to click like finding the right key in a dungeon.
Before (the struggle):
# NOTE: This is a *bad* example – do NOT copy!
def calculate_size(usdt_balance: float, price: float) -> float:
"""How much BTC can we buy with our USDT balance?"""
return usdt_balance / price # <-- floating point division
balance = 100.0 # USDT
price = 0.02537 # BTC/USDT
size = calculate_size(balance, price)
print(f"Buy {size:.8f} BTC") # 3941.??? actually 3941.???
Running this on a real exchange often gave me a size like 3941.9999999999995. The exchange rejected it because the minimum order size was 0.0001 BTC and my number, after rounding down, fell below the threshold.
After (the victory):
from decimal import Decimal, getcontext
# Set enough precision for crypto (8‑10 decimal places is safe)
getcontext().prec = 12
def calculate_size_decimal(usdt_balance: Decimal, price: Decimal) -> Decimal:
return usdt_balance / price
balance = Decimal('100.0')
price = Decimal('0.02537')
size = calculate_size_decimal(balance, price)
print(f"Buy {size.normalize():.8f} BTC") # 3941.??? exactly as expected
Using Decimal guarantees base‑10 arithmetic, so the result matches what a human (or an exchange’s pricing engine) expects. The trade now passes validation every time.
Before (the struggle):
import asyncio
# Simple in‑memory order book – a disaster waiting to happen
order_book = {"bids": [], "asks": []}
async def handle_trade_update(update):
# Called whenever the exchange pushes a new trade
if update["side"] == "buy":
order_book["bids"].append(update) # <-- no lock!
else:
order_book["asks"].append(update)
async def calculate_mid_price():
while True:
await asyncio.sleep(0.1)
if order_book["bids"] and order_book["asks"]:
best_bid = order_book["bids"][-1]["price"]
best_ask = order_book["asks"][-1]["price"]
mid = (best_bid + best_ask) / 2
print(f"Mid price: {mid}")
If two trade updates arrive back‑to‑back, one coroutine might append to bids while another is reading it for the mid‑price calculation. The list could be in an inconsistent state, leading to absurd price spikes or crashes.
After (the victory):
import asyncio
from collections import deque
# Thread‑safe (actually async‑safe) structures
bids = deque()
asks = deque()
book_lock = asyncio.Lock()
async def handle_trade_update(update):
async with book_lock:
if update["side"] == "buy":
bids.append(update)
else:
asks.append(update)
async def calculate_mid_price():
while True:
await asyncio.sleep(0.1)
async with book_lock:
if bids and asks:
best_bid = bids[-1]["price"]
best_ask = asks[-1]["price"]
mid = (best_bid + best_ask) / 2
print(f"Mid price: {mid}")
Now the book_lock ensures that only one coroutine mutates or reads the order book at a time. The mid‑price calculation is always based on a consistent snapshot, and the system stays sane even under high‑frequency updates.
With these two fixes in place, my bot stopped leaking money like a sieve. The Decimal class kept every cent (or satoshi) exactly where it belonged, and the lock‑guarded structures eliminated the phantom orders that used to haunt my logs.
What does this mean for you?
In short, treating money as a precise decimal and guarding shared state with proper synchronization transforms a fragile script into a robust trading engine. It’s like upgrading from a wooden sword to a forged blade: suddenly you can take on tougher bosses without breaking.
I dare you to take a small snippet of your own trading code (or a mock‑up if you’re just starting) and apply these two patterns:
float used for currency, fees, or sizes with Decimal (or your language’s equivalent fixed‑point type).
Run a quick test with a simulated feed—throw in a few rapid updates and watch the numbers stay clean.
Got questions? Hit me in the comments. I’d love to hear how your quest goes, what monsters you slayed, and any new tricks you discover along the way. Happy coding, and may your trades always be in the green!