One line of pandas quietly changed every backtest. It is why we built a language.

A single line of handwritten pandas in our scanner silently turned enter-once-on-the-signal into enter-every-bar, and it flattered the backtest before we caught it. That bug is why PRIOR exists: a small open-source language for trading strategies where a whole class of backtest lies cannot be written.

Here is the line that started it:

entries = combined & ~combined.shift(1).fillna(False)

combined is a boolean series, True on every bar where the strategy’s condition holds. The intent of that second line is a rising edge: fire only on the bar where the condition first becomes true, not on every bar it stays true. Buy the touch, not the whole time the price sits at the band. If you have ever written a mean-reversion strategy, you have written some version of this line.

It does not do that. Under modern pandas, shifting a boolean series promotes it to object dtype. The ~ operator on an object series of True/False values does not give you a clean logical NOT, it gives you truthy integers. The mask ends up all-true, and combined & (all true) is just combined. The rising edge silently collapsed back into “every bar the condition holds.” One position per fresh touch quietly became a new position on every single bar of the streak.

It shipped. The v0.4.0 scanner’s promote path generated strategy code with that line in it, which means strategies promoted before we caught it re-entered on every bar instead of on fresh touches, and the backtest numbers those strategies produced were computed against the wrong behavior. Nobody typo’d anything. The code read correctly. It passed review. It ran without an error. It was wrong in the one direction that matters most, the direction that makes the equity curve look busier and more active than the strategy you actually described.

We fixed it in one argument, shift(1, fill_value=False), added a regression test, and re-ran the affected numbers. But the fix is not the interesting part. The interesting part is what the bug told us about the whole approach.

It was never going to be the last one

The reflex after a bug like this is to feel bad at pandas and move on. That is the wrong lesson. We are not unusually bad at pandas. The problem is that expressing a trading strategy as general-purpose code gives you an enormous surface area for silent, backtest-flattering mistakes, and the worst ones share a signature: they do not crash, they do not look wrong on the page, and they bias the result in the optimistic direction.

Two of them are famous enough to have names, and both are one careless line away in any hand-written backtester.

The first is look-ahead: a strategy that references data it would not have had at decision time. The textbook version is comparing against a future bar, but the version that actually bites you is subtler. You decide today’s entry using today’s closing price, then fill at today’s open. You compute an indicator over the full series before slicing to your test window, so the “warmup” already saw the answers. Every one of these is a normal-looking line of code.

The second is repaint: a signal on a higher timeframe that changes after the fact. You resample daily bars to weekly to check a trend filter, but on Wednesday the current weekly bar has not closed, so the value you read on Wednesday is not the value that will exist on Friday. Your backtest reads the finished weekly bar and looks brilliant. Live, the signal flickers all week and fires on a bar that had not happened yet. PineScript traders fight this constantly with security().

Our rising-edge bug was a third member of the same family: code that reads as one behavior and executes as another, wrong in the flattering direction, invisible until you go looking. You can catch any individual instance with enough tests and enough scars. What you cannot do is catch the class. There will always be one more line.

So we stopped trying to catch them and asked a different question. What if the language could not express the bug in the first place?

PRIOR

PRIOR is a tiny open-source language for writing trading strategies. It is deliberately not a general-purpose programming language. There are no variables, no loops, no arithmetic, no way to index into a future bar because there is no way to index into bars at all. The whole vocabulary is a set of bracket tags, and each tag bundles what a competent quant means by a phrase.

A complete strategy looks like this:

when $NVDA at [lower_bollinger std=1]
  buy [5% portfolio]

sell when $NVDA at [middle_bollinger]
  or [stop 1.5%]
  or [after 5 bars]

[lower_bollinger std=1] is not a value you compute and then compare. It is the 20-period, one-standard-deviation band, with the warmup handled, evaluated with touch-once semantics: it fires on the bar the price reaches the band and does not fire again until the price has left and come back. That “fire once on a fresh touch” behavior, the exact thing our pandas line got wrong, is not something you write in PRIOR. It is baked into what the tag means. You cannot get the rising edge wrong because you never write the rising edge. It is the compiler’s job, written once, tested once, and shared by every strategy that uses the tag.

That is the whole idea. The fifteen lines of careful pandas that a tag stands for are the lines where the silent bugs live, so PRIOR writes them for you and hides them, the same way a database hides the fifteen lines of B-tree traversal behind SELECT.

Look-ahead gets the same treatment, except stronger. It is not that PRIOR checks for look-ahead and warns you. It is that the grammar has no way to name a future bar, so a look-ahead strategy is not a bug you might write and we might catch, it is a sentence the language cannot form. And higher-timeframe conditions, the ones that repaint, are computed on closed bars only. price above [sma 200 on 1d] evaluated on an intraday chart uses yesterday’s finished daily bar, never today’s forming one. The weekly gate cannot flip mid-week because the language will not let it read a bar that has not closed. Repaint stops being a thing you have to remember and becomes a thing that cannot happen.

None of this is magic, and we are not asking you to trust it. PRIOR compiles to an open JSON format and then to plain Python you can read. prior explain prints every layer, including an English readback of exactly what your strategy does. The generated code is the same code you would have written by hand, minus the one line you would have gotten wrong.

Where it lives

The language, the compiler, the formatter, and a local backtester are open source and MIT licensed. pip install prior-lang and you have all of it. There is free sample data for stocks, crypto, and forex, so prior sample stocks and a one-line strategy get you a real backtest about a minute after install, no API keys, no account.

The same strategy runs locally on your own bars (prior backtest strategy.prior --data bars.csv), and when a backtest earns your trust, prior deploy walks it into AutoQuant to run on paper or live through your own broker connection. Same file the whole way down, from the idea to the order. But the language stands on its own, and you never have to touch any of the paid pieces to use it.

What it deliberately cannot do

PRIOR is not going to express everything. It has no loops and no arithmetic on purpose, which means genuinely stateful, path-dependent logic is out of scope by design. It is not a research framework and it is not trying to be QuantConnect. It covers the large, unglamorous middle of what retail traders actually post, the mean reversions and momentum rotations and wheels and pairs, and it covers that space with a guarantee those tools cannot make: the most common ways a backtest lies to you are not available.

That guarantee only covers the lies that live in your code. The other half of the problem lives in your head, in the fact that you looked at the chart before you wrote the strategy, and no language can fix that one. We wrote about that half separately, in the backtest killer nobody talks about. PRIOR closes the door our own bug walked through. Closing the other one is still on you.

The repo is at github.com/prior-lang/prior. It exists because one honest line of pandas taught us that the strategies were never the hard part. Writing them down without lying was.