The reference. Every option, every finding explained, and the things you only need once you are past the first run. Use the contents table to jump to the section you need.
Contents
Straud works with results from any tool — vectorbt, backtrader, Lean, TradingView, MetaTrader, or a script you wrote yourself. If you can export a list of trades, you can use it.
One row per trade. One column of returns. Everything else optional.
Here is the example file:
| date | symbol | gross_return | net_return | signal_strength | entry_price |
|---|---|---|---|---|---|
| 2024-01-02 | SYM25 | 0.015220 | 0.012720 | −1.4768 | 53.47 |
| 2024-01-02 | SYM18 | 0.000332 | −0.002168 | −0.3527 | 45.27 |
What each column unlocks:
| Column | Flag | Why it helps |
|---|---|---|
| net return | --column |
Required. Return after costs. |
| date/time | --timestamps |
Era consistency; how much history you gave. |
| date/group | --cluster |
The most valuable optional flag. See below. |
| gross return | --gross |
Cost sweep: how much cost the edge can survive. |
| signal | --signal |
Look-ahead check. |
| price | --price |
Sanity checks for stock strategies. |
| daily volume | --adv-column |
Capacity: how big you can trade. |
If several of your trades happen at the same time or on the same day, they are not independent. A market-wide move hits all of them together, so twenty trades on one day carry closer to one day's worth of information, not twenty.
If you ignore this, your results look far more certain than they are.
Use whatever column identifies the shared moment — usually the date:
straud audit examples/example_trades.csv --column net_return --cluster date
Rule of thumb: if two rows could be affected by the same piece of news, they belong in the same cluster.
The cluster column answers when. It says nothing about who.
Those are different problems, and fixing one does not fix the other. A study can be perfectly clustered by date and still have one symbol firing fifty times and carrying the whole result.
This is not hypothetical. A real campaign produced this:
| grouped by | units | mean | median | t |
|---|---|---|---|---|
| date | 49 | +192bp | +60bp | 2.62 |
| symbol | 30 | +48bp | −18bp | 1.33 |
Same 243 events. Clustered by date it clears the usual bar and gets promoted. Clustered by symbol there is no edge at all — two symbols carried 96% of the profit. Only the date grouping had been checked.
So pass both:
straud audit trades.csv --column net_return --cluster date --entity symbol
Straud then reports n events, n dates and n entities, each entity's share of the profit, and whether the two groupings disagree about whether you have anything. If the top one or two entities carry more than half the winnings, your event count is fiction.
If your universe is narrow, expect Straud to be quieter here. A t-statistic grows with the number of groups, so an entity axis with 60 symbols loses significance against a date axis with 500 dates purely because it has fewer groups — not because anything is wrong. Crypto perps (~120 tradeable names) and FX (~30 currencies) sit in exactly that position. So Straud does not report a disagreement on significance alone: it requires the entity-level effect to actually be smaller, and to be smaller in the shape concentration produces — a few names pulling the average up above the median. An average dragged below its median is a few bad names in a small sample, which is noise, and it is left alone.
It also separates two things that look identical in the numbers:
Add --timestamps and --entity-window-minutes to also catch the same
name re-entered before the previous position closed, which inflates your
event count directly.
--trials is how many strategy variants you tested against this data
before choosing this one. Every variant. Including those discarded.
This matters more than anything else you tell Straud.
Try 400 combinations of settings on pure random noise, keep the best one, and it will look excellent. That is not a flaw in your idea — it is arithmetic. The best of 400 coin-flip sequences always looks impressive.
Count everything:
If you are unsure, guess high. Guessing low is the mistake that costs money.
Straud cannot verify this number — it is the one input it must take on trust. So instead it tells you what your number is worth:
This result survives up to ~94 tested variants.
If the true count is higher than that, the result is not supported. You do not need to be honest with Straud; you need to be honest with yourself, and that sentence gives you something concrete to be honest about.
If you do not know at all, use a ledger — it counts for you.
| Verdict | Meaning |
|---|---|
| Fatal | Something is definitely wrong. Do not trade this. |
| Unproven | No critical finding, but the evidence does not support the claim. This is the normal result for most strategies. |
| Conditional | Real signal, but it depends on assumptions that probably will not hold. |
| Survives | Passed everything Straud could test. |
The strongest claim available is "survives the tests we ran" — which is why the "what we did NOT test" section is part of every report.
Getting Unproven is not failure. Most strategies are unproven most of the time. The tool is doing its job.
Every finding has two separate labels:
Severity — how bad if true
| 🔴 Critical | Stop. |
| ⚠ Serious | Fix before risking money. |
| ▲ Caution | Know about it. |
| · Note | For the record. |
Confidence — how sure Straud is
| Confirmed | It ran the test; the test failed. |
| Likely | Strong indication, but an innocent explanation exists. |
| Possible | A flag that needs your knowledge to resolve. |
Two labels, not one, so Straud can raise a worry without overclaiming.
Straud's checks make two different kinds of claim, and they do not carry the same weight.
Some checks measure the thing you are claiming, or establish a fact. A placebo cutoff doing as well as your real one. An assumed cost below the venue's published fee. A trial-adjusted Sharpe of 0.001. There is no gap between what these measure and what they mean, so one of them alone is enough to condemn a result.
Other checks spot a pattern and infer a cause. Those can be right about the measurement and wrong about the reason. A real example: Straud once saw that grouping by date and grouping by symbol disagreed, and concluded the result was fake. The measurement was correct — but the true cause was that the edge lived in symbols that recur, which is a useful finding, not a critical one.
So a pattern-based check is not allowed to condemn a result on its own. If nothing independent agrees with it, it is reported at Serious instead of Critical, and it says so in the evidence. If something independent does agree — another confirmed finding, from a different check — the inference stands and Critical is kept.
This costs nothing in detection. On Straud's own corpus of planted defects, every single one that reaches Critical does so through a fact-based check, so none of them are affected.
Some findings ask you something:
Were these shortable at signal time? If not, this becomes Critical.
Straud does not know your market. When it cannot check something itself, it asks rather than guessing — and tells you what the answer would change.
straud --version
Worth knowing, because Straud's checks and thresholds change between versions: the same data can produce a different verdict under a different build. Every report records the version that produced it, so an old result can always be traced back to the code behind it. CHANGELOG lists what changed.
| Code | Meaning |
|---|---|
0 |
Nothing at or above your failing level |
1 |
Something at your failing level |
2 |
At least one Critical finding |
straud audit results.csv --column net --trials 50 || echo "needs attention"
How much severity should stop your pipeline is a risk decision, and it depends on what you are doing. Exploring a new idea is not the same as signing off a strategy that will trade real money. So it is yours to set:
--policy |
stops on | use when |
|---|---|---|
strict |
Caution and above | live capital, or a result someone else relies on |
standard (default) |
Serious and above | normal work |
lenient |
confirmed Critical only | early exploration, where you want the findings but not the gate |
straud audit results.csv --column net --trials 50 --policy strict
The report does not change. The same findings, the same evidence, the same remedies are printed under all three — only the exit code moves. The policy decides what stops a pipeline, not what you are told.
Two things it deliberately cannot do:
lenient is for ignoring
Serious findings, not for waving through critical ones.Pick one. They do the same thing.
| Best for | Needs Python? | |
|---|---|---|
| A. Standalone download | Trying it, or never touching Python | No |
| B. Command line | Regular use, Parquet files | Yes |
| C. Python library | Your backtest already runs in Python | Yes |
| D. AI assistant | Researching with Claude or similar | Yes |
The simplest way to run Straud. Everything is inside one download — no
Python, no pip, no setup.
straud-0.1.0-darwin-arm64.zip)../straud audit examples/example_trades.csv --column net_return
On Windows: straud.exe audit examples\example_trades.csv --column net_return
Then point it at your own file:
./straud audit /path/to/your_results.csv --column net_return --trials 40
Every flag in this manual works exactly the same way — just write ./straud
instead of straud.
The first run takes 15 seconds to about a minute. Every run after that is under a second. The folder holds ~140 code libraries and your operating system verifies each one the first time an unrecognised program runs. The result is then cached. Measured on an M-series Mac: 13–45s first, 0.37s after.
Get it over with before you need it by running the example once:
./straud audit examples/example_trades.csv --column net_return
./straud --help is not enough — it loads only a small part of the program,
so your first real command would still be slow. Measured: --help first at
13.2s, then a real audit still cost 6.2s. Running the example audit first
warms everything, and subsequent commands take ~0.5s.
Your system may warn that the developer cannot be verified. This build is not signed with a paid developer certificate. To allow it:
straud → Open → Open. Or in a terminal:
xattr -d com.apple.quarantine straudThis build reads CSV files only. Parquet support needs the Python package
(pip install 'straud[parquet]'). Straud will tell you this plainly if you
point it at a .parquet file.
A true single file has to unpack itself on every run, and because the build is unsigned your operating system re-verifies it each time — measured at 12 to 42 seconds per command. The folder version pays that cost once and then starts in about half a second. You still download one zip; only one of the two is pleasant to use afterwards.
Best if your results are in a CSV or Parquet file and you have Python.
Minimum:
straud audit results.csv --column net_return
Realistic:
straud audit results.csv \
--column net_return \
--trials 120 \
--cluster date \
--timestamps date \
--gross gross_return \
--cost 0.0025
Save an HTML report you can read or send:
straud audit results.csv --column net_return --trials 120 --html report.html
The report is titled after the input file. Give it a clearer name with
--name, which is worth doing whenever you will keep the report:
straud audit results.csv --column net_return --trials 120 \
--name "reverse-split fade, 10-day hold"
Machine-readable output:
straud audit results.csv --column net_return --json
Best if your backtest already runs in Python.
from straud.audit import audit
report = audit(
returns, # your per-trade returns
name="My strategy",
n_trials=120, # variants tested, including discarded ones
cluster=dates, # optional but valuable
timestamps=dates,
gross_returns=gross,
assumed_cost=0.0025,
)
print(report.render()) # markdown to the terminal
print(report.verdict.value) # "Conditional"
for f in report.findings:
print(f.severity.label, f.title)
Save an HTML report:
from straud.render import write_report
write_report(report, "report.html")
Straud's output contains arrows and severity glyphs. Windows consoles default
to cp1252, where printing them raises UnicodeEncodeError. One line fixes it:
from straud.console import enable_utf8
enable_utf8()
The straud command does this for you; only the Python API needs it. Writing
to a file is always safe — reports are written as UTF-8 explicitly.
Let it count your trials for you — add one line to your existing sweep:
from straud import Lab
lab = Lab("out/ledger.json")
@lab.trial("my_idea") # ← the only line you add
def backtest(threshold=0.6, hold=60):
...
return returns
Now every call is logged automatically, with the arguments it was actually called with. Re-running the same code does not inflate the count; editing the logic does. See the ledger.
If you research with Claude Code (or any MCP-compatible assistant), Straud can plug in as a tool the assistant must pass.
Why this matters: an AI has no memory across attempts of how many variants it already tried. The 47th backtest feels exactly like the first, so it will happily overfit and report the result with complete confidence. Straud gives it the memory it lacks.
Setup. Copy the block from mcp.json.example into a .mcp.json file in
your project folder, adjusting the paths:
{
"mcpServers": {
"straud": {
"command": "/full/path/to/.venv/bin/python",
"args": ["-m", "straud.mcp_server"],
"cwd": "/full/path/to/straud",
"env": {
"STRAUD_LEDGER": "/full/path/to/ledger.json",
"PYTHONPATH": "/full/path/to/straud"
}
}
}
}
Restart your assistant. It now has six tools:
| Tool | What it does |
|---|---|
log_trial |
Record one tested variant; returns the running verdict |
status |
How many variants have been tested so far |
verdict |
Full trial-adjusted verdict on the best one |
audit_artifact |
Check a data file for stale assumptions |
seal_holdout |
Commit to one variant before looking |
reveal_holdout |
Look, once — permanently recorded |
Use one ledger per research programme. A fresh ledger every session would reset the trial count and hand back a flattering verdict.
STRAUD_ROOT — set this deliberatelySTRAUD_ROOT controls which files the MCP tools are allowed to read. It
defaults to the directory the server starts in.
Set it to the research folder you intend to expose, and nothing wider.
The reason is specific: unlike the command line, where you type the path yourself, these tools take paths chosen by the assistant — and an assistant can be influenced by text inside the very data it is reading. Confining the root means a stray instruction in a downloaded CSV cannot make it read your private files and repeat them back into the conversation.
Never set it to / or to your home directory.
Optional but recommended: copy skill/straud-gate/ into
.claude/skills/. It instructs the assistant to log every variant rather
than only the ones it likes — which is the difference between a real trial
count and a flattering one.
Every flag below unlocks checks that otherwise cannot run. Anything you leave out appears under "what we did NOT test".
--gross gross_return --cost 0.0025
--cost is the round-trip cost as a decimal (0.0025 = 0.25% = 25 basis
points). Unlocks the cost sweep: at what cost level the edge dies, and
how much headroom you have. Under 1.5× headroom is fragile.
--signal signal_strength
Checks whether your signal is secretly reading the outcome it claims to predict.
--domain crypto_perp --venue binance_usdm \
--hold-seconds 60 --funding-interval 8 --funding-debited
Checks whether your holds cross funding settlements (and whether you charged yourself for them), and whether your assumed cost is even achievable at that venue's published fees.
--domain us_equity_short --borrow 0.52 --hold-days 10 --price entry_price
--borrow is annualised borrow cost as a decimal (0.52 = 52%/year).
Checks borrow plausibility and Reg SHO short-sale-restriction exposure.
--adv-column daily_volume --notional 25000 --window-minutes 1 --event-multiple 5
--adv-column — average daily traded value for the instrument--notional — how much you put on per trade--window-minutes — minutes of the day you can actually trade (a
one-minute strategy is 1, an all-day strategy is 390)--event-multiple — how busy your moment is versus an average minute⚠️ The event multiple dominates the answer, so Straud makes you state it rather than hiding a guess. Do not assume event moments are busier: on one measured strategy the trigger minute had 15% of normal depth, not more.
These cost no data and both are cheap to answer wrongly.
Can this test detect what you are claiming? Pass the effect your theory predicts, in return units:
straud audit trades.csv --column net_return --expected-effect 0.004
If the sample is too small to detect an effect that size, Straud says so and calls the test incapable rather than failed. This matters because an underpowered test tells you nothing either way, and it still burns a trial against your deflation budget. One strategy in the fleet Straud was built against was paper-traded for 34 days when detecting its own claimed Sharpe needed roughly 727 — the eventual "it didn't work" was uninformative by construction.
Who is losing? An edge is somebody else's loss.
straud audit trades.csv --column net_return \
--counterparty "index funds that must rebalance at the close and cannot defer past the effective date"
Straud cannot check whether your answer is true. It can check that you gave one, and it rejects the non-answers — "the market", "whoever is on the other side", "retail". If you cannot name who transacts at a worse price than they need to, how often, and what stops them from quitting, you have a pattern rather than a mechanism, and patterns stop working without warning.
A holdout is data you promise not to look at until you have committed to one specific strategy.
This is the only way to earn a claim. Everything else is the best of some search; a sealed test is not.
The discipline:
With an AI assistant, use seal_holdout and reveal_holdout. From the
command line, point at a registry file:
straud audit holdout_results.csv --column net --holdout seals.json --family my_idea
Every reveal is recorded permanently. A holdout you looked at twice is not a holdout, and the record will say so. That is the point.
If you cannot remember how many variants you tested, stop guessing and let Straud count.
from straud import Lab
lab = Lab("out/ledger.json")
@lab.trial("momentum_idea")
def backtest(lookback=20, threshold=1.5):
...
return returns
for lb in (10, 20, 30):
for th in (1.0, 1.5, 2.0):
backtest(lookback=lb, threshold=th) # all 9 logged automatically
Check what it has recorded:
straud ledger out/ledger.json
Then use it in an audit — if the ledger holds more trials than you declared, the larger, more conservative number wins:
straud audit results.csv --column net --trials 20 --ledger out/ledger.json
Three things the ledger gets right:
Sometimes the problem is not your maths — it is the file.
straud artifact events.csv --date-column date
This looks for:
This is worth running on any results file you did not create today. In real use, a stale cost column understated a working strategy by 2.5× on Sharpe while looking completely plausible.
To silence a column you have checked, write a .meta.json beside the file
recording what the assumption is:
{
"columns": {
"net_return": {
"formula": "gross_return - 0.0025",
"assumption": "25bp round-trip cost"
}
}
}
Documented columns are shown but not flagged.
If you run several strategies together, two questions only exist at the book level.
from straud.portfolio import portfolio_stats, co_tail, to_daily
book = {
"strategy_a": to_daily(returns_a, dates_a),
"strategy_b": to_daily(returns_b, dates_b),
}
stats = portfolio_stats(book, campaign_trials=300)
print(stats["corr"]) # correlation on overlapping days only
print(stats["book"]["deflated_sharpe"])
print(co_tail(book)) # do they lose on the SAME days?
co_tail matters more than correlation. Two strategies can look
uncorrelated all year and still share every bad day.campaign_trials deflates the whole book. Your portfolio is also a
selection — you kept the winners — and nobody else corrects for that.If the strategies do not overlap in time, Straud says the comparison is unmeasurable rather than comparing two different periods and calling the difference diversification.
| Finding | What to do |
|---|---|
| Non-finite values in the returns series | Find what produced the NaN/infinity before trusting anything else. |
| The returns series has zero variance | Your column is broken — a constant fill or a bad join. |
| Signal may be reading the outcome (look-ahead) | Rebuild the signal from strictly earlier data. |
| Events may be fabricated by holes in the archive | If your event is defined by something missing, prove the data exists first. |
| Finding | What to do |
|---|---|
| Trial-adjusted edge is not distinguishable from luck | Commit to one variant, test on sealed data. |
| Sample is too short to support the claim | Collect more data, or test fewer things. |
| Observations are not independent | Use the --cluster flag and re-read the numbers. |
| The cluster key cannot reveal clustering | Your key has one row per group — cluster by the shared driver. |
| The two independence axes disagree | Report n events, n dates and n entities. Treat the weakest axis as the real sample size. |
| Overlapping positions counted as independent | Keep one event per entity per window, and re-run. |
| Underpowered by construction | Gather more observations, widen the population, or drop the candidate. |
| This result survives up to ~N variants | Compare N against how many you really tried. |
| Too few observations to evaluate | Nothing can be concluded yet. |
| No holdout has been sealed | Seal one before deciding. |
| Holdout revealed N times | It stopped being a holdout after the first look; say so. |
| The search has grown since sealing | Re-seal, or report the originally committed variant. |
| Finding | What to do |
|---|---|
| The chosen operating point is a spike | A real edge degrades gently off its optimum. This one does not. |
| The entire edge rests on a few clusters | You are betting those episodes repeat. Size accordingly. |
| The edge lives in entities that RECUR | Condition on recurrence and re-test, or report first-time and repeat entities separately. |
| A handful of entities carry the result | Re-test with the dominant names excluded. If the edge does not survive, it is a claim about those names. |
| The average entity wins, the typical one loses | Report the median beside the mean, and say which names supply the tail. |
| No loser is named | Say who transacts at a worse price, how often, and what prevents them stopping. |
| A loser is named, but not what holds them there | State the constraint, or size the edge as temporary. |
| Arbitrary cutoffs do as well as the real one | Your threshold is not carrying the result. |
| The edge is not consistent across eras | It is a statement about one regime, not a general effect. |
| The submitted sample spans a single era | Submit more history if you have it. |
| Probability of backtest overfitting = N | ⚠️ This measures whether your selection is informative — not whether the edge is real. Do not read a high value alone as a dead strategy. |
| Finding | What to do |
|---|---|
| The edge does not survive its own cost assumption | There is nothing to trade here. |
| The edge dies just above the assumed cost | Under 1.5× headroom: measure real fills before sizing. |
| Declared gross and net returns are identical | You passed the same column twice; cost is being charged twice. |
| Assumed cost is below the venue's published fee | That fill is unavailable at any size. |
| Holds cross funding settlements, charge not debited | Debit funding per crossing and re-run. |
| Borrow assumption below the observed band | Sample borrow at signal time for your universe. |
| Borrow assumption far above the band | You are understating your own strategy. |
| Shorts entering under Reg SHO SSR | Model up-bid-only fills for those trades. |
| Finding | What to do |
|---|---|
| Trade size exceeds accessible liquidity | Measure event-window depth, then re-size. |
| Limited capacity headroom | Treat current size as near the ceiling. |
straud: command not found
If you pip-installed it, use python3 -m straud.cli instead — same arguments.
If you downloaded the standalone build, you must be inside the unzipped
folder and write ./straud (with the ./), not straud.
"straud cannot be opened because the developer cannot be verified" (macOS)
The standalone build is unsigned. Right-click straud → Open → Open, or
run xattr -d com.apple.quarantine straud. See A.
The standalone build is very slow
Only the first run, which can take up to a minute while your system verifies
~140 libraries. After that it starts in under a second. If every run is
slow, you are probably running a --onefile build — use the folder version,
which is what ships here.
--column 'x' not found
Straud lists the available columns in the error. Check spelling and case.
"Too few observations to evaluate" Fewer than 10 usable rows. Check for text or blanks in your returns column.
Everything comes back Critical
Check your returns are decimals, not percentages. 2.5 means +250%, not
+2.5%.
The verdict got worse when I added flags Working as intended. More information means more checks can run. The earlier verdict was not better — it was less informed.
Reading a Parquet file fails
pip install -e '.[parquet]'
The AI assistant says a file is "outside the permitted directory"
Working as intended. The MCP tools only read below STRAUD_ROOT (see C).
Move the file into that folder, or restart the server with STRAUD_ROOT set
to the folder you mean to expose. Do not widen it to your whole home
directory.
"non-finite values in the returns series" Your file has NaN or infinity. An infinity usually means a division by zero or a mishandled total loss upstream. Worth finding.
Every release publishes SHA256SUMS.txt alongside the archives. Check it
before running anything:
shasum -a 256 -c SHA256SUMS.txt
The binaries are not code-signed, so macOS will warn on first run and
Windows SmartScreen may too. That is expected; the checksum is the
verification path. START_HERE.txt in the archive explains the first-run
steps.
STRAUD_ROOTThe MCP server reads files and hands what it finds to an assistant. Point
STRAUD_ROOT at the research directory you intend to expose and nothing
wider — it is a hard boundary, and paths outside it are refused, including
via symlinks.
Column names and ticker symbols come from exchanges, vendors and whatever wrote the file — not from you. Text inside them can be crafted to look like an instruction to an AI assistant, and an assistant that follows it could be talked into reporting a pass.
Straud defends against this: values copied out of a file are stripped of
characters that could fake formatting, capped in length, labelled as data
rather than instructions, and flagged when an identifier does not look like
an identifier. If you see a suspicious_identifiers warning, treat the
file's provenance as the question — it can equally mean an attack, a vendor
feed change, or a column that shifted during a join.
Reads are capped so that an assistant cannot be induced to exhaust your memory. Parquet is checked by row and column count before it is opened, because a compressed file can expand thousands of times over. If your data is genuinely larger than the defaults, raise them:
STRAUD_MAX_CELLS=200000000 STRAUD_MAX_CSV_BYTES=2000000000 straud audit ...
The trial ledger records every variant you have tested. On macOS and Linux Straud creates it readable only by your account, which matters on shared and cloud machines.
On Windows this does not apply. Windows uses ACLs rather than POSIX permissions, and Straud cannot set those portably, so the ledger inherits whatever its folder grants. Keep it somewhere already restricted to you — not a shared drive. Back it up as you would any other proprietary research file.
Stated plainly, because a tool about honesty should be honest about itself.
A clean report means "survived the tests we ran". It never means "safe".
Straud is not investment advice and carries no warranty. It has no market data, no broker connection, and no knowledge of your positions — it reads a column of numbers. Nothing it produces is a recommendation to buy, sell or hold anything. Trading involves substantial risk of loss, and backtested results are hypothetical.
Read DISCLAIMER in full before relying on any output.
Your data stays with you. Straud runs locally, transmits nothing, has no telemetry or analytics, and requires no account. Nothing you audit leaves your machine.
Backtest — simulating a strategy on historical data.
Basis point (bp) — one hundredth of a percent. 25bp = 0.25% = 0.0025.
Borrow cost — what you pay to borrow shares to sell short, quoted annualised.
Cluster — a group of trades that share a driver (same day, same event) and therefore are not independent observations.
Deflated Sharpe — the Sharpe ratio after correcting for how many variants you tried. The single most useful number Straud produces.
Drawdown — a peak-to-trough fall in your account.
Holdout — data set aside and untouched, used once to test a committed strategy.
Look-ahead — accidentally using information that would not have been available at the time. Makes a backtest look far better than reality.
MinBTL (minimum backtest length) — the shortest sample that could support your claim given how many things you tried. Below it, no result is meaningful.
Multiple testing — the problem that testing many ideas guarantees some look good by chance.
Overfitting — tuning a strategy to historical noise rather than a real effect.
PBO (probability of backtest overfitting) — how often the in-sample best variant turns out below average out-of-sample. Measures your selection, not your edge.
Sharpe ratio — return divided by volatility. Straud works in per-trade Sharpe, which is much smaller than the annualised figure you may be used to.
Slippage — the difference between the price you expected and the price you got.
Survivorship bias — analysing only instruments that still exist today, which quietly removes the failures.
Trial — one tested variant of a strategy. Including the ones you threw away.