Skip to content

Commit e085c9d

Browse files
jackhai9claude
andauthored
feat(orderbook): configure the USDT rebalance allocation (#327)
Automatic and manually confirmed rebalancing now read one whole-percent allocation from src/binance-orderbook-trade/config.js, set to Funding 70%, Spot 20%, and Futures 10% (7:2:1). The planner takes the allocation explicitly and rejects anything but the three accounts with whole percentages of at least 1 that total 100. The action tooltip, confirmation summary, and already-balanced status are built from the same values, and browser fixtures derive their balances from it, so a new allocation needs no other edit. The locale primitives move from the orderbook copy map to src/shared/ui-locale.js. The five other Binance scripts imported them from contracts/panel-copy.js, so each installer also carried the whole orderbook copy map; they now bundle only the primitives, and orderbook copy changes no longer alter their installers. All six scripts are bumped because their generated installers change. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent f9f2ab8 commit e085c9d

46 files changed

Lines changed: 617 additions & 929 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/binance-orderbook-ui.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ on:
1515
- "src/shared/tradingview-target.js"
1616
- "src/shared/chart-marker-save-controller.js"
1717
- "src/shared/chart-mutation-owners.js"
18+
- "src/shared/ui-locale.js"
1819
- "test/unit/binance-strategy29-bollinger/**"
1920
- "test/dom/binance-strategy29-bollinger/**"
2021
- "scripts/binance-orderbook-trade.user.js"
@@ -39,6 +40,7 @@ on:
3940
- "src/shared/tradingview-target.js"
4041
- "src/shared/chart-marker-save-controller.js"
4142
- "src/shared/chart-mutation-owners.js"
43+
- "src/shared/ui-locale.js"
4244
- "test/unit/binance-strategy29-bollinger/**"
4345
- "test/dom/binance-strategy29-bollinger/**"
4446
- "scripts/binance-orderbook-trade.user.js"

‎.github/workflows/binance-strategy27-events.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ on:
1212
- "src/binance-strategy27-events/**"
1313
- "src/shared/binance-futures-route.js"
1414
- "src/shared/spa-route-change.js"
15+
- "src/shared/ui-locale.js"
1516
- "scripts/binance-strategy27-events.user.js"
1617
- "test/unit/binance-strategy27-events/**"
1718
- "test/dom/binance-strategy27-events/**"
@@ -30,6 +31,7 @@ on:
3031
- "src/binance-strategy27-events/**"
3132
- "src/shared/binance-futures-route.js"
3233
- "src/shared/spa-route-change.js"
34+
- "src/shared/ui-locale.js"
3335
- "scripts/binance-strategy27-events.user.js"
3436
- "test/unit/binance-strategy27-events/**"
3537
- "test/dom/binance-strategy27-events/**"

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ The userscript UI supports exactly `zh-CN` and English. Resolve the locale from
253253

254254
Keep script-owned UI copy in `contracts/panel-copy.js` as paired `localizedText(zhCN, en)` values. Dynamic status formatters must retain both languages until render time so current order, round, and cancellation counters survive a locale switch. Raw technical exceptions, Binance response messages, error codes, and native `TypeError` text stay unchanged so they remain copyable for diagnosis.
255255

256-
Strategy27, Strategy29, and Strategy31 also import this module's locale helpers. Changes to its initialized copy map affect all four generated artifacts; bump their source metadata and rebuild each affected script together.
256+
The locale primitives (`localizedText`, `formatLocalizedText`, `resolveUiLocaleFromPathname`, and related helpers) live in `src/shared/ui-locale.js`, which all six Binance scripts import. Only the orderbook bundles `contracts/panel-copy.js`, so its copy map can change without touching the other installers. A change to `src/shared/ui-locale.js` affects all six generated artifacts; bump their source metadata and rebuild them together.
257257

258258
Run `npm run lint:ui-copy` for status changes. CI rejects direct unpaired text or
259259
titles at `setLadderStatus` and `setAutomaticUsdtRebalanceStatus`, including
@@ -364,7 +364,12 @@ USDT account rebalancing has automatic and manually confirmed paths that share t
364364

365365
Automatic rebalancing runs once per verified globally flat episode after the same
366366
two-second zero-counter window. Both paths allocate total available USDT to
367-
Funding 50%, Spot 40%, and Futures 10% through `buildUsdtRebalancePlan`.
367+
the whole-percent targets in `src/binance-orderbook-trade/config.js` through
368+
`buildUsdtRebalancePlan`. The allocation must name exactly Funding, Spot, and
369+
Futures with whole percentages of at least 1 that total 100; the script rejects
370+
any other value when it loads. The action tooltip, confirmation summary, and
371+
already-balanced status derive the reduced ratio (70/20/10 is shown as 7:2:1)
372+
from the same configuration, so changing the allocation needs no other edit.
368373
Funding and Spot targets are floored to eight decimals; Futures receives the
369374
remainder so the complete balance is conserved. Any of the three accounts may
370375
donate to an underallocated account, including refilling Futures after a loss
@@ -438,7 +443,7 @@ revalidates its original account identity and exact previewed balances.
438443

439444
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.
440445

441-
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.
446+
For manual full rebalancing, the user must approve one native confirmation that shows the current balances, configured 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.
442447

443448
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.
444449

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

Lines changed: 4 additions & 6 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.15 retains this installation's private gateway configuration and
17+
Version 0.6.16 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;
@@ -198,7 +198,7 @@ ADR 032 in CorsairQuant owns the server-side rule and transport contract. The
198198
browser does not reconstruct candidates from ordinary events or recalculate
199199
market evidence. The client, lifecycle, panel, SVG chart layer and optional-job
200200
controller are wired into the entrypoint and tested together. The source and
201-
generated install artifact are version 0.6.15 with identical metadata headers.
201+
generated install artifact are version 0.6.16 with identical metadata headers.
202202
The generated artifact passes syntax, release-contract and isolated execution
203203
checks, including candidate delivery, paired SVG markers, clear and context stop.
204204
Binance operator-page validation remains outstanding. Server/gateway rollout
@@ -353,10 +353,8 @@ so the banner reports the last event evidence, not a current membership guarante
353353

354354
The interface follows the existing Binance userscript locale contract: the
355355
`/zh-CN/` route selects Simplified Chinese, and other routes select English.
356-
Strategy 27 reuses the existing orderbook locale helpers without importing its
357-
runtime or trading actions. The helper module also contains a static copy table;
358-
its small initialization remains in the bundle until a separate shared-helper
359-
extraction can migrate consumers together.
356+
Strategy 27 imports the shared locale primitives from `src/shared/ui-locale.js`
357+
without importing orderbook runtime, copy, or trading actions.
360358

361359
Panel headings, monitoring/connection/history notices, ordinary and compound
362360
facts, candidate labels, menus and prompts all use the selected locale. Proper

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ 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.13 directly and reload the page. The current orderbook
17-
script is optional; install CorsairQuant signal client 0.6.15 for the remote
16+
Install Strategy29 0.5.14 directly and reload the page. The current orderbook
17+
script is optional; install CorsairQuant signal client 0.6.16 for the remote
1818
summary. This personal-use project targets the current scripts, without
1919
old-version runtime branches or preference migration.
2020

@@ -61,8 +61,8 @@ Strategy27 uses the same SVG renderer with its own event and compound placement
6161
## Panel Language and Position
6262

6363
The summary follows the existing pathname locale contract: `/zh-CN/` uses Chinese,
64-
while English and other routes use English. Strategy29 imports the existing pure
65-
orderbook locale helpers without modifying orderbook or shared runtime behavior.
64+
while English and other routes use English. Strategy29 imports the shared locale
65+
primitives in `src/shared/ui-locale.js` without modifying orderbook or shared runtime behavior.
6666
Headers, connection and selection states, signal names, processing labels,
6767
notification totals and empty states have bilingual copy. Technical
6868
identifiers and arbitrary server diagnostic details remain verbatim, with localized

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Strategy31 volume reversal signals
22

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

88
CorsairQuant confirms consecutive closed red/green candles when green base volume
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { USDT_REBALANCE_TARGET_PERCENTS } from '../../../src/binance-orderbook-trade/config.js';
2+
3+
const SCALE = 8;
4+
const ACCOUNTS = ['FUNDING', 'MAIN', 'UMFUTURE'];
5+
6+
/**
7+
* Fixture balances follow the shipped allocation so every scenario keeps its transfer
8+
* shape for any valid configuration. A 200 USDT total gives each account an integer
9+
* target of at least 2 USDT, which leaves room for whole-unit perturbations.
10+
*/
11+
export const REBALANCE_TOTAL = 200;
12+
13+
function units(value) {
14+
const text = String(value);
15+
const negative = text.startsWith('-');
16+
const [whole, fraction = ''] = (negative ? text.slice(1) : text).split('.');
17+
const magnitude = BigInt(whole) * 10n ** BigInt(SCALE) + BigInt(fraction.padEnd(SCALE, '0'));
18+
return negative ? -magnitude : magnitude;
19+
}
20+
21+
function amount(value) {
22+
const sign = value < 0n ? '-' : '';
23+
const absolute = value < 0n ? -value : value;
24+
const whole = absolute / 10n ** BigInt(SCALE);
25+
const fraction = (absolute % 10n ** BigInt(SCALE)).toString().padStart(SCALE, '0').replace(/0+$/, '');
26+
return sign + whole + (fraction ? '.' + fraction : '');
27+
}
28+
29+
export const REBALANCE_TARGET = Object.freeze(Object.fromEntries(ACCOUNTS.map(account => [
30+
account, String(USDT_REBALANCE_TARGET_PERCENTS[account] * REBALANCE_TOTAL / 100),
31+
])));
32+
33+
export const REBALANCE_PERCENTS = USDT_REBALANCE_TARGET_PERCENTS;
34+
35+
const gcd = (left, right) => (right === 0 ? left : gcd(right, left % right));
36+
const RATIO_DIVISOR = ACCOUNTS.reduce((value, account) => gcd(value, USDT_REBALANCE_TARGET_PERCENTS[account]), 0);
37+
/** Reduced Funding:Spot:Futures label the panel must show, derived without production code. */
38+
export const REBALANCE_RATIO = ACCOUNTS.map(account => USDT_REBALANCE_TARGET_PERCENTS[account] / RATIO_DIVISOR).join(':');
39+
40+
/** An allocation that is still exact after every balance is multiplied by a whole factor. */
41+
export function scaledTarget(factor) {
42+
return Object.fromEntries(ACCOUNTS.map(account => [account, String(Number(REBALANCE_TARGET[account]) * factor)]));
43+
}
44+
45+
/** Shift the exact targets by known amounts; the shifts must conserve the total. */
46+
export function shiftedFromTarget(shifts) {
47+
const balances = Object.fromEntries(ACCOUNTS.map(account => [
48+
account, units(REBALANCE_TARGET[account]) + units(shifts[account] ?? '0'),
49+
]));
50+
if (ACCOUNTS.reduce((sum, account) => sum + balances[account], 0n) !== units(REBALANCE_TOTAL)) {
51+
throw new Error('Fixture shifts must conserve the rebalance total');
52+
}
53+
if (ACCOUNTS.some(account => balances[account] < 0n)) throw new Error('Fixture balance cannot be negative');
54+
return Object.fromEntries(ACCOUNTS.map(account => [account, amount(balances[account])]));
55+
}
56+
57+
/** All USDT starts in one account, as on a fresh deposit or a full Futures withdrawal. */
58+
export function allIn(account) {
59+
return Object.fromEntries(ACCOUNTS.map(code => [code, code === account ? String(REBALANCE_TOTAL) : '0']));
60+
}
61+
62+
export function transferBody(amountValue, kindType) {
63+
return { asset: 'USDT', amount: amount(units(amountValue)), kindType };
64+
}

‎e2e/binance-orderbook/helpers/account-lifecycle-host.js‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ACCOUNT_PATHS, createAccountRebalanceApi } from '../fixtures/account-re
33
import { CURRENT_SYMBOL, OTHER_SYMBOL, ORDER_SETS, createCancelScenario } from '../scenarios/cancel-current-symbol.js';
44
import { openUserscriptScenario, readFixtureState } from './userscript-page.js';
55
import { installScenarioClock, pauseScenarioClock } from './scenario-clock.js';
6+
import { REBALANCE_TARGET } from '../fixtures/usdt-allocation.js';
67

