diff --git a/docs/binance-strategy27-events-development.md b/docs/binance-strategy27-events-development.md index 5d790a8..3bdc668 100644 --- a/docs/binance-strategy27-events-development.md +++ b/docs/binance-strategy27-events-development.md @@ -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.5.0 with identical metadata headers. +generated install artifact are version 0.5.1 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 @@ -345,3 +345,15 @@ The English detail column wraps long labels without overlapping fact values. Source/generated entrypoint tests cover language-only switching without new requests or entity removal. The isolated panel preview covers both languages; it does not certify the production gateway or a live compound sample. + + +## Canonical Unicode symbols + +The client and Strategy27 server accept exact `BASE/USDT:USDT` symbols whose +nonempty base consists of Unicode letters or numbers (categories L and N) and +is unchanged by Unicode uppercasing. This includes Binance's Chinese asset names. +Wire symbols are never trimmed or normalized. Binance pathname segments are +percent-decoded before validating the route; malformed encodings are rejected. +Ordinary events, compound candidates, bootstrap queries, and live polling use the +same contract. Compound hashes use sorted compact JSON encoded directly as UTF-8, +matching Python `ensure_ascii=False`; existing ASCII candidate hashes do not change. diff --git a/scripts/binance-coinmarketcap-data.user.js b/scripts/binance-coinmarketcap-data.user.js index 57b1786..627e867 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.15 +// @version 0.1.16 // @author jackhai9 // @description 在 Binance 合约页面显示当前币种的 CoinMarketCap 中文页关键估值与供应量数据 // @match https://www.binance.com/*/futures/* @@ -20,11 +20,21 @@ // ==/UserScript== (() => { // src/shared/binance-futures-route.js - var FUTURES_TRADING_PATH_RE = /^\/(?:[a-z]{2}(?:-[A-Za-z]{2})?\/)?futures\/([A-Za-z0-9_]{3,})\/?$/; + var FUTURES_TRADING_PATH_RE = /^\/(?:[a-z]{2}(?:-[A-Za-z]{2})?\/)?futures\/([^/]+)\/?$/; + var TRADING_SYMBOL_RE = /^[\p{L}\p{N}_]{3,}$/u; function parseFuturesTradingSymbolFromPathname(pathname) { const normalized = String(pathname || "").split(/[?#]/, 1)[0]; const match = normalized.match(FUTURES_TRADING_PATH_RE); - return match?.[1] ? match[1].toUpperCase() : null; + if (!match || match[0] !== normalized) return null; + let symbol; + try { + symbol = decodeURIComponent(match[1]); + } catch (error) { + if (error instanceof URIError) return null; + throw error; + } + const symbolMatch = symbol.match(TRADING_SYMBOL_RE); + return symbolMatch && symbolMatch[0] === symbol ? symbol.toUpperCase() : null; } function isFuturesTradingPathname(pathname) { return Boolean(parseFuturesTradingSymbolFromPathname(pathname)); diff --git a/scripts/binance-orderbook-trade.user.js b/scripts/binance-orderbook-trade.user.js index d6bfad3..def0bda 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.202 +// @version 2.7.203 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -1981,11 +1981,21 @@ } // src/shared/binance-futures-route.js - var FUTURES_TRADING_PATH_RE = /^\/(?:[a-z]{2}(?:-[A-Za-z]{2})?\/)?futures\/([A-Za-z0-9_]{3,})\/?$/; + var FUTURES_TRADING_PATH_RE = /^\/(?:[a-z]{2}(?:-[A-Za-z]{2})?\/)?futures\/([^/]+)\/?$/; + var TRADING_SYMBOL_RE = /^[\p{L}\p{N}_]{3,}$/u; function parseFuturesTradingSymbolFromPathname(pathname) { const normalized = String(pathname || "").split(/[?#]/, 1)[0]; const match = normalized.match(FUTURES_TRADING_PATH_RE); - return match?.[1] ? match[1].toUpperCase() : null; + if (!match || match[0] !== normalized) return null; + let symbol; + try { + symbol = decodeURIComponent(match[1]); + } catch (error) { + if (error instanceof URIError) return null; + throw error; + } + const symbolMatch = symbol.match(TRADING_SYMBOL_RE); + return symbolMatch && symbolMatch[0] === symbol ? symbol.toUpperCase() : null; } function isFuturesTradingPathname(pathname) { return Boolean(parseFuturesTradingSymbolFromPathname(pathname)); diff --git a/scripts/binance-strategy27-events.user.js b/scripts/binance-strategy27-events.user.js index 28efc2f..a4ad910 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.5.0 +// @version 0.5.1 // @author jackhai9 // @description 统一配置 CorsairQuant 网关,显示 Strategy 27 事件与 Strategy 29 跨周期汇总 // @match https://www.binance.com/*/futures/* @@ -31,8 +31,8 @@ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method); // src/binance-strategy27-events/core/live-event-contract.js - var CANONICAL_SYMBOL_PATTERN = /^([A-Z0-9]+)\/USDT:USDT$/; - var ROUTE_SYMBOL_PATTERN = /^([A-Z0-9]+)USDT$/; + var CANONICAL_SYMBOL_PATTERN = /^([\p{L}\p{N}]+)\/USDT:USDT$/u; + var ROUTE_SYMBOL_PATTERN = /^([\p{L}\p{N}]+)USDT$/u; var STREAM_ID_PATTERN = /^(0|[1-9]\d*)-(0|[1-9]\d*)$/; var EPOCH_PATTERN = /^[0-9a-f]{32}$/; var EVENT_ID_PATTERN = /^[0-9a-f]{64}$/; @@ -285,14 +285,19 @@ } } } + function isCanonicalStrategy27Symbol(value) { + if (typeof value !== "string") return false; + const match = value.match(CANONICAL_SYMBOL_PATTERN); + return Boolean(match && match[0] === value && match[1] === match[1].toUpperCase()); + } function routeSymbolToCanonical(routeSymbol) { const match = String(routeSymbol).match(ROUTE_SYMBOL_PATTERN); - assertCondition(match && match[1].length > 0, "Invalid Binance futures route symbol"); + assertCondition(typeof routeSymbol === "string" && match && match[0] === routeSymbol && match[1] === match[1].toUpperCase(), "Invalid Binance futures route symbol"); return `${match[1]}/USDT:USDT`; } function canonicalSymbolToRoute(canonicalSymbol) { const match = String(canonicalSymbol).match(CANONICAL_SYMBOL_PATTERN); - assertCondition(match && match[1].length > 0, "Invalid canonical Strategy 27 symbol"); + assertCondition(isCanonicalStrategy27Symbol(canonicalSymbol), "Invalid canonical Strategy 27 symbol"); const routeSymbol = `${match[1]}USDT`; assertCondition(routeSymbolToCanonical(routeSymbol) === canonicalSymbol, "Canonical Strategy 27 symbol does not round-trip"); return routeSymbol; @@ -2145,7 +2150,6 @@ ${t("候选", "Candidate")} ${annotation.candidateId}`, return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalCompoundJson(value[key])}`).join(",")}}`; } check(value === null || typeof value === "string" || Number.isSafeInteger(value), "canonical value is invalid"); - if (typeof value === "string") check(/^[\x00-\x7f]*$/.test(value), "canonical text must be ASCII"); return JSON.stringify(value); } async function compoundHash(value) { @@ -2197,7 +2201,7 @@ ${t("候选", "Candidate")} ${annotation.candidateId}`, hash(value.profile_id, "profile_id"); hash(value.source_id, "source_id"); profile(value.profile); - check(typeof value.symbol === "string" && /^[A-Z0-9]+\/USDT:USDT$/.test(value.symbol), "symbol is invalid"); + check(isCanonicalStrategy27Symbol(value.symbol), "symbol is invalid"); check(["impact_failure", "passive_support_loss", "failed_rebound"].includes(value.family), "family is invalid"); check(["high", "low"].includes(value.direction), "direction is invalid"); check(value.validation_status === (value.direction === "high" ? "exploratory" : "unvalidated_mirror"), "validation status does not match direction"); @@ -2332,7 +2336,7 @@ ${t("候选", "Candidate")} ${annotation.candidateId}`, function createCompoundCandidateClient({ request, gatewayBaseUrl, authSecret, canonicalSymbol, onResponse, onConnectionStateChange, reconnectDelayMs = 2e3 }) { if (typeof request !== "function" || typeof onResponse !== "function" || typeof onConnectionStateChange !== "function") throw new Error("Compound client callbacks are required"); if (typeof authSecret !== "string" || authSecret.length === 0) throw new Error("Compound gateway secret is not configured"); - if (typeof canonicalSymbol !== "string" || !/^[A-Z0-9]+\/USDT:USDT$/.test(canonicalSymbol)) throw new Error("Compound canonical symbol is invalid"); + if (!isCanonicalStrategy27Symbol(canonicalSymbol)) throw new Error("Compound canonical symbol is invalid"); if (!Number.isSafeInteger(reconnectDelayMs) || reconnectDelayMs < 0) throw new Error("Compound reconnect delay is invalid"); const origin = normalizeGatewayBaseUrl(gatewayBaseUrl); let cursor = null; @@ -2926,11 +2930,21 @@ ${t("候选", "Candidate")} ${annotation.candidateId}`, } // src/shared/binance-futures-route.js - var FUTURES_TRADING_PATH_RE = /^\/(?:[a-z]{2}(?:-[A-Za-z]{2})?\/)?futures\/([A-Za-z0-9_]{3,})\/?$/; + var FUTURES_TRADING_PATH_RE = /^\/(?:[a-z]{2}(?:-[A-Za-z]{2})?\/)?futures\/([^/]+)\/?$/; + var TRADING_SYMBOL_RE = /^[\p{L}\p{N}_]{3,}$/u; function parseFuturesTradingSymbolFromPathname(pathname) { const normalized = String(pathname || "").split(/[?#]/, 1)[0]; const match = normalized.match(FUTURES_TRADING_PATH_RE); - return match?.[1] ? match[1].toUpperCase() : null; + if (!match || match[0] !== normalized) return null; + let symbol; + try { + symbol = decodeURIComponent(match[1]); + } catch (error) { + if (error instanceof URIError) return null; + throw error; + } + const symbolMatch = symbol.match(TRADING_SYMBOL_RE); + return symbolMatch && symbolMatch[0] === symbol ? symbol.toUpperCase() : null; } // src/shared/spa-route-change.js diff --git a/scripts/binance-strategy29-bollinger.user.js b/scripts/binance-strategy29-bollinger.user.js index cc6e24a..940f131 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.4.0 +// @version 0.4.1 // @author jackhai9 // @description Native Bollinger/SMA60 markers with an optional read-only cross-timeframe summary // @match https://www.binance.com/*/futures/* @@ -1317,11 +1317,21 @@ } // src/shared/binance-futures-route.js - var FUTURES_TRADING_PATH_RE = /^\/(?:[a-z]{2}(?:-[A-Za-z]{2})?\/)?futures\/([A-Za-z0-9_]{3,})\/?$/; + var FUTURES_TRADING_PATH_RE = /^\/(?:[a-z]{2}(?:-[A-Za-z]{2})?\/)?futures\/([^/]+)\/?$/; + var TRADING_SYMBOL_RE = /^[\p{L}\p{N}_]{3,}$/u; function parseFuturesTradingSymbolFromPathname(pathname) { const normalized = String(pathname || "").split(/[?#]/, 1)[0]; const match = normalized.match(FUTURES_TRADING_PATH_RE); - return match?.[1] ? match[1].toUpperCase() : null; + if (!match || match[0] !== normalized) return null; + let symbol; + try { + symbol = decodeURIComponent(match[1]); + } catch (error) { + if (error instanceof URIError) return null; + throw error; + } + const symbolMatch = symbol.match(TRADING_SYMBOL_RE); + return symbolMatch && symbolMatch[0] === symbol ? symbol.toUpperCase() : null; } function isFuturesTradingPathname(pathname) { return Boolean(parseFuturesTradingSymbolFromPathname(pathname)); diff --git a/scripts/binance-trading-data.user.js b/scripts/binance-trading-data.user.js index 4ec505c..b93cea3 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.13 +// @version 1.1.14 // @author jackhai9 // @description 在合约交易页面叠加浮动面板,定时拉取交易数据(持仓量、多空比、资金费率等)并显示当前值 + 多空信号 // @match https://www.binance.com/*/futures/* @@ -17,11 +17,21 @@ // ==/UserScript== (() => { // src/shared/binance-futures-route.js - var FUTURES_TRADING_PATH_RE = /^\/(?:[a-z]{2}(?:-[A-Za-z]{2})?\/)?futures\/([A-Za-z0-9_]{3,})\/?$/; + var FUTURES_TRADING_PATH_RE = /^\/(?:[a-z]{2}(?:-[A-Za-z]{2})?\/)?futures\/([^/]+)\/?$/; + var TRADING_SYMBOL_RE = /^[\p{L}\p{N}_]{3,}$/u; function parseFuturesTradingSymbolFromPathname(pathname) { const normalized = String(pathname || "").split(/[?#]/, 1)[0]; const match = normalized.match(FUTURES_TRADING_PATH_RE); - return match?.[1] ? match[1].toUpperCase() : null; + if (!match || match[0] !== normalized) return null; + let symbol; + try { + symbol = decodeURIComponent(match[1]); + } catch (error) { + if (error instanceof URIError) return null; + throw error; + } + const symbolMatch = symbol.match(TRADING_SYMBOL_RE); + return symbolMatch && symbolMatch[0] === symbol ? symbol.toUpperCase() : null; } function isFuturesTradingPathname(pathname) { return Boolean(parseFuturesTradingSymbolFromPathname(pathname)); diff --git a/src/binance-coinmarketcap-data/index.user.js b/src/binance-coinmarketcap-data/index.user.js index e5170e1..9e9ccfa 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.15 +// @version 0.1.16 // @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 42a293f..60b4862 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.202 +// @version 2.7.203 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* diff --git a/src/binance-strategy27-events/core/compound-candidate-client.js b/src/binance-strategy27-events/core/compound-candidate-client.js index 6c035b6..2c61c21 100644 --- a/src/binance-strategy27-events/core/compound-candidate-client.js +++ b/src/binance-strategy27-events/core/compound-candidate-client.js @@ -1,3 +1,4 @@ +import { isCanonicalStrategy27Symbol } from './live-event-contract.js'; import { normalizeGatewayBaseUrl, Strategy27GatewayTransportError } from './live-event-client.js'; import { validateCompoundBootstrapResponse, validateCompoundGatewayResponse } from './compound-candidate-contract.js'; @@ -27,7 +28,7 @@ function cursorRegressed(next, previous) { export function createCompoundCandidateClient({ request, gatewayBaseUrl, authSecret, canonicalSymbol, onResponse, onConnectionStateChange, reconnectDelayMs = 2000 }) { if (typeof request !== 'function' || typeof onResponse !== 'function' || typeof onConnectionStateChange !== 'function') throw new Error('Compound client callbacks are required'); if (typeof authSecret !== 'string' || authSecret.length === 0) throw new Error('Compound gateway secret is not configured'); - if (typeof canonicalSymbol !== 'string' || !/^[A-Z0-9]+\/USDT:USDT$/.test(canonicalSymbol)) throw new Error('Compound canonical symbol is invalid'); + if (!isCanonicalStrategy27Symbol(canonicalSymbol)) throw new Error('Compound canonical symbol is invalid'); if (!Number.isSafeInteger(reconnectDelayMs) || reconnectDelayMs < 0) throw new Error('Compound reconnect delay is invalid'); const origin = normalizeGatewayBaseUrl(gatewayBaseUrl); let cursor = null; diff --git a/src/binance-strategy27-events/core/compound-candidate-contract.js b/src/binance-strategy27-events/core/compound-candidate-contract.js index 8f1119a..f9cfc67 100644 --- a/src/binance-strategy27-events/core/compound-candidate-contract.js +++ b/src/binance-strategy27-events/core/compound-candidate-contract.js @@ -1,3 +1,5 @@ +import { isCanonicalStrategy27Symbol } from './live-event-contract.js'; + /** Exact ADR 032 display contract; no market interpretation belongs here. */ const HASH = /^[a-f0-9]{64}$/; const DECIMAL = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]*[1-9])?$/; @@ -45,7 +47,6 @@ export function canonicalCompoundJson(value) { return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalCompoundJson(value[key])}`).join(',')}}`; } check(value === null || typeof value === 'string' || Number.isSafeInteger(value), 'canonical value is invalid'); - if (typeof value === 'string') check(/^[\x00-\x7f]*$/.test(value), 'canonical text must be ASCII'); return JSON.stringify(value); } @@ -102,7 +103,7 @@ export async function validateCompoundCandidate(value) { hash(value.profile_id, 'profile_id'); hash(value.source_id, 'source_id'); profile(value.profile); - check(typeof value.symbol === 'string' && /^[A-Z0-9]+\/USDT:USDT$/.test(value.symbol), 'symbol is invalid'); + check(isCanonicalStrategy27Symbol(value.symbol), 'symbol is invalid'); check(['impact_failure', 'passive_support_loss', 'failed_rebound'].includes(value.family), 'family is invalid'); check(['high', 'low'].includes(value.direction), 'direction is invalid'); check(value.validation_status === (value.direction === 'high' ? 'exploratory' : 'unvalidated_mirror'), 'validation status does not match direction'); diff --git a/src/binance-strategy27-events/core/live-event-contract.js b/src/binance-strategy27-events/core/live-event-contract.js index 2f1bb35..17ee8cf 100644 --- a/src/binance-strategy27-events/core/live-event-contract.js +++ b/src/binance-strategy27-events/core/live-event-contract.js @@ -1,5 +1,5 @@ -const CANONICAL_SYMBOL_PATTERN = /^([A-Z0-9]+)\/USDT:USDT$/; -const ROUTE_SYMBOL_PATTERN = /^([A-Z0-9]+)USDT$/; +const CANONICAL_SYMBOL_PATTERN = /^([\p{L}\p{N}]+)\/USDT:USDT$/u; +const ROUTE_SYMBOL_PATTERN = /^([\p{L}\p{N}]+)USDT$/u; const STREAM_ID_PATTERN = /^(0|[1-9]\d*)-(0|[1-9]\d*)$/; const EPOCH_PATTERN = /^[0-9a-f]{32}$/; const EVENT_ID_PATTERN = /^[0-9a-f]{64}$/; @@ -268,15 +268,22 @@ function validateOutcome(value) { } } +/** Match the server's Unicode letter/number base and exact uppercase wire form. */ +export function isCanonicalStrategy27Symbol(value) { + if (typeof value !== 'string') return false; + const match = value.match(CANONICAL_SYMBOL_PATTERN); + return Boolean(match && match[0] === value && match[1] === match[1].toUpperCase()); +} + export function routeSymbolToCanonical(routeSymbol) { const match = String(routeSymbol).match(ROUTE_SYMBOL_PATTERN); - assertCondition(match && match[1].length > 0, 'Invalid Binance futures route symbol'); + assertCondition(typeof routeSymbol === 'string' && match && match[0] === routeSymbol && match[1] === match[1].toUpperCase(), 'Invalid Binance futures route symbol'); return `${match[1]}/USDT:USDT`; } export function canonicalSymbolToRoute(canonicalSymbol) { const match = String(canonicalSymbol).match(CANONICAL_SYMBOL_PATTERN); - assertCondition(match && match[1].length > 0, 'Invalid canonical Strategy 27 symbol'); + assertCondition(isCanonicalStrategy27Symbol(canonicalSymbol), 'Invalid canonical Strategy 27 symbol'); const routeSymbol = `${match[1]}USDT`; assertCondition(routeSymbolToCanonical(routeSymbol) === canonicalSymbol, 'Canonical Strategy 27 symbol does not round-trip'); return routeSymbol; diff --git a/src/binance-strategy27-events/index.user.js b/src/binance-strategy27-events/index.user.js index c3d3341..a2f8a54 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.5.0 +// @version 0.5.1 // @author jackhai9 // @description 统一配置 CorsairQuant 网关,显示 Strategy 27 事件与 Strategy 29 跨周期汇总 // @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 55b89d2..0dada3f 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.4.0 +// @version 0.4.1 // @author jackhai9 // @description Native Bollinger/SMA60 markers with an optional 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 ffa0dd6..dfbbe2e 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.13 +// @version 1.1.14 // @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 d0f5a82..d748e79 100644 --- a/src/shared/binance-futures-route.js +++ b/src/shared/binance-futures-route.js @@ -1,9 +1,20 @@ -const FUTURES_TRADING_PATH_RE = /^\/(?:[a-z]{2}(?:-[A-Za-z]{2})?\/)?futures\/([A-Za-z0-9_]{3,})\/?$/; +const FUTURES_TRADING_PATH_RE = /^\/(?:[a-z]{2}(?:-[A-Za-z]{2})?\/)?futures\/([^/]+)\/?$/; +const TRADING_SYMBOL_RE = /^[\p{L}\p{N}_]{3,}$/u; export function parseFuturesTradingSymbolFromPathname(pathname) { const normalized = String(pathname || '').split(/[?#]/, 1)[0]; const match = normalized.match(FUTURES_TRADING_PATH_RE); - return match?.[1] ? match[1].toUpperCase() : null; + if (!match || match[0] !== normalized) return null; + let symbol; + try { + // Browsers percent-encode Unicode segments; malformed paths are not trading routes. + symbol = decodeURIComponent(match[1]); + } catch (error) { + if (error instanceof URIError) return null; + throw error; + } + const symbolMatch = symbol.match(TRADING_SYMBOL_RE); + return symbolMatch && symbolMatch[0] === symbol ? symbol.toUpperCase() : null; } export function isFuturesTradingPathname(pathname) { diff --git a/test/dom/binance-strategy27-events/strategy27-entrypoint.test.js b/test/dom/binance-strategy27-events/strategy27-entrypoint.test.js index acf8ce0..2d5e44d 100644 --- a/test/dom/binance-strategy27-events/strategy27-entrypoint.test.js +++ b/test/dom/binance-strategy27-events/strategy27-entrypoint.test.js @@ -15,16 +15,16 @@ async function until(predicate) { } } -async function harness(t, { generated = false, beforeCreate, locale = 'zh-CN', migrationRecord } = {}) { +async function harness(t, { generated = false, beforeCreate, locale = 'zh-CN', migrationRecord, routeSymbol = 'BTCUSDT', candidateFixture = fixtures[0] } = {}) { const dom = loadFixtureDom('
'); - dom.reconfigure({ url: `https://www.binance.com/${locale}/futures/BTCUSDT` }); + dom.reconfigure({ url: `https://www.binance.com/${locale}/futures/${routeSymbol}` }); const page = dom.window; if (migrationRecord !== undefined) Object.defineProperty(page, Symbol.for('jh-userscripts.strategy29-preferences-migration'), { value: migrationRecord }); const shapes = new Map([['user-owned', {}]]); let resolution = '1S'; let shapeSequence = 0; const chart = { - resolution: () => resolution, symbol: () => 'BTCUSDT', + resolution: () => resolution, symbol: () => routeSymbol, createShape: async (point, options) => { const id = `entry-owned-${++shapeSequence}`; if (beforeCreate) await beforeCreate(); @@ -101,7 +101,7 @@ async function harness(t, { generated = false, beforeCreate, locale = 'zh-CN', m await respond('compound', { schema_version: 1, status: 'ok', requested_cursor: cursor, next_cursor: next, messages: [{ schema_version: 1, projection_kind: 'compound_candidate', runtime_epoch: 'a'.repeat(32), - sequence, message_kind: 'candidate', symbol: fixtures[0].symbol, observed_at_ms: 7000, payload: fixtures[0] }], + sequence, message_kind: 'candidate', symbol: candidateFixture.symbol, observed_at_ms: 7000, payload: candidateFixture }], }); await until(() => pending('compound').length === 1); } @@ -708,3 +708,18 @@ for (const generated of [false, true]) { assert.equal(h.menus.size, 4); }); } + + +for (const generated of [false, true]) { +test(`${generated ? 'generated' : 'source'} Unicode URL and chart symbol start both clients and draw the Python candidate`, async (t) => { + const candidateFixture = JSON.parse(readFileSync(new URL('../../fixtures/strategy27-unicode-candidate.json', import.meta.url), 'utf8')); + const h = await harness(t, {generated, routeSymbol: '币安人生USDT', candidateFixture}); + assert.equal(h.pending('ordinary').length, 1); + assert.equal(h.pending('compound').length, 1); + await h.reset(); + await h.candidate(); + assert.equal(h.rows(), 1); + assert.equal(h.shapes.size, 3); + assert.ok(h.requests.every((request) => new URL(request.options.url).searchParams.get('symbol') === candidateFixture.symbol)); +}); +} diff --git a/test/fixtures/strategy27-unicode-candidate.json b/test/fixtures/strategy27-unicode-candidate.json new file mode 100644 index 0000000..b8c0415 --- /dev/null +++ b/test/fixtures/strategy27-unicode-candidate.json @@ -0,0 +1,87 @@ +{ + "profile": { + "revision": "compound-exploration-1", + "context_seconds": 30, + "minimum_context_seconds": 5, + "context_move_bps": "5", + "significant_flow_ratio": "0.05", + "quiet_flow_ratio": "0.05", + "confirmation_move_bps": "1", + "confirmation_seconds": 20, + "rebound_move_bps": "1", + "reinforcement_seconds": 30 + }, + "symbol": "币安人生/USDT:USDT", + "source_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "family": "impact_failure", + "direction": "high", + "parent_candidate_id": null, + "context": { + "start_ms": 0, + "end_ms": 5000, + "opening_mid": "100", + "closing_mid": "100.1", + "minimum_mid": "100", + "maximum_mid": "100.1" + }, + "established_extreme": "100.1", + "seed": { + "start_ms": 5000, + "end_ms": 6000, + "opening_mid": "100.1", + "closing_mid": "100.09", + "minimum_mid": "100.09", + "maximum_mid": "100.1", + "buy_notional": "6", + "sell_notional": "0", + "buy_count": 1, + "sell_count": 0, + "bid_addition": "0", + "bid_decrease": "1", + "ask_addition": "0", + "ask_decrease": "0", + "bid_depth": "100", + "ask_depth": "100" + }, + "confirmation": { + "start_ms": 6000, + "end_ms": 7000, + "opening_mid": "100.09", + "closing_mid": "100.07", + "minimum_mid": "100.07", + "maximum_mid": "100.09", + "buy_notional": "0", + "sell_notional": "100", + "buy_count": 0, + "sell_count": 5, + "bid_addition": "0", + "bid_decrease": "0", + "ask_addition": "0", + "ask_decrease": "0", + "bid_depth": "100", + "ask_depth": "100" + }, + "trough": null, + "rebound": null, + "decision": { + "start_ms": 6000, + "end_ms": 7000, + "opening_mid": "100.09", + "closing_mid": "100.07", + "minimum_mid": "100.07", + "maximum_mid": "100.09", + "buy_notional": "0", + "sell_notional": "100", + "buy_count": 0, + "sell_count": 5, + "bid_addition": "0", + "bid_decrease": "0", + "ask_addition": "0", + "ask_decrease": "0", + "bid_depth": "100", + "ask_depth": "100" + }, + "profile_id": "7497661840ed679fa244a0276f48be70bc077c0b8496b0c627f3744eb7c48961", + "validation_status": "exploratory", + "candidate_id": "07bd5eb977953eb1256784ac14f6cff262c0a2bddfa8928e6c4ab2ddc092b0a1" +} diff --git a/test/unit/binance-orderbook-trade/route.test.js b/test/unit/binance-orderbook-trade/route.test.js index 3cae840..86ad803 100644 --- a/test/unit/binance-orderbook-trade/route.test.js +++ b/test/unit/binance-orderbook-trade/route.test.js @@ -22,3 +22,14 @@ test('identifies only futures trading page paths', () => { assert.equal(isFuturesTradingPathname('/en/futures/btcusdc'), true); assert.equal(isFuturesTradingPathname('/zh-CN/my/wallet/futures/balance'), false); }); + + +test('parses Unicode futures symbols from actual URL pathnames', () => { + for (const symbol of ['币安人生USDT', '龙虾USDT']) { + assert.equal(parseFuturesTradingSymbolFromPathname('/zh-CN/futures/' + symbol), symbol); + assert.equal(parseFuturesTradingSymbolFromPathname(new URL('https://www.binance.com/zh-CN/futures/' + symbol).pathname), symbol); + } + for (const path of ['/futures/%ZZUSDT', '/futures/BTC%2FUSDT', '/futures/BTC%20USDT', '/futures/BTCUSDT%0A']) { + assert.equal(parseFuturesTradingSymbolFromPathname(path), null); + } +}); diff --git a/test/unit/binance-strategy27-events/compound-candidate-client.test.js b/test/unit/binance-strategy27-events/compound-candidate-client.test.js index c6ca243..3e27127 100644 --- a/test/unit/binance-strategy27-events/compound-candidate-client.test.js +++ b/test/unit/binance-strategy27-events/compound-candidate-client.test.js @@ -10,7 +10,7 @@ const ok = (requested = '5-0', next = '8-0') => ({ schema_version: 1, status: 'o const response = (body, status = 200) => ({ status, responseText: JSON.stringify(body) }); const unavailable = (code = 'compound_unavailable') => response({ schema_version: 1, status: 'error', error_code: code }, 503); -function harness(steps, { onResponse = () => {}, onState = () => {} } = {}) { +function harness(steps, { onResponse = () => {}, onState = () => {}, canonicalSymbol = 'BTR/USDT:USDT' } = {}) { const controller = new AbortController(); const calls = []; const states = []; @@ -18,7 +18,7 @@ function harness(steps, { onResponse = () => {}, onState = () => {} } = {}) { const client = createCompoundCandidateClient({ gatewayBaseUrl: 'http://127.0.0.1:18765', authSecret: 'fixture-only-not-a-credential', - canonicalSymbol: 'BTR/USDT:USDT', + canonicalSymbol, reconnectDelayMs: 0, request: async ({ url }) => { calls.push(new URL(url)); @@ -131,3 +131,15 @@ test('bootstrap wrapper validates its exact metadata and record bound', async () await assert.rejects(validateCompoundBootstrapResponse({ ...bootstrap(), extra: true }, 200)); await assert.rejects(validateCompoundBootstrapResponse({ ...bootstrap(), records: Array(81).fill({}) }, 200)); }); + + +test('Unicode canonical symbol is encoded in bootstrap and live requests', async () => { + const symbol = '币安人生/USDT:USDT'; + const h = harness([response(bootstrap()), response(ok())], { + canonicalSymbol: symbol, + onResponse: (payload, controller) => { if (payload.status === 'ok') controller.abort(); }, + }); + await h.run(); + assert.deepEqual(h.calls.map((url) => url.searchParams.get('symbol')), [symbol, symbol]); + assert.ok(h.calls.every((url) => url.href.includes('%E5%B8%81'))); +}); diff --git a/test/unit/binance-strategy27-events/compound-candidate-contract.test.js b/test/unit/binance-strategy27-events/compound-candidate-contract.test.js index 8cbe936..675e95d 100644 --- a/test/unit/binance-strategy27-events/compound-candidate-contract.test.js +++ b/test/unit/binance-strategy27-events/compound-candidate-contract.test.js @@ -102,3 +102,20 @@ test('envelope cannot claim a decision before it is available or on a different other.symbol = 'ETH/USDT:USDT'; await assert.rejects(validateCompoundEnvelope(other), /identity\/time/); }); + + +test('Unicode canonical JSON uses the same UTF-8 digest as Python', async () => { + assert.equal(await compoundHash({symbol: '币安人生/USDT:USDT'}), '3ab4f4a7e5017943dedd5caae3bd17996392a32d843a0cad485f39fba7f96759'); + const value = candidate(); + value.symbol = '币安人生/USDT:USDT'; + const {candidate_id, ...record} = value; + value.candidate_id = await compoundHash(record); + assert.deepEqual(await validateCompoundEnvelope(envelope(value)), envelope(value)); +}); + + +test('Python Unicode detector fixture validates with its original candidate hash', async () => { + const fixture = JSON.parse(readFileSync(new URL('../../fixtures/strategy27-unicode-candidate.json', import.meta.url), 'utf8')); + assert.equal(fixture.candidate_id, '07bd5eb977953eb1256784ac14f6cff262c0a2bddfa8928e6c4ab2ddc092b0a1'); + assert.deepEqual(await validateCompoundCandidate(fixture), fixture); +}); 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 183bf61..3d39968 100644 --- a/test/unit/binance-strategy27-events/live-event-contract.test.js +++ b/test/unit/binance-strategy27-events/live-event-contract.test.js @@ -457,3 +457,14 @@ test('bounds retained lifecycle events and reports exact count and age evictions assert.equal(ignored.type, 'event_evicted'); assert.equal(lifecycle.size, 0); }); + + +test('canonical Unicode symbols round-trip without normalizing invalid wire input', () => { + for (const base of ['币安人生', '龙虾', '1000PEPE']) { + assert.equal(routeSymbolToCanonical(base + 'USDT'), base + '/USDT:USDT'); + assert.equal(canonicalSymbolToRoute(base + '/USDT:USDT'), base + 'USDT'); + } + for (const symbol of ['btc/USDT:USDT', '币 安/USDT:USDT', '/USDT:USDT', 'BTC/USDT:USDT\n', 'BTC_/USDT:USDT']) { + assert.throws(() => canonicalSymbolToRoute(symbol)); + } +}); diff --git a/test/unit/binance-strategy29-bollinger/source-regressions.test.js b/test/unit/binance-strategy29-bollinger/source-regressions.test.js index 42d2a95..656153e 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.4.0'); + assert.equal(readUserscriptVersion(entrySource), '0.4.1'); assert.deepEqual( [...entrySource.matchAll(/^\/\/ @grant\s+(\S+)\s*$/gm)].map(match => match[1]), ['unsafeWindow', 'GM_getValue'], diff --git a/test/unit/userscript-release-contract.test.js b/test/unit/userscript-release-contract.test.js index 5ee3278..9e9ae35 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.4.0'); + assert.equal(contract.version, '0.4.1'); 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); @@ -60,7 +60,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.5.0'); + assert.equal(contract.version, '0.5.1'); assert.equal(contract.runAt, 'document-idle'); assert.equal(contract.updateURL, contract.downloadURL); assert.deepEqual(contract.matches, [