react-native-vroom-chart 0.5.0 → 0.7.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/android/CMakeLists.txt +143 -0
- package/android/README.md +60 -5
- package/android/build.gradle +115 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/VroomChartJni.cpp +24 -0
- package/android/src/main/java/com/vroom/chart/VroomChartModule.kt +31 -0
- package/android/src/main/java/com/vroom/chart/VroomChartPackage.kt +31 -0
- package/cpp/VroomChartHostObject.cpp +368 -33
- package/cpp/VroomJsiInstaller.cpp +11 -1
- package/cpp/VroomSkiaContext.cpp +15 -7
- package/cpp/_core_include/vroom/vroom_chart.h +250 -22
- package/cpp/_core_src/bollinger.h +1 -1
- package/cpp/_core_src/candles.cpp +138 -41
- package/cpp/_core_src/candles.h +11 -1
- package/cpp/_core_src/chart.cpp +98 -40
- package/cpp/_core_src/chart.h +82 -20
- package/cpp/_core_src/chart_facade.cpp +297 -66
- package/cpp/_core_src/gradient.cpp +46 -0
- package/cpp/_core_src/gradient.h +31 -0
- package/cpp/_core_src/labels.cpp +49 -16
- package/cpp/_core_src/labels.h +33 -4
- package/cpp/_core_src/liquidity.cpp +3 -37
- package/cpp/_core_src/ma_overlay.cpp +174 -12
- package/cpp/_core_src/ma_overlay.h +55 -3
- package/cpp/_core_src/macd.cpp +13 -42
- package/cpp/_core_src/macd.h +11 -8
- package/cpp/_core_src/macd_pane.cpp +57 -27
- package/cpp/_core_src/price_line_layout.cpp +87 -0
- package/cpp/_core_src/price_line_layout.h +80 -0
- package/cpp/_core_src/price_lines.cpp +428 -0
- package/cpp/_core_src/price_lines.h +56 -0
- package/cpp/_core_src/rsi.cpp +4 -18
- package/cpp/_core_src/rsi.h +6 -6
- package/cpp/_core_src/rsi_pane.cpp +34 -19
- package/cpp/_core_src/series_ma.cpp +64 -0
- package/cpp/_core_src/series_ma.h +30 -0
- package/cpp/_core_src/style_inherit.h +35 -0
- package/cpp/_core_src/theme.cpp +2 -1
- package/cpp/_core_src/viewport.cpp +36 -12
- package/cpp/_core_src/viewport.h +50 -0
- package/cpp/_core_src/volume.cpp +33 -8
- package/cpp/_core_src/volume.h +12 -1
- package/cpp/_core_src/volume_anim.cpp +32 -0
- package/cpp/_core_src/volume_anim.h +41 -0
- package/lib/index.d.mts +261 -25
- package/lib/index.d.ts +261 -25
- package/lib/index.js +252 -36
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +252 -36
- package/lib/index.mjs.map +1 -1
- package/package.json +5 -4
- package/src/VroomChart.tsx +162 -11
- package/src/easing.ts +40 -0
- package/src/index.ts +5 -0
- package/src/jsi.d.ts +124 -20
- package/src/theme.ts +1 -0
- package/src/types.ts +5 -0
- package/src/useChartCore.ts +166 -28
package/cpp/_core_src/labels.cpp
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
#include <cstdio>
|
|
16
16
|
#include <cstring>
|
|
17
17
|
#include <ctime>
|
|
18
|
+
#include <vector>
|
|
18
19
|
|
|
19
20
|
#include "chart.h"
|
|
20
21
|
#include "fonts.h"
|
|
@@ -46,8 +47,25 @@ void advance(Fade& f, float step, float dt) {
|
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
// Drives a whole axis off the morph envelope instead of the per-label fades:
|
|
51
|
+
// everything in the phase's tick set shares one opacity, and anything outside it
|
|
52
|
+
// is dropped outright. At the midpoint the axis is transparent, so swapping the
|
|
53
|
+
// set there costs nothing visually.
|
|
54
|
+
template <typename Fade>
|
|
55
|
+
void apply_envelope(std::vector<Fade>& fades, float opacity) {
|
|
56
|
+
for (auto& f : fades) f.opacity = f.target > 0.f ? opacity : 0.f;
|
|
57
|
+
}
|
|
58
|
+
|
|
49
59
|
} // namespace
|
|
50
60
|
|
|
61
|
+
IntervalPhase interval_phase(const VroomChart& chart) {
|
|
62
|
+
if (chart.morph_from.empty() || chart.interval_morph_t >= 1.f) return {};
|
|
63
|
+
const float t = chart.interval_morph_t;
|
|
64
|
+
constexpr float kMid = 0.5f;
|
|
65
|
+
if (t < kMid) return {true, true, 1.f - t / kMid};
|
|
66
|
+
return {true, false, (t - kMid) / (1.f - kMid)};
|
|
67
|
+
}
|
|
68
|
+
|
|
51
69
|
// ----- Y-axis ---------------------------------------------------------------
|
|
52
70
|
|
|
53
71
|
void update_y_fades(VroomChart& chart,
|
|
@@ -81,6 +99,11 @@ void update_y_fades(VroomChart& chart,
|
|
|
81
99
|
}
|
|
82
100
|
}
|
|
83
101
|
|
|
102
|
+
const auto phase = interval_phase(chart);
|
|
103
|
+
if (phase.active) {
|
|
104
|
+
apply_envelope(chart.y_fades, phase.opacity);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
84
107
|
const float step = kFadeRate * chart.last_dt_seconds;
|
|
85
108
|
for (auto& f : chart.y_fades) advance(f, step, chart.last_dt_seconds);
|
|
86
109
|
}
|
|
@@ -162,24 +185,27 @@ void gc_y_fades(VroomChart& chart) {
|
|
|
162
185
|
|
|
163
186
|
// ----- X-axis ---------------------------------------------------------------
|
|
164
187
|
|
|
165
|
-
void update_x_fades(VroomChart& chart,
|
|
188
|
+
void update_x_fades(VroomChart& chart,
|
|
189
|
+
const Layout& lay,
|
|
190
|
+
int64_t start_ms,
|
|
191
|
+
int64_t end_ms) {
|
|
166
192
|
if (chart.candles.empty()) return;
|
|
167
|
-
if (
|
|
193
|
+
if (end_ms <= start_ms) return;
|
|
168
194
|
|
|
169
195
|
const float candle_area_w =
|
|
170
196
|
lay.width_px - lay.y_axis_width_px - lay.right_padding_px;
|
|
171
197
|
if (candle_area_w <= 0.f) return;
|
|
172
198
|
|
|
173
|
-
const int64_t window_ms =
|
|
199
|
+
const int64_t window_ms = end_ms - start_ms;
|
|
174
200
|
const vroom::TimeTick tick =
|
|
175
201
|
vroom::pick_time_tick(window_ms, candle_area_w);
|
|
176
202
|
|
|
177
203
|
for (auto& f : chart.x_fades) f.target = 0.f;
|
|
178
204
|
|
|
179
|
-
int64_t t = vroom::first_tick_at_or_after(
|
|
205
|
+
int64_t t = vroom::first_tick_at_or_after(start_ms, tick);
|
|
180
206
|
constexpr int kMaxLabels = 64;
|
|
181
207
|
int promoted = 0;
|
|
182
|
-
for (; t <=
|
|
208
|
+
for (; t <= end_ms && promoted < kMaxLabels;
|
|
183
209
|
t = vroom::next_tick(t, tick), ++promoted) {
|
|
184
210
|
bool found = false;
|
|
185
211
|
for (auto& f : chart.x_fades) {
|
|
@@ -194,16 +220,23 @@ void update_x_fades(VroomChart& chart, const Layout& lay) {
|
|
|
194
220
|
}
|
|
195
221
|
}
|
|
196
222
|
|
|
223
|
+
const auto phase = interval_phase(chart);
|
|
224
|
+
if (phase.active) {
|
|
225
|
+
apply_envelope(chart.x_fades, phase.opacity);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
197
228
|
const float step = kFadeRate * chart.last_dt_seconds;
|
|
198
229
|
for (auto& f : chart.x_fades) advance(f, step, chart.last_dt_seconds);
|
|
199
230
|
}
|
|
200
231
|
|
|
201
232
|
void draw_x_gridlines(SkCanvas* canvas,
|
|
202
233
|
const VroomChart& chart,
|
|
234
|
+
int64_t start_ms,
|
|
235
|
+
int64_t end_ms,
|
|
203
236
|
float candle_area_w,
|
|
204
237
|
float candle_area_h) {
|
|
205
|
-
if (
|
|
206
|
-
const int64_t window_ms =
|
|
238
|
+
if (end_ms <= start_ms) return;
|
|
239
|
+
const int64_t window_ms = end_ms - start_ms;
|
|
207
240
|
if (window_ms <= 0 || candle_area_w <= 0.f) return;
|
|
208
241
|
|
|
209
242
|
SkPaint grid;
|
|
@@ -212,9 +245,8 @@ void draw_x_gridlines(SkCanvas* canvas,
|
|
|
212
245
|
grid.setAntiAlias(true);
|
|
213
246
|
for (const auto& f : chart.x_fades) {
|
|
214
247
|
if (f.opacity <= 1e-3f) continue;
|
|
215
|
-
const float frac =
|
|
216
|
-
|
|
217
|
-
static_cast<float>(window_ms);
|
|
248
|
+
const float frac = static_cast<float>(f.time_ms - start_ms) /
|
|
249
|
+
static_cast<float>(window_ms);
|
|
218
250
|
const float x = frac * candle_area_w;
|
|
219
251
|
if (x < 0.f || x > candle_area_w) continue;
|
|
220
252
|
grid.setAlphaf(f.opacity);
|
|
@@ -224,10 +256,12 @@ void draw_x_gridlines(SkCanvas* canvas,
|
|
|
224
256
|
|
|
225
257
|
void draw_x_labels(SkCanvas* canvas,
|
|
226
258
|
const VroomChart& chart,
|
|
227
|
-
const Layout& lay
|
|
259
|
+
const Layout& lay,
|
|
260
|
+
int64_t start_ms,
|
|
261
|
+
int64_t end_ms) {
|
|
228
262
|
auto tf = vroom::axis_typeface();
|
|
229
263
|
if (!tf || chart.candles.empty()) return;
|
|
230
|
-
if (
|
|
264
|
+
if (end_ms <= start_ms) return;
|
|
231
265
|
|
|
232
266
|
// X-axis labels live in the bottom strip, which stays anchored regardless
|
|
233
267
|
// of any indicator pane above it.
|
|
@@ -236,7 +270,7 @@ void draw_x_labels(SkCanvas* canvas,
|
|
|
236
270
|
lay.width_px - lay.y_axis_width_px - lay.right_padding_px;
|
|
237
271
|
if (candle_area_w <= 0.f) return;
|
|
238
272
|
|
|
239
|
-
const int64_t window_ms =
|
|
273
|
+
const int64_t window_ms = end_ms - start_ms;
|
|
240
274
|
// Recompute the cadence here just to decide label formatting.
|
|
241
275
|
const vroom::TimeTick tick =
|
|
242
276
|
vroom::pick_time_tick(window_ms, candle_area_w);
|
|
@@ -262,9 +296,8 @@ void draw_x_labels(SkCanvas* canvas,
|
|
|
262
296
|
tick.step_ms >= 24LL * 60 * 60 * 1000;
|
|
263
297
|
for (const auto& f : chart.x_fades) {
|
|
264
298
|
if (f.opacity <= 1e-3f) continue;
|
|
265
|
-
const float frac =
|
|
266
|
-
|
|
267
|
-
static_cast<float>(window_ms);
|
|
299
|
+
const float frac = static_cast<float>(f.time_ms - start_ms) /
|
|
300
|
+
static_cast<float>(window_ms);
|
|
268
301
|
const float x_center = frac * candle_area_w;
|
|
269
302
|
|
|
270
303
|
char buf[16];
|
package/cpp/_core_src/labels.h
CHANGED
|
@@ -34,14 +34,33 @@ struct XLabelFade {
|
|
|
34
34
|
float target = 1.f;
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
-
// Opacity step per second (1 / 0.2s ≈ 200ms full fade).
|
|
37
|
+
// Opacity step per second (1 / 0.2s ≈ 200ms full fade). Paces the incremental
|
|
38
|
+
// per-label fades; an interval morph overrides them with the envelope below.
|
|
38
39
|
inline constexpr float kFadeRate = 5.f;
|
|
39
40
|
|
|
41
|
+
// How an axis behaves during an interval morph: the pre-switch ticks fade out
|
|
42
|
+
// over the first half, the new ones fade in over the second. Nothing translates
|
|
43
|
+
// — the tick set is swapped at the midpoint, while the axis is fully
|
|
44
|
+
// transparent, so a label is never seen moving between two positions.
|
|
45
|
+
//
|
|
46
|
+
// Both halves are clocked off the morph's own eased progress, so the axes take
|
|
47
|
+
// `transitionMs`, follow `transitionEasing`, and land with the candles.
|
|
48
|
+
struct IntervalPhase {
|
|
49
|
+
bool active = false; // false: fade per-label at kFadeRate as usual
|
|
50
|
+
bool outgoing = false; // true: first half — pre-switch ticks and scale
|
|
51
|
+
float opacity = 1.f; // whole-axis multiplier, 1 → 0 → 1
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Derived purely from the chart's morph state, so callers can ask for it freely.
|
|
55
|
+
IntervalPhase interval_phase(const VroomChart& chart);
|
|
56
|
+
|
|
40
57
|
// Y-axis (price) -----------------------------------------------------------
|
|
41
58
|
|
|
42
59
|
// Sets targets, walks the active price-interval set, advances opacities by
|
|
43
60
|
// `chart.last_dt_seconds`. Must be called once per frame before either
|
|
44
|
-
// `draw_y_gridlines` or `draw_y_labels`.
|
|
61
|
+
// `draw_y_gridlines` or `draw_y_labels`. `bounds` is the scale to lay ticks out
|
|
62
|
+
// against, which during an interval morph's outgoing half is the pre-switch one
|
|
63
|
+
// (`chart.morph_from_bounds`) — pass the same value to the draw calls.
|
|
45
64
|
void update_y_fades(VroomChart& chart,
|
|
46
65
|
const Layout& lay,
|
|
47
66
|
const PriceBounds& bounds);
|
|
@@ -62,16 +81,26 @@ void gc_y_fades(VroomChart& chart);
|
|
|
62
81
|
|
|
63
82
|
// X-axis (time) ------------------------------------------------------------
|
|
64
83
|
|
|
65
|
-
|
|
84
|
+
// `start_ms`/`end_ms` are the window to lay ticks out against — the pre-switch
|
|
85
|
+
// one during an interval morph's outgoing half, `chart.visible_*` otherwise.
|
|
86
|
+
// Every x-axis call in a frame must be given the same window.
|
|
87
|
+
void update_x_fades(VroomChart& chart,
|
|
88
|
+
const Layout& lay,
|
|
89
|
+
int64_t start_ms,
|
|
90
|
+
int64_t end_ms);
|
|
66
91
|
|
|
67
92
|
void draw_x_gridlines(SkCanvas* canvas,
|
|
68
93
|
const VroomChart& chart,
|
|
94
|
+
int64_t start_ms,
|
|
95
|
+
int64_t end_ms,
|
|
69
96
|
float candle_area_w,
|
|
70
97
|
float candle_area_h);
|
|
71
98
|
|
|
72
99
|
void draw_x_labels(SkCanvas* canvas,
|
|
73
100
|
const VroomChart& chart,
|
|
74
|
-
const Layout& lay
|
|
101
|
+
const Layout& lay,
|
|
102
|
+
int64_t start_ms,
|
|
103
|
+
int64_t end_ms);
|
|
75
104
|
|
|
76
105
|
void gc_x_fades(VroomChart& chart);
|
|
77
106
|
|
|
@@ -11,47 +11,12 @@
|
|
|
11
11
|
#include "include/core/SkPoint.h"
|
|
12
12
|
#include "include/core/SkRect.h"
|
|
13
13
|
#include "include/core/SkShader.h"
|
|
14
|
-
#include "include/core/SkTileMode.h"
|
|
15
|
-
// Skia's linear-gradient API differs across the Skia versions we build against:
|
|
16
|
-
// the WASM build uses a newer Skia (SkGradient.h + SkShaders::LinearGradient),
|
|
17
|
-
// while react-native-skia bundles an older one (SkGradientShader::MakeLinear).
|
|
18
|
-
// Neither ships both headers, so select whichever is present.
|
|
19
|
-
#if __has_include("include/effects/SkGradient.h")
|
|
20
|
-
# include "include/core/SkSpan.h"
|
|
21
|
-
# include "include/effects/SkGradient.h"
|
|
22
|
-
# define VROOM_SK_MODERN_GRADIENT 1
|
|
23
|
-
#else
|
|
24
|
-
# include "include/effects/SkGradientShader.h"
|
|
25
|
-
# define VROOM_SK_MODERN_GRADIENT 0
|
|
26
|
-
#endif
|
|
27
14
|
#pragma clang diagnostic pop
|
|
28
15
|
|
|
29
16
|
#include "chart.h"
|
|
17
|
+
#include "gradient.h"
|
|
30
18
|
|
|
31
19
|
namespace vroom::liquidity {
|
|
32
|
-
namespace {
|
|
33
|
-
|
|
34
|
-
// A horizontal shader fading from transparent at the left of `pts` to `base` at
|
|
35
|
-
// opacity `alpha` on the right, keeping RGB constant so the faded edge doesn't
|
|
36
|
-
// darken toward black. Abstracts over the two Skia gradient APIs above.
|
|
37
|
-
sk_sp<SkShader> left_fade_shader(const SkPoint pts[2], SkColor base, float alpha) {
|
|
38
|
-
#if VROOM_SK_MODERN_GRADIENT
|
|
39
|
-
const float r = SkColorGetR(base) / 255.f;
|
|
40
|
-
const float g = SkColorGetG(base) / 255.f;
|
|
41
|
-
const float b = SkColorGetB(base) / 255.f;
|
|
42
|
-
const SkColor4f colors[2] = {SkColor4f{r, g, b, 0.f}, SkColor4f{r, g, b, alpha}};
|
|
43
|
-
const SkGradient::Colors grad_colors(
|
|
44
|
-
SkSpan<const SkColor4f>(colors, 2), SkTileMode::kClamp);
|
|
45
|
-
const SkGradient grad(grad_colors, SkGradient::Interpolation{});
|
|
46
|
-
return SkShaders::LinearGradient(pts, grad);
|
|
47
|
-
#else
|
|
48
|
-
const auto a = static_cast<U8CPU>(alpha * 255.f + 0.5f);
|
|
49
|
-
const SkColor colors[2] = {SkColorSetA(base, 0), SkColorSetA(base, a)};
|
|
50
|
-
return SkGradientShader::MakeLinear(pts, colors, nullptr, 2, SkTileMode::kClamp);
|
|
51
|
-
#endif
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
} // namespace
|
|
55
20
|
|
|
56
21
|
void draw(SkCanvas* canvas,
|
|
57
22
|
const VroomChart& chart,
|
|
@@ -102,7 +67,8 @@ void draw(SkCanvas* canvas,
|
|
|
102
67
|
|
|
103
68
|
SkPaint paint;
|
|
104
69
|
paint.setAntiAlias(true);
|
|
105
|
-
|
|
70
|
+
// Transparent at the left of the band's reach, full `alpha` at the axis.
|
|
71
|
+
paint.setShader(vroom::linear_alpha_ramp(pts, base, 0.f, alpha));
|
|
106
72
|
canvas->drawRect(SkRect::MakeLTRB(left_x, y_top, candle_right, y_bot), paint);
|
|
107
73
|
}
|
|
108
74
|
|
|
@@ -7,16 +7,107 @@
|
|
|
7
7
|
#include "include/core/SkPaint.h"
|
|
8
8
|
#include "include/core/SkPath.h"
|
|
9
9
|
#include "include/core/SkPathBuilder.h"
|
|
10
|
+
#include "include/core/SkPoint.h"
|
|
10
11
|
#include "include/core/SkRect.h"
|
|
12
|
+
#include "include/core/SkShader.h"
|
|
11
13
|
#pragma clang diagnostic pop
|
|
12
14
|
|
|
13
15
|
#include <algorithm>
|
|
14
16
|
#include <cmath>
|
|
15
17
|
|
|
18
|
+
#include "gradient.h"
|
|
16
19
|
#include "viewport.h"
|
|
17
20
|
|
|
18
21
|
namespace vroom::ma_overlay {
|
|
19
22
|
|
|
23
|
+
namespace {
|
|
24
|
+
// Strokes a built polyline, clipped to the candle area so a line that runs off
|
|
25
|
+
// range doesn't bleed into the axis strips or any indicator pane below.
|
|
26
|
+
void stroke_path(SkCanvas* canvas,
|
|
27
|
+
const SkPath& path,
|
|
28
|
+
float candle_right,
|
|
29
|
+
float candle_area_h,
|
|
30
|
+
uint32_t color,
|
|
31
|
+
float width,
|
|
32
|
+
float opacity) {
|
|
33
|
+
SkPaint line;
|
|
34
|
+
line.setAntiAlias(true);
|
|
35
|
+
line.setColor(static_cast<SkColor>(color));
|
|
36
|
+
// Fade the line in during the candle→line morph (multiplies the color alpha).
|
|
37
|
+
if (opacity < 1.f) {
|
|
38
|
+
line.setAlphaf(line.getAlphaf() * opacity);
|
|
39
|
+
}
|
|
40
|
+
line.setStyle(SkPaint::kStroke_Style);
|
|
41
|
+
line.setStrokeWidth(width > 0.f ? width : 1.5f);
|
|
42
|
+
|
|
43
|
+
canvas->save();
|
|
44
|
+
canvas->clipRect(SkRect::MakeLTRB(0.f, 0.f, candle_right, candle_area_h));
|
|
45
|
+
canvas->drawPath(path, line);
|
|
46
|
+
canvas->restore();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
inline float lerp(float a, float b, float t) { return a + (b - a) * t; }
|
|
50
|
+
|
|
51
|
+
// The close-price polyline, walked left to right. Shared by the stroke and the
|
|
52
|
+
// gradient fill so both derive from identical geometry, mid-morph included.
|
|
53
|
+
//
|
|
54
|
+
// Descending slots — position counting back from the right edge, the pairing a
|
|
55
|
+
// timeframe switch preserves — which walks the line left to right, so the vertex
|
|
56
|
+
// order matches draw()'s at morph_t == 1. Every candle has a close, so there are
|
|
57
|
+
// no gaps to break the subpath on.
|
|
58
|
+
SkPath build_close_path(const Layout& lay,
|
|
59
|
+
const PriceBounds& bounds,
|
|
60
|
+
const ::VroomCandle* visible,
|
|
61
|
+
std::size_t n,
|
|
62
|
+
int64_t window_ms,
|
|
63
|
+
int64_t visible_start_ms,
|
|
64
|
+
int64_t candle_duration_ms,
|
|
65
|
+
const CandleSnapshot* from,
|
|
66
|
+
std::size_t from_n,
|
|
67
|
+
float morph_t) {
|
|
68
|
+
const std::size_t from_count = vroom::morph_from_count(from, from_n, morph_t);
|
|
69
|
+
const std::size_t slots = std::max(n, from_count);
|
|
70
|
+
if (slots == 0) return SkPath();
|
|
71
|
+
const float area_w = vroom::candle_area_width(lay);
|
|
72
|
+
|
|
73
|
+
SkPathBuilder path;
|
|
74
|
+
bool pen_down = false;
|
|
75
|
+
for (std::size_t k = slots; k-- > 0;) {
|
|
76
|
+
const ::VroomCandle* to = (k < n) ? &visible[n - 1 - k] : nullptr;
|
|
77
|
+
const CandleSnapshot* frm = (k < from_count) ? &from[k] : nullptr;
|
|
78
|
+
|
|
79
|
+
float x, y;
|
|
80
|
+
if (to) {
|
|
81
|
+
const float tx = vroom::candle_center_x(
|
|
82
|
+
lay, to->time_ms, candle_duration_ms, visible_start_ms, window_ms);
|
|
83
|
+
const float ty = vroom::price_to_y(lay, bounds, to->close);
|
|
84
|
+
if (frm) {
|
|
85
|
+
// The capture is in band fractions, so it lands on the same pixels
|
|
86
|
+
// it occupied pre-switch even though the bounds changed.
|
|
87
|
+
x = lerp(frm->x * area_w, tx, morph_t);
|
|
88
|
+
y = lerp(vroom::y_at_fraction(lay, frm->close), ty, morph_t);
|
|
89
|
+
} else {
|
|
90
|
+
x = tx;
|
|
91
|
+
y = ty;
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
// A slot only the outgoing data had. Counts match in practice, so this
|
|
95
|
+
// just keeps gappy or short-history data from breaking the line.
|
|
96
|
+
x = frm->x * area_w;
|
|
97
|
+
y = vroom::y_at_fraction(lay, frm->close);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (pen_down) {
|
|
101
|
+
path.lineTo(x, y);
|
|
102
|
+
} else {
|
|
103
|
+
path.moveTo(x, y);
|
|
104
|
+
pen_down = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return path.detach();
|
|
108
|
+
}
|
|
109
|
+
} // namespace
|
|
110
|
+
|
|
20
111
|
void draw(SkCanvas* canvas,
|
|
21
112
|
const Layout& lay,
|
|
22
113
|
const PriceBounds& bounds,
|
|
@@ -60,21 +151,92 @@ void draw(SkCanvas* canvas,
|
|
|
60
151
|
}
|
|
61
152
|
}
|
|
62
153
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
154
|
+
stroke_path(canvas, path.detach(), candle_right, candle_area_h, color, width,
|
|
155
|
+
opacity);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
void draw_close_line(SkCanvas* canvas,
|
|
159
|
+
const Layout& lay,
|
|
160
|
+
const PriceBounds& bounds,
|
|
161
|
+
const ::VroomCandle* visible,
|
|
162
|
+
std::size_t n,
|
|
163
|
+
int64_t window_ms,
|
|
164
|
+
int64_t visible_start_ms,
|
|
165
|
+
int64_t candle_duration_ms,
|
|
166
|
+
float candle_right,
|
|
167
|
+
float candle_area_h,
|
|
168
|
+
uint32_t color,
|
|
169
|
+
float width,
|
|
170
|
+
float opacity,
|
|
171
|
+
const CandleSnapshot* from,
|
|
172
|
+
std::size_t from_n,
|
|
173
|
+
float morph_t) {
|
|
174
|
+
if (!canvas || candle_right <= 0.f || candle_area_h <= 0.f) return;
|
|
175
|
+
opacity = std::clamp(opacity, 0.f, 1.f);
|
|
176
|
+
if (opacity <= 0.f) return;
|
|
177
|
+
morph_t = std::clamp(morph_t, 0.f, 1.f);
|
|
178
|
+
|
|
179
|
+
const SkPath path = build_close_path(lay, bounds, visible, n, window_ms,
|
|
180
|
+
visible_start_ms, candle_duration_ms,
|
|
181
|
+
from, from_n, morph_t);
|
|
182
|
+
if (path.isEmpty()) return;
|
|
183
|
+
|
|
184
|
+
stroke_path(canvas, path, candle_right, candle_area_h, color, width, opacity);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
void draw_close_gradient(SkCanvas* canvas,
|
|
188
|
+
const Layout& lay,
|
|
189
|
+
const PriceBounds& bounds,
|
|
190
|
+
const ::VroomCandle* visible,
|
|
191
|
+
std::size_t n,
|
|
192
|
+
int64_t window_ms,
|
|
193
|
+
int64_t visible_start_ms,
|
|
194
|
+
int64_t candle_duration_ms,
|
|
195
|
+
float candle_right,
|
|
196
|
+
float candle_area_h,
|
|
197
|
+
uint32_t color,
|
|
198
|
+
float gradient_opacity,
|
|
199
|
+
float opacity,
|
|
200
|
+
const CandleSnapshot* from,
|
|
201
|
+
std::size_t from_n,
|
|
202
|
+
float morph_t) {
|
|
203
|
+
if (!canvas || candle_right <= 0.f || candle_area_h <= 0.f) return;
|
|
204
|
+
gradient_opacity = std::clamp(gradient_opacity, 0.f, 1.f);
|
|
205
|
+
opacity = std::clamp(opacity, 0.f, 1.f);
|
|
206
|
+
const float alpha = gradient_opacity * opacity;
|
|
207
|
+
if (alpha <= 0.f) return;
|
|
208
|
+
morph_t = std::clamp(morph_t, 0.f, 1.f);
|
|
209
|
+
|
|
210
|
+
const SkPath line = build_close_path(lay, bounds, visible, n, window_ms,
|
|
211
|
+
visible_start_ms, candle_duration_ms,
|
|
212
|
+
from, from_n, morph_t);
|
|
213
|
+
if (line.isEmpty()) return;
|
|
214
|
+
|
|
215
|
+
// The polyline runs strictly left to right, so its bounds give the first and
|
|
216
|
+
// last vertex x plus the peak the ramp starts from. A peak above the pane
|
|
217
|
+
// (line zoomed off the top) would stretch the ramp, so anchor at the pane.
|
|
218
|
+
const SkRect b = line.getBounds();
|
|
219
|
+
const float top = std::max(b.top(), 0.f);
|
|
220
|
+
if (top >= candle_area_h) return;
|
|
221
|
+
|
|
222
|
+
// Drop from the last vertex to the pane bottom and back along it, turning the
|
|
223
|
+
// polyline into the closed region beneath the line.
|
|
224
|
+
SkPathBuilder area(line);
|
|
225
|
+
area.lineTo(b.right(), candle_area_h);
|
|
226
|
+
area.lineTo(b.left(), candle_area_h);
|
|
227
|
+
area.close();
|
|
228
|
+
|
|
229
|
+
const SkPoint pts[2] = {SkPoint::Make(0.f, top),
|
|
230
|
+
SkPoint::Make(0.f, candle_area_h)};
|
|
231
|
+
SkPaint fill;
|
|
232
|
+
fill.setAntiAlias(true);
|
|
233
|
+
fill.setStyle(SkPaint::kFill_Style);
|
|
234
|
+
fill.setShader(
|
|
235
|
+
vroom::linear_alpha_ramp(pts, static_cast<SkColor>(color), alpha, 0.f));
|
|
72
236
|
|
|
73
|
-
// Clip to the candle area so a line that runs off-range doesn't bleed into
|
|
74
|
-
// the axis strips or any indicator pane below.
|
|
75
237
|
canvas->save();
|
|
76
238
|
canvas->clipRect(SkRect::MakeLTRB(0.f, 0.f, candle_right, candle_area_h));
|
|
77
|
-
canvas->drawPath(
|
|
239
|
+
canvas->drawPath(area.detach(), fill);
|
|
78
240
|
canvas->restore();
|
|
79
241
|
}
|
|
80
242
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
class SkCanvas;
|
|
12
12
|
|
|
13
13
|
namespace vroom {
|
|
14
|
+
struct CandleSnapshot;
|
|
14
15
|
struct Layout;
|
|
15
16
|
struct PriceBounds;
|
|
16
17
|
} // namespace vroom
|
|
@@ -40,12 +41,63 @@ void draw(SkCanvas* canvas,
|
|
|
40
41
|
const unsigned char* break_before = nullptr,
|
|
41
42
|
float opacity = 1.f);
|
|
42
43
|
|
|
44
|
+
// The close-price polyline of line-chart mode. Equivalent to draw() fed the
|
|
45
|
+
// visible closes, plus the interval morph: `from` / `from_n` is the outgoing
|
|
46
|
+
// geometry indexed from the right of the visible slice (slot 0 = newest) and
|
|
47
|
+
// `morph_t` (0..1) is the eased progress, so each vertex slides from the close it
|
|
48
|
+
// had before the timeframe switch to its new one. `morph_t == 1` (the default)
|
|
49
|
+
// draws `visible` alone.
|
|
50
|
+
//
|
|
51
|
+
// Separate from draw() because the capture holds candle closes — it can't stand
|
|
52
|
+
// in for an indicator's values.
|
|
53
|
+
void draw_close_line(SkCanvas* canvas,
|
|
54
|
+
const Layout& lay,
|
|
55
|
+
const PriceBounds& bounds,
|
|
56
|
+
const ::VroomCandle* visible,
|
|
57
|
+
std::size_t n,
|
|
58
|
+
int64_t window_ms,
|
|
59
|
+
int64_t visible_start_ms,
|
|
60
|
+
int64_t candle_duration_ms,
|
|
61
|
+
float candle_right,
|
|
62
|
+
float candle_area_h,
|
|
63
|
+
uint32_t color,
|
|
64
|
+
float width,
|
|
65
|
+
float opacity = 1.f,
|
|
66
|
+
const CandleSnapshot* from = nullptr,
|
|
67
|
+
std::size_t from_n = 0,
|
|
68
|
+
float morph_t = 1.f);
|
|
69
|
+
|
|
70
|
+
// The area beneath the close-price polyline, filled with `color` ramping from
|
|
71
|
+
// `gradient_opacity` at the line's peak to fully transparent at the pane bottom.
|
|
72
|
+
// Takes the same geometry and morph arguments as draw_close_line and builds the
|
|
73
|
+
// identical polyline, so the fill tracks the line through a timeframe switch.
|
|
74
|
+
//
|
|
75
|
+
// `opacity` multiplies the ramp, which is what fades the fill in alongside the
|
|
76
|
+
// line during the candle→line morph. Draws nothing when either opacity is 0.
|
|
77
|
+
// Belongs behind the volume bars, so it's a separate call rather than part of
|
|
78
|
+
// draw_close_line.
|
|
79
|
+
void draw_close_gradient(SkCanvas* canvas,
|
|
80
|
+
const Layout& lay,
|
|
81
|
+
const PriceBounds& bounds,
|
|
82
|
+
const ::VroomCandle* visible,
|
|
83
|
+
std::size_t n,
|
|
84
|
+
int64_t window_ms,
|
|
85
|
+
int64_t visible_start_ms,
|
|
86
|
+
int64_t candle_duration_ms,
|
|
87
|
+
float candle_right,
|
|
88
|
+
float candle_area_h,
|
|
89
|
+
uint32_t color,
|
|
90
|
+
float gradient_opacity,
|
|
91
|
+
float opacity = 1.f,
|
|
92
|
+
const CandleSnapshot* from = nullptr,
|
|
93
|
+
std::size_t from_n = 0,
|
|
94
|
+
float morph_t = 1.f);
|
|
95
|
+
|
|
43
96
|
// Fills the closed region between two aligned series (NaN where undefined)
|
|
44
97
|
// with `color` at its alpha × `opacity` — used for the Bollinger Band fill.
|
|
45
98
|
// Runs where either series is NaN are skipped, so the fill never bridges the
|
|
46
|
-
// warmup gap. Plain-alpha SkPaint fill, no gradient
|
|
47
|
-
//
|
|
48
|
-
// candle area like draw().
|
|
99
|
+
// warmup gap. Plain-alpha SkPaint fill, no gradient ramp (see gradient.h).
|
|
100
|
+
// Clipped to the candle area like draw().
|
|
49
101
|
void fill_between(SkCanvas* canvas,
|
|
50
102
|
const Layout& lay,
|
|
51
103
|
const PriceBounds& bounds,
|
package/cpp/_core_src/macd.cpp
CHANGED
|
@@ -2,61 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
#include <cmath> // std::nan, std::isfinite
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
namespace {
|
|
8
|
-
// SMA-seeded EMA over `src` (which may have a leading NaN run). Fills `out`
|
|
9
|
-
// (resized to src.size()): NaN until the seed index, the seed = SMA of the
|
|
10
|
-
// first `period` finite values, then ema = alpha*src + (1-alpha)*prev.
|
|
11
|
-
void ema_seeded(const std::vector<double>& src, int period,
|
|
12
|
-
std::vector<double>& out) {
|
|
13
|
-
const std::size_t n = src.size();
|
|
14
|
-
out.assign(n, std::nan(""));
|
|
15
|
-
if (period < 1) return;
|
|
16
|
-
const std::size_t P = static_cast<std::size_t>(period);
|
|
17
|
-
|
|
18
|
-
std::size_t f = 0;
|
|
19
|
-
while (f < n && !std::isfinite(src[f])) ++f;
|
|
20
|
-
if (f >= n || f + P > n) return; // not enough finite values to seed
|
|
21
|
-
|
|
22
|
-
const std::size_t seed = f + P - 1;
|
|
23
|
-
double sum = 0.0;
|
|
24
|
-
for (std::size_t k = f; k <= seed; ++k) sum += src[k];
|
|
25
|
-
double prev = sum / static_cast<double>(P);
|
|
26
|
-
out[seed] = prev;
|
|
5
|
+
#include "ma.h"
|
|
6
|
+
#include "series_ma.h"
|
|
27
7
|
|
|
28
|
-
|
|
29
|
-
for (std::size_t i = seed + 1; i < n; ++i) {
|
|
30
|
-
if (!std::isfinite(src[i])) break; // series are contiguous after f
|
|
31
|
-
prev = alpha * src[i] + (1.0 - alpha) * prev;
|
|
32
|
-
out[i] = prev;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
} // namespace
|
|
8
|
+
namespace vroom::macd {
|
|
36
9
|
|
|
37
10
|
void compute(const ::VroomCandle* candles, std::size_t n, int fast, int slow,
|
|
38
|
-
int signal,
|
|
39
|
-
std::vector<double>&
|
|
11
|
+
int signal, int source, int ma_kind, int signal_ma_kind,
|
|
12
|
+
std::vector<double>& macd_out, std::vector<double>& signal_out,
|
|
13
|
+
std::vector<double>& hist_out) {
|
|
40
14
|
macd_out.assign(n, std::nan(""));
|
|
41
15
|
signal_out.assign(n, std::nan(""));
|
|
42
16
|
hist_out.assign(n, std::nan(""));
|
|
43
17
|
if (!candles || fast < 1 || slow < 1 || signal < 1) return;
|
|
44
18
|
|
|
45
|
-
std::vector<double>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
std::vector<double> ema_slow;
|
|
50
|
-
ema_seeded(closes, fast, ema_fast);
|
|
51
|
-
ema_seeded(closes, slow, ema_slow);
|
|
19
|
+
std::vector<double> ma_fast;
|
|
20
|
+
std::vector<double> ma_slow;
|
|
21
|
+
vroom::ma::compute(candles, n, ma_kind, fast, source, ma_fast);
|
|
22
|
+
vroom::ma::compute(candles, n, ma_kind, slow, source, ma_slow);
|
|
52
23
|
|
|
53
24
|
for (std::size_t i = 0; i < n; ++i) {
|
|
54
|
-
if (std::isfinite(
|
|
55
|
-
macd_out[i] =
|
|
25
|
+
if (std::isfinite(ma_fast[i]) && std::isfinite(ma_slow[i])) {
|
|
26
|
+
macd_out[i] = ma_fast[i] - ma_slow[i];
|
|
56
27
|
}
|
|
57
28
|
}
|
|
58
29
|
|
|
59
|
-
|
|
30
|
+
vroom::series_ma::smooth(macd_out, signal_ma_kind, signal, signal_out);
|
|
60
31
|
|
|
61
32
|
for (std::size_t i = 0; i < n; ++i) {
|
|
62
33
|
if (std::isfinite(macd_out[i]) && std::isfinite(signal_out[i])) {
|
package/cpp/_core_src/macd.h
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// MACD — pure computation, no Skia. Kept Skia-free so it builds into the
|
|
2
2
|
// unit-test target. Mirrors rsi.cpp's NaN-warmup convention.
|
|
3
3
|
//
|
|
4
|
-
// MACD line =
|
|
5
|
-
// Signal line =
|
|
4
|
+
// MACD line = MA(fast) - MA(slow) of the chosen price source
|
|
5
|
+
// Signal line = MA(signal) of the MACD line
|
|
6
6
|
// Histogram = MACD - Signal
|
|
7
7
|
// EMAs are SMA-seeded: NaN until the first `period` values exist (the seed is
|
|
8
8
|
// their simple average), then ema = alpha*src + (1-alpha)*prev, alpha=2/(p+1).
|
|
@@ -16,14 +16,17 @@
|
|
|
16
16
|
|
|
17
17
|
namespace vroom::macd {
|
|
18
18
|
|
|
19
|
-
// Computes MACD over
|
|
20
|
-
//
|
|
19
|
+
// Computes MACD over [candles, candles+n). fast/slow/signal are clamped to >= 1
|
|
20
|
+
// (callers pass 12/26/9 by default; slow should exceed fast). `source` selects
|
|
21
|
+
// the price series (vroom::ma::Source); `ma_kind` picks the averaging for the
|
|
22
|
+
// fast/slow legs and `signal_ma_kind` for the signal line (vroom::ma::Kind).
|
|
21
23
|
// Fills three series (each resized to n, NaN where undefined):
|
|
22
|
-
// macd_out —
|
|
23
|
-
// signal_out —
|
|
24
|
+
// macd_out — MA(fast) - MA(slow), defined from index slow-1
|
|
25
|
+
// signal_out — MA(signal) of macd_out, defined from (slow-1)+(signal-1)
|
|
24
26
|
// hist_out — macd_out - signal_out, defined where both are
|
|
25
27
|
void compute(const ::VroomCandle* candles, std::size_t n, int fast, int slow,
|
|
26
|
-
int signal,
|
|
27
|
-
std::vector<double>&
|
|
28
|
+
int signal, int source, int ma_kind, int signal_ma_kind,
|
|
29
|
+
std::vector<double>& macd_out, std::vector<double>& signal_out,
|
|
30
|
+
std::vector<double>& hist_out);
|
|
28
31
|
|
|
29
32
|
} // namespace vroom::macd
|