DpExchange.Core.Capabilities (DpExchangeCore v0.1.1)

Copy Markdown View Source

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 :experimentalthe 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 servesupported_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.

t()

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

catalog_size()

@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.

ceiling()

@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.

credential_benefit()

@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.

data_kind()

@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.

maturity()

@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.

t()

@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

active?(declaration, endpoint)

@spec active?(
  t(),
  {atom(), arity()}
) :: boolean()

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

data_kinds()

@spec data_kinds() :: [data_kind()]

The normalised data kinds a subscription can deliver.

endpoints_at(capabilities, maturity)

@spec endpoints_at(t(), maturity()) :: [{atom(), arity()}]

Every endpoint declared at maturity.

maturities()

@spec maturities() :: [maturity()]

The maturity vocabulary.

maturity(capabilities, endpoint)

@spec maturity(
  t(),
  {atom(), arity()}
) :: maturity()

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

new(fields)

@spec new(keyword() | map()) :: t()

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]