drawings from higher timeframes to lower fixes
This commit is contained in:
parent
913eec34ea
commit
eaf118ef8b
7 changed files with 120 additions and 26 deletions
|
|
@ -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
|
anchor cannot introduce a visible style seam. Browser coverage checks 30m-on-5m
|
||||||
and native 30m rendering separately.
|
and native 30m rendering separately.
|
||||||
|
|
||||||
Sloped manual lines also stop propagating upward: a 30m line is available on
|
Drawing objects also stop propagating upward by default: a 30m trendline, price
|
||||||
30m and lower charts but hidden on 1h/1d. This is a display filter only; the
|
level, Fibonacci drawing, comment, or symbol is available on 30m and lower
|
||||||
drawing remains loaded and flat price levels remain visible everywhere. The
|
charts but hidden on 1h/1d. **Hide lower-TF drawings** in Config persists this
|
||||||
daily-chart regression creates a real 30m drawing, confirms it is rendered on
|
display preference in `localStorage`, defaults on, and can restore every drawing
|
||||||
the lower chart, switches to 1d, and requires only its series to disappear.
|
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
|
### 2026-08-15 — 1m zoom-out died at ~1am because the socket sent 1,000 bars
|
||||||
|
|
||||||
|
|
|
||||||
15
docs/plan.md
15
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
|
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
|
Derived overlays stay visible on every base timeframe — the 200DMA on a 1-minute
|
||||||
200DMA on a 1-minute chart is the point, not a side effect. Sloped manual
|
chart is the point, not a side effect. Drawing objects follow one configurable
|
||||||
trendlines are visible on their attributed timeframe and lower charts, but not
|
rule: visible on their attributed timeframe and lower charts, but not on higher
|
||||||
on higher charts: 30m structure belongs on 30m/15m/5m/1m, not on 1h or 1d.
|
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
|
☐ Hidden levels still count toward confluence
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Derived and flat levels stay across base timeframes.** A 200DMA is equally
|
- **Derived overlays stay across base timeframes.** A 200DMA is equally valid on
|
||||||
valid on a 1m chart. Sloped manual trendlines are the exception: hide them
|
a 1m chart. Drawing objects default to native/lower charts only. The persisted
|
||||||
above their attributed timeframe while retaining them on native/lower charts.
|
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
|
- **The group checkbox is a master toggle** — unchecking "Daily MAs" hides all five at
|
||||||
once; individual periods nest under it.
|
once; individual periods nest under it.
|
||||||
- The colour swatch beside each timeframe is that timeframe's hue, used identically on
|
- The colour swatch beside each timeframe is that timeframe's hue, used identically on
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,7 @@ createApp({
|
||||||
const animateCurrentPrice = ref(localStorage.getItem('chart-animate-current-price') !== 'false');
|
const animateCurrentPrice = ref(localStorage.getItem('chart-animate-current-price') !== 'false');
|
||||||
const autoScrollLivePrice = ref(localStorage.getItem('chart-auto-scroll-live-price') !== 'false');
|
const autoScrollLivePrice = ref(localStorage.getItem('chart-auto-scroll-live-price') !== 'false');
|
||||||
const extraDetail = ref(localStorage.getItem('chart-extra-detail') === 'true');
|
const extraDetail = ref(localStorage.getItem('chart-extra-detail') === 'true');
|
||||||
|
const hideLowerTfDrawings = ref(localStorage.getItem('chart-hide-lower-tf-drawings') !== 'false');
|
||||||
const optionPrefs = (() => {
|
const optionPrefs = (() => {
|
||||||
try { return JSON.parse(localStorage.getItem('chart-es-options')) || {}; }
|
try { return JSON.parse(localStorage.getItem('chart-es-options')) || {}; }
|
||||||
catch { return {}; }
|
catch { return {}; }
|
||||||
|
|
@ -721,14 +722,24 @@ createApp({
|
||||||
comments.value = (payload.drawings || []).filter(item => ['comment', 'symbol'].includes(item.kind));
|
comments.value = (payload.drawings || []).filter(item => ['comment', 'symbol'].includes(item.kind));
|
||||||
fibs.value = (payload.drawings || []).filter(item => item.kind === 'fibonacci');
|
fibs.value = (payload.drawings || []).filter(item => item.kind === 'fibonacci');
|
||||||
syncComments();
|
syncComments();
|
||||||
if (chartApi) chartApi.setFibs(prefs.value.enabled.drawings
|
syncFibs();
|
||||||
? fibs.value.filter(item => !item.hidden) : []);
|
}
|
||||||
|
|
||||||
|
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() {
|
function syncComments() {
|
||||||
if (!chartApi) return;
|
if (!chartApi) return;
|
||||||
const visible = prefs.value.enabled.drawings
|
const visible = prefs.value.enabled.drawings
|
||||||
? comments.value.filter(item => !item.hidden)
|
? comments.value.filter(item => !item.hidden && drawingVisible(item))
|
||||||
: [];
|
: [];
|
||||||
chartApi.setComments(visible);
|
chartApi.setComments(visible);
|
||||||
}
|
}
|
||||||
|
|
@ -829,7 +840,7 @@ createApp({
|
||||||
pinned: !commentFloat.value,
|
pinned: !commentFloat.value,
|
||||||
anchor_t: result.time ?? null,
|
anchor_t: result.time ?? null,
|
||||||
anchor_p: result.price ?? null,
|
anchor_p: result.price ?? null,
|
||||||
x: result.x, y: result.y,
|
x: result.x, y: result.y, tf: timeframe.value,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
@ -850,6 +861,7 @@ createApp({
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
text: choice.name, icon, color: symbolColor.value, scale: symbolScale.value, pinned: true,
|
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,
|
anchor_t: result.time, anchor_p: result.price, x: result.x, y: result.y,
|
||||||
|
tf: timeframe.value,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
@ -994,7 +1006,7 @@ createApp({
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
price: alertPrice.value, note: alertNote.value,
|
price: alertPrice.value, note: alertNote.value,
|
||||||
color: levelColor.value, line_width: levelWidth.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; }
|
if (!response.ok) { console.error(`Unable to add price alert: HTTP ${response.status}`); return; }
|
||||||
|
|
@ -1339,6 +1351,9 @@ createApp({
|
||||||
function selectTimeframe(tf) {
|
function selectTimeframe(tf) {
|
||||||
timeframe.value = tf;
|
timeframe.value = tf;
|
||||||
prefs.value.base_tf = tf;
|
prefs.value.base_tf = tf;
|
||||||
|
syncVisibleLevels();
|
||||||
|
syncComments();
|
||||||
|
syncFibs();
|
||||||
if (socket?.readyState === WebSocket.OPEN) {
|
if (socket?.readyState === WebSocket.OPEN) {
|
||||||
socket.send(JSON.stringify({ type: 'subscribe', tf }));
|
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 === 'ma') return (prefs.value.enabled.ma[level.tf] || []).includes(level.period);
|
||||||
if (level.kind === 'manual') {
|
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);
|
&& drawingVisible(level);
|
||||||
}
|
}
|
||||||
if (level.kind === 'horizontal') return prefs.value.enabled.horizontal;
|
if (level.kind === 'horizontal') return prefs.value.enabled.horizontal;
|
||||||
if (level.kind === 'vwap') return prefs.value.enabled.vwap;
|
if (level.kind === 'vwap') return prefs.value.enabled.vwap;
|
||||||
|
|
@ -1408,8 +1423,7 @@ createApp({
|
||||||
syncVisibleLevels();
|
syncVisibleLevels();
|
||||||
syncComments();
|
syncComments();
|
||||||
if (chartApi) chartApi.setRthEnabled(prefs.value.enabled.rth);
|
if (chartApi) chartApi.setRthEnabled(prefs.value.enabled.rth);
|
||||||
if (chartApi) chartApi.setFibs(prefs.value.enabled.drawings
|
syncFibs();
|
||||||
? fibs.value.filter(item => !item.hidden) : []);
|
|
||||||
sendPrefs();
|
sendPrefs();
|
||||||
}, { deep: true });
|
}, { deep: true });
|
||||||
|
|
||||||
|
|
@ -1475,6 +1489,13 @@ createApp({
|
||||||
localStorage.setItem('chart-extra-detail', String(value));
|
localStorage.setItem('chart-extra-detail', String(value));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(hideLowerTfDrawings, value => {
|
||||||
|
localStorage.setItem('chart-hide-lower-tf-drawings', String(value));
|
||||||
|
syncVisibleLevels();
|
||||||
|
syncComments();
|
||||||
|
syncFibs();
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
chartApi = new ConfluenceChart();
|
chartApi = new ConfluenceChart();
|
||||||
// Deliberate debug handle. Chart geometry bugs are invisible from the
|
// Deliberate debug handle. Chart geometry bugs are invisible from the
|
||||||
|
|
@ -1518,6 +1539,6 @@ createApp({
|
||||||
window.removeEventListener('keydown', handleKeydown);
|
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');
|
}).mount('#app');
|
||||||
|
|
|
||||||
|
|
@ -1250,9 +1250,10 @@ class ConfluenceChart {
|
||||||
return level.kind === 'ma' || level.kind === 'vwap' || level.kind === 'horizontal';
|
return level.kind === 'ma' || level.kind === 'vwap' || level.kind === 'horizontal';
|
||||||
}
|
}
|
||||||
|
|
||||||
static manualLevelVisibleOn(level, displayedTf) {
|
static drawingVisibleOn(drawing, displayedTf) {
|
||||||
if (level.kind !== 'manual' || ConfluenceChart.isFlat(level)) return true;
|
const drawingRank = ConfluenceChart.tfRank[drawing?.tf];
|
||||||
return ConfluenceChart.tfRank[level.tf] >= ConfluenceChart.tfRank[displayedTf];
|
const displayedRank = ConfluenceChart.tfRank[displayedTf];
|
||||||
|
return drawingRank == null || displayedRank == null || drawingRank >= displayedRank;
|
||||||
}
|
}
|
||||||
|
|
||||||
manualLineDisplayStyle(level) {
|
manualLineDisplayStyle(level) {
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@
|
||||||
<summary>Config</summary>
|
<summary>Config</summary>
|
||||||
<label><input type="checkbox" v-model="animateCurrentPrice"> Animate current price</label>
|
<label><input type="checkbox" v-model="animateCurrentPrice"> Animate current price</label>
|
||||||
<label><input type="checkbox" v-model="autoScrollLivePrice"> Autoscroll to live price</label>
|
<label><input type="checkbox" v-model="autoScrollLivePrice"> Autoscroll to live price</label>
|
||||||
|
<label><input type="checkbox" v-model="hideLowerTfDrawings"> Hide lower-TF drawings</label>
|
||||||
<label><input type="checkbox" v-model="extraDetail"> Extra detail</label>
|
<label><input type="checkbox" v-model="extraDetail"> Extra detail</label>
|
||||||
</details>
|
</details>
|
||||||
<details class="sidebar-section tools-section" open>
|
<details class="sidebar-section tools-section" open>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,68 @@ import {
|
||||||
withChart, chartBox, at, armTool, drawingIds, assertNoPageErrors,
|
withChart, chartBox, at, armTool, drawingIds, assertNoPageErrors,
|
||||||
} from './helpers.mjs';
|
} from './helpers.mjs';
|
||||||
|
|
||||||
|
test('the timeframe visibility setting governs every drawing type',
|
||||||
|
{ timeout: 180000 }, async () => {
|
||||||
|
await withChart(async page => {
|
||||||
|
const ids = await page.evaluate(async () => {
|
||||||
|
const bars = (await (await fetch('/api/bars?tf=30m&limit=2')).json()).bars;
|
||||||
|
const [first, second] = bars;
|
||||||
|
const post = async (url, body) => (await fetch(url, {
|
||||||
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
})).json();
|
||||||
|
const comment = await post('/api/comments', {
|
||||||
|
text: `tf comment ${Date.now()}`, tf: '30m', pinned: true,
|
||||||
|
anchor_t: first.t, anchor_p: first.c,
|
||||||
|
});
|
||||||
|
const symbol = await post('/api/comments', {
|
||||||
|
text: 'TF symbol', icon: 'skull', tf: '30m', pinned: true,
|
||||||
|
anchor_t: first.t, anchor_p: first.c,
|
||||||
|
});
|
||||||
|
const fib = await post('/api/lines', {
|
||||||
|
kind: 'fibonacci', tf: '30m', side: 'support',
|
||||||
|
anchor_t: first.t, anchor_p: first.l, end_t: second.t, end_p: second.h,
|
||||||
|
});
|
||||||
|
const level = await post('/api/lines/price', {
|
||||||
|
tf: '30m', price: first.c, note: `tf level ${Date.now()}`,
|
||||||
|
});
|
||||||
|
return { comment: comment.id, symbol: symbol.id, fib: fib.id, level: level.id };
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.reload({ waitUntil: 'networkidle' });
|
||||||
|
await page.waitForFunction(() => window.__chart?.bars?.length > 0);
|
||||||
|
const allVisible = expected => page.waitForFunction(([values, visible]) => {
|
||||||
|
const chart = window.__chart;
|
||||||
|
const comments = chart.comments.some(item => item.id === values.comment)
|
||||||
|
&& chart.comments.some(item => item.id === values.symbol);
|
||||||
|
const fib = chart.fibs.some(item => item.id === values.fib);
|
||||||
|
const level = chart.priceLines.has(values.level);
|
||||||
|
return visible ? comments && fib && level : !comments && !fib && !level;
|
||||||
|
}, [ids, expected]);
|
||||||
|
await allVisible(true);
|
||||||
|
|
||||||
|
await page.click('.timeframes button:text-is("1d")');
|
||||||
|
await page.waitForFunction(() => window.__chart.bars[0]?.tf === '1d');
|
||||||
|
await allVisible(false);
|
||||||
|
|
||||||
|
await page.locator('.config-section summary').click();
|
||||||
|
const setting = page.locator('.config-section label:has-text("Hide lower-TF drawings") input');
|
||||||
|
assert.equal(await setting.isChecked(), true, 'timeframe filtering does not default on');
|
||||||
|
await setting.uncheck();
|
||||||
|
await allVisible(true);
|
||||||
|
assert.equal(await page.evaluate(() => localStorage.getItem('chart-hide-lower-tf-drawings')),
|
||||||
|
'false', 'the Config choice was not persisted');
|
||||||
|
await page.reload({ waitUntil: 'networkidle' });
|
||||||
|
await page.waitForFunction(() => window.__chart?.bars?.length > 0);
|
||||||
|
await page.locator('.config-section summary').click();
|
||||||
|
assert.equal(await page.locator(
|
||||||
|
'.config-section label:has-text("Hide lower-TF drawings") input').isChecked(), false,
|
||||||
|
'the Config choice reset on reload');
|
||||||
|
await allVisible(true);
|
||||||
|
assertNoPageErrors(page, assert);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('editing a drawing name with Backspace or Delete cannot delete the drawing',
|
test('editing a drawing name with Backspace or Delete cannot delete the drawing',
|
||||||
{
|
{
|
||||||
timeout: 180000,
|
timeout: 180000,
|
||||||
|
|
|
||||||
|
|
@ -383,13 +383,17 @@ test('a lower-timeframe trendline is hidden from the daily chart',
|
||||||
const state = await page.evaluate(async id => ({
|
const state = await page.evaluate(async id => ({
|
||||||
drawingStillStored: (await (await fetch('/api/drawings')).json()).drawings
|
drawingStillStored: (await (await fetch('/api/drawings')).json()).drawings
|
||||||
.some(drawing => drawing.id === id),
|
.some(drawing => drawing.id === id),
|
||||||
flatLevelVisible: ConfluenceChart.manualLevelVisibleOn(
|
flatLevelVisible: ConfluenceChart.drawingVisibleOn(
|
||||||
{ kind: 'manual', slope: 0, tf: '30m' }, '1d',
|
{ kind: 'manual', slope: 0, tf: '30m' }, '1d',
|
||||||
),
|
),
|
||||||
}), created.id);
|
}), created.id);
|
||||||
|
|
||||||
assert.equal(state.drawingStillStored, true, 'daily filtering deleted the drawing state');
|
assert.equal(state.drawingStillStored, true, 'daily filtering deleted the drawing state');
|
||||||
assert.equal(state.flatLevelVisible, true, 'daily filtering hid a flat price level');
|
assert.equal(state.flatLevelVisible, false, 'a lower-TF flat level bypassed the general rule');
|
||||||
|
|
||||||
|
await page.locator('.config-section summary').click();
|
||||||
|
await page.locator('.config-section label:has-text("Hide lower-TF drawings") input').uncheck();
|
||||||
|
await page.waitForFunction(id => window.__chart.levelSeries.has(id), created.id);
|
||||||
assertNoPageErrors(page, assert);
|
assertNoPageErrors(page, assert);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue