⎇ 72758e75ce515f355f27615671c95ecaf8c2a8bc
crates/modules/ui_engine/widgets/lapaloma-financial/src/candlestick.rs
Historia
Quién tocó qué
crudo
feat(lapaloma-financial): OHLC + candlesticks con preserva-volatilidad
1//! Render de candlesticks sobre cualquier `Canvas`.
2//!
3//! Por cada bar visible:
4//! - **Wick** = línea vertical de `(t, low)` a `(t, high)`.
5//! - **Body** = rect de `(t - body_w/2, open)` a `(t + body_w/2, close)`,
6//! relleno bull (close > open) / bear (close < open) / neutro.
7//!
8//! Esta función es agnóstica de gpui — habla contra el trait
9//! `Canvas`. El `Element` GPUI que la consume vive en `element.rs`.
10
11use lapaloma_cartesian::CoordinateSystem;
12use lapaloma_render::{Canvas, Color, Point, Rect, StrokeStyle};
13
14use crate::ohlc_buffer::OhlcBuffer;
15
16/// Estilo visual de los candlesticks.
17#[derive(Debug, Clone, Copy)]
18pub struct CandlestickStyle {
19 pub bull_color: Color,
20 pub bear_color: Color,
21 /// Color del body cuando open == close. Suele ser el axis color.
22 pub neutral_color: Color,
23 /// Ancho del wick (línea central). En píxeles.
24 pub wick_width: f32,
25 /// Ancho mínimo del body, en píxeles. Cuando el spacing entre
26 /// bars cae por debajo, el body usa este floor.
27 pub body_min_width: f32,
28 /// Fracción del spacing entre bars consecutivas que ocupa el body.
29 /// 0.7 deja un gap del 30% entre velas.
30 pub body_width_ratio: f32,
31}
32
33impl Default for CandlestickStyle {
34 fn default() -> Self {
35 Self {
36 bull_color: Color::from_hex(0x88c08a),
37 bear_color: Color::from_hex(0xbf616a),
38 neutral_color: Color::rgba(0.7, 0.7, 0.75, 1.0),
39 wick_width: 1.0,
40 body_min_width: 2.0,
41 body_width_ratio: 0.7,
42 }
43 }
44}
45
46/// Dibuja todas las velas del buffer visibles en el viewport del
47/// `CoordinateSystem`. Bars fuera de rango se skippean.
48pub fn paint_candlesticks(
49 canvas: &mut dyn Canvas,
50 cs: &CoordinateSystem,
51 data: &OhlcBuffer,
52 style: CandlestickStyle,
53) {
54 let n = data.len();
55 if n == 0 {
56 return;
57 }
58
59 let plot = cs.plot;
60 let viewport = cs.viewport;
61
62 // Spacing entre bars consecutivas en píxeles. Asume bars
63 // aproximadamente equiespaciadas en X (caso típico OHLC
64 // post-aggregation).
65 let body_width = if n >= 2 {
66 let first_t = data.bar(0).t as f64;
67 let last_t = data.bar(n - 1).t as f64;
68 let span_t = (last_t - first_t).max(f32::EPSILON as f64);
69 let span_px = (span_t / viewport.x_span()) * plot.w as f64;
70 let spacing = span_px / (n as f64 - 1.0);
71 ((spacing * style.body_width_ratio as f64) as f32).max(style.body_min_width)
72 } else {
73 style.body_min_width
74 };
75 let half_body = body_width * 0.5;
76
77 for i in 0..n {
78 let bar = data.bar(i);
79
80 // Clip aproximado: si el bar entero queda fuera del viewport
81 // X, lo saltamos.
82 if (bar.t as f64) < viewport.x_min - viewport.x_span() * 0.05
83 || (bar.t as f64) > viewport.x_max + viewport.x_span() * 0.05
84 {
85 continue;
86 }
87
88 let px_center = cs.data_to_pixel(bar.t as f64, bar.o as f64).x;
89 let py_open = cs.data_to_pixel(bar.t as f64, bar.o as f64).y;
90 let py_close = cs.data_to_pixel(bar.t as f64, bar.c as f64).y;
91 let py_high = cs.data_to_pixel(bar.t as f64, bar.h as f64).y;
92 let py_low = cs.data_to_pixel(bar.t as f64, bar.l as f64).y;
93
94 let color = if bar.is_bull() {
95 style.bull_color
96 } else if bar.is_bear() {
97 style.bear_color
98 } else {
99 style.neutral_color
100 };
101
102 // Wick: línea vertical de high a low.
103 canvas.stroke_line(
104 Point::new(px_center, py_high),
105 Point::new(px_center, py_low),
106 StrokeStyle::new(style.wick_width, color),
107 );
108
109 // Body: rect entre open y close.
110 let (y_top, y_bot) = if py_open < py_close {
111 (py_open, py_close)
112 } else {
113 (py_close, py_open)
114 };
115 let body_h = (y_bot - y_top).max(1.0); // floor 1px para doji
116 canvas.fill_rect(
117 Rect::new(px_center - half_body, y_top, body_width, body_h),
118 color,
119 );
120 }
121}