The whole report, start to finish, so you can see what you would be buying. The strategy, the client and every number in it are made up.
SAMPLE, NOT CLIENT WORK. A made-up strategy, audited against all eight checks: the format is real, the findings are not. For mistakes we really found, see our ten findings.
Reads a crypto exchange's order book and, every 15 minutes, calls 12 perpetual futures markets up, down or stay out.
The short version
The signal is real.
The number is not.
Signal v4 really does know something about where prices go next. On six months of data it had never been tuned on, it called direction right 53.1% of the time, against 33.3% for guessing. The idea is not worthless.
The +38% a year. Six problems, all in what the test assumed about costs, prices and timing, none in the idea. Fixed, the same signal makes +2.2% a year, too close to zero to call a profit.
Not bad luck: a pattern that has happened 14 times and will happen again. See FC-5.
Each red bar is one problem, and how much it took off the yearly return. Hover or tap a row for what it was.
Only six of the eight checks appear here. FC-1 passed, and FC-8 is about how the figure was presented, so neither changes the number. Several of these problems hit the same trades, so how much each one takes away depends on the order we apply them. The final figure doesn't. We applied them in the order shown. On its own, for example, the cherry-picking problem (FC-6) cuts the claim from +38.0% to +21.9%. Applied last, after the others, it takes off a further 1.9 points.
| Step | Correction | Change | Return per year |
|---|---|---|---|
| As claimed | +38.0% | ||
| FC-2 | Closed markets put back | −1.8 | +36.2% |
| FC-3 | Real trading fees | −6.6 | +29.6% |
| FC-4 | Marked against the right price | −3.5 | +26.1% |
| FC-7 | Prices that really existed | −17.9 | +8.2% |
| FC-5 | The market seen at real speed | −4.1 | +4.1% |
| FC-6 | Tested on untouched data | −1.9 | +2.2% |
| After corrections (likely range −5.4% to +9.9%) | +2.2% |
In order, FC-1 to FC-8. Open any check for what we asked, what we found, and the fix.
What we asked: if we run your code again, on a different computer, do we get the same number?
We ran it 20 times on two different machines, and again with a hidden setting changed that often reshuffles how programs order their work. The yearly return never moved by more than 0.31 points, and the hit rate was identical every time.
This is a real credential. Most strategies that reach us fail here; our own first finding is one that did. Because the number came back the same, the other seven checks could test the method instead of chasing a moving target. Passing doesn't mean the number is right, only that it's stable. The next six sections are all mistakes in a perfectly repeatable test.
Lock those settings in your automated tests, so the result stays repeatable as the code changes.
Effort: under an hour, onceWhat we asked: was the list of markets built as it looked at the time, or from the ones still around today?
The 12 markets are the 12 busiest as measured in early 2026, after the test period ended. Two markets that were in the top 12 during 2023 have since closed, and are missing from the test. A third only opened in mid-2024, yet the test traded it for nine months before it existed.
| Markets in the test | 12 |
| Markets missing because they later closed | 2 |
| Months traded in a market that didn't exist yet | 9 |
| Effect of rebuilding the list as it was on each date | −1.8 points a year |
Small, and we say so. This check often produces the biggest finding. It doesn't here, because these are 12 large, busy markets rather than hundreds of small ones. We report the size either way: “we checked, and it was minor” is a result too, and it shows the large findings elsewhere were measured rather than guessed. (We have made this mistake ourselves: finding 10 on our front page.)
Build the market list from the dates each market opened and closed, not from what is trading today. Then test the property itself: no market should appear before it opened or after it closed. Checking only that “the numbers changed” is how this mistake survives a fix. Reusable across every strategy you run.
Effort: about a dayWhat we asked: where did the trading-fee figure come from, and is it charged correctly?
Exchanges charge less, and sometimes even pay a small rebate (the maker rebate), when your order waits in the order book to be filled. They charge more when you trade immediately at the current price.
The test gave every single trade the maker rebate. The client's own exchange statements show only 61% of real trades got it. And the other 39% weren't spread evenly: they bunched up on the fast, chaotic moments, because that's when a waiting order doesn't get filled and the system has to trade immediately. The cost mistake is biggest exactly where most of the profit came from.
| Test assumed | Real trading | |
|---|---|---|
| Trades that got the cheaper rate | 100% | 61% |
| … on the fastest 1% of moments | 100% | 23% |
| Cost of a round trip, all trades | earned 1.0 bp | paid 4.4 bps |
| Cost of a round trip, fastest 1% of moments | earned 1.0 bp | paid 9.1 bps |
Use the real share of cheaper-rate trades from your exchange statements, moment by moment, instead of assuming all of them. The statements already exist, so this is a lookup, not new modelling. Re-check it after the FC-7 fix, because the two are linked.
Effort: half a dayThe finding that surprised the client most.
What we asked: was every piece of data checked, and could one source quietly spoil the result?
Nothing was wrong with either of the two data sources. The problem is that there were two, and they weren't the same market.
The signal read data from the exchange it actually trades on. But each call was marked right or wrong against a price chart from a charting website, which shows a blend of prices from many exchanges, not the one traded. Both are accurate. They are just different prices.
Worse: the chart stamps each 15-minute bar with its start time, and the test read it as the end time. So every call was marked against a price 15 minutes in the future.
| Gap between the chart's price and the traded price | |
|---|---|
| Typical | 0.8 bp |
| Worst 1% of the time | 11 bps |
| Worst 1% of the time, during the fastest moments | 41 bps |
Re-marking every call against the real traded price, at the right time: 7.1% of “right” calls became wrong and 1.4% of “wrong” calls became right. Net, the hit rate fell by 5.7 points.
On 11 March 2024 the exchange's data froze for 71 minutes. The signal read the frozen picture as a strong, steady signal and made 14 calls, 12 marked as wins. Removing that window costs only 0.4 points, but it is the same kind of mistake as finding 2 on our front page, and worth guarding against.
Mark calls against the traded exchange's own prices, and correct the start-versus-end time. Do this before any other fix: until it's done, every number you use to measure the other fixes is measured against the wrong market. Separately, make the data feed raise an alarm when the picture stops changing, instead of passing it on as a strong signal.
Effort: about an hourThe finding that explains the loss the client already knew about.
What we asked: how old is the market picture at the moment the signal makes its call?
The test assumed the signal sees the market instantly. Live, it can't. From the client's own system logs, we measured how old its picture of the market really is:
| Age of the L2 snapshot when the call is made | |
|---|---|
| Usually | 0.24 seconds |
| Worst 1% of the time | 1.9 seconds |
| Worst 1% of the time, during the fastest moments | 6.2 seconds |
| Longest seen | 11.4 seconds |
The delay is worst exactly when the market moves fastest. On the moments that carried a third of the profit, the live system was typically looking at a picture 4.4 seconds old. In a fast market, the buyers the signal “sees” have often already left. It reads a wall that is no longer there as strength.
This is what happened in the client's known loss. We didn't go looking for it; it came out of the measurements. A buy call into a move that had already happened, because the picture behind it was seconds old.
One event or a pattern? We found every moment in the three years that looked like it: a very fast move, just after one side of the order book thinned out sharply.
| Moments with the same shape, 2023–2025 | 14 |
| Calls that flip the wrong way at the real delay | 9 of 14 |
| … of which the test had marked as wins | 9 of 9 |
| Share of the claimed +38.0% coming from those 9 calls | 11.4 points |
A pattern, not an accident. The known loss is these 14 moments behaving normally, and it will happen again on the next one.
(a) A live safety switch: skip any call made on a market picture older than a set limit. This stops the next one of these reaching the market. On this data it would have skipped 9 of the 14, and none of them were calls you wanted. (b) Give the test the real delays, so research and live trading agree on what the signal can see. (c) Reduce the delay itself, the biggest job and the only one that wins those calls back.
Effort: (a) ~1 day · (b) ~2 days · (c) an infrastructure project−16.1 points on its own (see why the order matters), and the biggest effect on the hit rate.
What we asked: how many versions were tried, and was this one chosen before or after seeing how it did?
The code contains 41 different versions of the strategy's settings, made over fourteen months, and a script that tests all of them on the full three years. The version that was shipped is simply the one that scored best. No data was held back to check it on.
This is not an accusation. Nobody did anything dishonest. It's the most common thing we find, and we find it in our own work (finding 7 on our front page). “Try the variations, keep the one that works” is good instinct everywhere except here. The problem is arithmetic: with 41 tries on the same data, the best one is mostly the luckiest one, and luck doesn't repeat.
How we measured it. The client's own history shows no settings were changed after 14 June 2025. So the last six months of 2025 are data no version was ever chosen on. We tested the shipped version, unchanged, on those months:
| Data it was chosen on | Untouched data | |
|---|---|---|
| Calls made | 7,104 | 1,180 |
| How often it's right | 61.4% | 53.1% |
| Return per year (before the other fixes) | +38.0% | +21.9% |
| Rank among the 41 versions | 1st | 17th |
That last row is the finding. On data it wasn't chosen on, the shipped version is ordinary: 17th of 41, in the middle of its own family.
53.1% right against 33.3% for guessing, on 1,180 fresh calls, has less than a 1-in-1,000 chance of being luck. Cherry-picking inflated the number. It didn't invent it.
You can't un-see 41 results. From now on: (a) set aside a stretch of data before the next settings change, and don't look at it until the change is final. (b) For anything you show investors, write down the pass mark before the test period starts. And don't switch to a different version because of the 17th-place ranking: that ranking is one roll of the dice too.
Effort: a policy decision, plus a day of toolingThe biggest correction in this report.
What we asked: could every trade really have happened, at the price used, at that size?
First, where the profit came from. We sorted every 15-minute window by how wild the price moves were:
| Windows, by how fast the market moved | Share of time | Share of claimed profit |
|---|---|---|
| Fastest 1% | 0.8% | 34.1% |
| Fastest 5% | 4.9% | 61.3% |
| The other 95% | 95.1% | 38.7% |
A third of the profit came from less than 1% of the time. Whatever the test assumed about those moments is very nearly the whole result.
The test assumed every trade was filled, as a waiting order, at the mid-price at the moment of the call, with certainty. On a fast move that's backwards: the waiting orders that get filled are the ones the market is running away from. On the fastest 1% of moments we checked whether the assumed price was still there:
| Fast moments where the price moved past the order within 1 second | 71.4% |
| Typical distance the price had moved before the order could even sit in the order book | 19 bps |
| Trades filled at a price never seen again in that window | 2,847 of 7,104 |
We re-ran the test with one change: when the price had already moved away, the trade happens immediately at the price that actually existed. The client's own exchange data has both. Everything else was left alone. +26.1% became +8.2%.
Why this is a mistake, not a matter of opinion. Whether a waiting order gets filled depends on what happens after the call. The test answered that question with information it couldn't have had yet, and picked the favourable answer every time.
Assume every trade happens immediately at the real price, unless the exchange data shows a waiting order really would have been filled. That makes the test cautious instead of optimistic. To prove the change worked: with position limits set so high they never bite, old and new tests must agree exactly.
Effort: 2–3 days, including re-running the testsWhat we asked: is the headline one exact number, or a range? And is the range wide enough to include zero?
“+38.0% a year” was stated to one decimal place, with no range, no margin of error and no number of trades beside it. FC-6 already showed what it really was: the best of 41 tries.
So we asked what the corrected number can support. We re-sampled the corrected results 10,000 times, a week at a time, to see how far the figure could move:
| Best estimate | Likely range (95%) | |
|---|---|---|
| Return per year | +2.2% | −5.4% to +9.9% |
| Return for the risk taken (Sharpe ratio) | 0.14 | −0.35 to 0.63 |
| How often it's right (untouched data) | 53.1% | 50.2% to 56.0% |
These three rows don't say the same thing:
That gap is the real state of this strategy, and a common one: a signal that knows something, but whose edge is smaller than the cost of acting on it in real trading.
Beating guesswork isn't the real test. The simple alternative is: just holding the same 12 markets, equally weighted, over 2023–2025 made clearly more than +2.2% a year. It isn't exactly the same kind of trade, but none of the documents names any comparison at all, and a return with nothing to compare it to doesn't yet say anything about value. Choosing the benchmark is the client's call, and it should be written down in advance.
Quote a range, with the number of trades beside it, and name what you're comparing against. A number an investor can check is worth more than a bigger one they can't.
Effort: a decision, and an afternoon to redo the one-pagerAn audit that only lists damage isn't worth paying for. So, plainly:
Every fix above, ordered by how much it changes the claim for the work it takes.
Each fix is written precisely enough for your own team to do without us, on purpose. We don't carry out fixes on a strategy we have audited. An auditor who also sells the repair has a reason to make the problems look bigger, and this report is only worth anything because we don't. Nor do we tell you how to make the signal better. Making your measurement honest is our job; making the strategy better is your edge, and it should stay yours.
Every figure in this report comes out of a script delivered with it. Nothing is claimed that can't be shown.
| To reproduce | Script |
|---|---|
| FC-1 · the same answer on every run | fctools/fc1_reproduce.py |
| FC-2 · the market list as it really was | audit/fc2_asof_universe.py |
| FC-3 · real trading fees | audit/fc3_cost_reconcile.py |
| FC-4 · the right price, at the right time | audit/fc4_venue_reconcile.py |
| FC-5 · the delay, and the 14 moments | audit/fc5_snapshot_age.py |
| FC-6 · untouched data, and the rank among 41 | audit/fc6_holdout.py |
| FC-7 · real prices, trade by trade | audit/fc7_fill_realism.py |
| FC-8 · the likely ranges | fctools/fc8_bootstrap.py |
| The whole chart above, end to end | audit/run_all.py --report |
Full trade-by-trade output is delivered too, not just summaries: 7,104 rows for FC-7, 14 for FC-5, 41 for FC-6, 10,000 for FC-8. The two fctools scripts are free and public: download.
Start with a free 30-minute call. Tell us what the strategy is and what you have: code, data, and the result being claimed. If there isn't enough there to reproduce anything, we'll tell you on that call and turn the work down.
Backtest forensics · OsloPrices are on the front page; the eight checks in detail are on what we check.
Research due-diligence and code-audit services only. This is not investment advice, not a recommendation to buy or sell anything, and not an offer to manage money. This page is a made-up sample: it describes no client, no engagement and no real strategy.