1use crate::scene::Color;
2use crate::types::Value;
3use serde::{Deserialize, Serialize};
4use std::sync::Arc;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
8pub enum TimeOrTick {
9 #[default]
10 Time,
11 Tick,
12}
13
14#[derive(Debug, Clone, Copy, Default, PartialEq)]
15pub struct TradeAggregate {
16 pub buy_qty: f64,
17 pub sell_qty: f64,
18 pub buy_count: u64,
19 pub sell_count: u64,
20}
21
22impl TradeAggregate {
23 pub fn trade_count(self) -> u64 {
24 self.buy_count.saturating_add(self.sell_count)
25 }
26}
27
28#[derive(Debug, Clone, Copy, Default, PartialEq)]
30pub struct InstrumentContext {
31 pub min_tick: Option<f64>,
32}
33
34#[derive(Debug, Clone, PartialEq)]
35pub struct ChartFrame {
36 pub open: Option<f64>,
37 pub high: Option<f64>,
38 pub low: Option<f64>,
39 pub close: Option<f64>,
40 pub volume: Option<f64>,
41 pub directional_volume_available: bool,
42 pub buy_qty: f64,
43 pub sell_qty: f64,
44 pub trade: Option<TradeAggregate>,
45}
46
47impl ChartFrame {
48 pub fn to_value(&self) -> Value {
49 let mut fields = std::collections::BTreeMap::new();
50 fields.insert("open".to_owned(), optional_float(self.open));
51 fields.insert("high".to_owned(), optional_float(self.high));
52 fields.insert("low".to_owned(), optional_float(self.low));
53 fields.insert("close".to_owned(), optional_float(self.close));
54 fields.insert("volume".to_owned(), optional_float(self.volume));
55 fields.insert(
56 "directional_volume_available".to_owned(),
57 Value::Bool(self.directional_volume_available),
58 );
59 fields.insert(
60 "buy_qty".to_owned(),
61 directional_float(self.directional_volume_available, self.buy_qty),
62 );
63 fields.insert(
64 "sell_qty".to_owned(),
65 directional_float(self.directional_volume_available, self.sell_qty),
66 );
67 fields.insert("trade".to_owned(), trade_value(self.trade));
68 Value::Record(fields)
69 }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum FrameField {
74 ChartTime,
75 ChartOpen,
76 ChartHigh,
77 ChartLow,
78 ChartClose,
79 ChartHl2,
80 ChartHlc3,
81 ChartOhlc4,
82 ChartHlcc4,
83 ChartVolume,
84 ChartDirectionalVolumeAvailable,
85 ChartBuyQty,
86 ChartSellQty,
87 ChartTrade,
88 ChartTradeAvailable,
89 ChartTradeBuyQty,
90 ChartTradeSellQty,
91 ChartTradeBuyCount,
92 ChartTradeSellCount,
93 ChartTradeCount,
94 InstrumentMinTick,
95 OpenInterest,
96 Chart,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
101#[serde(rename_all = "snake_case")]
102pub enum BaseSeriesSource {
103 Open,
104 High,
105 Low,
106 Close,
107 Hl2,
108 Hlc3,
109 Ohlc4,
110 Hlcc4,
111}
112
113impl BaseSeriesSource {
114 pub const ALL: [Self; 8] = [
115 Self::Open,
116 Self::High,
117 Self::Low,
118 Self::Close,
119 Self::Hl2,
120 Self::Hlc3,
121 Self::Ohlc4,
122 Self::Hlcc4,
123 ];
124
125 pub const fn label(self) -> &'static str {
126 match self {
127 Self::Open => "Open",
128 Self::High => "High",
129 Self::Low => "Low",
130 Self::Close => "Close",
131 Self::Hl2 => "(H + L)/2",
132 Self::Hlc3 => "(H + L + C)/3",
133 Self::Ohlc4 => "(O + H + L + C)/4",
134 Self::Hlcc4 => "(H + L + C + C)/4",
135 }
136 }
137
138 pub fn value(self, frame: &IndicatorFrame) -> Option<f64> {
139 let finite = |value: Option<f64>| value.filter(|value| value.is_finite());
140 let average = |values: &[Option<f64>]| {
141 let sum = values
142 .iter()
143 .try_fold(0.0, |sum, value| Some(sum + finite(*value)?))?;
144 Some(sum / values.len() as f64).filter(|value| value.is_finite())
145 };
146
147 match self {
148 Self::Open => finite(frame.chart.open),
149 Self::High => finite(frame.chart.high),
150 Self::Low => finite(frame.chart.low),
151 Self::Close => finite(frame.chart.close),
152 Self::Hl2 => average(&[frame.chart.high, frame.chart.low]),
153 Self::Hlc3 => average(&[frame.chart.high, frame.chart.low, frame.chart.close]),
154 Self::Ohlc4 => average(&[
155 frame.chart.open,
156 frame.chart.high,
157 frame.chart.low,
158 frame.chart.close,
159 ]),
160 Self::Hlcc4 => average(&[
161 frame.chart.high,
162 frame.chart.low,
163 frame.chart.close,
164 frame.chart.close,
165 ]),
166 }
167 }
168}
169
170impl FrameField {
171 pub const fn source_input_label(self) -> Option<&'static str> {
172 match self {
173 Self::ChartOpen => Some(BaseSeriesSource::Open.label()),
174 Self::ChartHigh => Some(BaseSeriesSource::High.label()),
175 Self::ChartLow => Some(BaseSeriesSource::Low.label()),
176 Self::ChartClose => Some(BaseSeriesSource::Close.label()),
177 Self::ChartHl2 => Some(BaseSeriesSource::Hl2.label()),
178 Self::ChartHlc3 => Some(BaseSeriesSource::Hlc3.label()),
179 Self::ChartOhlc4 => Some(BaseSeriesSource::Ohlc4.label()),
180 Self::ChartHlcc4 => Some(BaseSeriesSource::Hlcc4.label()),
181 Self::ChartVolume => Some("Volume"),
182 Self::OpenInterest => Some("Open interest"),
183 _ => None,
184 }
185 }
186}
187
188#[derive(Debug, Clone, PartialEq)]
189pub enum SeriesInputValue {
190 Default,
191 Base(BaseSeriesSource),
192 Value(Option<f64>),
193}
194
195#[derive(Debug, Clone, PartialEq)]
196pub struct IndicatorFrame {
197 pub index: usize,
198 pub time: i64,
199 pub basis: TimeOrTick,
200 pub instrument: InstrumentContext,
201 pub chart: ChartFrame,
202 pub open_interest: Option<f64>,
203 pub series_inputs: Arc<[SeriesInputValue]>,
204}
205
206impl IndicatorFrame {
207 pub(crate) fn primary_value(&self, field: FrameField) -> Value {
208 match field {
209 FrameField::ChartTime => Value::Int(self.time),
210 FrameField::ChartOpen => optional_float(self.chart.open),
211 FrameField::ChartHigh => optional_float(self.chart.high),
212 FrameField::ChartLow => optional_float(self.chart.low),
213 FrameField::ChartClose => optional_float(self.chart.close),
214 FrameField::ChartHl2 => BaseSeriesSource::Hl2
215 .value(self)
216 .map_or(Value::Na, Value::Float),
217 FrameField::ChartHlc3 => BaseSeriesSource::Hlc3
218 .value(self)
219 .map_or(Value::Na, Value::Float),
220 FrameField::ChartOhlc4 => BaseSeriesSource::Ohlc4
221 .value(self)
222 .map_or(Value::Na, Value::Float),
223 FrameField::ChartHlcc4 => BaseSeriesSource::Hlcc4
224 .value(self)
225 .map_or(Value::Na, Value::Float),
226 FrameField::ChartVolume => optional_float(self.chart.volume),
227 FrameField::ChartDirectionalVolumeAvailable => {
228 Value::Bool(self.chart.directional_volume_available)
229 }
230 FrameField::ChartBuyQty => {
231 directional_float(self.chart.directional_volume_available, self.chart.buy_qty)
232 }
233 FrameField::ChartSellQty => {
234 directional_float(self.chart.directional_volume_available, self.chart.sell_qty)
235 }
236 FrameField::ChartTrade => trade_value(self.chart.trade),
237 FrameField::ChartTradeAvailable => Value::Bool(self.chart.trade.is_some()),
238 FrameField::ChartTradeBuyQty => trade_float(self.chart.trade, |trade| trade.buy_qty),
239 FrameField::ChartTradeSellQty => trade_float(self.chart.trade, |trade| trade.sell_qty),
240 FrameField::ChartTradeBuyCount => trade_int(self.chart.trade, |trade| trade.buy_count),
241 FrameField::ChartTradeSellCount => {
242 trade_int(self.chart.trade, |trade| trade.sell_count)
243 }
244 FrameField::ChartTradeCount => trade_int(self.chart.trade, TradeAggregate::trade_count),
245 FrameField::InstrumentMinTick => optional_float(self.instrument.min_tick),
246 FrameField::OpenInterest => optional_float(self.open_interest),
247 FrameField::Chart => self.chart.to_value(),
248 }
249 }
250
251 pub fn field(&self, field: FrameField) -> Value {
252 self.primary_value(field)
253 }
254
255 pub fn series_input(&self, slot: usize) -> SeriesInputValue {
256 self.series_inputs
257 .get(slot)
258 .cloned()
259 .unwrap_or(SeriesInputValue::Default)
260 }
261}
262
263#[derive(Debug, Clone, PartialEq)]
264pub struct PlotOutput {
265 pub id: String,
266 pub value: Value,
267 pub color: Option<Color>,
268}
269
270#[derive(Debug, Clone, PartialEq)]
271pub struct MarkerOutput {
272 pub id: String,
273 pub value: Option<f64>,
274 pub color: Option<Color>,
275 pub text: Option<String>,
276}
277
278#[derive(Debug, Clone, PartialEq)]
279pub struct SegmentOutput {
280 pub id: String,
281 pub start_index: usize,
282 pub start_value: Option<f64>,
283 pub end_index: usize,
284 pub end_value: Option<f64>,
285 pub color: Option<Color>,
286 pub text: Option<String>,
287}
288
289#[derive(Debug, Clone, PartialEq)]
290pub struct HorizontalLevelOutput {
291 pub id: String,
292 pub value: Option<f64>,
293 pub color: Option<Color>,
294}
295
296#[derive(Debug, Clone, PartialEq)]
297pub struct OhlcOutput {
298 pub id: String,
299 pub open: Option<f64>,
300 pub high: Option<f64>,
301 pub low: Option<f64>,
302 pub close: Option<f64>,
303 pub color: Option<Color>,
304}
305
306#[derive(Debug, Clone, PartialEq)]
307pub struct RangeOutput {
308 pub id: String,
309 pub upper: Option<f64>,
310 pub lower: Option<f64>,
311 pub color: Option<Color>,
312}
313
314#[derive(Debug, Clone, PartialEq)]
315pub struct RegionOutput {
316 pub id: String,
317 pub key: i64,
318 pub left_index: usize,
319 pub right_index: usize,
320 pub upper: f64,
321 pub lower: f64,
322 pub border_color: Option<Color>,
323 pub fill_color: Option<Color>,
324 pub border_color_slot: Option<String>,
325 pub fill_color_slot: Option<String>,
326 pub extend_right: bool,
327 pub width: f32,
328}
329
330#[derive(Debug, Clone, PartialEq)]
331pub enum RegionUpdate {
332 Upsert(RegionOutput),
333 Remove { id: String, key: i64 },
334}
335
336#[derive(Debug, Clone, Default, PartialEq)]
337pub struct FrameOutput {
338 pub index: usize,
339 pub time: i64,
340 pub plots: Arc<[PlotOutput]>,
341 pub markers: Arc<[MarkerOutput]>,
342 pub segments: Arc<[SegmentOutput]>,
343 pub levels: Arc<[HorizontalLevelOutput]>,
344 pub ohlc: Arc<[OhlcOutput]>,
345 pub ranges: Arc<[RangeOutput]>,
346 pub regions: Arc<[RegionOutput]>,
347 pub region_updates: Arc<[RegionUpdate]>,
348}
349
350fn optional_float(value: Option<f64>) -> Value {
351 value.map_or(Value::Na, Value::Float)
352}
353
354fn directional_float(available: bool, value: f64) -> Value {
355 if available {
356 Value::Float(value)
357 } else {
358 Value::Na
359 }
360}
361
362fn trade_float(trade: Option<TradeAggregate>, value: impl FnOnce(TradeAggregate) -> f64) -> Value {
363 trade.map_or(Value::Na, |trade| Value::Float(value(trade)))
364}
365
366fn trade_int(trade: Option<TradeAggregate>, value: impl FnOnce(TradeAggregate) -> u64) -> Value {
367 trade.map_or(Value::Na, |trade| Value::Int(value(trade) as i64))
368}
369
370fn trade_value(trade: Option<TradeAggregate>) -> Value {
371 let mut fields = std::collections::BTreeMap::new();
372 fields.insert("available".to_owned(), Value::Bool(trade.is_some()));
373 fields.insert(
374 "buy_qty".to_owned(),
375 trade_float(trade, |trade| trade.buy_qty),
376 );
377 fields.insert(
378 "sell_qty".to_owned(),
379 trade_float(trade, |trade| trade.sell_qty),
380 );
381 fields.insert(
382 "buy_count".to_owned(),
383 trade_int(trade, |trade| trade.buy_count),
384 );
385 fields.insert(
386 "sell_count".to_owned(),
387 trade_int(trade, |trade| trade.sell_count),
388 );
389 fields.insert(
390 "count".to_owned(),
391 trade_int(trade, TradeAggregate::trade_count),
392 );
393 Value::Record(fields)
394}