trendline extension slope fix

This commit is contained in:
Chris Amow 2026-08-14 23:15:00 -05:00
parent aae47b6681
commit 333ca4c111
3 changed files with 64 additions and 2 deletions

View file

@ -925,6 +925,12 @@ Worth generalising: a measurement that looks absurd is worth chasing rather than
clamping. The absurd number here was "67 bars of padding", and the bug behind it
had nothing to do with padding.
The same median must govern every conversion past the final bar, not only the
future slots. Leaving `timeAtIndex()` and `indexAt()` on their final raw gap
made the first future 5m slot after the 17:00-18:00 settlement break advance a
trendline by 65 minutes. Source-timeframe lines therefore became thirteen times
steeper immediately beyond the live edge. All three paths now use `barInterval`.
### 2026-08-14 — trendlineproblem: high-TF lines used the wrong bar grid
**Confirmed and fixed behind `TRENDLINE_SOURCE_GEOMETRY`.** No drawing records

View file

@ -1661,7 +1661,10 @@ class ConfluenceChart {
}
const last = bars.length - 1;
if (time >= bars[last].t) {
const step = bars[last].t - bars[last - 1].t;
// Future slots use the median interval, not the tail gap. After the CME
// settlement break the final gap is 65 minutes, and using it here makes a
// 5m future projection advance thirteen bars per visible slot.
const step = ConfluenceChart.barInterval(bars);
return last + (step ? (time - bars[last].t) / step : 0);
}
let lo = 0;
@ -1759,7 +1762,8 @@ class ConfluenceChart {
if (index <= 0) return Math.round(bars[0].t + index * (bars[1].t - bars[0].t));
const last = bars.length - 1;
if (index >= last) {
return Math.round(bars[last].t + (index - last) * (bars[last].t - bars[last - 1].t));
const step = ConfluenceChart.barInterval(bars);
return Math.round(bars[last].t + (index - last) * step);
}
const lower = Math.floor(index);
const fraction = index - lower;

View file

@ -106,6 +106,58 @@ test('a 30m line keeps source geometry when the 1m window starts after its ancho
assert.equal(await page.locator('.chart-line-hit').evaluate(node => getComputedStyle(node).fill),
'none', 'the invisible polyline hit region filled the chart as a black polygon');
assertNoPageErrors(page, assert);
});
});
test('future 5m projections keep their slope after the settlement break',
{ timeout: 180000 }, async () => {
await withChart(async page => {
const result = await page.evaluate(() => {
const c = window.__chart;
const fiveMinutes = 5 * 60;
const thirtyMinutes = 30 * 60;
// The final 65-minute gap is the 17:00-18:00 CME settlement break.
const displayed = Array.from({ length: 30 }, (_, index) => ({
tf: '5m', t: index * fiveMinutes, o: 100, h: 101, l: 99, c: 100, v: 1, closed: true,
}));
displayed.push({
tf: '5m', t: displayed.at(-1).t + 13 * fiveMinutes,
o: 100, h: 101, l: 99, c: 100, v: 1, closed: true,
});
const line = {
id: 'settlement-gap', kind: 'manual', tf: '30m',
anchor_t: 0, anchor_p: 100, last_t: thirtyMinutes,
slope: 6 / thirtyMinutes, cutoff_t: null,
};
c.bars = displayed;
c.setTrendlineGeometry({
mode: 'source_tf',
series: {
'30m': {
times: [0, thirtyMinutes, 2 * thirtyMinutes, 3 * thirtyMinutes,
4 * thirtyMinutes, 7 * thirtyMinutes],
duration: thirtyMinutes,
future_times: [8 * thirtyMinutes, 9 * thirtyMinutes],
},
},
});
const last = displayed.length - 1;
const before = c.linePriceAtIndex(line, last - 1);
const current = c.linePriceAtIndex(line, last);
const future = c.linePriceAtIndex(line, last + 1);
return {
historicalChange: current - before,
futureChange: future - current,
futureTime: c.timeAtIndex(last + 1),
expectedFutureTime: displayed[last].t + fiveMinutes,
};
});
assert.equal(result.futureTime, result.expectedFutureTime,
'one future 5m slot inherited the 65-minute settlement gap');
assert.ok(Math.abs(result.futureChange - result.historicalChange) < 1e-9,
`future slope changed from ${result.historicalChange} to ${result.futureChange}`);
assertNoPageErrors(page, assert);
});
});