T16 review fixes: an unknown system zone stops instead of reading as UTC, the view window refits when data shrinks, the volume profile counts narrow and flat candles whole, Fibonacci labels stay in their pane, the DOM ladder shows its tick's decimals, the tape flashes on each new trade, typed numbers reach their owner before a press, and a smaller loss than expected reads as a beat
This commit is contained in:
14 files changed
+230
-65
No files matched your search
+20
-9
@@ -16,7 +16,10 @@ use crate::{
|
|||||||
motion::Flash,
|
motion::Flash,
|
||||||
tables::{Aggregate, Cell, Column, DataTable, Row},
|
tables::{Aggregate, Cell, Column, DataTable, Row},
|
||||||
theme::{ActiveTheme, TextSize},
|
theme::{ActiveTheme, TextSize},
|
||||||
typography::{format, tabular},
|
typography::{
|
||||||
|
format::{self, system_zone},
|
||||||
|
tabular,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A held position: its symbol, how many, what each cost on average, and the last price.
|
/// A held position: its symbol, how many, what each cost on average, and the last price.
|
||||||
@@ -115,7 +118,7 @@ pub struct Working {
|
|||||||
pub struct OrderTable {
|
pub struct OrderTable {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
orders: Vec<Working>,
|
orders: Vec<Working>,
|
||||||
zone: TimeZone,
|
zone: Option<TimeZone>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OrderTable {
|
impl OrderTable {
|
||||||
@@ -132,13 +135,13 @@ impl OrderTable {
|
|||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
orders,
|
orders,
|
||||||
zone: TimeZone::system(),
|
zone: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The time zone its times read in; the system's unless set.
|
/// The time zone its times read in; the system's unless set.
|
||||||
pub fn zone(mut self, zone: TimeZone) -> Self {
|
pub fn zone(mut self, zone: TimeZone) -> Self {
|
||||||
self.zone = zone;
|
self.zone = Some(zone);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,6 +156,10 @@ fn side_tag(side: Side) -> Cell {
|
|||||||
|
|
||||||
impl RenderOnce for OrderTable {
|
impl RenderOnce for OrderTable {
|
||||||
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
|
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
|
||||||
|
let zone = self
|
||||||
|
.zone
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| system_zone("OrderTable"));
|
||||||
let columns = [
|
let columns = [
|
||||||
Column::new("time", "Placed"),
|
Column::new("time", "Placed"),
|
||||||
Column::new("symbol", "Symbol"),
|
Column::new("symbol", "Symbol"),
|
||||||
@@ -175,7 +182,7 @@ impl RenderOnce for OrderTable {
|
|||||||
};
|
};
|
||||||
let time: SharedString = working
|
let time: SharedString = working
|
||||||
.time
|
.time
|
||||||
.to_zoned(self.zone.clone())
|
.to_zoned(zone.clone())
|
||||||
.strftime("%H:%M:%S")
|
.strftime("%H:%M:%S")
|
||||||
.to_string()
|
.to_string()
|
||||||
.into();
|
.into();
|
||||||
@@ -214,7 +221,7 @@ pub struct Fill {
|
|||||||
pub struct TradeHistoryTable {
|
pub struct TradeHistoryTable {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
fills: Vec<Fill>,
|
fills: Vec<Fill>,
|
||||||
zone: TimeZone,
|
zone: Option<TimeZone>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TradeHistoryTable {
|
impl TradeHistoryTable {
|
||||||
@@ -224,19 +231,23 @@ impl TradeHistoryTable {
|
|||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
fills,
|
fills,
|
||||||
zone: TimeZone::system(),
|
zone: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The time zone its times read in; the system's unless set.
|
/// The time zone its times read in; the system's unless set.
|
||||||
pub fn zone(mut self, zone: TimeZone) -> Self {
|
pub fn zone(mut self, zone: TimeZone) -> Self {
|
||||||
self.zone = zone;
|
self.zone = Some(zone);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for TradeHistoryTable {
|
impl RenderOnce for TradeHistoryTable {
|
||||||
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
|
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
|
||||||
|
let zone = self
|
||||||
|
.zone
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| system_zone("TradeHistoryTable"));
|
||||||
let columns = [
|
let columns = [
|
||||||
Column::new("time", "Time"),
|
Column::new("time", "Time"),
|
||||||
Column::new("symbol", "Symbol"),
|
Column::new("symbol", "Symbol"),
|
||||||
@@ -261,7 +272,7 @@ impl RenderOnce for TradeHistoryTable {
|
|||||||
let total = signed * fill.quantity * fill.price - fill.fee;
|
let total = signed * fill.quantity * fill.price - fill.fee;
|
||||||
let time: SharedString = fill
|
let time: SharedString = fill
|
||||||
.time
|
.time
|
||||||
.to_zoned(self.zone.clone())
|
.to_zoned(zone.clone())
|
||||||
.strftime("%b %-d %H:%M")
|
.strftime("%b %-d %H:%M")
|
||||||
.to_string()
|
.to_string()
|
||||||
.into();
|
.into();
|
||||||
|
|||||||
+35
-10
@@ -11,7 +11,11 @@ use crate::{
|
|||||||
forms::flag,
|
forms::flag,
|
||||||
tables::{Cell, Column, DataTable, Row},
|
tables::{Cell, Column, DataTable, Row},
|
||||||
theme::{ActiveTheme, TextSize},
|
theme::{ActiveTheme, TextSize},
|
||||||
typography::{Ellipsis, format, tabular},
|
typography::{
|
||||||
|
Ellipsis,
|
||||||
|
format::{self, system_zone},
|
||||||
|
tabular,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// An economic release: when, for which region, what, how much it moves markets from one to three, and its figure as it came, as forecast and as before.
|
/// An economic release: when, for which region, what, how much it moves markets from one to three, and its figure as it came, as forecast and as before.
|
||||||
@@ -31,7 +35,7 @@ pub struct Release {
|
|||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct EconomicCalendar {
|
pub struct EconomicCalendar {
|
||||||
releases: Vec<Release>,
|
releases: Vec<Release>,
|
||||||
zone: TimeZone,
|
zone: Option<TimeZone>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EconomicCalendar {
|
impl EconomicCalendar {
|
||||||
@@ -45,18 +49,22 @@ impl EconomicCalendar {
|
|||||||
);
|
);
|
||||||
Self {
|
Self {
|
||||||
releases,
|
releases,
|
||||||
zone: TimeZone::system(),
|
zone: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn zone(mut self, zone: TimeZone) -> Self {
|
pub fn zone(mut self, zone: TimeZone) -> Self {
|
||||||
self.zone = zone;
|
self.zone = Some(zone);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for EconomicCalendar {
|
impl RenderOnce for EconomicCalendar {
|
||||||
fn render(mut self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(mut self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let zone = self
|
||||||
|
.zone
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| system_zone("EconomicCalendar"));
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let colors = theme.colors.clone();
|
let colors = theme.colors.clone();
|
||||||
self.releases.sort_by_key(|release| release.time);
|
self.releases.sort_by_key(|release| release.time);
|
||||||
@@ -68,7 +76,7 @@ impl RenderOnce for EconomicCalendar {
|
|||||||
let mut out: Vec<gpui::AnyElement> = Vec::new();
|
let mut out: Vec<gpui::AnyElement> = Vec::new();
|
||||||
let mut day: Option<Date> = None;
|
let mut day: Option<Date> = None;
|
||||||
for release in &self.releases {
|
for release in &self.releases {
|
||||||
let zoned = release.time.to_zoned(self.zone.clone());
|
let zoned = release.time.to_zoned(zone.clone());
|
||||||
if day != Some(zoned.date()) {
|
if day != Some(zoned.date()) {
|
||||||
day = Some(zoned.date());
|
day = Some(zoned.date());
|
||||||
out.push(
|
out.push(
|
||||||
@@ -179,6 +187,11 @@ pub struct Earnings {
|
|||||||
pub reported: Option<f64>,
|
pub reported: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How far a report beat its estimate, as a share of the estimate's size: positive when better, whatever the sign.
|
||||||
|
pub(crate) fn surprise(estimate: f64, reported: f64) -> f64 {
|
||||||
|
(reported - estimate) / estimate.abs()
|
||||||
|
}
|
||||||
|
|
||||||
/// Earnings by date: each company, when in the day it reports, what is expected, and once reported, how far it beat or missed.
|
/// Earnings by date: each company, when in the day it reports, what is expected, and once reported, how far it beat or missed.
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct EarningsCalendar {
|
pub struct EarningsCalendar {
|
||||||
@@ -222,7 +235,7 @@ impl RenderOnce for EarningsCalendar {
|
|||||||
);
|
);
|
||||||
let surprise = match earnings.reported {
|
let surprise = match earnings.reported {
|
||||||
Some(reported) if earnings.estimate != 0.0 => {
|
Some(reported) if earnings.estimate != 0.0 => {
|
||||||
let share = reported / earnings.estimate - 1.0;
|
let share = surprise(earnings.estimate, reported);
|
||||||
Cell::Tag(
|
Cell::Tag(
|
||||||
format::percent(share, 1, true).into(),
|
format::percent(share, 1, true).into(),
|
||||||
if share >= 0.0 {
|
if share >= 0.0 {
|
||||||
@@ -270,7 +283,7 @@ type OnStory = Rc<dyn Fn(usize, &mut Window, &mut App)>;
|
|||||||
pub struct NewsFeed {
|
pub struct NewsFeed {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
stories: Vec<Story>,
|
stories: Vec<Story>,
|
||||||
zone: TimeZone,
|
zone: Option<TimeZone>,
|
||||||
on_open: Option<OnStory>,
|
on_open: Option<OnStory>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,13 +292,13 @@ impl NewsFeed {
|
|||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
stories: stories.into_iter().collect(),
|
stories: stories.into_iter().collect(),
|
||||||
zone: TimeZone::system(),
|
zone: None,
|
||||||
on_open: None,
|
on_open: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn zone(mut self, zone: TimeZone) -> Self {
|
pub fn zone(mut self, zone: TimeZone) -> Self {
|
||||||
self.zone = zone;
|
self.zone = Some(zone);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,6 +311,7 @@ impl NewsFeed {
|
|||||||
|
|
||||||
impl RenderOnce for NewsFeed {
|
impl RenderOnce for NewsFeed {
|
||||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let zone = self.zone.clone().unwrap_or_else(|| system_zone("NewsFeed"));
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let colors = theme.colors.clone();
|
let colors = theme.colors.clone();
|
||||||
let mut order: Vec<usize> = (0..self.stories.len()).collect();
|
let mut order: Vec<usize> = (0..self.stories.len()).collect();
|
||||||
@@ -315,7 +329,7 @@ impl RenderOnce for NewsFeed {
|
|||||||
let open = self.on_open.clone();
|
let open = self.on_open.clone();
|
||||||
let when = story
|
let when = story
|
||||||
.time
|
.time
|
||||||
.to_zoned(self.zone.clone())
|
.to_zoned(zone.clone())
|
||||||
.strftime("%b %-d, %H:%M")
|
.strftime("%b %-d, %H:%M")
|
||||||
.to_string();
|
.to_string();
|
||||||
div()
|
div()
|
||||||
@@ -376,3 +390,14 @@ impl RenderOnce for NewsFeed {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::surprise;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_smaller_loss_than_expected_is_a_beat() {
|
||||||
|
assert_eq!(surprise(-1.0, -0.5), 0.5);
|
||||||
|
assert_eq!(surprise(2.0, 1.5), -0.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
-2
@@ -162,14 +162,22 @@ pub(crate) fn profile(
|
|||||||
(low, high): (f64, f64),
|
(low, high): (f64, f64),
|
||||||
bands: usize,
|
bands: usize,
|
||||||
) -> Vec<(f64, f64, f64)> {
|
) -> Vec<(f64, f64, f64)> {
|
||||||
|
assert!(bands > 0 && high > low, "a profile needs bands over a span");
|
||||||
let step = (high - low) / bands as f64;
|
let step = (high - low) / bands as f64;
|
||||||
let mut traded = vec![0.0; bands];
|
let mut traded = vec![0.0; bands];
|
||||||
for candle in candles {
|
for candle in candles {
|
||||||
let reach = (candle.high - candle.low).max(step);
|
let span = candle.high - candle.low;
|
||||||
|
if span <= 0.0 {
|
||||||
|
let band = ((candle.close - low) / step)
|
||||||
|
.floor()
|
||||||
|
.clamp(0.0, (bands - 1) as f64);
|
||||||
|
traded[band as usize] += candle.volume;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
for (band, volume) in traded.iter_mut().enumerate() {
|
for (band, volume) in traded.iter_mut().enumerate() {
|
||||||
let (from, to) = (low + step * band as f64, low + step * (band + 1) as f64);
|
let (from, to) = (low + step * band as f64, low + step * (band + 1) as f64);
|
||||||
let overlap = (to.min(candle.high) - from.max(candle.low)).max(0.0);
|
let overlap = (to.min(candle.high) - from.max(candle.low)).max(0.0);
|
||||||
*volume += candle.volume * overlap / reach;
|
*volume += candle.volume * overlap / span;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
traded
|
traded
|
||||||
@@ -250,4 +258,20 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!((bands[3].0, bands[3].1), (13.0, 14.0));
|
assert_eq!((bands[3].0, bands[3].1), (13.0, 14.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn narrow_and_flat_candles_count_all_their_volume() {
|
||||||
|
let narrow = Candle::new(Timestamp::UNIX_EPOCH, (10.2, 10.3, 10.1, 10.2), 100.0);
|
||||||
|
let flat = Candle::new(Timestamp::UNIX_EPOCH, (12.5, 12.5, 12.5, 12.5), 40.0);
|
||||||
|
let traded: Vec<f64> = profile(&[narrow, flat], (10.0, 14.0), 4)
|
||||||
|
.iter()
|
||||||
|
.map(|band| band.2)
|
||||||
|
.collect();
|
||||||
|
assert!(
|
||||||
|
(traded[0] - 100.0).abs() < 1e-9,
|
||||||
|
"{traded:?}: a narrow candle's whole volume"
|
||||||
|
);
|
||||||
|
assert_eq!(traded[2], 40.0, "a flat candle lands in its band");
|
||||||
|
assert!((traded.iter().sum::<f64>() - 140.0).abs() < 1e-9);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -225,7 +225,15 @@ pub(crate) fn labels(
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let x = axes.visible.x(from.0.min(to.0), (main.x, main.w));
|
let x = axes.visible.x(from.0.min(to.0), (main.x, main.w));
|
||||||
for (share, value) in Drawing::levels(from, to) {
|
if x > main.x + main.w {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let x = x.max(main.x);
|
||||||
|
let inside = |y: f32| y >= main.y && y <= main.y + main.h;
|
||||||
|
for (share, value) in Drawing::levels(from, to)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(_, value)| inside(price.at(*value)))
|
||||||
|
{
|
||||||
let words = format!(
|
let words = format!(
|
||||||
"{} {}",
|
"{} {}",
|
||||||
format::percent(share, 1, false),
|
format::percent(share, 1, false),
|
||||||
|
|||||||
+17
-5
@@ -19,6 +19,7 @@ use super::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
charts::{Linear, Rect, measure},
|
charts::{Linear, Rect, measure},
|
||||||
theme::ActiveTheme,
|
theme::ActiveTheme,
|
||||||
|
typography::format::system_zone,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// How a market chart draws its prices.
|
/// How a market chart draws its prices.
|
||||||
@@ -57,7 +58,7 @@ pub struct CandlestickChart {
|
|||||||
compare: Option<(SharedString, Rc<Vec<Candle>>)>,
|
compare: Option<(SharedString, Rc<Vec<Candle>>)>,
|
||||||
sync: Option<Entity<ChartSync>>,
|
sync: Option<Entity<ChartSync>>,
|
||||||
red_up: bool,
|
red_up: bool,
|
||||||
zone: TimeZone,
|
zone: Option<TimeZone>,
|
||||||
drawings: Vec<Drawing>,
|
drawings: Vec<Drawing>,
|
||||||
tool: Option<Tool>,
|
tool: Option<Tool>,
|
||||||
on_draw: Option<OnDraw>,
|
on_draw: Option<OnDraw>,
|
||||||
@@ -85,7 +86,7 @@ impl CandlestickChart {
|
|||||||
compare: None,
|
compare: None,
|
||||||
sync: None,
|
sync: None,
|
||||||
red_up: false,
|
red_up: false,
|
||||||
zone: TimeZone::system(),
|
zone: None,
|
||||||
drawings: Vec::new(),
|
drawings: Vec::new(),
|
||||||
tool: None,
|
tool: None,
|
||||||
on_draw: None,
|
on_draw: None,
|
||||||
@@ -181,7 +182,7 @@ impl CandlestickChart {
|
|||||||
|
|
||||||
/// The time zone its times read in; the system's unless set.
|
/// The time zone its times read in; the system's unless set.
|
||||||
pub fn zone(mut self, zone: TimeZone) -> Self {
|
pub fn zone(mut self, zone: TimeZone) -> Self {
|
||||||
self.zone = zone;
|
self.zone = Some(zone);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,6 +195,10 @@ impl Styled for CandlestickChart {
|
|||||||
|
|
||||||
impl RenderOnce for CandlestickChart {
|
impl RenderOnce for CandlestickChart {
|
||||||
fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let zone = self
|
||||||
|
.zone
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| system_zone("CandlestickChart"));
|
||||||
let sync = self.sync.clone();
|
let sync = self.sync.clone();
|
||||||
let stage: Entity<Stage> =
|
let stage: Entity<Stage> =
|
||||||
window.use_keyed_state((self.id.clone(), "stage"), cx, |_, cx| Stage {
|
window.use_keyed_state((self.id.clone(), "stage"), cx, |_, cx| Stage {
|
||||||
@@ -236,11 +241,18 @@ impl RenderOnce for CandlestickChart {
|
|||||||
{
|
{
|
||||||
visible.pan((total - seen) as f64, total)
|
visible.pan((total - seen) as f64, total)
|
||||||
}
|
}
|
||||||
Some(visible) => visible,
|
Some(visible) => visible.fitted(total),
|
||||||
None => Visible::latest(total, f64::from(main.w / pixels(sizes.candle)).max(1.0)),
|
None => Visible::latest(total, f64::from(main.w / pixels(sizes.candle)).max(1.0)),
|
||||||
};
|
};
|
||||||
if seen != total {
|
if seen != total {
|
||||||
|
log::debug!("market chart: {seen} candles to {total}");
|
||||||
stage.update(cx, |stage, _| stage.seen = total);
|
stage.update(cx, |stage, _| stage.seen = total);
|
||||||
|
if held.0.is_some() {
|
||||||
|
match &self.sync {
|
||||||
|
Some(sync) => sync.update(cx, |sync, _| sync.visible = Some(visible)),
|
||||||
|
None => stage.update(cx, |stage, _| stage.visible = Some(visible)),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let range = visible.range(total);
|
let range = visible.range(total);
|
||||||
let shown = Rc::new(if self.kind == ChartKind::HeikinAshi {
|
let shown = Rc::new(if self.kind == ChartKind::HeikinAshi {
|
||||||
@@ -362,7 +374,7 @@ impl RenderOnce for CandlestickChart {
|
|||||||
range: range.clone(),
|
range: range.clone(),
|
||||||
hover,
|
hover,
|
||||||
row,
|
row,
|
||||||
zone: self.zone.clone(),
|
zone: zone.clone(),
|
||||||
title: self.title.clone(),
|
title: self.title.clone(),
|
||||||
compare: self.compare.as_ref().map(|(name, _)| name.clone()),
|
compare: self.compare.as_ref().map(|(name, _)| name.clone()),
|
||||||
drawings: marks.clone(),
|
drawings: marks.clone(),
|
||||||
|
|||||||
+12
-5
@@ -17,7 +17,10 @@ use crate::{
|
|||||||
ChartTooltip, Linear, Rect, Tiles, anchored, at, finish, measure, place, ring, tracked,
|
ChartTooltip, Linear, Rect, Tiles, anchored, at, finish, measure, place, ring, tracked,
|
||||||
},
|
},
|
||||||
theme::{ActiveTheme, TextSize},
|
theme::{ActiveTheme, TextSize},
|
||||||
typography::{format, tabular},
|
typography::{
|
||||||
|
format::{self, system_zone},
|
||||||
|
tabular,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A mark on a price-only chart: its slot, the prices it spans, whether it rose, and its words.
|
/// A mark on a price-only chart: its slot, the prices it spans, whether it rose, and its words.
|
||||||
@@ -221,7 +224,7 @@ pub struct RenkoChart {
|
|||||||
id: ElementId,
|
id: ElementId,
|
||||||
candles: Rc<Vec<Candle>>,
|
candles: Rc<Vec<Candle>>,
|
||||||
size: f64,
|
size: f64,
|
||||||
zone: TimeZone,
|
zone: Option<TimeZone>,
|
||||||
red_up: bool,
|
red_up: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,7 +239,7 @@ impl RenkoChart {
|
|||||||
id: id.into(),
|
id: id.into(),
|
||||||
candles: candles.into(),
|
candles: candles.into(),
|
||||||
size,
|
size,
|
||||||
zone: TimeZone::system(),
|
zone: None,
|
||||||
red_up: false,
|
red_up: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,7 +251,7 @@ impl RenkoChart {
|
|||||||
|
|
||||||
/// The time zone its dates read in; the system's unless set.
|
/// The time zone its dates read in; the system's unless set.
|
||||||
pub fn zone(mut self, zone: TimeZone) -> Self {
|
pub fn zone(mut self, zone: TimeZone) -> Self {
|
||||||
self.zone = zone;
|
self.zone = Some(zone);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -261,13 +264,17 @@ impl Styled for RenkoChart {
|
|||||||
|
|
||||||
impl RenderOnce for RenkoChart {
|
impl RenderOnce for RenkoChart {
|
||||||
fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let zone = self
|
||||||
|
.zone
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| system_zone("RenkoChart"));
|
||||||
let marks = renko(&self.candles, self.size)
|
let marks = renko(&self.candles, self.size)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(slot, brick)| {
|
.map(|(slot, brick)| {
|
||||||
let day = self.candles[brick.at]
|
let day = self.candles[brick.at]
|
||||||
.time
|
.time
|
||||||
.to_zoned(self.zone.clone())
|
.to_zoned(zone.clone())
|
||||||
.strftime("%b %-d, %Y")
|
.strftime("%b %-d, %Y")
|
||||||
.to_string();
|
.to_string();
|
||||||
let reading = format!(
|
let reading = format!(
|
||||||
|
|||||||
@@ -26,6 +26,15 @@ impl Visible {
|
|||||||
.kept(total)
|
.kept(total)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Held to `total` candles after the data changed: no wider than they allow, and in reach of them.
|
||||||
|
pub(crate) fn fitted(self, total: usize) -> Self {
|
||||||
|
let count = self.count.clamp(
|
||||||
|
CLOSEST.min(total as f64).max(1.0),
|
||||||
|
(total as f64).max(1.0) * 1.1,
|
||||||
|
);
|
||||||
|
Self { count, ..self }.kept(total)
|
||||||
|
}
|
||||||
|
|
||||||
/// Kept to what `total` candles allow: never before the first, at most a quarter window past the last.
|
/// Kept to what `total` candles allow: never before the first, at most a quarter window past the last.
|
||||||
fn kept(self, total: usize) -> Self {
|
fn kept(self, total: usize) -> Self {
|
||||||
let last = (total as f64 - self.count * 0.75).max(0.0);
|
let last = (total as f64 - self.count * 0.75).max(0.0);
|
||||||
@@ -107,6 +116,21 @@ pub(crate) fn decimals(step: f64) -> usize {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_window_fits_data_that_shrank_under_it() {
|
||||||
|
let window = Visible {
|
||||||
|
start: 150.0,
|
||||||
|
count: 50.0,
|
||||||
|
};
|
||||||
|
let fitted = window.fitted(12);
|
||||||
|
assert!(fitted.count <= 12.0 * 1.1);
|
||||||
|
let range = fitted.range(12);
|
||||||
|
assert!(
|
||||||
|
!range.is_empty() && range.end == 12,
|
||||||
|
"{range:?} shows the newest candles"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_window_maps_candles_across_its_frame() {
|
fn a_window_maps_candles_across_its_frame() {
|
||||||
let window = Visible {
|
let window = Visible {
|
||||||
|
|||||||
+13
-8
@@ -9,12 +9,13 @@ use jiff::tz::TimeZone;
|
|||||||
use super::{
|
use super::{
|
||||||
book::{Side, Trade, cell, heading, level},
|
book::{Side, Trade, cell, heading, level},
|
||||||
quotes::{moves, price},
|
quotes::{moves, price},
|
||||||
|
stage::decimals,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
charts::compact,
|
charts::compact,
|
||||||
motion::Flash,
|
motion::Flash,
|
||||||
theme::{ActiveTheme, Density, TextSize},
|
theme::{ActiveTheme, Density, TextSize},
|
||||||
typography::tabular,
|
typography::{format::system_zone, 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.
|
/// 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.
|
||||||
@@ -25,7 +26,7 @@ pub struct TimeAndSales {
|
|||||||
large: f64,
|
large: f64,
|
||||||
rows: usize,
|
rows: usize,
|
||||||
places: usize,
|
places: usize,
|
||||||
zone: TimeZone,
|
zone: Option<TimeZone>,
|
||||||
red_up: bool,
|
red_up: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ impl TimeAndSales {
|
|||||||
large: f64::MAX,
|
large: f64::MAX,
|
||||||
rows: 12,
|
rows: 12,
|
||||||
places: 2,
|
places: 2,
|
||||||
zone: TimeZone::system(),
|
zone: None,
|
||||||
red_up: false,
|
red_up: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,7 +56,7 @@ impl TimeAndSales {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn zone(mut self, zone: TimeZone) -> Self {
|
pub fn zone(mut self, zone: TimeZone) -> Self {
|
||||||
self.zone = zone;
|
self.zone = Some(zone);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,10 +68,13 @@ impl TimeAndSales {
|
|||||||
|
|
||||||
impl RenderOnce for TimeAndSales {
|
impl RenderOnce for TimeAndSales {
|
||||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let zone = self
|
||||||
|
.zone
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| system_zone("TimeAndSales"));
|
||||||
let (rise, fall) = moves(self.red_up, cx);
|
let (rise, fall) = moves(self.red_up, cx);
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let colors = theme.colors.clone();
|
let colors = theme.colors.clone();
|
||||||
let newest = self.trades.len();
|
|
||||||
let rows = self
|
let rows = self
|
||||||
.trades
|
.trades
|
||||||
.iter()
|
.iter()
|
||||||
@@ -81,7 +85,7 @@ impl RenderOnce for TimeAndSales {
|
|||||||
let ink = if trade.side == Side::Buy { rise } else { fall };
|
let ink = if trade.side == Side::Buy { rise } else { fall };
|
||||||
let time = trade
|
let time = trade
|
||||||
.time
|
.time
|
||||||
.to_zoned(self.zone.clone())
|
.to_zoned(zone.clone())
|
||||||
.strftime("%H:%M:%S")
|
.strftime("%H:%M:%S")
|
||||||
.to_string();
|
.to_string();
|
||||||
let row = div()
|
let row = div()
|
||||||
@@ -104,7 +108,7 @@ impl RenderOnce for TimeAndSales {
|
|||||||
.child(cell(price(trade.price, self.places), ink))
|
.child(cell(price(trade.price, self.places), ink))
|
||||||
.child(cell(compact(trade.size), colors.fg));
|
.child(cell(compact(trade.size), colors.fg));
|
||||||
if ix == 0 {
|
if ix == 0 {
|
||||||
Flash::new((self.id.clone(), "newest"), newest)
|
Flash::new((self.id.clone(), "newest"), *trade)
|
||||||
.child(row)
|
.child(row)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
} else {
|
} else {
|
||||||
@@ -192,6 +196,7 @@ fn rungs(last: f64, tick: f64, rows: usize) -> Vec<f64> {
|
|||||||
|
|
||||||
impl RenderOnce for DomLadder {
|
impl RenderOnce for DomLadder {
|
||||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let places = self.places.max(decimals(self.tick));
|
||||||
let (rise, fall) = moves(self.red_up, cx);
|
let (rise, fall) = moves(self.red_up, cx);
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let colors = theme.colors.clone();
|
let colors = theme.colors.clone();
|
||||||
@@ -255,7 +260,7 @@ impl RenderOnce for DomLadder {
|
|||||||
.font_weight(FontWeight::SEMIBOLD)
|
.font_weight(FontWeight::SEMIBOLD)
|
||||||
})
|
})
|
||||||
.when(!marked, |price| price.text_color(colors.fg_muted))
|
.when(!marked, |price| price.text_color(colors.fg_muted))
|
||||||
.child(price(at, self.places)),
|
.child(price(at, places)),
|
||||||
)
|
)
|
||||||
.child(side_cell(ask, Side::Sell, fall))
|
.child(side_cell(ask, Side::Sell, fall))
|
||||||
});
|
});
|
||||||
|
|||||||
+13
-5
@@ -13,7 +13,11 @@ use crate::{
|
|||||||
forms::{Combobox, NumberInput, TextInput},
|
forms::{Combobox, NumberInput, TextInput},
|
||||||
primitives::{Icon, IconName},
|
primitives::{Icon, IconName},
|
||||||
theme::{ActiveTheme, IconSize, Radius, TextSize},
|
theme::{ActiveTheme, IconSize, Radius, TextSize},
|
||||||
typography::{Ellipsis, format, tabular},
|
typography::{
|
||||||
|
Ellipsis,
|
||||||
|
format::{self, system_zone},
|
||||||
|
tabular,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type OnConvert = Rc<dyn Fn(f64, SharedString, SharedString, &mut Window, &mut App)>;
|
type OnConvert = Rc<dyn Fn(f64, SharedString, SharedString, &mut Window, &mut App)>;
|
||||||
@@ -309,7 +313,7 @@ pub struct Transfer {
|
|||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct TransactionList {
|
pub struct TransactionList {
|
||||||
transfers: Vec<Transfer>,
|
transfers: Vec<Transfer>,
|
||||||
zone: TimeZone,
|
zone: Option<TimeZone>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TransactionList {
|
impl TransactionList {
|
||||||
@@ -318,13 +322,13 @@ impl TransactionList {
|
|||||||
transfers.sort_by_key(|transfer| Reverse(transfer.time));
|
transfers.sort_by_key(|transfer| Reverse(transfer.time));
|
||||||
Self {
|
Self {
|
||||||
transfers,
|
transfers,
|
||||||
zone: TimeZone::system(),
|
zone: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The time zone its times read in; the system's unless set.
|
/// The time zone its times read in; the system's unless set.
|
||||||
pub fn zone(mut self, zone: TimeZone) -> Self {
|
pub fn zone(mut self, zone: TimeZone) -> Self {
|
||||||
self.zone = zone;
|
self.zone = Some(zone);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -342,6 +346,10 @@ fn amount(value: f64, unit: &str) -> String {
|
|||||||
|
|
||||||
impl RenderOnce for TransactionList {
|
impl RenderOnce for TransactionList {
|
||||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let zone = self
|
||||||
|
.zone
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| system_zone("TransactionList"));
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let colors = theme.colors.clone();
|
let colors = theme.colors.clone();
|
||||||
div()
|
div()
|
||||||
@@ -397,7 +405,7 @@ impl RenderOnce for TransactionList {
|
|||||||
.child(
|
.child(
|
||||||
transfer
|
transfer
|
||||||
.time
|
.time
|
||||||
.to_zoned(self.zone.clone())
|
.to_zoned(zone.clone())
|
||||||
.strftime("%b %-d, %H:%M")
|
.strftime("%b %-d, %H:%M")
|
||||||
.to_string(),
|
.to_string(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ mod zone;
|
|||||||
use jiff::{
|
use jiff::{
|
||||||
Timestamp, Zoned,
|
Timestamp, Zoned,
|
||||||
civil::{Date, Time},
|
civil::{Date, Time},
|
||||||
tz::TimeZone,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::typography::format::system_zone;
|
||||||
|
|
||||||
pub use calendar::Calendar;
|
pub use calendar::Calendar;
|
||||||
pub use cron::CronEditor;
|
pub use cron::CronEditor;
|
||||||
pub use period::{MonthPicker, QuarterPicker, YearPicker};
|
pub use period::{MonthPicker, QuarterPicker, YearPicker};
|
||||||
@@ -26,10 +27,7 @@ pub use zone::TimezoneSelect;
|
|||||||
|
|
||||||
/// The system's clock in its own zone. An unknown zone stops here, not in UTC.
|
/// The system's clock in its own zone. An unknown zone stops here, not in UTC.
|
||||||
pub(crate) fn zoned_now() -> Zoned {
|
pub(crate) fn zoned_now() -> Zoned {
|
||||||
let zone = TimeZone::try_system().unwrap_or_else(|error| {
|
Timestamp::now().to_zoned(system_zone("date pickers"))
|
||||||
panic!("the system time zone is unknown; pass a fixed date: {error}")
|
|
||||||
});
|
|
||||||
Timestamp::now().to_zoned(zone)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A day as "Sep 25, 2026".
|
/// A day as "Sep 25, 2026".
|
||||||
|
|||||||
+30
-12
@@ -102,6 +102,7 @@ fn commit(state: &Entity<Numeric>, next: f64, window: &mut Window, cx: &mut App)
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
let value = settle(next, min, max, precision);
|
let value = settle(next, min, max, precision);
|
||||||
|
state.update(cx, |numeric, _| numeric.value = value);
|
||||||
show(&input, on_change.as_ref(), value, precision, window, cx);
|
show(&input, on_change.as_ref(), value, precision, window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,19 +113,36 @@ fn numeric(id: &ElementId, window: &mut Window, cx: &mut App) -> Entity<Numeric>
|
|||||||
.filter(|ch| ch.is_ascii_digit() || matches!(ch, '.' | ',' | '-' | format::MINUS))
|
.filter(|ch| ch.is_ascii_digit() || matches!(ch, '.' | ',' | '-' | format::MINUS))
|
||||||
});
|
});
|
||||||
let events = cx.subscribe_in(&input, window, |numeric, input, event, window, cx| {
|
let events = cx.subscribe_in(&input, window, |numeric, input, event, window, cx| {
|
||||||
if !matches!(event, InputEvent::Blur | InputEvent::Submit) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let (min, max, _, precision) = numeric.limits;
|
let (min, max, _, precision) = numeric.limits;
|
||||||
let value = settle(numeric.current(cx), min, max, precision);
|
match event {
|
||||||
show(
|
InputEvent::Blur | InputEvent::Submit => {
|
||||||
input,
|
let value = settle(numeric.current(cx), min, max, precision);
|
||||||
numeric.on_change.as_ref(),
|
numeric.value = value;
|
||||||
value,
|
show(
|
||||||
precision,
|
input,
|
||||||
window,
|
numeric.on_change.as_ref(),
|
||||||
cx,
|
value,
|
||||||
);
|
precision,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
InputEvent::Changed if input.read(cx).focus().is_focused(window) => {
|
||||||
|
let Some(typed) = parse(input.read(cx).text()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let settled = typed == settle(typed, min, max, precision);
|
||||||
|
if typed == numeric.value || !settled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log::info!("number input: typed {typed}");
|
||||||
|
numeric.value = typed;
|
||||||
|
if let Some(on_change) = numeric.on_change.clone() {
|
||||||
|
on_change(typed, window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
Numeric {
|
Numeric {
|
||||||
input,
|
input,
|
||||||
|
|||||||
@@ -183,6 +183,26 @@ fn stepping_starts_from_the_typed_number(cx: &mut TestAppContext) {
|
|||||||
assert_eq!(view.read_with(cx, |view, _| view.value), 21.0);
|
assert_eq!(view.read_with(cx, |view, _| view.value), 21.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn a_typed_number_reaches_the_owner_before_any_blur(cx: &mut TestAppContext) {
|
||||||
|
setup(cx);
|
||||||
|
let (view, cx) = cx.add_window_view(|_, _| Stepped { value: 100.0 });
|
||||||
|
cx.update(|window, _| window.focus_next());
|
||||||
|
cx.simulate_keystrokes("cmd-a");
|
||||||
|
cx.simulate_input("250");
|
||||||
|
assert_eq!(
|
||||||
|
view.read_with(cx, |view, _| view.value),
|
||||||
|
250.0,
|
||||||
|
"a press elsewhere reads the typed number"
|
||||||
|
);
|
||||||
|
cx.simulate_input(".");
|
||||||
|
assert_eq!(
|
||||||
|
view.read_with(cx, |view, _| view.value),
|
||||||
|
250.0,
|
||||||
|
"a half-typed number waits"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
struct Pin {
|
struct Pin {
|
||||||
code: String,
|
code: String,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,6 +173,13 @@ pub fn relative(then: Timestamp, now: Timestamp) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The system's time zone for `user`. An unknown zone stops here rather than reading as UTC.
|
||||||
|
pub(crate) fn system_zone(user: &str) -> TimeZone {
|
||||||
|
TimeZone::try_system().unwrap_or_else(|error| {
|
||||||
|
panic!("{user}: the system time zone is unknown ({error}); pass a zone")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// strftime in `zone`; a bad pattern is an error.
|
/// strftime in `zone`; a bad pattern is an error.
|
||||||
pub fn datetime(at: Timestamp, zone: &TimeZone, pattern: &str) -> Result<String, jiff::Error> {
|
pub fn datetime(at: Timestamp, zone: &TimeZone, pattern: &str) -> Result<String, jiff::Error> {
|
||||||
strtime::format(pattern, &at.to_zoned(zone.clone()))
|
strtime::format(pattern, &at.to_zoned(zone.clone()))
|
||||||
|
|||||||
@@ -229,9 +229,7 @@ impl RenderOnce for DateTimeText {
|
|||||||
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
|
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
|
||||||
let zone = match self.zone {
|
let zone = match self.zone {
|
||||||
Some(zone) => zone,
|
Some(zone) => zone,
|
||||||
None => TimeZone::try_system().unwrap_or_else(|error| {
|
None => format::system_zone("DateTimeText"),
|
||||||
panic!("DateTimeText: system time zone unknown ({error}); pass .zone()")
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
let text = format::datetime(self.at, &zone, self.pattern)
|
let text = format::datetime(self.at, &zone, self.pattern)
|
||||||
.unwrap_or_else(|error| panic!("DateTimeText pattern {:?}: {error}", self.pattern));
|
.unwrap_or_else(|error| panic!("DateTimeText pattern {:?}: {error}", self.pattern));
|
||||||
|
|||||||
Reference in new issue
Block a user