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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion docs/binance-strategy27-events-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,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.4.5 with identical metadata headers.
generated install artifact are version 0.4.6 with identical metadata headers.
The generated artifact passes syntax, release-contract and isolated execution
checks, including candidate delivery, paired entities, clear and context stop.
Binance operator-page validation remains outstanding. Server/gateway rollout
Expand Down Expand Up @@ -284,3 +284,33 @@ for the current symbol. Both clients use the same gateway with separate existing
paths; neither a successful empty response nor compound connectivity can mark a
symbol as re-added. No live universe-membership endpoint is available to this UI,
so the banner reports the last event evidence, not a current membership guarantee.

## Interface Language

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

Panel headings, monitoring/connection/history notices, ordinary and compound
facts, candidate labels, menus and prompts all use the selected locale. Proper
terms such as Strategy 27, bid, ask, bps, USDT and diagnostic identities retain
their original spelling. Raw diagnostic errors retain their technical details
under a localized failure prefix.

A language-only route change repaints the same panel and preserves selection,
collapse state, monitoring evidence, both requests/cursors, history retention
and marker IDs. Bilingual annotation copies retain the first directional
candidate presentation in both languages. Compound labels update only owned
text properties with saving defaults disabled, including labels that finish
creation or repair after a language change. Changing symbol/chart/interval and
explicit reconnect still use their existing context lifecycle.

The menu uses Tampermonkey 5's returned registration IDs to update its four
entries in place. No additional grants or menu duplicates are introduced.
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.
695 changes: 491 additions & 204 deletions scripts/binance-strategy27-events.user.js

Large diffs are not rendered by default.

60 changes: 33 additions & 27 deletions src/binance-strategy27-events/core/compound-candidate-annotation.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import { createStrategy27Translator, createLocalizedAnnotation } from './ui-copy.js';
import { formatNotional } from './event-annotation.js';

const DIRECTIONS = Object.freeze({
high: Object.freeze({ title: '复合候选高', label: '候选高', shape: 'arrow_down', color: '#B71C3B' }),
low: Object.freeze({ title: '复合候选低', label: '候选低', shape: 'arrow_up', color: '#087F5B' }),
});
const FAMILY_LABELS = Object.freeze({
high: Object.freeze({ impact_failure: '买入推动失效', passive_support_loss: '被动承接转弱', failed_rebound: '反弹失败强化' }),
low: Object.freeze({ impact_failure: '卖出推动失效', passive_support_loss: '被动抛压转弱', failed_rebound: '回落失败强化' }),
});

