Forráskód Böngészése

Unique pane states (#11)

* initial commit

* chore: conciser/better pane management by a trait

* feat: size filter getters/setters

* chore: even more cleanups, looks like it needs a top to bottom refactor

* feat: full pane refactoring to handle same content in diff panes

* uuid for panes to handle pane grid structures

* styling starter pane

* removed unnecessary crates

* feat: managed diff. exchange streams for heatmap working

* refactors to work with new unique panes

* use pane stream types as websocket identifier

* feat: unique time&sales panes, +stream names as pane titles

* avoided cloning by ref counting and other struct refactors

* fix: arg type change to work without cloning data

* ref counting to avoid cloning

* fix: arg type change to work without cloning data

* feat unified pane data event handler

* some refactors on stream type handling, +other small chores

* fix stream type handling to work alongside

* removed unnecessary/old content and structs

* conciser pane content handling, +some other chores, cleanup

* add default derives for data structs

* chore: renaming for clarity

* refactored for code modularity

* more refactors for modularity

* chore: improve ticksize change handling

* chore: simplified ticksize change handling

* fix bybit kline fetch

* chore for making kline data structures in not so confusing way

* better kline stream handling

* refactors for market streams to work independently with multiple tickers

* chore: simplified websocket subscription method

* chore: cleanups, preparing for better err handling

* chore: removed unnecessary method

* feat: trade size filtering now works with this setup

* styling and theme tweaks

* fix: duplicated pane titles, +new styling for picklist

* new style for picklist

* feat: exchange logos for streamlined pane info

* proper handling for timeframe change inputs, +streams can auto drop when no pane found for it

* universal timeframe u16 conversion

* handled stream/pane match on depth websocket end

* fix duplicated websocket connection on timeframe change

* feat different streams getter
Berke 1 éve
szülő
commit
e48f6dbb9e

+ 49 - 292
Cargo.lock

@@ -69,27 +69,6 @@ version = "0.2.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
 
-[[package]]
-name = "android-activity"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee91c0c2905bae44f84bfa4e044536541df26b7703fd0888deeb9060fcc44289"
-dependencies = [
- "android-properties",
- "bitflags 2.5.0",
- "cc",
- "cesu8",
- "jni",
- "jni-sys",
- "libc",
- "log",
- "ndk 0.8.0",
- "ndk-context",
- "ndk-sys 0.5.0+25.2.9519653",
- "num_enum",
- "thiserror",
-]
-
 [[package]]
 name = "android-activity"
 version = "0.6.0"
@@ -104,7 +83,7 @@ dependencies = [
  "jni-sys",
  "libc",
  "log",
- "ndk 0.9.0",
+ "ndk",
  "ndk-context",
  "ndk-sys 0.6.0+11769913",
  "num_enum",
@@ -422,32 +401,13 @@ dependencies = [
  "generic-array",
 ]
 
-[[package]]
-name = "block-sys"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae85a0696e7ea3b835a453750bf002770776609115e6d25c6d2ff28a8200f7e7"
-dependencies = [
- "objc-sys",
-]
-
-[[package]]
-name = "block2"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "15b55663a85f33501257357e6421bb33e769d5c9ffb5ba0921c975a123e35e68"
-dependencies = [
- "block-sys",
- "objc2 0.4.1",
-]
-
 [[package]]
 name = "block2"
 version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
 dependencies = [
- "objc2 0.5.2",
+ "objc2",
 ]
 
 [[package]]
@@ -880,15 +840,6 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b"
 
-[[package]]
-name = "deranged"
-version = "0.3.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
-dependencies = [
- "powerfmt",
-]
-
 [[package]]
 name = "detect-desktop-environment"
 version = "0.2.0"
@@ -1887,20 +1838,6 @@ dependencies = [
  "cc",
 ]
 
-[[package]]
-name = "iced"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d4eb0fbbefb8c428b70680e77ed9013887b17c1d6be366b40f264f956d1a096"
-dependencies = [
- "iced_core 0.12.3",
- "iced_futures 0.12.0",
- "iced_renderer 0.12.1",
- "iced_widget 0.12.3",
- "iced_winit 0.12.2",
- "thiserror",
-]
-
 [[package]]
 name = "iced"
 version = "0.13.0-dev"
@@ -1910,7 +1847,7 @@ dependencies = [
  "iced_futures 0.13.0-dev",
  "iced_renderer 0.13.0-dev",
  "iced_widget 0.13.0-dev",
- "iced_winit 0.13.0-dev",
+ "iced_winit",
  "image",
  "thiserror",
 ]
@@ -1932,8 +1869,7 @@ dependencies = [
  "http-body-util",
  "hyper",
  "hyper-util",
- "iced 0.13.0-dev",
- "iced_aw",
+ "iced",
  "iced_futures 0.12.0",
  "iced_table",
  "native-tls",
@@ -1952,24 +1888,10 @@ dependencies = [
  "tokio-tungstenite",
  "tungstenite",
  "url",
+ "uuid",
  "webpki-roots 0.23.1",
 ]
 
-[[package]]
-name = "iced_aw"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "129deba9897243dd59c2038e2267a691e392c94e569680066ee63b1164429490"
-dependencies = [
- "cfg-if",
- "chrono",
- "iced 0.12.1",
- "itertools",
- "num-traits",
- "once_cell",
- "time",
-]
-
 [[package]]
 name = "iced_core"
 version = "0.12.3"
@@ -2016,7 +1938,6 @@ dependencies = [
  "futures",
  "iced_core 0.12.3",
  "log",
- "tokio",
  "wasm-bindgen-futures",
  "wasm-timer",
 ]
@@ -2231,7 +2152,6 @@ dependencies = [
  "iced_runtime 0.12.1",
  "iced_style",
  "num-traits",
- "ouroboros",
  "thiserror",
  "unicode-segmentation",
 ]
@@ -2250,24 +2170,6 @@ dependencies = [
  "unicode-segmentation",
 ]
 
-[[package]]
-name = "iced_winit"
-version = "0.12.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63f66831d0e399b93f631739121a6171780d344b275d56808b9504d8ca75c7d2"
-dependencies = [
- "iced_graphics 0.12.1",
- "iced_runtime 0.12.1",
- "iced_style",
- "log",
- "thiserror",
- "tracing",
- "web-sys",
- "winapi",
- "window_clipboard",
- "winit 0.29.15",
-]
-
 [[package]]
 name = "iced_winit"
 version = "0.13.0-dev"
@@ -2284,18 +2186,7 @@ dependencies = [
  "web-sys",
  "winapi",
  "window_clipboard",
- "winit 0.30.1",
-]
-
-[[package]]
-name = "icrate"
-version = "0.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99d3aaff8a54577104bafdf686ff18565c3b6903ca5782a2026ef06e2c7aa319"
-dependencies = [
- "block2 0.3.0",
- "dispatch",
- "objc2 0.4.1",
+ "winit",
 ]
 
 [[package]]
@@ -2732,21 +2623,6 @@ dependencies = [
  "tempfile",
 ]
 
-[[package]]
-name = "ndk"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7"
-dependencies = [
- "bitflags 2.5.0",
- "jni-sys",
- "log",
- "ndk-sys 0.5.0+25.2.9519653",
- "num_enum",
- "raw-window-handle",
- "thiserror",
-]
-
 [[package]]
 name = "ndk"
 version = "0.9.0"
@@ -2799,12 +2675,6 @@ 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"
@@ -2846,15 +2716,6 @@ dependencies = [
  "syn 2.0.67",
 ]
 
-[[package]]
-name = "num_threads"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
-dependencies = [
- "libc",
-]
-
 [[package]]
 name = "objc"
 version = "0.2.7"
@@ -2882,16 +2743,6 @@ version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310"
 
-[[package]]
-name = "objc2"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "559c5a40fdd30eb5e344fbceacf7595a81e242529fb4e21cf5f43fb4f11ff98d"
-dependencies = [
- "objc-sys",
- "objc2-encode 3.0.0",
-]
-
 [[package]]
 name = "objc2"
 version = "0.5.2"
@@ -2899,7 +2750,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804"
 dependencies = [
  "objc-sys",
- "objc2-encode 4.0.3",
+ "objc2-encode",
 ]
 
 [[package]]
@@ -2909,9 +2760,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
 dependencies = [
  "bitflags 2.5.0",
- "block2 0.5.1",
+ "block2",
  "libc",
- "objc2 0.5.2",
+ "objc2",
  "objc2-core-data",
  "objc2-core-image",
  "objc2-foundation",
@@ -2925,8 +2776,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
 dependencies = [
  "bitflags 2.5.0",
- "block2 0.5.1",
- "objc2 0.5.2",
+ "block2",
+ "objc2",
  "objc2-core-location",
  "objc2-foundation",
 ]
@@ -2937,8 +2788,8 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
 dependencies = [
- "block2 0.5.1",
- "objc2 0.5.2",
+ "block2",
+ "objc2",
  "objc2-foundation",
 ]
 
@@ -2949,8 +2800,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
 dependencies = [
  "bitflags 2.5.0",
- "block2 0.5.1",
- "objc2 0.5.2",
+ "block2",
+ "objc2",
  "objc2-foundation",
 ]
 
@@ -2960,8 +2811,8 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
 dependencies = [
- "block2 0.5.1",
- "objc2 0.5.2",
+ "block2",
+ "objc2",
  "objc2-foundation",
  "objc2-metal",
 ]
@@ -2972,18 +2823,12 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
 dependencies = [
- "block2 0.5.1",
- "objc2 0.5.2",
+ "block2",
+ "objc2",
  "objc2-contacts",
  "objc2-foundation",
 ]
 
-[[package]]
-name = "objc2-encode"
-version = "3.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d079845b37af429bfe5dfa76e6d087d788031045b25cfc6fd898486fd9847666"
-
 [[package]]
 name = "objc2-encode"
 version = "4.0.3"
@@ -2997,10 +2842,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
 dependencies = [
  "bitflags 2.5.0",
- "block2 0.5.1",
+ "block2",
  "dispatch",
  "libc",
- "objc2 0.5.2",
+ "objc2",
 ]
 
 [[package]]
@@ -3009,8 +2854,8 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
 dependencies = [
- "block2 0.5.1",
- "objc2 0.5.2",
+ "block2",
+ "objc2",
  "objc2-app-kit",
  "objc2-foundation",
 ]
@@ -3022,8 +2867,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
 dependencies = [
  "bitflags 2.5.0",
- "block2 0.5.1",
- "objc2 0.5.2",
+ "block2",
+ "objc2",
  "objc2-foundation",
 ]
 
@@ -3034,8 +2879,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
 dependencies = [
  "bitflags 2.5.0",
- "block2 0.5.1",
- "objc2 0.5.2",
+ "block2",
+ "objc2",
  "objc2-foundation",
  "objc2-metal",
 ]
@@ -3046,7 +2891,7 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc"
 dependencies = [
- "objc2 0.5.2",
+ "objc2",
  "objc2-foundation",
 ]
 
@@ -3057,8 +2902,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
 dependencies = [
  "bitflags 2.5.0",
- "block2 0.5.1",
- "objc2 0.5.2",
+ "block2",
+ "objc2",
  "objc2-cloud-kit",
  "objc2-core-data",
  "objc2-core-image",
@@ -3077,8 +2922,8 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
 dependencies = [
- "block2 0.5.1",
- "objc2 0.5.2",
+ "block2",
+ "objc2",
  "objc2-foundation",
 ]
 
@@ -3089,8 +2934,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
 dependencies = [
  "bitflags 2.5.0",
- "block2 0.5.1",
- "objc2 0.5.2",
+ "block2",
+ "objc2",
  "objc2-core-location",
  "objc2-foundation",
 ]
@@ -3532,12 +3377,6 @@ 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.17"
@@ -3701,15 +3540,6 @@ dependencies = [
  "bitflags 1.3.2",
 ]
 
-[[package]]
-name = "redox_syscall"
-version = "0.3.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
-dependencies = [
- "bitflags 1.3.2",
-]
-
 [[package]]
 name = "redox_syscall"
 version = "0.4.1"
@@ -4017,19 +3847,6 @@ dependencies = [
  "untrusted 0.9.0",
 ]
 
-[[package]]
-name = "sctk-adwaita"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82b2eaf3a5b264a521b988b2e73042e742df700c4f962cde845d1541adb46550"
-dependencies = [
- "ab_glyph",
- "log",
- "memmap2 0.9.4",
- "smithay-client-toolkit",
- "tiny-skia",
-]
-
 [[package]]
 name = "sctk-adwaita"
 version = "0.9.0"
@@ -4286,7 +4103,7 @@ dependencies = [
  "js-sys",
  "log",
  "memmap2 0.9.4",
- "objc2 0.5.2",
+ "objc2",
  "objc2-app-kit",
  "objc2-foundation",
  "objc2-quartz-core",
@@ -4493,27 +4310,6 @@ dependencies = [
  "weezl",
 ]
 
-[[package]]
-name = "time"
-version = "0.3.36"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
-dependencies = [
- "deranged",
- "libc",
- "num-conv",
- "num_threads",
- "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"
@@ -4900,6 +4696,15 @@ version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
 
+[[package]]
+name = "uuid"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
+dependencies = [
+ "getrandom",
+]
+
 [[package]]
 name = "vcpkg"
 version = "0.2.15"
@@ -5563,64 +5368,16 @@ version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
 
-[[package]]
-name = "winit"
-version = "0.29.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d59ad965a635657faf09c8f062badd885748428933dad8e8bdd64064d92e5ca"
-dependencies = [
- "ahash 0.8.11",
- "android-activity 0.5.2",
- "atomic-waker",
- "bitflags 2.5.0",
- "bytemuck",
- "calloop",
- "cfg_aliases 0.1.1",
- "core-foundation",
- "core-graphics",
- "cursor-icon",
- "icrate",
- "js-sys",
- "libc",
- "log",
- "memmap2 0.9.4",
- "ndk 0.8.0",
- "ndk-sys 0.5.0+25.2.9519653",
- "objc2 0.4.1",
- "once_cell",
- "orbclient",
- "percent-encoding",
- "raw-window-handle",
- "redox_syscall 0.3.5",
- "rustix",
- "sctk-adwaita 0.8.1",
- "smithay-client-toolkit",
- "smol_str",
- "unicode-segmentation",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "wayland-backend",
- "wayland-client",
- "wayland-protocols",
- "wayland-protocols-plasma",
- "web-sys",
- "web-time 0.2.4",
- "windows-sys 0.48.0",
- "x11-dl",
- "x11rb",
- "xkbcommon-dl",
-]
-
 [[package]]
 name = "winit"
 version = "0.30.1"
 source = "git+https://github.com/iced-rs/winit.git?rev=254d6b3420ce4e674f516f7a2bd440665e05484d#254d6b3420ce4e674f516f7a2bd440665e05484d"
 dependencies = [
  "ahash 0.8.11",
- "android-activity 0.6.0",
+ "android-activity",
  "atomic-waker",
  "bitflags 2.5.0",
- "block2 0.5.1",
+ "block2",
  "bytemuck",
  "calloop",
  "cfg_aliases 0.2.1",
@@ -5632,8 +5389,8 @@ dependencies = [
  "js-sys",
  "libc",
  "memmap2 0.9.4",
- "ndk 0.9.0",
- "objc2 0.5.2",
+ "ndk",
+ "objc2",
  "objc2-app-kit",
  "objc2-foundation",
  "objc2-ui-kit",
@@ -5643,7 +5400,7 @@ dependencies = [
  "raw-window-handle",
  "redox_syscall 0.4.1",
  "rustix",
- "sctk-adwaita 0.9.0",
+ "sctk-adwaita",
  "smithay-client-toolkit",
  "smol_str",
  "tracing",

+ 1 - 1
Cargo.toml

@@ -29,7 +29,6 @@ sha2 = "0.10.8"
 hex = "0.4.3"
 iced_table = "0.12.0"
 iced_futures = "0.12.0"
-iced_aw = { version = "0.8.0", features = ["quad", "menu"] }
 anyhow = "1.0.86"
 bytes = "1.6.0"
 sonic-rs = "0.3.7"
@@ -39,6 +38,7 @@ hyper = { version = "1", features = ["http1", "server", "client"] }
 hyper-util = { version = "0.1.0", features = ["tokio"] }
 tokio-rustls = "0.24.0"
 webpki-roots = "0.23.0"
+uuid = { version = "1.10.0", features = ["v4"] }
 [dependencies.async-tungstenite]
 version = "0.25"
 features = ["tokio-rustls-webpki-roots"]

+ 6 - 3
src/charts.rs

@@ -9,6 +9,7 @@ use iced::{
 pub mod heatmap;
 pub mod footprint;
 pub mod candlestick;
+pub mod timeandsales;
 
 #[derive(Debug, Clone, Copy)]
 pub enum Message {
@@ -95,9 +96,11 @@ impl Default for Interaction {
     }
 }
 
-fn chart_button(_theme: &Theme, _status: button::Status, is_active: bool) -> button::Style {
+fn chart_button(theme: &Theme, _status: button::Status, is_active: bool) -> button::Style {
+    let palette = theme.extended_palette();
+
     button::Style {
-        background: Some(Color::from_rgba8(20, 20, 20, 1.0).into()),
+        background: Some(Color::BLACK.into()),
         border: Border {
             color: {
                 if is_active {
@@ -109,7 +112,7 @@ fn chart_button(_theme: &Theme, _status: button::Status, is_active: bool) -> but
             width: 1.0,
             radius: 2.0.into(),
         },
-        text_color: Color::WHITE,
+        text_color: palette.background.base.text,
         ..button::Style::default()
     }
 }

+ 25 - 42
src/charts/candlestick.rs

@@ -3,19 +3,19 @@ use iced::{
     alignment, mouse, widget::{button, canvas::{self, event::{self, Event}, stroke::Stroke, Cache, Canvas, Geometry, Path}}, Color, Element, Length, Point, Rectangle, Renderer, Size, Theme
 };
 use iced::widget::{Column, Row, Container, Text};
-use crate::{data_providers::Kline, Timeframe};
+use crate::data_providers::Kline;
 
 use super::{Chart, CommonChartData, Message, Interaction, AxisLabelXCanvas, AxisLabelYCanvas};
 use super::{chart_button, calculate_price_step, calculate_time_step};
 
 pub struct CandlestickChart {
     chart: CommonChartData,
-    data_points: BTreeMap<i64, (f32, f32, f32, f32, f32, f32)>,
+    data_points: BTreeMap<i64, Kline>,
     timeframe: u16,
 }
 
 impl Chart for CandlestickChart {
-    type DataPoint = BTreeMap<i64, (f32, f32, f32, f32, f32, f32)>;
+    type DataPoint = BTreeMap<i64, Kline>;
 
     fn get_common_data(&self) -> &CommonChartData {
         &self.chart
@@ -29,23 +29,13 @@ impl CandlestickChart {
     const MIN_SCALING: f32 = 0.1;
     const MAX_SCALING: f32 = 2.0;
 
-    pub fn new(klines: Vec<Kline>, timeframe: Timeframe) -> CandlestickChart {
+    pub fn new(klines: Vec<Kline>, timeframe: u16) -> CandlestickChart {
         let mut klines_raw = BTreeMap::new();
 
         for kline in klines {
-            let buy_volume = kline.taker_buy_base_asset_volume;
-            let sell_volume = kline.volume - buy_volume;
-            klines_raw.insert(kline.time as i64, (kline.open, kline.high, kline.low, kline.close, buy_volume, sell_volume));
+            klines_raw.insert(kline.time as i64, kline);
         }
 
-        let timeframe = match timeframe {
-            Timeframe::M1 => 1,
-            Timeframe::M3 => 3,
-            Timeframe::M5 => 5,
-            Timeframe::M15 => 15,
-            Timeframe::M30 => 30,
-        };
-
         CandlestickChart {
             chart: CommonChartData::default(),
             data_points: klines_raw,
@@ -53,15 +43,8 @@ impl CandlestickChart {
         }
     }
 
-    pub fn insert_datapoint(&mut self, kline: &Kline) {
-        let buy_volume: f32 = kline.taker_buy_base_asset_volume;
-        let sell_volume: f32 = if buy_volume != -1.0 {
-            kline.volume - buy_volume
-        } else {
-            kline.volume
-        };
-
-        self.data_points.insert(kline.time as i64, (kline.open, kline.high, kline.low, kline.close, buy_volume, sell_volume));
+    pub fn update_latest_kline(&mut self, kline: &Kline) {
+        self.data_points.insert(kline.time as i64, *kline);
 
         self.render_start();
     }
@@ -100,10 +83,10 @@ impl CandlestickChart {
         let visible_klines = self.data_points.range(earliest..=latest);
     
         let (highest, lowest, avg_body_height, count) = visible_klines.fold((f32::MIN, f32::MAX, 0.0f32, 0), |(highest, lowest, total_body_height, count), (_, kline)| {
-            let body_height = (kline.0 - kline.3).abs();
+            let body_height = (kline.open - kline.close).abs();
             (
-                highest.max(kline.1),
-                lowest.min(kline.2),
+                highest.max(kline.high),
+                lowest.min(kline.low),
                 total_body_height + body_height,
                 count + 1,
             )
@@ -437,18 +420,18 @@ impl canvas::Program<Message> for CandlestickChart {
             let mut max_volume: f32 = 0.0;
 
             for (_, kline) in self.data_points.range(earliest..=latest) {
-                max_volume = max_volume.max(kline.4.max(kline.5));
+                max_volume = max_volume.max(kline.volume.0.max(kline.volume.1));
             }
 
-            for (time, (open, high, low, close, buy_volume, sell_volume)) in self.data_points.range(earliest..=latest) {
+            for (time, kline) in self.data_points.range(earliest..=latest) {
                 let x_position: f64 = ((time - earliest) as f64 / (latest - earliest) as f64) * bounds.width as f64;
                 
-                let y_open = candlesticks_area_height - ((open - lowest) / y_range * candlesticks_area_height);
-                let y_high = candlesticks_area_height - ((high - lowest) / y_range * candlesticks_area_height);
-                let y_low = candlesticks_area_height - ((low - lowest) / y_range * candlesticks_area_height);
-                let y_close = candlesticks_area_height - ((close - lowest) / y_range * candlesticks_area_height);
+                let y_open = candlesticks_area_height - ((kline.open - lowest) / y_range * candlesticks_area_height);
+                let y_high = candlesticks_area_height - ((kline.high - lowest) / y_range * candlesticks_area_height);
+                let y_low = candlesticks_area_height - ((kline.low - lowest) / y_range * candlesticks_area_height);
+                let y_close = candlesticks_area_height - ((kline.close - lowest) / y_range * candlesticks_area_height);
                 
-                let color = if close >= open { Color::from_rgb8(81, 205, 160) } else { Color::from_rgb8(192, 80, 77) };
+                let color = if kline.close >= kline.open { Color::from_rgb8(81, 205, 160) } else { Color::from_rgb8(192, 80, 77) };
 
                 let body = Path::rectangle(
                     Point::new(x_position as f32 - (2.0 * chart.scaling), y_open.min(y_close)), 
@@ -462,9 +445,9 @@ impl canvas::Program<Message> for CandlestickChart {
                 );
                 frame.stroke(&wick, Stroke::default().with_color(color).with_width(1.0));
 
-                if *buy_volume != -1.0 {
-                    let buy_bar_height = (buy_volume / max_volume) * volume_area_height;
-                    let sell_bar_height = (sell_volume / max_volume) * volume_area_height;
+                if kline.volume.0 != -1.0 {
+                    let buy_bar_height = (kline.volume.0 / max_volume) * volume_area_height;
+                    let sell_bar_height = (kline.volume.1 / max_volume) * volume_area_height;
                     
                     let buy_bar = Path::rectangle(
                         Point::new(x_position as f32, bounds.height - buy_bar_height), 
@@ -478,13 +461,13 @@ impl canvas::Program<Message> for CandlestickChart {
                     );
                     frame.fill(&sell_bar, Color::from_rgb8(192, 80, 77)); 
                 } else {
-                    let bar_height = ((sell_volume) / max_volume) * volume_area_height;
+                    let bar_height = ((kline.volume.1) / max_volume) * volume_area_height;
                     
                     let bar = Path::rectangle(
                         Point::new(x_position as f32 - (2.0 * chart.scaling), bounds.height - bar_height), 
                         Size::new(4.0 * chart.scaling, bar_height)
                     );
-                    let color = if close >= open { Color::from_rgba8(81, 205, 160, 0.8) } else { Color::from_rgba8(192, 80, 77, 0.8) };
+                    let color = if kline.close >= kline.open { Color::from_rgba8(81, 205, 160, 0.8) } else { Color::from_rgba8(192, 80, 77, 0.8) };
 
                     frame.fill(&bar, color);
                 }
@@ -517,15 +500,15 @@ impl canvas::Program<Message> for CandlestickChart {
                         .find(|(time, _)| **time == rounded_timestamp as i64) {
 
                         
-                        let tooltip_text: String = if kline.4 != -1.0 {
+                        let tooltip_text: String = if kline.volume.0 != -1.0 {
                             format!(
                                 "O: {} H: {} L: {} C: {}\nBuyV: {:.0} SellV: {:.0}",
-                                kline.0, kline.1, kline.2, kline.3, kline.4, kline.5
+                                kline.open, kline.high, kline.low, kline.close, kline.volume.0, kline.volume.1
                             )
                         } else {
                             format!(
                                 "O: {} H: {} L: {} C: {}\nVolume: {:.0}",
-                                kline.0, kline.1, kline.2, kline.3, kline.5
+                                kline.open, kline.high, kline.low, kline.close, kline.volume.1
                             )
                         };
 

+ 51 - 49
src/charts/footprint.rs

@@ -8,16 +8,9 @@ use crate::data_providers::{Kline, Trade};
 use super::{Chart, CommonChartData, Message, Interaction, AxisLabelXCanvas, AxisLabelYCanvas};
 use super::chart_button;
 
-pub struct FootprintChart {
-    chart: CommonChartData,
-    data_points: BTreeMap<i64, (HashMap<i64, (f32, f32)>, (f32, f32, f32, f32, f32, f32))>,
-    timeframe: u16,
-    tick_size: f32,
-    raw_trades: Vec<Trade>,
-}
 
 impl Chart for FootprintChart {
-    type DataPoint = BTreeMap<i64, (HashMap<i64, (f32, f32)>, (f32, f32, f32, f32, f32, f32))>;
+    type DataPoint = BTreeMap<i64, (HashMap<i64, (f32, f32)>, Kline)>;
 
     fn get_common_data(&self) -> &CommonChartData {
         &self.chart
@@ -27,25 +20,32 @@ impl Chart for FootprintChart {
     }
 }
 
+pub struct FootprintChart {
+    chart: CommonChartData,
+    data_points: BTreeMap<i64, (HashMap<i64, (f32, f32)>, Kline)>,
+    timeframe: u16,
+    tick_size: f32,
+    raw_trades: Vec<Trade>,
+}
+
 impl FootprintChart {
     const MIN_SCALING: f32 = 0.4;
     const MAX_SCALING: f32 = 3.6;
 
-    pub fn new(timeframe: u16, tick_size: f32, klines_raw: Vec<(i64, f32, f32, f32, f32, f32, f32)>, raw_trades: Vec<Trade>) -> Self {
+    pub fn new(timeframe: u16, tick_size: f32, klines_raw: Vec<Kline>, raw_trades: Vec<Trade>) -> Self {
         let mut data_points = BTreeMap::new();
         let aggregate_time = 1000 * 60 * timeframe as i64;
 
         for kline in klines_raw {
-            let kline_raw = (kline.1, kline.2, kline.3, kline.4, kline.5, kline.6);
-            data_points.entry(kline.0).or_insert((HashMap::new(), kline_raw));
+            data_points.entry(kline.time as i64).or_insert((HashMap::new(), kline));
         };
         for trade in &raw_trades {
             let rounded_time = (trade.time / aggregate_time) * aggregate_time;
             let price_level: i64 = (trade.price * (1.0 / tick_size)).round() as i64;
 
-            let entry: &mut (HashMap<i64, (f32, f32)>, (f32, f32, f32, f32, f32, f32)) = data_points
+            let entry = data_points
                 .entry(rounded_time)
-                .or_insert((HashMap::new(), (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)));
+                .or_insert((HashMap::new(), Kline::default()));
 
             if let Some((buy_qty, sell_qty)) = entry.0.get_mut(&price_level) {
                 if trade.is_sell {
@@ -69,13 +69,13 @@ impl FootprintChart {
         }
     }
 
-    pub fn insert_datapoint(&mut self, mut trades_buffer: Vec<Trade>, depth_update: i64) {
+    pub fn insert_datapoint(&mut self, trades_buffer: &[Trade], depth_update: i64) {
         let aggregate_time = 1000 * 60 * self.timeframe as i64;
         let rounded_depth_update = (depth_update / aggregate_time) * aggregate_time;
     
-        self.data_points.entry(rounded_depth_update).or_insert((HashMap::new(), (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)));
+        self.data_points.entry(rounded_depth_update).or_insert((HashMap::new(), Kline::default()));
         
-        for trade in trades_buffer.drain(..) {
+        for trade in trades_buffer {
             let price_level: i64 = (trade.price * (1.0 / self.tick_size)).round() as i64;
             if let Some((trades, _)) = self.data_points.get_mut(&rounded_depth_update) {     
                 if let Some((buy_qty, sell_qty)) = trades.get_mut(&price_level) {
@@ -91,27 +91,29 @@ impl FootprintChart {
                 }
             }
 
-            self.raw_trades.push(trade);
+            self.raw_trades.push(*trade);
         }
     }
 
     pub fn update_latest_kline(&mut self, kline: &Kline) {
         if let Some((_, kline_value)) = self.data_points.get_mut(&(kline.time as i64)) {
-            kline_value.0 = kline.open;
-            kline_value.1 = kline.high;
-            kline_value.2 = kline.low;
-            kline_value.3 = kline.close;
-            kline_value.4 = kline.taker_buy_base_asset_volume;
-            
-            if kline_value.4 != -1.0 {
-                kline_value.5 = kline.volume - kline.taker_buy_base_asset_volume;
-            } else {
-                kline_value.5 = kline.volume;
-            }
-        }
+            kline_value.open = kline.open;
+            kline_value.high = kline.high;
+            kline_value.low = kline.low;
+            kline_value.close = kline.close;
+            kline_value.volume = kline.volume;
+        } 
 
         self.render_start();
     }
+
+    pub fn get_raw_trades(&self) -> Vec<Trade> {
+        self.raw_trades.clone()
+    }
+    
+    pub fn get_tick_size(&self) -> f32 {
+        self.tick_size
+    }
     
     pub fn change_tick_size(&mut self, new_tick_size: f32) {
         let mut new_data_points = BTreeMap::new();
@@ -127,7 +129,7 @@ impl FootprintChart {
 
             let entry = new_data_points
                 .entry(rounded_time)
-                .or_insert((HashMap::new(), (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)));
+                .or_insert((HashMap::new(), Kline::default()));
 
             if let Some((buy_qty, sell_qty)) = entry.0.get_mut(&price_level) {
                 if trade.is_sell {
@@ -186,11 +188,11 @@ impl FootprintChart {
         let mut lowest: f32 = std::f32::MAX;
 
         for (_, (_, kline)) in self.data_points.range(earliest..=latest) {
-            if kline.1 > highest {
-                highest = kline.1;
+            if kline.high > highest {
+                highest = kline.high;
             }
-            if kline.2 < lowest {
-                lowest = kline.2;
+            if kline.low < lowest {
+                lowest = kline.low;
             }
         }
 
@@ -482,7 +484,7 @@ impl canvas::Program<Message> for FootprintChart {
                 for trade in trades {            
                     max_trade_qty = max_trade_qty.max(trade.1.0.max(trade.1.1));
                 }
-                max_volume = max_volume.max(kline.4.max(kline.5));
+                max_volume = max_volume.max(kline.volume.0.max(kline.volume.1));
             }
             
             for (time, (trades, kline)) in self.data_points.range(earliest..=latest) {
@@ -492,13 +494,13 @@ impl canvas::Program<Message> for FootprintChart {
                     continue;
                 }
 
-                let y_open = heatmap_area_height - ((kline.0 - lowest) / y_range * heatmap_area_height);
-                let y_high = heatmap_area_height - ((kline.1 - lowest) / y_range * heatmap_area_height);
-                let y_low = heatmap_area_height - ((kline.2 - lowest) / y_range * heatmap_area_height);
-                let y_close = heatmap_area_height - ((kline.3 - lowest) / y_range * heatmap_area_height);
+                let y_open = heatmap_area_height - ((kline.open - lowest) / y_range * heatmap_area_height);
+                let y_high = heatmap_area_height - ((kline.high - lowest) / y_range * heatmap_area_height);
+                let y_low = heatmap_area_height - ((kline.low - lowest) / y_range * heatmap_area_height);
+                let y_close = heatmap_area_height - ((kline.close - lowest) / y_range * heatmap_area_height);
                 
-                let body_color = if kline.3 >= kline.0 { Color::from_rgba8(81, 205, 160, 0.8) } else { Color::from_rgba8(192, 80, 77, 0.8) };
-                let wick_color = if kline.3 >= kline.0 { Color::from_rgba8(81, 205, 160, 0.4) } else { Color::from_rgba8(192, 80, 77, 0.4) };
+                let body_color = if kline.close >= kline.open { Color::from_rgba8(81, 205, 160, 0.8) } else { Color::from_rgba8(192, 80, 77, 0.8) };
+                let wick_color = if kline.close >= kline.open { Color::from_rgba8(81, 205, 160, 0.4) } else { Color::from_rgba8(192, 80, 77, 0.4) };
 
                 let wick = Path::line(
                     Point::new(x_position, y_high), 
@@ -535,9 +537,9 @@ impl canvas::Program<Message> for FootprintChart {
                 }
 
                 if max_volume > 0.0 {
-                    if kline.4 != -1.0 {
-                        let buy_bar_height = (kline.4 / max_volume) * volume_area_height;
-                        let sell_bar_height = (kline.5 / max_volume) * volume_area_height;
+                    if kline.volume.0 != -1.0 {
+                        let buy_bar_height = (kline.volume.0 / max_volume) * volume_area_height;
+                        let sell_bar_height = (kline.volume.1 / max_volume) * volume_area_height;
 
                         let sell_bar_width = 8.0 * chart.scaling;
                         let sell_bar_x_position = x_position - (5.0*chart.scaling) - sell_bar_width;
@@ -553,12 +555,12 @@ impl canvas::Program<Message> for FootprintChart {
                         );
                         frame.fill(&buy_bar, Color::from_rgb8(81, 205, 160));
                     } else {
-                        let bar_height = (kline.5 / max_volume) * volume_area_height;
+                        let bar_height = (kline.volume.1 / max_volume) * volume_area_height;
                         let bar = Path::rectangle(
                             Point::new(x_position - (3.0*chart.scaling), bounds.height - bar_height), 
                             Size::new(6.0 * chart.scaling, bar_height)
                         );
-                        let color = if kline.3 >= kline.0 { Color::from_rgba8(81, 205, 160, 0.8) } else { Color::from_rgba8(192, 80, 77, 0.8) };
+                        let color = if kline.close >= kline.open { Color::from_rgba8(81, 205, 160, 0.8) } else { Color::from_rgba8(192, 80, 77, 0.8) };
 
                         frame.fill(&bar, color);
                     }
@@ -605,15 +607,15 @@ impl canvas::Program<Message> for FootprintChart {
                     if let Some((_, kline)) = self.data_points.iter()
                         .find(|(time, _)| **time == rounded_timestamp) {
 
-                            let tooltip_text: String = if kline.1.4 != -1.0 {
+                            let tooltip_text: String = if kline.1.volume.0 != -1.0 {
                                 format!(
                                     "O: {} H: {} L: {} C: {}\nBuyV: {:.0} SellV: {:.0}",
-                                    kline.1.0, kline.1.1, kline.1.2, kline.1.3, kline.1.4, kline.1.5
+                                    kline.1.open, kline.1.high, kline.1.low, kline.1.close, kline.1.volume.0, kline.1.volume.1
                                 )
                             } else {
                                 format!(
                                     "O: {} H: {} L: {} C: {}\nVolume: {:.0}",
-                                    kline.1.0, kline.1.1, kline.1.2, kline.1.3, kline.1.5
+                                    kline.1.open, kline.1.high, kline.1.low, kline.1.close, kline.1.volume.1
                                 )
                             };
 

+ 13 - 8
src/charts/heatmap.rs

@@ -1,4 +1,4 @@
-use std::collections::{BTreeMap, HashMap};
+use std::{collections::BTreeMap, rc::Rc};
 use chrono::NaiveDateTime;
 use iced::{
     alignment, color, mouse, widget::{button, canvas::{self, event::{self, Event}, stroke::Stroke, Cache, Canvas, Geometry, Path}}, window, Border, Color, Element, Length, Point, Rectangle, Renderer, Size, Theme, Vector
@@ -11,7 +11,7 @@ use super::{Chart, CommonChartData, Message, chart_button, Interaction, AxisLabe
 
 pub struct HeatmapChart {
     chart: CommonChartData,
-    data_points: BTreeMap<i64, (Depth, Box<[Trade]>)>,
+    data_points: BTreeMap<i64, (Rc<Depth>, Box<[Trade]>)>,
     tick_size: f32,
     y_scaling: f32,
     size_filter: f32,
@@ -45,6 +45,9 @@ impl HeatmapChart {
     pub fn set_size_filter(&mut self, size_filter: f32) {
         self.size_filter = size_filter;
     }
+    pub fn get_size_filter(&self) -> f32 {
+        self.size_filter
+    }
 
     pub fn get_raw_trades(&mut self) -> Vec<Trade> {
         let mut trades_source = vec![];
@@ -56,21 +59,23 @@ impl HeatmapChart {
         trades_source
     }
 
-    pub fn insert_datapoint(&mut self, trades_buffer: Vec<Trade>, depth_update: i64, depth: Depth) {
+    pub fn insert_datapoint(&mut self, trades_buffer: &[Trade], depth_update: i64, depth: Rc<Depth>) {
         let aggregate_time = 100; // 100 ms
         let rounded_depth_update = (depth_update / aggregate_time) * aggregate_time;
         
-        self.data_points.entry(rounded_depth_update).or_insert((depth, trades_buffer.into_boxed_slice()));
+        self.data_points.insert(rounded_depth_update, (depth, trades_buffer.into()));
         
-        if self.data_points.len() > 3600 {
-            while let Some((&key_to_remove, _)) = self.data_points.iter().next() {
+        while self.data_points.len() > 3600 {
+            if let Some((&key_to_remove, _)) = self.data_points.first_key_value() {
                 self.data_points.remove(&key_to_remove);
                 if self.data_points.len() <= 3000 {
                     break;
                 }
+            } else {
+                break;
             }
-        }     
-
+        }
+        
         self.render_start();
     }
 

+ 108 - 0
src/charts/timeandsales.rs

@@ -0,0 +1,108 @@
+use chrono::NaiveDateTime;
+use iced::{
+    alignment, Element, Length
+};
+use iced::widget::{Column, Row, Container, Text, container, Space};
+use crate::{Message, style, data_providers::Trade};
+
+struct ConvertedTrade {
+    time: NaiveDateTime,
+    price: f32,
+    qty: f32,
+    is_sell: bool,
+}
+pub struct TimeAndSales {
+    recent_trades: Vec<ConvertedTrade>,
+    size_filter: f32,
+    filter_sync_heatmap: bool,
+}
+impl TimeAndSales {
+    pub fn new() -> Self {
+        Self {
+            recent_trades: Vec::new(),
+            size_filter: 0.0,
+            filter_sync_heatmap: false,
+        }
+    }
+    
+    pub fn set_size_filter(&mut self, value: f32) {
+        self.size_filter = value;
+    }
+    pub fn get_size_filter(&self) -> f32 {
+        self.size_filter
+    }
+
+    pub fn set_filter_sync_heatmap(&mut self, value: bool) {
+        self.filter_sync_heatmap = value;
+    }
+    pub fn get_filter_sync_heatmap(&self) -> bool {
+        self.filter_sync_heatmap
+    }
+
+    pub fn update(&mut self, trades_buffer: &[Trade]) {
+        for trade in trades_buffer {
+            let trade_time = NaiveDateTime::from_timestamp(trade.time / 1000, (trade.time % 1000) as u32 * 1_000_000);
+            let converted_trade = ConvertedTrade {
+                time: trade_time,
+                price: trade.price,
+                qty: trade.qty,
+                is_sell: trade.is_sell,
+            };
+            self.recent_trades.push(converted_trade);
+        }
+
+        if self.recent_trades.len() > 2000 {
+            let drain_to = self.recent_trades.len() - 2000;
+            self.recent_trades.drain(0..drain_to);
+        }
+    }
+    pub fn view(&self) -> Element<'_, Message> {
+        let mut trades_column = Column::new()
+            .height(Length::Fill)
+            .padding(10);
+
+        let filtered_trades: Vec<_> = self.recent_trades.iter().filter(|trade| (trade.qty*trade.price) >= self.size_filter).collect();
+
+        let max_qty = filtered_trades.iter().map(|trade| trade.qty).fold(0.0, f32::max);
+    
+        if filtered_trades.is_empty() {
+            trades_column = trades_column.push(
+                Text::new("No trades")
+                    .width(Length::Fill)
+                    .height(Length::Fill)
+                    .size(16)
+            );
+        } else {
+            for trade in filtered_trades.iter().rev().take(80) {
+                let trade: &ConvertedTrade = trade;
+
+                let trade_row = Row::new()
+                    .push(
+                        container(Text::new(format!("{}", trade.time.format("%M:%S.%3f"))).size(14))
+                            .width(Length::FillPortion(8)).align_x(alignment::Horizontal::Center)
+                    )
+                    .push(
+                        container(Text::new(format!("{}", trade.price)).size(14))
+                            .width(Length::FillPortion(6))
+                    )
+                    .push(
+                        container(Text::new(if trade.is_sell { "Sell" } else { "Buy" }).size(14))
+                            .width(Length::FillPortion(4)).align_x(alignment::Horizontal::Left)
+                    )
+                    .push(
+                        container(Text::new(format!("{}", trade.qty)).size(14))
+                            .width(Length::FillPortion(4))
+                    );
+
+                let color_alpha = trade.qty / max_qty;
+    
+                trades_column = trades_column.push(container(trade_row)
+                    .style( move |_| if trade.is_sell { style::sell_side_red(color_alpha) } else { style::buy_side_green(color_alpha) }));
+    
+                trades_column = trades_column.push(Container::new(Space::new(Length::Fixed(0.0), Length::Fixed(5.0))));
+            }
+        }
+    
+        trades_column.into()  
+    }    
+}

+ 157 - 5
src/data_providers.rs

@@ -1,6 +1,21 @@
 pub mod binance;
 pub mod bybit;
 
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
+pub enum StreamType {
+    Kline {
+        exchange: Exchange,
+        ticker: Ticker,
+        timeframe: Timeframe,
+    },
+    DepthAndTrades {
+        exchange: Exchange,
+        ticker: Ticker,
+    },
+    None,
+}
+
+// data types
 #[derive(Debug, Clone, Copy, Default)]
 pub struct Order {
     pub price: f32,
@@ -109,7 +124,7 @@ impl LocalDepthCache {
     }
 }
 
-#[derive(Debug, Clone, Copy)]
+#[derive(Default, Debug, Clone, Copy)]
 pub struct Trade {
     pub time: i64,
     pub is_sell: bool,
@@ -117,18 +132,17 @@ pub struct Trade {
     pub qty: f32,
 }
 
-#[derive(Debug, Clone, Copy)]
+#[derive(Default, Debug, Clone, Copy)]
 pub struct Kline {
     pub time: u64,
     pub open: f32,
     pub high: f32,
     pub low: f32,
     pub close: f32,
-    pub volume: f32,
-    pub taker_buy_base_asset_volume: f32,
+    pub volume: (f32, f32),
 }
 
-#[derive(Debug, Clone, Copy)]
+#[derive(Default, Debug, Clone, Copy)]
 pub struct FeedLatency {
     pub time: i64,
     pub depth_latency: i64,
@@ -140,3 +154,141 @@ pub trait DataProvider {
 
     fn get_trades(&self, symbol: &str) -> Result<Vec<Trade>, Box<dyn std::error::Error>>;
 }
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct TickMultiplier(pub u16);
+
+impl std::fmt::Display for TickMultiplier {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        write!(f, "{}x", self.0)
+    }
+}
+
+impl TickMultiplier {
+    pub fn multiply_with_min_tick_size(&self, min_tick_size: f32) -> f32 {
+        self.0 as f32 * min_tick_size
+    }
+}
+
+// connection types
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum Exchange {
+    BinanceFutures,
+    BybitLinear,
+}
+
+impl std::fmt::Display for Exchange {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "{}",
+            match self {
+                Exchange::BinanceFutures => "Binance Futures",
+                Exchange::BybitLinear => "Bybit Linear",
+            }
+        )
+    }
+}
+impl Exchange {
+    pub const ALL: [Exchange; 2] = [Exchange::BinanceFutures, Exchange::BybitLinear];
+}
+
+impl std::fmt::Display for Ticker {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "{}",
+            match self {
+                Ticker::BTCUSDT => "BTCUSDT",
+                Ticker::ETHUSDT => "ETHUSDT",
+                Ticker::SOLUSDT => "SOLUSDT",
+                Ticker::LTCUSDT => "LTCUSDT",
+            }
+        )
+    }
+}
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum Ticker {
+    BTCUSDT,
+    ETHUSDT,
+    SOLUSDT,
+    LTCUSDT,
+}
+impl Ticker {
+    pub const ALL: [Ticker; 4] = [Ticker::BTCUSDT, Ticker::ETHUSDT, Ticker::SOLUSDT, Ticker::LTCUSDT];
+}
+
+impl std::fmt::Display for Timeframe {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "{}",
+            match self {
+                Timeframe::M1 => "1m",
+                Timeframe::M3 => "3m",
+                Timeframe::M5 => "5m",
+                Timeframe::M15 => "15m",
+                Timeframe::M30 => "30m",
+            }
+        )
+    }
+}
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum Timeframe {
+    M1,
+    M3,
+    M5,
+    M15,
+    M30,
+}
+impl Timeframe {
+    pub const ALL: [Timeframe; 5] = [Timeframe::M1, Timeframe::M3, Timeframe::M5, Timeframe::M15, Timeframe::M30];
+
+    pub fn to_minutes(&self) -> u16 {
+        match self {
+            Timeframe::M1 => 1,
+            Timeframe::M3 => 3,
+            Timeframe::M5 => 5,
+            Timeframe::M15 => 15,
+            Timeframe::M30 => 30,
+        }
+    }
+}
+
+#[derive(Debug)]
+pub enum BinanceWsState {
+    Connected(binance::market_data::Connection),
+    Disconnected,
+}
+impl Default for BinanceWsState {
+    fn default() -> Self {
+        Self::Disconnected
+    }
+}
+
+#[derive(Debug)]
+pub enum BybitWsState {
+    Connected(bybit::market_data::Connection),
+    Disconnected,
+}
+impl Default for BybitWsState {
+    fn default() -> Self {
+        Self::Disconnected
+    }
+}
+
+pub enum UserWsState {
+    Connected(binance::user_data::Connection),
+    Disconnected,
+}
+impl Default for UserWsState {
+    fn default() -> Self {
+        Self::Disconnected
+    }
+}
+
+#[derive(Debug, Clone)]
+pub enum MarketEvents {
+    Binance(binance::market_data::Event),
+    Bybit(bybit::market_data::Event),
+}

+ 46 - 32
src/data_providers/binance/market_data.rs

@@ -39,8 +39,8 @@ enum State {
 pub enum Event {
     Connected(Connection),
     Disconnected,
-    DepthReceived(FeedLatency, i64, Depth, Vec<Trade>),
-    KlineReceived(Kline, Timeframe),
+    DepthReceived(Ticker, FeedLatency, i64, Depth, Vec<Trade>),
+    KlineReceived(Ticker, Kline, Timeframe),
 }
 
 #[derive(Debug, Clone)]
@@ -127,6 +127,8 @@ struct SonicKline {
 
 #[derive(Deserialize, Debug, Clone)]
 struct SonicKlineWrap {
+    #[serde(rename = "s")]
+    symbol: String,
     #[serde(rename = "k")]
     kline: SonicKline,
 }
@@ -135,7 +137,7 @@ struct SonicKlineWrap {
 enum StreamData {
 	Trade(SonicTrade),
 	Depth(SonicDepth),
-    Kline(SonicKline),
+    Kline(Ticker, SonicKline),
 }
 
 #[derive(Debug)]
@@ -146,12 +148,16 @@ enum StreamName {
     Unknown,
 }
 impl StreamName {
-    fn from_symbol_and_type(symbol: &str, stream_type: &str) -> Self {
-        match stream_type {
-            _ if stream_type == format!("{symbol}@depth@100ms") => StreamName::Depth,
-            _ if stream_type == format!("{symbol}@trade") => StreamName::Trade,
-            _ if stream_type.starts_with(&format!("{symbol}@kline_")) => StreamName::Kline,
-            _ => StreamName::Unknown,
+    fn from_stream_type(stream_type: &str) -> Self {
+        if let Some(after_at) = stream_type.split('@').nth(1) {
+            match after_at {
+                _ if after_at.starts_with("dep") => StreamName::Depth,
+                _ if after_at.starts_with("tra") => StreamName::Trade,
+                _ if after_at.starts_with("kli") => StreamName::Kline,
+                _ => StreamName::Unknown,
+            }
+        } else {
+            StreamName::Unknown
         }
     }
 }
@@ -163,7 +169,7 @@ enum StreamWrapper {
     Kline,
 }
 
-fn feed_de(bytes: &Bytes, symbol: &str) -> Result<StreamData> {
+fn feed_de(bytes: &Bytes) -> Result<StreamData> {
 	let mut stream_type: Option<StreamWrapper> = None;
 
 	let iter: sonic_rs::ObjectJsonIter = unsafe { to_object_iter_unchecked(bytes) };
@@ -174,7 +180,7 @@ fn feed_de(bytes: &Bytes, symbol: &str) -> Result<StreamData> {
 
 		if k == "stream" {
 			if let Some(val) = v.as_str() {
-                match StreamName::from_symbol_and_type(symbol, val) {
+                match StreamName::from_stream_type(val) {
 					StreamName::Depth => {
 						stream_type = Some(StreamWrapper::Depth);
 					},
@@ -207,7 +213,15 @@ fn feed_de(bytes: &Bytes, symbol: &str) -> Result<StreamData> {
                     let kline_wrap: SonicKlineWrap = sonic_rs::from_str(&v.as_raw_faststr())
                         .context("Error parsing kline")?;
 
-                    return Ok(StreamData::Kline(kline_wrap.kline));
+                    let ticker = match &kline_wrap.symbol[..] {
+                        "BTCUSDT" => Ticker::BTCUSDT,
+                        "ETHUSDT" => Ticker::ETHUSDT,
+                        "SOLUSDT" => Ticker::SOLUSDT,
+                        "LTCUSDT" => Ticker::LTCUSDT,
+                        _ => Ticker::BTCUSDT,
+                    };
+
+                    return Ok(StreamData::Kline(ticker, kline_wrap.kline));
                 },
 				_ => {
 					eprintln!("Unknown stream type");
@@ -286,16 +300,16 @@ where
   }
 }
 
-pub fn connect_market_stream(selected_ticker: Ticker) -> Subscription<Event> {
-    struct Connect;
-
+pub fn connect_market_stream(stream: Ticker) -> Subscription<Event> {
     subscription::channel(
-        std::any::TypeId::of::<Connect>(),
+        stream,
         100,
         move |mut output| async move {
             let mut state = State::Disconnected;     
             let mut trades_buffer: Vec<Trade> = Vec::new(); 
 
+            let selected_ticker = stream;
+
             let symbol_str = match selected_ticker {
                 Ticker::BTCUSDT => "btcusdt",
                 Ticker::ETHUSDT => "ethusdt",
@@ -365,7 +379,7 @@ pub fn connect_market_stream(selected_ticker: Ticker) -> Subscription<Event> {
                                 OpCode::Text => {                    
                                     let json_bytes: Bytes = Bytes::from(msg.payload.to_vec());
                     
-                                    if let Ok(data) = feed_de(&json_bytes, symbol_str) {
+                                    if let Ok(data) = feed_de(&json_bytes) {
                                         match data {
                                             StreamData::Trade(de_trade) => {
                                                 let trade = Trade {
@@ -464,6 +478,7 @@ pub fn connect_market_stream(selected_ticker: Ticker) -> Subscription<Event> {
     
                                                     let _ = output.send(
                                                         Event::DepthReceived(
+                                                            selected_ticker,
                                                             feed_latency,
                                                             time, 
                                                             current_depth,
@@ -499,19 +514,15 @@ pub fn connect_market_stream(selected_ticker: Ticker) -> Subscription<Event> {
     )
 }
 
-pub fn connect_kline_stream(vec: Vec<(Ticker, Timeframe)>) -> Subscription<Event> {
-    struct Connect;
-
+pub fn connect_kline_stream(streams: Vec<(Ticker, Timeframe)>) -> Subscription<Event> {
     subscription::channel(
-        std::any::TypeId::of::<Connect>(),
+        streams.clone(),
         100,
         move |mut output| async move {
             let mut state = State::Disconnected;    
 
-            let mut symbol_str: &str = "";
-
-            let stream_str = vec.iter().map(|(ticker, timeframe)| {
-                symbol_str = match ticker {
+            let stream_str = streams.iter().map(|(ticker, timeframe)| {
+                let symbol_str = match ticker {
                     Ticker::BTCUSDT => "btcusdt",
                     Ticker::ETHUSDT => "ethusdt",
                     Ticker::SOLUSDT => "solusdt",
@@ -551,19 +562,21 @@ pub fn connect_kline_stream(vec: Vec<(Ticker, Timeframe)>) -> Subscription<Event
                                 OpCode::Text => {                    
                                     let json_bytes: Bytes = Bytes::from(msg.payload.to_vec());
                     
-                                    if let Ok(StreamData::Kline(de_kline)) = feed_de(&json_bytes, symbol_str) {
+                                    if let Ok(StreamData::Kline(ticker, de_kline)) = feed_de(&json_bytes) {
+                                        let buy_volume = str_f32_parse(&de_kline.taker_buy_base_asset_volume);
+                                        let sell_volume = str_f32_parse(&de_kline.volume) - buy_volume;
+
                                         let kline = Kline {
                                             time: de_kline.time,
                                             open: str_f32_parse(&de_kline.open),
                                             high: str_f32_parse(&de_kline.high),
                                             low: str_f32_parse(&de_kline.low),
                                             close: str_f32_parse(&de_kline.close),
-                                            volume: str_f32_parse(&de_kline.volume),
-                                            taker_buy_base_asset_volume: str_f32_parse(&de_kline.taker_buy_base_asset_volume),
+                                            volume: (buy_volume, sell_volume),
                                         };
 
-                                        if let Some(timeframe) = vec.iter().find(|(_, tf)| tf.to_string() == de_kline.interval) {
-                                            let _ = output.send(Event::KlineReceived(kline, timeframe.1)).await;
+                                        if let Some(timeframe) = streams.iter().find(|(_, tf)| tf.to_string() == de_kline.interval) {
+                                            let _ = output.send(Event::KlineReceived(ticker, kline, timeframe.1)).await;
                                         }
                                     } else {
                                         eprintln!("\nUnknown data: {:?}", &json_bytes);
@@ -618,14 +631,15 @@ struct FetchedKlines (
 );
 impl From<FetchedKlines> for Kline {
     fn from(fetched: FetchedKlines) -> Self {
+        let sell_volume = fetched.5 - fetched.9;
+
         Self {
             time: fetched.0,
             open: fetched.1,
             high: fetched.2,
             low: fetched.3,
             close: fetched.4,
-            volume: fetched.5,
-            taker_buy_base_asset_volume: fetched.9,
+            volume: (fetched.9, sell_volume),
         }
     }
 }

+ 70 - 49
src/data_providers/bybit/market_data.rs

@@ -23,7 +23,7 @@ use tokio::net::TcpStream;
 use tokio_rustls::rustls::{ClientConfig, OwnedTrustAnchor};
 use tokio_rustls::TlsConnector;
 
-use crate::data_providers::{LocalDepthCache, Trade, Depth, Order, FeedLatency};
+use crate::data_providers::{Depth, FeedLatency, Kline, LocalDepthCache, Order, Trade};
 use crate::{Ticker, Timeframe};
 
 #[allow(clippy::large_enum_variant)]
@@ -38,8 +38,8 @@ enum State {
 pub enum Event {
     Connected(Connection),
     Disconnected,
-    DepthReceived(FeedLatency, i64, Depth, Vec<Trade>),
-    KlineReceived(Kline, Timeframe),
+    DepthReceived(Ticker, FeedLatency, i64, Depth, Vec<Trade>),
+    KlineReceived(Ticker, Kline, Timeframe),
 }
 
 #[derive(Debug, Clone)]
@@ -97,24 +97,50 @@ pub struct SonicKline {
 enum StreamData {
 	Trade(Vec<SonicTrade>),
 	Depth(SonicDepth, String, i64),
-    Kline(Vec<SonicKline>),
+    Kline(Ticker, Vec<SonicKline>),
 }
 
 #[derive(Debug)]
 enum StreamName {
-    Depth,
-    Trade,
-    Kline,
+    Depth(Ticker),
+    Trade(Ticker),
+    Kline(Ticker),
     Unknown,
 }
 impl StreamName {
-    fn from_symbol_and_type(symbol: &str, stream_type: &str) -> Self {
-        match stream_type {
-            _ if stream_type == format!("orderbook.200.{symbol}") => StreamName::Depth,
-            _ if stream_type == format!("publicTrade.{symbol}") => StreamName::Trade,
-            _ if stream_type.starts_with(&format!("kline")) => StreamName::Kline,
-            _ => StreamName::Unknown,
-        }
+    fn from_topic(topic: &str) -> Self {
+        topic.split('.').collect::<Vec<&str>>().as_slice().split_first().map(|(first, rest)| {
+            match *first {
+                "publicTrade" => {
+                    match rest {
+                        [ticker] if *ticker == "BTCUSDT" => StreamName::Trade(Ticker::BTCUSDT),
+                        [ticker] if *ticker == "ETHUSDT" => StreamName::Trade(Ticker::ETHUSDT),
+                        [ticker] if *ticker == "SOLUSDT" => StreamName::Trade(Ticker::SOLUSDT),
+                        [ticker] if *ticker == "LTCUSDT" => StreamName::Trade(Ticker::LTCUSDT),
+                        _ => StreamName::Unknown,
+                    }
+                },
+                "orderbook" => {
+                    match rest {
+                        [_, ticker] if *ticker == "BTCUSDT" => StreamName::Depth(Ticker::BTCUSDT),
+                        [_, ticker] if *ticker == "ETHUSDT" => StreamName::Depth(Ticker::ETHUSDT),
+                        [_, ticker] if *ticker == "SOLUSDT" => StreamName::Depth(Ticker::SOLUSDT),
+                        [_, ticker] if *ticker == "LTCUSDT" => StreamName::Depth(Ticker::LTCUSDT),
+                        _ => StreamName::Unknown,
+                    }
+                },
+                "kline" => {
+                    match rest {
+                        [_, ticker] if *ticker == "BTCUSDT" => StreamName::Kline(Ticker::BTCUSDT),
+                        [_, ticker] if *ticker == "ETHUSDT" => StreamName::Kline(Ticker::ETHUSDT),
+                        [_, ticker] if *ticker == "SOLUSDT" => StreamName::Kline(Ticker::SOLUSDT),
+                        [_, ticker] if *ticker == "LTCUSDT" => StreamName::Kline(Ticker::LTCUSDT),
+                        _ => StreamName::Unknown,
+                    }
+                },
+                _ => StreamName::Unknown,
+            }
+        }).unwrap_or(StreamName::Unknown)
     }
 }
 
@@ -125,7 +151,7 @@ enum StreamWrapper {
     Kline,
 }
 
-fn feed_de(bytes: &Bytes, symbol: &str) -> Result<StreamData> {
+fn feed_de(bytes: &Bytes) -> Result<StreamData> {
     let mut stream_type: Option<StreamWrapper> = None;
 
     let mut depth_wrap: Option<SonicDepth> = None;
@@ -134,20 +160,28 @@ fn feed_de(bytes: &Bytes, symbol: &str) -> Result<StreamData> {
 
     let iter: sonic_rs::ObjectJsonIter = unsafe { to_object_iter_unchecked(bytes) };
 
+    let mut topic_ticker = Ticker::BTCUSDT;
+
     for elem in iter {
         let (k, v) = elem.context("Error parsing stream")?;
 
         if k == "topic" {
             if let Some(val) = v.as_str() {
-                match StreamName::from_symbol_and_type(symbol, val) {
-                    StreamName::Depth => {
+                match StreamName::from_topic(val) {
+                    StreamName::Depth(ticker) => {
                         stream_type = Some(StreamWrapper::Depth);
+
+                        topic_ticker = ticker;
                     },
-                    StreamName::Trade => {
+                    StreamName::Trade(ticker) => {
                         stream_type = Some(StreamWrapper::Trade);
+
+                        topic_ticker = ticker;
                     },
-                    StreamName::Kline => {
+                    StreamName::Kline(ticker) => {
                         stream_type = Some(StreamWrapper::Kline);
+
+                        topic_ticker = ticker;
                     },
                     _ => {
                         eprintln!("Unknown stream name");
@@ -179,7 +213,7 @@ fn feed_de(bytes: &Bytes, symbol: &str) -> Result<StreamData> {
                     let kline_wrap: Vec<SonicKline> = sonic_rs::from_str(&v.as_raw_faststr())
                         .context("Error parsing kline")?;
 
-                    return Ok(StreamData::Kline(kline_wrap));
+                    return Ok(StreamData::Kline(topic_ticker, kline_wrap));
                 },
                 _ => {
                     eprintln!("Unknown stream type");
@@ -272,27 +306,17 @@ fn string_to_timeframe(interval: &str) -> Option<Timeframe> {
     Timeframe::ALL.iter().find(|&tf| tf.to_string() == format!("{}m", interval)).copied()
 }
 
-#[derive(Deserialize, Debug, Clone, Copy)]
-pub struct Kline {
-    pub time: u64,
-    pub open: f32,
-    pub high: f32,
-    pub low: f32,
-    pub close: f32,
-    pub volume: f32,
-}
-
-pub fn connect_market_stream(selected_ticker: Ticker) -> Subscription<Event> {
-    struct Connect;
-
+pub fn connect_market_stream(stream: Ticker) -> Subscription<Event> {
     subscription::channel(
-        std::any::TypeId::of::<Connect>(),
+        stream,
         100,
         move |mut output| async move {
             let mut state: State = State::Disconnected;  
 
             let mut trades_buffer: Vec<Trade> = Vec::new();    
 
+            let selected_ticker = stream;
+
             let symbol_str = match selected_ticker {
                 Ticker::BTCUSDT => "BTCUSDT",
                 Ticker::ETHUSDT => "ETHUSDT",
@@ -335,7 +359,7 @@ pub fn connect_market_stream(selected_ticker: Ticker) -> Subscription<Event> {
 
                             let _ = output.send(Event::Disconnected).await;
                         }
-                    }
+                    },
                     State::Connected(websocket) => {
                         let feed_latency: FeedLatency;
 
@@ -344,7 +368,7 @@ pub fn connect_market_stream(selected_ticker: Ticker) -> Subscription<Event> {
                                 OpCode::Text => {       
                                     let json_bytes: Bytes = Bytes::from(msg.payload.to_vec());
 
-                                    if let Ok(data) = feed_de(&json_bytes, symbol_str) {
+                                    if let Ok(data) = feed_de(&json_bytes) {
                                         match data {
                                             StreamData::Trade(de_trade_vec) => {
                                                 for de_trade in de_trade_vec.iter() {
@@ -399,6 +423,7 @@ pub fn connect_market_stream(selected_ticker: Ticker) -> Subscription<Event> {
 
                                                     let _ = output.send(
                                                         Event::DepthReceived(
+                                                            selected_ticker,
                                                             feed_latency,
                                                             time, 
                                                             current_depth,
@@ -430,19 +455,15 @@ pub fn connect_market_stream(selected_ticker: Ticker) -> Subscription<Event> {
     )
 }
  
-pub fn connect_kline_stream(vec: Vec<(Ticker, Timeframe)>) -> Subscription<Event> {
-    struct Connect;
-
+pub fn connect_kline_stream(streams: Vec<(Ticker, Timeframe)>) -> Subscription<Event> {
     subscription::channel(
-        std::any::TypeId::of::<Connect>(),
+        streams.clone(),
         100,
         move |mut output| async move {
             let mut state = State::Disconnected;    
 
-            let mut symbol_str: &str = "";
-
-            let stream_str = vec.iter().map(|(ticker, timeframe)| {
-                symbol_str = match ticker {
+            let stream_str = streams.iter().map(|(ticker, timeframe)| {
+                let symbol_str = match ticker {
                     Ticker::BTCUSDT => "BTCUSDT",
                     Ticker::ETHUSDT => "ETHUSDT",
                     Ticker::SOLUSDT => "SOLUSDT",
@@ -494,7 +515,7 @@ pub fn connect_kline_stream(vec: Vec<(Ticker, Timeframe)>) -> Subscription<Event
                                 OpCode::Text => {                    
                                     let json_bytes: Bytes = Bytes::from(msg.payload.to_vec());
                     
-                                    if let Ok(StreamData::Kline(de_kline_vec)) = feed_de(&json_bytes, symbol_str) {
+                                    if let Ok(StreamData::Kline(ticker, de_kline_vec)) = feed_de(&json_bytes) {
                                         for de_kline in de_kline_vec.iter() {
                                             let kline = Kline {
                                                 time: de_kline.time,
@@ -502,13 +523,13 @@ pub fn connect_kline_stream(vec: Vec<(Ticker, Timeframe)>) -> Subscription<Event
                                                 high: str_f32_parse(&de_kline.high),
                                                 low: str_f32_parse(&de_kline.low),
                                                 close: str_f32_parse(&de_kline.close),
-                                                volume: str_f32_parse(&de_kline.volume),
+                                                volume: (-1.0, str_f32_parse(&de_kline.volume)),
                                             };
 
                                             if let Some(timeframe) = string_to_timeframe(&de_kline.interval) {
-                                                let _ = output.send(Event::KlineReceived(kline, timeframe)).await;
+                                                let _ = output.send(Event::KlineReceived(ticker, kline, timeframe)).await;
                                             } else {
-                                                eprintln!("Failed to find timeframe: {}, {:?}", &de_kline.interval, vec);
+                                                eprintln!("Failed to find timeframe: {}, {:?}", &de_kline.interval, streams);
                                             }
                                         }
                                          
@@ -596,7 +617,7 @@ pub async fn fetch_klines(ticker: Ticker, timeframe: Timeframe) -> Result<Vec<Kl
             high: high?,
             low: low?,
             close: close?,
-            volume: volume?,
+            volume: (-1.0, volume?),
         })
     }).collect();
 

+ 9 - 0
src/fonts/LICENSE.txt

@@ -10,3 +10,12 @@ Font license info
    Homepage:  http://fortawesome.github.com/Font-Awesome/
 
 
+## Entypo
+
+   Copyright (C) 2012 by Daniel Bruce
+
+   Author:    Daniel Bruce
+   License:   SIL (http://scripts.sil.org/OFL)
+   Homepage:  http://www.entypo.com
+
+

+ 62 - 0
src/fonts/config.json

@@ -47,6 +47,68 @@
       "css": "cog",
       "code": 59398,
       "src": "entypo"
+    },
+    {
+      "uid": "815503841e980c848f55e0271deacead",
+      "css": "link",
+      "code": 59399,
+      "src": "entypo"
+    },
+    {
+      "uid": "bac5560321c6d563a572b825ee747568",
+      "css": "bybit",
+      "code": 59400,
+      "src": "custom_icons",
+      "selected": true,
+      "svg": {
+        "path": "M695 364.4S643.7 348.1 640.6 350L391.2 22.5S343.1 44.4 340 46.9L550.6 308.1C546.9 312.5 499.4 312.5 495 312.5L272.5 91.9 234.3 135.6 418.7 312.5C415 316.9 373.1 333.8 369.4 338.8L191.8 203.8C191.8 206.9 170 251.9 168.1 255L311.8 357.5C311.8 361.9 275.6 391.3 272.5 396.3L165 339.4S165 391.9 165 394.4L227.5 433.8 223.1 443.1 165.6 431.9 206.8 463.1C124.3 578.1 143.7 762.5 249.3 889.4L210 779.4H218.1L291.2 945S340.6 961.3 344.3 960L226.8 703.1S241.2 666.9 243.7 665.6L425 995.7C428.7 995.7 480.6 1001.9 487.5 999.4L270 635S326.2 663.1 332.5 660.6L561.9 995.7C565 995.7 613.1 979.4 615.6 976.9L396.8 681.3C400.6 681.3 456.2 692.5 459.4 689.4L685 939.4S726.9 905 729.4 901.9L536.9 700.7C540.6 696.3 588.1 687.5 591.9 683.2L779.4 838.8 812.5 793.2 653.7 668.2C653.7 663.2 695.6 638.8 698.7 633.8L830.6 714.4C830.6 711.3 837.5 661.9 838.7 659.4L749.4 599.4C749.4 595.6 778.7 565.6 780.6 562.5L838.7 574.4 796.2 543.1C876.9 436.9 853.7 230.6 745 105.6L800 250S768.7 254.4 765.6 256.3L666.2 45C663.1 45 610.6 22.5 606.9 24.4L754.4 320.6C751.2 320.6 715.6 316.3 712.5 320.6L525.6 0C521.9 0 472.5 6.3 469.4 8.8Z",
+        "width": 1000
+      },
+      "search": [
+        "bybit"
+      ]
+    },
+    {
+      "uid": "9292a7baaacec6cda7efa476f962ce17",
+      "css": "binance_futures",
+      "code": 59401,
+      "src": "custom_icons",
+      "selected": true,
+      "svg": {
+        "path": "M306.3 418.8L500 225 693.8 418.8 806.3 306.3 500 0 193.7 306.3 306.2 418.8ZM0 500L112.5 387.5 225 500 112.5 612.5 0 500ZM306.3 581.3L500 775 693.8 581.3 806.3 693.8 500 1000 193.7 693.8 306.2 581.2ZM775 500L887.5 387.5 1000 500 887.5 612.5 775 500ZM612.5 500L500 387.5 387.5 500 500 612.5 612.5 500Z",
+        "width": 1000
+      },
+      "search": [
+        "binance_futures"
+      ]
+    },
+    {
+      "uid": "e392b748bd095b278c27391b3e05286c",
+      "css": "disconnect-svgrepo-com",
+      "code": 59399,
+      "src": "custom_icons",
+      "selected": false,
+      "svg": {
+        "path": "M1040.8 239.3C935 133.5 763.9 133.5 658.3 239.3L537.1 360.4 600.9 424.1 722 303C789.3 235.8 902.8 228.6 977 303 1051.4 377.4 1044.3 490.8 977 558L855.9 679.1 919.8 743 1040.9 621.9C1146.4 516.1 1146.4 345 1040.8 239.2ZM558.1 977C490.9 1044.3 377.4 1051.4 303.1 977 228.8 902.6 235.9 789.3 303.1 722L424.3 600.9 360.4 537 239.2 658.1C133.5 763.9 133.5 935 239.2 1040.6S516.1 1146.4 621.8 1040.6L742.9 919.5 679.1 855.8 558.1 977ZM325.4 261.8A10 10 0 0 0 311.3 261.8L261.8 311.3A10 10 0 0 0 261.8 325.4L954.8 1018.4C958.6 1022.3 965 1022.3 968.9 1018.4L1018.4 968.9C1022.3 965 1022.3 958.6 1018.4 954.8L325.4 261.8Z",
+        "width": 1000
+      },
+      "search": [
+        "disconnect-svgrepo-com"
+      ]
+    },
+    {
+      "uid": "336e273abd6f7e289f16dbc34b8d97d5",
+      "css": "circle-svgrepo-com",
+      "code": 59393,
+      "src": "custom_icons",
+      "selected": false,
+      "svg": {
+        "path": "",
+        "width": 1000
+      },
+      "search": [
+        "circle-svgrepo-com"
+      ]
     }
   ]
 }

BIN
src/fonts/icons.ttf


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 222 - 668
src/main.rs


+ 1 - 0
src/screen.rs

@@ -0,0 +1 @@
+pub mod dashboard;

+ 286 - 0
src/screen/dashboard.rs

@@ -0,0 +1,286 @@
+pub mod pane;
+
+pub use pane::{Uuid, PaneState, PaneContent, PaneSettings};
+
+use crate::{
+    charts::{candlestick::CandlestickChart, footprint::FootprintChart, Message}, 
+    data_providers::{
+        Depth, Exchange, Kline, TickMultiplier, Ticker, Timeframe, Trade
+    }, 
+    StreamType
+};
+
+use std::{collections::{HashMap, HashSet}, rc::Rc};
+use iced::widget::pane_grid::{self, Configuration};
+
+pub struct Dashboard {
+    pub panes: pane_grid::State<PaneState>,
+    pub show_layout_modal: bool,
+    pub focus: Option<pane_grid::Pane>,
+    pub first_pane: pane_grid::Pane,
+    pub pane_lock: bool,
+    pub pane_state_cache: HashMap<Uuid, (Option<Ticker>, Option<Timeframe>, Option<f32>)>,
+    pub last_axis_split: Option<pane_grid::Axis>,
+}
+impl Dashboard {
+    pub fn empty(pane_config: Configuration<PaneState>) -> Self {
+        let panes: pane_grid::State<PaneState> = pane_grid::State::with_configuration(pane_config);
+        let first_pane: pane_grid::Pane = *panes.panes.iter().next().unwrap().0;
+        
+        Self { 
+            show_layout_modal: false,
+            panes,
+            focus: None,
+            first_pane,
+            pane_lock: false,
+            pane_state_cache: HashMap::new(),
+            last_axis_split: None,
+        }
+    }
+
+    pub fn update_chart_state(&mut self, pane_id: Uuid, message: Message) -> Result<(), &str> {
+        for (_, pane_state) in self.panes.iter_mut() {
+            if pane_state.id == pane_id {
+                match pane_state.content {
+                    PaneContent::Heatmap(ref mut chart) => {
+                        chart.update(&message);
+
+                        return Ok(());
+                    },
+                    PaneContent::Footprint(ref mut chart) => {
+                        chart.update(&message);
+
+                        return Ok(());
+                    },
+                    PaneContent::Candlestick(ref mut chart) => {
+                        chart.update(&message);
+
+                        return Ok(());
+                    },
+                    _ => {
+                        return Err("No chart found");
+                    }
+                }
+            }
+        }
+        Err("No pane found")
+    }
+
+    pub fn get_pane_stream_mut(&mut self, pane_id: Uuid) -> Result<&mut Vec<StreamType>, &str> {
+        for (_, pane_state) in self.panes.iter_mut() {
+            if pane_state.id == pane_id {
+                return Ok(&mut pane_state.stream);
+            }
+        }
+        Err("No pane found")
+    }
+
+    pub fn get_pane_settings_mut(&mut self, pane_id: Uuid) -> Result<&mut PaneSettings, &str> {
+        for (_, pane_state) in self.panes.iter_mut() {
+            if pane_state.id == pane_id {
+                return Ok(&mut pane_state.settings);
+            }
+        }
+        Err("No pane found")
+    }
+
+    pub fn set_pane_content(&mut self, pane_id: Uuid, content: PaneContent) -> Result<(), &str> {
+        for (_, pane_state) in self.panes.iter_mut() {
+            if pane_state.id == pane_id {
+                pane_state.content = content;
+
+                return Ok(());
+            }
+        }
+        Err("No pane found")
+    }
+
+    pub fn pane_change_ticksize(&mut self, pane_id: Uuid, new_tick_multiply: TickMultiplier) -> Result<(), &str> {
+        for (_, pane_state) in self.panes.iter_mut() {
+            if pane_state.id == pane_id {
+                pane_state.settings.tick_multiply = Some(new_tick_multiply);
+
+                if let Some(min_tick_size) = pane_state.settings.min_tick_size {
+                    match pane_state.content {
+                        PaneContent::Footprint(ref mut chart) => {
+                            chart.change_tick_size(
+                                new_tick_multiply.multiply_with_min_tick_size(min_tick_size)
+                            );
+                            
+                            return Ok(());
+                        },
+                        _ => {
+                            return Err("No footprint chart found");
+                        }
+                    }
+                } else {
+                    return Err("No min tick size found");
+                }
+            }
+        }
+        Err("No pane found")
+    }
+    
+    pub fn pane_change_timeframe(&mut self, pane_id: Uuid, new_timeframe: Timeframe) -> Result<&StreamType, &str> {
+        for (_, pane_state) in self.panes.iter_mut() {
+            if pane_state.id == pane_id {
+                pane_state.settings.selected_timeframe = Some(new_timeframe);
+
+                for stream_type in pane_state.stream.iter_mut() {
+                    match stream_type {
+                        StreamType::Kline { timeframe, .. } => {
+                            *timeframe = new_timeframe;
+
+                            match pane_state.content {
+                                PaneContent::Candlestick(_) => {
+                                    return Ok(stream_type);
+                                },
+                                PaneContent::Footprint(_) => {
+                                    return Ok(stream_type);
+                                },
+                                _ => {}
+                            }
+                        },
+                        _ => {}
+                    }
+                }
+            }
+        }
+        Err("No pane found")
+    }
+
+    pub fn pane_set_size_filter(&mut self, pane_id: Uuid, new_size_filter: f32) -> Result<(), &str> {
+        for (_, pane_state) in self.panes.iter_mut() {
+            if pane_state.id == pane_id {
+                match pane_state.content {
+                    PaneContent::Heatmap(ref mut chart) => {
+                        chart.set_size_filter(new_size_filter);
+                        
+                        return Ok(());
+                    },
+                    PaneContent::TimeAndSales(ref mut chart) => {
+                        chart.set_size_filter(new_size_filter);
+                        
+                        return Ok(());
+                    },
+                    _ => {
+                        return Err("No footprint chart found");
+                    }
+                }
+            }
+        }
+        Err("No pane found")
+    }
+
+    pub fn insert_klines_vec(&mut self, stream_type: &StreamType, klines: &Vec<Kline>, pane_id: Uuid) {
+        for (_, pane_state) in self.panes.iter_mut() {
+            if pane_state.id == pane_id {
+                match stream_type {
+                    StreamType::Kline { timeframe, .. } => {
+                        let timeframe_u16 = timeframe.to_minutes();
+
+                        match &mut pane_state.content {
+                            PaneContent::Candlestick(chart) => {
+                                *chart = CandlestickChart::new(klines.to_vec(), timeframe_u16);
+                            },
+                            PaneContent::Footprint(chart) => {
+                                let raw_trades = chart.get_raw_trades();
+
+                                let tick_size = chart.get_tick_size();
+
+                                *chart = FootprintChart::new(timeframe_u16, tick_size, klines.to_vec(), raw_trades);
+                            },
+                            _ => {}
+                        }
+                    },
+                    _ => {}
+                }
+            }
+        }
+    }
+
+    pub fn update_latest_klines(&mut self, stream_type: &StreamType, kline: &Kline) -> Result<(), &str> {
+        let mut found_match = false;
+    
+        for (_, pane_state) in self.panes.iter_mut() {
+            if pane_state.matches_stream(&stream_type) {
+                match &mut pane_state.content {
+                    PaneContent::Candlestick(chart) => chart.update_latest_kline(kline),
+                    PaneContent::Footprint(chart) => chart.update_latest_kline(kline),
+                    _ => {}
+                }
+                found_match = true;
+            }
+        }
+    
+        if found_match {
+            Ok(())
+        } else {
+            Err("No matching pane found for the stream")
+        }
+    }
+
+    pub fn update_depth_and_trades(&mut self, stream_type: StreamType, depth_update_t: i64, depth: Depth, trades_buffer: Vec<Trade>) -> Result<(), &str> {
+        let mut found_match = false;
+        
+        let depth = Rc::new(depth);
+
+        let trades_buffer = trades_buffer.into_boxed_slice();
+
+        for (_, pane_state) in self.panes.iter_mut() {
+            if pane_state.matches_stream(&stream_type) {
+                match &mut pane_state.content {
+                    PaneContent::Heatmap(chart) => {
+                        chart.insert_datapoint(&trades_buffer, depth_update_t, Rc::clone(&depth));
+                    },
+                    PaneContent::Footprint(chart) => {
+                        chart.insert_datapoint(&trades_buffer, depth_update_t);
+                    },
+                    PaneContent::TimeAndSales(chart) => {
+                        chart.update(&trades_buffer);
+                    },
+                    _ => {}
+                }
+
+                found_match = true;
+            }
+        }
+
+        if found_match {
+            Ok(())
+        } else {
+            Err("No matching pane found for the stream")
+        }
+    }
+
+    pub fn get_all_diff_streams(&self) -> HashMap<Exchange, HashMap<Ticker, HashSet<StreamType>>> {
+        let mut pane_streams = HashMap::new();
+
+        for (_, pane_state) in self.panes.iter() {
+            for stream_type in &pane_state.stream {
+                match stream_type {
+                    StreamType::Kline { exchange, ticker, timeframe } => {
+                        let exchange = exchange.clone();
+                        let ticker = ticker.clone();
+                        let timeframe = timeframe.clone();
+
+                        let exchange_map = pane_streams.entry(exchange.clone()).or_insert(HashMap::new());
+                        let ticker_map = exchange_map.entry(ticker).or_insert(HashSet::new());
+                        ticker_map.insert(StreamType::Kline { exchange, ticker, timeframe });
+                    },
+                    StreamType::DepthAndTrades { exchange, ticker } => {
+                        let exchange = exchange.clone();
+                        let ticker = ticker.clone();
+
+                        let exchange_map = pane_streams.entry(exchange).or_insert(HashMap::new());
+                        let ticker_map = exchange_map.entry(ticker).or_insert(HashSet::new());
+                        ticker_map.insert(StreamType::DepthAndTrades { exchange, ticker });
+                    },
+                    _ => {}
+                }
+            }
+        }
+
+        pane_streams
+    }
+}

+ 65 - 0
src/screen/dashboard/pane.rs

@@ -0,0 +1,65 @@
+pub use uuid::Uuid;
+
+use crate::{
+    charts::{
+        candlestick::CandlestickChart, footprint::FootprintChart, heatmap::HeatmapChart, timeandsales::TimeAndSales
+    }, 
+    data_providers::{
+        Exchange, TickMultiplier, Ticker, Timeframe
+    }, 
+    StreamType
+};
+
+pub struct PaneState {
+    pub id: Uuid,
+    pub show_modal: bool,
+    pub stream: Vec<StreamType>,
+    pub content: PaneContent,
+    pub settings: PaneSettings,
+}
+
+impl PaneState {
+    pub fn new(id: Uuid, stream: Vec<StreamType>, settings: PaneSettings) -> Self {
+        Self {
+            id,
+            show_modal: false,
+            stream,
+            content: PaneContent::Starter,
+            settings,
+        }
+    }
+
+    pub fn matches_stream(&self, stream_type: &StreamType) -> bool {
+        self.stream.iter().any(|stream| stream == stream_type)
+    }
+}
+
+pub enum PaneContent {
+    Heatmap(HeatmapChart),
+    Footprint(FootprintChart),
+    Candlestick(CandlestickChart),
+    TimeAndSales(TimeAndSales),
+    Starter,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct PaneSettings {
+    pub min_tick_size: Option<f32>,
+    pub trade_size_filter: Option<f32>,
+    pub tick_multiply: Option<TickMultiplier>,
+    pub selected_ticker: Option<Ticker>,
+    pub selected_exchange: Option<Exchange>,
+    pub selected_timeframe: Option<Timeframe>,
+}
+impl Default for PaneSettings {
+    fn default() -> Self {
+        Self {
+            min_tick_size: None,
+            trade_size_filter: Some(0.0),
+            tick_multiply: Some(TickMultiplier(10)),
+            selected_ticker: None,
+            selected_exchange: None,
+            selected_timeframe: Some(Timeframe::M1),
+        }
+    }
+}

+ 239 - 0
src/style.rs

@@ -0,0 +1,239 @@
+use iced::widget::button::Status;
+use iced::widget::container::Style;
+use iced::{theme, Border, Color, Font, Theme, overlay};
+use iced::widget::pick_list;
+
+pub const ICON_BYTES: &[u8] = include_bytes!("fonts/icons.ttf");
+pub const ICON_FONT: Font = Font::with_name("icons");
+
+pub enum Icon {
+    Locked,
+    Unlocked,
+    ResizeFull,
+    ResizeSmall,
+    Close,
+    Layout,
+    Cog,
+    Link,
+    BinanceLogo,
+    BybitLogo,
+}
+
+impl From<Icon> for char {
+    fn from(icon: Icon) -> Self {
+        match icon {
+            Icon::Unlocked => '\u{E800}',
+            Icon::Locked => '\u{E801}',
+            Icon::ResizeFull => '\u{E802}',
+            Icon::ResizeSmall => '\u{E803}',
+            Icon::Close => '\u{E804}',
+            Icon::Layout => '\u{E805}',
+            Icon::Cog => '\u{E806}',
+            Icon::Link => '\u{E807}',
+            Icon::BybitLogo => '\u{E808}',
+            Icon::BinanceLogo => '\u{E809}',
+        }
+    }
+}
+
+pub fn tooltip(theme: &Theme) -> Style {
+    let palette = theme.extended_palette();
+
+    Style {
+        background: Some(palette.background.weak.color.into()),
+        border: Border {
+            width: 1.0,
+            color: palette.primary.weak.color,
+            radius: 4.0.into(),
+        },
+        ..Default::default()
+    }
+}
+
+pub fn title_bar_active(theme: &Theme) -> Style {
+    let palette = theme.extended_palette();
+
+    Style {
+        text_color: Some(palette.background.base.text),
+        background: Some(Color::BLACK.into()),
+        ..Default::default()
+    }
+}
+pub fn title_bar_focused(theme: &Theme) -> Style {
+    let palette = theme.extended_palette();
+
+    Style {
+        text_color: Some(palette.background.weak.text),
+        background: Some(Color::TRANSPARENT.into()),
+        ..Default::default()
+    }
+}
+pub fn pane_active(theme: &Theme) -> Style {
+    let palette = theme.extended_palette();
+
+    Style {
+        text_color: Some(palette.background.base.text),
+        background: Some(Color::BLACK.into()),
+        ..Default::default()
+    }
+}
+pub fn pane_focused(theme: &Theme) -> Style {
+    let palette = theme.extended_palette();
+
+    Style {
+        text_color: Some(palette.background.weak.text),
+        background: Some(Color::BLACK.into()),
+        border: Border {
+            width: 1.0,
+            color: palette.background.weak.color,
+            radius: 4.0.into(),
+        },
+        ..Default::default()
+    }
+}
+
+pub fn chart_modal(theme: &Theme) -> Style {
+    let palette = theme.extended_palette();
+
+    Style {
+        text_color: Some(palette.background.base.text),
+        background: Some(palette.background.base.color.into()),
+        border: Border {
+            width: 1.0,
+            color: palette.background.weak.color,
+            radius: 4.0.into(),
+        },
+        ..Default::default()
+    }
+}
+
+pub fn button_primary(theme: &Theme, status: Status) -> iced::widget::button::Style {
+    let palette = theme.extended_palette();
+
+    match status {
+        Status::Active => iced::widget::button::Style {
+            background: Some(Color::BLACK.into()),
+            text_color: palette.background.base.text,
+            border: Border {
+                radius: 3.0.into(),
+                ..Default::default()
+            },
+            ..Default::default()
+        },
+        Status::Pressed => iced::widget::button::Style {
+            background: Some(Color::BLACK.into()),
+            text_color: palette.background.base.text,
+            border: Border {
+                color: palette.primary.weak.color,
+                width: 2.0,
+                radius: 6.0.into(),
+                ..Default::default()
+            },
+            ..Default::default()
+        },
+        Status::Hovered => iced::widget::button::Style {
+            background: Some(Color::BLACK.into()),
+            text_color: palette.background.weak.text,
+            border: Border {
+                color: palette.primary.strong.color,
+                width: 1.0,
+                radius: 3.0.into(),
+                ..Default::default()
+            },
+            ..Default::default()
+        },
+        Status::Disabled => iced::widget::button::Style {
+            background: Some(Color::BLACK.into()),
+            text_color: palette.background.base.text,
+            border: Border {
+                radius: 3.0.into(),
+                ..Default::default()
+            },
+            ..Default::default()
+        }
+    }
+}
+
+pub fn picklist_primary(theme: &Theme, status: pick_list::Status) -> pick_list::Style {
+    let palette = theme.extended_palette();
+    
+    match status {
+        pick_list::Status::Active => pick_list::Style {
+            text_color: palette.background.base.text,
+            placeholder_color: palette.background.base.text,
+            handle_color: palette.background.base.text,
+            background: palette.background.base.color.into(),
+            border: Border {
+                radius: 3.0.into(),
+                width: 1.0,
+                color: palette.background.weak.color,
+                ..Default::default()
+            },
+        },
+        pick_list::Status::Opened => pick_list::Style {
+            text_color: palette.background.base.text,
+            placeholder_color: palette.background.base.text,
+            handle_color: palette.background.base.text,
+            background: palette.background.base.color.into(),
+            border: Border {
+                radius: 3.0.into(),
+                width: 1.0,
+                color: palette.primary.base.color,
+                ..Default::default()
+            },
+        },
+        pick_list::Status::Hovered => pick_list::Style {
+            text_color: palette.background.weak.text,
+            placeholder_color: palette.background.weak.text,
+            handle_color: palette.background.weak.text,
+            background: palette.background.base.color.into(),
+            border: Border {
+                radius: 3.0.into(),
+                width: 1.0,
+                color: palette.primary.strong.color,
+                ..Default::default()
+            },
+        },
+    }
+}
+
+pub fn picklist_menu_primary(theme: &Theme) -> overlay::menu::Style {
+    let palette = theme.extended_palette();
+
+    overlay::menu::Style {
+        text_color: palette.background.base.text,
+        background: palette.background.base.color.into(),
+        border: Border {
+            radius: 3.0.into(),
+            width: 1.0,
+            color: palette.background.base.color,
+            ..Default::default()
+        },
+        selected_text_color: palette.background.weak.text,
+        selected_background: palette.secondary.weak.color.into(),
+    }
+}
+
+pub fn sell_side_red(color_alpha: f32) -> Style {
+    Style {
+        text_color: Color::from_rgba(192.0 / 255.0, 80.0 / 255.0, 77.0 / 255.0, 1.0).into(),
+        border: Border {
+            width: 1.0,
+            color: Color::from_rgba(192.0 / 255.0, 80.0 / 255.0, 77.0 / 255.0, color_alpha),
+            ..Border::default()
+        },
+        ..Default::default()
+    }
+}
+
+pub fn buy_side_green(color_alpha: f32) -> Style {
+    Style {
+        text_color: Color::from_rgba(81.0 / 255.0, 205.0 / 255.0, 160.0 / 255.0, 1.0).into(),
+        border: Border {
+            width: 1.0,
+            color: Color::from_rgba(81.0 / 255.0, 205.0 / 255.0, 160.0 / 255.0, color_alpha),
+            ..Border::default()
+        },
+        ..Default::default()
+    }
+}

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott