Browse Source

add candlestick chart

Berke 1 year ago
parent
commit
55943ccf85
1 changed files with 205 additions and 22 deletions
  1. 205 22
      src/main.rs

+ 205 - 22
src/main.rs

@@ -1,12 +1,14 @@
 mod ws_binance;
-
+use serde::Deserialize;
+use std::collections::HashMap;
 use chrono::{DateTime, Utc};
 use iced::{
     executor, font, widget::{
         Row, button, canvas::{Cache, Frame, Geometry}, pick_list, shader::wgpu::hal::auxil::db, Column, Container, Text
     }, Alignment, Application, Command, Element, Event, Font, Length, Settings, Size, Subscription, Theme
 };
-use std::{ops::Sub, sync::Arc, time::Duration};
+use ws_binance::Kline;
+use futures::TryFutureExt;
 use plotters::prelude::ChartBuilder;
 use plotters_backend::DrawingBackend;
 use chrono::NaiveDateTime;
@@ -75,14 +77,16 @@ fn main() {
 enum Message {
     TickerSelected(Ticker),
     WsEvent(ws_binance::Event),
-    WsToggle(),}
+    WsToggle(),
+    FetchEvent(Result<Vec<ws_binance::Kline>, std::string::String>),
+}
 
 struct State {
-    chart: Option<ExampleChart>,
+    trades_chart: Option<LineChart>,
+    candlestick_chart: Option<CandlestickChart>,
     selected_ticker: Option<Ticker>,
     ws_state: WsState,
     ws_running: bool,
-    trades: Vec<ws_binance::Trade>,
 }
 
 impl Application for State {
@@ -94,11 +98,11 @@ impl Application for State {
     fn new(_flags: Self::Flags) -> (Self, Command<Self::Message>) {
         (
             Self { 
-                chart: None,
+                trades_chart: None,
+                candlestick_chart: None,
                 selected_ticker: None,
                 ws_state: WsState::Disconnected,
                 ws_running: false,
-                trades: Vec::new(),
             },
            
             Command::batch([
@@ -123,8 +127,25 @@ impl Application for State {
             Message::WsToggle() => {
                 self.ws_running =! self.ws_running;
                 dbg!(&self.ws_running);
-                self.trades.clear();
-                self.chart = Some(ExampleChart::new());
+                self.trades_chart = Some(LineChart::new());
+                Command::perform(
+                    fetch_klines(self.selected_ticker.unwrap())
+                        .map_err(|err| format!("{}", err)), 
+                    |klines| {
+                        Message::FetchEvent(klines)
+                    }
+                )
+            },
+            Message::FetchEvent(klines) => {
+                match klines {
+                    Ok(klines) => {
+                        self.candlestick_chart = Some(CandlestickChart::new(klines));
+                    },
+                    Err(err) => {
+                        eprintln!("Error fetching klines: {}", err);
+                        self.candlestick_chart = Some(CandlestickChart::new(vec![]));
+                    },
+                }
                 Command::none()
             },
             Message::WsEvent(event) => match event {
@@ -136,12 +157,18 @@ impl Application for State {
                     self.ws_state = WsState::Disconnected;
                     Command::none()
                 }
-                ws_binance::Event::MessageReceived(trades_buffer) => {
-                    if let Some(chart) = &mut self.chart {
+                ws_binance::Event::TradeReceived(trades_buffer) => {
+                    if let Some(chart) = &mut self.trades_chart {
                         chart.update(trades_buffer);
                     }
                     Command::none()
                 }
+                ws_binance::Event::KlineReceived(kline) => {
+                    if let Some(chart) = &mut self.candlestick_chart {
+                        chart.update(kline);
+                    }
+                    Command::none()
+                }
             },  
         }
     }
@@ -157,8 +184,12 @@ impl Application for State {
         )
         .placeholder("Choose a ticker...");
 
-        let chart = match self.chart {
-            Some(ref chart) => chart.view(),
+        let trades_chart = match self.trades_chart {
+            Some(ref trades_chart) => trades_chart.view(),
+            None => Text::new("").into(),
+        };
+        let candlestick_chart = match self.candlestick_chart {
+            Some(ref candlestick_chart) => candlestick_chart.view(),
             None => Text::new("Loading...").into(),
         };
 
@@ -174,7 +205,8 @@ impl Application for State {
             .width(Length::Fill)
             .height(Length::Fill)
             .push(controls)
-            .push(chart);
+            .push(trades_chart)
+            .push(candlestick_chart);
 
         Container::new(content)
             .width(Length::Fill)
@@ -197,12 +229,114 @@ impl Application for State {
     }
 }
 
-struct ExampleChart {
+
+struct CandlestickChart {
+    cache: Cache,
+    data_points: HashMap<DateTime<Utc>, (f32, f32, f32, f32)>,
+}
+
+impl CandlestickChart {
+    fn new(klines: Vec<ws_binance::Kline>) -> Self {
+        let mut data_points = HashMap::new();
+
+        for kline in klines {
+            let time = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(kline.time as i64 / 1000, 0), Utc);
+            let open = kline.open;
+            let high = kline.high;
+            let low = kline.low;
+            let close = kline.close;
+            data_points.insert(time, (open, high, low, close));
+        }
+
+        Self {
+            cache: Cache::new(),
+            data_points,
+        }
+    }
+
+    fn update(&mut self, kline: ws_binance::Kline) {
+        let time = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(kline.time as i64 / 1000, 0), Utc);
+        let open = kline.open;
+        let high = kline.high;
+        let low = kline.low;
+        let close = kline.close;
+        self.data_points.insert(time, (open, high, low, close));
+
+        self.cache.clear();
+    }
+
+    fn view(&self) -> Element<Message> {
+        let chart = ChartWidget::new(self)
+            .width(Length::Fill)
+            .height(Length::Fill);
+
+        chart.into()
+    }
+}
+
+impl Chart<Message> for CandlestickChart {
+    type State = ();
+
+    #[inline]
+    fn draw<R: Renderer, F: Fn(&mut Frame)>(
+        &self,
+        renderer: &R,
+        bounds: Size,
+        draw_fn: F,
+    ) -> Geometry {
+        renderer.draw_cache(&self.cache, bounds, draw_fn)
+    }
+
+    fn build_chart<DB: DrawingBackend>(&self, _state: &Self::State, mut chart: ChartBuilder<DB>) {
+        use plotters::prelude::*;
+
+        let oldest_time = *self.data_points.keys().min().unwrap_or(&Utc::now());
+        let newest_time = *self.data_points.keys().max().unwrap_or(&Utc::now());
+
+        let mut y_min = f32::MAX;
+        let mut y_max = f32::MIN;
+        for (_time, (open, high, low, close)) in &self.data_points {
+            y_min = y_min.min(*low);
+            y_max = y_max.max(*high);
+        }
+
+        let mut chart = chart
+            .x_label_area_size(0)
+            .y_label_area_size(28)
+            .margin(20)
+            .build_cartesian_2d(oldest_time..newest_time, y_min..y_max)
+            .expect("failed to build chart");
+
+        chart
+            .configure_mesh()
+            .bold_line_style(GREY.mix(0.1))
+            .light_line_style(GREY.mix(0.05))
+            .axis_style(ShapeStyle::from(GREY.mix(0.45)).stroke_width(1))
+            .y_labels(10)
+            .y_label_style(
+                ("Noto Sans", 12)
+                    .into_font()
+                    .color(&GREY.mix(0.65))
+                    .transform(FontTransform::Rotate90),
+            )
+            .y_label_formatter(&|y| format!("{}", y))
+            .draw()
+            .expect("failed to draw chart mesh");
+
+        chart.draw_series(
+            self.data_points.iter().map(|(time, (open, high, low, close))| {
+                CandleStick::new(*time, *open, *high, *low, *close, GREEN.filled(), RED.filled(), 15)
+            }),
+        ).expect("failed to draw chart data");
+    }
+}
+
+struct LineChart {
     cache: Cache,
     data_points: VecDeque<(DateTime<Utc>, f32)>,
 }
 
-impl ExampleChart {
+impl LineChart {
     fn new() -> Self {
         Self {
             cache: Cache::new(),
@@ -210,10 +344,10 @@ impl ExampleChart {
         }
     }
 
-    fn update(&mut self, mut trades_buffer: Vec<ws_binance::TradeWrapper>) {
-        for ws_binance::TradeWrapper { stream: _, data } in trades_buffer.drain(..) {
-            let time = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(data.time as i64 / 1000, 0), Utc);
-            let price = data.price;
+    fn update(&mut self, mut trades_buffer: Vec<ws_binance::Trade>) {
+        for trade in trades_buffer.drain(..) {
+            let time = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(trade.time as i64 / 1000, 0), Utc);
+            let price = trade.price;
             self.data_points.push_back((time, price));
         }
 
@@ -233,7 +367,7 @@ impl ExampleChart {
     }
 }
 
-impl Chart<Message> for ExampleChart {
+impl Chart<Message> for LineChart {
     type State = ();
     // fn update(
     //     &mut self,
@@ -317,7 +451,56 @@ impl Chart<Message> for ExampleChart {
                     )
                     .border_style(ShapeStyle::from(PLOT_LINE_COLOR).stroke_width(2)),
                 )
-            .expect("failed to draw chart data");
+                .expect("failed to draw chart data");
+        }
+    }
+}
+
+
+mod string_to_f32 {
+    use serde::{self, Deserialize, Deserializer};
+
+    pub fn deserialize<'de, D>(deserializer: D) -> Result<f32, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let s = String::deserialize(deserializer)?;
+        s.parse::<f32>().map_err(serde::de::Error::custom)
+    }
+}
+#[derive(Deserialize, Debug, Clone)]
+struct FetchedKlines (
+    u64,
+    #[serde(with = "string_to_f32")] f32,
+    #[serde(with = "string_to_f32")] f32,
+    #[serde(with = "string_to_f32")] f32,
+    #[serde(with = "string_to_f32")] f32,
+    #[serde(with = "string_to_f32")] f32,
+    u64,
+    String,
+    u32,
+    #[serde(with = "string_to_f32")] f32,
+    String,
+    String,
+);
+impl From<FetchedKlines> for Kline {
+    fn from(fetched: FetchedKlines) -> Self {
+        Self {
+            time: fetched.0,
+            open: fetched.1,
+            high: fetched.2,
+            low: fetched.3,
+            close: fetched.4,
+            volume: fetched.5,
+            taker_buy_base_asset_volume: fetched.9,
         }
     }
+}
+async fn fetch_klines(ticker: Ticker) -> Result<Vec<Kline>, reqwest::Error> {
+    let url = format!("https://fapi.binance.com/fapi/v1/klines?symbol={}&interval=1m&limit=30", ticker.to_string().to_lowercase());
+    let response = reqwest::get(&url).await?;
+    let value: serde_json::Value = response.json().await?;
+    let fetched_klines: Result<Vec<FetchedKlines>, _> = serde_json::from_value(value);
+    let klines: Vec<Kline> = fetched_klines.unwrap().into_iter().map(Kline::from).collect();
+    Ok(klines)
 }