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