优化做市引擎、偏移做市引擎和趋势引擎的订单管理逻辑,将待取消订单的类型从数字更改为字符串,确保在处理订单时的类型一致性。同时,增强风险检查逻辑,确保在平仓时使用正确的价格,提升交易策略的稳定性和准确性。

This commit is contained in:
discountry
2025-09-27 17:05:51 +08:00
parent dde7ab71a9
commit 0bb22d891f
9 changed files with 217 additions and 92 deletions
+1 -2
View File
@@ -6,7 +6,7 @@ export async function safeCancelOrder(
exchange: ExchangeAdapter, exchange: ExchangeAdapter,
symbol: string, symbol: string,
order: AsterOrder, order: AsterOrder,
onResolved: (orderId: number) => void, onResolved: (orderId: number | string) => void,
onUnknown: () => void, onUnknown: () => void,
onError: (err: unknown) => void onError: (err: unknown) => void
): Promise<void> { ): Promise<void> {
@@ -19,4 +19,3 @@ export async function safeCancelOrder(
} }
} }
+18 -16
View File
@@ -60,7 +60,7 @@ export class MakerEngine {
private readonly locks: OrderLockMap = {}; private readonly locks: OrderLockMap = {};
private readonly timers: OrderTimerMap = {}; private readonly timers: OrderTimerMap = {};
private readonly pending: OrderPendingMap = {}; private readonly pending: OrderPendingMap = {};
private readonly pendingCancelOrders = new Set<number>(); private readonly pendingCancelOrders = new Set<string>();
private readonly tradeLog: ReturnType<typeof createTradeLog>; private readonly tradeLog: ReturnType<typeof createTradeLog>;
private readonly listeners = new Map<MakerEvent, Set<MakerListener>>(); private readonly listeners = new Map<MakerEvent, Set<MakerListener>>();
@@ -145,7 +145,7 @@ export class MakerEngine {
this.openOrders = Array.isArray(orders) this.openOrders = Array.isArray(orders)
? orders.filter((order) => order.type !== "MARKET" && order.symbol === this.config.symbol) ? orders.filter((order) => order.type !== "MARKET" && order.symbol === this.config.symbol)
: []; : [];
const currentIds = new Set(this.openOrders.map((order) => order.orderId)); const currentIds = new Set(this.openOrders.map((order) => String(order.orderId)));
for (const id of Array.from(this.pendingCancelOrders)) { for (const id of Array.from(this.pendingCancelOrders)) {
if (!currentIds.has(id)) { if (!currentIds.has(id)) {
this.pendingCancelOrders.delete(id); this.pendingCancelOrders.delete(id);
@@ -246,6 +246,8 @@ export class MakerEngine {
return; return;
} }
const closeBidPrice = roundDownToTick(topBid, this.config.priceTick);
const closeAskPrice = roundDownToTick(topAsk, this.config.priceTick);
const bidPrice = roundDownToTick(topBid - this.config.bidOffset, this.config.priceTick); const bidPrice = roundDownToTick(topBid - this.config.bidOffset, this.config.priceTick);
const askPrice = roundDownToTick(topAsk + this.config.askOffset, this.config.priceTick); const askPrice = roundDownToTick(topAsk + this.config.askOffset, this.config.priceTick);
const position = getPosition(this.accountSnapshot, this.config.symbol); const position = getPosition(this.accountSnapshot, this.config.symbol);
@@ -261,14 +263,14 @@ export class MakerEngine {
} }
} else { } else {
const closeSide: "BUY" | "SELL" = position.positionAmt > 0 ? "SELL" : "BUY"; const closeSide: "BUY" | "SELL" = position.positionAmt > 0 ? "SELL" : "BUY";
const closePrice = closeSide === "SELL" ? askPrice : bidPrice; const closePrice = closeSide === "SELL" ? closeAskPrice : closeBidPrice;
desired.push({ side: closeSide, price: closePrice, amount: absPosition, reduceOnly: true }); desired.push({ side: closeSide, price: closePrice, amount: absPosition, reduceOnly: true });
} }
this.desiredOrders = desired; this.desiredOrders = desired;
this.updateSessionVolume(position); this.updateSessionVolume(position);
await this.syncOrders(desired); await this.syncOrders(desired);
await this.checkRisk(position, bidPrice, askPrice); await this.checkRisk(position, closeBidPrice, closeAskPrice);
this.emitUpdate(); this.emitUpdate();
} catch (error) { } catch (error) {
if (isRateLimitError(error)) { if (isRateLimitError(error)) {
@@ -291,9 +293,9 @@ export class MakerEngine {
if (Math.abs(position.positionAmt) < EPS) return; if (Math.abs(position.positionAmt) < EPS) return;
const { topBid, topAsk } = getTopPrices(this.depthSnapshot); const { topBid, topAsk } = getTopPrices(this.depthSnapshot);
if (topBid == null || topAsk == null) return; if (topBid == null || topAsk == null) return;
const bidPrice = roundDownToTick(topBid - this.config.bidOffset, this.config.priceTick); const closeBidPrice = roundDownToTick(topBid, this.config.priceTick);
const askPrice = roundDownToTick(topAsk + this.config.askOffset, this.config.priceTick); const closeAskPrice = roundDownToTick(topAsk, this.config.priceTick);
await this.checkRisk(position, bidPrice, askPrice); await this.checkRisk(position, closeBidPrice, closeAskPrice);
await this.flushOrders(); await this.flushOrders();
} }
@@ -328,12 +330,12 @@ export class MakerEngine {
private async syncOrders(targets: DesiredOrder[]): Promise<void> { private async syncOrders(targets: DesiredOrder[]): Promise<void> {
const tolerance = this.config.priceChaseThreshold; const tolerance = this.config.priceChaseThreshold;
const availableOrders = this.openOrders.filter((o) => !this.pendingCancelOrders.has(o.orderId)); const availableOrders = this.openOrders.filter((o) => !this.pendingCancelOrders.has(String(o.orderId)));
const { toCancel, toPlace } = makeOrderPlan(availableOrders, targets, tolerance); const { toCancel, toPlace } = makeOrderPlan(availableOrders, targets, tolerance);
for (const order of toCancel) { for (const order of toCancel) {
if (this.pendingCancelOrders.has(order.orderId)) continue; if (this.pendingCancelOrders.has(String(order.orderId))) continue;
this.pendingCancelOrders.add(order.orderId); this.pendingCancelOrders.add(String(order.orderId));
await safeCancelOrder( await safeCancelOrder(
this.exchange, this.exchange,
this.config.symbol, this.config.symbol,
@@ -346,12 +348,12 @@ export class MakerEngine {
}, },
() => { () => {
this.tradeLog.push("order", "撤销时发现订单已被成交/取消,忽略"); this.tradeLog.push("order", "撤销时发现订单已被成交/取消,忽略");
this.pendingCancelOrders.delete(order.orderId); this.pendingCancelOrders.delete(String(order.orderId));
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId); this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
}, },
(error) => { (error) => {
this.tradeLog.push("error", `撤销订单失败: ${String(error)}`); this.tradeLog.push("error", `撤销订单失败: ${String(error)}`);
this.pendingCancelOrders.delete(order.orderId); this.pendingCancelOrders.delete(String(order.orderId));
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId); this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
} }
); );
@@ -440,8 +442,8 @@ export class MakerEngine {
private async flushOrders(): Promise<void> { private async flushOrders(): Promise<void> {
if (!this.openOrders.length) return; if (!this.openOrders.length) return;
for (const order of this.openOrders) { for (const order of this.openOrders) {
if (this.pendingCancelOrders.has(order.orderId)) continue; if (this.pendingCancelOrders.has(String(order.orderId))) continue;
this.pendingCancelOrders.add(order.orderId); this.pendingCancelOrders.add(String(order.orderId));
await safeCancelOrder( await safeCancelOrder(
this.exchange, this.exchange,
this.config.symbol, this.config.symbol,
@@ -451,12 +453,12 @@ export class MakerEngine {
}, },
() => { () => {
this.tradeLog.push("order", "订单已不存在,撤销跳过"); this.tradeLog.push("order", "订单已不存在,撤销跳过");
this.pendingCancelOrders.delete(order.orderId); this.pendingCancelOrders.delete(String(order.orderId));
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId); this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
}, },
(error) => { (error) => {
this.tradeLog.push("error", `撤销订单失败: ${String(error)}`); this.tradeLog.push("error", `撤销订单失败: ${String(error)}`);
this.pendingCancelOrders.delete(order.orderId); this.pendingCancelOrders.delete(String(order.orderId));
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId); this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
} }
); );
+22 -14
View File
@@ -54,7 +54,7 @@ export class OffsetMakerEngine {
private readonly locks: OrderLockMap = {}; private readonly locks: OrderLockMap = {};
private readonly timers: OrderTimerMap = {}; private readonly timers: OrderTimerMap = {};
private readonly pending: OrderPendingMap = {}; private readonly pending: OrderPendingMap = {};
private readonly pendingCancelOrders = new Set<number>(); private readonly pendingCancelOrders = new Set<string>();
private readonly tradeLog: ReturnType<typeof createTradeLog>; private readonly tradeLog: ReturnType<typeof createTradeLog>;
private readonly listeners = new Map<MakerEvent, Set<MakerListener>>(); private readonly listeners = new Map<MakerEvent, Set<MakerListener>>();
@@ -145,7 +145,7 @@ export class OffsetMakerEngine {
this.openOrders = Array.isArray(orders) this.openOrders = Array.isArray(orders)
? orders.filter((order) => order.type !== "MARKET" && order.symbol === this.config.symbol) ? orders.filter((order) => order.type !== "MARKET" && order.symbol === this.config.symbol)
: []; : [];
const currentIds = new Set(this.openOrders.map((order) => order.orderId)); const currentIds = new Set(this.openOrders.map((order) => String(order.orderId)));
for (const id of Array.from(this.pendingCancelOrders)) { for (const id of Array.from(this.pendingCancelOrders)) {
if (!currentIds.has(id)) { if (!currentIds.has(id)) {
this.pendingCancelOrders.delete(id); this.pendingCancelOrders.delete(id);
@@ -259,6 +259,8 @@ export class OffsetMakerEngine {
return; return;
} }
const closeBidPrice = roundDownToTick(topBid!, this.config.priceTick);
const closeAskPrice = roundDownToTick(topAsk!, this.config.priceTick);
const bidPrice = roundDownToTick(topBid! - this.config.bidOffset, this.config.priceTick); const bidPrice = roundDownToTick(topBid! - this.config.bidOffset, this.config.priceTick);
const askPrice = roundDownToTick(topAsk! + this.config.askOffset, this.config.priceTick); const askPrice = roundDownToTick(topAsk! + this.config.askOffset, this.config.priceTick);
const absPosition = Math.abs(position.positionAmt); const absPosition = Math.abs(position.positionAmt);
@@ -275,14 +277,14 @@ export class OffsetMakerEngine {
} }
} else { } else {
const closeSide: "BUY" | "SELL" = position.positionAmt > 0 ? "SELL" : "BUY"; const closeSide: "BUY" | "SELL" = position.positionAmt > 0 ? "SELL" : "BUY";
const closePrice = closeSide === "SELL" ? askPrice : bidPrice; const closePrice = closeSide === "SELL" ? closeAskPrice : closeBidPrice;
desired.push({ side: closeSide, price: closePrice, amount: absPosition, reduceOnly: true }); desired.push({ side: closeSide, price: closePrice, amount: absPosition, reduceOnly: true });
} }
this.desiredOrders = desired; this.desiredOrders = desired;
this.updateSessionVolume(position); this.updateSessionVolume(position);
await this.syncOrders(desired); await this.syncOrders(desired);
await this.checkRisk(position, bidPrice, askPrice); await this.checkRisk(position, closeBidPrice, closeAskPrice);
this.emitUpdate(); this.emitUpdate();
} catch (error) { } catch (error) {
if (isRateLimitError(error)) { if (isRateLimitError(error)) {
@@ -306,6 +308,9 @@ export class OffsetMakerEngine {
await this.flushOrders(); await this.flushOrders();
const absPosition = Math.abs(position.positionAmt); const absPosition = Math.abs(position.positionAmt);
const side: "BUY" | "SELL" = position.positionAmt > 0 ? "SELL" : "BUY"; const side: "BUY" | "SELL" = position.positionAmt > 0 ? "SELL" : "BUY";
const { topBid, topAsk } = getTopPrices(this.depthSnapshot);
const closeBidPrice = topBid != null ? roundDownToTick(topBid, this.config.priceTick) : null;
const closeAskPrice = topAsk != null ? roundDownToTick(topAsk, this.config.priceTick) : null;
try { try {
await marketClose( await marketClose(
this.exchange, this.exchange,
@@ -319,7 +324,10 @@ export class OffsetMakerEngine {
(type, detail) => this.tradeLog.push(type, detail), (type, detail) => this.tradeLog.push(type, detail),
{ {
markPrice: position.markPrice, markPrice: position.markPrice,
expectedPrice: Number(side === "SELL" ? this.depthSnapshot?.bids?.[0]?.[0] : this.depthSnapshot?.asks?.[0]?.[0]) || null, expectedPrice:
side === "SELL"
? (closeAskPrice != null ? Number(closeAskPrice) : null)
: (closeBidPrice != null ? Number(closeBidPrice) : null),
maxPct: this.config.maxCloseSlippagePct, maxPct: this.config.maxCloseSlippagePct,
} }
); );
@@ -423,12 +431,12 @@ export class OffsetMakerEngine {
private async syncOrders(targets: DesiredOrder[]): Promise<void> { private async syncOrders(targets: DesiredOrder[]): Promise<void> {
const tolerance = this.config.priceChaseThreshold; const tolerance = this.config.priceChaseThreshold;
const availableOrders = this.openOrders.filter((o) => !this.pendingCancelOrders.has(o.orderId)); const availableOrders = this.openOrders.filter((o) => !this.pendingCancelOrders.has(String(o.orderId)));
const { toCancel, toPlace } = makeOrderPlan(availableOrders, targets, tolerance); const { toCancel, toPlace } = makeOrderPlan(availableOrders, targets, tolerance);
for (const order of toCancel) { for (const order of toCancel) {
if (this.pendingCancelOrders.has(order.orderId)) continue; if (this.pendingCancelOrders.has(String(order.orderId))) continue;
this.pendingCancelOrders.add(order.orderId); this.pendingCancelOrders.add(String(order.orderId));
await safeCancelOrder( await safeCancelOrder(
this.exchange, this.exchange,
this.config.symbol, this.config.symbol,
@@ -442,12 +450,12 @@ export class OffsetMakerEngine {
}, },
() => { () => {
this.tradeLog.push("order", "撤销时发现订单已被成交/取消,忽略"); this.tradeLog.push("order", "撤销时发现订单已被成交/取消,忽略");
this.pendingCancelOrders.delete(order.orderId); this.pendingCancelOrders.delete(String(order.orderId));
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId); this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
}, },
(error) => { (error) => {
this.tradeLog.push("error", `撤销订单失败: ${String(error)}`); this.tradeLog.push("error", `撤销订单失败: ${String(error)}`);
this.pendingCancelOrders.delete(order.orderId); this.pendingCancelOrders.delete(String(order.orderId));
// 避免同一轮内重复操作同一张已出错的本地挂单,直接从本地缓存移除,等待下一次订单推送重建 // 避免同一轮内重复操作同一张已出错的本地挂单,直接从本地缓存移除,等待下一次订单推送重建
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId); this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
} }
@@ -534,8 +542,8 @@ export class OffsetMakerEngine {
private async flushOrders(): Promise<void> { private async flushOrders(): Promise<void> {
if (!this.openOrders.length) return; if (!this.openOrders.length) return;
for (const order of this.openOrders) { for (const order of this.openOrders) {
if (this.pendingCancelOrders.has(order.orderId)) continue; if (this.pendingCancelOrders.has(String(order.orderId))) continue;
this.pendingCancelOrders.add(order.orderId); this.pendingCancelOrders.add(String(order.orderId));
await safeCancelOrder( await safeCancelOrder(
this.exchange, this.exchange,
this.config.symbol, this.config.symbol,
@@ -545,12 +553,12 @@ export class OffsetMakerEngine {
}, },
() => { () => {
this.tradeLog.push("order", "订单已不存在,撤销跳过"); this.tradeLog.push("order", "订单已不存在,撤销跳过");
this.pendingCancelOrders.delete(order.orderId); this.pendingCancelOrders.delete(String(order.orderId));
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId); this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
}, },
(error) => { (error) => {
this.tradeLog.push("error", `撤销订单失败: ${String(error)}`); this.tradeLog.push("error", `撤销订单失败: ${String(error)}`);
this.pendingCancelOrders.delete(order.orderId); this.pendingCancelOrders.delete(String(order.orderId));
// 与同步撤单路径保持一致,移除本地异常订单,等待订单流重建 // 与同步撤单路径保持一致,移除本地异常订单,等待订单流重建
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId); this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
} }
+6 -5
View File
@@ -85,7 +85,7 @@ export class TrendEngine {
private prevPositionAmt = 0; private prevPositionAmt = 0;
private initializedPosition = false; private initializedPosition = false;
private cancelAllRequested = false; private cancelAllRequested = false;
private readonly pendingCancelOrders = new Set<number>(); private readonly pendingCancelOrders = new Set<string>();
private readonly rateLimit: RateLimitController; private readonly rateLimit: RateLimitController;
// 控制入场频率:同一分钟内最多入场一次 // 控制入场频率:同一分钟内最多入场一次
@@ -166,7 +166,7 @@ export class TrendEngine {
this.openOrders = Array.isArray(orders) this.openOrders = Array.isArray(orders)
? orders.filter((order) => order.type !== "MARKET" && order.symbol === this.config.symbol) ? orders.filter((order) => order.type !== "MARKET" && order.symbol === this.config.symbol)
: []; : [];
const currentIds = new Set(this.openOrders.map((order) => order.orderId)); const currentIds = new Set(this.openOrders.map((order) => String(order.orderId)));
for (const id of Array.from(this.pendingCancelOrders)) { for (const id of Array.from(this.pendingCancelOrders)) {
if (!currentIds.has(id)) { if (!currentIds.has(id)) {
this.pendingCancelOrders.delete(id); this.pendingCancelOrders.delete(id);
@@ -588,17 +588,18 @@ export class TrendEngine {
try { try {
if (this.openOrders.length > 0) { if (this.openOrders.length > 0) {
const orderIdList = this.openOrders.map((order) => order.orderId); const orderIdList = this.openOrders.map((order) => order.orderId);
const orderIdSet = new Set(orderIdList.map(String));
try { try {
await this.exchange.cancelOrders({ symbol: this.config.symbol, orderIdList }); await this.exchange.cancelOrders({ symbol: this.config.symbol, orderIdList });
orderIdList.forEach((id) => this.pendingCancelOrders.add(id)); orderIdSet.forEach((id) => this.pendingCancelOrders.add(id));
} catch (err) { } catch (err) {
if (isUnknownOrderError(err)) { if (isUnknownOrderError(err)) {
this.tradeLog.push("order", "止损前撤单发现订单已不存在"); this.tradeLog.push("order", "止损前撤单发现订单已不存在");
// 清理本地缓存,避免重复对同一订单执行撤单 // 清理本地缓存,避免重复对同一订单执行撤单
for (const id of orderIdList) { for (const id of orderIdSet) {
this.pendingCancelOrders.delete(id); this.pendingCancelOrders.delete(id);
} }
this.openOrders = this.openOrders.filter((o) => !orderIdList.includes(o.orderId)); this.openOrders = this.openOrders.filter((o) => !orderIdSet.has(String(o.orderId)));
} else { } else {
throw err; throw err;
} }
+8 -3
View File
@@ -707,10 +707,12 @@ function updateAccountSnapshot(snapshot: AsterAccountSnapshot | null, event: { e
} }
function mergeOrderSnapshot(map: Map<number, AsterOrder>, order: AsterOrder): void { function mergeOrderSnapshot(map: Map<number, AsterOrder>, order: AsterOrder): void {
const numericId = typeof order.orderId === "number" ? order.orderId : Number(order.orderId);
if (!Number.isFinite(numericId)) return;
if (FINAL_ORDER_STATUSES.has(order.status)) { if (FINAL_ORDER_STATUSES.has(order.status)) {
map.delete(order.orderId); map.delete(numericId);
} else { } else {
map.set(order.orderId, order); map.set(numericId, { ...order, orderId: numericId });
} }
} }
@@ -1003,7 +1005,10 @@ export class AsterGateway {
await this.rest.cancelAllOrders(params); await this.rest.cancelAllOrders(params);
for (const order of Array.from(this.openOrders.values())) { for (const order of Array.from(this.openOrders.values())) {
if (order.symbol === params.symbol) { if (order.symbol === params.symbol) {
this.openOrders.delete(order.orderId); const numericId = typeof order.orderId === "number" ? order.orderId : Number(order.orderId);
if (Number.isFinite(numericId)) {
this.openOrders.delete(numericId);
}
} }
} }
this.ordersEvent.emit(Array.from(this.openOrders.values())); this.ordersEvent.emit(Array.from(this.openOrders.values()));
+140 -38
View File
@@ -1,7 +1,12 @@
import { setInterval, clearInterval } from "timers"; import { setInterval, clearInterval } from "timers";
import { randomInt } from "crypto"; import { randomInt } from "crypto";
import axios from "axios"; import axios, { AxiosHeaders } from "axios";
import { TDG, MDG } from "@grvt/client"; import { TDG, MDG } from "@grvt/client";
import { ECandlestickInterval } from "@grvt/client/interfaces/codegen/enums/candlestick-interval";
import { ECandlestickType } from "@grvt/client/interfaces/codegen/enums/candlestick-type";
import { ETimeInForce } from "@grvt/client/interfaces/codegen/enums/time-in-force";
import { ETriggerType } from "@grvt/client/interfaces/codegen/enums/trigger-type";
import { ETriggerBy } from "@grvt/client/interfaces/codegen/enums/trigger-by";
import { keccak256 } from "ethereum-cryptography/keccak"; import { keccak256 } from "ethereum-cryptography/keccak";
import { secp256k1 } from "ethereum-cryptography/secp256k1"; import { secp256k1 } from "ethereum-cryptography/secp256k1";
import { bytesToHex, hexToBytes, utf8ToBytes, concatBytes } from "ethereum-cryptography/utils"; import { bytesToHex, hexToBytes, utf8ToBytes, concatBytes } from "ethereum-cryptography/utils";
@@ -14,6 +19,7 @@ import type {
IApiTickerResponse, IApiTickerResponse,
IApiCandlestickResponse, IApiCandlestickResponse,
IApiCreateOrderResponse, IApiCreateOrderResponse,
IOrder,
} from "@grvt/client/interfaces"; } from "@grvt/client/interfaces";
import type { import type {
AsterAccountSnapshot, AsterAccountSnapshot,
@@ -24,10 +30,12 @@ import type {
AsterTicker, AsterTicker,
CreateOrderParams, CreateOrderParams,
OrderSide, OrderSide,
GrvtOrder,
GrvtSignedOrder, GrvtSignedOrder,
GrvtSignature, GrvtSignature,
GrvtUnsignedOrder, GrvtUnsignedOrder,
GrvtTimeInForce,
GrvtOrderMetadataInput,
GrvtTriggerMetadata,
} from "../types"; } from "../types";
const DEFAULT_ACCOUNT_POLL_INTERVAL_MS = 5000; const DEFAULT_ACCOUNT_POLL_INTERVAL_MS = 5000;
@@ -65,7 +73,7 @@ const ENVIRONMENT_ALIASES: Record<string, keyof typeof ENVIRONMENT_HOSTS> = {
}; };
const DEFAULT_MARK_PRICE_TRIGGER = "MARK"; const DEFAULT_MARK_PRICE_TRIGGER = "MARK";
const DEFAULT_TIME_IN_FORCE = "GOOD_TILL_TIME"; const DEFAULT_TIME_IN_FORCE: GrvtTimeInForce = "GOOD_TILL_TIME";
const TRAILING_NOT_SUPPORTED_ERROR = const TRAILING_NOT_SUPPORTED_ERROR =
"GRVT exchange adapter does not yet support trailing stop orders"; "GRVT exchange adapter does not yet support trailing stop orders";
@@ -190,7 +198,13 @@ export class GrvtGateway {
private readonly instrument: string; private readonly instrument: string;
private readonly symbol: string; private readonly symbol: string;
private readonly subAccountId: string; private readonly subAccountId: string;
private readonly pollIntervals: Required<GrvtGatewayOptions["pollIntervals"]>; private readonly pollIntervals: {
account: number;
orders: number;
depth: number;
ticker: number;
klines: number;
};
private readonly hosts: HostsConfig; private readonly hosts: HostsConfig;
private readonly chainId: number; private readonly chainId: number;
private headers: Record<string, string> = {}; private headers: Record<string, string> = {};
@@ -220,7 +234,7 @@ export class GrvtGateway {
private klineTimer: ReturnType<typeof setInterval> | null = null; private klineTimer: ReturnType<typeof setInterval> | null = null;
private initialized = false; private initialized = false;
private klineInterval = "CI_1_M"; private klineInterval: ECandlestickInterval = ECandlestickInterval.CI_1_M;
constructor(options: GrvtGatewayOptions) { constructor(options: GrvtGatewayOptions) {
const envKey = normalizeEnvironment(options.env); const envKey = normalizeEnvironment(options.env);
@@ -254,13 +268,25 @@ export class GrvtGateway {
this.tdg.axios.interceptors.request.use(async (config) => { this.tdg.axios.interceptors.request.use(async (config) => {
await this.ensureSession(); await this.ensureSession();
config.headers = { ...(config.headers ?? {}), ...this.headers }; const existing =
config.headers instanceof AxiosHeaders ? config.headers.toJSON() : config.headers ?? {};
const merged = AxiosHeaders.from(existing);
for (const [key, value] of Object.entries(this.headers)) {
merged.set(key, value);
}
config.headers = merged;
return config; return config;
}); });
this.mdg.axios.interceptors.request.use(async (config) => { this.mdg.axios.interceptors.request.use(async (config) => {
await this.ensureSession(); await this.ensureSession();
config.headers = { ...(config.headers ?? {}), ...this.headers }; const existing =
config.headers instanceof AxiosHeaders ? config.headers.toJSON() : config.headers ?? {};
const merged = AxiosHeaders.from(existing);
for (const [key, value] of Object.entries(this.headers)) {
merged.set(key, value);
}
config.headers = merged;
return config; return config;
}); });
} }
@@ -388,7 +414,7 @@ export class GrvtGateway {
const signedOrder: GrvtSignedOrder = { ...unsignedOrder, signature }; const signedOrder: GrvtSignedOrder = { ...unsignedOrder, signature };
try { try {
const response = await this.tdg.createOrder({ order: signedOrder }); const response = await this.tdg.createOrder({ order: toApiOrderPayload(signedOrder) });
const order = mapCreateOrderResponse(response, this.symbol); const order = mapCreateOrderResponse(response, this.symbol);
this.mergeOrder(order); this.mergeOrder(order);
return order; return order;
@@ -528,7 +554,7 @@ export class GrvtGateway {
const response = await this.mdg.candlestick({ const response = await this.mdg.candlestick({
instrument: this.instrument, instrument: this.instrument,
interval: this.klineInterval, interval: this.klineInterval,
type: "TRADE", type: ECandlestickType.TRADE,
limit: 500, limit: 500,
}); });
const klines = mapKlines(response, this.symbol); const klines = mapKlines(response, this.symbol);
@@ -725,14 +751,17 @@ export class GrvtGateway {
} }
const privateKeyBytes = hexToBytes(padPrivateKey(this.apiSecret)); const privateKeyBytes = hexToBytes(padPrivateKey(this.apiSecret));
const leg = context.order.legs[0]; const leg = context.order.legs[0];
if (!leg) {
throw new Error("GRVT order leg missing for signing");
}
const contractSize = scaleDecimal(leg.size, context.instrument.baseDecimals); const contractSize = scaleDecimal(leg.size, context.instrument.baseDecimals);
const limitPriceSource = const limitPriceSource =
leg.limit_price ?? (context.isMarket ? "0" : context.price?.toString() ?? "0"); leg.limit_price ?? (context.isMarket ? "0" : context.price?.toString() ?? "0");
const limitPrice = scaleDecimal(limitPriceSource, 9); const limitPrice = scaleDecimal(limitPriceSource, 9);
const types: EIP712Types = { const types: EIP712Types = {
EIP712Domain: EIP712_DOMAIN_FIELDS, EIP712Domain: EIP712_DOMAIN_FIELDS.map((field) => ({ ...field })),
Order: EIP712_ORDER_TYPES.Order, Order: EIP712_ORDER_TYPES.Order.map((field) => ({ ...field })),
OrderLeg: EIP712_ORDER_TYPES.OrderLeg, OrderLeg: EIP712_ORDER_TYPES.OrderLeg.map((field) => ({ ...field })),
}; };
const message = { const message = {
subAccountID: BigInt(context.subAccountId), subAccountID: BigInt(context.subAccountId),
@@ -786,7 +815,10 @@ function normalizeEnvironment(env: GrvtEnvironment | undefined): BaseEnvironment
return normalized as BaseEnvironment; return normalized as BaseEnvironment;
} }
if (normalized in ENVIRONMENT_ALIASES) { if (normalized in ENVIRONMENT_ALIASES) {
return ENVIRONMENT_ALIASES[normalized]; const alias = ENVIRONMENT_ALIASES[normalized as keyof typeof ENVIRONMENT_ALIASES];
if (alias) {
return alias;
}
} }
return "testnet"; return "testnet";
} }
@@ -803,7 +835,8 @@ function resolveHosts(env: BaseEnvironment, override?: GrvtHostsOverride): Hosts
function normalizeCookieValue(value: string): string { function normalizeCookieValue(value: string): string {
const parsed = parseSetCookieHeader(value); const parsed = parseSetCookieHeader(value);
if (parsed?.cookie) return parsed.cookie; if (parsed?.cookie) return parsed.cookie;
return value.split(";")[0].trim(); const [cookie] = value.split(";");
return (cookie ?? value).trim();
} }
function defaultLogger(context: string, error: unknown): void { function defaultLogger(context: string, error: unknown): void {
@@ -875,7 +908,8 @@ function scaleDecimal(value: string | number | undefined, decimals: number): big
const strValue = typeof value === "number" ? value.toString() : value; const strValue = typeof value === "number" ? value.toString() : value;
if (!strValue.includes("e") && !strValue.includes("E")) { if (!strValue.includes("e") && !strValue.includes("E")) {
const [intPartRaw, fracRaw = ""] = strValue.split("."); const [intPartRaw, fracRaw = ""] = strValue.split(".");
const intPart = intPartRaw === "" ? "0" : intPartRaw.replace(/^\+/, ""); const sanitizedIntPart = (intPartRaw ?? "").replace(/^\+/, "");
const intPart = sanitizedIntPart === "" ? "0" : sanitizedIntPart;
const fraction = fracRaw.padEnd(decimals, "0").slice(0, decimals); const fraction = fracRaw.padEnd(decimals, "0").slice(0, decimals);
const combined = `${intPart}${fraction}`; const combined = `${intPart}${fraction}`;
return BigInt(combined || "0"); return BigInt(combined || "0");
@@ -947,7 +981,7 @@ function mapOpenOrders(response: IApiOpenOrdersResponse, symbol: string): AsterO
return (response.result ?? []).map((order) => mapOrder(order, symbol)); return (response.result ?? []).map((order) => mapOrder(order, symbol));
} }
function mapOrder(order: GrvtOrder, symbol: string): AsterOrder { function mapOrder(order: IOrder, symbol: string): AsterOrder {
const leg = order.legs?.[0]; const leg = order.legs?.[0];
const state = order.state; const state = order.state;
const metadata = order.metadata; const metadata = order.metadata;
@@ -1030,7 +1064,7 @@ function mapKlines(response: IApiCandlestickResponse, symbol: string): AsterKlin
} }
function mapCreateOrderResponse(response: IApiCreateOrderResponse, symbol: string): AsterOrder { function mapCreateOrderResponse(response: IApiCreateOrderResponse, symbol: string): AsterOrder {
const order = response.result ?? (response as unknown as { order?: GrvtOrder }).order; const order = response.result ?? (response as unknown as { order?: IOrder }).order;
if (!order) { if (!order) {
return { return {
orderId: cryptoRandomId(), orderId: cryptoRandomId(),
@@ -1049,7 +1083,75 @@ function mapCreateOrderResponse(response: IApiCreateOrderResponse, symbol: strin
closePosition: false, closePosition: false,
}; };
} }
return mapOrder(order as unknown as GrvtOrder, symbol); return mapOrder(order as IOrder, symbol);
}
function toApiOrderPayload(order: GrvtSignedOrder): IOrder {
const metadata = order.metadata ? toApiOrderMetadata(order.metadata) : undefined;
return {
...order,
time_in_force: toApiTimeInForce(order.time_in_force),
metadata,
};
}
function toApiOrderMetadata(metadata: GrvtOrderMetadataInput): IOrder["metadata"] {
const trigger = metadata.trigger;
return {
client_order_id: metadata.client_order_id,
trigger: trigger
? {
trigger_type: toApiTriggerType(trigger.trigger_type),
tpsl: {
trigger_by: toApiTriggerBy(trigger.tpsl.trigger_by),
trigger_price: trigger.tpsl.trigger_price,
close_position: trigger.tpsl.close_position,
},
}
: undefined,
};
}
function toApiTimeInForce(timeInForce: GrvtTimeInForce): ETimeInForce {
switch (timeInForce) {
case "ALL_OR_NONE":
return ETimeInForce.ALL_OR_NONE;
case "IMMEDIATE_OR_CANCEL":
return ETimeInForce.IMMEDIATE_OR_CANCEL;
case "FILL_OR_KILL":
return ETimeInForce.FILL_OR_KILL;
case "GOOD_TILL_TIME":
default:
return ETimeInForce.GOOD_TILL_TIME;
}
}
function toApiTriggerType(triggerType: GrvtTriggerMetadata["trigger_type"] | undefined): ETriggerType {
switch (triggerType) {
case "TAKE_PROFIT":
return ETriggerType.TAKE_PROFIT;
case "STOP_LOSS":
return ETriggerType.STOP_LOSS;
case "UNSPECIFIED":
default:
return ETriggerType.UNSPECIFIED;
}
}
function toApiTriggerBy(triggerBy: GrvtTriggerMetadata["tpsl"]["trigger_by"] | undefined): ETriggerBy {
switch (triggerBy) {
case "INDEX":
return ETriggerBy.INDEX;
case "LAST":
return ETriggerBy.LAST;
case "MID":
return ETriggerBy.MID;
case "MARK":
return ETriggerBy.MARK;
case "UNSPECIFIED":
default:
return ETriggerBy.UNSPECIFIED;
}
} }
function buildUnsignedOrder(params: { function buildUnsignedOrder(params: {
@@ -1080,10 +1182,10 @@ function buildUnsignedOrder(params: {
orderParams.timeInForce && orderParams.timeInForce.toUpperCase() === "GTX" orderParams.timeInForce && orderParams.timeInForce.toUpperCase() === "GTX"
); );
const reduceOnly = normalizeBoolean(orderParams.reduceOnly); const reduceOnly = normalizeBoolean(orderParams.reduceOnly);
const priceValue = isMarketOrder ? undefined : orderParams.price; const priceValue = isMarketOrder ? undefined : orderParams.price;
if (!isMarketOrder && (priceValue == null || !Number.isFinite(Number(priceValue)))) { if (!isMarketOrder && (priceValue == null || !Number.isFinite(Number(priceValue)))) {
throw new Error("GRVT limit orders require a valid price"); throw new Error("GRVT limit orders require a valid price");
} }
const trigger = buildTriggerMetadata(orderParams); const trigger = buildTriggerMetadata(orderParams);
const metadata = { const metadata = {
@@ -1141,41 +1243,41 @@ function buildTriggerMetadata(params: CreateOrderParams): GrvtUnsignedOrder["met
return undefined; return undefined;
} }
function mapIntervalToGrvt(interval: string): string { function mapIntervalToGrvt(interval: string): ECandlestickInterval {
const normalized = interval.trim().toLowerCase(); const normalized = interval.trim().toLowerCase();
switch (normalized) { switch (normalized) {
case "1m": case "1m":
return "CI_1_M"; return ECandlestickInterval.CI_1_M;
case "3m": case "3m":
return "CI_3_M"; return ECandlestickInterval.CI_3_M;
case "5m": case "5m":
return "CI_5_M"; return ECandlestickInterval.CI_5_M;
case "15m": case "15m":
return "CI_15_M"; return ECandlestickInterval.CI_15_M;
case "30m": case "30m":
return "CI_30_M"; return ECandlestickInterval.CI_30_M;
case "1h": case "1h":
return "CI_1_H"; return ECandlestickInterval.CI_1_H;
case "2h": case "2h":
return "CI_2_H"; return ECandlestickInterval.CI_2_H;
case "4h": case "4h":
return "CI_4_H"; return ECandlestickInterval.CI_4_H;
case "6h": case "6h":
return "CI_6_H"; return ECandlestickInterval.CI_6_H;
case "8h": case "8h":
return "CI_8_H"; return ECandlestickInterval.CI_8_H;
case "12h": case "12h":
return "CI_12_H"; return ECandlestickInterval.CI_12_H;
case "1d": case "1d":
return "CI_1_D"; return ECandlestickInterval.CI_1_D;
case "1w": case "1w":
return "CI_1_W"; return ECandlestickInterval.CI_1_W;
default: default:
return "CI_1_M"; return ECandlestickInterval.CI_1_M;
} }
} }
function mapTimeInForceToGrvt(timeInForce: string | undefined): string { function mapTimeInForceToGrvt(timeInForce: string | undefined): GrvtTimeInForce {
switch ((timeInForce ?? "GTC").toUpperCase()) { switch ((timeInForce ?? "GTC").toUpperCase()) {
case "IOC": case "IOC":
return "IMMEDIATE_OR_CANCEL"; return "IMMEDIATE_OR_CANCEL";
+9 -2
View File
@@ -51,10 +51,17 @@ export interface GrvtOrderLeg {
is_buying_asset?: boolean; is_buying_asset?: boolean;
} }
export type GrvtTimeInForce =
| "GOOD_TILL_TIME"
| "ALL_OR_NONE"
| "IMMEDIATE_OR_CANCEL"
| "FILL_OR_KILL";
export interface GrvtOrderMetadata { export interface GrvtOrderMetadata {
client_order_id?: string; client_order_id?: string;
create_time?: string; create_time?: string;
broker?: string | null; broker?: string | null;
trigger?: GrvtTriggerMetadata;
} }
export interface GrvtOrderState { export interface GrvtOrderState {
@@ -71,7 +78,7 @@ export interface GrvtOrder {
client_order_id?: string; client_order_id?: string;
sub_account_id?: string; sub_account_id?: string;
is_market?: boolean; is_market?: boolean;
time_in_force?: string; time_in_force?: GrvtTimeInForce;
post_only?: boolean; post_only?: boolean;
reduce_only?: boolean; reduce_only?: boolean;
legs?: GrvtOrderLeg[]; legs?: GrvtOrderLeg[];
@@ -229,7 +236,7 @@ export interface GrvtOrderMetadataInput {
export interface GrvtUnsignedOrder { export interface GrvtUnsignedOrder {
sub_account_id: string; sub_account_id: string;
is_market: boolean; is_market: boolean;
time_in_force: string; time_in_force: GrvtTimeInForce;
post_only: boolean; post_only: boolean;
reduce_only: boolean; reduce_only: boolean;
legs: GrvtUnsignedOrderLeg[]; legs: GrvtUnsignedOrderLeg[];
+2 -1
View File
@@ -14,7 +14,8 @@ export function roundQtyDownToStep(value: number, step: number): number {
export function decimalsOf(step: number): number { export function decimalsOf(step: number): number {
const s = step.toString(); const s = step.toString();
if (!s.includes(".")) return 0; if (!s.includes(".")) return 0;
return s.split(".")[1].length; const fraction = s.split(".")[1];
return fraction ? fraction.length : 0;
} }
export function isNearlyZero(value: number, epsilon = 1e-5): boolean { export function isNearlyZero(value: number, epsilon = 1e-5): boolean {
+11 -11
View File
@@ -9,18 +9,18 @@ export function shouldStopLoss(
const absPosition = Math.abs(position.positionAmt); const absPosition = Math.abs(position.positionAmt);
if (absPosition < 1e-5) return false; if (absPosition < 1e-5) return false;
if (!Number.isFinite(position.entryPrice) || Math.abs(position.entryPrice) < 1e-8) {
return false;
}
const closePrice = position.positionAmt > 0 ? bestBid : bestAsk;
if (!Number.isFinite(closePrice)) return false;
const pnl = position.positionAmt > 0 const pnl = position.positionAmt > 0
? (bestBid - position.entryPrice) * absPosition ? (closePrice - position.entryPrice) * absPosition
: (position.entryPrice - bestAsk) * absPosition; : (position.entryPrice - closePrice) * absPosition;
const unrealized = Number.isFinite(position.unrealizedProfit) if (!Number.isFinite(pnl)) return false;
? (position.unrealizedProfit as number)
: null;
const derivedLoss = pnl < -lossLimit; return pnl < -lossLimit;
const snapshotLoss = Boolean(unrealized != null && unrealized < -lossLimit && pnl <= 0);
return derivedLoss || snapshotLoss;
} }