PaperEx.Adapter behaviour (PaperEx v0.8.0)

Copy Markdown View Source

Behaviour an exchange adapter implements so PaperEx can stay exchange-agnostic.

The adapter is a translation layer, not a network client. paper_ex does no IO; it never fetches a market or an order book. Adapters turn exchange-specific data the caller already has into normalized paper_ex structs (PaperEx.Instrument, PaperEx.MarketSnapshot, PaperEx.Fill), and optionally provide an exchange-specific fill simulator.

Required callbacks

Optional callbacks

  • normalize_fill/2 — map an exchange-specific filled-trade payload onto a PaperEx.Fill. Use this when an adapter receives actual fills (e.g. for live-mirror reconciliation) rather than simulating its own.
  • simulate_fill/3 — simulate a fill for an order against a snapshot. Though listed in @optional_callbacks, it is effectively required for any adapter the engine simulates against: without it PaperEx.Engine.apply_order/4 returns {:error, :adapter_does_not_simulate}. It is optional only for adapters that merely normalize/reconcile actual exchange fills (via normalize_fill/2) and never ask the engine to simulate.
  • fee/2 — compute the fee for a Fill. If absent the engine treats fee as 0.
  • reason_codes/0 — return a bounded list of adapter-specific reason-code atoms. The engine never enforces this list; it's a documentation/analytics contract between adapter and consumer. See PaperEx.ReasonCodes for the generic engine codes that apply regardless of adapter.

Bounded reason codes

Reason codes — both engine ones from PaperEx.ReasonCodes and adapter ones — must be short atoms or short strings. Long error detail goes in PaperEx.Execution.metadata. This rule exists so reason-code histograms remain useful for analytics and dashboards.

Implementing this behaviour

See test/support/paper_ex/mock_adapter.ex for a minimal implementation used in tests. A real adapter — paper_ex_polymarket — translates CLOB book snapshots and trade payloads into the normalized structs.

An adapter that the engine simulates against must implement simulate_fill/3; without it PaperEx.Engine.apply_order/4 returns {:error, :adapter_does_not_simulate}. simulate_fill/3 is listed under @optional_callbacks only so that normalize-only / live-mirror-reconciliation adapters (which supply normalize_fill/2 instead and never simulate) can omit it.

Summary

Types

An adapter-specific payload of any shape the adapter chooses.

Callbacks

Compute the fee in quote currency for one Fill. The engine uses this only if the adapter implements it; otherwise fee is 0.

Map an exchange-specific filled-trade payload onto a PaperEx.Fill.

Build a PaperEx.Instrument from exchange-specific data.

Build a PaperEx.MarketSnapshot from exchange-specific data plus the already-normalized Instrument the snapshot belongs to.

Bounded list of adapter-specific reason-code atoms this adapter may emit. Documentation/analytics contract only — the engine does not validate.

Simulate a fill for order against snapshot, producing a list of PaperEx.Fills and a status hint.

Types

exchange_payload()

@type exchange_payload() :: term()

An adapter-specific payload of any shape the adapter chooses.

Callbacks

fee(t, keyword)

(optional)
@callback fee(
  PaperEx.Fill.t(),
  keyword()
) :: number()

Compute the fee in quote currency for one Fill. The engine uses this only if the adapter implements it; otherwise fee is 0.

normalize_fill(exchange_payload, t)

(optional)
@callback normalize_fill(exchange_payload(), PaperEx.Instrument.t()) ::
  {:ok, PaperEx.Fill.t()} | {:error, term()}

Map an exchange-specific filled-trade payload onto a PaperEx.Fill.

Use this when the adapter is reconciling actual exchange fills (live mirror) rather than simulating its own. Optional.

normalize_instrument(exchange_payload)

@callback normalize_instrument(exchange_payload()) ::
  {:ok, PaperEx.Instrument.t()} | {:error, term()}

Build a PaperEx.Instrument from exchange-specific data.

normalize_snapshot(exchange_payload, t)

@callback normalize_snapshot(exchange_payload(), PaperEx.Instrument.t()) ::
  {:ok, PaperEx.MarketSnapshot.t()} | {:error, term()}

Build a PaperEx.MarketSnapshot from exchange-specific data plus the already-normalized Instrument the snapshot belongs to.

The adapter is responsible for sorting bids descending and asks ascending; MarketSnapshot.new/1 does not re-sort.

reason_codes()

(optional)
@callback reason_codes() :: [atom()]

Bounded list of adapter-specific reason-code atoms this adapter may emit. Documentation/analytics contract only — the engine does not validate.

simulate_fill(t, t, keyword)

(optional)
@callback simulate_fill(PaperEx.Order.t(), PaperEx.MarketSnapshot.t(), keyword()) ::
  {:ok, :filled | :partial, [PaperEx.Fill.t()]}
  | {:ok, :missed, atom() | String.t()}
  | {:error, term()}

Simulate a fill for order against snapshot, producing a list of PaperEx.Fills and a status hint.

Return values:

  • {:ok, :filled, [Fill.t()]} — order filled (fully or per the adapter's policy). May be a single-element list for a single-level fill.
  • {:ok, :partial, [Fill.t()]} — some quantity filled; remainder should be treated as missed or pending per engine config.
  • {:ok, :missed, reason} — adapter chose not to fill. reason must be a bounded reason code (see PaperEx.ReasonCodes or the adapter's own reason_codes/0).
  • {:error, term()} — adapter failure; engine surfaces this as :adapter_rejected.

The opts keyword list carries engine-level config (price tolerance, slippage budget, etc.). Adapters should ignore options they do not understand.

Listed under @optional_callbacks, but effectively required for any adapter the engine simulates against: if it is absent, PaperEx.Engine.apply_order/4 returns {:error, :adapter_does_not_simulate} rather than falling back to a built-in simulator. Omit it only for a normalize-only adapter that reconciles actual fills via normalize_fill/2.