From 913eec34eaa6c5d59b6408693bc0bff9fedefa68 Mon Sep 17 00:00:00 2001 From: Chris Amow Date: Sun, 23 Aug 2026 21:10:09 -0500 Subject: [PATCH] lower time frame trendlines blocked from dma chart --- docs/implementation.md | 6 ++++++ docs/plan.md | 11 +++++++---- static/app.js | 3 ++- static/chart.js | 5 +++++ tests/e2e/trendline.test.mjs | 36 ++++++++++++++++++++++++++++++++++++ 5 files changed, 56 insertions(+), 5 deletions(-) diff --git a/docs/implementation.md b/docs/implementation.md index 3c0df1d..c8c516b 100644 --- a/docs/implementation.md +++ b/docs/implementation.md @@ -1094,6 +1094,12 @@ 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. + ### 2026-08-15 — 1m zoom-out died at ~1am because the socket sent 1,000 bars Compressing the 1m time scale stopped around 01:00, then looked empty. diff --git a/docs/plan.md b/docs/plan.md index 3c0cf2e..ac71ef7 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -122,8 +122,10 @@ if your parser doesn't filter those, that fixture will catch it. 1d base chart + THE DAILY MAs weight 16 ← a switchable base timeframe ``` -Base timeframe controls the candles only. **Every level stays visible on every base -timeframe** — the 200DMA on a 1-minute chart is the point, not a side effect. +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. --- @@ -917,8 +919,9 @@ LAYERS ☐ Hidden levels still count toward confluence ``` -- **The base timeframe switcher changes only the candles.** Every level stays on screen - — a 200DMA is equally valid on a 1m chart. That is the entire premise of the product. +- **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. - **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 7e251c3..891d50f 100644 --- a/static/app.js +++ b/static/app.js @@ -1347,7 +1347,8 @@ createApp({ function enabled(level) { 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; + return prefs.value.enabled.drawings && prefs.value.enabled.manual + && ConfluenceChart.manualLevelVisibleOn(level, timeframe.value); } if (level.kind === 'horizontal') return prefs.value.enabled.horizontal; if (level.kind === 'vwap') return prefs.value.enabled.vwap; diff --git a/static/chart.js b/static/chart.js index b3a363b..4073832 100644 --- a/static/chart.js +++ b/static/chart.js @@ -1250,6 +1250,11 @@ 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]; + } + manualLineDisplayStyle(level) { const baseWidth = level.line_width || 1; const displayedTf = this.bars[0]?.tf; diff --git a/tests/e2e/trendline.test.mjs b/tests/e2e/trendline.test.mjs index b1b30c8..00e204d 100644 --- a/tests/e2e/trendline.test.mjs +++ b/tests/e2e/trendline.test.mjs @@ -358,6 +358,42 @@ test('higher-timeframe trendlines are dashed and twice their stored width', }); }); +test('a lower-timeframe trendline is hidden from the daily chart', + { timeout: 180000 }, async () => { + await withChart(async page => { + const created = await page.evaluate(async () => { + const bars = (await (await fetch('/api/bars?tf=30m&limit=2')).json()).bars; + const first = bars[0]; + const second = bars[1]; + const response = await fetch('/api/lines', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + tf: '30m', side: 'support', anchor_t: first.t, anchor_p: first.l, + end_t: second.t, end_p: first.l + 1, note: `daily filter ${Date.now()}`, + }), + }); + return response.json(); + }); + await page.waitForFunction(id => window.__chart.levelSeries.has(id), created.id); + + await page.click('.timeframes button:text-is("1d")'); + await page.waitForFunction(() => window.__chart.bars[0]?.tf === '1d'); + await page.waitForFunction(id => !window.__chart.levelSeries.has(id), created.id); + const state = await page.evaluate(async id => ({ + drawingStillStored: (await (await fetch('/api/drawings')).json()).drawings + .some(drawing => drawing.id === id), + flatLevelVisible: ConfluenceChart.manualLevelVisibleOn( + { kind: 'manual', slope: 0, tf: '30m' }, '1d', + ), + }), created.id); + + assert.equal(state.drawingStillStored, true, 'daily filtering deleted the drawing state'); + assert.equal(state.flatLevelVisible, true, 'daily filtering hid a flat price level'); + assertNoPageErrors(page, assert); + }); + }); + test('a click, a move, then a click starts at the first click', { timeout: 180000 }, async () => { await withChart(async page => { const box = await chartBox(page);