diagnose line problem
This commit is contained in:
parent
796e6cdeb8
commit
e1d2c60af7
3 changed files with 54 additions and 0 deletions
|
|
@ -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
|
then immediately `DELETE /api/debug/captures/{id}`. The server also expires
|
||||||
captures after 24 hours and caps the directory at 50 files.
|
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
|
## Keep the two documents current
|
||||||
|
|
||||||
This is a running system under continual change, not a build being executed, so
|
This is a running system under continual change, not a build being executed, so
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ class ConfluenceChart {
|
||||||
this.priceLines = new Map();
|
this.priceLines = new Map();
|
||||||
this.currentPriceLine = null;
|
this.currentPriceLine = null;
|
||||||
this.currentPricePulse = null;
|
this.currentPricePulse = null;
|
||||||
|
this.projectionDiagnostic = null;
|
||||||
this.animateCurrentPrice = true;
|
this.animateCurrentPrice = true;
|
||||||
this.autoScrollLivePrice = true;
|
this.autoScrollLivePrice = true;
|
||||||
this.lastCurrentPrice = null;
|
this.lastCurrentPrice = null;
|
||||||
|
|
@ -262,6 +263,10 @@ class ConfluenceChart {
|
||||||
this.currentPricePulse.className = 'current-price-pulse';
|
this.currentPricePulse.className = 'current-price-pulse';
|
||||||
this.currentPricePulse.hidden = true;
|
this.currentPricePulse.hidden = true;
|
||||||
this.overlayLayer.appendChild(this.currentPricePulse);
|
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');
|
const preview = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||||
preview.classList.add('chart-preview');
|
preview.classList.add('chart-preview');
|
||||||
preview.setAttribute('aria-hidden', 'true');
|
preview.setAttribute('aria-hidden', 'true');
|
||||||
|
|
@ -401,6 +406,7 @@ class ConfluenceChart {
|
||||||
this.chart.timeScale().subscribeVisibleLogicalRangeChange(() => {
|
this.chart.timeScale().subscribeVisibleLogicalRangeChange(() => {
|
||||||
this.renderAnchorHandles();
|
this.renderAnchorHandles();
|
||||||
this.renderLineBridges();
|
this.renderLineBridges();
|
||||||
|
this.renderProjectionDiagnostic();
|
||||||
this.renderComments();
|
this.renderComments();
|
||||||
this.renderContextLabels();
|
this.renderContextLabels();
|
||||||
});
|
});
|
||||||
|
|
@ -438,6 +444,7 @@ class ConfluenceChart {
|
||||||
this.renderComments();
|
this.renderComments();
|
||||||
this.renderContextLabels();
|
this.renderContextLabels();
|
||||||
this.renderLineBridges();
|
this.renderLineBridges();
|
||||||
|
this.renderProjectionDiagnostic();
|
||||||
}
|
}
|
||||||
|
|
||||||
setTrendlineGeometry(geometry) {
|
setTrendlineGeometry(geometry) {
|
||||||
|
|
@ -543,6 +550,7 @@ class ConfluenceChart {
|
||||||
this.renderAnchorHandles();
|
this.renderAnchorHandles();
|
||||||
if (advanced || this.lineBridgeLayer?.childElementCount) this.renderLineBridges();
|
if (advanced || this.lineBridgeLayer?.childElementCount) this.renderLineBridges();
|
||||||
this.renderContextLabels();
|
this.renderContextLabels();
|
||||||
|
this.renderProjectionDiagnostic();
|
||||||
}
|
}
|
||||||
|
|
||||||
extendManualLineSeries(previous, bar) {
|
extendManualLineSeries(previous, bar) {
|
||||||
|
|
@ -1074,6 +1082,7 @@ class ConfluenceChart {
|
||||||
this.renderAnchorHandles();
|
this.renderAnchorHandles();
|
||||||
this.renderLineBridges();
|
this.renderLineBridges();
|
||||||
this.renderContextLabels();
|
this.renderContextLabels();
|
||||||
|
this.renderProjectionDiagnostic();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderContextLabels() {
|
renderContextLabels() {
|
||||||
|
|
@ -1177,6 +1186,7 @@ class ConfluenceChart {
|
||||||
this.syncOverlayLayer();
|
this.syncOverlayLayer();
|
||||||
this.renderAnchorHandles();
|
this.renderAnchorHandles();
|
||||||
this.renderLineBridges();
|
this.renderLineBridges();
|
||||||
|
this.renderProjectionDiagnostic();
|
||||||
this.renderComments();
|
this.renderComments();
|
||||||
this.renderContextLabels();
|
this.renderContextLabels();
|
||||||
if (this.armedTool) this.renderGesture();
|
if (this.armedTool) this.renderGesture();
|
||||||
|
|
@ -1891,6 +1901,44 @@ class ConfluenceChart {
|
||||||
this.lineBridgeLayer.replaceChildren(...bridges);
|
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) {
|
lineData(level) {
|
||||||
// Interpolated across bars, matching what the server scores the level at.
|
// Interpolated across bars, matching what the server scores the level at.
|
||||||
// Doing it per second instead kicked the line upward at every session gap,
|
// Doing it per second instead kicked the line upward at every session gap,
|
||||||
|
|
|
||||||
|
|
@ -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); }
|
.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; }
|
#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%); } }
|
.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; } }
|
@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); } }
|
@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; }
|
.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; }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue