Write a pricing plugin¶
A pricing plugin, one holding the role pricing, answers the
analytics component's price
batches for the pricing models a deployment admin routes to it: a firm's
own implementation of a model, run as its own plugin behind the same API.
It is never called by a client, holds no accounts and keeps no records:
for each unit of a batch, one instrument under one scenario with exactly
the market objects its model declares, it answers a value per unit, and
nothing else. The role has a conformance suite, carried by the SDK. This
page starts one from the template and holds it to the suite.
You need CLI 0.1.60 and SDK 0.36.0 or later: meridian plugin
new --role pricing writes this template on the role's common part, on SDK
0.35.0, for a runtime on chart 0.1.408 or later; meridian plugin migrate moves it
to 0.36.0.
Make the plugin¶
It writes a whole plugin holding pricing, pinned to
open-meridian==0.35.0, that answers quoted_price batches with the
built-in model's own computation:
| File | What it is |
|---|---|
src/my_pricing/model.py |
Its models, by model id. Replace them with your own. |
src/my_pricing/__main__.py |
Connects to its sidecar and runs the SDK's Pricer over the models: the batches still waiting, then each batch as it is routed, until it is stopped. |
src/my_pricing/declaration.py |
Its declaration: its page and its documentation. It declares no storage: pricing is not a role at the edge. |
src/my_pricing/page.py, templates/ |
Its one page under Manage, Batches: the model ids it implements, the batches answered since it started, the last sequence answered, and each batch left unanswered with why. Counts and codes, never a price. Also a read tool for an agent, read_batches. |
tests/test_suite.py |
The pricing suite, each case run through the Pricer over the plugin's own models. |
tests/test_page.py |
Its page's tests. |
docs/ |
The documentation a deployment admin reads before launching it: see Write a plugin's documentation. |
AGENTS.md teaches a coding agent the role's rules beside the template's
own.
What a batch carries¶
A batch (PriceBatchRequest) names its model_id and model_version, a
deadline_ns, a batch_hash naming it, its place in the plugin's
sequence, and its units. Each unit (PricingUnit) is one instrument
under one scenario:
| Field | What |
|---|---|
instrument_id, template_version |
The instrument, and the version of the template its record names |
scenario_key |
The scenario it is priced under: the base, or another the batch names |
inputs |
Exactly the market objects its model declares, each a MarketObjectValue: its subject_id, the lake's kind (close, nav, mid, last, or outcome), its value, a Money in the asset it is quoted in, and the dataset it came from |
For quoted_price, the inputs are the instrument's price, of the kind its
template names, then each conversion leg to the account's base currency:
the price of the currency in hand in the next. For cash, the conversion
legs alone. For binary_event, once the venue's determination is final,
an outcome input, the settlement value per contract in the payout's
currency, takes the price's place.
Write a model¶
A model is a function of one unit, (batch, unit) -> meridian.Money: the
unit's value per unit, in the base currency its inputs convert to. The
template's model.py maps model ids to models:
from meridian.roles import pricing
MODELS: dict[str, pricing.Model] = {"quoted_price": pricing.reference_model}
pricing.reference_model is the built-in models' own computation for
cash, quoted_price and binary_event. A model of your own reads the
unit's inputs and nothing else, every number a Decimal, never a float.
This one is quoted_price written out, the price times each conversion
leg, until none leads further:
import meridian
from meridian.plugin.v1 import operations_pb2 as ops
def quoted_price(batch: ops.PriceBatchRequest, unit: ops.PricingUnit) -> meridian.Money:
prices = {}
for each in unit.inputs:
money = meridian.as_money(each.value)
prices[each.subject_id] = (money.amount, money.instrument_id)
value, held = prices[unit.instrument_id]
while held in prices: # each conversion leg, until none leads further
rate, held = prices[held]
value *= rate
return meridian.Money(amount=value, instrument_id=held)
MODELS: dict[str, pricing.Model] = {"quoted_price": quoted_price}
The rules a model keeps:
- Answer in the base currency, naming it. The value's
instrument_idis the cash instrument the unit's last conversion leg is in. A value in another asset is not taken, and the figure through it is not evaluated. - Equal the built-in model on its model id. A v26 model id is defined by the built-in implementation, and the suite holds your answer to it exactly, digit for digit.
- Read nothing else. No position, no account, no other price: the batch carries everything the model declares.
- Raise rather than guess. A model raising for a unit leaves the whole
batch unanswered, and the figures through it are not evaluated,
IMPLEMENTATION_UNAVAILABLE: never half a batch. A batch for a model id the plugin does not implement is left unanswered too. Either is listed on the Batches page with why. - Send no sensitivities. No model in this release declares one, so
the
Priceranswers each batch with none.
The Pricer's work¶
meridian.roles.pricing.Pricer does the rest:
- Each batch answered whole, with
answer_price_batch: aPricedUnitper unit, its instrument, its scenario and its value per unit. - After a restart, the batches still waiting after the last sequence it
answered, with
list_price_batches, then each batch as it arrives. It keeps that sequence in memory (pricing.Sequence()), since the plugin holds no storage, so a restart reads every batch still waiting. A batch answered twice is harmless: named by its hash, it is taken once, and an answer after its deadline is not taken at all.
Hold it to the suite¶
tests/test_suite.py runs the pricing suite through the Pricer over your
models, against a Recorder that records the plugin's operations rather
than sending them. Its five cases:
| Case | Given | Expected |
|---|---|---|
answers-a-batch |
A quoted_price batch, one instrument under the base scenario with its close |
An answer naming the batch, with the instrument, its scenario and a value per unit |
answers-every-scenario-of-a-batch |
One instrument under several historical days' closes | A value per unit for each day, under its scenario |
equals-the-built-in-model |
The equity template's test case: a close of 227.50 USD | 227.50 USD per unit, exactly |
carries-no-sensitivity |
A model declaring no sensitivity | No sensitivities |
reads-the-batches-waiting-after-a-restart |
A restart with batches routed and unanswered | The batches read after the last sequence answered |
As you change the models, keep every case passing. Then hold the plugin to the framework's rules as a verified plugin, in a virtual environment of its own:
python3 -m venv .venv && . .venv/bin/activate
pip install -e . pytest
meridian plugin check --verified --run-tests
--verified fails role-suite where no test runs the pricing suite, and
--run-tests runs it; see plugin check.
Put it in a deployment¶
or, on a development deployment, meridian plugin dev --instance
my-pricing-1. Launched, it answers nothing until a deployment admin
routes a model id to it: on the dashboard's Settings, Model routing
tab, Change that model id's implementation to my-pricing-1, saying
why, or through dashboard__set_model_routing with a note. The routing is
deployment-wide, per model id, and each change is its own record. Every
job's answer then names my-pricing-1 as that model's implementation in
its provenance.
A routed plugin must answer in time. A batch not answered by its
deadline, five seconds by default, leaves every figure through the plugin
not evaluated, naming it; the built-in model is never put in its place.
Before stopping a routed plugin, route its model ids back to built-in.
Related¶
- The analytics component: jobs, the measures, the models and the pricing seam.
- The
pricingrole. - The analytics component's operations.
- Each role's common logic in the Python SDK.
- Release a plugin version and Publish a plugin to a registry.