Skip to main content
Glama
simoneb

ibkr-risk-mcp

by simoneb

ibkr-risk-mcp

MCP server exposing Interactive Brokers' portfolio risk: IB's model greeks, IB's implied volatility surface, IB's what-if margin, and a local stress engine that rebuilds the P&L-versus-underlying curve and finds its trough.

It deliberately does not duplicate the official IBKR connector. Positions, balances, orders, trades, performance, allocation, spot and historical prices, option chains, watchlists and alerts all come from there. This server exists to fill the one gap that connector leaves — risk analysis — and nothing else.

The questions it is built to answer:

  • where is the trough of the portfolio's P&L curve across underlying shocks, at constant volatility?

  • and how much of that answer is the constant-volatility assumption itself?

  • if I add N puts at strike K expiring E, where does that trough move to?

  • how much margin does this hypothetical structure need, now and under stress?

  • what does IB's volatility surface look like for this underlying?

Risk Navigator's risk model is not exposed by any API, so the strategy is to pull the inputs from IB — per-contract implied volatility, model greeks, what-if margin — and rebuild the curves locally, rather than trying to read Risk Navigator itself.

Prerequisites

  1. TWS or IB Gateway running and logged in on this machine. The server talks to its local socket; it never reaches IBKR over the internet.

  2. The API enabled. File → Global Configuration → API → Settings → tick Enable ActiveX and Socket Clients. Until you do, TWS opens no port at all and check_connection reports not_listening.

  3. Market data for the instruments you hold. Model greeks come from IB's own option model, so a contract the account cannot price has no greeks and is reported under missing. Contrary to what is widely repeated, delayed data does carry model greeks — verified against live TWS, where an unsubscribed account got nothing from market data types 1 and 2 and implied volatilities from type 3. If you lack the subscription, set IBKR_MARKET_DATA_TYPE=3 and read the numbers as a quarter of an hour old.

  4. For whatif_order only: Read-Only API must be off in that same TWS screen. That setting blocks what-if orders too.

Default ports: 7496 TWS live, 7497 TWS paper, 4001 Gateway live, 4002 Gateway paper. If the configured one is dead, check_connection scans all four and tells you which is answering.

Related MCP server: ibkr-mcp-server

Install

{
  "mcpServers": {
    "ibkr-risk": {
      "command": "uvx",
      "args": [
        "--from",
        "https://github.com/simoneb/ibkr-risk-mcp/archive/refs/tags/<TAG>.tar.gz",
        "ibkr-risk-mcp"
      ],
      "env": {
        "IBKR_PORT": "7496",
        "IBKR_CLIENT_ID": "17",
        "IBKR_ENABLE_WHATIF": "false"
      }
    }
  }
}

Client

File

Claude Desktop

%APPDATA%\Claude\claude_desktop_config.json, or ~/Library/Application Support/Claude/

Claude Code

claude mcp add

Cursor

~/.cursor/mcp.json, or .cursor/mcp.json per project

VS Code

.vscode/mcp.json, keyed under servers with "type": "stdio"

Codex CLI

~/.codex/config.toml

Pin a tag rather than a branch, and prefer the archive URL over git+https://… — the git form needs git on PATH, and some clients hand the server too small an environment to find one.

For a local checkout:

uv venv && uv pip install -e ".[dev]"
uv run python -m ibkr_risk_mcp.server

As a Claude Desktop extension, manifest.json surfaces host, port, client id, account, market data type, risk-free rate and the what-if gate as a settings form, so none of them need editing by hand.

Environment variables

Variable

Default

IBKR_HOST

127.0.0.1

Where TWS listens. A remote host must also be in the API's Trusted IPs

IBKR_PORT

7496

7496/7497 TWS live/paper, 4001/4002 Gateway live/paper

IBKR_CLIENT_ID

17

Must differ from every other script on this TWS. Never 0 — TWS reserves that for orders placed by hand

IBKR_ACCOUNT

Required on a multi-account login. Without it every position/account tool refuses rather than combining accounts

IBKR_MARKET_DATA_TYPE

1

1 live, 2 frozen, 3 delayed, 4 delayed-frozen. 3 works and does carry greeks

IBKR_WHATIF_TIMEOUT

5

Seconds to wait for a what-if reply. IB sometimes never sends one

IBKR_ENABLE_WHATIF

false

The gate on whatif_order. Also decides whether the connection itself is opened read-only

IBKR_RISK_FREE_RATE

0.04

Used to discount and to carry spot to the forward in the local repricing

IBKR_GREEKS_TIMEOUT

4

Seconds to wait for greeks on one contract. Short by design — IB answers fast or never, and explicit refusals cut the wait short anyway

IBKR_MAX_MKT_DATA_LINES

40

Concurrent market data subscriptions. IB allows about 50

IBKR_CONNECT_TIMEOUT

6

Seconds for the API handshake

IBKR_CALIBRATION_FILE

~/.ibkr-risk-mcp/vol_coord.json

Where calibrate_vol_coord stores the fitted vol_coord_decay. The only file this server writes

Copy .env.example to .env for local runs.

Tools

Tool

check_connection

Is TWS reachable, and if not, which of the four failure modes it is

get_margin_summary

NetLiq, margin requirements, available funds and excess liquidity — per segment

get_position_greeks

IB's model greeks for every option position, with both expiry dates

get_vol_surface

IB's implied volatility grid for an underlying, by expiry and strike

stress_portfolio

The P&L curve across underlying shocks, and its trough

stress_curve

The same curve under several volatility regimes and valuation dates at once — risk-graph data, with the vol assumption as a visible parameter

stress_whatif

The same curve with hypothetical legs added: base, with-legs, and the difference

calibrate_vol_coord

Refit the volatility-coordinated model against your own Risk Navigator, and keep the fit

whatif_order

IB's margin impact of a structure, per leg and cumulatively. Needs the gate below

Each tool carries the protocol's annotations, so a client can group them by permission. Seven are marked read-only. whatif_order is not, because it puts something on IB's order channel even though nothing is routable; calibrate_vol_coord is not either, because it writes the fit to disk — neither of them changes anything in the account.

Reading the curve: by symbol, by expiry, at another date

Every stress result carries the P&L broken down per shock. By default that breakdown is keyed on the symbol, which is the right unit for a book of many underlyings and the wrong one for a book running one underlying across many expiries: nine ES expiries all land under a single ES key, and the operative question — which expiry is holding the trough down, and which short do I buy back — has to be reconstructed by hand from the position list.

breakdown changes the key. expiry groups on the option's settlement date (ES 2026-10-30), so a quarterly and a weekly that settle the same morning are one row rather than two names for one expiry. Positions with no expiry get a key naming their class — ES (future), AAPL (equity) — so the breakdown still sums to the point's total and can be checked against it rather than trusted. both returns symbol and expiry, none neither, and symbol remains the default: the responses are already large, and a second dictionary at every one of twenty-six shocks is not free.

With an expiry breakdown the result also carries troughByExpiry, two columns per expiry that answer two different questions:

  • pnl — that expiry's own worst point along the curve, which is what it can cost.

  • pnlAtPortfolioTrough — what it contributes at the shock where the account's floor actually sits, which is what says whether closing it would move that floor.

They come apart, and the gap is the useful part. An expiry whose own minimum sits at −35% while the book troughs at −22% is not the one to buy back, and reading only the first column would nominate it.

Valuation dates. date_offset_days rolls the clock forward; valuation_date takes the ISO date instead, so "the curve at 30 September" does not have to be counted out by hand over a weekend. There is no calendar adjustment — the date is the date — and both forms mean the same thing: the P&L is still measured from today, at today's spot and today's implied volatilities, with time advanced. That is decay and the change in convexity that comes with it, not a forecast of where the market will be.

stress_curve takes a family of them, date_offsets: [0, 3] or valuation_dates: [...], and crosses them with the volatility scenarios. That is not a convenience: comparing today against Monday used to take two calls, and the book and the market moved between them, so part of the difference between the two curves was not the three days at all. One call, one loading of positions and prices, and time is the only thing that changed. Each entry under curves carries its own valuationDate and dateOffsetDays; name stays the scenario's and label distinguishes the pair.

The what-if gate

whatif_order works only with IBKR_ENABLE_WHATIF=true. Otherwise it sends nothing and returns success: false with blocked: true.

