Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **Forex (currency exchange) channel:** new `ForexContext` / `ForexContextSync` for the currency-exchange flow — `quote` (`POST /v1/forex/quote`), `submit_order` (`POST /v1/forex/order`) and `order` (`GET /v1/forex/order`) — across all SDK layers (Rust core async + blocking, C, C++, Java, Node.js, Python). Conversion is asynchronous: a successful `submit_order` only means the order was accepted, so callers poll `order` by `client_order_id` until it reaches a terminal state. The order state is an integer-coded enum `ForexOrderStatus` (`Processing` / `Success` / `Failed`); the quote `rate` and the order's `rate` / `from_amount` / `to_amount` are decimals, with the three order amounts modeled as nullable (they arrive empty until the order is filled). `rate` / `expire_at` (Unix ms) / `ccy_pair` follow the standard currency-pair convention (`1 BASE = rate QUOTE`), independent of the request's from/to direction. Mirrored into `c/csrc/include/longbridge.h` (`lb_forex_context_*`, `lb_forex_quote_t`, `lb_forex_order_detail_t`, `lb_forex_order_status_t`), `nodejs/index.d.ts`, the Java classes under `com.longbridge.forex`, and the `openapi.pyi` stub; the separate Go SDK is pending.

## [5.2.0] - 2026-09-30

### Added
Expand Down
7 changes: 7 additions & 0 deletions c/cbindgen.toml
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,11 @@ cpp_compat = true
"CGetFundTransactionsOptions" = "lb_get_fund_transactions_options_t"
"CValidateFundOrderOptions" = "lb_validate_fund_order_options_t"
"CSubmitFundOrderOptions" = "lb_submit_fund_order_options_t"
# --- forex module: context + response views ---
"CForexContext" = "lb_forex_context_t"
"CForexQuote" = "lb_forex_quote_t"
"CForexOrderDetail" = "lb_forex_order_detail_t"
"CForexOrderStatus" = "lb_forex_order_status_t"
"CGetStockPositionsOptions" = "lb_get_stock_positions_options_t"
"CStockPositionsResponse" = "lb_stock_position_response_t"
"CStockPositionChannel" = "lb_stock_position_channel_t"
Expand Down Expand Up @@ -511,6 +516,8 @@ include = [
"CFundPositionNav", "CFundPositionPerformance", "CFundPositionProfits", "CFundPositions",
"CFundQuarterlyReturn", "CFundStockHolding", "CFundTransaction", "CFundTrend",
"CFundTrendContrast", "CFundUnitValue", "CHotFund",
# ForexContext: currency-exchange response views (via void* async data pointer)
"CForexQuote", "CForexOrderDetail", "CForexOrderStatus",
# TradeContext: grid trading push (stays on trade side)
"CPushGridOrderChanged",
"CEstimateMaxPurchaseQuantityResponse",
Expand Down
110 changes: 110 additions & 0 deletions c/csrc/include/longbridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -1784,6 +1784,24 @@ typedef enum lb_charge_category_code_t {
ChargeCategoryCodeThird,
} lb_charge_category_code_t;

/**
* Forex order state.
*/
typedef enum lb_forex_order_status_t {
/**
* Processing (includes manual review); keep polling.
*/
ForexOrderStatusProcessing,
/**
* Conversion succeeded.
*/
ForexOrderStatusSuccess,
/**
* Conversion failed; frozen funds have been returned.
*/
ForexOrderStatusFailed,
} lb_forex_order_status_t;

/**
* Data granularity
*/
Expand Down Expand Up @@ -1992,6 +2010,8 @@ typedef struct lb_decimal_t lb_decimal_t;

typedef struct lb_error_t lb_error_t;

typedef struct lb_forex_context_t lb_forex_context_t;

typedef struct lb_fund_context_t lb_fund_context_t;

typedef struct lb_fundamental_context_t lb_fundamental_context_t;
Expand Down Expand Up @@ -9055,6 +9075,54 @@ typedef struct lb_hot_fund_t {
const char *time_interval;
} lb_hot_fund_t;

/**
* A forex quote.
*/
typedef struct lb_forex_quote_t {
/**
* Quote id, used when submitting the order
*/
const char *quote_id;
/**
* Customer execution rate (standard currency-pair terms)
*/
const struct lb_decimal_t *rate;
/**
* Quote expiry, Unix milliseconds
*/
int64_t expire_at;
/**
* The standard currency pair for `rate`, format `BASE/QUOTE`
*/
const char *ccy_pair;
} lb_forex_quote_t;

/**
* A forex order's state.
*/
typedef struct lb_forex_order_detail_t {
/**
* Order state
*/
enum lb_forex_order_status_t state;
/**
* Execution rate (standard currency-pair terms); null until filled
*/
const struct lb_decimal_t *rate;
/**
* Converted-from amount (actual value once filled); null until filled
*/
const struct lb_decimal_t *from_amount;
/**
* Converted-to amount (actual value once filled); null until filled
*/
const struct lb_decimal_t *to_amount;
/**
* Failure reason; empty when not failed
*/
const char *fail_reason;
} lb_forex_order_detail_t;

/**
* Options for estimate maximum purchase quantity
*/
Expand Down Expand Up @@ -14676,6 +14744,48 @@ int64_t lb_error_code(const struct lb_error_t *error);

enum lb_error_kind_t lb_error_kind(const struct lb_error_t *error);

const struct lb_forex_context_t *lb_forex_context_new(const struct lb_config_t *config);

void lb_forex_context_retain(const struct lb_forex_context_t *ctx);

void lb_forex_context_release(const struct lb_forex_context_t *ctx);

/**
* Get a forex quote.
*
* @param[in] from Convert-out currency (ISO 4217, e.g. `USD`)
* @param[in] to Convert-in currency (e.g. `HKD`)
* @param[in] amount Convert-out amount (can be null)
* @param[in] target_amount Convert-in amount (can be null)
*/
void lb_forex_context_quote(const struct lb_forex_context_t *ctx,
const char *from,
const char *to,
const struct lb_decimal_t *amount,
const struct lb_decimal_t *target_amount,
lb_async_callback_t callback,
void *userdata);

/**
* Submit a forex order.
*
* @param[in] quote_id Quote id returned by the quote endpoint
* @param[in] client_order_id Caller-supplied unique order id
*/
void lb_forex_context_submit_order(const struct lb_forex_context_t *ctx,
const char *quote_id,
const char *client_order_id,
lb_async_callback_t callback,
void *userdata);

/**
* Query a forex order by `client_order_id`.
*/
void lb_forex_context_order(const struct lb_forex_context_t *ctx,
const char *client_order_id,
lb_async_callback_t callback,
void *userdata);

const struct lb_fund_context_t *lb_fund_context_new(const struct lb_config_t *config);

void lb_fund_context_retain(const struct lb_fund_context_t *ctx);
Expand Down
100 changes: 100 additions & 0 deletions c/src/forex_context/context.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use std::{ffi::c_void, os::raw::c_char, sync::Arc};

use longbridge::forex::{ForexContext, GetForexQuoteOptions, SubmitForexOrderOptions};

use crate::{
async_call::{CAsyncCallback, execute_async},
config::CConfig,
forex_context::types::{CForexOrderDetailOwned, CForexQuoteOwned},
types::{CCow, CDecimal, cstr_to_rust},
};

pub struct CForexContext {
ctx: ForexContext,
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn lb_forex_context_new(config: *const CConfig) -> *const CForexContext {
Arc::into_raw(Arc::new(CForexContext {
ctx: ForexContext::new(Arc::new((*config).0.clone())),
}))
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn lb_forex_context_retain(ctx: *const CForexContext) {
Arc::increment_strong_count(ctx);
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn lb_forex_context_release(ctx: *const CForexContext) {
let _ = Arc::from_raw(ctx);
}

/// Get a forex quote.
///
/// @param[in] from Convert-out currency (ISO 4217, e.g. `USD`)
/// @param[in] to Convert-in currency (e.g. `HKD`)
/// @param[in] amount Convert-out amount (can be null)
/// @param[in] target_amount Convert-in amount (can be null)
#[unsafe(no_mangle)]
pub unsafe extern "C" fn lb_forex_context_quote(
ctx: *const CForexContext,
from: *const c_char,
to: *const c_char,
amount: *const CDecimal,
target_amount: *const CDecimal,
callback: CAsyncCallback,
userdata: *mut c_void,
) {
let ctx_inner = (*ctx).ctx.clone();
let from = cstr_to_rust(from);
let to = cstr_to_rust(to);
let mut options = GetForexQuoteOptions::new();
if !amount.is_null() {
options = options.amount((*amount).value);
}
if !target_amount.is_null() {
options = options.target_amount((*target_amount).value);
}
execute_async(callback, ctx, userdata, async move {
let resp: CCow<CForexQuoteOwned> = CCow::new(ctx_inner.quote(from, to, options).await?);
Ok(resp)
});
}

/// Submit a forex order.
///
/// @param[in] quote_id Quote id returned by the quote endpoint
/// @param[in] client_order_id Caller-supplied unique order id
#[unsafe(no_mangle)]
pub unsafe extern "C" fn lb_forex_context_submit_order(
ctx: *const CForexContext,
quote_id: *const c_char,
client_order_id: *const c_char,
callback: CAsyncCallback,
userdata: *mut c_void,
) {
let ctx_inner = (*ctx).ctx.clone();
let options =
SubmitForexOrderOptions::new(cstr_to_rust(quote_id), cstr_to_rust(client_order_id));
execute_async(callback, ctx, userdata, async move {
ctx_inner.submit_order(options).await?;
Ok(())
});
}

/// Query a forex order by `client_order_id`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn lb_forex_context_order(
ctx: *const CForexContext,
client_order_id: *const c_char,
callback: CAsyncCallback,
userdata: *mut c_void,
) {
let ctx_inner = (*ctx).ctx.clone();
let client_order_id = cstr_to_rust(client_order_id);
execute_async(callback, ctx, userdata, async move {
let resp: CCow<CForexOrderDetailOwned> = CCow::new(ctx_inner.order(client_order_id).await?);
Ok(resp)
});
}
2 changes: 2 additions & 0 deletions c/src/forex_context/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mod context;
pub(crate) mod types;
Loading
Loading