DeskPricer

Local HTTP pricing microservice for vanilla European and American equity options.

DeskPricer v3.4.0

Local HTTP pricing microservice for vanilla European and American equity options. Designed for Excel WEBSERVICE + FILTERXML integration — no VBA, no Bloomberg terminal calls inside the service.

Design intent: DeskPricer is a local-only tool for personal desk pricing and option analytics. It is not intended to be run or served as a public/server-style service. All design choices — localhost binding, no auth, no TLS, no rate limiting, XML-by-default — reflect this.


Use with AI Agents (MCP)

DeskPricer is available as an MCP server. Add it to Cursor, Claude Desktop, or any MCP-compatible agent:

pip install deskpricer

Published on PyPI: https://pypi.org/project/deskpricer/

Cursor — add to ~/.cursor/mcp.json:

{
  "mcpServers": {
    "deskpricer": {
      "command": "deskpricer-mcp",
      "args": []
    }
  }
}

Claude Desktop — add to claude_desktop_config.json:

{
  "mcpServers": {
    "deskpricer": {
      "command": "deskpricer-mcp"
    }
  }
}

If deskpricer-mcp is not on your PATH, use the full path to the executable in your virtualenv.

Tools: price_option, implied_volatility, pnl_attribution, portfolio_greeks — same pricing engine as the HTTP API.

See docs/mcp_quickstart.md for full setup, conventions, and example prompts.


Quickstart

Go from clean clone to a working pricing call in under 5 minutes:

# 1. Install
python -m venv .venv
.venv\Scripts\activate
pip install -e ".[dev]"

# 2. Run
python -m deskpricer.main

# 3. Test with curl
curl "http://127.0.0.1:8765/v1/greeks?s=100&k=105&t=0.25&r=0.05&q=0.02&b=0.0&v=0.20&type=call&style=european"

# 4. Test with Excel (copy into a cell)
# =FILTERXML(WEBSERVICE("http://127.0.0.1:8765/v1/greeks?s=100&k=105&t=0.25&r=0.05&q=0.02&b=0.0&v=0.20&type=call&style=european"),"//outputs/price")

Expected output for the curl call (XML):

<?xml version="1.0" encoding="UTF-8"?>
<greeks>
  <meta>
    <service_version>3.4.0</service_version>
    <quantlib_version>1.42.1</quantlib_version>
    <engine>analytic</engine>
    <valuation_date>2026-04-22</valuation_date>
  </meta>
  <inputs>
    <s>100.0</s>
    <k>105.0</k>
    <t>0.25</t>
    <r>0.05</r>
    <q>0.02</q>
    <b>0.0</b>
    <v>0.2</v>
    <type>call</type>
    <style>european</style>
  </inputs>
  <outputs>
    <price>2.288743</price>
    <delta>0.356244</delta>
    <gamma>0.037206</gamma>
    <vega>0.185519</vega>
    <theta>-0.033315</theta>
    <rho>0.083111</rho>
    <charm>-0.001241</charm>
  </outputs>
</greeks>

For JSON, send Accept: application/json or append ?format=json.


Try the Demo Workbook

Open sample/DeskPricer_Bitcoin_Demo.xlsx for a ready-to-run example. It contains 3 sheets:

SheetWhat it shows
GreeksBitcoin European Call — $75K spot, $100K strike, 3M expiry, 50% vol
ImpliedVolBack out ~68.3% implied vol from a $3,398.71 market price
PnL AttributionDecompose PnL when spot rallies $75K → $80K and vol widens 50% → 55%

Each sheet has the actual WEBSERVICE and FILTERXML formulas pre-loaded. Just start DeskPricer and the cells will populate automatically.


What it does

  • Price + Greeks for single options or multi-leg portfolios
  • Implied volatility solver (Brent method via QuantLib)
  • PnL attribution — decompose option PnL into delta, gamma, vega, theta, rho, vanna, volga, and residual
  • XML by default — Excel WEBSERVICE + FILTERXML work out of the box; JSON available via Accept: application/json
  • Localhost-only — binds to 127.0.0.1; no network exposure