An order carrying whatIf=True is evaluated by IB's margin engine and discarded — never routed, never acknowledged as live, never in the order book. The gate exists anyway, for two reasons. It is the only thing in this server that touches the order path at all, so being read-only should be provable rather than asserted; and with the gate closed the ib_async connection itself is opened in read-only mode, which makes an order impossible below this server as well as inside it.

No tool here can submit a live order. Those belong somewhere else.

The traps this server handles

These are not hypothetical; each one produces a confidently wrong number if you skip it.

Expiry dates are reported twice, and AM settlement does not always move the date. For AM-settled contracts — the quarterly ES options, trading class ES — some TWS builds report lastTradeDateOrContractMonth as the day before settlement, so an 18 December expiry shows as the 17th while a PM weekly expiring the same morning shows as the 18th; time to expiry taken from the last trading day is then a day short.

But that is not universal, and assuming it is would introduce the error it was meant to prevent. Measured on TWS server 178: the December ES quarterly reports 20261218 — the correct date — with the AM settlement visible only in lastTradeTime='08:30:00' against the weekly's 15:00:00. IB's own ContractDetails.realExpirationDate exists for exactly this ambiguity and is preferred wherever the details have been fetched; the class-and-date heuristic is only the fallback. Every tool returns both lastTradeDate and settlementDate plus an amSettled flag, and everything downstream uses settlement.

The underlying is not implied by the expiry — and two options expiring the same morning need not share one. Confirmed against live TWS: the 18 December 2026 ES quarterly is written on ESZ6, while the EW3 weekly expiring that same day is written on ESH7. The 30 September end-of-month options are on ESZ6, not ESU6. underConId comes from the contract details and is never inferred from a date.

Portfolio contracts arrive unqualified. The contracts on IB's PortfolioItem come back with exchange empty, and reqMktData on a contract without an exchange returns nothing at all — no ticks, no error. This is why a live book returned no model greeks at all for any option position while a surface request on the same underlying worked perfectly: those contracts had come from reqContractDetails already qualified. Every position is requalified before its data is requested.

impliedVol and undPrice arrive in separate ticks. Taking the greeks the moment a volatility appears leaves the underlying unset a good fraction of the time — a sizeable minority of positions on a measured live run — and without a forward there is nothing to reprice against, so the position silently drops out of the curve. Both fields are waited for, and a missing forward is backfilled from another position on the same underlying before it is fetched again.

A refusal is an answer; don't wait it out. IB replies to a market data request in about a second or not at all, and when it is "not at all" it usually says so at once with error 354 or 10091. Those are watched for and end the wait immediately. Without that, an unentitled book pays the full timeout on every contract — the difference between a check taking 30 seconds and taking minutes.

Where IB won't publish greeks, the price is already in the portfolio. Error 10091 — "requires additional subscription for API" — refuses the option's greeks and a quote on its underlying, so the obvious fallback of implying a volatility locally dies for want of a spot price. But a book holding GOOGL stock next to a GOOGL option already knows what GOOGL is worth: every position carries its own mark on the portfolio update, which arrives over the account channel and is gated by no market data entitlement at all. Underlying prices are taken from the account's own positions before a market data line is ever spent. Measured on a live account, this is the difference between two long puts priced and the same two held flat across every shock — worth 2,826 at a 15% fall and 4,414 at 20%, on a 138k account. A long option contributes a multiple of its premium under shock, so judging it by its market value understates it badly.

reqCalcImpliedVolatility can take the connection down, so it is opt-in. Asking IB to run its own American-exercise model on prices you supply is the better answer than implying a European volatility locally — when it works. Measured against live TWS: ib_async's request is answered with error 320, "Error reading request. Please use 'Key=Value' format for Misc Options", and TWS then closes the API connection. A protocol error halfway through a portfolio load costs the whole load, which is far worse than the one contract it was trying to rescue. It is behind IBKR_USE_IB_IMPLIED_VOL, off by default, and disables itself for the rest of the process after a single failure. The local implication is stamped with its source and never passed off as IB's own either way.

A bare root is ambiguous. ES is the E-mini S&P 500 future and Eversource Energy on NYSE. With no sec_type the stock wins, and you get a plausible-looking volatility surface with 75-dollar strikes. Every resolution reports what else the symbol matched, and get_vol_surface returns the contract it actually used.

IB requires transmit=True on a what-if order. With transmit=False — the intuitive choice for something meant not to trade — TWS rejects it with error 321 and, because it rejects rather than answers, the call never returns at all. whatIf=True is what keeps the order off the market; transmit has nothing to do with it. Every what-if here is also bounded by a timeout, because IB not answering is an ordinary outcome.

The volatility level does not move with the shock unless you make it. Both vol modes decide which volatility a strike gets, not how high the surface sits: sticky_strike pins each strike to the volatility it holds today, sticky_moneyness slides a strike along the smile the portfolio already has. Neither raises the level, and vol_bump cannot either — it is flat along the shock axis by construction. So the default curve prices the move in the underlying and none of the move in volatility that comes with it, which for a net short option book is the optimistic half of the answer and can be the larger half. vol_slope_down puts it in: 1.0 adds one volatility point per 1% fall, so a −20% shock reprices at +20 points. It is applied as a parallel shift across every tenor — a real surface also steepens in a sell-off, and a 120-day volatility moves less than the front month — so it is an input to be chosen and stated, in the same class as bond_duration_years. Expect it to change the shape of the curve and not only its depth: on the measured fixture it deepens the −10% and −20% points while lifting the −30% tail, because the long wings pick up vega faster than the short body does.

Volatility has a term structure. ES at 139 days can sit near 15% at the money while the front month prints 12%. Using one ATM number for every tenor understates a long-dated position badly. The surface interpolates per tenor, in total variance rather than in volatility, which is also what keeps it free of calendar arbitrage.

Bonds are quoted as a percentage of nominal. 50,000 nominal at 97.85 is worth 48,925, not 4,892,500 — quantity × price is wrong by 100×. Position values come from IB's own marketValue, and the reconciliation below is what proves it worked.

Futures have no market value to add. Variation margin settles daily, so a future contributes its unrealised P&L to NetLiquidation, not its notional. Adding the notional instead moves the total by a quarter of a typical account.

Two accounts must never be added together. ib.portfolio("") and ib.accountValues("") do not mean "the default account", they mean all of them. On a multi-account login with no IBKR_ACCOUNT set, that would combine two portfolios and reconcile the total against one account's NetLiquidation — a wrong answer wearing the shape of a right one. Every tool that reads positions or account values refuses until an account is chosen, and names the candidates. Related: IB permits only one reqAccountUpdates subscription at a time, so asking about a second account silently cancels the first; only the resolved account is ever subscribed.

Reconciliation. stress_portfolio rebuilds the portfolio at zero shock — cash, plus the securities' market value, plus the futures' unrealised P&L — and compares it against NetLiquidation. A residual over 1% returns reconciled: false with the residual attached. Nothing derived from a portfolio that does not reconcile should be presented as fact.

A minimum at the edge of the range is not a trough. If the curve is still falling at −30% the engine says so, rather than reporting the boundary as the worst case. Some portfolios simply keep losing past the edge of the window.

Known limitations

Risk Navigator's volatility shock model is not public. It is not exposed by any API and IB does not document it. sticky_strike — each strike keeps its current implied volatility — is the approximation corresponding to Risk Navigator's default blue curve. Expect the shape to match and the last few percent not to. Do not present a number from this server as "what Risk Navigator says"; it is what a documented model, fed IB's own volatilities, says.

One shock, all underlyings — and only the equity ones. Every underlying in scope is moved by the same percentage at once, which is Risk Navigator's own default assumption. There is no correlation matrix and no per-underlying scenario.

Because of that, scope defaults to equity: only equity underlyings are on the axis, and FX, rates and the rest are excluded outright and listed under excluded with their market value. This is not a refinement, it is what makes the number mean anything — one percentage applied to every underlying at once is nonsense off the equity axis, where a 20% shock on a currency future prices an exchange rate that has never traded there. On a live account a single CAD strangle was contributing −21,716 at −20% and −7,183 at +10%, against −29,027 and +2,408 for an entire ES campaign: it dominated both tails of a curve that was supposed to be about equities. TWS Risk Navigator draws the same line in its Equity tab, and once it is drawn here too the two curves agree to 34 dollars on 29,000 at a 15% fall. scope='all' restores the old behaviour.

The classification is a table plus one heuristic — a three-letter currency code on a FUT or FOP is IB's own naming for a currency future — not a deduction, because IB publishes no reliable asset class for futures and a bond or gold ETF quoted as STK lands in equity with no field to say otherwise. So the group is reported on every position and risk_groups overrides it per symbol.

