Local options pricing for Excel and AI agents — HTTP API and MCP server for Greeks, IV, and PnL attribution
Project description
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
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:
| Sheet | What it shows |
|---|---|
| Greeks | Bitcoin European Call — $75K spot, $100K strike, 3M expiry, 50% vol |
| ImpliedVol | Back out ~68.3% implied vol from a $3,398.71 market price |
| PnL Attribution | Decompose 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+FILTERXMLwork out of the box; JSON available viaAccept: application/json - Localhost-only — binds to
127.0.0.1; no network exposure
Conventions
| Greek | Unit / Convention |
|---|---|
delta |
Absolute (∂V/∂S) |
gamma |
Absolute (∂²V/∂S²) |
vega |
Per 1% vol point |
theta |
Per calendar day (ACT/365). Negative for typical long options (time decay). Sign is opposite of Bloomberg DM, which reports theta as positive decay. |
rho |
Per 1% rate point (risk-free rate only; no dividend-yield or borrow-cost rho) |
charm |
Per 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:
| Cell | Formula |
|---|---|
| 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:
| Column | Label | Example Value |
|---|---|---|
| C | Spot | 100 |
| K | Strike | 105 |
| T | Time to expiry (years) | 0.25 |
| R | Risk-free rate | 0.05 |
| Q | Dividend yield | 0.02 |
| B | Borrow cost (optional, default 0.0) | 0.0 |
| V | Volatility | 0.20 |
| TYPE | Option type | call |
| STYLE | Style | european |
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:
| Output | Formula |
|---|---|
| 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:
| Greek | Value |
|---|---|
| Price | 2.288743 |
| Delta | 0.356244 |
| Gamma | 0.037206 |
| Vega | 0.185519 |
| Theta | -0.033315 |
| Rho | 0.083111 |
| Charm | -0.001241 |
Tip: Wrap each
FILTERXMLinIFERROR(...,"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:
| Field | t-1 Value | t Value |
|---|---|---|
| Spot | 100 |
102 |
| Time | 0.25 |
0.2466 |
| Vol | 0.20 |
0.22 |
| Rate | 0.05 |
0.05 |
| Div | 0.02 |
0.02 |
| Borrow | 0.0 |
0.0 |
| Qty | 10 |
— |
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:
| Bucket | Formula |
|---|---|
| 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:
| Bucket | Value |
|---|---|
| price_t_minus_1 | 2.288743 |
| price_t | 3.448862 |
| Actual PnL | 11.601 |
| Delta PnL | 7.125 |
| Gamma PnL | 0.744 |
| Vega PnL | 3.710 |
| Theta PnL | -0.333 |
| Rho PnL | 0.0 |
| Vanna PnL | 0.343 |
| Volga PnL | 0.031 |
| Explained PnL | 11.621 |
| Residual | -0.020 |
The
residual_pnlcaptures higher-order effects and model differences between t-1 and t. Enablecross_greeks=trueto 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
| Greek | Unit | Notes |
|---|---|---|
| Delta | absolute | Per $1 spot move |
| Gamma | absolute | Per $1 spot move |
| Vega | per 1 vol point | i.e. decimal vol × 100 |
| Theta | per calendar day | Forward P&L of one calendar day passing (1-calendar-day revalue − today's price). Negative for a decaying long option. |
| Rho | per 1% rate point | i.e. decimal rate × 100 |
| Charm | per 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_DIRenv var overrides the default (C:\ProgramData\DeskPricer\logson Windows,~/.local/share/deskpricer/logselsewhere). - Format: Uses Python's stdlib
loggingmodule with a custom JSON formatter andRotatingFileHandler(10 MB rotation, 5 backups). This replaces the earlier hand-rolledopen()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→ onlyengine=analyticstyle=american→ onlyengine=binomial_crrorbinomial_jr
XML vs JSON
Excel receives XML by default. For JSON, send Accept: application/json or ?format=json.
Error codes
| Code | Meaning |
|---|---|
INVALID_INPUT |
Business-rule or schema validation failed |
UNSUPPORTED_COMBINATION |
Engine/style mismatch |
PRICING_FAILURE |
Unexpected 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 ranges —
bump_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.1can 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
WEBSERVICEfunction does not sendAccept: 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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file deskpricer-3.4.0.tar.gz.
File metadata
- Download URL: deskpricer-3.4.0.tar.gz
- Upload date:
- Size: 71.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
42c3c3608c9f3697b499fc6b188ed538e1c66a84b943bd509daba84c804c5ffc
|
|
| MD5 |
447e1a6412b5f896190875e4861ee90f
|
|
| BLAKE2b-256 |
ca447e9207626af4c2cfce91ba7f6f80464c0b389a98c39e8253e95563570707
|
File details
Details for the file deskpricer-3.4.0-py3-none-any.whl.
File metadata
- Download URL: deskpricer-3.4.0-py3-none-any.whl
- Upload date:
- Size: 49.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b09b95eac3d33b9cbfada7ec3f282076288601740f7d8ea22d8ec1e120335b07
|
|
| MD5 |
89e8aed1a7b38584fb0a861eb7d75f23
|
|
| BLAKE2b-256 |
e5c2e4e2ed76c937a63483862e2986b158e3d939a84e4e238f369cb4830a5f07
|