footprint.rs 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023
  1. use std::collections::{BTreeMap, HashMap};
  2. use chrono::NaiveDateTime;
  3. use iced::{
  4. alignment, color, mouse, widget::{button, canvas::{self, event::{self, Event}, stroke::Stroke, Cache, Canvas, Geometry, Path}}, window, Border, Color, Element, Length, Point, Rectangle, Renderer, Size, Theme, Vector
  5. };
  6. use iced::widget::{Column, Row, Container, Text};
  7. use crate::data_providers::binance::market_data::{Kline, Trade};
  8. #[derive(Debug, Clone)]
  9. pub enum Message {
  10. Translated(Vector),
  11. Scaled(f32, Option<Vector>),
  12. ChartBounds(Rectangle),
  13. AutoscaleToggle,
  14. CrosshairToggle,
  15. CrosshairMoved(Point),
  16. }
  17. #[derive(Debug)]
  18. pub struct Footprint {
  19. heatmap_cache: Cache,
  20. crosshair_cache: Cache,
  21. x_labels_cache: Cache,
  22. y_labels_cache: Cache,
  23. y_croshair_cache: Cache,
  24. x_crosshair_cache: Cache,
  25. translation: Vector,
  26. scaling: f32,
  27. data_points: BTreeMap<i64, (HashMap<i64, (f32, f32)>, (f32, f32, f32, f32, f32, f32))>,
  28. autoscale: bool,
  29. crosshair: bool,
  30. crosshair_position: Point,
  31. x_min_time: i64,
  32. x_max_time: i64,
  33. y_min_price: f32,
  34. y_max_price: f32,
  35. bounds: Rectangle,
  36. timeframe: u16,
  37. tick_size: f32,
  38. }
  39. impl Footprint {
  40. const MIN_SCALING: f32 = 0.4;
  41. const MAX_SCALING: f32 = 3.6;
  42. pub fn new(timeframe: u16, tick_size: f32, klines_raw: Vec<(i64, f32, f32, f32, f32, f32, f32)>, trades_raw: Vec<Trade>) -> Footprint {
  43. let _size = window::Settings::default().size;
  44. let mut data_points = BTreeMap::new();
  45. let aggregate_time = 1000 * 60 * timeframe as i64;
  46. for kline in klines_raw {
  47. let kline_raw = (kline.1, kline.2, kline.3, kline.4, kline.5, kline.6);
  48. data_points.entry(kline.0).or_insert((HashMap::new(), kline_raw));
  49. }
  50. for trade in trades_raw {
  51. let rounded_time = (trade.time / aggregate_time) * aggregate_time;
  52. let price_level = (trade.price / tick_size).round() as i64 * (tick_size * 100.0) as i64;
  53. let entry: &mut (HashMap<i64, (f32, f32)>, (f32, f32, f32, f32, f32, f32)) = data_points
  54. .entry(rounded_time)
  55. .or_insert((HashMap::new(), (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)));
  56. if let Some((buy_qty, sell_qty)) = entry.0.get_mut(&price_level) {
  57. if trade.is_sell {
  58. *sell_qty += trade.qty;
  59. } else {
  60. *buy_qty += trade.qty;
  61. }
  62. } else {
  63. if trade.is_sell {
  64. entry.0.insert(price_level, (0.0, trade.qty));
  65. } else {
  66. entry.0.insert(price_level, (trade.qty, 0.0));
  67. }
  68. }
  69. }
  70. Footprint {
  71. bounds: Rectangle::default(),
  72. heatmap_cache: canvas::Cache::default(),
  73. crosshair_cache: canvas::Cache::default(),
  74. x_labels_cache: canvas::Cache::default(),
  75. y_labels_cache: canvas::Cache::default(),
  76. y_croshair_cache: canvas::Cache::default(),
  77. x_crosshair_cache: canvas::Cache::default(),
  78. translation: Vector::default(),
  79. scaling: 1.0,
  80. autoscale: true,
  81. crosshair: false,
  82. crosshair_position: Point::new(0.0, 0.0),
  83. x_min_time: 0,
  84. x_max_time: 0,
  85. y_min_price: 0.0,
  86. y_max_price: 0.0,
  87. timeframe,
  88. tick_size,
  89. data_points,
  90. }
  91. }
  92. pub fn insert_datapoint(&mut self, mut trades_buffer: Vec<Trade>, depth_update: i64) {
  93. let aggregate_time = 1000 * 60 * self.timeframe as i64;
  94. let rounded_depth_update = (depth_update / aggregate_time) * aggregate_time;
  95. self.data_points.entry(rounded_depth_update).or_insert((HashMap::new(), (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)));
  96. for trade in trades_buffer.drain(..) {
  97. let price_level = (trade.price / self.tick_size).round() as i64 * (self.tick_size * 100.0) as i64;
  98. if let Some((trades, _)) = self.data_points.get_mut(&rounded_depth_update) {
  99. if let Some((buy_qty, sell_qty)) = trades.get_mut(&price_level) {
  100. if trade.is_sell {
  101. *sell_qty += trade.qty;
  102. } else {
  103. *buy_qty += trade.qty;
  104. }
  105. } else {
  106. if trade.is_sell {
  107. trades.insert(price_level, (0.0, trade.qty));
  108. } else {
  109. trades.insert(price_level, (trade.qty, 0.0));
  110. }
  111. }
  112. }
  113. }
  114. self.render_start();
  115. }
  116. pub fn change_tick_size(&mut self, trades_raw: Vec<Trade>, new_tick_size: f32) {
  117. let mut new_data_points = BTreeMap::new();
  118. let aggregate_time = 1000 * 60 * self.timeframe as i64;
  119. for (time, (_, kline_values)) in self.data_points.iter() {
  120. new_data_points.entry(*time).or_insert((HashMap::new(), *kline_values));
  121. }
  122. for trade in trades_raw {
  123. let rounded_time = (trade.time / aggregate_time) * aggregate_time;
  124. let price_level = (trade.price / new_tick_size).round() as i64 * (new_tick_size * 100.0) as i64;
  125. let entry = new_data_points
  126. .entry(rounded_time)
  127. .or_insert((HashMap::new(), (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)));
  128. if let Some((buy_qty, sell_qty)) = entry.0.get_mut(&price_level) {
  129. if trade.is_sell {
  130. *sell_qty += trade.qty;
  131. } else {
  132. *buy_qty += trade.qty;
  133. }
  134. } else {
  135. if trade.is_sell {
  136. entry.0.insert(price_level, (0.0, trade.qty));
  137. } else {
  138. entry.0.insert(price_level, (trade.qty, 0.0));
  139. }
  140. }
  141. }
  142. self.data_points = new_data_points;
  143. self.tick_size = new_tick_size;
  144. }
  145. pub fn update_latest_kline(&mut self, kline: Kline) {
  146. if let Some((_, kline_value)) = self.data_points.get_mut(&(kline.time as i64)) {
  147. kline_value.0 = kline.open;
  148. kline_value.1 = kline.high;
  149. kline_value.2 = kline.low;
  150. kline_value.3 = kline.close;
  151. kline_value.4 = kline.taker_buy_base_asset_volume;
  152. kline_value.5 = kline.volume - kline.taker_buy_base_asset_volume;
  153. }
  154. }
  155. pub fn render_start(&mut self) {
  156. let timestamp_latest = self.data_points.keys().last().unwrap_or(&0);
  157. let latest: i64 = *timestamp_latest as i64 - ((self.translation.x*1000.0)*(self.timeframe as f32)) as i64;
  158. let earliest: i64 = latest - ((640000.0*self.timeframe as f32) / (self.scaling / (self.bounds.width/800.0))) as i64;
  159. let mut highest: f32 = 0.0;
  160. let mut lowest: f32 = std::f32::MAX;
  161. for (_, (_, kline)) in self.data_points.range(earliest..=latest) {
  162. if kline.1 > highest {
  163. highest = kline.1;
  164. }
  165. if kline.2 < lowest {
  166. lowest = kline.2;
  167. }
  168. }
  169. if highest == 0.0 || lowest == std::f32::MAX || lowest == 0.0 {
  170. return;
  171. }
  172. highest = highest + (highest - lowest) * 0.05;
  173. lowest = lowest - (highest - lowest) * 0.05;
  174. if earliest != self.x_min_time || latest != self.x_max_time {
  175. self.x_labels_cache.clear();
  176. self.x_crosshair_cache.clear();
  177. }
  178. if lowest != self.y_min_price || highest != self.y_max_price {
  179. self.y_labels_cache.clear();
  180. self.y_croshair_cache.clear();
  181. }
  182. self.x_min_time = earliest;
  183. self.x_max_time = latest;
  184. self.y_min_price = lowest;
  185. self.y_max_price = highest;
  186. self.crosshair_cache.clear();
  187. self.heatmap_cache.clear();
  188. }
  189. pub fn update(&mut self, message: Message) {
  190. match message {
  191. Message::Translated(translation) => {
  192. if self.autoscale {
  193. self.translation.x = translation.x;
  194. } else {
  195. self.translation = translation;
  196. }
  197. self.crosshair_position = Point::new(0.0, 0.0);
  198. self.render_start();
  199. }
  200. Message::Scaled(scaling, translation) => {
  201. self.scaling = scaling;
  202. if let Some(translation) = translation {
  203. if self.autoscale {
  204. self.translation.x = translation.x;
  205. } else {
  206. self.translation = translation;
  207. }
  208. }
  209. self.crosshair_position = Point::new(0.0, 0.0);
  210. self.render_start();
  211. }
  212. Message::ChartBounds(bounds) => {
  213. self.bounds = bounds;
  214. }
  215. Message::AutoscaleToggle => {
  216. self.autoscale = !self.autoscale;
  217. }
  218. Message::CrosshairToggle => {
  219. self.crosshair = !self.crosshair;
  220. }
  221. Message::CrosshairMoved(position) => {
  222. self.crosshair_position = position;
  223. if self.crosshair {
  224. self.crosshair_cache.clear();
  225. self.y_croshair_cache.clear();
  226. self.x_crosshair_cache.clear();
  227. }
  228. }
  229. }
  230. }
  231. pub fn view(&self) -> Element<Message> {
  232. let chart = Canvas::new(self)
  233. .width(Length::FillPortion(10))
  234. .height(Length::FillPortion(10));
  235. let axis_labels_x = Canvas::new(
  236. AxisLabelXCanvas {
  237. labels_cache: &self.x_labels_cache,
  238. min: self.x_min_time,
  239. max: self.x_max_time,
  240. crosshair_cache: &self.x_crosshair_cache,
  241. crosshair_position: self.crosshair_position,
  242. crosshair: self.crosshair,
  243. timeframe: self.timeframe
  244. })
  245. .width(Length::FillPortion(10))
  246. .height(Length::Fixed(26.0));
  247. let axis_labels_y = Canvas::new(
  248. AxisLabelYCanvas {
  249. labels_cache: &self.y_labels_cache,
  250. y_croshair_cache: &self.y_croshair_cache,
  251. min: self.y_min_price,
  252. max: self.y_max_price,
  253. crosshair_position: self.crosshair_position,
  254. crosshair: self.crosshair
  255. })
  256. .width(Length::Fixed(60.0))
  257. .height(Length::FillPortion(10));
  258. let autoscale_button = button(
  259. Text::new("A")
  260. .size(12)
  261. .horizontal_alignment(alignment::Horizontal::Center)
  262. )
  263. .width(Length::Fill)
  264. .height(Length::Fill)
  265. .on_press(Message::AutoscaleToggle)
  266. .style(|_theme: &Theme, _status: iced::widget::button::Status| chart_button(_theme, &_status, self.autoscale));
  267. let crosshair_button = button(
  268. Text::new("+")
  269. .size(12)
  270. .horizontal_alignment(alignment::Horizontal::Center)
  271. )
  272. .width(Length::Fill)
  273. .height(Length::Fill)
  274. .on_press(Message::CrosshairToggle)
  275. .style(|_theme: &Theme, _status: iced::widget::button::Status| chart_button(_theme, &_status, self.crosshair));
  276. let chart_controls = Container::new(
  277. Row::new()
  278. .push(autoscale_button)
  279. .push(crosshair_button).spacing(2)
  280. ).padding([0, 2, 0, 2])
  281. .width(Length::Fixed(60.0))
  282. .height(Length::Fixed(26.0));
  283. let chart_and_y_labels = Row::new()
  284. .push(chart)
  285. .push(axis_labels_y);
  286. let bottom_row = Row::new()
  287. .push(axis_labels_x)
  288. .push(chart_controls);
  289. let content = Column::new()
  290. .push(chart_and_y_labels)
  291. .push(bottom_row)
  292. .spacing(0)
  293. .padding(5);
  294. content.into()
  295. }
  296. }
  297. fn chart_button(_theme: &Theme, _status: &button::Status, is_active: bool) -> button::Style {
  298. button::Style {
  299. background: Some(Color::from_rgba8(20, 20, 20, 1.0).into()),
  300. border: Border {
  301. color: {
  302. if is_active {
  303. Color::from_rgba8(50, 50, 50, 1.0)
  304. } else {
  305. Color::from_rgba8(20, 20, 20, 1.0)
  306. }
  307. },
  308. width: 1.0,
  309. radius: 2.0.into(),
  310. },
  311. text_color: Color::WHITE,
  312. ..button::Style::default()
  313. }
  314. }
  315. #[derive(Debug, Clone, Copy)]
  316. pub enum Interaction {
  317. None,
  318. Drawing,
  319. Erasing,
  320. Panning { translation: Vector, start: Point },
  321. }
  322. impl Default for Interaction {
  323. fn default() -> Self {
  324. Self::None
  325. }
  326. }
  327. impl canvas::Program<Message> for Footprint {
  328. type State = Interaction;
  329. fn update(
  330. &self,
  331. interaction: &mut Interaction,
  332. event: Event,
  333. bounds: Rectangle,
  334. cursor: mouse::Cursor,
  335. ) -> (event::Status, Option<Message>) {
  336. if bounds != self.bounds {
  337. return (event::Status::Ignored, Some(Message::ChartBounds(bounds)));
  338. }
  339. if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event {
  340. *interaction = Interaction::None;
  341. }
  342. let Some(cursor_position) = cursor.position_in(bounds) else {
  343. return (event::Status::Ignored,
  344. if self.crosshair {
  345. Some(Message::CrosshairMoved(Point::new(0.0, 0.0)))
  346. } else {
  347. None
  348. }
  349. );
  350. };
  351. match event {
  352. Event::Mouse(mouse_event) => match mouse_event {
  353. mouse::Event::ButtonPressed(button) => {
  354. let message = match button {
  355. mouse::Button::Right => {
  356. *interaction = Interaction::Drawing;
  357. None
  358. }
  359. mouse::Button::Left => {
  360. *interaction = Interaction::Panning {
  361. translation: self.translation,
  362. start: cursor_position,
  363. };
  364. None
  365. }
  366. _ => None,
  367. };
  368. (event::Status::Captured, message)
  369. }
  370. mouse::Event::CursorMoved { .. } => {
  371. let message = match *interaction {
  372. Interaction::Drawing => None,
  373. Interaction::Erasing => None,
  374. Interaction::Panning { translation, start } => {
  375. Some(Message::Translated(
  376. translation
  377. + (cursor_position - start)
  378. * (1.0 / self.scaling),
  379. ))
  380. }
  381. Interaction::None =>
  382. if self.crosshair && cursor.is_over(bounds) {
  383. Some(Message::CrosshairMoved(cursor_position))
  384. } else {
  385. None
  386. },
  387. };
  388. let event_status = match interaction {
  389. Interaction::None => event::Status::Ignored,
  390. _ => event::Status::Captured,
  391. };
  392. (event_status, message)
  393. }
  394. mouse::Event::WheelScrolled { delta } => match delta {
  395. mouse::ScrollDelta::Lines { y, .. }
  396. | mouse::ScrollDelta::Pixels { y, .. } => {
  397. if y < 0.0 && self.scaling > Self::MIN_SCALING
  398. || y > 0.0 && self.scaling < Self::MAX_SCALING
  399. {
  400. //let old_scaling = self.scaling;
  401. let scaling = (self.scaling * (1.0 + y / 30.0))
  402. .clamp(
  403. Self::MIN_SCALING, // 0.1
  404. Self::MAX_SCALING, // 2.0
  405. );
  406. //let translation =
  407. // if let Some(cursor_to_center) =
  408. // cursor.position_from(bounds.center())
  409. // {
  410. // let factor = scaling - old_scaling;
  411. // Some(
  412. // self.translation
  413. // - Vector::new(
  414. // cursor_to_center.x * factor
  415. // / (old_scaling
  416. // * old_scaling),
  417. // cursor_to_center.y * factor
  418. // / (old_scaling
  419. // * old_scaling),
  420. // ),
  421. // )
  422. // } else {
  423. // None
  424. // };
  425. (
  426. event::Status::Captured,
  427. Some(Message::Scaled(scaling, None)),
  428. )
  429. } else {
  430. (event::Status::Captured, None)
  431. }
  432. }
  433. },
  434. _ => (event::Status::Ignored, None),
  435. },
  436. _ => (event::Status::Ignored, None),
  437. }
  438. }
  439. fn draw(
  440. &self,
  441. _state: &Self::State,
  442. renderer: &Renderer,
  443. _theme: &Theme,
  444. bounds: Rectangle,
  445. cursor: mouse::Cursor,
  446. ) -> Vec<Geometry> {
  447. let (latest, earliest) = (self.x_max_time, self.x_min_time);
  448. let (lowest, highest) = (self.y_min_price, self.y_max_price);
  449. let y_range: f32 = highest - lowest;
  450. if y_range == 0.0 {
  451. return vec![];
  452. }
  453. let volume_area_height: f32 = bounds.height / 8.0;
  454. let heatmap_area_height: f32 = bounds.height - volume_area_height;
  455. let heatmap = self.heatmap_cache.draw(renderer, bounds.size(), |frame| {
  456. let mut max_trade_qty: f32 = 0.0;
  457. let mut max_volume: f32 = 0.0;
  458. for (_, (trades, kline)) in self.data_points.range(earliest..=latest) {
  459. for trade in trades {
  460. max_trade_qty = max_trade_qty.max(trade.1.0.max(trade.1.1));
  461. }
  462. max_volume = max_volume.max(kline.4.max(kline.5));
  463. }
  464. for (time, (trades, kline)) in self.data_points.range(earliest..=latest) {
  465. let x_position: f32 = ((time - earliest) as f32 / (latest - earliest) as f32) * bounds.width as f32;
  466. if x_position.is_nan() || x_position.is_infinite() {
  467. continue;
  468. }
  469. for trade in trades {
  470. let price = *trade.0 as f32 / 100.0;
  471. let y_position = heatmap_area_height - ((price - lowest) / y_range * heatmap_area_height);
  472. if trade.1.0 > 0.0 {
  473. let bar_width = (trade.1.0 / max_trade_qty) * bounds.width / 30.0 * self.scaling;
  474. let bar = Path::rectangle(
  475. Point::new(x_position + (5.0*self.scaling), y_position),
  476. Size::new(bar_width, 1.0)
  477. );
  478. frame.fill(&bar, Color::from_rgba8(81, 205, 160, 1.0));
  479. }
  480. if trade.1.1 > 0.0 {
  481. let bar_width = -(trade.1.1 / max_trade_qty) * bounds.width / 30.0 * self.scaling;
  482. let bar = Path::rectangle(
  483. Point::new(x_position - (5.0*self.scaling), y_position),
  484. Size::new(bar_width, 1.0)
  485. );
  486. frame.fill(&bar, Color::from_rgba8(192, 80, 77, 1.0));
  487. };
  488. }
  489. if max_volume > 0.0 {
  490. let buy_bar_height = (kline.4 / max_volume) * volume_area_height;
  491. let sell_bar_height = (kline.5 / max_volume) * volume_area_height;
  492. let sell_bar_width = 8.0 * self.scaling;
  493. let sell_bar_x_position = x_position - (5.0*self.scaling) - sell_bar_width;
  494. let sell_bar = Path::rectangle(
  495. Point::new(sell_bar_x_position, (bounds.height - sell_bar_height) as f32),
  496. Size::new(sell_bar_width, sell_bar_height as f32)
  497. );
  498. frame.fill(&sell_bar, Color::from_rgb8(192, 80, 77));
  499. let buy_bar = Path::rectangle(
  500. Point::new(x_position + (5.0*self.scaling), (bounds.height - buy_bar_height) as f32),
  501. Size::new(8.0 * self.scaling, buy_bar_height as f32)
  502. );
  503. frame.fill(&buy_bar, Color::from_rgb8(81, 205, 160));
  504. }
  505. let y_open = heatmap_area_height - ((kline.0 - lowest) / y_range * heatmap_area_height);
  506. let y_high = heatmap_area_height - ((kline.1 - lowest) / y_range * heatmap_area_height);
  507. let y_low = heatmap_area_height - ((kline.2 - lowest) / y_range * heatmap_area_height);
  508. let y_close = heatmap_area_height - ((kline.3 - lowest) / y_range * heatmap_area_height);
  509. let color = if kline.3 >= kline.0 { Color::from_rgba8(81, 205, 160, 0.6) } else { Color::from_rgba8(192, 80, 77, 0.6) };
  510. let wick = Path::line(
  511. Point::new(x_position as f32, y_high),
  512. Point::new(x_position as f32, y_low)
  513. );
  514. frame.stroke(&wick, Stroke::default().with_color(color).with_width(1.0));
  515. let body = Path::rectangle(
  516. Point::new(x_position as f32 - (2.0 * self.scaling), y_open.min(y_close)),
  517. Size::new(4.0 * self.scaling, (y_open - y_close).abs())
  518. );
  519. frame.fill(&body, color);
  520. }
  521. let text_size = 9.0;
  522. let text_content = format!("{:.2}", max_volume);
  523. let text_width = (text_content.len() as f32 * text_size) / 1.5;
  524. let text_position = Point::new(bounds.width - text_width, bounds.height - volume_area_height);
  525. frame.fill_text(canvas::Text {
  526. content: text_content,
  527. position: text_position,
  528. size: iced::Pixels(text_size),
  529. color: Color::from_rgba8(81, 81, 81, 1.0),
  530. ..canvas::Text::default()
  531. });
  532. });
  533. if self.crosshair {
  534. let crosshair = self.crosshair_cache.draw(renderer, bounds.size(), |frame| {
  535. if let Some(cursor_position) = cursor.position_in(bounds) {
  536. let line = Path::line(
  537. Point::new(0.0, cursor_position.y),
  538. Point::new(bounds.width as f32, cursor_position.y)
  539. );
  540. frame.stroke(&line, Stroke::default().with_color(Color::from_rgba8(200, 200, 200, 0.6)).with_width(1.0));
  541. let crosshair_ratio = cursor_position.x as f64 / bounds.width as f64;
  542. let crosshair_millis = earliest as f64 + crosshair_ratio * (latest - earliest) as f64;
  543. let rounded_timestamp = (crosshair_millis / (self.timeframe as f64 * 60.0 * 1000.0)).round() as i64 * self.timeframe as i64 * 60 * 1000;
  544. let snap_ratio = (rounded_timestamp as f64 - earliest as f64) / (latest as f64 - earliest as f64);
  545. let snap_x = snap_ratio * bounds.width as f64;
  546. let line = Path::line(
  547. Point::new(snap_x as f32, 0.0),
  548. Point::new(snap_x as f32, bounds.height as f32)
  549. );
  550. frame.stroke(&line, Stroke::default().with_color(Color::from_rgba8(200, 200, 200, 0.6)).with_width(1.0));
  551. if let Some((_, kline)) = self.data_points.iter()
  552. .find(|(time, _)| **time == rounded_timestamp) {
  553. let tooltip_text = format!(
  554. "O: {} H: {} L: {} C: {}\nBuyV: {:.0} SellV: {:.0}",
  555. kline.1.0, kline.1.1, kline.1.2, kline.1.3, kline.1.4, kline.1.5
  556. );
  557. let text = canvas::Text {
  558. content: tooltip_text,
  559. position: Point::new(10.0, 10.0),
  560. size: iced::Pixels(12.0),
  561. color: Color::from_rgba8(120, 120, 120, 1.0),
  562. ..canvas::Text::default()
  563. };
  564. frame.fill_text(text);
  565. }
  566. }
  567. });
  568. return vec![crosshair, heatmap];
  569. } else {
  570. return vec![heatmap];
  571. }
  572. }
  573. fn mouse_interaction(
  574. &self,
  575. interaction: &Interaction,
  576. bounds: Rectangle,
  577. cursor: mouse::Cursor,
  578. ) -> mouse::Interaction {
  579. match interaction {
  580. Interaction::Drawing => mouse::Interaction::Crosshair,
  581. Interaction::Erasing => mouse::Interaction::Crosshair,
  582. Interaction::Panning { .. } => mouse::Interaction::Grabbing,
  583. Interaction::None if cursor.is_over(bounds) => {
  584. if self.crosshair {
  585. mouse::Interaction::Crosshair
  586. } else {
  587. mouse::Interaction::default()
  588. }
  589. }
  590. Interaction::None => { mouse::Interaction::default() }
  591. }
  592. }
  593. }
  594. const PRICE_STEPS: [f32; 15] = [
  595. 1000.0,
  596. 500.0,
  597. 200.0,
  598. 100.0,
  599. 50.0,
  600. 20.0,
  601. 10.0,
  602. 5.0,
  603. 2.0,
  604. 1.0,
  605. 0.5,
  606. 0.2,
  607. 0.1,
  608. 0.05,
  609. 0.01,
  610. ];
  611. fn calculate_price_step(highest: f32, lowest: f32, labels_can_fit: i32) -> (f32, f32) {
  612. let range = highest - lowest;
  613. let mut step = 1000.0;
  614. for &s in PRICE_STEPS.iter().rev() {
  615. if range / s <= labels_can_fit as f32 {
  616. step = s;
  617. break;
  618. }
  619. }
  620. let rounded_lowest = (lowest / step).floor() * step;
  621. (step, rounded_lowest)
  622. }
  623. const M1_TIME_STEPS: [i64; 5] = [
  624. 1000 * 60 * 30, // 30 minutes
  625. 1000 * 60 * 15, // 15 minutes
  626. 1000 * 60 * 5, // 5 minutes
  627. 1000 * 60 * 2, // 2 minutes
  628. 60 * 1000, // 1 minute
  629. ];
  630. const M3_TIME_STEPS: [i64; 5] = [
  631. 1000 * 60 * 60, // 1 hour
  632. 1000 * 60 * 30, // 30 minutes
  633. 1000 * 60 * 15, // 15 minutes
  634. 1000 * 60 * 9, // 9 minutes
  635. 1000 * 60 * 3, // 3 minutes
  636. ];
  637. const M5_TIME_STEPS: [i64; 5] = [
  638. 1000 * 60 * 60, // 1 hour
  639. 1000 * 60 * 30, // 30 minutes
  640. 1000 * 60 * 15, // 15 minutes
  641. 1000 * 60 * 5, // 5 minutes
  642. 1000 * 60 * 2, // 2 minutes
  643. ];
  644. const M15_TIME_STEPS: [i64; 5] = [
  645. 1000 * 60 * 240, // 4 hour
  646. 1000 * 60 * 120, // 2 hour
  647. 1000 * 60 * 60, // 1 hour
  648. 1000 * 60 * 30, // 30 minutes
  649. 1000 * 60 * 15, // 15 minutes
  650. ];
  651. const M30_TIME_STEPS: [i64; 5] = [
  652. 1000 * 60 * 480, // 8 hour
  653. 1000 * 60 * 240, // 4 hour
  654. 1000 * 60 * 120, // 2 hour
  655. 1000 * 60 * 60, // 1 hour
  656. 1000 * 60 * 30, // 30 minutes
  657. ];
  658. fn calculate_time_step(earliest: i64, latest: i64, labels_can_fit: i32, timeframe: u16) -> (i64, i64) {
  659. let duration = latest - earliest;
  660. let time_steps = match timeframe {
  661. 1 => &M1_TIME_STEPS,
  662. 3 => &M3_TIME_STEPS,
  663. 5 => &M5_TIME_STEPS,
  664. 15 => &M15_TIME_STEPS,
  665. 30 => &M30_TIME_STEPS,
  666. _ => &M1_TIME_STEPS,
  667. };
  668. let mut selected_step = time_steps[0];
  669. for &step in time_steps.iter() {
  670. if duration / step >= labels_can_fit as i64 {
  671. selected_step = step;
  672. break;
  673. }
  674. if step <= duration {
  675. selected_step = step;
  676. }
  677. }
  678. let rounded_earliest = (earliest / selected_step) * selected_step;
  679. (selected_step, rounded_earliest)
  680. }
  681. pub struct AxisLabelXCanvas<'a> {
  682. labels_cache: &'a Cache,
  683. crosshair_cache: &'a Cache,
  684. crosshair_position: Point,
  685. crosshair: bool,
  686. min: i64,
  687. max: i64,
  688. timeframe: u16,
  689. }
  690. impl canvas::Program<Message> for AxisLabelXCanvas<'_> {
  691. type State = Interaction;
  692. fn update(
  693. &self,
  694. _interaction: &mut Interaction,
  695. _event: Event,
  696. _bounds: Rectangle,
  697. _cursor: mouse::Cursor,
  698. ) -> (event::Status, Option<Message>) {
  699. (event::Status::Ignored, None)
  700. }
  701. fn draw(
  702. &self,
  703. _state: &Self::State,
  704. renderer: &Renderer,
  705. _theme: &Theme,
  706. bounds: Rectangle,
  707. _cursor: mouse::Cursor,
  708. ) -> Vec<Geometry> {
  709. if self.max == 0 {
  710. return vec![];
  711. }
  712. let latest_in_millis = self.max;
  713. let earliest_in_millis = self.min;
  714. let x_labels_can_fit = (bounds.width / 120.0) as i32;
  715. let (time_step, rounded_earliest) = calculate_time_step(self.min, self.max, x_labels_can_fit, self.timeframe);
  716. let labels = self.labels_cache.draw(renderer, bounds.size(), |frame| {
  717. frame.with_save(|frame| {
  718. let mut time: i64 = rounded_earliest;
  719. let latest_time: i64 = latest_in_millis;
  720. while time <= latest_time {
  721. 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;
  722. if x_position >= 0.0 && x_position <= bounds.width as f64 {
  723. let text_size = 12.0;
  724. let time_as_datetime = NaiveDateTime::from_timestamp((time / 1000) as i64, 0);
  725. let label = canvas::Text {
  726. content: time_as_datetime.format("%H:%M").to_string(),
  727. position: Point::new(x_position as f32 - text_size, bounds.height as f32 - 20.0),
  728. size: iced::Pixels(text_size),
  729. color: Color::from_rgba8(200, 200, 200, 1.0),
  730. ..canvas::Text::default()
  731. };
  732. label.draw_with(|path, color| {
  733. frame.fill(&path, color);
  734. });
  735. }
  736. time = time + time_step;
  737. }
  738. let line = Path::line(
  739. Point::new(0.0, bounds.height as f32 - 30.0),
  740. Point::new(bounds.width as f32, bounds.height as f32 - 30.0)
  741. );
  742. frame.stroke(&line, Stroke::default().with_color(Color::from_rgba8(81, 81, 81, 0.2)).with_width(1.0));
  743. });
  744. });
  745. let crosshair = self.crosshair_cache.draw(renderer, bounds.size(), |frame| {
  746. if self.crosshair && self.crosshair_position.x > 0.0 {
  747. let crosshair_ratio = self.crosshair_position.x as f64 / bounds.width as f64;
  748. let crosshair_millis = earliest_in_millis as f64 + crosshair_ratio * (latest_in_millis - earliest_in_millis) as f64;
  749. let crosshair_time = NaiveDateTime::from_timestamp((crosshair_millis / 1000.0) as i64, 0);
  750. let crosshair_timestamp = crosshair_time.timestamp();
  751. let rounded_timestamp = (crosshair_timestamp as f64 / (self.timeframe as f64 * 60.0)).round() as i64 * self.timeframe as i64 * 60;
  752. let rounded_time = NaiveDateTime::from_timestamp(rounded_timestamp, 0);
  753. let snap_ratio = (rounded_timestamp as f64 * 1000.0 - earliest_in_millis as f64) / (latest_in_millis as f64 - earliest_in_millis as f64);
  754. let snap_x = snap_ratio * bounds.width as f64;
  755. let text_size = 12.0;
  756. let text_content = rounded_time.format("%H:%M").to_string();
  757. let growth_amount = 6.0;
  758. let rectangle_position = Point::new(snap_x as f32 - 14.0 - growth_amount, bounds.height as f32 - 20.0);
  759. let text_position = Point::new(snap_x as f32 - 14.0, bounds.height as f32 - 20.0);
  760. let text_background = canvas::Path::rectangle(rectangle_position, Size::new(text_content.len() as f32 * text_size/2.0 + 2.0 * growth_amount + 1.0, text_size + text_size/2.0));
  761. frame.fill(&text_background, Color::from_rgba8(200, 200, 200, 1.0));
  762. let crosshair_label = canvas::Text {
  763. content: text_content,
  764. position: text_position,
  765. size: iced::Pixels(text_size),
  766. color: Color::from_rgba8(0, 0, 0, 1.0),
  767. ..canvas::Text::default()
  768. };
  769. crosshair_label.draw_with(|path, color| {
  770. frame.fill(&path, color);
  771. });
  772. }
  773. });
  774. vec![labels, crosshair]
  775. }
  776. fn mouse_interaction(
  777. &self,
  778. interaction: &Interaction,
  779. bounds: Rectangle,
  780. cursor: mouse::Cursor,
  781. ) -> mouse::Interaction {
  782. match interaction {
  783. Interaction::Drawing => mouse::Interaction::Crosshair,
  784. Interaction::Erasing => mouse::Interaction::Crosshair,
  785. Interaction::Panning { .. } => mouse::Interaction::ResizingHorizontally,
  786. Interaction::None if cursor.is_over(bounds) => {
  787. mouse::Interaction::ResizingHorizontally
  788. }
  789. Interaction::None => mouse::Interaction::default(),
  790. }
  791. }
  792. }
  793. pub struct AxisLabelYCanvas<'a> {
  794. labels_cache: &'a Cache,
  795. y_croshair_cache: &'a Cache,
  796. min: f32,
  797. max: f32,
  798. crosshair_position: Point,
  799. crosshair: bool,
  800. }
  801. impl canvas::Program<Message> for AxisLabelYCanvas<'_> {
  802. type State = Interaction;
  803. fn update(
  804. &self,
  805. _interaction: &mut Interaction,
  806. _event: Event,
  807. _bounds: Rectangle,
  808. _cursor: mouse::Cursor,
  809. ) -> (event::Status, Option<Message>) {
  810. (event::Status::Ignored, None)
  811. }
  812. fn draw(
  813. &self,
  814. _state: &Self::State,
  815. renderer: &Renderer,
  816. _theme: &Theme,
  817. bounds: Rectangle,
  818. _cursor: mouse::Cursor,
  819. ) -> Vec<Geometry> {
  820. if self.max == 0.0 {
  821. return vec![];
  822. }
  823. let y_labels_can_fit = (bounds.height / 32.0) as i32;
  824. let (step, rounded_lowest) = calculate_price_step(self.max, self.min, y_labels_can_fit);
  825. let volume_area_height = bounds.height / 8.0;
  826. let candlesticks_area_height = bounds.height - volume_area_height;
  827. let labels = self.labels_cache.draw(renderer, bounds.size(), |frame| {
  828. frame.with_save(|frame| {
  829. let y_range = self.max - self.min;
  830. let mut y = rounded_lowest;
  831. while y <= self.max {
  832. let y_position = candlesticks_area_height - ((y - self.min) / y_range * candlesticks_area_height);
  833. let text_size = 12.0;
  834. let decimal_places = if step < 0.5 { 2 } else if step < 1.0 { 1 } else { 0 };
  835. let label_content = format!("{:.*}", decimal_places, y);
  836. let label = canvas::Text {
  837. content: label_content,
  838. position: Point::new(10.0, y_position - text_size / 2.0),
  839. size: iced::Pixels(text_size),
  840. color: Color::from_rgba8(200, 200, 200, 1.0),
  841. ..canvas::Text::default()
  842. };
  843. label.draw_with(|path, color| {
  844. frame.fill(&path, color);
  845. });
  846. y += step;
  847. }
  848. });
  849. });
  850. let crosshair = self.y_croshair_cache.draw(renderer, bounds.size(), |frame| {
  851. if self.crosshair && self.crosshair_position.y > 0.0 {
  852. let text_size = 12.0;
  853. let y_range = self.max - self.min;
  854. let decimal_places = if step < 1.0 { 2 } else { 1 };
  855. let label_content = format!("{:.*}", decimal_places, self.min + (y_range * (candlesticks_area_height - self.crosshair_position.y) / candlesticks_area_height));
  856. let growth_amount = 3.0;
  857. let rectangle_position = Point::new(8.0 - growth_amount, self.crosshair_position.y - text_size / 2.0 - 3.0);
  858. let text_position = Point::new(8.0, self.crosshair_position.y - text_size / 2.0 - 3.0);
  859. let text_background = canvas::Path::rectangle(rectangle_position, Size::new(label_content.len() as f32 * text_size / 2.0 + 2.0 * growth_amount + 4.0, text_size + text_size / 1.8));
  860. frame.fill(&text_background, Color::from_rgba8(200, 200, 200, 1.0));
  861. let label = canvas::Text {
  862. content: label_content,
  863. position: text_position,
  864. size: iced::Pixels(text_size),
  865. color: Color::from_rgba8(0, 0, 0, 1.0),
  866. ..canvas::Text::default()
  867. };
  868. label.draw_with(|path, color| {
  869. frame.fill(&path, color);
  870. });
  871. }
  872. });
  873. vec![labels, crosshair]
  874. }
  875. fn mouse_interaction(
  876. &self,
  877. interaction: &Interaction,
  878. bounds: Rectangle,
  879. cursor: mouse::Cursor,
  880. ) -> mouse::Interaction {
  881. match interaction {
  882. Interaction::Drawing => mouse::Interaction::Crosshair,
  883. Interaction::Erasing => mouse::Interaction::Crosshair,
  884. Interaction::Panning { .. } => mouse::Interaction::ResizingVertically,
  885. Interaction::None if cursor.is_over(bounds) => {
  886. mouse::Interaction::ResizingVertically
  887. }
  888. Interaction::None => mouse::Interaction::default(),
  889. }
  890. }
  891. }