betas scales that shock per symbol, and it reaches every class that responds to one — an option is repriced at its own beta-scaled move of its underlying, with strike, smile and convexity all measured at the forward it would actually reach, rather than having its P&L scaled after the fact. Keys are tried most specific first: local symbol (ESZ6 P5800), then root (ES), then underlying (ESZ6).

This is what lets a foreign underlying be stood down off an equity axis — a short EUR strangle is not a 20%-down position when the S&P falls 20%, and a single shock across every underlying says it is. But read what a beta does and does not do. It scales the underlying's move only: vega and theta are untouched, so a position at beta 0 still contributes P&L the moment vol_bump or date_offset_days is set, and pnl_by_symbol is the only clean exclusion. More importantly, standing a position down is not measuring it — an attenuated strangle carries its whole gap risk and none of that risk is anywhere on the curve. Every run that applies a beta other than 1 says so in warnings.

Volatility surface interpolation is local. Under sticky_moneyness the surface is built from the strikes the portfolio actually holds — one skew per expiry that has at least three of them, assembled into a surface and interpolated across tenors in total variance. An expiry too thin to define its own slope borrows its shape from the tenors that do; an underlying where no expiry defines one falls back to sticky_strike rather than being handed an invented flat smile. Either way it is said in warnings. fetch_skew=true pulls neighbouring strikes from IB instead, at the cost of more market data requests.

The surface supplies the change in volatility as a strike slides to new moneyness, not the level. The level stays IB's own per-contract implied volatility, which comes out of a model that prices American exercise and is a better number than any fit through it. Reading the level off the surface would also break the curve's zero: a strike whose shape was borrowed from another tenor would not get its own volatility back at zero shock. Every result carries volSurfaceUsed, the quotes the repricing actually read — if it is empty under sticky_moneyness, no smile was built, every option silently fell back to sticky_strike, and the result is not the model you asked for.

The volatility response is IB's own model, and both curves are validated against Risk Navigator. Risk Navigator draws two lines: a constant-volatility curve, and one it labels Vol.Coord. where volatility moves as a deterministic function of the price shock. IB documents that second model's shape — the nominal shock is -X on a rise and -10X on a fall, applied relatively rather than in points, then damped across tenors by a response function VR(t) that is 1 at zero and decreasing. vol_coord implements it. Measured against a live index ratio book, on a −30% to 0% axis:

RMS against Risk Navigator, as a fraction of trough depth

const vs its blue curve

~2%

vol_coord vs its Vol.Coord. curve

~3.5%

with the residual at every shock inside the error of reading the targets off a chart by eye. VR(t) itself is not published: vol_coord_decay is fitted here, exp(-4.736 t), on one book from nine points read off a chart by eye. Two things follow, and the engine says both out loud rather than leaving them in the docs.

Every vol_coord curve running on the shipped decay says so in warnings. And the fit was constrained only out to 0.345 years, because that is all the book it came from held; past there an exponential does not merely lose accuracy, it decays to nothing. At one year VR is 0.009, so this model would reprice a LEAPS as though a 20% crash barely touched its volatility. Any position beyond vol_coord_calibrated_to_years is priced anyway and named in warnings, because a silent extrapolation that understates long-dated vega is exactly the failure this server exists not to have. A floor on VR would tidy the symptom away and hide it, so there isn't one.

To replace the number rather than trust it, call calibrate_vol_coord with four or more readings off Risk Navigator's own Vol.Coord. curve on its Equity tab — {shock: -0.20, pnl: -28000}, shocks as fractions. The same fit is available from a shell:

uv run python scripts/calibrate_vol_coord.py -- -0.05=-8000 -0.10=-22000 -0.15=-31500 -0.20=-28000 -0.25=-12000

Either route refits the decay and returns the residual at every point, the tenor range your positions actually constrain, and the most extreme volatility the fit produces — a decay that reproduces the curve by pricing a wing at 150% has fitted the chart rather than the market, and it tells you so.

The fit is kept. It goes to ~/.ibkr-risk-mcp/vol_coord.json (IBKR_CALIBRATION_FILE moves it) and becomes the default vol_coord_decay for every later stress_curve on that machine, no restart and nothing to carry by hand — which was the actual reason the shipped number kept being the one in use. Stored beside it is what it was fitted against: the targets, the residuals, the account, the date. A calibrated run then reports that provenance in assumptions.volCoordDecaySource and in warnings instead of the "factory decay" caveat, and a decay you pass explicitly is described as neither — this server did not fit it and does not vouch for it.

A fit taken against a portfolio that does not reconcile is returned but never stored. The asymmetry is the point: a curve missing a position announces itself through reconciled, while a decay that absorbed the same gap would go on deforming every later run with nothing to give it away.

Being relative is the part that matters, and it is why the additive slopes were removed from the defaults. Multiplying every volatility by the same factor puts more points on a wing already quoted at 41% than on a 31% at-the-money, so the surface steepens by itself. A parallel shift in points cannot do that at any slope, and on a ratio book the difference is not a matter of degree: the additive model made the curve monotonically worse through the region where Risk Navigator turns it back up, and put its crossover around −35% where Risk Navigator puts it near −18%. Measured there, vol_coord troughs at roughly 60% of the depth of the constant-volatility curve and at a much shallower shock — so a rising-volatility regime came out as the better one, the opposite of what a naive short-vega reading predicts. That is the reason the regimes are returned as separate curves rather than as a band.

The additive slope alternative is your input, not a measurement. Neither vol mode moves it: sticky_strike pins volatility to the strike and sticky_moneyness slides a strike along today's smile, so with both slopes at zero the curve prices the move in the underlying and not the move in volatility that comes with it — the optimistic half of the answer for a net short option book. vol_slope_down puts it in, at volatility points per 1% fall. It is applied as a parallel shift, flat across tenors, and a real surface does neither: it steepens in a sell-off, which understates a long out-of-the-money put, and the front month moves more than a 120-day tenor. A steepening term was tried and removed — the values that reproduced the observed shape priced the long wings above 100% implied volatility, which is curve fitting rather than modelling.

stress_curve exists because of that. Rather than burying one regime in one result, it returns a curve per regime over a single loading of the portfolio and a single surface, so the curves differ by assumption alone — by default the same two Risk Navigator draws. Read the slope-0 curve first: it is the constant-volatility case and the only one with an external check against Risk Navigator's blue line — and it passes. Measured against a live index ratio book, stress_curve at vol_mode='sticky_strike', slope 0, tracks Risk Navigator's blue curve to within 1-3% at every shock from 0 to −30%. That check only works in sticky_strike, which is why it is the default here; sticky_moneyness is a different model and on the same book put the trough 1.8x deeper. And do not assume the steepest slope is the worst case everywhere — on a book holding long wings the ordering reverses in the far tail, where the least-deep long puts carry the most vega and a rising volatility starts helping.

The local repricing is European; most of these options are American. Black-76 has no early exercise, while equity options and CME futures options both do. The gap is negligible out of the money and real once an option is in the money. Measured against live IB data: a 75-strike put with spot at 71.94 priced at 4.51 locally against IB's 4.65, a 2.9% shortfall that is the early-exercise premium and nothing else. It is reported per position as modelVsMarket rather than hidden, and it means the curve slightly understates losses deep in the money.

Rates are an input, not a measurement. IB does not publish the rate behind its own model. IBKR_RISK_FREE_RATE moves option values by little over the horizons this server deals with, but it is not zero either, and it is part of the modelVsMarket residual above.

Currency. Everything is summed in the account's base currency as IB reports it. A portfolio with positions IB values in another currency will show up as a reconciliation residual rather than being converted.

No exchange holiday calendar. The AM-settlement shift moves to the next weekday. An expiry the day after a holiday would be off by one, and that shows in settlementDate rather than hiding.

Tests

uv run pytest -q                      # unit tests, no TWS needed
uv run python scripts/smoke_test.py   # end-to-end, needs TWS

The unit tests run the whole repricing layer against recorded JSON fixtures in tests/fixtures/, with the valuation date pinned, so a pricing bug is distinguishable from a market data problem and the numbers do not drift as time passes. The fixture portfolio holds an AM-settled quarterly and a PM-settled weekly on the same morning, a future, an equity and a bond — every trap above, in one file.

