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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
//! OI-to-Volume Ratio — open interest relative to traded volume.
use crate::derivatives::DerivativesTick;
use crate::traits::Indicator;
/// OI-to-Volume Ratio — open interest divided by the tick's total taker volume, a
/// measure of how much position is *held* versus *turned over*.
///
/// ```text
/// OIVR = open_interest / (taker_buy_volume + taker_sell_volume)
/// ```
///
/// A high ratio means open interest dwarfs the volume trading it — positions are
/// being held, not churned (low participation, potential complacency or a coiling
/// market). A low ratio means heavy volume relative to outstanding interest —
/// active churn, often around breakouts or capitulation. Watching the ratio change
/// distinguishes new-money trends (OI and volume both rising) from short-covering
/// or position rolls.
///
/// The ratio is non-negative; a tick with zero taker volume reports `0` rather than
/// dividing by zero. It is stateless — each tick yields one value (no warmup). Each
/// `update` is O(1).
///
/// # Example
///
/// ```
/// use wickra_core::{DerivativesTick, Indicator, OiToVolumeRatio};
///
/// let mut indicator = OiToVolumeRatio::new();
/// let tick = DerivativesTick::new(0.0, 100.0, 100.0, 100.0, 5_000.0, 0.0, 0.0, 400.0, 600.0, 0.0, 0.0, 0).unwrap();
/// let oivr = indicator.update(tick).unwrap();
/// assert!((oivr - 5.0).abs() < 1e-12); // 5000 / (400 + 600)
/// ```
#[derive(Debug, Clone, Default)]
pub struct OiToVolumeRatio {
ready: bool,
}
impl OiToVolumeRatio {
/// Construct a new OI-to-Volume Ratio. The indicator is parameter-free.
#[must_use]
pub const fn new() -> Self {
Self { ready: false }
}
}
impl Indicator for OiToVolumeRatio {
type Input = DerivativesTick;
type Output = f64;
fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
let volume = tick.taker_buy_volume + tick.taker_sell_volume;
let ratio = if volume > 0.0 {
tick.open_interest / volume
} else {
0.0
};
self.ready = true;
Some(ratio)
}
fn reset(&mut self) {
self.ready = false;
}
fn warmup_period(&self) -> usize {
1
}
fn is_ready(&self) -> bool {
self.ready
}
fn name(&self) -> &'static str {
"OiToVolumeRatio"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
fn tick(oi: f64, buy: f64, sell: f64) -> DerivativesTick {
DerivativesTick::new_unchecked(
0.0, 100.0, 100.0, 100.0, oi, 0.0, 0.0, buy, sell, 0.0, 0.0, 0,
)
}
#[test]
fn accessors_and_metadata() {
let o = OiToVolumeRatio::new();
assert_eq!(o.warmup_period(), 1);
assert_eq!(o.name(), "OiToVolumeRatio");
assert!(!o.is_ready());
}
#[test]
fn ratio_reference_value() {
let mut o = OiToVolumeRatio::new();
assert_relative_eq!(
o.update(tick(5_000.0, 400.0, 600.0)).unwrap(),
5.0,
epsilon = 1e-12
);
}
#[test]
fn more_volume_lowers_ratio() {
let mut o = OiToVolumeRatio::new();
let held = o.update(tick(5_000.0, 100.0, 100.0)).unwrap();
let churned = o.update(tick(5_000.0, 1_000.0, 1_000.0)).unwrap();
assert!(churned < held);
}
#[test]
fn zero_volume_is_zero() {
let mut o = OiToVolumeRatio::new();
assert_relative_eq!(
o.update(tick(5_000.0, 0.0, 0.0)).unwrap(),
0.0,
epsilon = 1e-12
);
}
#[test]
fn ready_after_first_update() {
let mut o = OiToVolumeRatio::new();
assert!(!o.is_ready());
o.update(tick(5_000.0, 100.0, 100.0));
assert!(o.is_ready());
}
#[test]
fn reset_clears_state() {
let mut o = OiToVolumeRatio::new();
o.update(tick(5_000.0, 100.0, 100.0));
assert!(o.is_ready());
o.reset();
assert!(!o.is_ready());
}
#[test]
fn batch_equals_streaming() {
let ticks: Vec<DerivativesTick> = (0..40)
.map(|i| tick(5_000.0, 100.0 + f64::from(i), 100.0))
.collect();
let batch = OiToVolumeRatio::new().batch(&ticks);
let mut b = OiToVolumeRatio::new();
let streamed: Vec<_> = ticks.iter().map(|x| b.update(*x)).collect();
assert_eq!(batch, streamed);
}
}