Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions data/src/panel/timeandsales.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,79 @@ use crate::util::ok_or_default;

const TRADE_RETENTION_MS: u64 = 120_000;

/// Time display format for trades in the Time & Sales panel
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum TimeFormat {
/// mm:ss.fff (e.g., 45:23.123)
#[default]
MinSecMs,
/// hh:mm:ss (e.g., 14:45:23)
HourMinSec,
/// hh:mm:ss.fff (e.g., 14:45:23.123)
HourMinSecMs,
}

impl TimeFormat {
pub const ALL: [TimeFormat; 3] = [
TimeFormat::MinSecMs,
TimeFormat::HourMinSec,
TimeFormat::HourMinSecMs,
];

/// Returns the chrono format string for this time format
pub fn format_str(&self) -> &'static str {
match self {
TimeFormat::MinSecMs => "%M:%S%.3f",
TimeFormat::HourMinSec => "%H:%M:%S",
TimeFormat::HourMinSecMs => "%H:%M:%S%.3f",
}
}

/// Formats a timestamp in milliseconds according to the time format and timezone
pub fn format_timestamp(&self, ts_ms: u64, timezone: crate::UserTimezone) -> String {
use chrono::DateTime;

let Some(datetime) = DateTime::from_timestamp(
ts_ms as i64 / 1000,
(ts_ms % 1000) as u32 * 1_000_000,
) else {
return String::new();
};

let format_str = self.format_str();

match timezone {
crate::UserTimezone::Local => datetime
.with_timezone(&chrono::Local)
.format(format_str)
.to_string(),
crate::UserTimezone::Utc => datetime
.with_timezone(&chrono::Utc)
.format(format_str)
.to_string(),
}
}
}

impl std::fmt::Display for TimeFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TimeFormat::MinSecMs => write!(f, "mm:ss.fff"),
TimeFormat::HourMinSec => write!(f, "hh:mm:ss"),
TimeFormat::HourMinSecMs => write!(f, "hh:mm:ss.fff"),
}
}
}

#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct Config {
pub trade_size_filter: f32,
#[serde(default = "default_buffer_filter")]
pub trade_retention: Duration,
#[serde(deserialize_with = "ok_or_default", default)]
pub stacked_bar: Option<StackedBar>,
#[serde(deserialize_with = "ok_or_default", default)]
pub time_format: TimeFormat,
}

