Using dp_exchange_core

Copy Markdown View Source

EXPERIMENTAL. This package has not run in production. While it is 0.x a minor version may break you — pin all three segments (~> 0.1.0). Maturity is declared per endpoint through capabilities/0, not per package: read that, not this banner.

dp_exchange_core is the contract for the DpExchange family. It talks to no exchange. It defines what a venue package is, so a consumer can drive any venue through one identical facade and add a new one without editing anything outside its own repo.

The one thing to understand first

Every venue package exposes the same facade, and nothing crosses it. Transport, rate limiting, credential handling, session lifecycle and supervision are internal to a venue package. You cannot tell from the facade whether data arrives over a WebSocket, an MQTT session or a polling loop, and you must not try to find out.

What differs between venues is which facade functions are active, declared once in capabilities/0. Branch on capabilities, never on venue identity. A case venue do in your code is the coupling this contract exists to remove.

As a consumer

# In your supervision tree. Nothing starts itself — a package you have not asked for
# never opens a connection.
children = [
  {DpExchange.Coinbase, credentials: coinbase_credentials()}
]

# Pull.
{:ok, %DpExchange.Core.Types.Quote{} = quote} = DpExchange.Coinbase.get_price("BTC-USD", [])

# Push. The same struct arrives as a message; one handler serves both.
:ok = DpExchange.Coinbase.subscribe(["BTC-USD"], to: self())
receive do
  {:dp_exchange, :coinbase, %DpExchange.Core.Types.Quote{} = quote} -> handle(quote)
end

Both endpoints exist on every venue. A venue with no streaming API implements subscribe/2 over its own polling and pushes the results. There is no flag for this and nothing to branch on.

Three return shapes, and the third matters

ShapeMeaningWhat to do
{:ok, value}it workedproceed
{:refused, reason}the venue does not carry this — permanentstop asking
{:error, reason}it failed, possibly transientlyretry as your policy allows

Collapsing :refused into :error makes a delisted symbol look like a network blip forever. Collapsing it into :ok is worse.

{:error, :not_supported} is the atom

Never the string. Match the atom.

Read capabilities/0 before you rely on anything

caps = DpExchange.Coinbase.capabilities()

case DpExchange.Core.Capabilities.maturity(caps, {:place_order, 3}) do
  :proven -> place_it()
  :experimental -> place_it_but_watch()
  :unsupported -> use_something_else()
end

:proven and :experimental both mean it works — maturity says how well a thing is known, never whether it runs. :unsupported means the function exists and returns {:error, :not_supported}; it will not raise and will not quietly return degraded data.

Anything undeclared is :experimental. That is the honest default, not a claim.

Coverage is observed, never intended

coverage/1 reports what is actually arriving, by route. It is not a list of what you subscribed to. A venue that cannot observe delivery answers :not_covered rather than reporting a success it cannot see.

This is the strongest guarantee in the contract, and it exists because a venue once reported 325 symbols subscribed and confirmed while 174 were delivering.

Notices: a prompt to re-read, never the record

subscribe_notices/1 carries what the package says about itself — link state, credentials rejected, sustained rate limiting, coverage change, catalogue change.

Delivery is not guaranteed, and your correctness must not depend on it. A :catalog_change notice is a reason to call list_instruments/1; it is not the authority that a pair was delisted. Treat a notice as a nudge and a dropped message costs latency. Treat it as the record and a dropped message is silent, wrong state — which has happened: two symbols suspended at 03:14 and 03:27 opened fresh positions at 21:46 because the message that would have stopped them vanished with a restarting process.

Credentials

Passed as arguments, per call. A package never reads them from a vault, an environment variable or your config, and never stores them. Notices never carry them.

Detailed guides