Deflated Sharpe and PBO checks. A verdict is not admission to anything and is not a forecast.
This MCP server provides access to canlicapital.com’s free, keyed validation API for backtest validation. It is designed to run four named validators and return both success/error status and the full API envelope, including explanatory text about what results do and do not establish.
🛠️ Key Features
MCP server for canlicapital.com free, keyed validation API
Seven tools: issue a free key, run four validators, fetch a stored receipt, read service status
Deflated Sharpe ratio: the probability (0 to 1) that the selected strategy's Sharpe beats the best that luck gives across the variants tried, with the probabilistic Sharpe and that luck benchmark. Send the seven statistics or a return series. With every variant's returns use validate_overfitting; luck as a trial count, validate_luck_trials; a multiple-testing haircut, validate_haircut_sharpe. A deflated Sharpe or overfitting probability above or below any threshold is not admission to anything and is not a forecast.
Parameters8
observed_sharpe_annualized
number
optional
Annualized Sharpe as observed.
observations
integer
optional
Number of return observations.
periods_per_year
number
optional
Periods per year: 252 daily, 365 crypto, 52 weekly, 12 monthly.
skew
number
optional
Skewness of returns; 0 if Normal.
non_excess_kurtosis
number
optional
Kurtosis, not excess kurtosis; 3 if Normal.
effective_independent_trials
integer
optional
Independent variants tried before choosing this one.
cross_trial_sharpe_sd_annualized
number
optional
Standard deviation of annualized Sharpe across those trials.
returns
array
optional
Periodic returns as fractions (0.01 = 1%), oldest first; replaces the Sharpe, observations, skew and kurtosis.
Probability of backtest overfitting (0 to 1) by CSCV: how often the in-sample best variant falls below the out-of-sample median. Needs every variant's returns (periods by variants); with summary statistics only, use validate_deflated_sharpe. A deflated Sharpe or overfitting probability above or below any threshold is not admission to anything and is not a forecast.
Parameters4
matrix
array
required
Returns as fractions, one row per period, one column per variant.
n_splits
integer
optional
Even number of blocks, at least 2; default 16.
max_combinations
integer
optional
Most splits evaluated, up to 2000 (default).
seed
number
optional
Sampling seed; default 42.
Raw schema
{
"type": "object",
"properties": {
"matrix": {
"minItems": 2,
"maxItems": 20000,
"type": "array",
"items": {
"maxItems": 200,
"type": "array",
"items": {
"type": "number"
}
},
"description": "Returns as fractions, one row per period, one column per variant."
},
"n_splits": {
"description": "Even number of blocks, at least 2; default 16.",
"type": "integer",
"exclusiveMinimum": 0
},
"max_combinations": {
"description": "Most splits evaluated, up to 2000 (default).",
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 2000
},
"seed": {
"description": "Sampling seed; default 42.",
"type": "number"
}
},
"required": [
"matrix"
],
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false
}
validate_reality_check
Data-snooping tests on every variant a search tried: Hansen's SPA p-value that the best beat the benchmark only by luck, White's Reality Check, and the variants Romano-Wolf StepM finds better. Send all variants tried, not only the winners. A deflated Sharpe or overfitting probability above or below any threshold is not admission to anything and is not a forecast.
Parameters7
matrix
array
optional
Returns of every variant the search tried, one row per period, one column per variant.
matrix_file
string
optional
Path to a CSV or JSON with one numeric column per variant, instead of matrix.
benchmark
array
optional
Benchmark return per period; default zero.
block_length
integer
optional
Mean bootstrap block in periods; default round(n^(1/3)).
reps
integer
optional
Bootstrap draws; default 2000.
seed
integer
optional
Sampling seed; default 42.
alpha
number
optional
Familywise error for StepM; default 0.05.
Raw schema
{
"type": "object",
"properties": {
"matrix": {
"description": "Returns of every variant the search tried, one row per period, one column per variant.",
"minItems": 30,
"maxItems": 20000,
"type": "array",
"items": {
"maxItems": 200,
"type": "array",
"items": {
"type": "number"
}
}
},
"matrix_file": {
"description": "Path to a CSV or JSON with one numeric column per variant, instead of matrix.",
"type": "string",
"minLength": 1,
"maxLength": 4096
},
"benchmark": {
"description": "Benchmark return per period; default zero.",
"minItems": 30,
"maxItems": 20000,
"type": "array",
"items": {
"type": "number"
}
},
"block_length": {
"description": "Mean bootstrap block in periods; default round(n^(1/3)).",
"type": "integer",
"minimum": 1,
"maximum": 10000
},
"reps": {
"description": "Bootstrap draws; default 2000.",
"type": "integer",
"minimum": 500,
"maximum": 400000
},
"seed": {
"description": "Sampling seed; default 42.",
"type": "integer"
},
"alpha": {
"description": "Familywise error for StepM; default 0.05.",
"type": "number",
"exclusiveMinimum": 0,
"maximum": 0.5
}
},
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false
}
validate_paper_evidence
Checks paper-evidence.v0 structure and disclosures. With CANLI_LOCAL=1, record_file reads an export bundle and journal_file verifies signatures, source hashes and recomputed claims. Without a journal, source facts and signatures are unchecked. Local files are never uploaded. This verdict is about the series exactly as submitted. The service never saw the data source, its costs, survivorship, or any lookahead in how the series was built.
Parameters4
record
object
optional
A canli.paper-evidence.v0 record.
record_file
string
optional
Local record or export-bundle JSON path; requires CANLI_LOCAL=1.
journal_file
string
optional
Local signed journal to recompute and bind; requires CANLI_LOCAL=1.
signature
object
optional
Detached record signature for a signed inline record; requires journal_file.
Book Sharpe ceiling from adding sleeves of this quality and correlation, the Sharpe at a sleeve count, and the sleeves a target needs. For portfolio construction; it validates no single strategy. This verdict is about the series exactly as submitted. The service never saw the data source, its costs, survivorship, or any lookahead in how the series was built.
Parameters4
sleeve_sharpe
number
required
Annualized Sharpe of one sleeve.
average_pairwise_correlation
number
required
Average correlation between sleeves, -1 to 1.
sleeves
integer
optional
Sleeve count, for that book's Sharpe.
target
number
optional
Target book Sharpe, for the sleeves it needs.
Raw schema
{
"type": "object",
"properties": {
"sleeve_sharpe": {
"type": "number",
"exclusiveMinimum": 0,
"description": "Annualized Sharpe of one sleeve."
},
"average_pairwise_correlation": {
"type": "number",
"minimum": -1,
"maximum": 1,
"description": "Average correlation between sleeves, -1 to 1."
},
"sleeves": {
"description": "Sleeve count, for that book's Sharpe.",
"type": "integer",
"minimum": 1,
"maximum": 500
},
"target": {
"description": "Target book Sharpe, for the sleeves it needs.",
"type": "number",
"exclusiveMinimum": 0
}
},
"required": [
"sleeve_sharpe",
"average_pairwise_correlation"
],
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false
}
validate_track_record
Minimum track record length (observations and years) for an observed Sharpe to beat a benchmark at a confidence level; with observations, the record's probabilistic Sharpe so far. For live or paper records; to size a backtest for its trials, use validate_backtest_length. A deflated Sharpe or overfitting probability above or below any threshold is not admission to anything and is not a forecast.
Parameters7
observed_sharpe_annualized
number
required
Annualized Sharpe as observed.
periods_per_year
number
required
Periods per year: 252 daily, 365 crypto, 52 weekly, 12 monthly.
skew
number
required
Skewness of returns; 0 if Normal.
non_excess_kurtosis
number
required
Kurtosis, not excess kurtosis; 3 if Normal.
benchmark_sharpe_annualized
number
optional
Annualized Sharpe to beat; default 0.
confidence
number
optional
Between 0 and 1; default 0.95.
observations
integer
optional
Record length so far, for its probabilistic Sharpe.
Minimum backtest length (years) before the best of N independent trials is not expected to reach a target Sharpe by luck; with backtest_years, the most trials those years allow. For planning a search; once it has a result, use validate_deflated_sharpe. A deflated Sharpe or overfitting probability above or below any threshold is not admission to anything and is not a forecast.
Backtest length in years, for the most trials it allows.
target_sharpe_annualized
number
optional
In-sample annualized Sharpe you would call a discovery; default 1.
Raw schema
{
"type": "object",
"properties": {
"effective_independent_trials": {
"description": "Independent trials tried (backtests, parameter sets, ideas).",
"type": "integer",
"minimum": 2,
"maximum": 1000000000
},
"backtest_years": {
"description": "Backtest length in years, for the most trials it allows.",
"type": "number",
"exclusiveMinimum": 0,
"maximum": 1000
},
"target_sharpe_annualized": {
"description": "In-sample annualized Sharpe you would call a discovery; default 1.",
"type": "number",
"exclusiveMinimum": 0,
"maximum": 10
}
},
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false
}
validate_haircut_sharpe
Haircut Sharpe for multiple testing (Harvey and Liu 2015): the Sharpe a single test would have needed, by Bonferroni and independent tests, and with the other tests' Sharpes, Holm and BHY. For the probability the Sharpe is real, use validate_deflated_sharpe. A deflated Sharpe or overfitting probability above or below any threshold is not admission to anything and is not a forecast.
Parameters6
observed_sharpe_annualized
number
required
Annualized Sharpe as observed.
periods_per_year
number
required
Periods per year: 252 daily, 365 crypto, 52 weekly, 12 monthly.
observations
integer
required
Return observations behind the Sharpe.
tests
integer
optional
Tests run, this one included; gives the Bonferroni and independent-test haircuts.
autocorrelation
number
optional
Lag-1 autocorrelation of returns, -1 to 1; default 0. Corrects the annualized Sharpe (Lo 2002).
other_sharpe_ratios_annualized
array
optional
Annualized Sharpes of the other tests over the same observations; adds Holm and BHY.
Raw schema
{
"type": "object",
"properties": {
"observed_sharpe_annualized": {
"type": "number",
"exclusiveMinimum": 0,
"maximum": 10,
"description": "Annualized Sharpe as observed."
},
"periods_per_year": {
"type": "number",
"minimum": 1,
"maximum": 10000,
"description": "Periods per year: 252 daily, 365 crypto, 52 weekly, 12 monthly."
},
"observations": {
"type": "integer",
"minimum": 3,
"maximum": 1000000,
"description": "Return observations behind the Sharpe."
},
"tests": {
"description": "Tests run, this one included; gives the Bonferroni and independent-test haircuts.",
"type": "integer",
"minimum": 1,
"maximum": 100000000
},
"autocorrelation": {
"description": "Lag-1 autocorrelation of returns, -1 to 1; default 0. Corrects the annualized Sharpe (Lo 2002).",
"type": "number",
"exclusiveMinimum": -1,
"exclusiveMaximum": 1
},
"other_sharpe_ratios_annualized": {
"description": "Annualized Sharpes of the other tests over the same observations; adds Holm and BHY.",
"minItems": 1,
"maxItems": 10000,
"type": "array",
"items": {
"type": "number",
"minimum": -10,
"maximum": 10
}
}
},
"required": [
"observed_sharpe_annualized",
"periods_per_year",
"observations"
],
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false
}
validate_luck_trials
How many skill-less strategies a search would need for its best to reach this Sharpe by luck (Monte Carlo), and with a trial count, the chance it did. States luck as the best of N random tries; for the probability the Sharpe is real, use validate_deflated_sharpe. A deflated Sharpe or overfitting probability above or below any threshold is not admission to anything and is not a forecast.
Parameters6
observed_sharpe_annualized
number
required
Annualized Sharpe as observed.
periods_per_year
number
required
Periods per year: 252 daily, 365 crypto, 52 weekly, 12 monthly.
observations
integer
required
Return observations behind the Sharpe.
effective_independent_trials
integer
optional
Independent trials tried; adds the chance the best reached this Sharpe by luck.
skew
number
optional
Skewness; below -0.5 the reading warns the counts are too generous.
autocorrelation
number
optional
Lag-1 autocorrelation of returns, -1 to 1; default 0. Corrects the annualized Sharpe (Lo 2002).
Raw schema
{
"type": "object",
"properties": {
"observed_sharpe_annualized": {
"type": "number",
"minimum": -20,
"maximum": 20,
"description": "Annualized Sharpe as observed."
},
"periods_per_year": {
"type": "number",
"minimum": 1,
"maximum": 10000,
"description": "Periods per year: 252 daily, 365 crypto, 52 weekly, 12 monthly."
},
"observations": {
"type": "integer",
"minimum": 3,
"maximum": 1000000,
"description": "Return observations behind the Sharpe."
},
"effective_independent_trials": {
"description": "Independent trials tried; adds the chance the best reached this Sharpe by luck.",
"type": "integer",
"minimum": 1,
"maximum": 1000000000
},
"skew": {
"description": "Skewness; below -0.5 the reading warns the counts are too generous.",
"type": "number",
"minimum": -100,
"maximum": 100
},
"autocorrelation": {
"description": "Lag-1 autocorrelation of returns, -1 to 1; default 0. Corrects the annualized Sharpe (Lo 2002).",
"type": "number",
"exclusiveMinimum": -1,
"exclusiveMaximum": 1
}
},
"required": [
"observed_sharpe_annualized",
"periods_per_year",
"observations"
],
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false
}
audit_backtest
One-call audit of a strategy's returns. Headline: one test whose false-positive rate was measured on nine return shapes (Hansen's SPA with variants). Then deflated Sharpe, minimum track record and, with variants, overfitting and out-of-sample decay, each with its receipt, plus fix_next. Send every variant tried (variants_file) so trials are counted; point returns_file at a CSV instead of pasting. A deflated Sharpe or overfitting probability above or below any threshold is not admission to anything and is not a forecast.
Parameters11
returns
array
optional
Periodic returns as fractions (0.01 = 1%), oldest first; replaces the Sharpe, observations, skew and kurtosis.
returns_file
string
optional
Path to a CSV or JSON of the returns on this machine (not on the hosted endpoint), instead of returns.
returns_column
any
optional
Column name or 1-based position, when returns_file has several numeric columns.
periods_per_year
number
required
Periods per year: 252 daily, 365 crypto, 52 weekly, 12 monthly.
effective_independent_trials
integer
optional
Independent variants tried before choosing this one. Optional with variants: counted from them, and the larger number is used.
cross_trial_sharpe_sd_annualized
number
optional
Standard deviation of annualized Sharpe across those trials. Optional with variants: measured from them, and the larger is used.
benchmark_sharpe_annualized
number
optional
Annualized Sharpe to beat; default 0.
confidence
number
optional
Between 0 and 1; default 0.95.
variants
array
optional
Optional returns of every variant tried (this one included), one row per period, one column per variant; adds the overfitting check.
variants_file
string
optional
Path to a CSV or JSON with one numeric column per variant, instead of variants.
n_splits
integer
optional
Even number of blocks, at least 2; default 16.
Raw schema
{
"type": "object",
"properties": {
"returns": {
"description": "Periodic returns as fractions (0.01 = 1%), oldest first; replaces the Sharpe, observations, skew and kurtosis.",
"minItems": 2,
"maxItems": 20000,
"type": "array",
"items": {
"type": "number"
}
},
"returns_file": {
"description": "Path to a CSV or JSON of the returns on this machine (not on the hosted endpoint), instead of returns.",
"type": "string",
"minLength": 1,
"maxLength": 4096
},
"returns_column": {
"description": "Column name or 1-based position, when returns_file has several numeric columns.",
"anyOf": [
{
"type": "string",
"minLength": 1,
"maxLength": 200
},
{
"type": "integer",
"minimum": 1
}
]
},
"periods_per_year": {
"type": "number",
"minimum": 1,
"maximum": 10000,
"description": "Periods per year: 252 daily, 365 crypto, 52 weekly, 12 monthly."
},
"effective_independent_trials": {
"description": "Independent variants tried before choosing this one. Optional with variants: counted from them, and the larger number is used.",
"type": "integer",
"minimum": 2,
"maximum": 10000000
},
"cross_trial_sharpe_sd_annualized": {
"description": "Standard deviation of annualized Sharpe across those trials. Optional with variants: measured from them, and the larger is used.",
"type": "number",
"minimum": 0,
"maximum": 10
},
"benchmark_sharpe_annualized": {
"description": "Annualized Sharpe to beat; default 0.",
"type": "number",
"minimum": -10,
"maximum": 10
},
"confidence": {
"description": "Between 0 and 1; default 0.95.",
"type": "number",
"exclusiveMinimum": 0,
"exclusiveMaximum": 1
},
"variants": {
"description": "Optional returns of every variant tried (this one included), one row per period, one column per variant; adds the overfitting check.",
"minItems": 2,
"maxItems": 20000,
"type": "array",
"items": {
"maxItems": 200,
"type": "array",
"items": {
"type": "number"
}
}
},
"variants_file": {
"description": "Path to a CSV or JSON with one numeric column per variant, instead of variants.",
"type": "string",
"minLength": 1,
"maxLength": 4096
},
"n_splits": {
"description": "Even number of blocks, at least 2; default 16.",
"type": "integer",
"exclusiveMinimum": 0
}
},
"required": [
"periods_per_year"
],
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false
}
verify_receipt
Verify a receipt offline: its Ed25519 signature against the bundled canlicapital.com key, its output hash and its id. Send an id to fetch it first, or the receipt itself. The receipt is content-hashed, reproducible from the open-source core it names, and signed with Ed25519 by a key published at https://canlicapital.com/.well-known/canli-receipt-keys.json.
Parameters3
id
string
optional
Receipt id from a validation result.
receipt
object
optional
A receipt object, to verify without fetching.
include_receipt
boolean
optional
With id: also return the stored receipt.
Raw schema
{
"type": "object",
"properties": {
"id": {
"description": "Receipt id from a validation result.",
"type": "string",
"pattern": "^[0-9a-f]{24}$"
},
"receipt": {
"description": "A receipt object, to verify without fetching.",
"type": "object"
},
"include_receipt": {
"description": "With id: also return the stored receipt.",
"type": "boolean"
}
},
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false
}
service_status
Whether the validation API is up, with its quotas; check after a timeout before resubmitting. No key. This verdict is about the series exactly as submitted. The service never saw the data source, its costs, survivorship, or any lookahead in how the series was built.
Backtest a rule on your prices over a whole parameter grid, then validate the best with the count of variants actually run: deflated Sharpe, overfitting probability, minimum track record, vs buy and hold, after costs. A backtest, deflated or not, is not a forecast and not advice.
A long price or return series in about 100 words plus fields: growth, risk, dated drawdowns, trend, volatility regime, tails, jumps, stale data. Use instead of reading raw bars. Nothing here is a forecast or advice.
Parameters9
prices
array
optional
Prices, oldest first; or returns, or series_file.
returns
array
optional
Returns as fractions, oldest first.
series_file
string
optional
CSV or JSON of prices or returns on this machine; local server only.
{
"type": "object",
"properties": {
"prices": {
"description": "Prices, oldest first; or returns, or series_file.",
"type": "array",
"items": {
"type": "number"
}
},
"returns": {
"description": "Returns as fractions, oldest first.",
"type": "array",
"items": {
"type": "number"
}
},
"series_file": {
"type": "string",
"description": "CSV or JSON of prices or returns on this machine; local server only."
},
"series_column": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
],
"description": "Column name or 1-based position."
},
"series_kind": {
"description": "series_file holds; default prices.",
"type": "string",
"enum": [
"prices",
"returns"
]
},
"dates": {
"type": "array",
"items": {
"type": "string"
},
"description": "Optional ISO dates, one per value."
},
"periods_per_year": {
"type": "number",
"description": "252 daily (default), 365 crypto, 52 weekly, 12 monthly."
},
"benchmark_returns": {
"description": "Same-period benchmark returns; adds beta.",
"type": "array",
"items": {
"type": "number"
}
},
"name": {
"description": "Label for the text.",
"type": "string"
}
},
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false
}
stress_test
Stress a strategy's returns: bootstrap histories (how often the drawdown limit breaks or Sharpe turns negative) and named crash, volatility, repeat and stuck-position scenarios, with a fragility share. Frequencies over resampled histories are not forecasts of the future.
Parameters11
returns
array
optional
Strategy returns as fractions, oldest first.
returns_file
string
optional
CSV or JSON of returns on this machine; local server only.
Check a trading plan before it trades: broker order-rate and minimum-order limits, 2026 US day-trading rules, square-root market impact with crowding, and the capital where costs eat the return. Checks the plan as described against published broker limits and a market-impact model; it never contacts a broker or sees an account.
Parameters15
broker
string
optional
Default other.
asset_class
string
optional
account
string
optional
capital_usd
number
required
orders_per_rebalance
integer
required
rebalances_per_year
number
required
turnover_per_year
number
required
Buys plus sells a year / capital.
adv_usd
number
required
Daily dollar volume of a typical holding.
daily_volatility
number
required
Of a typical holding, e.g. 0.02.
copies
integer
optional
Agents trading the same signal at once; default 1.
Does a signal look ahead? plan gives cuts; rerun your code on the first cut rows for each, then compare. Rows that changed used later rows; the result names the pattern and horizon. No code is sent. A pass covers only these cuts: the columns are the caller's, and survivorship, vendor revisions or a leak no cut reaches leave no trace.
Does your pipeline find edges in noise? plan writes placebo files (your data's returns in random orders, so nothing is predictable); run the whole pipeline on real.csv and on each, then compare for a p-value that counts every choice it makes. Beating the placebos shows the pipeline finds more in the real order of the returns than in random orders of the same returns; it does not show the edge survives costs, capacity or the future.
Parameters10
action
string
required
data_file
string
optional
plan, on your machine: CSV or JSON, a column per asset, dates optional.
columns
object
optional
plan: {asset: [values]} instead of data_file.
kind
string
optional
plan: default prices.
placebos
integer
optional
plan: default 19, up to 199.
method
string
optional
seed
integer
optional
real
number
optional
compare: the pipeline's result on the real data.
placebo_results
array
optional
compare: its result on each placebo, in file order.
lower_is_better
boolean
optional
Raw schema
{
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": [
"plan",
"compare"
]
},
"data_file": {
"description": "plan, on your machine: CSV or JSON, a column per asset, dates optional.",
"type": "string"
},
"columns": {
"description": "plan: {asset: [values]} instead of data_file.",
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "number"
}
}
},
"kind": {
"description": "plan: default prices.",
"type": "string",
"enum": [
"prices",
"returns"
]
},
"placebos": {
"description": "plan: default 19, up to 199.",
"type": "integer"
},
"method": {
"type": "string",
"enum": [
"permute",
"block_permute",
"stationary_bootstrap"
]
},
"seed": {
"type": "integer"
},
"real": {
"description": "compare: the pipeline's result on the real data.",
"type": "number"
},
"placebo_results": {
"description": "compare: its result on each placebo, in file order.",
"type": "array",
"items": {
"type": "number"
}
},
"lower_is_better": {
"type": "boolean"
}
},
"required": [
"action"
],
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false
}
Is your best backtest real, or just the luckiest of the variants you tried? This MCP server
lets Claude, Cursor or any MCP client answer that with the standard corrections: the deflated
Sharpe ratio, the CSCV probability of backtest overfitting, data-snooping tests on every variant
tried (Hansen's SPA, White's Reality Check, Romano-Wolf StepM), the minimum track record length,
the haircut Sharpe ratio, and luck-equivalent trials. Free and MIT-licensed.
Quick start
bash
# Claude Code, computed on your machine: no key, nothing sent anywhere
claude mcp add canli-local --env CANLI_LOCAL=1 -- npx -y canli-validation-mcp
# the same tools with signed, stored receipts from the free API (a free key is issued on first use)
claude mcp add canli -- npx -y canli-validation-mcp
# nothing to install: the hosted endpoint
claude mcp add --transport http canli https://canlicapital.com/mcp
Then ask, for example: "I tried 229 variants and kept the best: an annualised Sharpe of
1.5 over 730 daily returns (365 a year), skew -0.5, kurtosis 5, and the variants'
Sharpe ratios spread by 0.57. Is it real?" The assistant calls validate_deflated_sharpe: the
best of 229 skill-less variants would reach 1.60 by luck alone, so the probability that the
Sharpe is above zero falls from 98.1% to 44.4% once the search is counted. Claude Desktop and any
other stdio client (Cursor, VS Code) run the same npx command; see "Claude Desktop" and
"Generic stdio client" below.
What it does
An MCP (Model Context Protocol) server over canlicapital.com's free, keyed validation API, with a
lab that computes on your machine. It gives a coding agent twenty tools: issue a free key, run the
nine validators (deflated Sharpe, CSCV overfitting, data-snooping tests (Hansen's SPA, White's
Reality Check and Romano-Wolf StepM), paper-evidence conformance, breadth ceiling, minimum track
record length, minimum backtest length, haircut Sharpe ratio, luck-equivalent trials), audit one
backtest with several of them in a single call, verify a receipt's signature offline, read service
status, read a company's reported financial history from SEC filings, and six lab tools: backtest
a rule over a parameter grid, summarize a long series, stress-test a strategy's returns, check a
plan against a broker's limits, check a signal for lookahead, and test a whole research pipeline
on placebo data. Every validation result carries, beside the number, the sentences that say what
it does not establish and the receipt that records it, success or error, so the agent cannot see a
number without its limits.
This package is published to npm as canli-validation-mcp.
Also listed on the official MCP Registry (io.github.arhancanli/canli-validation-mcp) and
cursor.directory. Run it with npx, no install step, as shown below. A local checkout is only needed to develop or
test this package itself; see "Local checkout" near the bottom.
This README describes the version in package.json. Unversioned npx runs npm's latest
release; npx -y canli-validation-mcp@<version> pins one.
How well agents use it
A fixed benchmark gives models these tools and scores whether they pick the right one and return
the right answer, against ground truth computed from the same checked code. Results for three
models, with every run recorded, are in
bench/agent/README.md.
What the API is (and is not)
The engine is the product. The service runs your submitted numbers through the same honesty
arithmetic canlicapital.com's own paper record runs on itself and hands back a verdict anyone can
recompute from the receipt. It signs every receipt, does not accept market data, does not
grade a strategy, and never saw your data source, its costs, or any lookahead in how a series was
built. See docs/superpowers/specs/2026-09-05-developer-key-validation-api-design.md in the main
repository for the full design.
Tools
Unreleased source adds local journal binding to validate_paper_evidence: run
CANLI_LOCAL=1 node mcp/src/server.mjs from the repository, and send exactly one of record
or record_file, plus journal_file to check source hashes, signatures and recomputed claims.
record_file accepts a standard record, a {record,signature} envelope, or the full execution
export bundle; companion metrics/observations are checked for full bundles. Signed records
need the detached signature. bindings.all_match must be true for source validation to pass.
Without a journal, the result explicitly reports structure-only conformance. Files and
signatures are refused in hosted/remote mode and never uploaded. Local results have no Canli
receipt. The current npm/hosted release retains the existing record-only API.
Tool
Calls
Key required
get_key
POST /api/v1/keys (listed only when no key is configured)
no
validate_deflated_sharpe
POST /api/v1/validate/deflated-sharpe
yes
validate_overfitting
POST /api/v1/validate/overfitting
yes
validate_paper_evidence
POST /api/v1/validate/paper-evidence
yes
validate_breadth
POST /api/v1/validate/breadth
yes
validate_track_record
POST /api/v1/validate/track-record
yes
validate_backtest_length
POST /api/v1/validate/backtest-length
yes
validate_haircut_sharpe
POST /api/v1/validate/haircut-sharpe
yes
validate_luck_trials
POST /api/v1/validate/luck-trials
yes
validate_reality_check
POST /api/v1/validate/reality-check (a matrix_file is read on your machine and sent as numbers)
yes
audit_backtest
the deflated Sharpe and track record routes, then the headline: with variants, the overfitting and data-snooping routes; without, luck trials. One validation each
yes
verify_receipt
GET /api/v1/receipts/{id} when given an id (include_receipt returns the stored receipt); the checks run locally
no
service_status
GET /api/v1/validate/status
no
company_financial_history
GET /company-data/{cik}.json (a ticker resolves through GET /api/v1/company-tickers.json)
no
validate_deflated_sharpe accepts exactly one of two input shapes, never a mix of both:
or a return series plus the trials behind it: returns, periods_per_year,
effective_independent_trials, cross_trial_sharpe_sd_annualized.
Sending fields from both shapes, or from neither, is rejected before any request leaves the
process; see src/schemas.mjs.
validate_reality_check takes the returns of every variant the search tried, one row per period
and one column per variant, as matrix or as matrix_file (a CSV or JSON on your machine), and an
optional benchmark series; with none, variants are tested against zero. It runs three tests on one
seeded stationary bootstrap:
Hansen's SPA: the chance that the best variant's studentized excess return is this good if no
variant has an edge, with lower and upper bounds (spa.p_value, the consistent p-value, is the
headline, and monte_carlo_se its sampling error);
White's Reality Check: the same question without studentizing, so one volatile variant can
dominate it;
Romano and Wolf's StepM: which variants beat the benchmark, with the familywise error held at
alpha.
A result reproduces exactly from its seed. On three fixed-seed cases the p-values agree with
Python's arch 8.0 and with a numpy transcription of Hansen's formulas within Monte Carlo error, and
the StepM sets match arch's (js/snooping-core.test.js). Only the variants sent are counted: a
search that tried more than it sends makes luck look smaller than it was.
company_financial_history is different from the other tools: it reads the public company
reference at canlicapital.com, not the validation API. Give it a CIK (1 to 10 digits) to list a
company's available financial histories, or a CIK and a us-gaap concept such as Revenues to
get the observations, newest first (limit defaults to 40, maximum 200). Every observation
keeps its filing accession, form, filed date and unit, and the result carries the SHA-256 of the
original SEC response and the record's own boundary sentence: these are accounting values as
reported to the SEC, not market prices, returns or a recommendation. Companies and concepts
outside the current release return an error with the available concepts listed.
The lab: backtest, summarize, stress, feasibility, lookahead, placebos
Six tools compute in the server process itself, on your machine or on the hosted endpoint, with
nothing sent to the API and no receipt stored. Each result names the SHA-256 of exactly the numbers
it was computed from, and states what it does not establish.
backtest_strategy runs a rule over every parameter set in a grid, on your prices:
sma_cross (fast, slow), momentum (lookback, skip), mean_reversion (window, entry_z),
breakout (lookback) or buy_and_hold, long-only or with allow_short, after a cost per unit
of turnover. The position held over each period is decided from closes up to the one before it,
so no rule can see the future (a test changes later prices and checks that no earlier position
moves). It then validates the best variant with the number of variants this call ran, not a
number someone declared: the deflated Sharpe ratio across the grid's measured Sharpe dispersion,
the CSCV probability of backtest overfitting on every variant's returns, the probabilistic Sharpe
and the minimum track record, next to buy and hold over the same window. At most 200 variants and
20,000 prices per call. No code is ever run: a strategy is a family plus numbers.
summarize_series reads a long price or return series and states it in about a hundred words,
plus a field for every figure: growth, volatility, Sharpe and drift t-statistic, the worst
drawdown with its dates and recovery, trend, the volatility regime (terciles of its own 21-period
volatility), tails, jumps (beyond six robust standard deviations), autocorrelation, stale data and,
with a benchmark, correlation and beta. A five-year daily series is about 1,260 numbers; the
summary is a paragraph.
stress_test resamples a strategy's returns with a stationary block bootstrap (seeded, so a run
reproduces exactly) and reports how often the drawdown limit breaks and the Sharpe turns negative,
then applies four named scenarios with their rules stated: a crash at the equity high (default the
larger of 20% and three times the worst period), volatility multiplied, the worst stretch lived
twice, and positions stuck for several periods after the worst one. The fragility share is the part
of resampled histories that break the drawdown limit or lose money on a risk-adjusted basis.
Generative models are not used: trained on one series they memorize it or invent dynamics nobody
can check.
check_feasibility checks a plan against a real broker before it trades: the broker's published
order-rate limit against the rebalance's burst of orders (Alpaca 200 a minute, Interactive Brokers
about 50 a second), the minimum order (Alpaca 1 USD notional), US day-trading rules as they stand
in 2026 (FINRA retired the pattern day trader rule and its 25,000 USD minimum on 4 June 2026; firms
may phase in its replacement until 20 October 2027), T+1 settlement in cash accounts, each order's
share of daily volume, square-root market impact (coefficient Y, default 1) with every copy of the
same strategy counted, and the capital at which costs and impact eat the expected gross return.
Every broker fact carries the date it was checked and its source.
check_leakage finds lookahead in a signal without seeing its code. A causal signal computed
on the first n rows gives exactly the values the full run gives for those rows. plan picks the
cuts (seeded, between 40% and 95% of the series); you rerun your own code on the rows before each
cut, and compare checks every column: a value that changed when the later rows were removed
used them. On your machine, columns_file takes the path of a JSON file {cuts, columns} your
script wrote (Python's NaN reads as missing) instead of thousands of pasted numbers. The result names the pattern: future rows with their horizon (a shift(-5) reads as
five rows), the full sample (a normalization, rank, scaling or fit over the whole series), or
scattered rows (filling gaps backwards). On a fixed zoo of 22 pandas and statsmodels signals
(config/research/leakage-zoo.json, from scripts/research/leakage/), all 11 leaks are found
with the right pattern and none of the 11 honest indicators is flagged. A pass covers only the
cuts tested: survivorship, vendor revisions or a leak confined to the first rows leave no trace.
placebo_test tests a whole research pipeline at once. plan writes your panel (real.csv)
and 19 placebos to a new folder in the system's temporary directory: the same returns with the
periods in a random order, the same order for every asset, so each asset keeps its returns and each
day its cross-section, but nothing can be predicted. Run the pipeline unchanged on each, every
search, filter and parameter choice included, and compare ranks its real result among the
placebo results. Every choice the pipeline makes is counted, because it makes them again on every
placebo. A pre-registered study chose the reordering (scripts/research/placebo/v1/,
config/research/placebo-v1-evaluation.json): in all 30 combinations of simulated market and
pipeline where nothing can be predicted (10,000 tests each; five return shapes, flat,
drifting and with assets that differ only in their constant means; SMA, time-series and
cross-sectional momentum grids), its false-positive rate at a nominal 5% was 4.6% to 5.5%.
On the same markets a t-test on the best rule found an edge 10.9% to 78.9% of the time,
and Bonferroni's correction 8.0% to 10.3% wherever the market drifted and 58.5% to
59.7% where assets only differed in their means. With 19 placebos the smallest p is 0.05; 99
give a finer one.
On your own machine, prices_file, series_file and returns_file take a CSV or JSON path instead
of the numbers. Only numbers are read, plus an ISO date column when the file has one, so results can
name the day a drawdown began.
The trial ledger: count every variant an agent tries
An agent can try thousands of strategy variants in an hour, and the best of them looks skilled by
luck alone. How much luck depends on how many were tried and how spread out their results were, so
the honest verdict on a reported winner needs the whole search. The ledger keeps it.
Turn it on with CANLI_TOOLSETS=validate,lab,ledger (or all), then:
ledger_record_trial for every variant, as it is tried: its returns (or a returns file), or its
annualized Sharpe and sample size, plus a label and its parameters. The first call creates a
ledger and returns its name; reuse it. Each reply already counts the whole search so far.
ledger_summary for the verdict: the best trial, the Sharpe that the best of that many
skill-less trials reaches by luck, the best trial's deflated Sharpe ratio after counting every
trial, and the probability of backtest overfitting (CSCV) when every trial has aligned returns.
ledger_export to hand the search to someone else. Each trial is chained to the previous one
with SHA-256, so a removed, reordered or edited trial shows.
Ledgers are JSON-lines files under CANLI_LEDGER_DIR (default ~/.canli/ledgers); nothing leaves
your machine. Twenty variants of pure noise, recorded this way, give a best annualized Sharpe of
about 1.5, a luck-alone expectation of about 1.7 and a deflated Sharpe near 0.4: the ledger calls
that winner what it is. The statistics are the same checked code as validate_deflated_sharpe and
validate_overfitting; test/ledger.test.mjs checks both directions (noise is flagged, a real edge
among noise keeps a deflated Sharpe above 0.95).
Auditing a backtest in one call
audit_backtest takes one strategy's return series and, ideally, every variant's returns
(variants or variants_file, the dropped ones too); without them, the number of variants tried
and their Sharpe spread. The result opens with:
headline: one test chosen because its false-positive rate was measured before anyone looked at
your numbers. Null Zoo v1 (scripts/research/null-zoo/v1/ in the repository) drew hundreds of
thousands of simulated searches from nine return shapes with known ground truth; the bar and the
rule that picks the test were committed before the run. With variants the headline comes from
the data-snooping tests (validate_reality_check, with its receipt); without, from luck trials
with Lo's autocorrelation correction. Its sentence quotes the measured false-positive rate for
returns shaped like yours, and the worst across the nine shapes.
fix_next: at most five next steps, each {id, why, next} with a stable id.
computed: Lo's adjusted Sharpe, a stationary-bootstrap 95% interval for the Sharpe, the
haircut Sharpe, the minimum backtest length and, with variants, how many independent trials
they count as (Li and Ji) and the selected variant's out-of-sample decay.
Then the checks: validate_deflated_sharpe on the series (an estimate), validate_track_record
on the Sharpe, skew and kurtosis that check derived and, with variants, validate_overfitting.
Each is exactly what its own tool returns, with its own receipt; the deflated Sharpe runs with the
larger of the declared and counted trials and Sharpe spread, so a search cannot be made to look
smaller than it was. A check that refuses or cannot be reached is reported as that check's error.
The audit adds no grade of its own. Through the API it uses one validation per check.
When the server runs on your machine, returns_file and variants_file take the path of the
backtest's output instead of the numbers: a CSV (comma, semicolon or tab separated, with or without
a header; date and label columns are ignored, an unnamed or counting index column is skipped and
reported) or a JSON array. returns_column picks the column when there are several. Only the
numbers are read; the hosted endpoint refuses file paths. An agent copying a long series into a
call can drop values, and the copy costs tokens; in our agent benchmark, reading the file instead
took three audit questions from 4 of 9 to 8 of 9 answered correctly on gpt-5.4-mini.
Prompts, resources and structured results
Clients that show MCP prompts offer eight guided workflows: validate_backtest (deflated Sharpe, then
overfitting, then the track record needed, reported with what each number does not establish),
track_record_needed, backtest_and_validate, stress_my_strategy, production_check,
summarize_market_series, check_signal_for_lookahead and test_pipeline_on_placebos. Resources: canli://limits, the boundary sentences every result carries;
canli://sources, the papers behind each validator and how each is checked against them;
canli://strategy-spec, the JSON Schema of every strategy family and its parameters; and
canli://openapi, the API's OpenAPI 3.1 document. Two resource templates are for writing code
against the tools: canli://schemas/{tool} returns a tool's exact input and output JSON Schema, and
canli://examples/{language}/{tool} a working call in python, javascript or curl (both
variables complete). A test compiles every generated Python and JavaScript example and checks that
every example's arguments are valid input. Every tool result carries its envelope both as text and
as structuredContent.
Compact context (0.3.0)
An agent pays for every token a tool returns, including whitespace it never reads. Since 0.3.0
every result is minified JSON, and a company history returns its observations as one columns
header and one row per observation, with a unit shared by every row stated once. No field is
dropped: every boundary sentence, accession number, form, filed date and source hash is still in
the result, and columns + rows rebuild each observation exactly.
Measured with bench/token_cost.py on live records (tokenizer: tiktoken o200k_base; other
tokenizers give different absolute counts), 20 observations each:
Record
0.2.0 (indented)
minified
0.3.0 (minified, columnar)
Apple, StockholdersEquity
2,214
1,560 (−29.5%)
1,060 (−52.1%)
Microsoft, CashAndCashEquivalentsAtCarryingValue
2,231
1,574 (−29.4%)
1,065 (−52.3%)
The validation tools gain the minification only: the live service_status envelope measured 593
tokens indented and 475 minified (−19.9%); their boundary sentences are kept word for word. These
are measurements of these results, not a claim about any other server.
Breaking change from 0.2.0:history.observations is now {unit?, columns, rows} instead of
an array of objects.
Configuration
Variable
Default
Meaning
CANLI_API_BASE
https://canlicapital.com
Where the API lives. Point it at a preview deployment for testing.
CANLI_KEY
unset
A key already issued from POST /api/v1/keys. When set, get_key sends no request and reports the key is already configured; every other tool sends it as Authorization: Bearer <key>.
CANLI_FULL_ENVELOPE
unset
1 or true returns each validation's full API envelope instead of the compact result (below).
CANLI_TOOLSETS
all
Which tools to list: a comma-separated choice of validate, receipts, company, status and lab, or all. An unknown name is refused at startup. See "Toolsets" below.
CANLI_LOCAL
unset
1 or true runs the eight validators on this machine (private local mode, below): no key, no network, no receipt.
If CANLI_KEY is not set and local mode is off, call get_key once per session before the validators. The key it
returns lives only in this process's memory for the life of the session; it is not written to
disk.
HTTP failures and API error envelopes are marked as MCP tool errors while preserving
the complete JSON envelope. A successful validation with a negative verdict remains
a normal result. Requests have a 30-second deadline covering headers and body, reject
redirects, and are never retried automatically. A timeout may occur after the service
has processed a request; check service status before deciding to submit again.
Non-JSON response bodies and raw network errors are omitted from tool errors.
Install
No install step. npx fetches the published package on first run, so every client config below
just spawns npx -y canli-validation-mcp. See "Local checkout" near the bottom to develop or test
this package itself instead of running the published one.
Hosted endpoint (no install)
The same tools are served at https://canlicapital.com/mcp over MCP Streamable HTTP, for clients
that connect to a URL instead of spawning a process (Claude.ai connectors, ChatGPT, Cursor's remote
servers). Nothing to install, and no Node.js on your machine.
bash
claude mcp add --transport http canli https://canlicapital.com/mcp
Without a key, requests run under a shared anonymous key, so the daily validation quota is shared
by every hosted caller. When that shared quota is used up for the day, validations are still
answered, computed by the same code on the hosted endpoint, but without a stored receipt; the
result says so. For your own quota, issue a free key (see
/developers) and send it as a header:
Add ?toolsets= to the URL to list only some tools (see "Toolsets" below), for example
https://canlicapital.com/mcp?toolsets=company.
The endpoint is stateless. On it, get_key issues nothing and says which key is in use, because a
key issued there would not reach the next request. A malformed Authorization header is refused
rather than replaced with the shared key.
Claude Desktop
Add to claude_desktop_config.json (Settings, Developer, Edit Config):
Restart Claude Desktop afterward. Add an "env" object with CANLI_API_BASE to point this at a
preview deployment instead of the default.
Claude Code
bash
claude mcp add canli -- npx -y canli-validation-mcp
Run claude mcp list to confirm it is registered, and claude mcp remove canli to remove it.
Private local mode
Set CANLI_LOCAL=1 and the eight validators run on your machine: nothing about the series you
submit is sent to canlicapital.com, no key is needed, and no receipt is stored. The computation is
the API's own, shipped byte for byte in src/local (a test fails if it drifts), so a local result
equals the hosted one; it names no receipt id because none was made.
bash
claude mcp add canli-local --env CANLI_LOCAL=1 -- npx -y canli-validation-mcp
verify_receipt by id, service_status and company_financial_history still read from canlicapital.com;
they send no series. In the Claude Desktop extension this is the "Private local mode" setting.
Generic stdio client
Any MCP client that can spawn a process and speak stdio will work. Using the official SDK
directly, from Node:
js
import { Client } from"@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from"@modelcontextprotocol/sdk/client/stdio.js";
const transport = newStdioClientTransport({
command: "npx",
args: ["-y", "canli-validation-mcp"],
env: { ...process.env, CANLI_API_BASE: "https://canlicapital.com" },
});
const client = newClient({ name: "my-agent", version: "0.1.0" });
await client.connect(transport);
const { tools } = await client.listTools();
console.log(tools.map((t) => t.name));
const keyResult = await client.callTool({ name: "get_key", arguments: { label: "my-agent" } });
if (keyResult.isError) thrownewError("Key setup failed; inspect the error privately.");
// The session retains the issued key. Avoid printing its envelope into logs.const result = await client.callTool({
name: "validate_deflated_sharpe",
arguments: {
returns: [0.004, -0.002, 0.007, 0.001, -0.003, 0.005, 0.002, -0.001],
periods_per_year: 252,
effective_independent_trials: 30,
cross_trial_sharpe_sd_annualized: 0.5,
},
});
console.log(result.content[0].text); // the answer, its limits and its receiptawait client.close();
What a result does not establish (boundary language)
Every envelope this server returns carries these sentences, verbatim, from the API itself
(api/_lib/limits.js):
This verdict is about the series exactly as submitted. The service never saw the data source,
its costs, survivorship, or any lookahead in how the series was built.
A deflated Sharpe or overfitting probability above or below any threshold is not admission to
anything and is not a forecast.
Quotas: 1000 validations per key per UTC day, 5 keys per client per UTC day, 1048576 bytes per
validation request, 1024 bytes per key revocation request, 20000 observations per series, 200
variants per matrix.
Each tool's description also states one of these sentences, so an agent sees the boundary before
it calls the tool, not only after.
Compact results (tokens)
A validation result is the answer (data), the sentences above except the quota line, and the
receipt's id and URL; an error keeps its error. The rest of the API envelope (schema, endpoint,
timestamps, claim and capital class, the human page, the source-file hashes and the quota line)
describes the service rather than the answer, and an agent pays for every token of it on every
call. It stays in the stored receipt, which verify_receipt returns in full with include_receipt, and in service_status.
On a breadth result this is about half the text. Set CANLI_FULL_ENVELOPE=1 to receive every field.
Toolsets (tokens)
A client sends the model the whole tool list on every turn, and it is most of each turn's prompt:
a validation result is a few hundred tokens, the list of every tool several thousand. A client
that needs one kind of tool can list only that kind, with CANLI_TOOLSETS (stdio) or
?toolsets= (hosted endpoint). The default is every toolset except company, whose one tool
canli-fundamentals-mcp does better and point in time, and ledger, which keeps files on your machine
and is local only; all adds both. get_key is listed only when
no key is configured.
Measured with bench/tool_list_tokens.py (tokenizer: tiktoken o200k_base; other models'
tokenizers give different absolute counts), in the shape an OpenAI-style client sends the list:
CANLI_TOOLSETS
tools
tokens per turn
of all
(default)
19
5,978
85%
(default, with CANLI_KEY)
18
5,842
83%
all
23
7,019
100%
validate
11
3,658
52%
receipts
1
182
3%
company
1
273
4%
status
1
89
1%
lab
6
2,055
29%
ledger
3
772
11%
Providers cache a tool list that is identical from turn to turn and bill the cached part at a
fraction of the price (test/tool-list-stable.test.mjs keeps each list byte-stable); a smaller list
costs less either way.
Local checkout
Only needed to develop or test this package itself, not to run the published one.
bash
cd mcp
npm ci
node src/server.mjs
Point a client at the checkout instead of npm by spawning node /absolute/path/to/meridian/mcp/src/server.mjs
in place of npx -y canli-validation-mcp in any config above.
Testing
bash
npm test
npm run test:package
Runs node --test over test/*.test.mjs: schema round-trips against the API's own OpenAPI and
manifest examples, one success, error-envelope and quota-429 case per keyed tool (a fake fetch
stands in for the network), a check that every tool description carries a limits sentence, a
check that those sentences have not drifted from api/_lib/limits.js, a check that no shipped
file contains an em dash, and one test that spawns the actual server binary and performs a real
MCP tools/list and callTool handshake over stdio against a local HTTP stub, so the wiring is
proven rather than assumed.
test:package creates the actual npm tarball, checks its exact file list and license,
installs it in a temporary consumer directory, and runs the stdio test against that
installed entry. Dependency installation contacts npm; tool calls use only the local
HTTP stub. It neither publishes a package nor issues a production API key.
Dependencies
Only @modelcontextprotocol/server (pinned exact; the MCP SDK's server package, which itself
depends only on @modelcontextprotocol/core and zod) and zod (pinned exact): four packages in
the installed tree, where 0.7 and earlier pulled in about 95 through @modelcontextprotocol/sdk.
No other runtime dependency is added, and nothing in this package touches the site's root package.json,
.vercelignore, api/, scripts/, js/, or public/.