diagnose line problem

This commit is contained in:
Chris Amow 2026-08-15 02:54:19 -05:00
parent 796e6cdeb8
commit e1d2c60af7
3 changed files with 54 additions and 0 deletions

View file

@ -101,6 +101,11 @@ metadata remain authenticated. Inspect only a URL the user explicitly shares,
then immediately `DELETE /api/debug/captures/{id}`. The server also expires
captures after 24 hours and caps the directory at 50 files.
Diagnostic mode also shows a compact projection readout for visible manual
trendlines: historical/future canonical price changes, their screen slopes, and
whether the future canvas point exists. Include it in a capture when a line
looks kinked at the live edge; it separates bad geometry from a bad renderer.
## Keep the two documents current
This is a running system under continual change, not a build being executed, so

View file

@ -9,6 +9,7 @@ class ConfluenceChart {
this.priceLines = new Map();
this.currentPriceLine = null;
this.currentPricePulse = null;
this.projectionDiagnostic = null;
this.animateCurrentPrice = true;
this.autoScrollLivePrice = true;
this.lastCurrentPrice = null;
@ -262,6 +263,10 @@ class ConfluenceChart {
this.currentPricePulse.className = 'current-price-pulse';
this.currentPricePulse.hidden = true;
this.overlayLayer.appendChild(this.currentPricePulse);
this.projectionDiagnostic = document.createElement('pre');
this.projectionDiagnostic.className = 'projection-diagnostic';
this.projectionDiagnostic.hidden = true;
this.overlayLayer.appendChild(this.projectionDiagnostic);
const preview = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
preview.classList.add('chart-preview');
preview.setAttribute('aria-hidden', 'true');
@ -401,6 +406,7 @@ class ConfluenceChart {
this.chart.timeScale().subscribeVisibleLogicalRangeChange(() => {
this.renderAnchorHandles();
this.renderLineBridges();
this.renderProjectionDiagnostic();
this.renderComments();
this.renderContextLabels();
});
@ -438,6 +444,7 @@ class ConfluenceChart {
this.renderComments();
this.renderContextLabels();
this.renderLineBridges();
this.renderProjectionDiagnostic();
}
setTrendlineGeometry(geometry) {
@ -543,6 +550,7 @@ class ConfluenceChart {
this.renderAnchorHandles();
if (advanced || this.lineBridgeLayer?.childElementCount) this.renderLineBridges();
this.renderContextLabels();
this.renderProjectionDiagnostic();
}
extendManualLineSeries(previous, bar) {
@ -1074,6 +1082,7 @@ class ConfluenceChart {
this.renderAnchorHandles();
this.renderLineBridges();
this.renderContextLabels();
this.renderProjectionDiagnostic();
}
renderContextLabels() {
@ -1177,6 +1186,7 @@ class ConfluenceChart {
this.syncOverlayLayer();
this.renderAnchorHandles();
this.renderLineBridges();
this.renderProjectionDiagnostic();
this.renderComments();
this.renderContextLabels();
if (this.armedTool) this.renderGesture();
@ -1891,6 +1901,44 @@ class ConfluenceChart {
this.lineBridgeLayer.replaceChildren(...bridges);
}
renderProjectionDiagnostic() {
if (!this.projectionDiagnostic) return;
if (!ConfluenceChart.diagnosticMode() || this.bars.length < 2) {
this.projectionDiagnostic.hidden = true;
return;
}
const last = this.bars.length - 1;
const future = this.futureTimes(this.bars)[0];
const plotHeight = this.plotCanvas()?.clientHeight || 0;
if (!future || !plotHeight) {
this.projectionDiagnostic.hidden = true;
return;
}
const slope = (first, second) => {
if (first.x == null || second.x == null || first.y == null || second.y == null) return null;
return (second.y - first.y) / (second.x - first.x);
};
const lines = this.levels.filter(level => level.kind === 'manual' && !level.hidden
&& !ConfluenceChart.isFlat(level)).map(level => {
const previousTime = this.bars[last - 1].t;
const currentTime = this.bars[last].t;
const priceAt = time => this.linePriceAtIndex(level, this.indexAt(time));
const previous = { x: this.chart.timeScale().timeToCoordinate(previousTime),
y: this.candles.priceToCoordinate(priceAt(previousTime)), p: priceAt(previousTime) };
const current = { x: this.chart.timeScale().timeToCoordinate(currentTime),
y: this.candles.priceToCoordinate(priceAt(currentTime)), p: priceAt(currentTime) };
const next = { x: this.chart.timeScale().timeToCoordinate(future.time),
y: this.candles.priceToCoordinate(priceAt(future.time)), p: priceAt(future.time) };
const series = this.levelSeries.get(level.id)?.series.data()
.find(point => Number(point.time) === future.time)?.value;
return { level, previous, current, next, series, historical: slope(previous, current), future: slope(current, next) };
}).filter(row => row.current.y != null && row.current.y >= 0 && row.current.y <= plotHeight);
this.projectionDiagnostic.textContent = lines.map(row =>
`#${row.level.number} ${row.level.tf} dp ${((row.current.p ?? 0) - (row.previous.p ?? 0)).toFixed(3)}/${((row.next.p ?? 0) - (row.current.p ?? 0)).toFixed(3)} px ${(row.historical ?? 0).toFixed(3)}/${(row.future ?? 0).toFixed(3)} ${row.series == null ? 'MISSING' : 'OK'}`,
).join('\n');
this.projectionDiagnostic.hidden = !lines.length;
}
lineData(level) {
// Interpolated across bars, matching what the server scores the level at.
// Doing it per second instead kicked the line upward at every session gap,

View file

@ -15,6 +15,7 @@ button { border:1px solid var(--line); background:transparent; color:var(--muted
.drawing-tools { min-height:38px; padding:5px 12px; display:flex; align-items:center; gap:9px; border-bottom:1px solid var(--line); color:var(--muted); font-size:10px; }.drawing-tools button,.drawing-tools select,.drawing-tools .line-name { padding:4px 8px; font-size:10px; }.drawing-tools select,.drawing-tools .line-name { background:var(--panel); color:var(--fg); border:1px solid var(--line); }.drawing-tools .line-name { width:130px; font:inherit; }.drawing-tools label { display:flex; gap:4px; align-items:center; }.drawing-tools input { accent-color:var(--accent); }
#chart { position:relative; height:calc(100vh - 190px); min-height:420px; }.chart-preview,.chart-line-bridges,.chart-handles { position:absolute; inset:0; width:100%; height:100%; overflow:hidden; pointer-events:none; }.chart-preview,.chart-line-bridges { z-index:4; }.chart-line-bridges line { stroke-linecap:round; }.chart-handles { z-index:6; }.chart-preview line[hidden],.chart-anchor[hidden],.chart-line-hit[hidden],.chart-line-focus[hidden] { display:none; }.chart-line-focus { stroke-linecap:round; pointer-events:none; animation:line-focus .75s ease-out forwards; }.chart-line-hit { fill:none; stroke:transparent; stroke-width:16px; pointer-events:stroke; cursor:move; touch-action:none; }.chart-anchor { stroke:var(--panel); stroke-width:2px; cursor:grab; pointer-events:all; touch-action:none; }.chart-anchor:active { cursor:grabbing; }.chart-tooltip { position:absolute; z-index:5; padding:4px 7px; border:1px solid var(--line); background:var(--panel); color:var(--fg); font-size:10px; white-space:pre-line; pointer-events:none; }.chart-tooltip[hidden] { display:none; }
.current-price-pulse { position:absolute; left:0; right:0; z-index:5; height:3px; transform:translateY(-1.5px); overflow:hidden; opacity:0; background:repeating-linear-gradient(110deg,transparent 0 8px,color-mix(in srgb,var(--accent) 55%,white) 8px 13px,transparent 13px 19px); mask-image:linear-gradient(90deg,transparent 0,#000 8%,#000 100%); pointer-events:none; }.current-price-pulse::after { content:''; position:absolute; top:-2px; bottom:-2px; left:-24%; width:24%; background:linear-gradient(90deg,transparent,color-mix(in srgb,var(--accent) 35%,white),transparent); opacity:.8; }.current-price-pulse.active { opacity:.9; animation:current-price-flow 1.5s linear infinite; }.current-price-pulse.active::after { animation:current-price-scan 2.8s linear infinite; }@keyframes current-price-flow { to { background-position-x:57px; } }@keyframes current-price-scan { to { transform:translateX(520%); } }
.projection-diagnostic { position:absolute; top:8px; left:8px; z-index:9; margin:0; padding:5px 7px; border:1px solid var(--line); background:color-mix(in srgb,var(--panel) 92%,transparent); color:var(--fg); font:9px/1.35 "IBM Plex Mono", monospace; white-space:pre; }
@media (prefers-reduced-motion:reduce) { .current-price-pulse.active { animation:none; } }
@keyframes line-focus { 0% { opacity:.85; stroke-width:var(--line-focus-start-width,12px); } 80% { opacity:.85; } 100% { opacity:0; stroke-width:var(--line-focus-width,2px); } }
.chart-context-menu { position:absolute; z-index:8; width:165px; padding:4px; border:1px solid var(--line); background:var(--panel); box-shadow:0 5px 18px color-mix(in srgb,var(--fg) 15%,transparent); pointer-events:auto; }.chart-context-menu[hidden] { display:none; }.chart-context-menu button { width:100%; padding:6px 8px; text-align:left; color:var(--fg); font-size:10px; }