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
@@ -18,6 +18,7 @@
18
18
 
19
19
  #include "curve.h"
20
20
  #include "gradient.h"
21
+ #include "line_morph.h"
21
22
  #include "tip_pulse.h"
22
23
  #include "viewport.h"
23
24
 
@@ -51,6 +52,83 @@ void stroke_path(SkCanvas* canvas,
51
52
 
52
53
  inline float lerp(float a, float b, float t) { return a + (b - a) * t; }
53
54
 
55
+ // Slot `k` of an indicator series in screen space, blended from the outgoing
56
+ // capture toward the new value. Slot 0 is the rightmost vertex on both sides —
57
+ // the pairing a timeframe switch preserves, same as close_vertex below.
58
+ //
59
+ // The capture is in fractions of the candle area and the price band, so it
60
+ // lands on the pixels it occupied pre-switch even though both the surface and
61
+ // the bounds may have changed under it.
62
+ MorphVertex series_vertex(const Layout& lay,
63
+ const PriceBounds& bounds,
64
+ const ::VroomCandle* visible,
65
+ std::size_t n,
66
+ const double* values,
67
+ int64_t window_ms,
68
+ int64_t visible_start_ms,
69
+ int64_t candle_duration_ms,
70
+ int64_t time_shift_ms,
71
+ const LineMorph* from,
72
+ std::size_t from_count,
73
+ float morph_t,
74
+ std::size_t k) {
75
+ MorphVertex to;
76
+ if (values && k < n) {
77
+ const std::size_t i = n - 1 - k;
78
+ const double v = values[i];
79
+ if (std::isfinite(v)) {
80
+ to = MorphVertex{
81
+ vroom::candle_center_x(lay, visible[i].time_ms + time_shift_ms,
82
+ candle_duration_ms, visible_start_ms,
83
+ window_ms),
84
+ vroom::price_to_y(lay, bounds, v), true};
85
+ }
86
+ }
87
+ MorphVertex frm;
88
+ if (k < from_count) {
89
+ const LineSnapshot& s = from->pts[k];
90
+ if (s.valid) {
91
+ frm = MorphVertex{s.x * vroom::candle_area_width(lay),
92
+ vroom::y_at_fraction(lay, s.y), true};
93
+ }
94
+ }
95
+ return vroom::morph_vertex(to, frm, morph_t);
96
+ }
97
+
98
+ // The same series walked left to right into `out`, for the two fills — they
99
+ // need to look ahead for crossovers and back along the opposite edge, so they
100
+ // can't stream the way the stroke does. out[j] is slot slots-1-j.
101
+ void resolve_series(const Layout& lay,
102
+ const PriceBounds& bounds,
103
+ const ::VroomCandle* visible,
104
+ std::size_t n,
105
+ const double* values,
106
+ int64_t window_ms,
107
+ int64_t visible_start_ms,
108
+ int64_t candle_duration_ms,
109
+ int64_t time_shift_ms,
110
+ const LineMorph* from,
111
+ float morph_t,
112
+ std::size_t slots,
113
+ std::vector<MorphVertex>& out) {
114
+ const std::size_t from_count = vroom::morph_line_count(from, morph_t);
115
+ out.clear();
116
+ out.reserve(slots);
117
+ for (std::size_t j = 0; j < slots; ++j) {
118
+ out.push_back(series_vertex(lay, bounds, visible, n, values, window_ms,
119
+ visible_start_ms, candle_duration_ms,
120
+ time_shift_ms, from, from_count, morph_t,
121
+ slots - 1 - j));
122
+ }
123
+ }
124
+
125
+ // How many vertices a morphing series spans: the new slice and the capture can
126
+ // disagree on length, and every slot either defines has to be walked.
127
+ inline std::size_t series_slots(std::size_t n, const LineMorph* from,
128
+ float morph_t) {
129
+ return std::max(n, vroom::morph_line_count(from, morph_t));
130
+ }
131
+
54
132
  // Width of the background-colored ring that separates the tip dot from the line
55
133
  // and from the pulse expanding out behind it.
56
134
  constexpr float kTipBorderPx = 2.f;
@@ -198,31 +276,43 @@ void draw(SkCanvas* canvas,
198
276
  uint32_t color,
199
277
  float width,
200
278
  const unsigned char* break_before,
201
- float opacity) {
202
- if (!canvas || !values_visible || n == 0 || candle_right <= 0.f ||
203
- candle_area_h <= 0.f) {
204
- return;
205
- }
279
+ float opacity,
280
+ int64_t time_shift_ms,
281
+ const LineMorph* from,
282
+ float morph_t) {
283
+ if (!canvas || candle_right <= 0.f || candle_area_h <= 0.f) return;
206
284
  opacity = std::clamp(opacity, 0.f, 1.f);
207
285
  if (opacity <= 0.f) return;
286
+ morph_t = std::clamp(morph_t, 0.f, 1.f);
208
287
 
288
+ const std::size_t from_count = vroom::morph_line_count(from, morph_t);
289
+ const std::size_t slots = std::max(n, from_count);
290
+ if (slots == 0 || (!values_visible && from_count == 0)) return;
291
+
292
+ // Descending slots, which walks the line left to right. Settled (no
293
+ // capture) this visits exactly the source indices 0..n-1 in order, so the
294
+ // path is the one it has always emitted.
209
295
  // SkPathBuilder (not SkPath's edit methods, removed in newer Skia tips).
210
296
  SkPathBuilder path;
211
297
  bool pen_down = false;
212
- for (std::size_t i = 0; i < n; ++i) {
213
- const double v = values_visible[i];
214
- if (!std::isfinite(v)) {
298
+ for (std::size_t j = 0; j < slots; ++j) {
299
+ const std::size_t k = slots - 1 - j;
300
+ const MorphVertex p = series_vertex(
301
+ lay, bounds, visible, n, values_visible, window_ms,
302
+ visible_start_ms, candle_duration_ms, time_shift_ms, from,
303
+ from_count, morph_t, k);
304
+ if (!p.valid) {
215
305
  pen_down = false;
216
306
  continue;
217
307
  }
218
- const float x = vroom::candle_center_x(
219
- lay, visible[i].time_ms, candle_duration_ms, visible_start_ms,
220
- window_ms);
221
- const float y = vroom::price_to_y(lay, bounds, v);
222
- if (pen_down && !(break_before && break_before[i])) {
223
- path.lineTo(x, y);
308
+ // Session breaks are a property of the incoming data, so a slot the new
309
+ // series doesn't reach can't carry one.
310
+ const bool breaks =
311
+ break_before && k < n && break_before[n - 1 - k] != 0;
312
+ if (pen_down && !breaks) {
313
+ path.lineTo(p.x, p.y);
224
314
  } else {
225
- path.moveTo(x, y);
315
+ path.moveTo(p.x, p.y);
226
316
  pen_down = true;
227
317
  }
228
318
  }
@@ -419,42 +509,51 @@ void fill_between(SkCanvas* canvas,
419
509
  float candle_right,
420
510
  float candle_area_h,
421
511
  uint32_t color,
422
- float opacity) {
423
- if (!canvas || !upper_visible || !lower_visible || n == 0 ||
424
- candle_right <= 0.f || candle_area_h <= 0.f) {
425
- return;
426
- }
512
+ float opacity,
513
+ const LineMorph* upper_from,
514
+ const LineMorph* lower_from,
515
+ float morph_t) {
516
+ if (!canvas || candle_right <= 0.f || candle_area_h <= 0.f) return;
427
517
  opacity = std::clamp(opacity, 0.f, 1.f);
428
518
  if (opacity <= 0.f) return;
519
+ morph_t = std::clamp(morph_t, 0.f, 1.f);
520
+
521
+ const std::size_t slots =
522
+ std::max(series_slots(n, upper_from, morph_t),
523
+ series_slots(n, lower_from, morph_t));
524
+ if (slots == 0) return;
429
525
 
430
- // One closed contour per maximal run where both series are finite; a
526
+ // Both edges resolved left to right first: mid-morph a vertex is a blend of
527
+ // the captured shape and the new one, and the fill has to be stitched from
528
+ // the same positions the two lines are stroked at.
529
+ static thread_local std::vector<MorphVertex> upper;
530
+ static thread_local std::vector<MorphVertex> lower;
531
+ resolve_series(lay, bounds, visible, n, upper_visible, window_ms,
532
+ visible_start_ms, candle_duration_ms, 0, upper_from, morph_t,
533
+ slots, upper);
534
+ resolve_series(lay, bounds, visible, n, lower_visible, window_ms,
535
+ visible_start_ms, candle_duration_ms, 0, lower_from, morph_t,
536
+ slots, lower);
537
+
538
+ // One closed contour per maximal run where both edges are defined; a
431
539
  // single-point run has no area. Multiple runs (e.g. around a data gap)
432
540
  // become multiple contours in one path.
433
541
  SkPathBuilder path;
434
542
  std::size_t i = 0;
435
- while (i < n) {
436
- if (!std::isfinite(upper_visible[i]) ||
437
- !std::isfinite(lower_visible[i])) {
543
+ while (i < slots) {
544
+ if (!upper[i].valid || !lower[i].valid) {
438
545
  ++i;
439
546
  continue;
440
547
  }
441
548
  std::size_t e = i;
442
- while (e + 1 < n && std::isfinite(upper_visible[e + 1]) &&
443
- std::isfinite(lower_visible[e + 1])) {
444
- ++e;
445
- }
549
+ while (e + 1 < slots && upper[e + 1].valid && lower[e + 1].valid) ++e;
446
550
  if (e > i) {
447
- const auto x_at = [&](std::size_t k) {
448
- return vroom::candle_center_x(lay, visible[k].time_ms,
449
- candle_duration_ms,
450
- visible_start_ms, window_ms);
451
- };
452
- path.moveTo(x_at(i), vroom::price_to_y(lay, bounds, upper_visible[i]));
551
+ path.moveTo(upper[i].x, upper[i].y);
453
552
  for (std::size_t k = i + 1; k <= e; ++k) {
454
- path.lineTo(x_at(k), vroom::price_to_y(lay, bounds, upper_visible[k]));
553
+ path.lineTo(upper[k].x, upper[k].y);
455
554
  }
456
555
  for (std::size_t k = e + 1; k-- > i;) {
457
- path.lineTo(x_at(k), vroom::price_to_y(lay, bounds, lower_visible[k]));
556
+ path.lineTo(lower[k].x, lower[k].y);
458
557
  }
459
558
  path.close();
460
559
  }
@@ -473,4 +572,128 @@ void fill_between(SkCanvas* canvas,
473
572
  canvas->restore();
474
573
  }
475
574
 
575
+ void fill_cloud(SkCanvas* canvas,
576
+ const Layout& lay,
577
+ const PriceBounds& bounds,
578
+ const ::VroomCandle* visible,
579
+ std::size_t n,
580
+ const double* a_visible,
581
+ const double* b_visible,
582
+ int64_t window_ms,
583
+ int64_t visible_start_ms,
584
+ int64_t candle_duration_ms,
585
+ float candle_right,
586
+ float candle_area_h,
587
+ uint32_t above_color,
588
+ uint32_t below_color,
589
+ float opacity,
590
+ int64_t time_shift_ms,
591
+ const LineMorph* a_from,
592
+ const LineMorph* b_from,
593
+ float morph_t) {
594
+ if (!canvas || candle_right <= 0.f || candle_area_h <= 0.f) return;
595
+ opacity = std::clamp(opacity, 0.f, 1.f);
596
+ if (opacity <= 0.f) return;
597
+ morph_t = std::clamp(morph_t, 0.f, 1.f);
598
+
599
+ const std::size_t slots = std::max(series_slots(n, a_from, morph_t),
600
+ series_slots(n, b_from, morph_t));
601
+ if (slots == 0) return;
602
+
603
+ static thread_local std::vector<MorphVertex> edge_a;
604
+ static thread_local std::vector<MorphVertex> edge_b;
605
+ resolve_series(lay, bounds, visible, n, a_visible, window_ms,
606
+ visible_start_ms, candle_duration_ms, time_shift_ms, a_from,
607
+ morph_t, slots, edge_a);
608
+ resolve_series(lay, bounds, visible, n, b_visible, window_ms,
609
+ visible_start_ms, candle_duration_ms, time_shift_ms, b_from,
610
+ morph_t, slots, edge_b);
611
+
612
+ // A vertex of the cloud: one x with the two edge heights there. At a
613
+ // crossover the two collapse onto the same y, which is what lets the
614
+ // neighboring contours meet on a point instead of overlapping.
615
+ struct Vertex {
616
+ float x, ya, yb;
617
+ };
618
+
619
+ SkPathBuilder above_path;
620
+ SkPathBuilder below_path;
621
+ std::vector<Vertex> contour;
622
+
623
+ // Closes the accumulated vertices into the path for whichever side is on
624
+ // top: forward along edge a, back along edge b.
625
+ const auto flush = [&](int sign) {
626
+ if (contour.size() >= 2) {
627
+ SkPathBuilder& into = sign >= 0 ? above_path : below_path;
628
+ into.moveTo(contour[0].x, contour[0].ya);
629
+ for (std::size_t k = 1; k < contour.size(); ++k) {
630
+ into.lineTo(contour[k].x, contour[k].ya);
631
+ }
632
+ for (std::size_t k = contour.size(); k-- > 0;) {
633
+ into.lineTo(contour[k].x, contour[k].yb);
634
+ }
635
+ into.close();
636
+ }
637
+ contour.clear();
638
+ };
639
+
640
+ // Which tone a run takes, read off the drawn geometry rather than the raw
641
+ // values: y grows downward, so a above b means ya is the smaller. Mid-morph
642
+ // the values belong to two different resolutions and only the interpolated
643
+ // positions say where the edges actually sit.
644
+ const auto separation = [&](std::size_t k) {
645
+ return static_cast<double>(edge_b[k].y - edge_a[k].y);
646
+ };
647
+
648
+ std::size_t i = 0;
649
+ while (i < slots) {
650
+ if (!edge_a[i].valid || !edge_b[i].valid) {
651
+ ++i;
652
+ continue;
653
+ }
654
+ std::size_t e = i;
655
+ while (e + 1 < slots && edge_a[e + 1].valid && edge_b[e + 1].valid) ++e;
656
+
657
+ int sign = 0;
658
+ Vertex prev{};
659
+ for (std::size_t k = i; k <= e; ++k) {
660
+ const Vertex v = Vertex{edge_a[k].x, edge_a[k].y, edge_b[k].y};
661
+ const double d = separation(k);
662
+ const int s = d > 0.0 ? 1 : (d < 0.0 ? -1 : 0);
663
+ if (sign != 0 && s != 0 && s != sign) {
664
+ // The spans swapped between the previous bar and this one. Meet
665
+ // them at the crossing so both tones end on the same point.
666
+ const double d0 = separation(k - 1);
667
+ const float t = static_cast<float>(d0 / (d0 - d));
668
+ const float xc = prev.x + (v.x - prev.x) * t;
669
+ const float yc = prev.ya + (v.ya - prev.ya) * t;
670
+ contour.push_back(Vertex{xc, yc, yc});
671
+ flush(sign);
672
+ contour.push_back(Vertex{xc, yc, yc});
673
+ sign = s;
674
+ } else if (sign == 0) {
675
+ sign = s;
676
+ }
677
+ contour.push_back(v);
678
+ prev = v;
679
+ }
680
+ flush(sign);
681
+ i = e + 1;
682
+ }
683
+
684
+ SkPaint fill;
685
+ fill.setAntiAlias(true);
686
+ fill.setStyle(SkPaint::kFill_Style);
687
+
688
+ canvas->save();
689
+ canvas->clipRect(SkRect::MakeLTRB(0.f, 0.f, candle_right, candle_area_h));
690
+ fill.setColor(static_cast<SkColor>(above_color));
691
+ fill.setAlphaf(fill.getAlphaf() * opacity);
692
+ canvas->drawPath(above_path.detach(), fill);
693
+ fill.setColor(static_cast<SkColor>(below_color));
694
+ fill.setAlphaf(fill.getAlphaf() * opacity);
695
+ canvas->drawPath(below_path.detach(), fill);
696
+ canvas->restore();
697
+ }
698
+
476
699
  } // namespace vroom::ma_overlay
@@ -13,6 +13,7 @@ class SkCanvas;
13
13
  namespace vroom {
14
14
  struct CandleSnapshot;
15
15
  struct Layout;
16
+ struct LineMorph;
16
17
  struct PriceBounds;
17
18
  } // namespace vroom
18
19
 
@@ -25,6 +26,18 @@ namespace vroom::ma_overlay {
25
26
  // `break_before` (optional, aligned with `visible`) forces a new subpath at any
26
27
  // index where it is non-zero — used by VWAP to break the line at session
27
28
  // resets. Pass nullptr for continuous lines (SMA/EMA).
29
+ //
30
+ // `time_shift_ms` offsets where each value lands on the time axis, for a series
31
+ // drawn away from the bar it was computed on (Ichimoku's leading and lagging
32
+ // spans). The shifted time need not be a bar that exists: projection is linear,
33
+ // so a leading span runs past the newest candle into empty time. `visible` is
34
+ // then the *source* slice — see ichimoku::shifted_source_range.
35
+ //
36
+ // `from` / `morph_t` are the interval morph, the indicator counterpart of what
37
+ // draw_close_line does for the price series: the capture holds this series'
38
+ // outgoing shape indexed from the right (slot 0 = newest) and each vertex
39
+ // slides from where it sat before the timeframe switch to where it sits now.
40
+ // A null capture (the default) draws `values_visible` alone.
28
41
  void draw(SkCanvas* canvas,
29
42
  const Layout& lay,
30
43
  const PriceBounds& bounds,
@@ -39,7 +52,10 @@ void draw(SkCanvas* canvas,
39
52
  uint32_t color,
40
53
  float width,
41
54
  const unsigned char* break_before = nullptr,
42
- float opacity = 1.f);
55
+ float opacity = 1.f,
56
+ int64_t time_shift_ms = 0,
57
+ const LineMorph* from = nullptr,
58
+ float morph_t = 1.f);
43
59
 
44
60
  // The close-price polyline of line-chart mode. Equivalent to draw() fed the
45
61
  // visible closes, plus the interval morph: `from` / `from_n` is the outgoing
@@ -140,6 +156,9 @@ void draw_close_tip(SkCanvas* canvas,
140
156
  // Runs where either series is NaN are skipped, so the fill never bridges the
141
157
  // warmup gap. Plain-alpha SkPaint fill, no gradient ramp (see gradient.h).
142
158
  // Clipped to the candle area like draw().
159
+ //
160
+ // Takes a capture per edge so the fill is built from the same interpolated
161
+ // vertices its two lines are, and can't detach from them mid-morph.
143
162
  void fill_between(SkCanvas* canvas,
144
163
  const Layout& lay,
145
164
  const PriceBounds& bounds,
@@ -153,6 +172,42 @@ void fill_between(SkCanvas* canvas,
153
172
  float candle_right,
154
173
  float candle_area_h,
155
174
  uint32_t color,
156
- float opacity);
175
+ float opacity,
176
+ const LineMorph* upper_from = nullptr,
177
+ const LineMorph* lower_from = nullptr,
178
+ float morph_t = 1.f);
179
+
180
+ // Ichimoku's cloud: the region between two aligned series, filled with
181
+ // `above_color` where `a` is over `b` and `below_color` where it is under, both
182
+ // at their alpha × `opacity`.
183
+ //
184
+ // Each run is split at every crossover, with the interpolated meeting point
185
+ // added to the contour on both sides, so the two tones abut exactly instead of
186
+ // overlapping or leaving a seam. NaN in either series breaks the run, as in
187
+ // fill_between. `time_shift_ms` shifts the whole cloud along the time axis the
188
+ // same way draw() does.
189
+ //
190
+ // Morphs per edge like fill_between. Which tone a run takes is read off the
191
+ // interpolated vertices rather than the raw values, so a crossover that only
192
+ // exists mid-morph still splits the cloud where the two edges actually meet.
193
+ void fill_cloud(SkCanvas* canvas,
194
+ const Layout& lay,
195
+ const PriceBounds& bounds,
196
+ const ::VroomCandle* visible,
197
+ std::size_t n,
198
+ const double* a_visible,
199
+ const double* b_visible,
200
+ int64_t window_ms,
201
+ int64_t visible_start_ms,
202
+ int64_t candle_duration_ms,
203
+ float candle_right,
204
+ float candle_area_h,
205
+ uint32_t above_color,
206
+ uint32_t below_color,
207
+ float opacity,
208
+ int64_t time_shift_ms = 0,
209
+ const LineMorph* a_from = nullptr,
210
+ const LineMorph* b_from = nullptr,
211
+ float morph_t = 1.f);
157
212
 
158
213
  } // namespace vroom::ma_overlay
@@ -1,6 +1,7 @@
1
1
  #include "macd.h"
2
2
 
3
- #include <cmath> // std::nan, std::isfinite
3
+ #include <algorithm> // std::max
4
+ #include <cmath> // std::nan, std::isfinite, std::abs
4
5
 
5
6
  #include "ma.h"
6
7
  #include "series_ma.h"
@@ -36,4 +37,26 @@ void compute(const ::VroomCandle* candles, std::size_t n, int fast, int slow,
36
37
  }
37
38
  }
38
39
 
40
+ double autoscale(const double* macd, const double* signal, const double* hist,
41
+ std::size_t n) {
42
+ double scale = 0.0;
43
+ const auto track = [&](const double* s) {
44
+ if (!s) return;
45
+ for (std::size_t i = 0; i < n; ++i) {
46
+ if (std::isfinite(s[i])) scale = std::max(scale, std::abs(s[i]));
47
+ }
48
+ };
49
+ track(macd);
50
+ track(signal);
51
+ track(hist);
52
+ return scale;
53
+ }
54
+
55
+ double band_fraction(double v, double scale, double y_scale) {
56
+ // Nothing on show yet — everything collapses onto the zero line rather than
57
+ // dividing by zero.
58
+ if (!(scale > 0.0)) return 0.5;
59
+ return 0.5 + (v / scale) * 0.5 * kBandPadFraction * y_scale;
60
+ }
61
+
39
62
  } // namespace vroom::macd
@@ -16,6 +16,10 @@
16
16
 
17
17
  namespace vroom::macd {
18
18
 
19
+ // How much of the pane's half-band an extreme is allowed to fill, so a peak
20
+ // stops short of the separator instead of touching it.
21
+ constexpr double kBandPadFraction = 0.85;
22
+
19
23
  // Computes MACD over [candles, candles+n). fast/slow/signal are clamped to >= 1
20
24
  // (callers pass 12/26/9 by default; slow should exceed fast). `source` selects
21
25
  // the price series (vroom::ma::Source); `ma_kind` picks the averaging for the
@@ -29,4 +33,16 @@ void compute(const ::VroomCandle* candles, std::size_t n, int fast, int slow,
29
33
  std::vector<double>& macd_out, std::vector<double>& signal_out,
30
34
  std::vector<double>& hist_out);
31
35
 
36
+ // The amplitude the pane band is fitted to, symmetric about zero: the largest
37
+ // finite magnitude across the series on show, or 0 when there is nothing to
38
+ // plot. Pass nullptr for a hidden series so the rest fill the band.
39
+ double autoscale(const double* macd, const double* signal, const double* hist,
40
+ std::size_t n);
41
+
42
+ // Where a value sits in the pane band, as a fraction of its height — 0 at the
43
+ // bottom edge, 1 at the top, 0.5 on the zero line. `y_scale` is the user's
44
+ // y-axis zoom (1 = the default fit). Split out of the renderer so the
45
+ // interval-morph capture maps its geometry through the same math.
46
+ double band_fraction(double v, double scale, double y_scale);
47
+
32
48
  } // namespace vroom::macd