Ver Fonte

Visible range trades fetch(Binance) (#20)

* improve trade fetch
--handle ticksize change instantly by keeping the raw trades
--add info about trade fetch op. as notification

* implement rate limiter for binance

* fix trade fetch logic to get until earliest available trade raw

* improve error flow for the fetch implementation
--fixes edge case where replacing pane stream not stopping the batched fetch jobs

* implement hist. data download via data.binance.vision

* feat: hide notifications by clicking on them

* make trade fetch feature as a toggle option

* fix unnecessary range checks for disabled indicators on charts
+ store trade fetch optionality on dashboard state

* prevent open interest fetch requests on spot market tickers

* remove open interest indicator fully from spot tickers
-- use `TickerInfo` to let chart instances know what market type they're dealing with

* fix incorrect stream modifiers passed around chart
-- was causing unrelated pane settings showing for the chart type

* chore: reset `PaneSettings` on pane stream setup phase
-- should be preventing edge cases like switching pane content causing pane settings to transfer, while the content is unrelated

* impl. old data cleanup to prevent unnecessary excessive storage

* handle indicators properly for `HeatmapChart`
Berke há 10 meses atrás
pai
commit
375e497aa3

+ 2 - 1
.gitignore

@@ -2,4 +2,5 @@
 /target
 /.vscode
 *.json
-*.log
+*.log
+/data

+ 275 - 3
Cargo.lock

@@ -39,6 +39,17 @@ version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
 
+[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
 [[package]]
 name = "ahash"
 version = "0.7.8"
@@ -135,6 +146,15 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "arbitrary"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
+dependencies = [
+ "derive_arbitrary",
+]
+
 [[package]]
 name = "arrayref"
 version = "0.3.8"
@@ -533,6 +553,27 @@ version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
 
+[[package]]
+name = "bzip2"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8"
+dependencies = [
+ "bzip2-sys",
+ "libc",
+]
+
+[[package]]
+name = "bzip2-sys"
+version = "0.1.11+1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+]
+
 [[package]]
 name = "calloop"
 version = "0.12.4"
@@ -633,6 +674,16 @@ dependencies = [
  "windows-targets 0.52.6",
 ]
 
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
 [[package]]
 name = "clipboard-win"
 version = "5.4.0"
@@ -707,6 +758,12 @@ dependencies = [
  "crossbeam-utils",
 ]
 
+[[package]]
+name = "constant_time_eq"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
+
 [[package]]
 name = "core-foundation"
 version = "0.9.4"
@@ -779,6 +836,21 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "crc"
+version = "3.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
+
 [[package]]
 name = "crc32fast"
 version = "1.4.2"
@@ -829,6 +901,27 @@ dependencies = [
  "typenum",
 ]
 
+[[package]]
+name = "csv"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf"
+dependencies = [
+ "csv-core",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "csv-core"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "ctor-lite"
 version = "0.1.0"
@@ -869,6 +962,32 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b"
 
+[[package]]
+name = "deflate64"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b"
+
+[[package]]
+name = "deranged"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "derive_arbitrary"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.90",
+]
+
 [[package]]
 name = "detect-desktop-environment"
 version = "0.2.0"
@@ -1179,12 +1298,12 @@ dependencies = [
 
 [[package]]
 name = "flate2"
-version = "1.0.30"
+version = "1.0.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
+checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
 dependencies = [
  "crc32fast",
- "miniz_oxide 0.7.4",
+ "miniz_oxide 0.8.0",
 ]
 
 [[package]]
@@ -1201,6 +1320,7 @@ dependencies = [
  "base64 0.22.1",
  "bytes",
  "chrono",
+ "csv",
  "fastwebsockets",
  "fern",
  "futures",
@@ -1232,6 +1352,7 @@ dependencies = [
  "url",
  "uuid",
  "webpki-roots 0.23.1",
+ "zip",
 ]
 
 [[package]]
@@ -2181,6 +2302,15 @@ dependencies = [
  "hashbrown 0.14.5",
 ]
 
+[[package]]
+name = "inout"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
+dependencies = [
+ "generic-array",
+]
+
 [[package]]
 name = "instant"
 version = "0.1.13"
@@ -2380,6 +2510,12 @@ dependencies = [
  "scopeguard",
 ]
 
+[[package]]
+name = "lockfree-object-pool"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e"
+
 [[package]]
 name = "log"
 version = "0.4.22"
@@ -2444,6 +2580,16 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "lzma-rs"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e"
+dependencies = [
+ "byteorder",
+ "crc",
+]
+
 [[package]]
 name = "malloc_buf"
 version = "0.0.6"
@@ -2625,6 +2771,12 @@ dependencies = [
  "memoffset",
 ]
 
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
 [[package]]
 name = "num-traits"
 version = "0.2.19"
@@ -3113,6 +3265,16 @@ version = "1.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
 
+[[package]]
+name = "pbkdf2"
+version = "0.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
+dependencies = [
+ "digest",
+ "hmac",
+]
+
 [[package]]
 name = "percent-encoding"
 version = "2.3.1"
@@ -3238,6 +3400,12 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
 [[package]]
 name = "ppv-lite86"
 version = "0.2.18"
@@ -4386,6 +4554,25 @@ dependencies = [
  "weezl",
 ]
 
+[[package]]
+name = "time"
+version = "0.3.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
+dependencies = [
+ "deranged",
+ "num-conv",
+ "powerfmt",
+ "serde",
+ "time-core",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
+
 [[package]]
 name = "tiny-skia"
 version = "0.11.4"
@@ -5834,6 +6021,20 @@ name = "zeroize"
 version = "1.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
+dependencies = [
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.90",
+]
 
 [[package]]
 name = "zerovec"
@@ -5857,6 +6058,77 @@ dependencies = [
  "syn 2.0.90",
 ]
 
+[[package]]
+name = "zip"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99d52293fc86ea7cf13971b3bb81eb21683636e7ae24c729cdaf1b7c4157a352"
+dependencies = [
+ "aes",
+ "arbitrary",
+ "bzip2",
+ "constant_time_eq",
+ "crc32fast",
+ "crossbeam-utils",
+ "deflate64",
+ "displaydoc",
+ "flate2",
+ "hmac",
+ "indexmap",
+ "lzma-rs",
+ "memchr",
+ "pbkdf2",
+ "rand",
+ "sha1",
+ "thiserror 2.0.6",
+ "time",
+ "zeroize",
+ "zopfli",
+ "zstd",
+]
+
+[[package]]
+name = "zopfli"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946"
+dependencies = [
+ "bumpalo",
+ "crc32fast",
+ "lockfree-object-pool",
+ "log",
+ "once_cell",
+ "simd-adler32",
+]
+
+[[package]]
+name = "zstd"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9"
+dependencies = [
+ "zstd-safe",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "7.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059"
+dependencies = [
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-sys"
+version = "2.0.13+zstd.1.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
 [[package]]
 name = "zune-inflate"
 version = "0.2.54"

+ 2 - 0
Cargo.toml

@@ -38,6 +38,8 @@ ordered-float = "4.5.0"
 regex = "1.11.1"
 rust_decimal = "1.36.0"
 uuid = { version = "1.11.0", features = ["v4"] }
+zip = "2.2.1"
+csv = "1.3.1"
 
 [dependencies.async-tungstenite]
 version = "0.25"

+ 25 - 8
README.md

@@ -4,10 +4,11 @@
 </div>
 
 ### Some of the features:
+
 - Customizable and savable grid layouts, selectable themes
-- From Binance & Bybit: most of spot & linear perp pairs
+- Supports most of spot(USDT) & linear perp pairs from Binance & Bybit
 - Orderbook total bid/ask levels: 1000 for Binance Perp/Spot; 500 for Bybit Perps, 200 for Bybit Spot
-- Binance perp/spot & Bybit perp streams @100ms; Bybit spot pairs streams @200ms,
+- Binance perp/spot & Bybit perp streams @100ms; Bybit spot pairs streams @200ms
 - Tick size multipliers for price grouping on footprint and heatmap charts
 
 <div align="center">
@@ -15,15 +16,31 @@
   <img height="235" width="200" alt="iced-trade" src="https://github.com/user-attachments/assets/a93ff39f-e80a-4f87-a99b-d4582f4bb818">
 </div>
 
-##### There is no server-side. User receives market data directly from exchange APIs
-- As historical data, currently it can only fetch OHLCV and open interest. So, footprint chart gets populated via candlesticks but not historical trades. Trades gets inserted to the latest candlestick as we receive them from related websocket stream in real-time
-  
+##### User receives market data directly from exchange APIs.
+
+- As historical data, it can fetch OHLCV, open interest and partially, trades:
+
+#### Historical trades on footprint chart:
+
+Optionally, you can enable trade fetching from settings menu, experimental because of unreliability:
+
+- Binance connector supports downloading historical trades from [data.binance.vision](https://data.binance.vision), fast and easy way to get trades, but they dont support intraday data.
+Intraday trades fetched by pagination using Binance's public REST APIs: `/fapi/v1/aggTrades` & `api/v3/aggTrades`, it might be slow because of rate-limits
+
+- Bybit itself doesnt have a similar purpose public API
+
+Flowsurface tries to leverage this via Binance tickers, to visualize historical public trades while being independent of a 'middleman' database between exchange and the user.
+So, when a chart instance signal the exchange connector after a data integrity check, about missing trades in the visible range; it tries via fetching, downloading and/or loading from cache, whichever suitable, to ensure this integrity
+
 ## Build from source
+
 The releases might not be up-to-date with newest features.<sup>or bugs :)</sup>
-- For that, you could
-clone the repository into a directory of your choice and build with cargo.
+
+- For that you could
+  clone the repository into a directory of your choice and build with cargo.
 
 Requirements:
+
 - [Rust toolchain](https://www.rust-lang.org/tools/install)
 - [Git version control system](https://git-scm.com/)
 
@@ -40,4 +57,4 @@ cargo run --release
 
 <a href="https://github.com/iced-rs/iced">
   <img src="https://gist.githubusercontent.com/hecrj/ad7ecd38f6e47ff3688a38c79fd108f0/raw/74384875ecbad02ae2a926425e9bcafd0695bade/color.svg" width="130px">
-</a>
+</a>

+ 13 - 6
src/charts.rs

@@ -10,7 +10,7 @@ use indicators::Indicator;
 use uuid::Uuid;
 
 use crate::{
-    data_providers::fetcher::{FetchRange, ReqError, RequestHandler},
+    data_providers::{fetcher::{FetchRange, ReqError, RequestHandler}, TickerInfo},
     screen::UserTimezone,
     style,
     tooltip::{self, tooltip},
@@ -129,7 +129,11 @@ trait Chart: ChartConstants + canvas::Program<Message> {
         cursor: mouse::Cursor,
     ) -> Option<canvas::Action<Message>>;
 
-    fn view_indicator<I: Indicator>(&self, enabled: &[I]) -> Element<Message>;
+    fn view_indicator<I: Indicator>(
+        &self, 
+        enabled: &[I], 
+        ticker_info: Option<TickerInfo>
+    ) -> Option<Element<Message>>;
 
     fn get_visible_timerange(&self) -> (i64, i64);
 }
@@ -385,7 +389,11 @@ fn update_chart<T: Chart>(chart: &mut T, message: &Message) -> Task<Message> {
     Task::none()
 }
 
-fn view_chart<'a, T: Chart, I: Indicator>(chart: &'a T, indicators: &'a [I]) -> Element<'a, Message> {
+fn view_chart<'a, T: Chart, I: Indicator>(
+    chart: &'a T, 
+    indicators: &'a [I], 
+    ticker_info: Option<TickerInfo>,
+) -> Element<'a, Message> {
     let chart_state = chart.get_common_data();
 
     if chart_state.latest_x == 0 || chart_state.base_price_y == 0.0 {
@@ -464,9 +472,8 @@ fn view_chart<'a, T: Chart, I: Indicator>(chart: &'a T, indicators: &'a [I]) ->
     let mut indicators_row = row![];
     if !indicators.is_empty() {
         indicators_row = indicators_row
-            .push(container(chart.view_indicator(indicators))
-                .width(Length::FillPortion(10))
-                .height(Length::FillPortion(chart_state.indicators_height))
+            .push_maybe(
+                chart.view_indicator(indicators, ticker_info)
             )
     }
 

+ 110 - 69
src/charts/candlestick.rs

@@ -1,9 +1,11 @@
-use std::collections::BTreeMap;
+use std::collections::{BTreeMap, HashMap};
 
 use iced::widget::canvas::{LineDash, Path, Stroke};
-use iced::{mouse, Element, Point, Rectangle, Renderer, Size, Task, Theme, Vector};
+use iced::widget::container;
+use iced::{mouse, Element, Length, Point, Rectangle, Renderer, Size, Task, Theme, Vector};
 use iced::widget::{canvas::{self, Event, Geometry}, column};
 
+use crate::data_providers::TickerInfo;
 use crate::screen::UserTimezone;
 use crate::data_providers::{
     fetcher::{FetchRange, RequestHandler},
@@ -41,8 +43,12 @@ impl Chart for CandlestickChart {
         canvas_interaction(self, interaction, event, bounds, cursor)
     }
 
-    fn view_indicator<I: Indicator>(&self, enabled: &[I]) -> Element<Message> {
-        self.view_indicators(enabled)
+    fn view_indicator<I: Indicator>(
+        &self, 
+        indicators: &[I], 
+        ticker_info: Option<TickerInfo>
+    ) -> Option<Element<Message>> {
+        self.view_indicators(indicators, ticker_info)
     }
 
     fn get_visible_timerange(&self) -> (i64, i64) {
@@ -71,16 +77,16 @@ impl ChartConstants for CandlestickChart {
 }
 
 #[allow(dead_code)]
-enum Indicators {
+enum IndicatorData {
     Volume(Caches, BTreeMap<i64, (f32, f32)>),
     OpenInterest(Caches, BTreeMap<i64, f32>),
 }
 
-impl Indicators {
+impl IndicatorData {
     fn clear_cache(&mut self) {
         match self {
-            Indicators::Volume(caches, _) 
-            | Indicators::OpenInterest(caches, _) => {
+            IndicatorData::Volume(caches, _) 
+            | IndicatorData::OpenInterest(caches, _) => {
                 caches.clear_all();
             }
         }
@@ -90,7 +96,7 @@ impl Indicators {
 pub struct CandlestickChart {
     chart: CommonChartData,
     data_points: BTreeMap<i64, Kline>,
-    indicators: Vec<Indicators>,
+    indicators: HashMap<CandlestickIndicator, IndicatorData>,
     request_handler: RequestHandler,
     fetching_oi: bool,
 }
@@ -101,6 +107,7 @@ impl CandlestickChart {
         timeframe: Timeframe,
         tick_size: f32,
         timezone: UserTimezone,
+        enabled_indicators: &[CandlestickIndicator],
     ) -> CandlestickChart {
         let mut data_points = BTreeMap::new();
         let mut volume_data = BTreeMap::new();
@@ -138,10 +145,25 @@ impl CandlestickChart {
                 ..Default::default()
             },
             data_points,
-            indicators: vec![
-                Indicators::Volume(Caches::default(), volume_data.clone()),
-                Indicators::OpenInterest(Caches::default(), BTreeMap::new()),
-            ],
+            indicators: {
+                let mut indicators = HashMap::new();
+
+                for indicator in enabled_indicators {
+                    indicators.insert(
+                        *indicator,
+                        match indicator {
+                            CandlestickIndicator::Volume => {
+                                IndicatorData::Volume(Caches::default(), volume_data.clone())
+                            },
+                            CandlestickIndicator::OpenInterest => {
+                                IndicatorData::OpenInterest(Caches::default(), BTreeMap::new())
+                            }
+                        }
+                    );
+                }
+
+                indicators
+            },
             request_handler: RequestHandler::new(),
             fetching_oi: false,
         }
@@ -159,11 +181,10 @@ impl CandlestickChart {
     pub fn update_latest_kline(&mut self, kline: &Kline) -> Task<Message> {
         self.data_points.insert(kline.time as i64, *kline);
 
-        self.indicators.iter_mut().for_each(|indicator| {
-            if let Indicators::Volume(_, data) = indicator {
+        if let Some(IndicatorData::Volume(_, data)) = 
+            self.indicators.get_mut(&CandlestickIndicator::Volume) {
                 data.insert(kline.time as i64, (kline.volume.0, kline.volume.1));
-            }
-        });
+            };
 
         let chart = self.get_common_data_mut();
 
@@ -204,8 +225,8 @@ impl CandlestickChart {
             }
         }
 
-        for indicator in &self.indicators {
-            if let Indicators::OpenInterest(_, _) = indicator {
+        for data in self.indicators.values() {
+            if let IndicatorData::OpenInterest(_, _) = data {
                 if !self.fetching_oi {
                     let (oi_earliest, oi_latest) = self.get_oi_timerange(kline_latest);
 
@@ -244,11 +265,10 @@ impl CandlestickChart {
             self.data_points.entry(kline.time as i64).or_insert(*kline);
         }
 
-        self.indicators.iter_mut().for_each(|indicator| {
-            if let Indicators::Volume(_, data) = indicator {
+        if let Some(IndicatorData::Volume(_, data)) = 
+            self.indicators.get_mut(&CandlestickIndicator::Volume) {
                 data.extend(volume_data.clone());
-            }
-        });
+            };
 
         if klines_raw.len() > 1 {
             self.request_handler.mark_completed(req_id);
@@ -263,13 +283,12 @@ impl CandlestickChart {
     }
 
     pub fn insert_open_interest(&mut self, _req_id: Option<uuid::Uuid>, oi_data: Vec<OIData>) {
-        self.indicators.iter_mut().for_each(|indicator| {
-            if let Indicators::OpenInterest(_, data) = indicator {
+        if let Some(IndicatorData::OpenInterest(_, data)) = 
+            self.indicators.get_mut(&CandlestickIndicator::OpenInterest) {
                 data.extend(oi_data
                     .iter().map(|oi| (oi.time, oi.value))
                 );
-            }
-        });
+            };
     
         self.fetching_oi = false;
     }
@@ -290,14 +309,13 @@ impl CandlestickChart {
         let mut from_time = latest_kline;
         let mut to_time = i64::MIN;
 
-        self.indicators.iter().for_each(|indicator| {
-            if let Indicators::OpenInterest(_, data) = indicator {
+        if let Some(IndicatorData::OpenInterest(_, data)) = 
+            self.indicators.get(&CandlestickIndicator::OpenInterest) {
                 data.iter().for_each(|(time, _)| {
                     from_time = from_time.min(*time);
                     to_time = to_time.max(*time);
                 });
-            }
-        });
+            };
 
         (from_time, to_time)
     }
@@ -321,32 +339,41 @@ impl CandlestickChart {
 
         chart_state.cache.clear_all();
 
-        self.indicators.iter_mut().for_each(|indicator| {
-            indicator.clear_cache();
+        self.indicators.iter_mut().for_each(|(_, data)| {
+            data.clear_cache();
         });
     }
 
-    fn get_volume_indicator(&self) -> Option<(&Caches, &BTreeMap<i64, (f32, f32)>)> {
-        for indicator in &self.indicators {
-            if let Indicators::Volume(cache, data) = indicator {
-                return Some((cache, data));
-            }
-        }
-
-        None
-    }
-
-    fn get_oi_indicator(&self) -> Option<(&Caches, &BTreeMap<i64, f32>)> {
-        for indicator in &self.indicators {
-            if let Indicators::OpenInterest(cache, data) = indicator {
-                return Some((cache, data));
+    pub fn toggle_indicator(&mut self, indicator: CandlestickIndicator) {
+        if self.indicators.contains_key(&indicator) {
+            self.indicators.remove(&indicator);
+        } else {
+            match indicator {
+                CandlestickIndicator::Volume => {
+                    let volume_data = self.data_points.iter()
+                        .map(|(time, kline)| (*time, (kline.volume.0, kline.volume.1)))
+                        .collect();
+
+                    self.indicators.insert(
+                        indicator,
+                        IndicatorData::Volume(Caches::default(), volume_data)
+                    );
+                },
+                CandlestickIndicator::OpenInterest => {
+                    self.indicators.insert(
+                        indicator,
+                        IndicatorData::OpenInterest(Caches::default(), BTreeMap::new())
+                    );
+                }
             }
         }
-
-        None
     }
 
-    pub fn view_indicators<I: Indicator>(&self, enabled: &[I]) -> Element<Message> {
+    pub fn view_indicators<I: Indicator>(
+        &self, 
+        enabled: &[I], 
+        ticker_info: Option<TickerInfo>
+    ) -> Option<Element<Message>> {
         let chart_state: &CommonChartData = self.get_common_data();
 
         let visible_region = chart_state.visible_region(chart_state.bounds.size());
@@ -356,39 +383,53 @@ impl CandlestickChart {
 
         let mut indicators: iced::widget::Column<'_, Message> = column![];
 
-        for indicator in I::get_enabled(enabled) {
+        for indicator in I::get_enabled(
+            enabled, 
+            ticker_info.map(|info| info.market_type)
+        ) {
             if let Some(candlestick_indicator) = indicator
                 .as_any()
                 .downcast_ref::<CandlestickIndicator>() 
             {
                 match candlestick_indicator {
                     CandlestickIndicator::Volume => {
-                        if let Some((cache, data)) = self.get_volume_indicator() {
-                            indicators = indicators.push(
-                                indicators::volume::create_indicator_elem(chart_state, cache, data, earliest, latest)
-                            );
-                        }
+                        if let Some(IndicatorData::Volume(cache, data)) = self.indicators
+                            .get(&CandlestickIndicator::Volume) {
+                                indicators = indicators.push(
+                                    indicators::volume::create_indicator_elem(chart_state, cache, data, earliest, latest)
+                                );
+                            }
                     },
                     CandlestickIndicator::OpenInterest => {
-                        if let Some((cache, data)) = self.get_oi_indicator() {
-                            indicators = indicators.push(
-                                indicators::open_interest::create_indicator_elem(chart_state, cache, data, earliest, latest)
-                            );
-                        }
+                        if let Some(IndicatorData::OpenInterest(cache, data)) = 
+                            self.indicators.get(&CandlestickIndicator::OpenInterest) {
+                                indicators = indicators.push(
+                                    indicators::open_interest::create_indicator_elem(chart_state, cache, data, earliest, latest)
+                                );
+                            }
                     }
                 }
             }
         }
-
-        indicators.into()
+        
+        Some(
+            container(indicators)
+                .width(Length::FillPortion(10))
+                .height(Length::FillPortion(chart_state.indicators_height))
+                .into()
+        )
     }
 
     pub fn update(&mut self, message: &Message) -> Task<Message> {
         self.update_chart(message)
     }
 
-    pub fn view<'a, I: Indicator>(&'a self, indicators: &'a [I]) -> Element<Message> {
-        view_chart(self, indicators)
+    pub fn view<'a, I: Indicator>(
+        &'a self, 
+        indicators: &'a [I], 
+        ticker_info: Option<TickerInfo>
+    ) -> Element<Message> {
+        view_chart(self, indicators, ticker_info)
     }
 }
 
@@ -470,10 +511,10 @@ impl canvas::Program<Message> for CandlestickChart {
                     });
 
                 // last price line
-                chart.last_price.map(|price| {
+                if let Some(price) = &chart.last_price {
                     let (line_color, y_pos) = match price {
-                        PriceInfoLabel::Up(p) => (palette.success.weak.color, chart.price_to_y(p)),
-                        PriceInfoLabel::Down(p) => (palette.danger.weak.color, chart.price_to_y(p)),
+                        PriceInfoLabel::Up(p) => (palette.success.weak.color, chart.price_to_y(*p)),
+                        PriceInfoLabel::Down(p) => (palette.danger.weak.color, chart.price_to_y(*p)),
                     };
 
                     let marker_line = Stroke::with_color(
@@ -495,7 +536,7 @@ impl canvas::Program<Message> for CandlestickChart {
                         ),
                         marker_line,
                     );
-                });
+                };
             });
         });
 

+ 262 - 124
src/charts/footprint.rs

@@ -1,10 +1,12 @@
 use std::collections::{BTreeMap, HashMap};
 
 use iced::widget::canvas::{LineDash, Path, Stroke};
-use iced::{mouse, Alignment, Element, Point, Rectangle, Renderer, Size, Task, Theme, Vector};
+use iced::widget::container;
+use iced::{mouse, Alignment, Element, Length, Point, Rectangle, Renderer, Size, Task, Theme, Vector};
 use iced::widget::{column, canvas::{self, Event, Geometry}};
 use ordered_float::OrderedFloat;
 
+use crate::data_providers::TickerInfo;
 use crate::screen::UserTimezone;
 use crate::data_providers::{
     fetcher::{FetchRange, RequestHandler},
@@ -43,8 +45,12 @@ impl Chart for FootprintChart {
         canvas_interaction(self, interaction, event, bounds, cursor)
     }
 
-    fn view_indicator<I: Indicator>(&self, indicators: &[I]) -> Element<Message> {
-        self.view_indicators(indicators)
+    fn view_indicator<I: Indicator>(
+        &self, 
+        indicators: &[I], 
+        ticker_info: Option<TickerInfo>
+    ) -> Option<Element<Message>> {
+        self.view_indicators(indicators, ticker_info)
     }
 
     fn get_visible_timerange(&self) -> (i64, i64) {
@@ -61,16 +67,16 @@ impl Chart for FootprintChart {
 }
 
 #[allow(dead_code)]
-enum Indicators {
+enum IndicatorData {
     Volume(Caches, BTreeMap<i64, (f32, f32)>),
     OpenInterest(Caches, BTreeMap<i64, f32>),
 }
 
-impl Indicators {
+impl IndicatorData {
     fn clear_cache(&mut self) {
         match self {
-            Indicators::Volume(caches, _) 
-            | Indicators::OpenInterest(caches, _) => {
+            IndicatorData::Volume(caches, _) 
+            | IndicatorData::OpenInterest(caches, _) => {
                 caches.clear_all();
             }
         }
@@ -94,8 +100,9 @@ pub struct FootprintChart {
     chart: CommonChartData,
     data_points: BTreeMap<i64, (HashMap<OrderedFloat<f32>, (f32, f32)>, Kline)>,
     raw_trades: Vec<Trade>,
-    indicators: Vec<Indicators>,
+    indicators: HashMap<FootprintIndicator, IndicatorData>,
     fetching_oi: bool,
+    fetching_trades: bool,
     request_handler: RequestHandler,
 }
 
@@ -106,6 +113,7 @@ impl FootprintChart {
         klines_raw: Vec<Kline>,
         raw_trades: Vec<Trade>,
         timezone: UserTimezone,
+        enabled_indicators: &[FootprintIndicator],
     ) -> Self {
         let mut data_points = BTreeMap::new();
         let mut volume_data = BTreeMap::new();
@@ -173,44 +181,28 @@ impl FootprintChart {
             },
             data_points,
             raw_trades,
-            indicators: vec![
-                Indicators::Volume(Caches::default(), volume_data.clone()),
-                Indicators::OpenInterest(Caches::default(), BTreeMap::new()),
-            ],
-            fetching_oi: false,
-            request_handler: RequestHandler::new(),
-        }
-    }
-
-    pub fn insert_datapoint(&mut self, trades_buffer: &[Trade], depth_update: i64) {
-        let (tick_size, aggregate_time) = {
-            let chart = self.get_common_data();
-            (chart.tick_size, chart.timeframe as i64)
-        };
-
-        let rounded_depth_update = (depth_update / aggregate_time) * aggregate_time;
-
-        self.data_points
-            .entry(rounded_depth_update)
-            .or_insert((HashMap::new(), Kline::default()));
-
-        for trade in trades_buffer {
-            let price_level = OrderedFloat(round_to_tick(trade.price, tick_size));
-            if let Some((trades, _)) = self.data_points.get_mut(&rounded_depth_update) {
-                if let Some((buy_qty, sell_qty)) = trades.get_mut(&price_level) {
-                    if trade.is_sell {
-                        *sell_qty += trade.qty;
-                    } else {
-                        *buy_qty += trade.qty;
-                    }
-                } else if trade.is_sell {
-                    trades.insert(price_level, (0.0, trade.qty));
-                } else {
-                    trades.insert(price_level, (trade.qty, 0.0));
+            indicators: {
+                let mut indicators = HashMap::new();
+
+                for indicator in enabled_indicators {
+                    indicators.insert(
+                        *indicator,
+                        match indicator {
+                            FootprintIndicator::Volume => {
+                                IndicatorData::Volume(Caches::default(), volume_data.clone())
+                            },
+                            FootprintIndicator::OpenInterest => {
+                                IndicatorData::OpenInterest(Caches::default(), BTreeMap::new())
+                            }
+                        }
+                    );
                 }
-            }
 
-            self.raw_trades.push(*trade);
+                indicators
+            },
+            fetching_oi: false,
+            fetching_trades: false,
+            request_handler: RequestHandler::new(),
         }
     }
 
@@ -226,11 +218,10 @@ impl FootprintChart {
                 .insert(kline.time as i64, (HashMap::new(), *kline));
         }
 
-        self.indicators.iter_mut().for_each(|indicator| {
-            if let Indicators::Volume(_, data) = indicator {
+        if let Some(IndicatorData::Volume(_, data)) = 
+            self.indicators.get_mut(&FootprintIndicator::Volume) {
                 data.insert(kline.time as i64, (kline.volume.0, kline.volume.1));
-            }
-        });
+            };
 
         let chart = self.get_common_data_mut();
 
@@ -273,8 +264,28 @@ impl FootprintChart {
             }
         }
 
-        for indicator in &self.indicators {
-            if let Indicators::OpenInterest(_, _) = indicator {
+        if !self.fetching_trades {
+            let (kline_earliest, _) = self.get_trades_timerange(kline_latest);
+
+            if visible_earliest < kline_earliest {
+                let trade_earliest = self.raw_trades.iter()
+                    .filter(|trade| trade.time >= kline_earliest)
+                    .map(|trade| trade.time)
+                    .min();
+            
+                if let Some(earliest) = trade_earliest {
+                    if let Some(task) = request_fetch(
+                        &mut self.request_handler, FetchRange::Trades(visible_earliest, earliest)
+                    ) {
+                        self.fetching_trades = true;
+                        return task;
+                    }
+                }
+            }
+        }
+
+        for data in self.indicators.values() {
+            if let IndicatorData::OpenInterest(_, _) = data {
                 if !self.fetching_oi {
                     let (oi_earliest, oi_latest) = self.get_oi_timerange(kline_latest);
 
@@ -307,6 +318,9 @@ impl FootprintChart {
 
     pub fn reset_request_handler(&mut self) {
         self.request_handler = RequestHandler::new();
+        self.fetching_trades = false;
+        self.fetching_oi = false;
+        self.chart.already_fetching = false;
     }
 
     pub fn change_timezone(&mut self, timezone: UserTimezone) {
@@ -318,6 +332,56 @@ impl FootprintChart {
         self.raw_trades.clone()
     }
 
+    pub fn clear_trades(&mut self, clear_raw: bool) {
+        self.data_points.iter_mut().for_each(|(_, (trades, _))| {
+            trades.clear();
+        });
+
+        if clear_raw {
+            self.raw_trades.clear();
+        } else {
+            let aggregate_time = self.chart.timeframe as i64;
+            let tick_size = self.chart.tick_size;
+
+            for trade in &self.raw_trades {
+                let rounded_time = (trade.time / aggregate_time) * aggregate_time;
+                let price_level = OrderedFloat(round_to_tick(trade.price, tick_size));
+    
+                let entry = self.data_points
+                    .entry(rounded_time)
+                    .or_insert((HashMap::new(), Kline::default()));
+    
+                if let Some((buy_qty, sell_qty)) = entry.0.get_mut(&price_level) {
+                    if trade.is_sell {
+                        *sell_qty += trade.qty;
+                    } else {
+                        *buy_qty += trade.qty;
+                    }
+                } else if trade.is_sell {
+                    entry.0.insert(price_level, (0.0, trade.qty));
+                } else {
+                    entry.0.insert(price_level, (trade.qty, 0.0));
+                }
+            }
+        }
+    }
+
+    pub fn get_tick_size(&self) -> f32 {
+        self.chart.tick_size
+    }
+
+    pub fn change_tick_size(&mut self, new_tick_size: f32) {
+        let chart = self.get_common_data_mut();
+        let old_tick_size = chart.tick_size;
+
+        chart.base_range *= new_tick_size / old_tick_size;
+        chart.cell_height *= new_tick_size / old_tick_size;
+
+        chart.tick_size = new_tick_size;
+
+        self.clear_trades(false);
+    }
+
     fn get_kline_timerange(&self) -> (i64, i64) {
         let mut from_time = i64::MAX;
         let mut to_time = i64::MIN;
@@ -334,18 +398,96 @@ impl FootprintChart {
         let mut from_time = latest_kline;
         let mut to_time = i64::MIN;
 
-        self.indicators.iter().for_each(|indicator| {
-            if let Indicators::OpenInterest(_, data) = indicator {
+        if let Some(IndicatorData::OpenInterest(_, data)) = 
+            self.indicators.get(&FootprintIndicator::OpenInterest) {
                 data.iter().for_each(|(time, _)| {
                     from_time = from_time.min(*time);
                     to_time = to_time.max(*time);
                 });
-            }
-        });
+            };
 
         (from_time, to_time)
     }
 
+    fn get_trades_timerange(&self, latest_kline: i64) -> (i64, i64) {
+        let mut from_time = latest_kline;
+        let mut to_time = 0;
+
+        self.data_points
+            .iter()
+            .filter(|(_, (trades, _))| !trades.is_empty())
+            .for_each(|(time, _)| {
+                from_time = from_time.min(*time);
+                to_time = to_time.max(*time);
+            });
+
+        (from_time, to_time)
+    }
+
+    pub fn insert_datapoint(&mut self, trades_buffer: &[Trade], depth_update: i64) {
+        let (tick_size, aggregate_time) = {
+            let chart = self.get_common_data();
+            (chart.tick_size, chart.timeframe as i64)
+        };
+
+        let rounded_depth_update = (depth_update / aggregate_time) * aggregate_time;
+
+        self.data_points
+            .entry(rounded_depth_update)
+            .or_insert((HashMap::new(), Kline::default()));
+
+        for trade in trades_buffer {
+            let price_level = OrderedFloat(round_to_tick(trade.price, tick_size));
+            if let Some((trades, _)) = self.data_points.get_mut(&rounded_depth_update) {
+                if let Some((buy_qty, sell_qty)) = trades.get_mut(&price_level) {
+                    if trade.is_sell {
+                        *sell_qty += trade.qty;
+                    } else {
+                        *buy_qty += trade.qty;
+                    }
+                } else if trade.is_sell {
+                    trades.insert(price_level, (0.0, trade.qty));
+                } else {
+                    trades.insert(price_level, (trade.qty, 0.0));
+                }
+            }
+        }
+
+        self.raw_trades.extend_from_slice(trades_buffer);
+    }
+
+    pub fn insert_trades(&mut self, raw_trades: Vec<Trade>, is_batches_done: bool) {
+        let aggregate_time = self.chart.timeframe as i64;
+        let tick_size = self.chart.tick_size;
+
+        for trade in &raw_trades {
+            let rounded_time = (trade.time / aggregate_time) * aggregate_time;
+            let price_level = OrderedFloat(round_to_tick(trade.price, tick_size));
+
+            let entry = self.data_points
+                .entry(rounded_time)
+                .or_insert((HashMap::new(), Kline::default()));
+
+            if let Some((buy_qty, sell_qty)) = entry.0.get_mut(&price_level) {
+                if trade.is_sell {
+                    *sell_qty += trade.qty;
+                } else {
+                    *buy_qty += trade.qty;
+                }
+            } else if trade.is_sell {
+                entry.0.insert(price_level, (0.0, trade.qty));
+            } else {
+                entry.0.insert(price_level, (trade.qty, 0.0));
+            }
+        }
+
+        self.raw_trades.extend(raw_trades);
+
+        if is_batches_done {
+            self.fetching_trades = false;
+        }
+    }
+
     pub fn insert_new_klines(&mut self, req_id: uuid::Uuid, klines_raw: &Vec<Kline>) {
         let mut volume_data = BTreeMap::new();
 
@@ -356,11 +498,10 @@ impl FootprintChart {
                 .or_insert((HashMap::new(), *kline));
         }
 
-        self.indicators.iter_mut().for_each(|indicator| {
-            if let Indicators::Volume(_, data) = indicator {
+        if let Some(IndicatorData::Volume(_, data)) = 
+            self.indicators.get_mut(&FootprintIndicator::Volume) {
                 data.extend(volume_data.clone());
-            }
-        });
+            };
 
         if klines_raw.len() > 1 {
             self.request_handler.mark_completed(req_id);
@@ -375,42 +516,16 @@ impl FootprintChart {
     }
 
     pub fn insert_open_interest(&mut self, _req_id: Option<uuid::Uuid>, oi_data: Vec<OIData>) {
-        self.indicators.iter_mut().for_each(|indicator| {
-            if let Indicators::OpenInterest(_, data) = indicator {
+        if let Some(IndicatorData::OpenInterest(_, data)) = 
+            self.indicators.get_mut(&FootprintIndicator::OpenInterest) {
                 data.extend(oi_data
                     .iter().map(|oi| (oi.time, oi.value))
                 );
-            }
-        });
+            };
     
         self.fetching_oi = false;
     }
 
-    pub fn get_tick_size(&self) -> f32 {
-        self.chart.tick_size
-    }
-
-    pub fn clear_trades(&mut self, clear_raw: bool) {
-        if clear_raw {
-            self.raw_trades.clear();
-        }
-        self.data_points.iter_mut().for_each(|(_, (trades, _))| {
-            trades.clear();
-        });
-    }
-
-    pub fn change_tick_size(&mut self, new_tick_size: f32) {
-        self.clear_trades(true);
-
-        let chart = self.get_common_data_mut();
-        let old_tick_size = chart.tick_size;
-
-        chart.base_range *= new_tick_size / old_tick_size;
-        chart.cell_height *= new_tick_size / old_tick_size;
-
-        chart.tick_size = new_tick_size;
-    }
-
     fn calc_qty_scales(
         &self,
         earliest: i64,
@@ -460,32 +575,41 @@ impl FootprintChart {
 
         chart_state.cache.clear_all();
 
-        self.indicators.iter_mut().for_each(|indicator| {
-            indicator.clear_cache();
+        self.indicators.iter_mut().for_each(|(_, data)| {
+            data.clear_cache();
         });
     }
 
-    fn get_volume_indicator(&self) -> Option<(&Caches, &BTreeMap<i64, (f32, f32)>)> {
-        for indicator in &self.indicators {
-            if let Indicators::Volume(cache, data) = indicator {
-                return Some((cache, data));
-            }
-        }
-
-        None
-    }
-
-    fn get_oi_indicator(&self) -> Option<(&Caches, &BTreeMap<i64, f32>)> {
-        for indicator in &self.indicators {
-            if let Indicators::OpenInterest(cache, data) = indicator {
-                return Some((cache, data));
+    pub fn toggle_indicator(&mut self, indicator: FootprintIndicator) {
+        if self.indicators.contains_key(&indicator) {
+            self.indicators.remove(&indicator);
+        } else {
+            match indicator {
+                FootprintIndicator::Volume => {
+                    let data = self.data_points.iter()
+                        .map(|(time, (_, kline))| (*time, (kline.volume.0, kline.volume.1)))
+                        .collect();
+
+                    self.indicators.insert(
+                        indicator,
+                        IndicatorData::Volume(Caches::default(), data)
+                    );
+                },
+                FootprintIndicator::OpenInterest => {
+                    self.indicators.insert(
+                        indicator,
+                        IndicatorData::OpenInterest(Caches::default(), BTreeMap::new())
+                    );
+                }
             }
         }
-
-        None
     }
 
-    pub fn view_indicators<I: Indicator>(&self, enabled: &[I]) -> Element<Message> {
+    pub fn view_indicators<I: Indicator>(
+        &self, 
+        enabled: &[I], 
+        ticker_info: Option<TickerInfo>
+    ) -> Option<Element<Message>> {
         let chart_state: &CommonChartData = self.get_common_data();
 
         let mut indicators: iced::widget::Column<'_, Message> = column![];
@@ -495,39 +619,53 @@ impl FootprintChart {
         let earliest = chart_state.x_to_time(visible_region.x);
         let latest = chart_state.x_to_time(visible_region.x + visible_region.width);
 
-        for indicator in I::get_enabled(enabled) {
+        for indicator in I::get_enabled(
+            enabled, 
+            ticker_info.map(|info| info.market_type)
+        ) {
             if let Some(candlestick_indicator) = indicator
                 .as_any()
                 .downcast_ref::<FootprintIndicator>() 
             {
                 match candlestick_indicator {
                     FootprintIndicator::Volume => {
-                        if let Some((cache, data)) = self.get_volume_indicator() {
-                            indicators = indicators.push(
-                                indicators::volume::create_indicator_elem(chart_state, cache, data, earliest, latest)
-                            );
-                        }
+                        if let Some(IndicatorData::Volume(cache, data)) = 
+                            self.indicators.get(&FootprintIndicator::Volume) {
+                                indicators = indicators.push(
+                                    indicators::volume::create_indicator_elem(chart_state, cache, data, earliest, latest)
+                                );
+                            }
                     },
                     FootprintIndicator::OpenInterest => {
-                        if let Some((cache, data)) = self.get_oi_indicator() {
-                            indicators = indicators.push(
-                                indicators::open_interest::create_indicator_elem(chart_state, cache, data, earliest, latest)
-                            );
-                        }
+                        if let Some(IndicatorData::OpenInterest(cache, data)) = 
+                            self.indicators.get(&FootprintIndicator::OpenInterest) {
+                                indicators = indicators.push(
+                                    indicators::open_interest::create_indicator_elem(chart_state, cache, data, earliest, latest)
+                                );
+                            }
                     }
                 }
             }
         }
 
-        indicators.into()
+        Some(
+            container(indicators)
+                .width(Length::FillPortion(10))
+                .height(Length::FillPortion(chart_state.indicators_height))
+                .into()
+        )
     }
 
     pub fn update(&mut self, message: &Message) -> Task<Message> {
         self.update_chart(message)
     }
 
-    pub fn view<'a, I: Indicator>(&'a self, indicators: &'a [I]) -> Element<Message> {
-        view_chart(self, indicators)
+    pub fn view<'a, I: Indicator>(
+        &'a self, 
+        indicators: &'a [I], 
+        ticker_info: Option<TickerInfo>
+    ) -> Element<Message> {
+        view_chart(self, indicators, ticker_info)
     }
 }
 
@@ -709,10 +847,10 @@ impl canvas::Program<Message> for FootprintChart {
                     );
 
                 // last price line
-                chart.last_price.map(|price| {
+                if let Some(price) = &chart.last_price {
                     let (line_color, y_pos) = match price {
-                        PriceInfoLabel::Up(p) => (palette.success.weak.color, chart.price_to_y(p)),
-                        PriceInfoLabel::Down(p) => (palette.danger.weak.color, chart.price_to_y(p)),
+                        PriceInfoLabel::Up(p) => (palette.success.weak.color, chart.price_to_y(*p)),
+                        PriceInfoLabel::Down(p) => (palette.danger.weak.color, chart.price_to_y(*p)),
                     };
 
                     let marker_line = Stroke::with_color(
@@ -734,7 +872,7 @@ impl canvas::Program<Message> for FootprintChart {
                         ),
                         marker_line,
                     );
-                });
+                };
             });
         });
 

+ 82 - 42
src/charts/heatmap.rs

@@ -1,18 +1,19 @@
-use std::{cmp::Ordering, collections::BTreeMap};
+use std::{cmp::Ordering, collections::{BTreeMap, HashMap}};
 
 use iced::{
-    mouse, theme::palette::Extended, Alignment, Color, Element, Point, Rectangle, Renderer, Size,
-    Theme, Vector, Task
+    mouse, theme::palette::Extended, Alignment, Color, Element, 
+    Point, Rectangle, Renderer, Size, Task, Theme, Vector
 };
-use iced::widget::{column, canvas::{self, Event, Geometry, Path}};
+use iced::widget::canvas::{self, Event, Geometry, Path};
 
+use crate::data_providers::TickerInfo;
 use crate::{
     data_providers::{Depth, Trade},
     screen::UserTimezone,
 };
 
-use super::indicators::Indicator;
-use super::{Caches, Chart, ChartConstants, CommonChartData, Interaction, Message};
+use super::indicators::{HeatmapIndicator, Indicator};
+use super::{Chart, ChartConstants, CommonChartData, Interaction, Message};
 use super::{canvas_interaction, view_chart, update_chart, count_decimals, convert_to_qty_abbr};
 
 use ordered_float::OrderedFloat;
@@ -43,8 +44,12 @@ impl Chart for HeatmapChart {
         canvas_interaction(self, interaction, event, bounds, cursor)
     }
 
-    fn view_indicator<I: Indicator>(&self, indicators: &[I]) -> Element<Message> {
-        self.view_indicators(indicators)
+    fn view_indicator<I: Indicator>(
+        &self, 
+        indicators: &[I], 
+        ticker_info: Option<TickerInfo>
+    ) -> Option<Element<Message>> {
+        self.view_indicators(indicators, ticker_info)
     }
 
     fn get_visible_timerange(&self) -> (i64, i64) {
@@ -221,22 +226,21 @@ pub struct GroupedTrade {
 }
 
 #[allow(dead_code)]
-enum Indicators {
-    Volume(Caches, BTreeMap<i64, (f32, f32)>),
-    Spread(Caches, BTreeMap<i64, f32>),
+enum IndicatorData {
+    Volume,
 }
 
 pub struct HeatmapChart {
     chart: CommonChartData,
     data_points: Vec<(i64, Box<[GroupedTrade]>, (f32, f32))>,
-    indicators: Vec<Indicators>,
+    indicators: HashMap<HeatmapIndicator, IndicatorData>,
     orderbook: Orderbook,
     trade_size_filter: f32,
     order_size_filter: f32,
 }
 
 impl HeatmapChart {
-    pub fn new(tick_size: f32, aggr_time: i64, timezone: UserTimezone) -> Self {
+    pub fn new(tick_size: f32, aggr_time: i64, timezone: UserTimezone, enabled_indicators: &[HeatmapIndicator]) -> Self {
         HeatmapChart {
             chart: CommonChartData {
                 cell_width: Self::DEFAULT_CELL_WIDTH,
@@ -247,11 +251,26 @@ impl HeatmapChart {
                 timezone,
                 ..Default::default()
             },
+            indicators: {
+                let mut indicators = HashMap::new();
+
+                for indicator in enabled_indicators {
+                    indicators.insert(
+                        *indicator,
+                        match indicator {
+                            HeatmapIndicator::Volume => {
+                                IndicatorData::Volume
+                            },
+                        }
+                    );
+                }
+
+                indicators
+            },
             orderbook: Orderbook::new(tick_size, aggr_time),
             data_points: Vec::new(),
             trade_size_filter: 0.0,
             order_size_filter: 0.0,
-            indicators: vec![],
         }
     }
 
@@ -379,6 +398,21 @@ impl HeatmapChart {
         self.orderbook = Orderbook::new(new_tick_size, aggr_time);
     }
 
+    pub fn toggle_indicator(&mut self, indicator: HeatmapIndicator) {
+        if self.indicators.contains_key(&indicator) {
+            self.indicators.remove(&indicator);
+        } else {
+            match indicator {
+                HeatmapIndicator::Volume => {
+                    self.indicators.insert(
+                        indicator,
+                        IndicatorData::Volume,
+                    );
+                },
+            }
+        }
+    }
+
     fn render_start(&mut self) {
         let chart_state = self.get_common_data_mut();
 
@@ -447,18 +481,20 @@ impl HeatmapChart {
         }
     }
 
-    /// gonna have to implement this later
-    pub fn view_indicators<I: Indicator>(&self, _indis: &[I]) -> Element<Message> {
-        let indicators: iced::widget::Column<'_, Message> = column![];
-        indicators.into()
+    pub fn view_indicators<I: Indicator>(&self, _indis: &[I], _ticker_info: Option<TickerInfo>) -> Option<Element<Message>> {
+        None
     }
 
     pub fn update(&mut self, message: &Message) -> Task<Message> {
         self.update_chart(message)
     }
 
-    pub fn view<'a, I: Indicator>(&'a self, indicators: &'a [I]) -> Element<Message> {
-        view_chart(self, indicators)
+    pub fn view<'a, I: Indicator>(
+        &'a self, 
+        indicators: &'a [I], 
+        ticker_info: Option<TickerInfo>
+    ) -> Element<Message> {
+        view_chart(self, indicators, ticker_info)
     }
 }
 
@@ -493,6 +529,8 @@ impl canvas::Program<Message> for HeatmapChart {
         let bounds_size = bounds.size();
 
         let palette = theme.extended_palette();
+        
+        let volume_indicator = self.indicators.contains_key(&HeatmapIndicator::Volume);
 
         let heatmap = chart.cache.main.draw(renderer, bounds_size, |frame| {
             frame.with_save(|frame| {
@@ -638,31 +676,33 @@ impl canvas::Program<Message> for HeatmapChart {
                             }
                         });
 
-                        let bar_width = (chart.cell_width / 2.0) * 0.9;
-
-                        let buy_bar_height =
-                            (buy_volume / max_aggr_volume) * (bounds.height / chart.scaling) * 0.1;
-                        let sell_bar_height =
-                            (sell_volume / max_aggr_volume) * (bounds.height / chart.scaling) * 0.1;
-
-                        frame.fill_rectangle(
-                            Point::new(x_position, (region.y + region.height) - buy_bar_height),
-                            Size::new(bar_width, buy_bar_height),
-                            palette.success.base.color,
-                        );
-
-                        frame.fill_rectangle(
-                            Point::new(
-                                x_position - bar_width,
-                                (region.y + region.height) - sell_bar_height,
-                            ),
-                            Size::new(bar_width, sell_bar_height),
-                            palette.danger.base.color,
-                        );
+                        if volume_indicator {
+                            let bar_width = (chart.cell_width / 2.0) * 0.9;
+
+                            let buy_bar_height =
+                                (buy_volume / max_aggr_volume) * (bounds.height / chart.scaling) * 0.1;
+                            let sell_bar_height =
+                                (sell_volume / max_aggr_volume) * (bounds.height / chart.scaling) * 0.1;
+
+                            frame.fill_rectangle(
+                                Point::new(x_position, (region.y + region.height) - buy_bar_height),
+                                Size::new(bar_width, buy_bar_height),
+                                palette.success.base.color,
+                            );
+
+                            frame.fill_rectangle(
+                                Point::new(
+                                    x_position - bar_width,
+                                    (region.y + region.height) - sell_bar_height,
+                                ),
+                                Size::new(bar_width, sell_bar_height),
+                                palette.danger.base.color,
+                            );
+                        }
                     },
                 );
 
-                if max_aggr_volume > 0.0 {
+                if volume_indicator && max_aggr_volume > 0.0 {
                     let text_size = 9.0 / chart.scaling;
                     let text_content = convert_to_qty_abbr(max_aggr_volume);
                     let text_width = (text_content.len() as f32 * text_size) / 1.5;

+ 34 - 15
src/charts/indicators.rs

@@ -5,13 +5,16 @@ use std::{any::Any, fmt::{self, Debug, Display}};
 
 use serde::{Deserialize, Serialize};
 
+use crate::data_providers::MarketType;
+
 pub trait Indicator: PartialEq + Display + ToString + Debug + 'static  {
-    fn get_available() -> &'static [Self] where Self: Sized;
-    fn get_enabled(indicators: &[Self]) -> impl Iterator<Item = &Self> 
+    fn get_available(market_type: Option<MarketType>) -> &'static [Self] where Self: Sized;
+    
+    fn get_enabled(indicators: &[Self], market_type: Option<MarketType>) -> impl Iterator<Item = &Self> 
     where
         Self: Sized,
     {
-        Self::get_available()
+        Self::get_available(market_type)
             .iter()
             .filter(move |indicator| indicators.contains(indicator))
     }
@@ -19,15 +22,19 @@ pub trait Indicator: PartialEq + Display + ToString + Debug + 'static  {
 }
 
 /// Candlestick chart indicators
-#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
+#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, Eq, Hash)]
 pub enum CandlestickIndicator {
     Volume,
     OpenInterest,
 }
 
 impl Indicator for CandlestickIndicator {
-    fn get_available() -> &'static [Self] {
-        &Self::ALL
+    fn get_available(market_type: Option<MarketType>) -> &'static [Self] {
+        match market_type {
+            Some(MarketType::Spot) => &Self::SPOT,
+            Some(MarketType::LinearPerps) => &Self::PERPS,
+            _ => &Self::ALL,
+        }
     }
 
     fn as_any(&self) -> &dyn Any {
@@ -37,6 +44,8 @@ impl Indicator for CandlestickIndicator {
 
 impl CandlestickIndicator {
     const ALL: [CandlestickIndicator; 2] = [CandlestickIndicator::Volume, CandlestickIndicator::OpenInterest];
+    const SPOT: [CandlestickIndicator; 1] = [CandlestickIndicator::Volume];
+    const PERPS: [CandlestickIndicator; 2] = [CandlestickIndicator::Volume, CandlestickIndicator::OpenInterest];
 }
 
 impl Display for CandlestickIndicator {
@@ -49,15 +58,18 @@ impl Display for CandlestickIndicator {
 }
 
 /// Heatmap chart indicators
-#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
+#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, Eq, Hash)]
 pub enum HeatmapIndicator {
     Volume,
-    Spread,
 }
 
 impl Indicator for HeatmapIndicator {
-    fn get_available() -> &'static [Self] {
-        &Self::ALL
+    fn get_available(market_type: Option<MarketType>) -> &'static [Self] {
+        match market_type {
+            Some(MarketType::Spot) => &Self::SPOT,
+            Some(MarketType::LinearPerps) => &Self::PERPS,
+            _ => &Self::ALL,
+        }
     }
 
     fn as_any(&self) -> &dyn Any {
@@ -66,28 +78,33 @@ impl Indicator for HeatmapIndicator {
 }
 
 impl HeatmapIndicator {
-    const ALL: [HeatmapIndicator; 2] = [HeatmapIndicator::Volume, HeatmapIndicator::Spread];
+    const ALL: [HeatmapIndicator; 1] = [HeatmapIndicator::Volume];
+    const SPOT: [HeatmapIndicator; 1] = [HeatmapIndicator::Volume];
+    const PERPS: [HeatmapIndicator; 1] = [HeatmapIndicator::Volume];
 }
 
 impl Display for HeatmapIndicator {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         match self {
             HeatmapIndicator::Volume => write!(f, "Volume"),
-            HeatmapIndicator::Spread => write!(f, "Spread"),
         }
     }
 }
 
 /// Footprint chart indicators
-#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
+#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, Eq, Hash)]
 pub enum FootprintIndicator {
     Volume,
     OpenInterest,
 }
 
 impl Indicator for FootprintIndicator {
-    fn get_available() -> &'static [Self] {
-        &Self::ALL
+    fn get_available(market_type: Option<MarketType>) -> &'static [Self] {
+        match market_type {
+            Some(MarketType::Spot) => &Self::SPOT,
+            Some(MarketType::LinearPerps) => &Self::PERPS,
+            _ => &Self::ALL,
+        }
     }
 
     fn as_any(&self) -> &dyn Any {
@@ -97,6 +114,8 @@ impl Indicator for FootprintIndicator {
 
 impl FootprintIndicator {
     const ALL: [FootprintIndicator; 2] = [FootprintIndicator::Volume, FootprintIndicator::OpenInterest];
+    const SPOT: [FootprintIndicator; 1] = [FootprintIndicator::Volume];
+    const PERPS: [FootprintIndicator; 2] = [FootprintIndicator::Volume, FootprintIndicator::OpenInterest];
 }
 
 impl Display for FootprintIndicator {

+ 9 - 6
src/data_providers.rs

@@ -52,6 +52,7 @@ pub enum StreamError {
 pub struct TickerInfo {
     #[serde(rename = "tickSize")]
     pub tick_size: f32,
+    pub market_type: MarketType,
 }
 
 #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
@@ -215,7 +216,9 @@ impl TickMultiplier {
     /// Returns the final tick size after applying the user selected multiplier
     ///
     /// Usually used for price steps in chart scales
-    pub fn multiply_with_min_tick_size(&self, min_tick_size: f32) -> f32 {
+    pub fn multiply_with_min_tick_size(&self, ticker_info: TickerInfo) -> f32 {
+        let min_tick_size = ticker_info.tick_size;
+
         let multiplier = if let Some(m) = Decimal::from_f32(f32::from(self.0)) {
             m
         } else {
@@ -279,11 +282,11 @@ impl std::fmt::Display for Exchange {
     }
 }
 impl Exchange {
-    pub const ALL: [Exchange; 4] = [
-        Exchange::BinanceFutures,
-        Exchange::BybitLinear,
-        Exchange::BybitSpot,
-        Exchange::BinanceSpot,
+    pub const MARKET_TYPES: [(Exchange, MarketType); 4] = [
+        (Exchange::BinanceFutures, MarketType::LinearPerps),
+        (Exchange::BybitLinear, MarketType::LinearPerps),
+        (Exchange::BinanceSpot, MarketType::Spot),
+        (Exchange::BybitSpot, MarketType::Spot),
     ];
 }
 

+ 228 - 10
src/data_providers/binance.rs

@@ -1,5 +1,5 @@
-use std::collections::HashMap;
-
+use std::{collections::HashMap, io::BufReader};
+use csv::ReaderBuilder;
 use fastwebsockets::{FragmentCollector, OpCode};
 use ::futures::{SinkExt, Stream};
 use hyper::upgrade::Upgraded;
@@ -10,10 +10,9 @@ use serde::{Deserialize, Serialize};
 use sonic_rs::{to_object_iter_unchecked, FastStr};
 
 use super::{
-    deserialize_string_to_f32,
-    setup_tcp_connection, setup_tls_connection, setup_websocket_connection, 
-    Connection, Event, Kline, LocalDepthCache, MarketType, OpenInterest, Order, State, 
-    StreamError, Ticker, TickerInfo, TickerStats, Timeframe, Trade, VecLocalDepthCache,
+    deserialize_string_to_f32, setup_tcp_connection, setup_tls_connection, setup_websocket_connection, 
+    Connection, Event, Kline, LocalDepthCache, MarketType, OpenInterest, Order, State, StreamError,
+    Ticker, TickerInfo, TickerStats, Timeframe, Trade, VecLocalDepthCache,
 };
 
 async fn connect(
@@ -717,8 +716,8 @@ pub async fn fetch_klines(
     Ok(klines)
 }
 
-pub async fn fetch_ticksize(market: MarketType) -> Result<HashMap<Ticker, Option<TickerInfo>>, StreamError> {
-    let url = match market {
+pub async fn fetch_ticksize(market_type: MarketType) -> Result<HashMap<Ticker, Option<TickerInfo>>, StreamError> {
+    let url = match market_type {
         MarketType::Spot => "https://api.binance.com/api/v3/exchangeInfo".to_string(),
         MarketType::LinearPerps => "https://fapi.binance.com/fapi/v1/exchangeInfo".to_string(),
     };
@@ -728,6 +727,25 @@ pub async fn fetch_ticksize(market: MarketType) -> Result<HashMap<Ticker, Option
     let exchange_info: serde_json::Value = serde_json::from_str(&text)
         .map_err(|e| StreamError::ParseError(format!("Failed to parse exchange info: {e}")))?;
 
+    let rate_limits = exchange_info["rateLimits"]
+        .as_array()
+        .ok_or_else(|| StreamError::ParseError("Missing rateLimits array".to_string()))?;
+
+    let request_limit = rate_limits
+        .iter()
+        .find(|x| x["rateLimitType"].as_str().unwrap_or_default() == "REQUEST_WEIGHT")
+        .and_then(|x| x["limit"].as_i64())
+        .ok_or_else(|| StreamError::ParseError("Missing request weight limit".to_string()))?;
+
+    log::info!(
+        "Binance req. weight limit per minute {}: {:?}", 
+        match market_type {
+            MarketType::Spot => "Spot",
+            MarketType::LinearPerps => "Linear Perps",
+        },
+        request_limit
+    );
+
     let symbols = exchange_info["symbols"]
         .as_array()
         .ok_or_else(|| StreamError::ParseError("Missing symbols array".to_string()))?;
@@ -765,9 +783,9 @@ pub async fn fetch_ticksize(market: MarketType) -> Result<HashMap<Ticker, Option
                 .parse::<f32>()
                 .map_err(|e| StreamError::ParseError(format!("Failed to parse tickSize: {e}")))?;
 
-            ticker_info_map.insert(Ticker::new(ticker, market), Some(TickerInfo { tick_size }));
+            ticker_info_map.insert(Ticker::new(ticker, market_type), Some(TickerInfo { tick_size, market_type }));
         } else {
-            ticker_info_map.insert(Ticker::new(ticker, market), None);
+            ticker_info_map.insert(Ticker::new(ticker, market_type), None);
         }
     }
 
@@ -832,6 +850,206 @@ pub async fn fetch_ticker_prices(market: MarketType) -> Result<HashMap<Ticker, T
     Ok(ticker_price_map)
 }
 
+async fn handle_rate_limit(headers: &hyper::HeaderMap, max_limit: f32) -> Result<(), StreamError> {
+    let weight = headers
+        .get("x-mbx-used-weight-1m")
+        .ok_or_else(|| StreamError::ParseError("Missing rate limit header".to_string()))?
+        .to_str()
+        .map_err(|e| StreamError::ParseError(format!("Invalid header value: {e}")))?
+        .parse::<i32>()
+        .map_err(|e| StreamError::ParseError(format!("Invalid weight value: {e}")))?;
+
+    let usage_percentage = (weight as f32 / max_limit) * 100.0;
+
+    match usage_percentage {
+        p if p >= 95.0 => {
+            log::warn!("Rate limit critical ({:.1}%), sleeping for 10s", p);
+            tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
+        }
+        p if p >= 90.0 => {
+            log::warn!("Rate limit high ({:.1}%), sleeping for 5s", p);
+            tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
+        }
+        p if p >= 80.0 => {
+            log::warn!("Rate limit warning ({:.1}%), sleeping for 3s", p);
+            tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
+        }
+        _ => (),
+    }
+
+    Ok(())
+}
+
+pub async fn fetch_trades(
+    ticker: Ticker, 
+    from_time: i64,
+) -> Result<Vec<Trade>, StreamError> {
+    let today_midnight = chrono::Utc::now()
+        .date_naive()
+        .and_hms_opt(0, 0, 0)
+        .unwrap()
+        .and_utc();
+    
+    if from_time >= today_midnight.timestamp_millis() {
+        return fetch_intraday_trades(ticker, from_time).await;
+    }
+
+    let from_date = chrono::DateTime::from_timestamp_millis(from_time)
+        .ok_or_else(|| StreamError::ParseError("Invalid timestamp".into()))?
+        .date_naive();
+
+    match get_hist_trades(ticker, from_date).await {
+        Ok(trades) => Ok(trades),
+        Err(e) => {
+            log::warn!("Historical trades fetch failed: {}, falling back to intraday fetch", e);
+            fetch_intraday_trades(ticker, from_time).await
+        }
+    }
+}
+
+pub async fn fetch_intraday_trades(
+    ticker: Ticker,
+    from: i64,
+) -> Result<Vec<Trade>, StreamError> {
+    let (symbol_str, market_type) = ticker.get_string();
+    let base_url = match market_type {
+        MarketType::Spot => "https://api.binance.com/api/v3/aggTrades",
+        MarketType::LinearPerps => "https://fapi.binance.com/fapi/v1/aggTrades",
+    };
+
+    let mut url = format!(
+        "{base_url}?symbol={symbol_str}&limit=1000",
+    );
+
+    url.push_str(&format!("&startTime={}", from));
+
+    let response = reqwest::get(&url).await.map_err(StreamError::FetchError)?;
+
+    handle_rate_limit(
+        response.headers(), 
+        match market_type {
+            MarketType::Spot => 6000.0,
+            MarketType::LinearPerps => 2400.0,
+        },
+    ).await?;
+
+    let text = response.text().await.map_err(StreamError::FetchError)?;
+
+    let trades: Vec<Trade> = {
+        let de_trades: Vec<SonicTrade> = sonic_rs::from_str(&text)
+            .map_err(|e| StreamError::ParseError(format!("Failed to parse trades: {e}")))?;
+
+        de_trades.into_iter().map(|de_trade| Trade {
+            time: de_trade.time as i64,
+            is_sell: de_trade.is_sell,
+            price: str_f32_parse(&de_trade.price),
+            qty: str_f32_parse(&de_trade.qty),
+        }).collect()
+    };
+
+    Ok(trades)
+}
+
+pub async fn get_hist_trades(
+    ticker: Ticker,
+    date: chrono::NaiveDate,
+) -> Result<Vec<Trade>, StreamError> {    
+    let (symbol, market_type) = ticker.get_string();
+
+    let base_path = match market_type {
+        MarketType::Spot => format!("data/spot/daily/aggTrades/{symbol}"),
+        MarketType::LinearPerps => format!("data/futures/um/daily/aggTrades/{symbol}"),
+    };
+
+    std::fs::create_dir_all(&base_path)
+        .map_err(|e| StreamError::ParseError(format!("Failed to create directories: {e}")))?;
+
+    let zip_path = format!(
+        "{}/{}-aggTrades-{}.zip",
+        base_path,
+        symbol.to_uppercase(), 
+        date.format("%Y-%m-%d"),
+    );
+    
+    if std::fs::metadata(&zip_path).is_ok() {
+        log::info!("Using cached {}", zip_path);
+    } else {
+        let url = format!("https://data.binance.vision/{zip_path}");
+
+        log::info!("Downloading from {}", url);
+        
+        let resp = reqwest::get(&url).await.map_err(StreamError::FetchError)?;
+        
+        if !resp.status().is_success() {
+            return Err(StreamError::InvalidRequest(
+                format!("Failed to fetch from {}: {}", url, resp.status())
+            ));
+        }
+
+        let body = resp.bytes().await.map_err(StreamError::FetchError)?;
+        
+        std::fs::write(&zip_path, &body)
+            .map_err(|e| StreamError::ParseError(format!("Failed to write zip file: {e}")))?;
+    }
+
+    match std::fs::File::open(&zip_path) {
+        Ok(file) => {
+            let mut archive = zip::ZipArchive::new(file)
+                .map_err(|e| StreamError::ParseError(format!("Failed to unzip file: {e}")))?;
+
+            let mut trades = Vec::new();
+            for i in 0..archive.len() {
+                let csv_file = archive.by_index(i)
+                    .map_err(|e| StreamError::ParseError(format!("Failed to read csv: {e}")))?;
+
+                let mut csv_reader = ReaderBuilder::new()
+                    .has_headers(false)
+                    .from_reader(BufReader::new(csv_file));
+
+                trades.extend(csv_reader.records().filter_map(|record| {
+                    record.ok().and_then(|record| {
+                        let time = record[5].parse::<u64>().ok()?;
+                        let is_sell = record[6].parse::<bool>().ok()?;
+                        let price = str_f32_parse(&record[1]);
+                        let qty = str_f32_parse(&record[2]);
+                        
+                        Some(match market_type {
+                            MarketType::Spot => Trade {
+                                time: time as i64,
+                                is_sell,
+                                price,
+                                qty,
+                            },
+                            MarketType::LinearPerps => Trade {
+                                time: time as i64,
+                                is_sell,
+                                price,
+                                qty,
+                            }
+                        })
+                    })
+                }));
+            }
+            
+            if let Some(latest_trade) = trades.last() {
+                match fetch_intraday_trades(ticker, latest_trade.time).await {
+                    Ok(intraday_trades) => {
+                        trades.extend(intraday_trades);
+                    }
+                    Err(e) => {
+                        log::error!("Failed to fetch intraday trades: {}", e);
+                    }
+                }
+            }
+
+            Ok(trades)
+        }
+        Err(e) => Err(
+            StreamError::ParseError(format!("Failed to open compressed file: {e}"))
+        ),
+    }
+}
+
 #[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
 #[serde(rename_all = "camelCase")]
 struct DeOpenInterest {

+ 1 - 1
src/data_providers/bybit.rs

@@ -686,7 +686,7 @@ pub async fn fetch_ticksize(market_type: MarketType) -> Result<HashMap<Ticker, O
             .parse::<f32>()
             .map_err(|_| StreamError::ParseError("Failed to parse tick size".to_string()))?;
 
-        ticker_info_map.insert(Ticker::new(symbol, market_type), Some(TickerInfo { tick_size }));
+        ticker_info_map.insert(Ticker::new(symbol, market_type), Some(TickerInfo { tick_size, market_type }));
     }
 
     Ok(ticker_info_map)

+ 1 - 0
src/data_providers/fetcher.rs

@@ -68,6 +68,7 @@ impl RequestHandler {
 pub enum FetchRange {
     Kline(i64, i64),
     OpenInterest(i64, i64),
+    Trades(i64, i64),
 }
 
 #[derive(PartialEq, Debug)]

+ 236 - 17
src/layout.rs

@@ -1,18 +1,23 @@
-use std::collections::HashMap;
-use std::fs::File;
-use std::io::{Read, Write};
-use std::path::Path;
-use iced::widget::pane_grid;
-use iced::{Point, Size, Theme};
+use regex::Regex;
+use chrono::NaiveDate;
 use serde::{Deserialize, Serialize};
+use iced::widget::pane_grid::{self, Configuration};
+use iced::{Point, Size, Theme};
 
+use crate::charts::candlestick::CandlestickChart;
+use crate::charts::footprint::FootprintChart;
+use crate::charts::heatmap::HeatmapChart;
+use crate::charts::timeandsales::TimeAndSales;
 use crate::charts::indicators::{CandlestickIndicator, FootprintIndicator, HeatmapIndicator};
-use crate::data_providers::{Exchange, StreamType, Ticker};
-use crate::screen::dashboard::{Dashboard, PaneContent, PaneSettings, PaneState};
-use crate::pane::Axis;
-use crate::screen::UserTimezone;
+use crate::data_providers::{Exchange, StreamType, TickMultiplier, Ticker, Timeframe};
+use crate::screen::{UserTimezone, dashboard::{Dashboard, PaneContent, PaneSettings, PaneState}};
 use crate::style;
 
+use std::collections::HashMap;
+use std::io::{Read, Write};
+use std::fs::File;
+use std::path::Path;
+
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
 pub enum LayoutId {
     Layout1,
@@ -41,18 +46,13 @@ impl LayoutId {
     ];
 }
 
-#[derive(Debug, Clone, PartialEq, Copy, Deserialize, Serialize)]
+#[derive(Default, Debug, Clone, PartialEq, Copy, Deserialize, Serialize)]
 pub enum Sidebar {
+    #[default]
     Left,
     Right,
 }
 
-impl Default for Sidebar {
-    fn default() -> Self {
-        Sidebar::Left
-    }
-}
-
 impl std::fmt::Display for Sidebar {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match self {
@@ -219,6 +219,7 @@ impl SerializableState {
 pub struct SerializableDashboard {
     pub pane: SerializablePane,
     pub popout: Vec<(SerializablePane, (f32, f32), (f32, f32))>,
+    pub trade_fetch_enabled: bool,
 }
 
 impl<'a> From<&'a Dashboard> for SerializableDashboard {
@@ -265,6 +266,7 @@ impl<'a> From<&'a Dashboard> for SerializableDashboard {
                     })
                     .collect()
             },
+            trade_fetch_enabled: dashboard.trade_fetch_enabled,
         }
     }
 }
@@ -274,6 +276,7 @@ impl Default for SerializableDashboard {
         Self {
             pane: SerializablePane::Starter,
             popout: vec![],
+            trade_fetch_enabled: false,
         }
     }
 }
@@ -337,6 +340,167 @@ impl From<&PaneState> for SerializablePane {
     }
 }
 
+#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
+pub enum Axis {
+    Horizontal,
+    Vertical,
+}
+
+pub fn load_saved_state(file_path: &str) -> SavedState {
+    match read_from_file(file_path) {
+        Ok(state) => {
+            let mut de_state = SavedState {
+                selected_theme: state.selected_theme,
+                layouts: HashMap::new(),
+                favorited_tickers: state.favorited_tickers,
+                last_active_layout: state.last_active_layout,
+                window_size: state.window_size,
+                window_position: state.window_position,
+                timezone: state.timezone,
+                sidebar: state.sidebar,
+            };
+
+            fn configuration(pane: SerializablePane) -> Configuration<PaneState> {
+                match pane {
+                    SerializablePane::Split { axis, ratio, a, b } => Configuration::Split {
+                        axis: match axis {
+                            Axis::Horizontal => pane_grid::Axis::Horizontal,
+                            Axis::Vertical => pane_grid::Axis::Vertical,
+                        },
+                        ratio,
+                        a: Box::new(configuration(*a)),
+                        b: Box::new(configuration(*b)),
+                    },
+                    SerializablePane::Starter => {
+                        Configuration::Pane(PaneState::new(vec![], PaneSettings::default()))
+                    }
+                    SerializablePane::CandlestickChart {
+                        stream_type,
+                        settings,
+                        indicators,
+                    } => {
+                        let tick_size = settings.tick_multiply
+                            .unwrap_or(TickMultiplier(1))
+                            .multiply_with_min_tick_size(
+                                settings.ticker_info
+                                    .expect("No min tick size found, deleting dashboard_state.json probably fixes this")
+                            );
+
+                        let timeframe = settings.selected_timeframe.unwrap_or(Timeframe::M15);
+
+                        Configuration::Pane(PaneState::from_config(
+                            PaneContent::Candlestick(
+                                CandlestickChart::new(
+                                    vec![],
+                                    timeframe,
+                                    tick_size,
+                                    UserTimezone::default(),
+                                    &indicators,
+                                ),
+                                indicators,
+                            ),
+                            stream_type,
+                            settings,
+                        ))
+                    }
+                    SerializablePane::FootprintChart {
+                        stream_type,
+                        settings,
+                        indicators,
+                    } => {
+                        let tick_size = settings.tick_multiply
+                            .unwrap_or(TickMultiplier(50))
+                            .multiply_with_min_tick_size(
+                                settings.ticker_info
+                                    .expect("No min tick size found, deleting dashboard_state.json probably fixes this")
+                            );
+
+                        let timeframe = settings.selected_timeframe.unwrap_or(Timeframe::M5);
+
+                        Configuration::Pane(PaneState::from_config(
+                            PaneContent::Footprint(
+                                FootprintChart::new(
+                                    timeframe,
+                                    tick_size,
+                                    vec![],
+                                    vec![],
+                                    UserTimezone::default(),
+                                    &indicators,
+                                ),
+                                indicators,
+                            ),
+                            stream_type,
+                            settings,
+                        ))
+                    }
+                    SerializablePane::HeatmapChart {
+                        stream_type,
+                        settings,
+                        indicators,
+                    } => {
+                        let tick_size = settings.tick_multiply
+                            .unwrap_or(TickMultiplier(10))
+                            .multiply_with_min_tick_size(
+                                settings.ticker_info
+                                    .expect("No min tick size found, deleting dashboard_state.json probably fixes this")
+                            );
+
+                        Configuration::Pane(PaneState::from_config(
+                            PaneContent::Heatmap(
+                                HeatmapChart::new(
+                                    tick_size,
+                                    100,
+                                    UserTimezone::default(),
+                                    &indicators,
+                                ),
+                                indicators,
+                            ),
+                            stream_type,
+                            settings,
+                        ))
+                    }
+                    SerializablePane::TimeAndSales {
+                        stream_type,
+                        settings,
+                    } => Configuration::Pane(PaneState::from_config(
+                        PaneContent::TimeAndSales(TimeAndSales::new()),
+                        stream_type,
+                        settings,
+                    )),
+                }
+            }
+
+            for (id, dashboard) in &state.layouts {
+                let mut popout_windows: Vec<(Configuration<PaneState>, (Point, Size))> = Vec::new();
+
+                for (popout, pos, size) in &dashboard.popout {
+                    let configuration = configuration(popout.clone());
+                    popout_windows.push((
+                        configuration,
+                        (Point::new(pos.0, pos.1), Size::new(size.0, size.1)),
+                    ));
+                }
+
+                let dashboard = Dashboard::from_config(
+                    configuration(dashboard.pane.clone()), popout_windows, dashboard.trade_fetch_enabled
+                );
+
+                de_state.layouts.insert(*id, dashboard);
+            }
+
+            de_state
+        }
+        Err(e) => {
+            log::error!(
+                "Failed to load/find layout state: {}. Starting with a new layout.",
+                e
+            );
+
+            SavedState::default()
+        }
+    }
+}
+
 pub fn write_json_to_file(json: &str, file_path: &str) -> std::io::Result<()> {
     let path = Path::new(file_path);
     let mut file = File::create(path)?;
@@ -352,3 +516,58 @@ pub fn read_from_file(file_path: &str) -> Result<SerializableState, Box<dyn std:
 
     Ok(serde_json::from_str(&contents)?)
 }
+
+pub fn cleanup_old_data(data_path: &std::path::Path) -> usize {
+    if !data_path.exists() {
+        log::warn!("Data path {:?} does not exist, skipping cleanup", data_path);
+        return 0;
+    }
+
+    let re = Regex::new(r".*-(\d{4}-\d{2}-\d{2})\.zip$")
+        .expect("Cleanup regex pattern is valid");
+    let today = chrono::Local::now().date_naive();
+    let mut deleted_files = Vec::new();
+
+    let entries = match std::fs::read_dir(data_path) {
+        Ok(entries) => entries,
+        Err(e) => {
+            log::error!("Failed to read data directory: {}", e);
+            return 0;
+        }
+    };
+
+    for entry in entries.filter_map(Result::ok) {
+        let symbol_dir = match std::fs::read_dir(entry.path()) {
+            Ok(dir) => dir,
+            Err(e) => {
+                log::error!("Failed to read symbol directory {:?}: {}", entry.path(), e);
+                continue;
+            }
+        };
+
+        for file in symbol_dir.filter_map(Result::ok) {
+            let path = file.path();
+            let filename = match path.to_str() {
+                Some(name) => name,
+                None => continue,
+            };
+
+            if let Some(cap) = re.captures(filename) {
+                if let Ok(file_date) = NaiveDate::parse_from_str(&cap[1], "%Y-%m-%d") {
+                    let days_old = today.signed_duration_since(file_date).num_days();
+                    if days_old > 4 {
+                        if let Err(e) = std::fs::remove_file(&path) {
+                            log::error!("Failed to remove old file {}: {}", filename, e);
+                        } else {
+                            deleted_files.push(filename.to_string());
+                            log::info!("Removed old file: {}", filename);
+                        }
+                    }
+                }
+            }
+        }
+    }
+    
+    log::info!("File cleanup completed. Deleted {} files", deleted_files.len());
+    deleted_files.len()
+}

+ 146 - 241
src/main.rs

@@ -1,197 +1,45 @@
 #![windows_subsystem = "windows"]
 
+mod style;
 mod charts;
-mod data_providers;
+mod window;
 mod layout;
 mod logger;
 mod screen;
-mod style;
-mod tickers_table;
 mod tooltip;
-mod window;
+mod tickers_table;
+mod data_providers;
 
 use tooltip::tooltip;
-use screen::modal::dashboard_modal;
-use layout::{SerializableDashboard, SerializablePane, Sidebar};
-
-use futures::TryFutureExt;
-use iced_futures::MaybeSend;
+use tickers_table::TickersTable;
+use layout::{SerializableDashboard, Sidebar};
 use style::{get_icon_text, Icon, ICON_BYTES};
-
-use screen::{create_button, dashboard, handle_error, Notification, UserTimezone};
-use screen::dashboard::{Dashboard, pane, PaneContent, PaneSettings, PaneState};
+use screen::{
+    create_button, dashboard, handle_error, Notification, UserTimezone, 
+    dashboard::{Dashboard, pane},
+    modal::{confirmation_modal, dashboard_modal}
+};
 use data_providers::{
-    binance, bybit, Exchange, MarketType, StreamType, TickMultiplier, Ticker, TickerInfo, TickerStats, Timeframe
+    binance, bybit, Exchange, MarketType, StreamType, Ticker, TickerInfo, TickerStats, Timeframe
 };
-use tickers_table::TickersTable;
-
-use charts::footprint::FootprintChart;
-use charts::heatmap::HeatmapChart;
-use charts::candlestick::CandlestickChart;
-use charts::timeandsales::TimeAndSales;
 use window::{window_events, Window, WindowEvent};
-
-use std::future::Future;
-use std::{collections::HashMap, vec};
-
 use iced::{
-    widget::{button, pick_list, Space, column, container, row, text},
+    widget::{button, pick_list, Space, column, container, row, text, center, responsive, pane_grid},
     padding, Alignment, Element, Length, Point, Size, Subscription, Task, Theme,
 };
-use iced::widget::{center, responsive};
-use iced::widget::pane_grid::{self, Configuration};
+use iced_futures::MaybeSend;
+use futures::TryFutureExt;
+use std::{collections::HashMap, vec, future::Future};
 
 fn main() {
     logger::setup(false, false).expect("Failed to initialize logger");
 
-    let saved_state = match layout::read_from_file("dashboard_state.json") {
-        Ok(state) => {
-            let mut de_state = layout::SavedState {
-                selected_theme: state.selected_theme,
-                layouts: HashMap::new(),
-                favorited_tickers: state.favorited_tickers,
-                last_active_layout: state.last_active_layout,
-                window_size: state.window_size,
-                window_position: state.window_position,
-                timezone: state.timezone,
-                sidebar: state.sidebar,
-            };
+    std::thread::spawn(|| {
+        let data_dir_path = std::path::Path::new("data/futures/um/daily/aggTrades");
+        layout::cleanup_old_data(data_dir_path)
+    });
 
-            fn configuration(pane: SerializablePane) -> Configuration<PaneState> {
-                match pane {
-                    SerializablePane::Split { axis, ratio, a, b } => Configuration::Split {
-                        axis: match axis {
-                            pane::Axis::Horizontal => pane_grid::Axis::Horizontal,
-                            pane::Axis::Vertical => pane_grid::Axis::Vertical,
-                        },
-                        ratio,
-                        a: Box::new(configuration(*a)),
-                        b: Box::new(configuration(*b)),
-                    },
-                    SerializablePane::Starter => {
-                        Configuration::Pane(PaneState::new(vec![], PaneSettings::default()))
-                    }
-                    SerializablePane::CandlestickChart {
-                        stream_type,
-                        settings,
-                        indicators,
-                    } => {
-                        let tick_size = settings.tick_multiply
-                            .unwrap_or(TickMultiplier(1))
-                            .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_or(Timeframe::M5);
-
-                        Configuration::Pane(PaneState::from_config(
-                            PaneContent::Candlestick(
-                                CandlestickChart::new(
-                                    vec![],
-                                    timeframe,
-                                    tick_size,
-                                    UserTimezone::default(),
-                                ),
-                                indicators,
-                            ),
-                            stream_type,
-                            settings,
-                        ))
-                    }
-                    SerializablePane::FootprintChart {
-                        stream_type,
-                        settings,
-                        indicators,
-                    } => {
-                        let tick_size = settings.tick_multiply
-                            .unwrap_or(TickMultiplier(50))
-                            .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_or(Timeframe::M15);
-
-                        Configuration::Pane(PaneState::from_config(
-                            PaneContent::Footprint(
-                                FootprintChart::new(
-                                    timeframe,
-                                    tick_size,
-                                    vec![],
-                                    vec![],
-                                    UserTimezone::default(),
-                                ),
-                                indicators,
-                            ),
-                            stream_type,
-                            settings,
-                        ))
-                    }
-                    SerializablePane::HeatmapChart {
-                        stream_type,
-                        settings,
-                        indicators,
-                    } => {
-                        let tick_size = settings.tick_multiply
-                            .unwrap_or(TickMultiplier(10))
-                            .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(
-                                    tick_size,
-                                    100,
-                                    UserTimezone::default(),
-                                ),
-                                indicators,
-                            ),
-                            stream_type,
-                            settings,
-                        ))
-                    }
-                    SerializablePane::TimeAndSales {
-                        stream_type,
-                        settings,
-                    } => Configuration::Pane(PaneState::from_config(
-                        PaneContent::TimeAndSales(TimeAndSales::new()),
-                        stream_type,
-                        settings,
-                    )),
-                }
-            }
-
-            for (id, dashboard) in &state.layouts {
-                let mut popout_windows: Vec<(Configuration<PaneState>, (Point, Size))> = Vec::new();
-
-                for (popout, pos, size) in &dashboard.popout {
-                    let configuration = configuration(popout.clone());
-                    popout_windows.push((
-                        configuration,
-                        (Point::new(pos.0, pos.1), Size::new(size.0, size.1)),
-                    ));
-                }
-
-                let dashboard =
-                    Dashboard::from_config(configuration(dashboard.pane.clone()), popout_windows);
-
-                de_state.layouts.insert(*id, dashboard);
-            }
-
-            de_state
-        }
-        Err(e) => {
-            log::error!(
-                "Failed to load/find layout state: {}. Starting with a new layout.",
-                e
-            );
-
-            layout::SavedState::default()
-        }
-    };
+    let saved_state = layout::load_saved_state("dashboard_state.json");
 
     let window_size = saved_state.window_size.unwrap_or((1600.0, 900.0));
     let window_position = saved_state.window_position;
@@ -260,6 +108,7 @@ enum Message {
     ToggleModal(DashboardModal),
 
     MarketWsEvent(Exchange, data_providers::Event),
+    ToggleTradeFetch(bool),
 
     WindowEvent(WindowEvent),
     SaveAndExit(HashMap<window::Id, (Point, Size)>),
@@ -279,6 +128,7 @@ enum Message {
     FetchAndUpdateTickersTable,
 
     LoadLayout(layout::LayoutId),
+    ToggleDialogModal(Option<String>),
 }
 
 struct State {
@@ -292,6 +142,7 @@ struct State {
     ticker_info_map: HashMap<Exchange, HashMap<Ticker, Option<TickerInfo>>>,
     show_tickers_dashboard: bool,
     tickers_table: TickersTable,
+    confirmation_dialog: Option<String>,
 }
 
 #[allow(dead_code)]
@@ -305,52 +156,34 @@ impl State {
         let last_active_layout = saved_state.last_active_layout;
 
         let mut ticker_info_map = HashMap::new();
-        let mut ticksizes_tasks = Vec::new();
 
-        for exchange in &Exchange::ALL {
-            ticker_info_map.insert(*exchange, HashMap::new());
+        let exchange_fetch_tasks = {
+            Exchange::MARKET_TYPES.iter()
+                .flat_map(|(exchange, market_type)| {
+                    ticker_info_map.insert(*exchange, HashMap::new());
+                    
+                    let ticksizes_task = match exchange {
+                        Exchange::BinanceFutures | Exchange::BinanceSpot => {
+                            fetch_ticker_info(*exchange, binance::fetch_ticksize(*market_type))
+                        }
+                        Exchange::BybitLinear | Exchange::BybitSpot => {
+                            fetch_ticker_info(*exchange, bybit::fetch_ticksize(*market_type))
+                        }
+                    };
 
-            let fetch_ticksize = match exchange {
-                Exchange::BinanceFutures => {
-                    fetch_ticker_info(*exchange, binance::fetch_ticksize(MarketType::LinearPerps))
-                }
-                Exchange::BybitLinear => {
-                    fetch_ticker_info(*exchange, bybit::fetch_ticksize(MarketType::LinearPerps))
-                }
-                Exchange::BinanceSpot => {
-                    fetch_ticker_info(*exchange, binance::fetch_ticksize(MarketType::Spot))
-                }
-                Exchange::BybitSpot => {
-                    fetch_ticker_info(*exchange, bybit::fetch_ticksize(MarketType::Spot))
-                }
-            };
-            ticksizes_tasks.push(fetch_ticksize);
-        }
+                    let prices_task = match exchange {
+                        Exchange::BinanceFutures | Exchange::BinanceSpot => {
+                            fetch_ticker_prices(*exchange, binance::fetch_ticker_prices(*market_type))
+                        }
+                        Exchange::BybitLinear | Exchange::BybitSpot => {
+                            fetch_ticker_prices(*exchange, bybit::fetch_ticker_prices(*market_type))
+                        }
+                    };
 
-        let bybit_tickers_fetch = fetch_ticker_prices(
-            Exchange::BybitLinear,
-            bybit::fetch_ticker_prices(MarketType::LinearPerps),
-        );
-        let binance_tickers_fetch = fetch_ticker_prices(
-            Exchange::BinanceFutures,
-            binance::fetch_ticker_prices(MarketType::LinearPerps),
-        );
-        let binance_spot_tickers_fetch = fetch_ticker_prices(
-            Exchange::BinanceSpot,
-            binance::fetch_ticker_prices(MarketType::Spot),
-        );
-        let bybit_spot_tickers_fetch = fetch_ticker_prices(
-            Exchange::BybitSpot,
-            bybit::fetch_ticker_prices(MarketType::Spot),
-        );
-
-        let batch_fetch_tasks = Task::batch(vec![
-            bybit_tickers_fetch,
-            binance_tickers_fetch,
-            binance_spot_tickers_fetch,
-            bybit_spot_tickers_fetch,
-            Task::batch(ticksizes_tasks),
-        ]);
+                    vec![ticksizes_task, prices_task]
+                })
+                .collect::<Vec<_>>()
+        };
 
         (
             Self {
@@ -364,13 +197,14 @@ impl State {
                 show_tickers_dashboard: false,
                 sidebar_location: saved_state.sidebar,
                 tickers_table: TickersTable::new(saved_state.favorited_tickers),
+                confirmation_dialog: None,
             },
             open_main_window
                 .then(|_| Task::none())
                 .chain(Task::batch(vec![
                     Task::done(Message::LoadLayout(last_active_layout)),
                     Task::done(Message::SetTimezone(saved_state.timezone)),
-                    batch_fetch_tasks,
+                    Task::batch(exchange_fetch_tasks),
                 ])),
         )
     }
@@ -458,7 +292,7 @@ impl State {
                         Message::SaveAndExit
                     );
                 }
-            },
+            }
             Message::SaveAndExit(windows) => {
                 self.get_mut_dashboard(self.last_active_layout)
                     .popout
@@ -650,6 +484,18 @@ impl State {
             Message::SidebarPosition(pos) => {
                 self.sidebar_location = pos;
             }
+            Message::ToggleTradeFetch(checked) => {
+                self.layouts.values_mut().for_each(|dashboard| {
+                    dashboard.toggle_trade_fetch(checked, &self.main_window);
+                });
+                    
+                if checked {
+                    self.confirmation_dialog = None;
+                }
+            }
+            Message::ToggleDialogModal(dialog) => {
+                self.confirmation_dialog = dialog;
+            }
         }
         Task::none()
     }
@@ -790,7 +636,7 @@ impl State {
                 .view(&self.main_window)
                 .map(Message::Dashboard);
 
-            let content = column![
+            let base = column![
                 {
                     #[cfg(target_os = "macos")] {
                         center(
@@ -829,23 +675,50 @@ impl State {
 
             match self.active_modal {
                 DashboardModal::Settings => {
-                    let mut all_themes: Vec<Theme> = Theme::ALL.to_vec();
-                    all_themes.push(Theme::Custom(style::custom_theme().into()));
-    
-                    let theme_picklist =
-                        pick_list(all_themes, Some(self.theme.clone()), Message::ThemeSelected);
-    
-                    let timezone_picklist = pick_list(
-                        [UserTimezone::Utc, UserTimezone::Local],
-                        Some(dashboard.get_timezone()),
-                        Message::SetTimezone,
-                    );
-                    let sidebar_pos = pick_list(
-                        [Sidebar::Left, Sidebar::Right],
-                        Some(self.sidebar_location),
-                        Message::SidebarPosition,
-                    );
                     let settings_modal = {
+                        let mut all_themes: Vec<Theme> = Theme::ALL.to_vec();
+                        all_themes.push(Theme::Custom(style::custom_theme().into()));
+
+                        let trade_fetch_checkbox = {
+                            let is_active = dashboard.trade_fetch_enabled;
+                    
+                            let checkbox = iced::widget::checkbox("Fetch trades (Binance)", is_active)
+                                .on_toggle(|checked| {
+                                        if checked {
+                                            Message::ToggleDialogModal(
+                                                Some(
+                                                    "This might be unreliable and take some time to complete"
+                                                    .to_string()
+                                                ),
+                                            )
+                                        } else {
+                                            Message::ToggleTradeFetch(false)
+                                        }
+                                    }
+                                );
+                    
+                            tooltip(
+                                checkbox,
+                                Some("Try to fetch trades for footprint charts"),
+                                tooltip::Position::Top,
+                            )
+                        };
+
+                        let theme_picklist =
+                            pick_list(all_themes, Some(self.theme.clone()), Message::ThemeSelected);
+        
+                        let timezone_picklist = pick_list(
+                            [UserTimezone::Utc, UserTimezone::Local],
+                            Some(dashboard.get_timezone()),
+                            Message::SetTimezone,
+                        );
+
+                        let sidebar_pos = pick_list(
+                            [Sidebar::Left, Sidebar::Right],
+                            Some(self.sidebar_location),
+                            Message::SidebarPosition,
+                        );
+
                         container(
                             column![
                                 column![
@@ -854,6 +727,10 @@ impl State {
                                 ].spacing(4),
                                 column![text("Time zone").size(14), timezone_picklist,].spacing(4),
                                 column![text("Theme").size(14), theme_picklist,].spacing(4),
+                                column![
+                                    text("Experimental").size(14),
+                                    trade_fetch_checkbox,
+                                ].spacing(4),
                             ]
                             .spacing(16),
                         )
@@ -867,15 +744,43 @@ impl State {
                         Sidebar::Left => (Alignment::Start, padding::left(48).top(8)),
                         Sidebar::Right => (Alignment::End, padding::right(48).top(8)),
                     };
-    
-                    dashboard_modal(
-                        content,
+
+                    let base_content = dashboard_modal(
+                        base,
                         settings_modal,
                         Message::ToggleModal(DashboardModal::None),
                         padding,
                         Alignment::End,
                         align_x,
-                    )
+                    );
+
+                    if let Some(confirm_dialog) = &self.confirmation_dialog {
+                        let dialog_content = container(
+                            column![
+                                text(confirm_dialog).size(14),
+                                row![
+                                    button(text("Cancel"))
+                                        .style(|theme, status| style::button_transparent(theme, status, false))
+                                        .on_press(Message::ToggleDialogModal(None)),
+                                    button(text("Confirm"))
+                                        .on_press(Message::ToggleTradeFetch(true)),
+                                ]
+                                .spacing(8),
+                            ]
+                            .align_x(Alignment::Center)
+                            .spacing(16),
+                        )
+                        .padding(24)
+                        .style(style::dashboard_modal);
+    
+                        confirmation_modal(
+                            base_content, 
+                            dialog_content, 
+                            Message::ToggleDialogModal(None)
+                        )
+                    } else {
+                        base_content
+                    }
                 }
                 DashboardModal::Layout => {
                     let layout_picklist = pick_list(
@@ -964,7 +869,7 @@ impl State {
                     };
     
                     dashboard_modal(
-                        content,
+                        base,
                         manage_layout_modal,
                         Message::ToggleModal(DashboardModal::None),
                         padding,
@@ -972,7 +877,7 @@ impl State {
                         align_x,
                     )
                 }
-                DashboardModal::None => content.into(),
+                DashboardModal::None => base.into(),
             }
         }
     }

+ 59 - 17
src/screen.rs

@@ -6,7 +6,7 @@ use iced::{
 use iced_futures::MaybeSend;
 use serde::{Deserialize, Serialize};
 
-use crate::{style, tooltip};
+use crate::tooltip;
 
 pub mod dashboard;
 pub mod modal;
@@ -23,7 +23,7 @@ pub fn create_button<'a, M: Clone + 'a>(
         .on_press(message);
         
     if let Some(text) = tooltip_text {
-        tooltip(btn, Some(text), tooltip_pos).into()
+        tooltip(btn, Some(text), tooltip_pos)
     } else {
         btn.into()
     }
@@ -76,12 +76,14 @@ impl Serialize for UserTimezone {
     }
 }
 
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone, Copy, PartialEq)]
 pub enum InfoType {
     FetchingKlines,
+    FetchingTrades(usize),
+    FetchingOI,
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq)]
 pub enum Notification {
     Error(String),
     Info(InfoType),
@@ -139,6 +141,43 @@ impl NotificationManager {
             .push(notification);
     }
 
+    pub fn increment_fetching_trades(
+        &mut self,
+        window: window::Id,
+        pane: &pane_grid::Pane,
+        increment_by: usize,
+    ) {
+        if let Some(window_map) = self.notifications.get_mut(&window) {
+            if let Some(notification_list) = window_map.get_mut(pane) {
+                let found = notification_list.iter_mut().any(|notification| {
+                    if let Notification::Info(InfoType::FetchingTrades(count)) = notification {
+                        *count += increment_by;
+                        return true;
+                    }
+                    false
+                });
+
+                if !found {
+                    notification_list.push(Notification::Info(InfoType::FetchingTrades(increment_by)));
+                }
+            } else {
+                window_map.insert(*pane, vec![Notification::Info(InfoType::FetchingTrades(increment_by))]);
+            }
+        } else {
+            let mut pane_map = HashMap::new();
+            pane_map.insert(*pane, vec![Notification::Info(InfoType::FetchingTrades(increment_by))]);
+            self.notifications.insert(window, pane_map);
+        }
+    }
+
+    pub fn find_and_remove(&mut self, window: window::Id, pane: pane_grid::Pane, notification: Notification) {
+        if let Some(window_map) = self.notifications.get_mut(&window) {
+            if let Some(notification_list) = window_map.get_mut(&pane) {
+                notification_list.retain(|n| n != &notification);
+            }
+        }
+    }
+
     /// Remove notifications of a specific type for a pane in a window
     pub fn remove_info_type(
         &mut self,
@@ -234,29 +273,32 @@ impl NotificationManager {
     }
 }
 
-fn create_notis_column<'a, M: 'a>(notifications: &'a [Notification]) -> Column<'a, M> {
+fn create_notis_column<'a, M: 'a + Clone>(
+    notifications: &'a [Notification],
+    close_message: M,
+) -> Column<'a, M> {
     let mut notifications_column = column![].align_x(Alignment::End).spacing(6);
 
-    for (index, notification) in notifications.iter().rev().take(5).enumerate() {
+    for notification in notifications.iter().rev().take(5) {
         let notification_str = match notification {
             Notification::Error(error) => error.to_string(),
             Notification::Warn(warn) => warn.to_string(),
             Notification::Info(info) => match info {
                 InfoType::FetchingKlines => "Fetching klines...".to_string(),
+                InfoType::FetchingTrades(total_fetched) => format!(
+                    "Fetching trades...\n({} fetched)",
+                    total_fetched
+                ),
+                InfoType::FetchingOI => "Fetching open interest...".to_string(),
             },
         };
 
-        let color_alpha = 1.0 - (index as f32 * 0.25);
-
-        notifications_column =
-            notifications_column.push(container(text(notification_str)).padding(12).style(
-                move |theme| match notification {
-                    Notification::Error(_) => style::pane_err_notification(theme, color_alpha),
-                    Notification::Warn(_) | Notification::Info(_) => {
-                        style::pane_info_notification(theme, color_alpha)
-                    }
-                },
-            ));
+        notifications_column = notifications_column
+            .push(
+                button(container(text(notification_str)).padding(6))
+                    .on_press(close_message.clone()),
+            )
+            .padding(12);
     }
 
     notifications_column

+ 245 - 23
src/screen/dashboard.rs

@@ -63,6 +63,22 @@ pub enum Message {
     ),
     DistributeFetchedKlines(StreamType, Result<Vec<Kline>, String>),
     ChartMessage(pane_grid::Pane, window::Id, ChartMessage),
+
+    // Batched trade fetching
+    FetchTrades(
+        window::Id,
+        pane_grid::Pane,
+        i64,
+        i64,
+        StreamType,
+    ),
+    DistributeFetchedTrades(
+        window::Id,
+        pane_grid::Pane,
+        Vec<Trade>,
+        StreamType,
+        i64,
+    ),
 }
 
 pub struct Dashboard {
@@ -74,6 +90,7 @@ pub struct Dashboard {
     notification_manager: NotificationManager,
     tickers_info: HashMap<Exchange, HashMap<Ticker, Option<TickerInfo>>>,
     timezone: UserTimezone,
+    pub trade_fetch_enabled: bool,
 }
 
 impl Default for Dashboard {
@@ -93,6 +110,7 @@ impl Dashboard {
             tickers_info: HashMap::new(),
             popout: HashMap::new(),
             timezone: UserTimezone::default(),
+            trade_fetch_enabled: false,
         }
     }
 
@@ -148,6 +166,7 @@ impl Dashboard {
     pub fn from_config(
         panes: Configuration<PaneState>,
         popout_windows: Vec<(Configuration<PaneState>, (Point, Size))>,
+        trade_fetch_enabled: bool,
     ) -> Self {
         let panes = pane_grid::State::with_configuration(panes);
 
@@ -169,6 +188,7 @@ impl Dashboard {
             tickers_info: HashMap::new(),
             popout,
             timezone: UserTimezone::default(),
+            trade_fetch_enabled,
         }
     }
 
@@ -342,7 +362,11 @@ impl Dashboard {
 
                         // set pane's stream and content identifiers
                         if let Some(pane_state) = self.get_mut_pane(main_window.id, window, pane) {
-                            if let Err(err) = pane_state.set_content(ticker_info, &content_str, timezone) {
+                            if let Err(err) = pane_state.set_content(
+                                ticker_info,
+                                &content_str, 
+                                timezone
+                            ) {
                                 return err_occurred(err);
                             }
                         } else {
@@ -452,6 +476,9 @@ impl Dashboard {
                             pane_state.content.toggle_indicator(indicator_str);
                         }
                     }
+                    pane::Message::HideNotification(pane, notification) => {
+                        self.notification_manager.find_and_remove(window, pane, notification);
+                    }
                 }
             }
             Message::FetchEvent(req_id, klines, pane_stream, pane_id, window) => {
@@ -485,24 +512,30 @@ impl Dashboard {
                 }
             }
             Message::OIFetchEvent(req_id, oi, pane_stream, pane_id, window) => {
-                match oi {
-                    Ok(oi) => {
-                        if let StreamType::Kline { .. } = pane_stream {
-                            if let Some(pane_state) =
-                                self.get_mut_pane(main_window.id, window, pane_id)
-                            {
+                self.notification_manager.remove_info_type(
+                    window,
+                    &pane_id,
+                    &InfoType::FetchingOI,
+                );
+
+                if let Some(pane_state) =
+                    self.get_mut_pane(main_window.id, window, pane_id)
+                {
+                    match oi {
+                        Ok(oi) => {
+                            if let StreamType::Kline { .. } = pane_stream {
                                 pane_state.insert_oi_vec(req_id, oi);
                             }
                         }
-                    }
-                    Err(err) => {
-                        return Task::perform(async { err }, move |err: String| {
-                            Message::ErrorOccurred(
-                                window,
-                                Some(pane_id),
-                                DashboardError::Fetch(err),
-                            )
-                        })
+                        Err(err) => {
+                            return Task::perform(async { err }, move |err: String| {
+                                Message::ErrorOccurred(
+                                    window,
+                                    Some(pane_id),
+                                    DashboardError::Fetch(err),
+                                )
+                            })
+                        }
                     }
                 }
             }
@@ -538,16 +571,17 @@ impl Dashboard {
                             if state.matches_stream(&stream_type) {
                                 if let StreamType::Kline { timeframe, .. } = stream_type {
                                     match &mut state.content {
-                                        PaneContent::Candlestick(chart, _) => {
+                                        PaneContent::Candlestick(chart, indicators) => {
                                             let tick_size = chart.get_tick_size();
                                             *chart = CandlestickChart::new(
                                                 klines.clone(),
                                                 timeframe,
                                                 tick_size,
                                                 timezone,
+                                                indicators,
                                             );
                                         }
-                                        PaneContent::Footprint(chart, _) => {
+                                        PaneContent::Footprint(chart, indicators) => {
                                             let (raw_trades, tick_size) =
                                                 (chart.get_raw_trades(), chart.get_tick_size());
                                             *chart = FootprintChart::new(
@@ -556,6 +590,7 @@ impl Dashboard {
                                                 klines.clone(),
                                                 raw_trades,
                                                 timezone,
+                                                indicators,
                                             );
                                         }
                                         _ => {}
@@ -577,7 +612,118 @@ impl Dashboard {
                 Err(err) => {
                     log::error!("{err}");
                 }
-            },
+            }
+            Message::FetchTrades(
+                window_id,
+                pane,
+                from_time,
+                to_time,
+                stream_type,
+            ) => {
+                if let StreamType::DepthAndTrades { exchange, ticker } = stream_type {
+                    if exchange == Exchange::BinanceFutures || exchange == Exchange::BinanceSpot {
+                        return Task::perform(
+                            binance::fetch_trades(ticker, from_time),
+                            move |result| match result {
+                                Ok(trades) => Message::DistributeFetchedTrades(
+                                    window_id,
+                                    pane,
+                                    trades,
+                                    stream_type,
+                                    to_time,
+                                ),
+                                Err(err) => Message::ErrorOccurred(
+                                    window_id,
+                                    Some(pane),
+                                    DashboardError::Fetch(err.to_string()),
+                                ),
+                            },
+                        );
+                    } else {
+                        self.notification_manager.remove_info_type(
+                            window_id,
+                            &pane,
+                            &InfoType::FetchingTrades(0),
+                        );
+
+                        return Task::done(Message::ErrorOccurred(
+                            window_id,
+                            Some(pane),
+                            DashboardError::Fetch(format!(
+                                "No trade fetch support for {exchange:?}"
+                            )),
+                        ));
+                    }
+                }
+            }
+            Message::DistributeFetchedTrades(
+                window_id,
+                pane,
+                trades,
+                stream_type,
+                to_time,
+            ) => {
+                let last_trade_time = trades.last()
+                    .map_or(0, |trade| trade.time);
+
+                self.notification_manager.increment_fetching_trades(
+                    window_id,
+                    &pane,
+                    trades.len(),
+                );
+
+                if last_trade_time < to_time {
+                    match self.insert_fetched_trades(
+                        main_window.id,
+                        window_id,
+                        pane,
+                        &trades,
+                        false,
+                    ) {
+                        Ok(_) => {
+                            return Task::done(Message::FetchTrades(
+                                window_id,
+                                pane,
+                                last_trade_time,
+                                to_time,
+                                stream_type,
+                            ));
+                        }
+                        Err(err) => {
+                            self.notification_manager.remove_info_type(
+                                window_id,
+                                &pane,
+                                &InfoType::FetchingTrades(0),
+                            );
+
+                            return Task::done(
+                                Message::ErrorOccurred(window_id, Some(pane), err)
+                            );
+                        }
+                    }
+                } else {
+                    self.notification_manager.remove_info_type(
+                        window_id,
+                        &pane,
+                        &InfoType::FetchingTrades(0),
+                    );
+
+                    match self.insert_fetched_trades(
+                        main_window.id,
+                        window_id,
+                        pane,
+                        &trades,
+                        true,
+                    ) {
+                        Ok(_) => {}
+                        Err(err) => {
+                            return Task::done(
+                                Message::ErrorOccurred(window_id, Some(pane), err)
+                            );
+                        }
+                    }
+                }
+            }
             Message::RefreshStreams => {
                 self.pane_streams = self.get_all_diff_streams(main_window.id);
             }
@@ -622,6 +768,12 @@ impl Dashboard {
 
                                 if let Some(stream) = kline_stream {    
                                     let stream = *stream;
+
+                                    self.notification_manager.push(
+                                        window,
+                                        pane,
+                                        Notification::Info(InfoType::FetchingOI),
+                                    );
             
                                     return get_oi_fetch_task(
                                         window,
@@ -632,6 +784,37 @@ impl Dashboard {
                                     );
                                 }
                         }
+                        FetchRange::Trades(from, to) => {
+                            if !self.trade_fetch_enabled {
+                                return Task::none();
+                            }
+
+                            let trade_stream = self
+                                .get_pane(main_window.id, window, pane)
+                                .and_then(|pane| {
+                                    pane.stream.iter().find(|stream| {
+                                        matches!(stream, StreamType::DepthAndTrades { .. })
+                                    })
+                                });
+
+                            if let Some(stream) = trade_stream {
+                                let stream = *stream;
+
+                                self.notification_manager.push(
+                                    window,
+                                    pane,
+                                    Notification::Info(InfoType::FetchingTrades(0)),
+                                );
+
+                                return Task::done(Message::FetchTrades(
+                                    window,
+                                    pane,
+                                    from,
+                                    to,
+                                    stream,
+                                ));
+                            }
+                        }
                     }
                 }
             }
@@ -836,7 +1019,10 @@ impl Dashboard {
         if !self.notification_manager.global_notifications.is_empty() {
             dashboard_notification(
                 base,
-                create_notis_column(&self.notification_manager.global_notifications),
+                create_notis_column(
+                    &self.notification_manager.global_notifications, 
+                    Message::ClearLastGlobalNotification,
+                ),
             )
         } else {
             base.into()
@@ -913,18 +1099,18 @@ impl Dashboard {
         if let Some(pane_state) = self.get_mut_pane(main_window, window, pane) {
             pane_state.settings.tick_multiply = Some(new_tick_multiply);
 
-            if let Some(min_tick_size) = pane_state.settings.min_tick_size {
+            if let Some(ticker_info) = pane_state.settings.ticker_info {
                 match pane_state.content {
                     PaneContent::Footprint(ref mut chart, _) => {
                         chart.change_tick_size(
-                            new_tick_multiply.multiply_with_min_tick_size(min_tick_size),
+                            new_tick_multiply.multiply_with_min_tick_size(ticker_info),
                         );
 
                         chart.reset_request_handler();
                     }
                     PaneContent::Heatmap(ref mut chart, _) => {
                         chart.change_tick_size(
-                            new_tick_multiply.multiply_with_min_tick_size(min_tick_size),
+                            new_tick_multiply.multiply_with_min_tick_size(ticker_info),
                         );
                     }
                     _ => {
@@ -1050,6 +1236,42 @@ impl Dashboard {
         Task::none()
     }
 
+    pub fn toggle_trade_fetch(&mut self, is_enabled: bool, main_window: &Window) {
+        self.trade_fetch_enabled = is_enabled;
+
+        self.iter_all_panes_mut(main_window.id)
+            .for_each(|(_, _, pane_state)| {
+                if let PaneContent::Footprint(chart, _) = &mut pane_state.content {
+                    chart.reset_request_handler();
+                }
+            });
+    }
+
+    fn insert_fetched_trades(
+        &mut self,
+        main_window: window::Id,
+        window: window::Id,
+        pane: pane_grid::Pane,
+        trades: &[Trade],
+        is_batches_done: bool,
+    ) -> Result<(), DashboardError> {
+        self.get_mut_pane(main_window, window, pane)
+            .map_or_else(
+                || Err(
+                    DashboardError::Unknown("Couldnt get the pane for fetched trades".to_string())
+                ),
+                |pane_state| match &mut pane_state.content {
+                    PaneContent::Footprint(chart, _) => {
+                        chart.insert_trades(trades.to_owned(), is_batches_done);
+                        Ok(())
+                    }
+                    _ => Err(
+                        DashboardError::Unknown("No matching chart found for fetched trades".to_string())
+                    ),
+                }
+            )
+    }
+
     pub fn update_latest_klines(
         &mut self,
         stream_type: &StreamType,

+ 245 - 139
src/screen/dashboard/pane.rs

@@ -1,7 +1,7 @@
 use iced::{
     alignment::{Horizontal, Vertical}, padding, widget::{
-        button, center, column, container, pane_grid, row, scrollable, text, tooltip, Container, Slider
-    }, Alignment, Element, Length, Renderer, Task, Theme
+        button, center, column, container, pane_grid, row, scrollable, text, tooltip, Column, Slider
+    }, Alignment, Element, Length, Renderer, Task, Theme,
 };
 use serde::{Deserialize, Serialize};
 
@@ -13,7 +13,7 @@ use crate::{
     },
     data_providers::{format_with_commas, Exchange, Kline, MarketType, OpenInterest, TickMultiplier, Ticker, TickerInfo, Timeframe},
     screen::{
-        self, create_button, create_notis_column, modal::{pane_menu, pane_notification}, DashboardError, UserTimezone
+        self, create_button, modal::{pane_menu, pane_notification}, DashboardError, InfoType, Notification, UserTimezone
     },
     style::{self, get_icon_text, Icon},
     window::{self, Window},
@@ -28,12 +28,6 @@ pub enum PaneModal {
     None,
 }
 
-#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
-pub enum Axis {
-    Horizontal,
-    Vertical,
-}
-
 #[derive(Debug, Clone)]
 pub enum Message {
     PaneClicked(pane_grid::Pane),
@@ -51,6 +45,7 @@ pub enum Message {
     ChartUserUpdate(pane_grid::Pane, charts::Message),
     SliderChanged(pane_grid::Pane, f32, bool),
     ToggleIndicator(pane_grid::Pane, String),
+    HideNotification(pane_grid::Pane, Notification),
     Popout,
     Merge,
 }
@@ -86,11 +81,11 @@ impl PaneState {
     }
 
     /// sets the tick size. returns the tick size with the multiplier applied
-    pub fn set_tick_size(&mut self, multiplier: TickMultiplier, min_tick_size: f32) -> f32 {
+    pub fn set_tick_size(&mut self, multiplier: TickMultiplier, ticker_info: TickerInfo) -> f32 {
         self.settings.tick_multiply = Some(multiplier);
-        self.settings.min_tick_size = Some(min_tick_size);
+        self.settings.ticker_info = Some(ticker_info);
 
-        multiplier.multiply_with_min_tick_size(min_tick_size)
+        multiplier.multiply_with_min_tick_size(ticker_info)
     }
 
     /// gets the timeframe if exists, otherwise sets timeframe w given
@@ -135,7 +130,7 @@ impl PaneState {
                 let timeframe = self
                     .settings
                     .selected_timeframe
-                    .unwrap_or(Timeframe::M15);
+                    .unwrap_or(Timeframe::M5);
 
                 vec![
                     StreamType::DepthAndTrades { exchange, ticker },
@@ -150,7 +145,7 @@ impl PaneState {
                 let timeframe = self
                     .settings
                     .selected_timeframe
-                    .unwrap_or(Timeframe::M5);
+                    .unwrap_or(Timeframe::M15);
 
                 vec![StreamType::Kline {
                     exchange,
@@ -173,32 +168,47 @@ impl PaneState {
 
     pub fn set_content(
         &mut self, 
-        ticker_info: TickerInfo, 
+        ticker_info: TickerInfo,
         content_str: &str, 
         timezone: UserTimezone
     ) -> Result<(), DashboardError> {
+        self.settings = PaneSettings::default();
+
         self.content = match content_str {
             "heatmap" => {
                 let tick_size = self.set_tick_size(
                     TickMultiplier(10),
-                    ticker_info.tick_size,
+                    ticker_info,
                 );
+                let enabled_indicators = vec![HeatmapIndicator::Volume];
 
                 PaneContent::Heatmap(
                     HeatmapChart::new(
                         tick_size,
                         100,
                         timezone,
+                        &enabled_indicators,
                     ),
-                    vec![],
+                    enabled_indicators,
                 )
             }
             "footprint" => {
                 let tick_size = self.set_tick_size(
                     TickMultiplier(50),
-                    ticker_info.tick_size,
+                    ticker_info,
                 );
-                let timeframe = self.set_timeframe(Timeframe::M15);
+                let timeframe = self.set_timeframe(Timeframe::M5);
+                let enabled_indicators = {
+                    if ticker_info.market_type == MarketType::LinearPerps {
+                        vec![
+                            FootprintIndicator::Volume,
+                            FootprintIndicator::OpenInterest,
+                        ]
+                    } else {
+                        vec![FootprintIndicator::Volume]
+                    }
+                };
+
                 PaneContent::Footprint(
                     FootprintChart::new(
                         timeframe,
@@ -206,30 +216,37 @@ impl PaneState {
                         vec![],
                         vec![],
                         timezone,
+                        &enabled_indicators,
                     ),
-                    vec![
-                        FootprintIndicator::Volume,
-                        FootprintIndicator::OpenInterest,
-                    ],
+                    enabled_indicators,
                 )
             }
             "candlestick" => {
                 let tick_size = self.set_tick_size(
                     TickMultiplier(1),
-                    ticker_info.tick_size,
+                    ticker_info,
                 );
-                let timeframe = self.set_timeframe(Timeframe::M5);
+                let timeframe = self.set_timeframe(Timeframe::M15);
+                let enabled_indicators = {
+                    if ticker_info.market_type == MarketType::LinearPerps {
+                        vec![
+                            CandlestickIndicator::Volume,
+                            CandlestickIndicator::OpenInterest,
+                        ]
+                    } else {
+                        vec![CandlestickIndicator::Volume]
+                    }
+                };
+
                 PaneContent::Candlestick(
                     CandlestickChart::new(
                         vec![],
                         timeframe,
                         tick_size,
                         timezone,
+                        &enabled_indicators,
                     ),
-                    vec![
-                        CandlestickIndicator::Volume,
-                        CandlestickIndicator::OpenInterest,
-                    ],
+                    enabled_indicators,
                 )
             }
             "time&sales" => PaneContent::TimeAndSales(TimeAndSales::new()),
@@ -264,16 +281,22 @@ impl PaneState {
         timezone: UserTimezone,
     ) {
         match &mut self.content {
-            PaneContent::Candlestick(chart, _) => {
+            PaneContent::Candlestick(chart, indicators) => {
                 if let Some(id) = req_id {
                     chart.insert_new_klines(id, klines);
                 } else {
                     let tick_size = chart.get_tick_size();
 
-                    *chart = CandlestickChart::new(klines.clone(), timeframe, tick_size, timezone);
+                    *chart = CandlestickChart::new(
+                        klines.clone(), 
+                        timeframe, 
+                        tick_size, 
+                        timezone,
+                        indicators,
+                    );
                 }
             }
-            PaneContent::Footprint(chart, _) => {
+            PaneContent::Footprint(chart, indicators) => {
                 if let Some(id) = req_id {
                     chart.insert_new_klines(id, klines);
                 } else {
@@ -285,6 +308,7 @@ impl PaneState {
                         klines.clone(),
                         raw_trades,
                         timezone,
+                        indicators,
                     );
                 }
             }
@@ -417,134 +441,147 @@ impl PaneState {
     }
 }
 
+/// Pane `view()` traits that includes a chart with a `Canvas`
+/// 
+/// e.g. panes for Heatmap, Footprint, Candlestick charts
 trait ChartView {
     fn view<'a, I: Indicator>(
         &'a self, 
         pane: pane_grid::Pane, 
-        state: &PaneState, 
+        state: &'a PaneState, 
         indicators: &'a [I],
     ) -> Element<Message>;
 }
 
-trait PanelView {
-    fn view(&self, pane: pane_grid::Pane, state: &PaneState) -> Element<Message>;
+fn handle_chart_view<'a, F>(
+    underlay: Element<'a, Message>,
+    state: &'a PaneState,
+    pane: pane_grid::Pane,
+    indicators: &'a [impl Indicator],
+    settings_view: F,
+) -> Element<'a, Message>
+where
+    F: FnOnce() -> Element<'a, Message>,
+{
+    match state.modal {
+        PaneModal::StreamModifier => pane_menu(
+            underlay,
+            stream_modifier_view(
+                pane,
+                state.settings.tick_multiply,
+                state.settings.selected_timeframe,
+            ),
+            Message::ToggleModal(pane, PaneModal::None),
+            padding::left(36),
+            Alignment::Start,
+        ),
+        PaneModal::Indicators => pane_menu(
+            underlay,
+            indicators_view(
+                pane,
+                state.settings.ticker_info.map(|info| info.market_type),
+                indicators
+            ),
+            Message::ToggleModal(pane, PaneModal::None),
+            padding::right(12).left(12),
+            Alignment::End,
+        ),
+        PaneModal::Settings => {
+            pane_menu(
+                underlay,
+                settings_view(),
+                Message::ToggleModal(pane, PaneModal::None),
+                padding::right(12).left(12),
+                Alignment::End,
+            )
+        },
+        _ => underlay,
+    }
 }
 
 impl ChartView for HeatmapChart {
     fn view<'a, I: Indicator>(
-        &'a self, 
-        pane: pane_grid::Pane, 
-        state: &PaneState, 
+        &'a self,
+        pane: pane_grid::Pane,
+        state: &'a PaneState,
         indicators: &'a [I],
     ) -> Element<Message> {
         let underlay = self
-            .view(indicators)
+            .view(indicators, state.settings.ticker_info)
             .map(move |message| Message::ChartUserUpdate(pane, message));
 
-        match state.modal {
-            PaneModal::Settings => {
-                let (trade_size_filter, order_size_filter) = self.get_size_filters();
-                pane_menu(
-                    underlay,
-                    size_filter_view(Some(trade_size_filter), Some(order_size_filter), pane),
-                    Message::ToggleModal(pane, PaneModal::None),
-                    padding::right(12).left(12),
-                    Alignment::End,
-                )
-            }
-            PaneModal::StreamModifier => pane_menu(
-                underlay,
-                stream_modifier_view(
-                    pane,
-                    state.settings.tick_multiply,
-                    None,
-                ),
-                Message::ToggleModal(pane, PaneModal::None),
-                padding::left(36),
-                Alignment::Start,
-            ),
-            PaneModal::Indicators => pane_menu(
-                underlay,
-                indicators_view::<I>(pane, indicators),
-                Message::ToggleModal(pane, PaneModal::None),
-                padding::right(12).left(12),
-                Alignment::End,
-            ),
-            _ => underlay,
-        }
+        let settings_view = || {
+            let (trade_size_filter, order_size_filter) = self.get_size_filters();
+            size_filter_view(Some(trade_size_filter), Some(order_size_filter), pane)
+        };
+            
+        handle_chart_view(
+            underlay,
+            state,
+            pane, 
+            indicators, 
+            settings_view,
+        )
     }
 }
 
 impl ChartView for FootprintChart {
     fn view<'a, I: Indicator>(
-        &'a self, 
-        pane: pane_grid::Pane, 
-        state: &PaneState, 
+        &'a self,
+        pane: pane_grid::Pane,
+        state: &'a PaneState,
         indicators: &'a [I],
     ) -> Element<Message> {
         let underlay = self
-            .view(indicators)
+            .view(indicators, state.settings.ticker_info)
             .map(move |message| Message::ChartUserUpdate(pane, message));
 
-        match state.modal {
-            PaneModal::StreamModifier => pane_menu(
-                underlay,
-                stream_modifier_view(
-                    pane,
-                    state.settings.tick_multiply,
-                    state.settings.selected_timeframe,
-                ),
-                Message::ToggleModal(pane, PaneModal::None),
-                padding::left(36),
-                Alignment::Start,
-            ),
-            PaneModal::Indicators => pane_menu(
-                underlay,
-                indicators_view::<I>(pane, indicators),
-                Message::ToggleModal(pane, PaneModal::None),
-                padding::right(12).left(12),
-                Alignment::End,
-            ),
-            _ => underlay,
-        }
+        let settings_view = || {
+            blank_settings_view()
+        };
+
+        handle_chart_view(
+            underlay,
+            state,
+            pane, 
+            indicators, 
+            settings_view,
+        )
     }
 }
 
 impl ChartView for CandlestickChart {
     fn view<'a, I: Indicator>(
         &'a self,
-        pane: pane_grid::Pane, 
-        state: &PaneState, 
+        pane: pane_grid::Pane,
+        state: &'a PaneState,
         indicators: &'a [I],
     ) -> Element<Message> {
         let underlay = self
-            .view(indicators)
+            .view(indicators, state.settings.ticker_info)
             .map(move |message| Message::ChartUserUpdate(pane, message));
+            
+        let settings_view = || {
+            blank_settings_view()
+        };
 
-        match state.modal {
-            PaneModal::StreamModifier => pane_menu(
-                underlay,
-                stream_modifier_view(
-                    pane,
-                    None,
-                    state.settings.selected_timeframe,
-                ),
-                Message::ToggleModal(pane, PaneModal::None),
-                padding::left(36),
-                Alignment::Start,
-            ),
-            PaneModal::Indicators => pane_menu(
-                underlay,
-                indicators_view::<I>(pane, indicators),
-                Message::ToggleModal(pane, PaneModal::None),
-                padding::right(12).left(12),
-                Alignment::End,
-            ),
-            _ => underlay,
-        }
+        handle_chart_view(
+            underlay,
+            state,
+            pane, 
+            indicators, 
+            settings_view,
+        )
     }
 }
 
+/// Pane `view()` traits that doesnt include a chart, `Canvas`
+/// 
+/// e.g. Time&Sales pane
+trait PanelView {
+    fn view(&self, pane: pane_grid::Pane, state: &PaneState) -> Element<Message>;
+}
+
 impl PanelView for TimeAndSales {
     fn view(
         &self, 
@@ -569,9 +606,11 @@ impl PanelView for TimeAndSales {
     }
 }
 
+// Modal views, overlay
 fn indicators_view<I: Indicator> (
     pane: pane_grid::Pane,
-    selected: &[I]
+    market_type: Option<MarketType>,
+    selected: &[I],
 ) -> Element<Message> {
     let mut content_row = column![
         container(
@@ -581,7 +620,7 @@ fn indicators_view<I: Indicator> (
     ]
     .spacing(4);
 
-    for indicator in I::get_available() {
+    for indicator in I::get_available(market_type) {
         content_row = content_row.push(
             if selected.contains(indicator) {
                 button(text(indicator.to_string()))
@@ -672,7 +711,7 @@ fn stream_modifier_view<'a>(
     pane: pane_grid::Pane,
     selected_ticksize: Option<TickMultiplier>,
     selected_timeframe: Option<Timeframe>,
-) -> iced::Element<'a, Message> {
+) -> Element<'a, Message> {
     let create_button = |content: String, msg: Option<Message>| {
         let btn = button(text(content))
             .width(Length::Fill)
@@ -754,20 +793,63 @@ fn stream_modifier_view<'a>(
         .into()
 }
 
+fn blank_settings_view<'a>() -> Element<'a, Message> {
+    container(text("This chart type doesn't have any configurations, WIP..."))
+        .padding(16)
+        .width(Length::Shrink)
+        .max_width(500)
+        .style(style::chart_modal)
+        .into()
+}
+
+fn notification_modals(
+    pane: pane_grid::Pane,
+    notifications: &[Notification],
+) -> Column<Message> {
+    let mut notifications_column = column![].align_x(Alignment::End).spacing(6);
+
+    for notification in notifications.iter().rev().take(5) {
+        let notification_str = match notification {
+            Notification::Error(error) => error.to_string(),
+            Notification::Warn(warn) => warn.to_string(),
+            Notification::Info(info) => match info {
+                InfoType::FetchingKlines => "Fetching klines...".to_string(),
+                InfoType::FetchingTrades(total_fetched) => format!(
+                    "Fetching trades...\n({} fetched)",
+                    total_fetched
+                ),
+                InfoType::FetchingOI => "Fetching open interest...".to_string(),
+            },
+        };
+
+        notifications_column = notifications_column
+            .push(
+                button(container(text(notification_str)).padding(6))
+                    .on_press(Message::HideNotification(pane, notification.clone())),
+            )
+            .padding(12);
+    }
+
+    notifications_column
+}
+
+// Main pane content views, underlays
 fn view_panel<'a, C: PanelView>(
     pane: pane_grid::Pane,
     state: &'a PaneState,
     content: &'a C,
     notifications: Option<&'a Vec<screen::Notification>>,
 ) -> Element<'a, Message> {
-    let base: Container<'_, Message> = center(content.view(pane, state));
+    let base = center(content.view(pane, state));
 
     if let Some(notifications) = notifications {
-        if !notifications.is_empty() {
-            pane_notification(base, create_notis_column(notifications))
-        } else {
-            base.into()
-        }
+        pane_notification(
+            base, 
+            notification_modals(
+                pane,
+                notifications, 
+            )
+        )
     } else {
         base.into()
     }
@@ -780,19 +862,22 @@ fn view_chart<'a, C: ChartView, I: Indicator>(
     notifications: Option<&'a Vec<screen::Notification>>,
     indicators: &'a [I],
 ) -> Element<'a, Message> {
-    let base: Container<'_, Message> = center(content.view(pane, state, indicators));
+    let base = center(content.view(pane, state, indicators));
 
     if let Some(notifications) = notifications {
-        if !notifications.is_empty() {
-            pane_notification(base, create_notis_column(notifications))
-        } else {
-            base.into()
-        }
+        pane_notification(
+            base, 
+            notification_modals(
+                pane,
+                notifications, 
+            )
+        )
     } else {
         base.into()
     }
 }
 
+// Pane controls, title bar
 fn view_controls<'a>(
     pane: pane_grid::Pane,
     total_panes: usize,
@@ -874,11 +959,11 @@ fn view_controls<'a>(
 }
 
 pub enum PaneContent {
+    Starter,
     Heatmap(HeatmapChart, Vec<HeatmapIndicator>),
     Footprint(FootprintChart, Vec<FootprintIndicator>),
     Candlestick(CandlestickChart, Vec<CandlestickIndicator>),
     TimeAndSales(TimeAndSales),
-    Starter,
 }
 
 impl PaneContent {
@@ -893,7 +978,24 @@ impl PaneContent {
 
     pub fn toggle_indicator(&mut self, indicator_str: String) {
         match self {
-            PaneContent::Footprint(_, indicators) => {
+            PaneContent::Heatmap(chart, indicators) => {
+                let indicator = match indicator_str.as_str() {
+                    "Volume" => HeatmapIndicator::Volume,
+                    _ => {
+                        log::error!("indicator not found: {}", indicator_str);
+                        return
+                    },
+                };
+
+                if indicators.contains(&indicator) {
+                    indicators.retain(|i| i != &indicator);
+                } else {
+                    indicators.push(indicator);
+                }
+
+                chart.toggle_indicator(indicator);
+            }
+            PaneContent::Footprint(chart, indicators) => {
                 let indicator = match indicator_str.as_str() {
                     "Volume" => FootprintIndicator::Volume,
                     "Open Interest" => FootprintIndicator::OpenInterest,
@@ -908,8 +1010,10 @@ impl PaneContent {
                 } else {
                     indicators.push(indicator);
                 }
+
+                chart.toggle_indicator(indicator);
             }
-            PaneContent::Candlestick(_, indicators) => {
+            PaneContent::Candlestick(chart, indicators) => {
                 let indicator = match indicator_str.as_str() {
                     "Volume" => CandlestickIndicator::Volume,
                     "Open Interest" => CandlestickIndicator::OpenInterest,
@@ -924,6 +1028,8 @@ impl PaneContent {
                 } else {
                     indicators.push(indicator);
                 }
+
+                chart.toggle_indicator(indicator);
             }
             _ => {}
         }
@@ -932,8 +1038,8 @@ impl PaneContent {
 
 #[derive(Debug, Clone, Copy, Deserialize, Serialize, Default)]
 pub struct PaneSettings {
-    pub min_tick_size: Option<f32>,
+    pub ticker_info: Option<TickerInfo>,
     pub trade_size_filter: Option<f32>,
     pub tick_multiply: Option<TickMultiplier>,
     pub selected_timeframe: Option<Timeframe>,
-}
+}

+ 31 - 3
src/screen/modal.rs

@@ -1,7 +1,5 @@
 use iced::{
-    padding,
-    widget::{container, mouse_area, opaque, stack},
-    Alignment, Element, Length,
+    padding, widget::{center, container, mouse_area, opaque, stack}, Alignment, Color, Element, Length
 };
 
 pub fn dashboard_modal<'a, Message>(
@@ -30,6 +28,36 @@ where
     .into()
 }
 
+
+pub fn confirmation_modal<'a, Message>(
+    base: impl Into<Element<'a, Message>>,
+    content: impl Into<Element<'a, Message>>,
+    on_blur: Message,
+) -> Element<'a, Message>
+where
+    Message: Clone + 'a,
+{
+    stack![
+        base.into(),
+        opaque(
+            mouse_area(center(opaque(content)).style(|_theme| {
+                container::Style {
+                    background: Some(
+                        Color {
+                            a: 0.8,
+                            ..Color::BLACK
+                        }
+                        .into(),
+                    ),
+                    ..container::Style::default()
+                }
+            }))
+            .on_press(on_blur)
+        )
+    ]
+    .into()
+}
+
 pub fn pane_menu<'a, Message>(
     base: impl Into<Element<'a, Message>>,
     content: impl Into<Element<'a, Message>>,

+ 5 - 86
src/style.rs

@@ -87,7 +87,6 @@ pub fn branding_text(theme: &Theme) -> iced::widget::text::Style {
                 .color
                 .scale_alpha(if palette.is_dark { 0.1 } else { 0.8 })
         ),
-        ..Default::default()
     }
 }
 
@@ -352,84 +351,6 @@ pub fn pane_primary(theme: &Theme, is_focused: bool) -> Style {
 }
 
 // Modals
-pub fn pane_info_notification(theme: &Theme, alpha_factor: f32) -> Style {
-    let palette = theme.extended_palette();
-
-    Style {
-        text_color: Some(
-            palette
-                .background
-                .weak
-                .text
-                .scale_alpha(alpha_factor.max(0.3)),
-        ),
-        background: Some(
-            palette
-                .secondary
-                .base
-                .color
-                .scale_alpha(alpha_factor.max(0.3))
-                .into(),
-        ),
-        border: Border {
-            width: 1.0,
-            color: palette.secondary.strong.color.scale_alpha(alpha_factor),
-            radius: 4.0.into(),
-        },
-        shadow: Shadow {
-            offset: iced::Vector { x: 0.0, y: 0.0 },
-            blur_radius: 4.0,
-            color: Color::BLACK.scale_alpha(
-                if palette.is_dark {
-                    1.0
-                } else {
-                    0.4
-                }
-            ),
-        },
-        ..Default::default()
-    }
-}
-
-pub fn pane_err_notification(theme: &Theme, alpha_factor: f32) -> Style {
-    let palette = theme.extended_palette();
-
-    Style {
-        text_color: Some(
-            palette
-                .background
-                .weak
-                .text
-                .scale_alpha(alpha_factor.max(0.3)),
-        ),
-        background: Some(
-            palette
-                .secondary
-                .base
-                .color
-                .scale_alpha(alpha_factor.max(0.3))
-                .into(),
-        ),
-        border: Border {
-            width: 1.0,
-            color: palette.danger.base.color.scale_alpha(alpha_factor),
-            radius: 4.0.into(),
-        },
-        shadow: Shadow {
-            offset: iced::Vector { x: 0.0, y: 0.0 },
-            blur_radius: 4.0,
-            color: Color::BLACK.scale_alpha(
-                if palette.is_dark {
-                    1.0
-                } else {
-                    0.4
-                }
-            ),
-        },
-        ..Default::default()
-    }
-}
-
 pub fn chart_modal(theme: &Theme) -> Style {
     let palette = theme.extended_palette();
 
@@ -521,7 +442,6 @@ pub fn modal_container(theme: &Theme) -> Style {
                 }
             ),
         },
-        ..Default::default()
     }
 }
 
@@ -553,7 +473,6 @@ pub fn sorter_container(theme: &Theme) -> Style {
                 }
             ),
         },
-        ..Default::default()
     }
 }
 
@@ -721,18 +640,18 @@ pub fn scroll_bar(theme: &Theme, status: widget::scrollable::Status) -> widget::
         widget::scrollable::Status::Dragged { .. } 
         | widget::scrollable::Status::Hovered { .. } => {
             (
-                palette.background.weak.color.scale_alpha(0.2 * light_factor).into(),
-                palette.secondary.weak.color.scale_alpha(0.8 * light_factor).into(),
+                palette.background.weak.color.scale_alpha(0.2 * light_factor),
+                palette.secondary.weak.color.scale_alpha(0.8 * light_factor),
             )
         },
         _ => (
-            palette.background.weak.color.scale_alpha(0.1 * light_factor).into(),
-            palette.secondary.weak.color.scale_alpha(0.4 * light_factor).into(),
+            palette.background.weak.color.scale_alpha(0.1 * light_factor),
+            palette.secondary.weak.color.scale_alpha(0.4 * light_factor),
         ),
     };
 
     let rail = Rail {
-        background: Some(rail_bg),
+        background: Some(iced::Background::Color(rail_bg)),
         border: Border {
             radius: 4.0.into(),
             width: 1.0,