You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Framework only. The definitions and caveats are the parts worth holding. The
action signature and response shape are a starting point, not a decision.
Why this was created
The order book query surface covers the book, depth, and best prices, but not
activity. get_order_book, get_market_depth, and get_best_prices all answer
"what is resting right now". There is no action that answers "how much traded".
The data is already on the node. ob_order_events records every fill with
price, amount, outcome, participant, and block timestamp, so volume and unique
traders are computable without new state. What is missing is an action, and
therefore an SDK call.
Outcome
An SDK call returns volume and unique traders for a single order book over a
time period, in the same shape and at the same level as get_market_depth.
Current state
Nothing exists. The metrics are reachable today only by querying Postgres
directly, which requires node access and re-deriving the rule by hand each time.
Scope
Suggested action shape
Per bucket, matching get_market_depth. Callers that want a whole ladder
aggregate across its buckets, rather than the action taking a stream and a
settle time and baking the ladder structure into its signature.
CREATE OR REPLACE ACTION get_market_activity(
$query_id INT,
$from_ts INT8,
$to_ts INT8
) PUBLIC VIEW RETURNS TABLE(
bridge TEXT, -- units. volume is meaningless without it
volume_cents NUMERIC(78,0), -- the headline figure
direct_cents NUMERIC(78,0), -- counterparty trading
mint_burn_cents NUMERIC(78,0), -- boundary arbitrage
unique_traders INT,
fill_count INT, -- all fills, one row per economic event
direct_fill_count INT, -- subset: counterparty matches only
shares_traded INT8,
first_event_ts INT8, -- activity window actually observed
last_event_ts INT8,
coverage_from_block INT8, -- earliest retained event for this market
coverage_complete BOOL -- false if the market predates retention
)
PUBLIC VIEW, so read only, with no consensus or fee implications.
Why one object rather than a volume scalar
Every field above falls out of the same single pass over the event rows, so
returning them together costs nothing beyond the scan that volume already
requires. Splitting them into separate actions would multiply the scans.
Three of them earn their place for reasons worth stating:
bridge because volume is per collateral token and never summable across
tokens. A bare number invites exactly the addition the definition forbids.
direct_cents and mint_burn_cents because the split is diagnostic. Two
markets with identical volume behave very differently if one is counterparty
trading and the other is a single participant working the mint and burn
boundary. They respond to different stimuli, and a consumer charting volume
will want to know which it is looking at.
coverage_from_block and coverage_complete because they turn the
retention problem below from a silent wrong answer into a visible one.
A count is worth having next to unique_traders. A market with seventy small
matches and one with five large ones report the same volume and the same trader
count, and are not the same market.
Count fills on the same allowlist that defines volume. If a mint or a burn
counts as volume, it counts as a fill, for the same reason: it is an economic
decision with collateral moving. Excluding it from the count while including it
in the value would be inconsistent.
De-duplicate it the same way as well. Both patterns write two rows per
economic event, direct_buy_fill alongside direct_sell_fill for a match and
a YES row alongside a NO row for a mint or burn, so one row per event is the
count in both cases. direct_fill_count is then a subset of fill_count, and
the pair mirrors the direct_cents and mint_burn_cents split.
Definition to hold fixed
Volume is filled orders, counted once per trade, in cents of that market's
own collateral token.
Event
Volume
Counts as a trader
direct_buy_fill
price * amount
yes
direct_sell_fill
no, it is the same trade
yes
mint_fill, burn_fill
100 * amount, YES row only
yes
split_placed
no, it is a deposit
no
placements, cancels, amends, settled
no
no
Three points that are easy to get wrong:
Mint and burn are volume. They are two-party matches, not protocol
operations. Both sides agreed complementary prices, and the match settles by
creating or destroying a pair rather than transferring existing shares. They
are also the arbitrage boundaries that hold YES + NO near 100.
split_placed is not volume. It is unilateral: collateral in, pair out,
no counterparty and no agreed price. It moves open interest.
The line is mechanically checkable. Every fill carries a counterparty_id and nothing else does, so the allowlist can be validated
against the data rather than trusted.
Use an allowlist over event types, never a denylist. A denylist silently
admits any event type added later.
Volume is always per bridge and never summed across bridges. Base units
differ between collateral tokens by orders of magnitude, and this layer holds no
price feed to reconcile them. Unique traders, unlike volume, are comparable
across bridges.
Retention is the hard part
ob_order_events is ephemeral. trim_order_events() deletes rows once they
have been indexed, so the node holds a rolling window rather than full history.
A naive implementation returns 0 for a market whose events were trimmed, which
is indistinguishable from a market that genuinely had no activity. That silent
wrong answer is worse than a refusal.
Options worth weighing: return the coverage boundary alongside the figure so the
caller can tell truncated from empty, or ERROR when the requested range
precedes the earliest retained block. Either way the action should be explicit
that it answers for the retained window, not for all time.
Indexing
ob_order_events is indexed on id and block_height, but not on query_id.
A per-market aggregate is therefore a sequential scan today.
This is a public read that any caller can invoke and that every node executes,
so it should not be shipped without an index on (query_id, block_timestamp).
That index is cheap and helps the existing event queries as well.
Consistency with other implementations
The same metric is defined elsewhere in the stack. Wherever it is restated, the
implementations should be tested in lockstep against a shared fixture, meaning a
pinned market with a known figure, rather than each being retyped and trusted.
Out of scope
Historical metrics beyond the retained window. That belongs to a service that
keeps its own copy.
Protocol wide totals across bridges.
Charting endpoints and bucketed series.
Question
Is per bucket the right granularity, or should the action take a stream and a
settle time and aggregate the ladder itself? Per bucket matches the existing
query actions and keeps the signature stable if the ladder shape changes, at the
cost of callers making several calls.
Warning
@vinarmani this Goal has no ETA. Add an estimated completion date before work begins.
See the Wizard docs for guidance.
@vinarmani
Why this was created
The order book query surface covers the book, depth, and best prices, but not
activity.
get_order_book,get_market_depth, andget_best_pricesall answer"what is resting right now". There is no action that answers "how much traded".
The data is already on the node.
ob_order_eventsrecords every fill withprice, amount, outcome, participant, and block timestamp, so volume and unique
traders are computable without new state. What is missing is an action, and
therefore an SDK call.
Outcome
An SDK call returns volume and unique traders for a single order book over a
time period, in the same shape and at the same level as
get_market_depth.Current state
Nothing exists. The metrics are reachable today only by querying Postgres
directly, which requires node access and re-deriving the rule by hand each time.
Scope
Suggested action shape
Per bucket, matching
get_market_depth. Callers that want a whole ladderaggregate across its buckets, rather than the action taking a stream and a
settle time and baking the ladder structure into its signature.
PUBLIC VIEW, so read only, with no consensus or fee implications.Why one object rather than a volume scalar
Every field above falls out of the same single pass over the event rows, so
returning them together costs nothing beyond the scan that volume already
requires. Splitting them into separate actions would multiply the scans.
Three of them earn their place for reasons worth stating:
bridgebecause volume is per collateral token and never summable acrosstokens. A bare number invites exactly the addition the definition forbids.
direct_centsandmint_burn_centsbecause the split is diagnostic. Twomarkets with identical volume behave very differently if one is counterparty
trading and the other is a single participant working the mint and burn
boundary. They respond to different stimuli, and a consumer charting volume
will want to know which it is looking at.
coverage_from_blockandcoverage_completebecause they turn theretention problem below from a silent wrong answer into a visible one.
A count is worth having next to
unique_traders. A market with seventy smallmatches and one with five large ones report the same volume and the same trader
count, and are not the same market.
Count fills on the same allowlist that defines volume. If a mint or a burn
counts as volume, it counts as a fill, for the same reason: it is an economic
decision with collateral moving. Excluding it from the count while including it
in the value would be inconsistent.
De-duplicate it the same way as well. Both patterns write two rows per
economic event,
direct_buy_fillalongsidedirect_sell_fillfor a match anda YES row alongside a NO row for a mint or burn, so one row per event is the
count in both cases.
direct_fill_countis then a subset offill_count, andthe pair mirrors the
direct_centsandmint_burn_centssplit.Definition to hold fixed
Volume is filled orders, counted once per trade, in cents of that market's
own collateral token.
direct_buy_fillprice * amountdirect_sell_fillmint_fill,burn_fill100 * amount, YES row onlysplit_placedsettledThree points that are easy to get wrong:
operations. Both sides agreed complementary prices, and the match settles by
creating or destroying a pair rather than transferring existing shares. They
are also the arbitrage boundaries that hold YES + NO near 100.
split_placedis not volume. It is unilateral: collateral in, pair out,no counterparty and no agreed price. It moves open interest.
counterparty_idand nothing else does, so the allowlist can be validatedagainst the data rather than trusted.
admits any event type added later.
Volume is always per bridge and never summed across bridges. Base units
differ between collateral tokens by orders of magnitude, and this layer holds no
price feed to reconcile them. Unique traders, unlike volume, are comparable
across bridges.
Retention is the hard part
ob_order_eventsis ephemeral.trim_order_events()deletes rows once theyhave been indexed, so the node holds a rolling window rather than full history.
A naive implementation returns
0for a market whose events were trimmed, whichis indistinguishable from a market that genuinely had no activity. That silent
wrong answer is worse than a refusal.
Options worth weighing: return the coverage boundary alongside the figure so the
caller can tell truncated from empty, or
ERRORwhen the requested rangeprecedes the earliest retained block. Either way the action should be explicit
that it answers for the retained window, not for all time.
Indexing
ob_order_eventsis indexed onidandblock_height, but not onquery_id.A per-market aggregate is therefore a sequential scan today.
This is a public read that any caller can invoke and that every node executes,
so it should not be shipped without an index on
(query_id, block_timestamp).That index is cheap and helps the existing event queries as well.
Consistency with other implementations
The same metric is defined elsewhere in the stack. Wherever it is restated, the
implementations should be tested in lockstep against a shared fixture, meaning a
pinned market with a known figure, rather than each being retyped and trusted.
Out of scope
keeps its own copy.
Question
Is per bucket the right granularity, or should the action take a stream and a
settle time and aggregate the ladder itself? Per bucket matches the existing
query actions and keeps the signature stable if the ladder shape changes, at the
cost of callers making several calls.
Deadline
ETA: undefined