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
63 changes: 61 additions & 2 deletions e2e/binance-orderbook/specs/control-flows.pw.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { test, expect } from '../test.js';

import {
CURRENT_SYMBOL,
POSITION_SETS,
createCancelScenario,
} from '../scenarios/cancel-current-symbol.js';
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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({
Expand All @@ -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', {
Expand Down
41 changes: 32 additions & 9 deletions scripts/binance-orderbook-trade.user.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/*
Expand Down Expand Up @@ -148,7 +148,12 @@
cancel: "撤单",
cancelRunning: "撤单处理中",
noOrders: "无挂单",
stopLadder: "停止阶梯挂单"
stopLadderByAction: freezeCopy({
OPEN_LONG: "停止开多",
OPEN_SHORT: "停止开空",
CLOSE_LONG: "停止平多",
CLOSE_SHORT: "停止平空"
})
}),
status: freezeCopy({
precisionUpdated: "精度推荐已更新",
Expand Down Expand Up @@ -5655,11 +5660,17 @@
const cursor = disabled ? "not-allowed" : "pointer";
return `<button type="button" data-ladder-action="${actionType}"${disabledAttrs} style="height:${LADDER_CONTROL_BUTTON_HEIGHT}px;border:1px solid ${borderColor};border-radius:6px;background:${background};color:${borderColor};font-size:${LADDER_CONTROL_BUTTON_FONT_SIZE}px;font-weight:${CONTROL_FONT_WEIGHT};line-height:${LADDER_CONTROL_BUTTON_HEIGHT - 2}px;cursor:${cursor};opacity:1;">${label}</button>`;
}
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 `<button type="button" data-ladder-stop="true" data-ladder-action-origin="${actionType}" style="height:${LADDER_CONTROL_BUTTON_HEIGHT}px;border:1px solid var(--color-PrimaryYellow);border-radius:6px;background:var(--color-BadgeBg);color:#9a6700;font-size:${LADDER_CONTROL_BUTTON_FONT_SIZE}px;font-weight:${CONTROL_FONT_WEIGHT};line-height:${LADDER_CONTROL_BUTTON_HEIGHT - 2}px;cursor:pointer;">${PANEL_COPY.action.stopLadder}</button>`;
return `<button type="button" data-ladder-stop="true" data-ladder-action-origin="${actionType}" style="height:${LADDER_CONTROL_BUTTON_HEIGHT}px;border:1px solid var(--color-PrimaryYellow);border-radius:6px;background:var(--color-BadgeBg);color:#9a6700;font-size:${LADDER_CONTROL_BUTTON_FONT_SIZE}px;font-weight:${CONTROL_FONT_WEIGHT};line-height:${LADDER_CONTROL_BUTTON_HEIGHT - 2}px;cursor:pointer;">${PANEL_COPY.action.stopLadderByAction[actionType]}</button>`;
}
function getLadderControlSections(tradeMode, closeContext, symbol, precision) {
const ladderRunning = !!ladderTask;
Expand All @@ -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)
]
};
}
Expand All @@ -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)
)
]
};
}
Expand Down
7 changes: 6 additions & 1 deletion src/binance-orderbook-trade/contracts/panel-copy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: '精度推荐已更新',
Expand Down
34 changes: 26 additions & 8 deletions src/binance-orderbook-trade/index.user.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/*
Expand Down Expand Up @@ -4829,11 +4829,17 @@ import {
return `<button type="button" data-ladder-action="${actionType}"${disabledAttrs} style="height:${LADDER_CONTROL_BUTTON_HEIGHT}px;border:1px solid ${borderColor};border-radius:6px;background:${background};color:${borderColor};font-size:${LADDER_CONTROL_BUTTON_FONT_SIZE}px;font-weight:${CONTROL_FONT_WEIGHT};line-height:${LADDER_CONTROL_BUTTON_HEIGHT - 2}px;cursor:${cursor};opacity:1;">${label}</button>`;
}

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 `<button type="button" data-ladder-stop="true" data-ladder-action-origin="${actionType}" style="height:${LADDER_CONTROL_BUTTON_HEIGHT}px;border:1px solid var(--color-PrimaryYellow);border-radius:6px;background:var(--color-BadgeBg);color:#9a6700;font-size:${LADDER_CONTROL_BUTTON_FONT_SIZE}px;font-weight:${CONTROL_FONT_WEIGHT};line-height:${LADDER_CONTROL_BUTTON_HEIGHT - 2}px;cursor:pointer;">${PANEL_COPY.action.stopLadder}</button>`;
return `<button type="button" data-ladder-stop="true" data-ladder-action-origin="${actionType}" style="height:${LADDER_CONTROL_BUTTON_HEIGHT}px;border:1px solid var(--color-PrimaryYellow);border-radius:6px;background:var(--color-BadgeBg);color:#9a6700;font-size:${LADDER_CONTROL_BUTTON_FONT_SIZE}px;font-weight:${CONTROL_FONT_WEIGHT};line-height:${LADDER_CONTROL_BUTTON_HEIGHT - 2}px;cursor:pointer;">${PANEL_COPY.action.stopLadderByAction[actionType]}</button>`;
}

function getLadderControlSections(tradeMode, closeContext, symbol, precision) {
Expand All @@ -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),
],
};
}
Expand All @@ -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),
),
],
};
}
Expand Down
4 changes: 2 additions & 2 deletions test/unit/binance-orderbook-trade/ladder-options.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
7 changes: 6 additions & 1 deletion test/unit/binance-orderbook-trade/panel-copy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: '精度推荐已更新',
Expand Down
6 changes: 4 additions & 2 deletions test/unit/binance-orderbook-trade/source-regressions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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\)\)/);
Expand Down Expand Up @@ -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/);
Expand Down
Loading