Skip to content

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

Merged
hogan-yuan merged 12 commits into
mainfrom
feat/fund-openapi-sdk
Sep 30, 2026
Merged

hogan-yuan merged 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.
@hogan-yuan
hogan-yuan merged commit ba4a37c into main Sep 30, 2026
56 checks passed
@hogan-yuan
hogan-yuan deleted the feat/fund-openapi-sdk branch September 30, 2026 08:22
hogan-yuan added a commit to longbridge/openapi-go that referenced this pull request Sep 30, 2026
## Summary
Adds a pure-Go `fund` package with `FundContext` (**28 methods**) for
the
mutual-fund channel, mirroring the Rust core field-for-field. Method
groups:
catalog & market data, user fund positions, and orders/trading.

## Design notes
- **`counter_id`-only, request and response** — funds are identified by
`counter_id` (e.g. `UT/FD/HK0000384492`), the deliberate exception to
the
release-wide symbol migration. Single-fund endpoints pass `counter_id`
as a
query param (fixed sub-paths like `/v1/fund/funds/detail`); the order
filter
carries repeated `counter_id`. No `symbol` anywhere in the fund package.
- **Batch endpoints** (`Performance`, `Nav`, `PositionPerformance`) send
a
one-element JSON array in a `counter_ids` query param
(`withCounterIDs`).
- **`jsontypes.Int64`** — a string-tolerant int64 type is applied to the
27
int64 wire fields; the backend may send them as a number or a quoted
string
  (empty/null → 0).
- **Nullable nested objects** (`FundDetail.AssetAllocation`,
  `FundTrend.ContrastPerformances`, `FundPositionDetail.DetailValues`,
  `FundOrderDetail.Order`) are pointers so `null` deserializes cleanly.
- Server-defined "any" JSON fields → `json.RawMessage`.

## Verification
`go build ./...`, `go vet ./fund/...`, and `gofmt -l fund/` all clean.

## Related
Core + C/C++/Java/Node.js/Python: longbridge/openapi#598 · CLI:
longbridge/longbridge-terminal#327 · MCP: longbridge/longbridge-mcp#161
· Docs: longbridge/developers#1269
@hogan-yuan hogan-yuan mentioned this pull request Sep 30, 2026
hogan-yuan added a commit that referenced this pull request Sep 30, 2026
Release **v5.2.0**. Bumps workspace `5.1.0` → `5.2.0`.

### New since v5.1.0
- **Breaking:** `TradeContext.fund_positions` — the `FundPosition`
identifier field is renamed `symbol` → `counter_id` across all layers (C
`lb_fund_position_t.counter_id`, C++ `FundPosition::counter_id`, Java
`getCounterId()`, Node.js `counterId`, Python `counter_id`). The value
is now the full fund `counter_id` (e.g. `UT/FD/HK0000384492`); the ISIN
is recoverable as its last `/`-separated segment. Rust keeps a `symbol`
serde alias for transition; the request-side filter
`GetFundPositionsOptions.symbols` is unchanged (#598)
- **Added:** mutual-fund channel `FundContext` / `FundContextSync` — 28
endpoints across fund catalog & market data, the user's fund positions,
and fund orders/trading. Funds are addressed by `counter_id` (sent as a
query parameter; the `nav` / `performance` / `position_performance`
batch endpoints use a one-element `counter_ids` JSON array). Mirrored
across C, C++, Java, Node.js and Python (the Go SDK ships separately,
openapi-go#124) (#598)
- **Changed:** `CalendarContext.finance_calendar` now exposes pagination
— `count` / `offset` / `next` (with the new `CalendarPageDirection`
enum) (#597)
- **Fixed:** C/C++ — resolved a cbindgen name collision from the fund
FFI: `CFundPosition` / `CGetFundPositionsOptions` were renamed to
`CFundPositionItem` (`lb_fund_position_item_t`) /
`CFundPositionsOptions` (`lb_fund_positions_options_t`) so they no
longer overwrite the portfolio types (#598)

> #598 is technically breaking; released as minor per maintainer
decision.
hogan-yuan added a commit to longbridge/longbridge-mcp that referenced this pull request Sep 30, 2026
Adds the fund (mutual-fund) channel to the MCP server — **28 `fund_*`
tools**
wrapping the openapi fund SDK:

- **25 read tools** — catalog / market data, the user's positions, and
  orders / order / transactions. The positions-overview tool is
`fund_position_overview` to avoid colliding with the trade
`fund_positions`.
- **3 order tools** — `fund_validate_order` (pre-trade check, places
nothing),
and `fund_submit_order` / `fund_cancel_order` (writes, gated by the
two-step
  dry-run + `confirmation_code` flow; the confirmation binds every
  order-shaping field).

Funds are addressed by **`counter_id`** (`fund_orders` filters by
`counter_ids`). Reads + `fund_validate_order` are exposed on `/mcp` and
the
read-only `/v2`; the two writes are `/mcp`-only and excluded from `/v2`.
zh-CN / zh-HK locales added for all 28 tools, and all 28 are declared in
the
OAuth consent scope taxonomy (`data/scopes.json`).

Pins the openapi SDK to the fund branch (`rev = 47e7572eb`). **Swap to
the
release version once openapi#598 merges and ships.**

## Testing
All read tools + the write dry-run/confirm flow verified live end-to-end
against staging (local `--canary` server + OAuth); the submit → cancel
path is
verified via the identical fund SDK in the CLI. `cargo test` (315 tests:
classification / locale-coverage / gated-tool / v2-allowlist invariants)
+
`clippy` clean.

## Related
SDK: longbridge/openapi#598 · CLI: longbridge/longbridge-terminal#327 ·
Docs: longbridge/developers#1269
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