scripts/smoke_test.py exercises the live path: connection, greeks with a count of any missing modelGreeks, a surface, stress_portfolio from −30% to +30% in 1% steps with the reconciliation check, stress_curve over its three default regimes — checking both that the constant-volatility curve starts at zero and that volSurfaceUsed is not empty — stress_whatif, and a whatif_order on a single deeply out-of-the-money leg. Point .env at the paper port to try it safely.

Layout

src/ibkr_risk_mcp/
  server.py       MCP tool definitions — the docstrings are the interface
  connection.py   ib_async lifecycle, one connection, four failure modes
  marketdata.py   greeks, vol surface, rate limiting, portfolio snapshot
  pricing.py      Black-76 / Black-Scholes, skew and surface interpolation
  stress.py       the stress and what-if engine
  margin.py       whatif_order and the segmented margin summary
  contracts.py    expiry and underlying normalisation
  calibration.py  where the fitted vol_coord decay is stored between sessions
scripts/smoke_test.py
scripts/calibrate_vol_coord.py
tests/

pricing.py and contracts.py have no IB dependency at all, which is what makes them testable.

Available Tools

9 tools
calibrate_vol_coordA

Fit vol_coord_decay to YOUR Risk Navigator, and keep the result.

vol_coord reproduces IB's volatility-coordinated model. Its asymmetry — a fall moves volatility ten times as hard as a rise — is IB's, documented. Its term damping VR(t) is not: IB says only that the function exists and is decreasing. The number this server ships was fitted to one screenshot of somebody else's Risk Navigator, from nine points read off a chart by eye. It has no claim on your book, every vol_coord result says so in warnings, and this tool is how you replace it.

What to do:

  1. In TWS open Risk Navigator's risk graph, on the Equity tab if you use the default scope.

  2. Read the Vol.Coord. curve — the one that responds to volatility, not the constant-volatility line — at four or more shocks spread across the range you care about.

  3. Pass them in as {shock: -0.20, pnl: -28000} pairs, shocks as fractions.

The fit comes back with the residual at every point and, more usefully, with what to distrust about it: the tenor range your positions actually constrain, and the most extreme volatility the fitted decay produces. A decay that reproduces the curve by pricing a wing at 150% has fitted the chart rather than the market, and it says so rather than leaving you to find out three layers down in a P&L.

Unless persist=false, the fit is written to disk and becomes the default vol_coord_decay for every later stress_curve on this machine — no restart, no carrying the number by hand — together with what it was fitted against, which is then reported in assumptions.volCoordDecaySource. Set IBKR_CALIBRATION_FILE to move the file. It is the only thing this server writes.

A fit taken against a portfolio that does not reconcile is returned but not stored. The reason is asymmetry of failure: a curve that is missing a position announces itself through reconciled, while a decay that absorbed the same gap would go on silently deforming every vol_coord run afterwards.

Nothing here trades or quotes. It reads positions and reprices locally.

ParametersJSON Schema
NameRequiredDescriptionDefault
betasNo
scopeNoMatch the Risk Navigator tab the readings came from. Its Equity tab is 'equity', the default.equity
persistNoStore the fit as this machine's standing calibration, so every later stress_curve uses it without being told. Set false to see the fit without adopting it. A fit against a portfolio that does not reconcile is never stored, whatever this says.
targetsYesFour or more points read off Risk Navigator's Vol.Coord. curve, spread across the range you care about. Three is the minimum the fit will accept and it constrains almost nothing; points bunched near the money constrain nothing at all. Get them from the Equity tab if you run the default scope='equity', so the curve you are fitting to excludes FX and rates the same way this engine does.
vol_modeNoMatch the run you intend to use the decay in. sticky_strike is the default everywhere else and is what Risk Navigator's own curve does.sticky_strike
fetch_skewNo
risk_groupsNoSame override as elsewhere, e.g. {'TLT': 'rates'}. Use the same one you pass to stress_curve, or the fit is against a different book than the runs that will use it.
default_betaNo

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the annotations, the description discloses the critical side effect: unless persist=false, the fit is written to disk and becomes the standing calibration, and it names this as 'the only thing this server writes.' It also explains the asymmetric failure behavior around unreconciled portfolios and notes that nothing trades or quotes. This goes well beyond the structured annotations and adds genuinely important operational context.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long but earns its length: it front-loads the core purpose, uses bold to mark the most important caveats, and organizes content into workflow, persistence, and failure-mode sections. There is no filler, and every paragraph contributes either to correct invocation or to preventing misuse.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a complex calibration tool with eight parameters, the description covers the full invocation workflow, persistence semantics, failure conditions, and safety boundaries. It also references assumptions.volCoordDecaySource and the returned residuals, so with an output schema present, the agent has enough to call the tool correctly and interpret results.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The description substantially enriches the meaning of key parameters: targets must be four or more points spread across the range, shocks are fractions, persist=false means trial-only, and risk_groups should match stress_curve. However, three parameters (betas, fetch_skew, default_beta) have no schema description and are not explained in the description either, leaving a small but real gap.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource: 'Fit `vol_coord_decay` to YOUR Risk Navigator, and keep the result.' It clearly distinguishes this calibration tool from siblings like stress_curve by explaining that the fitted decay becomes the default for all later stress_curve calls. The purpose is unmistakable and not a tautology.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The 'What to do' section gives a numbered, actionable workflow, including where to read the curve in TWS, how many points to collect, and how to pass them. It also states when a fit is not stored (unreconciled portfolio) and how persist=false changes behavior. This is explicit, practical guidance an agent can follow directly.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

check_connectionA
Read-onlyIdempotent

Check whether TWS or IB Gateway is reachable and an account is logged in.

Call this first whenever another tool fails. It separates the four situations that all present as "cannot connect" and need different fixes:

  • not_listening — nothing is on the port. TWS is not running, or IBKR_PORT points at the wrong one; the response lists which of the four default ports (7496 TWS live, 7497 TWS paper, 4001/4002 Gateway) are answering.

  • api_not_enabled — the port answers but the API handshake never completes. "Enable ActiveX and Socket Clients" is off in TWS.

  • client_id_in_use — another script holds this client id.

  • not_logged_in — the API is up but no account is loaded.

  • connected — everything is in place.

hint says what to do about the state in each case.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already mark the tool as read-only and idempotent. The description adds substantial behavioral context beyond that, such as the meaning of each connection state, the specific default ports checked, and the content of the `hint` field. No contradiction with annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured: a one-sentence purpose, a clear 'when to use' directive, and a bulleted list of states with concise explanations. Each sentence adds value and is front-loaded with the most critical information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no parameters and an output schema present, the description fully covers what an agent needs to know: the purpose, when to call, the possible states, and what `hint` provides. Nothing crucial is missing for a diagnostic tool of this complexity.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has zero parameters, so the schema coverage is trivially 100%. The description correctly focuses on behavior and output rather than parameters. A baseline of 4 is appropriate since no parameter clarification is needed.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: 'Check whether TWS or IB Gateway is reachable and an account is logged in.' It also enumerates the five specific states it distinguishes, making the tool's role unmistakable and differentiating it from siblings that handle trading or risk operations.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly instructs to 'Call this first whenever another tool fails,' which is direct usage guidance. It also explains how to interpret each state and what `hint` provides, giving the agent clear context for when and how to use the tool.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_margin_summaryA
Read-onlyIdempotent

Margin and liquidity, split by segment.

Returns NetLiquidation, EquityWithLoanValue, FullInitMarginReq, FullMaintMarginReq, AvailableFunds, ExcessLiquidity, TotalCashValue, BuyingPower and Leverage — each as the account total and, where IB reports it, separately for the securities (-S) and commodities (-C) segments.

The segments are the point. Futures margin must be met in the commodities segment; IB covers a shortfall there by sweeping cash out of the securities segment. An account whose total excess liquidity looks healthy can still be one bad day away from a forced liquidation if the shortfall lands in commodities while securities is also falling. Report the segment figures, not only the totals, whenever futures are involved.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true, idempotentHint=true, and destructiveHint=false, so the safe-read nature is covered. The description adds valuable behavioral context beyond annotations: IB may sweep cash from securities to commodities, and a healthy total excess liquidity can still mask a segment-level shortfall. This teaches the agent how to interpret the returned numbers rather than just their type.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is somewhat long but each sentence earns its place: a crisp opening, a field list, and a bolded explanation of why segments matter. The formatting is clean with a lead sentence, an em-dash separated detail, and a clear call to action.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With zero parameters, an output schema present, and annotations covering safety, the description goes beyond the minimum by explaining the key subtlety of segment-level margin and how to act on it. The tool is fully contextualized for the agent, including a warning scenario and a reporting directive.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has zero parameters, so the input schema carries no burden; the baseline is 4. The description does not need to clarify parameters and instead focuses on explaining the output's segment semantics, which is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with 'Margin and liquidity, split by segment,' which clearly names the resource (margin and liquidity) and the specific scope (segment breakdown). It then enumerates the exact fields returned, distinguishing this tool from siblings like stress_portfolio or get_position_greeks.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly tells the agent when to emphasize segment figures: 'Report the segment figures, not only the totals, whenever futures are involved.' It also explains why segments matter by describing the commodities-segment shortfall and forced-liquidation risk, giving the agent actionable context for choosing how to use the results.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_position_greeksA
Read-onlyIdempotent

