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
35 changes: 30 additions & 5 deletions docs/binance-strategy27-events-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ 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
use the reset/bootstrap protocol while retaining verified display history. 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
Expand Down Expand Up @@ -95,8 +95,21 @@ 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 and explicit cursor resets remove
only those transient entities. Sequence/cursor contract violations stop the
panel records. Route, symbol, interval and manual clear remove only those
transient entities. Ordinary stream epochs and cursor resets reset protocol
validation while preserving verified display history in the same chart context.
The independent ordinary display registry retains at most 80 events (including
neutral events), evicts by last observation time with event ID as the tie-break,
and expires after two hours without another observation. Reset messages and
bootstrap replay do not refresh its observation timestamps. Protocol lifecycle
pruning does not delete this independently bounded display history.
Old panel records display Historical until that event receives another accepted
observation; this flag changes neither the last received event status nor the
protocol phase. Same-event replay merges by the source event ID, which is stable
across transport epochs, and preserves the first directional marker. A bootstrap
merges its records instead of deleting history absent from the new epoch's
snapshot. Full reload still depends on the server snapshot and cannot recover
previous-epoch records that were never retained by the server. 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 @@ -108,7 +121,8 @@ message arrives. Compound candidates restore only missing parts of their
icon/label pair, preserving the original slot and surviving entity IDs. Each
record shares one in-flight repair across timer and message callbacks. Cleanup
skips IDs proven absent, while native removal failures still stop the owning job.
Clear, reset, context changes and retention eviction invalidate repair ownership;
Manual clear, context changes and display retention eviction invalidate ordinary
repair ownership; compound resets also invalidate their own repair ownership;
late-created entities are removed instead of resurrecting retired records.
Reconciliation does not refresh retention timestamps. Drawings remain transient
and use `disableSave: true`, but a full page reload requests a bounded display
Expand All @@ -124,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.3 with identical metadata headers.
generated install artifact are version 0.4.4 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 @@ -241,3 +255,14 @@ Live validation must confirm the current Binance main-world chart API, exact
`1S` resolution, exact route symbol, successful create/readback/remove behavior,
Tampermonkey source readback, and the actually loaded source after a hard
reload. A source-level method name alone is not end-to-end proof.

## 2026-09-06 ordinary stream recovery evidence

