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,287 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
App, ElementId, FontWeight, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement,
|
||||
RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div, prelude::*,
|
||||
};
|
||||
use jiff::tz::TimeZone;
|
||||
|
||||
use super::{
|
||||
book::{Side, Trade, cell, heading, level},
|
||||
quotes::{moves, price},
|
||||
};
|
||||
use crate::{
|
||||
charts::compact,
|
||||
motion::Flash,
|
||||
theme::{ActiveTheme, Density, TextSize},
|
||||
typography::tabular,
|
||||
};
|
||||
|
||||
/// Trades as they print, newest on top: the time, price and size tinted by the side that took them; trades of at least `large` stand bold, and the newest flashes in.
|
||||
#[derive(IntoElement)]
|
||||
pub struct TimeAndSales {
|
||||
id: ElementId,
|
||||
trades: Vec<Trade>,
|
||||
large: f64,
|
||||
rows: usize,
|
||||
places: usize,
|
||||
zone: TimeZone,
|
||||
red_up: bool,
|
||||
}
|
||||
|
||||
impl TimeAndSales {
|
||||
/// Trades oldest first.
|
||||
pub fn new(id: impl Into<ElementId>, trades: impl IntoIterator<Item = Trade>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
trades: trades.into_iter().collect(),
|
||||
large: f64::MAX,
|
||||
rows: 12,
|
||||
places: 2,
|
||||
zone: TimeZone::system(),
|
||||
red_up: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The size from which a trade reads bold.
|
||||
pub fn large(mut self, size: f64) -> Self {
|
||||
self.large = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn rows(mut self, rows: usize) -> Self {
|
||||
self.rows = rows;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn zone(mut self, zone: TimeZone) -> Self {
|
||||
self.zone = zone;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn red_up(mut self) -> Self {
|
||||
self.red_up = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for TimeAndSales {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let (rise, fall) = moves(self.red_up, cx);
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.clone();
|
||||
let newest = self.trades.len();
|
||||
let rows = self
|
||||
.trades
|
||||
.iter()
|
||||
.rev()
|
||||
.take(self.rows)
|
||||
.enumerate()
|
||||
.map(|(ix, trade)| {
|
||||
let ink = if trade.side == Side::Buy { rise } else { fall };
|
||||
let time = trade
|
||||
.time
|
||||
.to_zoned(self.zone.clone())
|
||||
.strftime("%H:%M:%S")
|
||||
.to_string();
|
||||
let row = div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.px_2()
|
||||
.h(theme.table_row(Density::Compact))
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.when(trade.size >= self.large, |row| {
|
||||
row.font_weight(FontWeight::SEMIBOLD)
|
||||
})
|
||||
.child(
|
||||
tabular(div())
|
||||
.flex_1()
|
||||
.text_right()
|
||||
.text_color(colors.fg_muted)
|
||||
.child(time),
|
||||
)
|
||||
.child(cell(price(trade.price, self.places), ink))
|
||||
.child(cell(compact(trade.size), colors.fg));
|
||||
if ix == 0 {
|
||||
Flash::new((self.id.clone(), "newest"), newest)
|
||||
.child(row)
|
||||
.into_any_element()
|
||||
} else {
|
||||
row.into_any_element()
|
||||
}
|
||||
});
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(heading(&["Time", "Price", "Size"], cx))
|
||||
.children(rows)
|
||||
}
|
||||
}
|
||||
|
||||
type OnTrade = Rc<dyn Fn(Side, f64, &mut Window, &mut App)>;
|
||||
|
||||
/// The book as a ladder of prices around the last trade: size bid at each price on the left, asked on the right, the last price marked in the middle. A press on a size side trades at its price.
|
||||
#[derive(IntoElement)]
|
||||
pub struct DomLadder {
|
||||
id: ElementId,
|
||||
bids: Vec<(f64, f64)>,
|
||||
asks: Vec<(f64, f64)>,
|
||||
last: f64,
|
||||
tick: f64,
|
||||
rows: usize,
|
||||
places: usize,
|
||||
on_trade: Option<OnTrade>,
|
||||
red_up: bool,
|
||||
}
|
||||
|
||||
impl DomLadder {
|
||||
/// Bids and asks, each a price and the size waiting there; the last trade's price, and the price step between rows.
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
bids: impl IntoIterator<Item = (f64, f64)>,
|
||||
asks: impl IntoIterator<Item = (f64, f64)>,
|
||||
last: f64,
|
||||
tick: f64,
|
||||
) -> Self {
|
||||
assert!(
|
||||
tick > 0.0 && last.is_finite(),
|
||||
"a ladder needs a step and a last price"
|
||||
);
|
||||
Self {
|
||||
id: id.into(),
|
||||
bids: bids.into_iter().collect(),
|
||||
asks: asks.into_iter().collect(),
|
||||
last,
|
||||
tick,
|
||||
rows: 15,
|
||||
places: 2,
|
||||
on_trade: None,
|
||||
red_up: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rows(mut self, rows: usize) -> Self {
|
||||
self.rows = rows;
|
||||
self
|
||||
}
|
||||
|
||||
/// Gets a buy at a bid's price or a sell at an ask's, from a press on its size.
|
||||
pub fn on_trade(
|
||||
mut self,
|
||||
handler: impl Fn(Side, f64, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_trade = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn red_up(mut self) -> Self {
|
||||
self.red_up = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// The prices a ladder shows around `last`, highest first, each on a whole step.
|
||||
fn rungs(last: f64, tick: f64, rows: usize) -> Vec<f64> {
|
||||
let middle = (last / tick).round() as i64;
|
||||
let above = rows as i64 / 2;
|
||||
(0..rows as i64)
|
||||
.map(|ix| (middle + above - ix) as f64 * tick)
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl RenderOnce for DomLadder {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let (rise, fall) = moves(self.red_up, cx);
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.clone();
|
||||
let near = |side: &[(f64, f64)], at: f64| {
|
||||
side.iter()
|
||||
.find(|(level, _)| (level - at).abs() < self.tick / 2.0)
|
||||
.map(|(_, size)| *size)
|
||||
};
|
||||
let deepest = self
|
||||
.bids
|
||||
.iter()
|
||||
.chain(&self.asks)
|
||||
.map(|(_, size)| *size)
|
||||
.fold(0.0, f64::max);
|
||||
let last = (self.last / self.tick).round() * self.tick;
|
||||
let rows = rungs(self.last, self.tick, self.rows)
|
||||
.into_iter()
|
||||
.map(|at| {
|
||||
let (bid, ask) = (near(&self.bids, at), near(&self.asks, at));
|
||||
let side_cell = |size: Option<f64>, side: Side, ink: Hsla| {
|
||||
let key = format!("{}-{:?}", at, side);
|
||||
let body = level(
|
||||
size.unwrap_or(0.0) / deepest.max(f64::EPSILON),
|
||||
ink,
|
||||
side == Side::Buy,
|
||||
cx,
|
||||
)
|
||||
.flex_1()
|
||||
.child(cell(size.map(compact).unwrap_or_default(), colors.fg));
|
||||
let trade = self.on_trade.clone();
|
||||
div()
|
||||
.id((self.id.clone(), SharedString::from(key)))
|
||||
.flex_1()
|
||||
.cursor_pointer()
|
||||
.hover(|style| style.bg(colors.hover))
|
||||
.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
|
||||
.when_some(trade, |cell, trade| {
|
||||
cell.on_click(move |_, window, cx| {
|
||||
log::info!("dom ladder: {side:?} at {at}");
|
||||
trade(side, at, window, cx)
|
||||
})
|
||||
})
|
||||
.child(body)
|
||||
};
|
||||
let marked = (at - last).abs() < self.tick / 2.0;
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.border_b_1()
|
||||
.border_color(colors.border.opacity(0.5))
|
||||
.child(side_cell(bid, Side::Buy, rise))
|
||||
.child(
|
||||
tabular(div())
|
||||
.w(theme.label_width() * 0.6)
|
||||
.text_center()
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.when(marked, |price| {
|
||||
price
|
||||
.bg(colors.active)
|
||||
.text_color(colors.fg)
|
||||
.font_weight(FontWeight::SEMIBOLD)
|
||||
})
|
||||
.when(!marked, |price| price.text_color(colors.fg_muted))
|
||||
.child(price(at, self.places)),
|
||||
)
|
||||
.child(side_cell(ask, Side::Sell, fall))
|
||||
});
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.rounded(theme.radius(crate::theme::Radius::Md))
|
||||
.border_1()
|
||||
.border_color(colors.border)
|
||||
.overflow_hidden()
|
||||
.children(rows)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::rungs;
|
||||
|
||||
#[test]
|
||||
fn rungs_step_around_the_last() {
|
||||
let prices = rungs(100.02, 0.05, 5);
|
||||
assert_eq!(prices.len(), 5);
|
||||
assert!(
|
||||
(prices[2] - 100.0).abs() < 1e-9,
|
||||
"the middle rung is the last price on a whole step"
|
||||
);
|
||||
assert!(prices[0] > prices[4], "highest first");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user