Hey everyone, it's your friendly neighborhood dev-dad here. Mid-thirties, full-time engineer by day, battling AI trading bots by night (weekends, really).
Today, I want to share a subtle but potentially catastrophic bug I found in my bot. Seriously glad I caught this before deploying with real money. The symptom: Discord notifications for order fills just weren't arriving. The culprit: I forgot to load my .env variables consistently across multiple Python scripts.
This is a super common pitfall when you're linking several Python scripts in a personal project, and it can be a real headache.
What Happened: A "Silent Failure" Uncovered by a DRY_RUN
Last weekend, I was running my usual DRY_RUN tests for my FX bot. My bot's logic is split into two main parts: planner.py, which strategizes trades, and executor.py, which actually sends orders to the exchange.
The console logs looked perfectly normal. executor.py seemed to be doing its job: I saw messages like "[DRY_RUN] Order placed: ...". But the Discord notifications, which should have been firing, never appeared.
At first, I thought it was a Discord outage or just a delay. But after 30 minutes, nothing. Something was definitely wrong.
Thinking about what would have happened if this were real money sent shivers down my spine.
- "I thought I placed an order, but it never went through."
- "I thought I closed a position, but I was still holding it."
Bugs in notification systems are terrifying because they create these silent failures. You think everything is okay, but it's not. This is precisely how real money gets lost.
The Investigation: Unmasking the Culprit
To narrow things down, I first tried calling notify.py (which handles all notifications) directly. It worked flawlessly; the Discord notification came through. This pointed to an issue within executor.py, which calls notify.py.
I re-examined executor.py's logs more carefully and immediately saw it: the webhook URL being passed to the notification function was None. Ah, there it is.
But why None? I have the webhook URL stored in my .env file, and it's loaded correctly when planner.py executes.
Comparing planner.py and executor.py's code, the reason became painfully obvious.
planner.py correctly includes load_dotenv() at the top:
# ...
from dotenv import load_dotenv
load_dotenv() # Load environment variables
# ... planner's logic ...
However, executor.py, the problem child, was missing load_dotenv().
This meant that when I ran the full flow (where planner.py calls executor.py), planner.py would load the .env variables first, so executor.py appeared to work. But if I ran executor.py directly for testing, or if it was called from a different entry point, no one was loading .env. Consequently, the webhook URL was never set, and notifications silently failed.
My code was relying on an "implicit assumption" about its execution context. This is the kind of thing that would get flagged in a code review with a team, but in solo dev, it's easy to miss.
The Fix: Resolve Dependencies Where They're Used
The fix was straightforward once the cause was clear. I added load_dotenv() to executor.py before any notification calls.
Before:
# Before: Calling notification function without loading .env
from .notify import hub
hub.notify_investment('Execution result...') # Fails because webhook is not set
In this scenario, hub would initialize, and os.environ.get('DISCORD_WEBHOOK_INVESTMENT') would return None, leading to a silent failure.
After:
# After: Loading .env before notification
import os
from dotenv import load_dotenv, find_dotenv
from .notify import hub
# Find the .env file and load environment variables
# This ensures the webhook URL is set in os.environ
if 'DISCORD_WEBHOOK_INVESTMENT' not in os.environ: # Only load if not already set
load_dotenv(find_dotenv())
hub.notify_investment('Execution result...') # Notification sent successfully
I used find_dotenv() to ensure that no matter the current working directory, it will always locate the .env file in the project root. Now, executor.py will always correctly load the webhook URL, regardless of how it's executed.
It's a fundamental principle, really: if your code depends on a certain feature (like environment variables), the code using that feature is responsible for resolving that dependency. A simple truth, but easily overlooked.
Lessons Learned and Takeaways
I took away three key lessons from this experience:
DRY_RUN Tests are Gold
The value of catching "normal-looking anomalies" like this, without any financial impact, is immense. "Seems to be working" is the most dangerous state. Never skip DRY_RUNs before live deployment.Eliminate "Implicit Assumptions" Between Scripts
When splitting logic across files, it's easy to create implicit assumptions like "that other file must have initialized this." For project-wide settings like.env, explicitly loading them at each entry point, or at the top of any module that uses them, is a much more robust design.Consider Assertions for Critical Operations
While logs helped me catch this, for even greater robustness, adding an assertion right before critical operations like sending an order or a notification could be beneficial. Something likeassert os.environ.get('DISCORD_WEBHOOK_INVESTMENT') is not Nonewould immediately flag misconfigurations.
Operating a personal bot means constantly battling these subtle bugs. But each one you squash makes the system stronger. Another weekend, another step towards a smarter bot.
Top comments (0)