IB's model greeks for every option position — its numbers, not ones implied locally.

Returns per position: conid, symbol, secType, right, strike, lastTradeDate, settlementDate, daysToExpiry, position, multiplier, undPrice, impliedVol, delta, gamma, vega, theta, optPrice and pvDividend.

Things to know about the values:

  • undPrice is the forward IB used, not the index spot. For a futures option that is the future's price, which differs from the cash index by the basis; any repricing has to start from it.

  • multiplier comes from the contract. ES is 50 and MES is 5, and the difference is a factor of ten in every exposure figure.

  • settlementDate, not lastTradeDate, is the expiry. The quarterly ES options settle AM and stop trading the afternoon before, so TWS shows 17 December for something that expires on the 18th, while a weekly settling the same morning shows the 18th. Both are returned; pair positions on settlementDate.

  • Missing greeks are listed, not dropped. A contract IB never published model greeks for appears under missing with the reason. The usual causes are no market data subscription for that instrument, or a strike too illiquid for IB to imply a volatility. Note that delayed data does carry model greeks, so a missing row is rarely explained by the market data type alone.

ParametersJSON Schema
NameRequiredDescriptionDefault
symbolNoRestrict to options on this underlying root, e.g. 'ES'.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The annotations already declare readOnlyHint=true, idempotentHint=true, and destructiveHint=false. The description adds substantial behavior beyond these: undPrice is the forward IB used, multiplier is from the contract, settlementDate is the actual expiry, and missing greeks are listed rather than dropped. It also hints at data subscription requirements, providing rich behavioral context.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is lengthy but structured and front-loaded with the core purpose, followed by a return field list and a bulleted list of key nuances. Every sentence carries substantive information, and the bullet format improves readability without padding.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with an output schema and one well-documented optional parameter, the description is complete. It explains critical interpretation details (forward vs spot, multiplier scaling, settlement date vs last trade date, and missing-row behavior) that are not derivable from the schema alone. It fully covers the tool's context.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema has 100% description coverage for its single optional parameter ('symbol'), with a clear description. The tool description adds no additional parameter-specific meaning, so the baseline of 3 applies because the schema already handles parameter semantics adequately.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource: 'IB's model greeks for every option position.' It clearly distinguishes the tool from siblings, such as get_vol_surface, by noting these are 'its numbers, not ones implied locally.' This makes the tool's purpose unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear context for when to use this tool—when broker model greeks are needed, not local implied ones—and explains caveats like settlement date and missing greeks. However, it does not explicitly mention sibling alternatives or state when not to use the tool, so it stops short of a full 5.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_vol_surfaceA
Read-onlyIdempotent

IB's implied volatility surface for an underlying: a grid of (expiry, strike) with impliedVol, delta, optPrice, undPrice and daysToExpiry.

This is the input that makes local repricing deterministic. With IB's own volatilities in hand, a constant-volatility scenario needs no proprietary model — only Black-76 arithmetic on top of numbers IB published.

Do not collapse the surface to one number. Volatility has a term structure: ES at 139 days can sit near 15% at the money while the front month prints 12%. Using the front month for a longer tenor understates a long-dated position badly. Read the tenor you need, and interpolate between tenors in total variance if you must.

daysToExpiry counts to the settlement date. For AM-settled expiries that is one day past the last trading date TWS shows.

Strikes with no published volatility come back under missing with the reason rather than being silently absent — a surface missing its left wing looks identical to one that has none.

Check which instrument you got. A bare root is ambiguous: ES is the E-mini S&P 500 future and Eversource Energy on NYSE, and with no sec_type the stock wins. The resolved contract comes back under underlying, any collision is listed in warnings, and a surface on the wrong instrument looks perfectly reasonable until you notice the strikes are two orders of magnitude off.

