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
2 changes: 1 addition & 1 deletion docs/binance-orderbook-trade-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ and the evidence report contract are owned by

Do not auto-confirm destructive Binance dialogs. The script may open Binance's native cancel confirmation, but final confirmation remains manual.

USDT account rebalancing is a manual action, not an automatic post-close side effect. The inline `账户再平衡` action becomes available only after the account-order widget has continuously reported zero positions and zero open orders for three seconds, and a fresh all-symbol futures-position response also confirms that every position is flat. Starting any script trade or cancel task invalidates that eligibility immediately. The action opens a script-owned native `<dialog>` styled with Binance color variables; it shows current and target balances plus the exact transfer plan, focuses Cancel by default, and proceeds only after an explicit confirmation. Escape and Cancel close it without transferring funds.
USDT account rebalancing is a manual action, not an automatic post-close side effect. The inline `账户再平衡` action becomes available only after the account-order widget has continuously reported zero positions and zero open orders for two seconds, and a fresh all-symbol futures-position response also confirms that every position is flat. Starting any script trade or cancel task invalidates that eligibility immediately. When a cancellation task ends, readable zero account counters restart the same guarded eligibility window even if the counts did not change during the task. The action opens a script-owned native `<dialog>` styled with Binance color variables; it shows current and target balances plus the exact transfer plan, focuses Cancel by default, and proceeds only after an explicit confirmation. Escape and Cancel close it without transferring funds.

The rebalance path is pinned to the current Binance page bundle contract instead of automating the native transfer dialog. It reads Spot and Funding available USDT from `/bapi/asset/v2/private/asset-service/wallet/balance`, reads the U-M Futures transferable amount from `/bapi/futures/v1/private/future/user-data/getMaxWithdrawAmount`, and submits at most two sequential transfers through `/bapi/asset/v1/private/asset-service/wallet/transfer` with `{ asset, amount, kindType }`. Wallet rows are identified by the stable `accountType` values `MAIN`, `CARD`, and `FUTURE`; `walletName` is localized and must not be used as an identifier. The private transfer BAPI uses those same account codes; do not substitute the public SAPI names `FUNDING` or `UMFUTURE`. It does not save an API key and does not fall back to DOM clicking if this private BAPI contract changes.

Expand Down
2 changes: 1 addition & 1 deletion docs/test-migration-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ behavior. No partial replacement remains in this map.
| 67 | busy leverage reset retains and replays the latest symbol request | `user replays only the latest symbol after a busy account position check` ([Account lifecycle]); `user replays only the latest reset after switching symbols during its final position read` ([Account lifecycle]). |
| 68 | auto leverage reset is authorized by a fresh current-symbol position response | `user requires a fresh flat position response immediately before adjusting leverage` ([Account lifecycle]). |
| 69 | account position count changes schedule symbol-specific API checks | `user refreshes current-symbol position evidence when an account position count changes` ([Account lifecycle]); `user does not repeat account HTTP checks while counts and symbol remain unchanged` ([Account lifecycle]). |
| 70 | USDT rebalance waits for global flat stability and requires zero open orders | `user qualifies for account rebalance only after the full three-second flat window and its API response` ([Account lifecycle]); `user restarts the full rebalance window when a native open order reappears` ([Account lifecycle]) and the native-position/stale-response cases. |
| 70 | USDT rebalance waits for global flat stability and requires zero open orders | `user qualifies for account rebalance only after the full two-second flat window and its API response` ([Account lifecycle]); `user restarts the full rebalance window when a native open order reappears` ([Account lifecycle]); `user regains account rebalance eligibility after cancelling with no orders` ([Account lifecycle]); and the native-position/stale-response cases. |
| 71 | USDT rebalance uses direct Binance BAPI only after one explicit plan confirmation | `user completes exactly two USDT transfers only after confirming the complete account plan` ([Rebalance]); `user stops account transfers when the authoritative position changes during confirmation` ([Rebalance]) and the changed-balance case. |