Conventions

GreekUnit / Convention
deltaAbsolute (∂V/∂S)
gammaAbsolute (∂²V/∂S²)
vegaPer 1% vol point
thetaPer calendar day (ACT/365). Negative for typical long options (time decay). Sign is opposite of Bloomberg DM, which reports theta as positive decay.
rhoPer 1% rate point (risk-free rate only; no dividend-yield or borrow-cost rho)
charmPer calendar day (∂delta/∂t)

Borrow cost (b): Optional annualized stock borrow cost (decimal). Effective cost-of-carry is r − q − b. Omitted or 0.0 matches pre-3.4.0 behavior.

Time to expiry (t): Supplied in years under ACT/365. Internally converted to calendar days (round(t * 365)) with a hard floor of 1 day, then rolled to the next business day using the chosen calendar (hong_kong by default). Theta and charm are computed per calendar day, not per business day.

PnL attribution: calendar_days represents the actual elapsed calendar-day hold period. theta_pnl = theta × calendar_days_elapsed. If both valuation dates are omitted, calendar_days defaults to 1. Provide explicit dates for multi-day hold accuracy.


Installation Options

Standalone Executable (Recommended)

Download DeskPricer_v3.exe from the Releases page and run:

.\DeskPricer_v3.exe

The service starts on port 8765. To use a different port:

.\DeskPricer_v3.exe --port 9000

From Source

python -m venv .venv
.venv\Scripts\activate
pip install -e ".[dev]"
python -m deskpricer.main

Excel User Guide

Service Status

Check that the service is running before pulling prices:

CellFormula
Status=IFERROR(FILTERXML(WEBSERVICE("http://127.0.0.1:8765/v1/health"),"//status"),"DOWN")

Expected output: UP


Example 1: Price a Single Option + Greeks

Assume your sheet has:

ColumnLabelExample Value
CSpot100
KStrike105
TTime to expiry (years)0.25
RRisk-free rate0.05
QDividend yield0.02
BBorrow cost (optional, default 0.0)0.0
VVolatility0.20
TYPEOption typecall
STYLEStyleeuropean

Step 1 — Build the URL in a helper cell (e.g. H2):

="http://127.0.0.1:8765/v1/greeks?s="&C2&"&k="&K2&"&t="&T2&"&r="&R2&"&q="&Q2&"&b="&B2&"&v="&V2&"&type="&TYPE2&"&style="&STYLE2

Step 2 — Fetch the raw XML (e.g. I2):

=WEBSERVICE(H2)

Step 3 — Extract values into individual cells:

OutputFormula
Price=VALUE(FILTERXML(I2,"//outputs/price"))
Delta=VALUE(FILTERXML(I2,"//outputs/delta"))
Gamma=VALUE(FILTERXML(I2,"//outputs/gamma"))
Vega=VALUE(FILTERXML(I2,"//outputs/vega"))
Theta=VALUE(FILTERXML(I2,"//outputs/theta"))
Rho=VALUE(FILTERXML(I2,"//outputs/rho"))
Charm=VALUE(FILTERXML(I2,"//outputs/charm"))

Expected output for the example above:

GreekValue
Price2.288743
Delta0.356244
Gamma0.037206
Vega0.185519
Theta-0.033315
Rho0.083111
Charm-0.001241

Tip: Wrap each FILTERXML in IFERROR(...,"ERR") so one bad row doesn't break the whole sheet.


Example 2: Back Out Implied Volatility from Market Price

You observe a mid-market price of 6.50 for the same option and want the implied vol.

Step 1 — Build the URL (e.g. H2):

="http://127.0.0.1:8765/v1/impliedvol?s="&C2&"&k="&K2&"&t="&T2&"&r="&R2&"&q="&Q2&"&b="&B2&"&price=6.50&type="&TYPE2&"&style="&STYLE2

