瀏覽代碼

add depth bars showing current orderbook, +avoided unnecessary time conversions

Berke 1 年之前
父節點
當前提交
6cb4c0ddf5
共有 1 個文件被更改,包括 102 次插入59 次删除
  1. 102 59
      src/charts/heatmap.rs

+ 102 - 59
src/charts/heatmap.rs

@@ -1,7 +1,7 @@
 use std::collections::{BTreeMap, HashMap, VecDeque};
-use chrono::{DateTime, Utc, TimeZone, LocalResult, Duration, NaiveDateTime, Timelike};
+use chrono::NaiveDateTime;
 use iced::{
-    alignment, color, mouse, widget::{button, canvas::{self, event::{self, Event}, path, stroke::Stroke, Cache, Canvas, Geometry, Path}}, window, Border, Color, Element, Length, Point, Rectangle, Renderer, Size, Theme, Vector
+    advanced::graphics::core::time, alignment, color, mouse, widget::{button, canvas::{self, event::{self, Event}, path, stroke::Stroke, Cache, Canvas, Geometry, Path}}, window, Border, Color, Element, Length, Point, Rectangle, Renderer, Size, Theme, Vector
 };
 use iced::widget::{Column, Row, Container, Text};
 use crate::data_providers::binance::market_data::Trade;
@@ -27,8 +27,8 @@ pub struct Heatmap {
     translation: Vector,
     scaling: f32,
     
-    data_points: VecDeque<(DateTime<Utc>, f32, f32, bool)>,
-    depth: VecDeque<(DateTime<Utc>, Vec<(f32, f32)>, Vec<(f32, f32)>)>,
+    data_points: VecDeque<(i64, f32, f32, bool)>,
+    depth: VecDeque<(i64, Vec<(f32, f32)>, Vec<(f32, f32)>)>,
     size_filter: f32,
 
     autoscale: bool,
@@ -80,23 +80,18 @@ impl Heatmap {
     }
 
     pub fn insert_datapoint(&mut self, mut trades_buffer: Vec<Trade>, depth_update: u64, 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: DateTime<Utc> = match Utc.timestamp_opt(seconds, nanoseconds) {
-            LocalResult::Single(dt) => dt,
-            _ => return, 
-        };
-
+        let aggregate_time = 100; // 100 ms
+        let rounded_depth_update = ((depth_update / aggregate_time) * aggregate_time) as i64;
+        
         for trade in trades_buffer.drain(..) {
-            self.data_points.push_back((depth_update_time, trade.price, trade.qty, trade.is_sell));
+            self.data_points.push_back((rounded_depth_update, trade.price, trade.qty, trade.is_sell));
         }
         if let Some((time, _, _)) = self.depth.back() {
-            if *time == depth_update_time {
+            if *time == rounded_depth_update {
                 self.depth.pop_back();
             }
         }
-        self.depth.push_back((depth_update_time, bids, asks));
+        self.depth.push_back((rounded_depth_update, bids, asks));
 
         while self.data_points.len() > 6000 {
             self.data_points.pop_front();
@@ -108,18 +103,20 @@ impl Heatmap {
         self.render_start();
     }
     
-    pub fn render_start(&mut self) {
-        let timestamp_now = Utc::now().timestamp_millis();
-    
-        let latest: i64 = timestamp_now - ((self.translation.x*100.0)*(self.timeframe as f32)) as i64;
+    pub fn render_start(&mut self) {    
+        let timestamp_latest = match self.depth.back() {
+            Some((time, _, _)) => *time,
+            None => return,
+        };
+
+        let latest: i64 = timestamp_latest as i64 - ((self.translation.x*100.0)*(self.timeframe as f32)) as i64;
         let earliest: i64 = latest - ((64000.0*self.timeframe as f32) / (self.scaling / (self.bounds.width/800.0))) as i64;
     
         let mut highest: f32 = 0.0;
         let mut lowest: f32 = std::f32::MAX;
     
         for (time, bids, asks) in &self.depth {
-            let timestamp = time.timestamp_millis();
-            if timestamp >= earliest && timestamp <= latest {
+            if *time >= earliest && *time <= latest {
                 if let Some(max_price) = asks.iter().map(|(price, _)| price).max_by(|a, b| a.partial_cmp(b).unwrap()) {
                     highest = highest.max(*max_price);
                 }
@@ -443,8 +440,7 @@ impl canvas::Program<Message> for Heatmap {
         let heatmap = self.heatmap_cache.draw(renderer, bounds.size(), |frame| {
             let (filtered_visible_trades, visible_trades) = self.data_points.iter()
                 .filter(|(time, _, _, _)| {
-                    let timestamp = time.timestamp_millis();
-                    timestamp >= earliest && timestamp <= latest
+                    *time >= earliest && *time <= latest
                 })
                 .fold((vec![], vec![]), |(mut filtered, mut visible), trade| {
                     visible.push(*trade);
@@ -457,8 +453,7 @@ impl canvas::Program<Message> for Heatmap {
             // volume bars
             let mut aggregated_volumes: HashMap<i64, (f32, f32)> = HashMap::new();
             for &(time, _, qty, is_sell) in &visible_trades {
-                let timestamp = time.timestamp_millis();
-                aggregated_volumes.entry(timestamp).and_modify(|e: &mut (f32, f32)| {
+                aggregated_volumes.entry(time).and_modify(|e: &mut (f32, f32)| {
                     if is_sell {
                         e.1 += qty;
                     } else {
@@ -491,8 +486,7 @@ impl canvas::Program<Message> for Heatmap {
             if filtered_visible_trades.len() > 1 {
                 let (qty_max, qty_min) = filtered_visible_trades.iter().map(|(_, _, qty, _)| qty).fold((0.0f32, f32::MAX), |(max, min), &qty| (max.max(qty), min.min(qty)));
                 for &(time, price, qty, is_sell) in &filtered_visible_trades {
-                    let timestamp = time.timestamp_millis();
-                    let x_position = ((timestamp - earliest) as f64 / (latest - earliest) as f64) * bounds.width as f64;
+                    let x_position = ((time - earliest) as f64 / (latest - earliest) as f64) * bounds.width as f64;
                     let y_position = heatmap_area_height - ((price - lowest) / y_range * heatmap_area_height);
 
                     let color = if is_sell {
@@ -509,10 +503,9 @@ impl canvas::Program<Message> for Heatmap {
             }
             
             // orderbook heatmap
-            let visible_depth: Vec<&(DateTime<Utc>, Vec<(f32, f32)>, Vec<(f32, f32)>)> = self.depth.iter()
+            let visible_depth: Vec<&(i64, Vec<(f32, f32)>, Vec<(f32, f32)>)> = self.depth.iter()
                 .filter(|(time, _, _)| {
-                    let timestamp = time.timestamp_millis();
-                    timestamp >= earliest && timestamp <= latest
+                    *time >= earliest && *time <= latest
                 })
                 .collect::<Vec<_>>();
 
@@ -521,16 +514,16 @@ impl canvas::Program<Message> for Heatmap {
                 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)> = visible_depth.iter()
+                let bids_i: Vec<(&i64, f32, f32)> = visible_depth.iter()
                     .map(|&(time, bid, _ask)| (time, bid[i].0, bid[i].1)).collect();
-                let asks_i: Vec<(&DateTime<Utc>, f32, f32)> = visible_depth.iter()
+                let asks_i: Vec<(&i64, f32, f32)> = visible_depth.iter()
                     .map(|&(time, _bid, ask)| (time, ask[i].0, ask[i].1)).collect();
 
                 bids_i.iter().zip(asks_i.iter()).for_each(|((time, bid_price, bid_qty), (_, ask_price, ask_qty))| {
                     let bid_y_position = heatmap_area_height - ((bid_price - lowest) / y_range * heatmap_area_height);
                     let ask_y_position = heatmap_area_height - ((ask_price - lowest) / y_range * heatmap_area_height);
 
-                    let x_position = ((time.timestamp_millis() - earliest) as f64 / (latest - earliest) as f64) * bounds.width as f64;
+                    let x_position = ((**time - earliest) as f64 / (latest - earliest) as f64) * bounds.width as f64;
 
                     let bid_color_alpha = (bid_qty / max_order_quantity).min(1.0);
                     let ask_color_alpha = (ask_qty / max_order_quantity).min(1.0);
@@ -542,6 +535,55 @@ impl canvas::Program<Message> for Heatmap {
                     frame.fill(&ask_circle, Color::from_rgba8(192, 0, 192, ask_color_alpha));
                 });
             }
+
+            if let Some(latest_depth) = visible_depth.last() {
+                let latest_timestamp = latest_depth.0 + 200;
+
+                let latest_bids = latest_depth.1.iter().map(|(price, qty)| (*price, *qty)).collect::<Vec<_>>();
+                let latest_asks = latest_depth.2.iter().map(|(price, qty)| (*price, *qty)).collect::<Vec<_>>();
+
+                let max_qty = latest_bids.iter().map(|(_, qty)| qty).chain(latest_asks.iter().map(|(_, qty)| qty)).fold(f32::MIN, |arg0: f32, other: &f32| f32::max(arg0, *other));
+
+                let x_position = ((latest_timestamp - earliest) as f32 / (latest - earliest) as f32) * bounds.width as f32;
+
+                for (_, (price, qty)) in latest_bids.iter().enumerate() {      
+                    let y_position = heatmap_area_height - ((price - lowest) / y_range * heatmap_area_height);
+
+                    let bar_width = (qty / max_qty) * bounds.width / 20.0;
+                    let bar = Path::rectangle(
+                        Point::new(x_position, y_position), 
+                        Size::new(bar_width, 1.0) 
+                    );
+                    frame.fill(&bar, Color::from_rgba8(0, 144, 144, 0.4));
+                }
+                for (_, (price, qty)) in latest_asks.iter().enumerate() {
+                    let y_position = heatmap_area_height - ((price - lowest) / y_range * heatmap_area_height);
+
+                    let bar_width = (qty / max_qty) * bounds.width / 20.0; 
+                    let bar = Path::rectangle(
+                        Point::new(x_position, y_position), 
+                        Size::new(bar_width, 1.0)
+                    );
+                    frame.fill(&bar, Color::from_rgba8(192, 0, 192, 0.4));
+                }
+
+                let line = Path::line(
+                    Point::new(x_position, 0.0), 
+                    Point::new(x_position, bounds.height as f32)
+                );
+                frame.stroke(&line, Stroke::default().with_color(Color::from_rgba8(100, 100, 100, 0.1)).with_width(1.0));
+
+                let text_size = 8.0;
+                let text_content = format!("{:.2}", max_qty);
+                let text_position = Point::new(x_position + 46.0, bounds.height as f32 - 20.0);
+                frame.fill_text(canvas::Text {
+                    content: text_content,
+                    position: text_position,
+                    size: iced::Pixels(text_size),
+                    color: Color::from_rgba8(81, 81, 81, 1.0),
+                    ..canvas::Text::default()
+                });
+            };
         });
 
         if self.crosshair {
@@ -614,32 +656,28 @@ fn calculate_price_step(highest: f32, lowest: f32, labels_can_fit: i32) -> (f32,
 
     (step, rounded_lowest)
 }
-fn calculate_time_step(earliest: i64, latest: i64, labels_can_fit: i32) -> (Duration, NaiveDateTime) {
+const STEPS: [i64; 8] = [
+    60 * 1000, // 1 minute
+    30 * 1000, // 30 seconds
+    15 * 1000, // 15 seconds
+    10 * 1000, // 10 seconds
+    5 * 1000,  // 5 seconds
+    2 * 1000,  // 2 seconds
+    1 * 1000,  // 1 second
+    500,       // 500 milliseconds
+];
+fn calculate_time_step(earliest: i64, latest: i64, labels_can_fit: i32) -> (i64, i64) {
     let duration = latest - earliest;
 
-    let steps = [
-        Duration::minutes(1),
-        Duration::seconds(30),
-        Duration::seconds(15),
-        Duration::seconds(10),
-        Duration::seconds(5),
-        Duration::seconds(2),
-        Duration::seconds(1),
-        Duration::milliseconds(500),
-    ];
-
-    let mut selected_step = steps[0];
-    for &step in steps.iter() {
-        if duration / step.num_milliseconds() >= labels_can_fit as i64 {
+    let mut selected_step = STEPS[0];
+    for &step in STEPS.iter() {
+        if duration / step >= labels_can_fit as i64 {
             selected_step = step;
             break;
         }
     }
 
-    let rounded_earliest = NaiveDateTime::from_timestamp(
-        (earliest / 1000) / (selected_step.num_milliseconds() / 1000) * (selected_step.num_milliseconds() / 1000),
-        0
-    );
+    let rounded_earliest = (earliest / selected_step) * selected_step;
 
     (selected_step, rounded_earliest)
 }
@@ -684,18 +722,17 @@ impl canvas::Program<Message> for AxisLabelXCanvas<'_> {
 
         let labels = self.labels_cache.draw(renderer, bounds.size(), |frame| {
             frame.with_save(|frame| {
-                let mut time = rounded_earliest;
-                let latest_time = NaiveDateTime::from_timestamp(latest_in_millis / 1000, 0);
+                let mut time: i64 = rounded_earliest;
+                let latest_time: i64 = latest_in_millis;
 
-                while time <= latest_time {
-                    let time_in_millis = time.timestamp_millis();
-                    
-                    let x_position = ((time_in_millis - earliest_in_millis) as f64 / (latest_in_millis - earliest_in_millis) as f64) * bounds.width as f64;
+                while time <= latest_time {                    
+                    let x_position = ((time as i64 - earliest_in_millis as i64) as f64 / (latest_in_millis - earliest_in_millis) as f64) * bounds.width as f64;
 
                     if x_position >= 0.0 && x_position <= bounds.width as f64 {
                         let text_size = 12.0;
+                        let time_as_datetime = NaiveDateTime::from_timestamp((time / 1000) as i64, 0);
                         let label = canvas::Text {
-                            content: time.format("%M:%S").to_string(),
+                            content: time_as_datetime.format("%M:%S").to_string(),
                             position: Point::new(x_position as f32 - text_size, bounds.height as f32 - 20.0),
                             size: iced::Pixels(text_size),
                             color: Color::from_rgba8(200, 200, 200, 1.0),
@@ -709,6 +746,12 @@ impl canvas::Program<Message> for AxisLabelXCanvas<'_> {
                     
                     time = time + time_step;
                 }
+
+                let line = Path::line(
+                    Point::new(0.0, bounds.height as f32 - 30.0), 
+                    Point::new(bounds.width as f32, bounds.height as f32 - 30.0)
+                );
+                frame.stroke(&line, Stroke::default().with_color(Color::from_rgba8(81, 81, 81, 0.2)).with_width(1.0));
             });
         });
         let crosshair = self.crosshair_cache.draw(renderer, bounds.size(), |frame| {