Diagnostics¶
Three things answer "what went wrong, and what is going on?" — for a person at a terminal and for an agent calling RiskPY's tools.
| question | person | Python | agent (MCP) |
|---|---|---|---|
| Is this installation healthy? | riskpy-doctor |
riskpy.doctor() |
riskpy_doctor |
| What does this error mean? | riskpy-explain RP105 |
riskpy.errors.explain("RP105") |
riskpy_explain |
| What has been called, and what failed? | riskpy-tools trace |
riskpy.tools.trace() |
riskpy_status |
riskpy-doctor¶
riskpy-doctor # a table: each check ok / warn / fail, with the fix
riskpy-doctor --json # {"ok", "version", "summary", "checks": [{id, status, message, hint}, ...]}
riskpy-doctor — RiskPY 0.5.2
ok python Python 3.12.13 (CPython) at /…/bin/python
ok riskpy_version open-riskpy 0.5.2 (riskpy at /…/site-packages/riskpy)
ok core_import compiled core loaded from /…/riskpy/cpp_underwriter.cpython-312-darwin.so
ok core_compute the core simulates correctly (Poisson mean 3.0064 ≈ 3)
ok core_hardening the core refuses non-finite parameters (0.4.1+ build)
ok extra_numpy numpy 2.5.3
warn extra_pandas pandas is not installed — needed for the .to_frame() methods
→ pip install pandas
…
ok selfcheck 68 identities verified in 0.16s (core, special, quant, life, rates, credit)
ok tools 27 of 27 calculators available
ok mcp_server the MCP server builds and lists 30 tools; command /…/bin/riskpy-mcp
ok platform Darwin 25.1.0 arm64
14 ok, 1 warn, 0 fail — works, with warnings
| check | what it looks at |
|---|---|
python |
the interpreter; RiskPY needs 3.10+ and ships wheels for 3.10–3.14 |
riskpy_version |
the installed open-riskpy distribution, and whether the riskpy being imported is that installation — a source checkout on sys.path shadowing it is the commonest confusion |
core_import |
the compiled C++ core imports, from the same directory as the Python layer |
core_compute |
the core answers a known question (a Poisson count's mean) correctly |
core_hardening |
the core refuses NaN parameters, which only 0.4.1+ builds do — an old extension under a new Python layer shows up here |
extra_* |
NumPy, SciPy, pandas, Matplotlib, the MCP SDK, Tkinter: installed or not, with the exact pip line |
selfcheck |
a subset of riskpy-verify (core, special functions, quant, life, rates, credit) |
tools |
the agent tools load; which need an extra |
mcp_server |
the MCP server builds, how many tools it lists, and where riskpy-mcp is |
platform |
the OS and architecture, for bug reports |
A missing optional extra is a warn; a broken core, a failing identity or an MCP server that
will not build is a fail, and the command exits 1 — so riskpy-doctor can gate a CI job or a
deployment.
Error codes¶
Every deliberate error has a stable code (RP105), the parameter at fault and a hint. The full
list, with meanings and fixes, is on the error codes page; riskpy-explain CODE
prints one entry.
Over the tools, a failed call is data, not a traceback:
from riskpy import tools
tools.call("black_scholes", {"S": 100, "K": 100, "T": 1, "r": 0.05, "sigma": 20}, errors="return")
# {"error": {"code": "RP105", "param": "sigma",
# "message": "sigma = 20 looks like a percentage: volatility is a decimal, so sigma must be at most 5 (500%)",
# "hint": "Divide by 100 — 20% is 0.2.", "docs": "…/errors/#rp105"}}
riskpy-tools call prints the same object on stdout (exit status 1) and a one-line summary on
stderr; riskpy-mcp returns it as the text of a tool error. The default errors="raise" raises
the error instead, as the same ValueError / TypeError / KeyError earlier versions raised.
Percentages where decimals belong¶
The library computes with whatever numbers it is given: black_scholes(r=5) is a 500% rate and
returns a price. An agent that meant 5% gets a confident wrong answer. The tools therefore
refuse values no market quotes (riskpy.tools.PLAUSIBLE):
| parameters | refused when | code |
|---|---|---|
r, i, q, ytm, coupon, rate, spread, theta, par_rates |
above 1 (100%) in absolute value | RP104 |
sigma |
negative, or above 5 (500% a year) | RP105 |
lgd |
outside [0, 1] | RP103 |
expected_loss_ratio |
above 5 (500%) | RP104 |
The library functions underneath are unchanged.
The trace¶
The tool layer remembers its recent calls — tool, arguments (long lists abbreviated), duration, and the error code if the call failed:
tools.trace() # the last 200 calls in this process, oldest first
tools.status() # version, tools, limits, call and error counts by code, recent calls
riskpy-tools trace # this process's calls, plus RISKPY_TRACE_FILE if set
RISKPY_TRACE_FILE=~/riskpy-trace.jsonl riskpy-tools call black_scholes '{…}'
riskpy-tools trace --json --limit 20
The buffer lives in the process that made the calls, so over MCP the agent asks the server:
riskpy_status returns the counts and the recent calls, each with via: "mcp". Set
RISKPY_TRACE_FILE to append every call to a JSON-lines file as well, which is how separate
riskpy-tools call processes share one trace.
Explaining a tool¶
riskpy_explain takes an error code or a tool name. For a tool it returns the schema plus the
formula and conventions it uses — for black_scholes, that r and q are continuously
compounded and T = 0 gives the discounted intrinsic value. black_scholes and
bond_analytics also take explain: true and add the working to the result (for Black–Scholes,
d1, d2, N(d1), N(d2) and the discounted spot and strike).
Measured¶
benchmarks/diagnostics/ sends 19 realistic agent mistakes — a negative volatility, r=5, a
maturity of 0, a probability above 1, a misspelt tool, vol for sigma, a number sent as a
string, an impossible option price, a missing extra — over MCP and the CLI, and scores each reply
for a code, the parameter, a hint and structured JSON: 3 of 76 points for 0.4.0, 13 for the
unpublished 0.4.1, 76 for 0.5.0. Six of the nineteen mistakes returned a number before 0.5.0 —
a confident wrong answer. The table, the replies the agent actually saw and the caveats are in
benchmarks/diagnostics/RESULTS.md.