diff --git a/docs/binance-futures-blank-page-diagnosis.md b/docs/binance-futures-blank-page-diagnosis.md new file mode 100644 index 0000000..cbca2a4 --- /dev/null +++ b/docs/binance-futures-blank-page-diagnosis.md @@ -0,0 +1,111 @@ +# Binance Futures startup stall observed on 2026-10-02 + +This is incident evidence, not a standing diagnosis or permission to change a +browser profile. The assistant performed no refresh, extension change, cache +deletion, proxy change, or trading action during the investigation. + +## Page purpose + +`/zh-CN/futures/` is a real contract-entry route. The observed Binance application +registers `futures` with route ID `743f`. Its page module delegates to module +`41906`, which selects a valid previous UM contract or defaults to `BTCUSDT`. +An indefinitely empty page is not its intended final state. + +Sources: [entry module](https://bin.bnbstatic.com/static/chunks/page-743f.ce7935bd.js), +[contract selection](https://bin.bnbstatic.com/static/chunks/78955.382ce295.js). +The precise downstream history push/replace implementation was not inspected. + +## Observed failure and recovery + +Both a concrete USUSDT trading page and the contract-entry page had HTTP 200 +documents and completed load events, but remained in startup. The router was +not ready, its transition remained pending, and the trading page had no native +orderbook or chart container. A small userscript panel does not establish host +application readiness. + +The current [framework](https://bin.bnbstatic.com/static/common/framework.2922525e.js) +waits for route readiness before hydration. Its registered theme `beforeResolve` +hook records `get-cache-theme-config-start`, awaits configuration, then records +the end marker and advances the route. In the +[main bundle](https://bin.bnbstatic.com/static/main.37a180b9.js), module `99940` +first awaits a localForage cache read. A cache miss loads a theme JavaScript +chunk. The cache wait has no application-level deadline; webpack's separate +script loader does have a 120-second deadline. + +The user subsequently reported that the pages had recovered. The currently +loaded documents contained the following timing records; values are milliseconds +from each document's navigation start: + +| Document | Time origin | Event | Start | End | +| --- | --- | --- | ---: | ---: | +| USUSDT | 1790915945589.4 | Theme configuration | 924 | 1214752 | +| USUSDT | 1790915945589.4 | Language cache branch | 856 | 1214836 | +| Futures home | 1790915960355 | Language startup gate | 647 | 1200211 | + +The USUSDT theme gate and home language gate ended about 225 ms apart in wall +time. USUSDT then had a ready router, native orderbook, chart container, and +canvas. A subsequent theme read took 5 ms. A fresh read-only +`indexedDB.databases()` metadata query completed in 3 ms and confirmed the theme +database existed, without reading its records. + +Earlier, an IndexedDB metadata query remained pending at a subsequent check. +Intervening document navigations invalidated its remote object, so its eventual +outcome cannot be recovered or equated with the later successful query. These +samples span different document lifetimes; do not describe the entire session +as one uninterrupted navigation or claim that nobody refreshed it. + +The strongest supported conclusion is a stalled theme/configuration route gate, +with browser-local storage readiness implicated by the concurrent cache waits +and recovery. The precise storage driver, blocking connection, rejected versus +pending internal theme operation, and browser defect remain unproven. A rejected +async hook that never calls its continuation can also leave a route pending. + +## Network comparison + +macOS HTTP, HTTPS, and SOCKS settings pointed to `127.0.0.1:7890`. +One bounded request for the exact main bundle through that proxy returned HTTP +200 in 0.904 seconds. A direct request timed out during connection establishment +after 15 seconds. These results establish only those two shell request paths; +they do not certify every browser request or exclude an earlier transient +network failure. There is no evidence here that disabling the proxy is a fix. + +## Related upstream reports + +- [localForage #821](https://github.com/localForage/localForage/issues/821) + describes a first `getItem()` that never settles after cross-tab object-store + changes. [PR #807](https://github.com/localForage/localForage/pull/807) fixed a + connection/version-change problem. The maintainer + [released 1.10.0](https://github.com/localForage/localForage/pull/807#issuecomment-901439908) + and a reporter [confirmed the fix](https://github.com/localForage/localForage/pull/807#issuecomment-901563757). + The observed Binance bundle already contains localForage 1.10.0 and + `onversionchange` connection closing. Missing this old fix is not a supported + explanation for the current incident. +- [#685](https://github.com/localForage/localForage/issues/685) reports silent + `indexedDB.open()` hangs and community recovery after Chrome restart. Another + reporter [found site-data clearing also unresponsive](https://github.com/localForage/localForage/issues/685#issuecomment-381134641). + The [maintainer did not establish a root cause](https://github.com/localForage/localForage/issues/685#issuecomment-381339640). +- [#912](https://github.com/localForage/localForage/issues/912#issuecomment-883958597) + contains a Chrome multi-tab pending-operation report. An earlier comment + [associates clearing data across open tabs with a hang](https://github.com/localForage/localForage/issues/912#issuecomment-566066515). + These are community observations, not confirmed remedies for Binance. +- [Chrome Page Lifecycle guidance](https://developer.chrome.com/docs/web-platform/page-lifecycle-api) + recommends closing IndexedDB connections before freezing so other same-origin + tabs are not affected. [MDN's blocked event documentation](https://developer.mozilla.org/en-US/docs/Web/API/IDBOpenDBRequest/blocked_event) + explains version-change blocking. Neither proves a freeze or lock occurred here. + +The cited GitHub discussions were inspected with their full comment threads, +follow-ups, and linked fix. Direct Binance Reddit threads were inaccessible +during this investigation and are not treated as verified supporting evidence. + +## Next occurrence + +Preserve the failed page, record document time origin and the route/cache marks, +and compare browser storage readiness with bounded network evidence before +changing settings. Collect from navigation start if the goal is to identify the +specific unresolved storage operation. Browser restart is a community recovery +experiment, not a proven durable fix; clearing site data is not the first step. +Do not patch Binance's router or call its continuation from a userscript. + +The separate userscript bug was broad route recognition (`home` became `HOME`) +and Strategy31's non-trading status lifecycle. Fixing those defects does not +establish that the host startup stall is fixed. diff --git a/docs/binance-strategy27-events-development.md b/docs/binance-strategy27-events-development.md index 83c358e..3f310d5 100644 --- a/docs/binance-strategy27-events-development.md +++ b/docs/binance-strategy27-events-development.md @@ -14,7 +14,7 @@ V10 live projection. The VPS remains the only market-data and event-analysis authority. The userscript opens no Binance market-data WebSocket, uses no Binance API key, and does not recalculate the four force groups. -Version 0.6.6 retains this installation's private gateway configuration and +Version 0.6.7 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; @@ -169,7 +169,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.6 with identical metadata headers. +generated install artifact are version 0.6.7 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 3f4378a..bd04219 100644 --- a/docs/binance-strategy29-bollinger-development.md +++ b/docs/binance-strategy29-bollinger-development.md @@ -13,8 +13,8 @@ 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.6 with orderbook 2.7.199 or later, or use it alone. -Install CorsairQuant signal client 0.6.6 for the remote summary. +Install Strategy29 0.5.7 with orderbook 2.7.199 or later, or use it alone. +Install CorsairQuant signal client 0.6.7 for the remote summary. Do not combine it with the embedded observer in orderbook 2.7.198. After updating/disabling the old script, reload the page. An embedded observer is an explicit conflict: Strategy29 stops and displays an upgrade/reload notice. diff --git a/docs/binance-strategy31-volume-reversal-development.md b/docs/binance-strategy31-volume-reversal-development.md index b888d83..1edbea2 100644 --- a/docs/binance-strategy31-volume-reversal-development.md +++ b/docs/binance-strategy31-volume-reversal-development.md @@ -1,8 +1,8 @@ # Strategy31 volume reversal signals Source: `src/binance-strategy31-volume-reversal/`. Installer: -`scripts/binance-strategy31-volume-reversal.user.js` (0.1.0). -Install alongside CorsairQuant signal client 0.6.6, which owns private gateway +`scripts/binance-strategy31-volume-reversal.user.js` (0.1.1). +Install alongside CorsairQuant signal client 0.6.7, which owns private gateway authentication. The shared bridge explicitly advertises the `strategy31` capability. CorsairQuant confirms consecutive closed red/green candles when green base volume @@ -14,6 +14,9 @@ Hidden documents suspend requests; navigation and settings revisions invalidate late responses. Repeated snapshots reconcile one arrow per stable event ID. Interval changes preserve the native data-completion subscription. Unsupported periods and non-USDT markets pause observation until a supported chart is selected. +Non-trading routes remove the status and retire pending requests and chart +ownership. Route observation remains available after a terminal failure solely +to remove presentation on departure; it does not restart failed business work. A transient native candle snapshot inconsistency retains existing arrows and waits for the next sample; malformed signal contracts still stop the observer. Loading earlier diff --git a/docs/userscript-validation.md b/docs/userscript-validation.md index afd30fa..e432d55 100644 --- a/docs/userscript-validation.md +++ b/docs/userscript-validation.md @@ -54,6 +54,16 @@ read the Node version from `.nvmrc`. `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. +- The shared route parser activates scripts only for + `/[locale/]futures/USDT` and `USDC` perpetual routes, with an + optional trailing slash. The asset must be nonempty; Unicode letters, numbers, + and underscores retain their exchange identity. This is the scripts' supported + route syntax, not a live exchange listing check. Individual strategies retain + their narrower USDT market and timeframe requirements. Other quote assets, + dated contracts, landing pages, `home`, `quiz`, multi-chart pages, calculators, + and wallet routes do not activate panels or business requests. + Keep the broad metadata match and lightweight route watcher so SPA navigation + from a landing page can activate a supported contract and remove it on return. - 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 diff --git a/e2e/binance-orderbook/helpers/userscript-page.js b/e2e/binance-orderbook/helpers/userscript-page.js index fb661e5..d086d1a 100644 --- a/e2e/binance-orderbook/helpers/userscript-page.js +++ b/e2e/binance-orderbook/helpers/userscript-page.js @@ -29,7 +29,7 @@ export function readScenarioEvidence(page) { return evidenceByPage.get(page) || null; } -export async function openUserscriptScenario(page, scenario, { beforeOrderbook = '', afterOrderbook = '' } = {}) { +export async function openUserscriptScenario(page, scenario, { beforeOrderbook = '', afterOrderbook = '', afterNavigation } = {}) { const errors = []; page.on('pageerror', (error) => errors.push(String(error?.stack || error))); const userscriptSource = await readFile(USERSCRIPT_PATH, 'utf8'); @@ -128,6 +128,7 @@ export async function openUserscriptScenario(page, scenario, { beforeOrderbook = }); await page.goto(`https://www.binance.com/zh-CN/futures/${scenario.currentSymbol}`); + if (afterNavigation) await afterNavigation(page); await page.locator('#jh-binance-close-qty-multiplier-panel').waitFor({ state: 'visible' }); await page.locator('#jh-binance-ladder-body').waitFor({ state: 'visible' }); await page.evaluate(() => new Promise((resolve) => { diff --git a/e2e/binance-orderbook/specs/futures-route-scope.pw.js b/e2e/binance-orderbook/specs/futures-route-scope.pw.js new file mode 100644 index 0000000..cecba6b --- /dev/null +++ b/e2e/binance-orderbook/specs/futures-route-scope.pw.js @@ -0,0 +1,77 @@ +import { test, expect } from '../test.js'; +import { createCancelScenario } from '../scenarios/cancel-current-symbol.js'; +import { openUserscriptScenario, readFixtureState } from '../helpers/userscript-page.js'; +import { installScenarioClock, pauseScenarioClock } from '../helpers/scenario-clock.js'; + +test('user can enter a trading route after the orderbook installer starts on home', async ({ page }) => { + // Given the real installer starts on home while an isolated native host is available. + await page.route('**/*', route => route.abort('blockedbyclient')); + await installScenarioClock(page); + const businessRequests = []; + page.on('request', request => { + if (new URL(request.url()).pathname.includes('/fapi/')) businessRequests.push(request.url()); + }); + + // When another non-trading route precedes entry into a supported contract without reinjection. + const host = await openUserscriptScenario(page, createCancelScenario({ currentSymbol: 'USUSDT' }), { + beforeOrderbook: "history.replaceState({}, '', '/zh-CN/futures/home');", + afterNavigation: async () => { + await expect(page.locator('#jh-binance-close-qty-multiplier-panel')).toHaveCount(0); + expect(await page.evaluate(() => typeof window.__TM_CLOSE_LONG_DEBUG__)).toBe('undefined'); + expect(businessRequests).toEqual([]); + await page.evaluate(() => history.pushState({}, '', '/zh-CN/futures/quiz')); + await expect(page.locator('#jh-binance-close-qty-multiplier-panel')).toHaveCount(0); + await page.evaluate(() => history.pushState({}, '', '/zh-CN/futures/USUSDT')); + }, + }); + await pauseScenarioClock(page); + + // Then the installer starts exactly once and creates its trading panel. + await expect(page.locator('#jh-binance-close-qty-multiplier-panel')).toBeVisible(); + await expect(page.locator('#jh-binance-close-qty-multiplier-panel')).toHaveCount(1); + await page.evaluate(() => { window.routeTestFetch = window.fetch; window.routeTestWebSocket = window.WebSocket; }); + + // When repeated route notifications accompany leaving and returning to trading. + await page.evaluate(() => { + history.pushState({}, '', '/zh-CN/futures/home'); + history.pushState({}, '', '/zh-CN/futures/USUSDT'); + dispatchEvent(new PopStateEvent('popstate')); + dispatchEvent(new PopStateEvent('popstate')); + }); + await page.clock.runFor(1000); + + // Then the original network observers and single panel remain owned by one runtime. + await expect(page.locator('#jh-binance-close-qty-multiplier-panel')).toHaveCount(1); + expect(await page.evaluate(() => [window.fetch === window.routeTestFetch, window.WebSocket === window.routeTestWebSocket])).toEqual([true, true]); + expect((await readFixtureState(page)).events.filter(event => ['order-submitted', 'cancel-requested'].includes(event.type))).toEqual([]); + expect(host.errors).toEqual([]); +}); + +test('user sees the orderbook panel only on supported trading routes across SPA navigation', async ({ page }) => { + // Given the generated installer has mounted on a supported contract in an isolated host. + await page.route('**/*', route => route.abort('blockedbyclient')); + await installScenarioClock(page); + const host = await openUserscriptScenario(page, createCancelScenario()); + await pauseScenarioClock(page); + const panel = page.locator('#jh-binance-close-qty-multiplier-panel'); + await expect(panel).toBeVisible(); + const tradingPath = await page.evaluate(() => location.pathname); + + // When SPA navigation visits each non-trading futures page with stale trading DOM still present. + for (const path of ['/zh-CN/futures/home', '/zh-CN/futures/', '/zh-CN/futures/quiz', '/zh-CN/futures/multipleChart', '/zh-CN/futures/multi-symbols', `${tradingPath}/calculator`]) { + await page.evaluate(path => history.pushState({}, '', path), path); + await page.clock.runFor(1000); + // Then the off-route panel is removed even though the old orderbook DOM remains. + await expect(panel).toHaveCount(0); + } + + // When the user returns to the original supported contract. + await page.evaluate(path => history.pushState({}, '', path), tradingPath); + await page.clock.runFor(1000); + + // Then one panel resumes without any order placement or cancellation. + await expect(panel).toBeVisible(); + await expect(panel).toHaveCount(1); + expect((await readFixtureState(page)).events.filter(event => ['order-submitted', 'cancel-requested'].includes(event.type))).toEqual([]); + expect(host.errors).toEqual([]); +}); diff --git a/scripts/binance-coinmarketcap-data.user.js b/scripts/binance-coinmarketcap-data.user.js index a962901..6fc2f47 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.17 +// @version 0.1.18 // @author jackhai9 // @description 在 Binance 合约页面显示当前币种的 CoinMarketCap 中文页关键估值与供应量数据 // @match https://www.binance.com/*/futures/* @@ -25,7 +25,7 @@ // src/shared/binance-futures-route.js var FUTURES_TRADING_PATH_RE = /^\/(?:[a-z]{2}(?:-[A-Za-z]{2})?\/)?futures\/([^/]+)\/?$/; - var TRADING_SYMBOL_RE = new RegExp(`^[${BINANCE_SYMBOL_CHARACTERS}]{3,}$`, "u"); + var TRADING_SYMBOL_RE = new RegExp(`^[${BINANCE_SYMBOL_CHARACTERS}]+(?:USDT|USDC)$`, "iu"); function parseFuturesTradingSymbolFromPathname(pathname) { const normalized = String(pathname || "").split(/[?#]/, 1)[0]; const match = normalized.match(FUTURES_TRADING_PATH_RE); diff --git a/scripts/binance-orderbook-trade.user.js b/scripts/binance-orderbook-trade.user.js index a199958..87fabe8 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.215 +// @version 2.7.216 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -1966,7 +1966,7 @@ // src/shared/binance-futures-route.js var FUTURES_TRADING_PATH_RE = /^\/(?:[a-z]{2}(?:-[A-Za-z]{2})?\/)?futures\/([^/]+)\/?$/; - var TRADING_SYMBOL_RE = new RegExp(`^[${BINANCE_SYMBOL_CHARACTERS}]{3,}$`, "u"); + var TRADING_SYMBOL_RE = new RegExp(`^[${BINANCE_SYMBOL_CHARACTERS}]+(?:USDT|USDC)$`, "iu"); function parseFuturesTradingSymbolFromPathname(pathname) { const normalized = String(pathname || "").split(/[?#]/, 1)[0]; const match = normalized.match(FUTURES_TRADING_PATH_RE); @@ -5131,12 +5131,19 @@ } // src/binance-orderbook-trade/index.user.js - (function() { + (function initializeOrderbookTrade() { "use strict"; function isFuturesTradingPage() { return isFuturesTradingPathname(location.pathname); } - if (!isFuturesTradingPage()) return; + if (!isFuturesTradingPage()) { + const stopWaitingForTradingRoute = installSpaRouteChangeListener(window, () => { + if (!isFuturesTradingPage()) return; + stopWaitingForTradingRoute(); + initializeOrderbookTrade(); + }); + return; + } const nativeDepthSource = installBinanceNativeDepthSource(window); const CFG = { // true=只填数量;false=填数量并自动点“开多/开空/平多/平空” diff --git a/scripts/binance-strategy27-events.user.js b/scripts/binance-strategy27-events.user.js index 5dc7378..b5f3202 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.6 +// @version 0.6.7 // @author jackhai9 // @description Display Strategy 27 events and provide the shared private CorsairQuant gateway connection // @match https://www.binance.com/*/futures/* @@ -2967,7 +2967,7 @@ ${t("候选", "Candidate")} ${annotation.candidateId}`, // src/shared/binance-futures-route.js var FUTURES_TRADING_PATH_RE = /^\/(?:[a-z]{2}(?:-[A-Za-z]{2})?\/)?futures\/([^/]+)\/?$/; - var TRADING_SYMBOL_RE = new RegExp(`^[${BINANCE_SYMBOL_CHARACTERS}]{3,}$`, "u"); + var TRADING_SYMBOL_RE = new RegExp(`^[${BINANCE_SYMBOL_CHARACTERS}]+(?:USDT|USDC)$`, "iu"); 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 deb62c9..b7580b8 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.6 +// @version 0.5.7 // @author jackhai9 // @description Native Bollinger/SMA60 markers and the default read-only cross-timeframe summary // @match https://www.binance.com/*/futures/* @@ -1323,7 +1323,7 @@ // src/shared/binance-futures-route.js var FUTURES_TRADING_PATH_RE = /^\/(?:[a-z]{2}(?:-[A-Za-z]{2})?\/)?futures\/([^/]+)\/?$/; - var TRADING_SYMBOL_RE = new RegExp(`^[${BINANCE_SYMBOL_CHARACTERS}]{3,}$`, "u"); + var TRADING_SYMBOL_RE = new RegExp(`^[${BINANCE_SYMBOL_CHARACTERS}]+(?:USDT|USDC)$`, "iu"); function parseFuturesTradingSymbolFromPathname(pathname) { const normalized = String(pathname || "").split(/[?#]/, 1)[0]; const match = normalized.match(FUTURES_TRADING_PATH_RE); diff --git a/scripts/binance-strategy31-volume-reversal.user.js b/scripts/binance-strategy31-volume-reversal.user.js index e7472d2..fdd79b6 100644 --- a/scripts/binance-strategy31-volume-reversal.user.js +++ b/scripts/binance-strategy31-volume-reversal.user.js @@ -3,7 +3,7 @@ // @namespace binance.strategy31.volume-reversal // @icon data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E // @icon64 data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E -// @version 0.1.0 +// @version 0.1.1 // @author jackhai9 // @description Confirmed red-to-green volume signals from CorsairQuant // @match https://www.binance.com/*/futures/* @@ -36,7 +36,7 @@ // src/shared/binance-futures-route.js var FUTURES_TRADING_PATH_RE = /^\/(?:[a-z]{2}(?:-[A-Za-z]{2})?\/)?futures\/([^/]+)\/?$/; - var TRADING_SYMBOL_RE = new RegExp(`^[${BINANCE_SYMBOL_CHARACTERS}]{3,}$`, "u"); + var TRADING_SYMBOL_RE = new RegExp(`^[${BINANCE_SYMBOL_CHARACTERS}]+(?:USDT|USDC)$`, "iu"); function parseFuturesTradingSymbolFromPathname(pathname) { const normalized = String(pathname || "").split(/[?#]/, 1)[0]; const match = normalized.match(FUTURES_TRADING_PATH_RE); @@ -52,6 +52,52 @@ return symbolMatch && symbolMatch[0] === symbol ? symbol.toUpperCase() : null; } + // src/shared/spa-route-change.js + var ROUTE_CHANGE_EVENT = "jh-userscripts:spa-route-change"; + var ROUTE_PATCH_MARKER = Symbol.for("jh-userscripts.spa-route-change-patched"); + var ROUTE_DISPATCH_STATE = Symbol.for("jh-userscripts.spa-route-change-dispatch"); + function dispatchRouteChange(view) { + const href = view.location.href; + if (view[ROUTE_DISPATCH_STATE]?.href === href) return; + const state = { href }; + view[ROUTE_DISPATCH_STATE] = state; + view.dispatchEvent(new view.Event(ROUTE_CHANGE_EVENT)); + view.queueMicrotask(() => { + if (view[ROUTE_DISPATCH_STATE] === state) delete view[ROUTE_DISPATCH_STATE]; + }); + } + function patchHistoryMethod(view, methodName) { + const current = view.history[methodName]; + if (current[ROUTE_PATCH_MARKER]) return; + function routeAwareHistoryMethod(...args) { + const previousHref = view.location.href; + const result = Reflect.apply(current, this, args); + if (view.location.href !== previousHref) dispatchRouteChange(view); + return result; + } + Object.defineProperty(routeAwareHistoryMethod, ROUTE_PATCH_MARKER, { value: true }); + view.history[methodName] = routeAwareHistoryMethod; + } + function ensureSpaRouteChangePatched(view) { + if (!view?.history) throw new Error("SPA route patch requires a window"); + patchHistoryMethod(view, "pushState"); + patchHistoryMethod(view, "replaceState"); + } + function installSpaRouteChangeListener(view, listener) { + if (!view?.history || typeof listener !== "function") { + throw new Error("SPA route listener requires a window and callback"); + } + ensureSpaRouteChangePatched(view); + view.addEventListener(ROUTE_CHANGE_EVENT, listener); + view.addEventListener("popstate", listener); + view.addEventListener("hashchange", listener); + return () => { + view.removeEventListener(ROUTE_CHANGE_EVENT, listener); + view.removeEventListener("popstate", listener); + view.removeEventListener("hashchange", listener); + }; + } + // src/shared/chart-mutation-owners.js var OWNER_SLOT = Symbol.for("jh-userscripts.chart-mutation-owners"); var VERSION = 1; @@ -806,7 +852,7 @@ const retired = /* @__PURE__ */ new Set(); const document = view.document; function notice(text) { - if (!document.body) return; + if (!document.body || !parseFuturesTradingSymbolFromPathname(view.location.pathname)) return; let node = document.getElementById("jh-strategy31-status"); if (!node) { node = document.createElement("div"); @@ -839,9 +885,16 @@ } async function sample() { cleanup(); - if (disposed || failed || document.hidden || inflight) return; + if (disposed) return; const route = parseFuturesTradingSymbolFromPathname(view.location.pathname); - if (!route?.endsWith("USDT")) { + if (!route) { + releaseChart(); + cleanup(); + document.getElementById("jh-strategy31-status")?.remove(); + return; + } + if (failed || document.hidden || inflight) return; + if (!route.endsWith("USDT")) { releaseChart(); cleanup(); notice("Strategy31: unsupported market"); @@ -850,7 +903,7 @@ const symbol = usdtRouteToCanonical(route); const base = findBinanceTradingViewTarget(document); const chart = base?.tradingViewApi.activeChart?.(); - if (!route || !chart?.hasModel() || String(chart.symbol()).split("@")[0] !== route) { + if (!chart?.hasModel() || String(chart.symbol()).split("@")[0] !== route) { releaseChart(); cleanup(); return; @@ -943,10 +996,14 @@ void tick(); }, 5e3); document.addEventListener("visibilitychange", visibility); + const removeRouteListener = installSpaRouteChangeListener(view, () => { + if (!parseFuturesTradingSymbolFromPathname(view.location.pathname)) void tick(); + }); const runtime = Object.freeze({ sample: tick, dispose() { disposed = true; releaseChart(); cleanup(); + removeRouteListener(); document.removeEventListener("visibilitychange", visibility); document.getElementById("jh-strategy31-status")?.remove(); } }); diff --git a/scripts/binance-trading-data.user.js b/scripts/binance-trading-data.user.js index ad423bf..de11690 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.16 +// @version 1.1.17 // @author jackhai9 // @description 在合约交易页面叠加浮动面板,定时拉取交易数据(持仓量、多空比、资金费率等)并显示当前值 + 多空信号 // @match https://www.binance.com/*/futures/* @@ -22,7 +22,7 @@ // src/shared/binance-futures-route.js var FUTURES_TRADING_PATH_RE = /^\/(?:[a-z]{2}(?:-[A-Za-z]{2})?\/)?futures\/([^/]+)\/?$/; - var TRADING_SYMBOL_RE = new RegExp(`^[${BINANCE_SYMBOL_CHARACTERS}]{3,}$`, "u"); + var TRADING_SYMBOL_RE = new RegExp(`^[${BINANCE_SYMBOL_CHARACTERS}]+(?:USDT|USDC)$`, "iu"); 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 a232af5..a997125 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.17 +// @version 0.1.18 // @author jackhai9 // @description 在 Binance 合约页面显示当前币种的 CoinMarketCap 中文页关键估值与供应量数据 // @match https://www.binance.com/*/futures/* diff --git a/src/binance-orderbook-trade/index.user.js b/src/binance-orderbook-trade/index.user.js index 20f74f6..2711b7c 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.215 +// @version 2.7.216 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -252,14 +252,22 @@ import { } from './dom/chart-orders.js'; import { showUsdtRebalanceDialog } from './dom/usdt-rebalance-dialog.js'; -(function () { +(function initializeOrderbookTrade() { 'use strict'; function isFuturesTradingPage() { return isFuturesTradingPathname(location.pathname); } - if (!isFuturesTradingPage()) return; + if (!isFuturesTradingPage()) { + // Defer trading initialization while retaining SPA entry from the futures landing pages. + const stopWaitingForTradingRoute = installSpaRouteChangeListener(window, () => { + if (!isFuturesTradingPage()) return; + stopWaitingForTradingRoute(); + initializeOrderbookTrade(); + }); + return; + } const nativeDepthSource = installBinanceNativeDepthSource(window); diff --git a/src/binance-strategy27-events/index.user.js b/src/binance-strategy27-events/index.user.js index 37a568e..48434ee 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.6 +// @version 0.6.7 // @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 f7a945b..6a799e2 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.6 +// @version 0.5.7 // @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-strategy31-volume-reversal/index.user.js b/src/binance-strategy31-volume-reversal/index.user.js index 87462fa..5df1920 100644 --- a/src/binance-strategy31-volume-reversal/index.user.js +++ b/src/binance-strategy31-volume-reversal/index.user.js @@ -3,7 +3,7 @@ // @namespace binance.strategy31.volume-reversal // @icon data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E // @icon64 data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E -// @version 0.1.0 +// @version 0.1.1 // @author jackhai9 // @description Confirmed red-to-green volume signals from CorsairQuant // @match https://www.binance.com/*/futures/* diff --git a/src/binance-strategy31-volume-reversal/runtime.js b/src/binance-strategy31-volume-reversal/runtime.js index 82d87e0..6d66b54 100644 --- a/src/binance-strategy31-volume-reversal/runtime.js +++ b/src/binance-strategy31-volume-reversal/runtime.js @@ -1,5 +1,6 @@ import { SIGNAL_GATEWAY_BRIDGE } from '../shared/signal-gateway-bridge.js'; import { parseFuturesTradingSymbolFromPathname } from '../shared/binance-futures-route.js'; +import { installSpaRouteChangeListener } from '../shared/spa-route-change.js'; import { isChartMutationBlocked } from '../shared/chart-mutation-owners.js'; import { findBinanceTradingViewTarget } from '../shared/tradingview-target.js'; import { usdtRouteToCanonical } from '../shared/canonical-symbol.js'; @@ -16,7 +17,7 @@ export function installStrategy31(view) { const retired = new Set(); const document = view.document; function notice(text) { - if (!document.body) return; + if (!document.body || !parseFuturesTradingSymbolFromPathname(view.location.pathname)) return; let node = document.getElementById('jh-strategy31-status'); if (!node) { node = document.createElement('div'); @@ -50,13 +51,20 @@ export function installStrategy31(view) { } async function sample() { cleanup(); - if (disposed || failed || document.hidden || inflight) return; + if (disposed) return; const route = parseFuturesTradingSymbolFromPathname(view.location.pathname); - if (!route?.endsWith('USDT')) { releaseChart(); cleanup(); notice('Strategy31: unsupported market'); return; } + // Leaving the chart must retire pending work before request or visibility gates can defer cleanup. + if (!route) { + releaseChart(); cleanup(); + document.getElementById('jh-strategy31-status')?.remove(); + return; + } + if (failed || document.hidden || inflight) return; + if (!route.endsWith('USDT')) { releaseChart(); cleanup(); notice('Strategy31: unsupported market'); return; } const symbol = usdtRouteToCanonical(route); const base = findBinanceTradingViewTarget(document); const chart = base?.tradingViewApi.activeChart?.(); - if (!route || !chart?.hasModel() || String(chart.symbol()).split('@')[0] !== route) { releaseChart(); cleanup(); return; } + if (!chart?.hasModel() || String(chart.symbol()).split('@')[0] !== route) { releaseChart(); cleanup(); return; } // Keep the readiness subscription across interval changes: dataReady can still describe old bars. if (!intervalOwner || intervalOwner.chart !== chart || intervalOwner.route !== route) { releaseChart(); @@ -117,8 +125,13 @@ export function installStrategy31(view) { function visibility() { if (document.hidden) retire(); else void tick(); } const timer = view.setInterval(() => { void tick(); }, 5000); document.addEventListener('visibilitychange', visibility); + // Route cleanup must remain active after invalid contracts stop the sampling timer. + const removeRouteListener = installSpaRouteChangeListener(view, () => { + if (!parseFuturesTradingSymbolFromPathname(view.location.pathname)) void tick(); + }); const runtime = Object.freeze({ sample: tick, dispose() { disposed = true; releaseChart(); cleanup(); + removeRouteListener(); document.removeEventListener('visibilitychange', visibility); document.getElementById('jh-strategy31-status')?.remove(); } }); diff --git a/src/binance-trading-data/index.user.js b/src/binance-trading-data/index.user.js index c516b7c..cbce655 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.16 +// @version 1.1.17 // @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 adbf468..3b833ca 100644 --- a/src/shared/binance-futures-route.js +++ b/src/shared/binance-futures-route.js @@ -1,8 +1,9 @@ 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 = new RegExp(`^[${BINANCE_SYMBOL_CHARACTERS}]{3,}$`, 'u'); +const TRADING_SYMBOL_RE = new RegExp(`^[${BINANCE_SYMBOL_CHARACTERS}]+(?:USDT|USDC)$`, 'iu'); +/** Match the scripts' supported perpetual routes, not arbitrary futures subpages. */ export function parseFuturesTradingSymbolFromPathname(pathname) { const normalized = String(pathname || '').split(/[?#]/, 1)[0]; const match = normalized.match(FUTURES_TRADING_PATH_RE); diff --git a/test/dom/binance-strategy27-events/strategy27-entrypoint.test.js b/test/dom/binance-strategy27-events/strategy27-entrypoint.test.js index d3f5c77..422d728 100644 --- a/test/dom/binance-strategy27-events/strategy27-entrypoint.test.js +++ b/test/dom/binance-strategy27-events/strategy27-entrypoint.test.js @@ -119,6 +119,19 @@ async function harness(t, { generated = false, beforeCreate, locale = 'zh-CN', m }; } +for (const generated of [false, true]) { + test(`user gets no Strategy27 panel or requests on home from the ${generated ? 'generated' : 'source'} entrypoint`, async t => { + // Given a non-trading home route still contains stale native chart markup + const h = await harness(t, { generated, routeSymbol: 'home' }); + // When retained route observation samples the page again + h.tick(); + // Then neither strategy status nor clients nor drawings are created + assert.equal(h.page.document.getElementById('jh-strategy27-event-status'), null); + assert.equal(h.requests.length, 0); + assert.deepEqual([...h.shapes.keys()], ['user-owned']); + }); +} + test('user observes that real entrypoint starts independent clients and manual clear preserves compound replay identity', async (t) => { // Given the Binance page, gateway requests and Strategy 27 installation const h = await harness(t); diff --git a/test/dom/binance-strategy29-bollinger/summary-runtime-boundaries.test.js b/test/dom/binance-strategy29-bollinger/summary-runtime-boundaries.test.js index 2b45205..54fe907 100644 --- a/test/dom/binance-strategy29-bollinger/summary-runtime-boundaries.test.js +++ b/test/dom/binance-strategy29-bollinger/summary-runtime-boundaries.test.js @@ -133,7 +133,7 @@ test('user removes the remote panel after leaving futures and rejects a late nat const oldSignal = fixture.gateway.requests.at(-1).signal; // When browser navigation leaves futures before the response completes. - fixture.host.view.history.pushState({}, '', '/en/markets'); + fixture.host.view.history.pushState({}, '', '/en/futures/home'); await fixture.remote.sample(status.observed_at_ms + 10_000); late.respond({ ...events, events: [{ ...events.events[0], sequence: 80, event_id: 'd'.repeat(64) }], next_cursor: 80 }); await pending; diff --git a/test/dom/strategy31-runtime.test.js b/test/dom/strategy31-runtime.test.js index feb61f7..25f62d5 100644 --- a/test/dom/strategy31-runtime.test.js +++ b/test/dom/strategy31-runtime.test.js @@ -5,26 +5,174 @@ import { installStrategy31 } from '../../src/binance-strategy31-volume-reversal/ import { SIGNAL_GATEWAY_BRIDGE } from '../../src/shared/signal-gateway-bridge.js'; import { registerChartMutationOwner } from '../../src/shared/chart-mutation-owners.js'; -function fixture() { +function fixture({ pathname = '/en/futures/BTRUSDT', hiddenDuringInstall = true } = {}) { const host = createStrategy29ChartHost({ resolution: '5', bars: [ { time: 300, open: 10, high: 13, low: 9, close: 12 }, ] }); - host.dom.reconfigure({ url: 'https://www.binance.com/en/futures/BTRUSDT' }); + host.dom.reconfigure({ url: `https://www.binance.com${pathname}` }); const symbol = 'BTR/USDT:USDT'; const event = { id: `31_2_spec_v1:${symbol}:5m:300000`, symbol, timeframe: '5m', bar_open_ms: 300000, bar_close_ms: 600000, open: 10, high: 13, low: 9, close: 12, volume: 101, previous_volume: 100 }; const payload = { schema_version: 1, strategy_id: '31', spec_version: '31_2_spec_v1', symbol, timeframe: '5m', observed_at_ms: 600000, events: [event] }; let nextResponse = null, revision = 0; + const requests = []; host.view[SIGNAL_GATEWAY_BRIDGE] = { version: 1, capabilities: ['strategy31'], getState: () => ({ available: true, configured: true, settingsRevision: revision }), - request: () => nextResponse ?? Promise.resolve({ kind: 'response', status: 200, responseText: JSON.stringify(payload) }) }; - host.setHidden(true); + request: (path, signal) => { + requests.push({ path, signal }); + return nextResponse ?? Promise.resolve({ kind: 'response', status: 200, responseText: JSON.stringify(payload) }); + } }; + host.setHidden(hiddenDuringInstall); const runtime = installStrategy31(host.view); host.setHidden(false); - return { ...host, runtime, hold(promise) { nextResponse = promise; }, changeSettings() { revision += 1; }, payload }; + return { ...host, runtime, requests, hold(promise) { nextResponse = promise; }, changeSettings() { revision += 1; }, payload }; } +test('user sees no Strategy31 status or signal requests on a non-trading page', async (t) => { + // Given the observer starts on a visible futures landing page + const f = fixture({ pathname: '/en/futures/', hiddenDuringInstall: false }); + t.after(() => { f.runtime.dispose(); f.close(); }); + // When the observer samples the non-trading route again + await f.runtime.sample(); + // Then it stays absent without acquiring the native chart or requesting signals + assert.equal(f.document.getElementById('jh-strategy31-status'), null); + assert.equal(f.requests.length, 0); + assert.equal(f.shapes.size, 0); + assert.equal(f.intervalChanged.size, 0); + assert.equal(f.dataLoaded.size, 0); +}); + +test('user clears a stopped Strategy31 notice after SPA navigation without restarting signals', async (t) => { + // Given invalid server data has stopped the observer and exposed its failure status + const f = fixture(); + t.after(() => { f.runtime.dispose(); f.close(); }); + f.payload.schema_version = 0; + await f.runtime.sample(); + assert.equal(f.document.getElementById('jh-strategy31-status').textContent, 'Strategy31 stopped: invalid chart or signal data'); + assert.equal(f.requests.length, 1); + // When SPA navigation leaves the trading page after the sampling timer has stopped + f.view.history.pushState({}, '', '/en/futures/'); + // Then route observation removes the failure status without another sample or request + assert.equal(f.document.getElementById('jh-strategy31-status'), null); + assert.equal(f.shapes.size, 0); + assert.equal(f.requests.length, 1); + // When valid data becomes available and navigation returns to a supported chart + f.payload.schema_version = 1; + f.view.history.pushState({}, '', '/en/futures/BTRUSDT'); + await f.runtime.sample(); + // Then the failed observer remains stopped instead of implicitly retrying + assert.equal(f.requests.length, 1); + assert.equal(f.shapes.size, 0); +}); + +test('user sees no late failure notice when native chart export fails after leaving the trading page', async (t) => { + // Given a native candle export remains pending after a valid signal response + const f = fixture(); + t.after(() => { f.runtime.dispose(); f.close(); }); + await f.runtime.sample(); + const gate = f.holdNextExport(); + const sample = f.runtime.sample(); + await gate.entered; + // When navigation leaves the chart before the native export rejects + f.view.history.pushState({}, '', '/en/futures/'); + gate.reject(new TypeError('Invalid native chart export')); + await sample; + // Then the late failure keeps all Strategy31 UI absent on the landing page + assert.equal(f.document.getElementById('jh-strategy31-status'), null); + assert.equal(f.shapes.size, 0); + assert.equal(f.requests.length, 2); + // When navigation returns to the supported trading page + f.view.history.pushState({}, '', '/en/futures/BTRUSDT'); + await f.runtime.sample(); + // Then the invalid native contract has stopped further signal requests + assert.equal(f.requests.length, 2); + assert.equal(f.shapes.size, 0); +}); + +test('user clears Strategy31 from the futures landing page and resumes on a trading page', async (t) => { + // Given one native signal and a visible observer status on a supported market + const f = fixture(); + t.after(() => { f.runtime.dispose(); f.close(); }); + await f.runtime.sample(); + assert.equal(f.shapes.size, 1); + assert.equal(f.document.getElementById('jh-strategy31-status').textContent, 'Strategy31: 1 chart signals · 5m'); + // When navigation leaves the trading route while its native chart remains mounted + f.dom.reconfigure({ url: 'https://www.binance.com/en/futures/' }); + await f.runtime.sample(); + // Then the observer clears its UI and subscriptions without another gateway request + assert.equal(f.document.getElementById('jh-strategy31-status'), null); + assert.equal(f.shapes.size, 0); + assert.equal(f.requests.length, 1); + assert.equal(f.intervalChanged.size, 0); + assert.equal(f.dataLoaded.size, 0); + // When navigation returns to the supported trading route + f.dom.reconfigure({ url: 'https://www.binance.com/en/futures/BTRUSDT' }); + await f.runtime.sample(); + // Then the observer acquires fresh signals and restores its native arrow + assert.equal(f.requests.length, 2); + assert.equal(f.shapes.size, 1); + assert.equal(f.document.getElementById('jh-strategy31-status').textContent, 'Strategy31: 1 chart signals · 5m'); +}); + +test('user aborts pending signals on leaving a trading page without late UI resurrection', async (t) => { + // Given an existing arrow and a second gateway response still pending + const f = fixture(); + t.after(() => { f.runtime.dispose(); f.close(); }); + await f.runtime.sample(); + const response = Promise.withResolvers(); + f.hold(response.promise); + const sample = f.runtime.sample(); + assert.equal(f.requests.length, 2); + assert.equal(f.requests[1].signal.aborted, false); + // When the observer samples navigation away before the response completes + f.dom.reconfigure({ url: 'https://www.binance.com/en/futures/' }); + await f.runtime.sample(); + // Then the pending transport is cancelled and previous UI is removed immediately + assert.equal(f.requests[1].signal.aborted, true); + assert.equal(f.requests.length, 2); + assert.equal(f.shapes.size, 0); + assert.equal(f.document.getElementById('jh-strategy31-status'), null); + // When the cancelled request still delivers its old response + response.resolve({ kind: 'response', status: 200, responseText: JSON.stringify(f.payload) }); + await sample; + // Then neither drawings nor status return on the non-trading route + assert.equal(f.shapes.size, 0); + assert.equal(f.created.length, 1); + assert.equal(f.document.getElementById('jh-strategy31-status'), null); + // When the user returns to a supported route after the old request finishes + f.hold(null); + f.dom.reconfigure({ url: 'https://www.binance.com/en/futures/BTRUSDT' }); + await f.runtime.sample(); + // Then a new request restores observation without restarting the script + assert.equal(f.requests.length, 3); + assert.equal(f.requests[2].signal.aborted, false); + assert.equal(f.shapes.size, 1); + assert.equal(f.document.getElementById('jh-strategy31-status').textContent, 'Strategy31: 1 chart signals · 5m'); +}); + +test('user clears the landing-page status while native arrow cleanup waits for the chart owner', async (t) => { + // Given an existing signal and another script holding chart mutation ownership + const f = fixture(); + t.after(() => { f.runtime.dispose(); f.close(); }); + await f.runtime.sample(); + const release = registerChartMutationOwner(f.view, 'fixture-trading-owner', () => true); + t.after(release); + // When navigation leaves the trading route before ownership is released + f.dom.reconfigure({ url: 'https://www.binance.com/en/futures/' }); + await f.runtime.sample(); + // Then status disappears while the observer waits to mutate the native chart + assert.equal(f.document.getElementById('jh-strategy31-status'), null); + assert.equal(f.shapes.size, 1); + assert.equal(f.requests.length, 1); + // When the chart owner releases its mutation reservation + release(); + await f.runtime.sample(); + // Then deferred cleanup removes the observer arrow without a new request + assert.equal(f.shapes.size, 0); + assert.equal(f.requests.length, 1); +}); + test('user sees one native green arrow across repeated server snapshots', async () => { // Given the existing native-chart fixture and one confirmed server event const f = fixture(); @@ -112,6 +260,8 @@ test('user sees no USDT signals on a USDC chart and can resume a supported marke // Then no USDT event is drawn on the unsupported market assert.equal(f.created.length, 0); assert.equal(f.exports.length, 0); + assert.equal(f.requests.length, 0); + assert.equal(f.document.getElementById('jh-strategy31-status').textContent, 'Strategy31: unsupported market'); f.dom.reconfigure({ url: 'https://www.binance.com/en/futures/BTRUSDT' }); f.setSymbol('BTRUSDT@PRICETYPE=LAST'); await f.runtime.sample(); diff --git a/test/unit/binance-data-panel-route-regressions.test.js b/test/unit/binance-data-panel-route-regressions.test.js index 21d65c5..9643510 100644 --- a/test/unit/binance-data-panel-route-regressions.test.js +++ b/test/unit/binance-data-panel-route-regressions.test.js @@ -1,9 +1,46 @@ import { readFile } from 'node:fs/promises'; import test from 'node:test'; import assert from 'node:assert/strict'; -import { createDataPanelHost, activateTradingData, completeCmcData, cmcDetail, tradingDataset } from '../helpers/data-media-migration-host.js'; +import { createDataPanelHost, activateTradingData, completeCmcData, cmcDetail, tradingDataset, afterDataMediaResponseTurn } from '../helpers/data-media-migration-host.js'; for (const kind of ['cmc', 'trading']) { + for (const path of ['/zh-CN/futures/', '/zh-CN/futures/home', '/futures/quiz', '/futures/multipleChart', '/futures/multi-symbols', '/futures/USUSDT/calculator']) { + test(`user gets no ${kind} panel or requests on ${path}`, async t => { + // Given a non-trading route covered by the broad installation metadata + const host = createDataPanelHost(t, kind, { path }); + // When the generated script starts and business deadlines pass + await host.start(); + host.clock.tick(3_600_000); + await afterDataMediaResponseTurn(); + // Then neither presentation nor background data requests start + assert.equal(host.panel(), null); + assert.equal(host.network.requests.length, 0); + }); + } + + test(`user activates the ${kind} panel from home and removes it on returning home`, async t => { + // Given a visible futures home page with no active business loop + const host = createDataPanelHost(t, kind, { path: '/zh-CN/futures/home' }); + await host.start(); + assert.equal(host.panel(), null); + assert.equal(host.network.requests.length, 0); + // When SPA navigation enters a supported contract and its first refresh completes + host.navigate('/zh-CN/futures/USUSDT'); + if (kind === 'trading') await activateTradingData(host, tradingDataset(Date.now()), { symbol: 'USUSDT' }); + else await completeCmcData(host, cmcDetail({ id: 42, symbol: 'US' }), { symbol: 'US', slug: 'us' }); + // Then exactly one panel owns the current contract + assert.equal(host.document.querySelectorAll(`#${host.panelId}`).length, 1); + assert.equal(host.element('symbol').textContent, kind === 'trading' ? 'USUSDT' : 'US #1'); + // When returning home crosses all business refresh deadlines + host.navigate('/zh-CN/futures/home'); + const requestCount = host.network.requests.length; + host.clock.tick(3_600_000); + await afterDataMediaResponseTurn(); + // Then the panel is removed and no business requests continue + assert.equal(host.panel(), null); + assert.equal(host.network.requests.length, requestCount); + }); + test(`user excludes wallet futures pages when installing the ${kind} data panel`, async () => { // Given the script is installed through its public generated artifact const artifact = new URL(`../../scripts/binance-${kind === 'trading' ? 'trading' : 'coinmarketcap'}-data.user.js`, import.meta.url); diff --git a/test/unit/binance-orderbook-trade/route.test.js b/test/unit/binance-orderbook-trade/route.test.js index 2141396..86af460 100644 --- a/test/unit/binance-orderbook-trade/route.test.js +++ b/test/unit/binance-orderbook-trade/route.test.js @@ -60,3 +60,20 @@ test("user parses Unicode futures symbols from actual URL pathnames", () => { assert.deepEqual(encoded, symbols); assert.deepEqual(invalid, [null, null, null, null]); }); + +test('user activates scripts only for supported perpetual contract routes', () => { + // Given supported contract identities and non-trading or unsupported routes + const accepted = ['USUSDT', 'HOMEUSDT', 'BTCUSDC', '4USDT', 'WUSDC', '𐐀USDT', 'A_BUSDT']; + const rejected = [ + '', 'home', 'HOME', 'quiz', 'multipleChart', 'multi-symbols', 'funding-history', + 'USDT', 'USDC', 'BTC', '币安人生', 'BTCUSD_PERP', 'BTCUSDT_261225', + 'BTCUSD1', 'BTCU', 'ETHBTC', 'BTCBUSD', 'USUSDT/calculator', + 'multi-symbols/USUSDT', + ]; + // When localized and unlocalized paths pass through the shared route contract + const valid = accepted.map(symbol => parseFuturesTradingSymbolFromPathname(`/zh-CN/futures/${symbol}/`)); + const invalid = rejected.map(segment => isFuturesTradingPathname(`/futures/${segment}`)); + // Then supported identities survive and every other path remains inactive + assert.deepEqual(valid, accepted); + assert.deepEqual(invalid, rejected.map(() => false)); +}); diff --git a/test/unit/binance-strategy29-bollinger/remote-summary-controller.test.js b/test/unit/binance-strategy29-bollinger/remote-summary-controller.test.js index dcdaa31..b589af8 100644 --- a/test/unit/binance-strategy29-bollinger/remote-summary-controller.test.js +++ b/test/unit/binance-strategy29-bollinger/remote-summary-controller.test.js @@ -225,7 +225,7 @@ test('user observes that unsupported futures route is classified once without a const f = fixture(); let warnings = 0; f.view.console.warn = () => { warnings += 1; }; - f.view.location.pathname = '/en/futures/BTCUSD_PERP'; + f.view.location.pathname = '/en/futures/BTCUSDC'; // When f.summary.sample processes the configured inputs await f.summary.sample(0); await f.summary.sample(1_000); diff --git a/test/unit/binance-strategy29-bollinger/source-regressions.test.js b/test/unit/binance-strategy29-bollinger/source-regressions.test.js index a02d02b..ba3169c 100644 --- a/test/unit/binance-strategy29-bollinger/source-regressions.test.js +++ b/test/unit/binance-strategy29-bollinger/source-regressions.test.js @@ -19,7 +19,7 @@ test('user installs Strategy29 with only the reviewed sandbox capabilities', asy const [entrySource, remoteSource] = await Promise.all(paths.map(path => readFile(path, 'utf8'))); // Then metadata and page coordination keep the exact reviewed grants and transport boundary. - assert.equal(readUserscriptVersion(entrySource), '0.5.6'); + assert.equal(readUserscriptVersion(entrySource), '0.5.7'); 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 index b207de2..9d7720d 100644 --- a/test/unit/binance-symbol.test.js +++ b/test/unit/binance-symbol.test.js @@ -21,14 +21,13 @@ test('user observes that Binance identifiers support Unicode letters, numbers, a } }); -test('user observes that route length remains a Unicode code-point contract', () => { - // Given the supplied input retains its original contract values - const scenarioInput = '/futures/𐐀𐐀'; - // When the real operation processes that input - const observed = parseFuturesTradingSymbolFromPathname(scenarioInput); - // Then route length remains a Unicode code-point contract - assert.equal(observed, null); - assert.equal(parseFuturesTradingSymbolFromPathname('/futures/𐐀𐐀𐐀'), '𐐀𐐀𐐀'); +test('user needs a supported quote suffix even for Unicode asset routes', () => { + // Given Unicode assets with and without a perpetual quote suffix + const paths = ['/futures/𐐀𐐀', '/futures/𐐀𐐀𐐀', '/futures/𐐀USDT', '/futures/𐐀USDC']; + // When the shared route parser evaluates each complete pathname + const observed = paths.map(parseFuturesTradingSymbolFromPathname); + // Then only complete supported contracts activate scripts + assert.deepEqual(observed, [null, null, '𐐀USDT', '𐐀USDC']); }); test('user observes that shared gateway symbol conversion preserves Chinese and numeric asset identity', () => { diff --git a/test/unit/userscript-release-contract.test.js b/test/unit/userscript-release-contract.test.js index 82bb7e1..27a075e 100644 --- a/test/unit/userscript-release-contract.test.js +++ b/test/unit/userscript-release-contract.test.js @@ -24,7 +24,7 @@ test('user installs Strategy29 with its independent observation-only identity', // Then the installer retains its pinned identity and declared observation boundary. assert.equal(contract.name, '【自写】Binance Strategy 29 布林带信号'); assert.equal(contract.namespace, 'binance.strategy29.bollinger'); - assert.equal(contract.version, '0.5.6'); + assert.equal(contract.version, '0.5.7'); assert.equal(contract.runAt, 'document-start'); assert.equal(contract.updateURL, 'https://raw.githubusercontent.com/jackhai9/userscripts/main/scripts/binance-strategy29-bollinger.user.js'); assert.equal(contract.downloadURL, contract.updateURL); @@ -70,7 +70,7 @@ test('user identifies Strategy27 and its declared local gateway capabilities', ( // Then the installer retains its identity and exact gateway capability boundary. assert.equal(contract.name, '【自写】Binance Strategy 27 事件标注'); assert.equal(contract.namespace, 'binance.strategy27.events'); - assert.equal(contract.version, '0.6.6'); + assert.equal(contract.version, '0.6.7'); assert.equal(contract.runAt, 'document-idle'); assert.equal(contract.updateURL, contract.downloadURL); assert.deepEqual(contract.matches, [