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
29 changes: 21 additions & 8 deletions docs/binance-strategy27-events-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,23 @@ symbol.

The macOS operator machine keeps this forward under a `launchd` user agent so
the SSH process is restarted after sleep, network changes, or a broken
connection. The browser client treats only `GM_xmlhttpRequest` transport errors
and timeouts as recoverable: it retains the current cursor, displays a
reconnecting status, and retries after two seconds. HTTP responses, gateway
errors, malformed JSON, cursor violations, and rendering contract failures
still stop immediately.
connection. The ordinary browser client treats `GM_xmlhttpRequest` transport
errors/timeouts and protocol-validated HTTP 503 unavailability as recoverable:
it retains the current cursor and displayed history, shows a reconnecting status,
and retries after two seconds. A bootstrap 503 remains in bootstrap; a live
`redis_unavailable` response retries the same live cursor. Stale cursors still
use the existing reset/bootstrap contract. Other HTTP/gateway errors, malformed
JSON, cursor violations, and rendering contract failures stop immediately.

A terminal ordinary-job failure suspends drawing and polling without deleting
previously verified markers or panel history. Pending candle waits and late
creations/repairs lose presentation ownership; late entities are removed.
Retained history is frozen evidence, not a live connection, and the visible
error remains until explicit recovery or a context change. The existing context
timer continues two-hour retention pruning but does not repair a suspended
layer. Use the `Reconnect Strategy 27 and restore history` userscript menu after
resolving the error to start a new context and restore the gateway snapshot.
Manual clear remains available and does not dismiss a terminal error.

## Rendering Contract

Expand Down Expand Up @@ -83,8 +95,9 @@ be later than that snapshot's end when an ineligible bucket advances the event
to its lifecycle deadline without joining the event.

The script stores only its own returned marker IDs and its bounded in-memory
panel records. Route, symbol, interval, epoch, cursor, or sequence
discontinuities abort the request and remove only those transient entities.
panel records. Route, symbol, interval, epoch and explicit cursor resets remove
only those transient entities. Sequence/cursor contract violations stop the
ordinary job with an error while retaining its already verified history.
Marker count and age are bounded on the chart; the panel retains at most eight
events.

