diff --git a/src/exchanges/nado/gateway.ts b/src/exchanges/nado/gateway.ts index cb5c732..3fbbbea 100644 --- a/src/exchanges/nado/gateway.ts +++ b/src/exchanges/nado/gateway.ts @@ -1275,13 +1275,14 @@ export class NadoGateway { const origQty = fromX18(new BigNumber(order.amountX18).abs()).toFixed(); const remaining = fromX18(new BigNumber(order.unfilledAmountX18).abs()).toFixed(); const executed = new BigNumber(origQty).minus(remaining); + const status = executed.isFinite() && executed.gt(0) ? "PARTIALLY_FILLED" : "NEW"; orders.push({ orderId: order.digest, clientOrderId: order.clientId != null ? String(order.clientId) : order.digest, symbol: displaySymbol, side, type, - status: "NEW", + status, price: fromX18(order.priceX18).toFixed(), origQty, executedQty: executed.isFinite() ? executed.toFixed() : "0", diff --git a/src/strategy/maker-engine.ts b/src/strategy/maker-engine.ts index bf9a6a2..85179e5 100644 --- a/src/strategy/maker-engine.ts +++ b/src/strategy/maker-engine.ts @@ -10,6 +10,7 @@ import type { import { formatPriceToString } from "../utils/math"; import { createTradeLog, type TradeLogEntry } from "../logging/trade-log"; import { extractMessage, isInsufficientBalanceError, isUnknownOrderError, isRateLimitError } from "../utils/errors"; +import { isOrderActiveStatus } from "../utils/order-status"; import { getPosition } from "../utils/strategy"; import type { PositionSnapshot } from "../utils/strategy"; import { computePositionPnl } from "../utils/pnl"; @@ -183,7 +184,12 @@ export class MakerEngine { (orders) => { this.syncLocksWithOrders(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 && + isOrderActiveStatus(order.status) + ) : []; const currentIds = new Set(this.openOrders.map((order) => String(order.orderId))); for (const id of Array.from(this.pendingCancelOrders)) { @@ -386,10 +392,7 @@ export class MakerEngine { private async syncOrders(targets: DesiredOrder[]): Promise { const availableOrders = this.openOrders.filter((o) => !this.pendingCancelOrders.has(String(o.orderId))); - const openOrders = availableOrders.filter((order) => { - const status = (order.status ?? "").toUpperCase(); - return !status.includes("CLOSED") && !status.includes("FILLED") && !status.includes("CANCELED"); - }); + const openOrders = availableOrders.filter((order) => isOrderActiveStatus(order.status)); const { toCancel, toPlace } = makeOrderPlan(openOrders, targets); for (const order of toCancel) { diff --git a/src/strategy/offset-maker-engine.ts b/src/strategy/offset-maker-engine.ts index d3c6307..6bd8a1c 100644 --- a/src/strategy/offset-maker-engine.ts +++ b/src/strategy/offset-maker-engine.ts @@ -10,6 +10,7 @@ import type { import { formatPriceToString } from "../utils/math"; import { createTradeLog } from "../logging/trade-log"; import { isUnknownOrderError, isRateLimitError } from "../utils/errors"; +import { isOrderActiveStatus } from "../utils/order-status"; import { getPosition, parseSymbolParts } from "../utils/strategy"; import type { PositionSnapshot } from "../utils/strategy"; import { computeDepthStats } from "../utils/depth"; @@ -212,7 +213,12 @@ export class OffsetMakerEngine { this.syncLocksWithOrders(orders); this.feedStatus.orders = true; 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 && + isOrderActiveStatus(order.status) + ) : []; const currentIds = new Set(this.openOrders.map((order) => String(order.orderId))); for (const id of Array.from(this.pendingCancelOrders)) { @@ -650,10 +656,7 @@ export class OffsetMakerEngine { private async syncOrders(targets: DesiredOrder[]): Promise { const availableOrders = this.openOrders.filter((o) => !this.pendingCancelOrders.has(String(o.orderId))); - const openOrders = availableOrders.filter((order) => { - const status = (order.status ?? "").toUpperCase(); - return !status.includes("CLOSED") && !status.includes("FILLED") && !status.includes("CANCELED"); - }); + const openOrders = availableOrders.filter((order) => isOrderActiveStatus(order.status)); // Coalesce reprices for entry orders: if within tick threshold or within dwell window, keep existing order const adjustedTargets: DesiredOrder[] = targets.map((t) => ({ ...t })); diff --git a/src/utils/errors.test.ts b/src/utils/errors.test.ts new file mode 100644 index 0000000..90cc383 --- /dev/null +++ b/src/utils/errors.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; + +import { isUnknownOrderError } from "./errors"; + +describe("isUnknownOrderError", () => { + it("matches Nado digest-not-found by code", () => { + const err = Object.assign( + new Error("2020: Order with the provided digest (0xdeadbeef) could not be found."), + { code: 2020 } + ); + expect(isUnknownOrderError(err)).toBe(true); + }); + + it("matches Nado digest-not-found by message", () => { + const err = new Error("Order with the provided digest (0xdeadbeef) could not be found."); + expect(isUnknownOrderError(err)).toBe(true); + }); + + it("does not match unrelated errors", () => { + expect(isUnknownOrderError(new Error("insufficient balance"))).toBe(false); + }); +}); + diff --git a/src/utils/errors.ts b/src/utils/errors.ts index cbc0cf8..66fe61a 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -1,4 +1,10 @@ export function isUnknownOrderError(error: unknown): boolean { + if (!error) return false; + if (typeof error === "object" && error !== null && "code" in error) { + const code = Number((error as { code?: unknown }).code); + // Nado: 2020: Order with the provided digest ... could not be found. + if (Number.isFinite(code) && code === 2020) return true; + } const message = extractMessage(error); if (!message) return false; const upper = message.toUpperCase(); @@ -7,7 +13,9 @@ export function isUnknownOrderError(error: unknown): boolean { upper.includes("CODE\":-2011") || upper.includes("ORDER_ID_NOT_FOUND") || upper.includes("ORDER_IS_CLOSED") || - upper.includes("COULD NOT FIND ORDER") + upper.includes("COULD NOT FIND ORDER") || + upper.includes("ORDER WITH THE PROVIDED DIGEST") || + (upper.includes("COULD NOT BE FOUND") && upper.includes("ORDER")) ); } diff --git a/src/utils/order-status.test.ts b/src/utils/order-status.test.ts new file mode 100644 index 0000000..01f34b0 --- /dev/null +++ b/src/utils/order-status.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; + +import { isOrderActiveStatus } from "./order-status"; + +describe("isOrderActiveStatus", () => { + it("treats empty status as active", () => { + expect(isOrderActiveStatus(undefined)).toBe(true); + expect(isOrderActiveStatus("")).toBe(true); + expect(isOrderActiveStatus(" ")).toBe(true); + }); + + it("treats typical active statuses as active", () => { + expect(isOrderActiveStatus("NEW")).toBe(true); + expect(isOrderActiveStatus("PARTIALLY_FILLED")).toBe(true); + expect(isOrderActiveStatus("waiting_price")).toBe(true); + }); + + it("treats final/non-open statuses as inactive", () => { + expect(isOrderActiveStatus("FILLED")).toBe(false); + expect(isOrderActiveStatus("CANCELED")).toBe(false); + expect(isOrderActiveStatus("CANCELLED")).toBe(false); + expect(isOrderActiveStatus("REJECTED")).toBe(false); + expect(isOrderActiveStatus("EXPIRED")).toBe(false); + expect(isOrderActiveStatus("TRIGGERED")).toBe(false); + expect(isOrderActiveStatus("CLOSED")).toBe(false); + expect(isOrderActiveStatus("closed_by_user")).toBe(false); + }); +}); + diff --git a/src/utils/order-status.ts b/src/utils/order-status.ts new file mode 100644 index 0000000..9e08887 --- /dev/null +++ b/src/utils/order-status.ts @@ -0,0 +1,11 @@ +export function isOrderActiveStatus(status: string | undefined): boolean { + if (!status) return true; + const normalized = status.trim().toUpperCase(); + if (!normalized) return true; + if (normalized.includes("CLOSED")) return false; + if (normalized === "FILLED") return false; + if (normalized === "CANCELED" || normalized === "CANCELLED") return false; + if (normalized === "REJECTED" || normalized === "EXPIRED") return false; + if (normalized === "TRIGGERED") return false; + return true; +}