mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-12 01:38:07 +00:00
Compare commits
2
Commits
d493642935
...
12e8e3e064
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12e8e3e064 | ||
|
|
aa24995d28 |
+1
-1
@@ -18,7 +18,7 @@ STANDX_SYMBOL=BTC-USD
|
|||||||
# STANDX_REQUEST_PRIVATE_KEY=
|
# STANDX_REQUEST_PRIVATE_KEY=
|
||||||
# Token expiry configuration (recommended method: creation date + validity days)
|
# Token expiry configuration (recommended method: creation date + validity days)
|
||||||
# Get these values when generating API token at https://standx.com/user/session
|
# Get these values when generating API token at https://standx.com/user/session
|
||||||
# STANDX_TOKEN_CREATE_DATE=2025-01-15 # Token creation date (YYYY-MM-DD format)
|
# STANDX_TOKEN_CREATE_DATE=2026-01-15 # Token creation date (YYYY-MM-DD format)
|
||||||
# STANDX_TOKEN_VALIDITY_DAYS=30 # Token validity period in days
|
# STANDX_TOKEN_VALIDITY_DAYS=30 # Token validity period in days
|
||||||
# Legacy method: direct expiry timestamp (Unix seconds)
|
# Legacy method: direct expiry timestamp (Unix seconds)
|
||||||
# STANDX_TOKEN_EXPIRY=1737092800
|
# STANDX_TOKEN_EXPIRY=1737092800
|
||||||
|
|||||||
+4
-2
@@ -679,6 +679,8 @@ To receive order updates via [Order Response Stream](https://docs.standx.com/sta
|
|||||||
|
|
||||||
`GET /api/query_depth_book`
|
`GET /api/query_depth_book`
|
||||||
|
|
||||||
|
**⚠️ Note: The sequence of price levels in the asks and bids arrays is not guaranteed. Please implement local sorting on the client side based on your specific requirements.**
|
||||||
|
|
||||||
**Required Parameters**
|
**Required Parameters**
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
@@ -791,7 +793,7 @@ To receive order updates via [Order Response Stream](https://docs.standx.com/sta
|
|||||||
|
|
||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| countBack | u64 | The required amount of bars to load |
|
| countback | u64 | The required amount of bars to load |
|
||||||
|
|
||||||
**Response Example**:
|
**Response Example**:
|
||||||
|
|
||||||
@@ -840,4 +842,4 @@ For enums, constants, and error codes, see [API Reference](https://docs.standx.c
|
|||||||
|
|
||||||
Last updated on
|
Last updated on
|
||||||
|
|
||||||
[Perps Auth](https://docs.standx.com/standx-api/perps-auth "Perps Auth") [Perps WebSocket API](https://docs.standx.com/standx-api/perps-ws "Perps WebSocket API")
|
[Perps Auth SVM Example](https://docs.standx.com/standx-api/perps-auth-svm-example "Perps Auth SVM Example") [Perps WebSocket API](https://docs.standx.com/standx-api/perps-ws "Perps WebSocket API")
|
||||||
@@ -72,7 +72,7 @@ https://standx.com/user/session
|
|||||||
你会看到类似这样的信息:
|
你会看到类似这样的信息:
|
||||||
- **Token**(很长一串以 eyJ 开头的字符串)
|
- **Token**(很长一串以 eyJ 开头的字符串)
|
||||||
- **Ed25519 Private Key**(Base58 格式的私钥,类似 `HdsyJD7oWgT756124j3taSPGv...`)
|
- **Ed25519 Private Key**(Base58 格式的私钥,类似 `HdsyJD7oWgT756124j3taSPGv...`)
|
||||||
- **创建日期**(例如:2025-01-15)
|
- **创建日期**(例如:2026-01-15)
|
||||||
- **有效期天数**(例如:30 天)
|
- **有效期天数**(例如:30 天)
|
||||||
|
|
||||||
> 🔴 **请把这些值复制保存下来!**
|
> 🔴 **请把这些值复制保存下来!**
|
||||||
@@ -140,8 +140,8 @@ MAKER_POINTS_BAND_30_100=true
|
|||||||
|
|
||||||
# ===== Token 过期时间配置(推荐配置) =====
|
# ===== Token 过期时间配置(推荐配置) =====
|
||||||
# 填写你创建 API Token 时显示的创建日期和有效期天数
|
# 填写你创建 API Token 时显示的创建日期和有效期天数
|
||||||
# 创建日期格式:YYYY-MM-DD(例如:2025-01-15)
|
# 创建日期格式:YYYY-MM-DD(例如:2026-01-15)
|
||||||
STANDX_TOKEN_CREATE_DATE=2025-01-15
|
STANDX_TOKEN_CREATE_DATE=2026-01-15
|
||||||
# 有效期天数(例如:30)
|
# 有效期天数(例如:30)
|
||||||
STANDX_TOKEN_VALIDITY_DAYS=30
|
STANDX_TOKEN_VALIDITY_DAYS=30
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ STANDX_TOKEN_VALIDITY_DAYS=30
|
|||||||
假设你生成的 API Token 信息是:
|
假设你生成的 API Token 信息是:
|
||||||
- Token: `eyJhbGciOiJFUzI1NiIsImtpZCI6IlhnaEJQSVNuN0RQVHlMcWJtLUVHVkVhOU1lMFpwdU9iMk1Qc2gtbUFlencifQ...`
|
- Token: `eyJhbGciOiJFUzI1NiIsImtpZCI6IlhnaEJQSVNuN0RQVHlMcWJtLUVHVkVhOU1lMFpwdU9iMk1Qc2gtbUFlencifQ...`
|
||||||
- Ed25519 Private Key: `HdsyJD7oWgT756124j3taSPGv17vo5u7FafDq3vrun4f`
|
- Ed25519 Private Key: `HdsyJD7oWgT756124j3taSPGv17vo5u7FafDq3vrun4f`
|
||||||
- 创建日期: `2025-01-15`
|
- 创建日期: `2026-01-15`
|
||||||
- 有效期: `30` 天
|
- 有效期: `30` 天
|
||||||
|
|
||||||
那么你的 `.env` 应该这样写:
|
那么你的 `.env` 应该这样写:
|
||||||
@@ -176,7 +176,7 @@ MAKER_POINTS_MIN_REPRICE_BPS=3
|
|||||||
MAKER_POINTS_BAND_0_10=true
|
MAKER_POINTS_BAND_0_10=true
|
||||||
MAKER_POINTS_BAND_10_30=true
|
MAKER_POINTS_BAND_10_30=true
|
||||||
MAKER_POINTS_BAND_30_100=true
|
MAKER_POINTS_BAND_30_100=true
|
||||||
STANDX_TOKEN_CREATE_DATE=2025-01-15
|
STANDX_TOKEN_CREATE_DATE=2026-01-15
|
||||||
STANDX_TOKEN_VALIDITY_DAYS=30
|
STANDX_TOKEN_VALIDITY_DAYS=30
|
||||||
# TELEGRAM_BOT_TOKEN=你的BotToken
|
# TELEGRAM_BOT_TOKEN=你的BotToken
|
||||||
# TELEGRAM_CHAT_ID=你的ChatID
|
# TELEGRAM_CHAT_ID=你的ChatID
|
||||||
@@ -237,13 +237,13 @@ bun run pm2:start:maker-points
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 创建日期(格式:YYYY-MM-DD)
|
# 创建日期(格式:YYYY-MM-DD)
|
||||||
STANDX_TOKEN_CREATE_DATE=2025-01-15
|
STANDX_TOKEN_CREATE_DATE=2026-01-15
|
||||||
# 有效期天数
|
# 有效期天数
|
||||||
STANDX_TOKEN_VALIDITY_DAYS=30
|
STANDX_TOKEN_VALIDITY_DAYS=30
|
||||||
```
|
```
|
||||||
|
|
||||||
**示例计算:**
|
**示例计算:**
|
||||||
- 创建日期:2025-01-15
|
- 创建日期:2026-01-15
|
||||||
- 有效期:30 天
|
- 有效期:30 天
|
||||||
- 过期时间:2025-02-14 00:00:00 UTC
|
- 过期时间:2025-02-14 00:00:00 UTC
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ The WebSocket API provides two streams: **Market Stream** for market data and us
|
|||||||
|
|
||||||
Both WebSocket streams implement the following connection management behavior:
|
Both WebSocket streams implement the following connection management behavior:
|
||||||
|
|
||||||
|
### Connection Duration Limit
|
||||||
|
|
||||||
|
- **Maximum Duration**: A single WebSocket connection can be maintained for a maximum of **24 hours**
|
||||||
|
- After 24 hours, the connection will be automatically terminated
|
||||||
|
- Clients should implement reconnection logic to handle this gracefully
|
||||||
|
|
||||||
### Ping/Pong Mechanism
|
### Ping/Pong Mechanism
|
||||||
|
|
||||||
- **Server Ping Interval**: The server sends a WebSocket Ping frame every 10 seconds
|
- **Server Ping Interval**: The server sends a WebSocket Ping frame every 10 seconds
|
||||||
@@ -45,6 +51,7 @@ Base Endpoint: `wss://perps.standx.com/ws-stream/v1`
|
|||||||
// public channels
|
// public channels
|
||||||
{ channel: "price", symbol: "<symbol>" },
|
{ channel: "price", symbol: "<symbol>" },
|
||||||
{ channel: "depth_book", symbol: "<symbol>" },
|
{ channel: "depth_book", symbol: "<symbol>" },
|
||||||
|
{ channel: "public_trade", symbol: "<symbol>" },
|
||||||
// user-level authenticated channels
|
// user-level authenticated channels
|
||||||
{ channel: "order" },
|
{ channel: "order" },
|
||||||
{ channel: "position" },
|
{ channel: "position" },
|
||||||
@@ -55,6 +62,8 @@ Base Endpoint: `wss://perps.standx.com/ws-stream/v1`
|
|||||||
|
|
||||||
### Subscribe to Depth Book
|
### Subscribe to Depth Book
|
||||||
|
|
||||||
|
**⚠️ Note: The sequence of price levels in the asks and bids arrays is not guaranteed. Please implement local sorting on the client side based on your specific requirements.**
|
||||||
|
|
||||||
- Request:
|
- Request:
|
||||||
- Response:
|
- Response:
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -132,6 +132,8 @@ type PlaceOrderOptions = {
|
|||||||
priceTick: number;
|
priceTick: number;
|
||||||
qtyStep: number;
|
qtyStep: number;
|
||||||
skipDedupe?: boolean;
|
skipDedupe?: boolean;
|
||||||
|
slPrice?: number;
|
||||||
|
tpPrice?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function placeOrder(
|
export async function placeOrder(
|
||||||
@@ -176,9 +178,11 @@ export async function placeOrder(
|
|||||||
timeInForce: reduceOnly ? "GTC" : "GTX",
|
timeInForce: reduceOnly ? "GTC" : "GTX",
|
||||||
reduceOnly: reduceOnly ? true : undefined,
|
reduceOnly: reduceOnly ? true : undefined,
|
||||||
closePosition,
|
closePosition,
|
||||||
|
slPrice: opts?.slPrice,
|
||||||
|
tpPrice: opts?.tpPrice,
|
||||||
});
|
});
|
||||||
pendings[type] = String(order.orderId);
|
pendings[type] = String(order.orderId);
|
||||||
log("order", `挂限价单: ${side} @ ${priceNum} 数量 ${quantity} reduceOnly=${reduceOnly}`);
|
log("order", `挂限价单: ${side} @ ${priceNum} 数量 ${quantity} reduceOnly=${reduceOnly}${opts?.slPrice ? ` sl=${opts.slPrice}` : ""}`);
|
||||||
return order;
|
return order;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
unlockOperating(locks, timers, pendings, type);
|
unlockOperating(locks, timers, pendings, type);
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ export interface ExchangePrecision {
|
|||||||
minQuoteAmount?: number;
|
minQuoteAmount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ConnectionEventType = "disconnected" | "reconnected";
|
||||||
|
export interface ConnectionEventListener {
|
||||||
|
(event: ConnectionEventType, symbol: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ExchangeAdapter {
|
export interface ExchangeAdapter {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
supportsTrailingStops(): boolean;
|
supportsTrailingStops(): boolean;
|
||||||
@@ -61,4 +66,9 @@ export interface ExchangeAdapter {
|
|||||||
cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void>;
|
cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void>;
|
||||||
cancelAllOrders(params: { symbol: string }): Promise<void>;
|
cancelAllOrders(params: { symbol: string }): Promise<void>;
|
||||||
getPrecision?(): Promise<ExchangePrecision | null>;
|
getPrecision?(): Promise<ExchangePrecision | null>;
|
||||||
|
// 连接保护相关方法(可选,仅 StandX 支持)
|
||||||
|
onConnectionEvent?(listener: ConnectionEventListener): void;
|
||||||
|
offConnectionEvent?(listener: ConnectionEventListener): void;
|
||||||
|
queryOpenOrders?(): Promise<AsterOrder[]>;
|
||||||
|
forceCancelAllOrders?(): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ export interface BaseOrderIntent {
|
|||||||
|
|
||||||
export interface LimitOrderIntent extends BaseOrderIntent {
|
export interface LimitOrderIntent extends BaseOrderIntent {
|
||||||
price: number;
|
price: number;
|
||||||
|
// StandX TPSL 参数
|
||||||
|
slPrice?: number; // 止损价格
|
||||||
|
tpPrice?: number; // 止盈价格
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MarketOrderIntent extends BaseOrderIntent {
|
export interface MarketOrderIntent extends BaseOrderIntent {
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import type {
|
|||||||
} from "../adapter";
|
} from "../adapter";
|
||||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
import type { AsterOrder, CreateOrderParams } from "../types";
|
||||||
import { extractMessage } from "../../utils/errors";
|
import { extractMessage } from "../../utils/errors";
|
||||||
import { StandxGateway, type StandxGatewayOptions } from "./gateway";
|
import { StandxGateway, type StandxGatewayOptions, type ConnectionEventListener, type ConnectionEventType } from "./gateway";
|
||||||
|
|
||||||
|
export type { ConnectionEventListener, ConnectionEventType };
|
||||||
|
|
||||||
export interface StandxCredentials {
|
export interface StandxCredentials {
|
||||||
token?: string;
|
token?: string;
|
||||||
@@ -122,6 +124,38 @@ export class StandxExchangeAdapter implements ExchangeAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听连接事件(断连/重连)
|
||||||
|
*/
|
||||||
|
onConnectionEvent(listener: ConnectionEventListener): void {
|
||||||
|
this.gateway.onConnectionEvent(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消连接事件监听
|
||||||
|
*/
|
||||||
|
offConnectionEvent(listener: ConnectionEventListener): void {
|
||||||
|
this.gateway.offConnectionEvent(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前真实的挂单状态(通过 HTTP API)
|
||||||
|
* 用于验证实际挂单情况,防止取消请求丢失
|
||||||
|
*/
|
||||||
|
async queryOpenOrders(): Promise<AsterOrder[]> {
|
||||||
|
await this.ensureInitialized("queryOpenOrders");
|
||||||
|
return this.gateway.queryOpenOrders(this.symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强制取消所有挂单
|
||||||
|
* 会查询当前挂单然后取消,并验证取消成功
|
||||||
|
*/
|
||||||
|
async forceCancelAllOrders(): Promise<boolean> {
|
||||||
|
await this.ensureInitialized("forceCancelAllOrders");
|
||||||
|
return this.gateway.forceCancelAllOrders(this.symbol);
|
||||||
|
}
|
||||||
|
|
||||||
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
|
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
|
||||||
const wrapped = ((...args: any[]) => {
|
const wrapped = ((...args: any[]) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -80,6 +80,9 @@ export interface StandxGatewayOptions {
|
|||||||
logger?: (context: string, error: unknown) => void;
|
logger?: (context: string, error: unknown) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ConnectionEventType = "disconnected" | "reconnected";
|
||||||
|
export type ConnectionEventListener = (event: ConnectionEventType, symbol: string) => void;
|
||||||
|
|
||||||
class StandxRequestSigner {
|
class StandxRequestSigner {
|
||||||
private readonly privateKey: Uint8Array | null;
|
private readonly privateKey: Uint8Array | null;
|
||||||
|
|
||||||
@@ -396,6 +399,7 @@ export class StandxGateway {
|
|||||||
private readonly tickerListeners = new Map<string, Set<TickerListener>>();
|
private readonly tickerListeners = new Map<string, Set<TickerListener>>();
|
||||||
private readonly klineListeners = new Map<string, Set<KlineListener>>();
|
private readonly klineListeners = new Map<string, Set<KlineListener>>();
|
||||||
private readonly fundingListeners = new Map<string, Set<FundingRateListener>>();
|
private readonly fundingListeners = new Map<string, Set<FundingRateListener>>();
|
||||||
|
private readonly connectionListeners = new Set<ConnectionEventListener>();
|
||||||
|
|
||||||
private readonly openOrders = new Map<string, AsterOrder>();
|
private readonly openOrders = new Map<string, AsterOrder>();
|
||||||
private readonly positions = new Map<string, AsterAccountPosition>();
|
private readonly positions = new Map<string, AsterAccountPosition>();
|
||||||
@@ -417,6 +421,12 @@ export class StandxGateway {
|
|||||||
|
|
||||||
private lastPriceBySymbol = new Map<string, number>();
|
private lastPriceBySymbol = new Map<string, number>();
|
||||||
|
|
||||||
|
// 断连保护相关
|
||||||
|
private disconnectCancelRetryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
private disconnectCancelRetryActive = false;
|
||||||
|
private lastKnownOpenOrders: Array<{ orderId: string; clOrdId?: string }> = [];
|
||||||
|
private disconnectedSymbol: string | null = null;
|
||||||
|
|
||||||
constructor(options: StandxGatewayOptions) {
|
constructor(options: StandxGatewayOptions) {
|
||||||
this.token = options.token ?? process.env.STANDX_TOKEN ?? "";
|
this.token = options.token ?? process.env.STANDX_TOKEN ?? "";
|
||||||
if (!this.token) {
|
if (!this.token) {
|
||||||
@@ -511,6 +521,54 @@ export class StandxGateway {
|
|||||||
this.startFundingPolling(key);
|
this.startFundingPolling(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onConnectionEvent(listener: ConnectionEventListener): void {
|
||||||
|
this.connectionListeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
offConnectionEvent(listener: ConnectionEventListener): void {
|
||||||
|
this.connectionListeners.delete(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前真实的挂单状态(通过 HTTP API)
|
||||||
|
* 用于在网络恢复后验证实际挂单情况
|
||||||
|
*/
|
||||||
|
async queryOpenOrders(symbol: string): Promise<AsterOrder[]> {
|
||||||
|
const normalized = normalizeSymbol(symbol);
|
||||||
|
const ordersPayload = await this.requestJson<unknown>("/api/query_open_orders", {
|
||||||
|
method: "GET",
|
||||||
|
params: { symbol: normalized },
|
||||||
|
});
|
||||||
|
const orders = extractOrders(ordersPayload);
|
||||||
|
const result: AsterOrder[] = [];
|
||||||
|
for (const raw of orders) {
|
||||||
|
const order = this.mapOrder(raw);
|
||||||
|
result.push(order);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强制取消所有挂单(用于断连保护)
|
||||||
|
* 会不断重试直到成功或确认没有挂单
|
||||||
|
*/
|
||||||
|
async forceCancelAllOrders(symbol: string): Promise<boolean> {
|
||||||
|
const normalized = normalizeSymbol(symbol);
|
||||||
|
try {
|
||||||
|
const currentOrders = await this.queryOpenOrders(normalized);
|
||||||
|
if (currentOrders.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
await this.cancelAllOrders({ symbol: normalized });
|
||||||
|
// 再次查询确认
|
||||||
|
const afterCancel = await this.queryOpenOrders(normalized);
|
||||||
|
return afterCancel.length === 0;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger("forceCancelAllOrders", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
||||||
const normalizedSymbol = normalizeSymbol(params.symbol);
|
const normalizedSymbol = normalizeSymbol(params.symbol);
|
||||||
if (params.type === "STOP_MARKET") {
|
if (params.type === "STOP_MARKET") {
|
||||||
@@ -741,6 +799,13 @@ export class StandxGateway {
|
|||||||
}
|
}
|
||||||
payload.price = price;
|
payload.price = price;
|
||||||
}
|
}
|
||||||
|
// StandX TPSL 参数
|
||||||
|
if (params.slPrice != null && Number.isFinite(params.slPrice)) {
|
||||||
|
payload.sl_price = toDecimalString(params.slPrice);
|
||||||
|
}
|
||||||
|
if (params.tpPrice != null && Number.isFinite(params.tpPrice)) {
|
||||||
|
payload.tp_price = toDecimalString(params.tpPrice);
|
||||||
|
}
|
||||||
const response = await this.requestJson<{ code?: number; message?: string; request_id?: string }>(
|
const response = await this.requestJson<{ code?: number; message?: string; request_id?: string }>(
|
||||||
"/api/new_order",
|
"/api/new_order",
|
||||||
{
|
{
|
||||||
@@ -791,15 +856,26 @@ export class StandxGateway {
|
|||||||
this.sendAuthIfNeeded();
|
this.sendAuthIfNeeded();
|
||||||
};
|
};
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
|
const wasReady = this.marketWsReady;
|
||||||
this.marketWsReady = false;
|
this.marketWsReady = false;
|
||||||
this.marketWsAuthed = false;
|
this.marketWsAuthed = false;
|
||||||
this.marketWsAuthRequested = false;
|
this.marketWsAuthRequested = false;
|
||||||
this.marketWs = null;
|
this.marketWs = null;
|
||||||
this.logDebug("ws close");
|
this.logDebug("ws close");
|
||||||
|
// 触发断连事件,启动断连保护
|
||||||
|
if (wasReady) {
|
||||||
|
this.onDisconnect();
|
||||||
|
}
|
||||||
this.scheduleReconnect();
|
this.scheduleReconnect();
|
||||||
};
|
};
|
||||||
const handleError = (error: unknown) => {
|
const handleError = (error: unknown) => {
|
||||||
this.logger("marketWs", error);
|
this.logger("marketWs", error);
|
||||||
|
// 如果连接从未成功建立(握手失败),需要清理并重连
|
||||||
|
// 因为某些 WebSocket 实现在握手失败时可能不触发 close 事件
|
||||||
|
if (this.marketWs && !this.marketWsReady) {
|
||||||
|
this.marketWs = null;
|
||||||
|
this.scheduleReconnect();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if ("addEventListener" in this.marketWs && typeof this.marketWs.addEventListener === "function") {
|
if ("addEventListener" in this.marketWs && typeof this.marketWs.addEventListener === "function") {
|
||||||
@@ -823,8 +899,10 @@ export class StandxGateway {
|
|||||||
|
|
||||||
private scheduleReconnect(): void {
|
private scheduleReconnect(): void {
|
||||||
if (this.marketReconnectTimer) return;
|
if (this.marketReconnectTimer) return;
|
||||||
|
this.logDebug("scheduling reconnect in 2s");
|
||||||
this.marketReconnectTimer = setTimeout(() => {
|
this.marketReconnectTimer = setTimeout(() => {
|
||||||
this.marketReconnectTimer = null;
|
this.marketReconnectTimer = null;
|
||||||
|
this.logDebug("attempting reconnect");
|
||||||
this.connectMarketWs();
|
this.connectMarketWs();
|
||||||
}, WS_RECONNECT_DELAY);
|
}, WS_RECONNECT_DELAY);
|
||||||
}
|
}
|
||||||
@@ -854,6 +932,8 @@ export class StandxGateway {
|
|||||||
this.marketWsAuthed = true;
|
this.marketWsAuthed = true;
|
||||||
this.marketWsAuthRequested = false;
|
this.marketWsAuthRequested = false;
|
||||||
this.flushSubscriptions();
|
this.flushSubscriptions();
|
||||||
|
// 触发重连事件
|
||||||
|
this.onReconnect();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1117,9 +1197,8 @@ export class StandxGateway {
|
|||||||
const order = this.mapOrder(raw);
|
const order = this.mapOrder(raw);
|
||||||
mergeOrderSnapshot(this.openOrders, order);
|
mergeOrderSnapshot(this.openOrders, order);
|
||||||
}
|
}
|
||||||
if (orders.length) {
|
// 无论是否有挂单都触发推送,确保上层状态更新
|
||||||
this.emitOrders();
|
this.emitOrders();
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger("openOrders", error);
|
this.logger("openOrders", error);
|
||||||
}
|
}
|
||||||
@@ -1393,4 +1472,140 @@ export class StandxGateway {
|
|||||||
return text as unknown as T;
|
return text as unknown as T;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 断连时触发,记录当前挂单状态并启动持久重试取消
|
||||||
|
*/
|
||||||
|
private onDisconnect(): void {
|
||||||
|
// 记录最后已知的挂单状态
|
||||||
|
this.lastKnownOpenOrders = Array.from(this.openOrders.values()).map((order) => ({
|
||||||
|
orderId: String(order.orderId),
|
||||||
|
clOrdId: order.clientOrderId,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 获取当前订阅的 symbol
|
||||||
|
const symbols = new Set<string>();
|
||||||
|
for (const key of this.subscriptions) {
|
||||||
|
const [, symbol] = key.split(":");
|
||||||
|
if (symbol) symbols.add(symbol);
|
||||||
|
}
|
||||||
|
this.disconnectedSymbol = symbols.size > 0 ? Array.from(symbols)[0] ?? null : null;
|
||||||
|
|
||||||
|
this.logDebug("disconnect protection", {
|
||||||
|
openOrderCount: this.lastKnownOpenOrders.length,
|
||||||
|
symbol: this.disconnectedSymbol,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 触发断连事件
|
||||||
|
for (const listener of this.connectionListeners) {
|
||||||
|
try {
|
||||||
|
listener("disconnected", this.disconnectedSymbol ?? "");
|
||||||
|
} catch (error) {
|
||||||
|
this.logger("connectionListener", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动断连保护:持续重试取消所有挂单
|
||||||
|
if (this.lastKnownOpenOrders.length > 0 && this.disconnectedSymbol) {
|
||||||
|
this.startDisconnectCancelRetry(this.disconnectedSymbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重连成功时触发,停止断连保护并通知监听器
|
||||||
|
*/
|
||||||
|
private onReconnect(): void {
|
||||||
|
this.logDebug("reconnect protection", {
|
||||||
|
wasRetrying: this.disconnectCancelRetryActive,
|
||||||
|
symbol: this.disconnectedSymbol,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 停止断连保护重试
|
||||||
|
this.stopDisconnectCancelRetry();
|
||||||
|
|
||||||
|
// 清空本地挂单状态(重连后需要重新同步)
|
||||||
|
this.openOrders.clear();
|
||||||
|
|
||||||
|
// 主动触发一次数据推送,确保上层 feedStatus 能更新
|
||||||
|
this.emitOrders();
|
||||||
|
this.emitAccountSnapshot();
|
||||||
|
|
||||||
|
// 主动刷新账户和挂单数据
|
||||||
|
void this.refreshAccountSnapshot();
|
||||||
|
|
||||||
|
// 获取当前订阅的 symbols 并刷新数据
|
||||||
|
const subscribedSymbols = new Set<string>();
|
||||||
|
for (const key of this.subscriptions) {
|
||||||
|
const [, symbol] = key.split(":");
|
||||||
|
if (symbol) subscribedSymbols.add(symbol);
|
||||||
|
}
|
||||||
|
if (this.disconnectedSymbol) {
|
||||||
|
subscribedSymbols.add(this.disconnectedSymbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新每个 symbol 的数据
|
||||||
|
for (const symbol of subscribedSymbols) {
|
||||||
|
void this.refreshOpenOrders(symbol);
|
||||||
|
void this.fetchDepthSnapshot(symbol).catch((e) => this.logger("depthSnapshot", e));
|
||||||
|
void this.fetchTickerSnapshot(symbol).catch((e) => this.logger("tickerSnapshot", e));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发重连事件
|
||||||
|
for (const listener of this.connectionListeners) {
|
||||||
|
try {
|
||||||
|
listener("reconnected", this.disconnectedSymbol ?? "");
|
||||||
|
} catch (error) {
|
||||||
|
this.logger("connectionListener", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.disconnectedSymbol = null;
|
||||||
|
this.lastKnownOpenOrders = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动断连保护:持续重试取消所有挂单
|
||||||
|
* 即使网络不通也不停止重试
|
||||||
|
*/
|
||||||
|
private startDisconnectCancelRetry(symbol: string): void {
|
||||||
|
if (this.disconnectCancelRetryActive) return;
|
||||||
|
this.disconnectCancelRetryActive = true;
|
||||||
|
|
||||||
|
const retryCancel = async () => {
|
||||||
|
if (!this.disconnectCancelRetryActive) return;
|
||||||
|
|
||||||
|
this.logDebug("disconnect cancel retry attempt", { symbol });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const success = await this.forceCancelAllOrders(symbol);
|
||||||
|
if (success) {
|
||||||
|
this.logDebug("disconnect cancel retry success");
|
||||||
|
this.stopDisconnectCancelRetry();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger("disconnectCancelRetry", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果仍在重试状态,继续下一次重试
|
||||||
|
if (this.disconnectCancelRetryActive) {
|
||||||
|
this.disconnectCancelRetryTimer = setTimeout(() => {
|
||||||
|
void retryCancel();
|
||||||
|
}, 2000); // 每 2 秒重试一次
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void retryCancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止断连保护重试
|
||||||
|
*/
|
||||||
|
private stopDisconnectCancelRetry(): void {
|
||||||
|
this.disconnectCancelRetryActive = false;
|
||||||
|
if (this.disconnectCancelRetryTimer) {
|
||||||
|
clearTimeout(this.disconnectCancelRetryTimer);
|
||||||
|
this.disconnectCancelRetryTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterO
|
|||||||
quantity: intent.quantity,
|
quantity: intent.quantity,
|
||||||
price: intent.price,
|
price: intent.price,
|
||||||
timeInForce: intent.timeInForce ?? "GTX",
|
timeInForce: intent.timeInForce ?? "GTX",
|
||||||
|
slPrice: intent.slPrice,
|
||||||
|
tpPrice: intent.tpPrice,
|
||||||
},
|
},
|
||||||
intent
|
intent
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ export interface CreateOrderParams {
|
|||||||
reduceOnly?: StringBoolean;
|
reduceOnly?: StringBoolean;
|
||||||
closePosition?: StringBoolean;
|
closePosition?: StringBoolean;
|
||||||
triggerType?: "UNSPECIFIED" | "TAKE_PROFIT" | "STOP_LOSS";
|
triggerType?: "UNSPECIFIED" | "TAKE_PROFIT" | "STOP_LOSS";
|
||||||
|
// StandX TPSL 参数
|
||||||
|
slPrice?: number; // 止损价格
|
||||||
|
tpPrice?: number; // 止盈价格
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AsterAccountPosition {
|
export interface AsterAccountPosition {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { MakerPointsConfig } from "../config";
|
import type { MakerPointsConfig } from "../config";
|
||||||
import type { ExchangeAdapter } from "../exchanges/adapter";
|
import type { ExchangeAdapter, ConnectionEventType } from "../exchanges/adapter";
|
||||||
import type {
|
import type {
|
||||||
AsterAccountSnapshot,
|
AsterAccountSnapshot,
|
||||||
AsterDepth,
|
AsterDepth,
|
||||||
@@ -149,6 +149,12 @@ export class MakerPointsEngine {
|
|||||||
private lastPositionAmt = 0;
|
private lastPositionAmt = 0;
|
||||||
private lastPositionSide: "LONG" | "SHORT" | "FLAT" = "FLAT";
|
private lastPositionSide: "LONG" | "SHORT" | "FLAT" = "FLAT";
|
||||||
|
|
||||||
|
// 连接保护相关状态
|
||||||
|
private connectionState: "connected" | "disconnected" = "connected";
|
||||||
|
private reconnectResetPending = false;
|
||||||
|
private lastRepriceQueryTime = 0;
|
||||||
|
private readonly repriceQueryIntervalMs = 3000; // 最小查询间隔
|
||||||
|
|
||||||
constructor(private readonly config: MakerPointsConfig, private readonly exchange: ExchangeAdapter) {
|
constructor(private readonly config: MakerPointsConfig, private readonly exchange: ExchangeAdapter) {
|
||||||
this.tradeLog = createTradeLog(this.config.maxLogEntries);
|
this.tradeLog = createTradeLog(this.config.maxLogEntries);
|
||||||
this.rateLimit = new RateLimitController(this.config.refreshIntervalMs, (type, detail) =>
|
this.rateLimit = new RateLimitController(this.config.refreshIntervalMs, (type, detail) =>
|
||||||
@@ -290,6 +296,101 @@ export class MakerPointsEngine {
|
|||||||
processFail: (error) => t("log.process.tickerError", { error: String(error) }),
|
processFail: (error) => t("log.process.tickerError", { error: String(error) }),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 注册连接事件监听(如果交易所支持)
|
||||||
|
this.setupConnectionProtection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置连接保护机制
|
||||||
|
* 监听断连/重连事件,实现保护逻辑
|
||||||
|
*/
|
||||||
|
private setupConnectionProtection(): void {
|
||||||
|
if (!this.exchange.onConnectionEvent) return;
|
||||||
|
|
||||||
|
this.exchange.onConnectionEvent((event, symbol) => {
|
||||||
|
if (event === "disconnected") {
|
||||||
|
this.handleDisconnect(symbol);
|
||||||
|
} else if (event === "reconnected") {
|
||||||
|
this.handleReconnect(symbol);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理断连事件
|
||||||
|
*/
|
||||||
|
private handleDisconnect(symbol: string): void {
|
||||||
|
this.connectionState = "disconnected";
|
||||||
|
this.tradeLog.push("warn", `WebSocket 断连 (${symbol}),启动断连保护`);
|
||||||
|
this.notify({
|
||||||
|
type: "token_expired",
|
||||||
|
level: "warn",
|
||||||
|
symbol: this.config.symbol,
|
||||||
|
title: "连接断开",
|
||||||
|
message: "WebSocket 断连,正在尝试取消所有挂单",
|
||||||
|
details: { symbol },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理重连事件
|
||||||
|
* 重连后需要重新查询挂单并取消所有挂单
|
||||||
|
*/
|
||||||
|
private async handleReconnect(symbol: string): Promise<void> {
|
||||||
|
this.connectionState = "connected";
|
||||||
|
this.reconnectResetPending = true;
|
||||||
|
this.tradeLog.push("info", `WebSocket 重连成功 (${symbol}),开始重连保护流程`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 查询真实挂单状态
|
||||||
|
if (this.exchange.queryOpenOrders) {
|
||||||
|
const realOrders = await this.exchange.queryOpenOrders();
|
||||||
|
this.tradeLog.push("info", `重连后查询到 ${realOrders.length} 个挂单`);
|
||||||
|
|
||||||
|
if (realOrders.length > 0) {
|
||||||
|
// 取消所有挂单
|
||||||
|
if (this.exchange.forceCancelAllOrders) {
|
||||||
|
const success = await this.exchange.forceCancelAllOrders();
|
||||||
|
if (success) {
|
||||||
|
this.tradeLog.push("order", "重连保护:已取消所有挂单");
|
||||||
|
} else {
|
||||||
|
this.tradeLog.push("warn", "重连保护:取消挂单未完全成功,将在下次循环重试");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await this.exchange.cancelAllOrders({ symbol: this.config.symbol });
|
||||||
|
this.tradeLog.push("order", "重连保护:已取消所有挂单");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置本地挂单状态
|
||||||
|
this.openOrders = [];
|
||||||
|
this.pendingCancelOrders.clear();
|
||||||
|
unlockOperating(this.locks, this.timers, this.pending, "LIMIT");
|
||||||
|
|
||||||
|
// 重置 reprice 基准,强制下一次重新计算
|
||||||
|
this.lastQuoteBid1 = null;
|
||||||
|
this.lastQuoteAsk1 = null;
|
||||||
|
this.desiredOrders = [];
|
||||||
|
this.lastDesiredSummary = null;
|
||||||
|
|
||||||
|
// 标记启动重置需要重新执行
|
||||||
|
this.initialOrderResetDone = false;
|
||||||
|
|
||||||
|
this.notify({
|
||||||
|
type: "position_opened",
|
||||||
|
level: "info",
|
||||||
|
symbol: this.config.symbol,
|
||||||
|
title: "重连完成",
|
||||||
|
message: "WebSocket 重连成功,已清理挂单状态",
|
||||||
|
details: { symbol },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.tradeLog.push("error", `重连保护流程失败: ${extractMessage(error)}`);
|
||||||
|
} finally {
|
||||||
|
this.reconnectResetPending = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private syncLocksWithOrders(orders: AsterOrder[] | null | undefined): void {
|
private syncLocksWithOrders(orders: AsterOrder[] | null | undefined): void {
|
||||||
@@ -553,7 +654,14 @@ export class MakerPointsEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncOrders(targets: DesiredOrder[], closeOnly: boolean): Promise<void> {
|
private async syncOrders(targets: DesiredOrder[], _closeOnly: boolean): Promise<void> {
|
||||||
|
// 价格变化保护:如果需要 reprice 且距上次查询已过足够时间,先查询真实挂单
|
||||||
|
const shouldVerifyOrders = await this.verifyOrdersIfNeeded();
|
||||||
|
if (shouldVerifyOrders) {
|
||||||
|
// 如果发现有未预期的挂单,先取消所有挂单
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const availableOrders = this.openOrders.filter((o) => !this.pendingCancelOrders.has(String(o.orderId)));
|
const availableOrders = this.openOrders.filter((o) => !this.pendingCancelOrders.has(String(o.orderId)));
|
||||||
const openOrders = availableOrders.filter((order) => isOrderActiveStatus(order.status));
|
const openOrders = availableOrders.filter((order) => isOrderActiveStatus(order.status));
|
||||||
const { toCancel, toPlace } = makeOrderPlan(openOrders, targets);
|
const { toCancel, toPlace } = makeOrderPlan(openOrders, targets);
|
||||||
@@ -593,6 +701,9 @@ export class MakerPointsEngine {
|
|||||||
if (!target) continue;
|
if (!target) continue;
|
||||||
if (target.amount < EPS) continue;
|
if (target.amount < EPS) continue;
|
||||||
try {
|
try {
|
||||||
|
// 计算止损价格:买单在价格下方 $1,卖单在价格上方 $1
|
||||||
|
const priceNum = Number(target.price);
|
||||||
|
const slPrice = target.side === "BUY" ? priceNum - 1 : priceNum + 1;
|
||||||
await placeOrder(
|
await placeOrder(
|
||||||
this.exchange,
|
this.exchange,
|
||||||
this.config.symbol,
|
this.config.symbol,
|
||||||
@@ -610,6 +721,7 @@ export class MakerPointsEngine {
|
|||||||
priceTick: this.priceTick,
|
priceTick: this.priceTick,
|
||||||
qtyStep: this.qtyStep,
|
qtyStep: this.qtyStep,
|
||||||
skipDedupe: true,
|
skipDedupe: true,
|
||||||
|
slPrice,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -629,6 +741,75 @@ export class MakerPointsEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证真实挂单状态,防止取消请求丢失
|
||||||
|
* 在每次 reprice 时查询真实挂单,发现未预期的挂单时取消所有挂单
|
||||||
|
* @returns true 表示发现问题并执行了取消操作,调用方应跳过本轮挂单
|
||||||
|
*/
|
||||||
|
private async verifyOrdersIfNeeded(): Promise<boolean> {
|
||||||
|
// 如果交易所不支持查询挂单,跳过验证
|
||||||
|
if (!this.exchange.queryOpenOrders) return false;
|
||||||
|
|
||||||
|
// 限制查询频率
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - this.lastRepriceQueryTime < this.repriceQueryIntervalMs) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const realOrders = await this.exchange.queryOpenOrders();
|
||||||
|
this.lastRepriceQueryTime = now;
|
||||||
|
|
||||||
|
// 比较真实挂单与本地记录
|
||||||
|
const realOrderIds = new Set(realOrders.map((o) => String(o.orderId)));
|
||||||
|
const localOrderIds = new Set(this.openOrders.map((o) => String(o.orderId)));
|
||||||
|
|
||||||
|
// 查找本地以为已取消但实际还存在的订单
|
||||||
|
const unexpectedOrders = realOrders.filter((order) => {
|
||||||
|
const orderId = String(order.orderId);
|
||||||
|
// 如果本地没有这个订单,说明我们以为它已经被取消了
|
||||||
|
if (!localOrderIds.has(orderId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 如果本地记录这个订单在等待取消,但实际还存在
|
||||||
|
if (this.pendingCancelOrders.has(orderId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (unexpectedOrders.length > 0) {
|
||||||
|
this.tradeLog.push(
|
||||||
|
"warn",
|
||||||
|
`发现 ${unexpectedOrders.length} 个未预期挂单,执行强制取消`
|
||||||
|
);
|
||||||
|
|
||||||
|
// 强制取消所有挂单
|
||||||
|
if (this.exchange.forceCancelAllOrders) {
|
||||||
|
await this.exchange.forceCancelAllOrders();
|
||||||
|
} else {
|
||||||
|
await this.exchange.cancelAllOrders({ symbol: this.config.symbol });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置本地状态
|
||||||
|
this.openOrders = [];
|
||||||
|
this.pendingCancelOrders.clear();
|
||||||
|
this.tradeLog.push("order", "已强制取消所有挂单,重置本地状态");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新本地挂单状态以匹配真实状态
|
||||||
|
if (realOrders.length !== this.openOrders.length) {
|
||||||
|
// 移除本地记录中不存在于服务器的订单
|
||||||
|
this.openOrders = this.openOrders.filter((o) => realOrderIds.has(String(o.orderId)));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.tradeLog.push("error", `验证挂单状态失败: ${extractMessage(error)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private async checkStopLoss(): Promise<void> {
|
private async checkStopLoss(): Promise<void> {
|
||||||
if (this.stopLossProcessing) return;
|
if (this.stopLossProcessing) return;
|
||||||
const lossLimit = Number(this.config.stopLossUsd);
|
const lossLimit = Number(this.config.stopLossUsd);
|
||||||
|
|||||||
Reference in New Issue
Block a user