refactor(monorepo): reorganización lógica + renames + SDDs + split CHANGELOG
Reorganización física de crates/: - core/ (mezclaba 6 propósitos) se divide en protocol/, init/, runtime/, compat/ - shared/ (3 crates) se redistribuye en protocol/ e init/ - lapaloma (sub-módulo de ui_engine) se promueve a modules/pineal/ Renames de proyectos: - shipote → shuma (runtime de sandboxes) - nouser → akasha (explorador de Mónadas) - yahweh → nahual (motor GPUI, antes ui_engine/) - lapaloma → pineal (data-viz agnóstica) Fraccionamiento UI → core agnóstico: - vista-core (DeckState + snap, 175 LOC, 5 tests verdes) - barra-core (Task + render_html + sanitize, 90 LOC, 5 tests verdes) - vista-web y barra-web ahora son thin DOM bindings Documentación nueva: - 16 SDDs por subdirectorio (≤80 LOC c/u): protocol/init/runtime/compat + 10 módulos + apps/ - docs/STATUS.md con cifras reales por proyecto - docs/ROADMAP.md con plan a finalización (6 hitos, ~6-8 semanas) - CHANGELOG.md particionado en docs/changelog/<proyecto>.md (7 buckets) Automatización: - scripts/reorg.py — script idempotente que: git mv directorios, renombra package names, recomputa path = refs, reescribe imports rust, actualiza workspace Cargo.toml. Soporta --dry-run. - scripts/split-changelog.py — particiona CHANGELOG por componente. Validación: - cargo check --workspace pasa (124 crates + 2 nuevos cores). - 10 tests adicionales (5 en vista-core + 5 en barra-core) verdes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "pineal-financial"
|
||||
version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
publish = { workspace = true }
|
||||
description = "Lapaloma — gráficos financieros. OHLC / candlesticks con agregación que preserva volatilidad (no LTTB, time-bucketing con max/min de wicks)."
|
||||
|
||||
[dependencies]
|
||||
pineal-core = { path = "../core" }
|
||||
pineal-render = { path = "../render" }
|
||||
pineal-cartesian = { path = "../cartesian" }
|
||||
gpui = { workspace = true, optional = true }
|
||||
|
||||
[features]
|
||||
default = ["gpui"]
|
||||
gpui = ["dep:gpui", "pineal-render/gpui", "pineal-cartesian/gpui"]
|
||||
@@ -0,0 +1,165 @@
|
||||
//! Aggregation de OHLC por bucket de **tiempo** (no de índice).
|
||||
//!
|
||||
//! Bucket index = `floor((bar.t - t_start) / bucket_duration)`.
|
||||
//! Cuando cambia el bucket, commit del anterior:
|
||||
//!
|
||||
//! - `open` = primer `open` del bucket.
|
||||
//! - `close` = último `close` del bucket.
|
||||
//! - `high` = max(`high`) del bucket.
|
||||
//! - `low` = min(`low`) del bucket.
|
||||
//! - `volume` = sum(`volume`) del bucket.
|
||||
//! - `t` = timestamp del primer bar del bucket (canónico para
|
||||
//! ploteo; el doc original sugiere usar el inicio del bucket
|
||||
//! pero acá preferimos el sample real para no introducir bias).
|
||||
//!
|
||||
//! Buckets vacíos no se emiten — el length de salida es ≤ inputs.
|
||||
//! Fallback a index-bucketing si el span temporal es cero (todos
|
||||
//! los timestamps colapsados, e.g. tick-data).
|
||||
|
||||
use crate::ohlc_buffer::{Bar, OhlcBuffer, STRIDE};
|
||||
|
||||
/// Agrega `src` en buckets de duración `bucket_duration` (en
|
||||
/// unidades de `bar.t`). Escribe el output en `out` extendiéndolo
|
||||
/// (no se clearea; el caller decide).
|
||||
///
|
||||
/// Si `bucket_duration <= 0` o el span del input es cero, hace
|
||||
/// fallback a index-bucketing con `samples_per_bucket = 1` (es decir,
|
||||
/// copia el input tal cual). Esto evita panic con tick-data
|
||||
/// colapsado.
|
||||
pub fn aggregate_time_bucketed(src: &OhlcBuffer, bucket_duration: f32, out: &mut OhlcBuffer) {
|
||||
if src.is_empty() {
|
||||
return;
|
||||
}
|
||||
let n = src.len();
|
||||
let (t_first, t_last) = src.time_range().unwrap();
|
||||
|
||||
if bucket_duration <= 0.0 || (t_last - t_first).abs() < f32::EPSILON {
|
||||
// Fallback: copia tal cual.
|
||||
for i in 0..n {
|
||||
out.push_bar(src.bar(i));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let mut current_bucket = i64::MIN;
|
||||
let mut acc_t: f32 = 0.0;
|
||||
let mut acc_o: f32 = 0.0;
|
||||
let mut acc_h: f32 = f32::NEG_INFINITY;
|
||||
let mut acc_l: f32 = f32::INFINITY;
|
||||
let mut acc_c: f32 = 0.0;
|
||||
let mut acc_v: f32 = 0.0;
|
||||
let mut has_acc = false;
|
||||
|
||||
for i in 0..n {
|
||||
let b = src.bar(i);
|
||||
let bucket = ((b.t - t_first) / bucket_duration).floor() as i64;
|
||||
if bucket != current_bucket {
|
||||
if has_acc {
|
||||
out.push_bar(Bar {
|
||||
t: acc_t,
|
||||
o: acc_o,
|
||||
h: acc_h,
|
||||
l: acc_l,
|
||||
c: acc_c,
|
||||
v: acc_v,
|
||||
});
|
||||
}
|
||||
current_bucket = bucket;
|
||||
acc_t = b.t;
|
||||
acc_o = b.o;
|
||||
acc_h = b.h;
|
||||
acc_l = b.l;
|
||||
acc_c = b.c;
|
||||
acc_v = b.v;
|
||||
has_acc = true;
|
||||
} else {
|
||||
if b.h > acc_h {
|
||||
acc_h = b.h;
|
||||
}
|
||||
if b.l < acc_l {
|
||||
acc_l = b.l;
|
||||
}
|
||||
acc_c = b.c;
|
||||
acc_v += b.v;
|
||||
}
|
||||
}
|
||||
if has_acc {
|
||||
out.push_bar(Bar {
|
||||
t: acc_t,
|
||||
o: acc_o,
|
||||
h: acc_h,
|
||||
l: acc_l,
|
||||
c: acc_c,
|
||||
v: acc_v,
|
||||
});
|
||||
}
|
||||
|
||||
// Una métrica de cordura: el output nunca puede ser más largo
|
||||
// que el input.
|
||||
debug_assert!(out.bars().len() / STRIDE <= n);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn fixture() -> OhlcBuffer {
|
||||
// 10 bars con t en `[0, 9]`, valores deterministicos.
|
||||
let mut b = OhlcBuffer::with_capacity(10);
|
||||
for i in 0..10 {
|
||||
let t = i as f32;
|
||||
let base = 100.0 + (i as f32) * 0.5;
|
||||
b.push_values(t, base, base + 1.0, base - 1.0, base + 0.2, 10.0);
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bucket_de_3_agrega_a_4_bars() {
|
||||
// 10 inputs con t en `[0, 9]`, bucket 3 → buckets 0-2, 3-5, 6-8, 9.
|
||||
// = 4 buckets.
|
||||
let src = fixture();
|
||||
let mut out = OhlcBuffer::new();
|
||||
aggregate_time_bucketed(&src, 3.0, &mut out);
|
||||
assert_eq!(out.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aggregation_preserva_volatilidad() {
|
||||
// Inventamos un bucket donde un bar tiene spike alto y otro
|
||||
// spike bajo. El aggregate debe capturar AMBOS extremos.
|
||||
let mut src = OhlcBuffer::new();
|
||||
src.push_values(0.0, 10.0, 12.0, 9.0, 11.0, 5.0);
|
||||
src.push_values(0.5, 11.0, 20.0, 10.5, 11.5, 5.0); // spike up
|
||||
src.push_values(0.8, 11.5, 12.0, 2.0, 11.0, 5.0); // spike down
|
||||
let mut out = OhlcBuffer::new();
|
||||
aggregate_time_bucketed(&src, 1.0, &mut out);
|
||||
assert_eq!(out.len(), 1);
|
||||
let agg = out.bar(0);
|
||||
assert_eq!(agg.h, 20.0, "max H debe sobrevivir");
|
||||
assert_eq!(agg.l, 2.0, "min L debe sobrevivir");
|
||||
assert_eq!(agg.o, 10.0, "first open");
|
||||
assert_eq!(agg.c, 11.0, "last close");
|
||||
assert_eq!(agg.v, 15.0, "sum volumes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fallback_a_index_si_span_cero() {
|
||||
// Todos los t iguales — fallback copia 1:1.
|
||||
let mut src = OhlcBuffer::new();
|
||||
src.push_values(7.0, 1.0, 2.0, 0.0, 1.5, 1.0);
|
||||
src.push_values(7.0, 1.5, 2.5, 1.0, 2.0, 1.0);
|
||||
src.push_values(7.0, 2.0, 3.0, 1.0, 1.0, 1.0);
|
||||
let mut out = OhlcBuffer::new();
|
||||
aggregate_time_bucketed(&src, 1.0, &mut out);
|
||||
assert_eq!(out.len(), 3, "span 0 ⇒ copy 1:1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_no_emite() {
|
||||
let src = OhlcBuffer::new();
|
||||
let mut out = OhlcBuffer::new();
|
||||
aggregate_time_bucketed(&src, 1.0, &mut out);
|
||||
assert_eq!(out.len(), 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
//! Render de candlesticks sobre cualquier `Canvas`.
|
||||
//!
|
||||
//! Por cada bar visible:
|
||||
//! - **Wick** = línea vertical de `(t, low)` a `(t, high)`.
|
||||
//! - **Body** = rect de `(t - body_w/2, open)` a `(t + body_w/2, close)`,
|
||||
//! relleno bull (close > open) / bear (close < open) / neutro.
|
||||
//!
|
||||
//! Esta función es agnóstica de gpui — habla contra el trait
|
||||
//! `Canvas`. El `Element` GPUI que la consume vive en `element.rs`.
|
||||
|
||||
use pineal_cartesian::CoordinateSystem;
|
||||
use pineal_render::{Canvas, Color, Point, Rect, StrokeStyle};
|
||||
|
||||
use crate::ohlc_buffer::OhlcBuffer;
|
||||
|
||||
/// Estilo visual de los candlesticks.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct CandlestickStyle {
|
||||
pub bull_color: Color,
|
||||
pub bear_color: Color,
|
||||
/// Color del body cuando open == close. Suele ser el axis color.
|
||||
pub neutral_color: Color,
|
||||
/// Ancho del wick (línea central). En píxeles.
|
||||
pub wick_width: f32,
|
||||
/// Ancho mínimo del body, en píxeles. Cuando el spacing entre
|
||||
/// bars cae por debajo, el body usa este floor.
|
||||
pub body_min_width: f32,
|
||||
/// Fracción del spacing entre bars consecutivas que ocupa el body.
|
||||
/// 0.7 deja un gap del 30% entre velas.
|
||||
pub body_width_ratio: f32,
|
||||
}
|
||||
|
||||
impl Default for CandlestickStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
bull_color: Color::from_hex(0x88c08a),
|
||||
bear_color: Color::from_hex(0xbf616a),
|
||||
neutral_color: Color::rgba(0.7, 0.7, 0.75, 1.0),
|
||||
wick_width: 1.0,
|
||||
body_min_width: 2.0,
|
||||
body_width_ratio: 0.7,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Dibuja todas las velas del buffer visibles en el viewport del
|
||||
/// `CoordinateSystem`. Bars fuera de rango se skippean.
|
||||
pub fn paint_candlesticks(
|
||||
canvas: &mut dyn Canvas,
|
||||
cs: &CoordinateSystem,
|
||||
data: &OhlcBuffer,
|
||||
style: CandlestickStyle,
|
||||
) {
|
||||
let n = data.len();
|
||||
if n == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let plot = cs.plot;
|
||||
let viewport = cs.viewport;
|
||||
|
||||
// Spacing entre bars consecutivas en píxeles. Asume bars
|
||||
// aproximadamente equiespaciadas en X (caso típico OHLC
|
||||
// post-aggregation).
|
||||
let body_width = if n >= 2 {
|
||||
let first_t = data.bar(0).t as f64;
|
||||
let last_t = data.bar(n - 1).t as f64;
|
||||
let span_t = (last_t - first_t).max(f32::EPSILON as f64);
|
||||
let span_px = (span_t / viewport.x_span()) * plot.w as f64;
|
||||
let spacing = span_px / (n as f64 - 1.0);
|
||||
((spacing * style.body_width_ratio as f64) as f32).max(style.body_min_width)
|
||||
} else {
|
||||
style.body_min_width
|
||||
};
|
||||
let half_body = body_width * 0.5;
|
||||
|
||||
for i in 0..n {
|
||||
let bar = data.bar(i);
|
||||
|
||||
// Clip aproximado: si el bar entero queda fuera del viewport
|
||||
// X, lo saltamos.
|
||||
if (bar.t as f64) < viewport.x_min - viewport.x_span() * 0.05
|
||||
|| (bar.t as f64) > viewport.x_max + viewport.x_span() * 0.05
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let px_center = cs.data_to_pixel(bar.t as f64, bar.o as f64).x;
|
||||
let py_open = cs.data_to_pixel(bar.t as f64, bar.o as f64).y;
|
||||
let py_close = cs.data_to_pixel(bar.t as f64, bar.c as f64).y;
|
||||
let py_high = cs.data_to_pixel(bar.t as f64, bar.h as f64).y;
|
||||
let py_low = cs.data_to_pixel(bar.t as f64, bar.l as f64).y;
|
||||
|
||||
let color = if bar.is_bull() {
|
||||
style.bull_color
|
||||
} else if bar.is_bear() {
|
||||
style.bear_color
|
||||
} else {
|
||||
style.neutral_color
|
||||
};
|
||||
|
||||
// Wick: línea vertical de high a low.
|
||||
canvas.stroke_line(
|
||||
Point::new(px_center, py_high),
|
||||
Point::new(px_center, py_low),
|
||||
StrokeStyle::new(style.wick_width, color),
|
||||
);
|
||||
|
||||
// Body: rect entre open y close.
|
||||
let (y_top, y_bot) = if py_open < py_close {
|
||||
(py_open, py_close)
|
||||
} else {
|
||||
(py_close, py_open)
|
||||
};
|
||||
let body_h = (y_bot - y_top).max(1.0); // floor 1px para doji
|
||||
canvas.fill_rect(
|
||||
Rect::new(px_center - half_body, y_top, body_width, body_h),
|
||||
color,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
//! `LapalomaCandlestickElement` — Element GPUI para charts OHLC.
|
||||
//!
|
||||
//! Reusa `pineal_cartesian::axis::paint_axes` para los ejes y el
|
||||
//! `WindowCanvas` adapter de `pineal-render` para el output.
|
||||
//! El render mismo de las velas lo hace `paint_candlesticks` que
|
||||
//! es agnóstico de gpui — facilita futuros backends (SVG, wgpu).
|
||||
//!
|
||||
//! Sin cache pan-blit en v0.1: las velas se redibujan cada frame.
|
||||
//! Para hasta ~500 bars on-screen son sub-millisecond; con
|
||||
//! aggregation razonable (1 bar por columna de pixel) eso cubre
|
||||
//! cualquier caso humano. Si se necesita más, se replica el
|
||||
//! patrón de `LapalomaChartElement::with_cache`.
|
||||
|
||||
use std::panic;
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId,
|
||||
Pixels, Style, Window,
|
||||
};
|
||||
|
||||
use pineal_cartesian::axis::{self, AxisStyle};
|
||||
use pineal_cartesian::{ChartViewport, CoordinateSystem};
|
||||
use pineal_render::{Canvas, Color, Rect, WindowCanvas};
|
||||
|
||||
use crate::candlestick::{paint_candlesticks, CandlestickStyle};
|
||||
use crate::ohlc_buffer::OhlcBuffer;
|
||||
|
||||
const TARGET_TICKS_X: usize = 8;
|
||||
const TARGET_TICKS_Y: usize = 6;
|
||||
|
||||
pub struct LapalomaCandlestickElement {
|
||||
pub data: OhlcBuffer,
|
||||
pub viewport: ChartViewport,
|
||||
pub style: CandlestickStyle,
|
||||
pub background: Option<Color>,
|
||||
pub axis_color: Color,
|
||||
pub axis_style: AxisStyle,
|
||||
pub margin_bottom: f32,
|
||||
pub margin_left: f32,
|
||||
pub margin_top: f32,
|
||||
pub margin_right: f32,
|
||||
}
|
||||
|
||||
impl LapalomaCandlestickElement {
|
||||
pub fn new(data: OhlcBuffer, viewport: ChartViewport) -> Self {
|
||||
Self {
|
||||
data,
|
||||
viewport,
|
||||
style: CandlestickStyle::default(),
|
||||
background: None,
|
||||
axis_color: Color::rgba(0.6, 0.6, 0.65, 0.8),
|
||||
axis_style: AxisStyle::default(),
|
||||
margin_bottom: 24.0,
|
||||
margin_left: 48.0,
|
||||
margin_top: 8.0,
|
||||
margin_right: 8.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn background(mut self, color: Color) -> Self {
|
||||
self.background = Some(color);
|
||||
self
|
||||
}
|
||||
pub fn axis_color(mut self, color: Color) -> Self {
|
||||
self.axis_color = color;
|
||||
self
|
||||
}
|
||||
pub fn style(mut self, style: CandlestickStyle) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
pub fn margins(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
|
||||
self.margin_top = top;
|
||||
self.margin_right = right;
|
||||
self.margin_bottom = bottom;
|
||||
self.margin_left = left;
|
||||
self
|
||||
}
|
||||
|
||||
fn plot_rect(&self, bounds: Rect) -> Rect {
|
||||
Rect::new(
|
||||
bounds.x + self.margin_left,
|
||||
bounds.y + self.margin_top,
|
||||
(bounds.w - self.margin_left - self.margin_right).max(1.0),
|
||||
(bounds.h - self.margin_top - self.margin_bottom).max(1.0),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for LapalomaCandlestickElement {
|
||||
type Element = Self;
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for LapalomaCandlestickElement {
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = ();
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
fn source_location(&self) -> Option<&'static panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let mut style = Style::default();
|
||||
style.size.width = gpui::Length::Definite(gpui::DefiniteLength::Fraction(1.0));
|
||||
style.size.height = gpui::Length::Definite(gpui::DefiniteLength::Fraction(1.0));
|
||||
let id = window.request_layout(style, [], cx);
|
||||
(id, ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> Self::PrepaintState {
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
_prepaint: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) {
|
||||
let ox: f32 = bounds.origin.x.into();
|
||||
let oy: f32 = bounds.origin.y.into();
|
||||
let w: f32 = bounds.size.width.into();
|
||||
let h: f32 = bounds.size.height.into();
|
||||
let outer = Rect::new(ox, oy, w, h);
|
||||
let plot = self.plot_rect(outer);
|
||||
|
||||
let cs = CoordinateSystem::new(self.viewport, plot);
|
||||
let mut canvas = WindowCanvas::new(window);
|
||||
|
||||
if let Some(bg) = self.background {
|
||||
canvas.fill_rect(outer, bg);
|
||||
}
|
||||
|
||||
axis::paint_axes(
|
||||
&mut canvas,
|
||||
&cs,
|
||||
&self.viewport,
|
||||
self.axis_color,
|
||||
self.axis_style,
|
||||
TARGET_TICKS_X,
|
||||
TARGET_TICKS_Y,
|
||||
);
|
||||
|
||||
paint_candlesticks(&mut canvas, &cs, &self.data, self.style);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lapaloma_candlestick(
|
||||
data: OhlcBuffer,
|
||||
viewport: ChartViewport,
|
||||
) -> LapalomaCandlestickElement {
|
||||
LapalomaCandlestickElement::new(data, viewport)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//! `pineal-financial` — OHLC y candlesticks.
|
||||
//!
|
||||
//! Layout del buffer: 6 floats por bar `[t, o, h, l, c, v]` (time,
|
||||
//! open, high, low, close, volume). Mismo principio P1 del doc
|
||||
//! canónico: array plano, sin objetos por bar.
|
||||
//!
|
||||
//! Aggregation (sección 3.2 del ARCHITECTURE.md):
|
||||
//! - **Time bucketing** (no index bucketing) para que weekends /
|
||||
//! holidays no colapsen la rate.
|
||||
//! - `open` = primero del bucket, `close` = último, `high` = max,
|
||||
//! `low` = min, `volume` = sum.
|
||||
//! - **Preserva volatilidad** — LTTB caería los wicks; estos los
|
||||
//! conserva por construcción.
|
||||
//!
|
||||
//! Render: dos batches separados — barras alcistas (close > open,
|
||||
//! verdes) y bajistas (close < open, rojas). v0.1 emite un quad
|
||||
//! por body + un line por wick (≈ 2 draw calls por bar; aceptable
|
||||
//! hasta ~500 bars on-screen). Optimización futura: agrupar
|
||||
//! N bodies en un solo PathBuilder fill.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
pub mod ohlc_buffer;
|
||||
pub mod aggregate;
|
||||
|
||||
#[cfg(feature = "gpui")]
|
||||
pub mod candlestick;
|
||||
|
||||
#[cfg(feature = "gpui")]
|
||||
pub mod element;
|
||||
|
||||
pub use ohlc_buffer::{Bar, OhlcBuffer};
|
||||
pub use aggregate::aggregate_time_bucketed;
|
||||
|
||||
#[cfg(feature = "gpui")]
|
||||
pub use candlestick::{paint_candlesticks, CandlestickStyle};
|
||||
|
||||
#[cfg(feature = "gpui")]
|
||||
pub use element::{lapaloma_candlestick, LapalomaCandlestickElement};
|
||||
@@ -0,0 +1,186 @@
|
||||
//! `OhlcBuffer` — buffer plano de bars con stride 6 `f32`.
|
||||
//!
|
||||
//! Memoria contigua: `[t0, o0, h0, l0, c0, v0, t1, o1, …]`.
|
||||
//! Acceso O(1) por índice; un memcpy completo para hidratar desde
|
||||
//! una fuente externa.
|
||||
|
||||
/// Una barra OHLC + volumen. Valor leído del buffer; no es la
|
||||
/// representación de almacenamiento (que vive como `[f32; 6]`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Bar {
|
||||
pub t: f32,
|
||||
pub o: f32,
|
||||
pub h: f32,
|
||||
pub l: f32,
|
||||
pub c: f32,
|
||||
pub v: f32,
|
||||
}
|
||||
|
||||
impl Bar {
|
||||
pub fn is_bull(self) -> bool {
|
||||
self.c > self.o
|
||||
}
|
||||
pub fn is_bear(self) -> bool {
|
||||
self.c < self.o
|
||||
}
|
||||
}
|
||||
|
||||
pub const STRIDE: usize = 6;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct OhlcBuffer {
|
||||
bars: Vec<f32>,
|
||||
revision: u64,
|
||||
}
|
||||
|
||||
impl OhlcBuffer {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn with_capacity(n: usize) -> Self {
|
||||
Self {
|
||||
bars: Vec::with_capacity(n * STRIDE),
|
||||
revision: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_raw(bars: Vec<f32>) -> Self {
|
||||
assert!(bars.len() % STRIDE == 0, "OhlcBuffer: stride 6 required");
|
||||
Self { bars, revision: 0 }
|
||||
}
|
||||
|
||||
pub fn push_bar(&mut self, b: Bar) {
|
||||
self.bars.push(b.t);
|
||||
self.bars.push(b.o);
|
||||
self.bars.push(b.h);
|
||||
self.bars.push(b.l);
|
||||
self.bars.push(b.c);
|
||||
self.bars.push(b.v);
|
||||
self.revision = self.revision.wrapping_add(1);
|
||||
}
|
||||
|
||||
pub fn push_values(&mut self, t: f32, o: f32, h: f32, l: f32, c: f32, v: f32) {
|
||||
self.push_bar(Bar { t, o, h, l, c, v });
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.bars.len() / STRIDE
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.bars.is_empty()
|
||||
}
|
||||
|
||||
pub fn bar(&self, i: usize) -> Bar {
|
||||
let off = i * STRIDE;
|
||||
Bar {
|
||||
t: self.bars[off],
|
||||
o: self.bars[off + 1],
|
||||
h: self.bars[off + 2],
|
||||
l: self.bars[off + 3],
|
||||
c: self.bars[off + 4],
|
||||
v: self.bars[off + 5],
|
||||
}
|
||||
}
|
||||
|
||||
/// Slice plano del buffer subyacente.
|
||||
pub fn bars(&self) -> &[f32] {
|
||||
&self.bars
|
||||
}
|
||||
|
||||
pub fn revision(&self) -> u64 {
|
||||
self.revision
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.bars.clear();
|
||||
self.revision = self.revision.wrapping_add(1);
|
||||
}
|
||||
|
||||
/// Min/max de `low` y `high` sobre todo el buffer.
|
||||
/// Útil para autoscale del Y axis.
|
||||
pub fn price_range(&self) -> Option<(f32, f32)> {
|
||||
if self.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut lo = f32::INFINITY;
|
||||
let mut hi = f32::NEG_INFINITY;
|
||||
for i in 0..self.len() {
|
||||
let b = self.bar(i);
|
||||
if b.l < lo {
|
||||
lo = b.l;
|
||||
}
|
||||
if b.h > hi {
|
||||
hi = b.h;
|
||||
}
|
||||
}
|
||||
Some((lo, hi))
|
||||
}
|
||||
|
||||
/// Rango temporal `[t_min, t_max]`. None si vacío.
|
||||
pub fn time_range(&self) -> Option<(f32, f32)> {
|
||||
if self.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let first = self.bars[0];
|
||||
let last = self.bars[self.bars.len() - STRIDE];
|
||||
Some((first, last))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn push_y_lectura() {
|
||||
let mut b = OhlcBuffer::with_capacity(2);
|
||||
b.push_values(1.0, 10.0, 12.0, 9.0, 11.0, 100.0);
|
||||
b.push_values(2.0, 11.0, 13.0, 10.0, 10.5, 80.0);
|
||||
assert_eq!(b.len(), 2);
|
||||
assert_eq!(b.bar(0).c, 11.0);
|
||||
assert_eq!(b.bar(1).h, 13.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bull_y_bear() {
|
||||
let bull = Bar { t: 0.0, o: 10.0, h: 11.0, l: 9.0, c: 10.5, v: 0.0 };
|
||||
let bear = Bar { t: 0.0, o: 10.0, h: 11.0, l: 9.0, c: 9.5, v: 0.0 };
|
||||
assert!(bull.is_bull());
|
||||
assert!(!bull.is_bear());
|
||||
assert!(bear.is_bear());
|
||||
assert!(!bear.is_bull());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn price_range_correcto() {
|
||||
let mut b = OhlcBuffer::new();
|
||||
b.push_values(0.0, 10.0, 15.0, 8.0, 12.0, 0.0);
|
||||
b.push_values(1.0, 12.0, 14.0, 7.0, 9.0, 0.0);
|
||||
b.push_values(2.0, 9.0, 11.0, 9.0, 10.0, 0.0);
|
||||
let (lo, hi) = b.price_range().unwrap();
|
||||
assert_eq!(lo, 7.0);
|
||||
assert_eq!(hi, 15.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn time_range() {
|
||||
let mut b = OhlcBuffer::new();
|
||||
b.push_values(10.0, 0.0, 0.0, 0.0, 0.0, 0.0);
|
||||
b.push_values(50.0, 0.0, 0.0, 0.0, 0.0, 0.0);
|
||||
b.push_values(100.0, 0.0, 0.0, 0.0, 0.0, 0.0);
|
||||
assert_eq!(b.time_range(), Some((10.0, 100.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revision_bumps_en_push_y_clear() {
|
||||
let mut b = OhlcBuffer::new();
|
||||
let r0 = b.revision();
|
||||
b.push_values(0.0, 1.0, 1.0, 1.0, 1.0, 1.0);
|
||||
assert_ne!(r0, b.revision());
|
||||
let r1 = b.revision();
|
||||
b.clear();
|
||||
assert_ne!(r1, b.revision());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user