mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-09 08:18:07 +00:00
优化做市引擎、偏移做市引擎和趋势引擎的订单管理逻辑,将待取消订单的类型从数字更改为字符串,确保在处理订单时的类型一致性。同时,增强风险检查逻辑,确保在平仓时使用正确的价格,提升交易策略的稳定性和准确性。
This commit is contained in:
@@ -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<void> {
|
||||
@@ -19,4 +19,3 @@ export async function safeCancelOrder(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+18
-16
@@ -60,7 +60,7 @@ export class MakerEngine {
|
||||
private readonly locks: OrderLockMap = {};
|
||||
private readonly timers: OrderTimerMap = {};
|
||||
private readonly pending: OrderPendingMap = {};
|
||||
private readonly pendingCancelOrders = new Set<number>();
|
||||
private readonly pendingCancelOrders = new Set<string>();
|
||||
|
||||
private readonly tradeLog: ReturnType<typeof createTradeLog>;
|
||||
private readonly listeners = new Map<MakerEvent, Set<MakerListener>>();
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -54,7 +54,7 @@ export class OffsetMakerEngine {
|
||||
private readonly locks: OrderLockMap = {};
|
||||
private readonly timers: OrderTimerMap = {};
|
||||
private readonly pending: OrderPendingMap = {};
|
||||
private readonly pendingCancelOrders = new Set<number>();
|
||||
private readonly pendingCancelOrders = new Set<string>();
|
||||
|
||||
private readonly tradeLog: ReturnType<typeof createTradeLog>;
|
||||
private readonly listeners = new Map<MakerEvent, Set<MakerListener>>();
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ export class TrendEngine {
|
||||
private prevPositionAmt = 0;
|
||||
private initializedPosition = false;
|
||||
private cancelAllRequested = false;
|
||||
private readonly pendingCancelOrders = new Set<number>();
|
||||
private readonly pendingCancelOrders = new Set<string>();
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -707,10 +707,12 @@ function updateAccountSnapshot(snapshot: AsterAccountSnapshot | null, event: { e
|
||||
}
|
||||
|
||||
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)) {
|
||||
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()));
|
||||
|
||||
+140
-38
@@ -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<string, keyof typeof ENVIRONMENT_HOSTS> = {
|
||||
};
|
||||
|
||||
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<GrvtGatewayOptions["pollIntervals"]>;
|
||||
private readonly pollIntervals: {
|
||||
account: number;
|
||||
orders: number;
|
||||
depth: number;
|
||||
ticker: number;
|
||||
klines: number;
|
||||
};
|
||||
private readonly hosts: HostsConfig;
|
||||
private readonly chainId: number;
|
||||
private headers: Record<string, string> = {};
|
||||
@@ -220,7 +234,7 @@ export class GrvtGateway {
|
||||
private klineTimer: ReturnType<typeof setInterval> | 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";
|
||||
|
||||
@@ -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[];
|
||||
|
||||
+2
-1
@@ -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 {
|
||||
|
||||
+11
-11
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user