Skip to content

feat(quote): add QuoteTransport to serve quote pull APIs over HTTP - #604

Merged
hogan-yuan merged 5 commits into
mainfrom
feat/quote-http-transport
Oct 13, 2026
Merged

hogan-yuan merged 5 commits into
mainfrom
feat/quote-http-transport

Conversation

@hogan-yuan

@hogan-yuan hogan-yuan commented Oct 10, 2026 •

Copy link
Copy Markdown
Member

What

Adds Config::quote_transport(QuoteTransport::{WebSocket, Http}) (env LONGBRIDGE_QUOTE_TRANSPORT=ws|http, default WebSocket). With Http, the 20 QuoteContext pull methods backed by the 19 WebSocket commands that have a REST equivalent (static_info, quote, option_quote, warrant_quote, depth, brokers, participants, trades, intraday, candlesticks, history_candlesticks_by_offset/by_date, option_chain_expiry_date_list, warrant_issuers, warrant_list, trading_session, trading_days, capital_flow, capital_distribution, calc_indexes) go over POST /quote/* instead of the quote WebSocket. A process that only pulls data never opens a WebSocket connection. Public method signatures and return types are unchanged.

The option is exposed in all six layers (Rust, C, C++, Java, Node.js, Python), mirroring PushCandlestickMode.

Why

longbridge/longbridge-mcp#162 re-implemented every quote tool on top of the REST endpoints inside the MCP (per-tool JSON reshaping) to get rid of its per-user WebSocket pool. That logic belongs in the SDK: one switch in QuoteContext::request, and every SDK consumer (MCP, CLI, …) gets it with identical types.

Usage

Opt in per Config (default stays WebSocket, so nothing changes for existing users). Either set the env var:

export LONGBRIDGE_QUOTE_TRANSPORT=http   # ws (default) | http

or set it in code. After that, call the quote APIs exactly as before — same methods, same return types:

Rust

use std::sync::Arc;

use longbridge::{Config, QuoteTransport, quote::QuoteContext};

let config = Arc::new(Config::from_apikey_env()?.quote_transport(QuoteTransport::Http));
let (ctx, _) = QuoteContext::new(config);
let quotes = ctx.quote(["700.HK", "AAPL.US"]).await?; // POST /quote/quotes, no WebSocket opened

Python

from longbridge.openapi import Config, QuoteContext, QuoteTransport

config = Config.from_apikey(app_key, app_secret, access_token, quote_transport=QuoteTransport.Http)
ctx = QuoteContext(config)
quotes = ctx.quote(["700.HK", "AAPL.US"])

Node.js

const { Config, QuoteContext, QuoteTransport } = require("longbridge");

const config = Config.fromApikey(appKey, appSecret, accessToken, { quoteTransport: QuoteTransport.Http });
const ctx = QuoteContext.new(config);

Java

Config config = Config.fromApikeyEnv().quoteTransport(QuoteTransport.Http);

C++

Status status;
Config config = Config::from_apikey_env(status);
config.set_quote_transport(QuoteTransport::Http);

C

lb_config_t *config = lb_config_from_apikey_env(&error);
lb_config_set_quote_transport(config, QuoteTransport_Http);

Notes:

  • Subscriptions / push callbacks, realtime_*, member_id / quote_level / quote_package_details still use the WebSocket, which is then connected on first use. A process that only calls the pull APIs never opens it.
  • enable_overnight keeps its meaning on both transports (US overnight data is returned only when it is set).
  • Comparison tool: cd examples/rust && cargo run -p quote_http_compare calls every pull API over both transports and prints the diff.

How

  • rust/src/quote/cmd_code.rs::http_path maps WS command codes to REST paths; QuoteContext::request / request_without_body branch on the transport. The request is the same prost message serialised as JSON (nulls stripped); the response is decoded into the same prost message by rust/src/quote/http_json.rs, a lenient serde deserializer (int64-as-string, omitted/null → default, camelCase keys, unknown fields ignored), so all existing TryFrom conversions are reused.
  • rust/src/quote/overnight.rs aligns the one known server-side difference: REST always returns US overnight data and has no opt-out, while the WS only returns it when enable_overnight is set. On the HTTP path (and only there) the SDK applies the same rule for US equities: clears overnight_quote, filters overnight intraday lines and, for TradeSessions::All candlestick queries, drops OvernightTrade bars and tops up count/range-capped windows with POST /quote/history-candlesticks offset queries (≤1000 per request, ≤8 rounds; when a page yields nothing the cursor is inside the contiguous overnight block and the next page is a full 1000 to jump it; a shortfall is logged). The WebSocket path is functionally identical to main (the only visible change is the brokers region-restriction error path, "quote/brokers (WebSocket)" → "quote/brokers"). HTTP-path requests are logged through the quote context's log subscriber like every other SDK HTTP call.
  • examples/rust/quote_http_compare calls every pull API over both transports and diffs the results.

Verification

examples/rust/quote_http_compare was run against a staging environment and production, with overnight on and off:

  • Staging: 31/33 cases identical in both modes; the 2 remaining are the gateway error-code gap below.
  • Production: all successful cases structurally identical; the remaining value-level differences are a backend issue (two consecutive REST calls disagree with each other while the WS is stable — different /quote/* replicas serve different data).
  • cargo test -p longbridge --lib quote:: 20/20 (refill paging incl. the overnight-block jump in both directions, window targets, US overnight window in EDT/EST, proto-JSON decode edge cases, request null-stripping and the JSON sent for top-up offset queries); cargo clippy --all --all-features adds no warnings; cargo +nightly fmt --all.
  • Follow-up commits address review: overnight-block refill escalation, a single US-equity / TradeSessions::All predicate (suffix matched case-insensitively), window_target (a date-range page is capped only if it is full-size and does not reach start), HTTP logging through the SDK log subscriber, a warning only when the top-up hits its round limit, CHANGELOG caveats (incl. intraday possibly empty over HTTP during the overnight session) (top-up endpoint and request count, error variant, missing per-command rate limit, 429 retry, overnight-session values), "websocket" env alias removed. Please squash on merge.

Known gateway gaps (not fixable in the SDK)

  1. REST reports business errors as 500 internal server error instead of passing the code through (WS: 301600, 301604, 301607). Callers that branch on error codes will not work over HTTP until this is fixed.
  2. Production /quote/* data is not consistent across replicas.
  3. No REST opt-out for US overnight data (handled client-side here as a stopgap; quote::overnight should be removed once the REST endpoints honour need_over_night_quote).

Follow-up (not in this PR): a client-side rate limit / jittered retry for the HTTP path, which today relies only on the HTTP client's 429 back-off.

🤖 Generated with Claude Code

Add `Config::quote_transport(QuoteTransport::{WebSocket, Http})` (env
`LONGBRIDGE_QUOTE_TRANSPORT=ws|http`, default WebSocket). With `Http`, the
19 `QuoteContext` pull APIs that have a REST equivalent are sent as
`POST /quote/*` instead of over the quote WebSocket, so a process that only
pulls data never opens a WebSocket connection. Method signatures and return
types are unchanged: the request is the same prost message serialised as
JSON, and the gateway proto-JSON response is decoded back into the same
prost message by a lenient deserializer (int64-as-string, omitted/null
fields, camelCase keys), so the existing conversions are reused.

Results are aligned with the WebSocket, including US overnight data, which
the gateway always returns over REST but the WebSocket only returns when
`enable_overnight` is set: on the HTTP path the SDK strips
`overnight_quote`, filters overnight intraday lines, drops overnight
candlesticks and tops up count/range-capped candlestick windows with offset
queries (`quote::overnight`, unit-tested).

Expose the option in the C, C++, Java, Node.js and Python layers, mirroring
`PushCandlestickMode`, and add `examples/rust/quote_http_compare`, which
calls every pull API over both transports and diffs the results.
- refill: once a page yields no usable bar the cursor is inside the
  contiguous 20:00-04:00 overnight block; fetch a full page to jump it
  instead of inching through (regression tests for both directions)
- gate overnight dropping on the same US-equity predicate everywhere and
  compute the window target in one place (CandlestickWindow / window_target,
  unit-tested)
- CHANGELOG: split the entry, list the top-up endpoint, error-variant,
  429-retry and overnight-session caveats; 20 methods / 19 commands
- drop the undocumented "websocket" env alias; ignore only .env/.env.local;
  revert unrelated example reformatting
@hogan-yuan
hogan-yuan force-pushed the feat/quote-http-transport branch from ef2a9d6 to 7a24f57 Compare October 10, 2026 10:28
- only drop overnight candlesticks for TradeSessions::All queries, so a
  page is either left untouched or dropped and topped up (matches Go)
- a date-range page shorter than the request cap is never treated as
  capped; avoids a wasted top-up request for weekend/holiday starts
- warn when the top-up returns fewer candlesticks than the WebSocket would
- clear overnight_quote only for US equities, like intraday/candlesticks
- send /quote/* requests under the quote context's log subscriber, like
  every other HTTP call, so request/response logging lands in the SDK log
  and not in the host process's global subscriber; log quote_transport
- match the .US suffix case-insensitively when aligning overnight data
- only warn about a short top-up for count windows; a date range stopping
  at its start is expected
- doc/changelog: return types match, not every value; list the missing
  per-command rate limit, the up-to-9-request top-up and depth/capital_flow
  among the overnight-session caveats
- refill reports whether it stopped on MAX_REFILL_ROUNDS while still short;
  running out of data (e.g. a recent listing) no longer warns
- mark quote::overnight as a stopgap for the missing REST overnight opt-out
  and list the server behaviour it relies on
- tests: lock the JSON sent for a top-up offset query (New York date/minute,
  numeric enums, no date_request) and a few other request bodies
- changelog: intraday can be empty over HTTP during the overnight session,
  trades may carry overnight ticks around it, no client-side rate limiting
@hogan-yuan
hogan-yuan merged commit b16a927 into main Oct 13, 2026
56 checks passed
@hogan-yuan
hogan-yuan deleted the feat/quote-http-transport branch October 13, 2026 10:25
hogan-yuan added a commit to longbridge/openapi-go that referenced this pull request Oct 13, 2026
## What

Ports longbridge/openapi#604 to the Go SDK. Adds `quote.Transport`
(`quote.TransportWebSocket`, default / `quote.TransportHTTP`), set with
`quote.WithTransport(...)`, `config.Config.QuoteTransport` or env
`LONGBRIDGE_QUOTE_TRANSPORT=ws|http`. With `TransportHTTP` the 20
`QuoteContext` pull methods backed by the 19 WebSocket commands that
have a REST equivalent (`StaticInfo`, `Quote`, `OptionQuote`,
`WarrantQuote`, `Depth`, `Brokers`, `Participants`, `Trades`,
`Intraday`, `Candlesticks`, `HistoryCandlesticksByOffset/ByDate`,
`OptionChainExpiryDateList`, `WarrantIssuers`, `WarrantList`,
`TradingSession`, `TradingDays`, `CapitalFlow`, `CapitalDistribution`,
`CalcIndex`) go over `POST /quote/*` instead of the quote WebSocket.
Method signatures and return types are unchanged.

## Why

Same motivation as the Rust PR: longbridge/longbridge-mcp#162
re-implemented every quote tool on the REST endpoints inside the MCP to
get rid of its per-user WebSocket pool. That belongs in the SDK, and Go
consumers should get the same switch.

## Usage

Opt in per context (default stays `TransportWebSocket`, so nothing
changes for existing users). Either set the env var:

```bash
export LONGBRIDGE_QUOTE_TRANSPORT=http   # ws (default) | http
```

or set it in code. After that, call the quote APIs exactly as before —
same methods, same return types:

From config / env (recommended — the quote WebSocket URL follows the
configured region):

```go
cfg, err := config.New()
if err != nil {
	return err
}
cfg.QuoteTransport = "http" // or LONGBRIDGE_QUOTE_TRANSPORT=http
qctx, err := quote.NewFromCfg(cfg) // no WebSocket is dialed here with HTTP
if err != nil {
	return err
}
defer qctx.Close()

quotes, err := qctx.Quote(context.Background(), []string{"700.HK", "AAPL.US"}) // POST /quote/quotes
```

With options:

```go
httpClient, err := http.NewFromCfg(cfg)
if err != nil {
	return err
}
qctx, err := quote.New(
	quote.WithHttpClient(httpClient),
	quote.WithTransport(quote.TransportHTTP),
	quote.WithQuoteURL(cfg.QuoteUrl), // needed outside the global region if you will subscribe
)
```

Notes:
- `Subscribe` / `Unsubscribe`, `Profile()` and APIs without a REST
equivalent still use the WebSocket, which is then dialed on first use;
`On*` callbacks registered before that are queued and attached when it
happens. A context that only calls the pull APIs never opens it. After
`Close` nothing re-dials (`quote.ErrClosed`).
- `WithEnableOvernight` keeps its meaning on both transports (US
overnight data is returned only when it is set).
- Comparison tool: `cd examples && go run ./quote_http_compare` calls
every pull API over both transports and prints both results with an
`EQUAL` flag per call.

## How

- `quote/transport.go`: `httpPaths` maps WebSocket commands to REST
paths; `core.do` branches on the transport. Requests are the same
protobuf messages encoded with `protojson` (proto field names, absent
sub-messages omitted). Responses go through `normalizeProtoJSON`, a
descriptor-driven pass that rewrites the gateway encodings protojson
rejects (`""` on numeric/enum fields, quoted enum numbers,
`"true"`/`"false"`/`1`/`0` on bools, numbers on string fields, `null`
list elements), then `protojson` (`DiscardUnknown`, so the extra
`volume_str` is ignored). A `null` `data` is an empty response.
- **Lazy WebSocket**: `newCore` no longer dials unconditionally. With
`TransportWebSocket` it dials and fetches the profile up front exactly
as before; with `TransportHTTP` the socket is dialed on first need
(`Subscribe`, `Profile()`, or any command without a REST path) via
`core.ws()`, with a context detached from the caller's cancellation.
Push handlers registered before the dial are queued and attached when it
happens. A socket whose authentication fails is closed rather than
leaked. After `Close()` nothing re-dials. `Subscribe` now returns the
subscribe error (it was dropped before, which a lazy dial would have
turned into silent failures).
- `quote/overnight.go` aligns the one known server-side difference: REST
always returns US overnight data and has no opt-out, while the WebSocket
only returns it with `WithEnableOvernight(true)`. On the HTTP path (and
only there) the SDK clears `OverNightQuote`, filters overnight
`Intraday` lines for US equities, and for `CandlestickTradeSessionAll`
history queries drops `OVERNIGHT_TRADE` bars and tops up
count/range-capped windows with offset queries (≤1000 per request, ≤8
rounds). Changes visible on the default WebSocket transport (all listed
in the CHANGELOG): `Subscribe` now returns the subscribe error instead
of always `nil`; `Close` is idempotent (a second call used to panic) and
final; the socket is closed if the initial profile query fails;
`LONGBRIDGE_ENABLE_OVERNIGHT` is now read (users who set it per the
README will start receiving US overnight data); the `Brokers`
region-restriction error text is `quote/brokers`. `http.Client.Call`
returns an empty `*json.RawMessage` instead of `EOF` for a JSON response
without `data` (a non-JSON 200 is still an error).
- `examples/quote_http_compare` calls every pull API over both
transports and diffs the JSON output.

## Verification

- `go test -race ./quote/` passes (new tests: refill paging incl. the
overnight-block jump in both directions, `alignCandlesticks` /
`windowTarget` early exits and capped/uncapped date ranges, US overnight
window in EDT/EST, command→path map completeness, gateway proto-JSON
decode replay and normalisation edge cases, request encoding).
- New tests also cover `httpDo` end to end against `httptest` (request
path/body, absent and `null` `data`, a non-JSON 200, API errors) and the
closed-context lifecycle (never dialed and dialed-then-closed). The two
failing `config` package tests (`Test_YamlConfig` / `Test_TomlConfig`)
fail identically on `main` (missing fixture files) and are unrelated.
- Follow-up commits address review (`WithTransport` validation,
fail-fast `New` without an HTTP client, proto-JSON normalisation,
`Subscribe` error, close/leak handling, date-range capped rule aligned
with the Rust port). Please squash on merge.
- `go vet ./quote/` reports only the pre-existing `copylocks` warnings
from `util.Copy` (same 8 on `main`).
- `examples/quote_http_compare` against a staging environment: 30/32
cases identical between transports with overnight both off and on; the 2
remaining are the gateway error-code gap below. Production: all
successful cases structurally identical; remaining value-level
differences come from inconsistent `/quote/*` replicas (see below).

## Known gateway gaps (not fixable in the SDK)

1. REST reports business errors as `500 internal server error` instead
of passing the code through (WS: `301600`, `301604`, `301607`).
2. Production `/quote/*` replicas are not consistent with each other
(value-level differences in `avg_price`/`volume`; two consecutive REST
calls can disagree while the WS is stable).
3. No REST opt-out for US overnight data (handled client-side here as a
stopgap; `quote/overnight.go` should be removed once the REST endpoints
honour `need_over_night_quote`).

Follow-up (not in this PR): a client-side rate limit for the HTTP path;
reading `LONGBRIDGE_LANGUAGE` (same class of env-tag bug as
`LONGBRIDGE_ENABLE_OVERNIGHT`).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@hogan-yuan hogan-yuan mentioned this pull request Oct 14, 2026
hogan-yuan added a commit that referenced this pull request Oct 14, 2026
Release **v5.2.1**. Bumps workspace `5.2.0` → `5.2.1`.

### New since v5.2.0
- **Added:** `OrderStatus::DelayedNotReported` — monitoring, the state a
time-condition order sits in before it is reported to the exchange;
appended after `PartialWithdrawal` so C/C++/Node.js discriminants stay
stable (#595)
- **Added:** time-condition orders — `delayed_until` on submit /
multi-leg submit (held in `DelayedNotReported` and sent at that time, ≤
90 days ahead; multi-leg `LO` / `MO` only) and on replace (move the
time, or `clear_delayed_until` to turn it back into a regular order);
`Order` / `OrderDetail` gain `delayed_until`. Across Rust, C, C++, Java,
Node.js and Python (the Go SDK ships separately, openapi-go#123) (#595)
- **Added:** `QuoteTransport` config option to serve quote pull APIs
over HTTP, exposed in every binding layer (#604)

Only `Cargo.toml` (workspace version + internal dep reqs) and
`CHANGELOG.md` change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant