react-native-vroom-chart 0.14.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 (47) hide show
  1. package/cpp/VroomChartHostObject.cpp +289 -2
  2. package/cpp/_core_include/vroom/vroom_chart.h +221 -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 +477 -172
  8. package/cpp/_core_src/chart.h +173 -1
  9. package/cpp/_core_src/chart_facade.cpp +317 -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/footprints.cpp +226 -0
  13. package/cpp/_core_src/footprints.h +45 -0
  14. package/cpp/_core_src/footprints_layout.cpp +140 -0
  15. package/cpp/_core_src/footprints_layout.h +94 -0
  16. package/cpp/_core_src/fvg_overlay.cpp +262 -0
  17. package/cpp/_core_src/fvg_overlay.h +43 -0
  18. package/cpp/_core_src/ichimoku.cpp +65 -0
  19. package/cpp/_core_src/ichimoku.h +45 -0
  20. package/cpp/_core_src/labels.cpp +3 -0
  21. package/cpp/_core_src/line_morph.h +159 -0
  22. package/cpp/_core_src/ma_overlay.cpp +259 -36
  23. package/cpp/_core_src/ma_overlay.h +57 -2
  24. package/cpp/_core_src/macd.cpp +24 -1
  25. package/cpp/_core_src/macd.h +16 -0
  26. package/cpp/_core_src/macd_pane.cpp +71 -62
  27. package/cpp/_core_src/macd_pane.h +13 -1
  28. package/cpp/_core_src/pane_series.h +169 -0
  29. package/cpp/_core_src/rsi.cpp +4 -0
  30. package/cpp/_core_src/rsi.h +7 -0
  31. package/cpp/_core_src/rsi_pane.cpp +85 -29
  32. package/cpp/_core_src/rsi_pane.h +11 -1
  33. package/cpp/_core_src/tip_pulse.h +4 -4
  34. package/cpp/_core_src/viewport.h +35 -0
  35. package/lib/index.d.mts +385 -3
  36. package/lib/index.d.ts +385 -3
  37. package/lib/index.js +305 -7
  38. package/lib/index.js.map +1 -1
  39. package/lib/index.mjs +305 -7
  40. package/lib/index.mjs.map +1 -1
  41. package/package.json +1 -1
  42. package/src/VroomChart.tsx +108 -7
  43. package/src/dataTransitions.ts +47 -0
  44. package/src/index.ts +10 -0
  45. package/src/jsi.d.ts +136 -1
  46. package/src/types.ts +10 -0
  47. package/src/useChartCore.ts +330 -5
@@ -13,6 +13,7 @@
13
13
 
14
14
  #include "chart.h"
15
15
  #include "drawings.h"
16
+ #include "footprints.h"
16
17
  #include "labels.h"
17
18
  #include "price_lines.h"
18
19
  #include "viewport.h"
@@ -82,6 +83,32 @@ int64_t damp_future_delta(int64_t delta_ms, int64_t cur_future,
82
83
  return static_cast<int64_t>(static_cast<double>(delta_ms) * resist);
83
84
  }
84
85
 
86
+ // Empty time the Ichimoku leading spans need past the newest candle. Zero
87
+ // unless the indicator is on — the reserve is Ichimoku's alone, since it is the
88
+ // only thing that draws where there are no candles.
89
+ int64_t ichimoku_future_ms(const VroomChart* chart) {
90
+ if (!chart->ichimoku.enabled || chart->ichimoku.displacement <= 0) return 0;
91
+ return static_cast<int64_t>(chart->ichimoku.displacement) *
92
+ chart->candle_duration_ms;
93
+ }
94
+
95
+ // Pull the view forward far enough to show the leading spans. Only ever grows
96
+ // the future gap, and only as far as a pan gesture could already take it, so a
97
+ // user who has scrolled ahead keeps their position. A no-op before the view is
98
+ // framed — set_candles reserves the space itself in that case.
99
+ void reserve_ichimoku_future(VroomChart* chart) {
100
+ if (chart->candles.empty()) return;
101
+ const int64_t want = ichimoku_future_ms(chart);
102
+ if (want <= 0) return;
103
+ const int64_t window = chart->visible_end_ms - chart->visible_start_ms;
104
+ if (window <= 0) return;
105
+ const int64_t last_time = chart->candles.back().time_ms;
106
+ const int64_t target = std::min(want, max_future_gap(window, 0));
107
+ if (chart->visible_end_ms - last_time >= target) return;
108
+ chart->visible_end_ms = last_time + target;
109
+ chart->visible_start_ms = chart->visible_end_ms - window;
110
+ }
111
+
85
112
  // Frame the default view. When the consumer has set a target candle body width
