1use exchange::{TickMultiplier, TickerInfo, Timeframe};
2use serde::{Deserialize, Serialize};
3
4use crate::chart::heatmap;
5use crate::panel::{ladder, timesales};
6use crate::stream::PersistStreamKind;
7use crate::util::ok_or_default;
8
9use crate::chart::Basis;
10
11#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
12pub enum Axis {
13 Horizontal,
14 Vertical,
15}
16
17#[derive(Debug, Clone, Deserialize, Serialize)]
18pub enum Pane {
19 Split {
20 axis: Axis,
21 ratio: f32,
22 a: Box<Pane>,
23 b: Box<Pane>,
24 },
25 Starter {
26 #[serde(deserialize_with = "ok_or_default", default)]
27 link_group: Option<LinkGroup>,
28 },
29 ShaderHeatmap {
30 #[serde(deserialize_with = "ok_or_default", default)]
31 indicators: heatmap::indicators::HeatmapIndicatorConfig,
32 #[serde(deserialize_with = "ok_or_default", default)]
33 stream_type: Vec<PersistStreamKind>,
34 #[serde(deserialize_with = "ok_or_default")]
35 settings: Settings,
36 #[serde(deserialize_with = "ok_or_default", default)]
37 link_group: Option<LinkGroup>,
38 },
39 KlineChart {
40 #[serde(default)]
41 layout: Option<Box<crate::chart::kline::Config>>,
42 #[serde(deserialize_with = "ok_or_default", default)]
43 stream_type: Vec<PersistStreamKind>,
44 #[serde(deserialize_with = "ok_or_default")]
45 settings: Settings,
46 #[serde(deserialize_with = "ok_or_default", default)]
47 link_group: Option<LinkGroup>,
48 },
49 TimeAndSales {
50 stream_type: Vec<PersistStreamKind>,
51 settings: Settings,
52 #[serde(deserialize_with = "ok_or_default", default)]
53 link_group: Option<LinkGroup>,
54 },
55 Ladder {
56 stream_type: Vec<PersistStreamKind>,
57 settings: Settings,
58 #[serde(deserialize_with = "ok_or_default", default)]
59 link_group: Option<LinkGroup>,
60 },
61}
62
63impl Default for Pane {
64 fn default() -> Self {
65 Pane::Starter { link_group: None }
66 }
67}
68
69#[derive(Debug, Clone, Deserialize, Serialize, Default)]
70#[serde(default)]
71pub struct Settings {
72 pub tick_multiply: Option<exchange::TickMultiplier>,
73 pub visual_config: Option<VisualConfig>,
74 pub selected_basis: Option<Basis>,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
78pub enum LinkGroup {
79 A,
80 B,
81 C,
82 D,
83 E,
84 F,
85 G,
86 H,
87 I,
88}
89
90impl LinkGroup {
91 pub const ALL: [LinkGroup; 9] = [
92 LinkGroup::A,
93 LinkGroup::B,
94 LinkGroup::C,
95 LinkGroup::D,
96 LinkGroup::E,
97 LinkGroup::F,
98 LinkGroup::G,
99 LinkGroup::H,
100 LinkGroup::I,
101 ];
102}
103
104impl std::fmt::Display for LinkGroup {
105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106 let c = match self {
107 LinkGroup::A => "1",
108 LinkGroup::B => "2",
109 LinkGroup::C => "3",
110 LinkGroup::D => "4",
111 LinkGroup::E => "5",
112 LinkGroup::F => "6",
113 LinkGroup::G => "7",
114 LinkGroup::H => "8",
115 LinkGroup::I => "9",
116 };
117 write!(f, "{c}")
118 }
119}
120
121#[derive(Debug, Clone, Deserialize, Serialize)]
123pub enum VisualConfig {
124 Heatmap(heatmap::Config),
125 TimeAndSales(timesales::Config),
126 Ladder(ladder::Config),
127}
128
129impl VisualConfig {
130 pub fn heatmap(&self) -> Option<heatmap::Config> {
131 match self {
132 Self::Heatmap(cfg) => Some(*cfg),
133 _ => None,
134 }
135 }
136
137 pub fn time_and_sales(&self) -> Option<timesales::Config> {
138 match self {
139 Self::TimeAndSales(cfg) => Some(*cfg),
140 _ => None,
141 }
142 }
143
144 pub fn ladder(&self) -> Option<ladder::Config> {
145 match self {
146 Self::Ladder(cfg) => Some(*cfg),
147 _ => None,
148 }
149 }
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
153pub enum ContentKind {
154 Starter,
155 ShaderHeatmap,
156 CandlestickChart,
157 TimeAndSales,
158 Ladder,
159}
160
161impl ContentKind {
162 pub const ALL: [ContentKind; 5] = [
163 ContentKind::Starter,
164 ContentKind::ShaderHeatmap,
165 ContentKind::CandlestickChart,
166 ContentKind::TimeAndSales,
167 ContentKind::Ladder,
168 ];
169}
170
171impl std::fmt::Display for ContentKind {
172 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173 let s = match self {
174 ContentKind::Starter => "Starter Pane",
175 ContentKind::ShaderHeatmap => "Heatmap Chart",
176 ContentKind::CandlestickChart => "Candlestick Chart",
177 ContentKind::TimeAndSales => "Time&Sales",
178 ContentKind::Ladder => "DOM/Ladder",
179 };
180 write!(f, "{s}")
181 }
182}
183
184#[derive(Clone, Copy)]
185pub struct PaneSetup {
186 pub ticker_info: exchange::TickerInfo,
187 pub basis: Option<Basis>,
188 pub tick_multiplier: Option<TickMultiplier>,
189 pub price_step: exchange::unit::PriceStep,
190 pub depth_aggr: exchange::adapter::StreamTicksize,
191 pub push_freq: exchange::PushFrequency,
192}
193
194impl PaneSetup {
195 pub fn new(
196 content_kind: ContentKind,
197 base_ticker: TickerInfo,
198 prev_base_ticker: Option<TickerInfo>,
199 current_basis: Option<Basis>,
200 current_tick_multiplier: Option<TickMultiplier>,
201 ) -> Self {
202 let exchange = base_ticker.ticker.exchange;
203
204 let is_client_aggr = exchange.is_depth_client_aggr();
205 let prev_is_client_aggr = prev_base_ticker
206 .map(|ti| ti.ticker.exchange.is_depth_client_aggr())
207 .unwrap_or(is_client_aggr);
208
209 let basis =
210 match content_kind {
211 ContentKind::Ladder => Some(
212 current_basis.unwrap_or_else(|| Basis::default_heatmap_time(Some(base_ticker))),
213 ),
214 ContentKind::ShaderHeatmap => Some(
215 current_basis.unwrap_or_else(|| Basis::default_heatmap_time(Some(base_ticker))),
216 ),
217 ContentKind::CandlestickChart => {
218 let current = current_basis.and_then(|b| match b {
219 Basis::Time(tf) if exchange.supports_kline_timeframe(tf) => Some(b),
220 Basis::Tick(_) => Some(b),
221 _ => None,
222 });
223
224 Some(current.unwrap_or_else(|| {
225 Basis::default_kline_time(Some(base_ticker), Timeframe::M5)
226 }))
227 }
228 ContentKind::Starter | ContentKind::TimeAndSales => None,
229 };
230
231 let tick_multiplier = match content_kind {
232 ContentKind::Ladder | ContentKind::ShaderHeatmap => {
233 let tm = if !is_client_aggr && prev_is_client_aggr {
234 TickMultiplier(10)
235 } else if let Some(tm) = current_tick_multiplier {
236 tm
237 } else if is_client_aggr {
238 TickMultiplier(5)
239 } else {
240 TickMultiplier(10)
241 };
242 Some(tm)
243 }
244 ContentKind::CandlestickChart | ContentKind::TimeAndSales | ContentKind::Starter => {
245 current_tick_multiplier
246 }
247 };
248
249 let price_step = match tick_multiplier {
250 Some(tm) => tm.multiply_with_min_tick_step(base_ticker),
251 None => base_ticker.min_ticksize.into(),
252 };
253
254 let depth_aggr = exchange.stream_ticksize(tick_multiplier, TickMultiplier(50));
255
256 let push_freq = match content_kind {
257 ContentKind::ShaderHeatmap if exchange.is_custom_push_freq() => match basis {
258 Some(Basis::Time(tf)) if exchange.supports_heatmap_timeframe(tf) => {
259 exchange::PushFrequency::Custom(tf)
260 }
261 _ => exchange::PushFrequency::ServerDefault,
262 },
263 _ => exchange::PushFrequency::ServerDefault,
264 };
265
266 Self {
267 ticker_info: base_ticker,
268 basis,
269 tick_multiplier,
270 price_step,
271 depth_aggr,
272 push_freq,
273 }
274 }
275}