flowsurface-data 0.8.8

Data aggregation, indexing, and configuration utilities for Flowsurface
Documentation
use std::time::Duration;

use exchange::unit::{Price, Qty};
use serde::{Deserialize, Serialize};

use crate::util::ok_or_default;

const TRADE_RETENTION_MS: u64 = 120_000;

#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct Config {
    pub trade_size_filter: f32,
    #[serde(default = "default_buffer_filter")]
    pub trade_retention: Duration,
    #[serde(deserialize_with = "ok_or_default", default)]
    pub stacked_bar: Option<StackedBar>,
}

impl Default for Config {
    fn default() -> Self {
        Config {
            trade_size_filter: 0.0,
            trade_retention: Duration::from_millis(TRADE_RETENTION_MS),
            stacked_bar: StackedBar::Compact(StackedBarRatio::default()).into(),
        }
    }
}

fn default_buffer_filter() -> Duration {
    Duration::from_millis(TRADE_RETENTION_MS)
}

#[derive(Debug, Clone)]
pub struct TradeDisplay {
    pub time_str: String,
    pub price: Price,
    pub qty: Qty,
    pub is_sell: bool,
}

#[derive(Debug, Clone)]
pub struct TradeEntry {
    pub ts_ms: u64,
    pub display: TradeDisplay,
}

#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Copy)]
pub enum StackedBar {
    Compact(StackedBarRatio),
    Full(StackedBarRatio),
}

impl StackedBar {
    pub fn ratio(self) -> StackedBarRatio {
        match self {
            StackedBar::Compact(r) | StackedBar::Full(r) => r,
        }
    }

    pub fn with_ratio(self, r: StackedBarRatio) -> Self {
        match self {
            StackedBar::Compact(_) => StackedBar::Compact(r),
            StackedBar::Full(_) => StackedBar::Full(r),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Copy)]
pub enum StackedBarRatio {
    Count,
    #[default]
    Volume,
    AverageSize,
}

impl std::fmt::Display for StackedBarRatio {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            StackedBarRatio::Count => write!(f, "Count"),
            StackedBarRatio::AverageSize => write!(f, "Average trade size"),
            StackedBarRatio::Volume => write!(f, "Volume"),
        }
    }
}

impl StackedBarRatio {
    pub const ALL: [StackedBarRatio; 3] = [
        StackedBarRatio::Count,
        StackedBarRatio::Volume,
        StackedBarRatio::AverageSize,
    ];
}

#[derive(Debug, Clone, Copy)]
pub enum HistAggValues {
    Count { buy: u64, sell: u64 },
    Qty { buy: Qty, sell: Qty },
}

#[derive(Default)]
pub struct HistAgg {
    buy_count: u64,
    sell_count: u64,
    buy_sum: Qty,
    sell_sum: Qty,
}

impl HistAgg {
    fn average_qty(sum: Qty, count: u64) -> Qty {
        if count == 0 {
            return Qty::ZERO;
        }

        let c = count.min(i64::MAX as u64) as i64;
        let half = c / 2;
        let rounded = if sum.units >= 0 {
            sum.units.saturating_add(half).div_euclid(c)
        } else {
            sum.units.saturating_sub(half).div_euclid(c)
        };

        Qty::from_units(rounded)
    }

    pub fn add(&mut self, trade: &TradeDisplay) {
        let qty = trade.qty;

        if trade.is_sell {
            self.sell_count += 1;
            self.sell_sum += qty;
        } else {
            self.buy_count += 1;
            self.buy_sum += qty;
        }
    }

    pub fn remove(&mut self, trade: &TradeDisplay) {
        let qty = trade.qty;

        if trade.is_sell {
            self.sell_count = self.sell_count.saturating_sub(1);
            self.sell_sum = if self.sell_sum.units >= qty.units {
                self.sell_sum - qty
            } else {
                Qty::ZERO
            };
        } else {
            self.buy_count = self.buy_count.saturating_sub(1);
            self.buy_sum = if self.buy_sum.units >= qty.units {
                self.buy_sum - qty
            } else {
                Qty::ZERO
            };
        }
    }

    pub fn values_for(&self, ratio_kind: StackedBarRatio) -> Option<HistAggValues> {
        match ratio_kind {
            StackedBarRatio::Count => {
                let buy = self.buy_count;
                let sell = self.sell_count;
                let total = buy.saturating_add(sell);

                if total == 0 {
                    return None;
                }

                Some(HistAggValues::Count { buy, sell })
            }
            StackedBarRatio::Volume => {
                let buy = self.buy_sum.to_f32_lossy();
                let sell = self.sell_sum.to_f32_lossy();
                let total = buy + sell;

                if total <= 0.0 {
                    return None;
                }

                Some(HistAggValues::Qty {
                    buy: self.buy_sum,
                    sell: self.sell_sum,
                })
            }
            StackedBarRatio::AverageSize => {
                let buy_avg = Self::average_qty(self.buy_sum, self.buy_count);
                let sell_avg = Self::average_qty(self.sell_sum, self.sell_count);

                let denom = buy_avg.units.saturating_add(sell_avg.units);
                if denom <= 0 {
                    return None;
                }

                Some(HistAggValues::Qty {
                    buy: buy_avg,
                    sell: sell_avg,
                })
            }
        }
    }
}