Skip to main content

flowsurface_indicator_engine/
frame.rs

1use crate::scene::Color;
2use crate::types::Value;
3use serde::{Deserialize, Serialize};
4use std::sync::Arc;
5
6/// The two bar bases understood by the live indicator runtime.
7#[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/// Stable metadata shared by all bars for one instrument evaluation.
29#[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/// Standard chart-derived numeric sources exposed by `input.source`.
100#[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}