Draw volume, which travelled the whole pipeline unseen
Volume is parsed from both Schwab services, aggregated into every timeframe, stored and broadcast in every bar payload — and nothing ever drew it. An overlay histogram on its own hidden scale, confined to the bottom fifth and tinted by each bar's direction. An overlay rather than a second pane, and deliberately not on the price scale: volumes are five figures against four-figure prices, so sharing a scale would flatten the candles into a line. Verified in a browser that the price scales are untouched — the same price still maps to the identical coordinate through both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
16932f85ec
commit
bec445b599
2 changed files with 61 additions and 0 deletions
|
|
@ -1380,3 +1380,39 @@ trendline 147 points away.
|
||||||
One limit worth knowing: tick *placement* is still computed on UTC days, so the
|
One limit worth knowing: tick *placement* is still computed on UTC days, so the
|
||||||
day-change divider sits at 00:00 UTC rather than local midnight, labelled with
|
day-change divider sits at 00:00 UTC rather than local midnight, labelled with
|
||||||
the local date. The labels are right; the divider is in the UTC place.
|
the local date. The labels are right; the divider is in the UTC place.
|
||||||
|
|
||||||
|
### 2026-08-10 (afternoon) — update rate, the left scale, and volume
|
||||||
|
|
||||||
|
**Schwab conflates Level 1 to one update per second.** Chasing "still slow in
|
||||||
|
market hours" ended at a hard ceiling rather than a bug. In regular hours the
|
||||||
|
gaps between updates are whole multiples of 1.005s — 2.01, 3.02, 4.03 — which
|
||||||
|
only happens if the source emits on a one-second cadence and some seconds carry
|
||||||
|
no trade. `SCHWAB_TICK_SECONDS` was the limiter at 1.0 and is now 0.25, where it
|
||||||
|
no longer binds. **One update per second is the source's ceiling.** Anything
|
||||||
|
faster would mean inventing prices between trades, which a chart must not do.
|
||||||
|
|
||||||
|
Two real losses were found on the way and fixed:
|
||||||
|
|
||||||
|
- Higher timeframes only moved once a minute, because tick bars are 1m and the
|
||||||
|
socket filters by subscriber timeframe. `Runtime.provisional_higher` now
|
||||||
|
combines the aggregator's committed state with the live minute — without
|
||||||
|
mutating it, since the aggregator accumulates volume and would double count.
|
||||||
|
- Trades carrying only a trade stamp and a moved `TOTAL_VOLUME` — no
|
||||||
|
`LAST_PRICE`, no `LAST_SIZE` — were skipped. 66 → 74 updates per 90s.
|
||||||
|
|
||||||
|
**Daily context moved to the left price scale.** The right had prior-day levels,
|
||||||
|
session VWAP and five daily MAs competing with the live price and hand-drawn
|
||||||
|
intraday levels. The trap: a price scale takes its range from the series on it,
|
||||||
|
so moving levels across draws them against a different range and puts them at
|
||||||
|
the wrong height. A transparent candlestick mirror on the left scale feeds it
|
||||||
|
exactly the right scale's input; verified as a zero-pixel delta between the two.
|
||||||
|
Hand-drawn levels stay right, which is the space being cleared. `priceScaleId`
|
||||||
|
is fixed at series creation, so it is passed at construction and kept out of the
|
||||||
|
options reapplied afterwards.
|
||||||
|
|
||||||
|
**Volume is finally drawn.** It travelled the entire pipeline — parsed from both
|
||||||
|
Schwab services, aggregated, stored, broadcast in every bar — and nothing
|
||||||
|
rendered it. Now an overlay histogram on its own hidden scale in the bottom
|
||||||
|
fifth. An overlay rather than a pane, and emphatically not the price scale:
|
||||||
|
volumes are five figures against four-figure prices, and sharing a scale would
|
||||||
|
flatten the candles to a line.
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,20 @@ class ConfluenceChart {
|
||||||
wickUpColor: 'transparent', wickDownColor: 'transparent',
|
wickUpColor: 'transparent', wickDownColor: 'transparent',
|
||||||
lastValueVisible: false, priceLineVisible: false,
|
lastValueVisible: false, priceLineVisible: false,
|
||||||
});
|
});
|
||||||
|
// Volume as an overlay on its own hidden scale, confined to the bottom
|
||||||
|
// fifth. An overlay rather than a pane so it cannot alter the price scale:
|
||||||
|
// volumes are five figures and prices four, and sharing a scale would
|
||||||
|
// flatten the candles into a line.
|
||||||
|
this.volume = this.chart.addSeries(LightweightCharts.HistogramSeries, {
|
||||||
|
priceScaleId: 'volume',
|
||||||
|
priceFormat: { type: 'volume' },
|
||||||
|
lastValueVisible: false,
|
||||||
|
priceLineVisible: false,
|
||||||
|
});
|
||||||
|
this.chart.priceScale('volume').applyOptions({
|
||||||
|
scaleMargins: { top: 0.82, bottom: 0 },
|
||||||
|
visible: false,
|
||||||
|
});
|
||||||
this.resizeObserver = new ResizeObserver(() => {
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
this.chart.applyOptions({ width: el.clientWidth, height: el.clientHeight });
|
this.chart.applyOptions({ width: el.clientWidth, height: el.clientHeight });
|
||||||
requestAnimationFrame(() => this.renderAnchorHandles());
|
requestAnimationFrame(() => this.renderAnchorHandles());
|
||||||
|
|
@ -186,6 +200,7 @@ class ConfluenceChart {
|
||||||
const candleData = bars.map(this.toCandle);
|
const candleData = bars.map(this.toCandle);
|
||||||
this.candles.setData(candleData);
|
this.candles.setData(candleData);
|
||||||
this.leftMirror.setData(candleData);
|
this.leftMirror.setData(candleData);
|
||||||
|
this.volume.setData(bars.map(ConfluenceChart.toVolume));
|
||||||
// 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
|
||||||
|
|
@ -214,6 +229,7 @@ class ConfluenceChart {
|
||||||
if (last && bar.t < last.t) return;
|
if (last && bar.t < last.t) return;
|
||||||
this.candles.update(this.toCandle(bar));
|
this.candles.update(this.toCandle(bar));
|
||||||
this.leftMirror.update(this.toCandle(bar));
|
this.leftMirror.update(this.toCandle(bar));
|
||||||
|
this.volume.update(ConfluenceChart.toVolume(bar));
|
||||||
if (this.bars.length && this.bars[this.bars.length - 1].t === bar.t) this.bars[this.bars.length - 1] = bar;
|
if (this.bars.length && this.bars[this.bars.length - 1].t === bar.t) this.bars[this.bars.length - 1] = bar;
|
||||||
else this.bars.push(bar);
|
else this.bars.push(bar);
|
||||||
this.renderAnchorHandles();
|
this.renderAnchorHandles();
|
||||||
|
|
@ -724,6 +740,15 @@ class ConfluenceChart {
|
||||||
if (this.onLineEnd) this.onLineEnd({ ...level });
|
if (this.onLineEnd) this.onLineEnd({ ...level });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tinted by the bar's own direction, muted so the candles stay the subject.
|
||||||
|
static toVolume(bar) {
|
||||||
|
return {
|
||||||
|
time: bar.t,
|
||||||
|
value: bar.v,
|
||||||
|
color: bar.c >= bar.o ? 'rgba(39,130,92,.38)' : 'rgba(189,69,69,.38)',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
toCandle(bar) {
|
toCandle(bar) {
|
||||||
return { time: bar.t, open: bar.o, high: bar.h, low: bar.l, close: bar.c };
|
return { time: bar.t, open: bar.o, high: bar.h, low: bar.l, close: bar.c };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue