mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-09 08:18:07 +00:00
feat: enhance order handling in LighterGateway and introduce order identity normalization for improved precision and consistency
This commit is contained in:
@@ -166,14 +166,16 @@ export async function placeOrder(
|
||||
}
|
||||
lockOperating(locks, timers, pendings, type, log);
|
||||
try {
|
||||
const closePosition = reduceOnly ? true : undefined;
|
||||
const order = await routeLimitOrder({
|
||||
adapter,
|
||||
symbol,
|
||||
side,
|
||||
quantity,
|
||||
price: priceNum,
|
||||
timeInForce: "GTX",
|
||||
timeInForce: reduceOnly ? "GTC" : "GTX",
|
||||
reduceOnly: reduceOnly ? true : undefined,
|
||||
closePosition,
|
||||
});
|
||||
pendings[type] = String(order.orderId);
|
||||
log("order", `挂限价单: ${side} @ ${priceNum} 数量 ${quantity} reduceOnly=${reduceOnly}`);
|
||||
@@ -216,12 +218,14 @@ export async function placeMarketOrder(
|
||||
await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log);
|
||||
lockOperating(locks, timers, pendings, type, log);
|
||||
try {
|
||||
const closePosition = reduceOnly ? true : undefined;
|
||||
const order = await routeMarketOrder({
|
||||
adapter,
|
||||
symbol,
|
||||
side,
|
||||
quantity,
|
||||
reduceOnly: reduceOnly ? true : undefined,
|
||||
closePosition,
|
||||
});
|
||||
pendings[type] = String(order.orderId);
|
||||
log("order", `市价单: ${side} 数量 ${quantity} reduceOnly=${reduceOnly}`);
|
||||
|
||||
@@ -158,12 +158,23 @@ export class LighterExchangeAdapter implements ExchangeAdapter {
|
||||
}
|
||||
|
||||
private logError(context: string, error: unknown): void {
|
||||
if (process.env.LIGHTER_DEBUG === "1" || process.env.LIGHTER_DEBUG === "true") {
|
||||
console.error(`[LighterExchangeAdapter] ${context} failed: ${extractMessage(error)}`);
|
||||
if (process.env.LIGHTER_DEBUG !== "1" && process.env.LIGHTER_DEBUG !== "true") {
|
||||
return;
|
||||
}
|
||||
if (isSuccessfulResponse(error)) {
|
||||
console.info(`[LighterExchangeAdapter] ${context}: ${JSON.stringify(error)}`);
|
||||
return;
|
||||
}
|
||||
console.error(`[LighterExchangeAdapter] ${context} failed: ${extractMessage(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function isSuccessfulResponse(value: unknown): value is { code?: number } {
|
||||
if (typeof value !== "object" || value == null) return false;
|
||||
const code = (value as { code?: unknown }).code;
|
||||
return typeof code === "number" && code === 200;
|
||||
}
|
||||
|
||||
function resolveApiKeys(credentials: LighterCredentials): Record<number, string> {
|
||||
if (credentials.apiKeys && Object.keys(credentials.apiKeys).length) {
|
||||
return credentials.apiKeys;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
const TRUE_VALUES = new Set(["1", "true", "yes", "y", "on"]);
|
||||
const FALSE_VALUES = new Set(["0", "false", "no", "n", "off"]);
|
||||
|
||||
export function normalizeBooleanFlag(value: unknown): boolean | null {
|
||||
if (typeof value === "boolean") return value;
|
||||
if (typeof value === "number") {
|
||||
if (value === 1) return true;
|
||||
if (value === 0) return false;
|
||||
return null;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (!normalized) return null;
|
||||
if (TRUE_VALUES.has(normalized)) return true;
|
||||
if (FALSE_VALUES.has(normalized)) return false;
|
||||
return null;
|
||||
}
|
||||
if (typeof value === "bigint") {
|
||||
if (value === 1n) return true;
|
||||
if (value === 0n) return false;
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function coerceBooleanFlag(value: unknown, fallback = false): boolean {
|
||||
const normalized = normalizeBooleanFlag(value);
|
||||
if (normalized == null) return fallback;
|
||||
return normalized;
|
||||
}
|
||||
@@ -35,6 +35,8 @@ import {
|
||||
} from "./constants";
|
||||
import { decimalToScaled, scaledToDecimalString, scaleQuantityWithMinimum } from "./decimal";
|
||||
import { lighterOrderToAster, toAccountSnapshot, toDepth, toKlines, toOrders, toTicker } from "./mappers";
|
||||
import { normalizeOrderIdentity, orderIdentityEquals } from "./order-identity";
|
||||
import { shouldResetMarketOrders } from "./order-feed";
|
||||
|
||||
interface SimpleEvent<T> {
|
||||
add(handler: (value: T) => void): void;
|
||||
@@ -178,6 +180,7 @@ export class LighterGateway {
|
||||
private marketId: number | null = null;
|
||||
private priceDecimals: number | null = null;
|
||||
private sizeDecimals: number | null = null;
|
||||
private readonly orderIndexByClientId = new Map<string, string>();
|
||||
|
||||
private ws: WebSocket | null = null;
|
||||
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
@@ -299,9 +302,12 @@ export class LighterGateway {
|
||||
if (process.env.LIGHTER_DEBUG === "1" || process.env.LIGHTER_DEBUG === "true") {
|
||||
this.logger("createOrder.sendTx.response", response);
|
||||
}
|
||||
const clientOrderIndexStr = signParams.clientOrderIndex.toString();
|
||||
return lighterOrderToAster(this.displaySymbol, {
|
||||
order_index: Number(signParams.clientOrderIndex % 1_000_000_000n),
|
||||
client_order_index: Number(signParams.clientOrderIndex),
|
||||
order_index: clientOrderIndexStr,
|
||||
client_order_index: clientOrderIndexStr,
|
||||
order_id: clientOrderIndexStr,
|
||||
client_order_id: clientOrderIndexStr,
|
||||
market_index: signParams.marketIndex,
|
||||
initial_base_amount: baseAmountScaledString,
|
||||
remaining_base_amount: baseAmountScaledString,
|
||||
@@ -325,14 +331,8 @@ export class LighterGateway {
|
||||
await this.ensureInitialized();
|
||||
const marketIndex = params.marketIndex ?? this.marketId;
|
||||
if (marketIndex == null) throw new Error("Market index unknown");
|
||||
// Parse order id to BigInt without precision loss; prefer string input
|
||||
let indexValue: bigint;
|
||||
if (typeof params.orderId === "string") {
|
||||
indexValue = BigInt(params.orderId);
|
||||
} else {
|
||||
// Fallback for numeric ids (may be unsafe if beyond 2^53-1)
|
||||
indexValue = BigInt(Math.trunc(params.orderId));
|
||||
}
|
||||
const resolvedOrderId = this.resolveOrderIndex(String(params.orderId));
|
||||
const indexValue = BigInt(resolvedOrderId);
|
||||
const { apiKeyIndex, nonce } = this.nonceManager.next();
|
||||
try {
|
||||
const signed = await this.signer.signCancelOrder({
|
||||
@@ -344,10 +344,7 @@ export class LighterGateway {
|
||||
const auth = await this.ensureAuthToken();
|
||||
await this.http.sendTransaction(signed.txType, signed.txInfo, { authToken: auth });
|
||||
// Optimistically remove the order locally to avoid stale duplicates until WS confirms
|
||||
const key = String(params.orderId);
|
||||
this.orderMap.delete(key);
|
||||
this.orders = Array.from(this.orderMap.values());
|
||||
this.emitOrders();
|
||||
this.removeOrderLocally(String(params.orderId));
|
||||
} catch (error) {
|
||||
this.nonceManager.acknowledgeFailure(apiKeyIndex);
|
||||
throw error;
|
||||
@@ -843,19 +840,22 @@ export class LighterGateway {
|
||||
const marketKeys = Object.keys(ordersObject);
|
||||
if (snapshot && marketKeys.length === 0) {
|
||||
this.orderMap.clear();
|
||||
this.orderIndexByClientId.clear();
|
||||
this.orders = [];
|
||||
this.emitOrders();
|
||||
return;
|
||||
}
|
||||
if (snapshot) {
|
||||
this.orderMap.clear();
|
||||
this.orderIndexByClientId.clear();
|
||||
}
|
||||
for (const [market, bucket] of Object.entries(ordersObject)) {
|
||||
const marketId = Number(market);
|
||||
const normalized = this.normalizeOrders(bucket);
|
||||
if (Number.isFinite(marketId)) {
|
||||
const shouldReset = shouldResetMarketOrders(bucket, snapshot);
|
||||
if (shouldReset && Number.isFinite(marketId)) {
|
||||
this.clearOrdersForMarket(marketId);
|
||||
}
|
||||
const normalized = this.normalizeOrders(bucket);
|
||||
if (!normalized.length) continue;
|
||||
for (const order of normalized) {
|
||||
this.applyOrderUpdate(order);
|
||||
@@ -887,6 +887,7 @@ export class LighterGateway {
|
||||
this.clearOrdersForMarket(marketId);
|
||||
} else {
|
||||
this.orderMap.clear();
|
||||
this.orderIndexByClientId.clear();
|
||||
}
|
||||
}
|
||||
for (const order of orders) {
|
||||
@@ -897,26 +898,45 @@ export class LighterGateway {
|
||||
}
|
||||
|
||||
private applyOrderUpdate(order: LighterOrder): void {
|
||||
const key = String(order.order_index ?? order.order_id ?? order.client_order_index ?? "");
|
||||
const orderIndex = this.extractOrderIndex(order);
|
||||
const clientIndex = this.extractClientIndex(order);
|
||||
if (orderIndex && clientIndex) {
|
||||
this.orderIndexByClientId.set(clientIndex, orderIndex);
|
||||
}
|
||||
if (orderIndex) {
|
||||
this.orderIndexByClientId.set(orderIndex, orderIndex);
|
||||
}
|
||||
const key = orderIndex ?? clientIndex;
|
||||
if (!key) return;
|
||||
const status = (order.status ?? "").toLowerCase();
|
||||
if (TERMINAL_ORDER_STATUSES.has(status)) {
|
||||
const existing = this.orderMap.get(key);
|
||||
this.orderMap.delete(key);
|
||||
if (existing) {
|
||||
this.forgetOrderIdentity(existing);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (order.client_order_index != null || order.order_index != null) {
|
||||
if (
|
||||
order.client_order_index != null ||
|
||||
order.order_index != null ||
|
||||
order.client_order_id != null ||
|
||||
order.order_id != null
|
||||
) {
|
||||
for (const [existingKey, existingOrder] of Array.from(this.orderMap.entries())) {
|
||||
if (existingKey === key) continue;
|
||||
const sameOrderIndex =
|
||||
order.order_index != null &&
|
||||
existingOrder.order_index != null &&
|
||||
Number(existingOrder.order_index) === Number(order.order_index);
|
||||
orderIdentityEquals(order.order_index, existingOrder.order_index) ||
|
||||
orderIdentityEquals(order.order_id, existingOrder.order_id);
|
||||
const sameClientIndex =
|
||||
order.client_order_index != null &&
|
||||
existingOrder.client_order_index != null &&
|
||||
Number(existingOrder.client_order_index) === Number(order.client_order_index);
|
||||
orderIdentityEquals(order.client_order_index, existingOrder.client_order_index) ||
|
||||
orderIdentityEquals(order.client_order_id, existingOrder.client_order_id);
|
||||
if (sameOrderIndex || sameClientIndex) {
|
||||
const removed = this.orderMap.get(existingKey);
|
||||
this.orderMap.delete(existingKey);
|
||||
if (removed) {
|
||||
this.forgetOrderIdentity(removed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -927,8 +947,12 @@ export class LighterGateway {
|
||||
const normalized = Number(marketId);
|
||||
if (!Number.isFinite(normalized)) return;
|
||||
for (const [key, existing] of Array.from(this.orderMap.entries())) {
|
||||
if (Number(existing.market_index) === normalized) {
|
||||
const existingMarket =
|
||||
(existing as { market_index?: number | string; market_id?: number | string }).market_index ??
|
||||
(existing as { market_id?: number | string }).market_id;
|
||||
if (Number(existingMarket) === normalized) {
|
||||
this.orderMap.delete(key);
|
||||
this.forgetOrderIdentity(existing);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -974,6 +998,54 @@ export class LighterGateway {
|
||||
this.ordersEvent.emit(mapped);
|
||||
}
|
||||
|
||||
private resolveOrderIndex(orderId: string): string {
|
||||
const normalized = normalizeOrderIdentity(orderId);
|
||||
if (!normalized) {
|
||||
throw new Error(`Invalid order id: ${orderId}`);
|
||||
}
|
||||
return this.orderIndexByClientId.get(normalized) ?? normalized;
|
||||
}
|
||||
|
||||
private removeOrderLocally(orderId: string): void {
|
||||
const key = normalizeOrderIdentity(orderId);
|
||||
if (!key) return;
|
||||
const existing = this.orderMap.get(key);
|
||||
this.orderMap.delete(key);
|
||||
this.orderIndexByClientId.delete(key);
|
||||
if (existing) {
|
||||
this.forgetOrderIdentity(existing);
|
||||
}
|
||||
this.orders = Array.from(this.orderMap.values());
|
||||
this.emitOrders();
|
||||
}
|
||||
|
||||
private extractOrderIndex(order: LighterOrder): string | null {
|
||||
return (
|
||||
normalizeOrderIdentity(order.order_id) ??
|
||||
normalizeOrderIdentity(order.order_index) ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
private extractClientIndex(order: LighterOrder): string | null {
|
||||
return (
|
||||
normalizeOrderIdentity(order.client_order_id) ??
|
||||
normalizeOrderIdentity(order.client_order_index) ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
private forgetOrderIdentity(order: LighterOrder): void {
|
||||
const orderIndex = this.extractOrderIndex(order);
|
||||
const clientIndex = this.extractClientIndex(order);
|
||||
if (orderIndex) {
|
||||
this.orderIndexByClientId.delete(orderIndex);
|
||||
}
|
||||
if (clientIndex) {
|
||||
this.orderIndexByClientId.delete(clientIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private startPolling(): void {
|
||||
if (!this.pollers.ticker) {
|
||||
this.pollers.ticker = setInterval(() => {
|
||||
|
||||
@@ -19,6 +19,8 @@ import type {
|
||||
LighterOrderBookSnapshot,
|
||||
LighterPosition,
|
||||
} from "./types";
|
||||
import { coerceBooleanFlag, normalizeBooleanFlag } from "./flags";
|
||||
import { normalizeOrderIdentity } from "./order-identity";
|
||||
|
||||
export function toDepth(symbol: string, snapshot: LighterOrderBookSnapshot): AsterDepth {
|
||||
const toLevels = (levels: LighterOrderBookLevel[]): AsterDepthLevel[] =>
|
||||
@@ -74,13 +76,31 @@ export function toOrders(symbol: string, orders: LighterOrder[]): AsterOrder[] {
|
||||
}
|
||||
|
||||
export function lighterOrderToAster(symbol: string, order: LighterOrder): AsterOrder {
|
||||
const side: OrderSide = order.is_ask || order.side?.toLowerCase() === "sell" || order.side?.toLowerCase() === "ask"
|
||||
? "SELL"
|
||||
: "BUY";
|
||||
const booleanIsAsk = normalizeBooleanFlag(order.is_ask);
|
||||
const normalizedSide = order.side?.toLowerCase();
|
||||
const side: OrderSide =
|
||||
booleanIsAsk != null
|
||||
? booleanIsAsk
|
||||
? "SELL"
|
||||
: "BUY"
|
||||
: normalizedSide === "sell" || normalizedSide === "ask"
|
||||
? "SELL"
|
||||
: "BUY";
|
||||
const reduceOnly = coerceBooleanFlag(order.reduce_only, false);
|
||||
const orderIndex =
|
||||
normalizeOrderIdentity(order.order_id) ??
|
||||
normalizeOrderIdentity(order.order_index) ??
|
||||
normalizeOrderIdentity(order.client_order_index) ??
|
||||
normalizeOrderIdentity(order.client_order_id) ??
|
||||
"";
|
||||
const clientIndex =
|
||||
normalizeOrderIdentity(order.client_order_id) ??
|
||||
normalizeOrderIdentity(order.client_order_index) ??
|
||||
"";
|
||||
return {
|
||||
// Use string order id to avoid precision loss; prefer on-chain order_index for cancellation
|
||||
orderId: String(order.order_index ?? order.client_order_index ?? ""),
|
||||
clientOrderId: String(order.client_order_index ?? order.order_index ?? ""),
|
||||
orderId: orderIndex,
|
||||
clientOrderId: clientIndex || orderIndex,
|
||||
symbol,
|
||||
side,
|
||||
type: mapOrderType(order.type),
|
||||
@@ -91,8 +111,8 @@ export function lighterOrderToAster(symbol: string, order: LighterOrder): AsterO
|
||||
stopPrice: order.trigger_price ?? "0",
|
||||
time: order.created_at ?? Date.now(),
|
||||
updateTime: order.updated_at ?? Date.now(),
|
||||
reduceOnly: Boolean(order.reduce_only),
|
||||
closePosition: Boolean(order.reduce_only ?? order.owner_account_index === undefined ? false : order.is_ask),
|
||||
reduceOnly,
|
||||
closePosition: reduceOnly,
|
||||
workingType: "MARK_PRICE",
|
||||
activationPrice: order.trigger_price,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value != null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a per-market websocket payload should reset the cached
|
||||
* orders before applying its contents.
|
||||
*/
|
||||
export function shouldResetMarketOrders(bucket: unknown, snapshot: boolean): boolean {
|
||||
if (snapshot) return true;
|
||||
if (bucket == null) return false;
|
||||
if (Array.isArray(bucket)) {
|
||||
return bucket.length === 0;
|
||||
}
|
||||
if (isPlainObject(bucket)) {
|
||||
return Object.keys(bucket).length === 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Helpers for working with Lighter order identifiers without losing precision.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Normalizes any order identifier into a trimmed string representation.
|
||||
* Accepts string, number, or bigint inputs; returns null when the value
|
||||
* cannot be represented as a meaningful identifier.
|
||||
*/
|
||||
export function normalizeOrderIdentity(value: unknown): string | null {
|
||||
if (typeof value === "string") {
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
if (typeof value === "number") {
|
||||
if (!Number.isFinite(value)) {
|
||||
return null;
|
||||
}
|
||||
return Math.trunc(value).toString(10);
|
||||
}
|
||||
if (typeof value === "bigint") {
|
||||
return value.toString(10);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two identifier-like values without casting through Number(),
|
||||
* which would drop precision for large (>2^53) indices.
|
||||
*/
|
||||
export function orderIdentityEquals(a: unknown, b: unknown): boolean {
|
||||
const left = normalizeOrderIdentity(a);
|
||||
const right = normalizeOrderIdentity(b);
|
||||
return left != null && right != null && left === right;
|
||||
}
|
||||
@@ -8,12 +8,14 @@ export type LighterOrderType =
|
||||
| "take_profit_limit"
|
||||
| string;
|
||||
|
||||
type StrOrNum = string | number;
|
||||
|
||||
export interface LighterOrder {
|
||||
order_index: number;
|
||||
client_order_index: number;
|
||||
order_id?: string;
|
||||
client_order_id?: string;
|
||||
market_index: number;
|
||||
order_index: StrOrNum;
|
||||
client_order_index: StrOrNum;
|
||||
order_id?: string | null;
|
||||
client_order_id?: string | null;
|
||||
market_index: StrOrNum;
|
||||
owner_account_index?: number;
|
||||
initial_base_amount: string;
|
||||
remaining_base_amount: string;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { shouldResetMarketOrders } from "../../src/exchanges/lighter/order-feed";
|
||||
|
||||
describe("shouldResetMarketOrders", () => {
|
||||
it("always resets on snapshots", () => {
|
||||
expect(shouldResetMarketOrders([{ id: 1 }], true)).toBe(true);
|
||||
expect(shouldResetMarketOrders([], true)).toBe(true);
|
||||
expect(shouldResetMarketOrders(null, true)).toBe(true);
|
||||
});
|
||||
|
||||
it("resets when array bucket is empty", () => {
|
||||
expect(shouldResetMarketOrders([], false)).toBe(true);
|
||||
expect(shouldResetMarketOrders([{}], false)).toBe(false);
|
||||
});
|
||||
|
||||
it("resets when object bucket has no keys", () => {
|
||||
expect(shouldResetMarketOrders({}, false)).toBe(true);
|
||||
expect(shouldResetMarketOrders({ a: 1 }, false)).toBe(false);
|
||||
});
|
||||
|
||||
it("does not reset for non-empty updates", () => {
|
||||
expect(shouldResetMarketOrders([{ order_index: "1" }], false)).toBe(false);
|
||||
expect(shouldResetMarketOrders(null, false)).toBe(false);
|
||||
expect(shouldResetMarketOrders(undefined, false)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { normalizeOrderIdentity, orderIdentityEquals } from "../../src/exchanges/lighter/order-identity";
|
||||
|
||||
describe("order identity helpers", () => {
|
||||
it("treats large numeric strings as distinct values", () => {
|
||||
const first = "27584547724798440";
|
||||
const second = "27584547724798442";
|
||||
expect(orderIdentityEquals(first, second)).toBe(false);
|
||||
expect(orderIdentityEquals(first, first)).toBe(true);
|
||||
});
|
||||
|
||||
it("considers numeric inputs equal to their string counterparts", () => {
|
||||
expect(orderIdentityEquals(123456789, "123456789")).toBe(true);
|
||||
});
|
||||
|
||||
it("normalizes whitespace-only identifiers to null", () => {
|
||||
expect(normalizeOrderIdentity(" ")).toBeNull();
|
||||
});
|
||||
|
||||
it("falls back to truncated integers for floating inputs", () => {
|
||||
expect(normalizeOrderIdentity(42.9)).toBe("42");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { lighterOrderToAster } from "../../src/exchanges/lighter/mappers";
|
||||
import type { LighterOrder } from "../../src/exchanges/lighter/types";
|
||||
|
||||
function createOrder(overrides: Partial<LighterOrder> = {}): LighterOrder {
|
||||
return {
|
||||
order_index: 1,
|
||||
client_order_index: 1,
|
||||
market_index: 99,
|
||||
initial_base_amount: "0.1",
|
||||
remaining_base_amount: "0.1",
|
||||
price: "154.86",
|
||||
type: "limit",
|
||||
reduce_only: "No",
|
||||
side: "buy",
|
||||
...overrides,
|
||||
} as LighterOrder;
|
||||
}
|
||||
|
||||
describe("lighterOrderToAster", () => {
|
||||
it("treats textual reduce_only flags correctly", () => {
|
||||
const nonReduce = lighterOrderToAster("USDJPY", createOrder({ reduce_only: "No" }));
|
||||
expect(nonReduce.reduceOnly).toBe(false);
|
||||
|
||||
const reduce = lighterOrderToAster("USDJPY", createOrder({ reduce_only: "Yes" }));
|
||||
expect(reduce.reduceOnly).toBe(true);
|
||||
});
|
||||
|
||||
it("uses numeric is_ask flag for side inference", () => {
|
||||
const sell = lighterOrderToAster("USDJPY", createOrder({ is_ask: 1 }));
|
||||
expect(sell.side).toBe("SELL");
|
||||
|
||||
const buy = lighterOrderToAster("USDJPY", createOrder({ is_ask: 0 }));
|
||||
expect(buy.side).toBe("BUY");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user