Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
317 changes: 255 additions & 62 deletions scripts/binance-orderbook-trade.user.js

Large diffs are not rendered by default.

24 changes: 15 additions & 9 deletions src/binance-orderbook-trade/core/auto-open-leverage.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
const POSITION_STATUSES = new Set(['unknown', 'has_position', 'flat']);

function createPositionPayloadContractError(message) {
const error = new Error(message);
error.name = 'PositionPayloadContractError';
return error;
}

function parsePositionAmount(value) {
if (typeof value === 'number' && Number.isFinite(value)) return value;
if (typeof value === 'string' && /^-?(?:\d+\.?\d*|\.\d+)$/.test(value.trim())) {
return Number(value);
}
throw new Error(`持仓数量无效:${String(value)}`);
throw createPositionPayloadContractError(`持仓数量无效:${String(value)}`);
}

export function resolveSymbolPositionStatus(payload, symbol) {
if (payload?.success !== true) throw new Error('持仓接口返回失败');
if (!Array.isArray(payload.data)) throw new Error('持仓接口数据格式异常');
if (!symbol) throw new Error('持仓接口缺少交易对');
if (payload?.success !== true) throw createPositionPayloadContractError('持仓接口返回失败');
if (!Array.isArray(payload.data)) throw createPositionPayloadContractError('持仓接口数据格式异常');
if (!symbol) throw createPositionPayloadContractError('持仓接口缺少交易对');

const positions = payload.data.filter((position) => position?.symbol === symbol);
const hasPosition = positions.some((position) => parsePositionAmount(position.positionAmount) !== 0);
Expand All @@ -22,11 +28,11 @@ export function resolveSymbolPositionStatus(payload, symbol) {
}

export function resolveSymbolPositionSideStatus(payload, symbol, side) {
if (payload?.success !== true) throw new Error('持仓接口返回失败');
if (!Array.isArray(payload.data)) throw new Error('持仓接口数据格式异常');
if (!symbol) throw new Error('持仓接口缺少交易对');
if (payload?.success !== true) throw createPositionPayloadContractError('持仓接口返回失败');
if (!Array.isArray(payload.data)) throw createPositionPayloadContractError('持仓接口数据格式异常');
if (!symbol) throw createPositionPayloadContractError('持仓接口缺少交易对');
if (side !== 'LONG' && side !== 'SHORT') {
throw new Error(`目标持仓方向无效:${String(side)}`);
throw createPositionPayloadContractError(`目标持仓方向无效:${String(side)}`);
}

const positions = payload.data.filter((position) => position?.symbol === symbol);
Expand All @@ -35,7 +41,7 @@ export function resolveSymbolPositionSideStatus(payload, symbol, side) {
for (const position of positions) {
const positionSide = position.positionSide;
if (!['BOTH', 'LONG', 'SHORT'].includes(positionSide)) {
throw new Error(`持仓方向无效:${String(positionSide)}`);
throw createPositionPayloadContractError(`持仓方向无效:${String(positionSide)}`);
}
const amount = parsePositionAmount(position.positionAmount);
if (positionSide === side) {
Expand Down
37 changes: 33 additions & 4 deletions src/binance-orderbook-trade/core/continuous-ladder.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,25 @@ import {
export const CONTINUOUS_LADDER_COOLDOWN_MS = 1000;
export const CONTINUOUS_LADDER_READY_CHECK_MS = 50;
export const CONTINUOUS_LADDER_RECOVERY_COOLDOWN_MS = 3000;
export const CONTINUOUS_LADDER_LONG_RECOVERY_COOLDOWN_MS = 10000;

const CONTINUOUS_LADDER_RECOVERY = Object.freeze({
submit_unconfirmed: {
cooldownMs: CONTINUOUS_LADDER_RECOVERY_COOLDOWN_MS,
requiresSafeNoSubmit: false,
},
rate_limited: {
cooldownMs: CONTINUOUS_LADDER_LONG_RECOVERY_COOLDOWN_MS,
requiresSafeNoSubmit: false,
},
order_capacity_not_ready: {
cooldownMs: CONTINUOUS_LADDER_RECOVERY_COOLDOWN_MS,
requiresSafeNoSubmit: false,
},
open_orders_not_ready: {
cooldownMs: CONTINUOUS_LADDER_RECOVERY_COOLDOWN_MS,
requiresSafeNoSubmit: false,
},
input_unstable: {
cooldownMs: CONTINUOUS_LADDER_RECOVERY_COOLDOWN_MS,
},
Expand All @@ -26,6 +43,12 @@ const CONTINUOUS_LADDER_RECOVERY = Object.freeze({
market_data_not_ready: {
cooldownMs: CONTINUOUS_LADDER_RECOVERY_COOLDOWN_MS,
},
position_state_not_ready: {
cooldownMs: CONTINUOUS_LADDER_RECOVERY_COOLDOWN_MS,
},
position_quantity_not_ready: {
cooldownMs: CONTINUOUS_LADDER_LONG_RECOVERY_COOLDOWN_MS,
},
precision_changed: {
cooldownMs: CONTINUOUS_LADDER_COOLDOWN_MS,
reason: localizedText(
Expand Down Expand Up @@ -85,14 +108,20 @@ export function createContinuousLadderProgress() {
}

/**
* Recovers only when the failed submission attempt is proven not to have sent an order.
* Most recoveries require proof that no order was submitted. The explicit
* submit_unconfirmed policy accepts a possible duplicate in continuous mode
* and advances to a new round instead of retrying the uncertain order in place.
*/
export function resolveContinuousLadderRecovery(error) {
if (error?.safeNoSubmit !== true) return null;
const recovery = CONTINUOUS_LADDER_RECOVERY[error.continuousRecoveryKind];
const recovery = CONTINUOUS_LADDER_RECOVERY[error?.continuousRecoveryKind];
if (!recovery) return null;
if (recovery.requiresSafeNoSubmit !== false && error.safeNoSubmit !== true) return null;
const cooldownMs = error.continuousRecoveryCooldownMs ?? recovery.cooldownMs;
if (!Number.isFinite(cooldownMs) || cooldownMs < 0) {
throw new Error('连续阶梯恢复等待时间无效');
}
return {
cooldownMs: recovery.cooldownMs,
cooldownMs,
reason: recovery.reason || error.localizedText || error.message,
};
}
Expand Down
34 changes: 34 additions & 0 deletions src/binance-orderbook-trade/core/order-feedback.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,40 @@ export function isBinanceMaxOpenOrdersErrorCode(code) {
return code === BINANCE_MAX_OPEN_ORDERS_ERROR_CODE;
}

function parseRetryAfterMs(value) {
if (value == null || value === '') return null;
const seconds = Number(value);
return Number.isFinite(seconds) && seconds >= 0 ? seconds * 1000 : null;
}

/**
* HTTP 418/429 requires backoff. HTTP 5xx leaves an order submission outcome
* uncertain, so continuous mode may advance only under its explicit duplicate-
* tolerant policy. Other 4xx and business errors remain terminal.
*/
export function resolveBinanceSubmitResponseRecovery(diagnostics, apiErrors) {
if (!Array.isArray(diagnostics) || !Array.isArray(apiErrors)) {
throw new Error('下单响应恢复证据无效');
}
const rateLimitDiagnostic = diagnostics.find(({ httpStatus }) => (
httpStatus === 418 || httpStatus === 429
));
const hasRateLimitCode = apiErrors.some(({ code }) => code === -1003);
if (rateLimitDiagnostic || hasRateLimitCode) {
return {
kind: 'rate_limited',
cooldownMs: parseRetryAfterMs(rateLimitDiagnostic?.retryAfter) ?? 10000,
};
}
if (diagnostics.some(({ httpStatus }) => httpStatus >= 500 && httpStatus <= 599)) {
return {
kind: 'submit_unconfirmed',
cooldownMs: 3000,
};
}
return null;
}

export function getBinanceApiErrorCode(payload) {
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return null;
if (Number.isSafeInteger(payload.code)) return payload.code === 0 ? null : payload.code;
Expand Down
Loading
Loading