From 679c81b207b402dbbf2356e631c21ef7b5b768bd Mon Sep 17 00:00:00 2001 From: LiZhenhai-MBP14 <5935568+jackhai9@users.noreply.github.com> Date: Fri, 28 Aug 2026 10:05:29 +0800 Subject: [PATCH] fix: simplify Binance status symbols --- .../specs/cancel-covering-matrix.pw.js | 4 +- .../specs/cancel-current-symbol.pw.js | 24 +++---- .../specs/live-performance-probe.pw.js | 6 +- scripts/binance-orderbook-trade.user.js | 68 ++++++++++++------- .../core/status-symbol.js | 14 ++++ src/binance-orderbook-trade/index.user.js | 58 ++++++++-------- .../source-regressions.test.js | 19 +++++- .../status-symbol.test.js | 16 +++++ 8 files changed, 139 insertions(+), 70 deletions(-) create mode 100644 src/binance-orderbook-trade/core/status-symbol.js create mode 100644 test/unit/binance-orderbook-trade/status-symbol.test.js diff --git a/e2e/binance-orderbook/specs/cancel-covering-matrix.pw.js b/e2e/binance-orderbook/specs/cancel-covering-matrix.pw.js index d5c3a7c..a59ed3b 100644 --- a/e2e/binance-orderbook/specs/cancel-covering-matrix.pw.js +++ b/e2e/binance-orderbook/specs/cancel-covering-matrix.pw.js @@ -57,8 +57,8 @@ for (const entry of CANCEL_COVERING_SCENARIOS) { }).click(); await expect(page.getByText( vector.dialogOutcome === 'confirm' - ? `${scenario.currentSymbol} 撤单流程结束,已恢复筛选状态` - : `${scenario.currentSymbol} 已取消撤单,已恢复页面状态`, + ? '撤单流程结束,已恢复筛选状态' + : '已取消撤单,已恢复页面状态', )).toBeVisible(); } diff --git a/e2e/binance-orderbook/specs/cancel-current-symbol.pw.js b/e2e/binance-orderbook/specs/cancel-current-symbol.pw.js index 669a3ee..9cc9eeb 100644 --- a/e2e/binance-orderbook/specs/cancel-current-symbol.pw.js +++ b/e2e/binance-orderbook/specs/cancel-current-symbol.pw.js @@ -94,7 +94,7 @@ test('cancelling the native dialog preserves current and other orders and restor await expect(page.getByRole('dialog')).toBeVisible(); await expect.poll(async () => (await readFixtureState(page)).showOrders).toBe(false); await page.getByRole('button', { name: '取消' }).click(); - await expect(page.getByText('HYPEUSDT 已取消撤单,已恢复页面状态')).toBeVisible(); + await expect(page.getByText('已取消撤单,已恢复页面状态')).toBeVisible(); const state = await readFixtureState(page); expect(state.orders).toEqual(ORDER_SETS.both); @@ -116,7 +116,7 @@ test('confirming with mixed-symbol orders clears only the current symbol', async await page.getByRole('button', { name: '撤单' }).click(); await expect(page.getByRole('dialog')).toBeVisible(); await page.getByRole('button', { name: '确认' }).click(); - await expect(page.getByText('HYPEUSDT 撤单流程结束,已恢复筛选状态')).toBeVisible(); + await expect(page.getByText('撤单流程结束,已恢复筛选状态')).toBeVisible(); const state = await readFixtureState(page); expect(state.orders).toEqual(otherSymbolOrders(scenario)); @@ -136,7 +136,7 @@ test('an originally enabled symbol filter remains enabled after confirmation', a await page.getByRole('button', { name: '撤单' }).click(); await expect(page.getByRole('dialog')).toBeVisible(); await page.getByRole('button', { name: '确认' }).click(); - await expect(page.getByText('HYPEUSDT 撤单流程结束,已恢复筛选状态')).toBeVisible(); + await expect(page.getByText('撤单流程结束,已恢复筛选状态')).toBeVisible(); const state = await readFixtureState(page); expect(state.orders).toEqual(otherSymbolOrders(scenario)); @@ -186,7 +186,7 @@ for (const closeMethod of ['Escape', 'backdrop']) { } else { await page.locator('.bn-modal-root').click({ position: { x: 4, y: 4 } }); } - await expect(page.getByText('HYPEUSDT 已取消撤单,已恢复页面状态')).toBeVisible(); + await expect(page.getByText('已取消撤单,已恢复页面状态')).toBeVisible(); const state = await readFixtureState(page); expect(state.orders).toEqual(ORDER_SETS.both); @@ -211,7 +211,7 @@ test('a BFCache pagehide does not abort the active native dialog', async ({ page }))); await expect(page.getByRole('dialog')).toBeVisible(); await page.getByRole('button', { name: '取消' }).click(); - await expect(page.getByText('HYPEUSDT 已取消撤单,已恢复页面状态')).toBeVisible(); + await expect(page.getByText('已取消撤单,已恢复页面状态')).toBeVisible(); await expectRestoredState(page, scenario); expect(errors).toEqual([]); }); @@ -229,7 +229,7 @@ test('a real pagehide aborts dialog tracking without mutating orders', async ({ await page.evaluate(() => window.dispatchEvent(new PageTransitionEvent('pagehide', { persisted: false, }))); - await expect(page.getByText('HYPEUSDT 页面已离开,撤单确认跟踪已停止')).toBeVisible(); + await expect(page.getByText('原交易对 HYPE 页面已离开,撤单确认跟踪已停止')).toBeVisible(); const state = await readFixtureState(page); expect(state.orders).toEqual(ORDER_SETS.current); @@ -249,7 +249,7 @@ test('a missing native dialog stops cleanly and restores temporary UI state', as const { errors } = await openUserscriptScenario(page, scenario); await page.getByRole('button', { name: '撤单' }).click(); - await expect(page.getByText('HYPEUSDT 未识别到撤单确认弹窗,未继续撤单流程')).toBeVisible({ + await expect(page.getByText('未识别到撤单确认弹窗,未继续撤单流程')).toBeVisible({ timeout: 3_000, }); @@ -271,7 +271,7 @@ for (const dialogMode of ['extraAction', 'missingPrimary']) { await page.getByRole('button', { name: '撤单' }).click(); await expect(page.getByText( - 'HYPEUSDT 撤单确认弹窗结构异常,未执行弹窗操作', + '撤单确认弹窗结构异常,未执行弹窗操作', )).toBeVisible(); const state = await readFixtureState(page); @@ -299,8 +299,8 @@ test('a delayed confirmation keeps visible progress and clears only the current await page.getByRole('button', { name: '撤单' }).click(); await page.getByRole('button', { name: '确认' }).click(); - await expect(page.getByText('HYPEUSDT 已确认撤单,等待当前币挂单清空')).toBeVisible(); - await expect(page.getByText('HYPEUSDT 撤单流程结束,已恢复筛选状态')).toBeVisible(); + await expect(page.getByText('已确认撤单,等待当前币挂单清空')).toBeVisible(); + await expect(page.getByText('撤单流程结束,已恢复筛选状态')).toBeVisible(); const state = await readFixtureState(page); expect(state.orders).toEqual(otherSymbolOrders(scenario)); @@ -322,7 +322,7 @@ test('dialog tracking survives React replacing the native dialog subtree', async await readFixtureState(page) ).events.filter((event) => event.type === 'dialog-replaced').length).toBe(1); await page.getByRole('button', { name: '取消' }).click(); - await expect(page.getByText('HYPEUSDT 已取消撤单,已恢复页面状态')).toBeVisible(); + await expect(page.getByText('已取消撤单,已恢复页面状态')).toBeVisible(); const state = await readFixtureState(page); expect(state.orders).toEqual(ORDER_SETS.current); @@ -342,7 +342,7 @@ test('a confirmed dialog that does not clear current orders reports incomplete c await page.getByRole('button', { name: '撤单' }).click(); await page.getByRole('button', { name: '确认' }).click(); - await expect(page.getByText('HYPEUSDT 当前币挂单仍存在,撤单流程未完成')).toBeVisible({ + await expect(page.getByText('当前币挂单仍存在,撤单流程未完成')).toBeVisible({ timeout: 10_000, }); diff --git a/e2e/binance-orderbook/specs/live-performance-probe.pw.js b/e2e/binance-orderbook/specs/live-performance-probe.pw.js index ed20e62..fbea02e 100644 --- a/e2e/binance-orderbook/specs/live-performance-probe.pw.js +++ b/e2e/binance-orderbook/specs/live-performance-probe.pw.js @@ -33,7 +33,7 @@ test('live probe captures a no-order run and destroys every listener', async ({ const firstFeedback = snapshot.events.find((event) => event.kind === 'first-feedback'); expect(snapshot.finishedAtMonotonicMs - snapshot.startedAtMonotonicMs - firstFeedback.atMs) .toBeLessThan(50); - expect(snapshot.lastSemanticState.statusText).toBe('HYPEUSDT 当前币无挂单'); + expect(snapshot.lastSemanticState.statusText).toBe('当前币无挂单'); const finishedEventCount = snapshot.events.length; await page.evaluate(() => { @@ -75,7 +75,7 @@ test('live probe has no user-decision deadline and follows a replaced portal dia await page.getByRole('button', { name: '取消' }).click(); const snapshot = await finishLivePerformanceProbeWhenReady(page); - await expect(page.getByText('HYPEUSDT 已取消撤单,已恢复页面状态')).toBeVisible(); + await expect(page.getByText('已取消撤单,已恢复页面状态')).toBeVisible(); expect(() => validateLivePerformanceProbeSnapshot(snapshot)).not.toThrow(); expect(snapshot.events.map((event) => event.kind)).toEqual(expect.arrayContaining([ 'dialog-visible', @@ -102,7 +102,7 @@ test('live completion waits for confirmed cancellation cleanup', async ({ page } expect(() => validateLivePerformanceProbeSnapshot(snapshot)).not.toThrow(); expect(snapshot.events.find((event) => event.kind === 'dialog-action')?.detail?.primary).toBe(true); - expect(snapshot.lastSemanticState.statusText).toBe('HYPEUSDT 撤单流程结束,已恢复筛选状态'); + expect(snapshot.lastSemanticState.statusText).toBe('撤单流程结束,已恢复筛选状态'); await destroyLivePerformanceProbe(page); }); diff --git a/scripts/binance-orderbook-trade.user.js b/scripts/binance-orderbook-trade.user.js index b8d1e66..6448abe 100644 --- a/scripts/binance-orderbook-trade.user.js +++ b/scripts/binance-orderbook-trade.user.js @@ -3,7 +3,7 @@ // @namespace binance.orderbook.trade // @icon data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E // @icon64 data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E -// @version 2.7.143 +// @version 2.7.144 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -926,6 +926,21 @@ }); } + // src/binance-orderbook-trade/core/status-symbol.js + var STATUS_QUOTE_ASSETS = Object.freeze(["USDT", "USDC"]); + function formatStatusBaseAsset(symbol) { + if (typeof symbol !== "string") { + throw new Error(`Unsupported futures status symbol: ${symbol}`); + } + const normalized = symbol.trim().toUpperCase(); + const quoteAsset = STATUS_QUOTE_ASSETS.find((candidate) => normalized.endsWith(candidate)); + const baseAsset = quoteAsset ? normalized.slice(0, -quoteAsset.length) : ""; + if (!baseAsset) { + throw new Error(`Unsupported futures status symbol: ${symbol}`); + } + return baseAsset; + } + // src/binance-orderbook-trade/core/order-feedback.js function isPotentialOrderFeedbackText(text) { if (!text) return false; @@ -4744,7 +4759,7 @@ { allowPartial: true } )[0]; if (!row) { - throw new Error(`${plan.symbol} 当前币可撤挂单数量不足,已停止重挂`); + throw new Error("当前币可撤挂单数量不足,已停止重挂"); } currentRoot = row.root || currentRoot; const previousKeyCount = countOpenOrderRowsByKey(row.root, plan.symbol, row.key); @@ -4769,18 +4784,18 @@ throw new Error("逐行撤单时交易对已变化"); } if (outcome.status === "timeout") { - throw new Error(`${plan.symbol} 当前币挂单仍存在,已停止重挂`); + throw new Error("当前币挂单仍存在,已停止重挂"); } if (outcome.status === "dialog_open") { const { dialog } = outcome; - setLadderStatus(`${plan.spec.label}:${plan.symbol} 单行撤单确认弹窗已打开`); + setLadderStatus(`${plan.spec.label}:单行撤单确认弹窗已打开`); const dialogClosed = await waitForDialogToClose( dialog, ROW_CANCEL_DIALOG_CLOSE_TIMEOUT_MS, abortSignal ); if (!dialogClosed) { - const error = new Error(`${plan.symbol} 单行撤单确认弹窗仍未关闭,未恢复页面状态`); + const error = new Error("单行撤单确认弹窗仍未关闭,未恢复页面状态"); error.name = "DialogNotClosedError"; throw error; } @@ -4790,7 +4805,7 @@ previousKeyCount, abortSignal )) { - throw new Error(`${plan.symbol} 当前币挂单仍存在,已停止重挂`); + throw new Error("当前币挂单仍存在,已停止重挂"); } } throwIfAborted(abortSignal); @@ -4800,7 +4815,7 @@ previousKeyCount, abortSignal )) { - throw new Error(`${plan.symbol} 当前币挂单状态不稳定,已停止重挂`); + throw new Error("当前币挂单状态不稳定,已停止重挂"); } cancelQty = addDecimalStrings(cancelQty, row.qty); recordLadderCancelledOrder(progress); @@ -5091,7 +5106,7 @@ return { ok: false, status: "symbol_changing", message }; } if (getOpenOrdersTabCount() === 0) { - setLadderStatus(`${symbol} 当前币无挂单`); + setLadderStatus("当前币无挂单"); return { ok: true, status: "no_orders" }; } const previousAccountOrdersTabIdentity = getAccountOrdersTabIdentity2(findSelectedAccountOrdersTab2()); @@ -5148,7 +5163,7 @@ return { ok: false, status: "symbol_changed", message }; } if (!openOrdersEvidence.hasOrders) { - setLadderStatus(`${symbol} 当前币无挂单`); + setLadderStatus("当前币无挂单"); return { ok: true, status: "no_orders" }; } let cancelAllButton = openOrdersEvidence.cancelAllButton; @@ -5203,12 +5218,12 @@ dialogDecision = await waitForBinanceCancelAllDialogDecision( dialogDecisionWatcher.watcher, dialogDecisionWatcher.lifecycleSignal, - () => setLadderStatus(`${symbol} 撤单确认弹窗已打开`) + () => setLadderStatus("撤单确认弹窗已打开") ); } catch (error) { restoreTemporaryUiState = false; emit("ERR", "币安撤单确认弹窗结构异常", error); - const message = `${symbol} 撤单确认弹窗结构异常,未执行弹窗操作`; + const message = "撤单确认弹窗结构异常,未执行弹窗操作"; setLadderStatus(message); return { ok: false, status: "dialog_contract_invalid", message }; } finally { @@ -5217,7 +5232,8 @@ if (dialogDecision.status === "aborted") { restoreTemporaryUiState = false; restoreTradingViewOrdersState = false; - const message = `${symbol} 页面已离开,撤单确认跟踪已停止`; + const interruptedBaseAsset = formatStatusBaseAsset(symbol); + const message = `原交易对 ${interruptedBaseAsset} 页面已离开,撤单确认跟踪已停止`; setLadderStatus(message); return { ok: false, status: "aborted", message }; } @@ -5227,17 +5243,17 @@ return { ok: false, status: "symbol_changed", message }; } if (dialogDecision.status === "not_found") { - const message = `${symbol} 未识别到撤单确认弹窗,未继续撤单流程`; + const message = "未识别到撤单确认弹窗,未继续撤单流程"; setLadderStatus(message); return { ok: false, status: "dialog_not_found", message }; } if (dialogDecision.status === "cancelled") { - const message = `${symbol} 已取消撤单`; + const message = "已取消撤单"; successStatusMessage = `${message},已恢复页面状态`; return { ok: false, status: "cancelled", message }; } waitForTradeUiMutation({ timeoutMs: 800 }); - setLadderStatus(`${symbol} 已确认撤单,等待当前币挂单清空`); + setLadderStatus("已确认撤单,等待当前币挂单清空"); openOrdersScope = await waitForActiveOpenOrdersScope(); if (!openOrdersScope || !isCurrentObservedSymbol(symbol)) { const message = "未定位到当前委托面板"; @@ -5262,11 +5278,11 @@ setLadderStatus(message2); return { ok: false, status: "symbol_filter_not_confirmed", message: message2 }; } - const message = waitUntilCleared ? `${symbol} 当前币挂单仍存在,已停止重挂` : `${symbol} 当前币挂单仍存在,撤单流程未完成`; + const message = waitUntilCleared ? "当前币挂单仍存在,已停止重挂" : "当前币挂单仍存在,撤单流程未完成"; setLadderStatus(message); return { ok: false, status: "not_cleared", message }; } - successStatusMessage = waitUntilCleared ? `${symbol} 当前币挂单已撤,继续重挂` : `${symbol} 撤单流程结束,已恢复筛选状态`; + successStatusMessage = waitUntilCleared ? "当前币挂单已撤,继续重挂" : "撤单流程结束,已恢复筛选状态"; return { ok: true, status: "cleared" }; } finally { let temporaryUiRestoreSucceeded = true; @@ -5357,7 +5373,7 @@ let symbolFilterOriginalChecked = null; let restoreTemporaryUiState = true; try { - setPlanStepStatus(`查找 ${symbol} 当前委托`); + setPlanStepStatus("查找当前委托"); const tabReady = await activateOpenOrdersTab(abortSignal); throwIfAborted(abortSignal); if (!tabReady || !isCurrentObservedSymbol(symbol)) { @@ -5422,20 +5438,20 @@ throwIfAborted(abortSignal); if (!rows.length) { const directionLabel = getPlanDirectionLabel(plan); - const message = `未定位到 ${symbol}${directionLabel ? ` ${directionLabel}` : ""} 当前币可逐行撤单的基础单`; + const message = `未定位到当前币的${directionLabel || ""}可撤基础单`; setPlanStepStatus(message); return { ok: false, status: "rows_not_found", message }; } const rowsToCancel = selectOpenOrderRowsToCancelForPlan(plan, rows); if (!rowsToCancel.length) { - const message = `未选中 ${symbol} 当前币待撤挂单`; + const message = "未选中当前币待撤挂单"; setPlanStepStatus(message); return { ok: false, status: "rows_not_selected", message }; } - setPlanStepStatus(`${symbol} 撤销 ${rowsToCancel.length} 笔当前币挂单`); + setPlanStepStatus(`撤销 ${rowsToCancel.length} 笔当前币挂单`); await cancelOpenOrderRowsForPlan(openOrdersScope, plan, progress, abortSignal); throwIfAborted(abortSignal); - setPlanStepStatus(`${symbol} 当前币挂单已替换,继续重挂`); + setPlanStepStatus("当前币挂单已替换,继续重挂"); return { ok: true, status: "rows_cleared" }; } catch (e) { if (isLadderStoppedError(e)) throw e; @@ -5487,9 +5503,9 @@ } function formatOpenOrdersReplacementStatus(plan) { if (plan?.spec?.mode === "OPEN") { - return `${plan.spec.label}:${plan.symbol} 同向开仓挂单可能占用可开数量,准备替换`; + return `${plan.spec.label}:同向开仓挂单可能占用可开数量,准备替换`; } - return `${plan.spec.label}:${plan.symbol} 当前挂单占用可平数量,准备替换`; + return `${plan.spec.label}:当前挂单占用可平数量,准备替换`; } function createLadderExpectedContext(plan) { return { @@ -7080,7 +7096,9 @@ }); const currentSymbol = getCurrentSymbol(); if (!isCurrentObservedSymbol(qtyPlan.symbol)) { - throw new Error(`交易对已变化,点击时 ${qtyPlan.symbol},当前 ${currentSymbol || "-"}`); + const clickedBaseAsset = formatStatusBaseAsset(qtyPlan.symbol); + const currentBaseAsset = currentSymbol ? formatStatusBaseAsset(currentSymbol) : "未知"; + throw new Error(`交易对已变化,点击时 ${clickedBaseAsset},当前 ${currentBaseAsset}`); } if (getActiveTradeMode() !== action.mode) { throw new Error("开仓/平仓模式已变化,已停止提交"); diff --git a/src/binance-orderbook-trade/core/status-symbol.js b/src/binance-orderbook-trade/core/status-symbol.js new file mode 100644 index 0000000..ca3aab9 --- /dev/null +++ b/src/binance-orderbook-trade/core/status-symbol.js @@ -0,0 +1,14 @@ +const STATUS_QUOTE_ASSETS = Object.freeze(['USDT', 'USDC']); + +export function formatStatusBaseAsset(symbol) { + if (typeof symbol !== 'string') { + throw new Error(`Unsupported futures status symbol: ${symbol}`); + } + const normalized = symbol.trim().toUpperCase(); + const quoteAsset = STATUS_QUOTE_ASSETS.find((candidate) => normalized.endsWith(candidate)); + const baseAsset = quoteAsset ? normalized.slice(0, -quoteAsset.length) : ''; + if (!baseAsset) { + throw new Error(`Unsupported futures status symbol: ${symbol}`); + } + return baseAsset; +} diff --git a/src/binance-orderbook-trade/index.user.js b/src/binance-orderbook-trade/index.user.js index c921fe1..c84dff4 100644 --- a/src/binance-orderbook-trade/index.user.js +++ b/src/binance-orderbook-trade/index.user.js @@ -3,7 +3,7 @@ // @namespace binance.orderbook.trade // @icon data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E // @icon64 data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E -// @version 2.7.143 +// @version 2.7.144 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -90,6 +90,7 @@ import { throwIfAborted, waitForPromiseOrAbort, } from './core/abort.js'; +import { formatStatusBaseAsset } from './core/status-symbol.js'; import { evaluateOrderSubmitAcknowledgement, getBinanceApiErrorCode, @@ -3372,7 +3373,7 @@ import { { allowPartial: true } )[0]; if (!row) { - throw new Error(`${plan.symbol} 当前币可撤挂单数量不足,已停止重挂`); + throw new Error('当前币可撤挂单数量不足,已停止重挂'); } currentRoot = row.root || currentRoot; const previousKeyCount = countOpenOrderRowsByKey(row.root, plan.symbol, row.key); @@ -3397,18 +3398,18 @@ import { throw new Error('逐行撤单时交易对已变化'); } if (outcome.status === 'timeout') { - throw new Error(`${plan.symbol} 当前币挂单仍存在,已停止重挂`); + throw new Error('当前币挂单仍存在,已停止重挂'); } if (outcome.status === 'dialog_open') { const { dialog } = outcome; - setLadderStatus(`${plan.spec.label}:${plan.symbol} 单行撤单确认弹窗已打开`); + setLadderStatus(`${plan.spec.label}:单行撤单确认弹窗已打开`); const dialogClosed = await waitForDialogToClose( dialog, ROW_CANCEL_DIALOG_CLOSE_TIMEOUT_MS, abortSignal, ); if (!dialogClosed) { - const error = new Error(`${plan.symbol} 单行撤单确认弹窗仍未关闭,未恢复页面状态`); + const error = new Error('单行撤单确认弹窗仍未关闭,未恢复页面状态'); error.name = 'DialogNotClosedError'; throw error; } @@ -3418,7 +3419,7 @@ import { previousKeyCount, abortSignal, ))) { - throw new Error(`${plan.symbol} 当前币挂单仍存在,已停止重挂`); + throw new Error('当前币挂单仍存在,已停止重挂'); } } throwIfAborted(abortSignal); @@ -3428,7 +3429,7 @@ import { previousKeyCount, abortSignal, ))) { - throw new Error(`${plan.symbol} 当前币挂单状态不稳定,已停止重挂`); + throw new Error('当前币挂单状态不稳定,已停止重挂'); } cancelQty = addDecimalStrings(cancelQty, row.qty); recordLadderCancelledOrder(progress); @@ -3765,7 +3766,7 @@ import { return { ok: false, status: 'symbol_changing', message }; } if (getOpenOrdersTabCount() === 0) { - setLadderStatus(`${symbol} 当前币无挂单`); + setLadderStatus('当前币无挂单'); return { ok: true, status: 'no_orders' }; } @@ -3824,7 +3825,7 @@ import { return { ok: false, status: 'symbol_changed', message }; } if (!openOrdersEvidence.hasOrders) { - setLadderStatus(`${symbol} 当前币无挂单`); + setLadderStatus('当前币无挂单'); return { ok: true, status: 'no_orders' }; } @@ -3886,12 +3887,12 @@ import { dialogDecision = await waitForBinanceCancelAllDialogDecision( dialogDecisionWatcher.watcher, dialogDecisionWatcher.lifecycleSignal, - () => setLadderStatus(`${symbol} 撤单确认弹窗已打开`), + () => setLadderStatus('撤单确认弹窗已打开'), ); } catch (error) { restoreTemporaryUiState = false; emit('ERR', '币安撤单确认弹窗结构异常', error); - const message = `${symbol} 撤单确认弹窗结构异常,未执行弹窗操作`; + const message = '撤单确认弹窗结构异常,未执行弹窗操作'; setLadderStatus(message); return { ok: false, status: 'dialog_contract_invalid', message }; } finally { @@ -3900,7 +3901,8 @@ import { if (dialogDecision.status === 'aborted') { restoreTemporaryUiState = false; restoreTradingViewOrdersState = false; - const message = `${symbol} 页面已离开,撤单确认跟踪已停止`; + const interruptedBaseAsset = formatStatusBaseAsset(symbol); + const message = `原交易对 ${interruptedBaseAsset} 页面已离开,撤单确认跟踪已停止`; setLadderStatus(message); return { ok: false, status: 'aborted', message }; } @@ -3910,17 +3912,17 @@ import { return { ok: false, status: 'symbol_changed', message }; } if (dialogDecision.status === 'not_found') { - const message = `${symbol} 未识别到撤单确认弹窗,未继续撤单流程`; + const message = '未识别到撤单确认弹窗,未继续撤单流程'; setLadderStatus(message); return { ok: false, status: 'dialog_not_found', message }; } if (dialogDecision.status === 'cancelled') { - const message = `${symbol} 已取消撤单`; + const message = '已取消撤单'; successStatusMessage = `${message},已恢复页面状态`; return { ok: false, status: 'cancelled', message }; } waitForTradeUiMutation({ timeoutMs: 800 }); - setLadderStatus(`${symbol} 已确认撤单,等待当前币挂单清空`); + setLadderStatus('已确认撤单,等待当前币挂单清空'); openOrdersScope = await waitForActiveOpenOrdersScope(); if (!openOrdersScope || !isCurrentObservedSymbol(symbol)) { @@ -3947,14 +3949,14 @@ import { return { ok: false, status: 'symbol_filter_not_confirmed', message }; } const message = waitUntilCleared - ? `${symbol} 当前币挂单仍存在,已停止重挂` - : `${symbol} 当前币挂单仍存在,撤单流程未完成`; + ? '当前币挂单仍存在,已停止重挂' + : '当前币挂单仍存在,撤单流程未完成'; setLadderStatus(message); return { ok: false, status: 'not_cleared', message }; } successStatusMessage = waitUntilCleared - ? `${symbol} 当前币挂单已撤,继续重挂` - : `${symbol} 撤单流程结束,已恢复筛选状态`; + ? '当前币挂单已撤,继续重挂' + : '撤单流程结束,已恢复筛选状态'; return { ok: true, status: 'cleared' }; } finally { let temporaryUiRestoreSucceeded = true; @@ -4063,7 +4065,7 @@ import { let restoreTemporaryUiState = true; try { - setPlanStepStatus(`查找 ${symbol} 当前委托`); + setPlanStepStatus('查找当前委托'); const tabReady = await activateOpenOrdersTab(abortSignal); throwIfAborted(abortSignal); if (!tabReady || !isCurrentObservedSymbol(symbol)) { @@ -4134,22 +4136,22 @@ import { throwIfAborted(abortSignal); if (!rows.length) { const directionLabel = getPlanDirectionLabel(plan); - const message = `未定位到 ${symbol}${directionLabel ? ` ${directionLabel}` : ''} 当前币可逐行撤单的基础单`; + const message = `未定位到当前币的${directionLabel || ''}可撤基础单`; setPlanStepStatus(message); return { ok: false, status: 'rows_not_found', message }; } const rowsToCancel = selectOpenOrderRowsToCancelForPlan(plan, rows); if (!rowsToCancel.length) { - const message = `未选中 ${symbol} 当前币待撤挂单`; + const message = '未选中当前币待撤挂单'; setPlanStepStatus(message); return { ok: false, status: 'rows_not_selected', message }; } - setPlanStepStatus(`${symbol} 撤销 ${rowsToCancel.length} 笔当前币挂单`); + setPlanStepStatus(`撤销 ${rowsToCancel.length} 笔当前币挂单`); await cancelOpenOrderRowsForPlan(openOrdersScope, plan, progress, abortSignal); throwIfAborted(abortSignal); - setPlanStepStatus(`${symbol} 当前币挂单已替换,继续重挂`); + setPlanStepStatus('当前币挂单已替换,继续重挂'); return { ok: true, status: 'rows_cleared' }; } catch (e) { if (isLadderStoppedError(e)) throw e; @@ -4217,9 +4219,9 @@ import { function formatOpenOrdersReplacementStatus(plan) { if (plan?.spec?.mode === 'OPEN') { - return `${plan.spec.label}:${plan.symbol} 同向开仓挂单可能占用可开数量,准备替换`; + return `${plan.spec.label}:同向开仓挂单可能占用可开数量,准备替换`; } - return `${plan.spec.label}:${plan.symbol} 当前挂单占用可平数量,准备替换`; + return `${plan.spec.label}:当前挂单占用可平数量,准备替换`; } function createLadderExpectedContext(plan) { @@ -6057,7 +6059,9 @@ import { const currentSymbol = getCurrentSymbol(); if (!isCurrentObservedSymbol(qtyPlan.symbol)) { - throw new Error(`交易对已变化,点击时 ${qtyPlan.symbol},当前 ${currentSymbol || '-'}`); + const clickedBaseAsset = formatStatusBaseAsset(qtyPlan.symbol); + const currentBaseAsset = currentSymbol ? formatStatusBaseAsset(currentSymbol) : '未知'; + throw new Error(`交易对已变化,点击时 ${clickedBaseAsset},当前 ${currentBaseAsset}`); } if (getActiveTradeMode() !== action.mode) { throw new Error('开仓/平仓模式已变化,已停止提交'); diff --git a/test/unit/binance-orderbook-trade/source-regressions.test.js b/test/unit/binance-orderbook-trade/source-regressions.test.js index cae2432..838bcaf 100644 --- a/test/unit/binance-orderbook-trade/source-regressions.test.js +++ b/test/unit/binance-orderbook-trade/source-regressions.test.js @@ -800,13 +800,30 @@ test('ladder task statuses name the active action and observed outcome', () => { assert.match(startBody, /formatInterruptedLadderProgress\(\s*spec\.label,\s*'交易对已切换',\s*progress/); assert.match(startBody, /formatFailedLadderProgress\(spec\.label,\s*failureMessage,\s*progress\)/); assert.match(stopBody, /`\$\{activeSpec\.label\}停止中`/); - assert.match(cancelPlanBody, /setPlanStepStatus\(`\$\{symbol\} 撤销 \$\{rowsToCancel\.length\} 笔当前币挂单`\)/); + assert.match(cancelPlanBody, /setPlanStepStatus\(`撤销 \$\{rowsToCancel\.length\} 笔当前币挂单`\)/); assert.match(planStatusBody, /`\$\{plan\.spec\.label\}计划:/); assert.match(replacementStatusBody, /`\$\{plan\.spec\.label\}:/); assert.match(cancelPlanBody, /const setPlanStepStatus = \(message\) =>/); assert.doesNotMatch(cancelPlanBody, /setLadderStatus\(message\)/); }); +test('panel statuses omit the current full symbol and compact the retained interrupted symbol', () => { + const cancelBody = readFunctionBody('runCancelCurrentSymbolOpenOrders'); + const cancelRowsBody = readFunctionBody('cancelOpenOrderRowsForPlan'); + const cancelPlanBody = readFunctionBody('cancelCurrentSymbolOpenOrdersForPlan'); + const replacementStatusBody = readFunctionBody('formatOpenOrdersReplacementStatus'); + + for (const body of [cancelBody, cancelRowsBody, cancelPlanBody, replacementStatusBody]) { + assert.doesNotMatch(body, /`[^`]*\$\{symbol\}/); + assert.doesNotMatch(body, /`[^`]*\$\{plan\.symbol\}/); + } + assert.match(cancelBody, /const interruptedBaseAsset = formatStatusBaseAsset\(symbol\)/); + assert.match(cancelBody, /`原交易对 \$\{interruptedBaseAsset\} 页面已离开,撤单确认跟踪已停止`/); + assert.match(source, /const clickedBaseAsset = formatStatusBaseAsset\(qtyPlan\.symbol\)/); + assert.match(source, /const currentBaseAsset = currentSymbol \? formatStatusBaseAsset\(currentSymbol\) : '未知'/); + assert.doesNotMatch(source, /点击时 \$\{qtyPlan\.symbol\}/); +}); + test('bulk cancel settles hidden TradingView orders before opening the native dialog', () => { assert.match(tradingViewOrdersSource, /applyOverrides/); assert.match(tradingViewOrdersSource, /tradingProperties\.showOrders/); diff --git a/test/unit/binance-orderbook-trade/status-symbol.test.js b/test/unit/binance-orderbook-trade/status-symbol.test.js new file mode 100644 index 0000000..79f83a0 --- /dev/null +++ b/test/unit/binance-orderbook-trade/status-symbol.test.js @@ -0,0 +1,16 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { formatStatusBaseAsset } from '../../../src/binance-orderbook-trade/core/status-symbol.js'; + +test('status symbols omit supported futures quote assets', () => { + assert.equal(formatStatusBaseAsset('HYPEUSDT'), 'HYPE'); + assert.equal(formatStatusBaseAsset('BTCUSDC'), 'BTC'); + assert.equal(formatStatusBaseAsset('1000SHIBUSDT'), '1000SHIB'); +}); + +test('status symbols reject unknown contracts instead of guessing a suffix', () => { + assert.throws(() => formatStatusBaseAsset('BTCUSD'), /Unsupported futures status symbol/); + assert.throws(() => formatStatusBaseAsset('USDT'), /Unsupported futures status symbol/); + assert.throws(() => formatStatusBaseAsset(''), /Unsupported futures status symbol/); +});