Longbridge.TradeContext (longbridge v0.1.0)

Copy Markdown View Source

Trade context for Longbridge trading APIs.

Manages a WebSocket connection to the trade endpoint for push subscriptions (order-changed events) and provides HTTP-based APIs for order submission, order queries, account balance, positions, executions, cash flow, and risk checks.

Usage

{:ok, ctx} = Longbridge.TradeContext.start_link(config)

# Submit a limit order
{:ok, %{"order_id" => order_id}} =
  Longbridge.TradeContext.submit_order(ctx,
    symbol: "00700.HK",
    side: :buy,
    order_type: :lo,
    submitted_quantity: "100",
    time_in_force: :day,
    submitted_price: "375.00"
  )

# Cancel it
{:ok, _} = Longbridge.TradeContext.cancel_order(ctx, order_id)

# Today's orders
{:ok, %{"orders" => orders}} =
  Longbridge.TradeContext.today_orders(ctx)

# Account balance (USD)
{:ok, %{"list" => balances}} =
  Longbridge.TradeContext.account_balance(ctx, "USD")

Push events

Subscribe after starting:

:ok = Longbridge.TradeContext.subscribe(ctx, [:private])
:ok = Longbridge.TradeContext.set_on_order_changed(ctx, fn event ->
  IO.inspect(event, label: "order changed")
end)

Subscriptions are recorded on the context state and re-issued automatically after a WS reconnect. Order-changed events arrive as {:longbridge_push, ...} messages in the caller's mailbox and fire the registered :order_changed callback.

Two transports

start_link/2 splits the config into:

  • state.ws_configLongbridge.Config.with_socket_token/1 applied, used to authenticate the Mint WebSocket. Auth uses the OTP, not the long-lived access token.
  • state.http_config — the original config, used to sign REST requests with HMAC-SHA256. HTTP 401 Unauthorized responses are automatically retried once after a token refresh.

For OAuth users, inject token_refresher: to start_link/2 so the context can refresh the long-lived token after a 401.

Summary

Functions

Returns account balance across all currencies. Optionally filter by currency (e.g. "HKD", "USD").

Cancels an order by order_id.

Returns cash flow history.

Returns a specification to start this module under a supervisor.

Estimates the maximum purchase quantity for a symbol.

Returns fund positions. Optionally filter by symbols (list of strings).

Returns historical executions. Requires

Returns historical orders. Requires

Returns the initial/maintain margin ratios for a symbol.

Alias for set_on_order_changed/2. Mirrors the upstream OnOrderChanged method name.

Returns the full state for a single order.

Sets a callback for a specific push topic.

Removes the callback for a specific push topic.

Replaces (amends) an existing order.

Returns the current WS session info.

Sets a fallback callback invoked when push data arrives for a topic with no registered callback.

Sets a callback for order-changed push events.

Starts a TradeContext linked to the calling process.

Returns stock positions. Optionally filter by symbols (list of strings).

Submits a new order.

Subscribes to trade push data. Accepts a list of topics

Returns today's executions. Accepts optional filters

Returns today's orders. Accepts optional filters

Unsubscribes from trade push topics.

Types

market()

@type market() :: :us | :hk | :cn | :sg

order_side()

@type order_side() :: :buy | :sell

order_status()

@type order_status() ::
  :not_reported
  | :replaced
  | :cancelled
  | :rejected
  | :new
  | :partially_filled
  | :filled

order_type()

@type order_type() ::
  :lo | :elo | :alo | :odd | :lit | :mit | :tslpamt | :tslppct | :market

outside_rth()

@type outside_rth() :: :rth_only | :any_time | :overnight

time_in_force()

@type time_in_force() :: :day | :gtc | :gtd

Functions

account_balance(pid, currency \\ nil)

@spec account_balance(pid(), String.t() | nil) :: {:ok, map()} | {:error, term()}

Returns account balance across all currencies. Optionally filter by currency (e.g. "HKD", "USD").

cancel_order(pid, order_id)

@spec cancel_order(pid(), String.t()) :: {:ok, map()} | {:error, term()}

Cancels an order by order_id.

Returns {:ok, %{}} (the API responds with a 200 + empty body on success). If the order is already in a terminal state (:filled, :cancelled), the API returns an error that surfaces as {:error, reason} — check it before reporting success to the caller.

Endpoint: DELETE /v1/trade/order