Expand All @@ -111,7 +124,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.2 with identical metadata headers.
generated install artifact are version 0.4.3 with identical metadata headers.
The generated artifact passes syntax, release-contract and isolated execution
checks, including candidate delivery, paired entities, clear and context stop.
Binance operator-page validation remains outstanding. Server/gateway rollout
Expand Down
37 changes: 22 additions & 15 deletions scripts/binance-strategy27-events.user.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.4.2
// @version 0.4.3
// @author jackhai9
// @description 在 Binance 一秒图表标注 VPS Strategy 27 的实时订单流候选观察
// @match https://www.binance.com/*/futures/*
Expand Down Expand Up @@ -742,6 +742,7 @@
signal
});
} catch (error) {
if (signal.aborted) return;
if (!(error instanceof Strategy27GatewayTransportError)) throw error;
if (!reconnecting) {
reconnecting = true;
Expand All @@ -750,10 +751,11 @@
await waitForReconnect(reconnectDelayMs, signal);
continue;
}
if (signal.aborted) return;
const payload = parseResponseJson(response, needsBootstrap);
if (!needsBootstrap) assertCursorContract(payload, cursor);
if (payload.status === "error") {
if (needsBootstrap && response.status === 503) {
if (response.status === 503) {
if (!reconnecting) {
reconnecting = true;
onConnectionStateChange("reconnecting");
Expand All @@ -767,6 +769,7 @@
reconnecting = false;
onConnectionStateChange("connected");
}
if (signal.aborted) return;
await onResponse(payload);
if (!needsBootstrap && payload.status === "reset") {
cursor = null;
Expand Down Expand Up @@ -1202,6 +1205,7 @@
const pendingRenders = /* @__PURE__ */ new Map();
let renderGeneration = 0;
let reconciliation = null;
let suspended = false;
function removeRecord(eventId) {
pendingRenders.get(eventId)?.abort();
const record = registry.get(eventId);
Expand All @@ -1219,7 +1223,7 @@
}
function restoreMarker(eventId, record, liveIds) {
if (record.restoring) return record.restoring;
const current = () => registry.get(eventId) === record && isChartCurrent();
const current = () => !suspended && registry.get(eventId) === record && isChartCurrent();
if (!current()) return Promise.resolve(false);
if (liveIds.has(record.markerId)) return Promise.resolve(true);
record.restoring = (async () => {
Expand All @@ -1236,11 +1240,12 @@
return record.restoring;
}
function reconcile() {
if (suspended) return Promise.resolve();
if (reconciliation) return reconciliation;
reconciliation = (async () => {
let liveIds = readLiveShapeIds(chart);
for (const [eventId, record] of [...registry]) {
if (registry.get(eventId) !== record || !isChartCurrent()) continue;
if (suspended || registry.get(eventId) !== record || !isChartCurrent()) continue;
if (!record.restoring && liveIds.has(record.markerId)) continue;
await restoreMarker(eventId, record, liveIds);
liveIds = readLiveShapeIds(chart);
Expand All @@ -1251,6 +1256,7 @@
return reconciliation;
}
async function ensureMarker(eventId, annotation, observedAtMs) {
if (suspended) return false;
let record = registry.get(eventId);
if (record) {
record.observedAtMs = observedAtMs;
Expand Down Expand Up @@ -1286,6 +1292,12 @@
remove: removeRecord,
prune: pruneAge,
reconcile,
/** Stop new presentation without deleting verified history after a job failure. */
suspend() {
suspended = true;
renderGeneration += 1;
for (const controller of pendingRenders.values()) controller.abort();
},
clear() {
renderGeneration += 1;
for (const controller of pendingRenders.values()) controller.abort();
Expand Down Expand Up @@ -2703,19 +2715,13 @@
if (error.name === "AbortError" || active !== context || context.failed) return;
context.failed = true;
context.controller.abort();
let failure = error;
try {
context.layer.clear();
} catch (cleanupError) {
failure = new AggregateError([error, cleanupError], `${error.message}; ${cleanupError.message}`);
}
context.panel.clear();
showStatus(context.target.chartRoot, `Strategy 27 已停止:${failure.message}`, "error");
context.layer.suspend();
showStatus(context.target.chartRoot, `Strategy 27 stopped; history retained. Use the reconnect menu to resume: ${error.message}`, "error");
}
function reconcileOrdinary(context) {
if (context.failed) return;
try {
pruneOrdinaryEvents(context);
if (context.failed) return;
if (context.reconciliation) return;
context.reconciliation = context.layer.reconcile().catch((error) => failOrdinary(context, error)).finally(() => {
context.reconciliation = null;
Expand Down Expand Up @@ -2842,7 +2848,7 @@
authSecret,
canonicalSymbol,
onConnectionStateChange: (state) => {
if (active !== context) return;
if (active !== context || context.failed) return;
if (state === "reconnecting") {
showStatus(context.target.chartRoot, "Strategy 27 网关连接中断,正在重连", "inactive");
} else {
Expand Down Expand Up @@ -2938,8 +2944,9 @@
active?.compound.clear();
active?.layer.clear();
active?.panel.clear();
hideStatus();
if (!active?.failed) hideStatus();
});
GM_registerMenuCommand("Reconnect Strategy 27 and restore history", restart);
const removeRouteListener = installSpaRouteChangeListener(page, restart);
const contextTimer = page.setInterval(synchronizeContext, CONTEXT_CHECK_INTERVAL_MS);
page.addEventListener("beforeunload", () => {
Expand Down
6 changes: 5 additions & 1 deletion src/binance-strategy27-events/core/live-event-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export function createLiveEventClient({
signal,
});
} catch (error) {
if (signal.aborted) return;
if (!(error instanceof Strategy27GatewayTransportError)) throw error;
if (!reconnecting) {
reconnecting = true;
Expand All @@ -151,10 +152,12 @@ export function createLiveEventClient({
await waitForReconnect(reconnectDelayMs, signal);
continue;
}
if (signal.aborted) return;
const payload = parseResponseJson(response, needsBootstrap);
if (!needsBootstrap) assertCursorContract(payload, cursor);
if (payload.status === 'error') {
if (needsBootstrap && response.status === 503) {
// Validated unavailability does not invalidate the retained stream cursor.
if (response.status === 503) {
if (!reconnecting) {
reconnecting = true;
onConnectionStateChange('reconnecting');
Expand All @@ -168,6 +171,7 @@ export function createLiveEventClient({
reconnecting = false;
onConnectionStateChange('connected');
}
if (signal.aborted) return;
await onResponse(payload);
if (!needsBootstrap && payload.status === 'reset') {
cursor = null;
Expand Down
13 changes: 11 additions & 2 deletions src/binance-strategy27-events/dom/tradingview-event-layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ export function createTradingViewEventLayer(target, {
const pendingRenders = new Map();
let renderGeneration = 0;
let reconciliation = null;
let suspended = false;

function removeRecord(eventId) {
pendingRenders.get(eventId)?.abort();
Expand All @@ -289,7 +290,7 @@ export function createTradingViewEventLayer(target, {

function restoreMarker(eventId, record, liveIds) {
if (record.restoring) return record.restoring;
const current = () => registry.get(eventId) === record && isChartCurrent();
const current = () => !suspended && registry.get(eventId) === record && isChartCurrent();
if (!current()) return Promise.resolve(false);
if (liveIds.has(record.markerId)) return Promise.resolve(true);
record.restoring = (async () => {
Expand All @@ -305,11 +306,12 @@ export function createTradingViewEventLayer(target, {
}

function reconcile() {
if (suspended) return Promise.resolve();
if (reconciliation) return reconciliation;
reconciliation = (async () => {
let liveIds = readLiveShapeIds(chart);
for (const [eventId, record] of [...registry]) {
if (registry.get(eventId) !== record || !isChartCurrent()) continue;
if (suspended || registry.get(eventId) !== record || !isChartCurrent()) continue;
if (!record.restoring && liveIds.has(record.markerId)) continue;
await restoreMarker(eventId, record, liveIds);
// A native create yields; refresh before examining another record.
Expand All @@ -320,6 +322,7 @@ export function createTradingViewEventLayer(target, {
}

async function ensureMarker(eventId, annotation, observedAtMs) {
if (suspended) return false;
let record = registry.get(eventId);
if (record) {
record.observedAtMs = observedAtMs;
Expand Down Expand Up @@ -356,6 +359,12 @@ export function createTradingViewEventLayer(target, {
remove: removeRecord,
prune: pruneAge,
reconcile,
/** Stop new presentation without deleting verified history after a job failure. */
suspend() {
suspended = true;
renderGeneration += 1;
for (const controller of pendingRenders.values()) controller.abort();
},
clear() {
renderGeneration += 1;
for (const controller of pendingRenders.values()) controller.abort();
Expand Down
19 changes: 7 additions & 12 deletions src/binance-strategy27-events/index.user.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.4.2
// @version 0.4.3
// @author jackhai9
// @description 在 Binance 一秒图表标注 VPS Strategy 27 的实时订单流候选观察
// @match https://www.binance.com/*/futures/*
Expand Down Expand Up @@ -98,20 +98,14 @@ import { installSpaRouteChangeListener } from '../shared/spa-route-change.js';
if (error.name === 'AbortError' || active !== context || context.failed) return;
context.failed = true;
context.controller.abort();
let failure = error;
try {
context.layer.clear();
} catch (cleanupError) {
failure = new AggregateError([error, cleanupError], `${error.message}; ${cleanupError.message}`);
}
context.panel.clear();
showStatus(context.target.chartRoot, `Strategy 27 已停止:${failure.message}`, 'error');
context.layer.suspend();
showStatus(context.target.chartRoot, `Strategy 27 stopped; history retained. Use the reconnect menu to resume: ${error.message}`, 'error');
}

function reconcileOrdinary(context) {
if (context.failed) return;
try {
pruneOrdinaryEvents(context);
if (context.failed) return;
if (context.reconciliation) return;
context.reconciliation = context.layer.reconcile()
.catch((error) => failOrdinary(context, error))
Expand Down Expand Up @@ -237,7 +231,7 @@ import { installSpaRouteChangeListener } from '../shared/spa-route-change.js';
authSecret,
canonicalSymbol,
onConnectionStateChange: (state) => {
if (active !== context) return;
if (active !== context || context.failed) return;
if (state === 'reconnecting') {
showStatus(context.target.chartRoot, 'Strategy 27 网关连接中断,正在重连', 'inactive');
} else {
Expand Down Expand Up @@ -345,8 +339,9 @@ import { installSpaRouteChangeListener } from '../shared/spa-route-change.js';
active?.compound.clear();
active?.layer.clear();
active?.panel.clear();
hideStatus();
if (!active?.failed) hideStatus();
});
GM_registerMenuCommand('Reconnect Strategy 27 and restore history', restart);

const removeRouteListener = installSpaRouteChangeListener(page, restart);
const contextTimer = page.setInterval(synchronizeContext, CONTEXT_CHECK_INTERVAL_MS);
Expand Down
Loading
Loading