Skip to content

feat(fund): add FundContext for the mutual-fund channel (all layers) - #598

Open
hogan-yuan wants to merge 12 commits into
mainfrom
feat/fund-openapi-sdk
Open

hogan-yuan wants to merge 12 commits into
mainfrom
feat/fund-openapi-sdk

Conversation

@hogan-yuan

@hogan-yuan hogan-yuan commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

Summary

New FundContext for the Hong Kong mutual-fund channel — 28 endpoints
(catalog & market data, user positions, orders & trading) — across all six SDK
layers: Rust core (async + blocking), C, C++, Java, Node.js, Python.

Design notes

  • counter_id-only, request and response (e.g. UT/FD/HK0000384492); no
    symbol field. It contains /, so it's passed as a counter_id query param
    (fixed sub-paths like /v1/fund/funds/detail), never a path segment. Batch
    endpoints (nav, performance, position_performance) use a one-element
    counter_ids JSON array.
  • Nullable nested objects (asset_allocation, contrast_performances,
    detail_values, order) are optional/nullable in every layer; int64
    fields accept a JSON number or a quoted string.
  • Node.js / Python expose the position type as FundHoldingPosition to
    avoid a clash with the trade channel's FundPosition; C uses the lb_*_t
    naming convention like every other channel.

Related

Go: longbridge/openapi-go#124 · CLI: longbridge/longbridge-terminal#327 · MCP: longbridge/longbridge-mcp#161 · Docs (CLI only; excluded from SDK/API reference by the counter_id policy): longbridge/developers#1269

Add the `fund` module with `FundContext` (async) and `FundContextSync`
(blocking) covering 28 mutual-fund OpenAPI endpoints:

- catalog & market data: hot funds, fund list, filters, detail,
  analysis / analysis detail / trend, annual & quarterly returns,
  performance & comparison, latest & historical NAV, top-10 holdings,
  reverse stock holdings
- user positions: overview, single position, performance, profits,
  NAV history, dividends
- orders & trading: order list & detail, transactions, order
  validate / submit / cancel

Identifiers are exposed as `symbol`; the OpenAPI gateway maps them to the
backend `counter_id`. Response models use `#[serde(default)]` throughout.

Propagation to C / C++ / Go / Java / Node.js / Python is pending.
Mirror the grid_context C layer: `c/src/fund_context/{mod,types,context}.rs`
exposing the 28 fund methods as `lb_fund_context_*` extern "C" functions with
`C*`/`C*Owned` type pairs and `C*Options` request structs. Header
`c/csrc/include/longbridge.h` regenerated by cbindgen.

serde_json::Value ("any") fields are exposed as JSON strings; unix-second
timestamps as raw i64.
Mirror the Rust-core fund module (28 methods) across the binding layers:

- C: `lb_fund_context_*` FFI (c/src/fund_context) + option structs, header
  regenerated. Fixes a cbindgen name collision where the fund
  `CFundPosition`/`CGetFundPositionsOptions` clashed with the portfolio
  types and corrupted longbridge.h (renamed to `CFundPositionItem` /
  `CFundPositionsOptions`).
- C++: `longbridge::fund::FundContext` wrapping the C layer; CMake now
  compiles fund_context.cpp into the shared lib.
- Java: `com.longbridge.fund.FundContext` (JNI + gson classes).
- Node.js: napi `FundContext` (position entry exposed as
  `FundHoldingPosition` to avoid the trade `FundPosition` clash).
- Python: PyO3 `FundContext` + openapi.pyi stub.

Server-defined "any" JSON fields are surfaced as raw JSON strings across all
layers; unix-second timestamps as integers; numeric-string fields as strings.

Known follow-up: the fund response structs are not yet emitted into the public
C header (reached via the void* async-result pointer); the Go SDK (separate
repo) is not yet done.
Add the 39 fund response view structs to the cbindgen `[export] include`
list (with `lb_fund_*_t` renames), so C consumers can read the results of the
`lb_fund_context_*` calls instead of only receiving an opaque `void*`. All
target typedef names are collision-free with the existing header. C and C++
builds verified.
…dpoints