cash_flow(pid, opts \\ [])

@spec cash_flow(
  pid(),
  keyword()
) :: {:ok, map()} | {:error, term()}

Returns cash flow history.

Options

  • :start_at — Start time as Unix timestamp (seconds).
  • :end_at — End time as Unix timestamp (seconds).
  • :business_type:cash, :stock, or :fund.
  • :symbol — Stock symbol filter.
  • :page — Page number (default 1).
  • :size — Page size (default 50).

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

estimate_max_purchase_quantity(pid, opts)

@spec estimate_max_purchase_quantity(
  pid(),
  keyword()
) :: {:ok, map()} | {:error, term()}

Estimates the maximum purchase quantity for a symbol.

Options

  • :symbol — Stock symbol (required).
  • :order_type — Order type (required).
  • :side:buy or :sell (required).
  • :price — Price as a string (optional).
  • :currency — Currency (optional).
  • :fractional_shares — Boolean (optional, default false).

fund_positions(pid, symbols \\ nil)

@spec fund_positions(pid(), [String.t()] | nil) :: {:ok, map()} | {:error, term()}

Returns fund positions. Optionally filter by symbols (list of strings).

history_executions(pid, opts \\ [])

@spec history_executions(
  pid(),
  keyword()
) :: {:ok, map()} | {:error, term()}

Returns historical executions. Requires:

  • :start_at — Start time as a Unix timestamp (seconds).
  • :end_at — End time as a Unix timestamp (seconds).

history_orders(pid, opts \\ [])

@spec history_orders(
  pid(),
  keyword()
) :: {:ok, map()} | {:error, term()}

Returns historical orders. Requires:

  • :start_at — Start time as a Unix timestamp (seconds).
  • :end_at — End time as a Unix timestamp (seconds).

margin_ratio(pid, symbol)

@spec margin_ratio(pid(), String.t()) :: {:ok, map()} | {:error, term()}

Returns the initial/maintain margin ratios for a symbol.

Endpoint: GET /v1/risk/margin-ratio?symbol=<s>. Useful for margin checks before submitting levered orders. The response has im_factor (initial margin factor), mm_factor (maintain margin factor), fm_factor, mcm_factor, short_im_factor, and short_fm_factor — all decimal strings.

on_order_changed(pid, callback)

@spec on_order_changed(pid(), (map() -> any())) :: :ok

Alias for set_on_order_changed/2. Mirrors the upstream OnOrderChanged method name.

order_detail(pid, order_id)

@spec order_detail(pid(), String.t()) :: {:ok, map()} | {:error, term()}

Returns the full state for a single order.

Endpoint: GET /v1/trade/order?order_id=<id>. Includes current status, filled quantity, average fill price, fees, and timestamps. Use today_orders/2 (or history_orders/2) to find the order_id if you don't have it.

put_callback(pid, topic, callback)

@spec put_callback(pid(), String.t(), (map() -> any())) :: :ok

Sets a callback for a specific push topic.

The callback receives the decoded JSON event map for the given topic (e.g., @order_changed_topic).

See set_on_order_changed/2 for a convenience wrapper.

remove_callback(pid, topic)

@spec remove_callback(pid(), String.t()) :: :ok

Removes the callback for a specific push topic.

replace_order(pid, opts)

@spec replace_order(
  pid(),
  keyword()
) :: {:ok, map()} | {:error, term()}

Replaces (amends) an existing order.

Options

  • :order_id — The order to replace (required).
  • :quantity — New quantity as a string (required).
  • :price — New price as a string (required).

session(pid)

@spec session(pid()) :: {:ok, String.t() | nil, integer() | nil} | {:error, term()}

Returns the current WS session info.

Returns {:ok, session_id, heartbeat_interval_ms} once the underlying Longbridge.WSConnection has finished authenticating, or {:ok, nil, nil} before auth completes.

session_id is the opaque string assigned by the Longbridge backend (e.g. "15766270:21526413:...").

set_default_push_callback(pid, callback)

@spec set_default_push_callback(pid(), (map() -> any())) :: :ok

Sets a fallback callback invoked when push data arrives for a topic with no registered callback.

set_on_order_changed(pid, callback)

@spec set_on_order_changed(pid(), (map() -> any())) :: :ok

Sets a callback for order-changed push events.

The callback receives a map with fields like order_id, status, filled_qty, executed_price, etc.

