Skip to content

Commit d3a9e7d

Browse files
committed
fix: preserve confirmed automatic rebalance status
1 parent 4c2cfc8 commit d3a9e7d

18 files changed

Lines changed: 284 additions & 46 deletions

‎docs/binance-orderbook-trade-development.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,15 @@ terminal across reloads and later activity; neither a timer nor another tab
374374
clears an uncertain outcome. There is no automatic retry or rollback. Missing
375375
Web Locks, digest support, storage, or identity contracts surface an error.
376376

377+
The optional `outcome: 'automatic_completed'` belongs only to a `consumed` record
378+
and is written after every automatic transfer and its expected balance are
379+
confirmed. It preserves the completed Chinese/English message across reloads and
380+
currency tabs. Status-only records, including deployed records, manual transfers,
381+
zero-transfer episodes, and interrupted plans, retain their existing protection
382+
without claiming automatic success. New authoritative activity removes the
383+
completion marker together with the old episode; the marker never grants
384+
transfer eligibility.
385+
377386
Every automatic attempt and every transfer step verifies all-symbol positions,
378387
POST `/bapi/futures/v1/private/future/order/open-orders` with `{}`, and POST
379388
`/bapi/futures/v1/private/future/order/open-algo-order` with

‎docs/binance-strategy27-events-development.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ V10 live projection. The VPS remains the only market-data and event-analysis
1414
authority. The userscript opens no Binance market-data WebSocket, uses no
1515
Binance API key, and does not recalculate the four force groups.
1616

17-
Version 0.6.9 retains this installation's private gateway configuration and
17+
Version 0.6.10 retains this installation's private gateway configuration and
1818
provides a shared read-only transport. Strategy29 owns its own summary panel,
1919
lifecycle and panel position. The existing `strategy27GatewayOrigin` and
2020
`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
183183
browser does not reconstruct candidates from ordinary events or recalculate
184184
market evidence. The client, lifecycle, panel, SVG chart layer and optional-job
185185
controller are wired into the entrypoint and tested together. The source and
186-
generated install artifact are version 0.6.9 with identical metadata headers.
186+
generated install artifact are version 0.6.10 with identical metadata headers.
187187
The generated artifact passes syntax, release-contract and isolated execution
188188
checks, including candidate delivery, paired SVG markers, clear and context stop.
189189
Binance operator-page validation remains outstanding. Server/gateway rollout

‎docs/binance-strategy29-bollinger-development.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ already-loaded native chart candles. The summary reads the authenticated
1313
unified loopback gateway; it does not call Binance market-data or account APIs,
1414
submit orders, rotate hidden charts, or add remote events as chart drawings.
1515

16-
Install Strategy29 0.5.9 with orderbook 2.7.199 or later, or use it alone.
16+
Install Strategy29 0.5.10 with orderbook 2.7.199 or later, or use it alone.
1717
Install CorsairQuant signal client 0.6.7 for the remote summary.
1818
Do not combine it with the embedded observer in orderbook 2.7.198.
1919
After updating/disabling the old script, reload the page. An embedded observer

‎docs/binance-strategy31-volume-reversal-development.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Strategy31 volume reversal signals
22

33
Source: `src/binance-strategy31-volume-reversal/`. Installer:
4-
`scripts/binance-strategy31-volume-reversal.user.js` (0.1.3).
4+
`scripts/binance-strategy31-volume-reversal.user.js` (0.1.4).
55
Install alongside CorsairQuant signal client 0.6.7, which owns private gateway
66
authentication. The shared bridge explicitly advertises the `strategy31` capability.
77

‎e2e/binance-orderbook/specs/automatic-account-rebalance.pw.js‎

Lines changed: 171 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@ const EXCESS = { FUNDING: '0', MAIN: '0', UMFUTURE: '100' };
99
const TARGET = { FUNDING: '50', MAIN: '40', UMFUTURE: '10' };
1010
const transfers = api => api.snapshot().requests.filter(r => r.pathname === ACCOUNT_PATHS.transfer);
1111

12-
async function episodes(page) {
12+
async function episodeRecords(page) {
1313
return page.evaluate(prefix => Object.keys(localStorage).filter(k => k.startsWith(prefix))
14-
.map(k => JSON.parse(localStorage.getItem(k)).status), PREFIX);
14+
.map(k => JSON.parse(localStorage.getItem(k))), PREFIX);
15+
}
16+
17+
async function episodes(page) {
18+
return (await episodeRecords(page)).map(record => record.status);
1519
}
1620

1721
async function openAutomatic(page, api) {
@@ -129,15 +133,15 @@ test('user sees automatic rebalance status through SPA locale changes without re
129133

130134
// Then both planned withdrawals complete once and retain a Chinese result.
131135
await expect.poll(() => api.snapshot().balances).toEqual(TARGET);
132-
await expectAutomaticStatus(page, '自动再平衡已完成 · 2/2 笔');
136+
await expectAutomaticStatus(page, '已自动进行账户再平衡');
133137
expect(transfers(api)).toHaveLength(2);
134138
expect(await episodes(page)).toEqual(['consumed']);
135139

136140
// When the completed account page switches to English without reloading.
137141
await switchLocale(page, 'en');
138142

139143
// Then the completed result and tooltip translate while the episode stays consumed.
140-
await expectAutomaticStatus(page, 'Automatic USDT transfers completed: 2/2');
144+
await expectAutomaticStatus(page, 'Account automatically rebalanced');
141145
expect(transfers(api)).toHaveLength(2);
142146
expect(await episodes(page)).toEqual(['consumed']);
143147

@@ -146,7 +150,7 @@ test('user sees automatic rebalance status through SPA locale changes without re
146150
await page.clock.runFor(5000);
147151

148152
// Then the result returns to Chinese and no repeated allocation occurs.
149-
await expectAutomaticStatus(page, '自动再平衡已完成 · 2/2 笔');
153+
await expectAutomaticStatus(page, '已自动进行账户再平衡');
150154
expect(transfers(api)).toHaveLength(2);
151155
expect(api.snapshot().balances).toEqual(TARGET);
152156
expect(await episodes(page)).toEqual(['consumed']);
@@ -210,6 +214,18 @@ test('user automatically withdraws excess once after a stable empty account with
210214
await expect(page.getByRole('dialog')).toHaveCount(0);
211215
expect((await readFixtureState(page)).events.filter(e => /order-submitted|cancel-requested/.test(e.type))).toEqual([]);
212216
expect(errors).toEqual([]);
217+
218+
// When the user reloads after every planned transfer and balance has been confirmed.
219+
await reloadPageWithCoverage(page);
220+
await becomeFlat(page);
221+
222+
// Then the completed result survives without repeating a transfer.
223+
await expectAutomaticStatus(page, '已自动进行账户再平衡');
224+
expect(await episodeRecords(page)).toEqual([
225+
{ version: 1, status: 'consumed', outcome: 'automatic_completed' },
226+
]);
227+
expect(transfers(api)).toHaveLength(2);
228+
expect(api.snapshot().balances).toEqual(TARGET);
213229
});
214230

215231
test('user keeps reserves after a loss and does not repeat an empty episode after reload', async ({ page }) => {
@@ -234,9 +250,154 @@ test('user keeps reserves after a loss and does not repeat an empty episode afte
234250
await expect(page.locator('[data-usdt-rebalance]')).toBeEnabled();
235251

236252
// Then reloading never creates a second automatic execution entitlement.
237-
await expectAutomaticStatus(page, '本轮空仓已检查自动再平衡');
253+
await expectAutomaticStatus(page, '本轮不再自动执行账户再平衡');
238254
expect(await episodes(page)).toEqual(['consumed']);
239255
expect(transfers(api)).toEqual([]);
256+
257+
// When the retained no-transfer episode is displayed in English.
258+
await switchLocale(page, 'en');
259+
260+
// Then the message describes the episode without claiming a completed transfer or current balance.
261+
await expectAutomaticStatus(page, 'No further automatic rebalance in this round');
262+
expect(await episodeRecords(page)).toEqual([{ version: 1, status: 'consumed' }]);
263+
expect(api.snapshot().balances).toEqual(EXCESS);
264+
expect(transfers(api)).toEqual([]);
265+
});
266+
267+
test('user does not see a completed transfer inferred from an older episode record', async ({ page }) => {
268+
// Given an earlier script stored a consumed episode without a financial outcome.
269+
const api = createAccountRebalanceApi(EXCESS);
270+
await openAutomatic(page, api);
271+
await page.evaluate(async prefix => {
272+
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode('fixture-account'));
273+
const accountKey = prefix + Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('');
274+
localStorage.setItem(accountKey, JSON.stringify({ version: 1, status: 'consumed' }));
275+
}, PREFIX);
276+
277+
// When the current script observes the same empty episode.
278+
await becomeFlat(page);
279+
280+
// Then the old record remains protected without claiming success or moving funds.
281+
await expectAutomaticStatus(page, '本轮不再自动执行账户再平衡');
282+
expect(await episodeRecords(page)).toEqual([{ version: 1, status: 'consumed' }]);
283+
expect(transfers(api)).toEqual([]);
284+
expect(api.snapshot().balances).toEqual(EXCESS);
285+
});
286+
287+
test('user sees a localized refusal for an invalid persisted completion outcome', async ({ page }) => {
288+
// Given a stored completion marker conflicts with its in-flight protection.
289+
const api = createAccountRebalanceApi(EXCESS);
290+
await openAutomatic(page, api);
291+
await page.evaluate(async prefix => {
292+
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode('fixture-account'));
293+
const accountKey = prefix + Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('');
294+
localStorage.setItem(accountKey, JSON.stringify({ version: 1, status: 'in_flight', outcome: 'automatic_completed' }));
295+
}, PREFIX);
296+
297+
// When the script reads the conflicting record on the Chinese route.
298+
await becomeFlat(page);
299+
300+
// Then the refusal explains the invalid result without moving funds or resetting the record.
301+
await expectAutomaticStatus(page, '自动再平衡已停止:自动再平衡结果记录无效', '自动再平衡结果记录无效');
302+
expect(await episodeRecords(page)).toEqual([
303+
{ version: 1, status: 'in_flight', outcome: 'automatic_completed' },
304+
]);
305+
expect(transfers(api)).toEqual([]);
306+
307+
// When the same refusal is retained on the English route.
308+
await switchLocale(page, 'en');
309+
310+
// Then both its text and tooltip translate without retrying the refused action.
311+
await expectAutomaticStatus(page, 'Automatic USDT transfer stopped: Invalid automatic rebalance episode outcome', 'Invalid automatic rebalance episode outcome');
312+
expect(transfers(api)).toEqual([]);
313+
});
314+
315+
test('user does not see a manual rebalance reported as an automatic completion after reload', async ({ page }) => {
316+
// Given automatic qualification left the reserves alone and the user reviewed a manual plan.
317+
const api = createAccountRebalanceApi({ FUNDING: '100', MAIN: '0', UMFUTURE: '0' });
318+
await openAutomatic(page, api);
319+
await becomeFlat(page);
320+
await expectAutomaticStatus(page, '自动再平衡:合约账户无多余 USDT');
321+
await page.clock.runFor(32);
322+
await expect(page.locator('[data-usdt-rebalance]')).toBeVisible();
323+
await page.locator('[data-usdt-rebalance]').click();
324+
const dialog = page.getByRole('dialog', { name: '账户再平衡' });
325+
await expect(dialog).toBeVisible();
326+
327+
// When the user confirms the complete manual allocation.
328+
await dialog.getByRole('button', { name: '确认再平衡', exact: true }).click();
329+
330+
// Then the manual result reaches its target without an automatic completion marker.
331+
await expect(page.locator('#jh-binance-ladder-status')).toHaveText('账户再平衡已完成 · 2/2 笔');
332+
expect(await episodeRecords(page)).toEqual([{ version: 1, status: 'consumed' }]);
333+
expect(api.snapshot().balances).toEqual(TARGET);
334+
expect(transfers(api)).toHaveLength(2);
335+
336+
// When the user reloads the same account episode.
337+
await reloadPageWithCoverage(page);
338+
await becomeFlat(page);
339+
340+
// Then no automatic completion or duplicate transfer is reported.
341+
await expectAutomaticStatus(page, '本轮不再自动执行账户再平衡');
342+
expect(transfers(api)).toHaveLength(2);
343+
});
344+
345+
test('user sees automatic completion only after the final transferred balance is confirmed', async ({ page }) => {
346+
// Given the only planned transfer will be acknowledged before its balance is published.
347+
const before = { FUNDING: '50', MAIN: '30', UMFUTURE: '20' };
348+
const api = createAccountRebalanceApi(before, { commitTransfers: false });
349+
await openAutomatic(page, api);
350+
351+
// When the server accepts the transfer while its wallet still has the old balance.
352+
await becomeFlat(page);
353+
await expect.poll(() => api.snapshot().pendingTransfers).toBe(1);
354+
await expect.poll(() => api.snapshot().requests.filter(r => r.pathname === ACCOUNT_PATHS.wallets).length).toBe(3);
355+
356+
// Then the result remains pending and cannot carry a completed marker.
357+
await expectAutomaticStatus(page, '自动再平衡中 · 1/1 笔');
358+
expect(await episodeRecords(page)).toEqual([{ version: 1, status: 'in_flight' }]);
359+
expect(api.snapshot().balances).toEqual(before);
360+
361+
// When the authoritative balance publishes the accepted transfer.
362+
api.commitPendingTransfers();
363+
await page.clock.runFor(1000);
364+
365+
// Then the complete result is shown and persisted only after the balance matches.
366+
await expectAutomaticStatus(page, '已自动进行账户再平衡');
367+
expect(await episodeRecords(page)).toEqual([
368+
{ version: 1, status: 'consumed', outcome: 'automatic_completed' },
369+
]);
370+
expect(api.snapshot().balances).toEqual(TARGET);
371+
expect(transfers(api)).toHaveLength(1);
372+
});
373+
374+
test('user does not see partial automatic transfers reported as completed after reload', async ({ page }) => {
375+
// Given the next account check will fail after the first transfer has committed.
376+
const api = createAccountRebalanceApi(EXCESS);
377+
await openAutomatic(page, api);
378+
await page.route('https://www.binance.com' + ACCOUNT_PATHS.transfer, async route => {
379+
const response = api.handle({ pathname: ACCOUNT_PATHS.transfer, method: 'POST', body: route.request().postDataJSON() });
380+
api.failNext(ACCOUNT_PATHS.identity, { status: 401, body: { success: false } });
381+
await route.fulfill({ status: response.status, contentType: 'application/json', body: JSON.stringify(response.body) });
382+
});
383+
384+
// When the automatic plan stops between the first and second transfer.
385+
await becomeFlat(page);
386+
387+
// Then a confirmed partial transfer does not claim the whole plan completed.
388+
await expectAutomaticStatus(page, '自动再平衡已停止:Binance 登录态已失效', 'Binance 登录态已失效');
389+
expect(api.snapshot().balances).toEqual({ FUNDING: '50', MAIN: '0', UMFUTURE: '50' });
390+
expect(await episodeRecords(page)).toEqual([{ version: 1, status: 'consumed' }]);
391+
expect(transfers(api)).toHaveLength(1);
392+
393+
// When the user reloads the partially processed episode.
394+
await reloadPageWithCoverage(page);
395+
await becomeFlat(page);
396+
397+
// Then the result remains non-successful and the remaining transfer is not retried.
398+
await expectAutomaticStatus(page, '本轮不再自动执行账户再平衡');
399+
expect(transfers(api)).toHaveLength(1);
400+
expect(api.snapshot().balances).toEqual({ FUNDING: '50', MAIN: '0', UMFUTURE: '50' });
240401
});
241402

242403
for (const kind of ['basic', 'conditional']) {
@@ -268,12 +429,15 @@ test('user receives one automatic allocation across two simultaneous currency ta
268429
// When both pages qualify for the same empty account at the same time.
269430
await Promise.all([page, second].map(tab => tab.evaluate(() => window.__BINANCE_FIXTURE__.setOrders([]))));
270431
await page.clock.runFor(2032);
432+
await second.clock.runFor(2032);
271433

272434
// Then shared locking and durable state permit only the two planned transfers.
273435
await expect.poll(() => api.snapshot().balances).toEqual(TARGET);
274436
await expect.poll(() => episodes(page)).toEqual(['consumed']);
275437
expect(transfers(api)).toHaveLength(2);
276438
expect(await episodes(second)).toEqual(['consumed']);
439+
await expectAutomaticStatus(page, '已自动进行账户再平衡');
440+
await expectAutomaticStatus(second, '已自动进行账户再平衡');
277441
});
278442

279443
test('user qualifies again only after independently verified new account activity', async ({ page }) => {
@@ -288,6 +452,7 @@ test('user qualifies again only after independently verified new account activit
288452
await page.evaluate(orders => window.__BINANCE_FIXTURE__.setOrders(orders), ORDER_SETS.other);
289453
await page.clock.runFor(32);
290454
await expect.poll(() => episodes(page)).toEqual(['active']);
455+
expect(await episodeRecords(page)).toEqual([{ version: 1, status: 'active' }]);
291456
api.setOrders({ basic: [], conditional: [] });
292457
api.setBalances(EXCESS);
293458
await becomeFlat(page);

0 commit comments

Comments
 (0)