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.
- package/cpp/VroomChartHostObject.cpp +289 -2
- package/cpp/_core_include/vroom/vroom_chart.h +221 -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/chart.cpp +477 -172
- package/cpp/_core_src/chart.h +173 -1
- package/cpp/_core_src/chart_facade.cpp +317 -23
- package/cpp/_core_src/fair_value_gaps.cpp +76 -0
- package/cpp/_core_src/fair_value_gaps.h +54 -0
- package/cpp/_core_src/footprints.cpp +226 -0
- package/cpp/_core_src/footprints.h +45 -0
- package/cpp/_core_src/footprints_layout.cpp +140 -0
- package/cpp/_core_src/footprints_layout.h +94 -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/ma_overlay.cpp +259 -36
- package/cpp/_core_src/ma_overlay.h +57 -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/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/tip_pulse.h +4 -4
- package/cpp/_core_src/viewport.h +35 -0
- package/lib/index.d.mts +385 -3
- package/lib/index.d.ts +385 -3
- package/lib/index.js +305 -7
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +305 -7
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/VroomChart.tsx +108 -7
- package/src/dataTransitions.ts +47 -0
- package/src/index.ts +10 -0
- package/src/jsi.d.ts +136 -1
- package/src/types.ts +10 -0
- 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
|
|
105
|
-
//
|
|
106
|
-
//
|
|
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
|
-
|
|
357
|
-
|
|
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) +
|
|
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
|
-
//
|
|
680
|
-
//
|
|
681
|
-
|
|
682
|
-
|
|
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
|
-
|
|
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]
|
|
790
|
+
target = &panes[i];
|
|
703
791
|
break;
|
|
704
792
|
}
|
|
705
793
|
pane_top = pane_bottom;
|
|
706
794
|
}
|
|
707
|
-
if (target
|
|
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
|
|
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
|