Step 2 — Extract implied vol:

=VALUE(FILTERXML(WEBSERVICE(H2),"//outputs/implied_vol"))

Expected output: 0.417484 (≈ 41.7 % vol)


Example 3: PnL Attribution

You had a position yesterday (t-1) and want to explain today's PnL.

Assume:

Fieldt-1 Valuet Value
Spot100102
Time0.250.2466
Vol0.200.22
Rate0.050.05
Div0.020.02
Borrow0.00.0
Qty10

Step 1 — Build the URL:

="http://127.0.0.1:8765/v1/pnl_attribution?s_t_minus_1=100&s_t=102&k=105&t_t_minus_1=0.25&t_t=0.2466&r_t_minus_1=0.05&r_t=0.05&q_t_minus_1=0.02&q_t=0.02&b_t_minus_1=0.0&b_t=0.0&v_t_minus_1=0.2&v_t=0.22&type=call&style=european&qty=10&cross_greeks=true"

Step 2 — Extract attribution buckets:

BucketFormula
Actual PnL=VALUE(FILTERXML(WEBSERVICE(H2),"//outputs/actual_pnl"))
Delta PnL=VALUE(FILTERXML(WEBSERVICE(H2),"//outputs/delta_pnl"))
Gamma PnL=VALUE(FILTERXML(WEBSERVICE(H2),"//outputs/gamma_pnl"))
Vega PnL=VALUE(FILTERXML(WEBSERVICE(H2),"//outputs/vega_pnl"))
Theta PnL=VALUE(FILTERXML(WEBSERVICE(H2),"//outputs/theta_pnl"))
Rho PnL=VALUE(FILTERXML(WEBSERVICE(H2),"//outputs/rho_pnl"))
Vanna PnL=VALUE(FILTERXML(WEBSERVICE(H2),"//outputs/vanna_pnl"))
Volga PnL=VALUE(FILTERXML(WEBSERVICE(H2),"//outputs/volga_pnl"))
Residual=VALUE(FILTERXML(WEBSERVICE(H2),"//outputs/residual_pnl"))

Expected output:

BucketValue
price_t_minus_12.288743
price_t3.448862
Actual PnL11.601
Delta PnL7.125
Gamma PnL0.744
Vega PnL3.710
Theta PnL-0.333
Rho PnL0.0
Vanna PnL0.343
Volga PnL0.031
Explained PnL11.621
Residual-0.020

The residual_pnl captures higher-order effects and model differences between t-1 and t. Enable cross_greeks=true to include vanna and volga contributions.


Example 4: Portfolio / Bulk Greeks

For book-level aggregation, use the POST /v1/portfolio/greeks endpoint via Power Query or a small VBA helper. The endpoint accepts a JSON body with multiple legs and returns per-leg and aggregate Greeks.

See docs/api.md for the full request/response schema.


Greek Conventions

