mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-11 09:18:08 +00:00
优化做市引擎、偏移做市引擎和趋势引擎的订单管理逻辑,将待取消订单的类型从数字更改为字符串,确保在处理订单时的类型一致性。同时,增强风险检查逻辑,确保在平仓时使用正确的价格,提升交易策略的稳定性和准确性。
This commit is contained in:
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()));
|
||||||
|
|||||||
+136
-34
@@ -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: {
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user