From eaf118ef8be56aa58b6685a2bf90270c80180393 Mon Sep 17 00:00:00 2001 From: Chris Amow Date: Sun, 23 Aug 2026 21:37:19 -0500 Subject: [PATCH] drawings from higher timeframes to lower fixes --- docs/implementation.md | 14 +++++--- docs/plan.md | 15 +++++---- static/app.js | 39 +++++++++++++++++------ static/chart.js | 7 ++-- static/index.html | 1 + tests/e2e/drawings.test.mjs | 62 ++++++++++++++++++++++++++++++++++++ tests/e2e/trendline.test.mjs | 8 +++-- 7 files changed, 120 insertions(+), 26 deletions(-) diff --git a/docs/implementation.md b/docs/implementation.md index c8c516b..4599079 100644 --- a/docs/implementation.md +++ b/docs/implementation.md @@ -1094,11 +1094,15 @@ same effective style is applied to rare SVG endpoint bridges so an off-grid anchor cannot introduce a visible style seam. Browser coverage checks 30m-on-5m and native 30m rendering separately. -Sloped manual lines also stop propagating upward: a 30m line is available on -30m and lower charts but hidden on 1h/1d. This is a display filter only; the -drawing remains loaded and flat price levels remain visible everywhere. The -daily-chart regression creates a real 30m drawing, confirms it is rendered on -the lower chart, switches to 1d, and requires only its series to disappear. +Drawing objects also stop propagating upward by default: a 30m trendline, price +level, Fibonacci drawing, comment, or symbol is available on 30m and lower +charts but hidden on 1h/1d. **Hide lower-TF drawings** in Config persists this +display preference in `localStorage`, defaults on, and can restore every drawing +without changing backend data or alert behavior. New price levels, comments, +and symbols now save the creation timeframe; older price levels were historically +stored as 1d and therefore remain global because their original timeframe is +unknowable. Integration coverage creates all four non-trendline drawing types, +switches to 1d, and verifies the setting hides and restores them together. ### 2026-08-15 — 1m zoom-out died at ~1am because the socket sent 1,000 bars diff --git a/docs/plan.md b/docs/plan.md index ac71ef7..ba469ea 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -122,10 +122,11 @@ if your parser doesn't filter those, that fixture will catch it. 1d base chart + THE DAILY MAs weight 16 ← a switchable base timeframe ``` -Derived levels and flat price levels stay visible on every base timeframe — the -200DMA on a 1-minute chart is the point, not a side effect. Sloped manual -trendlines are visible on their attributed timeframe and lower charts, but not -on higher charts: 30m structure belongs on 30m/15m/5m/1m, not on 1h or 1d. +Derived overlays stay visible on every base timeframe — the 200DMA on a 1-minute +chart is the point, not a side effect. Drawing objects follow one configurable +rule: visible on their attributed timeframe and lower charts, but not on higher +charts. Thus 30m trendlines, levels, Fibonacci drawings, comments, and symbols +belong on 30m/15m/5m/1m, not on 1h or 1d. --- @@ -919,9 +920,9 @@ LAYERS ☐ Hidden levels still count toward confluence ``` -- **Derived and flat levels stay across base timeframes.** A 200DMA is equally - valid on a 1m chart. Sloped manual trendlines are the exception: hide them - above their attributed timeframe while retaining them on native/lower charts. +- **Derived overlays stay across base timeframes.** A 200DMA is equally valid on + a 1m chart. Drawing objects default to native/lower charts only. The persisted + Config setting **Hide lower-TF drawings** can restore all drawings everywhere. - **The group checkbox is a master toggle** — unchecking "Daily MAs" hides all five at once; individual periods nest under it. - The colour swatch beside each timeframe is that timeframe's hue, used identically on diff --git a/static/app.js b/static/app.js index 891d50f..11ce816 100644 --- a/static/app.js +++ b/static/app.js @@ -151,6 +151,7 @@ createApp({ const animateCurrentPrice = ref(localStorage.getItem('chart-animate-current-price') !== 'false'); const autoScrollLivePrice = ref(localStorage.getItem('chart-auto-scroll-live-price') !== 'false'); const extraDetail = ref(localStorage.getItem('chart-extra-detail') === 'true'); + const hideLowerTfDrawings = ref(localStorage.getItem('chart-hide-lower-tf-drawings') !== 'false'); const optionPrefs = (() => { try { return JSON.parse(localStorage.getItem('chart-es-options')) || {}; } catch { return {}; } @@ -721,14 +722,24 @@ createApp({ comments.value = (payload.drawings || []).filter(item => ['comment', 'symbol'].includes(item.kind)); fibs.value = (payload.drawings || []).filter(item => item.kind === 'fibonacci'); syncComments(); - if (chartApi) chartApi.setFibs(prefs.value.enabled.drawings - ? fibs.value.filter(item => !item.hidden) : []); + syncFibs(); + } + + function drawingVisible(item) { + return !hideLowerTfDrawings.value + || ConfluenceChart.drawingVisibleOn(item, timeframe.value); + } + + function syncFibs() { + if (!chartApi) return; + chartApi.setFibs(prefs.value.enabled.drawings + ? fibs.value.filter(item => !item.hidden && drawingVisible(item)) : []); } function syncComments() { if (!chartApi) return; const visible = prefs.value.enabled.drawings - ? comments.value.filter(item => !item.hidden) + ? comments.value.filter(item => !item.hidden && drawingVisible(item)) : []; chartApi.setComments(visible); } @@ -829,7 +840,7 @@ createApp({ pinned: !commentFloat.value, anchor_t: result.time ?? null, anchor_p: result.price ?? null, - x: result.x, y: result.y, + x: result.x, y: result.y, tf: timeframe.value, }), }); if (!response.ok) { @@ -850,6 +861,7 @@ createApp({ body: JSON.stringify({ text: choice.name, icon, color: symbolColor.value, scale: symbolScale.value, pinned: true, anchor_t: result.time, anchor_p: result.price, x: result.x, y: result.y, + tf: timeframe.value, }), }); if (!response.ok) { @@ -994,7 +1006,7 @@ createApp({ body: JSON.stringify({ price: alertPrice.value, note: alertNote.value, color: levelColor.value, line_width: levelWidth.value, - alert_early_points: alertEarlyPoints.value, + alert_early_points: alertEarlyPoints.value, tf: timeframe.value, }), }); if (!response.ok) { console.error(`Unable to add price alert: HTTP ${response.status}`); return; } @@ -1339,6 +1351,9 @@ createApp({ function selectTimeframe(tf) { timeframe.value = tf; prefs.value.base_tf = tf; + syncVisibleLevels(); + syncComments(); + syncFibs(); if (socket?.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ type: 'subscribe', tf })); } @@ -1348,7 +1363,7 @@ createApp({ if (level.kind === 'ma') return (prefs.value.enabled.ma[level.tf] || []).includes(level.period); if (level.kind === 'manual') { return prefs.value.enabled.drawings && prefs.value.enabled.manual - && ConfluenceChart.manualLevelVisibleOn(level, timeframe.value); + && drawingVisible(level); } if (level.kind === 'horizontal') return prefs.value.enabled.horizontal; if (level.kind === 'vwap') return prefs.value.enabled.vwap; @@ -1408,8 +1423,7 @@ createApp({ syncVisibleLevels(); syncComments(); if (chartApi) chartApi.setRthEnabled(prefs.value.enabled.rth); - if (chartApi) chartApi.setFibs(prefs.value.enabled.drawings - ? fibs.value.filter(item => !item.hidden) : []); + syncFibs(); sendPrefs(); }, { deep: true }); @@ -1475,6 +1489,13 @@ createApp({ localStorage.setItem('chart-extra-detail', String(value)); }); + watch(hideLowerTfDrawings, value => { + localStorage.setItem('chart-hide-lower-tf-drawings', String(value)); + syncVisibleLevels(); + syncComments(); + syncFibs(); + }); + onMounted(async () => { chartApi = new ConfluenceChart(); // Deliberate debug handle. Chart geometry bugs are invisible from the @@ -1518,6 +1539,6 @@ createApp({ window.removeEventListener('keydown', handleKeydown); }); - return { status, price, sessionOpen, quoteChange, animateCurrentPrice, autoScrollLivePrice, extraDetail, barAge, dataUpdatedAt, buildStamp, timeframe, timeframes, drawingColors, drawingColorRows, drawingColorName, colorRowLabels, symbolChoices, selectedSymbol, symbolColor, symbolScale, symbolScales, symbolPanelOpen, prefs, clusters, clustersByPrice, events, eventsMore, loadOlderEvents, diagnosticMode, captureBusy, captureDiagnostic, armedTool, drawName, drawColor, drawWidth, drawSide, snap, selectedDrawing, selectedDrawings, drawingList, startDrawingListResize, manualLines, hasDrawingSelection, allShownSelected, selectedAreHidden, alertPrice, alertNote, alertEarlyPoints, levelColor, levelWidth, addPriceAlert, armTool, selectTimeframe, allEnabled, toggleGroup, maValue, maAlertOn, toggleMaAlert, deleteSelected, deleteLine, toggleDrawingSelection, toggleSelectAll, toggleSelectedVisibility, renameLine, updateLineStyle, updateLevelNumber, setArmed, commentText, commentFloat, comments, drawings, filteredDrawings, drawingFilter, drawingKind, drawingTf, deleteDrawing, toggleComment, togglePinned, chooseSymbol, toggleSymbolPanel, startSymbolDrag, dropSymbol, optionExpirations, optionExpiryId, optionSide, optionMode, optionMin, optionMax, optionContracts, optionUnderlying, optionBusy, optionError, optionSearched, optionCopied, onOptionsToggle, searchOptions, copyOption, canUndo, undoTitle, undo }; + return { status, price, sessionOpen, quoteChange, animateCurrentPrice, autoScrollLivePrice, extraDetail, hideLowerTfDrawings, barAge, dataUpdatedAt, buildStamp, timeframe, timeframes, drawingColors, drawingColorRows, drawingColorName, colorRowLabels, symbolChoices, selectedSymbol, symbolColor, symbolScale, symbolScales, symbolPanelOpen, prefs, clusters, clustersByPrice, events, eventsMore, loadOlderEvents, diagnosticMode, captureBusy, captureDiagnostic, armedTool, drawName, drawColor, drawWidth, drawSide, snap, selectedDrawing, selectedDrawings, drawingList, startDrawingListResize, manualLines, hasDrawingSelection, allShownSelected, selectedAreHidden, alertPrice, alertNote, alertEarlyPoints, levelColor, levelWidth, addPriceAlert, armTool, selectTimeframe, allEnabled, toggleGroup, maValue, maAlertOn, toggleMaAlert, deleteSelected, deleteLine, toggleDrawingSelection, toggleSelectAll, toggleSelectedVisibility, renameLine, updateLineStyle, updateLevelNumber, setArmed, commentText, commentFloat, comments, drawings, filteredDrawings, drawingFilter, drawingKind, drawingTf, deleteDrawing, toggleComment, togglePinned, chooseSymbol, toggleSymbolPanel, startSymbolDrag, dropSymbol, optionExpirations, optionExpiryId, optionSide, optionMode, optionMin, optionMax, optionContracts, optionUnderlying, optionBusy, optionError, optionSearched, optionCopied, onOptionsToggle, searchOptions, copyOption, canUndo, undoTitle, undo }; }, }).mount('#app'); diff --git a/static/chart.js b/static/chart.js index 4073832..eca3400 100644 --- a/static/chart.js +++ b/static/chart.js @@ -1250,9 +1250,10 @@ class ConfluenceChart { return level.kind === 'ma' || level.kind === 'vwap' || level.kind === 'horizontal'; } - static manualLevelVisibleOn(level, displayedTf) { - if (level.kind !== 'manual' || ConfluenceChart.isFlat(level)) return true; - return ConfluenceChart.tfRank[level.tf] >= ConfluenceChart.tfRank[displayedTf]; + static drawingVisibleOn(drawing, displayedTf) { + const drawingRank = ConfluenceChart.tfRank[drawing?.tf]; + const displayedRank = ConfluenceChart.tfRank[displayedTf]; + return drawingRank == null || displayedRank == null || drawingRank >= displayedRank; } manualLineDisplayStyle(level) { diff --git a/static/index.html b/static/index.html index 99b6401..e2baece 100644 --- a/static/index.html +++ b/static/index.html @@ -76,6 +76,7 @@ Config +