78
export const LEVERAGE_PATH = '/bapi/futures/v1/private/future/user-data/adjustLeverage';
89
export const THIRD_SYMBOL = 'ETHUSDT';
@@ -69,7 +70,8 @@ export async function openAccountLifecycleHost(page, {
6970
orders = ORDER_SETS.current,
7071
apiPositions = [],
7172
leverage = 2,
72-
balances = { FUNDING: '50', MAIN: '40', UMFUTURE: '10' },
73+
// Already balanced wallets keep lifecycle scenarios free of transfers for any allocation.
74+
balances = REBALANCE_TARGET,
7375
} = {}) {
7476
await installScenarioClock(page);
7577
const loaded = await openUserscriptScenario(page, createCancelScenario({

‎e2e/binance-orderbook/specs/account-lifecycle-behavior.pw.js‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ACCOUNT_PATHS } from '../fixtures/account-rebalance-api.js';
33
import { CURRENT_SYMBOL, OTHER_SYMBOL, ORDER_SETS } from '../scenarios/cancel-current-symbol.js';
44
import { LEVERAGE_PATH, THIRD_SYMBOL, openAccountLifecycleHost } from '../helpers/account-lifecycle-host.js';
55
import { readFixtureState } from '../helpers/userscript-page.js';
6+
import { scaledTarget, shiftedFromTarget } from '../fixtures/usdt-allocation.js';
67

78
const ACTION = '[data-usdt-rebalance]';
89
const POSITION_TAB = '[data-account-tab="positions"]';
@@ -368,7 +369,7 @@ test('user previews balances published during qualification instead of an earlie
368369
await page.clock.runFor(1000);
369370

370371
// When a native wallet update doubles each balance before qualification completes.
371-
host.api.setBalances({ FUNDING: '100', MAIN: '80', UMFUTURE: '20' });
372+
host.api.setBalances(scaledTarget(2));
372373
await page.clock.runFor(1000);
373374
await host.waitForPositionResponses(3);
374375
await expectEligible(page);
@@ -379,16 +380,18 @@ test('user previews balances published during qualification instead of an earlie
379380
await host.expectNoTradingActions();
380381

381382
// When the user opens the now-eligible account preview.
382-
host.api.setBalances({ FUNDING: '70', MAIN: '30', UMFUTURE: '0' });
383+
// Funding gains what Spot and Futures lose, so the preview must plan exactly 1 and 2 USDT.
384+
const published = shiftedFromTarget({ FUNDING: '3', MAIN: '-1', UMFUTURE: '-2' });
385+
host.api.setBalances(published);
383386
await page.locator(ACTION).evaluate(button => button.click());
384387
const dialog = page.getByRole('dialog', { name: '账户再平衡' });
385388

386389
// Then the reviewed plan uses the newly published balances and remains unexecuted.
387390
await expect(dialog).toBeVisible();
388-
await expect(dialog.locator('.jh-rebalance-dialog-transfer-amount')).toHaveText(['10 USDT', '10 USDT']);
391+
await expect(dialog.locator('.jh-rebalance-dialog-transfer-amount')).toHaveText(['1 USDT', '2 USDT']);
389392
expect(host.api.snapshot().requests.filter(request => request.pathname === ACCOUNT_PATHS.wallets).length)
390393
.toBe(automaticWalletReads + 1);
391-
expect(host.api.snapshot().balances).toEqual({ FUNDING: '70', MAIN: '30', UMFUTURE: '0' });
394+
expect(host.api.snapshot().balances).toEqual(published);
392395
await dialog.getByRole('button', { name: '取消', exact: true }).evaluate(button => button.click());
393396
await host.expectNoTradingActions();
394397
});

0 commit comments

Comments
 (0)