line fix
This commit is contained in:
parent
e1d2c60af7
commit
73570d2904
3 changed files with 82 additions and 3 deletions
|
|
@ -1056,3 +1056,16 @@ or cutoff to the nearest canvas sample, never for the live-edge extension.
|
|||
Browser coverage requires the canvas series to own an already-existing future
|
||||
slot at the canonical source-space price, to extend its future tail on a new
|
||||
5m bar, and to have no SVG future-projection layer.
|
||||
|
||||
The production-only bend that remained was not renderer disagreement. Diagnostic
|
||||
mode showed #12 changing 0.139 points over the last historical display slot and
|
||||
0.015 over the first future slot, with screen slopes differing by the same factor.
|
||||
The final real 1m candles were nine minutes apart but Lightweight Charts placed
|
||||
them in adjacent logical slots; the future axis then resumed one-minute slots.
|
||||
The canonical source-space prices were right and the canvas point existed.
|
||||
|
||||
`futureSpace` now also owns whitespace timestamps inside short intraday data gaps,
|
||||
so nine elapsed minutes occupy nine logical slots and the line remains straight.
|
||||
Gaps over 30 minutes stay compressed, preserving the established treatment of the
|
||||
65-minute settlement break and weekends. A browser regression pins both the
|
||||
nine-slot spacing and equal historical/future screen slope.
|
||||
|
|
|
|||
|
|
@ -96,6 +96,10 @@ class ConfluenceChart {
|
|||
// convergence a few sessions out without dominating a zoomed-out view.
|
||||
static FUTURE_SLOTS = 180;
|
||||
|
||||
// Preserve short intraday gaps as empty slots. Longer closures remain
|
||||
// compressed, including the 65-minute CME settlement break and weekends.
|
||||
static MAX_INTRADAY_GAP_SECONDS = 30 * 60;
|
||||
|
||||
/**
|
||||
* The timeframe's spacing, measured rather than assumed.
|
||||
*
|
||||
|
|
@ -422,7 +426,7 @@ class ConfluenceChart {
|
|||
this.candles.setData(candleData);
|
||||
this.updateCurrentPrice(bars[bars.length - 1]?.c);
|
||||
this.volume.setData(bars.map(ConfluenceChart.toVolume));
|
||||
this.futureSpace.setData(this.futureTimes(bars));
|
||||
this.futureSpace.setData(this.timeScaleSlots(bars));
|
||||
// Anchored by time, not by logical index. A logical index addresses the
|
||||
// chart's *shared* scale — the union of every series' time points — not
|
||||
// this array. The daily MAs land straight after with hundreds of points
|
||||
|
|
@ -532,6 +536,20 @@ class ConfluenceChart {
|
|||
return slots;
|
||||
}
|
||||
|
||||
timeScaleSlots(bars) {
|
||||
const step = ConfluenceChart.barInterval(bars);
|
||||
if (!(step > 0)) return [];
|
||||
const slots = [];
|
||||
for (let index = 1; index < bars.length; index += 1) {
|
||||
const gap = bars[index].t - bars[index - 1].t;
|
||||
if (gap <= step || gap > ConfluenceChart.MAX_INTRADAY_GAP_SECONDS) continue;
|
||||
for (let time = bars[index - 1].t + step; time < bars[index].t; time += step) {
|
||||
slots.push({ time });
|
||||
}
|
||||
}
|
||||
return [...slots, ...this.futureTimes(bars)];
|
||||
}
|
||||
|
||||
updateBar(bar) {
|
||||
// update() throws on anything older than the series' last point, which
|
||||
// takes the whole app down rather than dropping one stale bar. Ticks made
|
||||
|
|
@ -558,8 +576,8 @@ class ConfluenceChart {
|
|||
if (!(step > 0)) return;
|
||||
// A session gap invalidates the old future horizon. Rebuild only the small
|
||||
// manual-line series; ordinary bars extend their already-owned future tail.
|
||||
if (previous && bar.t - previous.t > step * 2) {
|
||||
this.futureSpace.setData(this.futureTimes(this.bars));
|
||||
if (previous && bar.t - previous.t > step) {
|
||||
this.futureSpace.setData(this.timeScaleSlots(this.bars));
|
||||
for (const level of this.levels) {
|
||||
if (level.kind !== 'manual' || level.hidden || ConfluenceChart.isFlat(level)) continue;
|
||||
this.levelSeries.get(level.id)?.series.setData(this.seriesLineData(level));
|
||||
|
|
|
|||
|
|
@ -164,6 +164,54 @@ test('future 5m projections keep their slope after the settlement break',
|
|||
});
|
||||
});
|
||||
|
||||
test('a short final data gap does not kink a future trendline',
|
||||
{ timeout: 180000 }, async () => {
|
||||
await withChart(async page => {
|
||||
const result = await page.evaluate(() => {
|
||||
const c = window.__chart;
|
||||
const minute = 60;
|
||||
const bars = Array.from({ length: 20 }, (_, index) => ({
|
||||
tf: '1m', t: 100000 + index * minute, o: 100, h: 101, l: 99, c: 100,
|
||||
v: 1, closed: true,
|
||||
}));
|
||||
bars.push({ ...bars.at(-1), t: bars.at(-1).t + 9 * minute });
|
||||
const line = {
|
||||
id: 'sparse-tail', kind: 'manual', tf: '30m', side: 'support', label: 'probe',
|
||||
anchor_t: bars[0].t, anchor_p: 100, last_t: bars[0].t + 30 * minute,
|
||||
slope: 3 / (30 * minute), cutoff_t: null, hidden: false, color: '#65b7cf',
|
||||
line_width: 2, provisional: false,
|
||||
};
|
||||
const sourceTimes = Array.from({ length: 4 }, (_, index) => bars[0].t + index * 30 * minute);
|
||||
c.setBars(bars);
|
||||
c.setTrendlineGeometry({
|
||||
mode: 'source_tf', series: { '30m': { times: sourceTimes, duration: 30 * minute } },
|
||||
});
|
||||
c.syncLevels([line]);
|
||||
const previousTime = bars.at(-2).t;
|
||||
const currentTime = bars.at(-1).t;
|
||||
const futureTime = currentTime + minute;
|
||||
const point = time => ({
|
||||
x: c.chart.timeScale().timeToCoordinate(time),
|
||||
y: c.candles.priceToCoordinate(c.linePriceAtIndex(line, c.indexAt(time))),
|
||||
});
|
||||
const previous = point(previousTime);
|
||||
const current = point(currentTime);
|
||||
const future = point(futureTime);
|
||||
return {
|
||||
historicalSlope: (current.y - previous.y) / (current.x - previous.x),
|
||||
futureSlope: (future.y - current.y) / (future.x - current.x),
|
||||
historicalSlots: (current.x - previous.x) / (future.x - current.x),
|
||||
};
|
||||
});
|
||||
|
||||
assert.ok(Math.abs(result.historicalSlots - 9) < 0.01,
|
||||
`a nine-minute data gap occupied ${result.historicalSlots} slots`);
|
||||
assert.ok(Math.abs(result.historicalSlope - result.futureSlope) < 1e-9,
|
||||
`line kinked from ${result.historicalSlope} to ${result.futureSlope}`);
|
||||
assertNoPageErrors(page, assert);
|
||||
});
|
||||
});
|
||||
|
||||
test('a manual line series owns its future 5m slots without an SVG projection',
|
||||
{ timeout: 180000 }, async () => {
|
||||
await withChart(async page => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue