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,258 @@
|
||||
use ely_gpui_component::{
|
||||
finance::{
|
||||
Fill, GreeksTable, Leg, LeverageSlider, MarginIndicator, OptionChain, OptionQuote, Order,
|
||||
OrderConfirmDialog, OrderEntry, OrderKind, OrderTable, PayoffDiagram, PnLDisplay, Position,
|
||||
PositionTable, QuickTradeButtons, RiskMeter, Side, Strike, TradeHistoryTable, Working,
|
||||
},
|
||||
typography::Caption,
|
||||
};
|
||||
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
||||
use jiff::{Timestamp, tz::TimeZone};
|
||||
|
||||
use super::markets::WIDE;
|
||||
use crate::{
|
||||
probe::probe,
|
||||
ui::{keep, row, section, set},
|
||||
};
|
||||
|
||||
/// The demo symbol's last trade.
|
||||
const LAST: f64 = 187.25;
|
||||
|
||||
fn at(minutes: i64) -> Timestamp {
|
||||
Timestamp::from_second(1_790_343_000 + minutes * 60).expect("a time")
|
||||
}
|
||||
|
||||
pub fn ticket(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let order = keep(
|
||||
"finance-order",
|
||||
|| Order {
|
||||
symbol: "ELY".into(),
|
||||
side: Side::Buy,
|
||||
kind: OrderKind::Limit,
|
||||
quantity: 100.0,
|
||||
price: 187.20,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let asking = keep("finance-asking", || false, window, cx);
|
||||
let told = keep("finance-told", || None::<SharedString>, window, cx);
|
||||
let now = order.read(cx).clone();
|
||||
let dialog = (*asking.read(cx)).then(|| {
|
||||
let (shut, placed) = (asking.clone(), told.clone());
|
||||
OrderConfirmDialog::new("finance-confirm", now.clone(), LAST, move |_, cx| {
|
||||
set(&shut, false, cx)
|
||||
})
|
||||
.on_confirm(move |order, _, cx| {
|
||||
set(
|
||||
&placed,
|
||||
Some(format!("Placed: {}.", order.words(2)).into()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
});
|
||||
let caption = told
|
||||
.read(cx)
|
||||
.clone()
|
||||
.unwrap_or("Nothing placed yet.".into());
|
||||
let (store, ask, quick) = (order.clone(), asking.clone(), told.clone());
|
||||
let entry = OrderEntry::new("ticket", now, LAST)
|
||||
.on_change(move |order, _, cx| set(&store, order, cx))
|
||||
.on_submit(move |_, _, cx| set(&ask, true, cx));
|
||||
let buttons = QuickTradeButtons::new("quick", 187.24, 187.26).on_trade(move |side, _, cx| {
|
||||
let words = match side {
|
||||
Side::Buy => "Bought 100 ELY at 187.26.",
|
||||
Side::Sell => "Sold 100 ELY at 187.24.",
|
||||
};
|
||||
set(&quick, Some(words.into()), cx)
|
||||
});
|
||||
section(
|
||||
"OrderEntry / TradePanel / QuickTradeButtons / OrderConfirmDialog",
|
||||
"An order filled in field by field, with what it comes to; its button asks before the order goes. Quick buttons trade at the bid or the ask in one press.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_8()
|
||||
.child(probe("ticket", div().w(px(340.)).child(entry)))
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_4()
|
||||
.child(buttons)
|
||||
.child(Caption::new(caption)),
|
||||
),
|
||||
)
|
||||
.children(dialog)
|
||||
}
|
||||
|
||||
pub fn blotter(cx: &mut App) -> impl IntoElement + use<> {
|
||||
let positions = [
|
||||
("ELY", 300.0, 171.40, LAST),
|
||||
("NOVA", -150.0, 64.80, 62.08),
|
||||
("ORBIT", 80.0, 139.10, 131.52),
|
||||
("PINE", 1_200.0, 25.90, 27.33),
|
||||
]
|
||||
.map(|(symbol, quantity, cost, last)| Position {
|
||||
symbol: symbol.into(),
|
||||
quantity,
|
||||
cost,
|
||||
last,
|
||||
});
|
||||
let order = |symbol: &str, side, kind, quantity, price| Order {
|
||||
symbol: SharedString::from(symbol.to_string()),
|
||||
side,
|
||||
kind,
|
||||
quantity,
|
||||
price,
|
||||
};
|
||||
let working = [
|
||||
(
|
||||
4,
|
||||
order("ELY", Side::Buy, OrderKind::Limit, 200.0, 185.50),
|
||||
0.0,
|
||||
),
|
||||
(
|
||||
11,
|
||||
order("NOVA", Side::Sell, OrderKind::Stop, 150.0, 60.00),
|
||||
0.0,
|
||||
),
|
||||
(
|
||||
26,
|
||||
order("QUILL", Side::Buy, OrderKind::Limit, 500.0, 87.80),
|
||||
220.0,
|
||||
),
|
||||
]
|
||||
.map(|(minutes, order, filled)| Working {
|
||||
time: at(minutes),
|
||||
order,
|
||||
filled,
|
||||
});
|
||||
let fills = [
|
||||
(2, "ELY", Side::Buy, 100.0, 186.92, 1.00),
|
||||
(9, "PINE", Side::Buy, 400.0, 27.05, 1.60),
|
||||
(17, "ORBIT", Side::Sell, 40.0, 132.40, 1.00),
|
||||
(31, "ELY", Side::Buy, 200.0, 187.18, 1.20),
|
||||
(44, "NOVA", Side::Sell, 150.0, 62.31, 1.00),
|
||||
]
|
||||
.map(|(minutes, symbol, side, quantity, price, fee)| Fill {
|
||||
time: at(minutes),
|
||||
symbol: symbol.into(),
|
||||
side,
|
||||
quantity,
|
||||
price,
|
||||
fee,
|
||||
});
|
||||
section(
|
||||
"PositionTable / OrderTable / TradeHistoryTable / PnLDisplay",
|
||||
"What is held and what it has made, orders still waiting with how much has filled, and each fill with its fee. Results read green for a gain and red for a loss.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
row()
|
||||
.gap_12()
|
||||
.child(PnLDisplay::new("pnl-day", "Today", (1_284.50, 0.0092), "USD"))
|
||||
.child(PnLDisplay::new("pnl-open", "Open positions", (4_910.20, 0.0371), "USD"))
|
||||
.child(PnLDisplay::new("pnl-month", "This month", (-2_046.75, -0.0148), "USD")),
|
||||
)
|
||||
.child(PositionTable::new("positions", positions))
|
||||
.child(OrderTable::new("orders", working).zone(TimeZone::UTC))
|
||||
.child(TradeHistoryTable::new("fills", fills).zone(TimeZone::UTC))
|
||||
}
|
||||
|
||||
pub fn risk(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let leverage = keep("finance-leverage", || 5.0, window, cx);
|
||||
let now = *leverage.read(cx);
|
||||
let store = leverage.clone();
|
||||
let used = 21_500.0 * now / 5.0;
|
||||
section(
|
||||
"LeverageSlider / MarginIndicator / RiskMeter",
|
||||
"Leverage marked at the common steps; the margin it takes against the account, and the risk it carries. Each turns amber, then red, as it climbs. Drag the slider.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_12()
|
||||
.items_center()
|
||||
.child(
|
||||
div()
|
||||
.w(px(360.))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_8()
|
||||
.child(
|
||||
LeverageSlider::new("leverage", now, 100.0)
|
||||
.on_change(move |value, _, cx| set(&store, value, cx)),
|
||||
)
|
||||
.child(MarginIndicator::new("margin", used.min(96_000.0), 96_000.0)),
|
||||
)
|
||||
.child(RiskMeter::new("risk", (now * 0.9 + 8.0).min(100.0))),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn options(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let spot = LAST;
|
||||
let strikes = (0..9).map(|ix| {
|
||||
let strike = 170.0 + ix as f64 * 5.0;
|
||||
let (call, put) = ((spot - strike).max(0.0), (strike - spot).max(0.0));
|
||||
let time = 3.2 - (strike - spot).abs() * 0.04;
|
||||
let quote = |inner: f64, volume: f64, volatility: f64| OptionQuote {
|
||||
bid: inner + time.max(0.3) - 0.05,
|
||||
ask: inner + time.max(0.3) + 0.05,
|
||||
volume,
|
||||
volatility,
|
||||
};
|
||||
Strike {
|
||||
strike,
|
||||
call: quote(
|
||||
call,
|
||||
400.0 + ix as f64 * 120.0,
|
||||
0.24 + (ix as f64 - 4.0).abs() * 0.01,
|
||||
),
|
||||
put: quote(
|
||||
put,
|
||||
1_400.0 - ix as f64 * 110.0,
|
||||
0.26 + (ix as f64 - 4.0).abs() * 0.012,
|
||||
),
|
||||
}
|
||||
});
|
||||
let picked = keep("finance-picked", || None::<SharedString>, window, cx);
|
||||
let (told, pick) = (picked.read(cx).clone(), picked.clone());
|
||||
let spread = [
|
||||
Leg {
|
||||
call: true,
|
||||
strike: 185.0,
|
||||
quantity: 1.0,
|
||||
premium: 4.10,
|
||||
},
|
||||
Leg {
|
||||
call: true,
|
||||
strike: 195.0,
|
||||
quantity: -1.0,
|
||||
premium: 1.05,
|
||||
},
|
||||
];
|
||||
section(
|
||||
"OptionChain / GreeksTable / PayoffDiagram",
|
||||
"Calls to the left and puts to the right of each strike, the side in the money shaded and a line at the price; a position's greeks; and what a call spread pays at expiry. Hover the diagram to read any price.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
OptionChain::new("chain", strikes, spot).on_pick(move |strike, call, _, cx| {
|
||||
let words = format!("{} {strike:.0}", if call { "Call" } else { "Put" });
|
||||
set(&pick, Some(words.into()), cx)
|
||||
}),
|
||||
)
|
||||
.children(told.map(|words| Caption::new(format!("Picked: {words}."))))
|
||||
.child(div().w(px(640.)).child(GreeksTable::new([0.42, 0.031, -0.086, 0.121, 0.047])))
|
||||
.child(probe(
|
||||
"payoff",
|
||||
div()
|
||||
.w(px(WIDE))
|
||||
.child(PayoffDiagram::new("payoff", spread, (165.0, 215.0), spot)),
|
||||
))
|
||||
}
|
||||
Reference in new issue
Block a user