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,405 @@
|
||||
use ely_gpui_component::{
|
||||
data_display::Tone,
|
||||
finance::{
|
||||
Earnings, EarningsCalendar, EconomicCalendar, MarketHeatmap, MarketOverview, MarketStatus,
|
||||
NewsFeed, Release, Screener, Session, Story, SymbolBadge, SymbolSearch,
|
||||
TradingSessionClock, Watch, Watchlist,
|
||||
},
|
||||
forms::TextInput,
|
||||
tables::{Cell, Column, FilterRule, Row, Test},
|
||||
typography::Caption,
|
||||
};
|
||||
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
||||
use jiff::{Timestamp, civil::date, tz::TimeZone};
|
||||
|
||||
use super::markets::day;
|
||||
use crate::{
|
||||
probe::probe,
|
||||
ui::{keep, row, section, set},
|
||||
};
|
||||
|
||||
/// The demo's symbols: letters, name, where each trades, last close, and a seed for its day.
|
||||
const SYMBOLS: [(&str, &str, &str, f64, u64); 8] = [
|
||||
("ELY", "Ely Systems", "NASDAQ", 184.9, 41),
|
||||
("NOVA", "Nova Energy", "NYSE", 62.6, 43),
|
||||
("ORBIT", "Orbit Freight", "NYSE", 128.9, 47),
|
||||
("PINE", "Pine Foods", "NASDAQ", 27.7, 53),
|
||||
("QUILL", "Quill Media", "NASDAQ", 87.7, 59),
|
||||
("RIVER", "River Health", "NYSE", 45.2, 67),
|
||||
("SOLACE", "Solace Labs", "NASDAQ", 310.5, 73),
|
||||
("TERRA", "Terra Mining", "LSE", 10.1, 79),
|
||||
];
|
||||
|
||||
fn watches() -> Vec<Watch> {
|
||||
SYMBOLS
|
||||
.iter()
|
||||
.map(|(symbol, name, _, close, seed)| {
|
||||
let day = day(*seed, *close);
|
||||
Watch {
|
||||
symbol: (*symbol).into(),
|
||||
name: (*name).into(),
|
||||
last: f64::from(*day.last().expect("a day")),
|
||||
close: *close,
|
||||
day,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn watch(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let chosen = keep("finance-watched", || SharedString::from("ELY"), window, cx);
|
||||
let field = window.use_keyed_state("finance-symbol", cx, |window, cx| {
|
||||
TextInput::new(window, cx).placeholder("Search symbols")
|
||||
});
|
||||
let (now, pick, find) = (chosen.read(cx).clone(), chosen.clone(), chosen.clone());
|
||||
let symbols = SYMBOLS.iter().map(|(symbol, name, venue, ..)| {
|
||||
(
|
||||
SharedString::from(*symbol),
|
||||
SharedString::from(*name),
|
||||
SharedString::from(*venue),
|
||||
)
|
||||
});
|
||||
let badges = SYMBOLS.iter().take(4).map(|(symbol, _, venue, ..)| {
|
||||
SymbolBadge::new(SharedString::from(format!("badge-{symbol}")), *symbol).venue(*venue)
|
||||
});
|
||||
section(
|
||||
"Watchlist / SymbolBadge / SymbolSearch",
|
||||
"Symbols to follow, each with its day as a line, its price and its move; a symbol as a tile of its letters; and a search by letters or name.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_8()
|
||||
.child(
|
||||
div().w(px(520.)).child(
|
||||
Watchlist::new("watchlist", watches())
|
||||
.selected(now.clone())
|
||||
.on_select(move |symbol, _, cx| set(&pick, symbol.clone(), cx)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_4()
|
||||
.child(
|
||||
SymbolSearch::new("symbol-search", &field, symbols)
|
||||
.on_pick(move |symbol, _, cx| set(&find, symbol.clone(), cx)),
|
||||
)
|
||||
.child(row().children(badges))
|
||||
.child(Caption::new(format!("Watching {now}."))),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn heatmap(cx: &mut App) -> impl IntoElement + use<> {
|
||||
let tiles = [
|
||||
("SOLACE", 920.0, 0.0212),
|
||||
("ELY", 780.0, 0.0124),
|
||||
("ORBIT", 410.0, -0.0203),
|
||||
("NOVA", 380.0, -0.0081),
|
||||
("QUILL", 260.0, 0.0041),
|
||||
("RIVER", 240.0, 0.0),
|
||||
("PINE", 150.0, -0.0152),
|
||||
("TERRA", 120.0, -0.0312),
|
||||
("ATLAS", 110.0, 0.0088),
|
||||
("BIRCH", 90.0, 0.0263),
|
||||
("CEDAR", 70.0, -0.0047),
|
||||
("DUNE", 60.0, 0.0151),
|
||||
];
|
||||
let map = tiles.iter().fold(
|
||||
MarketHeatmap::new("heatmap"),
|
||||
|map, (symbol, weight, change)| map.tile(*symbol, *weight, *change),
|
||||
);
|
||||
section(
|
||||
"MarketHeatmap / SectorTreemap",
|
||||
"Each symbol a tile sized by its market value and tinted by its move: deeper green as it rises, deeper red as it falls. Hover a tile to read it.",
|
||||
cx,
|
||||
)
|
||||
.child(probe("heatmap", div().w(px(840.)).child(map.h(px(320.)))))
|
||||
}
|
||||
|
||||
pub fn screener(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let rules = keep(
|
||||
"finance-screen",
|
||||
|| {
|
||||
(
|
||||
vec![FilterRule {
|
||||
column: "change".into(),
|
||||
test: Test::Above,
|
||||
value: "0".into(),
|
||||
}],
|
||||
false,
|
||||
)
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let (now, any) = rules.read(cx).clone();
|
||||
let store = rules.clone();
|
||||
let columns = [
|
||||
Column::new("symbol", "Symbol"),
|
||||
Column::new("sector", "Sector"),
|
||||
Column::new("last", "Last").end().decimals(2),
|
||||
Column::new("change", "Change")
|
||||
.end()
|
||||
.decimals(2)
|
||||
.suffix("%"),
|
||||
Column::new("value", "Value").end().decimals(1).suffix(" B"),
|
||||
Column::new("earnings", "P/E").end().decimals(1),
|
||||
];
|
||||
let sectors = [
|
||||
"Software",
|
||||
"Energy",
|
||||
"Transport",
|
||||
"Food",
|
||||
"Media",
|
||||
"Health",
|
||||
"Software",
|
||||
"Materials",
|
||||
];
|
||||
let rows = SYMBOLS.iter().zip(watches()).zip(sectors).enumerate().map(
|
||||
|(ix, (((symbol, ..), watch), sector))| {
|
||||
let change = (watch.last / watch.close - 1.0) * 100.0;
|
||||
Row::new(
|
||||
*symbol,
|
||||
[
|
||||
Cell::Text((*symbol).into()),
|
||||
Cell::Tag(sector.into(), Tone::Neutral),
|
||||
Cell::Number(watch.last),
|
||||
Cell::Number(change),
|
||||
Cell::Number(12.0 + ix as f64 * 17.5),
|
||||
Cell::Number(9.0 + (ix * 7 % 30) as f64),
|
||||
],
|
||||
)
|
||||
},
|
||||
);
|
||||
section(
|
||||
"Screener / FilterPanel",
|
||||
"Securities narrowed by rules: build the rules above, and the table keeps only what passes them.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
Screener::new("screener", columns, rows)
|
||||
.rules(now, any)
|
||||
.on_rules(move |rules, any, _, cx| set(&store, (rules.to_vec(), any), cx)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn overview(cx: &mut App) -> impl IntoElement + use<> {
|
||||
let indexes = [
|
||||
("EGX 500", "Broad market", 5_812.4, 41),
|
||||
("EGX Tech", "Technology", 19_440.1, 43),
|
||||
("EGX Small", "Small companies", 2_231.8, 47),
|
||||
];
|
||||
let movers = [
|
||||
("BIRCH", 18.92, 18.43),
|
||||
("SOLACE", 317.08, 310.5),
|
||||
("TERRA", 9.78, 10.1),
|
||||
("ORBIT", 126.28, 128.9),
|
||||
];
|
||||
let board = indexes.iter().fold(
|
||||
MarketOverview::new("overview")
|
||||
.status(MarketStatus::new("New York", Session::Open).next("Closes in 2 h 14 min")),
|
||||
|board, (symbol, name, close, seed)| {
|
||||
let day = day(*seed, *close);
|
||||
let last = f64::from(*day.last().expect("a day"));
|
||||
board.index((*symbol, *name), (last, *close), day)
|
||||
},
|
||||
);
|
||||
let board = movers.iter().fold(board, |board, (symbol, last, close)| {
|
||||
board.mover(*symbol, *last, *close)
|
||||
});
|
||||
let clock = [
|
||||
("Sydney", 23.0, 5.0),
|
||||
("Tokyo", 0.0, 6.0),
|
||||
("Hong Kong", 1.5, 8.0),
|
||||
("Frankfurt", 7.0, 15.5),
|
||||
("London", 8.0, 16.5),
|
||||
("New York", 13.5, 20.0),
|
||||
]
|
||||
.iter()
|
||||
.fold(
|
||||
TradingSessionClock::new(15.75),
|
||||
|clock, (name, open, close)| clock.market(*name, *open, *close),
|
||||
);
|
||||
section(
|
||||
"MarketOverview / IndexCard / MarketStatus / TradingSessionClock",
|
||||
"A market at a glance: its session, its indexes as cards, and the day's movers. Below, each market's hours across one day in UTC, those open now lit.",
|
||||
cx,
|
||||
)
|
||||
.child(board)
|
||||
.child(
|
||||
row()
|
||||
.gap_6()
|
||||
.child(MarketStatus::new("London", Session::After).next("Opens in 15 h 15 min"))
|
||||
.child(MarketStatus::new("Tokyo", Session::Closed).next("Opens in 8 h 15 min"))
|
||||
.child(MarketStatus::new("Frankfurt", Session::Before).next("Opens in 30 min")),
|
||||
)
|
||||
.child(clock)
|
||||
}
|
||||
|
||||
fn stamp(hour: i64, minute: i64) -> Timestamp {
|
||||
Timestamp::from_second(1_790_294_400 + hour * 3_600 + minute * 60).expect("a time")
|
||||
}
|
||||
|
||||
pub fn calendars(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let release = |(hour, minute),
|
||||
region: &str,
|
||||
event: &str,
|
||||
weight,
|
||||
figures: (Option<f64>, Option<f64>, Option<f64>),
|
||||
unit: &str| Release {
|
||||
time: stamp(hour, minute),
|
||||
region: SharedString::from(region.to_string()),
|
||||
event: SharedString::from(event.to_string()),
|
||||
weight,
|
||||
actual: figures.0,
|
||||
forecast: figures.1,
|
||||
previous: figures.2,
|
||||
unit: SharedString::from(unit.to_string()),
|
||||
};
|
||||
let releases = [
|
||||
release(
|
||||
(1, 30),
|
||||
"JP",
|
||||
"Tokyo core consumer prices",
|
||||
2,
|
||||
(Some(2.4), Some(2.3), Some(2.2)),
|
||||
"%",
|
||||
),
|
||||
release(
|
||||
(8, 0),
|
||||
"DE",
|
||||
"Ifo business climate",
|
||||
2,
|
||||
(Some(87.1), Some(87.6), Some(87.3)),
|
||||
"",
|
||||
),
|
||||
release(
|
||||
(12, 30),
|
||||
"US",
|
||||
"Durable goods orders",
|
||||
2,
|
||||
(Some(0.8), Some(0.5), Some(-0.3)),
|
||||
"%",
|
||||
),
|
||||
release(
|
||||
(12, 30),
|
||||
"US",
|
||||
"Initial jobless claims",
|
||||
3,
|
||||
(None, Some(224.0), Some(231.0)),
|
||||
"K",
|
||||
),
|
||||
release(
|
||||
(14, 0),
|
||||
"US",
|
||||
"New home sales",
|
||||
1,
|
||||
(None, Some(668.0), Some(652.0)),
|
||||
"K",
|
||||
),
|
||||
];
|
||||
let earnings = [
|
||||
(
|
||||
date(2026, 9, 28),
|
||||
"ELY",
|
||||
"Ely Systems",
|
||||
false,
|
||||
1.84,
|
||||
Some(1.97),
|
||||
),
|
||||
(
|
||||
date(2026, 9, 29),
|
||||
"NOVA",
|
||||
"Nova Energy",
|
||||
true,
|
||||
0.62,
|
||||
Some(0.55),
|
||||
),
|
||||
(
|
||||
date(2026, 9, 30),
|
||||
"SOLACE",
|
||||
"Solace Labs",
|
||||
false,
|
||||
3.10,
|
||||
None,
|
||||
),
|
||||
(date(2026, 10, 1), "QUILL", "Quill Media", true, 0.48, None),
|
||||
]
|
||||
.map(
|
||||
|(date, symbol, name, before_open, estimate, reported)| Earnings {
|
||||
date,
|
||||
symbol: symbol.into(),
|
||||
name: name.into(),
|
||||
before_open,
|
||||
estimate,
|
||||
reported,
|
||||
},
|
||||
);
|
||||
let story = |(hour, minute), source: &str, headline: &str, symbols: &[&str], lean| Story {
|
||||
source: SharedString::from(source.to_string()),
|
||||
time: stamp(hour, minute),
|
||||
headline: SharedString::from(headline.to_string()),
|
||||
symbols: symbols
|
||||
.iter()
|
||||
.map(|symbol| SharedString::from(symbol.to_string()))
|
||||
.collect(),
|
||||
lean,
|
||||
};
|
||||
let stories = [
|
||||
story(
|
||||
(15, 42),
|
||||
"Wire",
|
||||
"Ely Systems lifts its outlook as cloud orders climb",
|
||||
&["ELY"],
|
||||
Some(true),
|
||||
),
|
||||
story(
|
||||
(15, 10),
|
||||
"Markets Desk",
|
||||
"Freight rates slide for a third week",
|
||||
&["ORBIT", "TERRA"],
|
||||
Some(false),
|
||||
),
|
||||
story(
|
||||
(14, 36),
|
||||
"Wire",
|
||||
"Nova Energy names a new chief financial officer",
|
||||
&["NOVA"],
|
||||
None,
|
||||
),
|
||||
story(
|
||||
(13, 58),
|
||||
"Daily Brief",
|
||||
"Solace Labs trial meets its main goal",
|
||||
&["SOLACE"],
|
||||
Some(true),
|
||||
),
|
||||
];
|
||||
let opened = keep("finance-story", || None::<usize>, window, cx);
|
||||
let (read, open) = (*opened.read(cx), opened.clone());
|
||||
section(
|
||||
"EconomicCalendar / EarningsCalendar / NewsFeed",
|
||||
"The day's releases with how much each moves markets and its figure beside the forecast; reports due by date; and stories newest first, each dot leaning green or red. Press a story to open it.",
|
||||
cx,
|
||||
)
|
||||
.child(EconomicCalendar::new(releases).zone(TimeZone::UTC))
|
||||
.child(EarningsCalendar::new("earnings", earnings))
|
||||
.child(
|
||||
div()
|
||||
.w(px(640.))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
NewsFeed::new("news", stories)
|
||||
.zone(TimeZone::UTC)
|
||||
.on_open(move |ix, _, cx| set(&open, Some(ix), cx)),
|
||||
)
|
||||
.children(read.map(|ix| Caption::new(format!("Opened story {}.", ix + 1)))),
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user