feat: 添加布林带宽度计算功能及相关配置项,优化趋势引擎以支持布林带宽度过滤

This commit is contained in:
discountry
2025-09-27 22:29:56 +08:00
parent 151130dfc4
commit d392f54f21
6 changed files with 88 additions and 4 deletions
+6
View File
@@ -12,6 +12,9 @@ export interface TradingConfig {
maxCloseSlippagePct: number;
priceTick: number; // price tick size, e.g. 0.1 for BTCUSDT
qtyStep: number; // quantity step size, e.g. 0.001 BTC
bollingerLength: number;
bollingerStdMultiplier: number;
minBollingerBandwidth: number;
}
function parseNumber(value: string | undefined, fallback: number): number {
@@ -34,6 +37,9 @@ export const tradingConfig: TradingConfig = {
maxCloseSlippagePct: parseNumber(process.env.MAX_CLOSE_SLIPPAGE_PCT, 0.05),
priceTick: parseNumber(process.env.PRICE_TICK, 0.1),
qtyStep: parseNumber(process.env.QTY_STEP, 0.001),
bollingerLength: parseNumber(process.env.BOLLINGER_LENGTH, 20),
bollingerStdMultiplier: parseNumber(process.env.BOLLINGER_STD_MULTIPLIER, 2),
minBollingerBandwidth: parseNumber(process.env.MIN_BOLLINGER_BANDWIDTH, 0.1),
};
export interface MakerConfig {
+33 -3
View File
@@ -11,6 +11,7 @@ import type {
import {
calcStopLossPrice,
calcTrailingActivationPrice,
computeBollingerBandwidth,
getPosition,
getSMA,
type PositionSnapshot,
@@ -37,6 +38,7 @@ export interface TrendEngineSnapshot {
symbol: string;
lastPrice: number | null;
sma30: number | null;
bollingerBandwidth: number | null;
trend: "做多" | "做空" | "无信号";
position: PositionSnapshot;
pnl: number;
@@ -78,6 +80,7 @@ export class TrendEngine {
private processing = false;
private lastPrice: number | null = null;
private lastSma30: number | null = null;
private lastBollingerBandwidth: number | null = null;
private totalProfit = 0;
private totalTrades = 0;
private lastOpenPlan: OpenOrderPlan = { side: null, price: null };
@@ -99,6 +102,7 @@ export class TrendEngine {
private lastEntryMinute: number | null = null;
// 止损后冷却:止损发生后的 60s 内忽略 SMA 入场信号
private lastStopLossAt: number | null = null;
private lastBollingerBlockLogged = 0;
private ordersSnapshotReady = false;
private startupLogged = false;
@@ -243,11 +247,12 @@ export class TrendEngine {
}
private isReady(): boolean {
const minKlines = Math.max(30, this.config.bollingerLength);
return Boolean(
this.accountSnapshot &&
this.tickerSnapshot &&
this.depthSnapshot &&
this.klineSnapshot.length >= 30
this.klineSnapshot.length >= minKlines
);
}
@@ -277,13 +282,19 @@ export class TrendEngine {
if (sma30 == null) {
return;
}
const bollingerBandwidth = computeBollingerBandwidth(
this.klineSnapshot,
this.config.bollingerLength,
this.config.bollingerStdMultiplier
);
this.lastBollingerBandwidth = bollingerBandwidth;
const ticker = this.tickerSnapshot!;
const price = Number(ticker.lastPrice);
const position = getPosition(this.accountSnapshot, this.config.symbol);
if (Math.abs(position.positionAmt) < 1e-5) {
if (!this.rateLimit.shouldBlockEntries()) {
await this.handleOpenPosition(price, sma30);
await this.handleOpenPosition(price, sma30, bollingerBandwidth);
}
} else {
const result = await this.handlePositionManagement(position, price);
@@ -345,7 +356,11 @@ export class TrendEngine {
this.startupLogged = true;
}
private async handleOpenPosition(currentPrice: number, currentSma: number): Promise<void> {
private async handleOpenPosition(
currentPrice: number,
currentSma: number,
currentBandwidth: number | null
): Promise<void> {
this.entryPricePendingLogged = false;
const now = Date.now();
const currentMinute = Math.floor(now / 60_000);
@@ -360,6 +375,20 @@ export class TrendEngine {
this.tradeLog.push("info", "本分钟已入场,忽略新的 SMA 入场信号");
return;
}
if (
Number.isFinite(currentBandwidth) &&
this.config.minBollingerBandwidth > 0 &&
Number(currentBandwidth) < this.config.minBollingerBandwidth
) {
if (now - this.lastBollingerBlockLogged > 15_000) {
this.tradeLog.push(
"info",
`布林带宽度不足:${Number(currentBandwidth).toFixed(4)} < ${this.config.minBollingerBandwidth},忽略入场信号`
);
this.lastBollingerBlockLogged = now;
}
return;
}
if (this.lastPrice == null) {
this.lastPrice = currentPrice;
return;
@@ -852,6 +881,7 @@ export class TrendEngine {
symbol: this.config.symbol,
lastPrice: price,
sma30,
bollingerBandwidth: this.lastBollingerBandwidth,
trend,
position,
pnl,
+31
View File
@@ -52,6 +52,37 @@ export function calcTrailingActivationPrice(entryPrice: number, qty: number, sid
return entryPrice - profit / Math.abs(qty);
}
export function computeBollingerBandwidth(
values: AsterKline[],
length: number,
stdMultiplier: number
): number | null {
const period = Number.isInteger(length) ? Number(length) : 0;
const multiplier = Number.isFinite(stdMultiplier) ? stdMultiplier : 0;
if (!Array.isArray(values) || period <= 0 || values.length < period || multiplier <= 0) {
return null;
}
const window = values.slice(-period);
const closes = window.map((kline) => Number(kline.close));
if (closes.some((close) => !Number.isFinite(close))) {
return null;
}
const mean = closes.reduce((sum, price) => sum + price, 0) / period;
if (!Number.isFinite(mean) || mean <= 0) {
return null;
}
const variance = closes.reduce((sum, price) => {
const diff = price - mean;
return sum + diff * diff;
}, 0) / period;
const std = Math.sqrt(Math.max(variance, 0));
const width = std * multiplier * 2;
if (!Number.isFinite(width)) {
return null;
}
return width / mean;
}
/**
* Return true if the intended order price is within the allowed deviation from mark price.
* - For BUY: orderPrice must be <= markPrice * (1 + maxPct)