use crate::aggr::time::DataPoint;
use exchange::{
Kline, Trade,
unit::price::{Price, PriceStep},
unit::qty::Qty,
};
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
#[derive(Clone)]
pub struct KlineDataPoint {
pub kline: Kline,
pub footprint: KlineTrades,
}
impl KlineDataPoint {
pub fn max_cluster_qty(&self, cluster_kind: ClusterKind, highest: Price, lowest: Price) -> Qty {
self.footprint
.max_cluster_qty(cluster_kind, highest, lowest)
}
pub fn add_trade(&mut self, trade: &Trade, step: PriceStep) {
self.footprint.add_trade_to_nearest_bin(trade, step);
}
pub fn poc_price(&self) -> Option<Price> {
self.footprint.poc_price()
}
pub fn set_poc_status(&mut self, status: NPoc) {
self.footprint.set_poc_status(status);
}
pub fn clear_trades(&mut self) {
self.footprint.clear();
}
pub fn calculate_poc(&mut self) {
self.footprint.calculate_poc();
}
pub fn last_trade_time(&self) -> Option<u64> {
self.footprint.last_trade_t()
}
pub fn first_trade_time(&self) -> Option<u64> {
self.footprint.first_trade_t()
}
}
impl DataPoint for KlineDataPoint {
fn add_trade(&mut self, trade: &Trade, step: PriceStep) {
self.add_trade(trade, step);
}
fn clear_trades(&mut self) {
self.clear_trades();
}
fn last_trade_time(&self) -> Option<u64> {
self.last_trade_time()
}
fn first_trade_time(&self) -> Option<u64> {
self.first_trade_time()
}
fn last_price(&self) -> Price {
self.kline.close
}
fn kline(&self) -> Option<&Kline> {
Some(&self.kline)
}
fn value_high(&self) -> Price {
self.kline.high
}
fn value_low(&self) -> Price {
self.kline.low
}
}
#[derive(Debug, Clone, Default)]
pub struct GroupedTrades {
pub buy_qty: Qty,
pub sell_qty: Qty,
pub first_time: u64,
pub last_time: u64,
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.last_time = trade.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 => 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>,
pub poc: Option<PointOfControl>,
}
impl KlineTrades {
pub fn new() -> Self {
Self {
trades: FxHashMap::default(),
poc: None,
}
}
pub fn first_trade_t(&self) -> Option<u64> {
self.trades.values().map(|group| group.first_time).min()
}
pub fn last_trade_t(&self) -> Option<u64> {
self.trades.values().map(|group| group.last_time).max()
}
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) {
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 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 calculate_poc(&mut self) {
if self.trades.is_empty() {
return;
}
let mut max_volume = 0.0;
let mut poc_price = Price::from_f32(0.0);
for (price, group) in &self.trades {
let total_volume = f32::from(group.total_qty());
if total_volume > max_volume {
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.poc = None;
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize, Serialize)]
pub enum KlineChartKind {
#[default]
Candles,
Footprint {
clusters: ClusterKind,
#[serde(default)]
scaling: ClusterScaling,
studies: Vec<FootprintStudy>,
},
}
impl KlineChartKind {
pub fn min_scaling(&self) -> f32 {
match self {
KlineChartKind::Footprint { .. } => 0.4,
KlineChartKind::Candles => 0.6,
}
}
pub fn max_scaling(&self) -> f32 {
match self {
KlineChartKind::Footprint { .. } => 1.2,
KlineChartKind::Candles => 2.5,
}
}
pub fn max_cell_width(&self) -> f32 {
match self {
KlineChartKind::Footprint { .. } => 360.0,
KlineChartKind::Candles => 16.0,
}
}
pub fn min_cell_width(&self) -> f32 {
match self {
KlineChartKind::Footprint { .. } => 80.0,
KlineChartKind::Candles => 1.0,
}
}
pub fn max_cell_height(&self) -> f32 {
match self {
KlineChartKind::Footprint { .. } => 90.0,
KlineChartKind::Candles => 8.0,
}
}
pub fn min_cell_height(&self) -> f32 {
match self {
KlineChartKind::Footprint { .. } => 1.0,
KlineChartKind::Candles => 0.001,
}
}
pub fn default_cell_width(&self) -> f32 {
match self {
KlineChartKind::Footprint { .. } => 80.0,
KlineChartKind::Candles => 4.0,
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, Deserialize, Serialize)]
pub enum ClusterKind {
#[default]
BidAsk,
VolumeProfile,
DeltaProfile,
}
impl ClusterKind {
pub const ALL: [ClusterKind; 3] = [
ClusterKind::BidAsk,
ClusterKind::VolumeProfile,
ClusterKind::DeltaProfile,
];
}
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"),
}
}
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Deserialize, Serialize)]
pub struct Config {}
#[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,
];
}
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 {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum FootprintStudy {
NPoC {
lookback: usize,
},
Imbalance {
threshold: usize,
color_scale: Option<usize>,
ignore_zeros: bool,
},
}
impl FootprintStudy {
pub fn is_same_type(&self, other: &Self) -> bool {
matches!(
(self, other),
(FootprintStudy::NPoC { .. }, FootprintStudy::NPoC { .. })
| (
FootprintStudy::Imbalance { .. },
FootprintStudy::Imbalance { .. }
)
)
}
}
impl FootprintStudy {
pub const ALL: [FootprintStudy; 2] = [
FootprintStudy::NPoC { lookback: 80 },
FootprintStudy::Imbalance {
threshold: 200,
color_scale: Some(400),
ignore_zeros: true,
},
];
}
impl std::fmt::Display for FootprintStudy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FootprintStudy::NPoC { .. } => write!(f, "Naked Point of Control"),
FootprintStudy::Imbalance { .. } => write!(f, "Imbalance"),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct PointOfControl {
pub price: Price,
pub volume: f32,
pub status: NPoc,
}
impl Default for PointOfControl {
fn default() -> Self {
Self {
price: Price::from_f32(0.0),
volume: 0.0,
status: NPoc::default(),
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum NPoc {
#[default]
None,
Naked,
Filled {
at: u64,
},
}
impl NPoc {
pub fn filled(&mut self, at: u64) {
*self = NPoc::Filled { at };
}
pub fn unfilled(&mut self) {
*self = NPoc::Naked;
}
}