diff --git a/e2e/binance-orderbook/specs/control-flows.pw.js b/e2e/binance-orderbook/specs/control-flows.pw.js index af3c878..6a6a66f 100644 --- a/e2e/binance-orderbook/specs/control-flows.pw.js +++ b/e2e/binance-orderbook/specs/control-flows.pw.js @@ -1,6 +1,7 @@ import { test, expect } from '../test.js'; import { + CURRENT_SYMBOL, POSITION_SETS, createCancelScenario, } from '../scenarios/cancel-current-symbol.js'; @@ -148,7 +149,7 @@ test('starting a ladder preserves action slots and turns only the active action const startLong = panel.getByRole('button', { name: '阶梯开多' }); const startShort = panel.getByRole('button', { name: '阶梯开空' }); const cancel = panel.getByRole('button', { name: '撤单' }); - const stop = panel.getByRole('button', { name: '停止阶梯挂单' }); + const stop = panel.getByRole('button', { name: '停止开多' }); await expect(startLong).toBeEnabled(); await expect(startShort).toBeEnabled(); @@ -201,6 +202,64 @@ test('starting a ladder preserves action slots and turns only the active action expect(errors).toEqual([]); }); +test('starting close short keeps unavailable close long in the standard disabled style', async ({ page }) => { + const scenario = createCancelScenario({ + positions: [{ symbol: CURRENT_SYMBOL, side: 'SHORT', quantity: '0.1' }], + ui: { tradeMode: 'CLOSE', orderbookPrecision: '0.1' }, + }); + const { errors } = await openUserscriptScenario(page, scenario); + const panel = page.locator(PANEL_SELECTOR); + const closeLong = panel.getByRole('button', { name: '阶梯平多' }); + const closeShort = panel.getByRole('button', { name: '阶梯平空' }); + + await expect(closeLong).toBeDisabled(); + await expect(closeLong).not.toHaveAttribute('data-ladder-preserve-tone', 'true'); + await expect(closeShort).toBeEnabled(); + await closeShort.click(); + await expect(panel.getByRole('button', { name: '停止平空' })).toBeEnabled(); + await expect(closeLong).toBeDisabled(); + await expect(closeLong).not.toHaveAttribute('data-ladder-preserve-tone', 'true'); + expect(errors).toEqual([]); +}); + +test('starting close short preserves close long tone only when a long position exists', async ({ page }) => { + const scenario = createCancelScenario({ + positions: [ + { symbol: CURRENT_SYMBOL, side: 'LONG', quantity: '0.1' }, + { symbol: CURRENT_SYMBOL, side: 'SHORT', quantity: '0.1' }, + ], + ui: { tradeMode: 'CLOSE', orderbookPrecision: '0.1' }, + }); + const { errors } = await openUserscriptScenario(page, scenario); + const panel = page.locator(PANEL_SELECTOR); + const closeLong = panel.getByRole('button', { name: '阶梯平多' }); + const closeShort = panel.getByRole('button', { name: '阶梯平空' }); + + await expect(closeLong).toBeEnabled(); + await expect(closeShort).toBeEnabled(); + const closeLongTone = await closeLong.evaluate((button) => { + const style = getComputedStyle(button); + return { + backgroundColor: style.backgroundColor, + borderColor: style.borderColor, + color: style.color, + }; + }); + await closeShort.click(); + await expect(panel.getByRole('button', { name: '停止平空' })).toBeEnabled(); + await expect(closeLong).toBeDisabled(); + await expect(closeLong).toHaveAttribute('data-ladder-preserve-tone', 'true'); + expect(await closeLong.evaluate((button) => { + const style = getComputedStyle(button); + return { + backgroundColor: style.backgroundColor, + borderColor: style.borderColor, + color: style.color, + }; + })).toEqual(closeLongTone); + expect(errors).toEqual([]); +}); + test('a complete ladder submits the planned five native orders and restores controls', async ({ page }) => { test.setTimeout(15_000); const scenario = createCancelScenario({ @@ -209,7 +268,7 @@ test('a complete ladder submits the planned five native orders and restores cont const { errors } = await openUserscriptScenario(page, scenario); const panel = page.locator(PANEL_SELECTOR); const startLong = panel.getByRole('button', { name: '阶梯开多' }); - const stop = panel.getByRole('button', { name: '停止阶梯挂单' }); + const stop = panel.getByRole('button', { name: '停止开多' }); await startLong.click(); await expect(panel.locator('#jh-binance-ladder-status')).toContainText('完成 5/5', { diff --git a/scripts/binance-orderbook-trade.user.js b/scripts/binance-orderbook-trade.user.js index a3af00e..0b46195 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.129 +// @version 2.7.130 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -148,7 +148,12 @@ cancel: "撤单", cancelRunning: "撤单处理中", noOrders: "无挂单", - stopLadder: "停止阶梯挂单" + stopLadderByAction: freezeCopy({ + OPEN_LONG: "停止开多", + OPEN_SHORT: "停止开空", + CLOSE_LONG: "停止平多", + CLOSE_SHORT: "停止平空" + }) }), status: freezeCopy({ precisionUpdated: "精度推荐已更新", @@ -5655,11 +5660,17 @@ const cursor = disabled ? "not-allowed" : "pointer"; return ``; } - function ladderExecutionButton(actionType, label, tone, disabled = false) { + function ladderExecutionButton(actionType, label, tone, disabled = false, preserveTone = false) { if (activeLadderActionType !== actionType) { - return ladderActionButton(actionType, label, tone, disabled, Boolean(activeLadderActionType)); + return ladderActionButton( + actionType, + label, + tone, + disabled, + Boolean(activeLadderActionType) && preserveTone + ); } - return ``; + return ``; } function getLadderControlSections(tradeMode, closeContext, symbol, precision) { const ladderRunning = !!ladderTask; @@ -5684,8 +5695,8 @@ ladderOptionRow(PANEL_COPY.field.interval, PANEL_COPY.tooltip.interval, LADDER_STEP_OPTIONS, getLadderStep(tradeMode, symbol, precision), "step", "") ], actionButtons: [ - ladderExecutionButton("OPEN_LONG", PANEL_COPY.action.openLong, "BUY", actionDisabled), - ladderExecutionButton("OPEN_SHORT", PANEL_COPY.action.openShort, "SELL", actionDisabled) + ladderExecutionButton("OPEN_LONG", PANEL_COPY.action.openLong, "BUY", actionDisabled, true), + ladderExecutionButton("OPEN_SHORT", PANEL_COPY.action.openShort, "SELL", actionDisabled, true) ] }; } @@ -5706,8 +5717,20 @@ ladderOptionRow(PANEL_COPY.field.interval, PANEL_COPY.tooltip.interval, LADDER_STEP_OPTIONS, getLadderStep(tradeMode, symbol, precision), "step", "") ], actionButtons: [ - ladderExecutionButton("CLOSE_LONG", PANEL_COPY.action.closeLong, "SELL", closeLongDisabled), - ladderExecutionButton("CLOSE_SHORT", PANEL_COPY.action.closeShort, "BUY", closeShortDisabled) + ladderExecutionButton( + "CLOSE_LONG", + PANEL_COPY.action.closeLong, + "SELL", + closeLongDisabled, + Boolean(closeContext?.knowsLong && closeContext?.hasLong) + ), + ladderExecutionButton( + "CLOSE_SHORT", + PANEL_COPY.action.closeShort, + "BUY", + closeShortDisabled, + Boolean(closeContext?.knowsShort && closeContext?.hasShort) + ) ] }; } diff --git a/src/binance-orderbook-trade/contracts/panel-copy.js b/src/binance-orderbook-trade/contracts/panel-copy.js index 95b3613..91a8176 100644 --- a/src/binance-orderbook-trade/contracts/panel-copy.js +++ b/src/binance-orderbook-trade/contracts/panel-copy.js @@ -23,7 +23,12 @@ export const PANEL_COPY = Object.freeze({ cancel: '撤单', cancelRunning: '撤单处理中', noOrders: '无挂单', - stopLadder: '停止阶梯挂单', + stopLadderByAction: freezeCopy({ + OPEN_LONG: '停止开多', + OPEN_SHORT: '停止开空', + CLOSE_LONG: '停止平多', + CLOSE_SHORT: '停止平空', + }), }), status: freezeCopy({ precisionUpdated: '精度推荐已更新', diff --git a/src/binance-orderbook-trade/index.user.js b/src/binance-orderbook-trade/index.user.js index 52d4ce9..b4101c7 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.129 +// @version 2.7.130 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -4829,11 +4829,17 @@ import { return ``; } - function ladderExecutionButton(actionType, label, tone, disabled = false) { + function ladderExecutionButton(actionType, label, tone, disabled = false, preserveTone = false) { if (activeLadderActionType !== actionType) { - return ladderActionButton(actionType, label, tone, disabled, Boolean(activeLadderActionType)); + return ladderActionButton( + actionType, + label, + tone, + disabled, + Boolean(activeLadderActionType) && preserveTone, + ); } - return ``; + return ``; } function getLadderControlSections(tradeMode, closeContext, symbol, precision) { @@ -4859,8 +4865,8 @@ import { ladderOptionRow(PANEL_COPY.field.interval, PANEL_COPY.tooltip.interval, LADDER_STEP_OPTIONS, getLadderStep(tradeMode, symbol, precision), 'step', ''), ], actionButtons: [ - ladderExecutionButton('OPEN_LONG', PANEL_COPY.action.openLong, 'BUY', actionDisabled), - ladderExecutionButton('OPEN_SHORT', PANEL_COPY.action.openShort, 'SELL', actionDisabled), + ladderExecutionButton('OPEN_LONG', PANEL_COPY.action.openLong, 'BUY', actionDisabled, true), + ladderExecutionButton('OPEN_SHORT', PANEL_COPY.action.openShort, 'SELL', actionDisabled, true), ], }; } @@ -4882,8 +4888,20 @@ import { ladderOptionRow(PANEL_COPY.field.interval, PANEL_COPY.tooltip.interval, LADDER_STEP_OPTIONS, getLadderStep(tradeMode, symbol, precision), 'step', ''), ], actionButtons: [ - ladderExecutionButton('CLOSE_LONG', PANEL_COPY.action.closeLong, 'SELL', closeLongDisabled), - ladderExecutionButton('CLOSE_SHORT', PANEL_COPY.action.closeShort, 'BUY', closeShortDisabled), + ladderExecutionButton( + 'CLOSE_LONG', + PANEL_COPY.action.closeLong, + 'SELL', + closeLongDisabled, + Boolean(closeContext?.knowsLong && closeContext?.hasLong), + ), + ladderExecutionButton( + 'CLOSE_SHORT', + PANEL_COPY.action.closeShort, + 'BUY', + closeShortDisabled, + Boolean(closeContext?.knowsShort && closeContext?.hasShort), + ), ], }; } diff --git a/test/unit/binance-orderbook-trade/ladder-options.test.js b/test/unit/binance-orderbook-trade/ladder-options.test.js index 8825fb7..cc9b17a 100644 --- a/test/unit/binance-orderbook-trade/ladder-options.test.js +++ b/test/unit/binance-orderbook-trade/ladder-options.test.js @@ -246,8 +246,8 @@ test('ladder quantity levels and step options share one stable five-slot grid', }); test('close ladder buttons match the Binance native long-short order', () => { - const closeLongButton = "ladderExecutionButton('CLOSE_LONG', PANEL_COPY.action.closeLong, 'SELL', closeLongDisabled)"; - const closeShortButton = "ladderExecutionButton('CLOSE_SHORT', PANEL_COPY.action.closeShort, 'BUY', closeShortDisabled)"; + const closeLongButton = "'CLOSE_LONG',\n PANEL_COPY.action.closeLong,\n 'SELL',\n closeLongDisabled,\n Boolean(closeContext?.knowsLong && closeContext?.hasLong)"; + const closeShortButton = "'CLOSE_SHORT',\n PANEL_COPY.action.closeShort,\n 'BUY',\n closeShortDisabled,\n Boolean(closeContext?.knowsShort && closeContext?.hasShort)"; assert.notEqual(source.indexOf(closeLongButton), -1); assert.notEqual(source.indexOf(closeShortButton), -1); diff --git a/test/unit/binance-orderbook-trade/panel-copy.test.js b/test/unit/binance-orderbook-trade/panel-copy.test.js index b436252..58ac4fd 100644 --- a/test/unit/binance-orderbook-trade/panel-copy.test.js +++ b/test/unit/binance-orderbook-trade/panel-copy.test.js @@ -30,7 +30,12 @@ test('panel copy keeps the approved labels and tooltips in one contract', () => cancel: '撤单', cancelRunning: '撤单处理中', noOrders: '无挂单', - stopLadder: '停止阶梯挂单', + stopLadderByAction: { + OPEN_LONG: '停止开多', + OPEN_SHORT: '停止开空', + CLOSE_LONG: '停止平多', + CLOSE_SHORT: '停止平空', + }, }, status: { precisionUpdated: '精度推荐已更新', diff --git a/test/unit/binance-orderbook-trade/source-regressions.test.js b/test/unit/binance-orderbook-trade/source-regressions.test.js index c0e844b..9aa96b6 100644 --- a/test/unit/binance-orderbook-trade/source-regressions.test.js +++ b/test/unit/binance-orderbook-trade/source-regressions.test.js @@ -884,7 +884,9 @@ test('cancel current-symbol open orders are single-flight and follow the native assert.match(source, /let activeLadderPanelContext = null/); assert.match(source, /data-ladder-preserve-tone="true"/); assert.match(source, /button:disabled:not\(\[data-ladder-preserve-tone="true"\]\)/); - assert.match(source, /Boolean\(activeLadderActionType\)/); + assert.match(source, /Boolean\(activeLadderActionType\) && preserveTone/); + assert.match(source, /Boolean\(closeContext\?\.knowsLong && closeContext\?\.hasLong\)/); + assert.match(source, /Boolean\(closeContext\?\.knowsShort && closeContext\?\.hasShort\)/); assert.match(panelBody, /controlSections\.actionButtons/); assert.doesNotMatch(panelBody, /grid-column:span 2/); assert.match(panelBody, /grid-template-columns:repeat\(3,minmax\(0,1fr\)\)/); @@ -930,7 +932,7 @@ test('cancel current-symbol open orders are single-flight and follow the native const actionRowsBody = readFunctionBody('getLadderControlSections'); assert.match(actionRowsBody, /actionDisabled = ladderRunning \|\| cancelCurrentSymbolOpenOrdersBlocksLadderActions/); assert.doesNotMatch(actionRowsBody, /!!cancelCurrentSymbolOpenOrdersTask/); - assert.match(actionRowsBody, /ladderExecutionButton\('OPEN_LONG',[\s\S]*actionDisabled\)/); + assert.match(actionRowsBody, /ladderExecutionButton\('OPEN_LONG',[\s\S]*actionDisabled, true\)/); assert.match(startBody, /activeLadderActionType = actionType/); assert.match(startBody, /activeLadderPanelContext = \{[\s\S]*mode: spec\.mode,[\s\S]*symbol: actionSymbol,[\s\S]*precision: readCurrentOrderbookPrecisionValue\(\)/); assert.match(startBody, /finally\(\(\) => \{[\s\S]*activeLadderActionType = null/);