T16 Finance: market charts with kinds, volume, profile, studies, drawings, compare, alerts and sync (T16a–b); quotes, book, tape and DOM (T16c); order entry, blotter, risk, options and payoff (T16d); markets, calendars, portfolio and wallet (T16e); typography::Ellipsis ends lines that gpui's truncate clipped; chart notes stack and ticks label round values
This commit is contained in:
76 files changed
+10311
-173
No files matched your search
@@ -0,0 +1,284 @@
|
||||
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<f32> {
|
||||
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<Candle> {
|
||||
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::<gpui::Entity<ChartSync>>,
|
||||
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)))
|
||||
}
|
||||
Reference in new issue
Block a user