From e1d2c60af775127d0976246457643936a44a2264 Mon Sep 17 00:00:00 2001 From: Chris Amow Date: Sat, 15 Aug 2026 02:54:19 -0500 Subject: [PATCH] diagnose line problem --- AGENTS.md | 5 +++++ static/chart.js | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ static/style.css | 1 + 3 files changed, 54 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 19afbc9..9bbbb5d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/static/chart.js b/static/chart.js index ff00979..74725af 100644 --- a/static/chart.js +++ b/static/chart.js @@ -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, diff --git a/static/style.css b/static/style.css index 784b009..a886855 100644 --- a/static/style.css +++ b/static/style.css @@ -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; }