mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-11 17:28:08 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a1cf24510 |
@@ -59,10 +59,6 @@ MAKER_REFRESH_INTERVAL_MS=500 # Maker refresh cadence (ms)
|
|||||||
MAKER_MAX_CLOSE_SLIPPAGE_PCT=0.05 # Maker close slippage guard (fallbacks to MAX_CLOSE_SLIPPAGE_PCT)
|
MAKER_MAX_CLOSE_SLIPPAGE_PCT=0.05 # Maker close slippage guard (fallbacks to MAX_CLOSE_SLIPPAGE_PCT)
|
||||||
MAKER_PRICE_TICK=0.1 # Maker price tick size (defaults to PRICE_TICK)
|
MAKER_PRICE_TICK=0.1 # Maker price tick size (defaults to PRICE_TICK)
|
||||||
|
|
||||||
# Maker-points Binance depth imbalance monitor
|
|
||||||
MAKER_POINTS_BINANCE_DEPTH_WINDOW_BPS=3 # Binance depth monitor window around best bid/ask (bps)
|
|
||||||
MAKER_POINTS_BINANCE_DEPTH_IMBALANCE_RATIO=9 # Imbalance threshold ratio (e.g. 9 => one side >= 9x)
|
|
||||||
|
|
||||||
# Grid strategy defaults
|
# Grid strategy defaults
|
||||||
GRID_LOWER_PRICE=25000 # Grid lower bound price (quote currency)
|
GRID_LOWER_PRICE=25000 # Grid lower bound price (quote currency)
|
||||||
GRID_UPPER_PRICE=35000 # Grid upper bound price
|
GRID_UPPER_PRICE=35000 # Grid upper bound price
|
||||||
|
|||||||
@@ -132,8 +132,6 @@ MAKER_POINTS_ORDER_AMOUNT=0.01
|
|||||||
MAKER_POINTS_CLOSE_THRESHOLD=0.1
|
MAKER_POINTS_CLOSE_THRESHOLD=0.1
|
||||||
MAKER_POINTS_STOP_LOSS_USD=0
|
MAKER_POINTS_STOP_LOSS_USD=0
|
||||||
MAKER_POINTS_MIN_REPRICE_BPS=3
|
MAKER_POINTS_MIN_REPRICE_BPS=3
|
||||||
MAKER_POINTS_BINANCE_DEPTH_WINDOW_BPS=3
|
|
||||||
MAKER_POINTS_BINANCE_DEPTH_IMBALANCE_RATIO=9
|
|
||||||
|
|
||||||
# ===== 挂单档位开关 =====
|
# ===== 挂单档位开关 =====
|
||||||
MAKER_POINTS_BAND_0_10=true
|
MAKER_POINTS_BAND_0_10=true
|
||||||
@@ -175,8 +173,6 @@ MAKER_POINTS_ORDER_AMOUNT=0.01
|
|||||||
MAKER_POINTS_CLOSE_THRESHOLD=0.1
|
MAKER_POINTS_CLOSE_THRESHOLD=0.1
|
||||||
MAKER_POINTS_STOP_LOSS_USD=0
|
MAKER_POINTS_STOP_LOSS_USD=0
|
||||||
MAKER_POINTS_MIN_REPRICE_BPS=3
|
MAKER_POINTS_MIN_REPRICE_BPS=3
|
||||||
MAKER_POINTS_BINANCE_DEPTH_WINDOW_BPS=3
|
|
||||||
MAKER_POINTS_BINANCE_DEPTH_IMBALANCE_RATIO=9
|
|
||||||
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
|
||||||
@@ -218,8 +214,6 @@ bun run pm2:start:maker-points
|
|||||||
| `MAKER_POINTS_ORDER_AMOUNT` | 每笔挂单数量 | 建议 `0.01` 起步 |
|
| `MAKER_POINTS_ORDER_AMOUNT` | 每笔挂单数量 | 建议 `0.01` 起步 |
|
||||||
| `MAKER_POINTS_CLOSE_THRESHOLD` | 持仓达到多少开始平仓 | 设为 `0` 表示不自动平仓 |
|
| `MAKER_POINTS_CLOSE_THRESHOLD` | 持仓达到多少开始平仓 | 设为 `0` 表示不自动平仓 |
|
||||||
| `MAKER_POINTS_STOP_LOSS_USD` | 亏损多少美元强制平仓 | 设为 `0` 表示关闭止损 |
|
| `MAKER_POINTS_STOP_LOSS_USD` | 亏损多少美元强制平仓 | 设为 `0` 表示关闭止损 |
|
||||||
| `MAKER_POINTS_BINANCE_DEPTH_WINDOW_BPS` | Binance 失衡检测窗口(bps) | 默认 `3` |
|
|
||||||
| `MAKER_POINTS_BINANCE_DEPTH_IMBALANCE_RATIO` | Binance 失衡比例阈值 | 默认 `9` |
|
|
||||||
| `MAKER_POINTS_BAND_*` | 三个挂单档位的开关 | 全部 `true` 即可 |
|
| `MAKER_POINTS_BAND_*` | 三个挂单档位的开关 | 全部 `true` 即可 |
|
||||||
| `STANDX_TOKEN_CREATE_DATE` | Token 创建日期 | 推荐配置,格式 YYYY-MM-DD |
|
| `STANDX_TOKEN_CREATE_DATE` | Token 创建日期 | 推荐配置,格式 YYYY-MM-DD |
|
||||||
| `STANDX_TOKEN_VALIDITY_DAYS` | Token 有效期天数 | 推荐配置,与创建日期配合使用 |
|
| `STANDX_TOKEN_VALIDITY_DAYS` | Token 有效期天数 | 推荐配置,与创建日期配合使用 |
|
||||||
|
|||||||
+2
-8
@@ -227,11 +227,7 @@ export interface MakerPointsConfig {
|
|||||||
minRepriceBps: number;
|
minRepriceBps: number;
|
||||||
/** 是否根据 Binance 盘口深度失衡自动取消单边挂单,默认 true */
|
/** 是否根据 Binance 盘口深度失衡自动取消单边挂单,默认 true */
|
||||||
enableBinanceDepthCancel: boolean;
|
enableBinanceDepthCancel: boolean;
|
||||||
/** Binance 深度监控窗口(bps),默认 3 */
|
/** 各档位最小深度阈值 (BTC),盘口到目标价之间的挂单量低于此值则跳过该档位,默认 50 */
|
||||||
binanceDepthWindowBps?: number;
|
|
||||||
/** Binance 深度失衡比例阈值,默认 9 */
|
|
||||||
binanceDepthImbalanceRatio?: number;
|
|
||||||
/** 各档位最小深度阈值 (BTC),盘口到目标价之间的挂单量低于此值则跳过该档位,默认 10 */
|
|
||||||
filterMinDepth: number;
|
filterMinDepth: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,9 +254,7 @@ export const makerPointsConfig: MakerPointsConfig = {
|
|||||||
band30To100Amount: parseNumber(process.env.MAKER_POINTS_BAND_30_100_AMOUNT, defaultMakerPointsAmount),
|
band30To100Amount: parseNumber(process.env.MAKER_POINTS_BAND_30_100_AMOUNT, defaultMakerPointsAmount),
|
||||||
minRepriceBps: parseNumber(process.env.MAKER_POINTS_MIN_REPRICE_BPS, 3),
|
minRepriceBps: parseNumber(process.env.MAKER_POINTS_MIN_REPRICE_BPS, 3),
|
||||||
enableBinanceDepthCancel: parseBoolean(process.env.MAKER_POINTS_BINANCE_DEPTH_CANCEL, true),
|
enableBinanceDepthCancel: parseBoolean(process.env.MAKER_POINTS_BINANCE_DEPTH_CANCEL, true),
|
||||||
binanceDepthWindowBps: parseNumber(process.env.MAKER_POINTS_BINANCE_DEPTH_WINDOW_BPS, 3),
|
filterMinDepth: parseNumber(process.env.MAKER_POINTS_FILTER_MIN_DEPTH, 50),
|
||||||
binanceDepthImbalanceRatio: parseNumber(process.env.MAKER_POINTS_BINANCE_DEPTH_IMBALANCE_RATIO, 9),
|
|
||||||
filterMinDepth: parseNumber(process.env.MAKER_POINTS_FILTER_MIN_DEPTH, 10),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface BasisArbConfig {
|
export interface BasisArbConfig {
|
||||||
|
|||||||
+2
-2
@@ -248,8 +248,8 @@ const translations: Record<string, TranslationEntry> = {
|
|||||||
en: "Quote mode: {mode} | BUY {buy} | SELL {sell}",
|
en: "Quote mode: {mode} | BUY {buy} | SELL {sell}",
|
||||||
},
|
},
|
||||||
"makerPoints.binanceLine": {
|
"makerPoints.binanceLine": {
|
||||||
zh: "Binance 深度(±{windowBps}bps): 买 {buy} | 卖 {sell} | 状态: {status}",
|
zh: "Binance 深度(±9bps): 买 {buy} | 卖 {sell} | 状态: {status}",
|
||||||
en: "Binance depth (±{windowBps}bps): bid {buy} | ask {sell} | Status: {status}",
|
en: "Binance depth (±9bps): bid {buy} | ask {sell} | Status: {status}",
|
||||||
},
|
},
|
||||||
"makerPoints.bandDepthLine": {
|
"makerPoints.bandDepthLine": {
|
||||||
zh: "StandX 档位 {band}bps 深度: 买 {buy} | 卖 {sell}",
|
zh: "StandX 档位 {band}bps 深度: 买 {buy} | 卖 {sell}",
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const RECONNECT_DELAY_MAX_MS = 60_000;
|
|||||||
|
|
||||||
const DEFAULT_REFRESH_SYNC_INTERVAL_MS = 30_000;
|
const DEFAULT_REFRESH_SYNC_INTERVAL_MS = 30_000;
|
||||||
const DEFAULT_DEPTH_WINDOW_BPS = 9;
|
const DEFAULT_DEPTH_WINDOW_BPS = 9;
|
||||||
const DEFAULT_IMBALANCE_RATIO = 2;
|
const DEFAULT_IMBALANCE_RATIO = 9;
|
||||||
const MAX_BUFFER_SIZE = 5000;
|
const MAX_BUFFER_SIZE = 5000;
|
||||||
const SYNC_SNAPSHOT_MAX_RETRIES = 5;
|
const SYNC_SNAPSHOT_MAX_RETRIES = 5;
|
||||||
const REST_FAILURE_DEFENSE_THRESHOLD = 1;
|
const REST_FAILURE_DEFENSE_THRESHOLD = 1;
|
||||||
|
|||||||
@@ -128,7 +128,6 @@ export class MakerPointsEngine {
|
|||||||
private processing = false;
|
private processing = false;
|
||||||
private stopLossProcessing = false;
|
private stopLossProcessing = false;
|
||||||
private stopLossCooldownUntil = 0;
|
private stopLossCooldownUntil = 0;
|
||||||
private forceTickRequested = false;
|
|
||||||
private desiredOrders: DesiredOrder[] = [];
|
private desiredOrders: DesiredOrder[] = [];
|
||||||
private accountUnrealized = 0;
|
private accountUnrealized = 0;
|
||||||
private initialOrderSnapshotReady = false;
|
private initialOrderSnapshotReady = false;
|
||||||
@@ -206,12 +205,8 @@ export class MakerPointsEngine {
|
|||||||
baseUrl: process.env.BINANCE_SPOT_WS_URL ?? process.env.BINANCE_WS_URL,
|
baseUrl: process.env.BINANCE_SPOT_WS_URL ?? process.env.BINANCE_WS_URL,
|
||||||
restBaseUrl: process.env.BINANCE_REST_URL,
|
restBaseUrl: process.env.BINANCE_REST_URL,
|
||||||
levels: 20,
|
levels: 20,
|
||||||
ratio: Number.isFinite(this.config.binanceDepthImbalanceRatio)
|
ratio: 9,
|
||||||
? Math.max(1.01, Number(this.config.binanceDepthImbalanceRatio))
|
depthWindowBps: 9,
|
||||||
: 9,
|
|
||||||
depthWindowBps: Number.isFinite(this.config.binanceDepthWindowBps)
|
|
||||||
? Math.max(1, Number(this.config.binanceDepthWindowBps))
|
|
||||||
: 3,
|
|
||||||
speedMs: 100,
|
speedMs: 100,
|
||||||
logger: (context, error) => {
|
logger: (context, error) => {
|
||||||
this.tradeLog.push("warn", `Binance ${context} 异常: ${extractMessage(error)}`);
|
this.tradeLog.push("warn", `Binance ${context} 异常: ${extractMessage(error)}`);
|
||||||
@@ -346,10 +341,6 @@ export class MakerPointsEngine {
|
|||||||
this.lastStandxDepthTime = Date.now();
|
this.lastStandxDepthTime = Date.now();
|
||||||
this.feedStatus.depth = true;
|
this.feedStatus.depth = true;
|
||||||
this.emitUpdate();
|
this.emitUpdate();
|
||||||
if (this.shouldTriggerImmediateDepthProtection(depth) || this.shouldTriggerImmediateReprice(depth)) {
|
|
||||||
this.forceTickRequested = true;
|
|
||||||
void this.tick();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
log,
|
log,
|
||||||
{
|
{
|
||||||
@@ -577,9 +568,7 @@ export class MakerPointsEngine {
|
|||||||
this.processing = true;
|
this.processing = true;
|
||||||
let hadRateLimit = false;
|
let hadRateLimit = false;
|
||||||
try {
|
try {
|
||||||
const forceRun = this.forceTickRequested;
|
const decision = this.rateLimit.beforeCycle();
|
||||||
this.forceTickRequested = false;
|
|
||||||
const decision = forceRun ? "run" : this.rateLimit.beforeCycle();
|
|
||||||
if (decision === "paused") {
|
if (decision === "paused") {
|
||||||
this.emitUpdate();
|
this.emitUpdate();
|
||||||
return;
|
return;
|
||||||
@@ -776,17 +765,17 @@ export class MakerPointsEngine {
|
|||||||
const shouldCheckDepth = minDepth > 0;
|
const shouldCheckDepth = minDepth > 0;
|
||||||
|
|
||||||
if (!skipBuy) {
|
if (!skipBuy) {
|
||||||
const targetPrice = this.normalizeDepthTargetPrice(bid1 * (1 - bps / 10000), priceDecimals);
|
const price = bid1 * (1 - bps / 10000);
|
||||||
if (targetPrice != null) {
|
if (Number.isFinite(price) && price > 0) {
|
||||||
if (shouldCheckDepth) {
|
if (shouldCheckDepth) {
|
||||||
const depthQty = getDepthBetweenPrices(depth, "BUY", targetPrice);
|
const depthQty = getDepthBetweenPrices(depth, "BUY", price);
|
||||||
if (depthQty < minDepth) {
|
if (depthQty < minDepth) {
|
||||||
this.logThinDepthSkip("BUY", bps, depthQty, minDepth);
|
this.logThinDepthSkip("BUY", bps, depthQty, minDepth);
|
||||||
} else {
|
} else {
|
||||||
this.resetThinDepthSkip("BUY", bps);
|
this.resetThinDepthSkip("BUY", bps);
|
||||||
desired.push({
|
desired.push({
|
||||||
side: "BUY",
|
side: "BUY",
|
||||||
price: formatPriceToString(targetPrice, priceDecimals),
|
price: formatPriceToString(price, priceDecimals),
|
||||||
amount,
|
amount,
|
||||||
reduceOnly: false,
|
reduceOnly: false,
|
||||||
});
|
});
|
||||||
@@ -794,7 +783,7 @@ export class MakerPointsEngine {
|
|||||||
} else {
|
} else {
|
||||||
desired.push({
|
desired.push({
|
||||||
side: "BUY",
|
side: "BUY",
|
||||||
price: formatPriceToString(targetPrice, priceDecimals),
|
price: formatPriceToString(price, priceDecimals),
|
||||||
amount,
|
amount,
|
||||||
reduceOnly: false,
|
reduceOnly: false,
|
||||||
});
|
});
|
||||||
@@ -802,17 +791,17 @@ export class MakerPointsEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!skipSell) {
|
if (!skipSell) {
|
||||||
const targetPrice = this.normalizeDepthTargetPrice(ask1 * (1 + bps / 10000), priceDecimals);
|
const price = ask1 * (1 + bps / 10000);
|
||||||
if (targetPrice != null) {
|
if (Number.isFinite(price) && price > 0) {
|
||||||
if (shouldCheckDepth) {
|
if (shouldCheckDepth) {
|
||||||
const depthQty = getDepthBetweenPrices(depth, "SELL", targetPrice);
|
const depthQty = getDepthBetweenPrices(depth, "SELL", price);
|
||||||
if (depthQty < minDepth) {
|
if (depthQty < minDepth) {
|
||||||
this.logThinDepthSkip("SELL", bps, depthQty, minDepth);
|
this.logThinDepthSkip("SELL", bps, depthQty, minDepth);
|
||||||
} else {
|
} else {
|
||||||
this.resetThinDepthSkip("SELL", bps);
|
this.resetThinDepthSkip("SELL", bps);
|
||||||
desired.push({
|
desired.push({
|
||||||
side: "SELL",
|
side: "SELL",
|
||||||
price: formatPriceToString(targetPrice, priceDecimals),
|
price: formatPriceToString(price, priceDecimals),
|
||||||
amount,
|
amount,
|
||||||
reduceOnly: false,
|
reduceOnly: false,
|
||||||
});
|
});
|
||||||
@@ -820,7 +809,7 @@ export class MakerPointsEngine {
|
|||||||
} else {
|
} else {
|
||||||
desired.push({
|
desired.push({
|
||||||
side: "SELL",
|
side: "SELL",
|
||||||
price: formatPriceToString(targetPrice, priceDecimals),
|
price: formatPriceToString(price, priceDecimals),
|
||||||
amount,
|
amount,
|
||||||
reduceOnly: false,
|
reduceOnly: false,
|
||||||
});
|
});
|
||||||
@@ -843,7 +832,6 @@ export class MakerPointsEngine {
|
|||||||
): boolean {
|
): boolean {
|
||||||
const minDepth = this.config.filterMinDepth;
|
const minDepth = this.config.filterMinDepth;
|
||||||
if (minDepth <= 0) return false;
|
if (minDepth <= 0) return false;
|
||||||
const priceDecimals = this.getPriceDecimals();
|
|
||||||
|
|
||||||
// 获取启用的所有档位
|
// 获取启用的所有档位
|
||||||
const targets = buildBpsTargets({
|
const targets = buildBpsTargets({
|
||||||
@@ -855,11 +843,11 @@ export class MakerPointsEngine {
|
|||||||
let changed = false;
|
let changed = false;
|
||||||
|
|
||||||
for (const bps of targets) {
|
for (const bps of targets) {
|
||||||
const buyTargetPrice = this.normalizeDepthTargetPrice(bid1 * (1 - bps / 10000), priceDecimals);
|
const buyPrice = bid1 * (1 - bps / 10000);
|
||||||
const sellTargetPrice = this.normalizeDepthTargetPrice(ask1 * (1 + bps / 10000), priceDecimals);
|
const sellPrice = ask1 * (1 + bps / 10000);
|
||||||
|
|
||||||
const buyDepthQty = getDepthBetweenPrices(depth, "BUY", buyTargetPrice ?? 0);
|
const buyDepthQty = getDepthBetweenPrices(depth, "BUY", buyPrice);
|
||||||
const sellDepthQty = getDepthBetweenPrices(depth, "SELL", sellTargetPrice ?? 0);
|
const sellDepthQty = getDepthBetweenPrices(depth, "SELL", sellPrice);
|
||||||
const currentBuyOk = buyDepthQty >= minDepth;
|
const currentBuyOk = buyDepthQty >= minDepth;
|
||||||
const currentSellOk = sellDepthQty >= minDepth;
|
const currentSellOk = sellDepthQty >= minDepth;
|
||||||
|
|
||||||
@@ -876,62 +864,6 @@ export class MakerPointsEngine {
|
|||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 当深度从“满足阈值”切换到“不满足阈值”时,立即触发一次主循环,优先撤销不再安全的挂单。
|
|
||||||
*/
|
|
||||||
private shouldTriggerImmediateDepthProtection(depth: AsterDepth | null): boolean {
|
|
||||||
if (!depth) return false;
|
|
||||||
if (this.defenseMode || this.reconnectResetPending || this.stopLossProcessing) return false;
|
|
||||||
|
|
||||||
const minDepth = this.config.filterMinDepth;
|
|
||||||
if (minDepth <= 0) return false;
|
|
||||||
|
|
||||||
const { topBid, topAsk } = getTopPrices(depth);
|
|
||||||
if (topBid == null || topAsk == null) return false;
|
|
||||||
|
|
||||||
const targets = buildBpsTargets({
|
|
||||||
band0To10: this.config.enableBand0To10,
|
|
||||||
band10To30: this.config.enableBand10To30,
|
|
||||||
band30To100: this.config.enableBand30To100,
|
|
||||||
});
|
|
||||||
const priceDecimals = this.getPriceDecimals();
|
|
||||||
|
|
||||||
for (const bps of targets) {
|
|
||||||
const lastStatus = this.lastDepthOkStatus[bps];
|
|
||||||
if (!lastStatus) continue;
|
|
||||||
|
|
||||||
const buyTargetPrice = this.normalizeDepthTargetPrice(topBid * (1 - bps / 10000), priceDecimals);
|
|
||||||
const sellTargetPrice = this.normalizeDepthTargetPrice(topAsk * (1 + bps / 10000), priceDecimals);
|
|
||||||
const buyDepthQty = getDepthBetweenPrices(depth, "BUY", buyTargetPrice ?? 0);
|
|
||||||
const sellDepthQty = getDepthBetweenPrices(depth, "SELL", sellTargetPrice ?? 0);
|
|
||||||
const currentBuyOk = buyDepthQty >= minDepth;
|
|
||||||
const currentSellOk = sellDepthQty >= minDepth;
|
|
||||||
|
|
||||||
if (lastStatus.buy && !currentBuyOk) return true;
|
|
||||||
if (lastStatus.sell && !currentSellOk) return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 当盘口相对上次报价偏移超过 minRepriceBps 时,立即触发一次主循环,优先撤销旧报价。
|
|
||||||
*/
|
|
||||||
private shouldTriggerImmediateReprice(depth: AsterDepth | null): boolean {
|
|
||||||
if (!depth) return false;
|
|
||||||
if (this.defenseMode || this.reconnectResetPending || this.stopLossProcessing) return false;
|
|
||||||
|
|
||||||
const hasActiveEntryOrders = this.openOrders.some(
|
|
||||||
(order) => order.symbol === this.config.symbol && !order.reduceOnly && isOrderActiveStatus(order.status)
|
|
||||||
);
|
|
||||||
if (!hasActiveEntryOrders) return false;
|
|
||||||
|
|
||||||
const { topBid, topAsk } = getTopPrices(depth);
|
|
||||||
if (topBid == null || topAsk == null) return false;
|
|
||||||
|
|
||||||
return this.shouldReprice(topBid, topAsk);
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildCloseOnlyOrders(
|
private buildCloseOnlyOrders(
|
||||||
position: PositionSnapshot,
|
position: PositionSnapshot,
|
||||||
bid1: number,
|
bid1: number,
|
||||||
@@ -1368,13 +1300,6 @@ export class MakerPointsEngine {
|
|||||||
return Math.max(0, Math.floor(raw + 1e-9));
|
return Math.max(0, Math.floor(raw + 1e-9));
|
||||||
}
|
}
|
||||||
|
|
||||||
private normalizeDepthTargetPrice(price: number, priceDecimals: number): number | null {
|
|
||||||
if (!Number.isFinite(price) || price <= 0) return null;
|
|
||||||
const normalized = Number(formatPriceToString(price, priceDecimals));
|
|
||||||
if (!Number.isFinite(normalized) || normalized <= 0) return null;
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
private emitUpdate(): void {
|
private emitUpdate(): void {
|
||||||
try {
|
try {
|
||||||
const snapshot = this.buildSnapshot();
|
const snapshot = this.buildSnapshot();
|
||||||
@@ -1429,13 +1354,12 @@ export class MakerPointsEngine {
|
|||||||
if (!this.depthSnapshot || topBid == null || topAsk == null) {
|
if (!this.depthSnapshot || topBid == null || topAsk == null) {
|
||||||
return bands;
|
return bands;
|
||||||
}
|
}
|
||||||
const priceDecimals = this.getPriceDecimals();
|
|
||||||
|
|
||||||
return bands.map((band) => {
|
return bands.map((band) => {
|
||||||
const buyTargetPrice = this.normalizeDepthTargetPrice(topBid * (1 - band.bps / 10000), priceDecimals);
|
const buyPrice = topBid * (1 - band.bps / 10000);
|
||||||
const sellTargetPrice = this.normalizeDepthTargetPrice(topAsk * (1 + band.bps / 10000), priceDecimals);
|
const sellPrice = topAsk * (1 + band.bps / 10000);
|
||||||
const buyDepth = getDepthBetweenPrices(this.depthSnapshot, "BUY", buyTargetPrice ?? 0);
|
const buyDepth = getDepthBetweenPrices(this.depthSnapshot, "BUY", buyPrice);
|
||||||
const sellDepth = getDepthBetweenPrices(this.depthSnapshot, "SELL", sellTargetPrice ?? 0);
|
const sellDepth = getDepthBetweenPrices(this.depthSnapshot, "SELL", sellPrice);
|
||||||
return { ...band, buyDepth, sellDepth };
|
return { ...band, buyDepth, sellDepth };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,7 +162,6 @@ export function MakerPointsApp({ onExit }: MakerPointsAppProps) {
|
|||||||
{t("makerPoints.binanceLine", {
|
{t("makerPoints.binanceLine", {
|
||||||
buy: formatNumber(snapshot.binanceDepth?.buySum ?? 0, 4),
|
buy: formatNumber(snapshot.binanceDepth?.buySum ?? 0, 4),
|
||||||
sell: formatNumber(snapshot.binanceDepth?.sellSum ?? 0, 4),
|
sell: formatNumber(snapshot.binanceDepth?.sellSum ?? 0, 4),
|
||||||
windowBps: snapshot.binanceDepth?.windowBps ?? 5,
|
|
||||||
status: imbalanceLabel,
|
status: imbalanceLabel,
|
||||||
})}
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -28,14 +28,10 @@ describe("config env parsing", () => {
|
|||||||
process.env.EXCHANGE = "standx";
|
process.env.EXCHANGE = "standx";
|
||||||
process.env.MAKER_POINTS_STOP_LOSS_USD = "1 # comment";
|
process.env.MAKER_POINTS_STOP_LOSS_USD = "1 # comment";
|
||||||
process.env.MAKER_POINTS_CLOSE_THRESHOLD = "2 ; comment";
|
process.env.MAKER_POINTS_CLOSE_THRESHOLD = "2 ; comment";
|
||||||
process.env.MAKER_POINTS_BINANCE_DEPTH_WINDOW_BPS = "6 # comment";
|
|
||||||
process.env.MAKER_POINTS_BINANCE_DEPTH_IMBALANCE_RATIO = "9 ; comment";
|
|
||||||
|
|
||||||
const { makerPointsConfig } = await loadConfig();
|
const { makerPointsConfig } = await loadConfig();
|
||||||
expect(makerPointsConfig.stopLossUsd).toBe(1);
|
expect(makerPointsConfig.stopLossUsd).toBe(1);
|
||||||
expect(makerPointsConfig.closeThreshold).toBe(2);
|
expect(makerPointsConfig.closeThreshold).toBe(2);
|
||||||
expect(makerPointsConfig.binanceDepthWindowBps).toBe(6);
|
|
||||||
expect(makerPointsConfig.binanceDepthImbalanceRatio).toBe(9);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("parses boolean maker-points env values with inline comments", async () => {
|
it("parses boolean maker-points env values with inline comments", async () => {
|
||||||
@@ -46,3 +42,4 @@ describe("config env parsing", () => {
|
|||||||
expect(makerPointsConfig.enableBand10To30).toBe(false);
|
expect(makerPointsConfig.enableBand10To30).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
|
||||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
|
||||||
import type { AsterAccountSnapshot, AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../src/exchanges/types";
|
|
||||||
import { t } from "../src/i18n";
|
|
||||||
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
|
|
||||||
|
|
||||||
class StubAdapter implements ExchangeAdapter {
|
|
||||||
id = "standx";
|
|
||||||
|
|
||||||
supportsTrailingStops(): boolean {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {}
|
|
||||||
watchOrders(_cb: (orders: AsterOrder[]) => void): void {}
|
|
||||||
watchDepth(_symbol: string, _cb: (depth: AsterDepth) => void): void {}
|
|
||||||
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {}
|
|
||||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {}
|
|
||||||
|
|
||||||
async createOrder(): Promise<AsterOrder> {
|
|
||||||
throw new Error("not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
async cancelOrder(): Promise<void> {}
|
|
||||||
async cancelOrders(): Promise<void> {}
|
|
||||||
async cancelAllOrders(): Promise<void> {}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("MakerPointsEngine Binance depth monitor config", () => {
|
|
||||||
it("uses default 3bps window and ratio 9", () => {
|
|
||||||
const engine = new MakerPointsEngine(
|
|
||||||
{
|
|
||||||
symbol: "BTC-USD",
|
|
||||||
perOrderAmount: 0.01,
|
|
||||||
closeThreshold: 0,
|
|
||||||
stopLossUsd: 1,
|
|
||||||
refreshIntervalMs: 500,
|
|
||||||
maxLogEntries: 20,
|
|
||||||
maxCloseSlippagePct: 0.05,
|
|
||||||
priceTick: 0.1,
|
|
||||||
qtyStep: 0.001,
|
|
||||||
enableBand0To10: true,
|
|
||||||
enableBand10To30: false,
|
|
||||||
enableBand30To100: false,
|
|
||||||
band0To10Amount: 0.01,
|
|
||||||
band10To30Amount: 0.01,
|
|
||||||
band30To100Amount: 0.01,
|
|
||||||
minRepriceBps: 3,
|
|
||||||
enableBinanceDepthCancel: true,
|
|
||||||
filterMinDepth: 0,
|
|
||||||
},
|
|
||||||
new StubAdapter()
|
|
||||||
);
|
|
||||||
|
|
||||||
const trackerOptions = ((engine as any).binanceDepth as { options?: { depthWindowBps?: number; ratio?: number } })
|
|
||||||
.options;
|
|
||||||
|
|
||||||
expect(trackerOptions?.depthWindowBps).toBe(3);
|
|
||||||
expect(trackerOptions?.ratio).toBe(9);
|
|
||||||
|
|
||||||
engine.stop();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uses configured window and ratio", () => {
|
|
||||||
const engine = new MakerPointsEngine(
|
|
||||||
{
|
|
||||||
symbol: "BTC-USD",
|
|
||||||
perOrderAmount: 0.01,
|
|
||||||
closeThreshold: 0,
|
|
||||||
stopLossUsd: 1,
|
|
||||||
refreshIntervalMs: 500,
|
|
||||||
maxLogEntries: 20,
|
|
||||||
maxCloseSlippagePct: 0.05,
|
|
||||||
priceTick: 0.1,
|
|
||||||
qtyStep: 0.001,
|
|
||||||
enableBand0To10: true,
|
|
||||||
enableBand10To30: false,
|
|
||||||
enableBand30To100: false,
|
|
||||||
band0To10Amount: 0.01,
|
|
||||||
band10To30Amount: 0.01,
|
|
||||||
band30To100Amount: 0.01,
|
|
||||||
minRepriceBps: 3,
|
|
||||||
enableBinanceDepthCancel: true,
|
|
||||||
binanceDepthWindowBps: 7,
|
|
||||||
binanceDepthImbalanceRatio: 11,
|
|
||||||
filterMinDepth: 0,
|
|
||||||
},
|
|
||||||
new StubAdapter()
|
|
||||||
);
|
|
||||||
|
|
||||||
const trackerOptions = ((engine as any).binanceDepth as { options?: { depthWindowBps?: number; ratio?: number } })
|
|
||||||
.options;
|
|
||||||
|
|
||||||
expect(trackerOptions?.depthWindowBps).toBe(7);
|
|
||||||
expect(trackerOptions?.ratio).toBe(11);
|
|
||||||
|
|
||||||
engine.stop();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders binance depth line with dynamic window bps", () => {
|
|
||||||
const line = t(
|
|
||||||
"makerPoints.binanceLine",
|
|
||||||
{ windowBps: 5, buy: "1.23", sell: "1.11", status: "Balanced" },
|
|
||||||
"en"
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(line).toContain("±5bps");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
||||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
|
||||||
import type { AsterAccountSnapshot, AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../src/exchanges/types";
|
|
||||||
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
|
|
||||||
|
|
||||||
class StubAdapter implements ExchangeAdapter {
|
|
||||||
id = "standx";
|
|
||||||
|
|
||||||
private depthListeners: Array<(depth: AsterDepth) => void> = [];
|
|
||||||
|
|
||||||
supportsTrailingStops(): boolean {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {}
|
|
||||||
watchOrders(_cb: (orders: AsterOrder[]) => void): void {}
|
|
||||||
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {}
|
|
||||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {}
|
|
||||||
|
|
||||||
watchDepth(_symbol: string, cb: (depth: AsterDepth) => void): void {
|
|
||||||
this.depthListeners.push(cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
emitDepth(depth: AsterDepth): void {
|
|
||||||
for (const listener of this.depthListeners) {
|
|
||||||
listener(depth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async createOrder(): Promise<AsterOrder> {
|
|
||||||
throw new Error("not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
async cancelOrder(): Promise<void> {}
|
|
||||||
async cancelOrders(): Promise<void> {}
|
|
||||||
async cancelAllOrders(): Promise<void> {}
|
|
||||||
|
|
||||||
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
vi.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("MakerPointsEngine immediate depth protection", () => {
|
|
||||||
it("triggers an immediate tick when depth drops below threshold", () => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
const adapter = new StubAdapter();
|
|
||||||
|
|
||||||
const engine = new MakerPointsEngine(
|
|
||||||
{
|
|
||||||
symbol: "BTC-USD",
|
|
||||||
perOrderAmount: 0.01,
|
|
||||||
closeThreshold: 0,
|
|
||||||
stopLossUsd: 1,
|
|
||||||
refreshIntervalMs: 10_000,
|
|
||||||
maxLogEntries: 20,
|
|
||||||
maxCloseSlippagePct: 0.05,
|
|
||||||
priceTick: 0.1,
|
|
||||||
qtyStep: 0.001,
|
|
||||||
enableBand0To10: true,
|
|
||||||
enableBand10To30: false,
|
|
||||||
enableBand30To100: false,
|
|
||||||
band0To10Amount: 0.01,
|
|
||||||
band10To30Amount: 0.01,
|
|
||||||
band30To100Amount: 0.01,
|
|
||||||
minRepriceBps: 3,
|
|
||||||
enableBinanceDepthCancel: false,
|
|
||||||
filterMinDepth: 10,
|
|
||||||
},
|
|
||||||
adapter
|
|
||||||
);
|
|
||||||
|
|
||||||
(engine as any).feedStatus = { account: true, depth: true, ticker: true, orders: true, binance: true };
|
|
||||||
(engine as any).initialOrderSnapshotReady = true;
|
|
||||||
(engine as any).defenseMode = false;
|
|
||||||
(engine as any).reconnectResetPending = false;
|
|
||||||
(engine as any).stopLossProcessing = false;
|
|
||||||
(engine as any).lastDepthOkStatus[9] = { buy: true, sell: true };
|
|
||||||
|
|
||||||
const tickSpy = vi.spyOn(engine as any, "tick").mockResolvedValue(undefined);
|
|
||||||
|
|
||||||
adapter.emitDepth({
|
|
||||||
lastUpdateId: 1,
|
|
||||||
bids: [["100", "1"]],
|
|
||||||
asks: [["101", "1"]],
|
|
||||||
eventTime: Date.now(),
|
|
||||||
symbol: "BTC-USD",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(tickSpy).toHaveBeenCalledTimes(1);
|
|
||||||
engine.stop();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
||||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
|
||||||
import type { AsterAccountSnapshot, AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../src/exchanges/types";
|
|
||||||
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
|
|
||||||
|
|
||||||
class StubAdapter implements ExchangeAdapter {
|
|
||||||
id = "standx";
|
|
||||||
|
|
||||||
private depthListeners: Array<(depth: AsterDepth) => void> = [];
|
|
||||||
|
|
||||||
supportsTrailingStops(): boolean {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {}
|
|
||||||
watchOrders(_cb: (orders: AsterOrder[]) => void): void {}
|
|
||||||
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {}
|
|
||||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {}
|
|
||||||
|
|
||||||
watchDepth(_symbol: string, cb: (depth: AsterDepth) => void): void {
|
|
||||||
this.depthListeners.push(cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
emitDepth(depth: AsterDepth): void {
|
|
||||||
for (const listener of this.depthListeners) {
|
|
||||||
listener(depth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async createOrder(): Promise<AsterOrder> {
|
|
||||||
throw new Error("not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
async cancelOrder(): Promise<void> {}
|
|
||||||
async cancelOrders(): Promise<void> {}
|
|
||||||
async cancelAllOrders(): Promise<void> {}
|
|
||||||
|
|
||||||
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
vi.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("MakerPointsEngine immediate reprice", () => {
|
|
||||||
it("triggers an immediate tick when min reprice bps threshold is reached", () => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
const adapter = new StubAdapter();
|
|
||||||
|
|
||||||
const engine = new MakerPointsEngine(
|
|
||||||
{
|
|
||||||
symbol: "BTC-USD",
|
|
||||||
perOrderAmount: 0.01,
|
|
||||||
closeThreshold: 0,
|
|
||||||
stopLossUsd: 1,
|
|
||||||
refreshIntervalMs: 10_000,
|
|
||||||
maxLogEntries: 20,
|
|
||||||
maxCloseSlippagePct: 0.05,
|
|
||||||
priceTick: 0.1,
|
|
||||||
qtyStep: 0.001,
|
|
||||||
enableBand0To10: true,
|
|
||||||
enableBand10To30: false,
|
|
||||||
enableBand30To100: false,
|
|
||||||
band0To10Amount: 0.01,
|
|
||||||
band10To30Amount: 0.01,
|
|
||||||
band30To100Amount: 0.01,
|
|
||||||
minRepriceBps: 3,
|
|
||||||
enableBinanceDepthCancel: false,
|
|
||||||
filterMinDepth: 0,
|
|
||||||
},
|
|
||||||
adapter
|
|
||||||
);
|
|
||||||
|
|
||||||
(engine as any).feedStatus = { account: true, depth: true, ticker: true, orders: true, binance: true };
|
|
||||||
(engine as any).initialOrderSnapshotReady = true;
|
|
||||||
(engine as any).defenseMode = false;
|
|
||||||
(engine as any).reconnectResetPending = false;
|
|
||||||
(engine as any).stopLossProcessing = false;
|
|
||||||
(engine as any).lastQuoteBid1 = 100;
|
|
||||||
(engine as any).lastQuoteAsk1 = 101;
|
|
||||||
(engine as any).openOrders = [
|
|
||||||
{
|
|
||||||
orderId: 1,
|
|
||||||
clientOrderId: "entry-order",
|
|
||||||
symbol: "BTC-USD",
|
|
||||||
side: "BUY",
|
|
||||||
type: "LIMIT",
|
|
||||||
status: "NEW",
|
|
||||||
price: "99.0",
|
|
||||||
origQty: "0.01",
|
|
||||||
executedQty: "0",
|
|
||||||
stopPrice: "0",
|
|
||||||
time: Date.now(),
|
|
||||||
updateTime: Date.now(),
|
|
||||||
reduceOnly: false,
|
|
||||||
closePosition: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const tickSpy = vi.spyOn(engine as any, "tick").mockResolvedValue(undefined);
|
|
||||||
|
|
||||||
adapter.emitDepth({
|
|
||||||
lastUpdateId: 1,
|
|
||||||
bids: [["99.9", "1"]],
|
|
||||||
asks: [["100.9", "1"]],
|
|
||||||
eventTime: Date.now(),
|
|
||||||
symbol: "BTC-USD",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(tickSpy).toHaveBeenCalledTimes(1);
|
|
||||||
engine.stop();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
|
||||||
import { getDepthBetweenPrices } from "../src/utils/price";
|
|
||||||
import type { AsterDepth } from "../src/exchanges/types";
|
|
||||||
|
|
||||||
describe("getDepthBetweenPrices boundary", () => {
|
|
||||||
it("SELL side excludes quantity exactly at target price", () => {
|
|
||||||
const depth: AsterDepth = {
|
|
||||||
lastUpdateId: 1,
|
|
||||||
bids: [],
|
|
||||||
asks: [
|
|
||||||
["69345", "1"],
|
|
||||||
["69349", "2"],
|
|
||||||
["69350", "999"],
|
|
||||||
["69351", "3"],
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const total = getDepthBetweenPrices(depth, "SELL", 69350);
|
|
||||||
expect(total).toBe(3); // 仅 69345 + 69349
|
|
||||||
});
|
|
||||||
|
|
||||||
it("BUY side excludes quantity exactly at target price", () => {
|
|
||||||
const depth: AsterDepth = {
|
|
||||||
lastUpdateId: 1,
|
|
||||||
bids: [
|
|
||||||
["69355", "1"],
|
|
||||||
["69351", "2"],
|
|
||||||
["69350", "999"],
|
|
||||||
["69349", "3"],
|
|
||||||
],
|
|
||||||
asks: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const total = getDepthBetweenPrices(depth, "BUY", 69350);
|
|
||||||
expect(total).toBe(3); // 仅 69355 + 69351
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user