From 0bb22d891f23cc6ab6fa4c5b27b8387dd85a448c Mon Sep 17 00:00:00 2001 From: discountry Date: Sat, 27 Sep 2025 17:05:51 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=81=9A=E5=B8=82=E5=BC=95?= =?UTF-8?q?=E6=93=8E=E3=80=81=E5=81=8F=E7=A7=BB=E5=81=9A=E5=B8=82=E5=BC=95?= =?UTF-8?q?=E6=93=8E=E5=92=8C=E8=B6=8B=E5=8A=BF=E5=BC=95=E6=93=8E=E7=9A=84?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E7=AE=A1=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E5=B0=86=E5=BE=85=E5=8F=96=E6=B6=88=E8=AE=A2=E5=8D=95=E7=9A=84?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E4=BB=8E=E6=95=B0=E5=AD=97=E6=9B=B4=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E5=AD=97=E7=AC=A6=E4=B8=B2=EF=BC=8C=E7=A1=AE=E4=BF=9D?= =?UTF-8?q?=E5=9C=A8=E5=A4=84=E7=90=86=E8=AE=A2=E5=8D=95=E6=97=B6=E7=9A=84?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E4=B8=80=E8=87=B4=E6=80=A7=E3=80=82=E5=90=8C?= =?UTF-8?q?=E6=97=B6=EF=BC=8C=E5=A2=9E=E5=BC=BA=E9=A3=8E=E9=99=A9=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E9=80=BB=E8=BE=91=EF=BC=8C=E7=A1=AE=E4=BF=9D=E5=9C=A8?= =?UTF-8?q?=E5=B9=B3=E4=BB=93=E6=97=B6=E4=BD=BF=E7=94=A8=E6=AD=A3=E7=A1=AE?= =?UTF-8?q?=E7=9A=84=E4=BB=B7=E6=A0=BC=EF=BC=8C=E6=8F=90=E5=8D=87=E4=BA=A4?= =?UTF-8?q?=E6=98=93=E7=AD=96=E7=95=A5=E7=9A=84=E7=A8=B3=E5=AE=9A=E6=80=A7?= =?UTF-8?q?=E5=92=8C=E5=87=86=E7=A1=AE=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/lib/orders.ts | 3 +- src/core/maker-engine.ts | 34 +++--- src/core/offset-maker-engine.ts | 36 ++++--- src/core/trend-engine.ts | 11 +- src/exchanges/aster/client.ts | 11 +- src/exchanges/grvt/gateway.ts | 178 +++++++++++++++++++++++++------- src/exchanges/types.ts | 11 +- src/utils/math.ts | 3 +- src/utils/risk.ts | 22 ++-- 9 files changed, 217 insertions(+), 92 deletions(-) diff --git a/src/core/lib/orders.ts b/src/core/lib/orders.ts index 5b3c7db..a09af32 100644 --- a/src/core/lib/orders.ts +++ b/src/core/lib/orders.ts @@ -6,7 +6,7 @@ export async function safeCancelOrder( exchange: ExchangeAdapter, symbol: string, order: AsterOrder, - onResolved: (orderId: number) => void, + onResolved: (orderId: number | string) => void, onUnknown: () => void, onError: (err: unknown) => void ): Promise { @@ -19,4 +19,3 @@ export async function safeCancelOrder( } } - diff --git a/src/core/maker-engine.ts b/src/core/maker-engine.ts index 215a5a0..4ac14fe 100644 --- a/src/core/maker-engine.ts +++ b/src/core/maker-engine.ts @@ -60,7 +60,7 @@ export class MakerEngine { private readonly locks: OrderLockMap = {}; private readonly timers: OrderTimerMap = {}; private readonly pending: OrderPendingMap = {}; - private readonly pendingCancelOrders = new Set(); + private readonly pendingCancelOrders = new Set(); private readonly tradeLog: ReturnType; private readonly listeners = new Map>(); @@ -145,7 +145,7 @@ export class MakerEngine { this.openOrders = Array.isArray(orders) ? 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)) { if (!currentIds.has(id)) { this.pendingCancelOrders.delete(id); @@ -246,6 +246,8 @@ export class MakerEngine { 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 askPrice = roundDownToTick(topAsk + this.config.askOffset, this.config.priceTick); const position = getPosition(this.accountSnapshot, this.config.symbol); @@ -261,14 +263,14 @@ export class MakerEngine { } } else { 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 }); } this.desiredOrders = desired; this.updateSessionVolume(position); await this.syncOrders(desired); - await this.checkRisk(position, bidPrice, askPrice); + await this.checkRisk(position, closeBidPrice, closeAskPrice); this.emitUpdate(); } catch (error) { if (isRateLimitError(error)) { @@ -291,9 +293,9 @@ export class MakerEngine { if (Math.abs(position.positionAmt) < EPS) return; const { topBid, topAsk } = getTopPrices(this.depthSnapshot); if (topBid == null || topAsk == null) return; - const bidPrice = roundDownToTick(topBid - this.config.bidOffset, this.config.priceTick); - const askPrice = roundDownToTick(topAsk + this.config.askOffset, this.config.priceTick); - await this.checkRisk(position, bidPrice, askPrice); + const closeBidPrice = roundDownToTick(topBid, this.config.priceTick); + const closeAskPrice = roundDownToTick(topAsk, this.config.priceTick); + await this.checkRisk(position, closeBidPrice, closeAskPrice); await this.flushOrders(); } @@ -328,12 +330,12 @@ export class MakerEngine { private async syncOrders(targets: DesiredOrder[]): Promise { 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); for (const order of toCancel) { - if (this.pendingCancelOrders.has(order.orderId)) continue; - this.pendingCancelOrders.add(order.orderId); + if (this.pendingCancelOrders.has(String(order.orderId))) continue; + this.pendingCancelOrders.add(String(order.orderId)); await safeCancelOrder( this.exchange, this.config.symbol, @@ -346,12 +348,12 @@ export class MakerEngine { }, () => { 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); }, (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); } ); @@ -440,8 +442,8 @@ export class MakerEngine { private async flushOrders(): Promise { if (!this.openOrders.length) return; for (const order of this.openOrders) { - if (this.pendingCancelOrders.has(order.orderId)) continue; - this.pendingCancelOrders.add(order.orderId); + if (this.pendingCancelOrders.has(String(order.orderId))) continue; + this.pendingCancelOrders.add(String(order.orderId)); await safeCancelOrder( this.exchange, this.config.symbol, @@ -451,12 +453,12 @@ export class MakerEngine { }, () => { 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); }, (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); } ); diff --git a/src/core/offset-maker-engine.ts b/src/core/offset-maker-engine.ts index d95e1db..0b7d0b1 100644 --- a/src/core/offset-maker-engine.ts +++ b/src/core/offset-maker-engine.ts @@ -54,7 +54,7 @@ export class OffsetMakerEngine { private readonly locks: OrderLockMap = {}; private readonly timers: OrderTimerMap = {}; private readonly pending: OrderPendingMap = {}; - private readonly pendingCancelOrders = new Set(); + private readonly pendingCancelOrders = new Set(); private readonly tradeLog: ReturnType; private readonly listeners = new Map>(); @@ -145,7 +145,7 @@ export class OffsetMakerEngine { this.openOrders = Array.isArray(orders) ? 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)) { if (!currentIds.has(id)) { this.pendingCancelOrders.delete(id); @@ -259,6 +259,8 @@ export class OffsetMakerEngine { 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 askPrice = roundDownToTick(topAsk! + this.config.askOffset, this.config.priceTick); const absPosition = Math.abs(position.positionAmt); @@ -275,14 +277,14 @@ export class OffsetMakerEngine { } } else { 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 }); } this.desiredOrders = desired; this.updateSessionVolume(position); await this.syncOrders(desired); - await this.checkRisk(position, bidPrice, askPrice); + await this.checkRisk(position, closeBidPrice, closeAskPrice); this.emitUpdate(); } catch (error) { if (isRateLimitError(error)) { @@ -306,6 +308,9 @@ export class OffsetMakerEngine { await this.flushOrders(); const absPosition = Math.abs(position.positionAmt); 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 { await marketClose( this.exchange, @@ -319,7 +324,10 @@ export class OffsetMakerEngine { (type, detail) => this.tradeLog.push(type, detail), { 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, } ); @@ -423,12 +431,12 @@ export class OffsetMakerEngine { private async syncOrders(targets: DesiredOrder[]): Promise { 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); for (const order of toCancel) { - if (this.pendingCancelOrders.has(order.orderId)) continue; - this.pendingCancelOrders.add(order.orderId); + if (this.pendingCancelOrders.has(String(order.orderId))) continue; + this.pendingCancelOrders.add(String(order.orderId)); await safeCancelOrder( this.exchange, this.config.symbol, @@ -442,12 +450,12 @@ export class OffsetMakerEngine { }, () => { 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); }, (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); } @@ -534,8 +542,8 @@ export class OffsetMakerEngine { private async flushOrders(): Promise { if (!this.openOrders.length) return; for (const order of this.openOrders) { - if (this.pendingCancelOrders.has(order.orderId)) continue; - this.pendingCancelOrders.add(order.orderId); + if (this.pendingCancelOrders.has(String(order.orderId))) continue; + this.pendingCancelOrders.add(String(order.orderId)); await safeCancelOrder( this.exchange, this.config.symbol, @@ -545,12 +553,12 @@ export class OffsetMakerEngine { }, () => { 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); }, (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); } diff --git a/src/core/trend-engine.ts b/src/core/trend-engine.ts index 69580e8..fa6e8a9 100644 --- a/src/core/trend-engine.ts +++ b/src/core/trend-engine.ts @@ -85,7 +85,7 @@ export class TrendEngine { private prevPositionAmt = 0; private initializedPosition = false; private cancelAllRequested = false; - private readonly pendingCancelOrders = new Set(); + private readonly pendingCancelOrders = new Set(); private readonly rateLimit: RateLimitController; // 控制入场频率:同一分钟内最多入场一次 @@ -166,7 +166,7 @@ export class TrendEngine { this.openOrders = Array.isArray(orders) ? 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)) { if (!currentIds.has(id)) { this.pendingCancelOrders.delete(id); @@ -588,17 +588,18 @@ export class TrendEngine { try { if (this.openOrders.length > 0) { const orderIdList = this.openOrders.map((order) => order.orderId); + const orderIdSet = new Set(orderIdList.map(String)); try { 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) { if (isUnknownOrderError(err)) { this.tradeLog.push("order", "止损前撤单发现订单已不存在"); // 清理本地缓存,避免重复对同一订单执行撤单 - for (const id of orderIdList) { + for (const id of orderIdSet) { 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 { throw err; } diff --git a/src/exchanges/aster/client.ts b/src/exchanges/aster/client.ts index 6a605ac..960ea64 100644 --- a/src/exchanges/aster/client.ts +++ b/src/exchanges/aster/client.ts @@ -707,10 +707,12 @@ function updateAccountSnapshot(snapshot: AsterAccountSnapshot | null, event: { e } function mergeOrderSnapshot(map: Map, 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)) { - map.delete(order.orderId); + map.delete(numericId); } else { - map.set(order.orderId, order); + map.set(numericId, { ...order, orderId: numericId }); } } @@ -1003,7 +1005,10 @@ export class AsterGateway { await this.rest.cancelAllOrders(params); for (const order of Array.from(this.openOrders.values())) { 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())); diff --git a/src/exchanges/grvt/gateway.ts b/src/exchanges/grvt/gateway.ts index 31c92f0..7b94df0 100644 --- a/src/exchanges/grvt/gateway.ts +++ b/src/exchanges/grvt/gateway.ts @@ -1,7 +1,12 @@ import { setInterval, clearInterval } from "timers"; import { randomInt } from "crypto"; -import axios from "axios"; +import axios, { AxiosHeaders } from "axios"; 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 { secp256k1 } from "ethereum-cryptography/secp256k1"; import { bytesToHex, hexToBytes, utf8ToBytes, concatBytes } from "ethereum-cryptography/utils"; @@ -14,6 +19,7 @@ import type { IApiTickerResponse, IApiCandlestickResponse, IApiCreateOrderResponse, + IOrder, } from "@grvt/client/interfaces"; import type { AsterAccountSnapshot, @@ -24,10 +30,12 @@ import type { AsterTicker, CreateOrderParams, OrderSide, - GrvtOrder, GrvtSignedOrder, GrvtSignature, GrvtUnsignedOrder, + GrvtTimeInForce, + GrvtOrderMetadataInput, + GrvtTriggerMetadata, } from "../types"; const DEFAULT_ACCOUNT_POLL_INTERVAL_MS = 5000; @@ -65,7 +73,7 @@ const ENVIRONMENT_ALIASES: Record = { }; 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 = "GRVT exchange adapter does not yet support trailing stop orders"; @@ -190,7 +198,13 @@ export class GrvtGateway { private readonly instrument: string; private readonly symbol: string; private readonly subAccountId: string; - private readonly pollIntervals: Required; + private readonly pollIntervals: { + account: number; + orders: number; + depth: number; + ticker: number; + klines: number; + }; private readonly hosts: HostsConfig; private readonly chainId: number; private headers: Record = {}; @@ -220,7 +234,7 @@ export class GrvtGateway { private klineTimer: ReturnType | null = null; private initialized = false; - private klineInterval = "CI_1_M"; + private klineInterval: ECandlestickInterval = ECandlestickInterval.CI_1_M; constructor(options: GrvtGatewayOptions) { const envKey = normalizeEnvironment(options.env); @@ -254,13 +268,25 @@ export class GrvtGateway { this.tdg.axios.interceptors.request.use(async (config) => { 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; }); this.mdg.axios.interceptors.request.use(async (config) => { 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; }); } @@ -388,7 +414,7 @@ export class GrvtGateway { const signedOrder: GrvtSignedOrder = { ...unsignedOrder, signature }; try { - const response = await this.tdg.createOrder({ order: signedOrder }); + const response = await this.tdg.createOrder({ order: toApiOrderPayload(signedOrder) }); const order = mapCreateOrderResponse(response, this.symbol); this.mergeOrder(order); return order; @@ -528,7 +554,7 @@ export class GrvtGateway { const response = await this.mdg.candlestick({ instrument: this.instrument, interval: this.klineInterval, - type: "TRADE", + type: ECandlestickType.TRADE, limit: 500, }); const klines = mapKlines(response, this.symbol); @@ -725,14 +751,17 @@ export class GrvtGateway { } const privateKeyBytes = hexToBytes(padPrivateKey(this.apiSecret)); 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 limitPriceSource = leg.limit_price ?? (context.isMarket ? "0" : context.price?.toString() ?? "0"); const limitPrice = scaleDecimal(limitPriceSource, 9); const types: EIP712Types = { - EIP712Domain: EIP712_DOMAIN_FIELDS, - Order: EIP712_ORDER_TYPES.Order, - OrderLeg: EIP712_ORDER_TYPES.OrderLeg, + EIP712Domain: EIP712_DOMAIN_FIELDS.map((field) => ({ ...field })), + Order: EIP712_ORDER_TYPES.Order.map((field) => ({ ...field })), + OrderLeg: EIP712_ORDER_TYPES.OrderLeg.map((field) => ({ ...field })), }; const message = { subAccountID: BigInt(context.subAccountId), @@ -786,7 +815,10 @@ function normalizeEnvironment(env: GrvtEnvironment | undefined): BaseEnvironment return normalized as BaseEnvironment; } 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"; } @@ -803,7 +835,8 @@ function resolveHosts(env: BaseEnvironment, override?: GrvtHostsOverride): Hosts function normalizeCookieValue(value: string): string { const parsed = parseSetCookieHeader(value); 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 { @@ -875,7 +908,8 @@ function scaleDecimal(value: string | number | undefined, decimals: number): big const strValue = typeof value === "number" ? value.toString() : value; if (!strValue.includes("e") && !strValue.includes("E")) { 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 combined = `${intPart}${fraction}`; return BigInt(combined || "0"); @@ -947,7 +981,7 @@ function mapOpenOrders(response: IApiOpenOrdersResponse, symbol: string): AsterO 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 state = order.state; const metadata = order.metadata; @@ -1030,7 +1064,7 @@ function mapKlines(response: IApiCandlestickResponse, symbol: string): AsterKlin } 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) { return { orderId: cryptoRandomId(), @@ -1049,7 +1083,75 @@ function mapCreateOrderResponse(response: IApiCreateOrderResponse, symbol: strin 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: { @@ -1080,10 +1182,10 @@ function buildUnsignedOrder(params: { orderParams.timeInForce && orderParams.timeInForce.toUpperCase() === "GTX" ); const reduceOnly = normalizeBoolean(orderParams.reduceOnly); - const priceValue = isMarketOrder ? undefined : orderParams.price; - if (!isMarketOrder && (priceValue == null || !Number.isFinite(Number(priceValue)))) { - throw new Error("GRVT limit orders require a valid price"); - } + const priceValue = isMarketOrder ? undefined : orderParams.price; + if (!isMarketOrder && (priceValue == null || !Number.isFinite(Number(priceValue)))) { + throw new Error("GRVT limit orders require a valid price"); + } const trigger = buildTriggerMetadata(orderParams); const metadata = { @@ -1141,41 +1243,41 @@ function buildTriggerMetadata(params: CreateOrderParams): GrvtUnsignedOrder["met return undefined; } -function mapIntervalToGrvt(interval: string): string { +function mapIntervalToGrvt(interval: string): ECandlestickInterval { const normalized = interval.trim().toLowerCase(); switch (normalized) { case "1m": - return "CI_1_M"; + return ECandlestickInterval.CI_1_M; case "3m": - return "CI_3_M"; + return ECandlestickInterval.CI_3_M; case "5m": - return "CI_5_M"; + return ECandlestickInterval.CI_5_M; case "15m": - return "CI_15_M"; + return ECandlestickInterval.CI_15_M; case "30m": - return "CI_30_M"; + return ECandlestickInterval.CI_30_M; case "1h": - return "CI_1_H"; + return ECandlestickInterval.CI_1_H; case "2h": - return "CI_2_H"; + return ECandlestickInterval.CI_2_H; case "4h": - return "CI_4_H"; + return ECandlestickInterval.CI_4_H; case "6h": - return "CI_6_H"; + return ECandlestickInterval.CI_6_H; case "8h": - return "CI_8_H"; + return ECandlestickInterval.CI_8_H; case "12h": - return "CI_12_H"; + return ECandlestickInterval.CI_12_H; case "1d": - return "CI_1_D"; + return ECandlestickInterval.CI_1_D; case "1w": - return "CI_1_W"; + return ECandlestickInterval.CI_1_W; 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()) { case "IOC": return "IMMEDIATE_OR_CANCEL"; diff --git a/src/exchanges/types.ts b/src/exchanges/types.ts index 43ce546..6ff56c4 100644 --- a/src/exchanges/types.ts +++ b/src/exchanges/types.ts @@ -51,10 +51,17 @@ export interface GrvtOrderLeg { is_buying_asset?: boolean; } +export type GrvtTimeInForce = + | "GOOD_TILL_TIME" + | "ALL_OR_NONE" + | "IMMEDIATE_OR_CANCEL" + | "FILL_OR_KILL"; + export interface GrvtOrderMetadata { client_order_id?: string; create_time?: string; broker?: string | null; + trigger?: GrvtTriggerMetadata; } export interface GrvtOrderState { @@ -71,7 +78,7 @@ export interface GrvtOrder { client_order_id?: string; sub_account_id?: string; is_market?: boolean; - time_in_force?: string; + time_in_force?: GrvtTimeInForce; post_only?: boolean; reduce_only?: boolean; legs?: GrvtOrderLeg[]; @@ -229,7 +236,7 @@ export interface GrvtOrderMetadataInput { export interface GrvtUnsignedOrder { sub_account_id: string; is_market: boolean; - time_in_force: string; + time_in_force: GrvtTimeInForce; post_only: boolean; reduce_only: boolean; legs: GrvtUnsignedOrderLeg[]; diff --git a/src/utils/math.ts b/src/utils/math.ts index 15f53e2..12c67a5 100644 --- a/src/utils/math.ts +++ b/src/utils/math.ts @@ -14,7 +14,8 @@ export function roundQtyDownToStep(value: number, step: number): number { export function decimalsOf(step: number): number { const s = step.toString(); 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 { diff --git a/src/utils/risk.ts b/src/utils/risk.ts index 0fa5c2f..b0fc8ba 100644 --- a/src/utils/risk.ts +++ b/src/utils/risk.ts @@ -9,18 +9,18 @@ export function shouldStopLoss( const absPosition = Math.abs(position.positionAmt); 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 - ? (bestBid - position.entryPrice) * absPosition - : (position.entryPrice - bestAsk) * absPosition; + ? (closePrice - position.entryPrice) * absPosition + : (position.entryPrice - closePrice) * absPosition; - const unrealized = Number.isFinite(position.unrealizedProfit) - ? (position.unrealizedProfit as number) - : null; + if (!Number.isFinite(pnl)) return false; - const derivedLoss = pnl < -lossLimit; - const snapshotLoss = Boolean(unrealized != null && unrealized < -lossLimit && pnl <= 0); - - return derivedLoss || snapshotLoss; + return pnl < -lossLimit; } - -