The Ups and the Downs
Months of building and running an automated trading scanner that
hasn't made me a dollar yet — and why I keep it running.
I've been running a 24/7 automated market scanner for months now. It
has survived earnings seasons, API outages, and its own bugs. It has
scored thousands of companies, sent me alerts at 7 PM on Thursday
nights, and tracked its own accuracy while I slept.
It has not made me money. Not yet.
This is a write-up about that — the system I built, the ways it
failed, what the failures taught me, and why "no success yet" turned
out to be the most educational outcome I could have gotten.
What I built
The system is a Python service that runs around the clock on a cloud
host, backed by PostgreSQL, with three scanners sharing one data
layer:
- A pre-market catalyst scanner — finds low-float stocks moving
before the open on a real catalyst (8-K filings, FDA news,
contracts) and scores them 0–100 for squeeze potential.
- An earnings-beat predictor — forecasts which companies will
beat estimates, times an options entry to the seasonal
implied-volatility low, and always exits before the report. The
edge isn't guessing the earnings result — it's riding the IV
expansion that happens before the announcement.
- A weekly universe scanner — sweeps 5,000+ tickers for weekly
EMA breakout setups.
Everything is delivered over Telegram as formatted alerts, and every
signal the system emits is written to a knowledge database with its
raw feature values and — once the outcome is known — a graded
verdict. That last part matters more than anything else in this
story.
The architecture constraints were the interesting part
The whole system runs on free and near-free data: Polygon.io (with a
hard 1,000-call/day budget I imposed), Finnhub, Alpha Vantage's
25-calls-a-day free tier, SEC EDGAR, and FINRA short-volume files.
Making that work forced most of the actual engineering:
- A three-pass scoring funnel. Pass 1 is free (symbol hygiene,
has-estimate checks, cheap gates). Pass 2 ranks survivors. Pass 3
spends real API calls, capped per scan. A naive implementation
would burn 8,000 calls per scan; mine uses under 120.
- A circuit breaker with entitlement awareness. A plan-gated 403
on an optional enrichment endpoint returns
None quietly; a real
outage trips the breaker. A missing entitlement can never take down
core scanning.
- A ticker-to-CUSIP-to-13F pipeline on SEC EDGAR — parsing
institutional holdings XML directly, because the paid endpoint for
this data ignored its own ticker filter.
- Resilience as a default posture. Every ticker loop is isolated
so one bad symbol can't kill a scan. Alerts are written to the
database before the Telegram send, so a network failure can't
lose a record. Jobs take heartbeat locks so a hung run can't
double-execute. The service has gone months without a crash.
I'm genuinely proud of this layer. It works. Which is exactly what
made the next part so instructive.
The downs
Here's the thing nobody tells you about building a trading system:
the infrastructure failing is the easy case. You notice a crash.
What almost killed this project — repeatedly — were the failures that
produced silence.
The scanner that couldn't find anything. For weeks the pre-market
scanner returned zero candidates. Market conditions? No — I was
requiring 3× average daily volume before 9:30 AM, comparing three
hours of pre-market trading against a full-day average. The bar was
mathematically almost unreachable. No error, no warning. Just quiet.
The predictions that were secretly wrong. A sign-handling bug in
the EPS comparison mislabeled roughly 15% of earnings outcomes —
companies that missed badly were recorded as beats when both numbers
were negative. My measured accuracy was inflated for weeks before a
database audit caught it. Correcting the labels dropped the
historical beat rate from 78% to 61%. Every conclusion I'd drawn on
top of that data had to be re-examined.
The gate that could never open. The earnings alert threshold was
set at a score of 70. Months went by with almost no alerts. When I
finally pulled the score distribution from production: 2,673
companies scored, average 33, maximum 76, exactly two had ever
crossed 70. Digging into the per-signal breakdown, about 40 points of
the 100-point scale came from signals that were dead in production —
one enrichment signal was frozen at its default for every single
company (its free-tier data source allows 25 calls a day against a
universe of thousands), the institutional-flow signal had data for
0.08% of rows, the insider-buying signal was net negative. The
realistic ceiling was about 60. I had built a bar and set it above
the building.
Measuring the wrong thing entirely. The strategy's profit comes
from pre-report IV expansion — you exit before the announcement.
But the system was grading itself on whether companies beat earnings.
The column that tracked the actual money metric — how far a play ran
before its report — existed in the schema and had zero rows.
Nothing had ever written to it. I was calibrating an archer by asking
about the weather.
The bugs that ate my first real data. When I finally lowered the
gate into a data-collection mode, the very first tracked plays
exposed two more silent failures within a week: a status-cleanup job
was marking plays "complete" days before they reported (so they
were never graded), and a partial API fetch caused a cleanup sweep to
mark 1,043 future calendar entries as stale — making the entire
upcoming earnings calendar invisible to the scanners. Both fixes took
a day. Finding them took instrumentation, forensic queries against
production, and the humility to assume the system was lying to me.
And one almost-comic dependency: when I fixed the calendar bug
and restored the full workload, API usage tripled — it turned out the
previous bug had been acting as accidental load-shedding, and the
restored grading traffic was quietly exhausting the daily API budget
before the evening scans ran, starving them. One bug had been hiding
the cost of the design behind it.
The ups
So why call any of this a success?
The measurement layer works, and it's telling the truth now. When
I bucketed 830 graded outcomes by predicted score, the beat rate
climbed monotonically — roughly 50% below a score of 40, 69% in the
40s, 86% in the low 50s, 97% in the 55–64 band, against a 55% base
rate. The model discriminates. It just discriminates in a score range
my alert gate was ignoring. That's not a dead system; that's a
miscalibrated one — and miscalibration is fixable with data.
The strategy's core premise validated itself on live data. One
tracked play this month beat earnings and the stock still dropped 20%
the next day. The pre-report window had offered a +5% move.
Exit-before-report — the rule the whole options strategy is built
around — was the difference between a win and a disaster, live, on a
real ticker.
The system now runs in calibration mode: a deliberately lowered
gate, every play stamped DO-NOT-TRADE, full tracking of entry IV and
pre-report expansion on every alert. I'm not trading it. I'm letting
it accumulate the dataset I should have demanded from the start, and
in a couple of months I'll re-derive the alert threshold from
measured outcomes instead of optimism.
What I actually learned
- Silent failure is the default failure mode. Every
consequential bug in this project produced an absence — no
candidates, no alerts, no grades — rather than an error. Alarms
for "nothing happened when something should have" are worth more
than exception handlers.
- An unreachable threshold is indistinguishable from an empty
market. For months I read "no alerts" as "no opportunities."
Distribution checks against production would have falsified that
in an afternoon.
- Grade the metric you get paid on. Beat/miss was convenient to
measure. It isn't what the strategy earns. The gap between a proxy
metric and the money metric is where systems quietly rot.
- Your accuracy numbers are hypotheses until audited. The
sign-flip bug means I now treat every aggregate the system reports
as suspect until I've traced a sample to raw data.
- Fixing one bug can unmask the cost of another design.
Load-shedding-by-accident is real. Budgets need to be enforced by
architecture, not by luck.
- Infrastructure competence and strategy validity are independent
axes. I built the reliable machine first and assumed the signal
would follow. The honest sequencing is the reverse: prove the
signal cheaply, then industrialize it.
Where it stands
The scanner is still running tonight. It will score this week's
reporters, track its plays, grade itself in the morning, and add a
few more rows to the dataset that will eventually tell me what the
alert gate should have been all along.
No profits yet. But I have a system that measures itself honestly, a
failure catalog I can defend line by line, and a calibration dataset
growing every week. In trading, most people lose money learning these
lessons. I got them for the price of API subscriptions.
I'll take that trade.
Stack: Python 3.12 · PostgreSQL/SQLAlchemy · APScheduler · Telegram
Bot API · Polygon.io · Finnhub · Alpha Vantage · SEC EDGAR · FINRA
RegSho.
← All posts