diff --git a/docs/binance-orderbook-trade-development.md b/docs/binance-orderbook-trade-development.md index 967e1dd..ed7bee5 100644 --- a/docs/binance-orderbook-trade-development.md +++ b/docs/binance-orderbook-trade-development.md @@ -332,14 +332,20 @@ 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 has separate automatic excess withdrawal and manual full-allocation paths. 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 or continuous-close task ends, readable zero account counters restart the same guarded eligibility window even if the counts did not change during the task or the earlier check was blocked by the running task. A continuous session restarts this window only after the whole session ends, not between rounds. Hidden pages defer qualification until the existing visibility lifecycle observes the account again. The action opens a script-owned native `` 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. - -Automatic withdrawal runs once per verified globally flat episode after the same -two-second zero-counter window. Its target keeps 10% of total available USDT in -Futures, sends only Futures excess toward Funding's 50% target, and sends the -remainder to Spot. It never debits Funding or Spot, and never refills Futures. -Targets display the balances actually reachable by these outward-only transfers. -The manual action keeps its explicit confirmation and full 5:4:1 allocation. +USDT account rebalancing has automatic and manually confirmed paths that share the same full-allocation planner. 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 or continuous-close task ends, readable zero account counters restart the same guarded eligibility window even if the counts did not change during the task or the earlier check was blocked by the running task. A continuous session restarts this window only after the whole session ends, not between rounds. Hidden pages defer qualification until the existing visibility lifecycle observes the account again. The action opens a script-owned native `` 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. + +Automatic rebalancing runs once per verified globally flat episode after the same +two-second zero-counter window. Both paths allocate total available USDT to +Funding 50%, Spot 40%, and Futures 10% through `buildUsdtRebalancePlan`. +Funding and Spot targets are floored to eight decimals; Futures receives the +remainder so the complete balance is conserved. Any of the three accounts may +donate to an underallocated account, including refilling Futures after a loss +once the account has no positions or open orders. Funding and Spot are adjusted +even when Futures already holds its target. At most two sequential transfers +complete the allocation, and unchanged final balances produce an empty plan for +either entrypoint. An empty automatic plan reports that no transfer is needed; +it does not record an automatic transfer completion. The manual action retains +its explicit confirmation. Automatic qualification, waiting, progress, completion, and failure messages use the separate `jh-binance-auto-rebalance-status` row. It starts hidden, uses safe text/title updates, and retains paired Chinese/English status and progress across @@ -383,6 +389,12 @@ without claiming automatic success. New authoritative activity removes the completion marker together with the old episode; the marker never grants transfer eligibility. +Upgrading the allocation policy preserves existing episode records. A consumed +episode, including an older completed outward-only plan, is not rerun because +its balances miss the new targets. New authoritative account activity is still +required before another automatic episode; an explicit manual rebalance can +adjust the current balances in the meantime. + Every automatic attempt and every transfer step verifies all-symbol positions, POST `/bapi/futures/v1/private/future/order/open-orders` with `{}`, and POST `/bapi/futures/v1/private/future/order/open-algo-order` with @@ -400,7 +412,7 @@ The rebalance path is pinned to the current Binance page bundle contract instead For manual full rebalancing, the user must approve one native confirmation that shows the current balances, 5:4:1 targets, and exact transfer list. After confirmation, the script rechecks all positions, all open orders, and the exact three-account balance snapshot before every transfer. Each successful response must then be reflected by a fresh balance read before the next transfer starts. An intervening position, order, or balance change stops the task; a partial completion is reported explicitly and is never retried or rolled back automatically. -Automatic withdrawal requires visibility again before each transfer. Hiding the tab after the user confirms a manual rebalance does not cancel its remaining transfers. The initial eligibility and preview still require a visible trading page; each confirmed transfer continues to require the same fresh account checks, matching balances, and current trading route while hidden. +Automatic rebalancing requires visibility again before each transfer. Hiding the tab after the user confirms a manual rebalance does not cancel its remaining transfers. The initial eligibility and preview still require a visible trading page; each confirmed transfer continues to require the same fresh account checks, matching balances, and current trading route while hidden. Ladder replacement must stay scoped and direction-aware. Automatic replacement may cancel only visible basic open-order rows for the current symbol and the same plan direction (`开多`, `开空`, `平多`, or `平空`). It must not use current-symbol cancel-all for ladder replacement, must not touch conditional/protection orders, and must retry the ladder plan only after the replacement path is validated by current DOM rows. diff --git a/docs/binance-strategy27-events-development.md b/docs/binance-strategy27-events-development.md index fe24c3b..53e599d 100644 --- a/docs/binance-strategy27-events-development.md +++ b/docs/binance-strategy27-events-development.md @@ -14,7 +14,7 @@ V10 live projection. The VPS remains the only market-data and event-analysis authority. The userscript opens no Binance market-data WebSocket, uses no Binance API key, and does not recalculate the four force groups. -Version 0.6.10 retains this installation's private gateway configuration and +Version 0.6.11 retains this installation's private gateway configuration and provides a shared read-only transport. Strategy29 owns its own summary panel, lifecycle and panel position. The existing `strategy27GatewayOrigin` and `strategy27GatewayAuthSecret` storage keys remain the single credential source; @@ -183,7 +183,7 @@ ADR 032 in CorsairQuant owns the server-side rule and transport contract. The browser does not reconstruct candidates from ordinary events or recalculate market evidence. The client, lifecycle, panel, SVG chart layer and optional-job controller are wired into the entrypoint and tested together. The source and -generated install artifact are version 0.6.10 with identical metadata headers. +generated install artifact are version 0.6.11 with identical metadata headers. The generated artifact passes syntax, release-contract and isolated execution checks, including candidate delivery, paired SVG markers, clear and context stop. Binance operator-page validation remains outstanding. Server/gateway rollout diff --git a/docs/binance-strategy29-bollinger-development.md b/docs/binance-strategy29-bollinger-development.md index 98cb084..2b95b35 100644 --- a/docs/binance-strategy29-bollinger-development.md +++ b/docs/binance-strategy29-bollinger-development.md @@ -13,7 +13,7 @@ already-loaded native chart candles. The summary reads the authenticated unified loopback gateway; it does not call Binance market-data or account APIs, submit orders, rotate hidden charts, or add remote events as chart drawings. -Install Strategy29 0.5.10 with orderbook 2.7.199 or later, or use it alone. +Install Strategy29 0.5.11 with orderbook 2.7.199 or later, or use it alone. Install CorsairQuant signal client 0.6.7 for the remote summary. Do not combine it with the embedded observer in orderbook 2.7.198. After updating/disabling the old script, reload the page. An embedded observer diff --git a/docs/binance-strategy31-volume-reversal-development.md b/docs/binance-strategy31-volume-reversal-development.md index 705e3ea..2b81d0e 100644 --- a/docs/binance-strategy31-volume-reversal-development.md +++ b/docs/binance-strategy31-volume-reversal-development.md @@ -1,7 +1,7 @@ # Strategy31 volume reversal signals Source: `src/binance-strategy31-volume-reversal/`. Installer: -`scripts/binance-strategy31-volume-reversal.user.js` (0.1.4). +`scripts/binance-strategy31-volume-reversal.user.js` (0.1.5). Install alongside CorsairQuant signal client 0.6.7, which owns private gateway authentication. The shared bridge explicitly advertises the `strategy31` capability. diff --git a/e2e/binance-orderbook/helpers/account-lifecycle-host.js b/e2e/binance-orderbook/helpers/account-lifecycle-host.js index 6c36234..60c0acd 100644 --- a/e2e/binance-orderbook/helpers/account-lifecycle-host.js +++ b/e2e/binance-orderbook/helpers/account-lifecycle-host.js @@ -69,7 +69,7 @@ export async function openAccountLifecycleHost(page, { orders = ORDER_SETS.current, apiPositions = [], leverage = 2, - balances = { FUNDING: '100', MAIN: '0', UMFUTURE: '0' }, + balances = { FUNDING: '50', MAIN: '40', UMFUTURE: '10' }, } = {}) { await installScenarioClock(page); const loaded = await openUserscriptScenario(page, createCancelScenario({ diff --git a/e2e/binance-orderbook/specs/account-lifecycle-behavior.pw.js b/e2e/binance-orderbook/specs/account-lifecycle-behavior.pw.js index 96b3ded..f204418 100644 --- a/e2e/binance-orderbook/specs/account-lifecycle-behavior.pw.js +++ b/e2e/binance-orderbook/specs/account-lifecycle-behavior.pw.js @@ -362,18 +362,18 @@ for (const [name, response] of [ } test('user previews balances published during qualification instead of an earlier account snapshot', async ({ page }) => { - // Given the account begins its flat window with one hundred USDT in Funding. + // Given already balanced accounts begin their flat qualification window. const host = await openSettledAccount(page); await host.setNativeOrders([]); 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' }); + // When a native wallet update doubles each balance before qualification completes. + host.api.setBalances({ FUNDING: '100', MAIN: '80', UMFUTURE: '20' }); await page.clock.runFor(1000); await host.waitForPositionResponses(3); await expectEligible(page); - // Then automatic qualification reads current wallets without transferring a futures deficit. + // Then automatic qualification reads the still-balanced wallets without a transfer. const automaticWalletReads = host.api.snapshot().requests.filter(request => request.pathname === ACCOUNT_PATHS.wallets).length; expect(automaticWalletReads).toBe(1); await host.expectNoTradingActions(); diff --git a/e2e/binance-orderbook/specs/account-rebalance-behavior.pw.js b/e2e/binance-orderbook/specs/account-rebalance-behavior.pw.js index 51aa8b4..83f4026 100644 --- a/e2e/binance-orderbook/specs/account-rebalance-behavior.pw.js +++ b/e2e/binance-orderbook/specs/account-rebalance-behavior.pw.js @@ -17,10 +17,18 @@ 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) }); }); + // Manual scenarios start after this empty episode has already been consumed. + await page.evaluate(async () => { + const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode('fixture-account')); + const accountKey = 'userscripts:automatic-usdt-rebalance:v1:' + + Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join(''); + localStorage.setItem(accountKey, JSON.stringify({ version: 1, status: 'consumed' })); + }); await page.clock.runFor(2000); const action = page.locator('[data-usdt-rebalance]'); await expect(action).toBeVisible(); await expect(action).toBeEnabled(); + await expect(page.locator('#jh-binance-auto-rebalance-status')).toHaveText('本轮不再自动执行账户再平衡'); return { api, errors, action, status: page.locator('#jh-binance-ladder-status') }; } diff --git a/e2e/binance-orderbook/specs/automatic-account-rebalance.pw.js b/e2e/binance-orderbook/specs/automatic-account-rebalance.pw.js index 8e2ab0c..0f0a609 100644 --- a/e2e/binance-orderbook/specs/automatic-account-rebalance.pw.js +++ b/e2e/binance-orderbook/specs/automatic-account-rebalance.pw.js @@ -7,6 +7,8 @@ import { installScenarioClock, pauseScenarioClock } from '../helpers/scenario-cl const PREFIX = 'userscripts:automatic-usdt-rebalance:v1:'; const EXCESS = { FUNDING: '0', MAIN: '0', UMFUTURE: '100' }; const TARGET = { FUNDING: '50', MAIN: '40', UMFUTURE: '10' }; +const REPORTED_BALANCES = { FUNDING: '77.50900867', MAIN: '59.49944996', UMFUTURE: '15.22316207' }; +const REPORTED_TARGET = { FUNDING: '76.11581035', MAIN: '60.89264828', UMFUTURE: '15.22316207' }; const transfers = api => api.snapshot().requests.filter(r => r.pathname === ACCOUNT_PATHS.transfer); async function episodeRecords(page) { @@ -18,6 +20,14 @@ async function episodes(page) { return (await episodeRecords(page)).map(record => record.status); } +async function seedEpisode(page, record) { + await page.evaluate(async ({ prefix, record }) => { + const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode('fixture-account')); + const accountKey = prefix + Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join(''); + localStorage.setItem(accountKey, JSON.stringify(record)); + }, { prefix: PREFIX, record }); +} + async function openAutomatic(page, api) { await installScenarioClock(page); const loaded = await openUserscriptScenario(page, createCancelScenario({ orders: ORDER_SETS.current })); @@ -228,62 +238,212 @@ test('user automatically withdraws excess once after a stable empty account with expect(api.snapshot().balances).toEqual(TARGET); }); -test('user keeps reserves after a loss and does not repeat an empty episode after reload', async ({ page }) => { - // Given Futures is below its allocation and reserves remain untouched. +test('user automatically corrects Funding and Spot allocation when Futures is already on target', async ({ page }) => { + // Given the reported account has excess Funding and a matching Spot deficit while Futures is on target. + const api = createAccountRebalanceApi(REPORTED_BALANCES); + const { errors } = await openAutomatic(page, api); + + // When native orders clear and the account stays empty for one millisecond less than two seconds. + await page.evaluate(() => window.__BINANCE_FIXTURE__.setOrders([])); + await page.clock.runFor(1999); + + // Then the stability gate keeps every balance untouched and does not expose a premature manual action. + await expectAutomaticStatus(page, '自动再平衡:等待账户持续无持仓、无挂单'); + await expect(page.locator('[data-usdt-rebalance]')).toBeHidden(); + expect(transfers(api)).toEqual([]); + expect(api.snapshot().balances).toEqual(REPORTED_BALANCES); + + // When the final millisecond qualifies the flat account through fresh API responses. + await page.clock.runFor(1); + + // Then one exact Funding-to-Spot transfer reaches the complete allocation without a dialog or a trade. + await expect.poll(() => api.snapshot().balances).toEqual(REPORTED_TARGET); + await expectAutomaticStatus(page, '已自动进行账户再平衡'); + expect(transfers(api).map(request => request.body)).toEqual([ + { asset: 'USDT', amount: '1.39319832', kindType: 'CARD_MAIN' }, + ]); + expect(await episodeRecords(page)).toEqual([ + { version: 1, status: 'consumed', outcome: 'automatic_completed' }, + ]); + await expect(page.getByRole('dialog')).toHaveCount(0); + expect((await readFixtureState(page)).events.filter(event => /order-submitted|cancel-requested/.test(event.type))).toEqual([]); + + // When the user reloads and the same empty episode passes the full qualification window again. + await reloadPageWithCoverage(page); + await becomeFlat(page); + + // Then the automatic completion remains durable without repeating the reserve correction. + await expectAutomaticStatus(page, '已自动进行账户再平衡'); + expect(await episodeRecords(page)).toEqual([ + { version: 1, status: 'consumed', outcome: 'automatic_completed' }, + ]); + expect(transfers(api)).toHaveLength(1); + expect(api.snapshot().balances).toEqual(REPORTED_TARGET); + expect((await readFixtureState(page)).events.filter(event => /order-submitted|cancel-requested/.test(event.type))).toEqual([]); + + // When the completed episode's watchdog advances and the user requests the manual account plan. + await page.clock.runFor(5000); + await page.locator('[data-usdt-rebalance]').click(); + + // Then the public manual workflow reports an already balanced account without another transfer to confirm. + await expect(page.locator('#jh-binance-ladder-status')).toHaveText('USDT 已按 5:4:1 分配'); + await expect(page.getByRole('dialog')).toHaveCount(0); + expect(transfers(api)).toHaveLength(1); + expect(api.snapshot().balances).toEqual(REPORTED_TARGET); + expect(errors).toEqual([]); +}); + +for (const allocation of [ + { + name: 'Spot excess to Funding when Futures is on target', + before: { FUNDING: '40', MAIN: '50', UMFUTURE: '10' }, + expectedTransfers: [{ asset: 'USDT', amount: '10', kindType: 'MAIN_CARD' }], + }, + { + name: 'Funding and Futures excess to Spot', + before: { FUNDING: '60', MAIN: '20', UMFUTURE: '20' }, + expectedTransfers: [ + { asset: 'USDT', amount: '10', kindType: 'CARD_MAIN' }, + { asset: 'USDT', amount: '10', kindType: 'FUTURE_MAIN' }, + ], + }, + { + name: 'Spot and Futures excess to Funding', + before: { FUNDING: '20', MAIN: '60', UMFUTURE: '20' }, + expectedTransfers: [ + { asset: 'USDT', amount: '20', kindType: 'MAIN_CARD' }, + { asset: 'USDT', amount: '10', kindType: 'FUTURE_CARD' }, + ], + }, +]) { + test(`user automatically transfers ${allocation.name}`, async ({ page }) => { + // Given a globally flat account can reach the complete allocation without replenishing Futures. + const api = createAccountRebalanceApi(allocation.before); + const { errors } = await openAutomatic(page, api); + + // When the native account clears its last order and completes the qualification window. + await becomeFlat(page); + + // Then the actual transfer requests reach every target once and preserve the completed episode. + await expect.poll(() => api.snapshot().balances).toEqual(TARGET); + await expectAutomaticStatus(page, '已自动进行账户再平衡'); + expect(transfers(api).map(request => request.body)).toEqual(allocation.expectedTransfers); + expect(await episodeRecords(page)).toEqual([ + { version: 1, status: 'consumed', outcome: 'automatic_completed' }, + ]); + await expect(page.getByRole('dialog')).toHaveCount(0); + expect((await readFixtureState(page)).events.filter(event => /order-submitted|cancel-requested/.test(event.type))).toEqual([]); + expect(errors).toEqual([]); + }); +} + +test('user restores the complete allocation after a loss only once per empty episode', async ({ page }) => { + // Given Futures is below its allocation while Funding and Spot can restore the full ratio. const below = { FUNDING: '50', MAIN: '40', UMFUTURE: '0' }; const api = createAccountRebalanceApi(below); await openAutomatic(page, api); - // When automatic qualification completes without any excess to transfer. + // When the globally flat account completes its automatic qualification window. await becomeFlat(page); - // Then no reserve funds are moved and the zero-transfer episode is consumed. - await expect.poll(() => episodes(page)).toEqual(['consumed']); - await expectAutomaticStatus(page, '自动再平衡:合约账户无多余 USDT'); - expect(transfers(api)).toEqual([]); - expect(api.snapshot().balances).toEqual(below); + // Then Funding and Spot replenish Futures exactly once and the complete allocation is confirmed. + await expect.poll(() => api.snapshot().balances).toEqual({ FUNDING: '45', MAIN: '36', UMFUTURE: '9' }); + await expectAutomaticStatus(page, '已自动进行账户再平衡'); + expect(transfers(api).map(request => request.body)).toEqual([ + { asset: 'USDT', amount: '5', kindType: 'CARD_FUTURE' }, + { asset: 'USDT', amount: '4', kindType: 'MAIN_FUTURE' }, + ]); + expect(await episodeRecords(page)).toEqual([ + { version: 1, status: 'consumed', outcome: 'automatic_completed' }, + ]); + await expect(page.getByRole('dialog')).toHaveCount(0); + expect((await readFixtureState(page)).events.filter(event => /order-submitted|cancel-requested/.test(event.type))).toEqual([]); - // When a manual deposit changes balances and the user reloads the same empty episode. + // When an external transfer changes balances and the user reloads the same empty episode. api.setBalances(EXCESS); await reloadPageWithCoverage(page); await becomeFlat(page); await expect(page.locator('[data-usdt-rebalance]')).toBeEnabled(); // Then reloading never creates a second automatic execution entitlement. - await expectAutomaticStatus(page, '本轮不再自动执行账户再平衡'); - expect(await episodes(page)).toEqual(['consumed']); - expect(transfers(api)).toEqual([]); + await expectAutomaticStatus(page, '已自动进行账户再平衡'); + expect(await episodeRecords(page)).toEqual([ + { version: 1, status: 'consumed', outcome: 'automatic_completed' }, + ]); + expect(transfers(api)).toHaveLength(2); + expect(api.snapshot().balances).toEqual(EXCESS); - // When the retained no-transfer episode is displayed in English. + // When the retained completion is displayed in English. await switchLocale(page, 'en'); - // Then the message describes the episode without claiming a completed transfer or current balance. - await expectAutomaticStatus(page, 'No further automatic rebalance in this round'); - expect(await episodeRecords(page)).toEqual([{ version: 1, status: 'consumed' }]); + // Then the historical completion translates without another replenishment or withdrawal. + await expectAutomaticStatus(page, 'Account automatically rebalanced'); expect(api.snapshot().balances).toEqual(EXCESS); - expect(transfers(api)).toEqual([]); + expect(transfers(api)).toHaveLength(2); }); -test('user does not see a completed transfer inferred from an older episode record', async ({ page }) => { - // Given an earlier script stored a consumed episode without a financial outcome. - const api = createAccountRebalanceApi(EXCESS); +test('user sees a localized no-transfer result for an already balanced automatic episode', async ({ page }) => { + // Given all three accounts already hold the exact full allocation. + const api = createAccountRebalanceApi(TARGET); await openAutomatic(page, api); - await page.evaluate(async prefix => { - const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode('fixture-account')); - const accountKey = prefix + Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join(''); - localStorage.setItem(accountKey, JSON.stringify({ version: 1, status: 'consumed' })); - }, PREFIX); - // When the current script observes the same empty episode. + // When the account becomes empty and qualifies for its automatic check. await becomeFlat(page); - // Then the old record remains protected without claiming success or moving funds. - await expectAutomaticStatus(page, '本轮不再自动执行账户再平衡'); + // Then the Chinese result explains that no transfer was needed without marking a completed transfer. + await expectAutomaticStatus(page, '自动再平衡:无需划转'); + expect(transfers(api)).toEqual([]); + expect(api.snapshot().balances).toEqual(TARGET); expect(await episodeRecords(page)).toEqual([{ version: 1, status: 'consumed' }]); + + // When the same no-transfer result is displayed in English. + await switchLocale(page, 'en'); + + // Then both the status and tooltip translate without moving any balance. + await expectAutomaticStatus(page, 'Automatic account rebalance: no transfer needed'); + expect(transfers(api)).toEqual([]); + expect(api.snapshot().balances).toEqual(TARGET); + + // When the user reloads the same already consumed episode. + await reloadPageWithCoverage(page); + await becomeFlat(page); + + // Then the episode stays protected without inventing a successful financial operation. + await expectAutomaticStatus(page, 'No further automatic rebalance in this round'); + expect(await episodeRecords(page)).toEqual([{ version: 1, status: 'consumed' }]); + expect(api.snapshot().balances).toEqual(TARGET); expect(transfers(api)).toEqual([]); - expect(api.snapshot().balances).toEqual(EXCESS); }); +for (const episode of [ + { + name: 'status-only consumed', + record: { version: 1, status: 'consumed' }, + expectedStatus: '本轮不再自动执行账户再平衡', + }, + { + name: 'completed automatic', + record: { version: 1, status: 'consumed', outcome: 'automatic_completed' }, + expectedStatus: '已自动进行账户再平衡', + }, +]) { + test(`user keeps misallocated balances unchanged for a stored ${episode.name} episode`, async ({ page }) => { + // Given an earlier episode is consumed while the current Funding and Spot balances need correction. + const api = createAccountRebalanceApi(REPORTED_BALANCES); + await openAutomatic(page, api); + await seedEpisode(page, episode.record); + + // When the current script observes the same empty episode with the stored record. + await becomeFlat(page); + + // Then the stored outcome is preserved and no new allocation runs without verified account activity. + await expectAutomaticStatus(page, episode.expectedStatus); + expect(await episodeRecords(page)).toEqual([episode.record]); + expect(transfers(api)).toEqual([]); + expect(api.snapshot().balances).toEqual(REPORTED_BALANCES); + }); +} + test('user sees a localized refusal for an invalid persisted completion outcome', async ({ page }) => { // Given a stored completion marker conflicts with its in-flight protection. const api = createAccountRebalanceApi(EXCESS); @@ -313,11 +473,12 @@ test('user sees a localized refusal for an invalid persisted completion outcome' }); test('user does not see a manual rebalance reported as an automatic completion after reload', async ({ page }) => { - // Given automatic qualification left the reserves alone and the user reviewed a manual plan. + // Given the automatic episode is already consumed and the user reviews a manual plan. const api = createAccountRebalanceApi({ FUNDING: '100', MAIN: '0', UMFUTURE: '0' }); await openAutomatic(page, api); + await seedEpisode(page, { version: 1, status: 'consumed' }); await becomeFlat(page); - await expectAutomaticStatus(page, '自动再平衡:合约账户无多余 USDT'); + await expectAutomaticStatus(page, '本轮不再自动执行账户再平衡'); await page.clock.runFor(32); await expect(page.locator('[data-usdt-rebalance]')).toBeVisible(); await page.locator('[data-usdt-rebalance]').click(); @@ -418,27 +579,53 @@ for (const kind of ['basic', 'conditional']) { }); } -test('user receives one automatic allocation across two simultaneous currency tabs', async ({ page, context }) => { - // Given two pages share one browser storage partition and one account. - const api = createAccountRebalanceApi(EXCESS); - await openAutomatic(page, api); - await page.clock.resume(); - const second = await context.newPage(); - await openAutomatic(second, api); - - // When both pages qualify for the same empty account at the same time. - await Promise.all([page, second].map(tab => tab.evaluate(() => window.__BINANCE_FIXTURE__.setOrders([])))); - await page.clock.runFor(2032); - await second.clock.runFor(2032); - - // Then shared locking and durable state permit only the two planned transfers. - await expect.poll(() => api.snapshot().balances).toEqual(TARGET); - await expect.poll(() => episodes(page)).toEqual(['consumed']); - expect(transfers(api)).toHaveLength(2); - expect(await episodes(second)).toEqual(['consumed']); - await expectAutomaticStatus(page, '已自动进行账户再平衡'); - await expectAutomaticStatus(second, '已自动进行账户再平衡'); -}); +for (const allocation of [ + { + name: 'Futures withdrawals', + before: EXCESS, + target: TARGET, + expectedTransfers: [ + { asset: 'USDT', amount: '50', kindType: 'FUTURE_CARD' }, + { asset: 'USDT', amount: '40', kindType: 'FUTURE_MAIN' }, + ], + }, + { + name: 'Funding-to-Spot correction', + before: REPORTED_BALANCES, + target: REPORTED_TARGET, + expectedTransfers: [{ asset: 'USDT', amount: '1.39319832', kindType: 'CARD_MAIN' }], + }, +]) { + test(`user receives ${allocation.name} only once across two simultaneous currency tabs`, async ({ page, context }) => { + // Given two pages share one browser storage partition and the same account balances. + const api = createAccountRebalanceApi(allocation.before); + const firstLoaded = await openAutomatic(page, api); + await page.clock.resume(); + const second = await context.newPage(); + const secondLoaded = await openAutomatic(second, api); + + // When both pages qualify for the same empty account at the same time. + await Promise.all([page, second].map(tab => tab.evaluate(() => window.__BINANCE_FIXTURE__.setOrders([])))); + await page.clock.runFor(2032); + await second.clock.runFor(2032); + + // Then shared locking and durable state permit exactly one complete allocation across both pages. + await expect.poll(() => api.snapshot().balances).toEqual(allocation.target); + await expectAutomaticStatus(page, '已自动进行账户再平衡'); + await expectAutomaticStatus(second, '已自动进行账户再平衡'); + expect(transfers(api).map(request => request.body)).toEqual(allocation.expectedTransfers); + expect(await episodeRecords(page)).toEqual([ + { version: 1, status: 'consumed', outcome: 'automatic_completed' }, + ]); + expect(await episodeRecords(second)).toEqual([ + { version: 1, status: 'consumed', outcome: 'automatic_completed' }, + ]); + expect((await readFixtureState(page)).events.filter(event => /order-submitted|cancel-requested/.test(event.type))).toEqual([]); + expect((await readFixtureState(second)).events.filter(event => /order-submitted|cancel-requested/.test(event.type))).toEqual([]); + expect(firstLoaded.errors).toEqual([]); + expect(secondLoaded.errors).toEqual([]); + }); +} test('user qualifies again only after independently verified new account activity', async ({ page }) => { // Given the first automatic episode has completed. diff --git a/e2e/binance-orderbook/specs/continuous-rebalance-behavior.pw.js b/e2e/binance-orderbook/specs/continuous-rebalance-behavior.pw.js index c9c2628..c35e8fb 100644 --- a/e2e/binance-orderbook/specs/continuous-rebalance-behavior.pw.js +++ b/e2e/binance-orderbook/specs/continuous-rebalance-behavior.pw.js @@ -9,6 +9,10 @@ const ACTION = '[data-usdt-rebalance]'; const STATUS = '#jh-binance-ladder-status'; const OTHER_POSITION = { symbol: OTHER_SYMBOL, side: 'LONG', quantity: '1' }; const API_OTHER_POSITION = { symbol: OTHER_SYMBOL, positionSide: 'LONG', positionAmount: '1' }; +const REBALANCE_TRANSFERS = [ + { asset: 'USDT', amount: '40', kindType: 'CARD_MAIN' }, + { asset: 'USDT', amount: '10', kindType: 'CARD_FUTURE' }, +]; /** Keep the last acknowledgement pending while native account updates settle. */ async function openPendingClose(page, side) { @@ -56,17 +60,18 @@ async function finishClose(page, host) { ); } -async function expectNoAdditionalTrading(page, host) { +async function expectNoAdditionalTrading(page, host, expectedTransfers = []) { const events = (await readFixtureState(page)).events; expect(events.filter(({ type }) => type === 'order-submitted').map(({ action }) => action)) .toEqual(Array(3).fill(host.action)); expect(events.filter(({ type }) => /cancel-requested/.test(type))).toEqual([]); - expect(host.api.snapshot().requests.filter(({ pathname }) => pathname === ACCOUNT_PATHS.transfer)).toEqual([]); + expect(host.api.snapshot().requests.filter(({ pathname }) => pathname === ACCOUNT_PATHS.transfer) + .map(({ body }) => body)).toEqual(expectedTransfers); expect(host.errors).toEqual([]); } for (const side of ['LONG', 'SHORT']) { - test(`user regains rebalance after continuous ${side} closing misses its flat check while busy`, async ({ page }) => { + test(`user automatically rebalances after continuous ${side} closing misses its flat check while busy`, async ({ page }) => { // Given the final close acknowledgement is held while the account becomes flat. const host = await openPendingClose(page, side); await publishAccount(page); @@ -79,6 +84,7 @@ for (const side of ['LONG', 'SHORT']) { await expect(page.locator('[data-account-tab="openOrders"]')).toHaveText('当前委托(0)'); await expect(page.getByRole('button', { name: `停止${host.action}`, exact: true })).toBeVisible(); await expect(page.locator(ACTION)).toBeHidden(); + await expectNoAdditionalTrading(page, host); // When the final acknowledgement arrives without another account-counter change. await finishClose(page, host); @@ -110,14 +116,15 @@ for (const side of ['LONG', 'SHORT']) { await (await response).finished(); await page.clock.runFor(32); - // Then the action returns automatically without any extra order or transfer. - await expect.poll(async () => { - await page.clock.runFor(32); - return page.locator(ACTION).isEnabled(); - }).toBe(true); - await expect(page.locator(ACTION)).toBeVisible(); - await expect(page.locator(ACTION)).toBeEnabled(); - await expectNoAdditionalTrading(page, host); + // Then the account reaches its complete allocation without any extra order. + await expect(page.locator('#jh-binance-auto-rebalance-status')).toHaveText('已自动进行账户再平衡'); + await page.clock.runFor(32); + await expect(page.locator(ACTION)).toBeHidden(); + await expect(page.locator(STATUS)).toHaveText( + `连续阶梯${host.action} · 已结束 · 当前方向已无持仓 · 1/1 轮 · 累计 3 笔`, + ); + expect(host.api.snapshot().balances).toEqual({ FUNDING: '50', MAIN: '40', UMFUTURE: '10' }); + await expectNoAdditionalTrading(page, host, REBALANCE_TRANSFERS); }); } @@ -171,6 +178,7 @@ test('user qualifies after returning to a page where continuous closing finished // Then completion cannot start a hidden account qualification request. expect(host.api.snapshot().requests).toHaveLength(completedRequests); await expect(page.locator(ACTION)).toBeHidden(); + await expectNoAdditionalTrading(page, host); // When the page returns and completes a fresh visible flat window. await setSimulatedVisibility(page, false); @@ -185,7 +193,10 @@ test('user qualifies after returning to a page where continuous closing finished await (await response).finished(); await page.clock.runFor(32); - // Then returning visibility restores the manual action without automatic transfers. - await expect(page.locator(ACTION)).toBeVisible(); - await expectNoAdditionalTrading(page, host); + // Then returning visibility allows one complete allocation and consumes the account action. + await expect(page.locator('#jh-binance-auto-rebalance-status')).toHaveText('已自动进行账户再平衡'); + await page.clock.runFor(32); + await expect(page.locator(ACTION)).toBeHidden(); + expect(host.api.snapshot().balances).toEqual({ FUNDING: '50', MAIN: '40', UMFUTURE: '10' }); + await expectNoAdditionalTrading(page, host, REBALANCE_TRANSFERS); }); diff --git a/scripts/binance-orderbook-trade.user.js b/scripts/binance-orderbook-trade.user.js index 348478d..154a640 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.224 +// @version 2.7.225 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -1119,7 +1119,7 @@ Copyright (C) 2014 Yusuke Suzuki blocked: localizedText("自动再平衡已阻止:请先核实上次划转结果", "Automatic USDT transfer blocked: previous outcome requires account review"), completed: localizedText("已自动进行账户再平衡", "Account automatically rebalanced"), notRepeated: localizedText("本轮不再自动执行账户再平衡", "No further automatic rebalance in this round"), - noExcess: localizedText("自动再平衡:合约账户无多余 USDT", "Automatic USDT transfer: no Futures excess"), + noTransfer: localizedText("自动再平衡:无需划转", "Automatic account rebalance: no transfer needed"), paused: localizedText("自动再平衡已暂停:执行条件已变化", "Automatic USDT transfer paused: eligibility changed"), stopped: localizedText("自动再平衡已停止:", "Automatic USDT transfer stopped: "), eligibilityFailed: localizedText("自动再平衡资格检查失败:", "Automatic USDT eligibility check failed: "), @@ -1134,7 +1134,6 @@ Copyright (C) 2014 Yusuke Suzuki identityChanged: localizedText("账户身份已变化", "Account identity changed"), identityChangedAfterConfirmation: localizedText("确认后账户身份已变化", "Account identity changed after confirmation"), reviewRequired: localizedText("请先核实上次划转结果", "Previous transfer outcome requires account review"), - excessOnly: localizedText("自动划转仅可转出合约账户多余资金", "Automatic transfers may only withdraw Futures excess"), flatRequired: localizedText("全账户持仓和当前委托必须为零", "Account-wide positions and open orders must be zero"), accountNotFlat: localizedText("全账户仍有持仓或当前委托", "Positions or open orders still exist in the account"), invalidOutcome: localizedText("自动再平衡结果记录无效", "Invalid automatic rebalance episode outcome") @@ -1770,47 +1769,6 @@ Copyright (C) 2014 Yusuke Suzuki transfers }; } - function buildAutomaticUsdtRebalancePlan(rawBalances) { - const beforeUnits = Object.fromEntries( - ACCOUNT_ORDER.map((accountCode) => [accountCode, decimalToUnits(rawBalances[accountCode])]) - ); - const totalUnits = ACCOUNT_ORDER.reduce((sum, accountCode) => sum + beforeUnits[accountCode], 0n); - const fundingTarget = totalUnits * 50n / 100n; - const spotTarget = totalUnits * 40n / 100n; - const futuresTarget = totalUnits - fundingTarget - spotTarget; - const targetUnits = { ...beforeUnits }; - const transfers = []; - if (beforeUnits.UMFUTURE > futuresTarget) { - const excess = beforeUnits.UMFUTURE - futuresTarget; - const fundingDeficit = fundingTarget > beforeUnits.FUNDING ? fundingTarget - beforeUnits.FUNDING : 0n; - const toFunding = excess < fundingDeficit ? excess : fundingDeficit; - const distributions = [ - { accountCode: "FUNDING", amount: toFunding }, - { accountCode: "MAIN", amount: excess - toFunding } - ]; - for (const { accountCode, amount } of distributions) { - if (amount === 0n) continue; - transfers.push({ - from: "UMFUTURE", - to: accountCode, - kindType: `${USDT_REBALANCE_ACCOUNTS.UMFUTURE.bapiCode}_${USDT_REBALANCE_ACCOUNTS[accountCode].bapiCode}`, - amount: unitsToDecimal(amount) - }); - targetUnits[accountCode] += amount; - targetUnits.UMFUTURE -= amount; - } - } - return { - total: unitsToDecimal(totalUnits), - before: Object.fromEntries( - ACCOUNT_ORDER.map((accountCode) => [accountCode, unitsToDecimal(beforeUnits[accountCode])]) - ), - targets: Object.fromEntries( - ACCOUNT_ORDER.map((accountCode) => [accountCode, unitsToDecimal(targetUnits[accountCode])]) - ), - transfers - }; - } function applyUsdtTransferToBalances(rawBalances, transfer) { if (!ACCOUNT_ORDER.includes(transfer?.from) || !ACCOUNT_ORDER.includes(transfer?.to)) { throw new Error("USDT 划转账户无效"); @@ -21029,9 +20987,6 @@ Copyright (C) 2014 Yusuke Suzuki } if (await readUsdtRebalanceAccountKey() !== accountKey) throw new Error("Account identity changed"); assertUsdtRebalanceLocalState(options); - if (options.automatic && (transfer.from !== "UMFUTURE" || !["FUNDING", "MAIN"].includes(transfer.to))) { - throw new Error("Automatic transfers may only withdraw Futures excess"); - } writeUsdtRebalanceEpisode(accountKey, "in_flight"); if (options.automatic) { setAutomaticUsdtRebalanceStatus(localizedText( @@ -21086,11 +21041,11 @@ Copyright (C) 2014 Yusuke Suzuki await assertUsdtRebalanceTradingState(options); const balances = await readCurrentUsdtRebalanceBalances(); assertUsdtRebalanceLocalState(options); - const plan = buildAutomaticUsdtRebalancePlan(balances); + const plan = buildUsdtRebalancePlan(balances); writeUsdtRebalanceEpisode(accountKey, "consumed"); if (plan.transfers.length === 0) { - setAutomaticUsdtRebalanceStatus(PANEL_COPY.automaticRebalance.noExcess); - return { status: "no_excess", plan }; + setAutomaticUsdtRebalanceStatus(PANEL_COPY.automaticRebalance.noTransfer); + return { status: "no_transfer", plan }; } return executeUsdtRebalancePlan(plan, accountKey, options); }, signal); diff --git a/scripts/binance-strategy27-events.user.js b/scripts/binance-strategy27-events.user.js index 559e2e0..52826c0 100644 --- a/scripts/binance-strategy27-events.user.js +++ b/scripts/binance-strategy27-events.user.js @@ -3,7 +3,7 @@ // @namespace binance.strategy27.events // @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 0.6.10 +// @version 0.6.11 // @author jackhai9 // @description Display Strategy 27 events and provide the shared private CorsairQuant gateway connection // @match https://www.binance.com/*/futures/* @@ -913,7 +913,7 @@ blocked: localizedText("自动再平衡已阻止:请先核实上次划转结果", "Automatic USDT transfer blocked: previous outcome requires account review"), completed: localizedText("已自动进行账户再平衡", "Account automatically rebalanced"), notRepeated: localizedText("本轮不再自动执行账户再平衡", "No further automatic rebalance in this round"), - noExcess: localizedText("自动再平衡:合约账户无多余 USDT", "Automatic USDT transfer: no Futures excess"), + noTransfer: localizedText("自动再平衡:无需划转", "Automatic account rebalance: no transfer needed"), paused: localizedText("自动再平衡已暂停:执行条件已变化", "Automatic USDT transfer paused: eligibility changed"), stopped: localizedText("自动再平衡已停止:", "Automatic USDT transfer stopped: "), eligibilityFailed: localizedText("自动再平衡资格检查失败:", "Automatic USDT eligibility check failed: "), @@ -928,7 +928,6 @@ identityChanged: localizedText("账户身份已变化", "Account identity changed"), identityChangedAfterConfirmation: localizedText("确认后账户身份已变化", "Account identity changed after confirmation"), reviewRequired: localizedText("请先核实上次划转结果", "Previous transfer outcome requires account review"), - excessOnly: localizedText("自动划转仅可转出合约账户多余资金", "Automatic transfers may only withdraw Futures excess"), flatRequired: localizedText("全账户持仓和当前委托必须为零", "Account-wide positions and open orders must be zero"), accountNotFlat: localizedText("全账户仍有持仓或当前委托", "Positions or open orders still exist in the account"), invalidOutcome: localizedText("自动再平衡结果记录无效", "Invalid automatic rebalance episode outcome") diff --git a/scripts/binance-strategy29-bollinger.user.js b/scripts/binance-strategy29-bollinger.user.js index 97bf617..86b589c 100644 --- a/scripts/binance-strategy29-bollinger.user.js +++ b/scripts/binance-strategy29-bollinger.user.js @@ -3,7 +3,7 @@ // @namespace binance.strategy29.bollinger // @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 0.5.10 +// @version 0.5.11 // @author jackhai9 // @description Bollinger/SMA60 overlay markers and the default read-only cross-timeframe summary // @match https://www.binance.com/*/futures/* @@ -1824,7 +1824,7 @@ blocked: localizedText("自动再平衡已阻止:请先核实上次划转结果", "Automatic USDT transfer blocked: previous outcome requires account review"), completed: localizedText("已自动进行账户再平衡", "Account automatically rebalanced"), notRepeated: localizedText("本轮不再自动执行账户再平衡", "No further automatic rebalance in this round"), - noExcess: localizedText("自动再平衡:合约账户无多余 USDT", "Automatic USDT transfer: no Futures excess"), + noTransfer: localizedText("自动再平衡:无需划转", "Automatic account rebalance: no transfer needed"), paused: localizedText("自动再平衡已暂停:执行条件已变化", "Automatic USDT transfer paused: eligibility changed"), stopped: localizedText("自动再平衡已停止:", "Automatic USDT transfer stopped: "), eligibilityFailed: localizedText("自动再平衡资格检查失败:", "Automatic USDT eligibility check failed: "), @@ -1839,7 +1839,6 @@ identityChanged: localizedText("账户身份已变化", "Account identity changed"), identityChangedAfterConfirmation: localizedText("确认后账户身份已变化", "Account identity changed after confirmation"), reviewRequired: localizedText("请先核实上次划转结果", "Previous transfer outcome requires account review"), - excessOnly: localizedText("自动划转仅可转出合约账户多余资金", "Automatic transfers may only withdraw Futures excess"), flatRequired: localizedText("全账户持仓和当前委托必须为零", "Account-wide positions and open orders must be zero"), accountNotFlat: localizedText("全账户仍有持仓或当前委托", "Positions or open orders still exist in the account"), invalidOutcome: localizedText("自动再平衡结果记录无效", "Invalid automatic rebalance episode outcome") diff --git a/scripts/binance-strategy31-volume-reversal.user.js b/scripts/binance-strategy31-volume-reversal.user.js index f483b99..8e76010 100644 --- a/scripts/binance-strategy31-volume-reversal.user.js +++ b/scripts/binance-strategy31-volume-reversal.user.js @@ -3,7 +3,7 @@ // @namespace binance.strategy31.volume-reversal // @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 0.1.4 +// @version 0.1.5 // @author jackhai9 // @description Confirmed red-to-green volume signals from CorsairQuant // @match https://www.binance.com/*/futures/* @@ -823,7 +823,7 @@ blocked: localizedText("自动再平衡已阻止:请先核实上次划转结果", "Automatic USDT transfer blocked: previous outcome requires account review"), completed: localizedText("已自动进行账户再平衡", "Account automatically rebalanced"), notRepeated: localizedText("本轮不再自动执行账户再平衡", "No further automatic rebalance in this round"), - noExcess: localizedText("自动再平衡:合约账户无多余 USDT", "Automatic USDT transfer: no Futures excess"), + noTransfer: localizedText("自动再平衡:无需划转", "Automatic account rebalance: no transfer needed"), paused: localizedText("自动再平衡已暂停:执行条件已变化", "Automatic USDT transfer paused: eligibility changed"), stopped: localizedText("自动再平衡已停止:", "Automatic USDT transfer stopped: "), eligibilityFailed: localizedText("自动再平衡资格检查失败:", "Automatic USDT eligibility check failed: "), @@ -838,7 +838,6 @@ identityChanged: localizedText("账户身份已变化", "Account identity changed"), identityChangedAfterConfirmation: localizedText("确认后账户身份已变化", "Account identity changed after confirmation"), reviewRequired: localizedText("请先核实上次划转结果", "Previous transfer outcome requires account review"), - excessOnly: localizedText("自动划转仅可转出合约账户多余资金", "Automatic transfers may only withdraw Futures excess"), flatRequired: localizedText("全账户持仓和当前委托必须为零", "Account-wide positions and open orders must be zero"), accountNotFlat: localizedText("全账户仍有持仓或当前委托", "Positions or open orders still exist in the account"), invalidOutcome: localizedText("自动再平衡结果记录无效", "Invalid automatic rebalance episode outcome") diff --git a/src/binance-orderbook-trade/contracts/panel-copy.js b/src/binance-orderbook-trade/contracts/panel-copy.js index 4b5a86f..020da88 100644 --- a/src/binance-orderbook-trade/contracts/panel-copy.js +++ b/src/binance-orderbook-trade/contracts/panel-copy.js @@ -129,7 +129,7 @@ export const PANEL_COPY = Object.freeze({ blocked: localizedText('自动再平衡已阻止:请先核实上次划转结果', 'Automatic USDT transfer blocked: previous outcome requires account review'), completed: localizedText('已自动进行账户再平衡', 'Account automatically rebalanced'), notRepeated: localizedText('本轮不再自动执行账户再平衡', 'No further automatic rebalance in this round'), - noExcess: localizedText('自动再平衡:合约账户无多余 USDT', 'Automatic USDT transfer: no Futures excess'), + noTransfer: localizedText('自动再平衡:无需划转', 'Automatic account rebalance: no transfer needed'), paused: localizedText('自动再平衡已暂停:执行条件已变化', 'Automatic USDT transfer paused: eligibility changed'), stopped: localizedText('自动再平衡已停止:', 'Automatic USDT transfer stopped: '), eligibilityFailed: localizedText('自动再平衡资格检查失败:', 'Automatic USDT eligibility check failed: '), @@ -144,7 +144,6 @@ export const PANEL_COPY = Object.freeze({ identityChanged: localizedText('账户身份已变化', 'Account identity changed'), identityChangedAfterConfirmation: localizedText('确认后账户身份已变化', 'Account identity changed after confirmation'), reviewRequired: localizedText('请先核实上次划转结果', 'Previous transfer outcome requires account review'), - excessOnly: localizedText('自动划转仅可转出合约账户多余资金', 'Automatic transfers may only withdraw Futures excess'), flatRequired: localizedText('全账户持仓和当前委托必须为零', 'Account-wide positions and open orders must be zero'), accountNotFlat: localizedText('全账户仍有持仓或当前委托', 'Positions or open orders still exist in the account'), invalidOutcome: localizedText('自动再平衡结果记录无效', 'Invalid automatic rebalance episode outcome'), diff --git a/src/binance-orderbook-trade/core/usdt-rebalance.js b/src/binance-orderbook-trade/core/usdt-rebalance.js index 8a55b8c..4ffb255 100644 --- a/src/binance-orderbook-trade/core/usdt-rebalance.js +++ b/src/binance-orderbook-trade/core/usdt-rebalance.js @@ -127,6 +127,10 @@ export function withFuturesTransferableBalance(balances, payload) { }; } +/** + * Automatic and manually confirmed transfers share exact eight-decimal targets. + * Rounding each reserve down leaves the remainder in Futures without losing USDT. + */ export function buildUsdtRebalancePlan(rawBalances) { const beforeUnits = Object.fromEntries( ACCOUNT_ORDER.map((accountCode) => [accountCode, decimalToUnits(rawBalances[accountCode])]), @@ -194,57 +198,6 @@ export function buildUsdtRebalancePlan(rawBalances) { }; } -/** - * Automatic distribution can only withdraw futures excess. Reserve wallets - * never donate, so `targets` describes reachable final balances rather than a - * mandatory 5:4:1 allocation. Both ratio floors leave decimal dust in futures, - * preserving the manual planner's reserve of at least ten percent. - */ -export function buildAutomaticUsdtRebalancePlan(rawBalances) { - const beforeUnits = Object.fromEntries( - ACCOUNT_ORDER.map((accountCode) => [accountCode, decimalToUnits(rawBalances[accountCode])]), - ); - const totalUnits = ACCOUNT_ORDER.reduce((sum, accountCode) => sum + beforeUnits[accountCode], 0n); - const fundingTarget = totalUnits * 50n / 100n; - const spotTarget = totalUnits * 40n / 100n; - const futuresTarget = totalUnits - fundingTarget - spotTarget; - const targetUnits = { ...beforeUnits }; - const transfers = []; - - if (beforeUnits.UMFUTURE > futuresTarget) { - const excess = beforeUnits.UMFUTURE - futuresTarget; - const fundingDeficit = fundingTarget > beforeUnits.FUNDING - ? fundingTarget - beforeUnits.FUNDING : 0n; - const toFunding = excess < fundingDeficit ? excess : fundingDeficit; - const distributions = [ - { accountCode: 'FUNDING', amount: toFunding }, - { accountCode: 'MAIN', amount: excess - toFunding }, - ]; - for (const { accountCode, amount } of distributions) { - if (amount === 0n) continue; - transfers.push({ - from: 'UMFUTURE', - to: accountCode, - kindType: `${USDT_REBALANCE_ACCOUNTS.UMFUTURE.bapiCode}_${USDT_REBALANCE_ACCOUNTS[accountCode].bapiCode}`, - amount: unitsToDecimal(amount), - }); - targetUnits[accountCode] += amount; - targetUnits.UMFUTURE -= amount; - } - } - - return { - total: unitsToDecimal(totalUnits), - before: Object.fromEntries( - ACCOUNT_ORDER.map((accountCode) => [accountCode, unitsToDecimal(beforeUnits[accountCode])]), - ), - targets: Object.fromEntries( - ACCOUNT_ORDER.map((accountCode) => [accountCode, unitsToDecimal(targetUnits[accountCode])]), - ), - transfers, - }; -} - export function applyUsdtTransferToBalances(rawBalances, transfer) { if (!ACCOUNT_ORDER.includes(transfer?.from) || !ACCOUNT_ORDER.includes(transfer?.to)) { throw new Error('USDT 划转账户无效'); diff --git a/src/binance-orderbook-trade/index.user.js b/src/binance-orderbook-trade/index.user.js index 842e861..9180c90 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.224 +// @version 2.7.225 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -66,7 +66,6 @@ import { applyUsdtTransferToBalances, areUsdtBalancesEqual, buildUsdtRebalancePlan, - buildAutomaticUsdtRebalancePlan, parseUsdtWalletBalances, resolveAllFuturesPositionStatus, withFuturesTransferableBalance, @@ -6875,10 +6874,6 @@ installChartStorageOptimizer(); } if (await readUsdtRebalanceAccountKey() !== accountKey) throw new Error('Account identity changed'); assertUsdtRebalanceLocalState(options); - if (options.automatic && (transfer.from !== 'UMFUTURE' - || !['FUNDING', 'MAIN'].includes(transfer.to))) { - throw new Error('Automatic transfers may only withdraw Futures excess'); - } // Persist before sending: an aborted response does not establish whether funds moved. writeUsdtRebalanceEpisode(accountKey, 'in_flight'); if (options.automatic) { @@ -6936,11 +6931,11 @@ installChartStorageOptimizer(); await assertUsdtRebalanceTradingState(options); const balances = await readCurrentUsdtRebalanceBalances(); assertUsdtRebalanceLocalState(options); - const plan = buildAutomaticUsdtRebalancePlan(balances); + const plan = buildUsdtRebalancePlan(balances); writeUsdtRebalanceEpisode(accountKey, 'consumed'); if (plan.transfers.length === 0) { - setAutomaticUsdtRebalanceStatus(PANEL_COPY.automaticRebalance.noExcess); - return { status: 'no_excess', plan }; + setAutomaticUsdtRebalanceStatus(PANEL_COPY.automaticRebalance.noTransfer); + return { status: 'no_transfer', plan }; } return executeUsdtRebalancePlan(plan, accountKey, options); }, signal); diff --git a/src/binance-strategy27-events/index.user.js b/src/binance-strategy27-events/index.user.js index 96da64b..c395fc3 100644 --- a/src/binance-strategy27-events/index.user.js +++ b/src/binance-strategy27-events/index.user.js @@ -3,7 +3,7 @@ // @namespace binance.strategy27.events // @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 0.6.10 +// @version 0.6.11 // @author jackhai9 // @description Display Strategy 27 events and provide the shared private CorsairQuant gateway connection // @match https://www.binance.com/*/futures/* diff --git a/src/binance-strategy29-bollinger/index.user.js b/src/binance-strategy29-bollinger/index.user.js index 9e53679..9c5f5d1 100644 --- a/src/binance-strategy29-bollinger/index.user.js +++ b/src/binance-strategy29-bollinger/index.user.js @@ -3,7 +3,7 @@ // @namespace binance.strategy29.bollinger // @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 0.5.10 +// @version 0.5.11 // @author jackhai9 // @description Bollinger/SMA60 overlay markers and the default read-only cross-timeframe summary // @match https://www.binance.com/*/futures/* diff --git a/src/binance-strategy31-volume-reversal/index.user.js b/src/binance-strategy31-volume-reversal/index.user.js index 5352086..ec5cf94 100644 --- a/src/binance-strategy31-volume-reversal/index.user.js +++ b/src/binance-strategy31-volume-reversal/index.user.js @@ -3,7 +3,7 @@ // @namespace binance.strategy31.volume-reversal // @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 0.1.4 +// @version 0.1.5 // @author jackhai9 // @description Confirmed red-to-green volume signals from CorsairQuant // @match https://www.binance.com/*/futures/* diff --git a/test/unit/binance-orderbook-trade/usdt-rebalance.test.js b/test/unit/binance-orderbook-trade/usdt-rebalance.test.js index 7a7ccdb..7fd0321 100644 --- a/test/unit/binance-orderbook-trade/usdt-rebalance.test.js +++ b/test/unit/binance-orderbook-trade/usdt-rebalance.test.js @@ -5,7 +5,6 @@ import assert from 'node:assert/strict'; import { applyUsdtTransferToBalances, areUsdtBalancesEqual, - buildAutomaticUsdtRebalancePlan, buildUsdtRebalancePlan, parseUsdtWalletBalances, resolveAllFuturesPositionStatus, @@ -13,7 +12,55 @@ import { withFuturesTransferableBalance, } from '../../../src/binance-orderbook-trade/core/usdt-rebalance.js'; +test('user aligns funding and spot when futures already holds its target allocation', () => { + // Given futures already holds ten percent while funding exceeds its target + const balances = { + FUNDING: '77.50900867', MAIN: '59.49944996', UMFUTURE: '15.22316207', + }; + + // When the shared planner calculates and applies the remaining allocation + const plan = buildUsdtRebalancePlan(balances); + const finalBalances = plan.transfers.reduce(applyUsdtTransferToBalances, balances); + + // Then one funding-to-spot transfer completes the exact allocation + assert.deepEqual(plan.transfers, [ + { from: 'FUNDING', to: 'MAIN', kindType: 'CARD_MAIN', amount: '1.39319832' }, + ]); + assert.deepEqual(finalBalances, { + FUNDING: '76.11581035', MAIN: '60.89264828', UMFUTURE: '15.22316207', + }); + assert.deepEqual(plan.targets, finalBalances); + assert.deepEqual(buildUsdtRebalancePlan(finalBalances).transfers, []); +}); + for (const { label, before, targets, transfers } of [ + { + label: 'spot reserves into funding and depleted futures', + before: { FUNDING: '0', MAIN: '100', UMFUTURE: '0' }, + targets: { FUNDING: '50', MAIN: '40', UMFUTURE: '10' }, + transfers: [ + { from: 'MAIN', to: 'FUNDING', kindType: 'MAIN_CARD', amount: '50' }, + { from: 'MAIN', to: 'UMFUTURE', kindType: 'MAIN_FUTURE', amount: '10' }, + ], + }, + { + label: 'funding reserves into spot and underallocated futures', + before: { FUNDING: '95', MAIN: '0', UMFUTURE: '5' }, + targets: { FUNDING: '50', MAIN: '40', UMFUTURE: '10' }, + transfers: [ + { from: 'FUNDING', to: 'MAIN', kindType: 'CARD_MAIN', amount: '40' }, + { from: 'FUNDING', to: 'UMFUTURE', kindType: 'CARD_FUTURE', amount: '5' }, + ], + }, + { + label: 'both reserve surpluses into depleted futures', + before: { FUNDING: '50', MAIN: '40', UMFUTURE: '0' }, + targets: { FUNDING: '45', MAIN: '36', UMFUTURE: '9' }, + transfers: [ + { from: 'FUNDING', to: 'UMFUTURE', kindType: 'CARD_FUTURE', amount: '5' }, + { from: 'MAIN', to: 'UMFUTURE', kindType: 'MAIN_FUTURE', amount: '4' }, + ], + }, { label: 'all futures funds into the 5:4:1 allocation', before: { FUNDING: '0', MAIN: '0', UMFUTURE: '100' }, @@ -24,16 +71,28 @@ for (const { label, before, targets, transfers } of [ ], }, { - label: 'excess to spot without withdrawing overfunded funding reserves', + label: 'funding and futures excess into the spot deficit', before: { FUNDING: '70', MAIN: '10', UMFUTURE: '20' }, - targets: { FUNDING: '70', MAIN: '20', UMFUTURE: '10' }, - transfers: [{ from: 'UMFUTURE', to: 'MAIN', kindType: 'FUTURE_MAIN', amount: '10' }], + targets: { FUNDING: '50', MAIN: '40', UMFUTURE: '10' }, + transfers: [ + { from: 'FUNDING', to: 'MAIN', kindType: 'CARD_MAIN', amount: '20' }, + { from: 'UMFUTURE', to: 'MAIN', kindType: 'FUTURE_MAIN', amount: '10' }, + ], }, { - label: 'excess to funding without withdrawing overfunded spot reserves', + label: 'spot and futures excess into the funding deficit', before: { FUNDING: '0', MAIN: '80', UMFUTURE: '20' }, - targets: { FUNDING: '10', MAIN: '80', UMFUTURE: '10' }, - transfers: [{ from: 'UMFUTURE', to: 'FUNDING', kindType: 'FUTURE_CARD', amount: '10' }], + targets: { FUNDING: '50', MAIN: '40', UMFUTURE: '10' }, + transfers: [ + { from: 'MAIN', to: 'FUNDING', kindType: 'MAIN_CARD', amount: '40' }, + { from: 'UMFUTURE', to: 'FUNDING', kindType: 'FUTURE_CARD', amount: '10' }, + ], + }, + { + label: 'spot excess when futures already holds its target reserve', + before: { FUNDING: '0', MAIN: '90', UMFUTURE: '10' }, + targets: { FUNDING: '50', MAIN: '40', UMFUTURE: '10' }, + transfers: [{ from: 'MAIN', to: 'FUNDING', kindType: 'MAIN_CARD', amount: '50' }], }, { label: 'only the funding deficit before directing the remaining excess to spot', @@ -47,8 +106,20 @@ for (const { label, before, targets, transfers } of [ { label: 'eight-decimal excess while preserving rounding dust in futures', before: { FUNDING: '70.00000001', MAIN: '10', UMFUTURE: '20' }, - targets: { FUNDING: '70.00000001', MAIN: '19.99999999', UMFUTURE: '10.00000001' }, - transfers: [{ from: 'UMFUTURE', to: 'MAIN', kindType: 'FUTURE_MAIN', amount: '9.99999999' }], + targets: { FUNDING: '50', MAIN: '40', UMFUTURE: '10.00000001' }, + transfers: [ + { from: 'FUNDING', to: 'MAIN', kindType: 'CARD_MAIN', amount: '20.00000001' }, + { from: 'UMFUTURE', to: 'MAIN', kindType: 'FUTURE_MAIN', amount: '9.99999999' }, + ], + }, + { + label: 'four minimum units with the same rounded targets as manual allocation', + before: { FUNDING: '0', MAIN: '0', UMFUTURE: '0.00000004' }, + targets: { FUNDING: '0.00000002', MAIN: '0.00000001', UMFUTURE: '0.00000001' }, + transfers: [ + { from: 'UMFUTURE', to: 'FUNDING', kindType: 'FUTURE_CARD', amount: '0.00000002' }, + { from: 'UMFUTURE', to: 'MAIN', kindType: 'FUTURE_MAIN', amount: '0.00000001' }, + ], }, { label: 'a single transferable unit without rounding futures below its reserve', @@ -57,58 +128,44 @@ for (const { label, before, targets, transfers } of [ transfers: [{ from: 'UMFUTURE', to: 'FUNDING', kindType: 'FUTURE_CARD', amount: '0.00000001' }], }, ]) { - test(`user automatically distributes ${label}`, () => { - // Given confirmed available balances with futures above its rounded reserve + test(`user distributes ${label}`, () => { + // Given confirmed available balances with at least one account outside its target const balances = { ...before }; - // When automatic withdrawal-only distribution is planned and applied - const plan = buildAutomaticUsdtRebalancePlan(balances); + // When the shared account allocation is planned and applied + const plan = buildUsdtRebalancePlan(balances); const finalBalances = plan.transfers.reduce(applyUsdtTransferToBalances, balances); - // Then only futures donates and targets describe the actual conserved final balances + // Then all three accounts reach their conserved targets with no remaining transfer assert.deepEqual(plan.before, before); assert.deepEqual(plan.targets, targets); assert.deepEqual(plan.transfers, transfers); assert.deepEqual(finalBalances, targets); + assert.deepEqual(buildUsdtRebalancePlan(finalBalances).transfers, []); assert.equal(buildUsdtRebalancePlan(finalBalances).total, plan.total); - assert.deepEqual(buildAutomaticUsdtRebalancePlan(finalBalances).transfers, []); assert.deepEqual(balances, before); }); } for (const balances of [ - { FUNDING: '0', MAIN: '100', UMFUTURE: '0' }, - { FUNDING: '95', MAIN: '0', UMFUTURE: '5' }, - { FUNDING: '0', MAIN: '90', UMFUTURE: '10' }, { FUNDING: '50', MAIN: '40', UMFUTURE: '10' }, { FUNDING: '0', MAIN: '0', UMFUTURE: '0' }, { FUNDING: '0', MAIN: '0', UMFUTURE: '0.00000001' }, ]) { - test(`user never automatically refills futures or redistributes reserves at ${JSON.stringify(balances)}`, () => { - // Given futures has no excess over its rounded ten-percent reserve + test(`user skips transfers when balances already meet rounded targets at ${JSON.stringify(balances)}`, () => { + // Given all three accounts meet their exact eight-decimal targets const before = { ...balances }; - // When automatic rebalancing evaluates the account - const plan = buildAutomaticUsdtRebalancePlan(before); + // When the shared rebalance planner evaluates the account + const plan = buildUsdtRebalancePlan(before); - // Then the account remains unchanged even if reserve wallets miss their nominal ratio + // Then the balanced account remains unchanged without a transfer assert.deepEqual(plan.transfers, []); assert.deepEqual(plan.before, balances); assert.deepEqual(plan.targets, balances); }); } -test('user rejects unsupported precision before planning automatic withdrawal', () => { - // Given futures has a balance finer than the supported USDT unit - const balances = { FUNDING: '0', MAIN: '0', UMFUTURE: '1.000000001' }; - - // When automatic rebalancing evaluates the balance - const failure = captureThrownError(() => buildAutomaticUsdtRebalancePlan(balances)); - - // Then precision is rejected rather than rounded into a transfer - assert.equal(failure.message, 'USDT 余额精度超过 8 位'); -}); - test("user sees that wallet response and private transfer use stable account codes from the current page bundle", () => { // Given the account identities shared by balance responses and transfer requests const accounts = USDT_REBALANCE_ACCOUNTS; diff --git a/test/unit/binance-strategy29-bollinger/source-regressions.test.js b/test/unit/binance-strategy29-bollinger/source-regressions.test.js index 328c8f9..c223496 100644 --- a/test/unit/binance-strategy29-bollinger/source-regressions.test.js +++ b/test/unit/binance-strategy29-bollinger/source-regressions.test.js @@ -19,7 +19,7 @@ test('user installs Strategy29 with only the reviewed sandbox capabilities', asy const [entrySource, remoteSource] = await Promise.all(paths.map(path => readFile(path, 'utf8'))); // Then metadata and page coordination keep the exact reviewed grants and transport boundary. - assert.equal(readUserscriptVersion(entrySource), '0.5.10'); + assert.equal(readUserscriptVersion(entrySource), '0.5.11'); assert.deepEqual( [...entrySource.matchAll(/^\/\/ @grant\s+(\S+)\s*$/gm)].map(match => match[1]), ['unsafeWindow', 'GM_getValue', 'GM_setValue'], diff --git a/test/unit/userscript-release-contract.test.js b/test/unit/userscript-release-contract.test.js index e431747..66608a3 100644 --- a/test/unit/userscript-release-contract.test.js +++ b/test/unit/userscript-release-contract.test.js @@ -24,7 +24,7 @@ test('user installs Strategy29 with its independent observation-only identity', // Then the installer retains its pinned identity and declared observation boundary. assert.equal(contract.name, '【自写】Binance Strategy 29 布林带信号'); assert.equal(contract.namespace, 'binance.strategy29.bollinger'); - assert.equal(contract.version, '0.5.10'); + assert.equal(contract.version, '0.5.11'); assert.equal(contract.runAt, 'document-start'); assert.equal(contract.updateURL, 'https://raw.githubusercontent.com/jackhai9/userscripts/main/scripts/binance-strategy29-bollinger.user.js'); assert.equal(contract.downloadURL, contract.updateURL); @@ -70,7 +70,7 @@ test('user identifies Strategy27 and its declared local gateway capabilities', ( // Then the installer retains its identity and exact gateway capability boundary. assert.equal(contract.name, '【自写】Binance Strategy 27 事件标注'); assert.equal(contract.namespace, 'binance.strategy27.events'); - assert.equal(contract.version, '0.6.10'); + assert.equal(contract.version, '0.6.11'); assert.equal(contract.runAt, 'document-idle'); assert.equal(contract.updateURL, contract.downloadURL); assert.deepEqual(contract.matches, [