增强做市引擎和偏移做市引擎的限频控制逻辑,添加限频控制器以管理交易频率,确保在高频交易情况下的稳定性。同时,更新趋势引擎以支持限频机制,提升交易策略的有效性和响应能力。

This commit is contained in:
discountry
2025-09-26 19:44:41 +08:00
parent 252bd76640
commit 49bcb9b6b4
6 changed files with 284 additions and 10 deletions
+122
View File
@@ -0,0 +1,122 @@
import type { LogHandler } from "../order-coordinator";
type RateLimitState = "normal" | "degraded" | "paused";
export type RateLimitDecision = "run" | "skip" | "paused";
const DEFAULT_PAUSE_MS = 30_000;
const DEFAULT_RECOVERY_MS = 60_000;
export class RateLimitController {
private state: RateLimitState = "normal";
private pausedUntil: number | null = null;
private lastCycleAt = 0;
private lastRateLimitAt = 0;
private readonly pauseMs: number;
private readonly recoveryMs: number;
private entriesSuppressed = false;
constructor(
private readonly baseInterval: number,
private readonly log: LogHandler,
options?: { pauseMs?: number; recoveryMs?: number }
) {
this.pauseMs = options?.pauseMs ?? DEFAULT_PAUSE_MS;
this.recoveryMs = options?.recoveryMs ?? DEFAULT_RECOVERY_MS;
}
shouldBlockEntries(): boolean {
return this.entriesSuppressed || this.state === "paused";
}
private suppressEntries(source?: string): void {
if (this.entriesSuppressed) return;
this.entriesSuppressed = true;
this.log("info", `${source ? `${source} ` : ""}限频期间暂停新开仓`);
}
private allowEntries(): void {
if (!this.entriesSuppressed) return;
this.entriesSuppressed = false;
this.log("info", "限频恢复,允许重新开仓");
}
beforeCycle(): RateLimitDecision {
const now = Date.now();
if (this.state === "paused") {
if (this.pausedUntil != null && now >= this.pausedUntil) {
this.state = "degraded";
this.pausedUntil = null;
this.log("info", "限频暂停结束,继续以降频模式运行");
} else {
this.lastCycleAt = now;
return "paused";
}
}
const interval = this.currentInterval();
if (now - this.lastCycleAt < interval) {
return "skip";
}
this.lastCycleAt = now;
return "run";
}
registerRateLimit(source?: string): void {
const now = Date.now();
this.lastRateLimitAt = now;
if (this.state === "normal") {
this.state = "degraded";
this.log(
"warn",
`${source ? `${source} ` : ""}触发 429,降频至 ${(this.currentInterval() / 1000).toFixed(2)}s`
);
this.lastCycleAt = now;
this.suppressEntries(source);
return;
}
if (this.state === "degraded") {
this.state = "paused";
this.pausedUntil = now + this.pauseMs;
this.log(
"warn",
`${source ? `${source} ` : ""}连续 429,暂停请求 ${(this.pauseMs / 1000).toFixed(0)}s`
);
this.suppressEntries(source);
return;
}
this.pausedUntil = now + this.pauseMs;
this.log(
"warn",
`${source ? `${source} ` : ""}限频仍在持续,延长暂停 ${(this.pauseMs / 1000).toFixed(0)}s`
);
this.suppressEntries(source);
}
onCycleComplete(hadRateLimit: boolean): void {
if (hadRateLimit) return;
if (this.state === "degraded" && this.lastRateLimitAt > 0) {
const now = Date.now();
if (now - this.lastRateLimitAt >= this.recoveryMs) {
this.state = "normal";
this.log("info", "限频恢复,重置为正常请求频率");
this.allowEntries();
this.lastRateLimitAt = 0;
}
}
if (this.state === "normal" && this.entriesSuppressed && this.lastRateLimitAt === 0) {
this.allowEntries();
}
}
private currentInterval(): number {
if (this.state === "degraded") {
return this.baseInterval * 2;
}
if (this.state === "paused") {
return this.baseInterval * 2;
}
return this.baseInterval;
}
}
+40 -4
View File
@@ -8,7 +8,7 @@ import type {
} from "../exchanges/types";
import { roundDownToTick } from "../utils/math";
import { createTradeLog, type TradeLogEntry } from "../state/trade-log";
import { isUnknownOrderError } from "../utils/errors";
import { isUnknownOrderError, isRateLimitError } from "../utils/errors";
import { getPosition, type PositionSnapshot } from "../utils/strategy";
import { computePositionPnl } from "../utils/pnl";
import { getTopPrices, getMidOrLast } from "../utils/price";
@@ -21,6 +21,7 @@ import {
import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "./order-coordinator";
import { makeOrderPlan } from "./lib/order-plan";
import { safeCancelOrder } from "./lib/orders";
import { RateLimitController } from "./lib/rate-limit";
interface DesiredOrder {
side: "BUY" | "SELL";
@@ -74,9 +75,13 @@ export class MakerEngine {
private initialOrderSnapshotReady = false;
private initialOrderResetDone = false;
private entryPricePendingLogged = false;
private readonly rateLimit: RateLimitController;
constructor(private readonly config: MakerConfig, 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.bootstrap();
}
@@ -215,7 +220,16 @@ export class MakerEngine {
private async tick(): Promise<void> {
if (this.processing) return;
this.processing = true;
let hadRateLimit = false;
try {
const decision = this.rateLimit.beforeCycle();
if (decision === "paused") {
this.emitUpdate();
return;
}
if (decision === "skip") {
return;
}
if (!this.isReady()) {
this.emitUpdate();
return;
@@ -237,11 +251,14 @@ export class MakerEngine {
const position = getPosition(this.accountSnapshot, this.config.symbol);
const absPosition = Math.abs(position.positionAmt);
const desired: DesiredOrder[] = [];
const canEnter = !this.rateLimit.shouldBlockEntries();
if (absPosition < EPS) {
this.entryPricePendingLogged = false;
desired.push({ side: "BUY", price: bidPrice, amount: this.config.tradeAmount, reduceOnly: false });
desired.push({ side: "SELL", price: askPrice, amount: this.config.tradeAmount, reduceOnly: false });
if (canEnter) {
desired.push({ side: "BUY", price: bidPrice, amount: this.config.tradeAmount, reduceOnly: false });
desired.push({ side: "SELL", price: askPrice, amount: this.config.tradeAmount, reduceOnly: false });
}
} else {
const closeSide: "BUY" | "SELL" = position.positionAmt > 0 ? "SELL" : "BUY";
const closePrice = closeSide === "SELL" ? askPrice : bidPrice;
@@ -254,13 +271,32 @@ export class MakerEngine {
await this.checkRisk(position, bidPrice, askPrice);
this.emitUpdate();
} catch (error) {
this.tradeLog.push("error", `做市循环异常: ${String(error)}`);
if (isRateLimitError(error)) {
hadRateLimit = true;
this.rateLimit.registerRateLimit("maker");
await this.enforceRateLimitStop();
this.tradeLog.push("warn", `MakerEngine 429: ${String(error)}`);
} else {
this.tradeLog.push("error", `做市循环异常: ${String(error)}`);
}
this.emitUpdate();
} finally {
this.rateLimit.onCycleComplete(hadRateLimit);
this.processing = false;
}
}
private async enforceRateLimitStop(): Promise<void> {
const position = getPosition(this.accountSnapshot, this.config.symbol);
if (Math.abs(position.positionAmt) < EPS) return;
const { topBid, topAsk } = getTopPrices(this.depthSnapshot);
if (topBid == null || topAsk == null) return;
const bidPrice = roundDownToTick(topBid - this.config.bidOffset, this.config.priceTick);
const askPrice = roundDownToTick(topAsk + this.config.askOffset, this.config.priceTick);
await this.checkRisk(position, bidPrice, askPrice);
await this.flushOrders();
}
private async ensureStartupOrderReset(): Promise<boolean> {
if (this.initialOrderResetDone) return true;
if (!this.initialOrderSnapshotReady) return false;
+59 -4
View File
@@ -8,7 +8,7 @@ import type {
} from "../exchanges/types";
import { roundDownToTick } from "../utils/math";
import { createTradeLog } from "../state/trade-log";
import { isUnknownOrderError } from "../utils/errors";
import { isUnknownOrderError, isRateLimitError } from "../utils/errors";
import { getPosition, type PositionSnapshot } from "../utils/strategy";
import { computeDepthStats } from "../utils/depth";
import { computePositionPnl } from "../utils/pnl";
@@ -23,6 +23,7 @@ import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "./order-coord
import type { MakerEngineSnapshot } from "./maker-engine";
import { makeOrderPlan } from "./lib/order-plan";
import { safeCancelOrder } from "./lib/orders";
import { RateLimitController } from "./lib/rate-limit";
interface DesiredOrder {
side: "BUY" | "SELL";
@@ -68,6 +69,7 @@ export class OffsetMakerEngine {
private initialOrderSnapshotReady = false;
private initialOrderResetDone = false;
private entryPricePendingLogged = false;
private readonly rateLimit: RateLimitController;
private lastBuyDepthSum10 = 0;
private lastSellDepthSum10 = 0;
@@ -77,6 +79,9 @@ export class OffsetMakerEngine {
constructor(private readonly config: MakerConfig, 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.bootstrap();
}
@@ -214,7 +219,16 @@ export class OffsetMakerEngine {
private async tick(): Promise<void> {
if (this.processing) return;
this.processing = true;
let hadRateLimit = false;
try {
const decision = this.rateLimit.beforeCycle();
if (decision === "paused") {
this.emitUpdate();
return;
}
if (decision === "skip") {
return;
}
if (!this.isReady()) {
this.emitUpdate();
return;
@@ -249,13 +263,14 @@ export class OffsetMakerEngine {
const askPrice = roundDownToTick(topAsk! + this.config.askOffset, this.config.priceTick);
const absPosition = Math.abs(position.positionAmt);
const desired: DesiredOrder[] = [];
const canEnter = !this.rateLimit.shouldBlockEntries();
if (absPosition < EPS) {
this.entryPricePendingLogged = false;
if (!skipBuySide) {
if (!skipBuySide && canEnter) {
desired.push({ side: "BUY", price: bidPrice, amount: this.config.tradeAmount, reduceOnly: false });
}
if (!skipSellSide) {
if (!skipSellSide && canEnter) {
desired.push({ side: "SELL", price: askPrice, amount: this.config.tradeAmount, reduceOnly: false });
}
} else {
@@ -270,13 +285,53 @@ export class OffsetMakerEngine {
await this.checkRisk(position, bidPrice, askPrice);
this.emitUpdate();
} catch (error) {
this.tradeLog.push("error", `偏移做市循环异常: ${String(error)}`);
if (isRateLimitError(error)) {
hadRateLimit = true;
this.rateLimit.registerRateLimit("offset-maker");
await this.enforceRateLimitStop();
this.tradeLog.push("warn", `OffsetMakerEngine 429: ${String(error)}`);
} else {
this.tradeLog.push("error", `偏移做市循环异常: ${String(error)}`);
}
this.emitUpdate();
} finally {
this.rateLimit.onCycleComplete(hadRateLimit);
this.processing = false;
}
}
private async enforceRateLimitStop(): Promise<void> {
const position = getPosition(this.accountSnapshot, this.config.symbol);
if (Math.abs(position.positionAmt) < EPS) return;
await this.flushOrders();
const absPosition = Math.abs(position.positionAmt);
const side: "BUY" | "SELL" = position.positionAmt > 0 ? "SELL" : "BUY";
try {
await marketClose(
this.exchange,
this.config.symbol,
this.openOrders,
this.locks,
this.timers,
this.pending,
side,
absPosition,
(type, detail) => this.tradeLog.push(type, detail),
{
markPrice: position.markPrice,
expectedPrice: Number(side === "SELL" ? this.depthSnapshot?.bids?.[0]?.[0] : this.depthSnapshot?.asks?.[0]?.[0]) || null,
maxPct: this.config.maxCloseSlippagePct,
}
);
} catch (error) {
if (isUnknownOrderError(error)) {
this.tradeLog.push("order", "限频强制平仓时订单已不存在");
} else {
this.tradeLog.push("error", `限频强制平仓失败: ${String(error)}`);
}
}
}
private async ensureStartupOrderReset(): Promise<boolean> {
if (this.initialOrderResetDone) return true;
if (!this.initialOrderSnapshotReady) return false;
+39 -2
View File
@@ -29,6 +29,8 @@ import { isUnknownOrderError } from "../utils/errors";
import { roundDownToTick } from "../utils/math";
import { createTradeLog, type TradeLogEntry } from "../state/trade-log";
import { decryptCopyright } from "../utils/copyright";
import { isRateLimitError } from "../utils/errors";
import { RateLimitController } from "./lib/rate-limit";
export interface TrendEngineSnapshot {
ready: boolean;
@@ -84,6 +86,7 @@ export class TrendEngine {
private initializedPosition = false;
private cancelAllRequested = false;
private readonly pendingCancelOrders = new Set<number>();
private readonly rateLimit: RateLimitController;
// 控制入场频率:同一分钟内最多入场一次
private lastEntryMinute: number | null = null;
@@ -102,6 +105,9 @@ export class TrendEngine {
constructor(private readonly config: TradingConfig, private readonly exchange: ExchangeAdapter) {
this.tradeLog = createTradeLog(this.config.maxLogEntries);
this.rateLimit = new RateLimitController(this.config.pollIntervalMs, (type, detail) =>
this.tradeLog.push(type, detail)
);
this.bootstrap();
}
@@ -240,7 +246,16 @@ export class TrendEngine {
private async tick(): Promise<void> {
if (this.processing) return;
this.processing = true;
let hadRateLimit = false;
try {
const decision = this.rateLimit.beforeCycle();
if (decision === "paused") {
this.emitUpdate();
return;
}
if (decision === "skip") {
return;
}
if (!this.ordersSnapshotReady) {
this.emitUpdate();
return;
@@ -259,7 +274,9 @@ export class TrendEngine {
const position = getPosition(this.accountSnapshot, this.config.symbol);
if (Math.abs(position.positionAmt) < 1e-5) {
await this.handleOpenPosition(price, sma30);
if (!this.rateLimit.shouldBlockEntries()) {
await this.handleOpenPosition(price, sma30);
}
} else {
const result = await this.handlePositionManagement(position, price);
if (result.closed) {
@@ -273,13 +290,33 @@ export class TrendEngine {
this.lastPrice = price;
this.emitUpdate();
} catch (error) {
this.tradeLog.push("error", `策略循环异常: ${String(error)}`);
if (isRateLimitError(error)) {
hadRateLimit = true;
this.rateLimit.registerRateLimit("trend");
await this.enforceRateLimitStop();
this.tradeLog.push("warn", `TrendEngine 429: ${String(error)}`);
} else {
this.tradeLog.push("error", `策略循环异常: ${String(error)}`);
}
this.emitUpdate();
} finally {
this.rateLimit.onCycleComplete(hadRateLimit);
this.processing = false;
}
}
private async enforceRateLimitStop(): Promise<void> {
const position = getPosition(this.accountSnapshot, this.config.symbol);
if (Math.abs(position.positionAmt) < 1e-5) return;
const price = this.getReferencePrice() ?? Number(this.tickerSnapshot?.lastPrice) ?? this.lastPrice;
if (!Number.isFinite(price) || price == null) return;
const result = await this.handlePositionManagement(position, Number(price));
if (result.closed) {
this.totalTrades += 1;
this.totalProfit += result.pnl;
}
}
private logStartupState(): void {
if (this.startupLogged) return;
const position = getPosition(this.accountSnapshot, this.config.symbol);
+1
View File
@@ -154,6 +154,7 @@ export interface AsterOrder {
reduceOnly: boolean;
closePosition: boolean;
workingType?: string;
activationPrice?: string;
avgPrice?: string;
cumQuote?: string;
origType?: string;
+23
View File
@@ -13,3 +13,26 @@ export function extractMessage(error: unknown): string {
return String(error);
}
}
export function isRateLimitError(error: unknown): boolean {
if (!error) return false;
if (typeof error === "object" && "status" in error) {
const status = Number((error as { status?: unknown }).status);
if (Number.isFinite(status) && status === 429) {
return true;
}
}
if (typeof error === "object" && "code" in error) {
const code = (error as { code?: unknown }).code;
if (code === 429 || code === "429") {
return true;
}
}
const message = extractMessage(error).toLowerCase();
return (
message.includes("429") ||
message.includes("too many requests") ||
message.includes("rate limit") ||
message.includes("request rate")
);
}