The fund identifier (counter_id, e.g. UT/FD/HK0000384492) contains "/", so it
cannot be a URL path segment. Move the 18 single-fund endpoints onto fixed
sub-paths (e.g. /v1/fund/funds/detail, /v1/fund/funds/nav) that carry the id as
a `counter_id` query parameter, and expose the identifier as `counter_id`
(not `symbol`). The three batch-backed endpoints — latest NAV, daily
performance and held-fund performance — send the id as a one-element JSON array
in a `counter_ids` query parameter, matching their backend contract.

Propagated across Rust core (async + blocking), C, C++, Java, Node.js and
Python. The order-flow body keeps its `symbol` field (the gateway passes the
counter_id value through unchanged).
The backend returns several nested fund objects as null for many funds:
FundDetail.asset_allocation, FundTrend.contrast_performances,
FundPositionDetail.detail_values and FundOrderDetail.order. They were typed
non-optional, so those responses failed to deserialize — e.g. `trend` on any
fund without benchmark contrast data errored with
"invalid type: null, expected struct FundTrendContrast".

Model all four as optional/nullable across every layer: Rust `Option`
(+ serde default), C nullable pointer via `COption`, C++ `std::optional`,
Node.js `T | null`, Python `Optional[T]`; Java is unchanged (its gson bridge
maps null to null). Regenerated longbridge.h and index.d.ts; updated openapi.pyi.
Fund endpoints are counter_id-only (request and response). Rename the remaining
request-side symbol fields to counter_id across every layer:
- GetFundOrdersOptions: symbols -> counter_ids (query key symbol -> counter_id)
- ValidateFundOrderOptions / SubmitFundOrderOptions: symbol -> counter_id
Rust core + C, C++, Java, Node.js, Python (Go handled separately). Responses
were already counter_id. Backend accepts counter_id on both the order body and
the orders filter (verified). Regenerated longbridge.h and index.d.ts; updated
openapi.pyi.
- Rust core: make int64_str accept both JSON numbers and quoted strings
  (untagged), so a numeric int64 value never fails the whole response;
  add a regression test. This fix crosses all six layers since the core
  decodes to i64 before FFI.
- Node.js: rename the fund position wrapper to FundHoldingPosition (drop
  the js_name alias) so the generated index.d.ts no longer emits a
  `FundPosition` alias that collided (TS2300) with the trade FundPosition
  class; regenerated index.d.ts / index.js.
- Python: rename the fund pyclass to FundHoldingPosition so it no longer
  collides with the trade FundPosition when registered into the same
  module; update mod.rs registration and the openapi.pyi stub.
- C: add CFundContext and the 14 fund option structs to the cbindgen
  [export.rename] map so they emit as lb_*_t like every other channel
  instead of raw C… names; update the C++ fund layer to the lb_*_t names.
- Rust: fix a broken rustdoc intra-doc link (FundContext::list_funds ->
  FundContext::funds).
TradeContext.fund_positions returned the fund's ISIN in a field named `symbol`.
The ISIN cannot be converted back to a fund `counter_id` (the gateway does no
ISIN->counter_id conversion and counter_ids have several formats), so it could
not be used with the fund channel. Expose the full `counter_id` instead (the
ISIN is its last `/`-separated segment). Renamed across all six layers; the
Rust core keeps a serde alias for the legacy `symbol` key during transition.
…name

The fund_position_response unit test still asserted position.symbol, which no
longer compiles under cargo test / clippy --all-targets. Assert counter_id; the
fixture still sends the legacy `symbol` JSON key, so it also proves the serde
alias deserializes the old key.
The Java JNI field-mapping macro (impl_java_class!) still listed `symbol`,
failing the java clippy check after the trade FundPosition.symbol -> counter_id
rename. Map `counter_id` (the macro camelCases it to the Java `counterId`
field).
- fund_position_response test now sends a full counter_id (UT/FD/HK0000447943)
  matching the documented format, plus a second case proving the legacy `symbol`
  key still deserializes via the serde alias.
- CHANGELOG: correct the CFundPositionItem note (it IS emitted as
  lb_fund_position_item_t), and note the request-side filter stays symbol-based
  while the response returns counter_id.

This branch has not been deployed

No deployments
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