167 lines
6.0 KiB
Rust
167 lines
6.0 KiB
Rust
use ely_gpui_component::{
|
|
finance::{
|
|
BidAskBar, DomLadder, Level2Quotes, OrderBook, PriceChangeBadge, PriceText, QuoteCard,
|
|
Side, SpreadIndicator, TickerTape, TimeAndSales, Trade,
|
|
},
|
|
theme::TextSize,
|
|
typography::Caption,
|
|
};
|
|
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
|
use jiff::{Timestamp, tz::TimeZone};
|
|
|
|
use super::markets::day;
|
|
use crate::ui::{keep, noise, row, section, set};
|
|
|
|
/// Prices, each with the size waiting there.
|
|
type Levels = Vec<(f64, f64)>;
|
|
|
|
/// A book around a price: bids below it and asks above, a tick apart, sizes drawn steadily.
|
|
fn levels(seed: u64, middle: f64, tick: f64, count: usize) -> (Levels, Levels) {
|
|
let mut rng = noise(seed);
|
|
let bids = (0..count)
|
|
.map(|ix| {
|
|
(
|
|
middle - tick * (ix as f64 + 1.0),
|
|
(120.0 + rng() * 800.0 + ix as f64 * 60.0).round(),
|
|
)
|
|
})
|
|
.collect();
|
|
let asks = (0..count)
|
|
.map(|ix| {
|
|
(
|
|
middle + tick * (ix as f64 + 1.0),
|
|
(120.0 + rng() * 800.0 + ix as f64 * 55.0).round(),
|
|
)
|
|
})
|
|
.collect();
|
|
(bids, asks)
|
|
}
|
|
|
|
pub fn quotes(cx: &mut App) -> impl IntoElement + use<> {
|
|
let tape = [
|
|
("ELY", 187.24, 0.0124),
|
|
("NOVA", 62.10, -0.0081),
|
|
("ORBIT", 131.52, 0.0203),
|
|
("PINE", 27.33, -0.0152),
|
|
("QUILL", 88.05, 0.0041),
|
|
("RIVER", 45.60, 0.0),
|
|
("SOLACE", 312.90, 0.0077),
|
|
("TERRA", 9.84, -0.0312),
|
|
]
|
|
.iter()
|
|
.fold(TickerTape::new("tape"), |tape, (symbol, now, share)| {
|
|
tape.quote(*symbol, *now, *share)
|
|
});
|
|
let cards = [
|
|
("ELY", "Ely Systems", 41, 184.9),
|
|
("NOVA", "Nova Energy", 43, 62.6),
|
|
("ORBIT", "Orbit Freight", 47, 128.9),
|
|
]
|
|
.map(|(symbol, name, seed, close)| {
|
|
let line = day(seed, close);
|
|
let now = f64::from(*line.last().expect("a day"));
|
|
div().flex_1().child(QuoteCard::new(
|
|
SharedString::from(format!("card-{symbol}")),
|
|
(symbol, name),
|
|
(now, close),
|
|
line,
|
|
))
|
|
});
|
|
section(
|
|
"Ticker / TickerTape / QuoteCard / PriceText / PriceChangeBadge",
|
|
"Quotes running past in a band, and symbols at a glance: the price flashes green or red as it moves, and the badge reads the move since the last close.",
|
|
cx,
|
|
)
|
|
.child(div().w_full().child(tape))
|
|
.child(div().flex().gap_4().children(cards))
|
|
.child(
|
|
row()
|
|
.child(PriceText::new("price-up", 187.26).from(187.24).size(TextSize::Lg))
|
|
.child(PriceText::new("price-down", 62.08).from(62.10).size(TextSize::Lg))
|
|
.child(PriceChangeBadge::new(184.9, 187.26))
|
|
.child(PriceChangeBadge::new(62.6, 62.08))
|
|
.child(PriceChangeBadge::new(187.26, 187.26)),
|
|
)
|
|
}
|
|
|
|
pub fn book(cx: &mut App) -> impl IntoElement + use<> {
|
|
let (bids, asks) = levels(51, 187.25, 0.05, 12);
|
|
let (bid_size, ask_size) = (
|
|
bids.iter().map(|level| level.1).sum::<f64>(),
|
|
asks.iter().map(|level| level.1).sum::<f64>(),
|
|
);
|
|
let mut rng = noise(61);
|
|
let venues = ["NYSE", "ARCA", "BATS", "EDGX", "IEX", "NSDQ"];
|
|
let level2 = (0..10).fold(Level2Quotes::new(), |quotes, ix| {
|
|
let venue = venues[ix % venues.len()];
|
|
let step = (ix / 2) as f64 * 0.05;
|
|
quotes
|
|
.bid(venue, 187.20 - step, (100.0 + rng() * 900.0).round())
|
|
.ask(
|
|
venues[(ix + 3) % venues.len()],
|
|
187.30 + step,
|
|
(100.0 + rng() * 900.0).round(),
|
|
)
|
|
});
|
|
section(
|
|
"OrderBook / SpreadIndicator / BidAskBar / Level2 Quotes",
|
|
"Orders waiting on each side, best in the middle, each level shaded by its running total; the spread and the balance between sides; and each venue's quotes side by side.",
|
|
cx,
|
|
)
|
|
.child(
|
|
div()
|
|
.flex()
|
|
.gap_8()
|
|
.child(div().w(px(320.)).child(OrderBook::new("book", bids.clone(), asks.clone())))
|
|
.child(
|
|
div()
|
|
.flex_1()
|
|
.flex()
|
|
.flex_col()
|
|
.gap_6()
|
|
.child(SpreadIndicator::new(bids[0].0, asks[0].0))
|
|
.child(BidAskBar::new(bid_size, ask_size))
|
|
.child(level2),
|
|
),
|
|
)
|
|
}
|
|
|
|
pub fn tape(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|
let mut rng = noise(71);
|
|
let start = 1_790_342_400;
|
|
let trades: Vec<Trade> = (0..40)
|
|
.map(|ix| {
|
|
let buy = rng() > 0.45;
|
|
Trade {
|
|
time: Timestamp::from_second(start + ix * 7).expect("a time"),
|
|
price: 187.25 + (rng() - 0.5) * 0.4,
|
|
size: (rng().powi(3) * 5_000.0 + 20.0).round(),
|
|
side: if buy { Side::Buy } else { Side::Sell },
|
|
}
|
|
})
|
|
.collect();
|
|
let (bids, asks) = levels(81, 187.25, 0.05, 10);
|
|
let traded = keep("finance-traded", || None::<SharedString>, window, cx);
|
|
let (told, tell) = (traded.read(cx).clone(), traded.clone());
|
|
section(
|
|
"TimeAndSales / TradeTape / MarketDepthLadder / DOM",
|
|
"Trades as they print, newest on top and large prints bold; and the book as a ladder around the last price. Press a size to trade at its price.",
|
|
cx,
|
|
)
|
|
.child(
|
|
div()
|
|
.flex()
|
|
.gap_8()
|
|
.child(div().w(px(320.)).child(TimeAndSales::new("prints", trades).large(2_000.0).zone(TimeZone::UTC)))
|
|
.child(
|
|
div().flex_1().flex().flex_col().gap_2().child(
|
|
DomLadder::new("ladder", bids, asks, 187.25, 0.05).on_trade(move |side, at, _, cx| {
|
|
let words = format!("{} at {at:.2}", if side == Side::Buy { "Bid" } else { "Offer" });
|
|
set(&tell, Some(words.into()), cx)
|
|
}),
|
|
)
|
|
.children(told.map(|words| Caption::new(format!("Placed: {words}.")))),
|
|
),
|
|
)
|
|
}
|