Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/binance-orderbook-trade-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions docs/binance-strategy27-events-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/binance-strategy29-bollinger-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions docs/userscript-validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion e2e/binance-orderbook/fixtures/binance-futures.js
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -340,9 +340,11 @@ export function renderBinanceFuturesFixture(scenario) {
const orders = visibleOrders();
if (!orders.length) return '<div data-empty-orders>暂无当前委托。</div>';
return orders.map((item) => '<div class="open-order-row" data-order-id="' + item.id + '">' +
'<span>2026-09-12 10:27:51</span>' +
'<span>' + item.symbol + ' 永续</span> <span>限价</span> <span>' + item.side + '</span> ' +
'<span>' + item.price + '</span> <span>' + item.quantity + '</span> ' +
'<span>0</span> <span>只做Maker</span> <span>--</span> <span>--</span> <span>--</span>' +
'<span><svg aria-label="撤销挂单" width="16" height="16" viewBox="0 0 16 16"><path d="M4 4L12 12M4 12L12 4" stroke="currentColor"/></svg></span>' +
'</div>').join('');
}

Expand Down
130 changes: 130 additions & 0 deletions e2e/binance-orderbook/specs/unicode-symbols.pw.js
Original file line number Diff line number Diff line change
@@ -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 = '<!doctype html><body style="margin:0"><div class="chart-markup-table price-axis-container" style="position:fixed;top:0;right:0;width:60px;height:260px;border-left:1px solid #ddd"></div><div id="fixture-symbol" style="padding:12px;font:14px Arial"></div></body>';
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([]);
});
}
10 changes: 7 additions & 3 deletions scripts/binance-coinmarketcap-data.user.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/*
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
Loading
Loading