potential 1m fix

This commit is contained in:
Chris Amow 2026-08-26 02:27:32 -05:00
parent eb18de9bb9
commit c28b0ce535
2 changed files with 70 additions and 5 deletions

View file

@ -2514,10 +2514,18 @@ class ConfluenceChart {
if (bar.t >= level.anchor_t && bar.t <= cutoff) stamps.add(bar.t); if (bar.t >= level.anchor_t && bar.t <= cutoff) stamps.add(bar.t);
} }
} else { } else {
const displayed = this.bars.filter(bar => bar.t >= level.anchor_t && bar.t <= cutoff);
for (const time of ConfluenceChart.sourceTimes(source) || []) { for (const time of ConfluenceChart.sourceTimes(source) || []) {
if (time >= first && time <= lastBar && time >= level.anchor_t && time <= cutoff) { if (time < first || time > lastBar || time < level.anchor_t || time > cutoff) continue;
stamps.add(time); let lo = 0;
let hi = displayed.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (displayed[mid].t < time) lo = mid + 1;
else hi = mid;
} }
const bar = displayed[lo] || displayed[displayed.length - 1];
if (bar) stamps.add(bar.t);
} }
if (lastBar >= level.anchor_t && lastBar <= cutoff) stamps.add(lastBar); if (lastBar >= level.anchor_t && lastBar <= cutoff) stamps.add(lastBar);
} }

View file

@ -94,9 +94,10 @@ test('a 30m line keeps source geometry when the 1m window starts after its ancho
'the rollback path no longer restores displayed-grid geometry'); 'the rollback path no longer restores displayed-grid geometry');
assert.equal(result.shifted, result.sourceSecondBar, assert.equal(result.shifted, result.sourceSecondBar,
'a source-timeframe nudge did not move by one 30m bar'); 'a source-timeframe nudge did not move by one 30m bar');
assert.deepEqual(result.sampledTimes.slice(0, result.displayedTimes.length), result.displayedTimes, const historical = result.sampledTimes.filter(time => time <= result.displayedTimes.at(-1));
'manual line samples no longer match the displayed candle grid'); assert.deepEqual(historical, [result.displayedTimes[0], result.displayedTimes.at(-1)],
assert.ok(result.sampledTimes.slice(result.displayedTimes.length) 'a 30m line on 1m should sample source-bar times, not every 1m candle');
assert.ok(result.sampledTimes.slice(historical.length)
.every(time => time > result.displayedTimes.at(-1)), .every(time => time > result.displayedTimes.at(-1)),
'manual-line extension mixed future points into candle history'); 'manual-line extension mixed future points into candle history');
assert.deepEqual(result.trimmedTimes, result.expectedTrimmedTimes, assert.deepEqual(result.trimmedTimes, result.expectedTrimmedTimes,
@ -353,6 +354,62 @@ test('a short final data gap does not kink a future trendline',
}); });
}); });
test('a 30m line stays straight on 1m when the bucket is missing its first minutes',
{ timeout: 180000 }, async () => {
await withChart(async page => {
const result = await page.evaluate(() => {
const c = window.__chart;
const minute = 60;
const half = 30 * minute;
const open = 400000;
const bars = [];
for (let t = open - 2 * half; t < open; t += minute) {
bars.push({ tf: '1m', t, o: 100, h: 101, l: 99, c: 100, v: 1, closed: true });
}
for (let t = open + 9 * minute; t < open + 2 * half; t += minute) {
bars.push({ tf: '1m', t, o: 100, h: 101, l: 99, c: 100, v: 1, closed: true });
}
const line = {
id: 'late-open', kind: 'manual', tf: '30m', side: 'support', label: 'late',
anchor_t: open - 2 * half, anchor_p: 100, last_t: open,
slope: 3 / half, cutoff_t: null, hidden: false, color: '#65b7cf',
line_width: 2, provisional: false,
};
c.setDisplayFutureTimes(Array.from(
{ length: ConfluenceChart.FUTURE_SLOTS }, (_, index) => bars.at(-1).t + (index + 1) * minute,
));
c.setBars(bars);
c.setTrendlineGeometry({
mode: 'source_tf',
series: { '30m': { times: [open - 2 * half, open - half, open, open + half], duration: half } },
});
c.syncLevels([line]);
const data = c.levelSeries.get(line.id).series.data();
const point = time => {
const row = data.find(value => Number(value.time) === time);
return {
x: c.chart.timeScale().timeToCoordinate(time),
y: row ? c.candles.priceToCoordinate(row.value) : null,
};
};
const a = point(open - half);
const b = point(open + 9 * minute);
const d = point(open + half);
return {
snappedOpen: data.some(value => Number(value.time) === open + 9 * minute),
injectedOpen: data.some(value => Number(value.time) === open),
lateSlope: (b.y - a.y) / (b.x - a.x),
fullSlope: (d.y - b.y) / (d.x - b.x),
};
});
assert.equal(result.injectedOpen, false, '30m open that is not a 1m bar was placed on the scale');
assert.equal(result.snappedOpen, true, 'missing 30m open was not snapped to the first 1m bar');
assert.ok(Math.abs(result.lateSlope - result.fullSlope) < 1e-6,
`late-open 30m segment kinked from ${result.lateSlope} to ${result.fullSlope}`);
assertNoPageErrors(page, assert);
});
});
test('painted canvas pixels keep the same slope through the live edge', test('painted canvas pixels keep the same slope through the live edge',
{ timeout: 180000 }, async () => { { timeout: 180000 }, async () => {
await withChart(async page => { await withChart(async page => {