mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-11 17:28:08 +00:00
Enhance NadoGateway with min size policy handling and related utility functions. Update .env.example to include NADO_MIN_SIZE_POLICY configuration option.
This commit is contained in:
@@ -134,6 +134,11 @@ NADO_MARKET_SLIPPAGE_PCT=0.01
|
|||||||
# Optional: stop trigger source for STOP_MARKET orders (oracle | last | mid)
|
# Optional: stop trigger source for STOP_MARKET orders (oracle | last | mid)
|
||||||
NADO_STOP_TRIGGER_SOURCE=oracle
|
NADO_STOP_TRIGGER_SOURCE=oracle
|
||||||
|
|
||||||
|
# Optional: how to handle orders smaller than Nado min_size (USDT0 notional)
|
||||||
|
# - adjust: round quantity up to the minimum allowed size (default)
|
||||||
|
# - reject: throw an error instead of auto-adjusting
|
||||||
|
NADO_MIN_SIZE_POLICY=adjust
|
||||||
|
|
||||||
# Optional endpoint overrides
|
# Optional endpoint overrides
|
||||||
# NADO_GATEWAY_WS_URL=wss://gateway.prod.nado.xyz/v1/ws
|
# NADO_GATEWAY_WS_URL=wss://gateway.prod.nado.xyz/v1/ws
|
||||||
# NADO_SUBSCRIPTIONS_WS_URL=wss://gateway.prod.nado.xyz/v1/subscribe
|
# NADO_SUBSCRIPTIONS_WS_URL=wss://gateway.prod.nado.xyz/v1/subscribe
|
||||||
|
|||||||
+133
-20
@@ -1,7 +1,7 @@
|
|||||||
import NodeWebSocket from "ws";
|
import NodeWebSocket from "ws";
|
||||||
import BigNumber from "bignumber.js";
|
import BigNumber from "bignumber.js";
|
||||||
import { IndexerClient } from "@nadohq/indexer-client";
|
import { IndexerClient } from "@nadohq/indexer-client";
|
||||||
import { TriggerClient } from "@nadohq/trigger-client";
|
import { TriggerClient, type PriceTriggerRequirementType } from "@nadohq/trigger-client";
|
||||||
import { ENGINE_WS_CLIENT_ENDPOINTS, ENGINE_WS_SUBSCRIPTION_CLIENT_ENDPOINTS } from "@nadohq/engine-client";
|
import { ENGINE_WS_CLIENT_ENDPOINTS, ENGINE_WS_SUBSCRIPTION_CLIENT_ENDPOINTS } from "@nadohq/engine-client";
|
||||||
import type { ChainEnv, WalletClientWithAccount } from "@nadohq/shared";
|
import type { ChainEnv, WalletClientWithAccount } from "@nadohq/shared";
|
||||||
import {
|
import {
|
||||||
@@ -12,7 +12,8 @@ import {
|
|||||||
getOrderVerifyingAddress,
|
getOrderVerifyingAddress,
|
||||||
packOrderAppendix,
|
packOrderAppendix,
|
||||||
} from "@nadohq/shared";
|
} from "@nadohq/shared";
|
||||||
import { createWalletClient, custom, privateKeyToAccount, type Address } from "viem";
|
import { createWalletClient, custom, type Address } from "viem";
|
||||||
|
import { privateKeyToAccount } from "viem/accounts";
|
||||||
import type {
|
import type {
|
||||||
AccountListener,
|
AccountListener,
|
||||||
DepthListener,
|
DepthListener,
|
||||||
@@ -32,7 +33,6 @@ import type {
|
|||||||
CreateOrderParams,
|
CreateOrderParams,
|
||||||
TimeInForce,
|
TimeInForce,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import type { PriceTriggerRequirementType } from "@nadohq/trigger-client";
|
|
||||||
import type {
|
import type {
|
||||||
NadoBestBidOfferEvent,
|
NadoBestBidOfferEvent,
|
||||||
NadoContractsResponse,
|
NadoContractsResponse,
|
||||||
@@ -62,6 +62,9 @@ const WS_PING_INTERVAL_MS = 30_000;
|
|||||||
const WS_STALE_TIMEOUT_MS = 75_000;
|
const WS_STALE_TIMEOUT_MS = 75_000;
|
||||||
|
|
||||||
const DEFAULT_MARKET_SLIPPAGE_PCT = 0.01;
|
const DEFAULT_MARKET_SLIPPAGE_PCT = 0.01;
|
||||||
|
const X18_BIGINT = 1_000_000_000_000_000_000n;
|
||||||
|
|
||||||
|
type MinSizePolicy = "adjust" | "reject";
|
||||||
|
|
||||||
export interface NadoGatewayOptions {
|
export interface NadoGatewayOptions {
|
||||||
env?: ChainEnv;
|
env?: ChainEnv;
|
||||||
@@ -80,6 +83,7 @@ export interface NadoGatewayOptions {
|
|||||||
};
|
};
|
||||||
marketSlippagePct?: number;
|
marketSlippagePct?: number;
|
||||||
stopTriggerSource?: NadoTriggerPriceSource;
|
stopTriggerSource?: NadoTriggerPriceSource;
|
||||||
|
minSizePolicy?: MinSizePolicy;
|
||||||
logger?: (context: string, error: unknown) => void;
|
logger?: (context: string, error: unknown) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,6 +205,47 @@ function nowMs(): number {
|
|||||||
return Date.now();
|
return Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function absBigInt(value: bigint): bigint {
|
||||||
|
return value < 0n ? -value : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ceilDiv(numerator: bigint, denominator: bigint): bigint {
|
||||||
|
if (denominator === 0n) {
|
||||||
|
throw new Error("Division by zero");
|
||||||
|
}
|
||||||
|
const quotient = numerator / denominator;
|
||||||
|
return numerator % denominator === 0n ? quotient : quotient + 1n;
|
||||||
|
}
|
||||||
|
|
||||||
|
function roundDownToIncrement(value: bigint, increment: bigint): bigint {
|
||||||
|
if (increment <= 0n) return value;
|
||||||
|
return (value / increment) * increment;
|
||||||
|
}
|
||||||
|
|
||||||
|
function roundUpToIncrement(value: bigint, increment: bigint): bigint {
|
||||||
|
if (increment <= 0n) return value;
|
||||||
|
const remainder = value % increment;
|
||||||
|
if (remainder === 0n) return value;
|
||||||
|
return value + (increment - remainder);
|
||||||
|
}
|
||||||
|
|
||||||
|
function alignPriceX18(
|
||||||
|
priceX18: bigint,
|
||||||
|
incrementX18: bigint,
|
||||||
|
side: "BUY" | "SELL",
|
||||||
|
mode: "aggressive" | "passive"
|
||||||
|
): bigint {
|
||||||
|
if (incrementX18 <= 0n) return priceX18;
|
||||||
|
if (mode === "aggressive") {
|
||||||
|
return side === "BUY"
|
||||||
|
? roundUpToIncrement(priceX18, incrementX18)
|
||||||
|
: roundDownToIncrement(priceX18, incrementX18);
|
||||||
|
}
|
||||||
|
return side === "BUY"
|
||||||
|
? roundDownToIncrement(priceX18, incrementX18)
|
||||||
|
: roundUpToIncrement(priceX18, incrementX18);
|
||||||
|
}
|
||||||
|
|
||||||
function nsToMs(ns: string): number {
|
function nsToMs(ns: string): number {
|
||||||
try {
|
try {
|
||||||
const nanos = BigInt(ns);
|
const nanos = BigInt(ns);
|
||||||
@@ -273,6 +318,7 @@ export class NadoGateway {
|
|||||||
private readonly pollIntervals: PollIntervals;
|
private readonly pollIntervals: PollIntervals;
|
||||||
private readonly marketSlippagePct: number;
|
private readonly marketSlippagePct: number;
|
||||||
private readonly stopTriggerSource: NadoTriggerPriceSource;
|
private readonly stopTriggerSource: NadoTriggerPriceSource;
|
||||||
|
private readonly minSizePolicy: MinSizePolicy;
|
||||||
|
|
||||||
private readonly symbolMetaBySymbol = new Map<string, SymbolMeta>();
|
private readonly symbolMetaBySymbol = new Map<string, SymbolMeta>();
|
||||||
private readonly symbolMetaByProductId = new Map<number, SymbolMeta>();
|
private readonly symbolMetaByProductId = new Map<number, SymbolMeta>();
|
||||||
@@ -351,6 +397,7 @@ export class NadoGateway {
|
|||||||
const slippage = options.marketSlippagePct ?? safeToNumber(process.env.NADO_MARKET_SLIPPAGE_PCT ?? "");
|
const slippage = options.marketSlippagePct ?? safeToNumber(process.env.NADO_MARKET_SLIPPAGE_PCT ?? "");
|
||||||
this.marketSlippagePct = Number.isFinite(slippage) && slippage > 0 ? slippage : DEFAULT_MARKET_SLIPPAGE_PCT;
|
this.marketSlippagePct = Number.isFinite(slippage) && slippage > 0 ? slippage : DEFAULT_MARKET_SLIPPAGE_PCT;
|
||||||
this.stopTriggerSource = options.stopTriggerSource ?? (process.env.NADO_STOP_TRIGGER_SOURCE as NadoTriggerPriceSource) ?? "oracle";
|
this.stopTriggerSource = options.stopTriggerSource ?? (process.env.NADO_STOP_TRIGGER_SOURCE as NadoTriggerPriceSource) ?? "oracle";
|
||||||
|
this.minSizePolicy = options.minSizePolicy ?? ((process.env.NADO_MIN_SIZE_POLICY ?? "").trim().toLowerCase() === "reject" ? "reject" : "adjust");
|
||||||
|
|
||||||
this.primarySymbol = options.symbol;
|
this.primarySymbol = options.symbol;
|
||||||
this.rememberDisplaySymbol(this.primarySymbol);
|
this.rememberDisplaySymbol(this.primarySymbol);
|
||||||
@@ -607,12 +654,12 @@ export class NadoGateway {
|
|||||||
if (!meta) return null;
|
if (!meta) return null;
|
||||||
const priceTick = fromX18(meta.priceIncrementX18).toNumber();
|
const priceTick = fromX18(meta.priceIncrementX18).toNumber();
|
||||||
const qtyStep = fromX18(meta.sizeIncrementX18).toNumber();
|
const qtyStep = fromX18(meta.sizeIncrementX18).toNumber();
|
||||||
const minBaseAmount = fromX18(meta.minSizeX18).toNumber();
|
const minQuoteAmount = fromX18(meta.minSizeX18).toNumber();
|
||||||
return {
|
return {
|
||||||
priceTick,
|
priceTick,
|
||||||
qtyStep,
|
qtyStep,
|
||||||
marketId: meta.productId,
|
marketId: meta.productId,
|
||||||
minBaseAmount,
|
minQuoteAmount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1439,7 +1486,7 @@ export class NadoGateway {
|
|||||||
},
|
},
|
||||||
primaryType: "StreamAuthentication",
|
primaryType: "StreamAuthentication",
|
||||||
message: {
|
message: {
|
||||||
sender,
|
sender: sender as `0x${string}`,
|
||||||
expiration: BigInt(expirationMs),
|
expiration: BigInt(expirationMs),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1759,7 +1806,27 @@ export class NadoGateway {
|
|||||||
throw new Error("Invalid order price");
|
throw new Error("Invalid order price");
|
||||||
}
|
}
|
||||||
|
|
||||||
const amountX18 = toX18BigInt(qty);
|
const amountX18Raw = toX18BigInt(qty);
|
||||||
|
const priceX18 = toX18BigInt(price);
|
||||||
|
const minSizeX18 = BigInt(meta.minSizeX18);
|
||||||
|
const sizeIncrementX18 = BigInt(meta.sizeIncrementX18);
|
||||||
|
const notionalX18 = (absBigInt(amountX18Raw) * priceX18) / X18_BIGINT;
|
||||||
|
|
||||||
|
const amountX18 = (() => {
|
||||||
|
if (notionalX18 >= minSizeX18) return absBigInt(amountX18Raw);
|
||||||
|
const required = roundUpToIncrement(ceilDiv(minSizeX18 * X18_BIGINT, priceX18), sizeIncrementX18);
|
||||||
|
if (this.minSizePolicy === "reject") {
|
||||||
|
const current = fromX18(notionalX18.toString()).toFixed();
|
||||||
|
const minimum = fromX18(minSizeX18.toString()).toFixed();
|
||||||
|
const requiredQty = fromX18(required.toString()).toFixed();
|
||||||
|
throw new Error(
|
||||||
|
`Order size too small: notional ${current} < min_size ${minimum} (USDT0). ` +
|
||||||
|
`At price ${new BigNumber(price).toFixed()} you need qty >= ${requiredQty}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return required;
|
||||||
|
})();
|
||||||
|
|
||||||
const signedAmount = side === "SELL" ? -amountX18 : amountX18;
|
const signedAmount = side === "SELL" ? -amountX18 : amountX18;
|
||||||
|
|
||||||
const appendix = packOrderAppendix({
|
const appendix = packOrderAppendix({
|
||||||
@@ -1827,7 +1894,7 @@ export class NadoGateway {
|
|||||||
type: "LIMIT",
|
type: "LIMIT",
|
||||||
status: "NEW",
|
status: "NEW",
|
||||||
price: String(price),
|
price: String(price),
|
||||||
origQty: String(qty),
|
origQty: fromX18(amountX18.toString()).toFixed(),
|
||||||
executedQty: "0",
|
executedQty: "0",
|
||||||
stopPrice: "0",
|
stopPrice: "0",
|
||||||
time: nowMs(),
|
time: nowMs(),
|
||||||
@@ -1844,6 +1911,7 @@ export class NadoGateway {
|
|||||||
if (!Number.isFinite(qty) || qty <= 0) {
|
if (!Number.isFinite(qty) || qty <= 0) {
|
||||||
throw new Error("Invalid order quantity");
|
throw new Error("Invalid order quantity");
|
||||||
}
|
}
|
||||||
|
const priceIncrementX18 = BigInt(meta.priceIncrementX18);
|
||||||
const bbo = this.bestBidOfferByProductId.get(meta.productId);
|
const bbo = this.bestBidOfferByProductId.get(meta.productId);
|
||||||
const reference = (() => {
|
const reference = (() => {
|
||||||
if (bbo) {
|
if (bbo) {
|
||||||
@@ -1857,11 +1925,33 @@ export class NadoGateway {
|
|||||||
if (!reference) {
|
if (!reference) {
|
||||||
throw new Error("Market order rejected: missing best bid/offer");
|
throw new Error("Market order rejected: missing best bid/offer");
|
||||||
}
|
}
|
||||||
const limitPrice = side === "BUY"
|
const limitPriceRaw = side === "BUY"
|
||||||
? reference.multipliedBy(1 + this.marketSlippagePct)
|
? reference.multipliedBy(1 + this.marketSlippagePct)
|
||||||
: reference.multipliedBy(1 - this.marketSlippagePct);
|
: reference.multipliedBy(1 - this.marketSlippagePct);
|
||||||
|
const limitPriceX18 = alignPriceX18(toX18BigInt(limitPriceRaw), priceIncrementX18, side, "aggressive");
|
||||||
|
const limitPrice = fromX18(limitPriceX18.toString());
|
||||||
|
|
||||||
|
const amountX18Raw = toX18BigInt(qty);
|
||||||
|
const priceX18 = limitPriceX18;
|
||||||
|
const minSizeX18 = BigInt(meta.minSizeX18);
|
||||||
|
const sizeIncrementX18 = BigInt(meta.sizeIncrementX18);
|
||||||
|
const notionalX18 = (absBigInt(amountX18Raw) * priceX18) / X18_BIGINT;
|
||||||
|
|
||||||
|
const amountX18 = (() => {
|
||||||
|
if (notionalX18 >= minSizeX18) return absBigInt(amountX18Raw);
|
||||||
|
const required = roundUpToIncrement(ceilDiv(minSizeX18 * X18_BIGINT, priceX18), sizeIncrementX18);
|
||||||
|
if (this.minSizePolicy === "reject") {
|
||||||
|
const current = fromX18(notionalX18.toString()).toFixed();
|
||||||
|
const minimum = fromX18(minSizeX18.toString()).toFixed();
|
||||||
|
const requiredQty = fromX18(required.toString()).toFixed();
|
||||||
|
throw new Error(
|
||||||
|
`Order size too small: notional ${current} < min_size ${minimum} (USDT0). ` +
|
||||||
|
`At price ${limitPrice.toFixed()} you need qty >= ${requiredQty}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return required;
|
||||||
|
})();
|
||||||
|
|
||||||
const amountX18 = toX18BigInt(qty);
|
|
||||||
const signedAmount = side === "SELL" ? -amountX18 : amountX18;
|
const signedAmount = side === "SELL" ? -amountX18 : amountX18;
|
||||||
|
|
||||||
const appendix = packOrderAppendix({
|
const appendix = packOrderAppendix({
|
||||||
@@ -1916,7 +2006,7 @@ export class NadoGateway {
|
|||||||
type: "MARKET",
|
type: "MARKET",
|
||||||
status: "NEW",
|
status: "NEW",
|
||||||
price: limitPrice.toFixed(),
|
price: limitPrice.toFixed(),
|
||||||
origQty: String(qty),
|
origQty: fromX18(amountX18.toString()).toFixed(),
|
||||||
executedQty: "0",
|
executedQty: "0",
|
||||||
stopPrice: "0",
|
stopPrice: "0",
|
||||||
time: nowMs(),
|
time: nowMs(),
|
||||||
@@ -1933,18 +2023,41 @@ export class NadoGateway {
|
|||||||
if (!Number.isFinite(qty) || qty <= 0) {
|
if (!Number.isFinite(qty) || qty <= 0) {
|
||||||
throw new Error("Invalid order quantity");
|
throw new Error("Invalid order quantity");
|
||||||
}
|
}
|
||||||
|
const priceIncrementX18 = BigInt(meta.priceIncrementX18);
|
||||||
const stopPrice = params.stopPrice ?? 0;
|
const stopPrice = params.stopPrice ?? 0;
|
||||||
if (!Number.isFinite(stopPrice) || stopPrice <= 0) {
|
if (!Number.isFinite(stopPrice) || stopPrice <= 0) {
|
||||||
throw new Error("Invalid stop price");
|
throw new Error("Invalid stop price");
|
||||||
}
|
}
|
||||||
|
|
||||||
const amountX18 = toX18BigInt(qty);
|
const stopPriceX18 = alignPriceX18(toX18BigInt(stopPrice), priceIncrementX18, side, "passive");
|
||||||
const signedAmount = side === "SELL" ? -amountX18 : amountX18;
|
const limitFromStopRaw = side === "BUY"
|
||||||
|
? fromX18(stopPriceX18.toString()).multipliedBy(1 + this.marketSlippagePct)
|
||||||
|
: fromX18(stopPriceX18.toString()).multipliedBy(1 - this.marketSlippagePct);
|
||||||
|
const limitFromStopX18 = alignPriceX18(toX18BigInt(limitFromStopRaw), priceIncrementX18, side, "aggressive");
|
||||||
|
const limitFromStop = fromX18(limitFromStopX18.toString());
|
||||||
|
|
||||||
const stopPriceX18 = toX18BigInt(stopPrice);
|
const amountX18Raw = toX18BigInt(qty);
|
||||||
const limitFromStop = side === "BUY"
|
const priceX18 = limitFromStopX18;
|
||||||
? fromX18(stopPriceX18).multipliedBy(1 + this.marketSlippagePct)
|
const minSizeX18 = BigInt(meta.minSizeX18);
|
||||||
: fromX18(stopPriceX18).multipliedBy(1 - this.marketSlippagePct);
|
const sizeIncrementX18 = BigInt(meta.sizeIncrementX18);
|
||||||
|
const notionalX18 = (absBigInt(amountX18Raw) * priceX18) / X18_BIGINT;
|
||||||
|
|
||||||
|
const amountX18 = (() => {
|
||||||
|
if (notionalX18 >= minSizeX18) return absBigInt(amountX18Raw);
|
||||||
|
const required = roundUpToIncrement(ceilDiv(minSizeX18 * X18_BIGINT, priceX18), sizeIncrementX18);
|
||||||
|
if (this.minSizePolicy === "reject") {
|
||||||
|
const current = fromX18(notionalX18.toString()).toFixed();
|
||||||
|
const minimum = fromX18(minSizeX18.toString()).toFixed();
|
||||||
|
const requiredQty = fromX18(required.toString()).toFixed();
|
||||||
|
throw new Error(
|
||||||
|
`Order size too small: notional ${current} < min_size ${minimum} (USDT0). ` +
|
||||||
|
`At price ${limitFromStop.toFixed()} you need qty >= ${requiredQty}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return required;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const signedAmount = side === "SELL" ? -amountX18 : amountX18;
|
||||||
|
|
||||||
const appendix = packOrderAppendix({
|
const appendix = packOrderAppendix({
|
||||||
orderExecutionType: "ioc",
|
orderExecutionType: "ioc",
|
||||||
@@ -1981,7 +2094,7 @@ export class NadoGateway {
|
|||||||
type: "price",
|
type: "price",
|
||||||
criteria: {
|
criteria: {
|
||||||
type: requirementType,
|
type: requirementType,
|
||||||
triggerPrice: new BigNumber(stopPrice).toFixed(),
|
triggerPrice: fromX18(stopPriceX18.toString()).toFixed(),
|
||||||
},
|
},
|
||||||
} as any,
|
} as any,
|
||||||
} as any);
|
} as any);
|
||||||
@@ -2011,9 +2124,9 @@ export class NadoGateway {
|
|||||||
type: "STOP_MARKET",
|
type: "STOP_MARKET",
|
||||||
status: "NEW",
|
status: "NEW",
|
||||||
price: limitFromStop.toFixed(),
|
price: limitFromStop.toFixed(),
|
||||||
origQty: String(qty),
|
origQty: fromX18(amountX18.toString()).toFixed(),
|
||||||
executedQty: "0",
|
executedQty: "0",
|
||||||
stopPrice: String(stopPrice),
|
stopPrice: fromX18(stopPriceX18.toString()).toFixed(),
|
||||||
time: nowMs(),
|
time: nowMs(),
|
||||||
updateTime: nowMs(),
|
updateTime: nowMs(),
|
||||||
reduceOnly: true,
|
reduceOnly: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user