use super::poc::{NPoc, PointOfControl};
use exchange::{
Trade, UnixMs,
unit::price::{Price, PriceStep},
unit::qty::Qty,
};
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default)]
pub struct GroupedTrades {
pub buy_qty: Qty,
pub sell_qty: Qty,
pub first_time: UnixMs,
pub last_time: UnixMs,
pub buy_count: usize,
pub sell_count: usize,
}
impl GroupedTrades {
fn new(trade: &Trade) -> Self {
Self {
buy_qty: if trade.is_sell {
Qty::default()
} else {
trade.qty
},
sell_qty: if trade.is_sell {
trade.qty
} else {
Qty::default()
},
first_time: trade.time,
last_time: trade.time,
buy_count: if trade.is_sell { 0 } else { 1 },
sell_count: if trade.is_sell { 1 } else { 0 },
}
}
fn add_trade(&mut self, trade: &Trade) {
if trade.is_sell {
self.sell_qty += trade.qty;
self.sell_count += 1;
} else {
self.buy_qty += trade.qty;
self.buy_count += 1;
}
self.first_time = self.first_time.min(trade.time);
self.last_time = self.last_time.max(trade.time);
}
pub fn merge(&mut self, other: &Self) {
self.buy_qty += other.buy_qty;
self.sell_qty += other.sell_qty;
self.buy_count += other.buy_count;
self.sell_count += other.sell_count;
self.first_time = self.first_time.min(other.first_time);
self.last_time = self.last_time.max(other.last_time);
}
pub fn total_qty(&self) -> Qty {
self.buy_qty + self.sell_qty
}
pub fn delta_qty(&self) -> Qty {
self.buy_qty - self.sell_qty
}
pub fn max_cluster_qty(&self, cluster_kind: ClusterKind) -> Qty {
match cluster_kind {
ClusterKind::BidAsk | ClusterKind::Table => self.buy_qty.max(self.sell_qty),
ClusterKind::DeltaProfile => self.buy_qty.abs_diff(self.sell_qty),
ClusterKind::VolumeProfile => self.total_qty(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct KlineTrades {
pub trades: FxHashMap<Price, GroupedTrades>,
exact_trades: FxHashMap<Price, GroupedTrades>,
pub poc: Option<PointOfControl>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct TradeSummary {
pub buy_count: u64,
pub sell_count: u64,
pub buy_qty: Qty,
pub sell_qty: Qty,
}
impl KlineTrades {
pub fn new() -> Self {
Self {
trades: FxHashMap::default(),
exact_trades: FxHashMap::default(),
poc: None,
}
}
pub fn first_trade_t(&self) -> Option<UnixMs> {
self.trades.values().map(|group| group.first_time).min()
}
pub fn last_trade_t(&self) -> Option<UnixMs> {
self.trades.values().map(|group| group.last_time).max()
}
pub fn trade_summary(&self) -> Option<TradeSummary> {
if self.trades.is_empty() {
return None;
}
let mut summary = TradeSummary::default();
for group in self.trades.values() {
summary.buy_count = summary.buy_count.saturating_add(group.buy_count as u64);
summary.sell_count = summary.sell_count.saturating_add(group.sell_count as u64);
summary.buy_qty += group.buy_qty;
summary.sell_qty += group.sell_qty;
}
Some(summary)
}
pub fn add_trade_to_side_bin(&mut self, trade: &Trade, step: PriceStep) {
let price = trade.price.round_to_side_step(trade.is_sell, step);
self.trades
.entry(price)
.and_modify(|group| group.add_trade(trade))
.or_insert_with(|| GroupedTrades::new(trade));
}
pub fn add_trade_to_nearest_bin(&mut self, trade: &Trade, step: PriceStep) {
self.exact_trades
.entry(trade.price)
.and_modify(|group| group.add_trade(trade))
.or_insert_with(|| GroupedTrades::new(trade));
let price = trade.price.round_to_step(step);
self.trades
.entry(price)
.and_modify(|group| group.add_trade(trade))
.or_insert_with(|| GroupedTrades::new(trade));
}
pub fn regroup(&mut self, step: PriceStep) {
self.trades.clear();
self.poc = None;
for (&price, group) in &self.exact_trades {
self.trades
.entry(price.round_to_step(step))
.and_modify(|current| current.merge(group))
.or_insert_with(|| group.clone());
}
self.calculate_poc();
}
fn max_qty_by<F>(&self, highest: Price, lowest: Price, f: F) -> Qty
where
F: Fn(&GroupedTrades) -> Qty,
{
let mut max_qty = Qty::default();
for (price, group) in &self.trades {
if *price >= lowest && *price <= highest {
max_qty = max_qty.max(f(group));
}
}
max_qty
}
pub fn max_cluster_qty(&self, cluster_kind: ClusterKind, highest: Price, lowest: Price) -> Qty {
self.max_qty_by(highest, lowest, |group| group.max_cluster_qty(cluster_kind))
}
pub fn max_cluster_qty_all(&self, cluster_kind: ClusterKind) -> Qty {
self.trades
.values()
.map(|group| group.max_cluster_qty(cluster_kind))
.max()
.unwrap_or_default()
}
pub fn calculate_poc(&mut self) {
if self.trades.is_empty() {
return;
}
let mut max_volume = Qty::ZERO;
let mut poc_price = Price::from_f32(0.0);
for (price, group) in &self.trades {
let total_volume = group.total_qty();
if total_volume > max_volume
|| (total_volume == max_volume && !total_volume.is_zero() && *price > poc_price)
{
max_volume = total_volume;
poc_price = *price;
}
}
self.poc = Some(PointOfControl {
price: poc_price,
volume: max_volume,
status: NPoc::default(),
});
}
pub fn set_poc_status(&mut self, status: NPoc) {
if let Some(poc) = &mut self.poc {
poc.status = status;
}
}
pub fn poc_price(&self) -> Option<Price> {
self.poc.map(|poc| poc.price)
}
pub fn clear(&mut self) {
self.trades.clear();
self.exact_trades.clear();
self.poc = None;
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, Deserialize, Serialize)]
pub enum ClusterKind {
#[default]
BidAsk,
VolumeProfile,
DeltaProfile,
Table,
}
impl ClusterKind {
pub const ALL: [ClusterKind; 4] = [
ClusterKind::BidAsk,
ClusterKind::VolumeProfile,
ClusterKind::DeltaProfile,
ClusterKind::Table,
];
}
impl std::fmt::Display for ClusterKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ClusterKind::BidAsk => write!(f, "Bid/Ask"),
ClusterKind::VolumeProfile => write!(f, "Volume Profile"),
ClusterKind::DeltaProfile => write!(f, "Delta Profile"),
ClusterKind::Table => write!(f, "Table"),
}
}
}
#[derive(Default, Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
pub enum ClusterScaling {
#[default]
VisibleRange,
Hybrid { weight: f32 },
Datapoint,
}
impl ClusterScaling {
pub const ALL: [ClusterScaling; 3] = [
ClusterScaling::VisibleRange,
ClusterScaling::Hybrid { weight: 0.2 },
ClusterScaling::Datapoint,
];
pub fn effective_qty(self, visible_max: f64, individual_max: Qty) -> f64 {
match self {
ClusterScaling::VisibleRange => Qty::scale_or_one(visible_max),
ClusterScaling::Datapoint => individual_max.to_scale_or_one(),
ClusterScaling::Hybrid { weight } => {
let w = weight.clamp(0.0, 1.0) as f64;
Qty::scale_or_one(visible_max * w + individual_max.to_f64() * (1.0 - w))
}
}
}
}
impl std::fmt::Display for ClusterScaling {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ClusterScaling::VisibleRange => write!(f, "Visible Range"),
ClusterScaling::Hybrid { weight } => write!(f, "Hybrid (weight: {:.2})", weight),
ClusterScaling::Datapoint => write!(f, "Per-candle"),
}
}
}
impl std::cmp::Eq for ClusterScaling {}