impl Default for Config {
Expand All @@ -22,6 +88,7 @@ impl Default for Config {
trade_size_filter: 0.0,
trade_retention: Duration::from_millis(TRADE_RETENTION_MS),
stacked_bar: StackedBar::Compact(StackedBarRatio::default()).into(),
time_format: TimeFormat::default(),
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,11 @@ impl Flowsurface {
}
Message::Tick(now) => {
let main_window_id = self.main_window.id;
let timezone = self.timezone;

return self
.active_dashboard_mut()
.tick(now, main_window_id)
.tick(now, timezone, main_window_id)
.map(move |msg| Message::Dashboard {
layout_id: None,
event: msg,
Expand Down
22 changes: 21 additions & 1 deletion src/modal/pane/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use data::chart::{
};
use data::layout::pane::VisualConfig;
use data::panel::ladder;
use data::panel::timeandsales::{StackedBar, StackedBarRatio};
use data::panel::timeandsales::{StackedBar, StackedBarRatio, TimeFormat};
use data::util::format_with_commas;

use iced::widget::{checkbox, space};
Expand Down Expand Up @@ -428,9 +428,29 @@ pub fn timesales_cfg_view<'a>(
.into()
};

let time_format_column = {
let time_format_picker = pick_list(
TimeFormat::ALL,
Some(cfg.time_format),
move |new_format| {
Message::VisualConfigChanged(
pane,
VisualConfig::TimeAndSales(timeandsales::Config {
time_format: new_format,
..cfg
}),
false,
)
},
);

column![text("Time format").size(14), time_format_picker].spacing(8)
};

let content = split_column![
trade_size_column,
history_column,
time_format_column,
stacked_bar,
row![space::horizontal(), sync_all_button(pane, VisualConfig::TimeAndSales(cfg))],
; spacing = 12, align_x = Alignment::Start
Expand Down
4 changes: 2 additions & 2 deletions src/screen/dashboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1002,12 +1002,12 @@ impl Dashboard {
});
}

pub fn tick(&mut self, now: Instant, main_window: window::Id) -> Task<Message> {
pub fn tick(&mut self, now: Instant, timezone: UserTimezone, main_window: window::Id) -> Task<Message> {
let mut tasks = vec![];
let layout_id = self.layout_id;

self.iter_all_panes_mut(main_window)
.for_each(|(_window_id, _pane, state)| match state.tick(now) {
.for_each(|(_window_id, _pane, state)| match state.tick(now, timezone) {
Some(pane::Action::Chart(action)) => match action {
chart::Action::ErrorOccurred(err) => {
state.status = pane::Status::Ready;
Expand Down
41 changes: 39 additions & 2 deletions src/screen/dashboard/pane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1373,8 +1373,40 @@ impl State {
where
F: FnOnce() -> Element<'a, Message>,
{
// Create watermark layer showing panel type
let watermark: Element<'a, Message> = {
let label = match self.content.kind() {
ContentKind::HeatmapChart => "Heatmap",
ContentKind::CandlestickChart => "Candlestick",
ContentKind::FootprintChart => "Footprint",
ContentKind::ComparisonChart => "Comparison",
ContentKind::TimeAndSales => "Time & Sales",
ContentKind::Ladder => "DOM/Ladder",
ContentKind::Starter => "",
};

if label.is_empty() {
iced::widget::Space::new().into()
} else {
center(
text(label)
.size(42)
.color(iced::Color { a: 0.06, ..iced::Color::WHITE })
)
.width(Length::Fill)
.height(Length::Fill)
.into()
}
};

// Stack watermark behind base, then add toast manager
let base_with_watermark: Element<'a, Message> = iced::widget::stack![watermark, base]
.width(Length::Fill)
.height(Length::Fill)
.into();

let base =
widget::toast::Manager::new(base, &self.notifications, Alignment::End, move |msg| {
widget::toast::Manager::new(base_with_watermark, &self.notifications, Alignment::End, move |msg| {
Message::PaneEvent(pane, Event::DeleteNotification(msg))
})
.into();
Expand Down Expand Up @@ -1519,10 +1551,15 @@ impl State {
self.content.last_tick()
}

pub fn tick(&mut self, now: Instant) -> Option<Action> {
pub fn tick(&mut self, now: Instant, timezone: UserTimezone) -> Option<Action> {
let invalidate_interval: Option<u64> = self.update_interval();
let last_tick: Option<Instant> = self.last_tick();

// Update timezone for TimeAndSales panel
if let Content::TimeAndSales(Some(panel)) = &mut self.content {
panel.set_timezone(timezone);
}

if let Some(streams) = self.streams.waiting_to_resolve()
&& !streams.is_empty()
{
Expand Down
14 changes: 12 additions & 2 deletions src/screen/dashboard/panel/timeandsales.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use data::config::theme::{darken, lighten};
pub use data::panel::timeandsales::Config;
use data::panel::timeandsales::{HistAgg, StackedBar, StackedBarRatio, TradeDisplay, TradeEntry};
use exchange::{TickerInfo, Trade, volume_size_unit};
use data::UserTimezone;

use iced::widget::canvas::{self, Text};
use iced::{Alignment, Event, Point, Rectangle, Renderer, Size, Theme, mouse};
Expand Down Expand Up @@ -80,6 +81,7 @@ pub struct TimeAndSales {
cache: canvas::Cache,
last_tick: Instant,
scroll_offset: f32,
timezone: UserTimezone,
}

impl TimeAndSales {
Expand All @@ -95,6 +97,14 @@ impl TimeAndSales {
cache: canvas::Cache::default(),
last_tick: Instant::now(),
scroll_offset: 0.0,
timezone: UserTimezone::default(),
}
}

pub fn set_timezone(&mut self, timezone: UserTimezone) {
if self.timezone != timezone {
self.timezone = timezone;
self.cache.clear();
}
}

Expand All @@ -118,7 +128,7 @@ impl TimeAndSales {
(trade_time_ms % 1000) as u32 * 1_000_000,
) {
let trade_display = TradeDisplay {
time_str: trade_time.format("%M:%S.%3f").to_string(),
time_str: trade_time.format(self.config.time_format.format_str()).to_string(),
price: trade.price,
qty: trade.qty,
is_sell: trade.is_sell,
Expand Down Expand Up @@ -520,7 +530,7 @@ impl canvas::Program<Message> for TimeAndSales {
);

let trade_time = create_text(
trade.time_str.clone(),
self.config.time_format.format_timestamp(entry.ts_ms, self.timezone),
Point {
x: row_width * 0.1,
y: y_position,
Expand Down