Diagnostic Report — MA Crossover Bot
Symptoms reported
- Bot stops trading overnight with nothing in the console, no crash message.
- After a restart, the bot sometimes places two orders for what should have been one signal.
- Orders are occasionally rejected by the exchange for "invalid precision."
All three are explained below — each maps to a specific, fixable bug, not a vague "the bot is flaky" problem.
Bugs found
| # | Severity | Bug | Symptom it causes |
|---|---|---|---|
| 1 | High | API credentials hardcoded in source | Secret leaks the moment the file is shared, committed, or pasted anywhere |
| 2 | Critical | Position state kept only in memory | Restart forgets any open position → re-buys into it → symptom 2 |
| 3 | Medium | Moving average divides by the fixed window, not the samples available | Fires real signals off partial, meaningless data right after startup, no error |
| 4 | High | Order quantity computed with plain float division | Quantity drifts past exchange precision → order rejected → symptom 3 |
| 5 | Medium | print() used instead of real logging | No timestamps, nothing persisted — the trail is gone once the terminal closes |
| 6 | Critical | Bare except: pass around every order attempt | Rate limits, insufficient funds, invalid orders, and real bugs all vanish silently |
| 7 | Critical | No error handling around the main polling loop | A single dropped connection kills the entire process → symptom 1 |
| 8 | Medium | Fixed 1-second poll, no rate-limit awareness | Guarantees throttling under real volume, which then hits bug 6 and disappears |
| 9 | Medium | No signal handling for Ctrl+C / process stop | In-memory state (see bug 2) is lost on any shutdown, planned or not |
| 10 | High | No idempotency key on order submission | A retried request after a timeout can't tell it already succeeded — can double-submit |
10 bugs found. 3 Critical, 4 High, 3 Medium. Bugs 2, 6, and 7 are the critical ones — between them they explain all three reported symptoms.
The fix that shows up most often
Bug 6 — a bare except: pass — is the single most common failure pattern across the bots we've
reviewed. It looks harmless. It is not: it means every real error the exchange can throw (rate limits,
insufficient funds, invalid orders) is treated identically to "nothing happened," with no log line, no
retry, and no way for the trader to ever find out.
# before/bot.py
def place_order(side, symbol, amount):
global position_open
try:
price = client.get_ticker(symbol)["last"]
qty = amount / price
order = client.create_order(symbol, side, qty)
print(f"Order placed: {order}")
position_open = (side == "buy")
except:
pass # <-- every failure, real or trivial, disappears here
# after/bot.py
def place_order(client, state, side, symbol, amount_usd, signal_id):
client_order_id = f"{signal_id}-{side}"
if state.get("last_client_order_id") == client_order_id:
logger.info("Order for signal %s already submitted; skipping duplicate.", signal_id)
return
try:
ticker = call_with_retry(client.get_ticker, symbol)
precision = client.get_precision(symbol)
qty = quantity_for(amount_usd, ticker["last"], precision)
order = call_with_retry(client.create_order, symbol, side, qty, client_order_id=client_order_id)
logger.info("Order placed: %s", order)
state["position_open"] = side == "buy"
state["last_client_order_id"] = client_order_id
save_state(state)
except InsufficientFunds as exc:
logger.error("Insufficient funds for %s %s: %s -- skipping this signal.", side, symbol, exc)
except InvalidOrder as exc:
logger.error("Order rejected as invalid for %s %s: %s -- skipping this signal.", side, symbol, exc)
except ExchangeError:
logger.exception("Unrecoverable exchange error placing %s order for %s", side, symbol)
Fixes applied
- Credentials load from environment variables; the bot refuses to start without them instead of falling back to a hardcoded value. (fixes #1)
- Position state persists to disk after every change via an atomic write, and reloads on startup. (fixes #2, #9)
moving_average()returnsNoneuntil enough real samples exist, instead of diluting the average with missing data. (fixes #3)- Order quantity uses
Decimal, rounded down to the exchange's published precision, and raises a clear error below the minimum instead of drifting silently. (fixes #4) - Structured logging (timestamped, leveled, rotating file) replaces every
print(). (fixes #5) - Bare
except: passremoved entirely. Network/rate-limit errors retry with backoff; wrong-request errors are logged and skipped, never blindly retried; anything unexpected logs a full traceback instead of vanishing. (fixes #6, #7, #8) - Every order carries a
client_order_idderived from the signal, so a retried submission returns the original order instead of opening a duplicate position. (fixes #10) - SIGINT/SIGTERM handled: state flushes to disk before the process exits. (fixes #9)
How this was verified
Not just "it compiles." A test suite reproduces each symptom against the original code first — proving the bugs are real — then proves the same scenario now behaves correctly against the fixed code, including a direct regression test that throws three consecutive dropped connections at the stabilized bot's main loop and confirms it keeps running instead of dying.
15 passed in 0.22s
What a real engagement looks like
For your bot, this same process applies: full read-through, a report like this one naming every bug with
severity and root cause, then — at the Debug & Stabilize tier and above — the fixes applied in place
against your actual code and exchange integration, with tests covering the failure paths that
used to crash silently. If your bot uses ccxt, python-binance, or a native
exchange SDK, the same error-handling and retry patterns shown here apply directly.
Send your bot's code and get a report like this one
Diagnostic Sprint from $149. Debug & Stabilize (fixes included) is $409.
Get a diagnostic →