A venue's capability declaration — the return of DpExchange.Core.Venue.capabilities/0,
and the thing a consumer branches on instead of on venue identity.
Three kinds, and conflating them is the easy mistake
Kind 1 — activation and maturity. Is this function answerable, and is it proven?
One map from facade function to :proven | :experimental | :unsupported. This is the
only kind that turns anything on or off.
Kind 2 — domain. For an active function, which arguments are valid? Which order types the venue takes, which quotes it lists, which candle widths it serves. These constrain a call; they do not gate one.
Kind 3 — parameters. Limits and shapes of an active function. Page sizes, rate ceilings, whether volume is reported. These tune behaviour and gate nothing.
Kind 1 is a map, not a field per endpoint
Three states cannot fit in a boolean, and a field per endpoint drifts the moment the facade grows: the field gets added, some venue forgets it, and the default decides. One map cannot fall out of step with the facade, and the conformance suite drives its bidirectional assertion straight off it.
Anything not named in the map is :experimental — the only honest default. Not
:unsupported, which would claim a refusal the venue never made, and certainly not
:proven, which is earned by production use rather than by careful implementation.
What is never declared: transport
There is no has_websocket, no websocket_module, no stream_channels and no
pairs_per_socket, and their absence is deliberate. Both endpoints exist on every
venue — every venue can be pulled and can be subscribed — so there is nothing to
declare and nothing for a consumer to branch on.
Those four fields existed because a host was starting and sharding the venue's
connections and needed to be told how. It no longer is. What a caller legitimately needs
is which kinds of data stream, not which channels carry them: "level2" is one venue's
word, :order_book is everyone's. See data_kind/0.
What is not here because it is not the venue's call
auto_collect, default_quotes and overview_suits_collection were in the declaration
and are not any more. They are consumer collection policy: which quote asset to
collect and what that costs in storage is a business decision, and no venue package ever
read them.
The split is clean. A venue declares what it can serve — supported_quotes, and a
catalog_size class so a consumer knows one venue lists thousands and another millions.
A consumer decides what it will collect.
Summary
Types
Roughly how many instruments the venue lists.
Requests per interval, as the venue publishes it.
What credentials buy on this venue.
A kind of data a subscription can deliver — normalised, so a consumer never learns a venue's channel vocabulary.
How well one endpoint is known.
Functions
Whether an endpoint is answerable at all.
The normalised data kinds a subscription can deliver.
Every endpoint declared at maturity.
The maturity vocabulary.
The maturity of one endpoint.
Builds a declaration, validating what a struct alone cannot express.
Types
@type catalog_size() :: :small | :large | :vast | :unknown
Roughly how many instruments the venue lists.
A class rather than a count, because the count changes daily and the decision it informs does not. It exists so a consumer can tell that re-pulling one venue's catalogue on a timer is fine and another's is not — the difference between a thousand instruments and millions is a different strategy, not a different number.
@type ceiling() :: %{limit: pos_integer(), per_ms: pos_integer()} | nil
Requests per interval, as the venue publishes it.
There are two of these on a venue with a better authenticated path, and which applies depends on what the caller supplied. Carrying one number means a package holding credentials meters itself against the public limit and leaves most of its budget unused.
@type credential_benefit() :: :no_difference | :higher_ceiling | :required
What credentials buy on this venue.
A boolean could not say this. The old field answered "does this venue reject public calls without credentials", which has two states, while the real question has three — and the middle one is the common case:
:no_difference— public data is served, and credentials change nothing about it.:higher_ceiling— public data is served, but the authenticated path has a materially higher rate limit. A package holding credentials should be using it.:required— public data is not served at all without credentials.
@type data_kind() :: :quotes | :order_book | :trades | :orders | :fills | :balances
A kind of data a subscription can deliver — normalised, so a consumer never learns a venue's channel vocabulary.
@type maturity() :: :proven | :experimental | :unsupported
How well one endpoint is known.
:experimental is the default and the only honest starting state. :proven is earned
per endpoint by production use.
@type t() :: %DpExchange.Core.Capabilities{ authenticated_ceiling: ceiling(), authenticated_streamable: [data_kind()], catalog_size: catalog_size(), credential_benefit: credential_benefit(), endpoints: %{optional({atom(), arity()}) => maturity()}, historical_timeframes: [String.t()], max_candles_per_request: pos_integer() | nil, max_leverage: Decimal.t() | nil, measured_against: String.t() | nil, measured_at: Date.t() | nil, public_ceiling: ceiling(), reports_trade_volume: boolean(), streamable: [data_kind()], supported_instrument_types: [atom()], supported_order_types: [atom()], supported_quotes: [String.t()], supported_time_in_force: [atom()], supports_fractional_shares: boolean(), supports_margin: boolean(), supports_short_selling: boolean() }
Functions
Whether an endpoint is answerable at all.
:proven and :experimental both mean it works — maturity says how well a thing is
known, never whether it runs.
Examples
iex> caps = DpExchange.Core.Capabilities.new(
...> endpoints: %{{:place_order, 3} => :unsupported},
...> supported_quotes: []
...> )
iex> DpExchange.Core.Capabilities.active?(caps, {:place_order, 3})
false
@spec data_kinds() :: [data_kind()]
The normalised data kinds a subscription can deliver.
Every endpoint declared at maturity.
@spec maturities() :: [maturity()]
The maturity vocabulary.
The maturity of one endpoint.
Anything undeclared is :experimental — a venue that forgot to mention an endpoint has
not thereby claimed it is proven, nor refused it.
Examples
iex> caps = DpExchange.Core.Capabilities.new(endpoints: %{}, supported_quotes: [])
iex> DpExchange.Core.Capabilities.maturity(caps, {:never_declared, 1})
:experimental
Builds a declaration, validating what a struct alone cannot express.
Raises rather than returning an error tuple: this runs at venue-package definition time, where a wrong value is a programming error rather than a runtime condition — and a declaration that is wrong is worse than one that is missing, because a consumer will act on it.
Examples
iex> caps = DpExchange.Core.Capabilities.new(
...> endpoints: %{{:get_price, 2} => :proven},
...> supported_quotes: ~w(USD)
...> )
iex> DpExchange.Core.Capabilities.maturity(caps, {:get_price, 2})
:proven
iex> DpExchange.Core.Capabilities.new(
...> endpoints: %{{:get_price, 2} => :probably_fine},
...> supported_quotes: ~w(USD)
...> )
** (ArgumentError) endpoint {:get_price, 2} declares unknown maturity :probably_fine — must be one of [:proven, :experimental, :unsupported]