Set this before calling subscribe/2 so you don't miss events.

start_link(config, opts \\ [])

@spec start_link(
  Longbridge.Config.t(),
  keyword()
) :: GenServer.on_start()

Starts a TradeContext linked to the calling process.

The context owns both:

  • a Longbridge.WSConnection to config.trade_ws_url for push subscriptions (order-changed events, etc.). Auth uses the OTP from Longbridge.Config.with_socket_token/1.
  • a per-instance HTTP config for signed REST requests (orders, account, executions, positions, cash flow). The original config.token is kept on state.http_config so HTTP requests sign with the long-lived access token, while WS auth uses the OTP from ws_config.

Options

  • :name — registered process name, passed through to GenServer.start_link/3.
  • :token_refreshernil (default) or a 1-arity function that refreshes the HTTP access token. The default uses Longbridge.Config.refresh_access_token/1, which only works for legacy app-key auth. OAuth users must inject a custom refresher that performs the refresh_token grant.
  • :skip_connection — when true, skip WS startup (HTTP-only context). Used by tests; not part of the stable API.
  • Any other GenServer option (:timeout, :spawn_opt, ...).

Example

{:ok, ctx} =
  Longbridge.TradeContext.start_link(config,
    token_refresher: fn cfg ->
      with {:ok, %OAuth.Token{access_token: t, expires_at: exp}} <-
             MyApp.OAuth.refresh(cfg.client_id) do
        {:ok, %{cfg | token: t, expired_at: exp}}
      end
    end
  )

stock_positions(pid, symbols \\ nil)

@spec stock_positions(pid(), [String.t()] | nil) :: {:ok, map()} | {:error, term()}

Returns stock positions. Optionally filter by symbols (list of strings).

submit_order(pid, opts)

@spec submit_order(
  pid(),
  keyword()
) :: {:ok, map()} | {:error, term()}

Submits a new order.

See the Longbridge Submit Order docs.

Options

  • :symbol — Stock symbol in ticker.region format (required).
  • :side:buy or :sell (required).
  • :order_type — Order type (required). See order_type/0.
  • :submitted_quantity — Quantity as a string (required).
  • :time_in_force:day, :gtc, or :gtd (required).
  • :submitted_price — Price as a string. Required for LO/ELO/ALO/ODD/LIT.
  • :trigger_price — Trigger price. Required for LIT/MIT.
  • :limit_offset — Limit offset. Required for TSLPAMT/TSLPPCT when limit_depth_level is 0.
  • :trailing_amount — Trailing amount for TSLPAMT.
  • :trailing_percent — Trailing percent for TSLPPCT.
  • :expire_date — Expiry date as "YYYY-MM-DD". Required for GTD.
  • :outside_rth — Outside RTH mode. For US stocks only.
  • :remark — Remark (max 255 chars).
  • :limit_depth_level — Bid/ask depth level (-5 to 5).
  • :monitor_price — Monitoring price for TSLPAMT/TSLPPCT.
  • :trigger_count — Trigger count (0-3).

subscribe(pid, topics \\ [:private])

@spec subscribe(pid(), [atom()]) :: :ok | {:error, term()}

Subscribes to trade push data. Accepts a list of topics:

  • :private — Order status updates.

Set a callback with set_on_order_changed/2 first to receive structured push events. Raw events arrive at the caller's mailbox as {:longbridge_push, {:push, cmd_code, body}} messages.

today_executions(pid, opts \\ [])

@spec today_executions(
  pid(),
  keyword()
) :: {:ok, map()} | {:error, term()}

Returns today's executions. Accepts optional filters:

  • :symbol — Stock symbol.
  • :order_id — A specific order ID.

today_orders(pid, opts \\ [])

@spec today_orders(
  pid(),
  keyword()
) :: {:ok, map()} | {:error, term()}

Returns today's orders. Accepts optional filters:

  • :symbol — Stock symbol.
  • :status — List of order statuses (atoms).
  • :side:buy or :sell.
  • :market:us, :hk, :cn, :sg.
  • :order_id — A specific order ID.

unsubscribe(pid, topics \\ [:private])

@spec unsubscribe(pid(), [atom()]) :: :ok | {:error, term()}

Unsubscribes from trade push topics.

topics is a list of atoms; currently :private is the only supported value (it controls order-changed events). Passing an empty list is a no-op. Mirrors Unsubscribe (cmd_code 17).