DpExchange.Webull.Options (DpExchangeWebull v0.4.167)

Copy Markdown View Source

The options each facade function reads, in one place, and the check that refuses the rest.

A misspelt limt: that is silently ignored returns a plausible answer to a different question than the caller asked, which is the "nearby substitute where there should be an error" this family exists to avoid. Every DpExchange.Webull function that takes a keyword opts, and its DpExchange.Webull.Fake twin, calls this module at the boundary: before anything is sent, before the facade adds its own :limiter, and after the positional arguments (symbol, order id, watchlist id, request) have been checked.

What a function may be given

A shared transport key (DpExchange.Core.Options.transport_options/0) is accepted only where the call reads it, and every one accepted is forwarded to Core.HttpClient.

  • A call that sends a request allows every shared key but :base_url (no call reads it; the hosts come from :environment), plus the keys in own_keys/1. oauth_token/3 also refuses :credentials: the client id and secret are arguments.
  • A write that is always sent exactly once (place_order/3, place_orders/3, cancel_order/3, replace_order/4, the watchlist writes, create_token/1 and oauth_token/3) reads neither :retry_attempts nor :retry_delay: both are {:error, {:unknown_option, key}}, whatever the value, before anything is sent.
  • get_fees/2, coverage/1, wanted/1, coverage_by_kind/1 and subscribe_notices/1 send no request and open no socket: they allow :credentials (exempt, a consumer hands one set to every call) and their own keys, so timeout: 5_000 is refused.
  • streaming?/1 and live?/1 allow :credentials and :environment, which they resolve, and nothing else.

A refused key is {:error, {:unknown_option, key}}. The table below is the second half, and the only place it is written down: check/3 reads it, DpExchange.Webull and DpExchange.Webull.Fake both call check/3 (or one of the positional helpers), so the two cannot disagree.

A key that the code reads only to refuse it is still in the table, because the more specific answer is the useful one: :limit on get_order_book/2 is {:error, {:unsupported_option, :limit}} (levels per side are :depth), :since on get_trades/2 likewise, :real_time on the bars (renamed :include_partial), :symbols on update_watchlist/2 is {:error, :membership_not_updatable_here}.

Two calls take part of their keys from the argument that names the endpoint, so the table cannot hold all of them: get_fundamental/3 (the fundamentals kind) and get_screener/2 (the screener). Rest.fundamental_option_keys/1 and Rest.screener_option_keys/1 answer those and are passed as extra to check/3; check_fundamental/3 and check_screener/2 do both steps. get_financials/3 is fixed: all four statement kinds take the same keys.

What it answers

  • :ok
  • {:error, {:unknown_option, key}} - the first key (in the caller's order) not allowed
  • {:error, {:invalid_options, opts}} - opts is not a keyword list

check!/2 is the same check for a plain-value read with no error channel (coverage/1, wanted/1, coverage_by_kind/1, streaming?/1, live?/1): it raises ArgumentError.

A function that answers Venue.not_supported() is not in the table and keeps doing so whatever it is given.

Summary

Functions

Checks opts against the keys function may be given.

check/3 for a call with no error channel: raises ArgumentError instead of answering.

The keys a get_fundamental/3 of kind may be given beyond the transport ones, or the unknown-kind refusal. Checked first, as the kind names the endpoint.

client_id and then client_secret are checked first ({:error, {:invalid_field, field}}, never echoing the value), then the options.

The order id is checked first ({:error, {:invalid_order_id, id}}), then the options.

The keys a get_screener/2 of name may be given beyond the transport ones, or the unknown-screener refusal. Checked first, as the name names the endpoint.

The symbol is checked first, then the options: {:error, {:invalid_symbol, symbol}} wins over an unknown option, as positional arguments do in every venue of this family.

The token is checked first ({:error, {:invalid_field, :token}}; the value is a credential, so the field is named and the value is not echoed), then the options.

The watchlist id is checked first ({:error, {:invalid_watchlist_id, id}}), then the options.

Functions

check(function, opts, extra \\ [])

@spec check(atom(), term(), [atom()]) ::
  :ok | {:error, {:unknown_option, atom()} | {:invalid_options, term()}}

Checks opts against the keys function may be given.

extra adds keys that depend on an argument (get_fundamental/3's kind, get_screener/2's name). Answers :ok, {:error, {:unknown_option, key}} or {:error, {:invalid_options, opts}}.

check!(function, opts)

@spec check!(atom(), term()) :: :ok

check/3 for a call with no error channel: raises ArgumentError instead of answering.

For coverage/1, wanted/1, coverage_by_kind/1, streaming?/1 and live?/1, whose specs are a map, a list and a boolean. Returning an empty map for a misspelt feed: would read as "nothing is arriving" and send the caller to debug the stream.

check_fundamental(kind, symbol, opts)

@spec check_fundamental(term(), term(), term()) :: :ok | {:error, term()}

The keys a get_fundamental/3 of kind may be given beyond the transport ones, or the unknown-kind refusal. Checked first, as the kind names the endpoint.

check_oauth(client_id, client_secret, opts)

@spec check_oauth(term(), term(), term()) :: :ok | {:error, term()}

client_id and then client_secret are checked first ({:error, {:invalid_field, field}}, never echoing the value), then the options.

check_order_id(function, id, opts)

@spec check_order_id(atom(), term(), term()) :: :ok | {:error, term()}

The order id is checked first ({:error, {:invalid_order_id, id}}), then the options.

check_screener(name, opts)

@spec check_screener(term(), term()) :: :ok | {:error, term()}

The keys a get_screener/2 of name may be given beyond the transport ones, or the unknown-screener refusal. Checked first, as the name names the endpoint.

check_symbol(function, symbol, opts)

@spec check_symbol(atom(), term(), term()) :: :ok | {:error, term()}

The symbol is checked first, then the options: {:error, {:invalid_symbol, symbol}} wins over an unknown option, as positional arguments do in every venue of this family.

check_token(token, opts)

@spec check_token(term(), term()) :: :ok | {:error, term()}

The token is checked first ({:error, {:invalid_field, :token}}; the value is a credential, so the field is named and the value is not echoed), then the options.

check_watchlist_id(function, id, opts)

@spec check_watchlist_id(atom(), term(), term()) :: :ok | {:error, term()}

The watchlist id is checked first ({:error, {:invalid_watchlist_id, id}}), then the options.

own_keys(function)

@spec own_keys(atom()) :: [atom()]

The keys function reads beyond DpExchange.Core.Options.transport_options/0.

Raises KeyError for a function that has no entry, which is a bug in the caller.