Browse Source

feat ticksize grouping on heatmap chart, +code cleanup and chore

Berke 1 year ago
parent
commit
a65d272392
3 changed files with 384 additions and 149 deletions
  1. 105 51
      src/charts/heatmap.rs
  2. 201 98
      src/main.rs
  3. 78 0
      src/screen/dashboard.rs

+ 105 - 51
src/charts/heatmap.rs

@@ -9,9 +9,22 @@ use crate::data_providers::{Depth, Trade};
 
 use super::{Chart, CommonChartData, Message, chart_button, Interaction, AxisLabelYCanvas, AxisLabelXCanvas};
 
+#[derive(Debug, Clone, Default)]
+pub struct GroupedDepth {
+    pub time: i64,
+    pub bids: BTreeMap<i64, f32>, // price -> quantity
+    pub asks: BTreeMap<i64, f32>, // price -> quantity
+}
+pub struct GroupedTrade {
+    pub time: i64,
+    pub is_sell: bool,
+    pub price: i64,
+    pub qty: f32,
+}
+
 pub struct HeatmapChart {
     chart: CommonChartData,
-    data_points: BTreeMap<i64, (Rc<Depth>, Box<[Trade]>)>,
+    data_points: BTreeMap<i64, (GroupedDepth, Vec<GroupedTrade>)>,
     tick_size: f32,
     y_scaling: f32,
     size_filter: f32,
@@ -29,14 +42,14 @@ impl Chart for HeatmapChart {
 }
 
 impl HeatmapChart {
-    const MIN_SCALING: f32 = 0.6;
-    const MAX_SCALING: f32 = 3.6;
+    const MIN_SCALING: f32 = 1.0;
+    const MAX_SCALING: f32 = 3.0;
 
-    pub fn new() -> Self {
+    pub fn new(tick_size: f32) -> Self {
         HeatmapChart {
             chart: CommonChartData::default(),
             data_points: BTreeMap::new(),
-            tick_size: 0.0,
+            tick_size,
             y_scaling: 0.0001,
             size_filter: 0.0,
         }
@@ -49,21 +62,43 @@ impl HeatmapChart {
         self.size_filter
     }
 
-    pub fn get_raw_trades(&mut self) -> Vec<Trade> {
-        let mut trades_source = vec![];
-
-        for (_, trades) in self.data_points.values() {
-            trades_source.extend(trades.iter().cloned());
-        }
+    pub fn change_tick_size(&mut self, tick_size: f32) {
+        self.tick_size = tick_size;
 
-        trades_source
+        self.data_points.clear();
+        self.chart.x_labels_cache.clear();
+        self.chart.y_labels_cache.clear();
     }
 
     pub fn insert_datapoint(&mut self, trades_buffer: &[Trade], depth_update: i64, depth: Rc<Depth>) {
         let aggregate_time = 100; // 100 ms
         let rounded_depth_update = (depth_update / aggregate_time) * aggregate_time;
+
+        let grouped_depth = GroupedDepth {
+            time: depth.time,
+            bids: depth.bids.iter().fold(BTreeMap::new(), |mut acc, order| {
+                let rounded_price = (order.price * (1.0 / self.tick_size)).round() as i64;
+                *acc.entry(rounded_price).or_insert(0.0) += order.qty;
+                acc
+            }),
+            asks: depth.asks.iter().fold(BTreeMap::new(), |mut acc, order| {
+                let rounded_price = (order.price * (1.0 / self.tick_size)).round() as i64;
+                *acc.entry(rounded_price).or_insert(0.0) += order.qty;
+                acc
+            }),
+        };
+
+        let grouped_trades: Vec<GroupedTrade> = trades_buffer
+            .iter()
+            .map(|trade| GroupedTrade {
+                time: trade.time,
+                is_sell: trade.is_sell,
+                price: (trade.price * (1.0 / self.tick_size)).round() as i64,
+                qty: trade.qty,
+            })
+            .collect();
         
-        self.data_points.insert(rounded_depth_update, (depth, trades_buffer.into()));
+        self.data_points.insert(rounded_depth_update, (grouped_depth, grouped_trades));
         
         while self.data_points.len() > 3600 {
             if let Some((&key_to_remove, _)) = self.data_points.first_key_value() {
@@ -112,15 +147,23 @@ impl HeatmapChart {
         let timestamp_latest: &i64 = self.data_points.keys().last().unwrap_or(&0);
 
         let latest: i64 = *timestamp_latest - (self.chart.translation.x*80.0) as i64;
-        let earliest: i64 = latest - (64000.0 / (self.chart.scaling / (self.chart.bounds.width/800.0))) as i64;
+        let earliest: i64 = latest - (48000.0 / (self.chart.scaling / (self.chart.bounds.width/800.0))) as i64;
     
         let mut max_ask_price = f32::MIN;
         let mut min_bid_price = f32::MAX;
 
         for (_, (depth, _)) in self.data_points.range(earliest..=latest) {
-            if !depth.asks.is_empty() && !depth.bids.is_empty() {        
-                let ask_price: f32 = depth.asks[std::cmp::min(20, depth.asks.len() - 1)].price;
-                let bid_price: f32 = depth.bids[std::cmp::min(20, depth.bids.len() - 1)].price;
+            if !depth.asks.is_empty() && !depth.bids.is_empty() {
+                let ask_price: f32 = self.price_to_float(
+                    *depth.asks.keys().nth(
+                        std::cmp::min(20, depth.asks.len() - 1)
+                    ).unwrap_or(&0)
+                );
+                let bid_price: f32 = self.price_to_float(
+                    *depth.bids.keys().nth(
+                        std::cmp::min(20, depth.bids.len() - 1)
+                    ).unwrap_or(&0)
+                );
     
                 if ask_price > max_ask_price {
                     max_ask_price = ask_price;
@@ -187,6 +230,10 @@ impl HeatmapChart {
         }
     }
 
+    fn price_to_float(&self, price: i64) -> f32 {
+        price as f32 * self.tick_size
+    }
+
     pub fn view(&self) -> Element<Message> {
         let chart = Canvas::new(self)
             .width(Length::FillPortion(10))
@@ -435,17 +482,20 @@ impl canvas::Program<Message> for HeatmapChart {
 
                     max_volume = max_volume.max(buy_volume).max(sell_volume);
             
-                    for ask in depth.asks.iter() {
-                        if ask.price > highest {
+                    for (&price_i64, &qty) in depth.asks.iter() {
+                        let price = self.price_to_float(price_i64);
+                        if price > highest {
                             continue;
                         };
-                        max_depth_qty = max_depth_qty.max(ask.qty);
+                        max_depth_qty = max_depth_qty.max(qty);
                     }
-                    for bid in depth.bids.iter() {
-                        if bid.price < lowest {
+                
+                    for (&price_i64, &qty) in depth.bids.iter() {
+                        let price = self.price_to_float(price_i64);
+                        if price < lowest {
                             continue;
                         };
-                        max_depth_qty = max_depth_qty.max(bid.qty);
+                        max_depth_qty = max_depth_qty.max(qty);
                     }   
                 };
                 
@@ -473,9 +523,11 @@ impl canvas::Program<Message> for HeatmapChart {
                             buy_volume += trade.qty;
                         }
 
-                        if trade.qty * trade.price > self.size_filter {
+                        let price = self.price_to_float(trade.price);
+
+                        if trade.qty * price > self.size_filter {
                             let x_position = ((time - earliest) as f64 / (latest - earliest) as f64) * bounds.width as f64;
-                            let y_position = heatmap_area_height - ((trade.price - lowest) / y_range * heatmap_area_height);
+                            let y_position = heatmap_area_height - ((price - lowest) / y_range * heatmap_area_height);
 
                             let color = if trade.is_sell {
                                 Color::from_rgba8(192, 80, 77, 1.0)
@@ -493,37 +545,41 @@ impl canvas::Program<Message> for HeatmapChart {
                         }
                     }
 
-                    for bid in depth.bids.iter() {
-                        if bid.price >= lowest {
-                            let y_position = heatmap_area_height - ((bid.price - lowest) / y_range * heatmap_area_height);
-                            let color_alpha = (bid.qty / max_depth_qty).min(1.0);
+                    for (&price_i64, &qty) in depth.bids.iter() {
+                        let price = self.price_to_float(price_i64);
+
+                        if price >= lowest {
+                            let y_position = heatmap_area_height - ((price - lowest) / y_range * heatmap_area_height);
+                            let color_alpha = (qty / max_depth_qty).min(1.0);
 
                             if let (Some(prev_price), Some(prev_qty), Some(prev_x)) = (prev_bid_price, prev_bid_qty, prev_x_position) {
-                                if prev_price != bid.price || prev_qty != bid.qty {
+                                if prev_price != price || prev_qty != qty {
                                     let path = Path::line(Point::new(prev_x as f32, y_position), Point::new(x_position as f32, y_position));
                                     let stroke = Stroke::default().with_color(Color::from_rgba8(0, 144, 144, color_alpha)).with_width(1.0);
                                     frame.stroke(&path, stroke);
                                 }
                             }
-                            prev_bid_price = Some(bid.price);
-                            prev_bid_qty = Some(bid.qty);
+                            prev_bid_price = Some(price);
+                            prev_bid_qty = Some(qty);
                         }
                     }
 
-                    for ask in depth.asks.iter() {
-                        if ask.price <= highest {
-                            let y_position = heatmap_area_height - ((ask.price - lowest) / y_range * heatmap_area_height);
-                            let color_alpha = (ask.qty / max_depth_qty).min(1.0);
+                    for (&price_i64, &qty) in depth.asks.iter() {
+                        let price = self.price_to_float(price_i64);
+
+                        if price <= highest {
+                            let y_position = heatmap_area_height - ((price - lowest) / y_range * heatmap_area_height);
+                            let color_alpha = (qty / max_depth_qty).min(1.0);
 
                             if let (Some(prev_price), Some(prev_qty), Some(prev_x)) = (prev_ask_price, prev_ask_qty, prev_x_position) {
-                                if prev_price != ask.price || prev_qty != ask.qty {
+                                if prev_price != price || prev_qty != qty {
                                     let path = Path::line(Point::new(prev_x as f32, y_position), Point::new(x_position as f32, y_position));
                                     let stroke = Stroke::default().with_color(Color::from_rgba8(192, 0, 192, color_alpha)).with_width(1.0);
                                     frame.stroke(&path, stroke);
                                 }
                             }
-                            prev_ask_price = Some(ask.price);
-                            prev_ask_qty = Some(ask.qty);
+                            prev_ask_price = Some(price);
+                            prev_ask_qty = Some(qty);
                         }
                     }
 
@@ -549,18 +605,16 @@ impl canvas::Program<Message> for HeatmapChart {
             };
         
             // current orderbook as bars
-            if let Some(latest_data_points) = self.data_points.iter().last() {
-                let latest_timestamp = latest_data_points.0 + 200;
-
-                let latest_bids: Vec<(f32, f32)> = latest_data_points.1.0.bids.iter()
-                    .filter(|order| (order.price) >= lowest)
-                    .map(|order| (order.price, order.qty))
-                    .collect::<Vec<_>>();
-
-                let latest_asks: Vec<(f32, f32)> = latest_data_points.1.0.asks.iter()
-                    .filter(|order| (order.price) <= highest)
-                    .map(|order| (order.price, order.qty))
-                    .collect::<Vec<_>>();
+            if let Some((&latest_timestamp, (grouped_depth, _))) = self.data_points.iter().last() {
+                let latest_bids: Vec<(f32, f32)> = grouped_depth.bids.iter()
+                    .map(|(&price_i64, &qty)| (self.price_to_float(price_i64), qty))
+                    .filter(|&(price, _)| price >= lowest)
+                    .collect();
+
+                let latest_asks: Vec<(f32, f32)> = grouped_depth.asks.iter()
+                    .map(|(&price_i64, &qty)| (self.price_to_float(price_i64), qty))
+                    .filter(|&(price, _)| price <= highest)
+                    .collect();
 
                 let max_qty = latest_bids.iter().map(|(_, qty)| qty).chain(latest_asks.iter().map(|(_, qty)| qty)).fold(f32::MIN, |arg0: f32, other: &f32| f32::max(arg0, *other));
 

+ 201 - 98
src/main.rs

@@ -73,7 +73,10 @@ fn main() -> iced::Result {
                     SerializablePane::FootprintChart { stream_type, settings } => {
                         let ticksize = settings.tick_multiply
                             .unwrap()
-                            .multiply_with_min_tick_size(settings.min_tick_size.unwrap());
+                            .multiply_with_min_tick_size(
+                                settings.min_tick_size
+                                    .expect("No min tick size found, deleting dashboard_state.json probably fixes this")
+                            );
                     
                         let timeframe = settings.selected_timeframe
                             .unwrap()
@@ -95,10 +98,17 @@ fn main() -> iced::Result {
                         )
                     },
                     SerializablePane::HeatmapChart { stream_type, settings } => {
+                        let ticksize = settings.tick_multiply
+                            .unwrap()
+                            .multiply_with_min_tick_size(
+                                settings.min_tick_size
+                                    .expect("No min tick size found, deleting dashboard_state.json probably fixes this")
+                            );
+
                         Configuration::Pane(
                             PaneState::from_config(
                                 PaneContent::Heatmap(
-                                    HeatmapChart::new()
+                                    HeatmapChart::new(ticksize)
                                 ),
                                 stream_type,
                                 settings
@@ -160,6 +170,8 @@ fn main() -> iced::Result {
 
 #[derive(Debug, Clone)]
 pub enum Message {
+    FetchDistributeKlines(StreamType, Result<Vec<data_providers::Kline>, std::string::String>),
+    FetchDistributeTicks(StreamType, Result<f32, std::string::String>),
     Debug(String),
     ErrorOccurred(String),
 
@@ -239,17 +251,13 @@ impl State {
         let dashboard = saved_state.layouts.get(&last_active_layout);
 
         if let Some(dashboard) = dashboard {
-            for (_, pane_state) in dashboard.panes.iter() {
-                match pane_state.content {
-                    PaneContent::Candlestick(_) | PaneContent::Footprint(_) => {
-                        tasks.extend(kline_fetch_tasks(pane_state).into_iter());
-                        if let PaneContent::Footprint(_) = pane_state.content {
-                            tasks.extend(ticksize_fetch_tasks(pane_state).into_iter());
-                        }
-                    },
-                    _ => {}
-                }
-            }
+            let sleep_and_fetch = Task::perform(
+                async { tokio::time::sleep(tokio::time::Duration::from_millis(200)).await; },
+                move |_| Message::LayoutSelected(last_active_layout)
+            );
+
+            tasks.push(sleep_and_fetch);
+
             pane_streams = dashboard.get_all_diff_streams();
         }
 
@@ -712,7 +720,7 @@ impl State {
                 let mut tasks = vec![];
                 
                 let pane_content = match content.as_str() {
-                    "Heatmap chart" => PaneContent::Heatmap(HeatmapChart::new()),
+                    "Heatmap chart" => PaneContent::Heatmap(HeatmapChart::new(1.0)),
                     "Footprint chart" => {
                         let footprint_chart = FootprintChart::new(1, 1.0, vec![], vec![]);
                         PaneContent::Footprint(footprint_chart)
@@ -736,48 +744,24 @@ impl State {
                 } else {
                     dbg!("Pane content set");
                 }
-            
-                if content == "Footprint chart" || content == "Candlestick chart" {
+                
+                if content == "Footprint chart" || content == "Candlestick chart" || content == "Heatmap chart" {
                     for stream in pane_stream.iter() {
                         if let StreamType::Kline { exchange, ticker, timeframe } = stream {
                             let stream_clone = stream.clone();
-                            let fetch_klines = match exchange {
-                                Exchange::BinanceFutures => Task::perform(
-                                    binance::market_data::fetch_klines(*ticker, *timeframe)
-                                        .map_err(|err| format!("{err}")),
-                                    move |klines| Message::FetchEvent(klines, stream_clone, pane_id)
-                                ),
-                                Exchange::BybitLinear => Task::perform(
-                                    bybit::market_data::fetch_klines(*ticker, *timeframe)
-                                        .map_err(|err| format!("{err}")),
-                                    move |klines| Message::FetchEvent(klines, stream_clone, pane_id)
-                                ),
-                                _ => continue,
-                            };
                 
-                            tasks.push(fetch_klines);
+                            if content == "Candlestick chart" || content == "Footprint Chart" {
+                                let fetch_klines = create_fetch_klines_task(exchange, ticker, timeframe, stream_clone, pane_id);
+                                tasks.push(fetch_klines);
+                            }
                 
                             if content == "Footprint chart" {
-                                let fetch_ticksize: Task<Message> = match exchange {
-                                    Exchange::BinanceFutures => Task::perform(
-                                        binance::market_data::fetch_ticksize(*ticker),
-                                        move |result| match result {
-                                            Ok(ticksize) => Message::SetMinTickSize(ticksize, pane_id),
-                                            Err(err) => Message::ErrorOccurred(err.to_string()),
-                                        }
-                                    ),
-                                    Exchange::BybitLinear => Task::perform(
-                                        bybit::market_data::fetch_ticksize(*ticker),
-                                        move |result| match result {
-                                            Ok(ticksize) => Message::SetMinTickSize(ticksize, pane_id),
-                                            Err(err) => Message::ErrorOccurred(err.to_string()),
-                                        }
-                                    ),
-                                    _ => continue,
-                                };
-                
+                                let fetch_ticksize = create_fetch_ticksize_task(exchange, ticker, pane_id);
                                 tasks.push(fetch_ticksize);
                             }
+                        } else if let StreamType::DepthAndTrades { exchange, ticker } = stream {
+                            let fetch_ticksize = create_fetch_ticksize_task(exchange, ticker, pane_id);
+                            tasks.push(fetch_ticksize);
                         }
                     }
                 }
@@ -815,35 +799,58 @@ impl State {
                 Task::none()
             },
             Message::LayoutSelected(layout_id) => {
+                self.last_active_layout = layout_id;
+            
+                Task::perform(
+                    async { tokio::time::sleep(tokio::time::Duration::from_millis(200)).await; },
+                    |_| Message::RefreshStreams
+                )
+            },
+            Message::RefreshStreams => {
                 let mut tasks = vec![];
 
-                self.last_active_layout = layout_id;    
+                self.pane_streams = self.get_dashboard().get_all_diff_streams();
+
+                let kline_tasks = klines_fetch_all_task(&self.pane_streams);
 
+                let ticksize_tasks = ticksize_fetch_all_task(&self.pane_streams);
+
+                tasks.extend(kline_tasks.into_iter());
+                tasks.extend(ticksize_tasks.into_iter());
+
+                Task::batch(tasks)
+            }
+        
+            Message::FetchDistributeKlines(stream_type, klines) => {
                 let dashboard = self.get_mut_dashboard();
 
-                for (_, pane_state) in dashboard.panes.iter() {
-                    match pane_state.content {
-                        PaneContent::Candlestick(_) | PaneContent::Footprint(_) => {
-                            tasks.extend(kline_fetch_tasks(pane_state).into_iter());
-                            if let PaneContent::Footprint(_) = pane_state.content {
-                                tasks.extend(ticksize_fetch_tasks(pane_state).into_iter());
-                            }
-                        },
-                        _ => {}
+                match klines {
+                    Ok(klines) => {
+                        if let Err(err) = dashboard.find_and_insert_klines(&stream_type, &klines) {
+                            eprintln!("{err}");
+                        }
+                    },
+                    Err(err) => {
+                        eprintln!("{err}");
                     }
                 }
 
-                let sleep_task = Task::perform(
-                    async { tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; },
-                    |_| Message::RefreshStreams
-                );
-
-                tasks.push(sleep_task);
+                Task::none()
+            }
+            
+            Message::FetchDistributeTicks(stream_type, min_tick_size) => {
+                let dashboard = self.get_mut_dashboard();
 
-                Task::batch(tasks)
-            },
-            Message::RefreshStreams => {
-                self.pane_streams = self.get_dashboard().get_all_diff_streams();
+                match min_tick_size {
+                    Ok(ticksize) => {
+                        if let Err(err) = dashboard.find_and_insert_ticksizes(&stream_type, ticksize) {
+                            eprintln!("{err}");
+                        }
+                    },
+                    Err(err) => {
+                        eprintln!("{err}");
+                    }
+                }
 
                 Task::none()
             }
@@ -1509,6 +1516,22 @@ fn view_controls<'a>(
 
     match pane_type {
         PaneContent::Heatmap(_) => {
+            let ticksize_picker = pick_list(
+                [TickMultiplier(1), TickMultiplier(2), TickMultiplier(5), TickMultiplier(10), TickMultiplier(25), TickMultiplier(50)],
+                settings.tick_multiply, 
+                move |tick_multiply| Message::TicksizeSelected(tick_multiply, pane_id)
+            ).placeholder("Ticksize multiplier...").text_size(11).width(iced::Pixels(80.0));
+
+            let ticksize_tooltip = tooltip(
+                ticksize_picker
+                    .style(style::picklist_primary)
+                    .menu_style(style::picklist_menu_primary),
+                    "Ticksize multiplier",
+                    tooltip::Position::FollowCursor
+                )
+                .style(style::tooltip);
+    
+            row = row.push(ticksize_tooltip);
         },
         PaneContent::TimeAndSales(_) => {
         },
@@ -1590,29 +1613,46 @@ fn view_controls<'a>(
     row.into()
 }
 
-fn kline_fetch_tasks(pane_state: &PaneState) -> Vec<Task<Message>> {
+fn klines_fetch_all_task(stream_types: &HashMap<Exchange, HashMap<Ticker, HashSet<StreamType>>>) -> Vec<Task<Message>> {
     let mut tasks: Vec<Task<Message>> = vec![];
 
-    for stream in pane_state.stream.iter() {
-        if let StreamType::Kline { ticker, timeframe, exchange } = stream {
-            let stream = stream.clone();
+    for (exchange, stream) in stream_types {
+        let mut kline_fetches = Vec::new();
+
+        for stream_types in stream.values() {
+            for stream_type in stream_types {
+                match stream_type {
+                    StreamType::Kline { ticker, timeframe, .. } => {
+                        kline_fetches.push((*ticker, *timeframe));
+                    },
+                    _ => {}
+                }
+            }
+        }
 
-            let pane_id = pane_state.id;
+        for (ticker, timeframe) in kline_fetches {
+            let ticker = ticker.clone();
+            let timeframe = timeframe.clone();
+            let exchange = exchange.clone();
 
             match exchange {
                 Exchange::BinanceFutures => {
                     let fetch_klines = Task::perform(
-                        binance::market_data::fetch_klines(*ticker, *timeframe)
+                        binance::market_data::fetch_klines(ticker, timeframe)
                             .map_err(|err| format!("{err}")),
-                        move |klines| Message::FetchEvent(klines, stream, pane_id)
+                        move |klines| Message::FetchDistributeKlines(
+                            StreamType::Kline { exchange, ticker, timeframe }, klines
+                        )
                     );
                     tasks.push(fetch_klines);
                 },
                 Exchange::BybitLinear => {
                     let fetch_klines = Task::perform(
-                        bybit::market_data::fetch_klines(*ticker, *timeframe)
+                        bybit::market_data::fetch_klines(ticker, timeframe)
                             .map_err(|err| format!("{err}")),
-                        move |klines| Message::FetchEvent(klines, stream, pane_id)
+                        move |klines| Message::FetchDistributeKlines(
+                            StreamType::Kline { exchange, ticker, timeframe }, klines
+                        )
                     );
                     tasks.push(fetch_klines);
                 }
@@ -1623,37 +1663,100 @@ fn kline_fetch_tasks(pane_state: &PaneState) -> Vec<Task<Message>> {
     tasks
 }
 
-fn ticksize_fetch_tasks(pane_state: &PaneState) -> Vec<Task<Message>> {
+fn ticksize_fetch_all_task(stream_types: &HashMap<Exchange, HashMap<Ticker, HashSet<StreamType>>>) -> Vec<Task<Message>> {
     let mut tasks: Vec<Task<Message>> = vec![];
 
-    for stream in pane_state.stream.iter() {
-        if let StreamType::Kline { ticker, exchange, .. } = stream {
-            let pane_id = pane_state.id;
+    for (exchange, stream) in stream_types {
+        let mut ticksize_fetches = Vec::new();
 
-            let fetch_ticksize = match exchange {
-                Exchange::BinanceFutures => Task::perform(
-                    binance::market_data::fetch_ticksize(*ticker),
-                    move |result| match result {
-                        Ok(ticksize) => Message::SetMinTickSize(ticksize, pane_id),
-                        Err(err) => Message::ErrorOccurred(err.to_string()),
-                    }
-                ),
-                Exchange::BybitLinear => Task::perform(
-                    bybit::market_data::fetch_ticksize(*ticker),
-                    move |result| match result {
-                        Ok(ticksize) => Message::SetMinTickSize(ticksize, pane_id),
-                        Err(err) => Message::ErrorOccurred(err.to_string()),
-                    }
-                ),
-            };
+        for stream_types in stream.values() {
+            for stream_type in stream_types {
+                match stream_type {
+                    StreamType::DepthAndTrades { ticker, .. } => {
+                        ticksize_fetches.push(*ticker);
+                    },
+                    _ => {}
+                }
+            }
+        }
+
+        for ticker in ticksize_fetches {
+            let ticker = ticker.clone();
+            let exchange = exchange.clone();
 
-            tasks.push(fetch_ticksize);
+            match exchange {
+                Exchange::BinanceFutures => {
+                    let fetch_ticksize = Task::perform(
+                        binance::market_data::fetch_ticksize(ticker)
+                            .map_err(|err| format!("{err}")),
+                        move |ticksize| Message::FetchDistributeTicks(
+                            StreamType::DepthAndTrades { exchange, ticker }, ticksize
+                        )
+                    );
+                    tasks.push(fetch_ticksize);
+                },
+                Exchange::BybitLinear => {
+                    let fetch_ticksize = Task::perform(
+                        bybit::market_data::fetch_ticksize(ticker)
+                            .map_err(|err| format!("{err}")),
+                        move |ticksize| Message::FetchDistributeTicks(
+                            StreamType::DepthAndTrades { exchange, ticker }, ticksize
+                        )
+                    );
+                    tasks.push(fetch_ticksize);
+                }
+            }
         }
     }
 
     tasks
 }
 
+fn create_fetch_klines_task(
+    exchange: &Exchange,
+    ticker: &Ticker,
+    timeframe: &Timeframe,
+    stream_clone: StreamType,
+    pane_id: Uuid,
+) -> Task<Message> {
+    match exchange {
+        Exchange::BinanceFutures => Task::perform(
+            binance::market_data::fetch_klines(*ticker, *timeframe)
+                .map_err(|err| format!("{err}")),
+            move |klines| Message::FetchEvent(klines, stream_clone, pane_id),
+        ),
+        Exchange::BybitLinear => Task::perform(
+            bybit::market_data::fetch_klines(*ticker, *timeframe)
+                .map_err(|err| format!("{err}")),
+            move |klines| Message::FetchEvent(klines, stream_clone, pane_id),
+        ),
+        _ => return Task::none(),
+    }
+}
+
+fn create_fetch_ticksize_task(
+    exchange: &Exchange,
+    ticker: &Ticker,
+    pane_id: Uuid,
+) -> Task<Message> {
+    match exchange {
+        Exchange::BinanceFutures => Task::perform(
+            binance::market_data::fetch_ticksize(*ticker),
+            move |result| match result {
+                Ok(ticksize) => Message::SetMinTickSize(ticksize, pane_id),
+                Err(err) => Message::ErrorOccurred(err.to_string()),
+            },
+        ),
+        Exchange::BybitLinear => Task::perform(
+            bybit::market_data::fetch_ticksize(*ticker),
+            move |result| match result {
+                Ok(ticksize) => Message::SetMinTickSize(ticksize, pane_id),
+                Err(err) => Message::ErrorOccurred(err.to_string()),
+            },
+        ),
+    }
+}
+
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum Event {
     CloseRequested(window::Id),

+ 78 - 0
src/screen/dashboard.rs

@@ -177,6 +177,13 @@ impl Dashboard {
                             
                             return Ok(());
                         },
+                        PaneContent::Heatmap(ref mut chart) => {
+                            chart.change_tick_size(
+                                new_tick_multiply.multiply_with_min_tick_size(min_tick_size)
+                            );
+                            
+                            return Ok(());
+                        },
                         _ => {
                             return Err("No footprint chart found");
                         }
@@ -267,6 +274,77 @@ impl Dashboard {
         }
     }
 
+    pub fn find_and_insert_klines(&mut self, stream_type: &StreamType, klines: &Vec<Kline>) -> Result<(), &str> {
+        dbg!(stream_type);
+
+        let mut found_match = false;
+
+        for (_, pane_state) in self.panes.iter_mut() {
+            if pane_state.matches_stream(&stream_type) {
+                match stream_type {
+                    StreamType::Kline { timeframe, .. } => {
+                        let timeframe_u16 = timeframe.to_minutes();
+
+                        match &mut pane_state.content {
+                            PaneContent::Candlestick(chart) => {
+                                *chart = CandlestickChart::new(klines.to_vec(), timeframe_u16);
+
+                                found_match = true;
+                            },
+                            PaneContent::Footprint(chart) => {
+                                let raw_trades = chart.get_raw_trades();
+
+                                let tick_size = chart.get_tick_size();
+
+                                *chart = FootprintChart::new(timeframe_u16, tick_size, klines.to_vec(), raw_trades);
+
+                                found_match = true;
+                            },
+                            _ => {}
+                        }
+                    },
+                    _ => {}
+                }
+            }
+        }
+
+        if found_match {
+            Ok(())
+        } else {
+            Err("No matching pane found for the stream")
+        }
+    }
+
+    pub fn find_and_insert_ticksizes(&mut self, stream_type: &StreamType, tick_sizes: f32) -> Result<(), &str> {
+        dbg!(stream_type);
+
+        let mut found_match = false;
+
+        for (_, pane_state) in self.panes.iter_mut() {
+            if pane_state.matches_stream(&stream_type) {
+                match &mut pane_state.content {
+                    PaneContent::Footprint(_) => {
+                        pane_state.settings.min_tick_size = Some(tick_sizes);
+
+                        found_match = true;
+                    },
+                    PaneContent::Heatmap(_) => {
+                        pane_state.settings.min_tick_size = Some(tick_sizes);
+
+                        found_match = true;
+                    },
+                    _ => {}
+                }
+            }
+        }
+
+        if found_match {
+            Ok(())
+        } else {
+            Err("No matching pane found for the stream")
+        }
+    }
+
     pub fn update_latest_klines(&mut self, stream_type: &StreamType, kline: &Kline) -> Result<(), &str> {
         let mut found_match = false;