react-native-vroom-chart 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cpp/VroomChartHostObject.cpp +171 -1
- package/cpp/_core_include/vroom/vroom_chart.h +137 -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 +461 -172
- package/cpp/_core_src/chart.h +150 -1
- package/cpp/_core_src/chart_facade.cpp +209 -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/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/viewport.h +35 -0
- package/lib/index.d.mts +251 -3
- package/lib/index.d.ts +251 -3
- package/lib/index.js +224 -6
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +224 -6
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/VroomChart.tsx +21 -3
- package/src/dataTransitions.ts +47 -0
- package/src/index.ts +5 -0
- package/src/jsi.d.ts +97 -1
- package/src/types.ts +5 -0
- package/src/useChartCore.ts +284 -5
package/cpp/_core_src/chart.h
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
#pragma once
|
|
10
10
|
|
|
11
11
|
#include <chrono>
|
|
12
|
+
#include <cstddef>
|
|
12
13
|
#include <string>
|
|
13
14
|
#include <vector>
|
|
14
15
|
|
|
@@ -22,14 +23,33 @@
|
|
|
22
23
|
#include "include/core/SkRefCnt.h"
|
|
23
24
|
#pragma clang diagnostic pop
|
|
24
25
|
|
|
26
|
+
#include "fair_value_gaps.h"
|
|
25
27
|
#include "footprints_layout.h"
|
|
26
28
|
#include "labels.h"
|
|
29
|
+
#include "line_morph.h"
|
|
27
30
|
#include "price_format.h"
|
|
28
31
|
#include "theme.h"
|
|
29
32
|
#include "viewport.h"
|
|
30
33
|
|
|
31
34
|
class SkCanvas;
|
|
32
35
|
|
|
36
|
+
namespace vroom {
|
|
37
|
+
|
|
38
|
+
// Which indicator owns a below-chart pane. Panes are otherwise
|
|
39
|
+
// interchangeable: they are all the same height and stack in enable order.
|
|
40
|
+
enum class PaneKind { Rsi, Macd, Atr };
|
|
41
|
+
|
|
42
|
+
// Count of PaneKind values — the most panes that can be stacked at once.
|
|
43
|
+
constexpr int kMaxPanes = 3;
|
|
44
|
+
|
|
45
|
+
// One enabled pane and the sequence number it claimed when it was turned on.
|
|
46
|
+
struct IndicatorPane {
|
|
47
|
+
int order;
|
|
48
|
+
PaneKind kind;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
} // namespace vroom
|
|
52
|
+
|
|
33
53
|
struct VroomChart {
|
|
34
54
|
// --- bridge / lifecycle -------------------------------------------------
|
|
35
55
|
VroomCallbacks cb{};
|
|
@@ -82,6 +102,19 @@ struct VroomChart {
|
|
|
82
102
|
std::vector<vroom::CandleSnapshot> morph_from;
|
|
83
103
|
float interval_morph_t = 1.f; // 1 = not morphing
|
|
84
104
|
bool interval_morph_fade = false;
|
|
105
|
+
// Set when the capture came from a live update rather than a timeframe
|
|
106
|
+
// switch. Same slot machinery, but the axis holds still: a stream morph
|
|
107
|
+
// only reshapes the last bar, and the ticks it sits between don't move, so
|
|
108
|
+
// running them through the interval envelope would flash the whole axis on
|
|
109
|
+
// every tick (see labels::interval_phase).
|
|
110
|
+
bool morph_is_stream = false;
|
|
111
|
+
// The outgoing shape of every indicator series that was on screen, captured
|
|
112
|
+
// alongside the candles and keyed by identity so a toggle landing in the
|
|
113
|
+
// same commit as the switch can't pair two different lines (see
|
|
114
|
+
// line_morph.h). A timeframe switch invalidates every indicator cache at
|
|
115
|
+
// once, so without this the lines would pop to their new values while the
|
|
116
|
+
// candles under them reshape.
|
|
117
|
+
std::vector<vroom::LineMorph> morph_lines;
|
|
85
118
|
// The pre-switch price scale and time window. The candle capture above is
|
|
86
119
|
// normalized against these, and the axes keep rendering their old ticks from
|
|
87
120
|
// them while fading out (see labels::interval_phase), so a label is never
|
|
@@ -123,7 +156,8 @@ struct VroomChart {
|
|
|
123
156
|
VroomRSI rsi{0, 14, 70.0, 30.0, 14, 0, 1,
|
|
124
157
|
0u, -1.f, 1, // RSI line
|
|
125
158
|
0u, -1.f, // trendline
|
|
126
|
-
0u, 1
|
|
159
|
+
0u, 1, // band rules
|
|
160
|
+
1}; // extreme shading
|
|
127
161
|
std::vector<double> rsi_cache; // RSI per candle (NaN where undefined)
|
|
128
162
|
std::vector<double> rsi_ma_cache; // MA of rsi_cache (empty if MA off)
|
|
129
163
|
bool rsi_dirty = true;
|
|
@@ -147,11 +181,23 @@ struct VroomChart {
|
|
|
147
181
|
// default auto-fit amplitude; >1 zooms in, <1 zooms out. Anchored at zero.
|
|
148
182
|
double macd_y_scale = 1.0;
|
|
149
183
|
|
|
184
|
+
// ATR, drawn in its own pane. atr_cache is aligned to `candles` (NaN
|
|
185
|
+
// warmup), recomputed lazily by ensure_atr() when atr_dirty.
|
|
186
|
+
// Defaults: 14-period true range under Wilder smoothing, style fields on
|
|
187
|
+
// their inherit sentinel.
|
|
188
|
+
VroomATR atr{0, 14, 0, 0u, -1.f};
|
|
189
|
+
std::vector<double> atr_cache;
|
|
190
|
+
bool atr_dirty = true;
|
|
191
|
+
// User y-axis zoom for the ATR pane (drag on its y-axis strip). 1.0 = the
|
|
192
|
+
// default 0..peak fit; >1 zooms in, <1 zooms out. Anchored at the baseline.
|
|
193
|
+
double atr_y_scale = 1.0;
|
|
194
|
+
|
|
150
195
|
// Stacking order for the indicator panes: each indicator gets the next
|
|
151
196
|
// sequence number on its off->on transition, so the most recently enabled
|
|
152
197
|
// pane sorts last (bottom). -1 = not currently enabled.
|
|
153
198
|
int rsi_order = -1;
|
|
154
199
|
int macd_order = -1;
|
|
200
|
+
int atr_order = -1;
|
|
155
201
|
int pane_seq = 0;
|
|
156
202
|
|
|
157
203
|
// Moving-average overlay lines (SMA/EMA) drawn on the price pane. Not panes
|
|
@@ -183,6 +229,46 @@ struct VroomChart {
|
|
|
183
229
|
std::vector<double> bb_lower_cache;
|
|
184
230
|
bool bollinger_dirty = true;
|
|
185
231
|
|
|
232
|
+
// Ichimoku overlay (price pane; no pane reserved). Caches aligned to
|
|
233
|
+
// `candles` (NaN warmup) and indexed by the bar each value was computed
|
|
234
|
+
// from — displacement is applied when drawing, so the caches survive a
|
|
235
|
+
// change to it. Recomputed lazily by ensure_ichimoku() when ichimoku_dirty.
|
|
236
|
+
// Defaults: the standard 9/26/52/26, blue tenkan / red kijun, green span A
|
|
237
|
+
// over orange span B, teal chikou, 15% cloud.
|
|
238
|
+
VroomIchimoku ichimoku{0, 9, 26, 52, 26,
|
|
239
|
+
0xff2962ff, 1.f, 1, // tenkan: blue
|
|
240
|
+
0xffef5350, 1.f, 1, // kijun: red
|
|
241
|
+
0xff26a69a, 1.f, 1, // senkou A: green
|
|
242
|
+
0xffff6d00, 1.f, 1, // senkou B: orange
|
|
243
|
+
0xff00bcd4, 1.f, 1, // chikou: teal
|
|
244
|
+
1, 0xff26a69a, 0xffef5350, 0.15f};
|
|
245
|
+
std::vector<double> ich_tenkan_cache;
|
|
246
|
+
std::vector<double> ich_kijun_cache;
|
|
247
|
+
std::vector<double> ich_senkou_a_cache;
|
|
248
|
+
std::vector<double> ich_senkou_b_cache;
|
|
249
|
+
std::vector<double> ich_chikou_cache;
|
|
250
|
+
bool ichimoku_dirty = true;
|
|
251
|
+
|
|
252
|
+
// Fair Value Gap overlay (price pane; no pane reserved). The cache holds the
|
|
253
|
+
// detected imbalances in absolute time/price, oldest first, so box geometry
|
|
254
|
+
// and style are free to change without re-scanning — only the detection
|
|
255
|
+
// inputs below dirty it. Recomputed lazily by ensure_fvg() when fvg_dirty.
|
|
256
|
+
//
|
|
257
|
+
// Defaults: 300 bars back, close-settled fills, deleted once filled, 20-slot
|
|
258
|
+
// boxes in green/red at 15%, solid 1px border, labels on.
|
|
259
|
+
VroomFairValueGaps fvg{0, 300, 0, 0, 1, 0, 20,
|
|
260
|
+
0xff26a69a, 0xffef5350, 0.15f,
|
|
261
|
+
1, 0, 1.f, 0xff26a69a, 0xffef5350,
|
|
262
|
+
1, nullptr, 10, 0u, 0.f,
|
|
263
|
+
0, 0xff26a69a, 0xffef5350, nullptr};
|
|
264
|
+
// Owns the label texts, so the caller may free theirs as soon as the setter
|
|
265
|
+
// returns. `fvg.label` and `fvg.inverse_label` are left null and these are
|
|
266
|
+
// what the renderer reads.
|
|
267
|
+
std::string fvg_label = "FVG";
|
|
268
|
+
std::string fvg_inverse_label = "iFVG";
|
|
269
|
+
std::vector<vroom::fvg::Gap> fvg_cache;
|
|
270
|
+
bool fvg_dirty = true;
|
|
271
|
+
|
|
186
272
|
// Volume bars (price pane, under the candles). On by default with every
|
|
187
273
|
// style field left on its inherit sentinel, so an untouched chart looks
|
|
188
274
|
// exactly as it did before the config existed. No cache — bar heights come
|
|
@@ -348,6 +434,69 @@ struct VroomChart {
|
|
|
348
434
|
// indicator is enabled.
|
|
349
435
|
void ensure_bollinger();
|
|
350
436
|
|
|
437
|
+
// Recomputes the Ichimoku caches when ichimoku_dirty and the indicator is
|
|
438
|
+
// enabled.
|
|
439
|
+
void ensure_ichimoku();
|
|
440
|
+
|
|
441
|
+
// Redetects the Fair Value Gaps when fvg_dirty and the indicator is enabled.
|
|
442
|
+
void ensure_fvg();
|
|
443
|
+
|
|
444
|
+
// Recomputes the ATR cache when atr_dirty and the indicator is enabled.
|
|
445
|
+
void ensure_atr();
|
|
446
|
+
|
|
447
|
+
// Fills morph_lines with the outgoing shape of every enabled indicator,
|
|
448
|
+
// normalized the way morph_from is. Called by begin_interval_morph while
|
|
449
|
+
// the pre-switch candles and caches are still resident — after the swap
|
|
450
|
+
// there is nothing left to capture. `range` is the visible slice and
|
|
451
|
+
// `bounds` the price band the capture is taken against, both shared with
|
|
452
|
+
// the candle capture so the two land on the same pixels.
|
|
453
|
+
void capture_morph_lines(const vroom::Layout& lay,
|
|
454
|
+
const vroom::PriceBounds& bounds,
|
|
455
|
+
vroom::IndexRange range, int64_t window_ms);
|
|
456
|
+
|
|
457
|
+
// Source indices whose *displaced* plot time lands in the visible window,
|
|
458
|
+
// for an Ichimoku span drawn at `time_ms + shift_ms`. A bar of padding
|
|
459
|
+
// either side keeps a run reaching the pane edge instead of stopping at the
|
|
460
|
+
// last bar strictly inside it. Shared by the cloud, the lines and the
|
|
461
|
+
// interval-morph capture so all three slice the same bars.
|
|
462
|
+
vroom::IndexRange ichimoku_source_range(int64_t shift_ms) const;
|
|
463
|
+
|
|
464
|
+
// The enabled panes written to `out` top to bottom, returning how many.
|
|
465
|
+
// Both the draw pass and the y-axis hit test read this, so the stack they
|
|
466
|
+
// see can't drift apart.
|
|
467
|
+
int indicator_panes(vroom::IndicatorPane out[vroom::kMaxPanes]) const;
|
|
468
|
+
|
|
469
|
+
// Draws the pane stack into the band below the candles, starting at
|
|
470
|
+
// `pane_top`. Shared by the settled draw and the interval-morph fade, which
|
|
471
|
+
// paint the panes at different points in the z-order.
|
|
472
|
+
void draw_indicator_panes(SkCanvas* canvas, const vroom::Layout& lay,
|
|
473
|
+
const VroomCandle* visible, std::size_t n,
|
|
474
|
+
std::size_t first, int64_t window_ms,
|
|
475
|
+
float candle_right, float pane_top,
|
|
476
|
+
bool use_capture, float morph_t);
|
|
477
|
+
|
|
478
|
+
// The price-pane indicator overlays, split where the candles sit in the
|
|
479
|
+
// z-order: the Bollinger band and Ichimoku cloud go behind them, the moving
|
|
480
|
+
// averages, VWAP, Bollinger and Ichimoku lines over them.
|
|
481
|
+
//
|
|
482
|
+
// `use_capture` and `morph_t` carry the interval morph, on the same terms
|
|
483
|
+
// the candles take it. A transform pairs each series with its capture (see
|
|
484
|
+
// morph_lines) and eases toward the new shape. A fade's outgoing half
|
|
485
|
+
// passes n = 0 with morph_t = 0, replaying the capture alone. A settled
|
|
486
|
+
// frame passes no capture at all.
|
|
487
|
+
void draw_overlay_fills(SkCanvas* canvas, const vroom::Layout& lay,
|
|
488
|
+
const vroom::PriceBounds& bounds,
|
|
489
|
+
const VroomCandle* visible, std::size_t n,
|
|
490
|
+
std::size_t first, int64_t window_ms,
|
|
491
|
+
float candle_right, float candle_area_h,
|
|
492
|
+
bool use_capture, float morph_t);
|
|
493
|
+
void draw_overlay_lines(SkCanvas* canvas, const vroom::Layout& lay,
|
|
494
|
+
const vroom::PriceBounds& bounds,
|
|
495
|
+
const VroomCandle* visible, std::size_t n,
|
|
496
|
+
std::size_t first, int64_t window_ms,
|
|
497
|
+
float candle_right, float candle_area_h,
|
|
498
|
+
bool use_capture, float morph_t);
|
|
499
|
+
|
|
351
500
|
// The main drawing pass. Calls into the labels and candles modules, and owns
|
|
352
501
|
// the per-frame animation clock (see begin_frame) so every host gets it —
|
|
353
502
|
// the web build draws straight through vroom_chart_draw rather than the
|
|
@@ -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,50 @@ 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
|
+
|
|
413
500
|
extern "C" void vroom_chart_set_interval_morph(VroomChart* chart, float t) {
|
|
414
501
|
if (!chart) return;
|
|
415
502
|
chart->interval_morph_t = std::clamp(t, 0.f, 1.f);
|
|
416
503
|
if (chart->interval_morph_t >= 1.f) {
|
|
417
504
|
chart->morph_from.clear();
|
|
418
505
|
chart->morph_from.shrink_to_fit();
|
|
506
|
+
chart->morph_lines.clear();
|
|
507
|
+
chart->morph_lines.shrink_to_fit();
|
|
419
508
|
chart->interval_morph_fade = false;
|
|
509
|
+
chart->morph_is_stream = false;
|
|
420
510
|
}
|
|
421
511
|
chart->mark_dirty();
|
|
422
512
|
}
|
|
@@ -631,7 +721,9 @@ extern "C" void vroom_chart_scale_time_axis(VroomChart* chart, float dx_px) {
|
|
|
631
721
|
extern "C" void vroom_chart_resize_indicator_pane(VroomChart* chart, float dy_px) {
|
|
632
722
|
if (!chart || dy_px == 0.f) return;
|
|
633
723
|
|
|
634
|
-
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);
|
|
635
727
|
if (pane_count == 0) return; // nothing below the chart to resize
|
|
636
728
|
|
|
637
729
|
const auto lay = chart->layout();
|
|
@@ -680,42 +772,41 @@ extern "C" void vroom_chart_scale_indicator_axis(VroomChart* chart, float y_px,
|
|
|
680
772
|
const auto lay = chart->layout();
|
|
681
773
|
if (lay.indicator_area_h <= 0.f) return;
|
|
682
774
|
|
|
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};
|
|
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);
|
|
690
779
|
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
780
|
|
|
697
781
|
const float pane_h =
|
|
698
782
|
chart->height_px * chart->theme.floats[VROOM_FLOAT_INDICATOR_HEIGHT_FRAC];
|
|
699
783
|
if (pane_h <= 0.f) return;
|
|
700
784
|
|
|
701
|
-
|
|
785
|
+
const vroom::IndicatorPane* target = nullptr;
|
|
702
786
|
float pane_top = vroom::price_pane_bottom(lay);
|
|
703
787
|
for (int i = 0; i < count; ++i) {
|
|
704
788
|
const float pane_bottom = pane_top + pane_h;
|
|
705
789
|
if (y_px >= pane_top && y_px < pane_bottom) {
|
|
706
|
-
target = panes[i]
|
|
790
|
+
target = &panes[i];
|
|
707
791
|
break;
|
|
708
792
|
}
|
|
709
793
|
pane_top = pane_bottom;
|
|
710
794
|
}
|
|
711
|
-
if (target
|
|
795
|
+
if (!target) return; // not over a pane
|
|
712
796
|
|
|
713
797
|
// Drag down (dy > 0) widens the visible value range (zoom out), matching
|
|
714
798
|
// scale_price_axis's sign. The zoom is the inverse of the range scale.
|
|
715
799
|
double range_scale = 1.0 + static_cast<double>(dy_px) / kAxisDragSensitivity;
|
|
716
800
|
if (range_scale < 0.05) range_scale = 0.05; // never collapse or flip
|
|
717
801
|
|
|
718
|
-
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;
|
|
719
810
|
double next = zoom / range_scale;
|
|
720
811
|
next = std::clamp(next, 0.1, 10.0);
|
|
721
812
|
if (next == zoom) return;
|
|
@@ -1382,6 +1473,7 @@ extern "C" void vroom_chart_set_rsi(VroomChart* chart, const VroomRSI* cfg) {
|
|
|
1382
1473
|
next.ma_visible = next.ma_visible ? 1 : 0;
|
|
1383
1474
|
next.line_visible = next.line_visible ? 1 : 0;
|
|
1384
1475
|
next.bands_visible = next.bands_visible ? 1 : 0;
|
|
1476
|
+
next.extreme_fill = next.extreme_fill ? 1 : 0;
|
|
1385
1477
|
if (next.period < 2) next.period = 2;
|
|
1386
1478
|
if (next.ma_period < 1) next.ma_period = 1;
|
|
1387
1479
|
next.upper_band = std::clamp(next.upper_band, 0.0, 100.0);
|
|
@@ -1438,6 +1530,27 @@ extern "C" void vroom_chart_set_macd(VroomChart* chart, const VroomMACD* cfg) {
|
|
|
1438
1530
|
chart->mark_dirty();
|
|
1439
1531
|
}
|
|
1440
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
|
+
|
|
1441
1554
|
extern "C" void vroom_chart_set_overlays(VroomChart* chart,
|
|
1442
1555
|
const VroomOverlay* overlays,
|
|
1443
1556
|
size_t count) {
|
|
@@ -1489,6 +1602,79 @@ extern "C" void vroom_chart_set_bollinger(VroomChart* chart,
|
|
|
1489
1602
|
chart->mark_dirty();
|
|
1490
1603
|
}
|
|
1491
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
|
+
|
|
1492
1678
|
extern "C" void vroom_chart_set_volume(VroomChart* chart,
|
|
1493
1679
|
const VroomVolume* cfg) {
|
|
1494
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
|