use std::rc::Rc; use ely_gpui_component::{ buttons::{Button, ButtonVariant}, finance::{ Arrangement, Candle, CandlestickChart, ChartKind, ChartSync, ChartTypeSwitcher, DepthChart, Drawing, DrawingToolbar, IndicatorSelector, IntervalSelector, MultiChartLayout, Overlay, PointFigureChart, RenkoChart, Study, TimeRangeSelector, Tool, }, theme::ActiveTheme, }; use gpui::{ App, AppContext as _, IntoElement, ParentElement, SharedString, Styled, Window, div, px, }; use jiff::Timestamp; use crate::{ probe::probe, ui::{keep, noise, row, section, set}, }; /// A demo chart's width, inside the page's column. pub(super) const WIDE: f32 = 840.0; /// Today's prices every five minutes from the last close, moving as a quiet stock does. pub(super) fn day(seed: u64, close: f64) -> Vec { let mut rng = noise(seed); let mut price = close; (0..78) .map(|_| { price *= 1.0 + (rng() - 0.5) * 0.004; price as f32 }) .collect() } /// A steady walk of candles `step` seconds apart from `start`, so captures repeat. pub(super) fn walk(seed: u64, count: usize, start: f64, step: i64) -> Vec { let mut rng = noise(seed); let first = 1_767_364_200; let mut close = start; (0..count) .map(|ix| { let open = close; let drift = (rng() - 0.47) * 0.034; close = (open * (1.0 + drift)).max(1.0); let high = open.max(close) * (1.0 + rng() * 0.011); let low = open.min(close) * (1.0 - rng() * 0.011); let volume = 1.0e6 * (0.5 + rng()) * (1.0 + drift.abs() * 24.0); let time = Timestamp::from_second(first + ix as i64 * step).expect("a time in range"); Candle::new(time, (open, high, low, close), volume) }) .collect() } /// How many candles of an interval a range holds, and each candle's seconds. fn span(range: &str, interval: &str) -> (usize, i64) { let seconds = match interval { "1m" => 60, "5m" => 300, "15m" => 900, "1h" => 3_600, "4h" => 14_400, "1W" => 604_800, "1M" => 2_592_000, _ => 86_400, }; let days = match range { "1D" => 1.0, "5D" => 5.0, "1M" => 22.0, "3M" => 63.0, "6M" => 126.0, "YTD" => 180.0, "1Y" => 252.0, "5Y" => 1_260.0, _ => 2_520.0, }; let per_day = (23_400.0 / seconds as f64).max(1.0 / (seconds as f64 / 86_400.0)); (((days * per_day) as usize).clamp(12, 2_400), seconds) } pub fn market(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { let kind = keep("finance-kind", || ChartKind::Candles, window, cx); let range = keep("finance-range", || SharedString::from("6M"), window, cx); let interval = keep("finance-interval", || SharedString::from("1D"), window, cx); let (now_kind, now_range, now_interval) = ( *kind.read(cx), range.read(cx).clone(), interval.read(cx).clone(), ); let (count, step) = span(&now_range, &now_interval); let seed = 7 + now_interval.len() as u64 * 13 + step as u64 % 97; let candles = walk(seed, count, 184.0, step); let alert = (candles.last().expect("a range holds candles").close * 1.04).round(); let (pick_kind, pick_range, pick_interval) = (kind.clone(), range.clone(), interval.clone()); section( "CandlestickChart / OHLCChart / HeikinAshiChart / LineQuoteChart / VolumeChart / ChartTypeSwitcher / TimeRangeSelector / IntervalSelector", "Prices as candles, bars, smoothed candles, a line or an area, with volume along the bottom and averages over the top. Drag or scroll sideways to move through time, hold Cmd and scroll to zoom, double-press to return to the newest.", cx, ) .child( row() .justify_between() .w_full() .child( row() .child(ChartTypeSwitcher::new("finance-kind", now_kind).on_change(move |kind, _, cx| set(&pick_kind, kind, cx))) .child(IntervalSelector::new("finance-interval", now_interval.clone()).on_change(move |key, _, cx| set(&pick_interval, key.clone(), cx))), ) .child(TimeRangeSelector::new("finance-range", now_range).on_change(move |key, _, cx| set(&pick_range, key.clone(), cx))), ) .child(probe( "market", div().w(px(WIDE)).child( CandlestickChart::new("market", candles) .kind(now_kind) .title(format!("ELY · {now_interval}")) .volume() .overlay(Overlay::Sma(20)) .overlay(Overlay::Ema(50)) .alert(alert, "Alert"), ), )) } pub fn indicators(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { let chosen = keep( "finance-indicators", || { ( vec![Overlay::Bollinger(20, 2.0)], vec![Study::Macd, Study::Rsi], ) }, window, cx, ); let (overlays, studies) = chosen.read(cx).clone(); let candles = walk(11, 160, 92.0, 86_400); let peer = walk(19, 160, 61.0, 86_400); let chart = CandlestickChart::new("studies", candles) .title("ELY · 1D") .compare("PEER", peer); let chart = overlays .iter() .fold(chart, |chart, overlay| chart.overlay(*overlay)); let chart = studies .iter() .fold(chart, |chart, study| chart.study(*study)); let picked = chosen.clone(); section( "IndicatorOverlay / IndicatorPane / IndicatorSelector / CompareSymbol / PriceAlertLine", "Bollinger bands over the prices, MACD and RSI in panes below, and a second symbol drawn from the same start so their moves compare. Choose indicators from the menu.", cx, ) .child(row().child(IndicatorSelector::new("finance-choose", overlays, studies).on_change(move |overlays, studies, _, cx| set(&picked, (overlays, studies), cx)))) .child(div().w_full().child(chart.alert(98.0, "Take profit"))) } pub fn drawings(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { let tool = keep("finance-tool", || Some(Tool::Trend), window, cx); let marks = keep( "finance-marks", || vec![Drawing::Fib((96.0, 118.0), (128.0, 142.0))], window, cx, ); let (now_tool, now_marks) = (*tool.read(cx), marks.read(cx).clone()); let (pick, kept, cleared) = (tool.clone(), marks.clone(), marks.clone()); section( "DrawingTools / DrawingToolbar", "Pick a tool and drag over the prices: a trend line, a level, a box, or retracement levels between a swing's two ends. The pointer moves through time again.", cx, ) .child( row() .child(DrawingToolbar::new("finance-tools", now_tool).on_change(move |tool, _, cx| set(&pick, tool, cx))) .child(Button::new("finance-clear", "Clear").variant(ButtonVariant::Ghost).on_click(move |_, _, cx| set(&cleared, Vec::new(), cx))), ) .child(probe( "drawings", div().w(px(WIDE)).child( CandlestickChart::new("drawn", walk(23, 140, 120.0, 86_400)) .title("ELY · 1D") .drawings(now_marks) .tool(now_tool) .on_draw(move |drawing, _, cx| { kept.update(cx, |marks, cx| { marks.push(drawing); cx.notify(); }) }), ), )) } pub fn depth(cx: &mut App) -> impl IntoElement + use<> { let mut rng = noise(29); let bids: Vec<(f64, f64)> = (0..40) .map(|ix| { ( 187.20 - ix as f64 * 0.05, 200.0 + rng() * 900.0 + ix as f64 * 40.0, ) }) .collect(); let asks: Vec<(f64, f64)> = (0..40) .map(|ix| { ( 187.26 + ix as f64 * 0.05, 200.0 + rng() * 900.0 + ix as f64 * 35.0, ) }) .collect(); section("DepthChart", "Every bid and ask as a running total out from the spread. Hover to read the total to any price.", cx) .child(probe("depth", div().w(px(WIDE)).child(DepthChart::new("depth", bids, asks)))) } pub fn profile(cx: &mut App) -> impl IntoElement + use<> { section( "MarketProfile / VolumeProfile", "How much traded at each price in view, as bars from the right edge; the busiest price is the darkest.", cx, ) .child(div().w_full().child(CandlestickChart::new("profile", walk(31, 120, 54.0, 86_400)).title("ELY · 1D").profile())) } pub fn bricks(cx: &mut App) -> impl IntoElement + use<> { let candles = Rc::new(walk(37, 260, 74.0, 86_400)); section( "RenkoChart / PointAndFigure", "Time drops out: a brick for each whole move of two dollars, and columns of Xs and Os that turn after three boxes the other way.", cx, ) .child( div() .flex() .gap_6() .child(div().flex_1().child(RenkoChart::new("renko", candles.clone(), 2.0))) .child(div().flex_1().child(PointFigureChart::new("figure", candles, 2.0, 3))), ) } pub fn together(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { let arrangement = keep("finance-arrangement", || Arrangement::Four, window, cx); let sync = keep( "finance-sync", || None::>, window, cx, ); if sync.read(cx).is_none() { let made = cx.new(|_| ChartSync::default()); set(&sync, Some(made), cx); } let shared = sync.read(cx).clone().expect("made above"); let now = *arrangement.read(cx); let pick = arrangement.clone(); let names = [ ("ELY", 3, 184.0), ("NOVA", 5, 62.0), ("ORBIT", 8, 131.0), ("PINE", 13, 27.0), ]; let layout = names.iter().fold( MultiChartLayout::new("finance-grid", now), |layout, (name, seed, start)| { let chart = CandlestickChart::new( SharedString::from(format!("sync-{name}")), walk(*seed, 120, *start, 86_400), ) .title(*name) .sync(&shared); layout.cell(chart.h(cx.theme().chart().height * 0.8)) }, ); section( "MultiChartLayout / ChartSync", "Four symbols moving together: pan, zoom or point at one and the others follow. Switch between one chart, two across, two down or four.", cx, ) .child(layout.on_arrange(move |arrangement, _, cx| set(&pick, arrangement, cx))) }