react-native-vroom-chart 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/cpp/VroomChartHostObject.cpp +171 -1
  2. package/cpp/_core_include/vroom/vroom_chart.h +137 -4
  3. package/cpp/_core_src/atr.cpp +67 -0
  4. package/cpp/_core_src/atr.h +44 -0
  5. package/cpp/_core_src/atr_pane.cpp +162 -0
  6. package/cpp/_core_src/atr_pane.h +48 -0
  7. package/cpp/_core_src/chart.cpp +461 -172
  8. package/cpp/_core_src/chart.h +150 -1
  9. package/cpp/_core_src/chart_facade.cpp +209 -23
  10. package/cpp/_core_src/fair_value_gaps.cpp +76 -0
  11. package/cpp/_core_src/fair_value_gaps.h +54 -0
  12. package/cpp/_core_src/fvg_overlay.cpp +262 -0
  13. package/cpp/_core_src/fvg_overlay.h +43 -0
  14. package/cpp/_core_src/ichimoku.cpp +65 -0
  15. package/cpp/_core_src/ichimoku.h +45 -0
  16. package/cpp/_core_src/labels.cpp +3 -0
  17. package/cpp/_core_src/line_morph.h +159 -0
  18. package/cpp/_core_src/ma_overlay.cpp +259 -36
  19. package/cpp/_core_src/ma_overlay.h +57 -2
  20. package/cpp/_core_src/macd.cpp +24 -1
  21. package/cpp/_core_src/macd.h +16 -0
  22. package/cpp/_core_src/macd_pane.cpp +71 -62
  23. package/cpp/_core_src/macd_pane.h +13 -1
  24. package/cpp/_core_src/pane_series.h +169 -0
  25. package/cpp/_core_src/rsi.cpp +4 -0
  26. package/cpp/_core_src/rsi.h +7 -0
  27. package/cpp/_core_src/rsi_pane.cpp +85 -29
  28. package/cpp/_core_src/rsi_pane.h +11 -1
  29. package/cpp/_core_src/viewport.h +35 -0
  30. package/lib/index.d.mts +251 -3
  31. package/lib/index.d.ts +251 -3
  32. package/lib/index.js +224 -6
  33. package/lib/index.js.map +1 -1
  34. package/lib/index.mjs +224 -6
  35. package/lib/index.mjs.map +1 -1
  36. package/package.json +1 -1
  37. package/src/VroomChart.tsx +21 -3
  38. package/src/dataTransitions.ts +47 -0
  39. package/src/index.ts +5 -0
  40. package/src/jsi.d.ts +97 -1
  41. package/src/types.ts +5 -0
  42. package/src/useChartCore.ts +284 -5
@@ -20,12 +20,16 @@
20
20
  #include "include/core/SkRect.h"
21
21
  #pragma clang diagnostic pop
22
22
 
23
+ #include "atr.h"
24
+ #include "atr_pane.h"
23
25
  #include "bollinger.h"
24
26
  #include "candles.h"
25
27
  #include "chart_internal.h"
26
28
  #include "crosshair.h"
27
29
  #include "drawings.h"
28
30
  #include "footprints.h"
31
+ #include "fvg_overlay.h"
32
+ #include "ichimoku.h"
29
33
  #include "labels.h"
30
34
  #include "liquidity.h"
31
35
  #include "ma.h"
