open_interest.rs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. use std::collections::BTreeMap;
  2. use iced::widget::{container, row, Canvas};
  3. use iced::{mouse, Element, Length, Point, Rectangle, Renderer, Size, Theme, Vector};
  4. use iced::widget::canvas::{self, Cache, Event, Geometry, LineDash, Path, Stroke};
  5. use crate::charts::{
  6. calc_price_step, convert_to_qty_abbr, round_to_tick, AxisLabel, Caches, CommonChartData, Interaction, Label, Message
  7. };
  8. use crate::data_providers::format_with_commas;
  9. pub fn create_indicator_elem<'a>(
  10. chart_state: &'a CommonChartData,
  11. cache: &'a Caches,
  12. data: &'a BTreeMap<i64, f32>,
  13. earliest: i64,
  14. latest: i64,
  15. ) -> Element<'a, Message> {
  16. let indi_chart = Canvas::new(OpenInterest {
  17. indicator_cache: &cache.main,
  18. crosshair_cache: &cache.crosshair,
  19. crosshair: chart_state.crosshair,
  20. max: chart_state.latest_x,
  21. scaling: chart_state.scaling,
  22. translation_x: chart_state.translation.x,
  23. timeframe: chart_state.timeframe as u32,
  24. cell_width: chart_state.cell_width,
  25. data_points: data,
  26. chart_bounds: chart_state.bounds,
  27. })
  28. .height(Length::Fill)
  29. .width(Length::Fill);
  30. let mut max_value: f32 = f32::MIN;
  31. let mut min_value: f32 = f32::MAX;
  32. data.range(earliest..=latest)
  33. .for_each(|(_, value)| {
  34. max_value = max_value.max(*value);
  35. min_value = min_value.min(*value);
  36. });
  37. let value_range = max_value - min_value;
  38. let padding = value_range * 0.01;
  39. max_value += padding;
  40. min_value -= padding;
  41. let indi_labels = Canvas::new(OpenInterestLabels {
  42. label_cache: &cache.y_labels,
  43. max: max_value,
  44. min: min_value,
  45. crosshair: chart_state.crosshair,
  46. chart_bounds: chart_state.bounds,
  47. })
  48. .height(Length::Fill)
  49. .width(Length::Fixed(60.0 + (chart_state.decimals as f32 * 2.0)));
  50. row![
  51. indi_chart,
  52. container(indi_labels),
  53. ].into()
  54. }
  55. pub struct OpenInterest<'a> {
  56. pub indicator_cache: &'a Cache,
  57. pub crosshair_cache: &'a Cache,
  58. pub crosshair: bool,
  59. pub max: i64,
  60. pub scaling: f32,
  61. pub translation_x: f32,
  62. pub timeframe: u32,
  63. pub cell_width: f32,
  64. pub data_points: &'a BTreeMap<i64, f32>,
  65. pub chart_bounds: Rectangle,
  66. }
  67. impl OpenInterest<'_> {
  68. fn visible_region(&self, size: Size) -> Rectangle {
  69. let width = size.width / self.scaling;
  70. let height = size.height / self.scaling;
  71. Rectangle {
  72. x: -self.translation_x - width / 2.0,
  73. y: 0.0,
  74. width,
  75. height,
  76. }
  77. }
  78. fn x_to_time(&self, x: f32) -> i64 {
  79. let time_per_cell = self.timeframe;
  80. self.max + ((x / self.cell_width) * time_per_cell as f32) as i64
  81. }
  82. fn time_to_x(&self, time: i64) -> f32 {
  83. let time_per_cell = self.timeframe;
  84. let x = (time - self.max) as f32 / time_per_cell as f32;
  85. x * self.cell_width
  86. }
  87. }
  88. impl canvas::Program<Message> for OpenInterest<'_> {
  89. type State = Interaction;
  90. fn update(
  91. &self,
  92. interaction: &mut Interaction,
  93. event: Event,
  94. bounds: Rectangle,
  95. cursor: mouse::Cursor,
  96. ) -> Option<canvas::Action<Message>> {
  97. match event {
  98. Event::Mouse(mouse::Event::CursorMoved { .. }) => {
  99. let message = match *interaction {
  100. Interaction::None => {
  101. if self.crosshair && cursor.is_over(bounds) {
  102. Some(Message::CrosshairMoved)
  103. } else {
  104. None
  105. }
  106. }
  107. _ => None,
  108. };
  109. let action =
  110. message.map_or(canvas::Action::request_redraw(), canvas::Action::publish);
  111. Some(match interaction {
  112. Interaction::None => action,
  113. _ => action.and_capture(),
  114. })
  115. }
  116. _ => None,
  117. }
  118. }
  119. fn draw(
  120. &self,
  121. _state: &Self::State,
  122. renderer: &Renderer,
  123. theme: &Theme,
  124. bounds: Rectangle,
  125. cursor: mouse::Cursor,
  126. ) -> Vec<Geometry> {
  127. if self.data_points.is_empty() {
  128. return vec![];
  129. }
  130. let center = Vector::new(bounds.width / 2.0, bounds.height / 2.0);
  131. let palette = theme.extended_palette();
  132. let indicator = self.indicator_cache.draw(renderer, bounds.size(), |frame| {
  133. frame.translate(center);
  134. frame.scale(self.scaling);
  135. frame.translate(Vector::new(
  136. self.translation_x,
  137. (-bounds.height / self.scaling) / 2.0,
  138. ));
  139. let region = self.visible_region(frame.size());
  140. frame.fill_rectangle(
  141. Point::new(region.x, 0.0),
  142. Size::new(region.width, 1.0 / self.scaling),
  143. if palette.is_dark {
  144. palette.background.weak.color.scale_alpha(0.2)
  145. } else {
  146. palette.background.strong.color.scale_alpha(0.2)
  147. },
  148. );
  149. let (earliest, latest) = (
  150. self.x_to_time(region.x) - i64::from(self.timeframe / 2),
  151. self.x_to_time(region.x + region.width) + i64::from(self.timeframe / 2),
  152. );
  153. let mut max_value: f32 = f32::MIN;
  154. let mut min_value: f32 = f32::MAX;
  155. self.data_points
  156. .range(earliest..=latest)
  157. .for_each(|(_, value)| {
  158. max_value = max_value.max(*value);
  159. min_value = min_value.min(*value);
  160. });
  161. let padding = (max_value - min_value) * 0.08;
  162. max_value += padding;
  163. min_value -= padding;
  164. let points: Vec<Point> = self.data_points
  165. .range(earliest..=latest)
  166. .map(|(timestamp, value)| {
  167. let x_position = self.time_to_x(*timestamp);
  168. let normalized_height = if max_value > min_value {
  169. (value - min_value) / (max_value - min_value)
  170. } else {
  171. 0.0
  172. };
  173. let y_position = (bounds.height / self.scaling) -
  174. (normalized_height * (bounds.height / self.scaling));
  175. Point::new(x_position - (self.cell_width / 2.0), y_position)
  176. })
  177. .collect();
  178. if points.len() >= 2 {
  179. for points in points.windows(2) {
  180. let stroke = Stroke {
  181. width: 1.0,
  182. ..Stroke::default()
  183. };
  184. frame.stroke(
  185. &Path::line(points[0], points[1]),
  186. Stroke::with_color(stroke, palette.secondary.strong.color)
  187. )
  188. }
  189. }
  190. let radius = (self.cell_width * 0.2).min(5.0);
  191. for point in points {
  192. frame.fill(
  193. &Path::circle(Point::new(point.x, point.y), radius),
  194. palette.secondary.strong.color,
  195. );
  196. }
  197. });
  198. if self.crosshair {
  199. let crosshair = self.crosshair_cache.draw(renderer, bounds.size(), |frame| {
  200. let dashed_line = Stroke::with_color(
  201. Stroke {
  202. width: 1.0,
  203. line_dash: LineDash {
  204. segments: &[4.0, 4.0],
  205. offset: 8,
  206. },
  207. ..Default::default()
  208. },
  209. palette.secondary.strong.color
  210. .scale_alpha(
  211. if palette.is_dark {
  212. 0.6
  213. } else {
  214. 1.0
  215. },
  216. ),
  217. );
  218. if let Some(cursor_position) = cursor.position_in(self.chart_bounds) {
  219. let region = self.visible_region(frame.size());
  220. // Vertical time line
  221. let earliest = self.x_to_time(region.x) as f64;
  222. let latest = self.x_to_time(region.x + region.width) as f64;
  223. let crosshair_ratio = f64::from(cursor_position.x / bounds.width);
  224. let crosshair_millis = earliest + crosshair_ratio * (latest - earliest);
  225. let rounded_timestamp =
  226. (crosshair_millis / (self.timeframe as f64)).round() as i64 * self.timeframe as i64;
  227. let snap_ratio = ((rounded_timestamp as f64 - earliest) / (latest - earliest)) as f32;
  228. frame.stroke(
  229. &Path::line(
  230. Point::new(snap_ratio * bounds.width, 0.0),
  231. Point::new(snap_ratio * bounds.width, bounds.height),
  232. ),
  233. dashed_line,
  234. );
  235. if let Some((_, oi_value)) = self
  236. .data_points
  237. .iter()
  238. .find(|(time, _)| **time == rounded_timestamp)
  239. {
  240. let next_value = self
  241. .data_points
  242. .range((rounded_timestamp + (self.timeframe as i64))..=i64::MAX)
  243. .next()
  244. .map(|(_, val)| *val);
  245. let change_text = if let Some(next_oi) = next_value {
  246. let difference = next_oi - *oi_value;
  247. let sign = if difference >= 0.0 { "+" } else { "" };
  248. format!("Change: {}{}", sign, format_with_commas(difference))
  249. } else {
  250. "Change: N/A".to_string()
  251. };
  252. let tooltip_text = format!(
  253. "Open Interest: {}\n{}",
  254. format_with_commas(*oi_value),
  255. change_text,
  256. );
  257. let text = canvas::Text {
  258. content: tooltip_text,
  259. position: Point::new(8.0, 2.0),
  260. size: iced::Pixels(10.0),
  261. color: palette.background.base.text,
  262. ..canvas::Text::default()
  263. };
  264. frame.fill_text(text);
  265. frame.fill_rectangle(
  266. Point::new(4.0, 0.0),
  267. Size::new(140.0, 28.0),
  268. palette.background.base.color,
  269. );
  270. }
  271. } else if let Some(cursor_position) = cursor.position_in(bounds) {
  272. // Horizontal price line
  273. let highest = self.max as f32;
  274. let lowest = 0.0;
  275. let crosshair_ratio = cursor_position.y / bounds.height;
  276. let crosshair_price = highest + crosshair_ratio * (lowest - highest);
  277. let rounded_price = round_to_tick(crosshair_price, 1.0);
  278. let snap_ratio = (rounded_price - highest) / (lowest - highest);
  279. frame.stroke(
  280. &Path::line(
  281. Point::new(0.0, snap_ratio * bounds.height),
  282. Point::new(bounds.width, snap_ratio * bounds.height),
  283. ),
  284. dashed_line,
  285. );
  286. }
  287. });
  288. vec![indicator, crosshair]
  289. } else {
  290. vec![indicator]
  291. }
  292. }
  293. fn mouse_interaction(
  294. &self,
  295. interaction: &Interaction,
  296. bounds: Rectangle,
  297. cursor: mouse::Cursor,
  298. ) -> mouse::Interaction {
  299. match interaction {
  300. Interaction::Panning { .. } => mouse::Interaction::Grabbing,
  301. Interaction::Zoomin { .. } => mouse::Interaction::ZoomIn,
  302. Interaction::None if cursor.is_over(bounds) => {
  303. if self.crosshair {
  304. mouse::Interaction::Crosshair
  305. } else {
  306. mouse::Interaction::default()
  307. }
  308. }
  309. _ => mouse::Interaction::default(),
  310. }
  311. }
  312. }
  313. pub struct OpenInterestLabels<'a> {
  314. pub label_cache: &'a Cache,
  315. pub crosshair: bool,
  316. pub max: f32,
  317. pub min: f32,
  318. pub chart_bounds: Rectangle,
  319. }
  320. impl canvas::Program<Message> for OpenInterestLabels<'_> {
  321. type State = Interaction;
  322. fn update(
  323. &self,
  324. _state: &mut Self::State,
  325. _event: Event,
  326. _bounds: Rectangle,
  327. _cursor: mouse::Cursor,
  328. ) -> Option<canvas::Action<Message>> {
  329. None
  330. }
  331. fn draw(
  332. &self,
  333. _state: &Self::State,
  334. renderer: &Renderer,
  335. theme: &Theme,
  336. bounds: Rectangle,
  337. cursor: mouse::Cursor,
  338. ) -> Vec<Geometry> {
  339. let palette = theme.extended_palette();
  340. let highest = self.max;
  341. let lowest = self.min;
  342. let text_size = 12.0;
  343. let labels = self.label_cache.draw(renderer, bounds.size(), |frame| {
  344. frame.fill_rectangle(
  345. Point::new(0.0, 0.0),
  346. Size::new(bounds.width, 1.0),
  347. if palette.is_dark {
  348. palette.background.weak.color.scale_alpha(0.2)
  349. } else {
  350. palette.background.strong.color.scale_alpha(0.2)
  351. },
  352. );
  353. frame.fill_rectangle(
  354. Point::new(0.0, 0.0),
  355. Size::new(1.0, bounds.height),
  356. if palette.is_dark {
  357. palette.background.weak.color.scale_alpha(0.4)
  358. } else {
  359. palette.background.strong.color.scale_alpha(0.4)
  360. },
  361. );
  362. let y_range = highest - lowest;
  363. let y_labels_can_fit: i32 = (bounds.height / (text_size * 2.0)) as i32;
  364. let mut all_labels: Vec<AxisLabel> =
  365. Vec::with_capacity((y_labels_can_fit + 2) as usize); // +2 for last_price and crosshair
  366. let rect = |y_pos: f32, label_amt: i16| {
  367. let label_offset = text_size + (f32::from(label_amt) * (text_size / 2.0) + 2.0);
  368. Rectangle {
  369. x: 6.0,
  370. y: y_pos - label_offset / 2.0,
  371. width: bounds.width - 8.0,
  372. height: label_offset,
  373. }
  374. };
  375. // Regular price labels (priority 1)
  376. let (step, rounded_lowest) = calc_price_step(highest, lowest, y_labels_can_fit, 1.0);
  377. let mut y = rounded_lowest;
  378. while y <= highest {
  379. let y_position = bounds.height - ((y - lowest) / y_range * bounds.height);
  380. if y > 0.0 {
  381. let text_content = convert_to_qty_abbr(y);
  382. let label = Label {
  383. content: text_content,
  384. background_color: None,
  385. marker_color: if palette.is_dark {
  386. palette.background.weak.color.scale_alpha(0.6)
  387. } else {
  388. palette.background.strong.color.scale_alpha(0.6)
  389. },
  390. text_color: palette.background.base.text,
  391. text_size: 12.0,
  392. };
  393. all_labels.push(AxisLabel::Y(rect(y_position, 1), label, None));
  394. }
  395. y += step;
  396. }
  397. // Crosshair price (priority 3)
  398. if self.crosshair {
  399. let common_bounds = Rectangle {
  400. x: self.chart_bounds.x,
  401. y: bounds.y,
  402. width: self.chart_bounds.width,
  403. height: bounds.height,
  404. };
  405. if let Some(crosshair_pos) = cursor.position_in(common_bounds) {
  406. let raw_price =
  407. lowest + (y_range * (bounds.height - crosshair_pos.y) / bounds.height);
  408. let rounded_price = round_to_tick(raw_price, 1.0);
  409. let y_position =
  410. bounds.height - ((rounded_price - lowest) / y_range * bounds.height);
  411. let text_content = convert_to_qty_abbr(rounded_price);
  412. let label = Label {
  413. content: text_content,
  414. background_color: Some(palette.secondary.base.color),
  415. marker_color: palette.background.strong.color,
  416. text_color: palette.secondary.base.text,
  417. text_size: 12.0,
  418. };
  419. all_labels.push(AxisLabel::Y(rect(y_position, 1), label, None));
  420. }
  421. }
  422. AxisLabel::filter_and_draw(&all_labels, frame);
  423. });
  424. vec![labels]
  425. }
  426. fn mouse_interaction(
  427. &self,
  428. interaction: &Interaction,
  429. bounds: Rectangle,
  430. cursor: mouse::Cursor,
  431. ) -> mouse::Interaction {
  432. match interaction {
  433. Interaction::Zoomin { .. } => mouse::Interaction::ResizingVertically,
  434. Interaction::Panning { .. } => mouse::Interaction::None,
  435. Interaction::None if cursor.is_over(bounds) => mouse::Interaction::ResizingVertically,
  436. _ => mouse::Interaction::default(),
  437. }
  438. }
  439. }