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/);