tawasuyu

tawasuyu/brahman

Rust 326TOML 123JSON 17Markdown 144606 Definitions

⎇ d3649bfd1a80153652c2a028315369bb4b184024 crates/modules/ui_engine/widgets/lapaloma-financial/src/candlestick.rs Rust · 121 lines · 4.0 KB History Who touched what raw

feat(lapaloma-financial): OHLC + candlesticks con preserva-volatilidad d1ce4c897 · sergio ·

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}