Enhance account snapshot handling and staleness checks in MakerPointsEngine

- Updated `emitAccountSnapshot` method in `StandxGateway` to accept an optional `updateTime` parameter, allowing for more accurate timestamping.
- Introduced logic to determine the appropriate `updateTime` based on the latest position or balance data.
- Added `time` property to `StandxPosition` interface for improved timestamp management.
- Implemented `applyAccountSnapshot` method in `MakerPointsEngine` to streamline account snapshot processing and ensure accurate time tracking.
- Added tests to validate the behavior of account staleness checks and defense mode activation based on account data freshness.
This commit is contained in:
discountry
2026-01-24 23:53:36 +08:00
parent 683352f737
commit 1d88ddefb5
4 changed files with 203 additions and 25 deletions
+63 -20
View File
@@ -95,10 +95,11 @@ const STOP_LOSS_CHECK_INTERVAL_MS = 250; // 止损检查最大间隔
const STOP_LOSS_RETRY_INTERVAL_MS = 500; // 止损失败后重试间隔
const DATA_STALE_THRESHOLD_MS = 5_000; // 数据过时阈值(5秒)
const DEFENSE_MODE_CHECK_INTERVAL_MS = 1000; // 防御模式检查间隔
const ACCOUNT_DATA_STALE_THRESHOLD_MS = 20_000; // 账户数据长期无更新阈值(StandX 有 REST 兜底,长期无更新通常意味着异常
const ACCOUNT_DATA_STALE_THRESHOLD_MS = 20_000; // 账户数据长期无更新阈值(会先尝试通过 REST 补拉验证,不直接进入防御模式
const STANDX_REST_ERROR_DEFENSE_THRESHOLD = 3;
const STANDX_MARGIN_MODE_CHECK_INTERVAL_MS = 500;
const STANDX_MARGIN_MODE_MAX_ATTEMPTS = 10;
const ACCOUNT_STALE_REST_PROBE_MIN_INTERVAL_MS = 5_000;
export class MakerPointsEngine {
private accountSnapshot: AsterAccountSnapshot | null = null;
@@ -177,6 +178,9 @@ export class MakerPointsEngine {
private lastStandxDepthTime = 0;
private lastStandxAccountTime = 0;
private lastBinanceDepthTime = 0;
private accountStaleRestProbeInFlight: Promise<void> | null = null;
private accountStaleRestProbeLastAttempt = 0;
private accountStaleRestProbeConsecutiveFailures = 0;
// 防御模式状态
private defenseMode = false;
private defenseModeNotified = false;
@@ -289,17 +293,7 @@ export class MakerPointsEngine {
safeSubscribe<AsterAccountSnapshot>(
this.exchange.watchAccount.bind(this.exchange),
(snapshot) => {
this.accountSnapshot = snapshot;
this.lastStandxAccountTime = Date.now();
const totalUnrealized = Number(snapshot.totalUnrealizedProfit ?? "0");
if (Number.isFinite(totalUnrealized)) {
this.accountUnrealized = totalUnrealized;
}
const position = getPosition(snapshot, this.config.symbol);
this.sessionVolume.update(position, this.getReferencePrice());
this.detectPositionChange(position);
this.feedStatus.account = true;
this.emitUpdate();
this.applyAccountSnapshot(snapshot);
},
log,
{
@@ -370,6 +364,51 @@ export class MakerPointsEngine {
this.setupConnectionProtection();
}
private applyAccountSnapshot(snapshot: AsterAccountSnapshot): void {
this.accountSnapshot = snapshot;
// StandX: WS 推送使用本地接收时间戳;REST 快照使用响应里的 time 字段映射到 snapshot.updateTime
this.lastStandxAccountTime =
this.exchange.id === "standx" && Number.isFinite(snapshot.updateTime) && snapshot.updateTime > 0
? snapshot.updateTime
: Date.now();
this.accountStaleRestProbeConsecutiveFailures = 0;
const totalUnrealized = Number(snapshot.totalUnrealizedProfit ?? "0");
if (Number.isFinite(totalUnrealized)) {
this.accountUnrealized = totalUnrealized;
}
const position = getPosition(snapshot, this.config.symbol);
this.sessionVolume.update(position, this.getReferencePrice());
this.detectPositionChange(position);
this.feedStatus.account = true;
this.emitUpdate();
}
private maybeProbeStandxAccountSnapshot(now: number): void {
if (this.exchange.id !== "standx") return;
if (!this.exchange.queryAccountSnapshot) return;
if (this.defenseMode) return;
if (this.accountStaleRestProbeInFlight) return;
if (this.accountStaleRestProbeLastAttempt > 0 && now - this.accountStaleRestProbeLastAttempt < ACCOUNT_STALE_REST_PROBE_MIN_INTERVAL_MS) {
return;
}
this.accountStaleRestProbeLastAttempt = now;
this.accountStaleRestProbeInFlight = (async () => {
try {
const next = await this.exchange.queryAccountSnapshot?.();
if (next) {
this.applyAccountSnapshot(next);
} else {
this.accountStaleRestProbeConsecutiveFailures += 1;
}
} catch {
this.accountStaleRestProbeConsecutiveFailures += 1;
} finally {
this.accountStaleRestProbeInFlight = null;
}
})();
}
/**
* 设置连接保护机制
* 监听断连/重连事件,实现保护逻辑
@@ -1616,8 +1655,16 @@ export class MakerPointsEngine {
const standxDepthStale = this.lastStandxDepthTime > 0 && (now - this.lastStandxDepthTime) > DATA_STALE_THRESHOLD_MS;
const binanceStale = this.lastBinanceDepthTime > 0 && (now - this.lastBinanceDepthTime) > DATA_STALE_THRESHOLD_MS;
const standxAccountAge = this.lastStandxAccountTime > 0 ? now - this.lastStandxAccountTime : 0;
const standxAccountStaleByAge = this.lastStandxAccountTime > 0 && standxAccountAge > ACCOUNT_DATA_STALE_THRESHOLD_MS;
if (standxAccountStaleByAge) {
this.maybeProbeStandxAccountSnapshot(now);
}
const standxAccountStale =
this.lastStandxAccountTime > 0 && (now - this.lastStandxAccountTime) > ACCOUNT_DATA_STALE_THRESHOLD_MS;
standxAccountStaleByAge &&
// WS 推送间隔可能较长,先给 REST 补拉一次机会;只有补拉失败后才进入防御模式
this.accountStaleRestProbeConsecutiveFailures > 0 &&
this.accountStaleRestProbeInFlight == null;
const accountHealth = validateAccountSnapshotForSymbol(this.accountSnapshot, this.config.symbol);
const accountInvalid = this.accountSnapshot != null && !accountHealth.ok;
const standxRestUnhealthy =
@@ -1647,7 +1694,7 @@ export class MakerPointsEngine {
marginMode,
standxDepthAge: this.lastStandxDepthTime > 0 ? now - this.lastStandxDepthTime : 0,
binanceAge: this.lastBinanceDepthTime > 0 ? now - this.lastBinanceDepthTime : 0,
standxAccountAge: this.lastStandxAccountTime > 0 ? now - this.lastStandxAccountTime : 0,
standxAccountAge,
accountIssues: accountInvalid ? accountHealth.issues : [],
});
} else if (!shouldDefend && this.defenseMode) {
@@ -1797,9 +1844,7 @@ export class MakerPointsEngine {
if (this.exchange.queryAccountSnapshot) {
const nextAccount = await this.exchange.queryAccountSnapshot();
if (nextAccount) {
this.accountSnapshot = nextAccount;
this.lastStandxAccountTime = Date.now();
this.feedStatus.account = true;
this.applyAccountSnapshot(nextAccount);
const health = validateAccountSnapshotForSymbol(nextAccount, this.config.symbol);
if (!health.ok) {
this.tradeLog.push("warn", `防御模式: 仓位数据仍异常: ${health.issues.join(",")}`);
@@ -1884,9 +1929,7 @@ export class MakerPointsEngine {
for (let attempt = 0; attempt < STANDX_MARGIN_MODE_MAX_ATTEMPTS; attempt++) {
const next = await queryAccount();
if (next) {
this.accountSnapshot = next;
this.lastStandxAccountTime = Date.now();
this.feedStatus.account = true;
this.applyAccountSnapshot(next);
}
const mode = this.getStandxMarginMode(this.accountSnapshot);
if (mode === "isolated") {