mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-09 08:18:07 +00:00
Add token expiry and Telegram notification features
- Introduced `STANDX_TOKEN_EXPIRY` configuration to manage token expiration, including handling logic for active, expired, and silent states. - Implemented Telegram notifications for key events such as order filled, position opened/closed, stop loss triggered, and token expiration. - Updated Maker Points engine to integrate token expiry checks and notification sending, enhancing user awareness of trading conditions. - Enhanced documentation to include details on configuring token expiry and Telegram notifications for improved user guidance.
This commit is contained in:
@@ -6,6 +6,45 @@
|
||||
import { resolveExchangeId, type SupportedExchangeId } from "./exchanges/create-adapter";
|
||||
import { language, type Language } from "./i18n";
|
||||
|
||||
export interface StandxTokenConfig {
|
||||
expiryTimestamp: number | null;
|
||||
}
|
||||
|
||||
function parseTimestamp(value: string | undefined): number | null {
|
||||
if (!value || !value.trim()) return null;
|
||||
const trimmed = value.trim();
|
||||
const asNumber = Number(trimmed);
|
||||
if (Number.isFinite(asNumber) && asNumber > 0) {
|
||||
return asNumber < 1e12 ? asNumber * 1000 : asNumber;
|
||||
}
|
||||
const asDate = Date.parse(trimmed);
|
||||
if (Number.isFinite(asDate) && asDate > 0) {
|
||||
return asDate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const standxTokenConfig: StandxTokenConfig = {
|
||||
expiryTimestamp: parseTimestamp(process.env.STANDX_TOKEN_EXPIRY),
|
||||
};
|
||||
|
||||
export function isStandxTokenExpired(): boolean {
|
||||
const expiry = standxTokenConfig.expiryTimestamp;
|
||||
if (expiry == null) return false;
|
||||
return Date.now() >= expiry;
|
||||
}
|
||||
|
||||
export function getStandxTokenExpiryInfo(): { expired: boolean; expiryTimestamp: number | null; remainingMs: number | null } {
|
||||
const expiry = standxTokenConfig.expiryTimestamp;
|
||||
if (expiry == null) {
|
||||
return { expired: false, expiryTimestamp: null, remainingMs: null };
|
||||
}
|
||||
const now = Date.now();
|
||||
const expired = now >= expiry;
|
||||
const remainingMs = expired ? 0 : expiry - now;
|
||||
return { expired, expiryTimestamp: expiry, remainingMs };
|
||||
}
|
||||
|
||||
export interface TradingConfig {
|
||||
symbol: string;
|
||||
tradeAmount: number;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export type {
|
||||
NotificationLevel,
|
||||
TradeNotification,
|
||||
NotificationSender,
|
||||
NotificationConfig,
|
||||
} from "./types";
|
||||
|
||||
export {
|
||||
TelegramNotifier,
|
||||
createTelegramNotifier,
|
||||
type TelegramConfig,
|
||||
} from "./telegram";
|
||||
@@ -0,0 +1,113 @@
|
||||
import type { NotificationSender, TradeNotification, NotificationConfig } from "./types";
|
||||
|
||||
export interface TelegramConfig extends NotificationConfig {
|
||||
botToken: string;
|
||||
chatId: string;
|
||||
}
|
||||
|
||||
const LEVEL_EMOJI: Record<string, string> = {
|
||||
info: "ℹ️",
|
||||
warn: "⚠️",
|
||||
error: "🚨",
|
||||
success: "✅",
|
||||
};
|
||||
|
||||
const TYPE_EMOJI: Record<string, string> = {
|
||||
order_filled: "📝",
|
||||
position_opened: "📈",
|
||||
position_closed: "📉",
|
||||
stop_loss: "🛑",
|
||||
token_expired: "⏰",
|
||||
custom: "📢",
|
||||
};
|
||||
|
||||
function formatNotificationMessage(notification: TradeNotification, accountLabel?: string): string {
|
||||
const levelEmoji = LEVEL_EMOJI[notification.level] ?? "";
|
||||
const typeEmoji = TYPE_EMOJI[notification.type] ?? "";
|
||||
const timestamp = notification.timestamp ?? Date.now();
|
||||
const time = new Date(timestamp).toISOString().replace("T", " ").substring(0, 19);
|
||||
|
||||
const label = notification.accountLabel ?? accountLabel ?? notification.symbol;
|
||||
|
||||
const lines: string[] = [
|
||||
`${typeEmoji}${levelEmoji} [${label}] ${notification.title}`,
|
||||
``,
|
||||
`${notification.message}`,
|
||||
];
|
||||
|
||||
if (notification.details && Object.keys(notification.details).length > 0) {
|
||||
lines.push(``);
|
||||
for (const [key, value] of Object.entries(notification.details)) {
|
||||
if (value != null) {
|
||||
lines.push(`• ${key}: ${value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lines.push(``);
|
||||
lines.push(`🕐 ${time} UTC`);
|
||||
lines.push(`📊 ${notification.symbol}`);
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export class TelegramNotifier implements NotificationSender {
|
||||
private readonly config: TelegramConfig;
|
||||
private readonly baseUrl: string;
|
||||
private sendQueue: Promise<void> = Promise.resolve();
|
||||
|
||||
constructor(config: Partial<TelegramConfig> = {}) {
|
||||
this.config = {
|
||||
enabled: Boolean(config.botToken && config.chatId),
|
||||
botToken: config.botToken ?? "",
|
||||
chatId: config.chatId ?? "",
|
||||
accountLabel: config.accountLabel,
|
||||
};
|
||||
this.baseUrl = `https://api.telegram.org/bot${this.config.botToken}`;
|
||||
}
|
||||
|
||||
isEnabled(): boolean {
|
||||
return this.config.enabled;
|
||||
}
|
||||
|
||||
async send(notification: TradeNotification): Promise<void> {
|
||||
if (!this.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendQueue = this.sendQueue
|
||||
.then(() => this.doSend(notification))
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
private async doSend(notification: TradeNotification): Promise<void> {
|
||||
const text = formatNotificationMessage(notification, this.config.accountLabel);
|
||||
const url = `${this.baseUrl}/sendMessage`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
chat_id: this.config.chatId,
|
||||
text,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => "unknown error");
|
||||
console.error(`[Telegram] Failed to send notification: ${response.status} ${errorText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[Telegram] Failed to send notification: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createTelegramNotifier(): TelegramNotifier {
|
||||
return new TelegramNotifier({
|
||||
botToken: process.env.TELEGRAM_BOT_TOKEN,
|
||||
chatId: process.env.TELEGRAM_CHAT_ID,
|
||||
accountLabel: process.env.TELEGRAM_ACCOUNT_LABEL,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export type NotificationLevel = "info" | "warn" | "error" | "success";
|
||||
|
||||
export interface TradeNotification {
|
||||
type: "order_filled" | "position_opened" | "position_closed" | "stop_loss" | "token_expired" | "custom";
|
||||
level: NotificationLevel;
|
||||
symbol: string;
|
||||
title: string;
|
||||
message: string;
|
||||
accountLabel?: string;
|
||||
details?: Record<string, string | number | boolean | null>;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
export interface NotificationSender {
|
||||
send(notification: TradeNotification): Promise<void>;
|
||||
isEnabled(): boolean;
|
||||
}
|
||||
|
||||
export interface NotificationConfig {
|
||||
enabled: boolean;
|
||||
accountLabel?: string;
|
||||
}
|
||||
@@ -29,6 +29,17 @@ import { SessionVolumeTracker } from "./common/session-volume";
|
||||
import { BinanceDepthTracker, type BinanceDepthSnapshot } from "./common/binance-depth";
|
||||
import { buildBpsTargets } from "./maker-points-logic";
|
||||
import { t } from "../i18n";
|
||||
import {
|
||||
checkStandxTokenExpiry,
|
||||
formatTokenExpiryMessage,
|
||||
isTokenExpiryConfigured,
|
||||
type TokenExpiryState,
|
||||
} from "../utils/standx-token-expiry";
|
||||
import {
|
||||
createTelegramNotifier,
|
||||
type NotificationSender,
|
||||
type TradeNotification,
|
||||
} from "../notifications";
|
||||
|
||||
interface DesiredOrder {
|
||||
side: "BUY" | "SELL";
|
||||
@@ -90,6 +101,7 @@ export class MakerPointsEngine {
|
||||
private readonly sessionVolume = new SessionVolumeTracker();
|
||||
private readonly rateLimit: RateLimitController;
|
||||
private readonly binanceDepth: BinanceDepthTracker;
|
||||
private readonly notifier: NotificationSender;
|
||||
|
||||
private priceTick: number = 0.1;
|
||||
private qtyStep: number = 0.001;
|
||||
@@ -128,11 +140,21 @@ export class MakerPointsEngine {
|
||||
private insufficientBalanceNotified = false;
|
||||
private lastInsufficientMessage: string | null = null;
|
||||
|
||||
private tokenExpiryState: TokenExpiryState = "active";
|
||||
private tokenExpiryLogged = false;
|
||||
private tokenExpiryCancelDone = false;
|
||||
private tokenExpiredCloseOnlyMode = false;
|
||||
private tokenExpiryNotified = false;
|
||||
|
||||
private lastPositionAmt = 0;
|
||||
private lastPositionSide: "LONG" | "SHORT" | "FLAT" = "FLAT";
|
||||
|
||||
constructor(private readonly config: MakerPointsConfig, private readonly exchange: ExchangeAdapter) {
|
||||
this.tradeLog = createTradeLog(this.config.maxLogEntries);
|
||||
this.rateLimit = new RateLimitController(this.config.refreshIntervalMs, (type, detail) =>
|
||||
this.tradeLog.push(type, detail)
|
||||
);
|
||||
this.notifier = createTelegramNotifier();
|
||||
this.priceTick = Math.max(1e-9, this.config.priceTick);
|
||||
this.qtyStep = Math.max(1e-9, this.config.qtyStep);
|
||||
this.binanceDepth = new BinanceDepthTracker(resolveBinanceSymbol(this.config.symbol), {
|
||||
@@ -201,6 +223,7 @@ export class MakerPointsEngine {
|
||||
}
|
||||
const position = getPosition(snapshot, this.config.symbol);
|
||||
this.sessionVolume.update(position, this.getReferencePrice());
|
||||
this.detectPositionChange(position);
|
||||
this.feedStatus.account = true;
|
||||
this.emitUpdate();
|
||||
},
|
||||
@@ -314,6 +337,14 @@ export class MakerPointsEngine {
|
||||
return;
|
||||
}
|
||||
|
||||
const position = getPosition(this.accountSnapshot, this.config.symbol);
|
||||
const absPosition = Math.abs(position.positionAmt);
|
||||
|
||||
if (await this.handleTokenExpiry(position, absPosition)) {
|
||||
this.emitUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
const depth = this.depthSnapshot!;
|
||||
const { topBid, topAsk } = getTopPrices(depth);
|
||||
if (topBid == null || topAsk == null) {
|
||||
@@ -321,13 +352,12 @@ export class MakerPointsEngine {
|
||||
return;
|
||||
}
|
||||
|
||||
const position = getPosition(this.accountSnapshot, this.config.symbol);
|
||||
const absPosition = Math.abs(position.positionAmt);
|
||||
const closeThreshold = Number(this.config.closeThreshold);
|
||||
const closeOnly =
|
||||
Number.isFinite(closeThreshold) &&
|
||||
this.tokenExpiredCloseOnlyMode ||
|
||||
(Number.isFinite(closeThreshold) &&
|
||||
closeThreshold > 0 &&
|
||||
absPosition >= closeThreshold - EPS;
|
||||
absPosition >= closeThreshold - EPS);
|
||||
const prevCloseOnly = this.lastCloseOnly;
|
||||
if (closeOnly !== prevCloseOnly) {
|
||||
this.tradeLog.push("info", closeOnly ? "进入平仓模式,仅挂 reduce-only" : "退出平仓模式");
|
||||
@@ -615,6 +645,19 @@ export class MakerPointsEngine {
|
||||
"stop",
|
||||
`触发止损: 未实现亏损 ${position.unrealizedProfit.toFixed(4)} USDT`
|
||||
);
|
||||
this.notifier.send({
|
||||
type: "stop_loss",
|
||||
level: "error",
|
||||
symbol: this.config.symbol,
|
||||
title: "止损触发",
|
||||
message: `未实现亏损 ${position.unrealizedProfit.toFixed(4)} USDT,强制平仓`,
|
||||
details: {
|
||||
side: position.positionAmt > 0 ? "LONG" : "SHORT",
|
||||
size: absPosition,
|
||||
unrealizedPnl: position.unrealizedProfit,
|
||||
lossLimit: -lossLimit,
|
||||
},
|
||||
});
|
||||
try {
|
||||
await this.flushOrders();
|
||||
await marketClose(
|
||||
@@ -828,6 +871,180 @@ export class MakerPointsEngine {
|
||||
}
|
||||
return active;
|
||||
}
|
||||
|
||||
private detectPositionChange(position: PositionSnapshot): void {
|
||||
const currentAmt = position.positionAmt;
|
||||
const currentSide: "LONG" | "SHORT" | "FLAT" =
|
||||
currentAmt > EPS ? "LONG" : currentAmt < -EPS ? "SHORT" : "FLAT";
|
||||
const prevAmt = this.lastPositionAmt;
|
||||
const prevSide = this.lastPositionSide;
|
||||
|
||||
if (Math.abs(currentAmt - prevAmt) < EPS && currentSide === prevSide) {
|
||||
return;
|
||||
}
|
||||
|
||||
const absChange = Math.abs(currentAmt - prevAmt);
|
||||
const reference = this.getReferencePrice() ?? 0;
|
||||
|
||||
if (prevSide === "FLAT" && currentSide !== "FLAT") {
|
||||
this.notifier.send({
|
||||
type: "position_opened",
|
||||
level: "info",
|
||||
symbol: this.config.symbol,
|
||||
title: "开仓",
|
||||
message: `${currentSide === "LONG" ? "做多" : "做空"} ${Math.abs(currentAmt).toFixed(6)}`,
|
||||
details: {
|
||||
side: currentSide,
|
||||
size: Math.abs(currentAmt),
|
||||
price: reference > 0 ? reference : null,
|
||||
},
|
||||
});
|
||||
} else if (currentSide === "FLAT" && prevSide !== "FLAT") {
|
||||
const pnl = position.unrealizedProfit;
|
||||
const closeType = this.tokenExpiredCloseOnlyMode ? "Token过期平仓" : "平仓";
|
||||
this.notifier.send({
|
||||
type: "position_closed",
|
||||
level: "success",
|
||||
symbol: this.config.symbol,
|
||||
title: closeType,
|
||||
message: `已平仓 ${Math.abs(prevAmt).toFixed(6)} (${prevSide === "LONG" ? "多" : "空"})`,
|
||||
details: {
|
||||
prevSide,
|
||||
closedSize: Math.abs(prevAmt),
|
||||
pnl: Number.isFinite(pnl) ? pnl : null,
|
||||
},
|
||||
});
|
||||
} else if (currentSide === prevSide && absChange > EPS) {
|
||||
const isIncrease = Math.abs(currentAmt) > Math.abs(prevAmt);
|
||||
if (isIncrease) {
|
||||
this.notifier.send({
|
||||
type: "order_filled",
|
||||
level: "info",
|
||||
symbol: this.config.symbol,
|
||||
title: "加仓",
|
||||
message: `${currentSide === "LONG" ? "做多" : "做空"} +${absChange.toFixed(6)} → ${Math.abs(currentAmt).toFixed(6)}`,
|
||||
details: {
|
||||
side: currentSide,
|
||||
added: absChange,
|
||||
totalSize: Math.abs(currentAmt),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.notifier.send({
|
||||
type: "order_filled",
|
||||
level: "info",
|
||||
symbol: this.config.symbol,
|
||||
title: "减仓",
|
||||
message: `${currentSide === "LONG" ? "多" : "空"} -${absChange.toFixed(6)} → ${Math.abs(currentAmt).toFixed(6)}`,
|
||||
details: {
|
||||
side: currentSide,
|
||||
reduced: absChange,
|
||||
totalSize: Math.abs(currentAmt),
|
||||
},
|
||||
});
|
||||
}
|
||||
} else if (currentSide !== prevSide && currentSide !== "FLAT" && prevSide !== "FLAT") {
|
||||
this.notifier.send({
|
||||
type: "position_opened",
|
||||
level: "info",
|
||||
symbol: this.config.symbol,
|
||||
title: "反向开仓",
|
||||
message: `${prevSide === "LONG" ? "多→空" : "空→多"} ${Math.abs(currentAmt).toFixed(6)}`,
|
||||
details: {
|
||||
prevSide,
|
||||
newSide: currentSide,
|
||||
size: Math.abs(currentAmt),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.lastPositionAmt = currentAmt;
|
||||
this.lastPositionSide = currentSide;
|
||||
}
|
||||
|
||||
private async handleTokenExpiry(position: PositionSnapshot, absPosition: number): Promise<boolean> {
|
||||
if (!isTokenExpiryConfigured()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const expiryStatus = checkStandxTokenExpiry({
|
||||
positionAmt: position.positionAmt,
|
||||
openOrderCount: this.openOrders.length,
|
||||
});
|
||||
|
||||
if (!expiryStatus.expired) {
|
||||
if (this.tokenExpiryState !== "active") {
|
||||
this.tokenExpiryState = "active";
|
||||
this.tokenExpiryLogged = false;
|
||||
this.tokenExpiryCancelDone = false;
|
||||
this.tokenExpiredCloseOnlyMode = false;
|
||||
this.tokenExpiryNotified = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const prevState = this.tokenExpiryState;
|
||||
this.tokenExpiryState = expiryStatus.state;
|
||||
|
||||
if (!this.tokenExpiryLogged) {
|
||||
const message = formatTokenExpiryMessage(expiryStatus);
|
||||
if (message) {
|
||||
this.tradeLog.push("warn", message);
|
||||
}
|
||||
this.tokenExpiryLogged = true;
|
||||
}
|
||||
|
||||
if (!this.tokenExpiryNotified) {
|
||||
this.notifier.send({
|
||||
type: "token_expired",
|
||||
level: "warn",
|
||||
symbol: this.config.symbol,
|
||||
title: "Token 已过期",
|
||||
message: expiryStatus.hasPosition
|
||||
? "Token 已过期,进入平仓模式,不再开新仓"
|
||||
: "Token 已过期,策略进入静默模式",
|
||||
details: {
|
||||
hasPosition: expiryStatus.hasPosition,
|
||||
hasOpenOrders: expiryStatus.hasOpenOrders,
|
||||
state: expiryStatus.state,
|
||||
},
|
||||
});
|
||||
this.tokenExpiryNotified = true;
|
||||
}
|
||||
|
||||
if (!this.tokenExpiryCancelDone && this.openOrders.length > 0) {
|
||||
try {
|
||||
await this.exchange.cancelAllOrders({ symbol: this.config.symbol });
|
||||
this.tradeLog.push("order", "Token 过期,已撤销所有挂单");
|
||||
this.openOrders = [];
|
||||
this.tokenExpiryCancelDone = true;
|
||||
} catch (error) {
|
||||
if (isUnknownOrderError(error)) {
|
||||
this.tradeLog.push("order", "Token 过期撤单时订单已不存在");
|
||||
this.tokenExpiryCancelDone = true;
|
||||
} else {
|
||||
this.tradeLog.push("error", `Token 过期撤单失败: ${extractMessage(error)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (expiryStatus.state === "expired_with_position") {
|
||||
if (!this.tokenExpiredCloseOnlyMode) {
|
||||
this.tokenExpiredCloseOnlyMode = true;
|
||||
this.tradeLog.push("info", "Token 过期,强制进入平仓模式,仅允许 reduce-only 订单");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expiryStatus.state === "silent") {
|
||||
if (prevState !== "silent") {
|
||||
this.tradeLog.push("info", "进入静默数据接收模式,不再进行任何交易操作");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveBinanceSymbol(symbol: string): string {
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { isStandxTokenExpired, getStandxTokenExpiryInfo, standxTokenConfig } from "../config";
|
||||
|
||||
export type TokenExpiryState = "active" | "expired" | "expired_with_position" | "silent";
|
||||
|
||||
export interface TokenExpiryStatus {
|
||||
state: TokenExpiryState;
|
||||
expired: boolean;
|
||||
expiryTimestamp: number | null;
|
||||
remainingMs: number | null;
|
||||
hasPosition: boolean;
|
||||
hasOpenOrders: boolean;
|
||||
}
|
||||
|
||||
export interface TokenExpiryCheckParams {
|
||||
positionAmt: number;
|
||||
openOrderCount: number;
|
||||
}
|
||||
|
||||
export function checkStandxTokenExpiry(params: TokenExpiryCheckParams): TokenExpiryStatus {
|
||||
const info = getStandxTokenExpiryInfo();
|
||||
const hasPosition = Math.abs(params.positionAmt) > 1e-8;
|
||||
const hasOpenOrders = params.openOrderCount > 0;
|
||||
|
||||
if (!info.expired) {
|
||||
return {
|
||||
state: "active",
|
||||
expired: false,
|
||||
expiryTimestamp: info.expiryTimestamp,
|
||||
remainingMs: info.remainingMs,
|
||||
hasPosition,
|
||||
hasOpenOrders,
|
||||
};
|
||||
}
|
||||
|
||||
if (hasPosition) {
|
||||
return {
|
||||
state: "expired_with_position",
|
||||
expired: true,
|
||||
expiryTimestamp: info.expiryTimestamp,
|
||||
remainingMs: 0,
|
||||
hasPosition: true,
|
||||
hasOpenOrders,
|
||||
};
|
||||
}
|
||||
|
||||
if (!hasOpenOrders) {
|
||||
return {
|
||||
state: "silent",
|
||||
expired: true,
|
||||
expiryTimestamp: info.expiryTimestamp,
|
||||
remainingMs: 0,
|
||||
hasPosition: false,
|
||||
hasOpenOrders: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
state: "expired",
|
||||
expired: true,
|
||||
expiryTimestamp: info.expiryTimestamp,
|
||||
remainingMs: 0,
|
||||
hasPosition,
|
||||
hasOpenOrders,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatTokenExpiryMessage(status: TokenExpiryStatus): string | null {
|
||||
if (!status.expired) {
|
||||
if (status.remainingMs != null && status.remainingMs < 3600_000) {
|
||||
const mins = Math.ceil(status.remainingMs / 60_000);
|
||||
return `StandX Token 将在 ${mins} 分钟后过期`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (status.state) {
|
||||
case "expired":
|
||||
return "StandX Token 已过期,正在取消所有挂单";
|
||||
case "expired_with_position":
|
||||
return "StandX Token 已过期,仅保留平仓/止损逻辑";
|
||||
case "silent":
|
||||
return "StandX Token 已过期,进入静默数据接收模式";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function isTokenExpiryConfigured(): boolean {
|
||||
return standxTokenConfig.expiryTimestamp != null;
|
||||
}
|
||||
|
||||
export { isStandxTokenExpired, getStandxTokenExpiryInfo };
|
||||
Reference in New Issue
Block a user