CI codecov Elixir License: MIT Hex.pm

Elixir SDK for the Longbridge OpenAPI trading platform — real-time market data, push subscriptions, trading, watchlists, screener, financial calendar, fundamental data, and portfolio analytics for US, HK, SG, and CN markets.

The SDK speaks Longbridge's binary protocol over WebSocket for streaming quote and trade data, and uses signed HTTP requests for everything else (order placement, account queries, market data on demand, screener, watchlist, calendar, fundamentals). Quote and trade WebSockets authenticate with a one-time password (OTP) derived from the long-lived access token; HTTP requests sign with HMAC-SHA256.

Status: beta. The quote endpoint is feature-complete; the trade endpoint supports subscription, order submission, position queries, account balance, and execution history via the Longbridge REST API. The API is subject to change before 1.0.0.

Installation

Add longbridge to your mix.exs:

def deps do
  [
    {:longbridge, "~> 0.1.0"}
  ]
end

protoc is a build-time dependency of protox (one of the transitive deps). On macOS:

brew install protobuf

Quick start

config = Longbridge.Config.new(
  token: System.fetch_env!("LONGBRIDGE_TOKEN"),
  app_key: System.fetch_env!("LONGBRIDGE_APP_KEY"),
  app_secret: System.fetch_env!("LONGBRIDGE_APP_SECRET")
)

{:ok, quote_ctx} = Longbridge.QuoteContext.start_link(config)

# One-shot request
{:ok, %Longbridge.Quote.V1.SecurityQuote{} = quote} =
  Longbridge.QuoteContext.quote(quote_ctx, ["AAPL.US", "00700.HK"])

IO.inspect(quote)

# Subscribe to real-time push
:ok = Longbridge.QuoteContext.subscribe(quote_ctx, ["AAPL.US"], [:QUOTE])

# Push frames arrive as messages in the *caller* process
receive do
  {:longbridge_push, {:push, 101, body}} ->
    IO.inspect(Protox.decode!(body, Longbridge.Quote.V1.PushQuote))
end

Trade example

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

# Submit an order (LO = limit order)
{:ok, %{"order_id" => order_id}} =
  Longbridge.TradeContext.submit_order(trade_ctx,
    symbol: "AAPL.US",
    side: :buy,
    order_type: :lo,
    submitted_quantity: "100",
    time_in_force: :day,
    submitted_price: "150.00"
  )

# Cancel an order
:ok = Longbridge.TradeContext.cancel_order(trade_ctx, order_id)

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

# Subscribe to order-change push events
:ok = Longbridge.TradeContext.subscribe(trade_ctx, [:private])
:ok = Longbridge.TradeContext.set_on_order_changed(trade_ctx, fn event ->
  IO.inspect(event, label: "order changed")
end)

Configuration

Longbridge.Config holds everything needed to open a connection:

