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
|
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
|
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.
|
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.
|
// convergence a few sessions out without dominating a zoomed-out view.
|
||||||
static FUTURE_SLOTS = 180;
|
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.
|
* The timeframe's spacing, measured rather than assumed.
|
||||||
*
|
*
|
||||||
|
|
@ -422,7 +426,7 @@ class ConfluenceChart {
|
||||||
this.candles.setData(candleData);
|
this.candles.setData(candleData);
|
||||||
this.updateCurrentPrice(bars[bars.length - 1]?.c);
|
this.updateCurrentPrice(bars[bars.length - 1]?.c);
|
||||||
this.volume.setData(bars.map(ConfluenceChart.toVolume));
|
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
|
// Anchored by time, not by logical index. A logical index addresses the
|
||||||
// chart's *shared* scale — the union of every series' time points — not
|
// chart's *shared* scale — the union of every series' time points — not
|
||||||
// this array. The daily MAs land straight after with hundreds of points
|
// this array. The daily MAs land straight after with hundreds of points
|
||||||
|
|
@ -532,6 +536,20 @@ class ConfluenceChart {
|
||||||
return slots;
|
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) {
|
updateBar(bar) {
|
||||||
// update() throws on anything older than the series' last point, which
|
// update() throws on anything older than the series' last point, which
|
||||||
// takes the whole app down rather than dropping one stale bar. Ticks made
|
// takes the whole app down rather than dropping one stale bar. Ticks made
|
||||||
|
|
@ -558,8 +576,8 @@ class ConfluenceChart {
|
||||||
if (!(step > 0)) return;
|
if (!(step > 0)) return;
|
||||||
// A session gap invalidates the old future horizon. Rebuild only the small
|
// A session gap invalidates the old future horizon. Rebuild only the small
|
||||||
// manual-line series; ordinary bars extend their already-owned future tail.
|
// manual-line series; ordinary bars extend their already-owned future tail.
|
||||||
if (previous && bar.t - previous.t > step * 2) {
|
if (previous && bar.t - previous.t > step) {
|
||||||
this.futureSpace.setData(this.futureTimes(this.bars));
|
this.futureSpace.setData(this.timeScaleSlots(this.bars));
|
||||||
for (const level of this.levels) {
|
for (const level of this.levels) {
|
||||||
if (level.kind !== 'manual' || level.hidden || ConfluenceChart.isFlat(level)) continue;
|
if (level.kind !== 'manual' || level.hidden || ConfluenceChart.isFlat(level)) continue;
|
||||||
this.levelSeries.get(level.id)?.series.setData(this.seriesLineData(level));
|
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',
|
test('a manual line series owns its future 5m slots without an SVG projection',
|
||||||
{ timeout: 180000 }, async () => {
|
{ timeout: 180000 }, async () => {
|
||||||
await withChart(async page => {
|
await withChart(async page => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue