├── .cargo └── config ├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Readme.md └── src ├── main.rs ├── netstrat ├── bus │ ├── bus.rs │ ├── channels.rs │ ├── errors.rs │ ├── message.rs │ └── mod.rs ├── drawer.rs ├── line_filter_highlight_layout.rs ├── mod.rs ├── syntetic_data │ ├── data.rs │ ├── dataset.rs │ ├── errors.rs │ └── mod.rs └── thread_pool.rs ├── network ├── mod.rs └── rest.rs ├── sources ├── binance │ ├── client.rs │ ├── errors.rs │ ├── interval.rs │ └── mod.rs └── mod.rs ├── widgets ├── candles │ ├── bounds.rs │ ├── candles_drawer.rs │ ├── data.rs │ ├── error.rs │ ├── loading_state.rs │ ├── mod.rs │ ├── pages.rs │ ├── props.rs │ ├── state.rs │ ├── symbols.rs │ ├── time_input.rs │ ├── time_range.rs │ └── time_range_settings.rs ├── history │ ├── history.rs │ ├── message.rs │ ├── mod.rs │ ├── step.rs │ └── step_difference.rs ├── image_drawer │ ├── mod.rs │ ├── state.rs │ └── widget.rs ├── matrix │ ├── adj_matrix_state.rs │ ├── elements.rs │ ├── mod.rs │ └── widget.rs ├── mod.rs ├── net_props │ ├── button_clicks.rs │ ├── cones.rs │ ├── edges_input.rs │ ├── graph │ │ ├── cycle.rs │ │ ├── elements │ │ │ ├── element.rs │ │ │ ├── element_id.rs │ │ │ ├── elements.rs │ │ │ ├── frozen_elements.rs │ │ │ └── mod.rs │ │ ├── graph.rs │ │ ├── mod.rs │ │ ├── path.rs │ │ └── state │ │ │ ├── builder.rs │ │ │ ├── metadata.rs │ │ │ ├── mod.rs │ │ │ └── state.rs │ ├── interactions.rs │ ├── mod.rs │ ├── nodes_and_edges.rs │ ├── nodes_input.rs │ ├── settings.rs │ └── widget.rs ├── open_drop_file.rs ├── simulation_props │ ├── controls.rs │ ├── messages.rs │ ├── mod.rs │ └── widget.rs ├── theme.rs └── widget.rs └── windows ├── candles.rs ├── debug.rs ├── mod.rs ├── net.rs ├── simulator.rs └── window.rs /.cargo/config: -------------------------------------------------------------------------------- 1 | [target.x86_64-pc-windows-gnu] 2 | linker = "x86_64-w64-mingw32-gcc" 3 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Test step 19 | run: echo 1 20 | # - uses: actions/checkout@v3 21 | # - name: Build 22 | # run: cargo build --verbose 23 | # - name: Run tests 24 | # run: cargo test --verbose 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "netstrat" 4 | version = "0.1.0" 5 | authors = ["Dmitrii Samsonov "] 6 | license = "MIT" 7 | repository = "https://github.com/qzarx1/netstrat" 8 | 9 | [dependencies] 10 | csv = "1.1" 11 | chrono = "0.4.19" 12 | crossbeam = "0.8.1" 13 | eframe = { version = "0.19" } 14 | egui = { version = "0.19" } 15 | egui_extras = { version = "0.19", features = ["chrono", "svg"] } 16 | futures = "0.3" 17 | poll-promise = { version = "0.2", features = ["tokio"] } 18 | reqwest = { version = "0.11.10", features = ["blocking"] } 19 | serde = { version = "1", features = ["derive"] } 20 | serde_json = "1.0.81" 21 | tokio = { version = "1.28", features = ["full"] } 22 | tracing = "0.1.34" 23 | tracing-subscriber = { version = "0.3.11", features = ["env-filter"] } 24 | rand = "0.8.5" 25 | quick-error = "2.0.1" 26 | egui-notify = "0.4" 27 | petgraph = { vesion = "0.6.2", features = ["serde-1"] } 28 | open = "4" 29 | urlencoding = "2.1.2" 30 | rfd = "0.11" 31 | lazy_static = "1.4.0" 32 | regex = "1.6.0" 33 | graphviz-rust = "0.6" 34 | ndarray = "0.15" 35 | uuid = { version = "1.2.2", features = ["v4", "serde"] } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # netstrat 2 | **netstrat** is a strategy backtesting and visualization tool using [egui](https://github.com/emilk/egui) for ui 3 | 4 | Screen Shot 2022-08-23 at 14 06 58 5 | 6 | Screen Shot 2022-10-21 at 13 23 18 7 | 8 | ### Status 9 | The project is under active development. No stable version yet. First release should include: 10 | 11 |
12 | Tool                                                          Completion
13 | ------------------------------------------------------------+------------
14 | - binance client for tick data download and visualization   |   90%
15 | - graph based trading strategy constructor                  |   10%
16 | - backtesting tool                                          |    0%
17 | - graph analysis tool to support ML based trading strategies|   40%
18 | 
19 | Short term plan is to build and use [my own egui based implementation for graph visualizaton](https://github.com/blitzarx1/egui_graphs) and get rid of graphviz dependency 20 | 21 | Screenshot 2023-04-08 at 14 33 14 22 | 23 | ### Depedencies 24 | You need to have [graphviz binary](https://graphviz.org/download/) and [rust](https://rustup.rs/) installed 25 | 26 | ### Run 27 | ```bash 28 | cargo run --release 29 | ``` 30 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | sync::{Arc, Mutex}, 3 | time::Duration, 4 | }; 5 | 6 | use crossbeam::channel::{unbounded, Receiver, Sender}; 7 | use eframe::{run_native, App, CreationContext, NativeOptions}; 8 | use egui::{Align, CentralPanel, Context, Layout, TopBottomPanel}; 9 | 10 | use netstrat::Drawer; 11 | use tracing::{debug, info, Level}; 12 | use tracing_subscriber::EnvFilter; 13 | 14 | use crate::{windows::{BuffWriter, Net}, netstrat::Bus}; 15 | use windows::{AppWindow, Debug, SymbolsGraph}; 16 | 17 | mod netstrat; 18 | mod network; 19 | mod sources; 20 | mod widgets; 21 | mod windows; 22 | 23 | struct TemplateApp { 24 | windows: Vec>, 25 | active_drawer: Option>>>, 26 | active_drawer_subs: Vec>>>>, 27 | } 28 | 29 | fn init_logger(s: Sender>) { 30 | let buff = BuffWriter::new(s); 31 | 32 | let has_config = std::env::var("RUST_LOG"); 33 | if has_config.is_err() { 34 | tracing_subscriber::fmt() 35 | .with_writer(Mutex::new(buff)) 36 | .with_ansi(false) 37 | .with_max_level(Level::INFO) 38 | .with_line_number(false) 39 | .with_file(false) 40 | .with_target(false) 41 | .without_time() 42 | .init(); 43 | 44 | return; 45 | } 46 | 47 | tracing_subscriber::fmt() 48 | .with_writer(Mutex::new(buff)) 49 | .with_ansi(false) 50 | .with_env_filter(EnvFilter::from_default_env()) 51 | .with_line_number(true) 52 | .with_file(true) 53 | .with_target(false) 54 | .init(); 55 | } 56 | 57 | impl TemplateApp { 58 | fn new(_ctx: &CreationContext<'_>) -> Self { 59 | let (buffer_s, buffer_r) = unbounded(); 60 | 61 | init_logger(buffer_s); 62 | 63 | info!("starting app"); 64 | let (net_drawer_s, net_drawer_r) = unbounded(); 65 | let (candles_drawer_s, candles_drawer_r) = unbounded(); 66 | 67 | let bus = Bus::new(); 68 | 69 | Self { 70 | windows: vec![ 71 | Box::new(Net::new(bus, net_drawer_s, false)), 72 | Box::new(SymbolsGraph::new(candles_drawer_s, false)), 73 | Box::new(Debug::new(buffer_r, false)), 74 | ], 75 | active_drawer_subs: vec![net_drawer_r, candles_drawer_r], 76 | active_drawer: None, 77 | } 78 | } 79 | 80 | fn check_drawer_event(&mut self) { 81 | self.active_drawer_subs.iter().for_each(|sub| { 82 | let drawer_wrapped = sub.recv_timeout(Duration::from_millis(1)); 83 | if let Ok(drawer) = drawer_wrapped { 84 | debug!("got active drawer event"); 85 | 86 | self.active_drawer = Some(drawer); 87 | } 88 | }); 89 | } 90 | } 91 | 92 | impl App for TemplateApp { 93 | fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) { 94 | TopBottomPanel::top("header").show(ctx, |ui| { 95 | ui.with_layout(Layout::left_to_right(Align::Center), |ui| { 96 | self.windows.iter_mut().for_each(|w| { 97 | w.as_mut().toggle_btn(ui); 98 | }); 99 | }); 100 | }); 101 | 102 | CentralPanel::default().show(ctx, |ui| { 103 | if let Some(drawer) = &mut self.active_drawer { 104 | drawer.lock().as_mut().unwrap().show(ui); 105 | } 106 | 107 | self.windows.iter_mut().for_each(|w| w.show(ui)); 108 | }); 109 | 110 | self.check_drawer_event(); 111 | } 112 | } 113 | 114 | #[tokio::main] 115 | async fn main() { 116 | run_native( 117 | "netstrat", 118 | NativeOptions { 119 | drag_and_drop_support: true, 120 | ..Default::default() 121 | }, 122 | Box::new(|cc| Box::new(TemplateApp::new(cc))), 123 | ); 124 | } 125 | -------------------------------------------------------------------------------- /src/netstrat/bus/bus.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, rc::Rc, sync::Mutex}; 2 | 3 | use crossbeam::channel::{unbounded, Receiver, Sender}; 4 | use std::time::Duration; 5 | use tracing::{debug, error, trace}; 6 | 7 | use super::{errors, Message}; 8 | 9 | #[derive(Default, Clone)] 10 | pub struct Bus { 11 | channels: Rc, Receiver)>>>, 12 | } 13 | 14 | impl Bus { 15 | pub fn new() -> Self { 16 | Self { 17 | channels: Default::default(), 18 | } 19 | } 20 | 21 | pub fn read(&mut self, ch_name: String) -> Result { 22 | trace!("reading from channel: {ch_name}"); 23 | 24 | let receiver = self.channel_get_or_create(ch_name).1; 25 | let msg = receiver.recv_timeout(Duration::from_nanos(1))?; 26 | 27 | trace!("successfully read from channel: {msg:?}"); 28 | 29 | Ok(msg) 30 | } 31 | 32 | pub fn write(&mut self, ch_name: String, msg: Message) -> Result<(), errors::Bus> { 33 | trace!("writing to channel; channel: {ch_name}, message: {msg:?}"); 34 | 35 | let sender = self.channel_get_or_create(ch_name).0; 36 | sender.send(msg)?; 37 | 38 | trace!("successfully sent to channel"); 39 | 40 | Ok(()) 41 | } 42 | 43 | fn channel_get_or_create(&mut self, ch_name: String) -> (Sender, Receiver) { 44 | let mut locked_channesls = self.channels.lock().unwrap(); 45 | let channel_wrapped = locked_channesls.get(&ch_name); 46 | if channel_wrapped.is_none() { 47 | debug!("channel not found: {ch_name}; creating new..."); 48 | 49 | let res = unbounded(); 50 | locked_channesls.insert(ch_name, res.clone()); 51 | return res; 52 | }; 53 | 54 | channel_wrapped.unwrap().clone() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/netstrat/bus/channels.rs: -------------------------------------------------------------------------------- 1 | pub const SIMULATION_CHANNEL: &str = "simulation"; 2 | pub const HISTORY_DIFFERENCE: &str = "history difference"; 3 | -------------------------------------------------------------------------------- /src/netstrat/bus/errors.rs: -------------------------------------------------------------------------------- 1 | use crossbeam::channel::{RecvTimeoutError, SendError}; 2 | use quick_error::quick_error; 3 | 4 | use super::Message; 5 | 6 | quick_error! { 7 | #[derive(Debug)] 8 | pub enum Bus { 9 | ChannelNotFound(name: String) { 10 | display("channel not found: {name}") 11 | } 12 | Recv(err: RecvTimeoutError) { 13 | from() 14 | display("{}", err) 15 | } 16 | Send(err: SendError) { 17 | from() 18 | display("{}", err) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/netstrat/bus/message.rs: -------------------------------------------------------------------------------- 1 | use chrono::{Date, DateTime, Utc}; 2 | 3 | #[derive(Debug)] 4 | pub struct Message { 5 | payload: String, 6 | ts: DateTime, 7 | } 8 | 9 | impl Message { 10 | pub fn new(payload: String) -> Self { 11 | Self { 12 | payload, 13 | ts: Utc::now(), 14 | } 15 | } 16 | 17 | pub fn payload(&self) -> String { 18 | self.payload.clone() 19 | } 20 | 21 | pub fn ts(&self) -> DateTime { 22 | self.ts 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/netstrat/bus/mod.rs: -------------------------------------------------------------------------------- 1 | mod bus; 2 | pub mod channels; 3 | mod errors; 4 | mod message; 5 | 6 | pub use self::bus::Bus; 7 | pub use self::message::Message; 8 | -------------------------------------------------------------------------------- /src/netstrat/drawer.rs: -------------------------------------------------------------------------------- 1 | use egui::ColorImage; 2 | 3 | use crate::widgets::AppWidget; 4 | 5 | pub trait Drawer: AppWidget { 6 | fn update_image(&mut self, image: ColorImage); 7 | fn has_unread_image(&self) -> bool; 8 | } 9 | -------------------------------------------------------------------------------- /src/netstrat/line_filter_highlight_layout.rs: -------------------------------------------------------------------------------- 1 | use eframe::epaint::text::TextWrapping; 2 | use egui::{text::LayoutJob, Color32, Stroke, TextFormat, Ui}; 3 | 4 | pub fn line_filter_highlight_layout( 5 | ui: &Ui, 6 | string: &str, 7 | filter: &String, 8 | is_strikethrough: bool, 9 | ) -> LayoutJob { 10 | let color = ui.visuals().text_color().linear_multiply({ 11 | match is_strikethrough { 12 | true => 0.5, 13 | false => 1.0, 14 | } 15 | }); 16 | let strikethrough = Stroke::new( 17 | { 18 | match is_strikethrough { 19 | true => 2.0, 20 | false => 0.0, 21 | } 22 | }, 23 | color, 24 | ); 25 | 26 | let mut job = LayoutJob { 27 | wrap: TextWrapping { 28 | break_anywhere: false, 29 | ..Default::default() 30 | }, 31 | ..Default::default() 32 | }; 33 | 34 | // need to work with 2 strings to preserve original register 35 | let mut text = string.to_string(); 36 | let mut normalized_text = text.to_lowercase(); 37 | while !text.is_empty() { 38 | let filter_offset_res = normalized_text.find(filter.to_lowercase().as_str()); 39 | 40 | let mut drain_bound = text.len(); 41 | if !filter.is_empty() { 42 | if let Some(filter_offset) = filter_offset_res { 43 | drain_bound = filter_offset + filter.len(); 44 | 45 | let plain = &text.as_str()[..filter_offset]; 46 | job.append( 47 | plain, 48 | 0.0, 49 | TextFormat { 50 | strikethrough, 51 | color, 52 | ..Default::default() 53 | }, 54 | ); 55 | 56 | let highlighted = &text.as_str()[filter_offset..drain_bound]; 57 | job.append( 58 | highlighted, 59 | 0.0, 60 | TextFormat { 61 | background: Color32::YELLOW, 62 | strikethrough, 63 | color, 64 | ..Default::default() 65 | }, 66 | ); 67 | 68 | text.drain(..drain_bound); 69 | normalized_text.drain(..drain_bound); 70 | continue; 71 | } 72 | } 73 | 74 | let plain = &text.as_str()[..drain_bound]; 75 | job.append( 76 | plain, 77 | 0.0, 78 | TextFormat { 79 | strikethrough, 80 | color, 81 | ..Default::default() 82 | }, 83 | ); 84 | text.drain(..drain_bound); 85 | } 86 | 87 | job 88 | } 89 | -------------------------------------------------------------------------------- /src/netstrat/mod.rs: -------------------------------------------------------------------------------- 1 | mod bus; 2 | mod drawer; 3 | mod line_filter_highlight_layout; 4 | mod syntetic_data; 5 | mod thread_pool; 6 | 7 | pub use self::bus::{channels, Bus, Message}; 8 | pub use self::drawer::Drawer; 9 | pub use self::line_filter_highlight_layout::line_filter_highlight_layout; 10 | pub use self::thread_pool::ThreadPool; 11 | -------------------------------------------------------------------------------- /src/netstrat/syntetic_data/data.rs: -------------------------------------------------------------------------------- 1 | use super::errors; 2 | 3 | /// Data is a lazy wrapper for function. It computes function values only 4 | /// when they are needed. 5 | pub struct Data { 6 | input: Vec, 7 | f: Box f64>, 8 | last_computed: Option, 9 | values: Vec, 10 | } 11 | 12 | impl Data { 13 | /// Input size must be >= 2. 14 | pub fn new(input: Vec, f: Box f64>) -> Result { 15 | if input.len() < 2 { 16 | return Err(errors::Data::InputSize(input.len())); 17 | } 18 | 19 | let input_len = input.len(); 20 | Ok(Self { 21 | last_computed: Default::default(), 22 | input, 23 | f, 24 | values: Vec::with_capacity(input_len), 25 | }) 26 | } 27 | 28 | /// Counts derivative for x index in self.values. 29 | /// We use only n-2 values if self.values has size n. 30 | /// If x>n-2 returns None 31 | pub fn derivative(&mut self, x: usize) -> Option { 32 | if x > self.input.len() - 2 { 33 | return None; 34 | } 35 | 36 | let left = self.value(x)?; 37 | let right = self.value(x + 1)?; 38 | 39 | Some((right - left) / (self.input.get(x + 1)? - self.input.get(x)?).abs()) 40 | } 41 | 42 | /// Computes values for inputs in range (self.last_computed, x] and stores results in self.values. 43 | /// Returns computed value for self.input at index x. 44 | pub fn value(&mut self, x: usize) -> Option { 45 | if x > self.input.len() - 1 { 46 | return None; 47 | } 48 | 49 | if self.last_computed.is_some() && x < self.last_computed.unwrap() { 50 | return Some(*self.values.get(x).unwrap()); 51 | } 52 | 53 | let mut start = 0; 54 | if self.last_computed.is_some() { 55 | start = self.last_computed.unwrap() + 1; 56 | } 57 | 58 | self.values 59 | .extend((start..x + 1).map(|idx| self.f.as_ref()(*self.input.get(idx).unwrap()))); 60 | 61 | self.last_computed = Some(x); 62 | 63 | Some(*self.values.get(x).unwrap()) 64 | } 65 | 66 | pub fn extend_input(&mut self, input_extension: Vec) { 67 | self.input.extend(input_extension.iter()); 68 | } 69 | } 70 | 71 | #[cfg(test)] 72 | mod test { 73 | use super::*; 74 | 75 | #[test] 76 | fn test_value() { 77 | let mut data = Data::new(vec![1.0, 2.0, 3.0, 4.0, 5.0], Box::new(|x| x)).unwrap(); 78 | 79 | assert_eq!(data.value(0).unwrap(), 1.0); 80 | assert_eq!(data.value(3).unwrap(), 4.0); 81 | 82 | assert_eq!(*data.values.get(0).unwrap(), 1.0); 83 | assert_eq!(*data.values.get(1).unwrap(), 2.0); 84 | assert_eq!(*data.values.get(2).unwrap(), 3.0); 85 | 86 | assert_eq!(data.values.get(4), None); 87 | 88 | assert_eq!(data.value(4).unwrap(), 5.0); 89 | assert_eq!(*data.values.get(4).unwrap(), 5.0); 90 | } 91 | 92 | #[test] 93 | fn derivative_line() { 94 | let mut data = Data::new(vec![1.0, 2.0, 3.0, 4.0, 5.0], Box::new(|x| x)).unwrap(); 95 | 96 | assert_eq!(data.derivative(0).unwrap(), 1.0); 97 | assert_eq!(data.derivative(1).unwrap(), 1.0); 98 | assert_eq!(data.derivative(2).unwrap(), 1.0); 99 | assert_eq!(data.derivative(3).unwrap(), 1.0); 100 | assert_eq!(data.derivative(4), None) 101 | } 102 | 103 | #[test] 104 | fn derivative_parabolic() { 105 | let mut data = Data::new(vec![1.0, 2.0, 3.0, 4.0, 5.0], Box::new(|x| x * x)).unwrap(); 106 | 107 | assert_eq!(data.derivative(0).unwrap(), 3.0); 108 | assert_eq!(data.derivative(1).unwrap(), 5.0); 109 | assert_eq!(data.derivative(2).unwrap(), 7.0); 110 | assert_eq!(data.derivative(3).unwrap(), 9.0); 111 | assert_eq!(data.derivative(4), None) 112 | } 113 | 114 | #[test] 115 | fn derivative_for_small_input_size() { 116 | let mut data = Data::new(vec![1.0, 2.0], Box::new(|x| x)).unwrap(); 117 | 118 | assert_eq!(data.derivative(0).unwrap(), 1.0); 119 | assert_eq!(data.derivative(1), None); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/netstrat/syntetic_data/dataset.rs: -------------------------------------------------------------------------------- 1 | use super::data::Data; 2 | use super::errors; 3 | 4 | const MIN_RESOLUTION_STEPS: usize = 10; 5 | 6 | /// Dataset computes training and test set with dynamic step size based on the derivative value 7 | #[derive(Debug)] 8 | pub struct Dataset { 9 | data: Vec<[f64; 2]>, 10 | training_part: f64, 11 | mesh: Vec, 12 | } 13 | 14 | impl Dataset { 15 | pub fn new( 16 | name: &str, 17 | x0: f64, 18 | steps: usize, 19 | mesh_size: f64, 20 | f: Box f64>, 21 | ) -> Result { 22 | if steps < 2 { 23 | return Err(errors::Data::StepsNumber(steps)); 24 | } 25 | 26 | let mesh_len = steps * MIN_RESOLUTION_STEPS; 27 | let mut mesh = Vec::with_capacity(mesh_len); 28 | (0..mesh_len).for_each(|i| { 29 | mesh.push(x0 + i as f64 * mesh_size); 30 | }); 31 | let data = Dataset::compute_data(mesh.clone(), steps, f)?; 32 | 33 | Ok(Self { 34 | data, 35 | mesh, 36 | training_part: 0.8, 37 | }) 38 | } 39 | 40 | pub fn training_set(&self) -> Vec<[f64; 2]> { 41 | let fin_idx = (self.training_part * self.data.len() as f64).ceil() as usize; 42 | self.data[0..fin_idx].to_vec() 43 | } 44 | 45 | pub fn test_set(&self) -> Vec<[f64; 2]> { 46 | let fin_training_idx = (self.training_part * self.data.len() as f64).ceil() as usize; 47 | self.data[fin_training_idx..].to_vec() 48 | } 49 | 50 | fn compute_data( 51 | mesh: Vec, 52 | steps: usize, 53 | f: Box f64>, 54 | ) -> Result, errors::Data> { 55 | let mut res = Vec::with_capacity(steps); 56 | let mut data = Data::new(mesh.clone(), f)?; 57 | 58 | let max_derivative = (0..mesh.len() - 1) 59 | .map(|i| data.derivative(i).unwrap()) 60 | .max_by(|left, right| left.partial_cmp(right).unwrap()) 61 | .unwrap(); 62 | 63 | let mut x_idx = 0; 64 | (0..steps).for_each(|_i| { 65 | res.push([*mesh.get(x_idx).unwrap(), data.value(x_idx).unwrap()]); 66 | 67 | let d = data.derivative(x_idx).unwrap(); 68 | let coeff = 1f64 - (d / max_derivative).abs(); 69 | x_idx += (MIN_RESOLUTION_STEPS as f64 * coeff).ceil() as usize 70 | }); 71 | 72 | Ok(res) 73 | } 74 | } 75 | 76 | #[cfg(test)] 77 | mod test { 78 | use super::*; 79 | 80 | #[test] 81 | fn test_compute_data() { 82 | let steps = 10; 83 | let x0 = 3.0; 84 | let mesh_size = 0.5; 85 | let mesh_len = steps * MIN_RESOLUTION_STEPS; 86 | let mut mesh = Vec::with_capacity(mesh_len); 87 | (0..mesh_len).for_each(|i| { 88 | mesh.push(x0 + i as f64 * mesh_size); 89 | }); 90 | 91 | Dataset::compute_data(mesh, steps, Box::new(|x| x.sin())); 92 | } 93 | 94 | #[test] 95 | fn test_dataset_split() { 96 | let dataset = Dataset::new("sin", 0.0, 100, 0.5, Box::new(|x| x.sin())).unwrap(); 97 | let training_set = dataset.training_set(); 98 | let test_set = dataset.test_set(); 99 | 100 | assert_eq!(training_set.len(), 80); 101 | assert_eq!(test_set.len(), 20); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/netstrat/syntetic_data/errors.rs: -------------------------------------------------------------------------------- 1 | use quick_error::quick_error; 2 | 3 | quick_error! { 4 | #[derive(Debug)] 5 | pub enum Data { 6 | InputSize(size: usize) { 7 | display("invalid input size: {size}") 8 | } 9 | StepsNumber(steps: usize) { 10 | display("invalid number of steps: {steps}") 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/netstrat/syntetic_data/mod.rs: -------------------------------------------------------------------------------- 1 | mod data; 2 | mod dataset; 3 | mod errors; 4 | -------------------------------------------------------------------------------- /src/netstrat/thread_pool.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | 3 | use crossbeam::channel::{unbounded, Receiver, Sender}; 4 | use tracing::{debug, error, trace}; 5 | 6 | struct Job { 7 | f: Box, 8 | } 9 | 10 | struct Worker { 11 | id: usize, 12 | thread: thread::JoinHandle<()>, 13 | } 14 | 15 | impl Worker { 16 | fn new(id: usize, receiver: Receiver) -> Self { 17 | debug!("initing worker with id: {id}"); 18 | 19 | let thread = thread::spawn(move || loop { 20 | if let Ok(job) = receiver.recv() { 21 | debug!("worker {id} got a job; executing..."); 22 | 23 | let f = job.f; 24 | f(); 25 | 26 | debug!("worker {id} finished the job"); 27 | } 28 | }); 29 | 30 | Self { id, thread } 31 | } 32 | } 33 | pub struct ThreadPool { 34 | workers: Vec, 35 | sender: Sender, 36 | } 37 | 38 | // TODO: graceful shutdown 39 | impl ThreadPool { 40 | pub fn new(size: usize) -> Self { 41 | debug!("initing thread_pool"); 42 | let (sender, receiver) = unbounded(); 43 | 44 | let mut workers = Vec::with_capacity(size); 45 | 46 | for id in 0..size { 47 | workers.push(Worker::new(id, receiver.clone())); 48 | } 49 | 50 | Self { workers, sender } 51 | } 52 | 53 | pub fn execute(&self, f: F) 54 | where 55 | F: FnOnce() + Send + 'static, 56 | { 57 | let job = Job { f: Box::new(f) }; 58 | 59 | if let Err(err) = self.sender.send(job) { 60 | error!("failed to send job with err: {err}"); 61 | }; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/network/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod rest; 2 | -------------------------------------------------------------------------------- /src/network/rest.rs: -------------------------------------------------------------------------------- 1 | use tracing::{trace, debug}; 2 | 3 | #[derive(Clone, Debug)] 4 | pub struct Rest { 5 | c: reqwest::blocking::Client, 6 | } 7 | 8 | impl Rest { 9 | pub fn new() -> Rest { 10 | Rest { 11 | c: reqwest::blocking::Client::new(), 12 | } 13 | } 14 | 15 | pub fn get(&self, url: &str) -> Result { 16 | let req = self.c.get(url); 17 | 18 | self.execute_request(req) 19 | } 20 | 21 | pub fn get_with_params( 22 | &self, 23 | url: &str, 24 | params: &[(&str, &str)], 25 | ) -> Result { 26 | let req = self.c.get(url).query(params); 27 | 28 | self.execute_request(req) 29 | } 30 | 31 | fn execute_request( 32 | &self, 33 | req: reqwest::blocking::RequestBuilder, 34 | ) -> Result { 35 | let req_builded = req.build()?; 36 | debug!( 37 | "sending request: method: {:?}; url: {:?}; headers: {:?}; body: {:?}.", 38 | req_builded.method(), 39 | req_builded.url().as_str(), 40 | req_builded.headers(), 41 | req_builded.body(), 42 | ); 43 | 44 | self.c.execute(req_builded) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/sources/binance/client.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | 3 | use reqwest::header::HeaderValue; 4 | use serde::{Deserialize, Serialize}; 5 | use serde_json; 6 | use tokio::task::spawn_blocking; 7 | use tracing::debug; 8 | 9 | use crate::network::rest::Rest; 10 | use crate::sources::binance::interval::Interval; 11 | 12 | use super::errors::ClientError; 13 | 14 | const HEADER_REQ_WEIGHT: &str = "x-mbx-used-weight-1m"; 15 | const HEADER_RETRY_AFTER: &str = "Retry-After"; 16 | 17 | #[derive(Clone, Debug, Default)] 18 | pub struct Client {} 19 | 20 | const BASE_URL: &str = "https://api.binance.com"; 21 | const PATH_KLINE: &str = "/api/v3/klines"; 22 | const PATH_INFO: &str = "/api/v3/exchangeInfo"; 23 | 24 | #[derive(Debug, Deserialize, Default)] 25 | pub struct Info { 26 | pub symbols: Vec, 27 | } 28 | 29 | #[derive(Deserialize)] 30 | struct KlineData( 31 | i64, // Open time 32 | String, // Open 33 | String, // High 34 | String, // Low 35 | String, // Close 36 | String, // Volume 37 | i64, // Close time 38 | String, // Quote asset volume 39 | i64, // Number of trades 40 | String, // Taker buy base asset volume 41 | String, // Taker buy quote asset volume 42 | String, // Ignore 43 | ); 44 | #[derive(Debug, Deserialize, Default, Clone)] 45 | pub struct Symbol { 46 | pub symbol: String, 47 | pub status: String, 48 | 49 | #[serde(rename = "baseAsset")] 50 | base_asset: String, 51 | 52 | #[serde(rename = "baseAssetPrecision")] 53 | base_asset_precision: usize, 54 | 55 | #[serde(rename = "quoteAsset")] 56 | quote_asset: String, 57 | 58 | #[serde(rename = "quotePrecision")] 59 | quote_precision: usize, 60 | 61 | #[serde(rename = "quoteAssetPrecision")] 62 | quote_asset_precision: usize, 63 | 64 | #[serde(rename = "baseCommissionPrecision")] 65 | base_commission_precision: usize, 66 | 67 | #[serde(rename = "quoteCommissionPrecision")] 68 | quote_commission_precision: usize, 69 | 70 | #[serde(rename = "icebergAllowed")] 71 | iceberg_allowed: bool, 72 | 73 | #[serde(rename = "ocoAllowed")] 74 | oco_allowed: bool, 75 | 76 | #[serde(rename = "quoteOrderQtyMarketAllowed")] 77 | quote_order_qty_market_allowed: bool, 78 | 79 | #[serde(rename = "allowTrailingStop")] 80 | allow_trailing_stop: bool, 81 | 82 | #[serde(rename = "isSpotTradingAllowed")] 83 | is_spot_trading_allowed: bool, 84 | 85 | #[serde(rename = "isMarginTradingAllowed")] 86 | is_margin_trading_allowed: bool, 87 | } 88 | 89 | impl Symbol { 90 | pub fn active(&self) -> bool { 91 | self.status == "TRADING" 92 | } 93 | } 94 | 95 | #[derive(PartialEq, Debug, Clone, Copy, Default, Serialize)] 96 | pub struct Kline { 97 | pub t_open: i64, 98 | pub open: f32, 99 | pub high: f32, 100 | pub low: f32, 101 | pub close: f32, 102 | pub volume: f32, 103 | pub t_close: i64, 104 | pub quote_asset_volume: f32, 105 | pub number_of_trades: i64, 106 | pub taker_buy_base_asset_volume: f32, 107 | pub taker_buy_quote_asset_volume: f32, 108 | } 109 | 110 | impl Kline { 111 | fn from_kline_data(data: KlineData) -> Self { 112 | Kline { 113 | t_open: data.0, 114 | open: data.1.parse::().unwrap(), 115 | high: data.2.parse::().unwrap(), 116 | low: data.3.parse::().unwrap(), 117 | close: data.4.parse::().unwrap(), 118 | volume: data.5.parse::().unwrap(), 119 | t_close: data.6, 120 | quote_asset_volume: data.7.parse::().unwrap(), 121 | number_of_trades: data.8, 122 | taker_buy_base_asset_volume: data.9.parse::().unwrap(), 123 | taker_buy_quote_asset_volume: data.10.parse::().unwrap(), 124 | } 125 | } 126 | } 127 | 128 | impl Ord for Kline { 129 | fn cmp(&self, other: &Self) -> Ordering { 130 | if self.close < other.close { 131 | return Ordering::Less; 132 | } 133 | 134 | if self.close > other.close { 135 | return Ordering::Greater; 136 | } 137 | 138 | Ordering::Equal 139 | } 140 | } 141 | 142 | impl Eq for Kline {} 143 | 144 | impl PartialOrd for Kline { 145 | fn partial_cmp(&self, other: &Self) -> Option { 146 | self.close.partial_cmp(&other.close) 147 | } 148 | } 149 | 150 | impl Client { 151 | pub fn kline( 152 | symbol: String, 153 | interval: Interval, 154 | start_time: i64, 155 | limit: usize, 156 | ) -> Result, ClientError> { 157 | let url = format!("{}{}", BASE_URL, PATH_KLINE); 158 | let params = &[ 159 | ("symbol", symbol.as_str()), 160 | ("interval", interval.as_str()), 161 | ("startTime", &start_time.to_string()), 162 | ("limit", &limit.to_string()), 163 | ]; 164 | 165 | let resp = Rest::new().get_with_params(&url, params)?; 166 | 167 | debug!( 168 | "got status: {} and req weight per minute: {} and retry after: {}", 169 | resp.status(), 170 | resp.headers() 171 | .get(HEADER_REQ_WEIGHT) 172 | .unwrap() 173 | .to_str() 174 | .unwrap(), 175 | resp.headers() 176 | .get(HEADER_RETRY_AFTER) 177 | .unwrap_or(&HeaderValue::from_str("").unwrap()) 178 | .to_str() 179 | .unwrap(), 180 | ); 181 | 182 | let json_str = &resp.text()?; 183 | let res = serde_json::from_str::>(json_str)?; 184 | 185 | Ok(res.into_iter().map(Kline::from_kline_data).collect()) 186 | } 187 | 188 | pub fn info() -> Info { 189 | let url = format!("{}{}", BASE_URL, PATH_INFO); 190 | let resp = Rest::new().get(&url).unwrap(); 191 | debug!( 192 | "got status: {} and req weight per minute: {} and retry after: {}", 193 | resp.status(), 194 | resp.headers() 195 | .get(HEADER_REQ_WEIGHT) 196 | .unwrap() 197 | .to_str() 198 | .unwrap(), 199 | resp.headers() 200 | .get(HEADER_RETRY_AFTER) 201 | .unwrap_or(&HeaderValue::from_str("").unwrap()) 202 | .to_str() 203 | .unwrap() 204 | ); 205 | let json_str = &resp.text().unwrap(); 206 | serde_json::from_str::(json_str).unwrap() 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/sources/binance/errors.rs: -------------------------------------------------------------------------------- 1 | use quick_error::quick_error; 2 | 3 | quick_error! { 4 | #[derive(Debug)] 5 | pub enum ClientError { 6 | Reqwest(err: reqwest::Error) { 7 | from() 8 | display("{}", err) 9 | } 10 | Serialization(err: serde_json::Error) { 11 | from() 12 | display("{}", err) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/sources/binance/interval.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Copy)] 2 | pub enum Interval { 3 | Minute, 4 | Hour, 5 | Day, 6 | } 7 | 8 | impl Interval { 9 | pub fn as_str(&self) -> &str { 10 | match self { 11 | Interval::Minute => "1m", 12 | Interval::Hour => "1h", 13 | Interval::Day => "1d", 14 | } 15 | } 16 | } 17 | 18 | impl PartialEq for Interval { 19 | fn eq(&self, other: &Self) -> bool { 20 | format!("{:?}", self) == format!("{:?}", other) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/sources/binance/mod.rs: -------------------------------------------------------------------------------- 1 | mod client; 2 | mod interval; 3 | 4 | pub use self::client::*; 5 | pub use self::interval::*; 6 | 7 | pub mod errors; 8 | -------------------------------------------------------------------------------- /src/sources/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod binance; 2 | -------------------------------------------------------------------------------- /src/widgets/candles/bounds.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::{max, min, Ordering}; 2 | 3 | #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] 4 | pub struct Bounds(pub i64, pub i64); 5 | 6 | impl Bounds { 7 | /// merges 2 bounds into 1 bound if merge is possible 8 | pub fn union(&self, other: &Bounds) -> Option { 9 | if !(self.contains(other) 10 | || other.contains(self) 11 | || self.intersects(other) 12 | || self.neighbours(other)) 13 | { 14 | return None; 15 | } 16 | 17 | Some(Bounds(min(self.0, other.0), max(self.1, other.1))) 18 | } 19 | 20 | /// intersects 2 bounds into 1 bound if intersect is possible 21 | pub fn intersect(&self, other: &Bounds) -> Option { 22 | if !(self.intersects(other) || self.contains(other) || other.contains(self)) { 23 | return None; 24 | } 25 | 26 | Some(Bounds(max(self.0, other.0), min(self.1, other.1))) 27 | } 28 | 29 | pub fn len(&self) -> usize { 30 | (self.1 - self.0) as usize 31 | } 32 | 33 | pub fn subtract(&self, other: &Bounds) -> Option { 34 | if !self.intersects(other) { 35 | return Some(BoundsSet::new(vec![*self])); 36 | } 37 | 38 | if other.contains(self) { 39 | return None; 40 | } 41 | 42 | if self < other { 43 | return Some(BoundsSet::new(vec![Bounds(self.0, other.0 - 1)])); 44 | } 45 | 46 | if self > other { 47 | return Some(BoundsSet::new(vec![Bounds(other.1 + 1, self.1)])); 48 | } 49 | 50 | if self.contains(other) { 51 | let mut res = BoundsSet::new(vec![]); 52 | if let Some(left_b) = self.subtract(&Bounds(other.0, self.1)) { 53 | res = res.concat(&left_b); 54 | } 55 | 56 | if let Some(right_b) = self.subtract(&Bounds(self.0, other.1)) { 57 | res = res.concat(&right_b); 58 | } 59 | 60 | if res.len() == 0 { 61 | return None; 62 | } 63 | 64 | return Some(res); 65 | } 66 | 67 | None 68 | } 69 | 70 | fn contains(&self, other: &Bounds) -> bool { 71 | self.0 <= other.0 && other.1 <= self.1 72 | } 73 | 74 | fn intersects(&self, other: &Bounds) -> bool { 75 | other.contains(self) 76 | || self.0 <= other.0 && other.0 <= self.1 77 | || self.0 <= other.1 && other.1 <= self.1 78 | } 79 | 80 | fn neighbours(&self, other: &Bounds) -> bool { 81 | self.1 + 1 == other.0 || other.1 + 1 == self.0 82 | } 83 | } 84 | 85 | impl PartialOrd for Bounds { 86 | fn partial_cmp(&self, other: &Self) -> Option { 87 | if self.1 <= other.0 || (self.1 <= other.1 && self.0 <= other.0) { 88 | return Some(Ordering::Less); 89 | } else if other.1 <= self.0 || (other.1 <= self.1 && other.0 <= self.0) { 90 | return Some(Ordering::Greater); 91 | } 92 | 93 | Some(Ordering::Equal) 94 | } 95 | } 96 | 97 | impl Ord for Bounds { 98 | fn cmp(&self, other: &Self) -> Ordering { 99 | self.partial_cmp(other).unwrap() 100 | } 101 | } 102 | 103 | #[cfg(test)] 104 | mod bounds_tests { 105 | use super::*; 106 | 107 | #[test] 108 | fn test_eq() { 109 | assert_eq!(Bounds(1, 2), Bounds(1, 2)); 110 | assert_ne!(Bounds(1, 4), Bounds(2, 3)); 111 | assert_ne!(Bounds(1, 4), Bounds(2, 5)); 112 | assert_ne!(Bounds(1, 4), Bounds(5, 6)); 113 | } 114 | 115 | #[test] 116 | fn test_ord() { 117 | assert!(Bounds(1, 2) < Bounds(3, 4)); 118 | assert!(Bounds(1, 2) < Bounds(2, 3)); 119 | assert!(Bounds(1, 3) < Bounds(2, 3)); 120 | assert!(Bounds(1, 3) > Bounds(0, 3)); 121 | } 122 | 123 | #[test] 124 | fn test_union() { 125 | // containment 126 | assert_eq!(Bounds(3, 5).union(&Bounds(3, 4)), Some(Bounds(3, 5))); 127 | assert_eq!(Bounds(3, 5).union(&Bounds(2, 6)), Some(Bounds(2, 6))); 128 | 129 | // overlap 130 | assert_eq!(Bounds(3, 5).union(&Bounds(4, 6)), Some(Bounds(3, 6))); 131 | assert_eq!(Bounds(3, 5).union(&Bounds(2, 4)), Some(Bounds(2, 5))); 132 | 133 | // following 134 | assert_eq!(Bounds(3, 5).union(&Bounds(6, 7)), Some(Bounds(3, 7))); 135 | assert_eq!(Bounds(3, 5).union(&Bounds(1, 2)), Some(Bounds(1, 5))); 136 | 137 | // len = 1 138 | assert_eq!(Bounds(2, 2).union(&Bounds(3, 7)), Some(Bounds(2, 7))); 139 | assert_eq!(Bounds(2, 2).union(&Bounds(4, 7)), None); 140 | 141 | // no merge 142 | assert_eq!(Bounds(3, 5).union(&Bounds(8, 10)), None); 143 | assert_eq!(Bounds(3, 5).union(&Bounds(0, 1)), None); 144 | } 145 | 146 | #[test] 147 | fn test_intersects() { 148 | // containment 149 | assert_eq!(Bounds(3, 5).intersects(&Bounds(3, 4)), true); 150 | assert_eq!(Bounds(3, 5).intersects(&Bounds(2, 6)), true); 151 | 152 | // overlap 153 | assert_eq!(Bounds(3, 5).intersects(&Bounds(4, 6)), true); 154 | assert_eq!(Bounds(3, 5).intersects(&Bounds(2, 4)), true); 155 | 156 | // following 157 | assert_eq!(Bounds(3, 5).intersects(&Bounds(6, 7)), false); 158 | assert_eq!(Bounds(3, 5).intersects(&Bounds(1, 2)), false); 159 | 160 | // len = 1 161 | assert_eq!(Bounds(2, 2).intersects(&Bounds(3, 7)), false); 162 | assert_eq!(Bounds(2, 2).intersects(&Bounds(4, 7)), false); 163 | 164 | // no merge 165 | assert_eq!(Bounds(3, 5).intersects(&Bounds(8, 10)), false); 166 | assert_eq!(Bounds(3, 5).intersects(&Bounds(0, 1)), false); 167 | } 168 | 169 | #[test] 170 | fn test_intersect() { 171 | // containment 172 | assert_eq!(Bounds(3, 5).intersect(&Bounds(3, 4)), Some(Bounds(3, 4))); 173 | assert_eq!(Bounds(3, 5).intersect(&Bounds(2, 6)), Some(Bounds(3, 5))); 174 | 175 | // overlap 176 | assert_eq!(Bounds(3, 5).intersect(&Bounds(4, 6)), Some(Bounds(4, 5))); 177 | assert_eq!(Bounds(3, 5).intersect(&Bounds(2, 4)), Some(Bounds(3, 4))); 178 | 179 | // following 180 | assert_eq!(Bounds(3, 5).intersect(&Bounds(6, 7)), None); 181 | assert_eq!(Bounds(3, 5).intersect(&Bounds(1, 2)), None); 182 | 183 | // len = 1 184 | assert_eq!(Bounds(2, 2).intersect(&Bounds(3, 7)), None); 185 | assert_eq!(Bounds(2, 2).intersect(&Bounds(2, 7)), Some(Bounds(2, 2))); 186 | assert_eq!(Bounds(2, 2).intersect(&Bounds(2, 7)), Some(Bounds(2, 2))); 187 | 188 | // gap between 189 | assert_eq!(Bounds(3, 5).intersect(&Bounds(8, 10)), None); 190 | assert_eq!(Bounds(3, 5).intersect(&Bounds(0, 1)), None); 191 | } 192 | 193 | #[test] 194 | fn test_subtract() { 195 | // no relation 196 | assert_eq!( 197 | Bounds(2, 5).subtract(&Bounds(6, 6)), 198 | Some(BoundsSet::new(vec![Bounds(2, 5)])) 199 | ); 200 | 201 | // overlap 202 | assert_eq!( 203 | Bounds(2, 5).subtract(&Bounds(4, 6)), 204 | Some(BoundsSet::new(vec![Bounds(2, 3)])) 205 | ); 206 | assert_eq!( 207 | Bounds(2, 5).subtract(&Bounds(1, 3)), 208 | Some(BoundsSet::new(vec![Bounds(4, 5)])) 209 | ); 210 | assert_eq!( 211 | Bounds(2, 5).subtract(&Bounds(3, 5)), 212 | Some(BoundsSet::new(vec![Bounds(2, 2)])) 213 | ); 214 | 215 | // containment in other 216 | assert_eq!(Bounds(2, 5).subtract(&Bounds(1, 6)), None); 217 | assert_eq!(Bounds(2, 5).subtract(&Bounds(2, 5)), None); 218 | 219 | // containment in self 220 | assert_eq!( 221 | Bounds(2, 6).subtract(&Bounds(3, 4)), 222 | Some(BoundsSet::new(vec![Bounds(2, 2), Bounds(5, 6)])) 223 | ); 224 | assert_eq!( 225 | Bounds(2, 7).subtract(&Bounds(4, 5)), 226 | Some(BoundsSet::new(vec![Bounds(2, 3), Bounds(6, 7)])) 227 | ); 228 | assert_eq!( 229 | Bounds(0, 20).subtract(&Bounds(1, 10)), 230 | Some(BoundsSet::new(vec![Bounds(0, 0), Bounds(11, 20)])) 231 | ); 232 | } 233 | } 234 | 235 | #[derive(Default, Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] 236 | pub struct BoundsSet { 237 | vals: Vec, 238 | } 239 | 240 | impl BoundsSet { 241 | pub fn new(vals: Vec) -> Self { 242 | Self { vals } 243 | } 244 | 245 | pub fn vals(&self) -> Vec { 246 | self.vals.clone() 247 | } 248 | 249 | pub fn len(&self) -> usize { 250 | self.vals.len() 251 | } 252 | 253 | pub fn concat(&self, other: &Self) -> Self { 254 | let mut vals = self.vals.clone(); 255 | vals.extend_from_slice(&other.vals); 256 | Self { vals } 257 | } 258 | 259 | pub fn sort(&self) -> Self { 260 | let mut new_vals = self.vals.clone(); 261 | new_vals.sort(); 262 | 263 | Self { vals: new_vals } 264 | } 265 | 266 | pub fn merge_single(&self, o: Bounds) -> Self { 267 | self.merge(&BoundsSet::new(vec![o])) 268 | } 269 | 270 | pub fn left_edge(&self) -> Option { 271 | Some(self.vals.first()?.0) 272 | } 273 | 274 | /// Concats, sorts and unions 2 bounds sequences. 275 | pub fn merge(&self, other: &BoundsSet) -> Self { 276 | let mut new_vals = self.concat(other).vals; 277 | 278 | new_vals.sort(); 279 | 280 | Self { 281 | vals: new_vals.iter().fold(Vec::new(), |mut acc, v| { 282 | if acc.is_empty() { 283 | acc.push(*v); 284 | 285 | return acc; 286 | } 287 | 288 | let last = acc.last_mut().unwrap(); 289 | if let Some(union) = last.union(v) { 290 | *last = union; 291 | } else { 292 | acc.push(*v); 293 | } 294 | 295 | acc 296 | }), 297 | } 298 | } 299 | 300 | /// Computes self - other difference. 301 | pub fn subtract(&self, other: &BoundsSet) -> Option { 302 | if other.len() == 0 { 303 | return Some(self.clone()); 304 | } 305 | 306 | let mut res = BoundsSet::new(self.vals.clone()); 307 | other.vals.iter().for_each(|o| { 308 | let mut curr_vals = BoundsSet::new(vec![]); 309 | for i in 0..res.len() { 310 | if let Some(diff) = res.vals[i].subtract(o) { 311 | curr_vals = curr_vals.concat(&diff); 312 | } 313 | } 314 | res = curr_vals; 315 | }); 316 | 317 | if res.len() == 0 { 318 | return None; 319 | } 320 | 321 | Some(res) 322 | } 323 | } 324 | 325 | #[cfg(test)] 326 | mod bounds_sequence_tests { 327 | use super::*; 328 | 329 | #[test] 330 | fn test_sort() { 331 | assert_eq!( 332 | BoundsSet::new(vec![Bounds(6, 10), Bounds(3, 5), Bounds(1, 2)]).sort(), 333 | BoundsSet::new(vec![Bounds(1, 2), Bounds(3, 5), Bounds(6, 10)]) 334 | ); 335 | 336 | assert_eq!( 337 | BoundsSet::new(vec![Bounds(6, 10), Bounds(2, 8), Bounds(1, 2)]).sort(), 338 | BoundsSet::new(vec![Bounds(1, 2), Bounds(2, 8), Bounds(6, 10)]) 339 | ); 340 | } 341 | 342 | #[test] 343 | fn test_merge() { 344 | assert_eq!( 345 | BoundsSet::new(vec![Bounds(6, 10)]) 346 | .merge(&BoundsSet::new(vec![Bounds(3, 5), Bounds(1, 2)])), 347 | BoundsSet::new(vec![Bounds(1, 10)]) 348 | ); 349 | 350 | assert_eq!( 351 | BoundsSet::new(vec![Bounds(6, 10)]) 352 | .merge(&BoundsSet::new(vec![Bounds(2, 5), Bounds(1, 2)])), 353 | BoundsSet::new(vec![Bounds(1, 10)]) 354 | ); 355 | 356 | assert_eq!( 357 | BoundsSet::new(vec![Bounds(0, 1)]) 358 | .merge(&BoundsSet::new(vec![Bounds(3, 6), Bounds(4, 5)])), 359 | BoundsSet::new(vec![Bounds(0, 1), Bounds(3, 6)]) 360 | ); 361 | 362 | assert_eq!( 363 | BoundsSet::new(vec![Bounds(4, 5)]) 364 | .merge(&BoundsSet::new(vec![Bounds(0, 1), Bounds(3, 6)])), 365 | BoundsSet::new(vec![Bounds(0, 1), Bounds(3, 6)]) 366 | ); 367 | 368 | assert_eq!( 369 | BoundsSet::new(vec![Bounds(0, 0)]) 370 | .merge(&BoundsSet::new(vec![Bounds(1, 1), Bounds(2, 2)])), 371 | BoundsSet::new(vec![Bounds(0, 2)]) 372 | ); 373 | } 374 | 375 | #[test] 376 | fn test_diff() { 377 | // other is empty 378 | assert_eq!( 379 | BoundsSet::new(vec![Bounds(1, 10)]).subtract(&BoundsSet::new(vec![])), 380 | Some(BoundsSet::new(vec![Bounds(1, 10)])), 381 | ); 382 | 383 | // other and self are equal 384 | assert_eq!( 385 | BoundsSet::new(vec![Bounds(1, 10)]).subtract(&BoundsSet::new(vec![Bounds(1, 10)])), 386 | None, 387 | ); 388 | 389 | // standart 390 | assert_eq!( 391 | BoundsSet::new(vec![Bounds(0, 20)]).subtract(&BoundsSet::new(vec![Bounds(1, 10)])), 392 | Some(BoundsSet::new(vec![Bounds(0, 0), Bounds(11, 20)])), 393 | ); 394 | 395 | // standart2 396 | assert_eq!( 397 | BoundsSet::new(vec![Bounds(0, 20)]) 398 | .subtract(&BoundsSet::new(vec![Bounds(1, 10), Bounds(13, 15)])), 399 | Some(BoundsSet::new(vec![ 400 | Bounds(0, 0), 401 | Bounds(11, 12), 402 | Bounds(16, 20) 403 | ])), 404 | ); 405 | 406 | // standart3 407 | assert_eq!( 408 | BoundsSet::new(vec![Bounds(0, 20)]).subtract(&BoundsSet::new(vec![ 409 | Bounds(1, 10), 410 | Bounds(13, 15), 411 | Bounds(18, 19) 412 | ])), 413 | Some(BoundsSet::new(vec![ 414 | Bounds(0, 0), 415 | Bounds(11, 12), 416 | Bounds(16, 17), 417 | Bounds(20, 20) 418 | ])), 419 | ); 420 | } 421 | } 422 | -------------------------------------------------------------------------------- /src/widgets/candles/candles_drawer.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use crossbeam::channel::{unbounded, Sender}; 3 | use egui::{ 4 | plot::{BoxElem, BoxPlot, BoxSpread, Plot}, 5 | Color32, Stroke, 6 | }; 7 | use tracing::{error, info}; 8 | 9 | use crate::{netstrat::Drawer, sources::binance::Kline, widgets::AppWidget}; 10 | 11 | use super::{bounds::Bounds, data::Data}; 12 | 13 | const BOUNDS_SEND_DELAY_MILLIS: i64 = 300; 14 | 15 | #[derive(Clone)] 16 | pub struct CandlesDrawer { 17 | data: Data, 18 | val: Vec, 19 | bounds_pub: Sender, 20 | incremental_drag_diff: f32, 21 | last_time_drag_happened: DateTime, 22 | drag_happened: bool, 23 | bounds: Bounds, 24 | enabled: bool, 25 | } 26 | 27 | impl Default for CandlesDrawer { 28 | fn default() -> Self { 29 | let (s_bounds, _) = unbounded(); 30 | 31 | Self { 32 | data: Data::new_candle(), 33 | val: Default::default(), 34 | bounds_pub: s_bounds, 35 | last_time_drag_happened: Utc::now(), 36 | drag_happened: Default::default(), 37 | bounds: Bounds(0, 0), 38 | incremental_drag_diff: 0.0, 39 | enabled: true, 40 | } 41 | } 42 | } 43 | 44 | // TODO: refactor drawer trait to be more generalized 45 | impl Drawer for CandlesDrawer { 46 | fn update_image(&mut self, image: egui::ColorImage) { 47 | todo!() 48 | } 49 | 50 | fn has_unread_image(&self) -> bool { 51 | todo!() 52 | } 53 | } 54 | 55 | impl CandlesDrawer { 56 | pub fn new(bounds_pub: Sender) -> Self { 57 | Self { 58 | bounds_pub, 59 | ..Default::default() 60 | } 61 | } 62 | 63 | pub fn get_ordered_data(&self) -> Data { 64 | let mut res = self.data.clone(); 65 | res.vals.sort_by(|a, b| a.t_close.cmp(&b.t_close)); 66 | res 67 | } 68 | 69 | pub fn add_data(&mut self, vals: &mut Vec) { 70 | self.data.append(vals); 71 | self.val = self 72 | .data 73 | .vals 74 | .iter() 75 | .map(|k| -> BoxElem { 76 | BoxElem::new( 77 | (k.t_open + k.t_close) as f64 / 2.0, 78 | BoxSpread::new( 79 | k.low as f64, 80 | { 81 | match k.open > k.close { 82 | true => k.close as f64, 83 | false => k.open as f64, 84 | } 85 | }, 86 | k.open as f64, // we don't need to see median for candle 87 | { 88 | match k.open > k.close { 89 | true => k.open as f64, 90 | false => k.close as f64, 91 | } 92 | }, 93 | k.high as f64, 94 | ), 95 | ) 96 | .name(Data::format_ts(k.t_close as f64)) 97 | .stroke(Stroke::new(1.0, Data::k_color(k))) 98 | .fill(Data::k_color(k)) 99 | .whisker_width(0.0) 100 | .box_width((k.t_open - k.t_close) as f64 * 0.9) 101 | }) 102 | .collect(); 103 | } 104 | 105 | pub fn set_enabled(&mut self, enabled: bool) { 106 | self.enabled = enabled 107 | } 108 | 109 | pub fn clear(&mut self) { 110 | self.data = Data::new_candle(); 111 | } 112 | } 113 | 114 | impl AppWidget for CandlesDrawer { 115 | fn show(&mut self, ui: &mut egui::Ui) { 116 | if self.drag_happened 117 | && Utc::now() 118 | .signed_duration_since(self.last_time_drag_happened) 119 | .num_milliseconds() 120 | > BOUNDS_SEND_DELAY_MILLIS 121 | { 122 | let msg = self.bounds; 123 | let send_res = self.bounds_pub.send(msg); 124 | match send_res { 125 | Ok(_) => info!("sent bounds: {msg:?}"), 126 | Err(err) => error!("failed to send bounds: {err}"), 127 | } 128 | 129 | self.drag_happened = false; 130 | } 131 | ui.add_enabled_ui(self.enabled, |ui| { 132 | Plot::new("candles") 133 | .label_formatter(|_, v| -> String { Data::format_ts(v.x) }) 134 | .x_axis_formatter(|v, _range| Data::format_ts(v)) 135 | .show(ui, |plot_ui| { 136 | plot_ui.box_plot( 137 | BoxPlot::new(self.val.clone()) 138 | .element_formatter(Box::new(|el, _| -> String { 139 | format!( 140 | "open: {:.8}\nclose: {:.8}\nhigh: {:.8}\nlow: {:.8}\n{}", 141 | { 142 | match el.fill == Color32::LIGHT_RED { 143 | true => el.spread.quartile3, 144 | false => el.spread.quartile1, 145 | } 146 | }, 147 | { 148 | match el.fill == Color32::LIGHT_RED { 149 | true => el.spread.quartile1, 150 | false => el.spread.quartile3, 151 | } 152 | }, 153 | el.spread.upper_whisker, 154 | el.spread.lower_whisker, 155 | Data::format_ts(el.argument), 156 | ) 157 | })) 158 | .vertical(), 159 | ); 160 | 161 | let plot_bounds = plot_ui.plot_bounds(); 162 | self.bounds = Bounds(plot_bounds.min()[0] as i64, plot_bounds.max()[0] as i64); 163 | 164 | let drag_diff = plot_ui.pointer_coordinate_drag_delta().x; 165 | if drag_diff.abs() > 0.0 { 166 | self.incremental_drag_diff += drag_diff; 167 | 168 | // TODO: use step to count min drag diff 169 | if self.incremental_drag_diff > (60 * 1000 * 5) as f32 { 170 | self.drag_happened = true; 171 | self.last_time_drag_happened = Utc::now(); 172 | self.incremental_drag_diff = 0.0; 173 | } 174 | } 175 | 176 | plot_ui.ctx().request_repaint(); 177 | }) 178 | }); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/widgets/candles/data.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | 3 | use chrono::{DateTime, NaiveDateTime, Utc}; 4 | use egui::Color32; 5 | use tracing::debug; 6 | 7 | use crate::sources::binance::Kline; 8 | 9 | #[derive(Default, Clone)] 10 | pub struct Data { 11 | pub vals: Vec, 12 | max_x: f64, 13 | min_x: f64, 14 | max_y: f64, 15 | min_y: f64, 16 | max_vol: f64, 17 | } 18 | 19 | impl Data { 20 | pub fn new_candle() -> Self { 21 | Self { 22 | ..Default::default() 23 | } 24 | } 25 | 26 | pub fn append(&mut self, vals: &mut Vec) { 27 | self.vals.append(vals); 28 | self.compute_stats(); 29 | } 30 | 31 | pub fn max_x(&self) -> f64 { 32 | self.max_x 33 | } 34 | 35 | pub fn max_y(&self) -> f64 { 36 | self.max_y 37 | } 38 | 39 | pub fn min_y(&self) -> f64 { 40 | self.min_y 41 | } 42 | 43 | pub fn min_x(&self) -> f64 { 44 | self.min_x 45 | } 46 | 47 | pub fn max_vol(&self) -> f64 { 48 | self.max_vol 49 | } 50 | 51 | pub fn format_ts(ts: f64) -> String { 52 | let secs = (ts / 1000f64) as i64; 53 | let naive = NaiveDateTime::from_timestamp(secs, 0); 54 | let datetime: DateTime = DateTime::from_utc(naive, Utc); 55 | 56 | datetime.format("%Y-%m-%d %H:%M:%S").to_string() 57 | } 58 | 59 | pub fn k_color(k: &Kline) -> Color32 { 60 | match k.open > k.close { 61 | true => Color32::LIGHT_RED, 62 | false => Color32::LIGHT_GREEN, 63 | } 64 | } 65 | 66 | fn compute_stats(&mut self) { 67 | self.vals.sort_by_key(|el| el.t_close); 68 | 69 | self.max_y = self 70 | .vals 71 | .iter() 72 | .max_by(|l, r| { 73 | if l.high > r.high { 74 | return Ordering::Greater; 75 | } 76 | 77 | Ordering::Less 78 | }) 79 | .unwrap() 80 | .high as f64; 81 | 82 | self.min_y = self 83 | .vals 84 | .iter() 85 | .min_by(|l, r| { 86 | if l.low < r.low { 87 | return Ordering::Less; 88 | } 89 | 90 | Ordering::Greater 91 | }) 92 | .unwrap() 93 | .low as f64; 94 | 95 | self.max_x = self.vals.last().unwrap().t_close as f64; 96 | self.min_x = self.vals.first().unwrap().t_open as f64; 97 | 98 | debug!( 99 | "computed data props : max_x: {}, min_x: {}, max_y: {}, min_y: {}", 100 | self.max_x, self.min_x, self.max_y, self.min_y, 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/widgets/candles/error.rs: -------------------------------------------------------------------------------- 1 | use quick_error::quick_error; 2 | 3 | quick_error! { 4 | #[derive(Debug)] 5 | pub enum CandlesError { 6 | Error {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/widgets/candles/loading_state.rs: -------------------------------------------------------------------------------- 1 | use tracing::{debug, info}; 2 | 3 | use super::{ 4 | bounds::BoundsSet, 5 | pages::{Page, Pages}, 6 | }; 7 | 8 | #[derive(Default, Debug, Clone)] 9 | pub struct LoadingState { 10 | loaded_pages: usize, 11 | pages: Pages, 12 | curr_page: Page, 13 | } 14 | 15 | impl LoadingState { 16 | pub fn new(bounds: &BoundsSet, step: usize, per_page_limit: usize) -> Option { 17 | debug!("initializing LoadingState: bounds: {bounds:?}; step: {step}; per page limit: {per_page_limit}"); 18 | 19 | Some(Self { 20 | pages: Pages::new(bounds.clone(), step, per_page_limit)?, 21 | ..Default::default() 22 | }) 23 | } 24 | 25 | pub fn left_edge(&self) -> i64 { 26 | self.curr_page.0 27 | } 28 | 29 | pub fn get_next_page(&mut self) -> Option { 30 | let res = self.pages.next(); 31 | if let Some(p) = res { 32 | self.curr_page = p.clone(); 33 | return Some(p); 34 | }; 35 | 36 | None 37 | } 38 | 39 | pub fn inc_loaded_pages(&mut self, cnt: usize) { 40 | self.loaded_pages += cnt; 41 | info!( 42 | "inced pages: {cnt}; {}/{}", 43 | self.loaded_pages, 44 | self.pages.len() 45 | ); 46 | } 47 | 48 | pub fn progress(&mut self) -> f32 { 49 | if self.pages.len() == 0 { 50 | return 1.0; 51 | } 52 | 53 | self.loaded_pages as f32 / self.pages.len() as f32 54 | } 55 | 56 | pub fn page_size(&self) -> usize { 57 | self.pages.page_size(self.curr_page.clone()) 58 | } 59 | 60 | pub fn pages(&self) -> usize { 61 | self.pages.len() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/widgets/candles/mod.rs: -------------------------------------------------------------------------------- 1 | mod bounds; 2 | mod candles_drawer; 3 | mod data; 4 | mod error; 5 | mod loading_state; 6 | mod pages; 7 | mod props; 8 | mod state; 9 | mod symbols; 10 | mod time_input; 11 | mod time_range; 12 | mod time_range_settings; 13 | 14 | pub use self::props::Props; 15 | pub use self::symbols::Symbols; 16 | pub use self::time_input::TimeInput; 17 | pub use self::time_range::TimeRange; 18 | -------------------------------------------------------------------------------- /src/widgets/candles/pages.rs: -------------------------------------------------------------------------------- 1 | use tracing::{debug, error}; 2 | 3 | use super::bounds::BoundsSet; 4 | 5 | #[derive(Debug, Default, Clone, PartialEq)] 6 | pub struct Page(pub i64, pub i64); 7 | 8 | #[derive(Debug, Default, Clone, PartialEq)] 9 | pub struct Pages { 10 | curr_page_idx: usize, 11 | vals: Vec, 12 | step: usize, 13 | } 14 | 15 | impl Pages { 16 | /// Creates a new Pages instance. 17 | /// 18 | /// Page is a pair of start and end 19 | /// where the start is included in the range and the end is not. 20 | pub fn new(bounds: BoundsSet, step: usize, limit: usize) -> Option { 21 | debug!("initializing new pages; bounds: {bounds:?}; step: {step}; limit: {limit}"); 22 | 23 | if step < 1 { 24 | error!("invalid step. Step must be greater than 0"); 25 | 26 | return None; 27 | } 28 | 29 | let mut vals = vec![]; 30 | bounds.vals().iter_mut().for_each(|b| { 31 | if b.len() <= limit * step as usize { 32 | debug!("not splitting bounds to pages; bounds: {b:?}; step: {step}"); 33 | 34 | vals.push(Page(b.0, b.1)); 35 | return; 36 | } 37 | 38 | debug!("iterating inside bounds constructing pages: bounds: {b:?}; step: {step}"); 39 | 40 | let mut page_start = b.0; 41 | loop { 42 | let mut page_end = page_start + (step * limit) as i64; 43 | if page_end > b.1 { 44 | page_end = b.1; 45 | } 46 | 47 | vals.push(Page(page_start, page_end)); 48 | if page_end == b.1 { 49 | break; 50 | } 51 | page_start = page_end; 52 | } 53 | }); 54 | 55 | debug!("computed pages: {vals:?}"); 56 | 57 | Some(Self { 58 | vals, 59 | step, 60 | ..Default::default() 61 | }) 62 | } 63 | 64 | pub fn len(&self) -> usize { 65 | self.vals.len() 66 | } 67 | 68 | pub fn next(&mut self) -> Option { 69 | if let Some(page) = self.vals.get(self.curr_page_idx) { 70 | self.curr_page_idx += 1; 71 | return Some(page.clone()); 72 | } 73 | 74 | None 75 | } 76 | 77 | pub fn page_size(&self, page: Page) -> usize { 78 | ((page.1 - page.0) / self.step as i64) as usize 79 | } 80 | } 81 | 82 | #[cfg(test)] 83 | mod pages_tests { 84 | use crate::widgets::candles::bounds::Bounds; 85 | 86 | use super::*; 87 | 88 | #[test] 89 | fn test_pages_new() { 90 | let pages_res = Pages::new(BoundsSet::new(vec![Bounds(0, 50), Bounds(60, 150)]), 1, 50); 91 | assert_ne!(pages_res, None); 92 | assert_eq!( 93 | pages_res.unwrap(), 94 | Pages { 95 | vals: vec![Page(0, 50), Page(60, 110), Page(110, 150)], 96 | step: 1, 97 | ..Default::default() 98 | } 99 | ); 100 | 101 | let pages_res = Pages::new(BoundsSet::new(vec![Bounds(0, 50), Bounds(60, 150)]), 2, 25); 102 | assert_ne!(pages_res, None); 103 | assert_eq!( 104 | pages_res.unwrap(), 105 | Pages { 106 | vals: vec![Page(0, 50), Page(60, 110), Page(110, 150)], 107 | step: 2, 108 | ..Default::default() 109 | } 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/widgets/candles/props.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::path::Path; 3 | use std::sync::{Arc, Mutex}; 4 | use std::time::Duration; 5 | 6 | use chrono::{Date, NaiveDateTime, Utc}; 7 | use crossbeam::channel::{unbounded, Receiver, Sender}; 8 | use egui::{CentralPanel, ProgressBar, TopBottomPanel, Ui}; 9 | use egui_notify::{Anchor, Toasts}; 10 | use tracing::{debug, error, info, trace}; 11 | 12 | use crate::netstrat::{Drawer, ThreadPool}; 13 | use crate::sources::binance::{Client, Kline}; 14 | use crate::widgets::candles::bounds::BoundsSet; 15 | use crate::widgets::AppWidget; 16 | 17 | use super::bounds::Bounds; 18 | use super::candles_drawer::CandlesDrawer; 19 | use super::error::CandlesError; 20 | use super::state::State; 21 | use super::time_range_settings::TimeRangeSettings; 22 | use super::TimeRange; 23 | 24 | const THREAD_POOL_SIZE: usize = 15; 25 | 26 | #[derive(Default)] 27 | struct ExportState { 28 | triggered: bool, 29 | } 30 | 31 | pub struct Props { 32 | time_range: Box, 33 | 34 | candles: CandlesDrawer, 35 | symbol: String, 36 | 37 | max_frame_pages: usize, 38 | data_changed: bool, 39 | state: State, 40 | export_state: ExportState, 41 | 42 | toasts: Toasts, 43 | 44 | pool: ThreadPool, 45 | 46 | klines_pub: Sender, CandlesError>>, 47 | klines_sub: Receiver, CandlesError>>, 48 | drawer_pub: Sender>>>, 49 | symbol_pub: Sender, 50 | symbol_sub: Receiver, 51 | props_pub: Sender, 52 | props_sub: Receiver, 53 | export_sub: Receiver, 54 | drag_sub: Receiver, 55 | } 56 | 57 | impl Default for Props { 58 | fn default() -> Self { 59 | let max_frame_pages = 50; 60 | let toasts = Toasts::default().with_anchor(Anchor::TopRight); 61 | 62 | let (s_symbols, r_symbols) = unbounded(); 63 | let (s_props, r_props) = unbounded(); 64 | let (s_props1, r_props1) = unbounded(); 65 | let (s_export, r_export) = unbounded(); 66 | let (s_klines, r_klines) = unbounded(); 67 | let (s_bounds, r_bounds) = unbounded(); 68 | let (s_drawer, _) = unbounded(); 69 | 70 | let time_range_chooser = Box::new(TimeRange::new( 71 | r_symbols.clone(), 72 | s_props, 73 | r_props1, 74 | s_export, 75 | TimeRangeSettings::default(), 76 | )); 77 | 78 | let candles = CandlesDrawer::new(s_bounds); 79 | 80 | let pool = ThreadPool::new(THREAD_POOL_SIZE); 81 | 82 | Self { 83 | max_frame_pages, 84 | 85 | time_range: time_range_chooser, 86 | 87 | candles, 88 | 89 | pool, 90 | 91 | toasts, 92 | 93 | symbol_sub: r_symbols, 94 | symbol_pub: s_symbols, 95 | drawer_pub: s_drawer, 96 | props_sub: r_props, 97 | props_pub: s_props1, 98 | export_sub: r_export, 99 | drag_sub: r_bounds, 100 | klines_sub: r_klines, 101 | klines_pub: s_klines, 102 | 103 | data_changed: Default::default(), 104 | symbol: Default::default(), 105 | state: Default::default(), 106 | export_state: Default::default(), 107 | } 108 | } 109 | } 110 | 111 | impl Props { 112 | pub fn new( 113 | symbol_sub: Receiver, 114 | drawer_pub: Sender>>>, 115 | ) -> Self { 116 | info!("initing widget graph"); 117 | Self { 118 | symbol_sub, 119 | drawer_pub, 120 | ..Default::default() 121 | } 122 | } 123 | 124 | fn update_data(&mut self, klines: &mut Vec) { 125 | info!( 126 | "adding {} entries to volume and candles widgets", 127 | klines.len() 128 | ); 129 | 130 | self.candles.add_data(klines); 131 | 132 | self.data_changed = true; 133 | } 134 | 135 | fn start_download(&mut self, props: TimeRangeSettings, reset_state: bool) { 136 | if reset_state { 137 | self.state = State::default(); 138 | } 139 | 140 | self.state.apply_props(&props); 141 | 142 | if self.state.loading.pages() == 0 { 143 | debug!("data already downloaded, skipping download"); 144 | return; 145 | } 146 | 147 | debug!( 148 | "data splitted in {} pages; starting download...", 149 | self.state.loading.pages() 150 | ); 151 | 152 | self.perform_data_request(); 153 | } 154 | 155 | fn perform_data_request(&mut self) { 156 | while self.state.loading.get_next_page().is_some() { 157 | let start_time = self.state.loading.left_edge(); 158 | let interval = self.state.props.interval; 159 | let limit = self.state.loading.page_size(); 160 | let symbol = self.symbol.to_string(); 161 | 162 | let sender = Mutex::new(self.klines_pub.clone()); 163 | self.pool.execute(move || { 164 | debug!("executing klines request: symbol: {symbol}, t_start: {start_time}, limit: {limit}"); 165 | let data = Client::kline(symbol, interval, start_time, limit); 166 | let res = match data { 167 | Ok(payload) => { 168 | Ok(payload) 169 | } 170 | Err(err) => { 171 | error!("got klines result with error: {err}"); 172 | Err(CandlesError::Error) 173 | } 174 | }; 175 | 176 | let send_res = sender.lock().unwrap().send(res); 177 | if let Err(err) = send_res { 178 | error!("failed to send klines to channel: {err}"); 179 | }; 180 | }); 181 | } 182 | } 183 | 184 | fn export_data(&mut self) { 185 | debug!("exporting data"); 186 | 187 | let name = format!( 188 | "{}_{}_{}_{:?}.csv", 189 | self.symbol, 190 | self.state.props.start_time().timestamp(), 191 | self.state.props.end_time().timestamp(), 192 | self.state.props.interval, 193 | ); 194 | 195 | let path = Path::new(&name); 196 | let f_res = File::create(path); 197 | if let Err(err) = f_res { 198 | error!("failed to create file with error: {err}"); 199 | return; 200 | } 201 | 202 | let abs_path = path.canonicalize().unwrap(); 203 | debug!("saving to file: {}", abs_path.display()); 204 | 205 | let mut wtr = csv::Writer::from_writer(f_res.unwrap()); 206 | 207 | let data = self.candles.get_ordered_data().vals; 208 | data.iter().for_each(|el| { 209 | wtr.serialize(el).unwrap(); 210 | }); 211 | 212 | if let Some(err) = wtr.flush().err() { 213 | error!("failed to write to file with error: {err}"); 214 | } else { 215 | self.toasts 216 | .success("File exported") 217 | .set_duration(Some(Duration::from_secs(3))); 218 | info!("exported to file: {abs_path:?}"); 219 | } 220 | 221 | self.export_state.triggered = false; 222 | } 223 | 224 | fn update(&mut self) { 225 | let drag_wrapped = self.drag_sub.recv_timeout(Duration::from_millis(1)); 226 | if let Ok(bounds) = drag_wrapped { 227 | debug!("got bounds: {bounds:?}"); 228 | 229 | let mut props = self.state.props.clone(); 230 | 231 | let dt_left = NaiveDateTime::from_timestamp((bounds.0 as f64 / 1000.0) as i64, 0); 232 | props.bounds = BoundsSet::new(vec![bounds]); 233 | props.date_start = Date::from_utc(dt_left.date(), Utc); 234 | props.time_start = dt_left.time(); 235 | 236 | let dt_right = NaiveDateTime::from_timestamp((bounds.1 as f64 / 1000.0) as i64, 0); 237 | props.bounds = BoundsSet::new(vec![bounds]); 238 | props.date_end = Date::from_utc(dt_right.date(), Utc); 239 | props.time_end = dt_right.time(); 240 | 241 | let send_result = self.props_pub.send(props.clone()); 242 | match send_result { 243 | Ok(_) => { 244 | debug!("sent props: {props:?}"); 245 | } 246 | Err(err) => { 247 | error!("failed to send props: {err}"); 248 | } 249 | } 250 | 251 | self.start_download(props, false); 252 | } 253 | 254 | let export_wrapped = self.export_sub.recv_timeout(Duration::from_millis(1)); 255 | if let Ok(props) = export_wrapped { 256 | debug!("got export msg: {props:?}"); 257 | 258 | self.export_state.triggered = true; 259 | 260 | self.candles.clear(); 261 | self.start_download(props, true); 262 | } 263 | 264 | let symbol_wrapped = self.symbol_sub.recv_timeout(Duration::from_millis(1)); 265 | if let Ok(symbol) = symbol_wrapped { 266 | debug!("got symbol: {symbol}"); 267 | 268 | self.symbol = symbol.clone(); 269 | self.symbol_pub.send(symbol).unwrap(); 270 | 271 | self.candles.clear(); 272 | 273 | self.start_download(TimeRangeSettings::default(), true); 274 | } 275 | 276 | let show_wrapped = self.props_sub.recv_timeout(Duration::from_millis(1)); 277 | if let Ok(props) = show_wrapped { 278 | debug!("got show button pressed: {props:?}"); 279 | 280 | self.start_download(props, true); 281 | } 282 | 283 | let mut got = 0; 284 | let mut res = vec![]; 285 | let mut has_error = false; 286 | loop { 287 | if got == self.max_frame_pages { 288 | break; 289 | } 290 | 291 | let package_res = self.klines_sub.recv_timeout(Duration::from_millis(1)); 292 | if package_res.is_err() { 293 | break; 294 | } 295 | 296 | let klines_res = package_res.unwrap(); 297 | match klines_res { 298 | Ok(klines) => klines.iter().for_each(|k| { 299 | res.push(*k); 300 | }), 301 | Err(_) => { 302 | has_error = true; 303 | } 304 | } 305 | 306 | got += 1; 307 | } 308 | 309 | if has_error { 310 | self.toasts.error("Failed to get candles from Binance"); 311 | } 312 | 313 | if got > 0 { 314 | trace!("received {} pages of data", got); 315 | self.state.loading.inc_loaded_pages(got); 316 | self.update_data(&mut res); 317 | } 318 | 319 | if self.state.loading.progress() == 1.0 && self.export_state.triggered { 320 | self.export_data(); 321 | } 322 | 323 | self.candles 324 | .set_enabled(self.state.loading.progress() == 1.0); 325 | } 326 | 327 | fn draw_data(&mut self, ui: &Ui) { 328 | if self.data_changed { 329 | ui.ctx().request_repaint(); 330 | self.drawer_pub 331 | .send(Arc::new(Mutex::new(Box::new(self.candles.clone())))) 332 | .unwrap(); 333 | self.data_changed = false; 334 | } 335 | } 336 | } 337 | 338 | impl AppWidget for Props { 339 | fn show(&mut self, ui: &mut Ui) { 340 | self.update(); 341 | 342 | self.draw_data(ui); 343 | 344 | if self.symbol.is_empty() { 345 | ui.label("Select a symbol"); 346 | return; 347 | } 348 | 349 | TopBottomPanel::top("graph_toolbar").show_inside(ui, |ui| { 350 | ui.horizontal(|ui| { 351 | if self.state.loading.progress() < 1.0 { 352 | ui.add( 353 | ProgressBar::new(self.state.loading.progress()) 354 | .show_percentage() 355 | .animate(true), 356 | ); 357 | } 358 | }); 359 | }); 360 | 361 | CentralPanel::default().show_inside(ui, |ui| { 362 | self.time_range.show(ui); 363 | }); 364 | 365 | self.toasts.show(ui.ctx()); 366 | } 367 | } 368 | -------------------------------------------------------------------------------- /src/widgets/candles/state.rs: -------------------------------------------------------------------------------- 1 | use tracing::{debug, error}; 2 | 3 | use crate::sources::binance::Interval; 4 | 5 | use super::{ 6 | bounds::BoundsSet, loading_state::LoadingState, time_range_settings::TimeRangeSettings, 7 | }; 8 | 9 | #[derive(Default, Debug, Clone)] 10 | pub struct State { 11 | pub loading: LoadingState, 12 | pub props: TimeRangeSettings, 13 | bounds: BoundsSet, 14 | } 15 | 16 | impl State { 17 | pub fn apply_props(&mut self, props: &TimeRangeSettings) { 18 | debug!("applying new props: {props:?}"); 19 | 20 | self.props = props.clone(); 21 | 22 | let subtract_res = props.bounds.subtract(&self.bounds); 23 | if subtract_res.is_none() { 24 | debug!("found nothing to load"); 25 | self.loading = LoadingState::default(); 26 | return; 27 | } 28 | let to_load = subtract_res.unwrap(); 29 | debug!("computed difference to load: {to_load:?}"); 30 | 31 | let loading_res = LoadingState::new(&to_load, State::step(props.interval), props.limit); 32 | if loading_res.is_none() { 33 | error!("failed to initialize loading state"); 34 | return; 35 | } 36 | let loading = loading_res.unwrap(); 37 | debug!("initialized loading state: {loading:?}"); 38 | 39 | let new_bounds = self.bounds.merge(&props.bounds); 40 | debug!("computed new_bounds: {new_bounds:?}"); 41 | 42 | self.loading = loading; 43 | self.bounds = new_bounds; 44 | self.props = props.clone(); 45 | } 46 | 47 | fn step(i: Interval) -> usize { 48 | match i { 49 | Interval::Minute => 60 * 1000, 50 | Interval::Hour => 60 * 60 * 1000, 51 | Interval::Day => 60 * 60 * 24 * 1000, 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/widgets/candles/symbols.rs: -------------------------------------------------------------------------------- 1 | use crossbeam::channel::{unbounded, Sender}; 2 | use egui::{Layout, ScrollArea, TextEdit, WidgetText}; 3 | use poll_promise::Promise; 4 | use tracing::{debug, error, info}; 5 | 6 | use crate::{ 7 | netstrat::line_filter_highlight_layout, 8 | sources::binance::{Client, Info, Symbol}, 9 | widgets::AppWidget, 10 | }; 11 | 12 | #[derive(Default)] 13 | struct FilterProps { 14 | value: String, 15 | active_only: bool, 16 | } 17 | 18 | pub struct Symbols { 19 | symbols: Vec, 20 | filter: FilterProps, 21 | filtered: Vec, 22 | loading: bool, 23 | selected_symbol: String, 24 | symbols_promise: Option>, 25 | symbol_pub: Sender, 26 | } 27 | 28 | impl Default for Symbols { 29 | fn default() -> Self { 30 | let (s, _) = unbounded(); 31 | let symbols_promise = Some(Promise::spawn_blocking(Client::info)); 32 | let loading = true; 33 | Self { 34 | loading, 35 | symbols_promise, 36 | symbol_pub: s, 37 | symbols: Default::default(), 38 | filter: Default::default(), 39 | filtered: Default::default(), 40 | selected_symbol: Default::default(), 41 | } 42 | } 43 | } 44 | 45 | impl Symbols { 46 | pub fn new(symbol_pub: Sender) -> Self { 47 | info!("initing widget symbols"); 48 | Self { 49 | symbol_pub, 50 | ..Default::default() 51 | } 52 | } 53 | 54 | fn update(&mut self, filter_value: String, active_only: bool, selected_symbol: String) { 55 | self.apply_filter(filter_value, active_only); 56 | 57 | if self.selected_symbol != selected_symbol { 58 | info!("setting symbol to {selected_symbol}"); 59 | self.selected_symbol = selected_symbol; 60 | } 61 | } 62 | 63 | fn apply_filter(&mut self, filter_value: String, active_only: bool) { 64 | let filter_normalized = filter_value.to_lowercase(); 65 | if filter_normalized == self.filter.value && active_only == self.filter.active_only { 66 | return; 67 | } 68 | 69 | info!("applying filter: {filter_value}; active_only: {active_only}"); 70 | 71 | if filter_normalized != self.filter.value 72 | && filter_normalized.contains(self.filter.value.as_str()) 73 | && self.filter.active_only == active_only 74 | { 75 | debug!("using optimized version"); 76 | 77 | self.filtered.retain(|el| { 78 | el.symbol 79 | .to_lowercase() 80 | .contains(filter_normalized.as_str()) 81 | }); 82 | } else { 83 | debug!("using heavy version"); 84 | 85 | self.filtered = self 86 | .symbols 87 | .iter() 88 | .filter(|el| { 89 | el.symbol 90 | .to_lowercase() 91 | .contains(filter_normalized.as_str()) 92 | }) 93 | .cloned() 94 | .collect(); 95 | } 96 | 97 | if active_only != self.filter.active_only && active_only { 98 | self.filtered.retain(|el| el.active() == active_only); 99 | } 100 | 101 | self.filter.value = filter_normalized; 102 | self.filter.active_only = active_only; 103 | } 104 | } 105 | 106 | impl AppWidget for Symbols { 107 | fn show(&mut self, ui: &mut egui::Ui) { 108 | let mut filter_value = self.filter.value.clone(); 109 | let mut active_only = self.filter.active_only; 110 | let mut selected_symbol = self.selected_symbol.clone(); 111 | 112 | if let Some(promise) = &self.symbols_promise { 113 | if let Some(result) = promise.ready() { 114 | self.loading = false; 115 | 116 | self.symbols = result.symbols.to_vec(); 117 | self.filtered = result.symbols.to_vec(); 118 | 119 | self.symbols_promise = None; 120 | } 121 | } 122 | 123 | if self.loading { 124 | ui.centered_and_justified(|ui| { 125 | ui.spinner(); 126 | }); 127 | return; 128 | } 129 | 130 | ui.with_layout(Layout::top_down(egui::Align::LEFT), |ui| { 131 | ui.add( 132 | TextEdit::singleline(&mut filter_value) 133 | .hint_text(WidgetText::from("filter symbols").italics()), 134 | ); 135 | 136 | ui.with_layout(Layout::top_down(egui::Align::RIGHT), |ui| { 137 | ui.checkbox(&mut active_only, "active only"); 138 | ui.label( 139 | WidgetText::from(format!("{}/{}", self.filtered.len(), self.symbols.len())) 140 | .small(), 141 | ); 142 | }); 143 | 144 | ui.add_space(5f32); 145 | 146 | ScrollArea::vertical() 147 | .auto_shrink([false; 2]) 148 | .max_height(ui.available_height()) 149 | .show(ui, |ui| { 150 | ui.with_layout(Layout::top_down(egui::Align::LEFT), |ui| { 151 | self.filtered.iter().for_each(|s| { 152 | let label = ui.selectable_label( 153 | s.symbol == selected_symbol, 154 | WidgetText::from(line_filter_highlight_layout( 155 | ui, 156 | &s.symbol, 157 | &self.filter.value, 158 | !s.active(), 159 | )), 160 | ); 161 | 162 | if label.clicked() { 163 | let send_result = self.symbol_pub.send(s.symbol.clone()); 164 | match send_result { 165 | Ok(_) => { 166 | debug!("sent symbol: {}", s.symbol); 167 | } 168 | Err(err) => { 169 | error!("failed to send symbol: {err}"); 170 | } 171 | } 172 | 173 | selected_symbol = s.symbol.clone(); 174 | }; 175 | }); 176 | }) 177 | }); 178 | }); 179 | 180 | self.update(filter_value, active_only, selected_symbol); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/widgets/candles/time_input.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use chrono::NaiveTime; 4 | use egui::widgets::{TextEdit, Widget}; 5 | use egui::Color32; 6 | use tracing::info; 7 | 8 | /// Time hold value for hours, minutes and seconds validating them. 9 | #[derive(Debug, Default, Clone, Copy, PartialEq)] 10 | struct Time { 11 | hours: u32, 12 | minutes: u32, 13 | seconds: u32, 14 | } 15 | 16 | impl Time { 17 | pub fn new(hours: u32, minutes: u32, seconds: u32) -> Option { 18 | let t = Self { 19 | hours, 20 | minutes, 21 | seconds, 22 | }; 23 | 24 | if !t.valid() { 25 | return None; 26 | } 27 | 28 | Some(t) 29 | } 30 | 31 | pub fn valid(&self) -> bool { 32 | self.hours < 24 && self.minutes < 60 && self.seconds < 60 33 | } 34 | } 35 | 36 | impl Display for Time { 37 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 38 | f.write_str(format!("{}:{}:{}", self.hours, self.minutes, self.seconds).as_str()) 39 | } 40 | } 41 | 42 | // TimeInput is a widget that allows the user to enter a time. 43 | #[derive(Default)] 44 | pub struct TimeInput { 45 | time: Time, 46 | val: String, 47 | valid: bool, 48 | } 49 | 50 | impl TimeInput { 51 | pub fn new(hours: u32, minutes: u32, seconds: u32) -> Self { 52 | let t = Time::new(hours, minutes, seconds); 53 | match t { 54 | Some(time) => Self { 55 | time, 56 | val: format!("{}", time), 57 | valid: true, 58 | }, 59 | None => Default::default(), 60 | } 61 | } 62 | 63 | /// Returns chrono::NaiveTime from the time input. 64 | pub fn get_time(&self) -> Option { 65 | if !self.valid { 66 | info!("faield to parse time from val: {}", self.val); 67 | return None; 68 | } 69 | 70 | let time = self.time; 71 | info!("parsed time: {time}"); 72 | Some(NaiveTime::from_hms(time.hours, time.minutes, time.seconds)) 73 | } 74 | 75 | fn parse_val(&mut self) -> Option