@@ -59,7 +63,8 @@ vroom::Layout VroomChart::layout() const {
59
63
  const float axis_w = axis_width_px > 0.f
60
64
  ? axis_width_px
61
65
  : width_px * theme.floats[VROOM_FLOAT_Y_AXIS_WIDTH_RATIO];
62
- const int pane_count = (rsi.enabled ? 1 : 0) + (macd.enabled ? 1 : 0);
66
+ const int pane_count =
67
+ (rsi.enabled ? 1 : 0) + (macd.enabled ? 1 : 0) + (atr.enabled ? 1 : 0);
63
68
  const float indicator_h = static_cast<float>(pane_count) * height_px *
64
69
  theme.floats[VROOM_FLOAT_INDICATOR_HEIGHT_FRAC];
65
70
  return vroom::Layout{
@@ -99,6 +104,90 @@ void VroomChart::ensure_macd() {
99
104
  macd_dirty = false;
100
105
  }
101
106
 
107
+ void VroomChart::ensure_atr() {
108
+ if (!atr.enabled || !atr_dirty) return;
109
+ vroom::atr::compute(candles.data(), candles.size(), atr.period,
110
+ atr.smoothing, atr_cache);
111
+ atr_dirty = false;
112
+ }
113
+
114
+ int VroomChart::indicator_panes(vroom::IndicatorPane out[vroom::kMaxPanes]) const {
115
+ int count = 0;
116
+ if (rsi.enabled) out[count++] = {rsi_order, vroom::PaneKind::Rsi};
117
+ if (macd.enabled) out[count++] = {macd_order, vroom::PaneKind::Macd};
118
+ if (atr.enabled) out[count++] = {atr_order, vroom::PaneKind::Atr};
119
+ // Insertion sort: at most three entries, already nearly ordered.
120
+ for (int i = 1; i < count; ++i) {
121
+ const vroom::IndicatorPane key = out[i];
122
+ int j = i - 1;
123
+ for (; j >= 0 && out[j].order > key.order; --j) out[j + 1] = out[j];
124
+ out[j + 1] = key;
125
+ }
126
+ return count;
127
+ }
128
+
129
+ void VroomChart::draw_indicator_panes(SkCanvas* canvas,
130
+ const vroom::Layout& lay,
131
+ const VroomCandle* visible,
132
+ std::size_t n, std::size_t first,
133
+ int64_t window_ms, float candle_right,
134
+ float pane_top, bool use_capture,
135
+ float morph_t) {
136
+ vroom::IndicatorPane panes[vroom::kMaxPanes];
137
+ const int count = indicator_panes(panes);
138
+ const float pane_h =
139
+ height_px * theme.floats[VROOM_FLOAT_INDICATOR_HEIGHT_FRAC];
140
+
141
+ // A cache only lines up with the candles once its ensure_* has run against
142
+ // the current series; until then there is nothing to plot.
143
+ const auto visible_slice = [&](const std::vector<double>& cache) -> const double* {
144
+ return cache.size() == candles.size() ? cache.data() + first : nullptr;
145
+ };
146
+ const auto capture = [&](vroom::LineKey key) -> const vroom::LineMorph* {
147
+ return use_capture ? vroom::find_line_morph(morph_lines, key) : nullptr;
148
+ };
149
+
150
+ for (int i = 0; i < count; ++i) {
151
+ const float pane_bottom = pane_top + pane_h;
152
+ switch (panes[i].kind) {
153
+ case vroom::PaneKind::Rsi: {
154
+ ensure_rsi();
155
+ vroom::rsi_pane::draw(
156
+ canvas, *this, lay, visible, n, visible_slice(rsi_cache),
157
+ rsi.ma_visible ? visible_slice(rsi_ma_cache) : nullptr,
158
+ window_ms, visible_start_ms, candle_duration_ms,
159
+ candle_right, pane_top, pane_bottom,
160
+ capture({vroom::LineKind::Rsi}),
161
+ rsi.ma_visible ? capture({vroom::LineKind::RsiMa}) : nullptr,
162
+ morph_t);
163
+ break;
164
+ }
165
+ case vroom::PaneKind::Macd: {
166
+ ensure_macd();
167
+ vroom::macd_pane::draw(
168
+ canvas, *this, lay, visible, n, visible_slice(macd_cache),
169
+ visible_slice(macd_signal_cache),
170
+ visible_slice(macd_hist_cache), window_ms, visible_start_ms,
171
+ candle_duration_ms, candle_right, pane_top, pane_bottom,
172
+ capture({vroom::LineKind::Macd}),
173
+ capture({vroom::LineKind::MacdSignal}),
174
+ capture({vroom::LineKind::MacdHistogram}), morph_t);
175
+ break;
176
+ }
177
+ case vroom::PaneKind::Atr: {
178
+ ensure_atr();
179
+ vroom::atr_pane::draw(canvas, *this, lay, visible, n,
180
+ visible_slice(atr_cache), window_ms,
181
+ visible_start_ms, candle_duration_ms,
182
+ candle_right, pane_top, pane_bottom,
183
+ capture({vroom::LineKind::Atr}), morph_t);
184
+ break;
185
+ }
186
+ }
187
+ pane_top = pane_bottom;
188
+ }
189
+ }
190
+
102
191
  void VroomChart::ensure_overlays() {
103
192
  if (!overlays_dirty) return;
104
193
  overlay_caches.resize(overlays.size());
@@ -126,6 +215,23 @@ void VroomChart::ensure_bollinger() {
126
215
  bollinger_dirty = false;
127
216
  }
128
217
 
218
+ void VroomChart::ensure_ichimoku() {
219
+ if (!ichimoku.enabled || !ichimoku_dirty) return;
220
+ vroom::ichimoku::compute(candles.data(), candles.size(),
221
+ ichimoku.tenkan_period, ichimoku.kijun_period,
222
+ ichimoku.senkou_b_period, ich_tenkan_cache,
223
+ ich_kijun_cache, ich_senkou_a_cache,
224
+ ich_senkou_b_cache, ich_chikou_cache);
225
+ ichimoku_dirty = false;
226
+ }
227
+
228
+ void VroomChart::ensure_fvg() {
229
+ if (!fvg.enabled || !fvg_dirty) return;
230
+ vroom::fvg::compute(candles.data(), candles.size(), fvg.max_bars_back,
231
+ fvg.wait_for_close != 0, fvg.fill_type, fvg_cache);
232
+ fvg_dirty = false;
233
+ }
234
+
129
235
  void VroomChart::ensure_footprint_buckets() {
130
236
  if (!footprint_buckets_dirty) return;
131
237
  footprint_buckets = vroom::footprints::build_buckets(
@@ -134,6 +240,301 @@ void VroomChart::ensure_footprint_buckets() {
134
240
  footprint_buckets_dirty = false;
135
241
  }
136
242
 
243
+ vroom::IndexRange VroomChart::ichimoku_source_range(int64_t shift_ms) const {
244
+ return vroom::ichimoku::shifted_source_range(
245
+ candles.data(), candles.size(), visible_start_ms - candle_duration_ms,
246
+ visible_end_ms + candle_duration_ms, shift_ms);
247
+ }
248
+
249
+ void VroomChart::draw_overlay_fills(SkCanvas* canvas, const vroom::Layout& lay,
250
+ const vroom::PriceBounds& bounds,
251
+ const VroomCandle* visible, std::size_t n,
252
+ std::size_t first, int64_t window_ms,
253
+ float candle_right, float candle_area_h,
254
+ bool use_capture, float morph_t) {
255
+ const std::size_t sz = candles.size();
256
+ const auto capture = [&](vroom::LineKey key) -> const vroom::LineMorph* {
257
+ return use_capture ? vroom::find_line_morph(morph_lines, key) : nullptr;
258
+ };
259
+
260
+ if (bollinger.enabled) {
261
+ ensure_bollinger();
262
+ if (bollinger.fill_enabled && bb_upper_cache.size() == sz &&
263
+ bb_lower_cache.size() == sz) {
264
+ vroom::ma_overlay::fill_between(
265
+ canvas, lay, bounds, visible, n, bb_upper_cache.data() + first,
266
+ bb_lower_cache.data() + first, window_ms, visible_start_ms,
267
+ candle_duration_ms, candle_right, candle_area_h,
268
+ bollinger.upper_color, bollinger.fill_opacity,
269
+ capture({vroom::LineKind::BollingerUpper}),
270
+ capture({vroom::LineKind::BollingerLower}), morph_t);
271
+ }
272
+ }
273
+
274
+ if (ichimoku.enabled && ichimoku.cloud_enabled) {
275
+ ensure_ichimoku();
276
+ if (ich_senkou_a_cache.size() == sz && ich_senkou_b_cache.size() == sz) {
277
+ const int64_t shift_ms =
278
+ static_cast<int64_t>(ichimoku.displacement) * candle_duration_ms;
279
+ const auto r = ichimoku_source_range(shift_ms);
280
+ // A fade's outgoing half has no new data to slice, but its capture
281
+ // still has to paint.
282
+ const bool has_new = n > 0 && r.end > r.start;
283
+ vroom::ma_overlay::fill_cloud(
284
+ canvas, lay, bounds,
285
+ has_new ? candles.data() + r.start : candles.data(),
286
+ has_new ? r.end - r.start : 0,
287
+ has_new ? ich_senkou_a_cache.data() + r.start : nullptr,
288
+ has_new ? ich_senkou_b_cache.data() + r.start : nullptr,
289
+ window_ms, visible_start_ms, candle_duration_ms, candle_right,
290
+ candle_area_h, ichimoku.bullish_cloud_color,
291
+ ichimoku.bearish_cloud_color, ichimoku.cloud_opacity, shift_ms,
292
+ capture({vroom::LineKind::IchimokuSenkouA}),
293
+ capture({vroom::LineKind::IchimokuSenkouB}), morph_t);
294
+ }
295
+ }
296
+ }
297
+
298
+ void VroomChart::draw_overlay_lines(SkCanvas* canvas, const vroom::Layout& lay,
299
+ const vroom::PriceBounds& bounds,
300
+ const VroomCandle* visible, std::size_t n,
301
+ std::size_t first, int64_t window_ms,
302
+ float candle_right, float candle_area_h,
303
+ bool use_capture, float morph_t) {
304
+ const std::size_t sz = candles.size();
305
+ const auto capture = [&](vroom::LineKey key) -> const vroom::LineMorph* {
306
+ return use_capture ? vroom::find_line_morph(morph_lines, key) : nullptr;
307
+ };
308
+ // One price-pane line against the visible slice. `n == 0` (a fade's
309
+ // outgoing half) leaves only the capture to draw.
310
+ const auto stroke = [&](vroom::LineKey key, const std::vector<double>& cache,
311
+ uint32_t color, float width,
312
+ const unsigned char* breaks) {
313
+ if (cache.size() != sz) return;
314
+ vroom::ma_overlay::draw(canvas, lay, bounds, visible, n,
315
+ cache.data() + first, window_ms,
316
+ visible_start_ms, candle_duration_ms,
317
+ candle_right, candle_area_h, color, width,
318
+ breaks, 1.f, 0, capture(key), morph_t);
319
+ };
320
+
321
+ if (!overlays.empty()) {
322
+ ensure_overlays();
323
+ for (std::size_t k = 0; k < overlays.size(); ++k) {
324
+ stroke(vroom::overlay_line_key(overlays[k], k), overlay_caches[k],
325
+ overlays[k].color, overlays[k].width, nullptr);
326
+ }
327
+ }
328
+
329
+ if (vwap.enabled) {
330
+ ensure_vwap();
331
+ stroke({vroom::LineKind::Vwap}, vwap_cache,
332
+ vroom::style::color_or(vwap.color, kVwapLine),
333
+ vroom::style::width_or(vwap.width, 1.5f),
334
+ vwap_breaks.size() == sz ? vwap_breaks.data() + first : nullptr);
335
+ }
336
+
337
+ if (bollinger.enabled) {
338
+ ensure_bollinger();
339
+ stroke({vroom::LineKind::BollingerUpper}, bb_upper_cache,
340
+ bollinger.upper_color, bollinger.upper_width, nullptr);
341
+ stroke({vroom::LineKind::BollingerLower}, bb_lower_cache,
342
+ bollinger.lower_color, bollinger.lower_width, nullptr);
343
+ stroke({vroom::LineKind::BollingerMiddle}, bb_middle_cache,
344
+ bollinger.middle_color, bollinger.middle_width, nullptr);
345
+ }
346
+
347
+ if (ichimoku.enabled) {
348
+ ensure_ichimoku();
349
+ const int64_t shift_ms =
350
+ static_cast<int64_t>(ichimoku.displacement) * candle_duration_ms;
351
+ // The displaced spans read from their own source slice, so they can't
352
+ // go through `stroke`: the query window shifts back by however far the
353
+ // drawing shifts forward.
354
+ const auto span = [&](vroom::LineKey key,
355
+ const std::vector<double>& cache, uint32_t color,
356
+ float width, int64_t line_shift_ms) {
357
+ if (cache.size() != sz) return;
358
+ const auto r = ichimoku_source_range(line_shift_ms);
359
+ const bool has_new = n > 0 && r.end > r.start;
360
+ vroom::ma_overlay::draw(
361
+ canvas, lay, bounds,
362
+ has_new ? candles.data() + r.start : candles.data(),
363
+ has_new ? r.end - r.start : 0,
364
+ has_new ? cache.data() + r.start : nullptr, window_ms,
365
+ visible_start_ms, candle_duration_ms, candle_right,
366
+ candle_area_h, color, width, nullptr, 1.f, line_shift_ms,
367
+ capture(key), morph_t);
368
+ };
369
+ // Span B under span A where the two edges touch, then the unshifted
370
+ // signal lines, then chikou on top of everything.
371
+ if (ichimoku.senkou_b_enabled) {
372
+ span({vroom::LineKind::IchimokuSenkouB}, ich_senkou_b_cache,
373
+ ichimoku.senkou_b_color, ichimoku.senkou_b_width, shift_ms);
374
+ }
375
+ if (ichimoku.senkou_a_enabled) {
376
+ span({vroom::LineKind::IchimokuSenkouA}, ich_senkou_a_cache,
377
+ ichimoku.senkou_a_color, ichimoku.senkou_a_width, shift_ms);
378
+ }
379
+ if (ichimoku.kijun_enabled) {
380
+ span({vroom::LineKind::IchimokuKijun}, ich_kijun_cache,
381
+ ichimoku.kijun_color, ichimoku.kijun_width, 0);
382
+ }
383
+ if (ichimoku.tenkan_enabled) {
384
+ span({vroom::LineKind::IchimokuTenkan}, ich_tenkan_cache,
385
+ ichimoku.tenkan_color, ichimoku.tenkan_width, 0);
386
+ }
387
+ if (ichimoku.chikou_enabled) {
388
+ span({vroom::LineKind::IchimokuChikou}, ich_chikou_cache,
389
+ ichimoku.chikou_color, ichimoku.chikou_width, -shift_ms);
390
+ }
391
+ }
392
+ }
393
+
394
+ void VroomChart::capture_morph_lines(const vroom::Layout& lay,
395
+ const vroom::PriceBounds& bounds,
396
+ vroom::IndexRange range,
397
+ int64_t window_ms) {
398
+ morph_lines.clear();
399
+ const float area_w = vroom::candle_area_width(lay);
400
+ const std::size_t n = range.end - range.start;
401
+ if (area_w <= 0.f || window_ms <= 0 || n == 0) return;
402
+ const ::VroomCandle* visible = candles.data() + range.start;
403
+ const std::size_t sz = candles.size();
404
+
405
+ // Captures one series newest-first, matching the slot indexing every morph
406
+ // draw pairs on. `src` is the slice the values were drawn against — the
407
+ // visible one for everything except Ichimoku's displaced spans, which plot
408
+ // at `time_ms + shift_ms` from a window of their own. `frac` maps a value
409
+ // into its band; it's the only part that differs between the price pane and
410
+ // an indicator pane.
411
+ const auto capture = [&](vroom::LineKey key, const ::VroomCandle* src,
412
+ std::size_t src_n, const double* values,
413
+ int64_t shift_ms, double scale, auto&& frac) {
414
+ if (!values || src_n == 0) return;
415
+ vroom::LineMorph line;
416
+ line.key = key;
417
+ line.scale = scale;
418
+ line.pts.resize(src_n);
419
+ for (std::size_t k = 0; k < src_n; ++k) {
420
+ const std::size_t i = src_n - 1 - k;
421
+ const double v = values[i];
422
+ const bool defined = std::isfinite(v);
423
+ line.pts[k] = vroom::LineSnapshot{
424
+ vroom::candle_center_x(lay, src[i].time_ms + shift_ms,
425
+ candle_duration_ms, visible_start_ms,
426
+ window_ms) /
427
+ area_w,
428
+ defined ? static_cast<float>(frac(v)) : 0.f,
429
+ defined,
430
+ };
431
+ }
432
+ morph_lines.push_back(std::move(line));
433
+ };
434
+
435
+ // Price-pane series share the band the candle capture was taken against, so
436
+ // they keep their position relative to the candles through the switch.
437
+ const auto price_frac = [&](double v) {
438
+ return vroom::price_fraction(bounds, v);
439
+ };
440
+ const auto price_series = [&](vroom::LineKey key,
441
+ const std::vector<double>& cache) {
442
+ if (cache.size() != sz) return;
443
+ capture(key, visible, n, cache.data() + range.start, 0, 0.0, price_frac);
444
+ };
445
+
446
+ if (!overlays.empty()) {
447
+ ensure_overlays();
448
+ for (std::size_t k = 0; k < overlays.size(); ++k) {
449
+ price_series(vroom::overlay_line_key(overlays[k], k),
450
+ overlay_caches[k]);
451
+ }
452
+ }
453
+
454
+ if (vwap.enabled) {
455
+ ensure_vwap();
456
+ price_series({vroom::LineKind::Vwap}, vwap_cache);
457
+ }
458
+
459
+ if (bollinger.enabled) {
460
+ ensure_bollinger();
461
+ price_series({vroom::LineKind::BollingerUpper}, bb_upper_cache);
462
+ price_series({vroom::LineKind::BollingerMiddle}, bb_middle_cache);
463
+ price_series({vroom::LineKind::BollingerLower}, bb_lower_cache);
464
+ }
465
+
466
+ if (ichimoku.enabled) {
467
+ ensure_ichimoku();
468
+ const int64_t shift_ms =
469
+ static_cast<int64_t>(ichimoku.displacement) * candle_duration_ms;
470
+ // Same source windows draw_chart uses, so slot 0 is the same rightmost
471
+ // vertex on both sides of the switch even for a span running past the
472
+ // newest candle into empty time.
473
+ const auto span = [&](vroom::LineKey key,
474
+ const std::vector<double>& cache,
475
+ int64_t line_shift_ms) {
476
+ if (cache.size() != sz) return;
477
+ const auto r = ichimoku_source_range(line_shift_ms);
478
+ if (r.end <= r.start) return;
479
+ capture(key, candles.data() + r.start, r.end - r.start,
480
+ cache.data() + r.start, line_shift_ms, 0.0, price_frac);
481
+ };
482
+ span({vroom::LineKind::IchimokuTenkan}, ich_tenkan_cache, 0);
483
+ span({vroom::LineKind::IchimokuKijun}, ich_kijun_cache, 0);
484
+ span({vroom::LineKind::IchimokuSenkouA}, ich_senkou_a_cache, shift_ms);
485
+ span({vroom::LineKind::IchimokuSenkouB}, ich_senkou_b_cache, shift_ms);
486
+ span({vroom::LineKind::IchimokuChikou}, ich_chikou_cache, -shift_ms);
487
+ }
488
+
489
+ // Pane series normalize against their pane's own band rather than the price
490
+ // one, so each pane's auto-fit is free to change across the switch without
491
+ // the capture moving.
492
+ const auto pane_slice = [&](const std::vector<double>& cache) -> const double* {
493
+ return cache.size() == sz ? cache.data() + range.start : nullptr;
494
+ };
495
+
496
+ if (rsi.enabled) {
497
+ ensure_rsi();
498
+ const auto frac = [&](double v) {
499
+ return vroom::rsi::band_fraction(v, rsi_y_scale);
500
+ };
501
+ capture({vroom::LineKind::Rsi}, visible, n, pane_slice(rsi_cache), 0, 0.0,
502
+ frac);
503
+ if (rsi.ma_visible) {
504
+ capture({vroom::LineKind::RsiMa}, visible, n,
505
+ pane_slice(rsi_ma_cache), 0, 0.0, frac);
506
+ }
507
+ }
508
+
509
+ if (macd.enabled) {
510
+ ensure_macd();
511
+ const double* line = pane_slice(macd_cache);
512
+ const double* signal = pane_slice(macd_signal_cache);
513
+ const double* hist = pane_slice(macd_hist_cache);
514
+ const double scale = vroom::macd::autoscale(
515
+ macd.line_visible ? line : nullptr,
516
+ macd.signal_visible ? signal : nullptr,
517
+ macd.hist_visible ? hist : nullptr, n);
518
+ const auto frac = [&](double v) {
519
+ return vroom::macd::band_fraction(v, scale, macd_y_scale);
520
+ };
521
+ capture({vroom::LineKind::Macd}, visible, n, line, 0, scale, frac);
522
+ capture({vroom::LineKind::MacdSignal}, visible, n, signal, 0, scale, frac);
523
+ capture({vroom::LineKind::MacdHistogram}, visible, n, hist, 0, scale,
524
+ frac);
525
+ }
526
+
527
+ if (atr.enabled) {
528
+ ensure_atr();
529
+ const double* line = pane_slice(atr_cache);
530
+ const double scale = vroom::atr::autoscale(line, n);
531
+ capture({vroom::LineKind::Atr}, visible, n, line, 0, scale,
532
+ [&](double v) {
533
+ return vroom::atr::band_fraction(v, scale, atr_y_scale);
534
+ });
535
+ }
536
+ }
537
+
137
538
  void VroomChart::draw_chart(SkCanvas* canvas) {
138
539
  begin_frame();
139
540
  const auto lay = layout();
@@ -206,12 +607,28 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
206
607
  const float morph_t = reshape ? interval_morph_t : 1.f;
207
608
 
208
609
  if (fade_out) {
209
- // First half: only the captured series, at the axis envelope opacity.
210
- // Layers with no snapshot (volume, overlays, indicators, drawings)
211
- // would already be the new data, so they stay hidden until the incoming
212
- // half. morph_t = 0 draws the capture pixel-identically.
610
+ // First half: only what was captured, at the axis envelope opacity.
611
+ // Layers with no snapshot (volume, liquidity, FVG, drawings) would
612
+ // already be the new data, so they stay hidden until the incoming half.
613
+ // morph_t = 0 draws every capture pixel-identically.
213
614
  const float layer = axis_phase.opacity;
615
+ // Groups the captured indicators into one layer rather than threading
616
+ // the envelope through as a per-paint alpha, so overlapping strokes
617
+ // composite once and the panes keep their signatures. The price series
618
+ // below fades per-paint, as it always has.
619
+ const auto push_fade_layer = [&]() -> bool {
620
+ if (layer >= 0.999f) return false;
621
+ canvas->saveLayerAlpha(nullptr,
622
+ static_cast<U8CPU>(layer * 255.f + 0.5f));
623
+ return true;
624
+ };
214
625
  if (layer > 0.f) {
626
+ // Behind the candles, as in the settled z-order.
627
+ const bool fills_wrapped = push_fade_layer();
628
+ draw_overlay_fills(canvas, lay, bounds, visible, 0, range.start,
629
+ window_ms, candle_right, candle_area_h, true, 0.f);
630
+ if (fills_wrapped) canvas->restore();
631
+
215
632
  if (fade > 0.f) {
216
633
  vroom::ma_overlay::draw_close_gradient(
217
634
  canvas, lay, bounds, visible, 0, window_ms,
@@ -248,6 +665,18 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
248
665
  morph_from.data(), morph_n, 0.f);
249
666
  }
250
667
  }
668
+
669
+ // Over the candles, and the panes below them. One layer for both:
670
+ // they occupy disjoint bands, so nothing overlaps across the two.
671
+ const bool lines_wrapped = push_fade_layer();
672
+ draw_overlay_lines(canvas, lay, bounds, visible, 0, range.start,
673
+ window_ms, candle_right, candle_area_h, true, 0.f);
674
+ if (lay.indicator_area_h > 0.f) {
675
+ draw_indicator_panes(canvas, lay, visible, 0, range.start,
676
+ window_ms, candle_right, candle_area_h,
677
+ true, 0.f);
678
+ }
679
+ if (lines_wrapped) canvas->restore();
251
680
  }
252
681
  } else {
253
682
  // Incoming fade: the new scene at envelope opacity, no slot lerp
@@ -284,22 +713,20 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
284
713
  vroom::liquidity::draw(canvas, *this, lay, bounds, candle_right,
285
714
  candle_area_h);
286
715
 
287
- // 4.7. Bollinger Band fill.
288
- if (bollinger.enabled) {
289
- ensure_bollinger();
290
- if (bollinger.fill_enabled &&
291
- bb_upper_cache.size() == candles.size() &&
292
- bb_lower_cache.size() == candles.size()) {
293
- vroom::ma_overlay::fill_between(
294
- canvas, lay, bounds, visible, n,
295
- bb_upper_cache.data() + range.start,
296
- bb_lower_cache.data() + range.start, window_ms,
297
- visible_start_ms, candle_duration_ms, candle_right,
298
- candle_area_h, bollinger.upper_color,
299
- bollinger.fill_opacity);
300
- }
716
+ // 4.65. Fair Value Gap boxes — behind the candles, so the bars that
717
+ // formed each imbalance still read over their own shading.
718
+ if (fvg.enabled) {
719
+ ensure_fvg();
720
+ vroom::fvg_overlay::draw_boxes(canvas, *this, lay, bounds,
721
+ window_ms, candle_right,
722
+ candle_area_h);
301
723
  }
302
724
 
725
+ // 4.7 / 4.75. Bollinger fill and Ichimoku cloud, behind the candles.
726
+ draw_overlay_fills(canvas, lay, bounds, visible, n, range.start,
727
+ window_ms, candle_right, candle_area_h, reshape,
728
+ morph_t);
729
+
303
730
  // 5. Price series — candles, a close-price line, or a blend.
304
731
  if (fade < 1.f) {
305
732
  vroom::candles::draw(canvas, visible, n, lay, theme, bounds,
@@ -317,59 +744,10 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
317
744
  theme.floats[VROOM_FLOAT_LINE_TENSION]);
318
745
  }
319
746
 
320
- // 5.5. Moving-average overlay lines.
321
- if (!overlays.empty()) {
322
- ensure_overlays();
323
- for (std::size_t k = 0; k < overlays.size(); ++k) {
324
- if (overlay_caches[k].size() != candles.size()) continue;
325
- const double* vis = overlay_caches[k].data() + range.start;
326
- vroom::ma_overlay::draw(
327
- canvas, lay, bounds, visible, n, vis, window_ms,
328
- visible_start_ms, candle_duration_ms, candle_right,
329
- candle_area_h, overlays[k].color, overlays[k].width);
330
- }
331
- }
332
-
333
- // 5.6. VWAP overlay.
334
- if (vwap.enabled) {
335
- ensure_vwap();
336
- if (vwap_cache.size() == candles.size()) {
337
- const double* vis = vwap_cache.data() + range.start;
338
- const unsigned char* brk =
339
- vwap_breaks.size() == candles.size()
340
- ? vwap_breaks.data() + range.start
341
- : nullptr;
342
- vroom::ma_overlay::draw(
343
- canvas, lay, bounds, visible, n, vis, window_ms,
344
- visible_start_ms, candle_duration_ms, candle_right,
345
- candle_area_h,
346
- vroom::style::color_or(vwap.color, kVwapLine),
347
- vroom::style::width_or(vwap.width, 1.5f), brk);
348
- }
349
- }
350
-
351
- // 5.65. Bollinger Band lines.
352
- if (bollinger.enabled) {
353
- ensure_bollinger();
354
- const std::size_t sz = candles.size();
355
- if (bb_upper_cache.size() == sz && bb_lower_cache.size() == sz &&
356
- bb_middle_cache.size() == sz) {
357
- const auto stroke = [&](const std::vector<double>& cache,
358
- uint32_t color, float width) {
359
- vroom::ma_overlay::draw(
360
- canvas, lay, bounds, visible, n,
361
- cache.data() + range.start, window_ms,
362
- visible_start_ms, candle_duration_ms, candle_right,
363
- candle_area_h, color, width);
364
- };
365
- stroke(bb_upper_cache, bollinger.upper_color,
366
- bollinger.upper_width);
367
- stroke(bb_lower_cache, bollinger.lower_color,
368
- bollinger.lower_width);
369
- stroke(bb_middle_cache, bollinger.middle_color,
370
- bollinger.middle_width);
371
- }
372
- }
747
+ // 5.5 - 5.67. Moving averages, VWAP, Bollinger and Ichimoku lines.
748
+ draw_overlay_lines(canvas, lay, bounds, visible, n, range.start,
749
+ window_ms, candle_right, candle_area_h, reshape,
750
+ morph_t);
373
751
 
374
752
  // 5.7. Drawing annotations.
375
753
  vroom::drawings::draw(canvas, *this, lay, bounds, candle_right,
@@ -395,62 +773,9 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
395
773
  // paint before axis masks so overflow still clips). Transforms
396
774
  // keep the settled z-order after the price indicator.
397
775
  if (fade_in && lay.indicator_area_h > 0.f) {
398
- struct ActivePane {
399
- int order;
400
- int type;
401
- };
402
- ActivePane panes[2];
403
- int count = 0;
404
- if (rsi.enabled) panes[count++] = {rsi_order, 0};
405
- if (macd.enabled) panes[count++] = {macd_order, 1};
406
- if (count == 2 && panes[0].order > panes[1].order) {
407
- const ActivePane tmp = panes[0];
408
- panes[0] = panes[1];
409
- panes[1] = tmp;
410
- }
411
-
412
- const float pane_h =
413
- height_px * theme.floats[VROOM_FLOAT_INDICATOR_HEIGHT_FRAC];
414
- float pane_top = candle_area_h;
415
- for (int i = 0; i < count; ++i) {
416
- const float pane_bottom = pane_top + pane_h;
417
- if (panes[i].type == 0) {
418
- ensure_rsi();
419
- const double* rsi_vis =
420
- rsi_cache.size() == candles.size()
421
- ? rsi_cache.data() + range.start
422
- : nullptr;
423
- const double* rsi_ma_vis =
424
- (rsi.ma_visible &&
425
- rsi_ma_cache.size() == candles.size())
426
- ? rsi_ma_cache.data() + range.start
427
- : nullptr;
428
- vroom::rsi_pane::draw(
429
- canvas, *this, lay, visible, n, rsi_vis, rsi_ma_vis,
430
- window_ms, visible_start_ms, candle_duration_ms,
431
- candle_right, pane_top, pane_bottom);
432
- } else {
433
- ensure_macd();
434
- const double* macd_vis =
435
- macd_cache.size() == candles.size()
436
- ? macd_cache.data() + range.start
437
- : nullptr;
438
- const double* sig_vis =
439
- macd_signal_cache.size() == candles.size()
440
- ? macd_signal_cache.data() + range.start
441
- : nullptr;
442
- const double* hist_vis =
443
- macd_hist_cache.size() == candles.size()
444
- ? macd_hist_cache.data() + range.start
445
- : nullptr;
446
- vroom::macd_pane::draw(
447
- canvas, *this, lay, visible, n, macd_vis, sig_vis,
448
- hist_vis, window_ms, visible_start_ms,
449
- candle_duration_ms, candle_right, pane_top,
450
- pane_bottom);
451
- }
452
- pane_top = pane_bottom;
453
- }
776
+ draw_indicator_panes(canvas, lay, visible, n, range.start,
777
+ window_ms, candle_right, candle_area_h,
778
+ false, 1.f);
454
779
  }
455
780
  }
456
781
 
@@ -483,6 +808,11 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
483
808
  vroom::price_indicator::draw(canvas, *this, lay, bounds,
484
809
  candle_right, candle_area_h);
485
810
 
811
+ // 7.54. Fair Value Gap labels — the boxes themselves are back behind
812
+ // the candles, but their text has to clear the bars it sits over.
813
+ vroom::fvg_overlay::draw_labels(canvas, *this, lay, bounds, window_ms,
814
+ candle_right, candle_area_h);
815
+
486
816
  // 7.55. Consumer-supplied price status lines — same tier as the
487
817
  // current-price indicator (their badges must cover the labels
488
818
  // underneath), but after it so a resting order at the last close
@@ -503,50 +833,9 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
503
833
  // INDICATOR_HEIGHT_FRAC of the height; the candle pane already shrank
504
834
  // to fit them (see layout()). A fade already drew them with the scene.
505
835
  if (!fade_swap && lay.indicator_area_h > 0.f) {
506
- struct ActivePane { int order; int type; }; // type: 0 = RSI, 1 = MACD
507
- ActivePane panes[2];
508
- int count = 0;
509
- if (rsi.enabled) panes[count++] = {rsi_order, 0};
510
- if (macd.enabled) panes[count++] = {macd_order, 1};
511
- if (count == 2 && panes[0].order > panes[1].order) {
512
- const ActivePane tmp = panes[0];
513
- panes[0] = panes[1];
514
- panes[1] = tmp;
515
- }
516
-
517
- const float pane_h =
518
- height_px * theme.floats[VROOM_FLOAT_INDICATOR_HEIGHT_FRAC];
519
- float pane_top = candle_area_h; // == price_pane_bottom(lay)
520
- for (int i = 0; i < count; ++i) {
521
- const float pane_bottom = pane_top + pane_h;
522
- if (panes[i].type == 0) {
523
- ensure_rsi();
524
- const double* rsi_vis = rsi_cache.size() == candles.size()
525
- ? rsi_cache.data() + range.start : nullptr;
526
- const double* rsi_ma_vis =
527
- (rsi.ma_visible && rsi_ma_cache.size() == candles.size())
528
- ? rsi_ma_cache.data() + range.start : nullptr;
529
- vroom::rsi_pane::draw(canvas, *this, lay, visible, n, rsi_vis,
530
- rsi_ma_vis, window_ms, visible_start_ms,
531
- candle_duration_ms, candle_right, pane_top,
532
- pane_bottom);
533
- } else {
534
- ensure_macd();
535
- const double* macd_vis = macd_cache.size() == candles.size()
536
- ? macd_cache.data() + range.start : nullptr;
537
- const double* sig_vis =
538
- macd_signal_cache.size() == candles.size()
539
- ? macd_signal_cache.data() + range.start : nullptr;
540
- const double* hist_vis =
541
- macd_hist_cache.size() == candles.size()
542
- ? macd_hist_cache.data() + range.start : nullptr;
543
- vroom::macd_pane::draw(canvas, *this, lay, visible, n, macd_vis,
544
- sig_vis, hist_vis, window_ms,
545
- visible_start_ms, candle_duration_ms,
546
- candle_right, pane_top, pane_bottom);
547
- }
548
- pane_top = pane_bottom;
549
- }
836
+ // candle_area_h == price_pane_bottom(lay), the top of the band.
837
+ draw_indicator_panes(canvas, lay, visible, n, range.start, window_ms,
838
+ candle_right, candle_area_h, reshape, morph_t);
550
839
  }
551
840
 
552
841
  // 7.7. Crosshair — drawn last so it sits on top of everything, including the