mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-11 09:18:08 +00:00
feat: 重构订单路由逻辑,添加订单意图类型以支持多交易所订单处理
This commit is contained in:
+100
-60
@@ -1,6 +1,13 @@
|
|||||||
import type { ExchangeAdapter } from "../exchanges/adapter";
|
import type { ExchangeAdapter } from "../exchanges/adapter";
|
||||||
import type { AsterOrder, CreateOrderParams } from "../exchanges/types";
|
import type { AsterOrder } from "../exchanges/types";
|
||||||
import { roundDownToTick, roundQtyDownToStep, formatPriceToString } from "../utils/math";
|
import {
|
||||||
|
routeCloseOrder,
|
||||||
|
routeLimitOrder,
|
||||||
|
routeMarketOrder,
|
||||||
|
routeStopOrder,
|
||||||
|
routeTrailingStopOrder,
|
||||||
|
} from "../exchanges/order-router";
|
||||||
|
import { roundDownToTick, roundQtyDownToStep } from "../utils/math";
|
||||||
import { isUnknownOrderError } from "../utils/errors";
|
import { isUnknownOrderError } from "../utils/errors";
|
||||||
import { isOrderPriceAllowedByMark } from "../utils/strategy";
|
import { isOrderPriceAllowedByMark } from "../utils/strategy";
|
||||||
|
|
||||||
@@ -146,25 +153,30 @@ export async function placeOrder(
|
|||||||
if (isOperating(locks, type)) return;
|
if (isOperating(locks, type)) return;
|
||||||
const priceNum = Number(price);
|
const priceNum = Number(price);
|
||||||
if (!enforceMarkPriceGuard(side, priceNum, guard, log, "限价单")) return;
|
if (!enforceMarkPriceGuard(side, priceNum, guard, log, "限价单")) return;
|
||||||
const priceTick = opts?.priceTick ?? 0.1;
|
|
||||||
const qtyStep = opts?.qtyStep ?? 0.001;
|
const qtyStep = opts?.qtyStep ?? 0.001;
|
||||||
const params: CreateOrderParams = {
|
const rawQuantity = Math.abs(amount);
|
||||||
symbol,
|
const roundedQuantity = roundQtyDownToStep(rawQuantity, qtyStep);
|
||||||
side,
|
const quantity = roundedQuantity > 0 ? roundedQuantity : rawQuantity;
|
||||||
type,
|
if (quantity <= 0) {
|
||||||
quantity: roundQtyDownToStep(amount, qtyStep),
|
log("error", "限价单数量无效,跳过下单");
|
||||||
price: priceNum, // 直接使用字符串转换的数字,不再格式化
|
return;
|
||||||
timeInForce: "GTX",
|
}
|
||||||
};
|
|
||||||
if (reduceOnly) params.reduceOnly = "true";
|
|
||||||
if (!opts?.skipDedupe) {
|
if (!opts?.skipDedupe) {
|
||||||
await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log);
|
await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log);
|
||||||
}
|
}
|
||||||
lockOperating(locks, timers, pendings, type, log);
|
lockOperating(locks, timers, pendings, type, log);
|
||||||
try {
|
try {
|
||||||
const order = await adapter.createOrder(params);
|
const order = await routeLimitOrder({
|
||||||
|
adapter,
|
||||||
|
symbol,
|
||||||
|
side,
|
||||||
|
quantity,
|
||||||
|
price: priceNum,
|
||||||
|
timeInForce: "GTX",
|
||||||
|
reduceOnly: reduceOnly ? true : undefined,
|
||||||
|
});
|
||||||
pendings[type] = String(order.orderId);
|
pendings[type] = String(order.orderId);
|
||||||
log("order", `挂限价单: ${side} @ ${params.price} 数量 ${params.quantity} reduceOnly=${reduceOnly}`);
|
log("order", `挂限价单: ${side} @ ${priceNum} 数量 ${quantity} reduceOnly=${reduceOnly}`);
|
||||||
return order;
|
return order;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
unlockOperating(locks, timers, pendings, type);
|
unlockOperating(locks, timers, pendings, type);
|
||||||
@@ -194,19 +206,25 @@ export async function placeMarketOrder(
|
|||||||
if (isOperating(locks, type)) return;
|
if (isOperating(locks, type)) return;
|
||||||
if (!enforceMarkPriceGuard(side, guard?.expectedPrice ?? null, guard, log, "市价单")) return;
|
if (!enforceMarkPriceGuard(side, guard?.expectedPrice ?? null, guard, log, "市价单")) return;
|
||||||
const qtyStep = opts?.qtyStep ?? 0.001;
|
const qtyStep = opts?.qtyStep ?? 0.001;
|
||||||
const params: CreateOrderParams = {
|
const rawQuantity = Math.abs(amount);
|
||||||
symbol,
|
const roundedQuantity = roundQtyDownToStep(rawQuantity, qtyStep);
|
||||||
side,
|
const quantity = roundedQuantity > 0 ? roundedQuantity : rawQuantity;
|
||||||
type,
|
if (quantity <= 0) {
|
||||||
quantity: roundQtyDownToStep(amount, qtyStep),
|
log("error", "市价单数量无效,跳过下单");
|
||||||
};
|
return;
|
||||||
if (reduceOnly) params.reduceOnly = "true";
|
}
|
||||||
await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log);
|
await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log);
|
||||||
lockOperating(locks, timers, pendings, type, log);
|
lockOperating(locks, timers, pendings, type, log);
|
||||||
try {
|
try {
|
||||||
const order = await adapter.createOrder(params);
|
const order = await routeMarketOrder({
|
||||||
|
adapter,
|
||||||
|
symbol,
|
||||||
|
side,
|
||||||
|
quantity,
|
||||||
|
reduceOnly: reduceOnly ? true : undefined,
|
||||||
|
});
|
||||||
pendings[type] = String(order.orderId);
|
pendings[type] = String(order.orderId);
|
||||||
log("order", `市价单: ${side} 数量 ${params.quantity} reduceOnly=${reduceOnly}`);
|
log("order", `市价单: ${side} 数量 ${quantity} reduceOnly=${reduceOnly}`);
|
||||||
return order;
|
return order;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
unlockOperating(locks, timers, pendings, type);
|
unlockOperating(locks, timers, pendings, type);
|
||||||
@@ -248,29 +266,32 @@ export async function placeStopLossOrder(
|
|||||||
}
|
}
|
||||||
const priceTick = opts?.priceTick ?? 0.1;
|
const priceTick = opts?.priceTick ?? 0.1;
|
||||||
const qtyStep = opts?.qtyStep ?? 0.001;
|
const qtyStep = opts?.qtyStep ?? 0.001;
|
||||||
|
const normalizedStop = roundDownToTick(stopPrice, priceTick);
|
||||||
const params: CreateOrderParams = {
|
const rawQuantity = Math.abs(quantity);
|
||||||
symbol,
|
const roundedQuantity = roundQtyDownToStep(rawQuantity, qtyStep);
|
||||||
side,
|
const normalizedQty = roundedQuantity > 0 ? roundedQuantity : rawQuantity;
|
||||||
type,
|
if (normalizedQty <= 0) {
|
||||||
quantity: roundQtyDownToStep(quantity, qtyStep),
|
log("error", "止损单数量无效,跳过下单");
|
||||||
stopPrice: roundDownToTick(stopPrice, priceTick),
|
return;
|
||||||
// Always mark reduce-only semantics; some exchanges (e.g. Aster) ignore this on STOP
|
}
|
||||||
reduceOnly: "true",
|
|
||||||
// Some exchanges prefer explicit close-position semantics; gateways will normalize
|
|
||||||
closePosition: "true",
|
|
||||||
timeInForce: "GTC",
|
|
||||||
// GRVT requires triggerType to match side semantics: BUY -> TAKE_PROFIT, SELL -> STOP_LOSS
|
|
||||||
triggerType: side === "BUY" ? "TAKE_PROFIT" : "STOP_LOSS",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Avoid forcing price for STOP_MARKET globally; keep this exchange-specific in gateways
|
// Avoid forcing price for STOP_MARKET globally; keep this exchange-specific in gateways
|
||||||
await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log);
|
await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log);
|
||||||
lockOperating(locks, timers, pendings, type, log);
|
lockOperating(locks, timers, pendings, type, log);
|
||||||
try {
|
try {
|
||||||
const order = await adapter.createOrder(params);
|
const order = await routeStopOrder({
|
||||||
|
adapter,
|
||||||
|
symbol,
|
||||||
|
side,
|
||||||
|
quantity: normalizedQty,
|
||||||
|
stopPrice: normalizedStop,
|
||||||
|
timeInForce: "GTC",
|
||||||
|
reduceOnly: true,
|
||||||
|
closePosition: true,
|
||||||
|
triggerType: side === "BUY" ? "TAKE_PROFIT" : "STOP_LOSS",
|
||||||
|
});
|
||||||
pendings[type] = String(order.orderId);
|
pendings[type] = String(order.orderId);
|
||||||
log("stop", `挂止损单: ${side} STOP_MARKET @ ${params.stopPrice}`);
|
log("stop", `挂止损单: ${side} STOP_MARKET @ ${normalizedStop}`);
|
||||||
return order;
|
return order;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
unlockOperating(locks, timers, pendings, type);
|
unlockOperating(locks, timers, pendings, type);
|
||||||
@@ -299,27 +320,38 @@ export async function placeTrailingStopOrder(
|
|||||||
): Promise<AsterOrder | undefined> {
|
): Promise<AsterOrder | undefined> {
|
||||||
const type = "TRAILING_STOP_MARKET";
|
const type = "TRAILING_STOP_MARKET";
|
||||||
if (isOperating(locks, type)) return;
|
if (isOperating(locks, type)) return;
|
||||||
|
if (!adapter.supportsTrailingStops()) {
|
||||||
|
log("error", "当前交易所不支持动态止盈单");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!enforceMarkPriceGuard(side, activationPrice, guard, log, "动态止盈单")) return;
|
if (!enforceMarkPriceGuard(side, activationPrice, guard, log, "动态止盈单")) return;
|
||||||
const priceTick = opts?.priceTick ?? 0.1;
|
const priceTick = opts?.priceTick ?? 0.1;
|
||||||
const qtyStep = opts?.qtyStep ?? 0.001;
|
const qtyStep = opts?.qtyStep ?? 0.001;
|
||||||
const params: CreateOrderParams = {
|
const normalizedActivation = roundDownToTick(activationPrice, priceTick);
|
||||||
symbol,
|
const rawQuantity = Math.abs(quantity);
|
||||||
side,
|
const roundedQuantity = roundQtyDownToStep(rawQuantity, qtyStep);
|
||||||
type,
|
const normalizedQty = roundedQuantity > 0 ? roundedQuantity : rawQuantity;
|
||||||
quantity,
|
if (normalizedQty <= 0) {
|
||||||
reduceOnly: "true",
|
log("error", "动态止盈单数量无效,跳过下单");
|
||||||
activationPrice: roundDownToTick(activationPrice, priceTick),
|
return;
|
||||||
callbackRate,
|
}
|
||||||
timeInForce: "GTC",
|
|
||||||
};
|
|
||||||
await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log);
|
await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log);
|
||||||
lockOperating(locks, timers, pendings, type, log);
|
lockOperating(locks, timers, pendings, type, log);
|
||||||
try {
|
try {
|
||||||
const order = await adapter.createOrder(params);
|
const order = await routeTrailingStopOrder({
|
||||||
|
adapter,
|
||||||
|
symbol,
|
||||||
|
side,
|
||||||
|
quantity: normalizedQty,
|
||||||
|
activationPrice: normalizedActivation,
|
||||||
|
callbackRate,
|
||||||
|
timeInForce: "GTC",
|
||||||
|
reduceOnly: true,
|
||||||
|
});
|
||||||
pendings[type] = String(order.orderId);
|
pendings[type] = String(order.orderId);
|
||||||
log(
|
log(
|
||||||
"order",
|
"order",
|
||||||
`挂动态止盈单: ${side} activation=${params.activationPrice} callbackRate=${callbackRate}`
|
`挂动态止盈单: ${side} activation=${normalizedActivation} callbackRate=${callbackRate}`
|
||||||
);
|
);
|
||||||
return order;
|
return order;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -349,18 +381,26 @@ export async function marketClose(
|
|||||||
if (isOperating(locks, type)) return;
|
if (isOperating(locks, type)) return;
|
||||||
if (!enforceMarkPriceGuard(side, guard?.expectedPrice ?? null, guard, log, "市价平仓")) return;
|
if (!enforceMarkPriceGuard(side, guard?.expectedPrice ?? null, guard, log, "市价平仓")) return;
|
||||||
|
|
||||||
const params: CreateOrderParams = {
|
const qtyStep = opts?.qtyStep;
|
||||||
symbol,
|
const rawQuantity = Math.abs(quantity);
|
||||||
side,
|
const normalizedQtyRaw = qtyStep != null ? roundQtyDownToStep(rawQuantity, qtyStep) : rawQuantity;
|
||||||
type,
|
const normalizedQty = normalizedQtyRaw > 0 ? normalizedQtyRaw : rawQuantity;
|
||||||
quantity,
|
if (normalizedQty <= 0) {
|
||||||
reduceOnly: "true",
|
log("error", "市价平仓数量无效,跳过下单");
|
||||||
};
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log);
|
await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log);
|
||||||
lockOperating(locks, timers, pendings, type, log);
|
lockOperating(locks, timers, pendings, type, log);
|
||||||
try {
|
try {
|
||||||
const order = await adapter.createOrder(params);
|
const order = await routeCloseOrder({
|
||||||
|
adapter,
|
||||||
|
symbol,
|
||||||
|
side,
|
||||||
|
quantity: normalizedQty,
|
||||||
|
reduceOnly: true,
|
||||||
|
closePosition: true,
|
||||||
|
});
|
||||||
pendings[type] = String(order.orderId);
|
pendings[type] = String(order.orderId);
|
||||||
log("close", `市价平仓: ${side}`);
|
log("close", `市价平仓: ${side}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import type { AsterOrder, CreateOrderParams } from "../types";
|
||||||
|
import type {
|
||||||
|
BaseOrderIntent,
|
||||||
|
ClosePositionIntent,
|
||||||
|
LimitOrderIntent,
|
||||||
|
MarketOrderIntent,
|
||||||
|
StopOrderIntent,
|
||||||
|
TrailingStopOrderIntent,
|
||||||
|
} from "../order-schema";
|
||||||
|
import { toStringBoolean } from "../order-schema";
|
||||||
|
|
||||||
|
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams {
|
||||||
|
if (params.quantity === undefined) {
|
||||||
|
params.quantity = intent.quantity;
|
||||||
|
}
|
||||||
|
if (params.timeInForce === undefined && intent.timeInForce) {
|
||||||
|
params.timeInForce = intent.timeInForce;
|
||||||
|
}
|
||||||
|
if (intent.reduceOnly !== undefined) {
|
||||||
|
params.reduceOnly = toStringBoolean(intent.reduceOnly);
|
||||||
|
}
|
||||||
|
if (intent.closePosition !== undefined) {
|
||||||
|
params.closePosition = toStringBoolean(intent.closePosition);
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
|
||||||
|
const params: CreateOrderParams = applyCommonFields(
|
||||||
|
{
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side: intent.side,
|
||||||
|
type: "LIMIT",
|
||||||
|
quantity: intent.quantity,
|
||||||
|
price: intent.price,
|
||||||
|
timeInForce: intent.timeInForce ?? "GTX",
|
||||||
|
},
|
||||||
|
intent
|
||||||
|
);
|
||||||
|
return intent.adapter.createOrder(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
|
||||||
|
const params: CreateOrderParams = applyCommonFields(
|
||||||
|
{
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side: intent.side,
|
||||||
|
type: "MARKET",
|
||||||
|
quantity: intent.quantity,
|
||||||
|
},
|
||||||
|
intent
|
||||||
|
);
|
||||||
|
return intent.adapter.createOrder(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
|
||||||
|
const params: CreateOrderParams = applyCommonFields(
|
||||||
|
{
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side: intent.side,
|
||||||
|
type: "STOP_MARKET",
|
||||||
|
quantity: intent.quantity,
|
||||||
|
stopPrice: intent.stopPrice,
|
||||||
|
timeInForce: intent.timeInForce ?? "GTC",
|
||||||
|
triggerType: intent.triggerType,
|
||||||
|
},
|
||||||
|
intent
|
||||||
|
);
|
||||||
|
return intent.adapter.createOrder(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTrailingStopOrder(intent: TrailingStopOrderIntent): Promise<AsterOrder> {
|
||||||
|
const params: CreateOrderParams = applyCommonFields(
|
||||||
|
{
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side: intent.side,
|
||||||
|
type: "TRAILING_STOP_MARKET",
|
||||||
|
quantity: intent.quantity,
|
||||||
|
activationPrice: intent.activationPrice,
|
||||||
|
callbackRate: intent.callbackRate,
|
||||||
|
timeInForce: intent.timeInForce ?? "GTC",
|
||||||
|
},
|
||||||
|
intent
|
||||||
|
);
|
||||||
|
return intent.adapter.createOrder(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createClosePositionOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
|
||||||
|
const params: CreateOrderParams = applyCommonFields(
|
||||||
|
{
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side: intent.side,
|
||||||
|
type: "MARKET",
|
||||||
|
quantity: intent.quantity,
|
||||||
|
reduceOnly: "true",
|
||||||
|
},
|
||||||
|
intent
|
||||||
|
);
|
||||||
|
return intent.adapter.createOrder(params);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import type { AsterOrder, CreateOrderParams } from "../types";
|
||||||
|
import type {
|
||||||
|
BaseOrderIntent,
|
||||||
|
ClosePositionIntent,
|
||||||
|
LimitOrderIntent,
|
||||||
|
MarketOrderIntent,
|
||||||
|
StopOrderIntent,
|
||||||
|
TrailingStopOrderIntent,
|
||||||
|
} from "../order-schema";
|
||||||
|
import { toStringBoolean } from "../order-schema";
|
||||||
|
|
||||||
|
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams {
|
||||||
|
if (params.quantity === undefined) {
|
||||||
|
params.quantity = intent.quantity;
|
||||||
|
}
|
||||||
|
if (params.timeInForce === undefined && intent.timeInForce) {
|
||||||
|
params.timeInForce = intent.timeInForce;
|
||||||
|
}
|
||||||
|
if (intent.reduceOnly !== undefined) {
|
||||||
|
params.reduceOnly = toStringBoolean(intent.reduceOnly);
|
||||||
|
}
|
||||||
|
if (intent.closePosition !== undefined) {
|
||||||
|
params.closePosition = toStringBoolean(intent.closePosition);
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
|
||||||
|
const params: CreateOrderParams = applyCommonFields(
|
||||||
|
{
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side: intent.side,
|
||||||
|
type: "LIMIT",
|
||||||
|
quantity: intent.quantity,
|
||||||
|
price: intent.price,
|
||||||
|
timeInForce: intent.timeInForce ?? "GTX",
|
||||||
|
},
|
||||||
|
intent
|
||||||
|
);
|
||||||
|
return intent.adapter.createOrder(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
|
||||||
|
const params: CreateOrderParams = applyCommonFields(
|
||||||
|
{
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side: intent.side,
|
||||||
|
type: "MARKET",
|
||||||
|
quantity: intent.quantity,
|
||||||
|
},
|
||||||
|
intent
|
||||||
|
);
|
||||||
|
return intent.adapter.createOrder(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
|
||||||
|
const params: CreateOrderParams = applyCommonFields(
|
||||||
|
{
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side: intent.side,
|
||||||
|
type: "STOP_MARKET",
|
||||||
|
quantity: intent.quantity,
|
||||||
|
stopPrice: intent.stopPrice,
|
||||||
|
timeInForce: intent.timeInForce ?? "GTC",
|
||||||
|
},
|
||||||
|
intent
|
||||||
|
);
|
||||||
|
return intent.adapter.createOrder(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTrailingStopOrder(_intent: TrailingStopOrderIntent): Promise<AsterOrder> {
|
||||||
|
throw new Error("Backpack exchange does not support trailing stop orders");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createClosePositionOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
|
||||||
|
const params: CreateOrderParams = applyCommonFields(
|
||||||
|
{
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side: intent.side,
|
||||||
|
type: "MARKET",
|
||||||
|
quantity: intent.quantity,
|
||||||
|
reduceOnly: "true",
|
||||||
|
},
|
||||||
|
intent
|
||||||
|
);
|
||||||
|
return intent.adapter.createOrder(params);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import type { AsterOrder, CreateOrderParams } from "../types";
|
||||||
|
import type {
|
||||||
|
BaseOrderIntent,
|
||||||
|
ClosePositionIntent,
|
||||||
|
LimitOrderIntent,
|
||||||
|
MarketOrderIntent,
|
||||||
|
StopOrderIntent,
|
||||||
|
TrailingStopOrderIntent,
|
||||||
|
} from "../order-schema";
|
||||||
|
import { toStringBoolean } from "../order-schema";
|
||||||
|
|
||||||
|
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams {
|
||||||
|
if (params.quantity === undefined) {
|
||||||
|
params.quantity = intent.quantity;
|
||||||
|
}
|
||||||
|
if (params.timeInForce === undefined && intent.timeInForce) {
|
||||||
|
params.timeInForce = intent.timeInForce;
|
||||||
|
}
|
||||||
|
if (intent.reduceOnly !== undefined) {
|
||||||
|
params.reduceOnly = toStringBoolean(intent.reduceOnly);
|
||||||
|
}
|
||||||
|
if (intent.closePosition !== undefined) {
|
||||||
|
params.closePosition = toStringBoolean(intent.closePosition);
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
|
||||||
|
const params: CreateOrderParams = applyCommonFields(
|
||||||
|
{
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side: intent.side,
|
||||||
|
type: "LIMIT",
|
||||||
|
quantity: intent.quantity,
|
||||||
|
price: intent.price,
|
||||||
|
timeInForce: intent.timeInForce ?? "GTX",
|
||||||
|
},
|
||||||
|
intent
|
||||||
|
);
|
||||||
|
return intent.adapter.createOrder(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
|
||||||
|
const params: CreateOrderParams = applyCommonFields(
|
||||||
|
{
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side: intent.side,
|
||||||
|
type: "MARKET",
|
||||||
|
quantity: intent.quantity,
|
||||||
|
},
|
||||||
|
intent
|
||||||
|
);
|
||||||
|
return intent.adapter.createOrder(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
|
||||||
|
const params: CreateOrderParams = applyCommonFields(
|
||||||
|
{
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side: intent.side,
|
||||||
|
type: "STOP_MARKET",
|
||||||
|
quantity: intent.quantity,
|
||||||
|
stopPrice: intent.stopPrice,
|
||||||
|
timeInForce: intent.timeInForce ?? "GTC",
|
||||||
|
triggerType: intent.triggerType ?? (intent.side === "BUY" ? "TAKE_PROFIT" : "STOP_LOSS"),
|
||||||
|
closePosition: toStringBoolean(intent.closePosition ?? true),
|
||||||
|
reduceOnly: toStringBoolean(intent.reduceOnly ?? true),
|
||||||
|
},
|
||||||
|
intent
|
||||||
|
);
|
||||||
|
return intent.adapter.createOrder(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTrailingStopOrder(_intent: TrailingStopOrderIntent): Promise<AsterOrder> {
|
||||||
|
throw new Error("GRVT exchange does not support trailing stop orders");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createClosePositionOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
|
||||||
|
const params: CreateOrderParams = applyCommonFields(
|
||||||
|
{
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side: intent.side,
|
||||||
|
type: "MARKET",
|
||||||
|
quantity: intent.quantity,
|
||||||
|
reduceOnly: "true",
|
||||||
|
closePosition: toStringBoolean(intent.closePosition ?? true),
|
||||||
|
},
|
||||||
|
intent
|
||||||
|
);
|
||||||
|
return intent.adapter.createOrder(params);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import type { AsterOrder, CreateOrderParams } from "../types";
|
||||||
|
import type {
|
||||||
|
BaseOrderIntent,
|
||||||
|
ClosePositionIntent,
|
||||||
|
LimitOrderIntent,
|
||||||
|
MarketOrderIntent,
|
||||||
|
StopOrderIntent,
|
||||||
|
TrailingStopOrderIntent,
|
||||||
|
} from "../order-schema";
|
||||||
|
import { toStringBoolean } from "../order-schema";
|
||||||
|
|
||||||
|
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams {
|
||||||
|
if (params.quantity === undefined) {
|
||||||
|
params.quantity = intent.quantity;
|
||||||
|
}
|
||||||
|
if (params.timeInForce === undefined && intent.timeInForce) {
|
||||||
|
params.timeInForce = intent.timeInForce;
|
||||||
|
}
|
||||||
|
if (intent.reduceOnly !== undefined) {
|
||||||
|
params.reduceOnly = toStringBoolean(intent.reduceOnly);
|
||||||
|
}
|
||||||
|
if (intent.closePosition !== undefined) {
|
||||||
|
params.closePosition = toStringBoolean(intent.closePosition);
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
|
||||||
|
const params: CreateOrderParams = applyCommonFields(
|
||||||
|
{
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side: intent.side,
|
||||||
|
type: "LIMIT",
|
||||||
|
quantity: intent.quantity,
|
||||||
|
price: intent.price,
|
||||||
|
timeInForce: intent.timeInForce ?? "GTC",
|
||||||
|
},
|
||||||
|
intent
|
||||||
|
);
|
||||||
|
return intent.adapter.createOrder(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
|
||||||
|
const params: CreateOrderParams = applyCommonFields(
|
||||||
|
{
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side: intent.side,
|
||||||
|
type: "MARKET",
|
||||||
|
quantity: intent.quantity,
|
||||||
|
timeInForce: intent.timeInForce ?? "IOC",
|
||||||
|
},
|
||||||
|
intent
|
||||||
|
);
|
||||||
|
return intent.adapter.createOrder(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
|
||||||
|
const params: CreateOrderParams = applyCommonFields(
|
||||||
|
{
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side: intent.side,
|
||||||
|
type: "STOP_MARKET",
|
||||||
|
quantity: intent.quantity,
|
||||||
|
stopPrice: intent.stopPrice,
|
||||||
|
timeInForce: intent.timeInForce ?? "GTC",
|
||||||
|
reduceOnly: toStringBoolean(intent.reduceOnly ?? true),
|
||||||
|
closePosition: toStringBoolean(intent.closePosition ?? true),
|
||||||
|
},
|
||||||
|
intent
|
||||||
|
);
|
||||||
|
return intent.adapter.createOrder(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTrailingStopOrder(_intent: TrailingStopOrderIntent): Promise<AsterOrder> {
|
||||||
|
throw new Error("Lighter exchange does not support trailing stop orders");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createClosePositionOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
|
||||||
|
const params: CreateOrderParams = applyCommonFields(
|
||||||
|
{
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side: intent.side,
|
||||||
|
type: "MARKET",
|
||||||
|
quantity: intent.quantity,
|
||||||
|
reduceOnly: "true",
|
||||||
|
closePosition: toStringBoolean(intent.closePosition ?? true),
|
||||||
|
timeInForce: intent.timeInForce ?? "IOC",
|
||||||
|
},
|
||||||
|
intent
|
||||||
|
);
|
||||||
|
return intent.adapter.createOrder(params);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import type { ExchangeAdapter } from "./adapter";
|
||||||
|
import type { AsterOrder } from "./types";
|
||||||
|
import type {
|
||||||
|
BaseOrderIntent,
|
||||||
|
ClosePositionIntent,
|
||||||
|
LimitOrderIntent,
|
||||||
|
MarketOrderIntent,
|
||||||
|
StopOrderIntent,
|
||||||
|
TrailingStopOrderIntent,
|
||||||
|
} from "./order-schema";
|
||||||
|
import * as asterOrders from "./aster/order";
|
||||||
|
import * as backpackOrders from "./backpack/order";
|
||||||
|
import * as grvtOrders from "./grvt/order";
|
||||||
|
import * as lighterOrders from "./lighter/order";
|
||||||
|
import * as paradexOrders from "./paradex/order";
|
||||||
|
|
||||||
|
type ExchangeKey = "aster" | "backpack" | "grvt" | "lighter" | "paradex";
|
||||||
|
|
||||||
|
interface ExchangeOrderHandlers {
|
||||||
|
limit(intent: LimitOrderIntent): Promise<AsterOrder>;
|
||||||
|
market(intent: MarketOrderIntent): Promise<AsterOrder>;
|
||||||
|
stop(intent: StopOrderIntent): Promise<AsterOrder>;
|
||||||
|
trailingStop?: (intent: TrailingStopOrderIntent) => Promise<AsterOrder>;
|
||||||
|
close(intent: ClosePositionIntent): Promise<AsterOrder>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlerMap: Record<ExchangeKey, ExchangeOrderHandlers> = {
|
||||||
|
aster: {
|
||||||
|
limit: asterOrders.createLimitOrder,
|
||||||
|
market: asterOrders.createMarketOrder,
|
||||||
|
stop: asterOrders.createStopOrder,
|
||||||
|
trailingStop: asterOrders.createTrailingStopOrder,
|
||||||
|
close: asterOrders.createClosePositionOrder,
|
||||||
|
},
|
||||||
|
backpack: {
|
||||||
|
limit: backpackOrders.createLimitOrder,
|
||||||
|
market: backpackOrders.createMarketOrder,
|
||||||
|
stop: backpackOrders.createStopOrder,
|
||||||
|
trailingStop: backpackOrders.createTrailingStopOrder,
|
||||||
|
close: backpackOrders.createClosePositionOrder,
|
||||||
|
},
|
||||||
|
grvt: {
|
||||||
|
limit: grvtOrders.createLimitOrder,
|
||||||
|
market: grvtOrders.createMarketOrder,
|
||||||
|
stop: grvtOrders.createStopOrder,
|
||||||
|
trailingStop: grvtOrders.createTrailingStopOrder,
|
||||||
|
close: grvtOrders.createClosePositionOrder,
|
||||||
|
},
|
||||||
|
lighter: {
|
||||||
|
limit: lighterOrders.createLimitOrder,
|
||||||
|
market: lighterOrders.createMarketOrder,
|
||||||
|
stop: lighterOrders.createStopOrder,
|
||||||
|
trailingStop: lighterOrders.createTrailingStopOrder,
|
||||||
|
close: lighterOrders.createClosePositionOrder,
|
||||||
|
},
|
||||||
|
paradex: {
|
||||||
|
limit: paradexOrders.createLimitOrder,
|
||||||
|
market: paradexOrders.createMarketOrder,
|
||||||
|
stop: paradexOrders.createStopOrder,
|
||||||
|
trailingStop: paradexOrders.createTrailingStopOrder,
|
||||||
|
close: paradexOrders.createClosePositionOrder,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const knownExchanges: ExchangeKey[] = ["aster", "backpack", "grvt", "lighter", "paradex"];
|
||||||
|
|
||||||
|
function normalizeExchangeId(value: string | undefined | null): string | undefined {
|
||||||
|
if (!value) return undefined;
|
||||||
|
return value.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveExchangeKey(adapter: ExchangeAdapter): ExchangeKey {
|
||||||
|
const fromEnv = normalizeExchangeId(process.env.TRADE_EXCHANGE ?? process.env.EXCHANGE);
|
||||||
|
const candidates = [fromEnv, normalizeExchangeId(adapter.id)];
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (!candidate) continue;
|
||||||
|
if ((knownExchanges as string[]).includes(candidate)) {
|
||||||
|
return candidate as ExchangeKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`Unsupported exchange for order routing: ${candidates.filter(Boolean).join(", ") || "unknown"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHandlers(intent: BaseOrderIntent): ExchangeOrderHandlers {
|
||||||
|
const exchangeKey = resolveExchangeKey(intent.adapter);
|
||||||
|
const handlers = handlerMap[exchangeKey];
|
||||||
|
if (!handlers) {
|
||||||
|
throw new Error(`Order handlers not implemented for exchange: ${exchangeKey}`);
|
||||||
|
}
|
||||||
|
return handlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function routeLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
|
||||||
|
return getHandlers(intent).limit(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function routeMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
|
||||||
|
return getHandlers(intent).market(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function routeStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
|
||||||
|
return getHandlers(intent).stop(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function routeTrailingStopOrder(intent: TrailingStopOrderIntent): Promise<AsterOrder> {
|
||||||
|
const handlers = getHandlers(intent);
|
||||||
|
if (!handlers.trailingStop) {
|
||||||
|
throw new Error("Trailing stop orders are not supported by the current exchange");
|
||||||
|
}
|
||||||
|
return handlers.trailingStop(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function routeCloseOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
|
||||||
|
return getHandlers(intent).close(intent);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import type { ExchangeAdapter } from "./adapter";
|
||||||
|
import type { OrderSide, TimeInForce } from "./types";
|
||||||
|
|
||||||
|
export interface BaseOrderIntent {
|
||||||
|
adapter: ExchangeAdapter;
|
||||||
|
symbol: string;
|
||||||
|
side: OrderSide;
|
||||||
|
quantity: number;
|
||||||
|
reduceOnly?: boolean;
|
||||||
|
closePosition?: boolean;
|
||||||
|
timeInForce?: TimeInForce | "GTX";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LimitOrderIntent extends BaseOrderIntent {
|
||||||
|
price: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarketOrderIntent extends BaseOrderIntent {
|
||||||
|
expectedPrice?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StopOrderIntent extends BaseOrderIntent {
|
||||||
|
stopPrice: number;
|
||||||
|
triggerType?: "UNSPECIFIED" | "TAKE_PROFIT" | "STOP_LOSS";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TrailingStopOrderIntent extends BaseOrderIntent {
|
||||||
|
activationPrice: number;
|
||||||
|
callbackRate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClosePositionIntent extends BaseOrderIntent {
|
||||||
|
expectedPrice?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ExchangeOrderType = "limit" | "market" | "stop" | "trailingStop" | "close";
|
||||||
|
|
||||||
|
export function toStringBoolean(value: boolean | undefined): "true" | "false" | undefined {
|
||||||
|
if (value === undefined) return undefined;
|
||||||
|
return value ? "true" : "false";
|
||||||
|
}
|
||||||
|
|
||||||
@@ -602,6 +602,9 @@ export class ParadexGateway {
|
|||||||
// Only omit amount for MARKET close-position orders; STOP requires explicit size
|
// Only omit amount for MARKET close-position orders; STOP requires explicit size
|
||||||
const shouldOmitAmount = isClosePosition && type === "market";
|
const shouldOmitAmount = isClosePosition && type === "market";
|
||||||
const amountArg: any = shouldOmitAmount ? undefined : amount;
|
const amountArg: any = shouldOmitAmount ? undefined : amount;
|
||||||
|
if (!shouldOmitAmount && amountArg != null && extraParams.size === undefined) {
|
||||||
|
extraParams.size = amountArg.toString();
|
||||||
|
}
|
||||||
const order = (await this.exchange.createOrder(
|
const order = (await this.exchange.createOrder(
|
||||||
symbol,
|
symbol,
|
||||||
type,
|
type,
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import type { AsterOrder, CreateOrderParams } from "../types";
|
||||||
|
import type {
|
||||||
|
BaseOrderIntent,
|
||||||
|
ClosePositionIntent,
|
||||||
|
LimitOrderIntent,
|
||||||
|
MarketOrderIntent,
|
||||||
|
StopOrderIntent,
|
||||||
|
TrailingStopOrderIntent,
|
||||||
|
} from "../order-schema";
|
||||||
|
import { toStringBoolean } from "../order-schema";
|
||||||
|
|
||||||
|
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams {
|
||||||
|
if (params.quantity === undefined) {
|
||||||
|
params.quantity = intent.quantity;
|
||||||
|
}
|
||||||
|
if (params.timeInForce === undefined && intent.timeInForce) {
|
||||||
|
params.timeInForce = intent.timeInForce;
|
||||||
|
}
|
||||||
|
if (intent.reduceOnly !== undefined) {
|
||||||
|
params.reduceOnly = toStringBoolean(intent.reduceOnly);
|
||||||
|
}
|
||||||
|
if (intent.closePosition !== undefined) {
|
||||||
|
params.closePosition = toStringBoolean(intent.closePosition);
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
|
||||||
|
const params: CreateOrderParams = applyCommonFields(
|
||||||
|
{
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side: intent.side,
|
||||||
|
type: "LIMIT",
|
||||||
|
quantity: intent.quantity,
|
||||||
|
price: intent.price,
|
||||||
|
timeInForce: intent.timeInForce ?? "GTC",
|
||||||
|
},
|
||||||
|
intent
|
||||||
|
);
|
||||||
|
return intent.adapter.createOrder(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
|
||||||
|
const params: CreateOrderParams = applyCommonFields(
|
||||||
|
{
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side: intent.side,
|
||||||
|
type: "MARKET",
|
||||||
|
quantity: intent.quantity,
|
||||||
|
timeInForce: intent.timeInForce,
|
||||||
|
},
|
||||||
|
intent
|
||||||
|
);
|
||||||
|
return intent.adapter.createOrder(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
|
||||||
|
const params: CreateOrderParams = applyCommonFields(
|
||||||
|
{
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side: intent.side,
|
||||||
|
type: "STOP_MARKET",
|
||||||
|
quantity: intent.quantity,
|
||||||
|
stopPrice: intent.stopPrice,
|
||||||
|
price: intent.stopPrice,
|
||||||
|
timeInForce: intent.timeInForce ?? "GTC",
|
||||||
|
reduceOnly: toStringBoolean(intent.reduceOnly ?? true),
|
||||||
|
closePosition: toStringBoolean(intent.closePosition ?? true),
|
||||||
|
},
|
||||||
|
intent
|
||||||
|
);
|
||||||
|
return intent.adapter.createOrder(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTrailingStopOrder(_intent: TrailingStopOrderIntent): Promise<AsterOrder> {
|
||||||
|
throw new Error("Paradex exchange does not support trailing stop orders");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createClosePositionOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
|
||||||
|
const params: CreateOrderParams = applyCommonFields(
|
||||||
|
{
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side: intent.side,
|
||||||
|
type: "MARKET",
|
||||||
|
quantity: intent.quantity,
|
||||||
|
reduceOnly: "true",
|
||||||
|
closePosition: toStringBoolean(intent.closePosition ?? true),
|
||||||
|
timeInForce: intent.timeInForce,
|
||||||
|
},
|
||||||
|
intent
|
||||||
|
);
|
||||||
|
return intent.adapter.createOrder(params);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi, beforeEach, afterAll } from "vitest";
|
||||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||||
import type { AsterOrder } from "../src/exchanges/types";
|
import type { AsterOrder } from "../src/exchanges/types";
|
||||||
import type { OrderLockMap, OrderTimerMap, OrderPendingMap } from "../src/core/order-coordinator";
|
import type { OrderLockMap, OrderTimerMap, OrderPendingMap } from "../src/core/order-coordinator";
|
||||||
@@ -12,6 +12,9 @@ import {
|
|||||||
unlockOperating,
|
unlockOperating,
|
||||||
} from "../src/core/order-coordinator";
|
} from "../src/core/order-coordinator";
|
||||||
|
|
||||||
|
const originalTradeExchange = process.env.TRADE_EXCHANGE;
|
||||||
|
const originalExchange = process.env.EXCHANGE;
|
||||||
|
|
||||||
const baseOrder: AsterOrder = {
|
const baseOrder: AsterOrder = {
|
||||||
orderId: 1,
|
orderId: 1,
|
||||||
clientOrderId: "client",
|
clientOrderId: "client",
|
||||||
@@ -47,6 +50,16 @@ function createMockExchange(overrides: Partial<ExchangeAdapter> = {}): ExchangeA
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("order-coordinator", () => {
|
describe("order-coordinator", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env.TRADE_EXCHANGE = "aster";
|
||||||
|
process.env.EXCHANGE = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
process.env.TRADE_EXCHANGE = originalTradeExchange;
|
||||||
|
process.env.EXCHANGE = originalExchange;
|
||||||
|
});
|
||||||
|
|
||||||
it("deduplicates orders by type and side", async () => {
|
it("deduplicates orders by type and side", async () => {
|
||||||
const adapter = createMockExchange();
|
const adapter = createMockExchange();
|
||||||
const locks: OrderLockMap = {};
|
const locks: OrderLockMap = {};
|
||||||
|
|||||||
Reference in New Issue
Block a user