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
20 changes: 19 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.4 with identical metadata headers.
generated install artifact are version 0.4.5 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 @@ -266,3 +266,21 @@ 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.

## Monitoring status and connection status

Version 0.4.5 adds a top-level last-reported monitoring status independent of
selected history, drawing completion and data connection status. An accepted
universe_removed event explicitly identifies removal; monitor_stopped identifies
stopped monitoring. These reports survive stream resets and manual history clear.
A newer event identity supersedes the report; delayed outcomes for older events
and active bootstrap replay for the same closed event cannot undo it. Outcome-only
bootstrap records also restore this status. A stream reset without a removal/stop
report displays that monitoring status awaits new evidence.

The ordinary connection line reports request delivery independently. The compound
connection text explicitly states that connectivity does not confirm monitoring
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.
58 changes: 53 additions & 5 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.4
// @version 0.4.5
// @author jackhai9
// @description 在 Binance 一秒图表标注 VPS Strategy 27 的实时订单流候选观察
// @match https://www.binance.com/*/futures/*
Expand Down Expand Up @@ -1548,6 +1548,19 @@
role: "panel-body",
styles: { overflow: "auto", maxHeight: "calc(100vh - 190px)" }
});
const monitoring = createElement(document, "div", {
role: "ordinary-monitoring-status",
text: "Monitoring status unknown. Waiting for event evidence.",
styles: { padding: "9px", fontWeight: "600", color: "#F0B90B", borderBottom: "1px solid rgba(132, 142, 156, .18)" }
});
monitoring.dataset.state = "unknown";
monitoring.setAttribute("aria-live", "polite");
const ordinaryConnection = createElement(document, "div", {
role: "ordinary-connection-status",
text: "Event data: Connecting",
styles: { padding: "5px 9px 0", color: "#848E9C", fontSize: "11px" }
});
let monitoringObservation = null;
const detail = createElement(document, "div", {
role: "event-detail",
styles: { display: "grid", gap: "5px", padding: "9px" }
Expand Down Expand Up @@ -1578,7 +1591,7 @@
role: "compound-list",
styles: { display: "grid", gap: "2px", padding: "0 6px 7px" }
});
body.append(detail, compoundTitle, compoundStatus, compoundRecent, recentTitle, recent);
body.append(monitoring, ordinaryConnection, detail, compoundTitle, compoundStatus, compoundRecent, recentTitle, recent);
panel.appendChild(body);
document.body.appendChild(panel);
const initialPosition = assertPanelPosition(loadPosition()) ?? createDefaultPosition(chartRoot);
Expand Down Expand Up @@ -1741,6 +1754,32 @@
render();
}
return Object.freeze({
/** Event identity order prevents delayed outcomes or bootstrap replay from undoing removal or reentry. */
observeOrdinaryEvent(event, observedAtMs) {
const previous = monitoringObservation;
if (previous && event.triggered_at_ms < previous.triggeredAtMs) return;
if (previous && event.triggered_at_ms === previous.triggeredAtMs) {
if (observedAtMs < previous.observedAtMs) return;
if (previous.terminal && event.event_status === "active") return;
}
monitoringObservation = {
triggeredAtMs: event.triggered_at_ms,
observedAtMs,
terminal: event.event_status !== "active"
};
const state = event.close_reason === "universe_removed" ? "removed" : event.close_reason === "monitor_stopped" ? "stopped" : "observed";
const text = state === "removed" ? "Symbol removed from monitoring. Retained records are historical." : state === "stopped" ? "Monitoring stopped. Retained records are historical." : "Observation received. Waiting for further events.";
monitoring.dataset.state = state;
monitoring.textContent = "Last reported monitoring status (" + formatClock(observedAtMs) + "): " + text;
monitoring.style.color = state === "observed" ? "#848E9C" : "#F0B90B";
},
setOrdinaryConnection(state) {
const labels = { connected: "Connected", reconnecting: "Reconnecting", stopped: "Stopped" };
if (!Object.hasOwn(labels, state)) throw new Error("Invalid ordinary connection state");
ordinaryConnection.textContent = "Event data: " + labels[state];
ordinaryConnection.dataset.state = state;
ordinaryConnection.style.color = state === "stopped" ? "#F6465D" : "#848E9C";
},
upsert(eventId, annotation, observedAtMs) {
upsertRecord(records, maxEvents, eventId, annotation, observedAtMs);
},
Expand All @@ -1755,6 +1794,11 @@
},
/** Retain facts and selection without presenting a previous stream as live. */
retainHistory() {
if (monitoring.dataset.state === "observed") {
monitoring.dataset.state = "historical";
monitoring.textContent = "Stream restarted. Monitoring status awaits new event evidence; retained records are historical.";
monitoring.style.color = "#F0B90B";
}
for (const record of records.values()) record.historical = true;
render();
},
Expand Down Expand Up @@ -2270,9 +2314,9 @@

// src/binance-strategy27-events/core/compound-candidate-controller.js
var CONNECTION_STATUS = Object.freeze({
connected: ["复合候选已连接", "normal"],
reconnecting: ["复合候选连接中断,正在重连", "inactive"],
unavailable: ["复合候选暂不可用,正在重连", "inactive"],
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"]
});
function createCompoundCandidateController({
Expand Down Expand Up @@ -2739,6 +2783,7 @@
context.failed = true;
context.controller.abort();
context.layer.suspend();
context.panel.setOrdinaryConnection("stopped");
showStatus(context.target.chartRoot, `Strategy 27 stopped; history retained. Use the reconnect menu to resume: ${error.message}`, "error");
}
function reconcileOrdinary(context) {
Expand All @@ -2755,6 +2800,7 @@
}
async function renderGatewayResponse(context, response) {
if (active !== context || context.failed) return;
context.panel.setOrdinaryConnection("connected");
pruneOrdinaryEvents(context);
if (response.status === "reset") {
context.lifecycle.reset(response.reason);
Expand Down Expand Up @@ -2791,6 +2837,7 @@
continue;
}
if (action.type === "event_evicted") continue;
context.panel.observeOrdinaryEvent(action.event, action.observedAtMs);
const retainedAtMs = retainOrdinaryEvent(context, action.eventId, action.observedAtMs);
if (!context.ordinaryHistory.has(action.eventId)) continue;
const annotation = stabilizeCandidatePresentation(
Expand Down Expand Up @@ -2864,6 +2911,7 @@
canonicalSymbol,
onConnectionStateChange: (state) => {
if (active !== context || context.failed) return;
context.panel.setOrdinaryConnection(state);
if (state === "reconnecting") {
showStatus(context.target.chartRoot, "Strategy 27 网关连接中断,正在重连", "inactive");
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { createCompoundCandidateClient } from './compound-candidate-client.js';
import { CompoundCandidateLifecycle } from './compound-candidate-lifecycle.js';

const CONNECTION_STATUS = Object.freeze({
connected: ['复合候选已连接', 'normal'],
reconnecting: ['复合候选连接中断,正在重连', 'inactive'],
unavailable: ['复合候选暂不可用,正在重连', 'inactive'],
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'],
});

Expand Down
49 changes: 48 additions & 1 deletion src/binance-strategy27-events/dom/strategy27-event-panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,18 @@ export function createStrategy27EventPanel(document, chartRoot, {
role: 'panel-body',
styles: { overflow: 'auto', maxHeight: 'calc(100vh - 190px)' },
});
const monitoring = createElement(document, 'div', {
role: 'ordinary-monitoring-status',
text: 'Monitoring status unknown. Waiting for event evidence.',
styles: { padding: '9px', fontWeight: '600', color: '#F0B90B', borderBottom: '1px solid rgba(132, 142, 156, .18)' },
});
monitoring.dataset.state = 'unknown';
monitoring.setAttribute('aria-live', 'polite');
const ordinaryConnection = createElement(document, 'div', {
role: 'ordinary-connection-status', text: 'Event data: Connecting',
styles: { padding: '5px 9px 0', color: '#848E9C', fontSize: '11px' },
});
let monitoringObservation = null;
const detail = createElement(document, 'div', {
role: 'event-detail',
styles: { display: 'grid', gap: '5px', padding: '9px' },
Expand Down Expand Up @@ -258,7 +270,7 @@ export function createStrategy27EventPanel(document, chartRoot, {
role: 'compound-list',
styles: { display: 'grid', gap: '2px', padding: '0 6px 7px' },
});
body.append(detail, compoundTitle, compoundStatus, compoundRecent, recentTitle, recent);
body.append(monitoring, ordinaryConnection, detail, compoundTitle, compoundStatus, compoundRecent, recentTitle, recent);
panel.appendChild(body);
document.body.appendChild(panel);
const initialPosition = assertPanelPosition(loadPosition()) ?? createDefaultPosition(chartRoot);
Expand Down Expand Up @@ -435,6 +447,36 @@ export function createStrategy27EventPanel(document, chartRoot, {
}

return Object.freeze({
/** Event identity order prevents delayed outcomes or bootstrap replay from undoing removal or reentry. */
observeOrdinaryEvent(event, observedAtMs) {
const previous = monitoringObservation;
if (previous && event.triggered_at_ms < previous.triggeredAtMs) return;
if (previous && event.triggered_at_ms === previous.triggeredAtMs) {
if (observedAtMs < previous.observedAtMs) return;
if (previous.terminal && event.event_status === 'active') return;
}
monitoringObservation = {
triggeredAtMs: event.triggered_at_ms,
observedAtMs,
terminal: event.event_status !== 'active',
};
const state = event.close_reason === 'universe_removed' ? 'removed'
: event.close_reason === 'monitor_stopped' ? 'stopped' : 'observed';
const text = state === 'removed'
? 'Symbol removed from monitoring. Retained records are historical.'
: state === 'stopped' ? 'Monitoring stopped. Retained records are historical.'
: 'Observation received. Waiting for further events.';
monitoring.dataset.state = state;
monitoring.textContent = 'Last reported monitoring status (' + formatClock(observedAtMs) + '): ' + text;
monitoring.style.color = state === 'observed' ? '#848E9C' : '#F0B90B';
},
setOrdinaryConnection(state) {
const labels = { connected: 'Connected', reconnecting: 'Reconnecting', stopped: 'Stopped' };
if (!Object.hasOwn(labels, state)) throw new Error('Invalid ordinary connection state');
ordinaryConnection.textContent = 'Event data: ' + labels[state];
ordinaryConnection.dataset.state = state;
ordinaryConnection.style.color = state === 'stopped' ? '#F6465D' : '#848E9C';
},
upsert(eventId, annotation, observedAtMs) {
upsertRecord(records, maxEvents, eventId, annotation, observedAtMs);
},
Expand All @@ -449,6 +491,11 @@ export function createStrategy27EventPanel(document, chartRoot, {
},
/** Retain facts and selection without presenting a previous stream as live. */
retainHistory() {
if (monitoring.dataset.state === 'observed') {
monitoring.dataset.state = 'historical';
monitoring.textContent = 'Stream restarted. Monitoring status awaits new event evidence; retained records are historical.';
monitoring.style.color = '#F0B90B';
}
for (const record of records.values()) record.historical = true;
render();
},
Expand Down
6 changes: 5 additions & 1 deletion 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.4
// @version 0.4.5
// @author jackhai9
// @description 在 Binance 一秒图表标注 VPS Strategy 27 的实时订单流候选观察
// @match https://www.binance.com/*/futures/*
Expand Down Expand Up @@ -120,6 +120,7 @@ import { installSpaRouteChangeListener } from '../shared/spa-route-change.js';
context.failed = true;
context.controller.abort();
context.layer.suspend();
context.panel.setOrdinaryConnection('stopped');
showStatus(context.target.chartRoot, `Strategy 27 stopped; history retained. Use the reconnect menu to resume: ${error.message}`, 'error');
}

Expand All @@ -138,6 +139,7 @@ import { installSpaRouteChangeListener } from '../shared/spa-route-change.js';

async function renderGatewayResponse(context, response) {
if (active !== context || context.failed) return;
context.panel.setOrdinaryConnection('connected');
pruneOrdinaryEvents(context);
if (response.status === 'reset') {
context.lifecycle.reset(response.reason);
Expand Down Expand Up @@ -176,6 +178,7 @@ import { installSpaRouteChangeListener } from '../shared/spa-route-change.js';
continue;
}
if (action.type === 'event_evicted') continue;
context.panel.observeOrdinaryEvent(action.event, action.observedAtMs);
const retainedAtMs = retainOrdinaryEvent(context, action.eventId, action.observedAtMs);
if (!context.ordinaryHistory.has(action.eventId)) continue;
const annotation = stabilizeCandidatePresentation(
Expand Down Expand Up @@ -245,6 +248,7 @@ import { installSpaRouteChangeListener } from '../shared/spa-route-change.js';
canonicalSymbol,
onConnectionStateChange: (state) => {
if (active !== context || context.failed) return;
context.panel.setOrdinaryConnection(state);
if (state === 'reconnecting') {
showStatus(context.target.chartRoot, 'Strategy 27 网关连接中断,正在重连', 'inactive');
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ test('controller composes real protocol/lifecycle/panel and renders each indepen
assert.deepEqual(h.renders.map((item) => item.decisionAtMs), [7000, 7000]);
assert.deepEqual(h.renders.map((item) => item.annotation.markerShape), ['arrow_down', 'arrow_up']);
assert.equal(h.layerCreates, 1);
assert.equal(h.status(), '复合候选已连接');
assert.equal(h.status(), 'Compound data: connected. Connection does not confirm symbol monitoring.');
assert.throws(() => h.controller.run(), /already started/);
h.controller.stop('route_changed');
assert.equal(h.shapes.size, 0);
Expand Down Expand Up @@ -162,7 +162,7 @@ test('503 clears compound state and recovers with a new cursor without clearing
await h.parked;
assert.equal(h.panel.compoundSize, 1);
assert.deepEqual([...h.shapes.keys()], [fixtures[1].candidate_id]);
assert.equal(h.status(), '复合候选已连接');
assert.equal(h.status(), 'Compound data: connected. Connection does not confirm symbol monitoring.');
});

test('network reconnect retains compound history and original cursor', async (t) => {
Expand Down
Loading
Loading