Files
Ely-GPUI-Components/examples/gallery/pages/finance/trading.rs
T

259 lines
8.6 KiB
Rust

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)),
))
}