What it solves
Screening small-cap stocks by hand does not scale past a handful of tickers a day, and the interesting signal is in how a name performs after it is flagged, not just whether it clears a filter today. stock_sandbox scores a whole universe every night, keeps every score, and connects that scoring to a real Schwab account so a screen is not just a list — it is a position that can actually be taken.
How it is built
The service is a single Go binary that also serves its own React SPA, so there is one thing to deploy and one thing to run. Firestore holds the scored universe and the account state. Two Cloud Run Jobs run off the same container image as the service itself: universe-job scores the whole universe each night, and label-job matures forward-return labels once enough time has passed to know how a prior day's picks actually did. Deploys are keyless, authenticated from GitHub Actions via Workload Identity Federation, and the deploy step is pinned to the exact image digest the build just produced — not a tag that could move under it.
The hard decision
The one worth writing about is splitting label-job out of universe-job instead of running them together. Coupling them is the simpler design and it is wrong: a forward-return label is defined against a date that has already passed, so if the scoring run fails on a given night, a combined job loses that night's outcome data permanently — there is no re-running your way back to it later. Decoupling the two means a bad night of scoring costs you that night's scores, but the label job keeps maturing outcomes for everything scored before it, so the data you can never get back keeps accruing regardless of what breaks upstream.
What it taught me
Some data has a shelf life and some does not, and the two need different failure modes. A scoring run can be retried tomorrow with no loss; a label that depends on today's date cannot. Once I saw the label job as the thing that must never silently stop, decoupling it from the heavier, more failure-prone scoring job stopped being an optimization and became the obviously correct shape.