react-native-vroom-chart 0.15.0 → 0.17.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.
- package/cpp/VroomChartHostObject.cpp +252 -1
- package/cpp/_core_include/vroom/vroom_chart.h +178 -4
- package/cpp/_core_src/atr.cpp +67 -0
- package/cpp/_core_src/atr.h +44 -0
- package/cpp/_core_src/atr_pane.cpp +162 -0
- package/cpp/_core_src/atr_pane.h +48 -0
- package/cpp/_core_src/candles.cpp +15 -14
- package/cpp/_core_src/chart.cpp +758 -200
- package/cpp/_core_src/chart.h +221 -3
- package/cpp/_core_src/chart_facade.cpp +236 -23
- package/cpp/_core_src/color_lerp.h +28 -0
- package/cpp/_core_src/fair_value_gaps.cpp +76 -0
- package/cpp/_core_src/fair_value_gaps.h +54 -0
- package/cpp/_core_src/fvg_overlay.cpp +262 -0
- package/cpp/_core_src/fvg_overlay.h +43 -0
- package/cpp/_core_src/ichimoku.cpp +65 -0
- package/cpp/_core_src/ichimoku.h +45 -0
- package/cpp/_core_src/labels.cpp +3 -0
- package/cpp/_core_src/line_morph.h +159 -0
- package/cpp/_core_src/loading_line.cpp +183 -0
- package/cpp/_core_src/loading_line.h +38 -0
- package/cpp/_core_src/loading_wave.h +140 -0
- package/cpp/_core_src/ma_overlay.cpp +273 -45
- package/cpp/_core_src/ma_overlay.h +64 -2
- package/cpp/_core_src/macd.cpp +24 -1
- package/cpp/_core_src/macd.h +16 -0
- package/cpp/_core_src/macd_pane.cpp +71 -62
- package/cpp/_core_src/macd_pane.h +13 -1
- package/cpp/_core_src/pane_series.h +169 -0
- package/cpp/_core_src/price_indicator.cpp +21 -7
- package/cpp/_core_src/price_indicator.h +11 -1
- package/cpp/_core_src/price_indicator_anim.h +81 -0
- package/cpp/_core_src/rsi.cpp +4 -0
- package/cpp/_core_src/rsi.h +7 -0
- package/cpp/_core_src/rsi_pane.cpp +85 -29
- package/cpp/_core_src/rsi_pane.h +11 -1
- package/cpp/_core_src/theme.cpp +5 -0
- package/cpp/_core_src/tip_geometry.h +55 -0
- package/cpp/_core_src/viewport.h +68 -0
- package/lib/index.d.mts +281 -3
- package/lib/index.d.ts +281 -3
- package/lib/index.js +292 -14
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +292 -14
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/VroomChart.tsx +33 -3
- package/src/dataTransitions.ts +47 -0
- package/src/index.ts +5 -0
- package/src/jsi.d.ts +139 -1
- package/src/theme.ts +1 -0
- package/src/types.ts +5 -0
- package/src/useChartCore.ts +402 -8
|
@@ -83,6 +83,32 @@ int64_t damp_future_delta(int64_t delta_ms, int64_t cur_future,
|
|
|
83
83
|
return static_cast<int64_t>(static_cast<double>(delta_ms) * resist);
|
|
84
84
|
}
|
|
85
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
|
+
|
|
86
112
|
// Frame the default view. When the consumer has set a target candle body width
|
|
87
113
|
// (default_candle_px), size the window so each candle renders ~that wide with the
|
|
88
114
|
// right edge pinned to the latest candle; otherwise fall back to the legacy
|
|
@@ -102,16 +128,17 @@ void apply_default_framing(VroomChart* chart) {
|
|
|
102
128
|
lay.width_px - lay.y_axis_width_px - lay.right_padding_px;
|
|
103
129
|
|
|
104
130
|
// Places a window of the given width with kRightGapPx of air past the newest
|
|
105
|
-
// candle
|
|
106
|
-
//
|
|
107
|
-
//
|
|
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.
|
|
108
135
|
const auto frame = [&](int64_t window_ms) {
|
|
109
136
|
const int64_t gap_ms =
|
|
110
137
|
usable > 0.0 && window_ms > 0
|
|
111
138
|
? static_cast<int64_t>((kRightGapPx * static_cast<double>(window_ms)) /
|
|
112
139
|
usable)
|
|
113
140
|
: 0;
|
|
114
|
-
chart->visible_end_ms = right_edge_ms + gap_ms;
|
|
141
|
+
chart->visible_end_ms = right_edge_ms + gap_ms + ichimoku_future_ms(chart);
|
|
115
142
|
chart->visible_start_ms = chart->visible_end_ms - window_ms;
|
|
116
143
|
};
|
|
117
144
|
|
|
@@ -184,9 +211,12 @@ extern "C" void vroom_chart_set_candles(VroomChart* chart, const VroomCandle* da
|
|
|
184
211
|
chart->candles.assign(data, data + count);
|
|
185
212
|
chart->rsi_dirty = true;
|
|
186
213
|
chart->macd_dirty = true;
|
|
214
|
+
chart->atr_dirty = true;
|
|
187
215
|
chart->overlays_dirty = true;
|
|
188
216
|
chart->vwap_dirty = true;
|
|
189
217
|
chart->bollinger_dirty = true;
|
|
218
|
+
chart->ichimoku_dirty = true;
|
|
219
|
+
chart->fvg_dirty = true;
|
|
190
220
|
// New bars (or a whole new interval) mean the footprint grouping no longer
|
|
191
221
|
// matches the data — regroup on next use.
|
|
192
222
|
chart->footprint_buckets_dirty = true;
|
|
@@ -215,9 +245,12 @@ extern "C" void vroom_chart_append_candle(VroomChart* chart, const VroomCandle*
|
|
|
215
245
|
chart->candles.push_back(*c);
|
|
216
246
|
chart->rsi_dirty = true;
|
|
217
247
|
chart->macd_dirty = true;
|
|
248
|
+
chart->atr_dirty = true;
|
|
218
249
|
chart->overlays_dirty = true;
|
|
219
250
|
chart->vwap_dirty = true;
|
|
220
251
|
chart->bollinger_dirty = true;
|
|
252
|
+
chart->ichimoku_dirty = true;
|
|
253
|
+
chart->fvg_dirty = true;
|
|
221
254
|
chart->mark_dirty();
|
|
222
255
|
}
|
|
223
256
|
|
|
@@ -226,9 +259,12 @@ extern "C" void vroom_chart_update_last(VroomChart* chart, const VroomCandle* c)
|
|
|
226
259
|
chart->candles.back() = *c;
|
|
227
260
|
chart->rsi_dirty = true;
|
|
228
261
|
chart->macd_dirty = true;
|
|
262
|
+
chart->atr_dirty = true;
|
|
229
263
|
chart->overlays_dirty = true;
|
|
230
264
|
chart->vwap_dirty = true;
|
|
231
265
|
chart->bollinger_dirty = true;
|
|
266
|
+
chart->ichimoku_dirty = true;
|
|
267
|
+
chart->fvg_dirty = true;
|
|
232
268
|
chart->mark_dirty();
|
|
233
269
|
}
|
|
234
270
|
|
|
@@ -302,6 +338,7 @@ extern "C" void vroom_chart_reset_view(VroomChart* chart) {
|
|
|
302
338
|
// An asset switch reframes wholesale, so a slot-paired morph is meaningless
|
|
303
339
|
// here — drop any capture rather than leaving it to reshape the wrong data.
|
|
304
340
|
chart->morph_from.clear();
|
|
341
|
+
chart->morph_lines.clear();
|
|
305
342
|
chart->interval_morph_t = 1.f;
|
|
306
343
|
chart->interval_morph_fade = false;
|
|
307
344
|
apply_default_framing(chart);
|
|
@@ -357,11 +394,17 @@ extern "C" void vroom_chart_preserve_price_envelope(VroomChart* chart,
|
|
|
357
394
|
chart->mark_dirty();
|
|
358
395
|
}
|
|
359
396
|
|
|
360
|
-
|
|
361
|
-
|
|
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) {
|
|
362
403
|
chart->morph_from.clear();
|
|
404
|
+
chart->morph_lines.clear();
|
|
363
405
|
chart->interval_morph_t = 1.f;
|
|
364
406
|
chart->interval_morph_fade = false;
|
|
407
|
+
chart->morph_is_stream = false;
|
|
365
408
|
|
|
366
409
|
const auto lay = chart->layout();
|
|
367
410
|
const float area_w = vroom::candle_area_width(lay);
|
|
@@ -397,11 +440,21 @@ extern "C" void vroom_chart_begin_interval_morph(VroomChart* chart, int32_t mode
|
|
|
397
440
|
};
|
|
398
441
|
}
|
|
399
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
|
+
|
|
400
447
|
// The scale the capture was taken against — the axes render their outgoing
|
|
401
448
|
// ticks from it, and it's about to be replaced on the chart itself.
|
|
402
449
|
chart->morph_from_bounds = bounds;
|
|
403
450
|
chart->morph_from_start_ms = chart->visible_start_ms;
|
|
404
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;
|
|
405
458
|
|
|
406
459
|
// Open the morph at 0 rather than leaving it at 1: the caller still has to
|
|
407
460
|
// push the new candles, and any frame painted in between should show the
|
|
@@ -410,13 +463,77 @@ extern "C" void vroom_chart_begin_interval_morph(VroomChart* chart, int32_t mode
|
|
|
410
463
|
chart->interval_morph_t = 0.f;
|
|
411
464
|
}
|
|
412
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
|
+
|
|
500
|
+
extern "C" void vroom_chart_set_loading(VroomChart* chart, int32_t on,
|
|
501
|
+
int32_t animate) {
|
|
502
|
+
if (!chart) return;
|
|
503
|
+
chart->set_loading(on != 0, animate != 0);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
extern "C" void vroom_chart_begin_loading_morph(VroomChart* chart) {
|
|
507
|
+
if (!chart) return;
|
|
508
|
+
chart->begin_loading_morph();
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
extern "C" void vroom_chart_set_loading_morph(VroomChart* chart, float t) {
|
|
512
|
+
if (!chart) return;
|
|
513
|
+
chart->loading_line_t = std::clamp(t, 0.f, 1.f);
|
|
514
|
+
chart->mark_dirty();
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
extern "C" void vroom_chart_begin_loading_reveal(VroomChart* chart) {
|
|
518
|
+
if (!chart) return;
|
|
519
|
+
chart->begin_loading_reveal();
|
|
520
|
+
}
|
|
521
|
+
|
|
413
522
|
extern "C" void vroom_chart_set_interval_morph(VroomChart* chart, float t) {
|
|
414
523
|
if (!chart) return;
|
|
415
524
|
chart->interval_morph_t = std::clamp(t, 0.f, 1.f);
|
|
416
525
|
if (chart->interval_morph_t >= 1.f) {
|
|
417
526
|
chart->morph_from.clear();
|
|
418
527
|
chart->morph_from.shrink_to_fit();
|
|
528
|
+
chart->morph_lines.clear();
|
|
529
|
+
chart->morph_lines.shrink_to_fit();
|
|
419
530
|
chart->interval_morph_fade = false;
|
|
531
|
+
chart->morph_is_stream = false;
|
|
532
|
+
// The loading line rides this same clock out (see draw_chart 5.9), so
|
|
533
|
+
// it has to be released here too or it would hang over a settled chart.
|
|
534
|
+
chart->loading_line_revealing = false;
|
|
535
|
+
chart->loading_line.clear();
|
|
536
|
+
chart->loading_line.shrink_to_fit();
|
|
420
537
|
}
|
|
421
538
|
chart->mark_dirty();
|
|
422
539
|
}
|
|
@@ -631,7 +748,9 @@ extern "C" void vroom_chart_scale_time_axis(VroomChart* chart, float dx_px) {
|
|
|
631
748
|
extern "C" void vroom_chart_resize_indicator_pane(VroomChart* chart, float dy_px) {
|
|
632
749
|
if (!chart || dy_px == 0.f) return;
|
|
633
750
|
|
|
634
|
-
const int pane_count = (chart->rsi.enabled ? 1 : 0) +
|
|
751
|
+
const int pane_count = (chart->rsi.enabled ? 1 : 0) +
|
|
752
|
+
(chart->macd.enabled ? 1 : 0) +
|
|
753
|
+
(chart->atr.enabled ? 1 : 0);
|
|
635
754
|
if (pane_count == 0) return; // nothing below the chart to resize
|
|
636
755
|
|
|
637
756
|
const auto lay = chart->layout();
|
|
@@ -680,42 +799,41 @@ extern "C" void vroom_chart_scale_indicator_axis(VroomChart* chart, float y_px,
|
|
|
680
799
|
const auto lay = chart->layout();
|
|
681
800
|
if (lay.indicator_area_h <= 0.f) return;
|
|
682
801
|
|
|
683
|
-
//
|
|
684
|
-
//
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
int count = 0;
|
|
688
|
-
if (chart->rsi.enabled) panes[count++] = {chart->rsi_order, 0};
|
|
689
|
-
if (chart->macd.enabled) panes[count++] = {chart->macd_order, 1};
|
|
802
|
+
// The same ordered pane stack the draw pass uses, so the pane under y_px is
|
|
803
|
+
// the one the user sees there.
|
|
804
|
+
vroom::IndicatorPane panes[vroom::kMaxPanes];
|
|
805
|
+
const int count = chart->indicator_panes(panes);
|
|
690
806
|
if (count == 0) return;
|
|
691
|
-
if (count == 2 && panes[0].order > panes[1].order) {
|
|
692
|
-
const ActivePane tmp = panes[0];
|
|
693
|
-
panes[0] = panes[1];
|
|
694
|
-
panes[1] = tmp;
|
|
695
|
-
}
|
|
696
807
|
|
|
697
808
|
const float pane_h =
|
|
698
809
|
chart->height_px * chart->theme.floats[VROOM_FLOAT_INDICATOR_HEIGHT_FRAC];
|
|
699
810
|
if (pane_h <= 0.f) return;
|
|
700
811
|
|
|
701
|
-
|
|
812
|
+
const vroom::IndicatorPane* target = nullptr;
|
|
702
813
|
float pane_top = vroom::price_pane_bottom(lay);
|
|
703
814
|
for (int i = 0; i < count; ++i) {
|
|
704
815
|
const float pane_bottom = pane_top + pane_h;
|
|
705
816
|
if (y_px >= pane_top && y_px < pane_bottom) {
|
|
706
|
-
target = panes[i]
|
|
817
|
+
target = &panes[i];
|
|
707
818
|
break;
|
|
708
819
|
}
|
|
709
820
|
pane_top = pane_bottom;
|
|
710
821
|
}
|
|
711
|
-
if (target
|
|
822
|
+
if (!target) return; // not over a pane
|
|
712
823
|
|
|
713
824
|
// Drag down (dy > 0) widens the visible value range (zoom out), matching
|
|
714
825
|
// scale_price_axis's sign. The zoom is the inverse of the range scale.
|
|
715
826
|
double range_scale = 1.0 + static_cast<double>(dy_px) / kAxisDragSensitivity;
|
|
716
827
|
if (range_scale < 0.05) range_scale = 0.05; // never collapse or flip
|
|
717
828
|
|
|
718
|
-
double
|
|
829
|
+
double* zoom_ptr = nullptr;
|
|
830
|
+
switch (target->kind) {
|
|
831
|
+
case vroom::PaneKind::Rsi: zoom_ptr = &chart->rsi_y_scale; break;
|
|
832
|
+
case vroom::PaneKind::Macd: zoom_ptr = &chart->macd_y_scale; break;
|
|
833
|
+
case vroom::PaneKind::Atr: zoom_ptr = &chart->atr_y_scale; break;
|
|
834
|
+
}
|
|
835
|
+
if (!zoom_ptr) return;
|
|
836
|
+
double& zoom = *zoom_ptr;
|
|
719
837
|
double next = zoom / range_scale;
|
|
720
838
|
next = std::clamp(next, 0.1, 10.0);
|
|
721
839
|
if (next == zoom) return;
|
|
@@ -1382,6 +1500,7 @@ extern "C" void vroom_chart_set_rsi(VroomChart* chart, const VroomRSI* cfg) {
|
|
|
1382
1500
|
next.ma_visible = next.ma_visible ? 1 : 0;
|
|
1383
1501
|
next.line_visible = next.line_visible ? 1 : 0;
|
|
1384
1502
|
next.bands_visible = next.bands_visible ? 1 : 0;
|
|
1503
|
+
next.extreme_fill = next.extreme_fill ? 1 : 0;
|
|
1385
1504
|
if (next.period < 2) next.period = 2;
|
|
1386
1505
|
if (next.ma_period < 1) next.ma_period = 1;
|
|
1387
1506
|
next.upper_band = std::clamp(next.upper_band, 0.0, 100.0);
|
|
@@ -1438,6 +1557,27 @@ extern "C" void vroom_chart_set_macd(VroomChart* chart, const VroomMACD* cfg) {
|
|
|
1438
1557
|
chart->mark_dirty();
|
|
1439
1558
|
}
|
|
1440
1559
|
|
|
1560
|
+
extern "C" void vroom_chart_set_atr(VroomChart* chart, const VroomATR* cfg) {
|
|
1561
|
+
if (!chart || !cfg) return;
|
|
1562
|
+
VroomATR next = *cfg;
|
|
1563
|
+
next.enabled = next.enabled ? 1 : 0;
|
|
1564
|
+
if (next.period < 1) next.period = 1;
|
|
1565
|
+
|
|
1566
|
+
// Only the series-affecting fields force a recompute; color and width
|
|
1567
|
+
// changes are render-only.
|
|
1568
|
+
const VroomATR& cur = chart->atr;
|
|
1569
|
+
const bool recompute = cur.enabled != next.enabled ||
|
|
1570
|
+
cur.period != next.period ||
|
|
1571
|
+
cur.smoothing != next.smoothing;
|
|
1572
|
+
|
|
1573
|
+
if (next.enabled && !cur.enabled) chart->atr_order = chart->pane_seq++;
|
|
1574
|
+
else if (!next.enabled) chart->atr_order = -1;
|
|
1575
|
+
|
|
1576
|
+
chart->atr = next;
|
|
1577
|
+
if (recompute) chart->atr_dirty = true;
|
|
1578
|
+
chart->mark_dirty();
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1441
1581
|
extern "C" void vroom_chart_set_overlays(VroomChart* chart,
|
|
1442
1582
|
const VroomOverlay* overlays,
|
|
1443
1583
|
size_t count) {
|
|
@@ -1489,6 +1629,79 @@ extern "C" void vroom_chart_set_bollinger(VroomChart* chart,
|
|
|
1489
1629
|
chart->mark_dirty();
|
|
1490
1630
|
}
|
|
1491
1631
|
|
|
1632
|
+
extern "C" void vroom_chart_set_ichimoku(VroomChart* chart,
|
|
1633
|
+
const VroomIchimoku* cfg) {
|
|
1634
|
+
if (!chart || !cfg) return;
|
|
1635
|
+
VroomIchimoku next = *cfg;
|
|
1636
|
+
next.enabled = next.enabled ? 1 : 0;
|
|
1637
|
+
next.tenkan_enabled = next.tenkan_enabled ? 1 : 0;
|
|
1638
|
+
next.kijun_enabled = next.kijun_enabled ? 1 : 0;
|
|
1639
|
+
next.senkou_a_enabled = next.senkou_a_enabled ? 1 : 0;
|
|
1640
|
+
next.senkou_b_enabled = next.senkou_b_enabled ? 1 : 0;
|
|
1641
|
+
next.chikou_enabled = next.chikou_enabled ? 1 : 0;
|
|
1642
|
+
next.cloud_enabled = next.cloud_enabled ? 1 : 0;
|
|
1643
|
+
if (next.tenkan_period < 1) next.tenkan_period = 1;
|
|
1644
|
+
if (next.kijun_period < 1) next.kijun_period = 1;
|
|
1645
|
+
if (next.senkou_b_period < 1) next.senkou_b_period = 1;
|
|
1646
|
+
if (next.displacement < 0) next.displacement = 0;
|
|
1647
|
+
next.cloud_opacity = std::clamp(next.cloud_opacity, 0.f, 1.f);
|
|
1648
|
+
|
|
1649
|
+
// Only the series-affecting fields force a recompute. Displacement is not
|
|
1650
|
+
// one of them — it shifts where the caches are drawn, not what's in them.
|
|
1651
|
+
const VroomIchimoku& cur = chart->ichimoku;
|
|
1652
|
+
const bool recompute = cur.enabled != next.enabled ||
|
|
1653
|
+
cur.tenkan_period != next.tenkan_period ||
|
|
1654
|
+
cur.kijun_period != next.kijun_period ||
|
|
1655
|
+
cur.senkou_b_period != next.senkou_b_period;
|
|
1656
|
+
const bool turned_on = !cur.enabled && next.enabled;
|
|
1657
|
+
chart->ichimoku = next;
|
|
1658
|
+
if (recompute) chart->ichimoku_dirty = true;
|
|
1659
|
+
// Turning it on mid-session misses the reserve the default framing makes,
|
|
1660
|
+
// and the leading spans would sit off the right edge until the user panned.
|
|
1661
|
+
if (turned_on) reserve_ichimoku_future(chart);
|
|
1662
|
+
chart->mark_dirty();
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
extern "C" void vroom_chart_set_fair_value_gaps(VroomChart* chart,
|
|
1666
|
+
const VroomFairValueGaps* cfg) {
|
|
1667
|
+
if (!chart || !cfg) return;
|
|
1668
|
+
VroomFairValueGaps next = *cfg;
|
|
1669
|
+
next.enabled = next.enabled ? 1 : 0;
|
|
1670
|
+
next.wait_for_close = next.wait_for_close ? 1 : 0;
|
|
1671
|
+
next.delete_after_fill = next.delete_after_fill ? 1 : 0;
|
|
1672
|
+
next.extend_boxes = next.extend_boxes ? 1 : 0;
|
|
1673
|
+
next.border_enabled = next.border_enabled ? 1 : 0;
|
|
1674
|
+
next.labels_enabled = next.labels_enabled ? 1 : 0;
|
|
1675
|
+
next.show_inverse = next.show_inverse ? 1 : 0;
|
|
1676
|
+
next.fill_type = next.fill_type == vroom::fvg::kFillWick
|
|
1677
|
+
? vroom::fvg::kFillWick
|
|
1678
|
+
: vroom::fvg::kFillClose;
|
|
1679
|
+
next.border_style = std::clamp(next.border_style, 0, 2);
|
|
1680
|
+
if (next.max_bars_back < 0) next.max_bars_back = 0;
|
|
1681
|
+
if (next.box_length < 1) next.box_length = 1;
|
|
1682
|
+
if (next.label_distance < 0) next.label_distance = 0;
|
|
1683
|
+
next.opacity = std::clamp(next.opacity, 0.f, 1.f);
|
|
1684
|
+
|
|
1685
|
+
// The labels are owned here; the struct's pointers are not retained, so the
|
|
1686
|
+
// caller may free theirs as soon as this returns.
|
|
1687
|
+
if (next.label) chart->fvg_label = next.label;
|
|
1688
|
+
next.label = nullptr;
|
|
1689
|
+
if (next.inverse_label) chart->fvg_inverse_label = next.inverse_label;
|
|
1690
|
+
next.inverse_label = nullptr;
|
|
1691
|
+
|
|
1692
|
+
// Only the detection inputs force a rescan. Box geometry, colors, borders,
|
|
1693
|
+
// labels and the inverse pass all read the same cache at draw time —
|
|
1694
|
+
// invalidation is detected unconditionally alongside the fill.
|
|
1695
|
+
const VroomFairValueGaps& cur = chart->fvg;
|
|
1696
|
+
const bool recompute = cur.enabled != next.enabled ||
|
|
1697
|
+
cur.max_bars_back != next.max_bars_back ||
|
|
1698
|
+
cur.wait_for_close != next.wait_for_close ||
|
|
1699
|
+
cur.fill_type != next.fill_type;
|
|
1700
|
+
chart->fvg = next;
|
|
1701
|
+
if (recompute) chart->fvg_dirty = true;
|
|
1702
|
+
chart->mark_dirty();
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1492
1705
|
extern "C" void vroom_chart_set_volume(VroomChart* chart,
|
|
1493
1706
|
const VroomVolume* cfg) {
|
|
1494
1707
|
if (!chart || !cfg) return;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Blending two packed ARGB colors.
|
|
2
|
+
//
|
|
3
|
+
// Shared because anything anchored to the newest candle has to cross-fade when
|
|
4
|
+
// that candle changes direction mid-morph: the body itself (candles.cpp) and
|
|
5
|
+
// the current-price indicator (price_indicator.cpp) both flip between the bull
|
|
6
|
+
// and bear accents, and a snap in either while the other eases is visible.
|
|
7
|
+
//
|
|
8
|
+
// Skia-free so the pure translation units can use it.
|
|
9
|
+
|
|
10
|
+
#pragma once
|
|
11
|
+
|
|
12
|
+
#include <cstdint>
|
|
13
|
+
|
|
14
|
+
namespace vroom {
|
|
15
|
+
|
|
16
|
+
// Channel-wise, including alpha. `t` is expected in [0,1].
|
|
17
|
+
inline uint32_t lerp_argb(uint32_t a, uint32_t b, float t) {
|
|
18
|
+
uint32_t out = 0;
|
|
19
|
+
for (int shift = 0; shift < 32; shift += 8) {
|
|
20
|
+
const float ca = static_cast<float>((a >> shift) & 0xFFu);
|
|
21
|
+
const float cb = static_cast<float>((b >> shift) & 0xFFu);
|
|
22
|
+
const auto v = static_cast<uint32_t>(ca + (cb - ca) * t + 0.5f);
|
|
23
|
+
out |= v << shift;
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
} // namespace vroom
|
|
@@ -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
|