FieldDefaultNotes
tokennilOAuth access token. Required for WS contexts and signed HTTP requests; WSConnection aborts with {:error, :no_token} if nil.
app_key / app_secretnilApp credentials. Required for legacy refresh_access_token/2. OAuth users only need them for register_client/1.
expired_atnilUnix timestamp when token expires. Set after refresh_access_token/2.
chinafalseWhen true, switch to .longbridge.cn endpoints (HTTP, quote WS, trade WS).
http_urlhttps://openapi.longbridge.{com,cn}HTTP API base. Used by every HTTP context and refresh_access_token/2.
quote_ws_urlwss://openapi-quote.longbridge.{com,cn}Quote WebSocket URL. Override for staging or proxies.
trade_ws_urlwss://openapi-trade.longbridge.{com,cn}Trade WebSocket URL. Same.
gzip_threshold1024 bytesBodies ≥ this size will be gzipped on send.
heartbeat_interval15_000 msClient→server keep-alive cadence.
request_timeout10_000 msPer-request timeout.
idle_timeout600_000 msClose the connection after this much inactivity (used by Mint.WebSocket).
headersnilExtra {name, value} headers appended to every HTTP and WS upgrade request (mirrors Rust SDK 4.0.6's Config::header).

To inject custom HTTP/WS request headers (e.g. X-Forwarded-For, custom auth, tenant routing), pass :headers as a list of {name, value} tuples. Mirrors Config::header(key, value) from the Rust SDK (4.0.6).

config = Longbridge.Config.new(
  token: "...",
  app_key: "...",
  app_secret: "...",
  headers: [{"X-Forwarded-For", "1.2.3.4"}, {"X-Tenant", "acme"}]
)

The headers are appended to every signed HTTP request and to the WebSocket upgrade request.

Refreshing the access token (legacy API key)

The legacy API Key access_token expires after 90 days. To obtain a new one, call Longbridge.Config.refresh_access_token/2 and use the returned config. The new token and expired_at are set on the result.

{:ok, new_config} = Longbridge.Config.refresh_access_token(config)
{:ok, quote_ctx} = Longbridge.QuoteContext.start_link(new_config)

Internally this calls Longbridge's GET /v1/token/refresh HTTP endpoint, signed with HMAC-SHA256 using the same scheme as the official Python / Go SDKs. No new dependency beyond the existing finch (HTTP) and jason (JSON) deps is required.

Working with monetary values

Longbridge returns monetary fields as strings to preserve precision. The Longbridge.Decimal helper provides two layers of support:

# No dependency required — returns int or float:
price = Longbridge.Decimal.parse_number("0.0723")

# For exact arithmetic, add :decimal to your app's deps and use:
dec = Longbridge.Decimal.to_bigdecimal("3241500000000")

to_bigdecimal/1 and sum_bigdecimal/1 raise ArgumentError with a hint if :decimal is not loaded.

OAuth 2.0 (browser flow)

# Desktop / interactive (browser available)
{:ok, config} = Longbridge.OAuth.authorize("your-client-id")
{:ok, ctx} = Longbridge.QuoteContext.start_link(config)

The first call opens a browser, exchanges the code for a token, and persists it to ~/.longbridge/openapi/tokens/<client_id>. Subsequent calls reuse the cached token transparently.

If you don't have a client_id yet, register one first:

{:ok, client_id} = Longbridge.OAuth.register_client("My App")

OAuth 2.0 (headless server)

OAuth 2.0 in Longbridge only supports authorization_code (browser required) and refresh_token grants. For server-side programs, authorize once on a developer's workstation, then copy the token file to the server.

OAuth 2.0 (custom token storage)

The default storage writes tokens to ~/.longbridge/openapi/tokens/<client_id>. For tests or ephemeral sessions, use the bundled in-memory implementation:

Longbridge.OAuth.authorize("client-id",
  storage: Longbridge.OAuth.InMemoryTokenStorage
)

To plug in Redis, Vault, an encrypted file, or anything else, implement the Longbridge.OAuth.TokenStorage behaviour and pass it via the :storage option:

defmodule MyApp.RedisStorage do
  @behaviour Longbridge.OAuth.TokenStorage

  @impl true
  def load(client_id) do
    case Redix.command(:redix, ["GET", "oauth:token:#{client_id}"]) do
      {:ok, nil} -> {:error, :not_found}
      {:ok, json} -> decode(json)
      {:error, _} -> {:error, :not_found}
    end
  end

  @impl true
  def save(client_id, token) do
    Redix.command(:redix, ["SET", "oauth:token:#{client_id}", Jason.encode!(token)])
    :ok
  end
end

Longbridge.OAuth.authorize("client-id", storage: MyApp.RedisStorage)

The :storage option is supported by authorize/2, refresh_token/2, load_token/2, and export_token/2.

The typical headless workflow:

# On dev machine (browser available)
mix run -e 'Longbridge.OAuth.authorize("your-client-id")'
# → writes ~/.longbridge/openapi/tokens/<client_id>

# Copy that file to the server (same path)

# On server
{:ok, config} = Longbridge.OAuth.load_token("your-client-id")

load_token/1 auto-refreshes expired tokens via the refresh_token grant. The Longbridge.OAuth module's full API:

  • authorize/2 — full browser flow
  • load_token/1 — load from disk, auto-refresh if expired
  • export_token/1 — export for cross-machine copy / secret manager
  • refresh_token/2 — manual refresh
  • register_client/1 — register a new OAuth client
  • authorize_url/5 — build the authorization URL (testable)
  • pkce_challenge/1 — PKCE S256 helper (testable)

config.token must be a fresh OAuth token from open.longbridge.com. For OAuth, token refresh is handled automatically by Longbridge.OAuth.load_token/1 and Longbridge.HTTPClient (when :refresh_on_401 is set on a request).

Architecture

lib/
 longbridge.ex                # top-level module, public entry, docs
 longbridge/
     _protos.ex              # use Protox, files: [protos/*.proto] — code-gen entry
     alert_context.ex         # price alert management (add/enable/disable/update/delete)
     application.ex           # supervision tree, starts Longbridge.Finch + Symbol.Store
     asset_context.ex         # account statement download
     calendar_context.ex      # financial calendar (earnings, dividends, IPOs, macro)
     config.ex                # Longbridge.Config struct + refresh_access_token/2
     connection/
        session.ex           # transport-agnostic reconnect / idle / broadcast logic
     content_context.ex       # news, community topics, announcements
     dca_context.ex           # dollar-cost averaging plan management
     decimal.ex               # optional helpers for wire-format string fields
     fundamental_context.ex   # financial reports, analyst ratings, dividends, valuation
     http_client.ex           # HMAC-SHA256 signed HTTP requests via Finch (+ 401 retry)
     market_context.ex        # market status, broker holdings, indices, anomaly alerts
     oauth.ex                 # OAuth 2.0 Authorization Code flow with PKCE
     oauth/
        file_token_storage.ex   # default ~/.longbridge/openapi/tokens/<id> JSON file
        in_memory_token_storage.ex
        token_storage.ex      # behaviour for plugging custom storage (Redis, Vault...)
     portfolio_context.ex     # exchange rates, portfolio P&L analysis
     protocol.ex              # wire-format constants + pack/unpack whole packets
     protocol/header.ex       # 11/10/5-byte header encode/decode (request/response/push)
     quote_context.ex         # public API: 25+ quote methods + push subscription
     quote_context/realtime_store.ex  # in-memory push-data cache
     quote_http_context.ex    # HTTP-backed quote methods (watchlist, market temp, ...)
     screener_context.ex      # screener strategies, indicator search, AI recommendations
     sharelist_context.ex     # community sharelist management
     symbol.ex                # user symbol ↔ counter_id conversion (with remote fallback)
     symbol/
        cache.ex             # resolved counter_id cache (disk-backed)
        directory.ex         # embedded US-ETF / US-IX / US-WT directory
        store.ex             # long-lived GenServer that owns the cache/dir ETS tables
     trade_context.ex         # public API: orders, positions, account, executions, push
     ws_connection.ex         # WebSocket GenServer (one per endpoint, Mint-based)
protos/
 api.proto                    # quote + trade request/response messages
 control.proto                # Auth, Heartbeat, Close
 error.proto                  # Error envelope
 subscribe.proto              # Subscribe/Unsubscribe/PushQuote/PushDepth/...

Wire format

Each WebSocket binary message is one or more length-prefixed protocol packets concatenated together. A single protocol packet is:


 length   (4 bytes, big-endian uint32)                       

 header   (11 / 10 / 5 bytes, big-endian)                    
   type:4  v:1  g:1  res:2  cmd:8  ...                 

 body     (Protobuf-encoded,  16 MiB)                       

The WS handshake is plain WebSocket upgrade (wss://openapi-quote.longbridge.com); the Longbridge protocol handshake is then sent as the first packet with version=1, codec=protobuf, platform=OpenAPI=9. See Longbridge.Protocol.handshake/0. The WSConnection module buffers and splits received binary messages so callers don't have to.

Connection lifecycle

Longbridge.WSConnection is a GenServer that owns the Mint WebSocket. Its lifecycle:

init    do_connect_and_auth    idle (heartbeat every heartbeat_interval, request/response pairing)
                                  
                                   on error / disconnect  Session.do_connect_and_auth (reconnect with back-off)

Longbridge.Connection.Session owns the transport-agnostic pieces (reconnect with exponential back-off, idle-timer scheduling, request broadcasting, push dispatch) so swapping the transport later wouldn't require rewriting them.

After auth succeeds, the connection enters active mode, sends a heartbeat every heartbeat_interval, dispatches response packets to in-flight request_id callers, and forwards push packets to subscribed context processes. The connection automatically reconnects with exponential back-off after a disconnect; once reconnected, every context that has subscribed resubscribes automatically (see Longbridge.QuoteContext and Longbridge.TradeContext).

Push data flow

Contexts subscribe themselves to the connection on start_link/2. When a push frame arrives:

  1. The connection's process_data/2 buffers bytes, splits on the 4-byte length prefix, and decodes the header + body.
  2. The connection's dispatch_push/3 sends {:longbridge, conn_pid, {:push, cmd_code, body}} to every subscriber.
  3. Each context receives this message, re-dispatches as {:longbridge_push, msg} to its own caller process, and the user pattern-matches in their own process.

The context's caller must be alive to receive push messages — Longbridge.QuoteContext.start_link/2 is the right way to spawn one per long-running process.

For typed callbacks instead of mailbox pattern-matching, Longbridge.QuoteContext also supports:

QuoteContext.set_on_quote(ctx, fn push ->
  IO.inspect(push.symbol)
end)

QuoteContext.set_on_depth(ctx, fn depth -> ... end)
QuoteContext.set_on_brokers(ctx, fn brokers -> ... end)
QuoteContext.set_on_trades(ctx, fn trades -> ... end)

# Fallback for any topic
QuoteContext.set_default_push_callback(ctx, fn msg -> ... end)

Each callback receives the decoded proto struct (Longbridge.Quote.V1.PushQuote, PushDepth, PushBrokers, or PushTrade). Mailbox delivery still works alongside the callbacks.

API reference

Longbridge.QuoteContext

MethodSub-commandNotes
user_quote_profile/2QueryUserQuoteProfile (4)language option, defaults to "en". Returns member_id, quote_level, rate_limit, quote_level_detail, subscribe_limit, history_candlestick_limit.
member_id/1(convenience)Extracts member_id from user_quote_profile/2.
quote_level/1(convenience)Extracts the user's quote level string (e.g. "Lv1", "Lv2").
quote_package_details/2(convenience)Returns subscribed quote packages by market.
static_info/2QuerySecurityStaticInfo (10)
quote/2QuerySecurityQuote (11)
option_quote/2QueryOptionQuote (12)
warrant_quote/2QueryWarrantQuote (13)
depth/2QueryDepth (14)
brokers/2QueryBrokers (15)
participant_broker_ids/1QueryParticipantBrokerIds (16)
trades/3QueryTrade (17)count defaults to 100.
intraday/3QueryIntraday (18)trade_session{:NORMAL_TRADE, :PRE_TRADE, :POST_TRADE, :OVERNIGHT_TRADE} (or 0–3).
candlesticks/6QueryCandlestick (19)period accepts :DAY, :ONE_MINUTE, :FIVE_MINUTE, :WEEK, :MONTH, :QUARTER, :YEAR, etc.

| history_candlesticks_by_offset/3 | QueryHistoryCandlestick (27) | Walks forward/backward from a date. direction: :forward | :backward. | | history_candlesticks_by_date/3 | QueryHistoryCandlestick (27) | Fetches within a start_date/end_date range. | | option_chain_date/2 | QueryOptionChainDate (20) | | | option_chain_strike_info/3 | QueryOptionChainDateStrikeInfo (21) | | | warrant_issuer_info/1 | QueryWarrantIssuerInfo (22) | | | warrant_list/2 | QueryWarrantFilterList (23) | Filtered HK warrant list. Options: :symbol, :language, :sort_by, :sort_order, :type, :expiry_date, :status, :price_type, :issuer. | | realtime_quote/2 | (cache) | Returns cached PushQuote for symbols from local push-data store. | | realtime_depth/2 | (cache) | Returns cached PushDepth for a symbol. | | realtime_brokers/2 | (cache) | Returns cached PushBrokers for a symbol. | | realtime_trades/3 | (cache) | Returns up to count cached trades (cap 500). | | reset_realtime_cache/1 | (cache) | Clears the local push-data cache. | | market_trade_period/1 | QueryMarketTradePeriod (8) | | | market_trade_day/4 | QueryMarketTradeDay (9) | | | calc_index/3 | QuerySecurityCalcIndex (26) | calc_indexes is a list of CalcIndex atoms. | | capital_flow_intraday/2 | QueryCapitalFlowIntraday (24) | | | capital_flow_distribution/2 | QueryCapitalFlowDistribution (25) | | | subscription/1 | Subscription (5) | | | subscribe/4 | Subscribe (6) | sub_types{:QUOTE, :DEPTH, :BROKERS, :TRADE}. is_first_push: true requests the current snapshot to be pushed immediately. | | unsubscribe/4 | Unsubscribe (7) | unsub_all: true removes every subscription for those symbols. |

QuoteContext.subscribe/4 and TradeContext.subscribe/2 both survive reconnect automatically: the context records every subscription and re-issues it after the WS reconnects, so push delivery resumes without any extra wiring on the caller's side.

Longbridge.TradeContext

All HTTP requests (order_detail, today_orders, etc.) automatically retry once on a 401 Unauthorized response after refreshing the access token. OAuth users must pass token_refresher: to start_link/2 (a (Config.t -> {:ok, Config.t} | {:error, term})) so the trade context knows how to obtain a fresh token — the default Config.refresh_access_token/2 only works for legacy app-key auth. If the refresh fails (e.g. invalid_grant), the original 401 error is returned unchanged so callers can detect a revoked refresh token and re-authorize.

Push & subscription

MethodDescription
subscribe/2Subscribe to trade push topics (:private for orders). Survives WS reconnect.
unsubscribe/2Unsubscribe from trade push
put_callback/3Register a topic-specific callback (e.g. "/v1/trade/order_changed")
remove_callback/2Remove a topic callback
set_default_push_callback/2Fallback callback for unregistered topics
set_on_order_changed/2Convenience wrapper for order-changed events
on_order_changed/2Alias for set_on_order_changed/2

Orders (HTTP — REST API)

MethodDescription

| submit_order/2 | Place a new order (:market | :lo | :elo | :alo | :mit | :odd | :lit | :tslpamt | :tslppct) | | replace_order/2 | Replace an existing order | | cancel_order/2 | Cancel an order by ID | | today_orders/2 | List today's orders (:status, :market, :side, pagination) | | history_orders/2 | Search historical orders (date range, status, market, side) | | order_detail/2 | Get a single order by ID |

Executions (HTTP)

MethodDescription
today_executions/2List today's fills (:symbol, :market, :start_at, :end_at, pagination)
history_executions/2Search historical fills (date range, symbol, market, side)

Account & positions (HTTP)

MethodDescription
account_balance/2Get cash / buying power (per currency, e.g. "USD")
stock_positions/2List stock holdings (optional symbol filter)
fund_positions/2List fund holdings (optional symbol filter)
cash_flow/2Cash flow history within a date range
margin_ratio/2Initial/maintain margin ratios for a symbol

| estimate_max_purchase_quantity/2 | Estimate the max qty buyable for :LO | :ELO | :ALO | :MIT orders |

Longbridge.AssetContext (HTTP)

MethodDescription
statements/2List account statements (daily/monthly)
download_url/2Get presigned download URL for a statement file

Longbridge.MarketContext (HTTP)

MethodDescription
trading_days/4Deprecated — removed upstream; always returns {:error, :removed_upstream}. Use Longbridge.CalendarContext.market_closures/2 instead.
market_session/2Current trading session for all markets (US/HK/CN/SG; no market filter — filter client-side).
broker_holdings/3Top broker holdings (buy/sell leaders) for a symbol. :period{:rct_1, :rct_5, :rct_20, :rct_60}.
ah_premium/2A/H share premium K-line data for a dual-listed H-share
trade_status/2Buy/sell/neutral trade statistics for a symbol
index_constituents/2Constituents of a market index (e.g. "HSI.HK", "/DJI.US")
anomaly_alerts/2Market anomaly alerts (trading halts, suspensions) per market
top_movers/2Stocks with anomalous 20d price movement + linked news. :sort{:hot, :time, :change}. Renamed from stock_events in longbridge/openapi 4.2.0.
rank_categories/1List rank categories for rank_list/3
rank_list/4Ranked securities for a category (adds ib_ prefix automatically)

Longbridge.ContentContext (HTTP)

MethodDescription
news/2News articles with filtering/pagination
topics/2Community topics/posts
my_topics/2Topics created by the current authenticated user
create_topic/2Create a community topic; returns {:ok, topic_id}
topic_detail/2Single topic by ID
list_topic_replies/3Replies for a topic
create_topic_reply/3Post a reply to a topic
announcements/2Company announcements

Longbridge.FundamentalContext (HTTP)

MethodDescription
company_profile/2Company profile / overview
financial_reports/3Income, balance, cash flow reports
analyst_ratings/2Analyst ratings and targets
dividends/2Dividend history
valuation/2PE, PB, PS metrics
shareholders/2Shareholder distribution
etf_asset_allocation/2ETF asset allocation (holdings, regional, asset class, industry)
filings/2Regulatory filings for a symbol (e.g. SEC 10-K, 10-Q, insider forms)
macroeconomic_indicators/2List macroeconomic indicators for a country
macroeconomic/3Historical data for a specific macroeconomic indicator
valuation_comparison/4Compare valuation metrics across stocks (auto-selects peers when comparison_symbols is nil)

Longbridge.CalendarContext (HTTP)

MethodDescription
earnings/4Upcoming earnings dates
dividend_dates/4Dividend schedule
stock_splits/4Stock split calendar
ipo_calendar/4IPO calendar
macro_events/4Macro economic event dates
market_closures/2Market closure dates (holidays)

Longbridge.PortfolioContext (HTTP)

MethodDescription
exchange_rates/3Real-time exchange rates
portfolio_pl/2Portfolio P&L analysis
portfolio_positions/1Portfolio position list

Longbridge.AlertContext (HTTP)

MethodDescription
add_alert/2Create a price alert
list_alerts/1List active price alerts
update/3Update an alert (enable/disable) — re-sends the full item, use this instead of enable_alert/disable_alert
enable_alert/2Deprecated. Enable a price alert by alert_id (may fail server-side for alerts created through add_alert/2).
disable_alert/2Deprecated. Disable a price alert by alert_id.
delete_alert/2Delete one or more price alerts by id (string or list of strings)

Longbridge.DCAContext (HTTP)

MethodDescription
create_plan/2Create a DCA plan
list_plans/2List DCA plans
plan_detail/2Get plan details
update_plan/2Update an existing plan
pause_plan/2Pause an active plan
resume_plan/2Resume a paused plan
delete_plan/2Delete a plan

Longbridge.SharelistContext (HTTP)

MethodDescription
create/2Create a new sharelist
list/2List owned sharelists
detail/2Get symbols in a sharelist
rename/3Rename a sharelist
add_symbols/3Add symbols to a sharelist
remove_symbols/3Remove symbols from a sharelist
delete/2Delete a sharelist

Longbridge.ScreenerContext (HTTP)

MethodDescription
recommend_strategies/2Pre-defined recommended screener strategies for a market
user_strategies/2Current user's saved screener strategies
strategy/2A single screener strategy by ID (strips filter_ prefix from filter.filters[].key)
search/3Run a screener search — Mode A (strategy_id:) or Mode B (typed conditions:)
indicators/1List available screener indicators (strips filter_ prefix; builds tech_values from tech_indicators)

Longbridge.QuoteHTTPContext (HTTP)

MethodDescription
short_positions/3Short interest data (.HK = HKEX daily, others = US FINRA bi-monthly)
option_volume/2Real-time call/put volume snapshot for an option symbol
option_volume_daily/4Daily option volume for a symbol within a date range
update_pinned/4Pin or unpin a security to the top of a watchlist group
security_list/2List securities available for a market (market, category, page, count)
market_temperature/2Current market temperature (0-100) for a market
history_market_temperature/4Historical market temperature within a YYYY-MM-DD date range
short_trades/3Recent short-selling trades (.HK/hk, others → /us; count, last_timestamp)
watchlist_groups/1List watchlist groups
create_watchlist_group/3Create a watchlist group; returns {:ok, group_id}
delete_watchlist_group/3Delete a watchlist group (purge: false by default)

| update_watchlist_group/5 | Update a watchlist group (:add | :remove | :replace modes) | | filings/2 | Regulatory filings for a symbol (returns list with id, title, file_urls, publish_at, ...) | | symbol_to_counter_ids/2 | Batch-convert user symbols to internal counter_ids (HTTP-only; for local-first resolution see Longbridge.Symbol.resolve_counter_ids/2) |

Longbridge.Symbol

MethodDescription
to_counter_id/1Convert a user symbol ("AAPL.US", "00700.HK") to its internal counter_id ("ST/US/AAPL", "ST/HK/700"). Uses the embedded US-ETF / US-IX / US-WT directory for known symbols and falls back to ST/{MARKET}/{CODE} otherwise.
index_to_counter_id/1Convert an index symbol (e.g. "HSI.HK", "/DJI.US") to its IX/... counter_id.
from_counter_id/1Inverse of to_counter_id/1 (best-effort — some counter_ids are internal-only and have no public symbol).
resolve_counter_ids/2Local-first batch resolution: embedded directory → on-disk cache → server symbol_to_counter_ids/2. Results for unknown symbols are cached in $LONGBRIDGE_CACHE_DIR/counter-ids.csv.

Push command codes (consumer-side)

CodeMessageDecode with
101PushQuoteLongbridge.Quote.V1.PushQuote
102PushDepthLongbridge.Quote.V1.PushDepth
103PushBrokersLongbridge.Quote.V1.PushBrokers
104PushTradeLongbridge.Quote.V1.PushTrade
18Notification (trade)Longbridge.Trade.V1.Notification

Errors and reconnect behavior

  • Every public function returns {:ok, value} | {:error, reason} so callers never see raw exceptions from the wire layer. Protox.decode!/2 errors are caught and returned as {:error, {:decode_error, exception}}.

  • HTTP 401 Unauthorized responses from Longbridge.HTTPClient.request/5 are automatically retried once after a token refresh when called with refresh_on_401: true (the default for the TradeContext). The refreshed config is kept on the connection state so subsequent requests use it without another refresh.
  • The WS connection (Longbridge.WSConnection) reconnects with exponential back-off (capped) after any disconnect or auth failure. Both QuoteContext and TradeContext re-apply their recorded push subscriptions on reconnect, so you do not need to call subscribe/4 again after a network blip.
  • Longbridge.HTTPClient.refresh_access_token/1 (used by the legacy API-key flow) updates both token and expired_at on the config so the next call doesn't refresh again.

Development

mix deps.get
mix compile        # generates 98 proto modules on first build
mix test           # unit tests
mix docs           # ExDoc HTML in doc/

The full quality gate is one command:

mix ci

It runs, in order: compile --all-warnings --warnings-as-errors, format --check-formatted, credo --strict, deps.unlock --check-unused, deps.audit, xref graph --label compile-connected --fail-above 0, dialyzer, ex_dna (duplication), reach.check --dead-code --smells. See mix.exs for the alias definition.

Proto specs

The four .proto files under protos/ are vendored from longbridge/openapi-protobufs, pinned to tag gen/go/v0.7.0 via a git dep in mix.exs. To upgrade:

# 1. bump the tag in mix.exs
# 2. mix deps.get
# 3. sync the vendored files
cp deps/openapi_protobuf_specs/control/*.proto protos/
cp deps/openapi_protobuf_specs/quote/*.proto   protos/
cp deps/openapi_protobuf_specs/trade/*.proto   protos/
# 4. mix compile && mix test

Don't hand-edit files under protos/ — the next sync will clobber your changes.

License

TBD. See the LICENSE file once one is added.