ParametersJSON Schema
NameRequiredDescriptionDefault
rightsNoPuts by default; pass both for the full smile.
strikesNoExplicit strikes. Each is snapped to the nearest listed one.
expiriesYesExpiries as YYYYMMDD or YYYY-MM-DD. Either the last trading date or the settlement date works; both resolve to the same contracts.
sec_typeNoWhich instrument the symbol means. Left empty the search tries STK, then IND, then FUT — so 'ES' returns Eversource Energy, not the E-mini S&P. Pass FUT for a futures root. Any collision is reported in `warnings` either way.
max_strikeNo
min_strikeNo
underlyingYesUnderlying root, e.g. 'ES' or 'SPY'.
trading_classNoNeeded when one underlying has two contracts expiring the same day, e.g. 'ES' (quarterly, AM-settled) against 'EW4' (weekly, PM-settled).
max_strikes_per_expiryNoCap per expiry. Each strike is one market data line and IB allows about fifty at once, so a wide grid over many expiries takes time.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description adds substantial behavioral context beyond the readOnly/idempotent/non-destructive annotations: missing strikes appear under 'missing' with reasons, daysToExpiry counts to settlement date (not just trading date), and instrument-root ambiguity is resolved via 'underlying' and 'warnings'. These are valuable behavioral traits not inferable from annotations alone, and there is no contradiction.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long but every section earns its place, with bolded headings breaking up distinct concepts (deterministic repricing, don't collapse, settlement dates, missing strikes, instrument ambiguity). It is front-loaded with the core definition and then layers critical warnings. No filler or redundant phrasing.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with 9 parameters and an output schema, the description covers the essential contextual pitfalls: settlement vs trading date, missing strike handling, root-symbol collisions, and usage cautions about term structure. The output schema exists, so return values need no elaboration. The tool is complex and the description fully equips the agent with the needed context.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 78%, so the schema already explains most parameters. The description adds meaningful context for several: it explains sec_type disambiguation with the 'ES' example, trading_class resolution for same-day expiries, strike snapping behavior, and the rate-limit implication of max_strikes_per_expiry. This goes beyond the schema's dry definitions, though not exhaustively for all parameters.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool returns IB's implied volatility surface for an underlying, with a grid of (expiry, strike) and specific fields. It uses a specific verb ('get') and resource ('volatility surface'), and the detailed behavior distinguishes it from sibling tools dealing with margin, greeks, stress, and orders.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Extensive usage guidance is provided: it explains when to use the surface for local repricing, warns against collapsing the surface to a single number, advises reading the correct tenor and interpolating in total variance, and tells users to check the underlying instrument to avoid ambiguity. It gives clear context and practical 'how to use' directions, though it doesn't name alternative tools explicitly.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

stress_curveA
Read-onlyIdempotent

The portfolio P&L curve under several volatility regimes at once — the data behind a risk graph, for plotting rather than for reading point by point.

Risk Navigator draws two curves: a constant-volatility line and one from its own implied-volatility model, which is not documented and cannot be reproduced. This returns as many as you ask for, and the volatility assumption behind each is a number in the output rather than a black box: volSlopeDown is volatility points per 1% fall.

Read the slope-0 curve first. It is the constant-volatility case and the only one with an external check — it should sit close to Risk Navigator's blue line. If it does not, the volatility lookup is wrong and no other scenario in the result means anything.

Check volSurfaceUsed. It lists every quote the repricing actually read, as (underlying, tenor, strike, iv). Empty under sticky_moneyness means no expiry held three strikes, so no smile could be built and every option silently fell back to sticky_strike — the result looks perfectly normal and is not the model you asked for. fetch_skew=true fixes it at the cost of extra market data requests.

How each curve is built, and where it is weakest:

  • Every scenario reprices one loading of the portfolio and one surface, so the curves differ by assumption alone. Calling the single-curve tool three times could not promise that: the book moves between calls.

  • The starting volatility is IB's own, per contract, out of a model that prices American exercise. The surface is used only for the change in volatility as a strike slides to new moneyness, which keeps IB's better number as the anchor and keeps every curve exactly zero at zero shock.

  • vol_coord reproduces IB's own model: volatility is multiplied, not shifted — a fall of X moves it by 10X relatively, a rise by -X, damped across tenors. Being relative is what makes the surface steepen by itself, since a wing already quoted at 41% takes more points than a 31% at-the-money out of the same scenario. The asymmetry is IB's documented one; the damping is fitted here and is not published, so vol_coord_decay is an input you should refit against your own Risk Navigator.

  • The additive volSlopeDown alternative is a parallel shift, flat across tenors. It cannot steepen at any value, and on a ratio book that is the difference between a curve that keeps falling and one that turns back up. Prefer vol_coord unless you specifically want a flat regime.

  • Equities move by the shock times their beta, futures and options by their own underlying's beta-scaled move. Bonds are flat unless bond_rate_shift_bp is set. FX is off the axis by default and reported under excluded with its market value — not held flat in silence.

  • Options IB would not model are repriced from a locally implied volatility where a mark price exists, flagged per position and in warnings, and held flat only when even that fails.

  • date_offsets=[0, 3] (or valuation_dates) crosses the scenarios with a family of valuation dates, so "today against Monday, when the August wings expire" is one call rather than two that the market moves between. Each entry under curves carries its own valuationDate and dateOffsetDays; name stays the scenario's and label distinguishes them. Time is the only thing that moves — today's spot and today's volatilities with the clock advanced, which is decay and not a forecast.

  • breakdown='expiry' puts pnl_by_expiry on every point and troughByExpiry on every curve, keyed on the option's settlement date. On a book running one root across many expiries that is the only way to see which expiry owns the trough; pnl_by_symbol shows one "ES" number for all of them.

pnl_pct_of_nlv is on every point, and netLiquidation at the top. Quote the fraction rather than the amount when comparing two dates or two accounts. Check reconciled before quoting any of it.

ParametersJSON Schema
NameRequiredDescriptionDefault
betasNoPer-symbol share of the PRICE shock — unrelated to a scenario's volatility slope. Scales the move of that position's own underlying, options and futures included.
scopeNoWhich underlyings are on the shock axis. 'equity' excludes FX, rates and the rest outright and lists them under `excluded`; 'all' shocks everything by the same percentage, which off the equity axis is meaningless.equity
shocksNoUnderlying moves as fractions: -0.20 is a 20% fall. Defaults to -0.40 to +0.10 in 2% steps, which is wide enough on the downside that a short-gamma trough falls inside the window rather than on its edge.
vol_modeNosticky_strike — the default — pins each strike to the volatility it holds today. This is what Risk Navigator's blue curve does, and it is the only setting under which the slope-0 curve can be checked against it: measured on a live index ratio book the two agree to within 1-3% at every shock from 0 to -30%. sticky_moneyness instead rereads each strike's volatility at the moneyness it lands on after the shock, off the portfolio's own surface, interpolated across strike and expiry — a defensible model, but a different one, and on that same book it deepened the trough by a factor of 1.8. Do not compare it to Risk Navigator.sticky_strike
breakdownNoWhich per-point P&L breakdowns to return. 'symbol' — the default and what this server has always returned — groups by root, which on an options book collapses every ES expiry under one 'ES' key. 'expiry' groups by the option's SETTLEMENT date instead ('ES 2026-10-30'), which is the breakdown that answers 'which expiry is holding the trough down, and which short do I buy back'. 'both' returns the two of them and 'none' neither. Positions with no expiry get a key naming their class ('ES (future)', 'AAPL (equity)') so the breakdown still sums to the point's total and can be checked against it. Responses are large: a book on nine expiries pays for the second dictionary at every one of twenty-six shocks, so ask for 'both' only when you want both, and 'none' when you only want the curve.symbol
fetch_skewNoLet the surface pull neighbouring strikes from IB for expiries the portfolio holds too thinly. Costs extra market data requests, and is what to reach for when `volSurfaceUsed` comes back thin or empty.
risk_groupsNoOverride a symbol's risk group, e.g. {'TLT': 'rates'}. IB publishes no asset class for a bond or gold ETF quoted as a stock.
date_offsetsNoA FAMILY of valuation dates in one call, as day offsets: [0, 3] is today and three days out. Every scenario is run at every offset, and all of them come out of ONE loading of the positions and prices — which is what makes the comparison mean anything, since calling this tool twice lets the book and the market move between the two answers. Mutually exclusive with date_offset_days and valuation_dates.
default_betaNo
vol_scenariosNoOne curve per volatility regime. Defaults to the two curves Risk Navigator itself draws: 'const' (no volatility response, its blue line) and 'vol_coord' (IB's own volatility-coordinated model). Always keep a const curve in the set — it is the one that can be checked against Risk Navigator, and if it does not line up nothing else in the result is worth reading. Additive slopes are still there for a regime you want to state by hand, but prefer vol_coord: a slope shifts the surface in parallel, which on a book that is short the middle and long both wings is the wrong shape and not merely the wrong size.
valuation_datesNoThe same family, given as ISO dates: ['2026-08-28', '2026-08-31']. Mutually exclusive with the offset forms. No calendar magic — the date is the date, weekend or not.
vol_coord_decayNoTerm damping of the vol_coord model: VR(t) = exp(-decay * t), so a front-month contract takes nearly the whole shock and a back month a fraction. IB documents that this function exists and is decreasing but not what it is, so this number is FITTED, not published. Left unset it uses your own stored calibration if calibrate_vol_coord has ever been run, and otherwise the factory fit of 4.736 — one number calibrated on one book, which has no claim on yours. `assumptions.volCoordDecaySource` says which of the three you got.
date_offset_daysNoRoll the valuation date forward this many days (time decay).
bond_rate_shift_bpNo
bond_duration_yearsNo
vol_coord_calibrated_to_yearsNoHow far out in tenor `vol_coord_decay` was actually constrained. Defaults alongside the decay: your calibration's reach if you have one, and otherwise 0.345, the four months the shipped fit was constrained over. An exponential extrapolates to zero, which would price a one-year option as carrying no volatility risk at all in a crash, so positions past this are priced anyway and named in `warnings` rather than passing in silence.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare read-only, idempotent, open-world, and non-destructive behavior, and the description adds substantial beyond-annotation detail: silent fallback from sticky_moneyness to sticky_strike, fitted unpublished decay parameters, bonds held flat unless a shift is set, FX reported under excluded rather than silently ignored, and the explicit warning to check reconciled before quoting results. No contradiction exists.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long, but every section earns its place: it front-loads the core purpose, then organizes verification, silent fallbacks, curve construction, valuation-date families, and breakdowns under clear bolded lead-ins. The depth is proportional to a 16-parameter analytical tool, and there is no tautology or padding.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With an output schema present, the description does not need to enumerate return fields, yet it still covers the critical ones: curves, valuationDate, dateOffsetDays, pnl_by_expiry, excluded, warnings, netLiquidation, and reconciled. It also explains verification, model provenance, and calibration state, making it complete for safe invocation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is high at 81%, but the description still adds real meaning: volSlopeDown is volatility points per 1% fall, vol_coord is multiplicative rather than shifted, date_offsets creates one consistent family of valuations, and breakdown='expiry' keys on settlement date. It turns parameter names into decision-relevant concepts rather than mere labels.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific deliverable: the portfolio P&L curve under several volatility regimes, positioned as the data behind a risk graph for plotting. It distinguishes itself from the single-curve sibling by emphasizing that this tool guarantees one loading of the portfolio across all scenarios.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides explicit operational guidance: read the slope-0 curve first as the external check, prefer vol_coord unless a flat regime is specifically wanted, use fetch_skew when volSurfaceUsed is thin, and use breakdown='expiry' to see which expiry drives the trough. It also names alternatives and failure conditions, leaving little to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

stress_portfolioA
Read-onlyIdempotent

Reprice the whole portfolio — options, equities, futures — across underlying shocks and return the P&L curve and its trough.

The trough is the primary output: the worst point of the curve and the shock at which it sits. troughRefined interpolates between grid points for where the minimum actually falls, and is labelled as interpolated.

The model, returned with every result in assumptions:

  • only equity underlyings are on the axis by default (scope='equity'). FX, rates and anything else is excluded outright and listed under excluded with its market value. This is what Risk Navigator's Equity tab does, and it is what makes the two comparable — verified against a live account, where the engine and Risk Navigator agreed to 7 dollars on 29,000 at a 15% fall once the FX leg was off both. Off the equity axis the single shock is meaningless: the same account's CAD strangle was contributing -21,716 at -20% and -7,183 at +10%, dominating both tails.

  • all underlyings in scope are shocked by the same percentage at once, which is Risk Navigator's own default assumption. betas scales that shock per symbol and reaches every class that responds to one — an option is repriced at its own beta-scaled move, not at the index move. Use it to stand a foreign underlying down off an equity axis, and read the warning it produces: a beta of 0 takes a position off this curve, it does not measure that position's own risk.

  • options are repriced with Black-76 on the shocked forward using IB's implied volatility. Equity options are carried from spot using IB's pvDividend, so both kinds go through one pricer.

  • the volatility level is flat along the shock axis unless you say otherwise. Neither vol mode raises it: sticky_strike pins volatility to the strike, sticky_moneyness slides a strike along today's smile. Real volatility rises when an index falls, and a net short option book pays for that on top of the delta and gamma this curve already counts. vol_bump does not fill the gap — it is constant across shocks. vol_slope_down does: 1.0 adds one volatility point per 1% fall. It is your input, not a measurement, and it is applied as a parallel shift across every tenor.

  • P&L is model-price-now against model-price-shocked, so the curve is exactly zero at zero shock by construction. The gap between the local model and IB's own price is reported per position as modelVsMarket instead of being folded into the curve. Expect it to be small out of the money and a couple of percent in the money, where IB prices the early exercise that Black-76 has no room for — so the curve understates losses slightly once options go deep in the money.

  • bonds are held flat unless bond_rate_shift_bp is set; anything this server does not model is held flat and named in warnings.

  • breakdown='expiry' adds pnl_by_expiry to every point and troughByExpiry beside the trough — each expiry's own worst point and what it contributes at the portfolio's trough. On a book holding one root across many expiries those are the two different numbers behind "which short is the problem", and pnl_by_symbol can answer neither.

Check reconciled before quoting anything. At zero shock the portfolio is rebuilt from its positions and compared against NetLiquidation; a residual over 1% returns reconciled: false with the residual attached. A curve that does not reconcile is missing something, and the number it gives for the trough is missing it too.

Risk Navigator's own volatility shock model is not published. sticky_strike is the approximation that corresponds to its default curve, not a reproduction of it — expect the shape to match and the last few percent not to.

ParametersJSON Schema
NameRequiredDescriptionDefault
betasNoPer-symbol share of the shock, e.g. {'AAPL': 1.2, 'EUR': 0.0}. Applies to options and futures as well as equities: the beta scales the move of that position's own underlying, and the option is then repriced there. Key it on the root ('ES'), the local symbol ('ESZ6 P5800') or the underlying ('ESZ6'); the most specific match wins. Use it to stand a foreign underlying down off an equity axis — but read the warning it produces: a beta of 0 removes a position from this curve, it does not measure its own risk.
scopeNoWhich underlyings are on the shock axis. 'equity' — the default — keeps only equity underlyings and excludes FX, rates and the rest outright, which is what TWS Risk Navigator's Equity tab does and what makes the curve comparable to it. Off the equity axis a single percentage shock is meaningless: a currency future moved 20% prices an exchange rate that has never traded there. 'all' shocks everything alike. Excluded positions are always listed under `excluded`, never dropped in silence.equity
shocksYesUnderlying moves as fractions: -0.10 is a 10% fall. A range like -0.30 to +0.30 in 0.01 steps is the usual ask.
vol_bumpNoAdded to every volatility, in points, flat along the shock axis: 0.05 is +5 points at every shock. For volatility that responds to the shock itself, use vol_slope_down.
vol_modeNosticky_strike keeps each strike's current volatility and is what Risk Navigator's default curve does. sticky_moneyness slides the smile with the forward.sticky_strike
breakdownNoWhich per-point P&L breakdowns to return. 'symbol' — the default and what this server has always returned — groups by root, which on an options book collapses every ES expiry under one 'ES' key. 'expiry' groups by the option's SETTLEMENT date instead ('ES 2026-10-30'), which is the breakdown that answers 'which expiry is holding the trough down, and which short do I buy back'. 'both' returns the two of them and 'none' neither. Positions with no expiry get a key naming their class ('ES (future)', 'AAPL (equity)') so the breakdown still sums to the point's total and can be checked against it. Responses are large: a book on nine expiries pays for the second dictionary at every one of twenty-six shocks, so ask for 'both' only when you want both, and 'none' when you only want the curve.symbol
fetch_skewNoLet sticky_moneyness pull neighbouring strikes from IB when the portfolio holds too few to define a smile. Costs extra market data requests.
risk_groupsNoOverride the risk group of a symbol, e.g. {'TLT': 'rates', 'GLD': 'metals'}. IB publishes no asset class for a bond or gold ETF quoted as a stock, so those are classified as equity unless named here. Groups: equity, fx, rates, metals, energy, other.
default_betaNo
vol_slope_upNoVolatility points removed per 1% RISE. Positive means volatility falls as the market rallies, which is the usual direction. Separate from vol_slope_down because the response is not symmetric.
valuation_dateNoValue the curve at this ISO date, e.g. '2026-09-30', instead of counting the days out by hand into date_offset_days. Mutually exclusive with it. No calendar magic: the date is the date, weekend or not, and time to expiry is ACT/365 throughout. The P&L is still measured FROM today at today's prices and volatilities, with the clock rolled forward — it is decay, not a forecast.
vol_slope_downNoVolatility points added per 1% FALL in the underlying: 1.0 means a -20% shock reprices at +20 points. Zero — the default — holds the volatility level flat, which prices the move in the underlying and not the move in volatility that comes with it. A net short option book loses real money on that term, so leaving this at zero is the optimistic half of the answer.
date_offset_daysNoRoll the valuation date forward this many days (time decay).
bond_rate_shift_bpNoParallel rate shift in basis points applied to bonds. Zero leaves them unchanged, which is the default.
bond_duration_yearsNoDuration assumed for bonds when a rate shift is applied. IB does not publish duration, so this is your input and the result is only as good as it.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare the tool read-only, idempotent, and non-destructive. The description goes far beyond that, disclosing model internals: Black-76 repricing, flat volatility assumptions, how troughRefined interpolates, how reconciled is computed, what happens to excluded and unmodeled positions, and the modelVsMarket gap. This is exemplary transparency for a complex analytical tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long but front-loaded with the core purpose and primary output, then organized into clear behavioral bullets. Each sentence carries meaningful caveats, though some points (e.g., beta semantics, scope semantics) restate or extend schema text, adding a bit of redundancy. Still, it is dense and purposeful rather than padded.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity — 15 parameters, multiple model assumptions, a reconciliation check, and a detailed output shape — the description is remarkably complete. It explains not only what is returned but how to interpret troughRefined, reconciled, excluded, warnings, pnl_by_expiry, and modelVsMarket. It also warns about known limitations relative to Risk Navigator, which an agent needs before quoting results.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is already high at 93%, so the baseline is 3. The description adds substantial meaning on top: betas scale the shock per symbol and remove positions at zero; scope='equity' matches Risk Navigator's Equity tab; vol_slope_down is a user input, not a measurement; breakdown='expiry' returns each expiry's trough contribution. Nearly every parameter gains operational context beyond its schema description.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource: 'Reprice the whole portfolio — options, equities, futures — across underlying shocks and return the P&L curve and its trough.' This clearly distinguishes the tool's scope. However, it does not explicitly contrast itself with sibling tools like stress_curve or stress_whatif, so it stops short of full sibling differentiation.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives strong situational guidance: when to use equity scope, when betas are appropriate, when to set vol_slope_down, when breakdown='expiry' answers a different question, and when to trust the trough only if reconciled. It stops short of explicitly saying 'use this instead of stress_curve when...' or naming excluded conditions, so no direct alternative routing is provided.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

stress_whatifA
Read-onlyIdempotent

The same stress run, with hypothetical legs added — three curves and three troughs: the portfolio as it stands, the portfolio plus the legs, and the difference.

This is what replaces reading Risk Navigator's What-If by hand: "if I add N puts at strike K expiring E, where does the trough move to?"

Read all three troughs. The trough of the difference is not the difference of the troughs — adding protection moves where the worst point sits as well as how deep it is, and comparing only the depths hides the move. A structure that lifts the bottom by very little may still have pushed it from −8% out to −15%, which is the part that matters.

Hypothetical options are priced off IB's current model greeks for those exact contracts, so both curves start from the same volatilities and the difference is the structure alone. A leg that cannot be resolved or priced is reported in legProblems and left out of the second curve; the comparison then covers only the legs that did resolve, and says so.

With breakdown='expiry' all three curves are broken out by settlement date, and the difference curve's pnl_by_expiry is where the structure actually landed: the expiry you traded against moves and the ones you did not read zero, which is how a hedge is told apart from a change of subject. A short expiry whose row goes to roughly nothing in withLegs is one the structure has closed out.

Nothing is sent to IB's order path here — this is pure local repricing. For what the structure costs in margin, use whatif_order.

ParametersJSON Schema
NameRequiredDescriptionDefault
legsYesHypothetical legs to add. Each is either a conid, or symbol+secType+expiry+strike+right, with action BUY/SELL and a quantity.
betasNo
scopeNoequity
shocksYesUnderlying moves as fractions, as above.
vol_bumpNo
vol_modeNosticky_strike
breakdownNoWhich per-point P&L breakdowns to return. 'symbol' — the default and what this server has always returned — groups by root, which on an options book collapses every ES expiry under one 'ES' key. 'expiry' groups by the option's SETTLEMENT date instead ('ES 2026-10-30'), which is the breakdown that answers 'which expiry is holding the trough down, and which short do I buy back'. 'both' returns the two of them and 'none' neither. Positions with no expiry get a key naming their class ('ES (future)', 'AAPL (equity)') so the breakdown still sums to the point's total and can be checked against it. Responses are large: a book on nine expiries pays for the second dictionary at every one of twenty-six shocks, so ask for 'both' only when you want both, and 'none' when you only want the curve.symbol
fetch_skewNo
risk_groupsNo
default_betaNo
vol_slope_upNo
valuation_dateNoValue the curve at this ISO date, e.g. '2026-09-30', instead of counting the days out by hand into date_offset_days. Mutually exclusive with it. No calendar magic: the date is the date, weekend or not, and time to expiry is ACT/365 throughout. The P&L is still measured FROM today at today's prices and volatilities, with the clock rolled forward — it is decay, not a forecast.
vol_slope_downNo
date_offset_daysNo
bond_rate_shift_bpNo
bond_duration_yearsNo

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.3/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already mark the operation read-only and idempotent, and the description adds substantial behavioral context: it is pure local repricing with no IB order-path interaction, hypotheticals are priced off current IB model greeks, unresolved legs surface in legProblems and are excluded, and the trough-of-difference caveat is explicitly warned about. It also clarifies how breakdown='expiry' changes what the curves mean.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with the core output, then proceeds through caveats, pricing behavior, breakdown semantics, and safety in a logical order. It is long but dense with useful information; a few sentences could be tightened, but none feel like padding.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 16-parameter tool, the description plus the existing output schema and annotations gives an agent enough to understand the primary use case, the required legs and shocks inputs, breakdown behavior, failure reporting, and the safety profile. Advanced parameters remain under-specified, but they have defaults and are not essential to the core scenario the description targets.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is only 25%, so the description must compensate. It does meaningfully explain legs and breakdown behavior, including unresolved-leg exclusion and pnl_by_expiry interpretation. However, it leaves many parameters such as betas, scope, vol_mode, vol_bump, fetch_skew, risk_groups, and the bond/rate/date parameters effectively unexplained, so the compensation is only partial.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific operation: take the same stress run, add hypothetical legs, and return three curves/troughs (base portfolio, portfolio plus legs, and the difference). It clearly distinguishes this from whatif_order, which is explicitly called out for margin-cost questions.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It frames the intended use case directly: answering What-If questions like 'if I add N puts at strike K expiring E, where does the trough move to?' It also gives an explicit exclusion, directing margin-cost needs to whatif_order and noting that no orders are sent. It does not enumerate every sibling, but the key alternative is named.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

whatif_orderA
Idempotent

IB's own margin impact for a hypothetical structure. Nothing reaches the market.

Each order carries whatIf=True, which IB evaluates in its margin engine and discards: it is never routed, never acknowledged as live, never appears in the order book. The tool is still gated behind IBKR_ENABLE_WHATIF=true because it is the only thing in this server that touches the order path at all; with the gate closed it sends nothing and returns blocked: true.

Returns initMargin, maintMargin and equityWithLoan before/after/change, plus commission and any warningText, in two views:

  • perLeg — each leg evaluated on its own.

  • cumulative — legs 1..k as a combo, for every k, so you can see where the offset appears.

Read the combined figure, not the sum of the legs. SPAN offsets the legs against each other and against what the account already holds, so the two differ — the difference is reported as offset.spanOffset. IB's what-if on arbitrary multi-leg combos is unreliable and will sometimes return nothing; that is reported per step and does not mean the structure is invalid.

If every call fails with no margin figures, check whether TWS has "Read-Only API" enabled — that setting blocks what-if orders too.

ParametersJSON Schema
NameRequiredDescriptionDefault
legsYesThe structure to evaluate. Each leg is a conid, or symbol+secType+expiry+strike+right, with action BUY/SELL and a quantity.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the annotations, the description discloses crucial behavioral traits: orders are never routed, never acknowledged as live, never appear in the order book, and return blocked when the gate is closed. It also surfaces a reliability caveat about multi-leg combos sometimes returning nothing, plus the Read-Only API failure mode — rich transparency beyond structured hints.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is longer than average, but every section earns its place: purpose, safety, return fields, view breakdown, interpretation warning, and troubleshooting. Bolded key takeaways and a clean bullet list make it scannable without padding.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with complex margin semantics, the description explains return values (initMargin, maintMargin, equityWithLoan, commission, warningText), two output views, SPAN offset behavior, failure modes, and environment prerequisites. The output schema exists, but the description goes beyond it to give operational context, making it highly complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema provides full description coverage: legs are explained as conid or descriptive fields with action and quantity. The tool description adds interpretive guidance ('Read the combined figure, not the sum of the legs') but does not meaningfully expand parameter semantics beyond the excellent schema documentation, so the baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description immediately states a specific purpose: 'IB's own margin impact for a hypothetical structure.' It adds a strong differentiator with '**Nothing reaches the market.**' and explains that each order carries whatIf=True, clearly distinguishing it from live-order or simple read-only siblings.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies when to use this tool — for hypothetical structures needing margin impact without market exposure — and includes practical caveats like the IBKR_ENABLE_WHATIF gate and the TWS Read-Only API check. It does not explicitly name an alternative tool or state when not to use it, but the context is clear enough.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

TDQS

A4.4/5.0
Disambiguation4/5

The three stress_* tools and two what-if tools live in the same conceptual neighborhood, so an agent must read carefully to pick between stress_portfolio, stress_curve, stress_whatif, and whatif_order. That said, each has a clearly distinct purpose—underlying-shock P&L, multi-volatility curves, hypothetical-leg P&L, and IB margin impact—and the descriptions draw those boundaries well.

Naming Consistency4/5

snake_case verb_noun names are the dominant pattern (check_connection, get_margin_summary, stress_portfolio, calibrate_vol_coord), but whatif_order breaks the verb convention and the stress_/get_/whatif_ prefixes are not uniformly aligned. Still, the names are predictable enough to group and search.

Tool Count5/5

Nine tools cover the server's stated risk-analysis scope without redundancy: connection health, margin, greeks, vol surface, stress curves, what-if, and calibration. Each tool occupies a distinct function and none feel like filler.

Completeness4/5

The risk workflow is well covered: users can check connectivity, inspect margin and greeks, fetch vol surfaces, stress the book, add hypothetical legs, and estimate margin impact. Minor gaps remain—there is no direct non-option position listing and no way to stress an individual underlying without running the whole portfolio—but these are workarounds rather than dead ends.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    Portfolio risk analytics MCP server — VaR, Monte Carlo simulation, stress testing, portfolio optimization, options Greeks, and correlation analysis. Real market data via Yahoo Finance. Free tier available, Pro at $29/mo.
    10
    116
    2
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    MCP server for Interactive Brokers that exposes portfolio data, market quotes, trading, and analysis to any MCP-compatible AI client, with support for EU investors and safety-gated trading.
    1
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    An MCP server that turns Interactive Brokers into a question-answering portfolio analyst.
    4
  • F
    license
    B
    quality
    C
    maintenance
    MCP server for Interactive Brokers via IB Gateway, enabling read access to account data and trading capabilities for paper accounts.
    11

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/simoneb/ibkr-risk-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server