Stop rebuilding the 1m time scale on every forming tick.
Identical future_times are now a no-op, and HTF ticks only resync lines when a source bar is actually appended. Document the slotted-hole kink.
This commit is contained in:
parent
f5ba154c98
commit
60ffaf4657
5 changed files with 99 additions and 15 deletions
|
|
@ -152,6 +152,10 @@ geometry change that needs explicit browser verification.
|
|||
- The displayed time scale, a drawing's source-timeframe bar space, and the
|
||||
server session calendar are different coordinate systems. Never substitute
|
||||
wall-clock seconds or the displayed grid for canonical source geometry.
|
||||
- A short 1m hole that already owns empty `futureSpace` slots still kinks a
|
||||
line if source index treats the surrounding bars as adjacent. Count both
|
||||
before changing geometry — see the 2026-08-26 entry in
|
||||
`docs/implementation.md`. Settlement and weekend compression are intended.
|
||||
- Daily and intraday future slots must skip non-session time. An endpoint that
|
||||
looks valid before a settlement/weekend break must remain resolvable when real
|
||||
history arrives.
|
||||
|
|
@ -258,6 +262,10 @@ cost on the hot path unless the screen or an alert has to change.
|
|||
not that.** `updateBar` must not `scheduleOverlays(true)` on every
|
||||
tick — that walked every trendline across 5k bars at ~4 Hz and
|
||||
melted the tab. We wrote this section and then shipped that anyway.
|
||||
- 1m is the expensive chart. A forming tick must not
|
||||
`futureSpace.setData`, `syncLevels`, or walk source times. Identical
|
||||
`future_times` are a no-op; `trendline_bar` only rebuilds lines when
|
||||
a source timestamp is actually appended.
|
||||
- Diagnostic `getImageData` / painted-pixel sampling is never on the
|
||||
live overlay path. `?diag=1` is not a license to sync the GPU every
|
||||
frame.
|
||||
|
|
|
|||
|
|
@ -1337,18 +1337,52 @@ Finally, source-dependent shift operations no longer silently keep the old time
|
|||
or fall back to displayed geometry beyond the supplied horizon. Drag, duplicate,
|
||||
and nudge abort cleanly when the canonical shift cannot be represented.
|
||||
|
||||
### 2026-08-26 — prod 1m holes were slotted, source index was not
|
||||
### 2026-08-26 — bent 1m trendlines: slotted holes, uncounted source index
|
||||
|
||||
A production 1m dump showed 23 holes. Every 1–10 minute tape hole already owned
|
||||
its empty `futureSpace` slots, including the Yahoo→Schwab join at 02:18–02:27
|
||||
CDT. Settlement (15:59–17:00) and the weekend stay compressed on purpose.
|
||||
This looked like a live-edge renderer bug, then like a Yahoo→Schwab join hole,
|
||||
then like missing `futureSpace` slots. It was none of those. Write this down
|
||||
before "fixing" the next kink.
|
||||
|
||||
The remaining bend is source index: a 1m/5m line still treated the bars on
|
||||
either side of a slotted hole as adjacent, so nine screen slots carried one
|
||||
minute of price. `fill_short_gaps` now inserts the missing bucket opens (same
|
||||
30-minute bound as the display) in trendline geometry and in
|
||||
`price_in_timeframe_space`. Weekends and settlement are unchanged.
|
||||
**What it looked like.** 30m/1m manual lines on the production 1m chart bent
|
||||
mid-session. Local Yahoo-only did not. The join after each Schwab deploy is a
|
||||
~9-minute 1m hole (today 02:18→02:27 CDT). That was the obvious suspect.
|
||||
|
||||
`?diag=1` also posts one `kind=geometry` SNAPDBG after bars load — gap ownership
|
||||
and off-median line segments — so the next check is `logs --since 20m`, not
|
||||
another console paste.
|
||||
**What we measured.** Last-80 1m times had that one hole. The full tape had 23
|
||||
holes. For each hole, count missing minutes vs timestamps already owned by
|
||||
`futureSpace`:
|
||||
|
||||
- 1–10 minute Yahoo holes, including the 02:18 join and the nightly
|
||||
22:58–23:09 gap: `owned === missing`. Slots were already there.
|
||||
- Daily 15:59→17:00 (60 missing) and Fri 15:59→Sun 17:10: `owned === 0`.
|
||||
Compressed on purpose (`MAX_INTRADAY_GAP_SECONDS = 30m`). Local Yahoo has
|
||||
those too.
|
||||
|
||||
So the join-hole theory was dead. Do not insert more slots for a hole that
|
||||
already owns them. Do not "fix" settlement or the weekend unless the product
|
||||
decision changes.
|
||||
|
||||
**What was actually bent.** The time scale and the line's source index
|
||||
disagreed. `futureSpace` gave the hole eight empty columns. Source times were
|
||||
still the real bars only, so `timeframeIndexAt` treated 02:18 and 02:27 as
|
||||
neighbours: one index step, nine slots of x, the segment went flat, then
|
||||
resumed. Worst on 1m-native and 5m (aggregator does not synthesise empty
|
||||
buckets). 30m often survived because Yahoo seeds 30m separately, so those
|
||||
opens still existed.
|
||||
|
||||
**The check, in order, next time a line kinks:**
|
||||
|
||||
1. List 1m holes and `futureSpace` ownership (`kind=geometry` SNAPDBG).
|
||||
2. If `owned === missing`, the scale is not the bug. Compare source index
|
||||
across the hole to slot count. They must match.
|
||||
3. If `owned === 0` and the hole is ≤30m, slots were dropped — that is a
|
||||
scale bug. If the hole is settlement/weekend, compression is intended.
|
||||
4. Do not patch live-edge geometry from a mid-session tape hole. Do not
|
||||
ask for another console paste; `?diag=1` posts the report.
|
||||
|
||||
**The fix.** `fill_short_gaps` inserts missing bucket opens inside the same
|
||||
30-minute bound, in `trendline_series`, in `price_in_timeframe_space`, and in
|
||||
the browser's `sourceTimes`. A 9-minute hole becomes nine index steps.
|
||||
Weekends and settlement stay one step.
|
||||
|
||||
`?diag=1` posts one `kind=geometry` SNAPDBG after bars load (gap ownership
|
||||
and off-median segments). Read it with `logs --since 20m`.
|
||||
|
|
|
|||
|
|
@ -518,6 +518,7 @@ createApp({
|
|||
if (message.tf && message.tf !== timeframe.value) return;
|
||||
dataReceivedAt.value = Date.now();
|
||||
if (message.trendline_first_t != null) {
|
||||
const lastSource = chartApi.trendlineSourceSeries.get(message.bar.tf)?.times.at(-1);
|
||||
const geometryCurrent = chartApi.updateTrendlineTime(
|
||||
message.bar.tf, message.bar.t, message.duration, message.trendline_first_t,
|
||||
message.trendline_previous_t,
|
||||
|
|
@ -525,6 +526,8 @@ createApp({
|
|||
);
|
||||
if (!geometryCurrent && socket?.readyState === WebSocket.OPEN) {
|
||||
socket.send(JSON.stringify({ type: 'trendline_geometry' }));
|
||||
} else if (chartApi.trendlineSourceSeries.get(message.bar.tf)?.times.at(-1) !== lastSource) {
|
||||
syncVisibleLevels();
|
||||
}
|
||||
}
|
||||
chartApi.setDisplayFutureTimes(message.future_times);
|
||||
|
|
@ -533,14 +536,16 @@ createApp({
|
|||
sessionOpen.value = message.session_open ?? sessionOpen.value;
|
||||
status.value.last_bar_t = message.bar.t;
|
||||
} else if (message.type === 'trendline_bar') {
|
||||
const lastSource = chartApi.trendlineSourceSeries.get(message.tf)?.times.at(-1);
|
||||
const geometryCurrent = chartApi.updateTrendlineTime(
|
||||
message.tf, message.t, message.duration, message.first_t, message.previous_t,
|
||||
message.future_times, message.future_durations,
|
||||
);
|
||||
if (!geometryCurrent && socket?.readyState === WebSocket.OPEN) {
|
||||
socket.send(JSON.stringify({ type: 'trendline_geometry' }));
|
||||
}
|
||||
} else if (chartApi.trendlineSourceSeries.get(message.tf)?.times.at(-1) !== lastSource) {
|
||||
syncVisibleLevels();
|
||||
}
|
||||
} else if (message.type === 'trendline_geometry') {
|
||||
chartApi.setTrendlineGeometry(message.geometry);
|
||||
syncVisibleLevels();
|
||||
|
|
|
|||
|
|
@ -705,7 +705,10 @@ class ConfluenceChart {
|
|||
}
|
||||
|
||||
setDisplayFutureTimes(times) {
|
||||
this.displayFutureTimes = (times || []).map(Number);
|
||||
const next = (times || []).map(Number);
|
||||
const prev = this.displayFutureTimes || [];
|
||||
if (next.length === prev.length && next.every((time, index) => time === prev[index])) return;
|
||||
this.displayFutureTimes = next;
|
||||
if (this.bars.length && this.futureSpace) {
|
||||
this.futureSpace.setData(this.timeScaleSlots(this.bars));
|
||||
}
|
||||
|
|
@ -748,7 +751,11 @@ class ConfluenceChart {
|
|||
} else if (t === source.times[last] && source.duration == null && duration != null) {
|
||||
source.durations[last] = Number(duration);
|
||||
}
|
||||
source.futureTimes = futureTimes.map(Number);
|
||||
const nextFuture = futureTimes.map(Number);
|
||||
const sameFuture = nextFuture.length === source.futureTimes.length
|
||||
&& nextFuture.every((time, index) => time === source.futureTimes[index]);
|
||||
if (sameFuture && last >= 0 && t === source.times[last]) return true;
|
||||
source.futureTimes = nextFuture;
|
||||
source.futureDurations = futureDurations.map(Number);
|
||||
source.allTimes = [...source.times, ...source.futureTimes];
|
||||
source._filled = null;
|
||||
|
|
|
|||
|
|
@ -468,6 +468,36 @@ test('a 1m line stays straight across a slotted mid-session hole',
|
|||
});
|
||||
});
|
||||
|
||||
test('repeating the same future times does not rebuild the time scale',
|
||||
{ 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: 600000 + index * minute, o: 100, h: 101, l: 99, c: 100,
|
||||
v: 1, closed: true,
|
||||
}));
|
||||
const future = Array.from(
|
||||
{ length: ConfluenceChart.FUTURE_SLOTS }, (_, index) => bars.at(-1).t + (index + 1) * minute,
|
||||
);
|
||||
c.setDisplayFutureTimes(future);
|
||||
c.setBars(bars);
|
||||
let writes = 0;
|
||||
const original = c.futureSpace.setData.bind(c.futureSpace);
|
||||
c.futureSpace.setData = data => {
|
||||
writes += 1;
|
||||
return original(data);
|
||||
};
|
||||
c.setDisplayFutureTimes(future.slice());
|
||||
c.setDisplayFutureTimes(future.map(Number));
|
||||
return { writes };
|
||||
});
|
||||
assert.equal(result.writes, 0, 'identical future times rebuilt futureSpace');
|
||||
assertNoPageErrors(page, assert);
|
||||
});
|
||||
});
|
||||
|
||||
test('painted canvas pixels keep the same slope through the live edge',
|
||||
{ timeout: 180000 }, async () => {
|
||||
await withChart(async page => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue