Zach Christensen
projects
July 2026personal

Brad

A quantitative research platform that scores five asset universes on mechanically-weighted factor models, publishes every pick in the open, and grades its own output honestly enough to say when it has no statistical confidence at all.

The Brad home page: five engine cards, each with its own track record, signal quality and latest sweep.

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.

1 / 7

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.

What I took from it