86
113
  // (default_candle_px), size the window so each candle renders ~that wide with the
87
114
  // right edge pinned to the latest candle; otherwise fall back to the legacy
@@ -101,16 +128,17 @@ void apply_default_framing(VroomChart* chart) {
101
128
  lay.width_px - lay.y_axis_width_px - lay.right_padding_px;
102
129
 
103
130
  // Places a window of the given width with kRightGapPx of air past the newest
104
- // candle. Shifts the window forward rather than widening it, so the framing
105
- // keeps whatever candle width it just computed. Falls back to a flush right
106
- // edge when the layout isn't measured yet and px can't be converted to time.
131
+ // candle, plus whatever empty time Ichimoku's leading spans need. Shifts the
132
+ // window forward rather than widening it, so the framing keeps whatever
133
+ // candle width it just computed. Falls back to a flush right edge when the
134
+ // layout isn't measured yet and px can't be converted to time.
107
135
  const auto frame = [&](int64_t window_ms) {
108
136
  const int64_t gap_ms =
109
137
  usable > 0.0 && window_ms > 0
110
138
  ? static_cast<int64_t>((kRightGapPx * static_cast<double>(window_ms)) /
111
139
  usable)
112
140
  : 0;
113
- chart->visible_end_ms = right_edge_ms + gap_ms;
141
+ chart->visible_end_ms = right_edge_ms + gap_ms + ichimoku_future_ms(chart);
114
142
  chart->visible_start_ms = chart->visible_end_ms - window_ms;
115
143
  };
116
144
 
@@ -183,9 +211,15 @@ extern "C" void vroom_chart_set_candles(VroomChart* chart, const VroomCandle* da
183
211
  chart->candles.assign(data, data + count);
184
212
  chart->rsi_dirty = true;
185
213
  chart->macd_dirty = true;
214
+ chart->atr_dirty = true;
186
215
  chart->overlays_dirty = true;
187
216
  chart->vwap_dirty = true;
188
217
  chart->bollinger_dirty = true;
218
+ chart->ichimoku_dirty = true;
219
+ chart->fvg_dirty = true;
220
+ // New bars (or a whole new interval) mean the footprint grouping no longer
221
+ // matches the data — regroup on next use.
222
+ chart->footprint_buckets_dirty = true;
189
223
 
190
224
  // Infer the candle period from the first interval. Robust enough for
191
225
  // uniform-duration series (the only kind we model today).
@@ -211,9 +245,12 @@ extern "C" void vroom_chart_append_candle(VroomChart* chart, const VroomCandle*
211
245
  chart->candles.push_back(*c);
212
246
  chart->rsi_dirty = true;
213
247
  chart->macd_dirty = true;
248
+ chart->atr_dirty = true;
214
249
  chart->overlays_dirty = true;
215
250
  chart->vwap_dirty = true;
216
251
  chart->bollinger_dirty = true;
252
+ chart->ichimoku_dirty = true;
253
+ chart->fvg_dirty = true;
217
254
  chart->mark_dirty();
218
255
  }
219
256
 
@@ -222,9 +259,12 @@ extern "C" void vroom_chart_update_last(VroomChart* chart, const VroomCandle* c)
222
259
  chart->candles.back() = *c;
223
260
  chart->rsi_dirty = true;
224
261
  chart->macd_dirty = true;
262
+ chart->atr_dirty = true;
225
263
  chart->overlays_dirty = true;
226
264
  chart->vwap_dirty = true;
227
265
  chart->bollinger_dirty = true;
266
+ chart->ichimoku_dirty = true;
267
+ chart->fvg_dirty = true;
228
268
  chart->mark_dirty();
229
269
  }
230
270
 
@@ -298,6 +338,7 @@ extern "C" void vroom_chart_reset_view(VroomChart* chart) {
298
338
  // An asset switch reframes wholesale, so a slot-paired morph is meaningless
299
339
  // here — drop any capture rather than leaving it to reshape the wrong data.
300
340
  chart->morph_from.clear();
341
+ chart->morph_lines.clear();
301
342
  chart->interval_morph_t = 1.f;
302
343
  chart->interval_morph_fade = false;
303
344
  apply_default_framing(chart);
@@ -353,11 +394,17 @@ extern "C" void vroom_chart_preserve_price_envelope(VroomChart* chart,
353
394
  chart->mark_dirty();
354
395
  }
355
396
 
356
- extern "C" void vroom_chart_begin_interval_morph(VroomChart* chart, int32_t mode) {
357
- if (!chart) return;
397
+ // Captures the visible candles and every indicator on screen into the chart's
398
+ // morph slots, normalized so the result survives the bounds change and any
399
+ // resize that lands mid-morph. Leaves `morph_from` empty (and the morph closed)
400
+ // when there is nothing to capture. Shared by the timeframe switch and the live
401
+ // stream update, which differ only in what they do with the capture afterwards.
402
+ static void capture_morph(VroomChart* chart) {
358
403
  chart->morph_from.clear();
404
+ chart->morph_lines.clear();
359
405
  chart->interval_morph_t = 1.f;
360
406
  chart->interval_morph_fade = false;
407
+ chart->morph_is_stream = false;
361
408
 
362
409
  const auto lay = chart->layout();
363
410
  const float area_w = vroom::candle_area_width(lay);
@@ -393,11 +440,21 @@ extern "C" void vroom_chart_begin_interval_morph(VroomChart* chart, int32_t mode
393
440
  };
394
441
  }
395
442
 
443
+ // Every indicator on screen, captured against the same slice and band so
444
+ // its line keeps its position relative to the candles it reshapes with.
445
+ chart->capture_morph_lines(lay, bounds, idx, window_ms);
446
+
396
447
  // The scale the capture was taken against — the axes render their outgoing
397
448
  // ticks from it, and it's about to be replaced on the chart itself.
398
449
  chart->morph_from_bounds = bounds;
399
450
  chart->morph_from_start_ms = chart->visible_start_ms;
400
451
  chart->morph_from_end_ms = chart->visible_end_ms;
452
+ }
453
+
454
+ extern "C" void vroom_chart_begin_interval_morph(VroomChart* chart, int32_t mode) {
455
+ if (!chart) return;
456
+ capture_morph(chart);
457
+ if (chart->morph_from.empty()) return;
401
458
 
402
459
  // Open the morph at 0 rather than leaving it at 1: the caller still has to
403
460
  // push the new candles, and any frame painted in between should show the
@@ -406,13 +463,50 @@ extern "C" void vroom_chart_begin_interval_morph(VroomChart* chart, int32_t mode
406
463
  chart->interval_morph_t = 0.f;
407
464
  }
408
465
 
466
+ extern "C" void vroom_chart_begin_stream_morph(VroomChart* chart) {
467
+ if (!chart) return;
468
+
469
+ // Ticks arrive faster than the morph lands, so nearly every one interrupts
470
+ // the last. Keep what was on screen and fold it into the fresh capture
471
+ // below; dropping it would snap the bar back to its un-morphed shape on
472
+ // every tick. Only a stream capture is worth continuing from — a timeframe
473
+ // switch in flight is a different, larger motion, and a tick landing in the
474
+ // middle of one should let it finish rather than inherit its geometry.
475
+ std::vector<vroom::CandleSnapshot> interrupted;
476
+ std::vector<vroom::LineMorph> interrupted_lines;
477
+ float interrupted_t = 1.f;
478
+ if (chart->morph_is_stream && chart->interval_morph_t < 1.f) {
479
+ interrupted.swap(chart->morph_from);
480
+ interrupted_lines.swap(chart->morph_lines);
481
+ interrupted_t = chart->interval_morph_t;
482
+ }
483
+
484
+ capture_morph(chart);
485
+ if (chart->morph_from.empty()) return;
486
+
487
+ if (!interrupted.empty()) {
488
+ vroom::blend_candle_snapshots(chart->morph_from.data(),
489
+ chart->morph_from.size(),
490
+ interrupted.data(), interrupted.size(),
491
+ interrupted_t);
492
+ vroom::blend_line_morphs(chart->morph_lines, interrupted_lines,
493
+ interrupted_t);
494
+ }
495
+
496
+ chart->morph_is_stream = true;
497
+ chart->interval_morph_t = 0.f;
498
+ }
499
+
409
500
  extern "C" void vroom_chart_set_interval_morph(VroomChart* chart, float t) {
410
501
  if (!chart) return;
411
502
  chart->interval_morph_t = std::clamp(t, 0.f, 1.f);
412
503
  if (chart->interval_morph_t >= 1.f) {
413
504
  chart->morph_from.clear();
414
505
  chart->morph_from.shrink_to_fit();
506
+ chart->morph_lines.clear();
507
+ chart->morph_lines.shrink_to_fit();
415
508
  chart->interval_morph_fade = false;
509
+ chart->morph_is_stream = false;
416
510
  }
417
511
  chart->mark_dirty();
418
512
  }
@@ -627,7 +721,9 @@ extern "C" void vroom_chart_scale_time_axis(VroomChart* chart, float dx_px) {
627
721
  extern "C" void vroom_chart_resize_indicator_pane(VroomChart* chart, float dy_px) {
628
722
  if (!chart || dy_px == 0.f) return;
629
723
 
630
- const int pane_count = (chart->rsi.enabled ? 1 : 0) + (chart->macd.enabled ? 1 : 0);
724
+ const int pane_count = (chart->rsi.enabled ? 1 : 0) +
725
+ (chart->macd.enabled ? 1 : 0) +
726
+ (chart->atr.enabled ? 1 : 0);
631
727
  if (pane_count == 0) return; // nothing below the chart to resize
632
728
 
633
729
  const auto lay = chart->layout();
@@ -676,42 +772,41 @@ extern "C" void vroom_chart_scale_indicator_axis(VroomChart* chart, float y_px,
676
772
  const auto lay = chart->layout();
677
773
  if (lay.indicator_area_h <= 0.f) return;
678
774
 
679
- // Rebuild the same ordered pane stack draw_chart uses (most recently
680
- // enabled pane sorts to the bottom) to find which pane y_px falls in.
681
- struct ActivePane { int order; int type; }; // type: 0 = RSI, 1 = MACD
682
- ActivePane panes[2];
683
- int count = 0;
684
- if (chart->rsi.enabled) panes[count++] = {chart->rsi_order, 0};
685
- if (chart->macd.enabled) panes[count++] = {chart->macd_order, 1};
775
+ // The same ordered pane stack the draw pass uses, so the pane under y_px is
776
+ // the one the user sees there.
777
+ vroom::IndicatorPane panes[vroom::kMaxPanes];
778
+ const int count = chart->indicator_panes(panes);
686
779
  if (count == 0) return;
687
- if (count == 2 && panes[0].order > panes[1].order) {
688
- const ActivePane tmp = panes[0];
689
- panes[0] = panes[1];
690
- panes[1] = tmp;
691
- }
692
780
 
693
781
  const float pane_h =
694
782
  chart->height_px * chart->theme.floats[VROOM_FLOAT_INDICATOR_HEIGHT_FRAC];
695
783
  if (pane_h <= 0.f) return;
696
784
 
697
- int target = -1; // 0 = RSI, 1 = MACD
785
+ const vroom::IndicatorPane* target = nullptr;
698
786
  float pane_top = vroom::price_pane_bottom(lay);
699
787
  for (int i = 0; i < count; ++i) {
700
788
  const float pane_bottom = pane_top + pane_h;
701
789
  if (y_px >= pane_top && y_px < pane_bottom) {
702
- target = panes[i].type;
790
+ target = &panes[i];
703
791
  break;
704
792
  }
705
793
  pane_top = pane_bottom;
706
794
  }
707
- if (target < 0) return; // not over a pane
795
+ if (!target) return; // not over a pane
708
796
 
709
797
  // Drag down (dy > 0) widens the visible value range (zoom out), matching
710
798
  // scale_price_axis's sign. The zoom is the inverse of the range scale.
711
799
  double range_scale = 1.0 + static_cast<double>(dy_px) / kAxisDragSensitivity;
712
800
  if (range_scale < 0.05) range_scale = 0.05; // never collapse or flip
713
801
 
714
- double& zoom = target == 0 ? chart->rsi_y_scale : chart->macd_y_scale;
802
+ double* zoom_ptr = nullptr;
803
+ switch (target->kind) {
804
+ case vroom::PaneKind::Rsi: zoom_ptr = &chart->rsi_y_scale; break;
805
+ case vroom::PaneKind::Macd: zoom_ptr = &chart->macd_y_scale; break;
806
+ case vroom::PaneKind::Atr: zoom_ptr = &chart->atr_y_scale; break;
807
+ }
808
+ if (!zoom_ptr) return;
809
+ double& zoom = *zoom_ptr;
715
810
  double next = zoom / range_scale;
716
811
  next = std::clamp(next, 0.1, 10.0);
717
812
  if (next == zoom) return;
@@ -1023,6 +1118,110 @@ extern "C" void vroom_chart_set_price_line_drag(VroomChart* chart, int32_t index
1023
1118
  chart->mark_dirty();
1024
1119
  }
1025
1120
 
1121
+ // ---- Footprints ------------------------------------------------------------
1122
+
1123
+ extern "C" void vroom_chart_set_footprints(VroomChart* chart,
1124
+ const VroomFootprint* prints,
1125
+ size_t count,
1126
+ const VroomFootprintStyle* style) {
1127
+ if (!chart) return;
1128
+ // Stored in the caller's order: vroom_chart_footprints_at hands these indices
1129
+ // back, and they're only useful if they still address the caller's array.
1130
+ if (count > 0 && prints) {
1131
+ chart->footprints.assign(prints, prints + count);
1132
+ } else {
1133
+ chart->footprints.clear();
1134
+ }
1135
+ if (style) chart->footprint_style = *style;
1136
+ chart->footprint_buckets_dirty = true;
1137
+ // A badge that no longer exists must not keep its highlight.
1138
+ if (chart->footprints.empty()) chart->hovered_footprint_side = -1;
1139
+ chart->mark_dirty();
1140
+ }
1141
+
1142
+ extern "C" bool vroom_chart_hit_test_footprint(VroomChart* chart, float x_px,
1143
+ float y_px,
1144
+ VroomFootprintHit* out) {
1145
+ if (!chart || chart->footprints.empty() || chart->candles.empty()) return false;
1146
+ chart->ensure_footprint_buckets();
1147
+ if (chart->footprint_buckets.empty()) return false;
1148
+
1149
+ // Same bounds and pane geometry as draw_chart, so the badges hit where they
1150
+ // render.
1151
+ const auto lay = chart->layout();
1152
+ const auto range = vroom::visible_indices(
1153
+ chart->candles.data(), chart->candles.size(),
1154
+ chart->visible_start_ms, chart->visible_end_ms);
1155
+ const size_t n = range.end - range.start;
1156
+ const auto bounds =
1157
+ chart->price_bounds_manual
1158
+ ? chart->price_bounds
1159
+ : vroom::auto_price_bounds(chart->candles.data() + range.start, n);
1160
+
1161
+ const auto hit = vroom::footprints::hit_test(*chart, lay, bounds, x_px, y_px);
1162
+ if (hit.side < 0) return false;
1163
+ if (out) *out = hit;
1164
+ return true;
1165
+ }
1166
+
1167
+ extern "C" int32_t vroom_chart_footprints_at(VroomChart* chart,
1168
+ int64_t candle_time_ms,
1169
+ int32_t* out_indices, int32_t max) {
1170
+ if (!chart || chart->footprints.empty()) return 0;
1171
+ chart->ensure_footprint_buckets();
1172
+
1173
+ const auto& buckets = chart->footprint_buckets;
1174
+ const auto it = std::lower_bound(
1175
+ buckets.begin(), buckets.end(), candle_time_ms,
1176
+ [](const vroom::footprints::Bucket& b, int64_t t) {
1177
+ return b.candle_time_ms < t;
1178
+ });
1179
+ if (it == buckets.end() || it->candle_time_ms != candle_time_ms) return 0;
1180
+
1181
+ // Both sides, ascending by time — merge the two already-sorted lists rather
1182
+ // than re-sorting them.
1183
+ const auto& buys = it->buys;
1184
+ const auto& sells = it->sells;
1185
+ const int32_t total = static_cast<int32_t>(buys.size() + sells.size());
1186
+ if (!out_indices || max <= 0) return total;
1187
+
1188
+ const auto& fps = chart->footprints;
1189
+ int32_t written = 0;
1190
+ size_t bi = 0;
1191
+ size_t si = 0;
1192
+ while (written < max && (bi < buys.size() || si < sells.size())) {
1193
+ bool take_buy;
1194
+ if (bi >= buys.size()) {
1195
+ take_buy = false;
1196
+ } else if (si >= sells.size()) {
1197
+ take_buy = true;
1198
+ } else {
1199
+ const int64_t tb = fps[static_cast<size_t>(buys[bi])].time_ms;
1200
+ const int64_t ts = fps[static_cast<size_t>(sells[si])].time_ms;
1201
+ take_buy = tb <= ts;
1202
+ }
1203
+ out_indices[written++] = take_buy ? buys[bi++] : sells[si++];
1204
+ }
1205
+ return total;
1206
+ }
1207
+
1208
+ extern "C" void vroom_chart_set_footprint_hover(VroomChart* chart,
1209
+ int64_t candle_time_ms,
1210
+ int32_t side) {
1211
+ if (!chart) return;
1212
+ if (side != VROOM_FOOTPRINT_BUY && side != VROOM_FOOTPRINT_SELL) {
1213
+ side = -1;
1214
+ candle_time_ms = 0;
1215
+ }
1216
+ if (chart->hovered_footprint_side == side &&
1217
+ chart->hovered_footprint_time_ms == candle_time_ms) {
1218
+ return; // hover fires on every pointer move; don't redraw for nothing
1219
+ }
1220
+ chart->hovered_footprint_side = side;
1221
+ chart->hovered_footprint_time_ms = candle_time_ms;
1222
+ chart->mark_dirty();
1223
+ }
1224
+
1026
1225
  extern "C" void vroom_chart_set_draft(VroomChart* chart, int64_t a_time,
1027
1226
  double a_price, bool has_b, int64_t b_time,
1028
1227
  double b_price, bool guide, uint32_t color,
@@ -1274,6 +1473,7 @@ extern "C" void vroom_chart_set_rsi(VroomChart* chart, const VroomRSI* cfg) {
1274
1473
  next.ma_visible = next.ma_visible ? 1 : 0;
1275
1474
  next.line_visible = next.line_visible ? 1 : 0;
1276
1475
  next.bands_visible = next.bands_visible ? 1 : 0;
1476
+ next.extreme_fill = next.extreme_fill ? 1 : 0;
1277
1477
  if (next.period < 2) next.period = 2;
1278
1478
  if (next.ma_period < 1) next.ma_period = 1;
1279
1479
  next.upper_band = std::clamp(next.upper_band, 0.0, 100.0);
@@ -1330,6 +1530,27 @@ extern "C" void vroom_chart_set_macd(VroomChart* chart, const VroomMACD* cfg) {
1330
1530
  chart->mark_dirty();
1331
1531
  }
1332
1532
 
1533
+ extern "C" void vroom_chart_set_atr(VroomChart* chart, const VroomATR* cfg) {
1534
+ if (!chart || !cfg) return;
1535
+ VroomATR next = *cfg;
1536
+ next.enabled = next.enabled ? 1 : 0;
1537
+ if (next.period < 1) next.period = 1;
1538
+
1539
+ // Only the series-affecting fields force a recompute; color and width
1540
+ // changes are render-only.
1541
+ const VroomATR& cur = chart->atr;
1542
+ const bool recompute = cur.enabled != next.enabled ||
1543
+ cur.period != next.period ||
1544
+ cur.smoothing != next.smoothing;
1545
+
1546
+ if (next.enabled && !cur.enabled) chart->atr_order = chart->pane_seq++;
1547
+ else if (!next.enabled) chart->atr_order = -1;
1548
+
1549
+ chart->atr = next;
1550
+ if (recompute) chart->atr_dirty = true;
1551
+ chart->mark_dirty();
1552
+ }
1553
+
1333
1554
  extern "C" void vroom_chart_set_overlays(VroomChart* chart,
1334
1555
  const VroomOverlay* overlays,
1335
1556
  size_t count) {
@@ -1381,6 +1602,79 @@ extern "C" void vroom_chart_set_bollinger(VroomChart* chart,
1381
1602
  chart->mark_dirty();
1382
1603
  }
1383
1604
 
1605
+ extern "C" void vroom_chart_set_ichimoku(VroomChart* chart,
1606
+ const VroomIchimoku* cfg) {
1607
+ if (!chart || !cfg) return;
1608
+ VroomIchimoku next = *cfg;
1609
+ next.enabled = next.enabled ? 1 : 0;
1610
+ next.tenkan_enabled = next.tenkan_enabled ? 1 : 0;
1611
+ next.kijun_enabled = next.kijun_enabled ? 1 : 0;
1612
+ next.senkou_a_enabled = next.senkou_a_enabled ? 1 : 0;
1613
+ next.senkou_b_enabled = next.senkou_b_enabled ? 1 : 0;
1614
+ next.chikou_enabled = next.chikou_enabled ? 1 : 0;
1615
+ next.cloud_enabled = next.cloud_enabled ? 1 : 0;
1616
+ if (next.tenkan_period < 1) next.tenkan_period = 1;
1617
+ if (next.kijun_period < 1) next.kijun_period = 1;
1618
+ if (next.senkou_b_period < 1) next.senkou_b_period = 1;
1619
+ if (next.displacement < 0) next.displacement = 0;
1620
+ next.cloud_opacity = std::clamp(next.cloud_opacity, 0.f, 1.f);
1621
+
1622
+ // Only the series-affecting fields force a recompute. Displacement is not
1623
+ // one of them — it shifts where the caches are drawn, not what's in them.
1624
+ const VroomIchimoku& cur = chart->ichimoku;
1625
+ const bool recompute = cur.enabled != next.enabled ||
1626
+ cur.tenkan_period != next.tenkan_period ||
1627
+ cur.kijun_period != next.kijun_period ||
1628
+ cur.senkou_b_period != next.senkou_b_period;
1629
+ const bool turned_on = !cur.enabled && next.enabled;
1630
+ chart->ichimoku = next;
1631
+ if (recompute) chart->ichimoku_dirty = true;
1632
+ // Turning it on mid-session misses the reserve the default framing makes,
1633
+ // and the leading spans would sit off the right edge until the user panned.
1634
+ if (turned_on) reserve_ichimoku_future(chart);
1635
+ chart->mark_dirty();
1636
+ }
1637
+
1638
+ extern "C" void vroom_chart_set_fair_value_gaps(VroomChart* chart,
1639
+ const VroomFairValueGaps* cfg) {
1640
+ if (!chart || !cfg) return;
1641
+ VroomFairValueGaps next = *cfg;
1642
+ next.enabled = next.enabled ? 1 : 0;
1643
+ next.wait_for_close = next.wait_for_close ? 1 : 0;
1644
+ next.delete_after_fill = next.delete_after_fill ? 1 : 0;
1645
+ next.extend_boxes = next.extend_boxes ? 1 : 0;
1646
+ next.border_enabled = next.border_enabled ? 1 : 0;
1647
+ next.labels_enabled = next.labels_enabled ? 1 : 0;
1648
+ next.show_inverse = next.show_inverse ? 1 : 0;
1649
+ next.fill_type = next.fill_type == vroom::fvg::kFillWick
1650
+ ? vroom::fvg::kFillWick
1651
+ : vroom::fvg::kFillClose;
1652
+ next.border_style = std::clamp(next.border_style, 0, 2);
1653
+ if (next.max_bars_back < 0) next.max_bars_back = 0;
1654
+ if (next.box_length < 1) next.box_length = 1;
1655
+ if (next.label_distance < 0) next.label_distance = 0;
1656
+ next.opacity = std::clamp(next.opacity, 0.f, 1.f);
1657
+
1658
+ // The labels are owned here; the struct's pointers are not retained, so the
1659
+ // caller may free theirs as soon as this returns.
1660
+ if (next.label) chart->fvg_label = next.label;
1661
+ next.label = nullptr;
1662
+ if (next.inverse_label) chart->fvg_inverse_label = next.inverse_label;
1663
+ next.inverse_label = nullptr;
1664
+
1665
+ // Only the detection inputs force a rescan. Box geometry, colors, borders,
1666
+ // labels and the inverse pass all read the same cache at draw time —
1667
+ // invalidation is detected unconditionally alongside the fill.
1668
+ const VroomFairValueGaps& cur = chart->fvg;
1669
+ const bool recompute = cur.enabled != next.enabled ||
1670
+ cur.max_bars_back != next.max_bars_back ||
1671
+ cur.wait_for_close != next.wait_for_close ||
1672
+ cur.fill_type != next.fill_type;
1673
+ chart->fvg = next;
1674
+ if (recompute) chart->fvg_dirty = true;
1675
+ chart->mark_dirty();
1676
+ }
1677
+
1384
1678
  extern "C" void vroom_chart_set_volume(VroomChart* chart,
1385
1679
  const VroomVolume* cfg) {
1386
1680
  if (!chart || !cfg) return;
@@ -0,0 +1,76 @@
1
+ #include "fair_value_gaps.h"
2
+
3
+ #include <algorithm>
4
+
5
+ namespace vroom::fvg {
6
+
7
+ namespace {
8
+
9
+ // True once `c` has traded through the far edge of the band as seen from
10
+ // `bullish` — the bottom when bullish, the top when bearish. Under kFillClose
11
+ // the candle has to close past it; under kFillWick the wick reaching it is
12
+ // enough.
13
+ //
14
+ // Polarity is a parameter rather than `g.bullish` because the same test settles
15
+ // both events: a gap is filled when price crosses it in its own direction, and
16
+ // the zone it inverts into is reclaimed when price crosses back the other way.
17
+ bool crossed(const ::VroomCandle& c, const Gap& g, bool bullish, int fill_type) {
18
+ if (fill_type == kFillWick) {
19
+ return bullish ? c.low <= g.bottom : c.high >= g.top;
20
+ }
21
+ return bullish ? c.close <= g.bottom : c.close >= g.top;
22
+ }
23
+
24
+ } // namespace
25
+
26
+ void compute(const ::VroomCandle* candles, std::size_t n, int max_bars_back,
27
+ bool wait_for_close, int fill_type, std::vector<Gap>& out) {
28
+ out.clear();
29
+ if (!candles || n < 3 || max_bars_back <= 0) return;
30
+
31
+ // The middle bar of the three. It needs a bar on each side, and under
32
+ // wait_for_close the third one must not be the still-forming newest bar.
33
+ const std::size_t last = wait_for_close ? n - 3 : n - 2;
34
+ const std::size_t window = static_cast<std::size_t>(max_bars_back);
35
+ const std::size_t first = std::max<std::size_t>(1, n > window ? n - window : 0);
36
+
37
+ for (std::size_t i = first; i <= last; ++i) {
38
+ const ::VroomCandle& prev = candles[i - 1];
39
+ const ::VroomCandle& next = candles[i + 1];
40
+
41
+ Gap g;
42
+ if (prev.high < next.low) {
43
+ g.bullish = true;
44
+ g.bottom = prev.high;
45
+ g.top = next.low;
46
+ } else if (prev.low > next.high) {
47
+ g.bullish = false;
48
+ g.bottom = next.high;
49
+ g.top = prev.low;
50
+ } else {
51
+ continue; // the wicks overlap — price left nothing behind
52
+ }
53
+ g.time_ms = candles[i].time_ms;
54
+
55
+ std::size_t j = i + 2;
56
+ for (; j < n; ++j) {
57
+ if (crossed(candles[j], g, g.bullish, fill_type)) {
58
+ g.filled_ms = candles[j].time_ms;
59
+ break;
60
+ }
61
+ }
62
+
63
+ // Resume after the filling bar with the polarity flipped. Starting at
64
+ // j + 1 is what keeps an earlier bar that happened to sit past the
65
+ // opposite edge from counting — the inversion does not exist yet.
66
+ for (++j; g.filled_ms != 0 && j < n; ++j) {
67
+ if (crossed(candles[j], g, !g.bullish, fill_type)) {
68
+ g.invalidated_ms = candles[j].time_ms;
69
+ break;
70
+ }
71
+ }
72
+ out.push_back(g);
73
+ }
74
+ }
75
+
76
+ } // namespace vroom::fvg
@@ -0,0 +1,54 @@
1
+ // Fair Value Gaps over a candle series — pure, no Skia, so it builds into the
2
+ // unit-test target. Drawn as shaded price-pane boxes over three-candle
3
+ // imbalances.
4
+
5
+ #pragma once
6
+
7
+ #include <cstddef>
8
+ #include <cstdint>
9
+ #include <vector>
10
+
11
+ #include "vroom/vroom_chart.h" // ::VroomCandle
12
+
13
+ namespace vroom::fvg {
14
+
15
+ // One detected imbalance, anchored in absolute time and price so the renderer
16
+ // can place it without knowing which bar it came from.
17
+ struct Gap {
18
+ int64_t time_ms = 0; // open time of the middle bar
19
+ double top = 0.0; // upper price edge
20
+ double bottom = 0.0; // lower price edge
21
+ bool bullish = false;
22
+ int64_t filled_ms = 0; // open time of the bar that filled it; 0 = unfilled
23
+ // Open time of the bar that reclaimed the zone after it inverted; 0 while
24
+ // the inversion still stands. Only meaningful once filled_ms is set.
25
+ int64_t invalidated_ms = 0;
26
+ };
27
+
28
+ // Which price settles that a gap has been traded back through.
29
+ enum FillType : int {
30
+ kFillClose = 0, // a candle must close past the far edge
31
+ kFillWick = 1, // a high or low reaching through is enough
32
+ };
33
+
34
+ // Detects the gaps in the last `max_bars_back` bars of [candles, candles+n),
35
+ // oldest first, and resolves each one's fill.
36
+ //
37
+ // A gap sits around the middle bar of a three-candle run whose outer wicks miss
38
+ // each other: bullish when candles[i-1].high < candles[i+1].low (spanning that
39
+ // range), bearish when candles[i-1].low > candles[i+1].high. `wait_for_close`
40
+ // withholds a gap whose third candle is still the newest, live bar.
41
+ //
42
+ // The fill scan walks forward from i+2 and records the first bar to reach the
43
+ // far edge — the bottom of a bullish gap, the top of a bearish one.
44
+ //
45
+ // Closing through a gap inverts it: the band price just rejected becomes a
46
+ // zone of the opposite polarity, resistance overhead where a bullish gap was
47
+ // and support underfoot where a bearish one was. A second scan resumes after
48
+ // the fill and records the bar that reclaims the band the other way, ending
49
+ // the inversion. Both scans always run, so whether a filled or inverted box is
50
+ // drawn at all stays a drawing concern.
51
+ void compute(const ::VroomCandle* candles, std::size_t n, int max_bars_back,
52
+ bool wait_for_close, int fill_type, std::vector<Gap>& out);
53
+
54
+ } // namespace vroom::fvg