function clock(ms) {
const date = new Date(ms);
return [date.getHours(), date.getMinutes(), date.getSeconds()].map((value) => String(value).padStart(2, '0')).join(':');
Expand All @@ -18,37 +10,47 @@ function priceWindow(value) {
return `${clock(value.start_ms)}–${clock(value.end_ms)} · ${value.opening_mid} → ${value.closing_mid}`;
}

function flow(value, count, label) {
return value === '0' && count === 0 ? '无主动成交' : `${formatNotional(value, label)} USDT · ${count} 笔`;
function flow(value, count, label, t) {
return value === '0' && count === 0 ? t('无主动成交', 'No aggressive trades') : `${formatNotional(value, label)} USDT · ${count} ${t('笔', 'trades')}`;
}

/** Format server-confirmed evidence only; do not infer additional stages. */
export function buildCompoundCandidateAnnotation(candidate) {
function formatCompoundCandidateAnnotation(candidate, locale) {
const t = createStrategy27Translator(locale);
const DIRECTIONS = Object.freeze({
high: Object.freeze({ title: t('复合候选高', 'Compound high candidate'), label: t('候选高', 'High candidate'), shape: 'arrow_down', color: '#B71C3B' }),
low: Object.freeze({ title: t('复合候选低', 'Compound low candidate'), label: t('候选低', 'Low candidate'), shape: 'arrow_up', color: '#087F5B' }),
});
const FAMILY_LABELS = Object.freeze({
high: Object.freeze({ impact_failure: t('买入推动失效', 'Buy impact failure'), passive_support_loss: t('被动承接转弱', 'Weakening passive support'), failed_rebound: t('反弹失败强化', 'Failed rebound reinforcement') }),
low: Object.freeze({ impact_failure: t('卖出推动失效', 'Sell impact failure'), passive_support_loss: t('被动抛压转弱', 'Weakening passive selling pressure'), failed_rebound: t('回落失败强化', 'Failed pullback reinforcement') }),
});

const direction = DIRECTIONS[candidate.direction];
const family = FAMILY_LABELS[candidate.direction]?.[candidate.family];
if (!direction || !family) throw new Error('Strategy 27 compound display rule is invalid');
const reinforcement = candidate.family === 'failed_rebound';
const detailRows = [
{ label: '规则', value: family },
{ label: '背景', value: priceWindow(candidate.context) },
{ label: '触发秒', value: priceWindow(candidate.seed) },
{ label: '主动买', value: flow(candidate.seed.buy_notional, candidate.seed.buy_count, 'seed buy') },
{ label: '主动卖', value: flow(candidate.seed.sell_notional, candidate.seed.sell_count, 'seed sell') },
{ label: 'bid', value: `增 ${formatNotional(candidate.seed.bid_addition, 'bid addition')} · 减 ${formatNotional(candidate.seed.bid_decrease, 'bid decrease')} USDT` },
{ label: 'ask', value: `增 ${formatNotional(candidate.seed.ask_addition, 'ask addition')} · 减 ${formatNotional(candidate.seed.ask_decrease, 'ask decrease')} USDT` },
{ label: '基础确认', value: priceWindow(candidate.confirmation) },
{ label: t('规则', 'Rule'), value: family },
{ label: t('背景', 'Context'), value: priceWindow(candidate.context) },
{ label: t('触发秒', 'Trigger second'), value: priceWindow(candidate.seed) },
{ label: t('主动买', 'Aggressive buy'), value: flow(candidate.seed.buy_notional, candidate.seed.buy_count, 'seed buy', t) },
{ label: t('主动卖', 'Aggressive sell'), value: flow(candidate.seed.sell_notional, candidate.seed.sell_count, 'seed sell', t) },
{ label: 'bid', value: `${t('增', 'Added')} ${formatNotional(candidate.seed.bid_addition, 'bid addition')} · ${t('减', 'Removed')} ${formatNotional(candidate.seed.bid_decrease, 'bid decrease')} USDT` },
{ label: 'ask', value: `${t('增', 'Added')} ${formatNotional(candidate.seed.ask_addition, 'ask addition')} · ${t('减', 'Removed')} ${formatNotional(candidate.seed.ask_decrease, 'ask decrease')} USDT` },
{ label: t('基础确认', 'Base confirmation'), value: priceWindow(candidate.confirmation) },
];
if (reinforcement) {
detailRows.push(
{ label: candidate.direction === 'high' ? '低点秒' : '高点秒', value: priceWindow(candidate.trough) },
{ label: candidate.direction === 'high' ? '反弹秒' : '回落秒', value: priceWindow(candidate.rebound) },
{ label: '强化确认', value: priceWindow(candidate.decision) },
{ label: '关联候选', value: candidate.parent_candidate_id },
{ label: candidate.direction === 'high' ? t('低点秒', 'Trough second') : t('高点秒', 'Peak second'), value: priceWindow(candidate.trough) },
{ label: candidate.direction === 'high' ? t('反弹秒', 'Rebound second') : t('回落秒', 'Pullback second'), value: priceWindow(candidate.rebound) },
{ label: t('强化确认', 'Reinforcement confirmation'), value: priceWindow(candidate.decision) },
{ label: t('关联候选', 'Related candidate'), value: candidate.parent_candidate_id },
);
}
detailRows.push({ label: '参数版本', value: candidate.profile.revision });
const notices = ['探索候选,尚未验证预测能力'];
if (candidate.direction === 'low') notices.push('镜像规则,尚未独立验证');
detailRows.push({ label: t('参数版本', 'Parameter revision'), value: candidate.profile.revision });
const notices = [t('探索候选,尚未验证预测能力', 'Exploratory candidate; predictive ability not validated')];
if (candidate.direction === 'low') notices.push(t('镜像规则,尚未独立验证', 'Mirrored rule; not independently validated'));
return Object.freeze({
kind: 'compound',
title: direction.title,
Expand All @@ -68,3 +70,7 @@ export function buildCompoundCandidateAnnotation(candidate) {
notices: Object.freeze(notices),
});
}

export function buildCompoundCandidateAnnotation(candidate, { locale = 'zh-CN' } = {}) {
return createLocalizedAnnotation((nextLocale) => formatCompoundCandidateAnnotation(candidate, nextLocale), locale);
}
52 changes: 37 additions & 15 deletions src/binance-strategy27-events/core/compound-candidate-controller.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import { createStrategy27Translator, localizeAnnotation } from './ui-copy.js';
import { buildCompoundCandidateAnnotation } from './compound-candidate-annotation.js';
import { createCompoundCandidateClient } from './compound-candidate-client.js';
import { CompoundCandidateLifecycle } from './compound-candidate-lifecycle.js';

const CONNECTION_STATUS = Object.freeze({
connected: ['Compound data: connected. Connection does not confirm symbol monitoring.', 'normal'],
reconnecting: ['Compound data connection lost; reconnecting.', 'inactive'],
unavailable: ['Compound data temporarily unavailable; reconnecting.', 'inactive'],
unsupported: ['网关尚未启用复合候选', 'inactive'],
});

/** Own the optional compound job, never the ordinary client's state.
*
* The lifecycle accepts immutable records before asynchronous drawing. A view
Expand All @@ -18,8 +12,25 @@ const CONNECTION_STATUS = Object.freeze({
*/
export function createCompoundCandidateController({
request, gatewayBaseUrl, authSecret, canonicalSymbol, panel, createLayer,
isCurrent, maxCandidates, maxAgeMs, nowMs = Date.now, reconnectDelayMs = 2000,
locale = 'zh-CN', isCurrent, maxCandidates, maxAgeMs, nowMs = Date.now, reconnectDelayMs = 2000,
}) {
let currentLocale = locale;
let t = createStrategy27Translator(locale);
let connectionState = 'connecting';
function connectionStatus() {
const CONNECTION_STATUS = Object.freeze({
connected: [t('复合候选数据:已连接。接口连通不代表该币种仍在监控中。', 'Compound data: connected. Connection does not confirm symbol monitoring.'), 'normal'],
reconnecting: [t('复合候选数据连接中断,正在重连。', 'Compound data connection lost; reconnecting.'), 'inactive'],
unavailable: [t('复合候选数据暂不可用,正在重连。', 'Compound data temporarily unavailable; reconnecting.'), 'inactive'],
connecting: [t('复合候选正在连接', 'Connecting to compound data'), 'inactive'],
unsupported: [t('网关尚未启用复合候选', 'Compound candidates are not enabled on the gateway'), 'inactive'],
});
return CONNECTION_STATUS;
}
function renderStatus() {
if (lastError) panel.setCompoundStatus(t('复合候选已停止:', 'Compound candidates stopped: ') + lastError.message, 'error');
else panel.setCompoundStatus(...connectionStatus()[connectionState]);
}
const lifecycle = new CompoundCandidateLifecycle(canonicalSymbol, { maxCandidates, maxAgeMs });
const abortController = new AbortController();
let layer = null;
Expand Down Expand Up @@ -61,19 +72,20 @@ export function createCompoundCandidateController({
lifecycle.reset('stopped');
const cleanupError = clear ? clearView() : null;
if (cleanupError) lastError = new AggregateError([error, cleanupError], `${error.message}; ${cleanupError.message}`);
panel.setCompoundStatus(`复合候选已停止:${lastError.message}`, 'error');
renderStatus();
}

function onConnectionStateChange(state) {
if (!current()) return;
const status = CONNECTION_STATUS[state];
const status = connectionStatus()[state];
if (!status) throw new Error(`Unknown compound connection state: ${state}`);
if (state === 'unavailable' || state === 'unsupported') {
lifecycle.reset('unavailable');
const error = clearView();
if (error) { failJob(error, { clear: false }); return; }
}
panel.setCompoundStatus(...status);
connectionState = state;
renderStatus();
}

async function onResponse(response) {
Expand Down Expand Up @@ -108,7 +120,7 @@ export function createCompoundCandidateController({
}
if (action.type !== 'candidate' || applicationGeneration !== viewGeneration) continue;
const id = action.candidate.candidate_id;
const annotation = buildCompoundCandidateAnnotation(action.candidate);
const annotation = buildCompoundCandidateAnnotation(action.candidate, { locale: currentLocale });
// Optional chart capabilities are tested inside this job's error boundary.
if (layer === null) layer = createLayer();
const renderGeneration = viewGeneration;
Expand All @@ -119,7 +131,7 @@ export function createCompoundCandidateController({
if (!current()) return;
prune();
if (rendered && renderGeneration === viewGeneration) {
panel.upsertCompound(id, annotation, action.observedAtMs);
panel.upsertCompound(id, localizeAnnotation(annotation, currentLocale), action.observedAtMs);
}
} finally {
pendingCandidateId = null;
Expand All @@ -131,13 +143,23 @@ export function createCompoundCandidateController({
}

return Object.freeze({
setLocale(nextLocale) {
t = createStrategy27Translator(nextLocale);
currentLocale = nextLocale;
renderStatus();
try {
layer?.setLocale(nextLocale);
} catch (error) {
failJob(error);
}
},
run() {
if (started) throw new Error('Compound controller already started');
started = true;
return (async () => {
if (!current()) return;
try {
panel.setCompoundStatus('复合候选正在连接', 'inactive');
renderStatus();
const client = createCompoundCandidateClient({
request, gatewayBaseUrl, authSecret, canonicalSymbol, reconnectDelayMs,
onResponse, onConnectionStateChange,
Expand Down Expand Up @@ -179,7 +201,7 @@ export function createCompoundCandidateController({
const error = clearView();
if (error) {
lastError = error;
panel.setCompoundStatus(`复合候选已停止:${error.message}`, 'error');
renderStatus();
}
},
// A late drawing rejection remains inspectable without touching a retired panel.
Expand Down
Loading
Loading