After the first version of my trading platform, I rebuilt it around a hard rule: the tool is not allowed to flatter me. Every screener and backtester I’ve seen, paid or free, makes it easy to fool yourself. This build is organized around removing those opportunities one at a time. The strategy logic stays private, but the architecture doesn’t need to, and the architecture is where the honesty lives anyway.
Provider abstraction, because data sources die
I’ve already had one data library go unmaintained under me, so this version talks to data through one interface, and swapping providers is one module instead of a rewrite:
class DataProvider(ABC):
@abstractmethod
def daily_bars(self, ticker: str, start: date, end: date) -> pd.DataFrame: ...
@abstractmethod
def intraday_bars(self, ticker: str, day: date, interval: str) -> pd.DataFrame: ...
class YFinanceProvider(DataProvider): ...
# next provider implements the same two methods, nothing upstream changes
One config, no subtly different copies
The screener scans the S&P 500 and hands candidates to the backtester through a shared handoff: the same config object drives both. A setup the screener flags gets tested under exactly the rules it was found with, because there is only one place those rules exist:
run:
date_range: 2023-01-01 .. 2025-06-30
preset: bull_market # presets bundle filters for market conditions
handoff:
finder_results: results/finder_2025-06-30.json # backtester consumes this
The failure this prevents is boring and universal: the scan and the test drift apart by one default parameter, and every backtest silently answers a different question than the one the screener asked.
Paranoia about fills
A backtest that assumes you got the daily close is lying to you about any fast-moving entry. So fills get verified against finer-resolution data: for each simulated trade, pull intraday bars for the entry day and confirm the fill price was actually available:
def verify_fill(trade, provider):
bars = provider.intraday_bars(trade.ticker, trade.date, "5m")
if not (bars.low.min() <= trade.fill_price <= bars.high.max()):
trade.flag("fill price never traded that day")
# tighter check: price must be reachable AFTER the signal bar
Every strategy result is also benchmarked against plain buy-and-hold on the same ticker over the same window. A strategy that underperforms doing nothing is not a strategy, it’s a hobby with extra steps.
The loss autopsy
The feature I use most: every losing trade gets pulled apart to show what the setup looked like at entry and what actually happened after. Each loss gets stored with its context so patterns across losses become visible:
loss record:
ticker, entry date, setup snapshot (indicator states at entry)
exit reason: stop / time / signal
what happened next: did it recover after the stop? by how much?
That last field is the humbling one. If most of your stopped-out trades recover immediately, your stop is the problem, not the entries. Winners teach you almost nothing. The losers are the curriculum.
Charts mark every trade on the price history so I can click a result and see it in context instead of trusting a summary statistic. Same rule as the garage: the fancy tool isn’t the one that tells you what you want to hear, it’s the one that shows you what’s actually there. A dyno that adds 50 horsepower to every pull would be popular, and useless.