GreekUnitNotes
DeltaabsolutePer $1 spot move
GammaabsolutePer $1 spot move
Vegaper 1 vol pointi.e. decimal vol × 100
Thetaper calendar dayForward P&L of one calendar day passing (1-calendar-day revalue − today's price). Negative for a decaying long option.
Rhoper 1% rate pointi.e. decimal rate × 100
Charmper calendar day∂delta/∂t (delta at t − 1/365 − delta today). Negative for a long call — delta decays toward expiry

Log Location and Structured Logging

  • Log path: DESKPRICER_LOG_DIR env var overrides the default (C:\ProgramData\DeskPricer\logs on Windows, ~/.local/share/deskpricer/logs elsewhere).
  • Format: Uses Python's stdlib logging module with a custom JSON formatter and RotatingFileHandler (10 MB rotation, 5 backups). This replaces the earlier hand-rolled open() approach.
  • Change the path:
    $env:DESKPRICER_LOG_DIR = "C:\MyLogs"
    python -m deskpricer.main
    

Troubleshooting

QuantLib install failures

If pip install fails on QuantLib, ensure you have a C++ compiler and CMake, or use a pre-built wheel. See docs/operator_guide.md for detailed steps.

Zero-DTE surprises

t=0 is floored to 1 calendar day to prevent QuantLib collapse. You will get a small time-value premium rather than pure intrinsic. This is intentional.

Engine/style mismatches

  • style=european → only engine=analytic
  • style=american → only engine=binomial_crr or binomial_jr

XML vs JSON

Excel receives XML by default. For JSON, send Accept: application/json or ?format=json.

Error codes

CodeMeaning
INVALID_INPUTBusiness-rule or schema validation failed
UNSUPPORTED_COMBINATIONEngine/style mismatch
PRICING_FAILUREUnexpected internal error (no traceback leaked)

Limitations

  • No FD engine — closed-form BSM for Europeans and equivalent Americans; binomial CRR/JR for other Americans.
  • Concurrent QuantLib via process pool — default min(4, cpu_count()) workers (DESKPRICER_WORKERS). Portfolio legs batch in a single worker call per request.
  • Bounded bump rangesbump_spot_rel ≤ 0.1, bump_vol_abs ≤ 0.01, bump_rate_abs ≤ 0.01.
  • No database or persistence — all state is in-memory per-request.
  • No auth, TLS, or rate limiting — local-only by design.

Running Tests

pytest tests -v

Design Decisions

Local-only by design

DeskPricer is built as a personal desktop tool, not a public API. This explains every intentional omission:

  • No authentication / authorization — only 127.0.0.1 can reach the service.
  • No TLS / HTTPS — local loopback traffic is unencrypted by design.
  • No rate limiting — no throttling; concurrent requests are handled by a process pool rather than serialized on a single QuantLib global state.
  • No Swagger / Redoc — OpenAPI docs are hidden in production builds to reduce attack surface.
  • XML by default — Excel's WEBSERVICE function does not send Accept: application/json.

If you need any of these features, DeskPricer is the wrong tool. Use a proper API gateway or a full-featured pricing platform.

Why QuantLib runs in a process pool

QuantLib's Python bindings rely on a single process-global Settings.instance() object. Rather than serializing all requests with an asyncio.Lock, DeskPricer dispatches QuantLib work to a ProcessPoolExecutor. Each worker process has its own isolated settings, so concurrent agent or Excel calls can price in parallel without corrupting evaluation dates. Pool size defaults to min(4, cpu_count()) and is configurable via DESKPRICER_WORKERS.

Europeans and economically equivalent Americans bypass QuantLib entirely and use a pure-Python BSM implementation (bsm_fast), validated against QuantLib to six decimal places.

Why PnL attribution uses GET with many query params

Excel's WEBSERVICE function only supports HTTP GET. Since the primary user of this service is Excel, the GET /v1/pnl_attribution endpoint is designed specifically for WEBSERVICE compatibility. Programmatic clients that need a cleaner JSON body can use POST /v1/portfolio/greeks today; a POST alternative for PnL attribution may be added in a future release.


Project Structure

DeskPricer/
├── pyproject.toml
├── README.md
├── src/deskpricer/          # FastAPI app + pricing core
│   ├── app.py               # Thin composition root
│   ├── routers/             # APIRouter modules
│   ├── services/            # Pricing orchestration + QL lock
│   ├── pricing/             # QuantLib pricing engines
│   ├── schemas.py           # Pydantic models
│   ├── responses.py         # XML/JSON serializers
│   ├── errors.py            # Custom exceptions
│   ├── logging_config.py    # Structured JSON logging
│   └── main.py              # Uvicorn entrypoint
├── tests/                   # pytest + hypothesis
├── tests/fixtures/          # Regression baseline JSONs
├── scripts/                 # Build + fixture generation
├── sample/                  # Demo Excel workbook
└── docs/                    # API ref + operator guide

License

MIT

Related Servers