The first version of my price-alert system had a fatal feature. It only worked when I was looking at it.
The rules lived in the browser. The evaluation ran in the browser. The whole thing was a tab. Close the tab, sleep the laptop, and the alerts I'd carefully set up went dark. Which is exactly when the market does something worth alerting about: while you're asleep, or out, or anywhere but staring at the chart.
So I split it apart. The thing that defines alerts and the thing that runs alerts are now two different programs on two different machines, and that one decision fixed almost everything.
Control plane vs execution plane
Borrow the framing from networking. The control plane decides what should happen. The execution plane makes it happen. They have completely different requirements, and jamming them into one process is how you get a system that's both fiddly to use and unreliable to run.
My control plane is a desktop app. It's where I draw charts, pick a stock, and say "ping me when this crosses its 5-day average." It's allowed to be pretty. It's allowed to be closed. It's allowed to run on a laptop that sleeps. None of that should matter to whether an alert fires.
My execution plane is a headless daemon on a Linux box that never sleeps. No UI. It wakes once a minute, pulls prices, checks every rule, and sends a Telegram message when one trips. It runs as a system service, so if it crashes it comes right back. It doesn't care whether I'm awake or whether any app is open. That's the entire point.
Between them sits a small bridge: a local service that speaks HTTP and owns the durable copy of the rules. The app pushes rule changes to it. The daemon reads rules from it. Neither one talks to the other directly, and neither one is the source of truth on its own.
The durability trick
Here's the bug that taught me the most.
Browser storage is a liar about permanence. localStorage feels durable until you clear a profile, switch browsers, or open the thing on your phone. Then it's empty, and an empty rule set is indistinguishable from "the user deleted everything." My first instinct, syncing the browser's rules to the daemon, made it worse: open the app on a fresh device, it has no rules, it dutifully pushes that emptiness to the daemon, and now the daemon has no rules either. One clean browser wipes out the whole system.
The fix flips the direction of trust. On startup, the app doesn't push. It pulls. It asks the daemon "what rules do you have?" and merges anything it's missing back into its own state. The daemon is the backstop. A cleared browser heals from the daemon instead of clobbering it.
There's a guard on the push side too: never send an empty rule set before that first pull has had its chance. So a freshly opened app can't nuke the daemon in the second and a half before it hydrates. Small rule, saved me from my own architecture.
Session awareness, because the market isn't one thing
A price isn't a single number you fetch from a single place. It depends on the hour.
During regular hours and after-hours, the daemon asks my broker. Overnight, it tries a different feed entirely, because the broker goes quiet and the overnight session trades somewhere else. When the market's fully closed, it doesn't fetch at all. It just logs "closed" and goes back to sleep.
Each window gets the right source, and the alert message says which one it used. (Overnight has its own war story about a frozen quote that I wrote up separately. Short version: check your timestamps.) The daemon knowing what time it is, in market terms, is what keeps it from comparing a number from one session against a number that only makes sense in another.
What this bought me
Alerts fire while my laptop's shut. I define them in a comfortable UI and run them in a boring, reliable service, and those two facts no longer fight each other. I can open the app on my phone, see the same rules, and trust that closing it changes nothing about whether they run.
The lesson generalizes past trading. Any time you've got a nice interface for setting something up and a need for that something to keep happening when the interface is gone, resist the urge to make them the same program. The UI wants to be rich and is allowed to be flaky. The engine wants to be dumb and has to be relentless. Let them be different things.
Put the part that has to keep running somewhere it can't be closed. Then make the pretty part pull from it, not the other way around.