diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 15a46de9d..176361cb8 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -49,6 +49,7 @@ work left a mark on it, you belong here.
| | 
[@dbydd](https://github.com/dbydd) | 🐛 🤔 🎨 | [Pi global + Workspace configuration layering and model reasoning capabilities (#662)](https://github.com/TraderAlice/OpenAlice/issues/662) — an unusually complete two-part reproduction that drove the native project-overlay architecture, safe migration of legacy `.pi-agent` state, and explicit reasoning-capability round trips in [#670](https://github.com/TraderAlice/OpenAlice/pull/670) |
| | 
[@enderzcx](https://github.com/enderzcx) | 🐛 🤔 | [Bitget Classic account-state blind spots (#951)](https://github.com/TraderAlice/OpenAlice/pull/951) — traced healthy-looking unscoped CCXT reads that omitted USDT-M funds and conditional-order namespaces, leading to the in-house Classic account model and routing fix |
| | 
[@roymeshulam](https://github.com/roymeshulam) | 🐛 🤔 | [Telegram Connector startup hid the health endpoint (#639)](https://github.com/TraderAlice/OpenAlice/pull/639) — isolated adapter init blocking Guardian's probe and proposed listen-before-start plus keeping degraded `lastError`, reimplemented in-house in [#1078](https://github.com/TraderAlice/OpenAlice/pull/1078) |
+| | 
[@fanfpy](https://github.com/fanfpy) | 🐛 🤔 | [Longbridge submit/replace failed at the N-API Decimal boundary (#959)](https://github.com/TraderAlice/OpenAlice/issues/959) — traced `decimal.js` values being passed into the SDK's native Decimal fields and proposed converting at the write boundary ([#704](https://github.com/TraderAlice/OpenAlice/pull/704)), reimplemented in-house |
---
diff --git a/services/uta/src/domain/trading/brokers/longbridge/LongbridgeBroker.spec.ts b/services/uta/src/domain/trading/brokers/longbridge/LongbridgeBroker.spec.ts
index cdc2fff00..71f1f00eb 100644
--- a/services/uta/src/domain/trading/brokers/longbridge/LongbridgeBroker.spec.ts
+++ b/services/uta/src/domain/trading/brokers/longbridge/LongbridgeBroker.spec.ts
@@ -1,6 +1,7 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import Decimal from 'decimal.js'
import { Contract, Order } from '@traderalice/ibkr'
+import { Decimal as LongbridgeDecimal } from 'longbridge'
import { LongbridgeBroker, ibkrOrderTypeToLb, ibkrTifToLb } from './LongbridgeBroker.js'
import { makeContract, parseLbSymbol, resolveSymbol, mapLbOrderStatus } from './longbridge-contracts.js'
import '../../contract-ext.js'
@@ -8,6 +9,18 @@ import '../../contract-ext.js'
// ==================== Longbridge SDK mock ====================
vi.mock('longbridge', () => {
+ class MockLongbridgeDecimal {
+ private readonly value: string
+
+ constructor(value: string | number) {
+ this.value = String(value)
+ }
+
+ toString(): string {
+ return this.value
+ }
+ }
+
// Numeric enum values mirror the const enum in node_modules/longbridge/index.d.ts.
const OrderSide = { Unknown: 0, Buy: 1, Sell: 2 } as const
const OrderType = {
@@ -18,6 +31,7 @@ vi.mock('longbridge', () => {
const Market = { Unknown: 0, US: 1, HK: 2, CN: 3, SG: 4, Crypto: 5 } as const
return {
+ Decimal: MockLongbridgeDecimal,
Config: { fromApikey: vi.fn(() => ({ __config: true })) },
TradeContext: {
new: vi.fn(() => ({
@@ -44,6 +58,12 @@ vi.mock('longbridge', () => {
}
})
+function expectLongbridgeDecimal(value: unknown, expected: string): void {
+ expect(value).toBeInstanceOf(LongbridgeDecimal)
+ expect(value).not.toBeInstanceOf(Decimal)
+ expect(String(value)).toBe(expected)
+}
+
// Helper: stamp mock contexts onto a freshly-constructed broker so we can
// drive method outcomes without going through init().
function attachMockContexts(broker: LongbridgeBroker): {
@@ -534,6 +554,48 @@ describe('LongbridgeBroker — getPositions()', () => {
describe('LongbridgeBroker — placeOrder()', () => {
beforeEach(() => vi.clearAllMocks())
+ it('converts decimal.js submit fields to SDK Decimal instances', async () => {
+ const broker = makeBroker()
+ const { trade } = attachMockContexts(broker)
+ trade.submitOrder.mockResolvedValue({ orderId: 'decimal-submit' })
+ const contract = makeContract('AAPL.US')
+ const order = new Order()
+ order.action = 'BUY'
+ order.orderType = 'TRAIL LIMIT'
+ order.totalQuantity = new Decimal('1.25')
+ order.lmtPrice = new Decimal('201.125')
+ order.auxPrice = new Decimal('199.875')
+ order.trailingPercent = new Decimal('1.5')
+ order.tif = 'DAY'
+
+ await broker.placeOrder(contract, order)
+
+ const sent = trade.submitOrder.mock.calls[0][0]
+ expectLongbridgeDecimal(sent.submittedQuantity, '1.25')
+ expectLongbridgeDecimal(sent.submittedPrice, '201.125')
+ expectLongbridgeDecimal(sent.triggerPrice, '199.875')
+ expectLongbridgeDecimal(sent.trailingPercent, '1.5')
+ })
+
+ it('preserves high-precision quantity without a JS number hop', async () => {
+ const broker = makeBroker()
+ const { trade } = attachMockContexts(broker)
+ trade.submitOrder.mockResolvedValue({ orderId: 'decimal-precise' })
+ const contract = makeContract('AAPL.US')
+ const order = new Order()
+ order.action = 'BUY'
+ order.orderType = 'LMT'
+ order.totalQuantity = new Decimal('123.4567890123456789')
+ order.lmtPrice = new Decimal('201.1250000000000001')
+ order.tif = 'DAY'
+
+ await broker.placeOrder(contract, order)
+
+ const sent = trade.submitOrder.mock.calls[0][0]
+ expectLongbridgeDecimal(sent.submittedQuantity, '123.4567890123456789')
+ expectLongbridgeDecimal(sent.submittedPrice, '201.1250000000000001')
+ })
+
it('translates HK MKT → ELO (HK quirk)', async () => {
const b = makeBroker()
const { trade } = attachMockContexts(b)
@@ -702,6 +764,24 @@ describe('LongbridgeBroker — modifyOrder()', () => {
const sent = trade.replaceOrder.mock.calls[0][0]
expect(sent.orderId).toBe('ord-1')
expect(sent.quantity.toString()).toBe('150')
+ expectLongbridgeDecimal(sent.quantity, '150')
+ })
+
+ it('converts decimal.js replacement fields to SDK Decimal instances', async () => {
+ const broker = makeBroker()
+ const { trade } = attachMockContexts(broker)
+ trade.replaceOrder.mockResolvedValue(undefined)
+
+ await broker.modifyOrder('ord-1', {
+ totalQuantity: new Decimal('2.5'),
+ lmtPrice: new Decimal('202.25'),
+ auxPrice: new Decimal('198.75'),
+ })
+
+ const sent = trade.replaceOrder.mock.calls[0][0]
+ expectLongbridgeDecimal(sent.quantity, '2.5')
+ expectLongbridgeDecimal(sent.price, '202.25')
+ expectLongbridgeDecimal(sent.triggerPrice, '198.75')
})
})
diff --git a/services/uta/src/domain/trading/brokers/longbridge/LongbridgeBroker.ts b/services/uta/src/domain/trading/brokers/longbridge/LongbridgeBroker.ts
index e48e74be9..a48021cab 100644
--- a/services/uta/src/domain/trading/brokers/longbridge/LongbridgeBroker.ts
+++ b/services/uta/src/domain/trading/brokers/longbridge/LongbridgeBroker.ts
@@ -19,6 +19,7 @@ import { z } from 'zod'
import Decimal from 'decimal.js'
import { Contract, ContractDescription, ContractDetails, Order, UNSET_DECIMAL } from '@traderalice/ibkr'
import {
+ Decimal as LongbridgeDecimal,
Config,
TradeContext,
QuoteContext,
@@ -68,6 +69,11 @@ import type {
const DERIVATIVE_OPTION = 0
const DERIVATIVE_WARRANT = 1
+/** Longbridge N-API writes require the SDK Decimal class, not decimal.js. */
+function toLongbridgeDecimal(value: Decimal): LongbridgeDecimal {
+ return new LongbridgeDecimal(value.toString())
+}
+
// ==================== Order-type translation ====================
/**
@@ -294,11 +300,11 @@ export class LongbridgeBroker implements IBroker {
orderType: lbType,
side,
timeInForce: lbTif,
- submittedQuantity: order.totalQuantity as unknown as never, // SDK accepts decimal.js or its own Decimal
+ submittedQuantity: toLongbridgeDecimal(order.totalQuantity),
}
- if (!order.lmtPrice.equals(UNSET_DECIMAL)) opts.submittedPrice = order.lmtPrice as unknown as never
- if (!order.auxPrice.equals(UNSET_DECIMAL)) opts.triggerPrice = order.auxPrice as unknown as never
- if (!order.trailingPercent.equals(UNSET_DECIMAL)) opts.trailingPercent = order.trailingPercent as unknown as never
+ if (!order.lmtPrice.equals(UNSET_DECIMAL)) opts.submittedPrice = toLongbridgeDecimal(order.lmtPrice)
+ if (!order.auxPrice.equals(UNSET_DECIMAL)) opts.triggerPrice = toLongbridgeDecimal(order.auxPrice)
+ if (!order.trailingPercent.equals(UNSET_DECIMAL)) opts.trailingPercent = toLongbridgeDecimal(order.trailingPercent)
try {
const resp = await this.tradeCtx.submitOrder(opts)
@@ -318,13 +324,13 @@ export class LongbridgeBroker implements IBroker {
}
const opts: ReplaceOrderOptions = {
orderId,
- quantity: changes.totalQuantity as unknown as never,
+ quantity: toLongbridgeDecimal(changes.totalQuantity),
}
if (changes.lmtPrice != null && !changes.lmtPrice.equals(UNSET_DECIMAL)) {
- opts.price = changes.lmtPrice as unknown as never
+ opts.price = toLongbridgeDecimal(changes.lmtPrice)
}
if (changes.auxPrice != null && !changes.auxPrice.equals(UNSET_DECIMAL)) {
- opts.triggerPrice = changes.auxPrice as unknown as never
+ opts.triggerPrice = toLongbridgeDecimal(changes.auxPrice)
}
try {
await this.tradeCtx.replaceOrder(opts)