Explorar o código

add size filtering & sync in between panes

Berke hai 1 ano
pai
achega
c78b5383a8
Modificáronse 2 ficheiros con 214 adicións e 15 borrados
  1. 6 1
      src/charts/heatmap.rs
  2. 208 14
      src/main.rs

+ 6 - 1
src/charts/heatmap.rs

@@ -19,6 +19,7 @@ pub struct LineChart {
     cache: Cache,
     data_points: VecDeque<(DateTime<Utc>, f32, f32, bool)>,
     depth: VecDeque<(DateTime<Utc>, Vec<(f32, f32)>, Vec<(f32, f32)>)>,
+    size_filter: f32,
 }
 impl LineChart {
     pub fn new() -> Self {
@@ -26,8 +27,12 @@ impl LineChart {
             cache: Cache::new(),
             data_points: VecDeque::new(),
             depth: VecDeque::new(),
+            size_filter: 0.0,
         }
     }
+    pub fn set_size_filter(&mut self, size_filter: f32) {
+        self.size_filter = size_filter;
+    }
 
     pub fn update(&mut self, depth_update: u64, mut trades_buffer: Vec<Trade>, bids: Vec<(f32, f32)>, asks: Vec<(f32, f32)>) {
         let aggregate_time = 100; 
@@ -97,7 +102,7 @@ impl Chart<Message> for LineChart {
             let mut y_min = f32::MAX;
             let mut y_max = f32::MIN;
             let recent_data_points: Vec<_> = self.data_points.iter().filter_map(|&(time, price, qty, bool)| {
-                if time >= oldest_time && time <= newest_time {
+                if time >= oldest_time && time <= newest_time && price*qty > self.size_filter {
                     Some((time, price, qty, bool))
                 } else {
                     None

+ 208 - 14
src/main.rs

@@ -1,3 +1,5 @@
+#![windows_subsystem = "windows"]
+
 mod data_providers;
 use data_providers::binance::{user_data, market_data};
 mod charts;
@@ -6,12 +8,13 @@ use charts::{heatmap, candlesticks};
 use crate::heatmap::LineChart;
 use crate::candlesticks::CandlestickChart;
 
+use core::sync;
 use std::cell::RefCell;
 use chrono::{NaiveDateTime, DateTime, Utc};
 use iced::{
-    alignment, executor, font, theme, widget::{
-        button, pick_list, text_input, tooltip, Column, Container, Row, Space, Text
-    }, Alignment, Application, Command, Element, Font, Length, Renderer, Settings, Size, Subscription, Theme, Color
+    advanced::overlay, alignment, executor, font, theme, time, widget::{
+        button, checkbox, pick_list, text_input, tooltip, Column, Container, Row, Slider, Space, Text
+    }, Alignment, Application, Color, Command, Element, Font, Length, Renderer, Settings, Size, Subscription, Theme
 };
 
 use iced::widget::pane_grid::{self, PaneGrid};
@@ -23,7 +26,7 @@ use futures::TryFutureExt;
 use plotters_iced::sample::lttb::DataPoint;
 
 use iced_aw::menu::{Item, Menu};
-use iced_aw::{menu_bar, menu_items};
+use iced_aw::{menu_bar, menu_items, modal, Card};
 
 use std::collections::HashMap;
 
@@ -77,6 +80,7 @@ enum Icon {
     ResizeSmall,
     Close,
     Layout,
+    Cog,
 }
 
 impl From<Icon> for char {
@@ -88,6 +92,7 @@ impl From<Icon> for char {
             Icon::ResizeSmall => '\u{E803}',
             Icon::Close => '\u{E804}',
             Icon::Layout => '\u{E805}',
+            Icon::Cog => '\u{E806}',
         }
     }
 }
@@ -124,11 +129,15 @@ pub enum PaneId {
 #[derive(Debug, Clone, Copy)]
 struct Pane {
     id: PaneId,
+    show_modal: bool,
 }
 
 impl Pane {
     fn new(id: PaneId) -> Self {
-        Self { id }
+        Self { 
+            id,
+            show_modal: false,
+        }
     }
 }
 
@@ -186,6 +195,14 @@ pub enum Message {
 
     // Font
     FontLoaded(Result<(), font::Error>),
+
+    // Modal
+    OpenModal(pane_grid::Pane),
+    CloseModal,
+
+    // Slider
+    SliderChanged(PaneId, f32),
+    SyncWithHeatmap(bool),
 }
 
 struct State {
@@ -230,6 +247,10 @@ struct State {
     footer_enabled: bool,
     min_width_enabled: bool,
     account_info_usdt: Option<user_data::FetchedBalance>,
+
+    size_filter_timesales: f32,
+    size_filter_heatmap: f32,
+    sync_heatmap: bool,
 }
 
 impl Application for State {
@@ -248,6 +269,10 @@ impl Application for State {
         panes_open.insert(PaneId::TradePanel, false);
         (
             Self { 
+                size_filter_timesales: 0.0,
+                size_filter_heatmap: 0.0,
+                sync_heatmap: false,
+
                 trades_chart: None,
                 candlestick_chart: None,
                 time_and_sales: None,
@@ -820,6 +845,52 @@ impl Application for State {
                 dbg!("Font loaded");
                 Command::none()
             },
+
+            Message::OpenModal(pane) => {
+                self.panes.get_mut(pane).map(|pane| {
+                    pane.show_modal = true;
+                });
+                Command::none()
+            },
+            Message::CloseModal => {
+                for pane in self.panes.panes.values_mut() {
+                    pane.show_modal = false;
+                }
+                Command::none()
+            },
+
+            Message::SliderChanged(pane_id, value) => {
+                if pane_id == PaneId::TimeAndSales {
+                    self.size_filter_timesales = value;
+                    if self.sync_heatmap {
+                        self.size_filter_heatmap = value;
+                    }
+                } else if pane_id == PaneId::HeatmapChart {
+                    self.size_filter_heatmap = value;
+                    self.sync_heatmap = false;
+                }
+
+                self.trades_chart.as_mut().map(|chart| {
+                    chart.set_size_filter(self.size_filter_heatmap);
+                });
+                self.time_and_sales.as_mut().map(|time_and_sales| {
+                    time_and_sales.set_size_filter(self.size_filter_timesales);
+                });
+
+                Command::none()
+            },
+            Message::SyncWithHeatmap(sync) => {
+                self.sync_heatmap = sync;
+            
+                if sync {
+                    self.size_filter_heatmap = self.size_filter_timesales;
+                    self.trades_chart.as_mut().map(|chart| {
+                        chart.set_size_filter(self.size_filter_heatmap);
+                    });
+                }
+            
+                Command::none()
+            },
         }
     }
 
@@ -833,6 +904,10 @@ impl Application for State {
             let content: pane_grid::Content<'_, Message, _, Renderer> = pane_grid::Content::new(responsive(move |size| {
                 view_content(
                     pane.id, 
+                    pane.show_modal,
+                    &self.size_filter_heatmap,
+                    &self.size_filter_timesales,
+                    self.sync_heatmap,
                     total_panes, 
                     size, 
                     &self.time_and_sales,
@@ -1037,6 +1112,10 @@ fn base_button<'a>(
 
 fn view_content<'a, 'b: 'a>(
     pane_id: PaneId,
+    show_modal: bool,
+    size_filter_heatmap: &'a f32,
+    size_filter_timesales: &'a f32,
+    sync_heatmap: bool,
     _total_panes: usize,
     _size: Size,
     time_and_sales: &'a Option<TimeAndSales>,
@@ -1058,10 +1137,116 @@ fn view_content<'a, 'b: 'a>(
     table_active_tab: &'b usize,
     account_info_usdt: &'b Option<user_data::FetchedBalance>,
 ) -> Element<'a, Message> {
-    let content = match pane_id {
-        PaneId::HeatmapChart => trades_chart.as_ref().map(LineChart::view).unwrap_or_else(|| Text::new("No data").into()),
-        PaneId::CandlestickChart => candlestick_chart.as_ref().map(CandlestickChart::view).unwrap_or_else(|| Text::new("No data").into()),
-        PaneId::TimeAndSales => time_and_sales.as_ref().map(TimeAndSales::view).unwrap_or_else(|| Text::new("No data").into()),
+    let content: Element<Message, Theme, Renderer> = match pane_id {
+        PaneId::HeatmapChart => {
+            let underlay = trades_chart.as_ref().map(LineChart::view).unwrap_or_else(|| Text::new("No data").into());
+            let overlay = if show_modal {
+                Some(
+                    Card::new(
+                        Text::new("Heatmap Chart -> Settings"),
+                        Column::new()
+                            .push(Text::new("Size Filtering"))
+                            .push(
+                                Slider::new(0.0..=50000.0, *size_filter_heatmap, move |value| Message::SliderChanged(PaneId::HeatmapChart, value))
+                                    .step(500.0)
+                            ),
+                    )
+                    .foot(
+                        Row::new()
+                            .spacing(10)
+                            .padding(5)
+                            .width(Length::Fill)
+                            .push(
+                                Text::new(format!("${}", size_filter_heatmap)).size(16)
+                            )
+                    )
+                    .max_width(500.0)
+                    .on_close(Message::CloseModal)
+                )
+            } else {
+                None
+            };
+
+            modal(underlay, overlay)
+                .backdrop(Message::CloseModal)
+                .on_esc(Message::CloseModal)
+                .align_y(alignment::Vertical::Center)
+                .into()
+        }, 
+        
+        PaneId::CandlestickChart => { 
+            let underlay = candlestick_chart.as_ref().map(CandlestickChart::view).unwrap_or_else(|| Text::new("No data").into());
+            let overlay = if show_modal {
+                Some(
+                    Card::new(
+                        Text::new("Candlestick Chart -> Settings"),
+                        Column::new()
+                            .push(Text::new("Test"))
+                    )
+                    .foot(
+                        Row::new()
+                            .spacing(10)
+                            .padding(5)
+                            .width(Length::Fill)
+                            .push(
+                                Text::new("Footer").size(16)
+                            )
+                    )
+                    .max_width(500.0)
+                    .on_close(Message::CloseModal)
+                )
+            } else {
+                None
+            };
+
+            modal(underlay, overlay)
+                .backdrop(Message::CloseModal)
+                .on_esc(Message::CloseModal)
+                .align_y(alignment::Vertical::Center)
+                .into()
+        },
+        
+        PaneId::TimeAndSales => { 
+            let underlay = time_and_sales.as_ref().map(TimeAndSales::view).unwrap_or_else(|| Text::new("No data").into()); 
+            let overlay = if show_modal {
+                Some(
+                    Card::new(
+                        Text::new("Time & Sales -> Settings"),
+                        Column::new()
+                            .push(Text::new("Size Filtering"))
+                            .push(
+                                Slider::new(0.0..=50000.0, *size_filter_timesales, move |value| Message::SliderChanged(PaneId::TimeAndSales, value))
+                                    .step(500.0)
+                            ),
+                    )
+                    .foot(
+                        Row::new()
+                            .spacing(10)
+                            .padding(5)
+                            .width(Length::Fill)
+                            .push(
+                                Text::new(format!("${}", size_filter_timesales)).size(16)
+                            )
+                            .push(Space::with_width(Length::Fill))
+                            .push(
+                                checkbox("Sync Heatmap with", sync_heatmap)
+                                    .on_toggle(Message::SyncWithHeatmap)
+                            )
+                    )
+                    .max_width(500.0)
+                    .on_close(Message::CloseModal)
+                )
+            } else {
+                None
+            };
+
+            modal(underlay, overlay)
+                .backdrop(Message::CloseModal)
+                .on_esc(Message::CloseModal)
+                .align_y(alignment::Vertical::Center)
+                .into()
+        },  
+        
         PaneId::TradePanel => if account_info_usdt.is_none() {
             Text::new("No account info").into()
         } else {
@@ -1217,6 +1402,7 @@ fn view_controls<'a>(
             (Icon::ResizeFull, Message::Maximize(pane))
         };
         let buttons = vec![
+            (container(text(char::from(Icon::Cog).to_string()).font(ICON).size(14)).width(25).center_x(), Message::OpenModal(pane)),
             (container(text(char::from(icon).to_string()).font(ICON).size(14)).width(25).center_x(), message),
             (container(text(char::from(Icon::Close).to_string()).font(ICON).size(14)).width(25).center_x(), Message::Close(pane)),
         ];
@@ -1241,13 +1427,19 @@ struct ConvertedTrade {
 }
 struct TimeAndSales {
     recent_trades: Vec<ConvertedTrade>,
+    size_filter: f32,
 }
 impl TimeAndSales {
     fn new() -> Self {
         Self {
             recent_trades: Vec::new(),
+            size_filter: 0.0,
         }
     }
+    fn set_size_filter(&mut self, value: f32) {
+        self.size_filter = value;
+    }
+
     fn update(&mut self, trades_buffer: &Vec<Trade>) {
         for trade in trades_buffer {
             let trade_time = NaiveDateTime::from_timestamp(trade.time as i64 / 1000, (trade.time % 1000) as u32 * 1_000_000);
@@ -1260,8 +1452,8 @@ impl TimeAndSales {
             self.recent_trades.push(converted_trade);
         }
 
-        if self.recent_trades.len() > 300 {
-            let drain_to = self.recent_trades.len() - 300;
+        if self.recent_trades.len() > 2000 {
+            let drain_to = self.recent_trades.len() - 2000;
             self.recent_trades.drain(0..drain_to);
         }
     }
@@ -1270,12 +1462,14 @@ impl TimeAndSales {
             .height(Length::Fill)
             .padding(10);
 
-        let max_qty = self.recent_trades.iter().map(|trade| trade.qty).fold(0.0, f32::max);
+        let filtered_trades: Vec<&ConvertedTrade> = self.recent_trades.iter().filter(|trade| (trade.qty*trade.price) >= self.size_filter).collect();
+
+        let max_qty = filtered_trades.iter().map(|trade| trade.qty).fold(0.0, f32::max);
     
-        if self.recent_trades.is_empty() {
+        if filtered_trades.is_empty() {
             trades_column = trades_column.push(Text::new("No trades").size(16));
         } else {
-            for trade in self.recent_trades.iter().rev().take(80) {
+            for trade in filtered_trades.iter().rev().take(80) {
                 let trade_row = Row::new()
                     .push(
                         container(Text::new(format!("{}", trade.time.format("%M:%S.%3f"))).size(14))