
The front page. Five engines, each carrying its own track record, and the headline figure is how far behind the S&P the stock engine is: minus 1.2 percent, in red, above the fold.
Most stock-scoring tools show you a number without showing you whether that number has ever predicted anything. Brad is an attempt to do the same job honestly.
It scores five separate asset universes: US large and mid-cap equities, small caps, crypto, a long/short book, and a two-region macro dashboard. Each one runs on mechanically-weighted factor models drawn from published finance literature rather than from intuition. Every pick is then tracked forward against the relevant benchmark and published free at getbradscore.com, alongside a statement of how much statistical confidence the output does and does not have.
That last part is the whole point. Publishing a score is easy. Publishing a score next to the evidence that it has not yet earned your trust is the thing almost nobody does.
The part I would show first
There are five scoring engines over a shared core. There is also a sixth package that scores nothing.
It exists only to audit the other five: deflated Sharpe ratios, purged combinatorial cross-validation, probability of backtest overfitting, cost modelling and null canaries. It imports the standard library and nothing else, and that restriction is enforced by walking its own syntax tree in CI. It structurally cannot reach a scoring parameter, which means it cannot be quietly tuned to flatter the thing it is measuring.
That is the design decision I am most pleased with, because it converts a promise into a property. "I would not fudge the validation" is a claim about my character. "The validator cannot see the parameters" is a claim about the code, and only one of those survives me being tired at midnight.
How it is built
About 74,000 lines of Python 3.12, with SQLite as a write-once store. The runtime dependency list is four packages, and there is no web framework, no ORM and no bundler anywhere in it. The absence is deliberate: the surfaces are a set of command-line entry points, a Rich and Textual terminal interface for driving sweeps, a static site of roughly 145 vanilla ES modules reading published JSON snapshots, and three zero-dependency serverless functions for email and contact that use native fetch specifically so no API key can reach the browser.
Underneath it sits about two dozen third-party data sources: market data and filings, exchanges and on-chain aggregators, US and Canadian government statistical APIs, each behind its own client with its own rate limiting, caching and staleness rules.
I built all of it and I maintain all of it. Before writing production code I spent months on the architecture up front. When the startup I work at brought on a CTO, I went through that planning with him and used the review to reshape the project into something properly structured rather than merely working. It was the first time I had designed a system at that scale, and the build itself is my own.
What makes it hard
The evidence clock is coupled to the model, and the coupling is invisible. About 80 scoring parameters are formally declared across the five engines. Move one and every prior measurement was taken on a different model, so the count of comparable periods resets to zero. Forget to declare a parameter and it changes scores silently, with neither the parameter diff nor the comparability check saying a word. One real instance went unnoticed for four months, because rank correlation held steady across the change. A monotone rescale is mathematically invisible to a rank check, so the guard was working exactly as designed and still saw nothing. That is the kind of bug that teaches you what a guard is actually promising.
A partly-threaded test seam is worse than no seam at all. The report builder takes three: where output lands, what it is computed from, and where series are read. Thread two of the three and the test still passes, because the file appears exactly where the test looks for it while its contents come from live production state. That recurred four times before I stopped counting seams from memory and made the code count them for me.
Every factor is measured as of the last closed bar, and any two numbers combined into a ratio have to share both an as-of date and a window convention. I have broken that rule three times without noticing. In the worst case a single day's volume was compared against a threshold calibrated on a thirty-day mean, so the same asset took a penalty on a quiet Sunday and none at all after a volume spike. A stale number and a fresh number look identical in a CSV.
One operational trap worth naming: never run a data backfill on a scoring day. There is a single shared rate limiter, and a rate-limit rejection on a live engine reads downstream as "no position in this window", which the return maths then books as a synthetic loss of one hundred percent.
How the project is governed
The practices are the part I would defend hardest, because on a solo project they are the only thing standing in for a second pair of eyes.
- Each engine has a canonical scoring spec that is the source of truth. A scoring change updates the spec, then the tests, then the code, in that order
- Tests never call a real API and never touch the real history database. That isolation exists because a suite once appended real rows to the committed evidence store, which is the sort of thing you only allow to happen once
- A new guard has to be seen to fail before it is trusted, because a stage that could not go red once shipped and passed silently for weeks
- Publishing is always a separate explicit step, emailing is separate from publishing, and neither is ever automatic
- Every judgement call gets a decision record with its reasoning, referenced by id in the commit. Entries are superseded, never edited
- Falsifiers are registered before the data exists, and an append-only trial ledger records every experiment so the search is properly deflated rather than quietly cherry-picked
- A change is not finished when the code works. It is finished when every document describing it is correct, and CI fails on a broken documentation link
What I took from it
- Turn promises into properties. The validator cannot reach the scoring parameters, so its independence is not something I have to keep choosing
- A guard that is working correctly can still see nothing. Knowing what a check is blind to matters as much as knowing what it catches
- Pre-register what would prove you wrong. It is the only reliable defence against finding what you were hoping for
- On a solo project, process is the substitute for review. The spec-first order, the decision records and the append-only ledger are all there because there is nobody else to catch me
- Publishing your own uncertainty is uncomfortable and it is the entire value of the thing. A score with no track record is a guess wearing a number