main.rs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  1. mod ws_binance;
  2. use std::collections::BTreeMap;
  3. use chrono::{DateTime, Utc, Duration, TimeZone, LocalResult};
  4. use iced::{
  5. executor, widget::{
  6. button, canvas::{path::lyon_path::geom::euclid::num::Round, Cache, Frame, Geometry}, pick_list, Column, Container, Row, Text
  7. }, Alignment, Application, Command, Element, Font, Length, Settings, Size, Subscription, Theme
  8. };
  9. use futures::TryFutureExt;
  10. use plotters::prelude::ChartBuilder;
  11. use plotters_backend::DrawingBackend;
  12. use plotters_iced::{
  13. sample::lttb::DataPoint,
  14. Chart, ChartWidget, Renderer,
  15. };
  16. use plotters::prelude::full_palette::GREY;
  17. use std::collections::VecDeque;
  18. struct Wrapper<'a>(&'a DateTime<Utc>, &'a f32);
  19. impl DataPoint for Wrapper<'_> {
  20. #[inline]
  21. fn x(&self) -> f64 {
  22. self.0.timestamp() as f64
  23. }
  24. #[inline]
  25. fn y(&self) -> f64 {
  26. *self.1 as f64
  27. }
  28. }
  29. impl std::fmt::Display for Ticker {
  30. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  31. write!(
  32. f,
  33. "{}",
  34. match self {
  35. Ticker::BTCUSDT => "BTCUSDT",
  36. Ticker::ETHUSDT => "ETHUSDT",
  37. Ticker::SOLUSDT => "SOLUSDT",
  38. Ticker::LTCUSDT => "LTCUSDT",
  39. }
  40. )
  41. }
  42. }
  43. #[derive(Debug, Clone, Copy, PartialEq, Eq)]
  44. enum Ticker {
  45. BTCUSDT,
  46. ETHUSDT,
  47. SOLUSDT,
  48. LTCUSDT,
  49. }
  50. impl Ticker {
  51. const ALL: [Ticker; 4] = [Ticker::BTCUSDT, Ticker::ETHUSDT, Ticker::SOLUSDT, Ticker::LTCUSDT];
  52. }
  53. enum WsState {
  54. Disconnected,
  55. Connected(ws_binance::Connection),
  56. }
  57. impl Default for WsState {
  58. fn default() -> Self {
  59. Self::Disconnected
  60. }
  61. }
  62. fn main() {
  63. State::run(Settings {
  64. antialiasing: true,
  65. default_font: Font::with_name("Noto Sans"),
  66. ..Settings::default()
  67. })
  68. .unwrap();
  69. }
  70. #[derive(Debug, Clone)]
  71. enum Message {
  72. TickerSelected(Ticker),
  73. TimeframeSelected(&'static str),
  74. WsEvent(ws_binance::Event),
  75. WsToggle(),
  76. FetchEvent(Result<Vec<ws_binance::Kline>, std::string::String>),
  77. }
  78. struct State {
  79. trades_chart: Option<LineChart>,
  80. candlestick_chart: Option<CandlestickChart>,
  81. selected_ticker: Option<Ticker>,
  82. selected_timeframe: Option<&'static str>,
  83. ws_state: WsState,
  84. ws_running: bool,
  85. }
  86. impl Application for State {
  87. type Message = self::Message;
  88. type Executor = executor::Default;
  89. type Flags = ();
  90. type Theme = Theme;
  91. fn new(_flags: Self::Flags) -> (Self, Command<Self::Message>) {
  92. (
  93. Self {
  94. trades_chart: None,
  95. candlestick_chart: None,
  96. selected_ticker: None,
  97. selected_timeframe: Some("1m"),
  98. ws_state: WsState::Disconnected,
  99. ws_running: false,
  100. },
  101. Command::batch([
  102. //Command::perform(tokio::task::spawn_blocking(generate_data), |data| {
  103. // Message::DataLoaded(data.unwrap())
  104. //}),
  105. ]),
  106. )
  107. }
  108. fn title(&self) -> String {
  109. "Iced Trade".to_owned()
  110. }
  111. fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
  112. match message {
  113. Message::TickerSelected(ticker) => {
  114. self.selected_ticker = Some(ticker);
  115. Command::none()
  116. },
  117. Message::TimeframeSelected(timeframe) => {
  118. self.selected_timeframe = Some(timeframe);
  119. Command::none()
  120. },
  121. Message::WsToggle() => {
  122. self.ws_running =! self.ws_running;
  123. dbg!(&self.ws_running);
  124. if self.ws_running {
  125. self.trades_chart = Some(LineChart::new());
  126. Command::perform(
  127. ws_binance::fetch_klines(self.selected_ticker.unwrap().to_string(), self.selected_timeframe.unwrap().to_string())
  128. .map_err(|err| format!("{}", err)),
  129. |klines| {
  130. Message::FetchEvent(klines)
  131. }
  132. )
  133. } else {
  134. self.trades_chart = None;
  135. self.candlestick_chart = None;
  136. Command::none()
  137. }
  138. },
  139. Message::FetchEvent(klines) => {
  140. match klines {
  141. Ok(klines) => {
  142. let timeframe_in_minutes = match self.selected_timeframe.unwrap() {
  143. "1m" => 1,
  144. "3m" => 3,
  145. "5m" => 5,
  146. "15m" => 15,
  147. "30m" => 30,
  148. _ => 1,
  149. };
  150. self.candlestick_chart = Some(CandlestickChart::new(klines, timeframe_in_minutes));
  151. },
  152. Err(err) => {
  153. eprintln!("Error fetching klines: {}", err);
  154. self.candlestick_chart = Some(CandlestickChart::new(vec![], 1));
  155. },
  156. }
  157. Command::none()
  158. },
  159. Message::WsEvent(event) => match event {
  160. ws_binance::Event::Connected(connection) => {
  161. self.ws_state = WsState::Connected(connection);
  162. Command::none()
  163. }
  164. ws_binance::Event::Disconnected => {
  165. self.ws_state = WsState::Disconnected;
  166. Command::none()
  167. }
  168. ws_binance::Event::DepthReceived(depth_update, bids, asks, trades_buffer) => {
  169. if let Some(chart) = &mut self.trades_chart {
  170. chart.update(depth_update, trades_buffer, bids, asks);
  171. }
  172. Command::none()
  173. }
  174. ws_binance::Event::KlineReceived(kline) => {
  175. if let Some(chart) = &mut self.candlestick_chart {
  176. chart.update(kline);
  177. }
  178. Command::none()
  179. }
  180. },
  181. }
  182. }
  183. fn view(&self) -> Element<'_, Self::Message> {
  184. let button_text = if self.ws_running { "Disconnect" } else { "Connect" };
  185. let ws_button = button(button_text).on_press(Message::WsToggle());
  186. let mut controls = Row::new()
  187. .spacing(20)
  188. .align_items(Alignment::Center)
  189. .push(ws_button);
  190. if !self.ws_running {
  191. let symbol_pick_list = pick_list(
  192. &Ticker::ALL[..],
  193. self.selected_ticker,
  194. Message::TickerSelected,
  195. )
  196. .placeholder("Choose a ticker...");
  197. let timeframe_pick_list = pick_list(
  198. &["1m", "3m", "5m", "15m", "30m"][..],
  199. self.selected_timeframe,
  200. Message::TimeframeSelected,
  201. );
  202. controls = controls.push(timeframe_pick_list)
  203. .push(symbol_pick_list);
  204. } else {
  205. controls = controls.push(Text::new(self.selected_ticker.unwrap().to_string()).size(20));
  206. }
  207. let trades_chart = match self.trades_chart {
  208. Some(ref trades_chart) => trades_chart.view(),
  209. None => Text::new("").into(),
  210. };
  211. let candlestick_chart = match self.candlestick_chart {
  212. Some(ref candlestick_chart) => candlestick_chart.view(),
  213. None => Text::new("").into(),
  214. };
  215. let content = Column::new()
  216. .spacing(20)
  217. .align_items(Alignment::Start)
  218. .width(Length::Fill)
  219. .height(Length::Fill)
  220. .push(controls)
  221. .push(trades_chart)
  222. .push(candlestick_chart);
  223. Container::new(content)
  224. .width(Length::Fill)
  225. .height(Length::Fill)
  226. .padding(20)
  227. .center_x()
  228. .center_y()
  229. .into()
  230. }
  231. fn subscription(&self) -> Subscription<Message> {
  232. match (&self.selected_ticker, self.ws_running) {
  233. (Some(selected_ticker), true) => ws_binance::connect(selected_ticker.to_string(), self.selected_timeframe.unwrap().to_string()).map(Message::WsEvent),
  234. _ => Subscription::none(),
  235. }
  236. }
  237. fn theme(&self) -> Self::Theme {
  238. Theme::Oxocarbon
  239. }
  240. }
  241. struct CandlestickChart {
  242. cache: Cache,
  243. data_points: BTreeMap<DateTime<Utc>, (f32, f32, f32, f32)>,
  244. timeframe_in_minutes: i16,
  245. }
  246. impl CandlestickChart {
  247. fn new(klines: Vec<ws_binance::Kline>, timeframe_in_minutes: i16) -> Self {
  248. let mut data_points = BTreeMap::new();
  249. for kline in klines {
  250. let time = match Utc.timestamp_opt(kline.time as i64 / 1000, 0) {
  251. LocalResult::Single(dt) => dt,
  252. _ => continue,
  253. };
  254. let open = kline.open;
  255. let high = kline.high;
  256. let low = kline.low;
  257. let close = kline.close;
  258. data_points.insert(time, (open, high, low, close));
  259. }
  260. Self {
  261. cache: Cache::new(),
  262. data_points,
  263. timeframe_in_minutes,
  264. }
  265. }
  266. fn update(&mut self, kline: ws_binance::Kline) {
  267. let time = match Utc.timestamp_opt(kline.time as i64 / 1000, 0) {
  268. LocalResult::Single(dt) => dt,
  269. _ => return,
  270. };
  271. let open = kline.open;
  272. let high = kline.high;
  273. let low = kline.low;
  274. let close = kline.close;
  275. self.data_points.insert(time, (open, high, low, close));
  276. self.cache.clear();
  277. }
  278. fn view(&self) -> Element<Message> {
  279. let chart = ChartWidget::new(self)
  280. .width(Length::Fill)
  281. .height(Length::Fill);
  282. chart.into()
  283. }
  284. }
  285. impl Chart<Message> for CandlestickChart {
  286. type State = ();
  287. #[inline]
  288. fn draw<R: Renderer, F: Fn(&mut Frame)>(
  289. &self,
  290. renderer: &R,
  291. bounds: Size,
  292. draw_fn: F,
  293. ) -> Geometry {
  294. renderer.draw_cache(&self.cache, bounds, draw_fn)
  295. }
  296. fn build_chart<DB: DrawingBackend>(&self, _state: &Self::State, mut chart: ChartBuilder<DB>) {
  297. use plotters::prelude::*;
  298. let drawing_area;
  299. {
  300. let dummy_chart = chart
  301. .build_cartesian_2d(0..1, 0..1)
  302. .expect("failed to build dummy chart");
  303. drawing_area = dummy_chart.plotting_area().dim_in_pixel();
  304. }
  305. let newest_time = *self.data_points.keys().last().unwrap_or(&Utc::now());
  306. let cutoff_number = (drawing_area.0 as i64 / 12).round();
  307. let oldest_time = newest_time - Duration::minutes((cutoff_number*self.timeframe_in_minutes as i64).max(1));
  308. let visible_data_points: Vec<_> = self.data_points.iter().filter(|&(time, _)| {
  309. time >= &oldest_time && time <= &newest_time
  310. }).collect();
  311. let mut y_min = f32::MAX;
  312. let mut y_max = f32::MIN;
  313. for (_time, (_open, high, low, _close)) in &visible_data_points {
  314. y_min = y_min.min(*low);
  315. y_max = y_max.max(*high);
  316. }
  317. let mut chart = chart
  318. .x_label_area_size(28)
  319. .y_label_area_size(28)
  320. .margin(20)
  321. .build_cartesian_2d(oldest_time..newest_time, y_min..y_max)
  322. .expect("failed to build chart");
  323. chart
  324. .configure_mesh()
  325. .bold_line_style(GREY.mix(0.05))
  326. .light_line_style(GREY.mix(0.02))
  327. .axis_style(ShapeStyle::from(GREY.mix(0.45)).stroke_width(1))
  328. .y_labels(10)
  329. .y_label_style(
  330. ("Noto Sans", 12)
  331. .into_font()
  332. .color(&GREY.mix(0.65))
  333. .transform(FontTransform::Rotate90),
  334. )
  335. .y_label_formatter(&|y| format!("{}", y))
  336. .x_labels(8)
  337. .x_label_style(
  338. ("Noto Sans", 12)
  339. .into_font()
  340. .color(&GREY.mix(0.65))
  341. )
  342. .x_label_formatter(&|x| {
  343. x.format("%H:%M").to_string()
  344. })
  345. .draw()
  346. .expect("failed to draw chart mesh");
  347. chart.draw_series(
  348. visible_data_points.iter().map(|(time, (open, high, low, close))| {
  349. CandleStick::new(**time, *open, *high, *low, *close, RGBColor(81, 205, 160).filled(), RGBColor(192, 80, 77).filled(), 8)
  350. }),
  351. ).expect("failed to draw chart data");
  352. }
  353. }
  354. struct LineChart {
  355. cache: Cache,
  356. data_points: VecDeque<(DateTime<Utc>, f32, f32, bool)>,
  357. depth: VecDeque<(DateTime<Utc>, Vec<(f32, f32)>, Vec<(f32, f32)>)>,
  358. }
  359. impl LineChart {
  360. fn new() -> Self {
  361. Self {
  362. cache: Cache::new(),
  363. data_points: VecDeque::new(),
  364. depth: VecDeque::new(),
  365. }
  366. }
  367. fn update(&mut self, depth_update: u64, mut trades_buffer: Vec<ws_binance::Trade>, bids: Vec<(f32, f32)>, asks: Vec<(f32, f32)>) {
  368. let aggregate_time = 100;
  369. let seconds = (depth_update / 1000) as i64;
  370. let nanoseconds = ((depth_update % 1000) / aggregate_time * aggregate_time * 1_000_000) as u32;
  371. let depth_update_time = match Utc.timestamp_opt(seconds, nanoseconds) {
  372. LocalResult::Single(dt) => dt,
  373. _ => return,
  374. };
  375. for trade in trades_buffer.drain(..) {
  376. self.data_points.push_back((depth_update_time, trade.price, trade.qty, trade.is_sell));
  377. }
  378. if let Some((time, _, _)) = self.depth.back() {
  379. if *time == depth_update_time {
  380. self.depth.pop_back();
  381. }
  382. }
  383. self.depth.push_back((depth_update_time, bids, asks));
  384. while self.data_points.len() > 6000 {
  385. self.data_points.pop_front();
  386. }
  387. while self.depth.len() > 1000 {
  388. self.depth.pop_front();
  389. }
  390. self.cache.clear();
  391. }
  392. fn view(&self) -> Element<Message> {
  393. let chart = ChartWidget::new(self)
  394. .width(Length::Fill)
  395. .height(Length::Fill);
  396. chart.into()
  397. }
  398. }
  399. impl Chart<Message> for LineChart {
  400. type State = ();
  401. #[inline]
  402. fn draw<R: Renderer, F: Fn(&mut Frame)>(
  403. &self,
  404. renderer: &R,
  405. bounds: Size,
  406. draw_fn: F,
  407. ) -> Geometry {
  408. renderer.draw_cache(&self.cache, bounds, draw_fn)
  409. }
  410. fn build_chart<DB: DrawingBackend>(&self, _state: &Self::State, mut chart: ChartBuilder<DB>) {
  411. use plotters::prelude::*;
  412. if self.data_points.len() > 1 {
  413. // x-axis range, acquire time range
  414. let drawing_area;
  415. {
  416. let dummy_chart = chart
  417. .build_cartesian_2d(0..1, 0..1)
  418. .expect("failed to build dummy chart");
  419. drawing_area = dummy_chart.plotting_area().dim_in_pixel();
  420. }
  421. let newest_time = self.depth.back().unwrap().0 + Duration::milliseconds(200);
  422. let oldest_time = newest_time - Duration::seconds(drawing_area.0 as i64 / 30);
  423. // y-axis range, acquire price range within the time range
  424. let mut y_min = f32::MAX;
  425. let mut y_max = f32::MIN;
  426. let recent_data_points: Vec<_> = self.data_points.iter().filter_map(|&(time, price, qty, bool)| {
  427. if time >= oldest_time && time <= newest_time {
  428. Some((time, price, qty, bool))
  429. } else {
  430. None
  431. }
  432. }).collect();
  433. let recent_depth: Vec<_> = self.depth.iter().filter_map(|(time, bids, asks)| {
  434. if time >= &oldest_time && time <= &newest_time {
  435. if let Some((bid_price, _)) = bids.last() {
  436. y_min = y_min.min(*bid_price);
  437. }
  438. if let Some((ask_price, _)) = asks.last() {
  439. y_max = y_max.max(*ask_price);
  440. }
  441. Some((time, bids, asks))
  442. } else {
  443. None
  444. }
  445. }).collect();
  446. let mut chart = chart
  447. .x_label_area_size(28)
  448. .y_label_area_size(28)
  449. .margin(20)
  450. .build_cartesian_2d(oldest_time..newest_time, y_min..y_max)
  451. .expect("failed to build chart");
  452. chart
  453. .configure_mesh()
  454. .bold_line_style(GREY.mix(0.04))
  455. .light_line_style(GREY.mix(0.01))
  456. .axis_style(ShapeStyle::from(GREY.mix(0.45)).stroke_width(1))
  457. .y_labels(10)
  458. .y_label_style(
  459. ("Noto Sans", 12)
  460. .into_font()
  461. .color(&GREY.mix(0.65))
  462. .transform(FontTransform::Rotate90),
  463. )
  464. .y_label_formatter(&|y| format!("{}", y))
  465. .x_labels(8)
  466. .x_label_style(
  467. ("Noto Sans", 12)
  468. .into_font()
  469. .color(&GREY.mix(0.65))
  470. )
  471. .x_label_formatter(&|x| {
  472. x.format("%M:%S").to_string()
  473. })
  474. .draw()
  475. .expect("failed to draw chart mesh");
  476. for i in 0..20 {
  477. let bids_i: Vec<(DateTime<Utc>, f32, f32)> = recent_depth.iter().map(|&(time, bid, _ask)| ((*time).clone(), bid[i].0, bid[i].1)).collect();
  478. let asks_i: Vec<(DateTime<Utc>, f32, f32)> = recent_depth.iter().map(|&(time, _bid, ask)| ((*time).clone(), ask[i].0, ask[i].1)).collect();
  479. let max_order_quantity = bids_i.iter()
  480. .map(|&(_time, _price, quantity)| quantity)
  481. .chain(asks_i.iter().map(|&(_time, _price, quantity)| quantity))
  482. .fold(f32::MIN, f32::max);
  483. chart
  484. .draw_series(
  485. bids_i.iter().map(|&(time, price, quantity)| {
  486. let alpha = 0.1 + 0.9 * (quantity / max_order_quantity);
  487. Pixel::new((time, price), RGBAColor(0, 144, 144, alpha.into()))
  488. }),
  489. )
  490. .expect(&format!("failed to draw bids_{}", i));
  491. chart
  492. .draw_series(
  493. asks_i.iter().map(|&(time, price, quantity)| {
  494. let alpha = 0.1 + 0.9 * (quantity / max_order_quantity);
  495. Pixel::new((time, price), RGBAColor(192, 0, 192, alpha.into()))
  496. }),
  497. )
  498. .expect(&format!("failed to draw asks_{}", i));
  499. }
  500. let qty_min = recent_data_points.iter().map(|&(_, _, qty, _)| qty).fold(f32::MAX, f32::min);
  501. let qty_max = recent_data_points.iter().map(|&(_, _, qty, _)| qty).fold(f32::MIN, f32::max);
  502. chart
  503. .draw_series(
  504. recent_data_points.iter().map(|&(time, price, qty, is_sell)| {
  505. let radius = 1.0 + (qty - qty_min) * (30.0 - 1.0) / (qty_max - qty_min);
  506. let color = if is_sell { RGBColor(192, 80, 77) } else { RGBColor(81, 205, 160)};
  507. Circle::new(
  508. (time, price),
  509. radius as i32,
  510. ShapeStyle::from(color).filled(),
  511. )
  512. }),
  513. )
  514. .expect("failed to draw circles");
  515. }
  516. }
  517. }