diff --git a/docs/binance-orderbook-trade-development.md b/docs/binance-orderbook-trade-development.md
index f1d370a..9c8e092 100644
--- a/docs/binance-orderbook-trade-development.md
+++ b/docs/binance-orderbook-trade-development.md
@@ -130,6 +130,24 @@ The userscript installs `core/binance-native-depth-source.js` at `document-start
The session must stop and invalidate old work on symbol change, non-trading routes, hidden documents, and `pagehide`. The overlay canvas uses `pointer-events: none`; only its compact collapse control may receive pointer input. Do not connect this visualization book to ladder pricing or any trading decision.
+## Symbol Identity
+
+`src/shared/binance-symbol.js` owns the Binance identifier character contract:
+Unicode letters, numbers, and underscores. Wire symbols remain uppercase and
+must match completely; whitespace, separators, or a trailing newline are invalid.
+The route retains its three-code-point minimum and percent decoding. The depth
+book, session, snapshot, and lowercase native stream use that same character set.
+Chinese names, one-character assets such as `4USDT` and `WUSDT`, and numeric
+prefixes such as `1INCHUSDT` and `1000PEPEUSDT` keep their full identities.
+
+Open-order scope reads preserve symbol-cell boundaries before scanning text.
+Every identified visible row must contain a complete contract, optionally followed
+by the known `永续` or `Perp` label; an unidentified row aborts the read instead of
+being removed from the evidence. Bare contracts are accepted only as complete
+lines. Timestamp parsing is local to an adjacent `HH:mm[:ss]`, never a reason to
+equate different numeric-prefixed contracts. Row matching compares the complete
+contract, so `龙虾BTCUSDT` cannot match `BTCUSDT` or `龙虾USDT`.
+
## UI Localization
The userscript UI supports exactly `zh-CN` and English. Resolve the locale from the first Binance pathname segment: `/zh-CN/...` uses Chinese, while `/en/...`, other locales, and locale-less futures paths use English. A Binance SPA language switch must rebuild only the userscript panel; it must not abort a ladder, cancel, single-order, or account-rebalance task, and it must not reset the active symbol-mode-precision profile.
diff --git a/docs/binance-strategy27-events-development.md b/docs/binance-strategy27-events-development.md
index a8f8118..cf0209e 100644
--- a/docs/binance-strategy27-events-development.md
+++ b/docs/binance-strategy27-events-development.md
@@ -11,7 +11,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.2 retains this installation's private gateway configuration and
+Version 0.6.3 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;
@@ -165,7 +165,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, native chart layer and optional-job
controller are wired into the entrypoint and tested together. The source and
-generated install artifact are version 0.6.2 with identical metadata headers.
+generated install artifact are version 0.6.3 with identical metadata headers.
The generated artifact passes syntax, release-contract and isolated execution
checks, including candidate delivery, paired entities, 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 a1d7ce3..cd73084 100644
--- a/docs/binance-strategy29-bollinger-development.md
+++ b/docs/binance-strategy29-bollinger-development.md
@@ -10,7 +10,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.2 with orderbook 2.7.199 or later, or use it alone.
+Install Strategy29 0.5.3 with orderbook 2.7.199 or later, or use it alone.
Install CorsairQuant signal client 0.6.1 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/userscript-validation.md b/docs/userscript-validation.md
index 3d6367b..fb723ec 100644
--- a/docs/userscript-validation.md
+++ b/docs/userscript-validation.md
@@ -36,6 +36,11 @@ they are not automatically executed by the test runner.
`scripts/*.user.js` file and checked directly.
- Route and symbol identity come from the documented pathname/route contract.
Do not infer a symbol from a page title, stale DOM, or a neighboring panel.
+- Binance identifiers share the Unicode letter/number/underscore character
+ contract in `src/shared/binance-symbol.js`. Include Chinese, one-character,
+ numeric-only, and numeric-prefixed assets when validating route, depth, order
+ scope, and available-balance parsing. Canonical gateway symbols retain the
+ separate exact server contract in `src/shared/canonical-symbol.js`.
- Async work must carry the symbol, route, and lifecycle identity that started
it. A route change, symbol change, hidden/closed panel, or page teardown
invalidates work before its result can render or update shared state.
@@ -72,6 +77,8 @@ base asset to one deterministic CoinMarketCap asset. Missing or ambiguous
mapping results remain visible failures; the panel must not select an arbitrary
same-symbol asset. API and page-snapshot data are labeled by their actual source
and timestamp, and a failed refresh must not overwrite a newer symbol's panel.
+The existing `1000` and `1000000` multiplier mapping recognizes Unicode letters
+as the start of an asset name; numeric-only names such as `4` retain their digits.
Route changes, hidden documents, panel close, and panel removal invalidate the
refresh epoch and clean up timers and DOM listeners. A route watcher may remain
diff --git a/e2e/binance-orderbook/fixtures/binance-futures.js b/e2e/binance-orderbook/fixtures/binance-futures.js
index 8fd1bc3..2ef2336 100644
--- a/e2e/binance-orderbook/fixtures/binance-futures.js
+++ b/e2e/binance-orderbook/fixtures/binance-futures.js
@@ -43,7 +43,7 @@ export function renderBinanceFuturesFixture(scenario) {
#account-tabs { margin-bottom: 12px; }
#OPEN_ORDERS { min-height: 160px; padding: 10px; }
[role="checkbox"] { display: inline-block; min-width: 150px; min-height: 28px; padding: 4px 8px; border: 1px solid #d8dce1; }
- .open-order-row { display: grid; grid-template-columns: repeat(10, minmax(60px, 1fr)); min-height: 28px; margin-top: 6px; }
+ .open-order-row { display: grid; grid-template-columns: repeat(12, minmax(60px, 1fr)); min-height: 28px; margin-top: 6px; }
.cursor-pointer { display: inline-block; min-width: 60px; min-height: 28px; margin-top: 8px; padding: 4px 8px; border: 1px solid #d8dce1; }
.bn-modal-root { position: fixed; inset: 0; z-index: 1000000; background: #0003; display: grid; place-items: center; }
[role="dialog"] { width: 340px; min-height: 150px; padding: 20px; background: #fff; }
@@ -340,9 +340,11 @@ export function renderBinanceFuturesFixture(scenario) {
const orders = visibleOrders();
if (!orders.length) return '
暂无当前委托。
';
return orders.map((item) => '' +
+ '
2026-09-12 10:27:51 ' +
'
' + item.symbol + ' 永续 限价 ' + item.side + ' ' +
'
' + item.price + ' ' + item.quantity + ' ' +
'
0 只做Maker -- -- -- ' +
+ '
' +
'
').join('');
}
diff --git a/e2e/binance-orderbook/specs/unicode-symbols.pw.js b/e2e/binance-orderbook/specs/unicode-symbols.pw.js
new file mode 100644
index 0000000..596e021
--- /dev/null
+++ b/e2e/binance-orderbook/specs/unicode-symbols.pw.js
@@ -0,0 +1,130 @@
+import { test, expect } from '../test.js';
+import { createCancelScenario } from '../scenarios/cancel-current-symbol.js';
+import { openUserscriptScenario, readFixtureState } from '../helpers/userscript-page.js';
+
+const nativeSocketFixture = `
+ window.__DEPTH_FIXTURE_SOCKET_COUNT__ = 0;
+ window.WebSocket = class extends EventTarget {
+ constructor() {
+ super();
+ window.__DEPTH_FIXTURE_SOCKET_COUNT__ += 1;
+ }
+ };
+`;
+
+async function mountDepthChart(page, symbol) {
+ await page.evaluate(async (symbol) => {
+ const previousFrame = document.querySelector('.chart-widget-root iframe');
+ const chartApi = previousFrame.contentWindow.tradingViewApi;
+ const host = document.createElement('div');
+ host.style.cssText = 'position:relative;height:260px';
+ const wrapper = document.createElement('div');
+ wrapper.style.height = '100%';
+ const frame = document.createElement('iframe');
+ frame.title = 'TradingView depth fixture';
+ frame.srcdoc = '
';
+ const loaded = new Promise((resolve) => frame.addEventListener('load', resolve, { once: true }));
+ host.append(wrapper);
+ wrapper.append(frame);
+ previousFrame.replaceWith(host);
+ await loaded;
+ frame.contentDocument.querySelector('#fixture-symbol').textContent = `${symbol} — isolated depth fixture`;
+ const scale = {
+ coordinateToPrice: (y) => 84 - y * 6 / 260,
+ getVisiblePriceRange: () => ({ from: 78, to: 84 }),
+ getMode: () => 0,
+ isInverted: () => false,
+ };
+ frame.contentWindow.tradingViewApi = {
+ ...chartApi,
+ activeChart: () => ({
+ hasModel: () => true,
+ getAllPanesHeight: () => [260],
+ getPanes: () => [{ getMainSourcePriceScale: () => scale }],
+ }),
+ };
+ window.dispatchEvent(new Event('resize'));
+ }, symbol);
+}
+
+for (const symbol of ['BTCUSDT', '龙虾USDT', '4USDT']) {
+ test(`generated userscript renders native depth for ${symbol}`, async ({ page }, testInfo) => {
+ await page.route('**/*', (route) => route.abort('blockedbyclient'));
+ const { errors } = await openUserscriptScenario(page, createCancelScenario({ currentSymbol: symbol }), {
+ beforeOrderbook: nativeSocketFixture,
+ });
+ const snapshotSymbols = [];
+ await page.route('https://www.binance.com/fapi/v1/rpiDepth**', async (route) => {
+ const url = new URL(route.request().url());
+ snapshotSymbols.push(url.searchParams.get('symbol'));
+ expect(url.searchParams.get('limit')).toBe('1000');
+ await route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ lastUpdateId: 101, bids: [['80', '1'], ['79', '2']], asks: [['82', '3'], ['83', '4']] }),
+ });
+ });
+ await mountDepthChart(page, symbol);
+ await expect(page.locator('#jh-binance-depth-profile canvas')).toBeVisible();
+ await page.evaluate(async (symbol) => {
+ const socket = new WebSocket('wss://depth-fixture.invalid/ws');
+ const response = fetch(`/fapi/v1/rpiDepth?${new URLSearchParams({ symbol, limit: '1000' })}`);
+ socket.dispatchEvent(new MessageEvent('message', { data: JSON.stringify({
+ stream: `${symbol.toLowerCase()}@rpiDepth@500ms`,
+ data: { e: 'depthUpdate', s: symbol, st: 1, U: 100, u: 102, pu: 99,
+ b: [['80', '2'], ['79', '3']], a: [['82', '4'], ['83', '5']] },
+ }) }));
+ await (await response).json();
+ }, symbol);
+ await expect.poll(() => page.evaluate(() => window.__TM_CLOSE_LONG_DEBUG__.nativeDepthState)).toMatchObject({
+ status: { symbol, status: 'ready' }, bidCount: 2, askCount: 2,
+ });
+ await expect(page.locator('#jh-binance-depth-profile .jh-depth-profile-status')).toHaveText('');
+ await expect.poll(() => page.locator('#jh-binance-depth-profile canvas').evaluate((canvas) => {
+ const data = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height).data;
+ let green = 0, red = 0;
+ for (let index = 0; index < data.length; index += 4) {
+ if (data[index] === 14 && data[index + 1] === 203 && data[index + 2] === 129 && data[index + 3] === 255) green += 1;
+ if (data[index] === 246 && data[index + 1] === 70 && data[index + 2] === 93 && data[index + 3] === 255) red += 1;
+ }
+ return { hasBids: green > 0, hasAsks: red > 0 };
+ })).toEqual({ hasBids: true, hasAsks: true });
+ expect(snapshotSymbols).toEqual([symbol]);
+ expect(await page.evaluate(() => window.__DEPTH_FIXTURE_SOCKET_COUNT__)).toBe(1);
+ const state = await readFixtureState(page);
+ expect(state.orders).toEqual([]);
+ expect(state.events.filter((event) => event.type === 'order-submitted' || event.type === 'cancel-requested')).toEqual([]);
+ expect(errors).toEqual([]);
+ await page.locator('.chart-widget-root').screenshot({ path: testInfo.outputPath('depth-profile.png') });
+ });
+}
+
+for (const symbol of ['龙虾USDT', '4USDT']) {
+ test(`generated cancel flow recognizes ${symbol} without consuming another contract`, async ({ page }) => {
+ await page.route('**/*', (route) => route.abort('blockedbyclient'));
+ const orders = [
+ { id: 'current', symbol, side: 'SELL', price: '90', quantity: '0.01' },
+ { id: 'other', symbol: `超级${symbol}`, side: 'SELL', price: '90', quantity: '0.01' },
+ ];
+ const { errors } = await openUserscriptScenario(page, createCancelScenario({ currentSymbol: symbol, orders }));
+ await page.getByRole('button', { name: '撤单', exact: true }).click();
+ await expect(page.getByRole('dialog')).toBeVisible();
+ await expect(page.locator('[data-order-id="current"]')).toBeVisible();
+ await expect(page.locator('[data-order-id="other"]')).toHaveCount(0);
+ expect((await readFixtureState(page)).events.filter((event) => event.type === 'cancel-requested')).toEqual([]);
+ await page.getByRole('button', { name: '取消', exact: true }).click();
+ await expect(page.getByText('撤单已取消')).toBeVisible();
+ expect((await readFixtureState(page)).orders).toEqual(orders);
+
+ await page.getByRole('button', { name: '撤单', exact: true }).click();
+ await expect(page.getByRole('dialog')).toBeVisible();
+ await page.getByRole('button', { name: '确认', exact: true }).click();
+ await expect(page.getByText('撤单已完成')).toBeVisible();
+ const state = await readFixtureState(page);
+ expect(state.orders).toEqual([orders[1]]);
+ expect(state.events.filter((event) => event.type === 'cancel-requested').map((event) => event.symbol)).toEqual([symbol]);
+ expect(state.accountTab).toBe('positions');
+ expect(state.hideOtherSymbols).toBe(false);
+ expect(errors).toEqual([]);
+ });
+}
diff --git a/scripts/binance-coinmarketcap-data.user.js b/scripts/binance-coinmarketcap-data.user.js
index 627e867..a962901 100644
--- a/scripts/binance-coinmarketcap-data.user.js
+++ b/scripts/binance-coinmarketcap-data.user.js
@@ -3,7 +3,7 @@
// @namespace binance.coinmarketcap.data
// @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.16
+// @version 0.1.17
// @author jackhai9
// @description 在 Binance 合约页面显示当前币种的 CoinMarketCap 中文页关键估值与供应量数据
// @match https://www.binance.com/*/futures/*
@@ -19,9 +19,13 @@
// @grant GM_xmlhttpRequest
// ==/UserScript==
(() => {
+ // src/shared/binance-symbol.js
+ var BINANCE_SYMBOL_CHARACTERS = "\\p{L}\\p{N}_";
+ var SYMBOL_PATTERN = new RegExp(`^[${BINANCE_SYMBOL_CHARACTERS}]+$`, "u");
+
// src/shared/binance-futures-route.js
var FUTURES_TRADING_PATH_RE = /^\/(?:[a-z]{2}(?:-[A-Za-z]{2})?\/)?futures\/([^/]+)\/?$/;
- var TRADING_SYMBOL_RE = /^[\p{L}\p{N}_]{3,}$/u;
+ var TRADING_SYMBOL_RE = new RegExp(`^[${BINANCE_SYMBOL_CHARACTERS}]{3,}$`, "u");
function parseFuturesTradingSymbolFromPathname(pathname) {
const normalized = String(pathname || "").split(/[?#]/, 1)[0];
const match = normalized.match(FUTURES_TRADING_PATH_RE);
@@ -139,7 +143,7 @@
}
function cmcSymbolFromBaseAsset(baseAsset) {
if (!baseAsset) return null;
- return baseAsset.replace(/^(1000000|1000)(?=[A-Z])/, "");
+ return baseAsset.replace(/^(1000000|1000)(?=\p{L})/u, "");
}
function normalizeCmcAsset(rawAsset, fallbackBaseAsset) {
if (!rawAsset || typeof rawAsset !== "object") return null;
diff --git a/scripts/binance-orderbook-trade.user.js b/scripts/binance-orderbook-trade.user.js
index 28c5120..bc31ac6 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.204
+// @version 2.7.205
// @author jackhai9
// @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板
// @match https://www.binance.com/*/futures/*
@@ -283,10 +283,24 @@
);
}
+ // src/shared/binance-symbol.js
+ var BINANCE_SYMBOL_CHARACTERS = "\\p{L}\\p{N}_";
+ var SYMBOL_PATTERN = new RegExp(`^[${BINANCE_SYMBOL_CHARACTERS}]+$`, "u");
+ function isBinanceSymbol(value) {
+ if (typeof value !== "string" || value !== value.toUpperCase()) return false;
+ const match = value.match(SYMBOL_PATTERN);
+ return Boolean(match && match[0] === value);
+ }
+
// src/binance-orderbook-trade/core/cancel-orders.js
var PERPETUAL_LABEL_PATTERN = buildBinanceTextAlternation(
BINANCE_PAGE_TEXT.accountOrders.perpetual
);
+ var ORDER_CONTRACT_PATTERN = `[${BINANCE_SYMBOL_CHARACTERS}]+(?:USDT|USDC)`;
+ var ORDER_SYMBOL_CELL_PATTERN = new RegExp(
+ `^(${ORDER_CONTRACT_PATTERN})(?:\\s*(?:${PERPETUAL_LABEL_PATTERN}))?$`,
+ "iu"
+ );
function normalizeText(value) {
return String(value || "").replace(/\s+/g, " ").trim();
}
@@ -296,48 +310,24 @@
function parseOpenOrdersTabCount(text) {
return parseBinanceTabCount(text, BINANCE_PAGE_TEXT.accountOrders.openOrdersTab);
}
- function escapeRegExp(value) {
- return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
- }
- function normalizeContractCandidate(candidate, separator) {
- const normalized = String(candidate || "").toUpperCase();
- if (separator === ":") {
- const timeJoinedMatch = /^\d{1,2}([A-Z][A-Z0-9]*(?:USDT|USDC))$/.exec(normalized);
- if (timeJoinedMatch) return timeJoinedMatch[1];
- }
- return normalized;
- }
- function isTimestampJoinedCandidate(candidate, symbol) {
- const normalizedCandidate = String(candidate || "").toUpperCase();
- const normalizedSymbol = String(symbol || "").toUpperCase();
- if (!normalizedCandidate || !normalizedSymbol || !normalizedCandidate.endsWith(normalizedSymbol)) {
- return false;
- }
- const prefix = normalizedCandidate.slice(0, -normalizedSymbol.length);
- return /^\d{1,2}$/.test(prefix);
+ function parseOpenOrderContractSymbol(text) {
+ const normalized = normalizeText(text);
+ const match = normalized.match(ORDER_SYMBOL_CELL_PATTERN);
+ return match && match[0] === normalized ? match[1].toUpperCase() : null;
}
- function hasVisibleContractText(text, symbol) {
- const normalizedSymbol = String(symbol || "").toUpperCase();
- if (!normalizedSymbol) return false;
- const symbolPattern = escapeRegExp(normalizedSymbol);
- return new RegExp(
- `(?:^|[^A-Z0-9]|\\d{1,2}:\\d{2})${symbolPattern}\\s*(?:${PERPETUAL_LABEL_PATTERN})(?=\\s|$)`,
- "i"
- ).test(String(text || ""));
+ function isOpenOrderRowCurrentSymbol(symbolText, symbol) {
+ return parseOpenOrderContractSymbol(symbolText) === String(symbol || "").toUpperCase();
}
function readVisibleOpenOrderSymbolsText(text) {
const normalized = String(text || "").toUpperCase();
const symbols = /* @__PURE__ */ new Set();
const pattern = new RegExp(
- `([A-Z0-9]{2,30}(?:USDT|USDC))\\s*(?:${PERPETUAL_LABEL_PATTERN})(?=\\s|$)`,
- "gi"
+ `(?:^|[^${BINANCE_SYMBOL_CHARACTERS}])(?:\\d{1,2}:\\d{2}(?::\\d{2})?)?(${ORDER_CONTRACT_PATTERN})\\s*(?:${PERPETUAL_LABEL_PATTERN})(?=\\s|$)|^[\\t ]*(${ORDER_CONTRACT_PATTERN})[\\t ]*$`,
+ "gimu"
);
let match = pattern.exec(normalized);
while (match) {
- const separator = normalized[match.index - 1] || "";
- if (!/[A-Z0-9]/.test(separator)) {
- symbols.add(normalizeContractCandidate(match[1], separator));
- }
+ symbols.add(match[1] ?? match[2]);
match = pattern.exec(normalized);
}
return Array.from(symbols);
@@ -346,7 +336,7 @@
const normalizedSymbol = String(symbol || "").toUpperCase();
if (!normalizedSymbol) return false;
const visibleSymbols = readVisibleOpenOrderSymbolsText(text);
- return visibleSymbols.length > 0 && visibleSymbols.every((visibleSymbol) => visibleSymbol === normalizedSymbol || hasVisibleContractText(text, normalizedSymbol) && isTimestampJoinedCandidate(visibleSymbol, normalizedSymbol));
+ return visibleSymbols.length > 0 && visibleSymbols.every((visibleSymbol) => visibleSymbol === normalizedSymbol);
}
function isOpenOrdersScopeConfirmedForSymbolText(text, symbol, filterChecked) {
if (filterChecked !== true) return false;
@@ -432,7 +422,7 @@
const normalizedSymbol = String(symbol || "").toUpperCase();
if (!normalizedSymbol) return false;
const visibleSymbols = readVisibleOpenOrderSymbolsText(scopeText);
- if (visibleSymbols.some((visibleSymbol) => visibleSymbol === normalizedSymbol || hasVisibleContractText(scopeText, normalizedSymbol) && isTimestampJoinedCandidate(visibleSymbol, normalizedSymbol))) return true;
+ if (visibleSymbols.includes(normalizedSymbol)) return true;
if (visibleSymbols.length > 0) return false;
return Boolean(symbolFilterOk && cancelAllAvailable);
}
@@ -1985,7 +1975,7 @@
// src/shared/binance-futures-route.js
var FUTURES_TRADING_PATH_RE = /^\/(?:[a-z]{2}(?:-[A-Za-z]{2})?\/)?futures\/([^/]+)\/?$/;
- var TRADING_SYMBOL_RE = /^[\p{L}\p{N}_]{3,}$/u;
+ var TRADING_SYMBOL_RE = new RegExp(`^[${BINANCE_SYMBOL_CHARACTERS}]{3,}$`, "u");
function parseFuturesTradingSymbolFromPathname(pathname) {
const normalized = String(pathname || "").split(/[?#]/, 1)[0];
const match = normalized.match(FUTURES_TRADING_PATH_RE);
@@ -2315,6 +2305,7 @@
}
// src/binance-orderbook-trade/dom/trade-form.js
+ var AVAILABLE_BALANCE_PATTERN = new RegExp(`^([\\d,]+(?:\\.\\d+)?)\\s+([${BINANCE_SYMBOL_CHARACTERS}]+)$`, "u");
function buttonTextMatches(button, labels) {
return includesBinancePageText(button?.textContent, labels);
}
@@ -2347,7 +2338,7 @@
const candidates = Array.from(root.querySelectorAll("span")).filter((label) => isVisibleElement(label) && matchesBinancePageText(label.textContent, BINANCE_PAGE_TEXT.availableBalance)).map((label) => {
const valueNodes = Array.from(label.parentElement?.children || []).filter((node) => node !== label && isVisibleElement(node));
if (valueNodes.length !== 1) return null;
- const match = /^([\d,]+(?:\.\d+)?)\s+([A-Z0-9]+)$/.exec(
+ const match = AVAILABLE_BALANCE_PATTERN.exec(
String(valueNodes[0].textContent || "").replace(/\s+/g, " ").trim()
);
return match ? { amount: match[1].replace(/,/g, ""), asset: match[2] } : null;
@@ -3662,7 +3653,7 @@
};
var MAX_BUFFERED_UPDATES = 500;
function assertSymbol(value) {
- if (typeof value !== "string" || !/^[A-Z0-9_]+$/.test(value)) {
+ if (!isBinanceSymbol(value)) {
throw new Error("Invalid depth profile symbol");
}
return value;
@@ -3848,13 +3839,13 @@
// src/binance-orderbook-trade/core/binance-native-depth-source.js
var NATIVE_RPI_DEPTH_PATH = "/fapi/v1/rpiDepth";
var NATIVE_RPI_DEPTH_LIMIT = "1000";
- var NATIVE_RPI_STREAM_PATTERN = /^([a-z0-9_]+)@rpiDepth@500ms$/;
+ var NATIVE_RPI_STREAM_PATTERN = new RegExp(`^([${BINANCE_SYMBOL_CHARACTERS}]+)@rpiDepth@500ms$`, "u");
function assertFunction(value, field) {
if (typeof value !== "function") throw new Error(`Invalid native depth ${field}`);
return value;
}
function assertSymbol2(value) {
- if (typeof value !== "string" || !/^[A-Z0-9_]+$/.test(value)) {
+ if (!isBinanceSymbol(value)) {
throw new Error("Invalid native depth symbol");
}
return value;
@@ -3976,7 +3967,7 @@
try {
envelope = JSON.parse(event.data);
const match = NATIVE_RPI_STREAM_PATTERN.exec(envelope?.stream);
- if (!match || !envelope.data || typeof envelope.data !== "object") {
+ if (!match || match[0] !== envelope.stream || match[1] !== match[1].toLowerCase() || !envelope.data || typeof envelope.data !== "object") {
throw new Error("Invalid Binance native RPI depth message");
}
const symbol = assertSymbol2(match[1].toUpperCase());
@@ -4080,7 +4071,7 @@
return value;
}
function assertSymbol3(value) {
- if (typeof value !== "string" || !/^[A-Z0-9_]+$/.test(value)) {
+ if (!isBinanceSymbol(value)) {
throw new Error("Invalid depth profile session symbol");
}
return value;
@@ -4632,6 +4623,28 @@
const cells = getVisibleDirectChildren(row, isVisibleElement);
return cells.length >= MIN_OPEN_ORDER_COLUMNS ? cells : [];
}
+ function readOpenOrdersScopeText(root, options) {
+ if (!root) return "";
+ const symbolCells = new Set(findOpenOrderRowElements(root, options).map((row) => {
+ const cell = getOpenOrderRowCells(row, options)[1];
+ if (!cell || !parseOpenOrderContractSymbol(cell.textContent)) {
+ throw new Error("Invalid visible open-order symbol");
+ }
+ return cell;
+ }));
+ const parts = [];
+ function appendText(node) {
+ if (symbolCells.has(node)) {
+ parts.push("\n", normalizeText(node.textContent), "\n");
+ } else if (node.nodeType === 3) {
+ parts.push(node.textContent);
+ } else {
+ for (const child of node.childNodes) appendText(child);
+ }
+ }
+ appendText(root);
+ return parts.join("");
+ }
// src/binance-orderbook-trade/dom/chart-orders.js
var ACTIVE_POPOVER_SELECTOR = ".bn-bubble.active";
@@ -8466,15 +8479,24 @@
if (ariaChecked === "false") return false;
return null;
}
+ function readOpenOrdersScopeText2(root) {
+ return readOpenOrdersScopeText(root, {
+ isVisibleElement,
+ isRowCancelIcon: (icon) => matchesBinancePageText(
+ icon.getAttribute("aria-label"),
+ BINANCE_PAGE_TEXT.accountOrders.rowCancel
+ )
+ });
+ }
function readVisibleOpenOrderSymbols(root) {
- return readVisibleOpenOrderSymbolsText(root?.textContent || "");
+ return readVisibleOpenOrderSymbolsText(readOpenOrdersScopeText2(root));
}
function isOpenOrdersScopeLimitedToSymbol(root, symbol) {
- return isOpenOrdersScopeLimitedToSymbolText(root?.textContent || "", symbol);
+ return isOpenOrdersScopeLimitedToSymbolText(readOpenOrdersScopeText2(root), symbol);
}
function hasCurrentSymbolOpenOrders(root, symbol, symbolFilterOk, cancelAllButton) {
return hasCurrentSymbolOpenOrdersEvidence({
- scopeText: root?.textContent || "",
+ scopeText: readOpenOrdersScopeText2(root),
symbol,
symbolFilterOk,
cancelAllAvailable: Boolean(cancelAllButton)
@@ -8489,7 +8511,7 @@
const cancelAllButton = findCurrentSymbolCancelAllButton(currentRoot);
const filterChecked = getCheckboxCheckedState(findHideOtherSymbolCheckbox(currentRoot));
if (isFilteredCurrentSymbolOpenOrdersEmpty({
- scopeText: currentRoot?.textContent || "",
+ scopeText: readOpenOrdersScopeText2(currentRoot),
symbol,
filterChecked,
cancelAllAvailable: Boolean(cancelAllButton)
@@ -8500,7 +8522,7 @@
return { hasOrders: true, cancelAllButton };
}
if (filterChecked === true && isOpenOrdersScopeConfirmedForSymbol(currentRoot, symbol) && isCurrentSymbolOpenOrdersDefinitivelyClear({
- scopeText: currentRoot?.textContent || "",
+ scopeText: readOpenOrdersScopeText2(currentRoot),
symbol,
openOrdersCount: getOpenOrdersTabCount()
})) {
@@ -8522,7 +8544,7 @@
function isOpenOrdersScopeConfirmedForSymbol(root, symbol) {
const checkbox = findHideOtherSymbolCheckbox(root);
return isOpenOrdersScopeConfirmedForSymbolText(
- root?.textContent || "",
+ readOpenOrdersScopeText2(root),
symbol,
getCheckboxCheckedState(checkbox)
);
@@ -8553,7 +8575,7 @@
} else {
lastStatus = "not_cleared";
const openOrdersCount = getOpenOrdersTabCount();
- const scopeText = currentRoot.textContent || "";
+ const scopeText = readOpenOrdersScopeText2(currentRoot);
clearCandidate = isCurrentSymbolOpenOrdersClearCandidate({
scopeText,
symbol,
@@ -8676,10 +8698,6 @@
};
}).filter((row) => isOpenOrderRowCurrentSymbol(row.symbolText, symbol) && isOpenOrderRowForPlan(row.sideText, plan) && row.price && isPositiveDecimalString(row.price) && row.qty && isPositiveDecimalString(row.qty) && row.cancelButton);
}
- function isOpenOrderRowCurrentSymbol(symbolText, symbol) {
- const tokens = String(symbolText || "").toUpperCase().match(/[A-Z0-9_]+/g) || [];
- return tokens.includes(String(symbol || "").toUpperCase());
- }
function isOpenOrderRowForPlan(sideText, plan) {
if (!plan) return true;
if (plan.spec?.mode === "OPEN" && plan.spec.side === "LONG") {
@@ -8708,7 +8726,7 @@
const checkbox = findHideOtherSymbolCheckbox(currentRoot);
const cancelAllButton = findCurrentSymbolCancelAllButton(currentRoot);
if (isFilteredCurrentSymbolOpenOrdersEmpty({
- scopeText: currentRoot.textContent || "",
+ scopeText: readOpenOrdersScopeText2(currentRoot),
symbol,
filterChecked: getCheckboxCheckedState(checkbox),
cancelAllAvailable: Boolean(cancelAllButton)
@@ -9131,7 +9149,7 @@
const currentCheckbox = findHideOtherSymbolCheckbox(currentRoot);
const cancelAllButton = findCurrentSymbolCancelAllButton(currentRoot);
return isCurrentSymbolOpenOrdersFilterReady({
- scopeText: currentRoot?.textContent || "",
+ scopeText: readOpenOrdersScopeText2(currentRoot),
symbol,
filterChecked: getCheckboxCheckedState(currentCheckbox),
cancelAllAvailable: Boolean(cancelAllButton)
diff --git a/scripts/binance-strategy27-events.user.js b/scripts/binance-strategy27-events.user.js
index 8c9b520..db028ab 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.2
+// @version 0.6.3
// @author jackhai9
// @description Display Strategy 27 events and provide the shared private CorsairQuant gateway connection
// @match https://www.binance.com/*/futures/*
@@ -2962,9 +2962,13 @@ ${t("候选", "Candidate")} ${annotation.candidateId}`,
} });
}
+ // src/shared/binance-symbol.js
+ var BINANCE_SYMBOL_CHARACTERS = "\\p{L}\\p{N}_";
+ var SYMBOL_PATTERN = new RegExp(`^[${BINANCE_SYMBOL_CHARACTERS}]+$`, "u");
+
// src/shared/binance-futures-route.js
var FUTURES_TRADING_PATH_RE = /^\/(?:[a-z]{2}(?:-[A-Za-z]{2})?\/)?futures\/([^/]+)\/?$/;
- var TRADING_SYMBOL_RE = /^[\p{L}\p{N}_]{3,}$/u;
+ var TRADING_SYMBOL_RE = new RegExp(`^[${BINANCE_SYMBOL_CHARACTERS}]{3,}$`, "u");
function parseFuturesTradingSymbolFromPathname(pathname) {
const normalized = String(pathname || "").split(/[?#]/, 1)[0];
const match = normalized.match(FUTURES_TRADING_PATH_RE);
diff --git a/scripts/binance-strategy29-bollinger.user.js b/scripts/binance-strategy29-bollinger.user.js
index 7ede58f..86fc80d 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.2
+// @version 0.5.3
// @author jackhai9
// @description Native Bollinger/SMA60 markers and the default read-only cross-timeframe summary
// @match https://www.binance.com/*/futures/*
@@ -1317,9 +1317,13 @@
return false;
}
+ // src/shared/binance-symbol.js
+ var BINANCE_SYMBOL_CHARACTERS = "\\p{L}\\p{N}_";
+ var SYMBOL_PATTERN = new RegExp(`^[${BINANCE_SYMBOL_CHARACTERS}]+$`, "u");
+
// src/shared/binance-futures-route.js
var FUTURES_TRADING_PATH_RE = /^\/(?:[a-z]{2}(?:-[A-Za-z]{2})?\/)?futures\/([^/]+)\/?$/;
- var TRADING_SYMBOL_RE = /^[\p{L}\p{N}_]{3,}$/u;
+ var TRADING_SYMBOL_RE = new RegExp(`^[${BINANCE_SYMBOL_CHARACTERS}]{3,}$`, "u");
function parseFuturesTradingSymbolFromPathname(pathname) {
const normalized = String(pathname || "").split(/[?#]/, 1)[0];
const match = normalized.match(FUTURES_TRADING_PATH_RE);
diff --git a/scripts/binance-trading-data.user.js b/scripts/binance-trading-data.user.js
index b93cea3..90c372b 100644
--- a/scripts/binance-trading-data.user.js
+++ b/scripts/binance-trading-data.user.js
@@ -3,7 +3,7 @@
// @namespace binance.trading.data
// @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 1.1.14
+// @version 1.1.15
// @author jackhai9
// @description 在合约交易页面叠加浮动面板,定时拉取交易数据(持仓量、多空比、资金费率等)并显示当前值 + 多空信号
// @match https://www.binance.com/*/futures/*
@@ -16,9 +16,13 @@
// @grant none
// ==/UserScript==
(() => {
+ // src/shared/binance-symbol.js
+ var BINANCE_SYMBOL_CHARACTERS = "\\p{L}\\p{N}_";
+ var SYMBOL_PATTERN = new RegExp(`^[${BINANCE_SYMBOL_CHARACTERS}]+$`, "u");
+
// src/shared/binance-futures-route.js
var FUTURES_TRADING_PATH_RE = /^\/(?:[a-z]{2}(?:-[A-Za-z]{2})?\/)?futures\/([^/]+)\/?$/;
- var TRADING_SYMBOL_RE = /^[\p{L}\p{N}_]{3,}$/u;
+ var TRADING_SYMBOL_RE = new RegExp(`^[${BINANCE_SYMBOL_CHARACTERS}]{3,}$`, "u");
function parseFuturesTradingSymbolFromPathname(pathname) {
const normalized = String(pathname || "").split(/[?#]/, 1)[0];
const match = normalized.match(FUTURES_TRADING_PATH_RE);
diff --git a/src/binance-coinmarketcap-data/index.user.js b/src/binance-coinmarketcap-data/index.user.js
index 9e9ccfa..a232af5 100644
--- a/src/binance-coinmarketcap-data/index.user.js
+++ b/src/binance-coinmarketcap-data/index.user.js
@@ -3,7 +3,7 @@
// @namespace binance.coinmarketcap.data
// @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.16
+// @version 0.1.17
// @author jackhai9
// @description 在 Binance 合约页面显示当前币种的 CoinMarketCap 中文页关键估值与供应量数据
// @match https://www.binance.com/*/futures/*
@@ -94,7 +94,7 @@ import {
function cmcSymbolFromBaseAsset(baseAsset) {
if (!baseAsset) return null;
- return baseAsset.replace(/^(1000000|1000)(?=[A-Z])/, '');
+ return baseAsset.replace(/^(1000000|1000)(?=\p{L})/u, '');
}
function normalizeCmcAsset(rawAsset, fallbackBaseAsset) {
diff --git a/src/binance-orderbook-trade/core/binance-native-depth-source.js b/src/binance-orderbook-trade/core/binance-native-depth-source.js
index 10e2b55..aefaccb 100644
--- a/src/binance-orderbook-trade/core/binance-native-depth-source.js
+++ b/src/binance-orderbook-trade/core/binance-native-depth-source.js
@@ -5,10 +5,11 @@ import {
DepthProfileSequenceError,
pushDepthProfileUpdate,
} from './depth-profile-book.js';
+import { BINANCE_SYMBOL_CHARACTERS, isBinanceSymbol } from '../../shared/binance-symbol.js';
const NATIVE_RPI_DEPTH_PATH = '/fapi/v1/rpiDepth';
const NATIVE_RPI_DEPTH_LIMIT = '1000';
-const NATIVE_RPI_STREAM_PATTERN = /^([a-z0-9_]+)@rpiDepth@500ms$/;
+const NATIVE_RPI_STREAM_PATTERN = new RegExp(`^([${BINANCE_SYMBOL_CHARACTERS}]+)@rpiDepth@500ms$`, 'u');
function assertFunction(value, field) {
if (typeof value !== 'function') throw new Error(`Invalid native depth ${field}`);
@@ -16,7 +17,7 @@ function assertFunction(value, field) {
}
function assertSymbol(value) {
- if (typeof value !== 'string' || !/^[A-Z0-9_]+$/.test(value)) {
+ if (!isBinanceSymbol(value)) {
throw new Error('Invalid native depth symbol');
}
return value;
@@ -156,7 +157,10 @@ export function installBinanceNativeDepthSource(globalObject) {
try {
envelope = JSON.parse(event.data);
const match = NATIVE_RPI_STREAM_PATTERN.exec(envelope?.stream);
- if (!match || !envelope.data || typeof envelope.data !== 'object') {
+ if (
+ !match || match[0] !== envelope.stream || match[1] !== match[1].toLowerCase()
+ || !envelope.data || typeof envelope.data !== 'object'
+ ) {
throw new Error('Invalid Binance native RPI depth message');
}
const symbol = assertSymbol(match[1].toUpperCase());
diff --git a/src/binance-orderbook-trade/core/cancel-orders.js b/src/binance-orderbook-trade/core/cancel-orders.js
index 4d57cc2..e6c65d4 100644
--- a/src/binance-orderbook-trade/core/cancel-orders.js
+++ b/src/binance-orderbook-trade/core/cancel-orders.js
@@ -6,10 +6,16 @@ import {
startsWithBinancePageText,
} from '../contracts/binance-page-text.js';
import { PANEL_COPY } from '../contracts/panel-copy.js';
+import { BINANCE_SYMBOL_CHARACTERS } from '../../shared/binance-symbol.js';
const PERPETUAL_LABEL_PATTERN = buildBinanceTextAlternation(
BINANCE_PAGE_TEXT.accountOrders.perpetual,
);
+const ORDER_CONTRACT_PATTERN = `[${BINANCE_SYMBOL_CHARACTERS}]+(?:USDT|USDC)`;
+const ORDER_SYMBOL_CELL_PATTERN = new RegExp(
+ `^(${ORDER_CONTRACT_PATTERN})(?:\\s*(?:${PERPETUAL_LABEL_PATTERN}))?$`,
+ 'iu',
+);
export function normalizeText(value) {
return String(value || '').replace(/\s+/g, ' ').trim();
@@ -23,53 +29,32 @@ export function parseOpenOrdersTabCount(text) {
return parseBinanceTabCount(text, BINANCE_PAGE_TEXT.accountOrders.openOrdersTab);
}
-function escapeRegExp(value) {
- return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
-}
-
-function normalizeContractCandidate(candidate, separator) {
- const normalized = String(candidate || '').toUpperCase();
- if (separator === ':') {
- const timeJoinedMatch = /^\d{1,2}([A-Z][A-Z0-9]*(?:USDT|USDC))$/.exec(normalized);
- if (timeJoinedMatch) return timeJoinedMatch[1];
- }
- return normalized;
+export function parseOpenOrderContractSymbol(text) {
+ const normalized = normalizeText(text);
+ const match = normalized.match(ORDER_SYMBOL_CELL_PATTERN);
+ return match && match[0] === normalized ? match[1].toUpperCase() : null;
}
-function isTimestampJoinedCandidate(candidate, symbol) {
- const normalizedCandidate = String(candidate || '').toUpperCase();
- const normalizedSymbol = String(symbol || '').toUpperCase();
- if (!normalizedCandidate || !normalizedSymbol || !normalizedCandidate.endsWith(normalizedSymbol)) {
- return false;
- }
- const prefix = normalizedCandidate.slice(0, -normalizedSymbol.length);
- return /^\d{1,2}$/.test(prefix);
-}
-
-function hasVisibleContractText(text, symbol) {
- const normalizedSymbol = String(symbol || '').toUpperCase();
- if (!normalizedSymbol) return false;
- const symbolPattern = escapeRegExp(normalizedSymbol);
- return new RegExp(
- `(?:^|[^A-Z0-9]|\\d{1,2}:\\d{2})${symbolPattern}\\s*(?:${PERPETUAL_LABEL_PATTERN})(?=\\s|$)`,
- 'i',
- )
- .test(String(text || ''));
+export function isOpenOrderRowCurrentSymbol(symbolText, symbol) {
+ return parseOpenOrderContractSymbol(symbolText) === String(symbol || '').toUpperCase();
}
export function readVisibleOpenOrderSymbolsText(text) {
const normalized = String(text || '').toUpperCase();
const symbols = new Set();
+ /**
+ * Strip time only at this contract's own HH:mm[:ss] boundary. Numeric asset
+ * prefixes are identity, and a bare contract is evidence only on its own line.
+ */
const pattern = new RegExp(
- `([A-Z0-9]{2,30}(?:USDT|USDC))\\s*(?:${PERPETUAL_LABEL_PATTERN})(?=\\s|$)`,
- 'gi',
+ `(?:^|[^${BINANCE_SYMBOL_CHARACTERS}])(?:\\d{1,2}:\\d{2}(?::\\d{2})?)?`
+ + `(${ORDER_CONTRACT_PATTERN})\\s*(?:${PERPETUAL_LABEL_PATTERN})(?=\\s|$)`
+ + `|^[\\t ]*(${ORDER_CONTRACT_PATTERN})[\\t ]*$`,
+ 'gimu',
);
let match = pattern.exec(normalized);
while (match) {
- const separator = normalized[match.index - 1] || '';
- if (!/[A-Z0-9]/.test(separator)) {
- symbols.add(normalizeContractCandidate(match[1], separator));
- }
+ symbols.add(match[1] ?? match[2]);
match = pattern.exec(normalized);
}
return Array.from(symbols);
@@ -79,10 +64,7 @@ export function isOpenOrdersScopeLimitedToSymbolText(text, symbol) {
const normalizedSymbol = String(symbol || '').toUpperCase();
if (!normalizedSymbol) return false;
const visibleSymbols = readVisibleOpenOrderSymbolsText(text);
- return visibleSymbols.length > 0 && visibleSymbols.every((visibleSymbol) => (
- visibleSymbol === normalizedSymbol ||
- (hasVisibleContractText(text, normalizedSymbol) && isTimestampJoinedCandidate(visibleSymbol, normalizedSymbol))
- ));
+ return visibleSymbols.length > 0 && visibleSymbols.every((visibleSymbol) => visibleSymbol === normalizedSymbol);
}
export function isOpenOrdersScopeConfirmedForSymbolText(text, symbol, filterChecked) {
@@ -204,10 +186,7 @@ export function hasCurrentSymbolOpenOrdersEvidence({
if (!normalizedSymbol) return false;
const visibleSymbols = readVisibleOpenOrderSymbolsText(scopeText);
- if (visibleSymbols.some((visibleSymbol) => (
- visibleSymbol === normalizedSymbol ||
- (hasVisibleContractText(scopeText, normalizedSymbol) && isTimestampJoinedCandidate(visibleSymbol, normalizedSymbol))
- ))) return true;
+ if (visibleSymbols.includes(normalizedSymbol)) return true;
if (visibleSymbols.length > 0) return false;
return Boolean(symbolFilterOk && cancelAllAvailable);
diff --git a/src/binance-orderbook-trade/core/depth-profile-book.js b/src/binance-orderbook-trade/core/depth-profile-book.js
index c95ac70..926599b 100644
--- a/src/binance-orderbook-trade/core/depth-profile-book.js
+++ b/src/binance-orderbook-trade/core/depth-profile-book.js
@@ -1,3 +1,5 @@
+import { isBinanceSymbol } from '../../shared/binance-symbol.js';
+
export class DepthProfileSequenceError extends Error {
constructor(message) {
super(message);
@@ -8,7 +10,7 @@ export class DepthProfileSequenceError extends Error {
const MAX_BUFFERED_UPDATES = 500;
function assertSymbol(value) {
- if (typeof value !== 'string' || !/^[A-Z0-9_]+$/.test(value)) {
+ if (!isBinanceSymbol(value)) {
throw new Error('Invalid depth profile symbol');
}
return value;
diff --git a/src/binance-orderbook-trade/core/depth-profile-session.js b/src/binance-orderbook-trade/core/depth-profile-session.js
index 1630047..985e568 100644
--- a/src/binance-orderbook-trade/core/depth-profile-session.js
+++ b/src/binance-orderbook-trade/core/depth-profile-session.js
@@ -1,10 +1,12 @@
+import { isBinanceSymbol } from '../../shared/binance-symbol.js';
+
function assertFunction(value, field) {
if (typeof value !== 'function') throw new Error(`Invalid depth profile ${field}`);
return value;
}
function assertSymbol(value) {
- if (typeof value !== 'string' || !/^[A-Z0-9_]+$/.test(value)) {
+ if (!isBinanceSymbol(value)) {
throw new Error('Invalid depth profile session symbol');
}
return value;
diff --git a/src/binance-orderbook-trade/dom/open-order-rows.js b/src/binance-orderbook-trade/dom/open-order-rows.js
index 0b86198..29d2f44 100644
--- a/src/binance-orderbook-trade/dom/open-order-rows.js
+++ b/src/binance-orderbook-trade/dom/open-order-rows.js
@@ -1,3 +1,5 @@
+import { normalizeText, parseOpenOrderContractSymbol } from '../core/cancel-orders.js';
+
const MIN_OPEN_ORDER_COLUMNS = 10;
function getVisibleDirectChildren(element, isVisibleElement) {
@@ -33,3 +35,30 @@ export function getOpenOrderRowCells(row, { isVisibleElement }) {
const cells = getVisibleDirectChildren(row, isVisibleElement);
return cells.length >= MIN_OPEN_ORDER_COLUMNS ? cells : [];
}
+
+/**
+ * textContent joins adjacent Binance cells without separators. Preserve complete
+ * symbol cells so another visible contract cannot disappear from scope evidence.
+ */
+export function readOpenOrdersScopeText(root, options) {
+ if (!root) return '';
+ const symbolCells = new Set(findOpenOrderRowElements(root, options).map((row) => {
+ const cell = getOpenOrderRowCells(row, options)[1];
+ if (!cell || !parseOpenOrderContractSymbol(cell.textContent)) {
+ throw new Error('Invalid visible open-order symbol');
+ }
+ return cell;
+ }));
+ const parts = [];
+ function appendText(node) {
+ if (symbolCells.has(node)) {
+ parts.push('\n', normalizeText(node.textContent), '\n');
+ } else if (node.nodeType === 3) {
+ parts.push(node.textContent);
+ } else {
+ for (const child of node.childNodes) appendText(child);
+ }
+ }
+ appendText(root);
+ return parts.join('');
+}
diff --git a/src/binance-orderbook-trade/dom/trade-form.js b/src/binance-orderbook-trade/dom/trade-form.js
index eb04688..df811be 100644
--- a/src/binance-orderbook-trade/dom/trade-form.js
+++ b/src/binance-orderbook-trade/dom/trade-form.js
@@ -3,6 +3,9 @@ import {
includesBinancePageText,
matchesBinancePageText,
} from '../contracts/binance-page-text.js';
+import { BINANCE_SYMBOL_CHARACTERS } from '../../shared/binance-symbol.js';
+
+const AVAILABLE_BALANCE_PATTERN = new RegExp(`^([\\d,]+(?:\\.\\d+)?)\\s+([${BINANCE_SYMBOL_CHARACTERS}]+)$`, 'u');
function buttonTextMatches(button, labels) {
return includesBinancePageText(button?.textContent, labels);
@@ -46,7 +49,7 @@ export function readTradeAvailableBalance(root, { isVisibleElement }) {
const valueNodes = Array.from(label.parentElement?.children || [])
.filter((node) => node !== label && isVisibleElement(node));
if (valueNodes.length !== 1) return null;
- const match = /^([\d,]+(?:\.\d+)?)\s+([A-Z0-9]+)$/.exec(
+ const match = AVAILABLE_BALANCE_PATTERN.exec(
String(valueNodes[0].textContent || '').replace(/\s+/g, ' ').trim(),
);
return match ? { amount: match[1].replace(/,/g, ''), asset: match[2] } : null;
diff --git a/src/binance-orderbook-trade/index.user.js b/src/binance-orderbook-trade/index.user.js
index 2f5490d..36d3508 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.204
+// @version 2.7.205
// @author jackhai9
// @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板
// @match https://www.binance.com/*/futures/*
@@ -22,6 +22,7 @@ import {
isCurrentSymbolOpenOrdersFilterReady,
isCurrentSymbolOpenOrdersClearCandidate,
isCurrentSymbolOpenOrdersDefinitivelyClear,
+ isOpenOrderRowCurrentSymbol,
isOpenOrdersScopeConfirmedForSymbolText,
isOpenOrdersScopeLimitedToSymbolText,
normalizeText,
@@ -239,6 +240,7 @@ import {
import {
findOpenOrderRowElements,
getOpenOrderRowCells,
+ readOpenOrdersScopeText as readOpenOrdersScopeTextDom,
} from './dom/open-order-rows.js';
import {
assertSameBinanceChartOrdersTarget,
@@ -4350,17 +4352,27 @@ import { showUsdtRebalanceDialog } from './dom/usdt-rebalance-dialog.js';
return null;
}
+ function readOpenOrdersScopeText(root) {
+ return readOpenOrdersScopeTextDom(root, {
+ isVisibleElement,
+ isRowCancelIcon: (icon) => matchesBinancePageText(
+ icon.getAttribute('aria-label'),
+ BINANCE_PAGE_TEXT.accountOrders.rowCancel,
+ ),
+ });
+ }
+
function readVisibleOpenOrderSymbols(root) {
- return readVisibleOpenOrderSymbolsText(root?.textContent || '');
+ return readVisibleOpenOrderSymbolsText(readOpenOrdersScopeText(root));
}
function isOpenOrdersScopeLimitedToSymbol(root, symbol) {
- return isOpenOrdersScopeLimitedToSymbolText(root?.textContent || '', symbol);
+ return isOpenOrdersScopeLimitedToSymbolText(readOpenOrdersScopeText(root), symbol);
}
function hasCurrentSymbolOpenOrders(root, symbol, symbolFilterOk, cancelAllButton) {
return hasCurrentSymbolOpenOrdersEvidence({
- scopeText: root?.textContent || '',
+ scopeText: readOpenOrdersScopeText(root),
symbol,
symbolFilterOk,
cancelAllAvailable: Boolean(cancelAllButton),
@@ -4376,7 +4388,7 @@ import { showUsdtRebalanceDialog } from './dom/usdt-rebalance-dialog.js';
const cancelAllButton = findCurrentSymbolCancelAllButton(currentRoot);
const filterChecked = getCheckboxCheckedState(findHideOtherSymbolCheckbox(currentRoot));
if (isFilteredCurrentSymbolOpenOrdersEmpty({
- scopeText: currentRoot?.textContent || '',
+ scopeText: readOpenOrdersScopeText(currentRoot),
symbol,
filterChecked,
cancelAllAvailable: Boolean(cancelAllButton),
@@ -4390,7 +4402,7 @@ import { showUsdtRebalanceDialog } from './dom/usdt-rebalance-dialog.js';
filterChecked === true &&
isOpenOrdersScopeConfirmedForSymbol(currentRoot, symbol) &&
isCurrentSymbolOpenOrdersDefinitivelyClear({
- scopeText: currentRoot?.textContent || '',
+ scopeText: readOpenOrdersScopeText(currentRoot),
symbol,
openOrdersCount: getOpenOrdersTabCount(),
})
@@ -4415,7 +4427,7 @@ import { showUsdtRebalanceDialog } from './dom/usdt-rebalance-dialog.js';
function isOpenOrdersScopeConfirmedForSymbol(root, symbol) {
const checkbox = findHideOtherSymbolCheckbox(root);
return isOpenOrdersScopeConfirmedForSymbolText(
- root?.textContent || '',
+ readOpenOrdersScopeText(root),
symbol,
getCheckboxCheckedState(checkbox),
);
@@ -4448,7 +4460,7 @@ import { showUsdtRebalanceDialog } from './dom/usdt-rebalance-dialog.js';
} else {
lastStatus = 'not_cleared';
const openOrdersCount = getOpenOrdersTabCount();
- const scopeText = currentRoot.textContent || '';
+ const scopeText = readOpenOrdersScopeText(currentRoot);
clearCandidate = isCurrentSymbolOpenOrdersClearCandidate({
scopeText,
symbol,
@@ -4597,11 +4609,6 @@ import { showUsdtRebalanceDialog } from './dom/usdt-rebalance-dialog.js';
));
}
- function isOpenOrderRowCurrentSymbol(symbolText, symbol) {
- const tokens = String(symbolText || '').toUpperCase().match(/[A-Z0-9_]+/g) || [];
- return tokens.includes(String(symbol || '').toUpperCase());
- }
-
function isOpenOrderRowForPlan(sideText, plan) {
if (!plan) return true;
if (plan.spec?.mode === 'OPEN' && plan.spec.side === 'LONG') {
@@ -4633,7 +4640,7 @@ import { showUsdtRebalanceDialog } from './dom/usdt-rebalance-dialog.js';
const checkbox = findHideOtherSymbolCheckbox(currentRoot);
const cancelAllButton = findCurrentSymbolCancelAllButton(currentRoot);
if (isFilteredCurrentSymbolOpenOrdersEmpty({
- scopeText: currentRoot.textContent || '',
+ scopeText: readOpenOrdersScopeText(currentRoot),
symbol,
filterChecked: getCheckboxCheckedState(checkbox),
cancelAllAvailable: Boolean(cancelAllButton),
@@ -5146,7 +5153,7 @@ import { showUsdtRebalanceDialog } from './dom/usdt-rebalance-dialog.js';
const currentCheckbox = findHideOtherSymbolCheckbox(currentRoot);
const cancelAllButton = findCurrentSymbolCancelAllButton(currentRoot);
return isCurrentSymbolOpenOrdersFilterReady({
- scopeText: currentRoot?.textContent || '',
+ scopeText: readOpenOrdersScopeText(currentRoot),
symbol,
filterChecked: getCheckboxCheckedState(currentCheckbox),
cancelAllAvailable: Boolean(cancelAllButton),
diff --git a/src/binance-strategy27-events/index.user.js b/src/binance-strategy27-events/index.user.js
index 97ad240..ef87b69 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.2
+// @version 0.6.3
// @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 b96d311..e6c1bf7 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.2
+// @version 0.5.3
// @author jackhai9
// @description Native Bollinger/SMA60 markers and the default read-only cross-timeframe summary
// @match https://www.binance.com/*/futures/*
diff --git a/src/binance-trading-data/index.user.js b/src/binance-trading-data/index.user.js
index dfbbe2e..0d0a87f 100644
--- a/src/binance-trading-data/index.user.js
+++ b/src/binance-trading-data/index.user.js
@@ -3,7 +3,7 @@
// @namespace binance.trading.data
// @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 1.1.14
+// @version 1.1.15
// @author jackhai9
// @description 在合约交易页面叠加浮动面板,定时拉取交易数据(持仓量、多空比、资金费率等)并显示当前值 + 多空信号
// @match https://www.binance.com/*/futures/*
diff --git a/src/shared/binance-futures-route.js b/src/shared/binance-futures-route.js
index d748e79..adbf468 100644
--- a/src/shared/binance-futures-route.js
+++ b/src/shared/binance-futures-route.js
@@ -1,5 +1,7 @@
+import { BINANCE_SYMBOL_CHARACTERS } from './binance-symbol.js';
+
const FUTURES_TRADING_PATH_RE = /^\/(?:[a-z]{2}(?:-[A-Za-z]{2})?\/)?futures\/([^/]+)\/?$/;
-const TRADING_SYMBOL_RE = /^[\p{L}\p{N}_]{3,}$/u;
+const TRADING_SYMBOL_RE = new RegExp(`^[${BINANCE_SYMBOL_CHARACTERS}]{3,}$`, 'u');
export function parseFuturesTradingSymbolFromPathname(pathname) {
const normalized = String(pathname || '').split(/[?#]/, 1)[0];
diff --git a/src/shared/binance-symbol.js b/src/shared/binance-symbol.js
new file mode 100644
index 0000000..df654be
--- /dev/null
+++ b/src/shared/binance-symbol.js
@@ -0,0 +1,10 @@
+export const BINANCE_SYMBOL_CHARACTERS = '\\p{L}\\p{N}_';
+
+const SYMBOL_PATTERN = new RegExp(`^[${BINANCE_SYMBOL_CHARACTERS}]+$`, 'u');
+
+/** Preserve exchange identifiers, including Chinese and numeric-only asset names. */
+export function isBinanceSymbol(value) {
+ if (typeof value !== 'string' || value !== value.toUpperCase()) return false;
+ const match = value.match(SYMBOL_PATTERN);
+ return Boolean(match && match[0] === value);
+}
diff --git a/test/unit/binance-data-panel-symbols.test.js b/test/unit/binance-data-panel-symbols.test.js
new file mode 100644
index 0000000..f2aa73a
--- /dev/null
+++ b/test/unit/binance-data-panel-symbols.test.js
@@ -0,0 +1,21 @@
+import { readFile } from 'node:fs/promises';
+import test from 'node:test';
+import assert from 'node:assert/strict';
+
+const source = await readFile(new URL('../../src/binance-coinmarketcap-data/index.user.js', import.meta.url), 'utf8');
+const start = source.indexOf(' function baseAssetFromSymbol(');
+const end = source.indexOf(' function normalizeCmcAsset(', start);
+assert(start >= 0 && end > start);
+const { baseAssetFromSymbol, cmcSymbolFromBaseAsset } = new Function(
+ `${source.slice(start, end)}\nreturn { baseAssetFromSymbol, cmcSymbolFromBaseAsset };`,
+)();
+
+test('CMC mapping retains Unicode and numeric base assets and removes only supported multipliers', () => {
+ for (const [symbol, expected] of [
+ ['龙虾USDT', '龙虾'], ['币安人生USDC', '币安人生'], ['4USDT', '4'],
+ ['1INCHUSDT', '1INCH'], ['1000PEPEUSDT', 'PEPE'], ['1000龙虾USDT', '龙虾'],
+ ['1000000龙虾USDT', '龙虾'], ['10000001USDT', '10000001'],
+ ]) {
+ assert.equal(cmcSymbolFromBaseAsset(baseAssetFromSymbol(symbol)), expected, symbol);
+ }
+});
diff --git a/test/unit/binance-orderbook-trade/binance-native-depth-source.test.js b/test/unit/binance-orderbook-trade/binance-native-depth-source.test.js
index 8a0bed0..4933359 100644
--- a/test/unit/binance-orderbook-trade/binance-native-depth-source.test.js
+++ b/test/unit/binance-orderbook-trade/binance-native-depth-source.test.js
@@ -2,6 +2,8 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import { installBinanceNativeDepthSource } from '../../../src/binance-orderbook-trade/core/binance-native-depth-source.js';
+import { createDepthProfileSession } from '../../../src/binance-orderbook-trade/core/depth-profile-session.js';
+import { parseFuturesTradingSymbolFromPathname } from '../../../src/shared/binance-futures-route.js';
class FakeResponse {
constructor(payload, { ok = true, status = 200 } = {}) {
@@ -312,3 +314,37 @@ test('recovers when Binance performs its next native snapshot synchronization',
assert.equal(statuses.at(-1).status, 'ready');
source.restore();
});
+
+for (const symbol of ['龙虾USDT', '币安人生USDT', '4USDT', '1INCHUSDT', '1000龙虾USDT']) {
+ test(`connects the encoded ${symbol} route to native snapshot, stream, and session`, async () => {
+ const { globalObject, source, fetchCalls } = createHarness();
+ const route = new URL(`https://www.binance.com/zh-CN/futures/${symbol}`);
+ const profiles = [];
+ const statuses = [];
+ const session = createDepthProfileSession({
+ symbol: parseFuturesTradingSymbolFromPathname(route.pathname), source,
+ onProfile: (profile) => profiles.push(profile), onStatus: (status) => statuses.push(status),
+ });
+ session.start();
+ const socket = new globalObject.WebSocket('wss://native-binance-stream.example/ws');
+ const params = new URLSearchParams({ symbol, limit: '1000' });
+ const response = globalObject.fetch(`/fapi/v1/rpiDepth?${params}`);
+ socket.message({ stream: `${symbol.toLowerCase()}@rpiDepth@500ms`, data: update({ s: symbol }) });
+ await response;
+ await new Promise((resolve) => setImmediate(resolve));
+
+ assert.equal(profiles.length, 1);
+ assert.equal(profiles[0].symbol, symbol);
+ assert.equal(profiles[0].bids[0].price, 100);
+ assert.equal(profiles[0].bids[0].cumulative, 2);
+ assert.equal(profiles[0].asks[0].price, 101);
+ assert.equal(profiles[0].asks[0].cumulative, 4);
+ assert.equal(statuses.at(-1).status, 'ready');
+ assert.equal(fetchCalls.length, 1);
+ assert.equal(FakeNativeSocket.instances.length, 1);
+ session.stop();
+ socket.message({ stream: `${symbol.toLowerCase()}@rpiDepth@500ms`, data: update({ s: symbol, U: 103, u: 104, pu: 102 }) });
+ assert.equal(profiles.length, 1);
+ source.restore();
+ });
+}
diff --git a/test/unit/binance-orderbook-trade/cancel.test.js b/test/unit/binance-orderbook-trade/cancel.test.js
index c752fa2..83f122c 100644
--- a/test/unit/binance-orderbook-trade/cancel.test.js
+++ b/test/unit/binance-orderbook-trade/cancel.test.js
@@ -7,11 +7,13 @@ import {
isCurrentSymbolOpenOrdersFilterReady,
isCurrentSymbolOpenOrdersClearCandidate,
isCurrentSymbolOpenOrdersDefinitivelyClear,
+ isOpenOrderRowCurrentSymbol,
isOpenOrdersScopeConfirmedForSymbolText,
isOpenOrdersScopeLimitedToSymbolText,
isOpenOrdersTabText,
normalizeText,
parseOpenOrdersTabCount,
+ parseOpenOrderContractSymbol,
readVisibleOpenOrderSymbolsText,
resolveCancelSymbolButtonPresentation,
shouldContinueOpenOrdersClearObservation,
@@ -95,6 +97,71 @@ test('parses symbol when Binance joins time text and contract text', () => {
}), true);
});
+for (const symbol of ['龙虾USDT', '币安人生USDC', '4USDT', 'WUSDT', '1INCHUSDT', '1000PEPEUSDT', '1000000龙虾USDT', 'A_BTCUSDT']) {
+ test(`keeps the complete ${symbol} contract in order evidence`, () => {
+ for (const label of ['永续', 'Perp']) {
+ assert.deepEqual(readVisibleOpenOrderSymbolsText(`${symbol}${label}`), [symbol]);
+ assert.deepEqual(readVisibleOpenOrderSymbolsText(`2026-09-12 10:27${symbol}${label} Limit`), [symbol]);
+ assert.deepEqual(readVisibleOpenOrderSymbolsText(`2026-09-12 10:27:51${symbol}${label} Limit`), [symbol]);
+ }
+ assert.equal(isOpenOrdersScopeLimitedToSymbolText(`${symbol}永续`, symbol), true);
+ assert.equal(isCurrentSymbolOpenOrdersFilterReady({
+ scopeText: `${symbol}永续`, symbol, filterChecked: true, cancelAllAvailable: true,
+ }), true);
+ assert.equal(isCurrentSymbolOpenOrdersClearCandidate({
+ scopeText: `${symbol}永续`, symbol, openOrdersCount: 1,
+ }), false);
+ });
+}
+
+test('other Unicode and numeric-prefix contracts cannot become current-symbol evidence', () => {
+ for (const other of ['龙虾USDT', '龙虾BTCUSDT', 'A_BTCUSDT', '27BTCUSDT', '4USDT']) {
+ const scopeText = `BTCUSDT 永续 ${other} 永续`;
+ assert.deepEqual(readVisibleOpenOrderSymbolsText(scopeText), ['BTCUSDT', other]);
+ assert.equal(isOpenOrdersScopeLimitedToSymbolText(scopeText, 'BTCUSDT'), false);
+ assert.equal(isOpenOrdersScopeConfirmedForSymbolText(scopeText, 'BTCUSDT', true), false);
+ assert.equal(isCurrentSymbolOpenOrdersFilterReady({
+ scopeText, symbol: 'BTCUSDT', filterChecked: true, cancelAllAvailable: true,
+ }), false);
+ assert.equal(isCurrentSymbolOpenOrdersClearCandidate({
+ scopeText, symbol: 'BTCUSDT', openOrdersCount: 0,
+ }), false);
+ assert.equal(hasCurrentSymbolOpenOrdersEvidence({
+ scopeText: `${other}永续`, symbol: 'BTCUSDT', symbolFilterOk: true, cancelAllAvailable: true,
+ }), false);
+ }
+ assert.equal(isOpenOrdersScopeLimitedToSymbolText('龙虾USDT永续', '虾USDT'), false);
+ assert.equal(isOpenOrdersScopeLimitedToSymbolText('超级龙虾USDT永续', '龙虾USDT'), false);
+});
+
+test('bare contracts are evidence only on their own complete lines', () => {
+ assert.deepEqual(readVisibleOpenOrderSymbolsText('\n龙虾USDT\nBTCUSDT永续\n4USDT\n'), ['龙虾USDT', 'BTCUSDT', '4USDT']);
+ assert.deepEqual(readVisibleOpenOrderSymbolsText('Account 龙虾USDT total'), []);
+ assert.equal(isOpenOrdersScopeLimitedToSymbolText('\n龙虾USDT\nBTCUSDT永续\n', 'BTCUSDT'), false);
+ assert.equal(isCurrentSymbolOpenOrdersClearCandidate({
+ scopeText: '\n龙虾USDT\n', symbol: '龙虾USDT', openOrdersCount: 1,
+ }), false);
+});
+
+test('order symbol cells match the complete contract and only supported labels', () => {
+ for (const symbol of ['龙虾USDT', '币安人生USDC', '4USDT', 'WUSDT', '1INCHUSDT', '1000PEPEUSDT', 'A_BTCUSDT']) {
+ for (const text of [symbol, `${symbol}永续`, `${symbol} Perp`, ` ${symbol}\n永续 `]) {
+ assert.equal(parseOpenOrderContractSymbol(text), symbol);
+ assert.equal(isOpenOrderRowCurrentSymbol(text, symbol), true);
+ }
+ }
+ for (const [text, symbol] of [
+ ['龙虾USDT永续', '虾USDT'], ['超级龙虾USDT永续', '龙虾USDT'],
+ ['龙虾BTCUSDT永续', 'BTCUSDT'], ['A_BTCUSDT永续', 'BTCUSDT'],
+ ['27BTCUSDT永续', 'BTCUSDT'], ['BTCUSDT 永续 龙虾USDT 永续', 'BTCUSDT'],
+ ['BTCUSDT永续限价', 'BTCUSDT'], ['', ''],
+ ]) {
+ assert.equal(isOpenOrderRowCurrentSymbol(text, symbol), false, `${text} / ${symbol}`);
+ }
+ assert.equal(parseOpenOrderContractSymbol('BTCUSDT?'), null);
+ assert.equal(parseOpenOrderContractSymbol('USDT'), null);
+});
+
test('visible open-order symbols include USDC perpetual contracts', () => {
assert.deepEqual(
readVisibleOpenOrderSymbolsText('BTCUSDC 永续 价格 数量 HYPEUSDT 永续'),
diff --git a/test/unit/binance-orderbook-trade/depth-profile-book.test.js b/test/unit/binance-orderbook-trade/depth-profile-book.test.js
index f063634..0b4f4d2 100644
--- a/test/unit/binance-orderbook-trade/depth-profile-book.test.js
+++ b/test/unit/binance-orderbook-trade/depth-profile-book.test.js
@@ -138,3 +138,21 @@ test('preserves every active price level accumulated from the native depth strea
assert.equal(profile.bids.at(-1).cumulative, 1001);
assert.equal(profile.asks.at(-1).cumulative, 1001);
});
+
+for (const symbol of ['龙虾USDT', '币安人生USDT', '4USDT', '1INCHUSDT', '1000龙虾USDT']) {
+ test(`synchronizes ${symbol} without changing quantity or sequence semantics`, () => {
+ const book = createDepthProfileBook(symbol);
+ assert.equal(pushDepthProfileUpdate(book, update({ s: symbol })), false);
+ assert.equal(applyDepthProfileSnapshot(book, snapshot()), true);
+ const profile = buildDepthProfile(book);
+ assert.equal(profile.symbol, symbol);
+ assert.deepEqual(profile.bids.map(({ price, cumulative }) => ({ price, cumulative })), [
+ { price: 100, cumulative: 2 }, { price: 99, cumulative: 5 },
+ ]);
+ assert.deepEqual(profile.asks.map(({ price, cumulative }) => ({ price, cumulative })), [
+ { price: 101, cumulative: 4 }, { price: 102, cumulative: 9 },
+ ]);
+ assert.throws(() => pushDepthProfileUpdate(book, update({ s: `其他${symbol}` })), /symbol mismatch/);
+ assert.throws(() => pushDepthProfileUpdate(book, update({ s: symbol, U: 105, u: 106, pu: 104 })), DepthProfileSequenceError);
+ });
+}
diff --git a/test/unit/binance-orderbook-trade/depth-profile-session.test.js b/test/unit/binance-orderbook-trade/depth-profile-session.test.js
index bfe48f8..2786a64 100644
--- a/test/unit/binance-orderbook-trade/depth-profile-session.test.js
+++ b/test/unit/binance-orderbook-trade/depth-profile-session.test.js
@@ -91,3 +91,24 @@ test('rejects duplicate starts', () => {
session.start();
assert.throws(() => session.start(), /already started/);
});
+
+for (const symbol of ['龙虾USDT', '币安人生USDT', '4USDT']) {
+ test(`subscribes to ${symbol} and preserves exact symbol ownership`, () => {
+ const source = new FakeNativeDepthSource();
+ const profiles = [];
+ const statuses = [];
+ const session = createDepthProfileSession({
+ symbol, source, onProfile: (profile) => profiles.push(profile), onStatus: (status) => statuses.push(status),
+ });
+ session.start();
+ assert.equal(source.subscriptions.length, 1);
+ assert.equal(source.subscriptions[0].symbol, symbol);
+ assert.deepEqual(statuses, [{ symbol, status: 'connecting', detail: '' }]);
+ const profile = { symbol, bids: [{ price: 1, cumulative: 2 }], asks: [{ price: 2, cumulative: 3 }] };
+ source.profile(profile);
+ assert.deepEqual(profiles, [profile]);
+ assert.throws(() => source.profile({ ...profile, symbol: `其他${symbol}` }), /symbol mismatch/);
+ session.stop();
+ assert.equal(source.subscriptions.length, 0);
+ });
+}
diff --git a/test/unit/binance-orderbook-trade/open-order-rows.test.js b/test/unit/binance-orderbook-trade/open-order-rows.test.js
index 7593553..dcffe1c 100644
--- a/test/unit/binance-orderbook-trade/open-order-rows.test.js
+++ b/test/unit/binance-orderbook-trade/open-order-rows.test.js
@@ -5,7 +5,14 @@ import { loadFixtureDom } from '../../helpers/dom.js';
import {
findOpenOrderRowElements,
getOpenOrderRowCells,
+ readOpenOrdersScopeText,
} from '../../../src/binance-orderbook-trade/dom/open-order-rows.js';
+import {
+ isCurrentSymbolOpenOrdersClearCandidate,
+ isCurrentSymbolOpenOrdersFilterReady,
+ isOpenOrdersScopeLimitedToSymbolText,
+ readVisibleOpenOrderSymbolsText,
+} from '../../../src/binance-orderbook-trade/core/cancel-orders.js';
function isVisibleElement(element) {
return Boolean(element?.isConnected && !element.closest('[data-hidden]'));
@@ -84,3 +91,79 @@ test('ignores hidden and unrelated SVG actions instead of treating list wrappers
assert.equal(rows.length, 1);
assert.equal(getOpenOrderRowCells(rows[0], { isVisibleElement })[1].textContent.trim(), 'HYPEUSDT永续');
});
+
+const scopeOptions = {
+ isVisibleElement,
+ isRowCancelIcon: (icon) => icon.getAttribute('aria-label') === '撤销挂单',
+};
+
+test('scope evidence preserves Unicode symbol cells beside concatenated order columns', () => {
+ for (const symbol of ['龙虾USDT', '4USDT']) {
+ const dom = loadFixtureDom(`${createRow({ symbol })}${createRow({ symbol: 'BTCUSDT' })} `);
+ const root = dom.window.document.querySelector('section');
+ assert.equal(root.textContent.includes(`${symbol}永续限价委托`), true);
+ const scopeText = readOpenOrdersScopeText(root, scopeOptions);
+ assert.deepEqual(readVisibleOpenOrderSymbolsText(scopeText), [symbol, 'BTCUSDT']);
+ assert.equal(isOpenOrdersScopeLimitedToSymbolText(scopeText, 'BTCUSDT'), false);
+ assert.equal(isCurrentSymbolOpenOrdersFilterReady({
+ scopeText, symbol: 'BTCUSDT', filterChecked: true, cancelAllAvailable: true,
+ }), false);
+ assert.equal(isCurrentSymbolOpenOrdersClearCandidate({ scopeText, symbol, openOrdersCount: 2 }), false);
+ }
+});
+
+test('scope evidence retains bare symbol cells and text split into nested spans', () => {
+ for (const cell of ['龙虾USDT', '龙虾 USDT 永续 ']) {
+ const row = createRow({ symbol: '龙虾USDT' }).replace('龙虾USDT永续', cell);
+ const dom = loadFixtureDom(`${row}${createRow({ symbol: 'BTCUSDT' })} `);
+ const scopeText = readOpenOrdersScopeText(dom.window.document.querySelector('section'), scopeOptions);
+ assert.deepEqual(readVisibleOpenOrderSymbolsText(scopeText), ['龙虾USDT', 'BTCUSDT']);
+ assert.equal(isOpenOrdersScopeLimitedToSymbolText(scopeText, 'BTCUSDT'), false);
+ }
+ const dom = loadFixtureDom(`${createRow({ symbol: '龙虾USDT' }).replace('龙虾USDT永续', '龙虾USDT')} `);
+ const scopeText = readOpenOrdersScopeText(dom.window.document.querySelector('section'), scopeOptions);
+ assert.deepEqual(readVisibleOpenOrderSymbolsText(scopeText), ['龙虾USDT']);
+ assert.equal(isCurrentSymbolOpenOrdersFilterReady({ scopeText, symbol: '龙虾USDT', filterChecked: true, cancelAllAvailable: true }), true);
+ assert.equal(isCurrentSymbolOpenOrdersClearCandidate({ scopeText, symbol: '龙虾USDT', openOrdersCount: 1 }), false);
+});
+
+test('scope evidence keeps contracts surrounded by Unicode whitespace', () => {
+ for (const [symbol, cell] of [
+ ['龙虾USDT', '\u00a0龙虾USDT\u00a0'],
+ ['4USDT', '\u202f4USDT\u202f'],
+ ]) {
+ const row = createRow({ symbol }).replace(`${symbol}永续`, cell);
+ const mixedDom = loadFixtureDom(`${row}${createRow({ symbol: 'BTCUSDT' })} `);
+ const mixedScopeText = readOpenOrdersScopeText(mixedDom.window.document.querySelector('section'), scopeOptions);
+ assert.deepEqual(readVisibleOpenOrderSymbolsText(mixedScopeText), [symbol, 'BTCUSDT']);
+ assert.equal(isOpenOrdersScopeLimitedToSymbolText(mixedScopeText, 'BTCUSDT'), false);
+ assert.equal(isCurrentSymbolOpenOrdersFilterReady({
+ scopeText: mixedScopeText, symbol: 'BTCUSDT', filterChecked: true, cancelAllAvailable: true,
+ }), false);
+ assert.equal(isCurrentSymbolOpenOrdersClearCandidate({
+ scopeText: mixedScopeText, symbol: 'BTCUSDT', openOrdersCount: 2,
+ }), false);
+
+ const singleDom = loadFixtureDom(``);
+ const singleScopeText = readOpenOrdersScopeText(singleDom.window.document.querySelector('section'), scopeOptions);
+ assert.deepEqual(readVisibleOpenOrderSymbolsText(singleScopeText), [symbol]);
+ assert.equal(isCurrentSymbolOpenOrdersFilterReady({
+ scopeText: singleScopeText, symbol, filterChecked: true, cancelAllAvailable: true,
+ }), true);
+ assert.equal(isCurrentSymbolOpenOrdersClearCandidate({
+ scopeText: singleScopeText, symbol, openOrdersCount: 1,
+ }), false);
+ }
+});
+
+test('scope evidence rejects an unidentified visible row instead of dropping it', () => {
+ const invalidRow = createRow({ symbol: '龙虾USDT' }).replace('龙虾USDT永续', 'unidentified contract');
+ const dom = loadFixtureDom(`${invalidRow}${createRow({ symbol: 'BTCUSDT' })} `);
+ assert.throws(() => readOpenOrdersScopeText(dom.window.document.querySelector('section'), scopeOptions), /Invalid visible open-order symbol/);
+});
+
+test('scope text preserves the existing empty-state contract', () => {
+ const dom = loadFixtureDom('');
+ assert.equal(readOpenOrdersScopeText(dom.window.document.querySelector('section'), scopeOptions), '隐藏其他合约暂无当前委托。');
+ assert.equal(readOpenOrdersScopeText(null, scopeOptions), '');
+});
diff --git a/test/unit/binance-orderbook-trade/route.test.js b/test/unit/binance-orderbook-trade/route.test.js
index 86ad803..b7d6faf 100644
--- a/test/unit/binance-orderbook-trade/route.test.js
+++ b/test/unit/binance-orderbook-trade/route.test.js
@@ -25,7 +25,7 @@ test('identifies only futures trading page paths', () => {
test('parses Unicode futures symbols from actual URL pathnames', () => {
- for (const symbol of ['币安人生USDT', '龙虾USDT']) {
+ for (const symbol of ['币安人生USDT', '龙虾USDT', '4USDT', 'WUSDT', '1INCHUSDT', '1000PEPEUSDT']) {
assert.equal(parseFuturesTradingSymbolFromPathname('/zh-CN/futures/' + symbol), symbol);
assert.equal(parseFuturesTradingSymbolFromPathname(new URL('https://www.binance.com/zh-CN/futures/' + symbol).pathname), symbol);
}
diff --git a/test/unit/binance-orderbook-trade/trade-form.test.js b/test/unit/binance-orderbook-trade/trade-form.test.js
index 7664b9d..558b698 100644
--- a/test/unit/binance-orderbook-trade/trade-form.test.js
+++ b/test/unit/binance-orderbook-trade/trade-form.test.js
@@ -101,6 +101,15 @@ test('rejects missing, malformed, or ambiguous available-balance contracts', ()
);
});
+test('available-balance asset identifiers retain Unicode and single-digit names', () => {
+ for (const asset of ['龙虾', '币安人生', '4', '1INCH', '1000PEPE', 'A_B']) {
+ const dom = loadFixtureDom(``);
+ assert.deepEqual(readTradeAvailableBalance(dom.window.document.querySelector('section'), {
+ isVisibleElement: () => true,
+ }), { amount: '1234.50', asset });
+ }
+});
+
test('panel spacer is restored before native trade mode after a rerender moves it', () => {
const dom = loadFixtureDom(fixture);
const { document } = dom.window;
diff --git a/test/unit/binance-strategy27-events/live-event-contract.test.js b/test/unit/binance-strategy27-events/live-event-contract.test.js
index 3d39968..d1ef9e6 100644
--- a/test/unit/binance-strategy27-events/live-event-contract.test.js
+++ b/test/unit/binance-strategy27-events/live-event-contract.test.js
@@ -460,7 +460,7 @@ test('bounds retained lifecycle events and reports exact count and age evictions
test('canonical Unicode symbols round-trip without normalizing invalid wire input', () => {
- for (const base of ['币安人生', '龙虾', '1000PEPE']) {
+ for (const base of ['币安人生', '龙虾', '4', 'W', '1INCH', '1000PEPE']) {
assert.equal(routeSymbolToCanonical(base + 'USDT'), base + '/USDT:USDT');
assert.equal(canonicalSymbolToRoute(base + '/USDT:USDT'), base + 'USDT');
}
diff --git a/test/unit/binance-strategy29-bollinger/source-regressions.test.js b/test/unit/binance-strategy29-bollinger/source-regressions.test.js
index 5221246..b440cf4 100644
--- a/test/unit/binance-strategy29-bollinger/source-regressions.test.js
+++ b/test/unit/binance-strategy29-bollinger/source-regressions.test.js
@@ -52,7 +52,7 @@ test('Bollinger chart alert failures distinguish snapshot races from contract fa
});
test('Strategy29 sandbox metadata is exact and coordinates through unsafeWindow', () => {
- assert.equal(readUserscriptVersion(entrySource), '0.5.2');
+ assert.equal(readUserscriptVersion(entrySource), '0.5.3');
assert.deepEqual(
[...entrySource.matchAll(/^\/\/ @grant\s+(\S+)\s*$/gm)].map(match => match[1]),
['unsafeWindow', 'GM_getValue', 'GM_setValue'],
diff --git a/test/unit/binance-symbol.test.js b/test/unit/binance-symbol.test.js
new file mode 100644
index 0000000..1049f74
--- /dev/null
+++ b/test/unit/binance-symbol.test.js
@@ -0,0 +1,30 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+
+import { isBinanceSymbol } from '../../src/shared/binance-symbol.js';
+import { parseFuturesTradingSymbolFromPathname } from '../../src/shared/binance-futures-route.js';
+import { canonicalUsdtToRoute, isCanonicalUsdtSymbol, usdtRouteToCanonical } from '../../src/shared/canonical-symbol.js';
+
+test('Binance identifiers support Unicode letters, numbers, and underscores without normalization', () => {
+ for (const symbol of ['BTCUSDT', '龙虾USDT', '币安人生USDC', '4', '4USDT', 'WUSDT', '1INCHUSDT', '1000PEPEUSDT', 'A_BTCUSDT']) {
+ assert.equal(isBinanceSymbol(symbol), true, symbol);
+ }
+ for (const symbol of ['', 'btcusdt', '龙虾usdt', '龙虾USDT\n', ' BTCUSDT', 'BTCUSDT ', '龙 虾USDT', 'BTC/USDT', 'BTC-USDT', null, 4]) {
+ assert.equal(isBinanceSymbol(symbol), false, String(symbol));
+ }
+});
+
+test('route length remains a Unicode code-point contract', () => {
+ assert.equal(parseFuturesTradingSymbolFromPathname('/futures/𐐀𐐀'), null);
+ assert.equal(parseFuturesTradingSymbolFromPathname('/futures/𐐀𐐀𐐀'), '𐐀𐐀𐐀');
+});
+
+test('shared gateway symbol conversion preserves Chinese and numeric asset identity', () => {
+ for (const base of ['龙虾', '币安人生', '4', 'W', '1INCH', '1000PEPE']) {
+ const route = `${base}USDT`;
+ const canonical = `${base}/USDT:USDT`;
+ assert.equal(usdtRouteToCanonical(route), canonical);
+ assert.equal(isCanonicalUsdtSymbol(canonical), true);
+ assert.equal(canonicalUsdtToRoute(canonical), route);
+ }
+});
diff --git a/test/unit/userscript-release-contract.test.js b/test/unit/userscript-release-contract.test.js
index 052e4a9..0d8c393 100644
--- a/test/unit/userscript-release-contract.test.js
+++ b/test/unit/userscript-release-contract.test.js
@@ -21,7 +21,7 @@ test('Strategy29 has an independent observation-only install identity', async ()
const metadata = parseUserscriptMetadata(text);
assert.equal(contract.name, '【自写】Binance Strategy 29 布林带信号');
assert.equal(contract.namespace, 'binance.strategy29.bollinger');
- assert.equal(contract.version, '0.5.2');
+ assert.equal(contract.version, '0.5.3');
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);
@@ -61,7 +61,7 @@ test('release contract identifies the generated Strategy 27 annotation artifact'
assert.equal(contract.name, '【自写】Binance Strategy 27 事件标注');
assert.equal(contract.namespace, 'binance.strategy27.events');
- assert.equal(contract.version, '0.6.2');
+ assert.equal(contract.version, '0.6.3');
assert.equal(contract.runAt, 'document-idle');
assert.equal(contract.updateURL, contract.downloadURL);
assert.deepEqual(contract.matches, [