1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
//! Taker Buy/Sell Ratio — aggressive buy volume relative to aggressive sell
//! volume.
use crate::derivatives::DerivativesTick;
use crate::traits::Indicator;
/// Taker Buy/Sell Ratio — the taker (market-order) buy volume divided by the
/// taker sell volume carried by each tick.
///
/// ```text
/// takerBuySellRatio = takerBuyVolume / takerSellVolume
/// ```
///
/// Taker volume is the volume that crossed the spread — the aggressive flow that
/// moves price. A ratio above `1` means buyers are lifting offers faster than
/// sellers are hitting bids (net aggressive buying); below `1` the reverse. It
/// is the perpetual-feed analogue of [trade imbalance], read straight off the
/// venue's taker-volume fields. When taker sell volume is zero the ratio is
/// undefined and the indicator reports `0.0`.
///
/// `Input = DerivativesTick`, `Output = f64`. Stateless; ready after the first
/// tick.
///
/// [trade imbalance]: crate::TradeImbalance
///
/// # Example
///
/// ```
/// use wickra_core::{DerivativesTick, Indicator, TakerBuySellRatio};
///
/// fn tick(buy: f64, sell: f64) -> DerivativesTick {
/// DerivativesTick::new(0.0, 100.0, 100.0, 100.0, 0.0, 0.0, 0.0, buy, sell, 0.0, 0.0, 0)
/// .unwrap()
/// }
///
/// let mut tbs = TakerBuySellRatio::new();
/// // 60 taker buys vs 40 taker sells -> 1.5.
/// assert_eq!(tbs.update(tick(60.0, 40.0)), Some(1.5));
/// ```
#[derive(Debug, Clone, Default)]
pub struct TakerBuySellRatio {
has_emitted: bool,
}
impl TakerBuySellRatio {
/// Construct a new taker buy/sell ratio indicator.
#[must_use]
pub const fn new() -> Self {
Self { has_emitted: false }
}
}
impl Indicator for TakerBuySellRatio {
type Input = DerivativesTick;
type Output = f64;
fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
self.has_emitted = true;
if tick.taker_sell_volume == 0.0 {
// No taker sell volume to divide by: the ratio is undefined.
return Some(0.0);
}
Some(tick.taker_buy_volume / tick.taker_sell_volume)
}
fn reset(&mut self) {
self.has_emitted = false;
}
fn warmup_period(&self) -> usize {
1
}
fn is_ready(&self) -> bool {
self.has_emitted
}
fn name(&self) -> &'static str {
"TakerBuySellRatio"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
fn tick(buy: f64, sell: f64) -> DerivativesTick {
DerivativesTick::new_unchecked(
0.0, 100.0, 100.0, 100.0, 0.0, 0.0, 0.0, buy, sell, 0.0, 0.0, 0,
)
}
#[test]
fn accessors_and_metadata() {
let tbs = TakerBuySellRatio::new();
assert_eq!(tbs.name(), "TakerBuySellRatio");
assert_eq!(tbs.warmup_period(), 1);
assert!(!tbs.is_ready());
}
#[test]
fn divides_buy_by_sell() {
let mut tbs = TakerBuySellRatio::new();
assert_eq!(tbs.update(tick(60.0, 40.0)), Some(1.5));
assert_eq!(tbs.update(tick(20.0, 80.0)), Some(0.25));
assert!(tbs.is_ready());
}
#[test]
fn zero_sell_is_zero() {
let mut tbs = TakerBuySellRatio::new();
assert_eq!(tbs.update(tick(60.0, 0.0)), Some(0.0));
}
#[test]
fn batch_equals_streaming() {
let ticks: Vec<DerivativesTick> = (0..20)
.map(|i| tick(50.0 + f64::from(i % 5) * 5.0, 40.0 + f64::from(i % 3) * 5.0))
.collect();
let mut a = TakerBuySellRatio::new();
let mut b = TakerBuySellRatio::new();
assert_eq!(
a.batch(&ticks),
ticks.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut tbs = TakerBuySellRatio::new();
tbs.update(tick(60.0, 40.0));
assert!(tbs.is_ready());
tbs.reset();
assert!(!tbs.is_ready());
}
}