The loaded 0.4.3 Binance script was observed through non-pausing CDP probes on
BTRUSDT at 1S. At 03:47:33.716 UTC a successful response delivered
stream_state / transport_recovered with a new runtime epoch. The entrypoint
then reached stream_reset_clear with two ordinary markers and eight panel rows;
the next response had zero ordinary markers. Earlier event_closed responses
had retained those markers. Version 0.4.4 separates protocol reset from bounded
display ownership. This evidence identifies the browser deletion path; it does
not distinguish the underlying server Redis connection error from a timeout.
63 changes: 40 additions & 23 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.3
// @version 0.4.4
// @author jackhai9
// @description 在 Binance 一秒图表标注 VPS Strategy 27 的实时订单流候选观察
// @match https://www.binance.com/*/futures/*
Expand Down Expand Up @@ -1627,11 +1627,12 @@
styles: { color: selectedKind === "compound" ? annotation.titleColor : annotation.markerColor ?? "#EAECEF", fontWeight: "700", flex: "1" }
}));
title.appendChild(createElement(document, "span", {
text: selectedKind === "compound" ? "探索版" : STATUS_LABELS[annotation.status],
text: selectedKind === "compound" ? "探索版" : record.historical ? "Historical" : STATUS_LABELS[annotation.status],
styles: { color: "#848E9C", fontSize: "11px" }
}));
detail.appendChild(title);
appendDetailLine(document, detail, "时间", formatClock(annotation.eventTimeMs));
if (record.historical) appendDetailLine(document, detail, "History", "Stream restarted; showing the last received observation.", "#F0B90B");
if (selectedKind === "compound") {
for (const row of annotation.detailRows) appendDetailLine(document, detail, row.label, row.value);
const identity = createElement(document, "details", { role: "compound-identity", styles: { color: "#848E9C" } });
Expand Down Expand Up @@ -1682,6 +1683,7 @@
});
row.type = "button";
row.dataset.eventId = eventId;
if (record.historical) row.dataset.historical = "true";
row.title = `${annotation.title}|${annotation.summary}`;
row.appendChild(createElement(document, "span", {
styles: {
Expand Down Expand Up @@ -1751,6 +1753,11 @@
removeCompound(eventId) {
removeRecord(compoundRecords, eventId);
},
/** Retain facts and selection without presenting a previous stream as live. */
retainHistory() {
for (const record of records.values()) record.historical = true;
render();
},
clear() {
records.clear();
reconcileSelection();
Expand Down Expand Up @@ -2704,13 +2711,29 @@
removeStrategy27StatusView(pageDocument);
statusView = null;
}
function removeOrdinaryEvent(context, eventId) {
context.ordinaryHistory.delete(eventId);
context.layer.remove(eventId);
context.panel.remove(eventId);
context.candidatePresentations.delete(eventId);
}
function pruneOrdinaryEvents(context) {
for (const eventId of context.lifecycle.prune(Date.now())) {
context.layer.remove(eventId);
context.panel.remove(eventId);
context.candidatePresentations.delete(eventId);
const now = Date.now();
context.lifecycle.prune(now);
for (const [eventId, observedAtMs] of context.ordinaryHistory) {
if (now - observedAtMs > MAX_EVENT_AGE_MS) removeOrdinaryEvent(context, eventId);
}
}
function retainOrdinaryEvent(context, eventId, observedAtMs) {
const previous = context.ordinaryHistory.get(eventId);
const retainedAtMs = previous === void 0 ? observedAtMs : Math.max(previous, observedAtMs);
context.ordinaryHistory.set(eventId, retainedAtMs);
const oldestFirst = [...context.ordinaryHistory].sort(([leftId, leftTime], [rightId, rightTime]) => leftTime - rightTime || leftId.localeCompare(rightId));
while (context.ordinaryHistory.size > MAX_RETAINED_EVENTS) {
removeOrdinaryEvent(context, oldestFirst.shift()[0]);
}
return retainedAtMs;
}
function failOrdinary(context, error) {
if (error.name === "AbortError" || active !== context || context.failed) return;
context.failed = true;
Expand All @@ -2735,9 +2758,7 @@
pruneOrdinaryEvents(context);
if (response.status === "reset") {
context.lifecycle.reset(response.reason);
context.layer.clear();
context.panel.clear();
context.candidatePresentations.clear();
context.panel.retainHistory();
hideStatus();
return;
}
Expand All @@ -2747,9 +2768,7 @@
runtimeEpoch: response.runtime_epoch,
observedAtMs: response.bootstrap_observed_at_ms
});
context.layer.clear();
context.panel.clear();
context.candidatePresentations.clear();
context.panel.retainHistory();
const bySequence = /* @__PURE__ */ new Map();
for (const record of response.records) {
for (const message of [record.marker_envelope, record.event_envelope, record.outcome_envelope]) {
Expand All @@ -2766,19 +2785,14 @@
for (const message of messages) {
if (active !== context || context.failed) return;
const action = context.lifecycle.apply(message);
for (const eventId of action.evictedEventIds ?? []) {
context.layer.remove(eventId);
context.panel.remove(eventId);
context.candidatePresentations.delete(eventId);
}
if (action.type === "stream_reset") {
context.layer.clear();
context.panel.clear();
context.candidatePresentations.clear();
context.panel.retainHistory();
hideStatus();
continue;
}
if (action.type === "event_evicted") continue;
const retainedAtMs = retainOrdinaryEvent(context, action.eventId, action.observedAtMs);
if (!context.ordinaryHistory.has(action.eventId)) continue;
const annotation = stabilizeCandidatePresentation(
context.candidatePresentations,
action.eventId,
Expand All @@ -2793,9 +2807,9 @@
event_closed: "renderClosed",
event_outcome: "renderOutcome"
}[action.messageKind];
const rendered = await context.layer[renderMethod](action.eventId, annotation, action.observedAtMs);
if (!rendered || active !== context || context.failed) continue;
context.panel.upsert(action.eventId, annotation, action.observedAtMs);
const rendered = await context.layer[renderMethod](action.eventId, annotation, retainedAtMs);
if (!rendered || active !== context || context.failed || !context.ordinaryHistory.has(action.eventId)) continue;
context.panel.upsert(action.eventId, annotation, retainedAtMs);
hideStatus();
}
if (response.status === "bootstrap") {
Expand Down Expand Up @@ -2825,6 +2839,7 @@
savePosition: (position) => GM_setValue(PANEL_POSITION_KEY, position)
}),
candidatePresentations: /* @__PURE__ */ new Map(),
ordinaryHistory: /* @__PURE__ */ new Map(),
reconciliation: null,
failed: false
};
Expand Down Expand Up @@ -2944,6 +2959,8 @@
active?.compound.clear();
active?.layer.clear();
active?.panel.clear();
active?.ordinaryHistory.clear();
active?.candidatePresentations.clear();
if (!active?.failed) hideStatus();
});
GM_registerMenuCommand("Reconnect Strategy 27 and restore history", restart);
Expand Down
9 changes: 8 additions & 1 deletion src/binance-strategy27-events/dom/strategy27-event-panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -316,11 +316,12 @@ export function createStrategy27EventPanel(document, chartRoot, {
styles: { color: selectedKind === 'compound' ? annotation.titleColor : annotation.markerColor ?? '#EAECEF', fontWeight: '700', flex: '1' },
}));
title.appendChild(createElement(document, 'span', {
text: selectedKind === 'compound' ? '探索版' : STATUS_LABELS[annotation.status],
text: selectedKind === 'compound' ? '探索版' : record.historical ? 'Historical' : STATUS_LABELS[annotation.status],
styles: { color: '#848E9C', fontSize: '11px' },
}));
detail.appendChild(title);
appendDetailLine(document, detail, '时间', formatClock(annotation.eventTimeMs));
if (record.historical) appendDetailLine(document, detail, 'History', 'Stream restarted; showing the last received observation.', '#F0B90B');
if (selectedKind === 'compound') {
for (const row of annotation.detailRows) appendDetailLine(document, detail, row.label, row.value);
const identity = createElement(document, 'details', { role: 'compound-identity', styles: { color: '#848E9C' } });
Expand Down Expand Up @@ -371,6 +372,7 @@ export function createStrategy27EventPanel(document, chartRoot, {
});
row.type = 'button';
row.dataset.eventId = eventId;
if (record.historical) row.dataset.historical = 'true';
row.title = `${annotation.title}|${annotation.summary}`;
row.appendChild(createElement(document, 'span', {
styles: {
Expand Down Expand Up @@ -445,6 +447,11 @@ export function createStrategy27EventPanel(document, chartRoot, {
removeCompound(eventId) {
removeRecord(compoundRecords, eventId);
},
/** Retain facts and selection without presenting a previous stream as live. */
retainHistory() {
for (const record of records.values()) record.historical = true;
render();
},
clear() {
records.clear();
reconcileSelection();
Expand Down
Loading
Loading