[Source]: ../test/unit/binance-orderbook-trade/source-regressions.test.js
Expand Down
64 changes: 53 additions & 11 deletions e2e/binance-orderbook/specs/account-lifecycle-behavior.pw.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,14 +243,14 @@ test('user preserves the existing leverage after an HTTP rejection and can retry
await host.expectNoTradingActions();
});

test('user qualifies for account rebalance only after the full three-second flat window and its API response', async ({ page }) => {
test('user qualifies for account rebalance only after the full two-second flat window and its API response', async ({ page }) => {
// Given a settled flat account still has one native open order.
const host = await openSettledAccount(page);
const sequence = host.holdPositionResponse();
const startedAt = await host.setNativeOrders([]);

// When the stable no-order window advances to one millisecond before its deadline.
await page.clock.runFor(2999);
await page.clock.runFor(1999);

// Then no qualification request or rebalance action is exposed early.
expect(host.pendingPositionResponses()).toEqual([]);
Expand All @@ -262,7 +262,7 @@ test('user qualifies for account rebalance only after the full three-second flat

// Then exactly the full window precedes the API request and HTTP completion is still required.
const latest = (await host.snapshot()).requests.filter(request => request.pathname === ACCOUNT_PATHS.positions).at(-1);
expect(latest.at).toBe(startedAt + 3000);
expect(latest.at).toBe(startedAt + 2000);
await expect(page.locator(ACTION)).toBeHidden();

// When the explicit flat response arrives.
Expand All @@ -276,10 +276,10 @@ test('user qualifies for account rebalance only after the full three-second flat

for (const changed of ['position', 'open order']) {
test(`user restarts the full rebalance window when a native ${changed} reappears`, async ({ page }) => {
// Given the account has already spent fifteen hundred milliseconds with no positions or orders.
// Given the account has already spent one thousand milliseconds with no positions or orders.
const host = await openSettledAccount(page);
await host.setNativeOrders([]);
await page.clock.runFor(1500);
await page.clock.runFor(1000);

// When an account activity arrives and clears again before the original deadline.
if (changed === 'position') {
Expand All @@ -291,12 +291,12 @@ for (const changed of ['position', 'open order']) {
await host.setNativeOrders(ORDER_SETS.other);
await host.setNativeOrders([]);
}
await page.clock.runFor(2999);
await page.clock.runFor(1999);

// Then the original flat time is discarded and the new window remains incomplete.
await expect(page.locator(ACTION)).toBeHidden();

// When the new activity-free window reaches three full seconds.
// When the new activity-free window reaches two full seconds.
await page.clock.runFor(1);
await host.waitForPositionResponses(3);

Expand All @@ -312,7 +312,7 @@ for (const changed of ['position', 'open order']) {
const host = await openSettledAccount(page);
const sequence = host.holdPositionResponse();
await host.setNativeOrders([]);
await page.clock.runFor(3000);
await page.clock.runFor(2000);
await expect.poll(host.pendingPositionResponses).toEqual([sequence]);

// When native account activity invalidates qualification before the captured flat response returns.
Expand Down Expand Up @@ -345,7 +345,7 @@ for (const [name, response] of [

// When the empty account completes its window and receives non-flat or invalid authoritative evidence.
await host.setNativeOrders([]);
await page.clock.runFor(3000);
await page.clock.runFor(2000);
await host.waitForPositionResponses(3);
await page.clock.runFor(32);

Expand All @@ -361,11 +361,11 @@ test('user previews balances published during qualification instead of an earlie
// Given the account begins its flat window with one hundred USDT in Funding.
const host = await openSettledAccount(page);
await host.setNativeOrders([]);
await page.clock.runFor(1500);
await page.clock.runFor(1000);

// When a native wallet update moves twenty USDT to Spot before qualification completes.
host.api.setBalances({ FUNDING: '80', MAIN: '20', UMFUTURE: '0' });
await page.clock.runFor(1500);
await page.clock.runFor(1000);
await host.waitForPositionResponses(3);
await expectEligible(page);

Expand All @@ -386,3 +386,45 @@ test('user previews balances published during qualification instead of an earlie
await dialog.getByRole('button', { name: '取消', exact: true }).evaluate(button => button.click());
await host.expectNoTradingActions();
});

test('user regains account rebalance eligibility after cancelling with no orders', async ({ page }) => {
// Given a flat account has completed qualification with no positions or open orders.
const host = await openSettledAccount(page);
await host.setNativeOrders([]);
await page.clock.runFor(2000);
await host.waitForPositionResponses(3);
await expectEligible(page);
const sequence = host.holdPositionResponse();

// When the user clicks cancellation even though the current symbol has no orders.
await page.locator('[data-ladder-cancel-symbol]').evaluate(button => button.click());
await expect(page.locator('#jh-binance-ladder-status')).toHaveText('当前交易对无挂单');
await page.clock.runFor(32);

// Then qualification is withdrawn without opening a native dialog or sending a trading request.
await expect(page.locator(ACTION)).toBeHidden();
await expect(page.getByRole('dialog')).toHaveCount(0);
await host.expectNoTradingActions();

// When the renewed flat window advances to one millisecond before its deadline.
await page.clock.runFor(1967);

// Then no authoritative qualification request starts early.
expect(host.pendingPositionResponses()).toEqual([]);
await expect(page.locator(ACTION)).toBeHidden();

// When the last millisecond expires while the fresh flat response remains held.
await page.clock.runFor(1);
await expect.poll(host.pendingPositionResponses).toEqual([sequence]);

// Then the action stays hidden until the current response is accepted.
await expect(page.locator(ACTION)).toBeHidden();

// When the explicit fresh flat response arrives.
await host.releasePositionResponse(sequence);
await host.waitForPositionResponses(sequence);

// Then the account action returns without any order, cancellation, or transfer request.
await expectEligible(page);
await host.expectNoTradingActions();
});
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ async function openRebalance(page, balances, options) {
const response = api.handle({ pathname, method: request.method(), body });
await route.fulfill({ status: response.status, contentType: 'application/json', body: JSON.stringify(response.body) });
});
await page.clock.runFor(3000);
await page.clock.runFor(2000);
const action = page.locator('[data-usdt-rebalance]');
await expect(action).toBeVisible();
await expect(action).toBeEnabled();
Expand Down
14 changes: 11 additions & 3 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.213
// @version 2.7.214
// @author jackhai9
// @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板
// @match https://www.binance.com/*/futures/*
Expand Down Expand Up @@ -5244,7 +5244,7 @@
const BINANCE_WALLET_BALANCE_BAPI_PATH = "/bapi/asset/v2/private/asset-service/wallet/balance?needBalanceDetail=true&quoteAsset=USDT";
const BINANCE_FUTURES_MAX_WITHDRAW_BAPI_PATH = "/bapi/futures/v1/private/future/user-data/getMaxWithdrawAmount";
const BINANCE_WALLET_TRANSFER_BAPI_PATH = "/bapi/asset/v1/private/asset-service/wallet/transfer";
const USDT_REBALANCE_FLAT_STABLE_MS = 3e3;
const USDT_REBALANCE_FLAT_STABLE_MS = 2e3;
const USDT_REBALANCE_REQUEST_TIMEOUT_MS = 5e3;
const USDT_REBALANCE_BALANCE_POLL_MS = 1e3;
const LADDER_OPEN_QTY_READY_TIMEOUT_MS = 1200;
Expand Down Expand Up @@ -9867,6 +9867,7 @@
if (cancelCurrentSymbolOpenOrdersTask === task) {
cancelCurrentSymbolOpenOrdersTask = null;
cancelCurrentSymbolOpenOrdersBlocksLadderActions = false;
restartUsdtRebalanceEligibilityFromCurrentAccountState();
}
scheduleRenderPanel();
}
Expand Down Expand Up @@ -10848,7 +10849,7 @@
if (hadPendingTimer || wasEligible || resetFlatStatus) scheduleRenderPanel();
}
async function confirmUsdtRebalanceEligibility(epoch) {
if (epoch !== usdtRebalanceEligibilityEpoch || document.hidden) return false;
if (epoch !== usdtRebalanceEligibilityEpoch || document.hidden || !isFuturesTradingPage()) return false;
if (readAccountPositionCount() !== 0 || getOpenOrdersTabCount() !== 0) return false;
if (ladderTask || continuousLadderTask || singleOrderTask || cancelCurrentSymbolOpenOrdersTask) {
return false;
Expand Down Expand Up @@ -10885,6 +10886,13 @@
}
invalidateUsdtRebalanceEligibility();
}
function restartUsdtRebalanceEligibilityFromCurrentAccountState() {
if (document.hidden || !isFuturesTradingPage()) return;
const positionCount = readAccountPositionCount();
const openOrdersCount = getOpenOrdersTabCount();
if (positionCount == null || openOrdersCount == null) return;
updateUsdtRebalanceEligibilityFromAccountCounts(positionCount, openOrdersCount);
}
function handleAccountPositionObservation(triggerSource) {
const positionCount = readAccountPositionCount();
const openOrdersCount = getOpenOrdersTabCount();
Expand Down
19 changes: 16 additions & 3 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.213
// @version 2.7.214
// @author jackhai9
// @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板
// @match https://www.binance.com/*/futures/*
Expand Down Expand Up @@ -373,7 +373,7 @@ import { showUsdtRebalanceDialog } from './dom/usdt-rebalance-dialog.js';
const BINANCE_WALLET_BALANCE_BAPI_PATH = '/bapi/asset/v2/private/asset-service/wallet/balance?needBalanceDetail=true&quoteAsset=USDT';
const BINANCE_FUTURES_MAX_WITHDRAW_BAPI_PATH = '/bapi/futures/v1/private/future/user-data/getMaxWithdrawAmount';
const BINANCE_WALLET_TRANSFER_BAPI_PATH = '/bapi/asset/v1/private/asset-service/wallet/transfer';
const USDT_REBALANCE_FLAT_STABLE_MS = 3000;
const USDT_REBALANCE_FLAT_STABLE_MS = 2000;
const USDT_REBALANCE_REQUEST_TIMEOUT_MS = 5000;
const USDT_REBALANCE_BALANCE_POLL_MS = 1000;
const LADDER_OPEN_QTY_READY_TIMEOUT_MS = 1200;
Expand Down Expand Up @@ -5839,6 +5839,7 @@ import { showUsdtRebalanceDialog } from './dom/usdt-rebalance-dialog.js';
if (cancelCurrentSymbolOpenOrdersTask === task) {
cancelCurrentSymbolOpenOrdersTask = null;
cancelCurrentSymbolOpenOrdersBlocksLadderActions = false;
restartUsdtRebalanceEligibilityFromCurrentAccountState();
}
scheduleRenderPanel();
}
Expand Down Expand Up @@ -6988,7 +6989,7 @@ import { showUsdtRebalanceDialog } from './dom/usdt-rebalance-dialog.js';
}

async function confirmUsdtRebalanceEligibility(epoch) {
if (epoch !== usdtRebalanceEligibilityEpoch || document.hidden) return false;
if (epoch !== usdtRebalanceEligibilityEpoch || document.hidden || !isFuturesTradingPage()) return false;
if (readAccountPositionCount() !== 0 || getOpenOrdersTabCount() !== 0) return false;
if (ladderTask || continuousLadderTask || singleOrderTask || cancelCurrentSymbolOpenOrdersTask) {
return false;
Expand Down Expand Up @@ -7030,6 +7031,18 @@ import { showUsdtRebalanceDialog } from './dom/usdt-rebalance-dialog.js';
invalidateUsdtRebalanceEligibility();
}

/**
* Cancellation invalidates eligibility before it starts, but a no-orders result leaves
* both observed counters at 0 and therefore produces no observer transition to restart it.
*/
function restartUsdtRebalanceEligibilityFromCurrentAccountState() {
if (document.hidden || !isFuturesTradingPage()) return;
const positionCount = readAccountPositionCount();
const openOrdersCount = getOpenOrdersTabCount();
if (positionCount == null || openOrdersCount == null) return;
updateUsdtRebalanceEligibilityFromAccountCounts(positionCount, openOrdersCount);
}

function handleAccountPositionObservation(triggerSource) {
const positionCount = readAccountPositionCount();
const openOrdersCount = getOpenOrdersTabCount();
Expand Down
Loading