Keep what your custody plugin converts¶
A plugin at the edge speaks to a vendor and converts what the vendor says into the contract's words. The contract asks it to keep what it converted from ("the edge keeps its own"), say how it came by every value the vendor did not send, and count each asset once, so that a reader written from the contract alone reconciles any custodian the same way. This page shows what a custody plugin does for each, with plugin-snaptrade as the worked example.
You need CLI 0.1.45 and SDK 0.28.0 (contract 24).
A custody plugin also reports each activity on an account, each with a raw record of its own: see Report the custodian's activity.
Declare what the version is¶
A version's declaration says three things beside its roles: the names of the secret settings it will ask for, what it receives from its vendor and does not carry, and the storage it asks for. It never holds a value, an account or an identifier.
from meridian.declaration import Declaration, NotCarried, Storage
DECLARATION = Declaration(
settings=SETTINGS, # its secret settings' names are declared, never their values
not_carried=(
NotCarried("custody", "myvendor:position", "open_pnl", "no_contract_meaning"),
),
storage=Storage(retention_days=30),
)
Name it in pyproject.toml, and register with it:
meridian plugin upload runs the SDK's meridian-declaration in the image it built, with no
network, and sends what it prints with the version, so a deployment admin sees it before launch.
A plugin holding no edge role (connectivity, custody, data, confirmation, reporting, servicing,
settlement) may not ask for storage: upload refuses it, meridian plugin check fails it
(edge-storage), and the deployment refuses it at registration.
Each time the plugin meets a name it declared as not carried, it counts it:
plugin.note_not_carried("myvendor:position", "open_pnl"). The counts ride on its heartbeat and
are drawn on its Summary, by name only, and they stay in the deployment.
Keep the raw record, and name it on every row¶
A deployment grants a plugin holding an edge role storage of its own, kept across restarts and new
versions and reached by no other plugin. The SDK says where: meridian.edge.storage_dir(), or
None where none is granted. Keep what the vendor answered there, by your own key, for as long as
your declaration says.
Every row and statement then names the raw record it was converted from:
await plugin.record_holding(
...,
raw_record=plugin.raw_record("ACCT-1/20261003T120000Z/positions"),
)
The key is yours and opaque past your plugin; the sidecar fills in your instance, and refuses a reference naming another's. A person follows it on your plugin's own page.
Declare its kinds, and move them past their window¶
A version declares the kinds of raw record it keeps, each with its default window, and the deployment's admins decide what is done past each window (see The archive):
from meridian.declaration import Declaration, RecordKind, Storage
DECLARATION = Declaration(
settings=SETTINGS,
storage=Storage(kinds=[
RecordKind("activity", "Reported activity", window_days=2555),
RecordKind("responses", "Raw responses", window_days=30),
]),
)
- Declare no window setting of your own. The SDK declares
<kind>_window_daysand<kind>_past_windowfor each kind, and refuses a plugin that takes either name;meridian plugin checksays so first (window-settings). - Keep each kind in units you can find again, a file or directory per account and day or month, and name a record by its path within its unit.
- When a unit's last record is past its window, do what
<kind>_past_windowsays:archived,plugin.archive_unit(...);deleted,plugin.delete_unit(...);kept, nothing. Never remove a unit any other way: the SDK reports each move before it removes anything, and a deletion inside the deployment's hold is refused,CommandRefusedwithREFUSAL_REASON_WITHIN_HOLD, and the unit kept. - Say what each kind holds in storage whenever it changes:
plugin.stored. - On your page, resolve a row's record with
plugin.find_record(key), and offer a restore under Open as a form postingrecord_kindandunitto/archive/restore, which the SDK declares.
Each call, argument by argument, is in The archive.
Say how you came by what the vendor did not send¶
A value your vendor did not send, and you closed, carries its provenance:
from meridian import edge
provenance=[
edge.derived("settle_date_quantity", "the quantity less the account's trades not settled"),
]
edge.derived(field, rule) for a value derived by a named rule, edge.supplied(field, person) for
one a person gave on your page, edge.second_source(field, source) for one from a second source of
your own, and edge.reported(field, raw_record) for one the vendor reported in another raw record.
It is never presented as the vendor's word.
A value the vendor sent that you cannot convert travels as the field's not-known value, with the
vendor's beside it: edge.as_reported(scheme, code, text). An account whose type says neither
cash, margin nor retirement is reported with account_kind unset and
account_kind_as_reported=edge.as_reported("myvendor:account-type", "INDIVIDUAL");
venue_account_type is deprecated.
A currency your source never states. Core requires a security's
currency, and the plugin gives it, always saying how: as the source states
it, derived by a named rule (edge.derived), or supplied by a person. A plugin whose source states none declares the common setting
meridian.account_currency(), Account currency: a linked account's
currency, with a plugin-wide default, which its admin sets.
edge.supplied_currency(rows, external_account_id, field) answers the
account's code with its provenance, supplied by the person who set the row
(PROVENANCE_KIND_SUPPLIED), and nothing while no row covers the account:
the plugin then reports nothing that needs the currency, never a guess.
The custody suite's currency-not-stated passes on either way, derived
with its rule or supplied with its person (any_of).
Count each asset once¶
The street counts each asset once. A custodian that counts a money market fund both as a holding
and inside the cash of its currency is yours to net: send the fund as the holding and the cash net
of it, with the provenance edge.derived("quantity", <your rule>), and keep the custodian's gross
cash in your raw records. Where you cannot serve the statement clean (no price for the fund, or a
fund worth more than the cash), withhold it and say why on your page. also_counted_in_cash and
currency_assumed are deprecated.
Whether a position is cash at all is yours to decide too, from what your vendor says: the street
and operations assume how cash and a money market fund behave, never how a vendor books a record.
Decide from the vendor's own data first, and where it says nothing, from a setting an admin of the
plugin fills, carrying the row's changed_by and changed_at as edge.supplied; never from what
a symbol looks like. A money market fund stays a fund. SnapTrade's
cash links are an
example.
The settled quantity and the pending quantities by value date (meridian.ReportedPending) are the
custody role's: state them as the vendor reports them, or close them with their provenance.
When a later contract adds a field, re-send a row from your raw records with
backfill=edge.backfill("v11", "raw_record"); the street journals it beside the row as first
recorded.
Pass the role's suite¶
A plugin holding custody is verified for it only by passing every case of the custody suite,
which the SDK carries. Map each case to your own exchange with your vendor, run your own conversion
against the runner's recorder, and assert the report:
from meridian.suites import run
def test_every_case_of_the_custody_suite_passes() -> None:
report = run("custody", PRODUCERS, not_presented=NOT_PRESENTED)
assert report.passed, report.failures
A case asserting one value of a closed list your vendor never presents may be named in
not_presented, with why; so may reads-a-calendar and asks-for-a-settlement-date, about calendars and settlement dates, where
your plugin's work never uses them:
NOT_PRESENTED = {
"reads-a-calendar": "it reports what the custodian states, and reads no calendar",
"asks-for-a-settlement-date": "it reports the custodian's settlement dates as stated",
}
So may the cases about things a custodian may never state, each marked about: the
three activity plan-code cases (plan fund codes),
cash-net-of-a-fund-counted-in-cash and fund-larger-than-cash (sweep
funds), activity-moves-no-cash (splits), and currency-not-stated
(currencies not stated), for a custodian that always states the
currency. A broker with no plan fund codes and no cash sweep marks those,
with why.
Every other case needs a producer. The test running the suite may not skip or expect a failure, and asserts report.passed in the
next statement. meridian plugin check --verified
fails a custody plugin with no such test (role-suite), and --run-tests runs it; without
--verified the rule is reported skipped. A data plugin passes its own suite the same way: see
Write a data plugin against its suite.