Просмотр исходного кода

Trading module test (#2)

* new time&sales panel

* better pane grid management

* more reasonable data stream modularity

* styling tweaks, add new icons

* add open limit order fetch ability

* add/refactor some types for user data stream from Binance

* initial user data stream for Binance

* refactors for modularity

* add ability to cancel limit orders

* limit orders in table now constructed more properly; being checked with user data stream

* add positions tab, ability to market order in&out

* add "positions" management state

* modified:   Cargo.lock
	modified:   Cargo.toml

* add open positions fetch

* order form adjustments

* org. modularity for a better file structure

* fixes for modularity

* add some lidl icon fonts

* some fixes on market orders

* better pane management

* add some fonts

* fix unfocused panes causing problems when managing them

* trying to find an optimal way to manage panes

* add user acc info fetches and ws streams

* add balance and positions fetches and ws streams

* modified:   Cargo.lock
	modified:   Cargo.toml

* Update README.md

* order form styling adjustments

* refactors for better readability

* fix edge case w/ panes losing focus; causing problem to create new panes. + some readability refactors

* Update README.md

* some refactors for a better pane grid logic

* fixes for Binance API error handling

* add user data stream protection; defaulted not to fetch user data at initialization
Berke 1 год назад
Родитель
Сommit
43039d6d53

+ 277 - 1
Cargo.lock

@@ -207,6 +207,12 @@ version = "0.6.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
 
+[[package]]
+name = "bit_field"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
+
 [[package]]
 name = "bitflags"
 version = "1.3.2"
@@ -610,6 +616,25 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
 [[package]]
 name = "crossbeam-utils"
 version = "0.8.19"
@@ -665,6 +690,15 @@ version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
 
+[[package]]
+name = "deranged"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
+dependencies = [
+ "powerfmt",
+]
+
 [[package]]
 name = "digest"
 version = "0.10.7"
@@ -673,6 +707,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
 dependencies = [
  "block-buffer",
  "crypto-common",
+ "subtle",
 ]
 
 [[package]]
@@ -824,6 +859,22 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "exr"
+version = "1.72.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4"
+dependencies = [
+ "bit_field",
+ "flume",
+ "half",
+ "lebe",
+ "miniz_oxide",
+ "rayon-core",
+ "smallvec",
+ "zune-inflate",
+]
+
 [[package]]
 name = "fast-srgb8"
 version = "1.0.0"
@@ -867,6 +918,15 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8"
 
+[[package]]
+name = "flume"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
+dependencies = [
+ "spin",
+]
+
 [[package]]
 name = "fnv"
 version = "1.0.7"
@@ -1130,6 +1190,16 @@ dependencies = [
  "weezl",
 ]
 
+[[package]]
+name = "gif"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2"
+dependencies = [
+ "color_quant",
+ "weezl",
+]
+
 [[package]]
 name = "gimli"
 version = "0.28.1"
@@ -1314,12 +1384,27 @@ version = "0.3.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
 
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
 [[package]]
 name = "hexf-parse"
 version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
 
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
 [[package]]
 name = "http"
 version = "1.1.0"
@@ -1450,6 +1535,7 @@ dependencies = [
  "iced_renderer",
  "iced_widget",
  "iced_winit",
+ "image",
  "thiserror",
 ]
 
@@ -1462,7 +1548,12 @@ dependencies = [
  "chrono",
  "futures",
  "futures-util",
+ "hex",
+ "hmac",
  "iced",
+ "iced_aw",
+ "iced_futures",
+ "iced_table",
  "native-tls",
  "plotters",
  "plotters-backend",
@@ -1471,6 +1562,7 @@ dependencies = [
  "reqwest",
  "serde",
  "serde_json",
+ "sha2",
  "tokio",
  "tokio-native-tls",
  "tokio-tungstenite",
@@ -1478,6 +1570,21 @@ dependencies = [
  "url",
 ]
 
+[[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",
+ "itertools",
+ "num-traits",
+ "once_cell",
+ "time",
+]
+
 [[package]]
 name = "iced_core"
 version = "0.12.3"
@@ -1522,6 +1629,8 @@ dependencies = [
  "half",
  "iced_core",
  "iced_futures",
+ "image",
+ "kamadak-exif",
  "log",
  "lyon_path",
  "once_cell",
@@ -1568,6 +1677,17 @@ dependencies = [
  "palette",
 ]
 
+[[package]]
+name = "iced_table"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4347f00d6cd7e5e3b26a9ac0bedd46f336b5eaa4be9b46256d1c8009562f6b51"
+dependencies = [
+ "iced_core",
+ "iced_style",
+ "iced_widget",
+]
+
 [[package]]
 name = "iced_tiny_skia"
 version = "0.12.1"
@@ -1667,9 +1787,13 @@ dependencies = [
  "bytemuck",
  "byteorder",
  "color_quant",
+ "exr",
+ "gif 0.13.1",
  "jpeg-decoder",
  "num-traits",
  "png",
+ "qoi",
+ "tiff",
 ]
 
 [[package]]
@@ -1748,6 +1872,9 @@ name = "jpeg-decoder"
 version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
+dependencies = [
+ "rayon",
+]
 
 [[package]]
 name = "js-sys"
@@ -1758,6 +1885,15 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "kamadak-exif"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef4fc70d0ab7e5b6bafa30216a6b48705ea964cdfc29c050f2412295eba58077"
+dependencies = [
+ "mutate_once",
+]
+
 [[package]]
 name = "khronos-egl"
 version = "6.0.0"
@@ -1791,6 +1927,12 @@ version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
+[[package]]
+name = "lebe"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
+
 [[package]]
 name = "libc"
 version = "0.2.153"
@@ -2009,6 +2151,12 @@ dependencies = [
  "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "mutate_once"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b"
+
 [[package]]
 name = "naga"
 version = "0.19.2"
@@ -2077,6 +2225,12 @@ dependencies = [
  "jni-sys",
 ]
 
+[[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.18"
@@ -2118,6 +2272,15 @@ dependencies = [
  "syn 2.0.57",
 ]
 
+[[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"
@@ -2495,7 +2658,7 @@ version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0cebbe1f70205299abc69e8b295035bb52a6a70ee35474ad10011f0a4efb8543"
 dependencies = [
- "gif",
+ "gif 0.12.0",
  "image",
  "plotters-backend",
 ]
@@ -2550,6 +2713,12 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
 [[package]]
 name = "ppv-lite86"
 version = "0.2.17"
@@ -2599,6 +2768,15 @@ version = "1.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58"
 
+[[package]]
+name = "qoi"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
+dependencies = [
+ "bytemuck",
+]
+
 [[package]]
 name = "quick-xml"
 version = "0.31.0"
@@ -2665,6 +2843,26 @@ version = "0.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "42a9830a0e1b9fb145ebb365b8bc4ccd75f290f98c0247deafbbe2c75cefb544"
 
+[[package]]
+name = "rayon"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
 [[package]]
 name = "read-fonts"
 version = "0.16.0"
@@ -3010,6 +3208,26 @@ dependencies = [
  "digest",
 ]
 
+[[package]]
+name = "sha2"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "simd-adler32"
 version = "0.3.7"
@@ -3137,6 +3355,9 @@ name = "spin"
 version = "0.9.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+dependencies = [
+ "lock_api",
+]
 
 [[package]]
 name = "spirv"
@@ -3281,6 +3502,38 @@ dependencies = [
  "syn 2.0.57",
 ]
 
+[[package]]
+name = "tiff"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e"
+dependencies = [
+ "flate2",
+ "jpeg-decoder",
+ "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"
@@ -3345,11 +3598,25 @@ dependencies = [
  "libc",
  "mio",
  "num_cpus",
+ "parking_lot 0.12.1",
  "pin-project-lite",
+ "signal-hook-registry",
  "socket2",
+ "tokio-macros",
  "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "tokio-macros"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.57",
+]
+
 [[package]]
 name = "tokio-native-tls"
 version = "0.3.1"
@@ -4454,3 +4721,12 @@ name = "zeroize"
 version = "1.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
+
+[[package]]
+name = "zune-inflate"
+version = "0.2.54"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
+dependencies = [
+ "simd-adler32",
+]

+ 8 - 2
Cargo.toml

@@ -7,12 +7,12 @@ edition = "2021"
 
 [dependencies]
 plotters-iced = "0.10"
-iced = { version = "0.12.1", features = ["canvas", "tokio", "lazy"] }
+iced = { version = "0.12.1", features = ["canvas", "tokio", "lazy", "image"] }
 plotters="0.3"
 chrono = "0.4.37"
 plotters-backend = "0.3.5"
 rand = "0.8.5"
-tokio = { version = "1.37.0", features = ["sync"] }
+tokio = { version = "1.37.0", features = ["full"] }
 tokio-tungstenite = "0.21.0"
 url = "2.5.0"
 tokio-native-tls = "0.3.1"
@@ -24,6 +24,12 @@ futures-util = "0.3.30"
 serde_json = "1.0.115"
 serde = { version = "1.0", features = ["derive"] }
 reqwest = { version = "0.12.2", features = ["json"] }
+hmac = "0.12.1"
+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"] }
 [dependencies.async-tungstenite]
 version = "0.25"
 features = ["tokio-rustls-webpki-roots"]

+ 3 - 1
README.md

@@ -1 +1,3 @@
-<img width="1496" alt="Screenshot 2024-04-08 at 1 25 51 PM" src="https://github.com/akenshaw/iced-trade/assets/63060680/25ac7500-c0df-4c69-bfa3-dd463e76a309">
+Trading implementation is highly experimental; advised not to use any trading functionality with a real account
+
+<img width="1379" alt="Screenshot 2024-05-05 at 3 49 36 PM" src="https://github.com/akenshaw/iced-trade/assets/63060680/e7b55751-b547-4548-ac95-5348c6c60385">

+ 2 - 0
src/charts.rs

@@ -0,0 +1,2 @@
+pub mod candlesticks;
+pub mod heatmap;

+ 143 - 0
src/charts/candlesticks.rs

@@ -0,0 +1,143 @@
+use chrono::{DateTime, Utc, Duration, TimeZone, LocalResult};
+use iced::{
+    widget::
+        canvas::{Cache, Frame, Geometry}
+    , Element, Length, Size
+};
+use plotters::prelude::ChartBuilder;
+use plotters_backend::DrawingBackend;
+use plotters_iced::{
+    Chart, ChartWidget, Renderer as plottersRenderer,
+};
+use plotters::prelude::full_palette::GREY;
+use std::collections::BTreeMap;
+
+use crate::market_data::Kline;
+use crate::Message;
+
+pub struct CandlestickChart {
+    cache: Cache,
+    data_points: BTreeMap<DateTime<Utc>, (f32, f32, f32, f32)>,
+    timeframe_in_minutes: i16,
+}
+impl CandlestickChart {
+    pub fn new(klines: Vec<Kline>, timeframe_in_minutes: i16) -> Self {
+        let mut data_points = BTreeMap::new();
+
+        for kline in klines {
+            let time = match Utc.timestamp_opt(kline.time as i64 / 1000, 0) {
+                LocalResult::Single(dt) => dt,
+                _ => continue, 
+            };
+            let open = kline.open;
+            let high = kline.high;
+            let low = kline.low;
+            let close = kline.close;
+            data_points.insert(time, (open, high, low, close));
+        }
+
+        Self {
+            cache: Cache::new(),
+            data_points,
+            timeframe_in_minutes,
+        }
+    }
+
+    pub fn update(&mut self, kline: Kline) {
+        let time = match Utc.timestamp_opt(kline.time as i64 / 1000, 0) {
+            LocalResult::Single(dt) => dt,
+            _ => return,
+        };
+        let open = kline.open;
+        let high = kline.high;
+        let low = kline.low;
+        let close = kline.close;
+        self.data_points.insert(time, (open, high, low, close));
+
+        self.cache.clear();
+    }
+
+    pub fn view(&self) -> Element<Message> {
+        let chart = ChartWidget::new(self)
+            .width(Length::Fill)
+            .height(Length::Fill);
+
+        chart.into()
+    }
+}
+impl Chart<Message> for CandlestickChart {
+    type State = ();
+    #[inline]
+    fn draw<R: plottersRenderer, F: Fn(&mut Frame)>(
+        &self,
+        renderer: &R,
+        bounds: Size,
+        draw_fn: F,
+    ) -> Geometry {
+        renderer.draw_cache(&self.cache, bounds, draw_fn)
+    }
+
+    fn build_chart<DB: DrawingBackend>(&self, _state: &Self::State, mut chart: ChartBuilder<DB>) {
+        use plotters::prelude::*;
+
+        let drawing_area;
+        {
+            let dummy_chart = chart
+                .build_cartesian_2d(0..1, 0..1) 
+                .expect("failed to build dummy chart");
+            drawing_area = dummy_chart.plotting_area().dim_in_pixel();
+        }
+        let newest_time = *self.data_points.keys().last().unwrap_or(&Utc::now());
+        let cutoff_number = ((drawing_area.0 as f64) / 12.0).round() as i64;
+        let oldest_time = newest_time - Duration::minutes((cutoff_number*self.timeframe_in_minutes as i64).max(1));
+        
+        let visible_data_points: Vec<_> = self.data_points.iter().filter(|&(time, _)| {
+            time >= &oldest_time && time <= &newest_time
+        }).collect();
+
+        let mut y_min = f32::MAX;
+        let mut y_max = f32::MIN;
+        for (_time, (_open, high, low, _close)) in &visible_data_points {
+            y_min = y_min.min(*low);
+            y_max = y_max.max(*high);
+        }
+
+        let mut chart = chart
+            .x_label_area_size(20)
+            .y_label_area_size(32)
+            .margin(20)
+            .build_cartesian_2d(oldest_time..newest_time, y_min..y_max)
+            .expect("failed to build chart");
+
+        chart
+            .configure_mesh()
+            .bold_line_style(GREY.mix(0.05))
+            .light_line_style(GREY.mix(0.02))
+            .axis_style(ShapeStyle::from(GREY.mix(0.45)).stroke_width(1))
+            .y_labels(10)
+            .y_label_style(
+                ("Noto Sans", 12)
+                    .into_font()
+                    .color(&GREY.mix(0.65))
+                    .transform(FontTransform::Rotate90),
+            )
+            .y_label_formatter(&|y| format!("{}", y))
+            .x_labels(8) 
+            .x_label_style(
+                ("Noto Sans", 12)
+                    .into_font()
+                    .color(&GREY.mix(0.65))
+            )
+            .x_label_formatter(&|x| {
+                x.format("%H:%M").to_string()
+            })
+            .draw()
+            .expect("failed to draw chart mesh");
+
+        chart.draw_series(
+            visible_data_points.iter().map(|(time, (open, high, low, close))| {
+                CandleStick::new(**time, *open, *high, *low, *close, RGBColor(81, 205, 160).filled(), RGBColor(192, 80, 77).filled(), 8)
+            }),
+        ).expect("failed to draw chart data");
+    }
+}

+ 200 - 0
src/charts/heatmap.rs

@@ -0,0 +1,200 @@
+use chrono::{DateTime, Utc, Duration, TimeZone, LocalResult};
+use iced::{
+    widget::
+        canvas::{Cache, Frame, Geometry}
+    , Element, Length, Size
+};
+use plotters::prelude::ChartBuilder;
+use plotters_backend::DrawingBackend;
+use plotters_iced::{
+    Chart, ChartWidget, Renderer as plottersRenderer,
+};
+use plotters::prelude::full_palette::GREY;
+use std::collections::VecDeque;
+
+use crate::market_data::Trade;
+use crate::Message;
+
+pub struct LineChart {
+    cache: Cache,
+    data_points: VecDeque<(DateTime<Utc>, f32, f32, bool)>,
+    depth: VecDeque<(DateTime<Utc>, Vec<(f32, f32)>, Vec<(f32, f32)>)>,
+}
+impl LineChart {
+    pub fn new() -> Self {
+        Self {
+            cache: Cache::new(),
+            data_points: VecDeque::new(),
+            depth: VecDeque::new(),
+        }
+    }
+
+    pub fn update(&mut self, depth_update: u64, mut trades_buffer: Vec<Trade>, bids: Vec<(f32, f32)>, asks: Vec<(f32, f32)>) {
+        let aggregate_time = 100; 
+        let seconds = (depth_update / 1000) as i64;
+        let nanoseconds = ((depth_update % 1000) / aggregate_time * aggregate_time * 1_000_000) as u32;
+        let depth_update_time = match Utc.timestamp_opt(seconds, nanoseconds) {
+            LocalResult::Single(dt) => dt,
+            _ => return, 
+        };
+
+        for trade in trades_buffer.drain(..) {
+            self.data_points.push_back((depth_update_time, trade.price, trade.qty, trade.is_sell));
+        }
+        if let Some((time, _, _)) = self.depth.back() {
+            if *time == depth_update_time {
+                self.depth.pop_back();
+            }
+        }
+        self.depth.push_back((depth_update_time, bids, asks));
+
+        while self.data_points.len() > 6000 {
+            self.data_points.pop_front();
+        }
+        while self.depth.len() > 1000 {
+            self.depth.pop_front();
+        }
+
+        self.cache.clear();
+    }
+
+    pub fn view(&self) -> Element<Message> {
+        let chart = ChartWidget::new(self)
+            .width(Length::Fill)
+            .height(Length::Fill);
+
+        chart.into()
+    }
+}
+impl Chart<Message> for LineChart {
+    type State = ();
+    #[inline]
+    fn draw<R: plottersRenderer, F: Fn(&mut Frame)>(
+        &self,
+        renderer: &R,
+        bounds: Size,
+        draw_fn: F,
+    ) -> Geometry {
+        renderer.draw_cache(&self.cache, bounds, draw_fn)
+    }
+
+    fn build_chart<DB: DrawingBackend>(&self, _state: &Self::State, mut chart: ChartBuilder<DB>) {
+        use plotters::prelude::*;
+        
+        if self.data_points.len() > 1 {
+            // x-axis range, acquire time range
+            let drawing_area;
+            {
+                let dummy_chart = chart
+                    .build_cartesian_2d(0..1, 0..1) 
+                    .expect("failed to build dummy chart");
+                drawing_area = dummy_chart.plotting_area().dim_in_pixel();
+            }
+            let newest_time = self.depth.back().unwrap().0 + Duration::milliseconds(200);
+            let oldest_time = newest_time - Duration::seconds(drawing_area.0 as i64 / 30);
+        
+            // y-axis range, acquire price range within the time range
+            let mut y_min = f32::MAX;
+            let mut y_max = f32::MIN;
+            let recent_data_points: Vec<_> = self.data_points.iter().filter_map(|&(time, price, qty, bool)| {
+                if time >= oldest_time && time <= newest_time {
+                    Some((time, price, qty, bool))
+                } else {
+                    None
+                }
+            }).collect();
+
+            let recent_depth: Vec<_> = self.depth.iter().filter_map(|(time, bids, asks)| {
+                if time >= &oldest_time && time <= &newest_time {
+                    if let Some((bid_price, _)) = bids.last() {
+                        y_min = y_min.min(*bid_price);
+                    } 
+                    if let Some((ask_price, _)) = asks.last() {
+                        y_max = y_max.max(*ask_price);
+                    }
+                    Some((time, bids, asks))
+                } else {
+                    None
+                }
+            }).collect();
+
+            let mut chart = chart
+                .x_label_area_size(20)
+                .y_label_area_size(32)
+                .margin(20)
+                .build_cartesian_2d(oldest_time..newest_time, y_min..y_max)
+                .expect("failed to build chart");
+
+            chart
+                .configure_mesh()
+                .bold_line_style(GREY.mix(0.04))
+                .light_line_style(GREY.mix(0.01))
+                .axis_style(ShapeStyle::from(GREY.mix(0.45)).stroke_width(1))
+                .y_labels(10)
+                .y_label_style(
+                    ("Noto Sans", 12)
+                        .into_font()
+                        .color(&GREY.mix(0.65))
+                        .transform(FontTransform::Rotate90),
+                )
+                .y_label_formatter(&|y| format!("{}", y))
+                .x_labels(8)
+                .x_label_style(
+                    ("Noto Sans", 12)
+                        .into_font()
+                        .color(&GREY.mix(0.65))
+                )
+                .x_label_formatter(&|x| {
+                    x.format("%M:%S").to_string()
+                })
+                .draw()
+                .expect("failed to draw chart mesh");
+
+            let max_order_quantity = recent_depth.iter()
+                .map(|(_, bids, asks)| {
+                bids.iter().map(|(_, qty)| qty).chain(asks.iter().map(|(_, qty)| qty)).fold(f32::MIN, |current_max: f32, qty: &f32| f32::max(current_max, *qty))
+            }).fold(f32::MIN, f32::max);
+            for i in 0..20 { 
+                let bids_i: Vec<(DateTime<Utc>, f32, f32)> = recent_depth.iter()
+                    .map(|&(time, bid, _ask)| ((*time).clone(), bid[i].0, bid[i].1)).collect();
+                let asks_i: Vec<(DateTime<Utc>, f32, f32)> = recent_depth.iter()
+                    .map(|&(time, _bid, ask)| ((*time).clone(), ask[i].0, ask[i].1)).collect();
+            
+                chart
+                    .draw_series(
+                        bids_i.iter().map(|&(time, price, quantity)| {
+                            let alpha = 0.1 + 0.9 * (quantity / max_order_quantity);
+                            Pixel::new((time, price), RGBAColor(0, 144, 144, alpha.into()))
+                        }),
+                    )
+                    .expect(&format!("failed to draw bids_{}", i));
+            
+                chart
+                    .draw_series(
+                        asks_i.iter().map(|&(time, price, quantity)| {
+                            let alpha = 0.1 + 0.9 * (quantity / max_order_quantity);
+                            Pixel::new((time, price), RGBAColor(192, 0, 192, alpha.into()))
+                        }),
+                    )
+                    .expect(&format!("failed to draw asks_{}", i));
+            }
+            
+            let (qty_min, qty_max) = recent_data_points.iter()
+                .map(|&(_, _, qty, _)| qty)
+                .fold((f32::MAX, f32::MIN), |(min, max), qty| (f32::min(min, qty), f32::max(max, qty)));
+            chart
+                .draw_series(
+                    recent_data_points.iter().map(|&(time, price, qty, is_sell)| {
+                        let radius = 1.0 + (qty - qty_min) * (35.0 - 1.0) / (qty_max - qty_min);
+                        let color = if is_sell { RGBColor(192, 80, 77) } else { RGBColor(81, 205, 160)};
+                        Circle::new(
+                            (time, price), 
+                            radius as i32,
+                            ShapeStyle::from(color).filled(),
+                        )
+                    }),
+                )
+                .expect("failed to draw circles");
+        }
+    }
+}

+ 1 - 0
src/data_providers.rs

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

+ 2 - 0
src/data_providers/binance.rs

@@ -0,0 +1,2 @@
+pub mod market_data;
+pub mod user_data;

+ 54 - 38
src/ws_binance.rs → src/data_providers/binance/market_data.rs

@@ -1,6 +1,8 @@
 use iced::futures;  
 use iced::subscription::{self, Subscription};
 use serde::Deserialize;
+use serde_json::json;
+use hmac::{Hmac, Mac};
 
 mod string_to_f32 {
     use serde::{self, Deserialize, Deserializer};
@@ -59,7 +61,7 @@ pub struct Kline {
     pub taker_buy_base_asset_volume: f32,
 }
 
-pub fn connect(selected_ticker: String, timeframe: String) -> Subscription<Event> {
+pub fn connect_market_stream(selected_ticker: String, timeframe: String) -> Subscription<Event> {
     struct Connect;
 
     subscription::channel(
@@ -100,50 +102,64 @@ pub fn connect(selected_ticker: String, timeframe: String) -> Subscription<Event
                             received = fused_websocket.select_next_some() => {
                                 match received {
                                     Ok(tungstenite::Message::Text(message)) => {
-                                        let parsed_message: Result<StreamWrapper, _> = serde_json::from_str(&message);
+                                        let parsed_message: Result<serde_json::Value, _> = serde_json::from_str(&message);
                                         match parsed_message {
-                                            Ok(mut wrapper) => {
-                                                if wrapper.stream.contains("aggTrade") {
-                                                    let trade: Result<Trade, _> = serde_json::from_str(&wrapper.data.to_string());
-                                                    match trade {
-                                                        Ok(trade) => {
-                                                            trades_buffer.push(trade);
-                                                        },
-                                                        Err(e) => {
-                                                            dbg!(e);
-                                                        }
-                                                    }
-                                                } else if wrapper.stream.contains("depth") {
-                                                    let update_time = wrapper.data.get("T").unwrap().as_u64().unwrap();
-
-                                                    if let Some(bids_data) = wrapper.data.get_mut("b") {
-                                                        let bids: Vec<(String, String)> = serde_json::from_value(bids_data.take()).unwrap();
-                                                        let bids: Vec<(f32, f32)> = bids.into_iter().map(|(price, qty)| (price.parse().unwrap(), qty.parse().unwrap())).collect();
-                                                
-                                                        if let Some(asks_data) = wrapper.data.get_mut("a") {
-                                                            let asks: Vec<(String, String)> = serde_json::from_value(asks_data.take()).unwrap();
-                                                            let asks: Vec<(f32, f32)> = asks.into_iter().map(|(price, qty)| (price.parse().unwrap(), qty.parse().unwrap())).collect();
-                                                
-                                                            let _ = output.send(Event::DepthReceived(update_time, bids, asks, std::mem::take(&mut trades_buffer))).await;
-                                                        }
-                                                    }
-                                                } else if wrapper.stream.contains("kline") {
-                                                    if let Some(kline) = wrapper.data.get_mut("k") {
-                                                        let kline: Result<Kline, _> = serde_json::from_value(kline.take());
-                                                        match kline {
-                                                            Ok(kline) => {
-                                                                let _ = output.send(Event::KlineReceived(kline)).await;
+                                            Ok(data) => {
+                                                if let Some(inner_data) = data.get("data") {
+                                                    if let Some(event_type) = inner_data["e"].as_str() {
+                                                        match event_type {
+                                                            "aggTrade" => {
+                                                                let trade: Result<Trade, _> = serde_json::from_value(data["data"].clone());
+                                                                match trade {
+                                                                    Ok(trade) => {
+                                                                        trades_buffer.push(trade);
+                                                                    },
+                                                                    Err(e) => {
+                                                                        dbg!(e);
+                                                                    }
+                                                                }
+                                                            },
+                                                            "depthUpdate" => {
+                                                                let update_time = data["data"]["T"].as_u64().unwrap();
+
+                                                                if let Some(bids_data) = data["data"]["b"].as_array() {
+                                                                    let bids: Vec<(f32, f32)> = bids_data.iter().map(|bid| {
+                                                                        let price = bid[0].as_str().unwrap().parse().unwrap();
+                                                                        let qty = bid[1].as_str().unwrap().parse().unwrap();
+                                                                        (price, qty)
+                                                                    }).collect();
+
+                                                                    if let Some(asks_data) = data["data"]["a"].as_array() {
+                                                                        let asks: Vec<(f32, f32)> = asks_data.iter().map(|ask| {
+                                                                            let price = ask[0].as_str().unwrap().parse().unwrap();
+                                                                            let qty = ask[1].as_str().unwrap().parse().unwrap();
+                                                                            (price, qty)
+                                                                        }).collect();
+
+                                                                        let _ = output.send(Event::DepthReceived(update_time, bids, asks, std::mem::take(&mut trades_buffer))).await;
+                                                                    }
+                                                                }
+                                                            },
+                                                            "kline" => {
+                                                                if let Some(kline) = data["data"]["k"].as_object() {
+                                                                    let kline: Result<Kline, _> = serde_json::from_value(json!(kline));
+                                                                    match kline {
+                                                                        Ok(kline) => {
+                                                                            let _ = output.send(Event::KlineReceived(kline)).await;
+                                                                        },
+                                                                        Err(e) => {
+                                                                            dbg!(e);
+                                                                        }
+                                                                    }
+                                                                }
                                                             },
-                                                            Err(e) => {
-                                                                dbg!(e);
-                                                            }
+                                                            _ => {}
                                                         }
                                                     }
                                                 }
                                             },
                                             Err(e) => {
-                                                dbg!(e);
-                                                dbg!(message); 
+                                                dbg!(e, message);
                                             }
                                         }
                                     }

+ 485 - 0
src/data_providers/binance/user_data.rs

@@ -0,0 +1,485 @@
+use iced::futures;  
+use iced::subscription::{self, Subscription};
+use reqwest::header::{HeaderMap, HeaderValue};
+use hmac::{Hmac, Mac};
+use sha2::Sha256;
+use hex;
+use futures::channel::mpsc;
+use futures::sink::SinkExt;
+use futures::stream::StreamExt;
+use chrono::Utc;
+use serde::Deserialize;
+use serde_json::json;
+use futures::FutureExt;
+use async_tungstenite::tungstenite;
+
+mod string_to_f32 {
+    use serde::{self, Deserialize, Deserializer};
+
+    pub fn deserialize<'de, D>(deserializer: D) -> Result<f32, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let s = String::deserialize(deserializer)?;
+        s.parse::<f32>().map_err(serde::de::Error::custom)
+    }
+}
+
+#[derive(Debug)]
+#[allow(clippy::large_enum_variant)]
+enum State {
+    Disconnected,
+    Connected(
+        async_tungstenite::WebSocketStream<
+            async_tungstenite::tokio::ConnectStream,
+        >,
+    ),
+}
+
+#[derive(Debug, Clone)]
+pub enum Event {
+    Connected(Connection),
+    Disconnected,
+    NewOrder(NewOrder),
+    CancelOrder(OrderTradeUpdate),
+    TestEvent(String),
+    NewPositions(Vec<Position>),
+    FetchedPositions(Vec<FetchedPosition>),
+    FetchedBalance(Vec<FetchedBalance>),
+}
+
+#[derive(Debug, Clone)]
+pub struct Connection(mpsc::Sender<String>);
+
+pub fn connect_user_stream(listen_key: String) -> Subscription<Event> {
+    struct Connect;
+
+    subscription::channel(
+        std::any::TypeId::of::<Connect>(),
+        100,
+        |mut output| async move {
+            let mut state = State::Disconnected;     
+ 
+            loop {
+                match &mut state {
+                    State::Disconnected => {
+                        let websocket_server = format!(
+                            "wss://stream.binancefuture.com/ws/{}",
+                            listen_key
+                        );
+        
+                        match async_tungstenite::tokio::connect_async(
+                            websocket_server,
+                        )
+                        .await
+                        {
+                            Ok((websocket, _)) => {
+                                state = State::Connected(websocket);
+                                dbg!("Connected to user stream");
+                            }
+                            Err(_) => {
+                                tokio::time::sleep(
+                                    tokio::time::Duration::from_secs(1),
+                                )
+                                .await;
+                                dbg!("Failed to connect to user stream");
+                                let _ = output.send(Event::Disconnected).await;
+                            }
+                        }
+                    }
+                    State::Connected(websocket) => {
+                        let mut fused_websocket = websocket.by_ref().fuse();
+
+                        futures::select! {
+                            received = fused_websocket.select_next_some() => {
+                                match received {
+                                    Ok(tungstenite::Message::Text(message)) => {
+                                        let parsed_message: Result<serde_json::Value, _> = serde_json::from_str(&message);
+                                        match parsed_message {
+                                            Ok(data) => {
+                                                let event;
+                                                if data["e"] == "ACCOUNT_UPDATE" {
+                                                    if let Some(account_update) = data["a"].as_object() {
+                                                        let account_update: AccountUpdate = serde_json::from_value(json!(account_update)).unwrap();
+                                                        if account_update.event_type == "ORDER" {
+                                                            event = Event::NewPositions(account_update.positions)
+                                                        } else {
+                                                            event = Event::TestEvent("Account Update".to_string());
+                                                        }
+                                                    } else {
+                                                        event = Event::TestEvent("Unknown".to_string());
+                                                    }
+                                                } else if data["e"] == "ORDER_TRADE_UPDATE" {
+                                                    if let Some(order_trade_update) = data["o"].as_object() {
+                                                        let order_trade_update: OrderTradeUpdate = serde_json::from_value(json!(order_trade_update)).unwrap();
+                                                        if order_trade_update.exec_type == "NEW" {
+                                                            event = Event::TestEvent("New Order".to_string());
+                                                        } else if order_trade_update.exec_type == "TRADE" {
+                                                            event = Event::TestEvent("Trade".to_string());
+                                                        } else if order_trade_update.exec_type == "CANCELED" {
+                                                            event = Event::CancelOrder(order_trade_update);
+                                                        } else {
+                                                            event = Event::TestEvent("Unknown".to_string());
+                                                        }
+                                                    } else {
+                                                        event = Event::TestEvent("Unknown".to_string());
+                                                    }
+
+                                                } else {
+                                                    event = Event::TestEvent("Unknown".to_string());
+                                                }
+                                                let _ = output.send(event).await;
+                                            },
+                                            Err(e) => {
+                                                dbg!(e, message);
+                                            }
+                                        }
+                                    }
+                                    Err(_) => {
+                                        dbg!("Disconnected from user stream");
+                                        let _ = output.send(Event::Disconnected).await;
+                                        state = State::Disconnected;
+                                    }
+                                    Ok(_) => continue,
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        },
+    )
+}
+
+pub fn fetch_user_stream(api_key: &str, secret_key: &str) -> Subscription<Event> {
+    struct Connect;
+
+    let api_key = api_key.to_owned();
+    let secret_key = secret_key.to_owned();
+
+    subscription::channel(
+        std::any::TypeId::of::<Connect>(),
+        100,
+        move |mut output| {
+            tokio::spawn(async move {
+                loop {
+                    let fetch_positions = fetch_open_positions(&api_key, &secret_key);
+                    let fetch_balance = fetch_acc_balance(&api_key, &secret_key);
+
+                    let (fetched_positions, fetched_balance) = futures::join!(fetch_positions, fetch_balance);
+
+                    match fetched_positions {
+                        Ok(positions) => {
+                            let _ = output.send(Event::FetchedPositions(positions)).await;
+                        }
+                        Err(e) => {
+                            eprintln!("Error fetching positions: {:?}", e);
+                        }
+                    }
+
+                    match fetched_balance {
+                        Ok(balance) => {
+                            let _ = output.send(Event::FetchedBalance(balance)).await;
+                        }
+                        Err(e) => {
+                            eprintln!("Error fetching balance: {:?}", e);
+                        }
+                    }
+
+                    tokio::time::sleep(std::time::Duration::from_secs(19)).await;
+                }
+            })
+        }.map(|result| result.expect("Failed to join"))
+    )
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct AccBalance {
+    #[serde(rename = "a")]
+    pub asset: String,
+    #[serde(rename = "wb")]
+    pub wallet_bal: String,
+    #[serde(rename = "cw")]
+    pub cross_bal: String,
+    #[serde(rename = "bc")]
+    pub balance_chg: String,
+}
+
+#[derive(Debug, Clone, Deserialize, PartialEq)]
+pub struct FetchedBalance {
+    pub asset: String,
+    #[serde(with = "string_to_f32", rename = "balance")]
+    pub balance: f32,
+    #[serde(with = "string_to_f32", rename = "crossWalletBalance")]
+    pub cross_bal: f32,
+    #[serde(with = "string_to_f32", rename = "crossUnPnl")]
+    pub cross_upnl: f32,
+    #[serde(with = "string_to_f32", rename = "availableBalance")]
+    pub available_bal: f32,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct Position {
+    #[serde(rename = "s")]
+    pub symbol: String,
+    #[serde(with = "string_to_f32", rename = "pa")]
+    pub pos_amt: f32,
+    #[serde(with = "string_to_f32", rename = "ep")]
+    pub entry_price: f32,
+    #[serde(with = "string_to_f32", rename = "bep")]
+    pub breakeven_price: f32,
+    #[serde(rename = "up")]
+    pub unrealized_pnl: String,
+    #[serde(rename = "mt")]
+    pub margin_type: String,
+    #[serde(rename = "iw")]
+    pub isolated_wallet: String,
+    #[serde(rename = "ps")]
+    pub pos_side: String,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct FetchedPosition {
+    pub symbol: String,
+    #[serde(with = "string_to_f32", rename = "positionAmt")]
+    pub pos_amt: f32,
+    #[serde(with = "string_to_f32", rename = "entryPrice")]
+    pub entry_price: f32,
+    #[serde(with = "string_to_f32", rename = "breakEvenPrice")]
+    pub breakeven_price: f32,
+    #[serde(with = "string_to_f32", rename = "markPrice")]
+    pub mark_price: f32,
+    #[serde(with = "string_to_f32", rename = "unRealizedProfit")]
+    pub unrealized_pnl: f32,
+    #[serde(with = "string_to_f32", rename = "liquidationPrice")]
+    pub liquidation_price: f32,
+    #[serde(with = "string_to_f32", rename = "leverage")]
+    pub leverage: f32,
+    #[serde(rename = "marginType")]
+    pub margin_type: String,
+}
+
+#[derive(Debug, Clone)]
+pub struct PositionInTable {
+    pub symbol: String,
+    pub size: f32,
+    pub entry_price: f32,
+    pub breakeven_price: f32,
+    pub mark_price: f32,
+    pub liquidation_price: f32,
+    pub margin_amt: f32,
+    pub unrealized_pnl: f32,
+}
+
+pub enum EventType {
+    AccountUpdate,
+    OrderTradeUpdate,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct AccountUpdate {
+    #[serde(rename = "m")]
+    pub event_type: String,
+    #[serde(rename = "B")]
+    pub balances: Vec<AccBalance>,
+    #[serde(rename = "P")]
+    pub positions: Vec<Position>,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct OrderTradeUpdate {
+    #[serde(rename = "s")]
+    pub symbol: String,
+    #[serde(rename = "S")]
+    pub side: String,
+    #[serde(rename = "o")]
+    pub order_type: String,
+    #[serde(rename = "x")]
+    pub exec_type: String,
+    #[serde(rename = "X")]
+    pub order_status: String,
+    #[serde(rename = "f")]
+    pub time_in_force: String,
+    #[serde(rename = "wt")]
+    pub working_type: String,
+    #[serde(rename = "i")]
+    pub order_id: i64,
+    #[serde(rename = "p")]
+    pub price: String,
+    #[serde(rename = "q")]
+    pub orig_qty: String,
+}
+
+#[derive(Debug)]
+pub enum BinanceError {
+    Reqwest(reqwest::Error),
+    BinanceAPI(String),
+}
+
+impl From<reqwest::Error> for BinanceError {
+    fn from(err: reqwest::Error) -> BinanceError {
+        BinanceError::Reqwest(err)
+    }
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct NewOrder {
+    #[serde(rename = "orderId")]
+    pub order_id: i64,
+    pub symbol: String,
+    pub side: String,
+    pub price: String,
+    #[serde(rename = "origQty")]
+    pub orig_qty: String,
+    #[serde(rename = "executedQty")]
+    pub executed_qty: String,
+    #[serde(rename = "timeInForce")]
+    pub time_in_force: String,
+    #[serde(rename = "type")]
+    pub order_type: String,
+    #[serde(rename = "reduceOnly")]
+    pub reduce_only: bool,
+    #[serde(rename = "updateTime")]
+    pub update_time: u64,
+}
+
+pub async fn create_limit_order (side: String, qty: String, price: String, api_key: &str, secret_key: &str) -> Result<NewOrder, BinanceError> {
+    let params = format!("symbol=BTCUSDT&side={}&type=LIMIT&timeInForce=GTC&quantity={}&price={}&timestamp={}", side, qty, price, Utc::now().timestamp_millis());
+    let signature = sign_params(&params, secret_key);
+
+    let url = format!("https://testnet.binancefuture.com/fapi/v1/order?{}&signature={}", params, signature);
+
+    let mut headers = HeaderMap::new();
+    headers.insert("X-MBX-APIKEY", HeaderValue::from_str(api_key).unwrap());
+
+    let client = reqwest::Client::new();
+    let res = client.post(&url).headers(headers).send().await?;
+
+    if res.status().is_success() {
+        let limit_order: NewOrder = res.json().await.map_err(BinanceError::Reqwest)?;
+        Ok(limit_order)
+    } else {
+        let error_msg: String = res.text().await.map_err(BinanceError::Reqwest)?;
+        Err(BinanceError::BinanceAPI(error_msg))
+    }
+}
+
+pub async fn create_market_order (side: String, qty: String, api_key: &str, secret_key: &str) -> Result<NewOrder, BinanceError> {
+    let params = format!("symbol=BTCUSDT&side={}&type=MARKET&quantity={}&timestamp={}", side, qty, Utc::now().timestamp_millis());
+    let signature = sign_params(&params, secret_key);
+
+    let url = format!("https://testnet.binancefuture.com/fapi/v1/order?{}&signature={}", params, signature);
+
+    let mut headers = HeaderMap::new();
+    headers.insert("X-MBX-APIKEY", HeaderValue::from_str(api_key).unwrap());
+
+    let client = reqwest::Client::new();
+    let res = client.post(&url).headers(headers).send().await?;
+
+    if res.status().is_success() {
+        let market_order: NewOrder = res.json().await.map_err(BinanceError::Reqwest)?;
+        Ok(market_order)
+    } else {
+        let error_msg: String = res.text().await.map_err(BinanceError::Reqwest)?;
+        Err(BinanceError::BinanceAPI(error_msg))
+    }
+}
+
+pub async fn cancel_order(order_id: String, api_key: &str, secret_key: &str) -> Result<(), BinanceError> {
+    let params = format!("symbol=BTCUSDT&orderId={}&timestamp={}", order_id, Utc::now().timestamp_millis());
+    let signature = sign_params(&params, secret_key);
+
+    let url = format!("https://testnet.binancefuture.com/fapi/v1/order?{}&signature={}", params, signature);
+
+    let mut headers = HeaderMap::new();
+    headers.insert("X-MBX-APIKEY", HeaderValue::from_str(api_key).unwrap());
+
+    let client = reqwest::Client::new();
+    let res = client.delete(&url).headers(headers).send().await?;
+
+    if res.status().is_success() {
+        Ok(())
+    } else {
+        let error_msg: String = res.text().await.map_err(BinanceError::Reqwest)?;
+        Err(BinanceError::BinanceAPI(error_msg))
+    }
+}
+
+pub async fn fetch_open_orders(symbol: String, api_key: &str, secret_key: &str) -> Result<Vec<NewOrder>, BinanceError> {
+    let params = format!("timestamp={}&symbol={}", Utc::now().timestamp_millis(), symbol);
+    let signature = sign_params(&params, secret_key);
+
+    let url = format!("https://testnet.binancefuture.com/fapi/v1/openOrders?{}&signature={}", params, signature);
+
+    let mut headers = HeaderMap::new();
+    headers.insert("X-MBX-APIKEY", HeaderValue::from_str(api_key).unwrap());
+
+    let client = reqwest::Client::new();
+    let res = client.get(&url).headers(headers).send().await?;
+
+    let open_orders: Vec<NewOrder> = res.json().await?;
+    Ok(open_orders)
+}
+
+pub async fn fetch_open_positions(api_key: &str, secret_key: &str) -> Result<Vec<FetchedPosition>, BinanceError> {
+    let params = format!("timestamp={}", Utc::now().timestamp_millis());
+    let signature = sign_params(&params, secret_key);
+
+    let url = format!("https://testnet.binancefuture.com/fapi/v2/positionRisk?{}&signature={}", params, signature);
+
+    let mut headers = HeaderMap::new();
+    headers.insert("X-MBX-APIKEY", HeaderValue::from_str(api_key).unwrap());
+
+    let client = reqwest::Client::new();
+    let res = client.get(&url).headers(headers).send().await?;
+
+    let positions: Vec<FetchedPosition> = res.json().await?;
+
+    Ok(positions)
+}
+
+pub async fn fetch_acc_balance(api_key: &str, secret_key: &str) -> Result<Vec<FetchedBalance>, BinanceError> {
+    let params = format!("timestamp={}", Utc::now().timestamp_millis());
+    let signature = sign_params(&params, secret_key);
+
+    let url = format!("https://testnet.binancefuture.com/fapi/v2/balance?{}&signature={}", params, signature);
+
+    let mut headers = HeaderMap::new();
+    headers.insert("X-MBX-APIKEY", HeaderValue::from_str(api_key).unwrap());
+
+    let client = reqwest::Client::new();
+    let res = client.get(&url).headers(headers).send().await?;
+
+    let acc_balance: Vec<FetchedBalance> = res.json().await?;
+    Ok(acc_balance)
+}
+
+pub async fn get_listen_key(api_key: &str, secret_key: &str) -> Result<String, BinanceError> {
+    let params = format!("timestamp={}", Utc::now().timestamp_millis());
+    let signature = sign_params(&params, secret_key);
+
+    let url = format!("https://testnet.binancefuture.com/fapi/v1/listenKey?{}&signature={}", params, signature);
+
+    let mut headers = HeaderMap::new();
+    headers.insert("X-MBX-APIKEY", HeaderValue::from_str(api_key).unwrap());
+
+    let client = reqwest::Client::new();
+    let res = client.post(&url).headers(headers).send().await?;
+
+    let listen_key: serde_json::Value = res.json().await?;
+    
+    if let Some(key) = listen_key.get("listenKey") {
+        Ok(key.as_str().unwrap().to_string())
+    } else {
+        Err(BinanceError::BinanceAPI("Failed to get listen key".to_string()))
+    }
+}
+
+fn sign_params(params: &str, secret_key: &str) -> String {
+    type HmacSha256 = Hmac<Sha256>;
+
+    let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes())
+        .expect("HMAC can take key of any size");
+    mac.update(params.as_bytes());
+    hex::encode(mac.finalize().into_bytes())
+}

+ 12 - 0
src/fonts/LICENSE.txt

@@ -0,0 +1,12 @@
+Font license info
+
+
+## Font Awesome
+
+   Copyright (C) 2016 by Dave Gandy
+
+   Author:    Dave Gandy
+   License:   SIL ()
+   Homepage:  http://fortawesome.github.com/Font-Awesome/
+
+

+ 52 - 0
src/fonts/config.json

@@ -0,0 +1,52 @@
+{
+  "name": "icons",
+  "css_prefix_text": "icon-",
+  "css_use_suffix": false,
+  "hinting": true,
+  "units_per_em": 1000,
+  "ascent": 850,
+  "glyphs": [
+    {
+      "uid": "c1f1975c885aa9f3dad7810c53b82074",
+      "css": "lock",
+      "code": 59393,
+      "src": "fontawesome"
+    },
+    {
+      "uid": "657ab647f6248a6b57a5b893beaf35a9",
+      "css": "lock-open",
+      "code": 59392,
+      "src": "fontawesome"
+    },
+    {
+      "uid": "e594fc6e5870b4ab7e49f52571d52577",
+      "css": "resize-full",
+      "code": 59394,
+      "src": "fontawesome"
+    },
+    {
+      "uid": "3c24ee33c9487bbf18796ca6dffa1905",
+      "css": "resize-small",
+      "code": 59395,
+      "src": "fontawesome"
+    },
+    {
+      "uid": "5211af474d3a9848f67f945e2ccaf143",
+      "css": "cancel",
+      "code": 59396,
+      "src": "fontawesome"
+    },
+    {
+      "uid": "1a5cfa186647e8c929c2b17b9fc4dac1",
+      "css": "plus-squared",
+      "code": 61694,
+      "src": "fontawesome"
+    },
+    {
+      "uid": "5e9f01871d44e56b45ecbfd00f4dbc3a",
+      "css": "layout",
+      "code": 59397,
+      "src": "entypo"
+    }
+  ]
+}

BIN
src/fonts/icons.ttf


Разница между файлами не показана из-за своего большого размера
+ 692 - 116
src/main.rs


Некоторые файлы не были показаны из-за большого количества измененных файлов