Backtest Forensics/ Sample report

What a full audit looks like

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.

Full audit · $12,000 · report in 2–3 weeks · large order-book datasets like this one are quoted separately

dvt@arcobaleno.cloud → Want one of these on your strategy? Start with a free 30-minute call.
Full audit · samplePrepared for a made-up client
Signal v4

Reads a crypto exchange's order book and, every 15 minutes, calls 12 perpetual futures markets up, down or stay out.

The claim we tested · 2023–2025, no leverage
61.4%of calls right
+38.0%a year
−11.2%max drawdown
We looked atthe code, 3 years of L2 snapshots, the price data used to score each call, and the two documents making the claim
Scopeall eight checks, FC-1 to FC-8

The short version

The signal is real.
The number is not.

Return per year
+38.0% claimed
+2.2%
Likely range −5.4% to +9.9%. That range includes zero: it could be making nothing.
How often it's right
61.4% claimed
53.1%
Guessing would score 33.3%. This part is real, with less than a 1-in-1,000 chance of being luck.
Max drawdown
−11.2% claimed
−15.7%
The bad stretches were deeper than the claim showed.
What holds up

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.

What doesn't

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.

The loss the client already knew about

Not bad luck: a pattern that has happened 14 times and will happen again. See FC-5.

How +38% becomes +2.2%

Each red bar is one problem, and how much it took off the yearly return. Hover or tap a row for what it was.

Why the order matters

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.

Show as a table
StepCorrectionChangeReturn per year
As claimed+38.0%
FC-2Closed markets put back−1.8+36.2%
FC-3Real trading fees−6.6+29.6%
FC-4Marked against the right price−3.5+26.1%
FC-7Prices that really existed−17.9+8.2%
FC-5The market seen at real speed−4.1+4.1%
FC-6Tested on untouched data−1.9+2.2%
After corrections (likely range −5.4% to +9.9%)+2.2%

The eight checks

In order, FC-1 to FC-8. Open any check for what we asked, what we found, and the fix.

FC-1

Does it repeat?

Passed
Same answer on every run and every machine. Read the finding ›Close ×

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.

Why this matters

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.

The fix: nothing to fix, one thing to keep

Lock those settings in your automated tests, so the result stays repeatable as the code changes.

Effort: under an hour, once
FC-2

Are the dead ones missing?

Minor · −1.8 points
Two markets that later closed were left out. Read the finding ›Close ×

What we asked: was the list of markets built as it looked at the time, or from the ones still around today?

What was wrong

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 test12
Markets missing because they later closed2
Months traded in a market that didn't exist yet9
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.)

The fix

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 day
FC-3

Are the costs real?

−6.6 points
The test paid itself a fee discount on every trade. Real trading got it on 61%. Read the finding ›Close ×

What 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.

What was wrong

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 assumedReal trading
Trades that got the cheaper rate100%61%
… on the fastest 1% of moments100%23%
Cost of a round trip, all tradesearned 1.0 bppaid 4.4 bps
Cost of a round trip, fastest 1% of momentsearned 1.0 bppaid 9.1 bps
The fix

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 day
FC-4

Is the data clean?

−3.5 points
Calls were marked right or wrong against the wrong price, and 15 minutes early. Read the finding ›Close ×

The 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.

What was wrong

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
Typical0.8 bp
Worst 1% of the time11 bps
Worst 1% of the time, during the fastest moments41 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.

Also found: one bad day

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.

The fix: best value per hour in this report

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 hour
FC-5

Is the data current?

−4.1 points
The live system sees the market seconds late. This explains the known loss. Read the finding ›Close ×

The 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?

What was wrong

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
Usually0.24 seconds
Worst 1% of the time1.9 seconds
Worst 1% of the time, during the fastest moments6.2 seconds
Longest seen11.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–202514
Calls that flip the wrong way at the real delay9 of 14
… of which the test had marked as wins9 of 9
Share of the claimed +38.0% coming from those 9 calls11.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.

The fix: three steps, and the cheap one matters most

(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
FC-6

Was it cherry-picked?

−1.9 points
41 versions were tried and the luckiest one was kept. Read the finding ›Close ×

−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?

What was wrong

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 onUntouched data
Calls made7,1041,180
How often it's right61.4%53.1%
Return per year (before the other fixes)+38.0%+21.9%
Rank among the 41 versions1st17th

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.

What survives

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.

The fix: only forwards, never backwards

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 tooling
FC-7

Could you have traded it?

−17.9 points
Trades filled at prices that had already gone. The biggest problem. Read the finding ›Close ×

The 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 movedShare of timeShare 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.

What was wrong

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 second71.4%
Typical distance the price had moved before the order could even sit in the order book19 bps
Trades filled at a price never seen again in that window2,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.

The fix: flip the default

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 tests
FC-8

What is really being claimed?

Can't rule out zero
The corrected return's likely range includes zero. Read the finding ›Close ×

What we asked: is the headline one exact number, or a range? And is the range wide enough to include zero?

What was wrong

“+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 estimateLikely 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:

  • The return range includes zero. On this evidence, the strategy can't be told apart from one that makes nothing.
  • The risk-adjusted range includes zero too. Same message.
  • The hit rate range is nowhere near guessing (33.3%). The signal's knowledge is proven. Its profit is not.

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.

The comparison nobody made

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.

The fix: change what you publish, not what you calculate

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-pager

What this report does not say

An audit that only lists damage isn't worth paying for. So, plainly:

  • The idea The idea is not disproved. 53.1% right against 33.3% for guessing, on untouched data. There is real knowledge in this signal, and none of the six corrections touched the signal itself.
  • The code The signal's own code had no mistakes. Every finding is in the testing around it: fills, timing, costs, the prices used for marking, the market list, and how the version was chosen.
  • FC-1 FC-1 passed cleanly. That's a genuine credential; most don't.
  • Honesty No dishonesty is suggested anywhere. We have made every one of these mistakes ourselves.
  • Not a forecast This is not a forecast. It says what the material supplied can and can't support today. A fixed version is a different strategy and needs its own evidence.

Your plan, in order

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.

  1. Mark calls against the right price first (FC-4). About an hour of work, and until it's done every other number is measured against the wrong market.
  2. Assume trades happen at real prices by default (FC-7). This turns the biggest source of over-statement into a cautious assumption.
  3. Measure the live delay and feed it into the test (FC-5). This is the finding that predicts the next live loss, and the safety switch stops it.
  4. Write down the pass mark before the next settings change (FC-6). Cherry-picking can't be undone, only prevented.
  5. Redo the investor one-pager (FC-8). Whatever the corrected figure turns out to be, publish it as a range, with the number of trades and a benchmark beside it.
What we did not check
  • Anything after 31 December 2025. The material supplied ends there.
  • The live trading system's own code. We used its logs to measure delays (FC-5), but the parts that place orders, manage risk and reconcile accounts were outside this audit.
  • Counterparty, custody, margin and liquidation risks.
  • Whether the exchange data supplied is complete. We checked it against the client's own statements, which tests consistency, not independent truth.
  • Any market beyond the 12 supplied, or any call interval other than 15 minutes.
Check every number yourself: the scripts, for your engineers

Every figure in this report comes out of a script delivered with it. Nothing is claimed that can't be shown.

To reproduceScript
FC-1 · the same answer on every runfctools/fc1_reproduce.py
FC-2 · the market list as it really wasaudit/fc2_asof_universe.py
FC-3 · real trading feesaudit/fc3_cost_reconcile.py
FC-4 · the right price, at the right timeaudit/fc4_venue_reconcile.py
FC-5 · the delay, and the 14 momentsaudit/fc5_snapshot_age.py
FC-6 · untouched data, and the rank among 41audit/fc6_holdout.py
FC-7 · real prices, trade by tradeaudit/fc7_fill_realism.py
FC-8 · the likely rangesfctools/fc8_bootstrap.py
The whole chart above, end to endaudit/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.

Want one of these on your strategy?

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 · Oslo
dvt@arcobaleno.cloud →

Prices 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.