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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::model::{Bar, Source};
use super::{Indicator, IndicatorOutput};
/// How the bars since the anchor are weighted into the average.
///
/// The two answer different questions and the published descriptions of "TWAP" do not always say
/// which one they mean, so it is named here rather than assumed.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(rename_all = "snake_case")
)]
pub enum TwapWeighting {
/// Every bar counts once, whatever span it covers. The plain mean of the source since the
/// anchor — which is what a "TWAP" drawn over a bar series usually is. The default.
#[default]
PerBar,
/// Every price is weighted by how long it stood, measured as the gap to the next observation.
/// On evenly spaced bars this equals [`TwapWeighting::PerBar`]; on gappy or irregular data it
/// does not, and that difference is the reason the option exists.
ByDuration,
}
/// Where the average starts over.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(rename_all = "snake_case")
)]
pub enum TwapAnchor {
/// Never: one average over the whole series.
Continuous,
/// At the start of every UTC day, shifted by the given number of seconds.
Daily { start_offset_seconds: i64 },
/// At one fixed point in time. Bars before it produce no output.
ManualTimestamp(i64),
}
/// Time Weighted Average Price since an anchor.
///
/// The average of a chosen price source over the bars since the anchor, weighted as
/// [`TwapWeighting`] says. It is **not** a VWAP: no volume enters this at any point, and a bar
/// that traded a thousand contracts counts exactly as much as one that traded ten. It is also not
/// an execution algorithm — this is a line on a chart, not a schedule for working an order.
///
/// Under [`TwapWeighting::ByDuration`] a price is taken to hold until the next bar arrives, so
/// its weight is the gap to that bar. The current bar's own price has not stood for any time yet
/// and therefore carries no weight; with a single bar since the anchor there is nothing to weight
/// at all, and the value is that bar's source price.
///
/// Gaps are what separates the two weightings: a missing hour makes the preceding price count for
/// that hour under `ByDuration`, and count once under `PerBar`. Neither is wrong, and neither is
/// guessed at here.
///
/// Output: `value`, in the price units of the series. First output: the first bar at or after the
/// anchor. [`Indicator::reset`] clears the average.
#[derive(Debug, Clone)]
pub struct AnchoredTwap {
anchor: TwapAnchor,
source: Source,
weighting: TwapWeighting,
sum: f64,
weight: f64,
bars: usize,
previous: Option<(i64, f64)>,
current_period: Option<i64>,
}
impl AnchoredTwap {
pub fn new(anchor: TwapAnchor, source: Source, weighting: TwapWeighting) -> Self {
Self {
anchor,
source,
weighting,
sum: 0.0,
weight: 0.0,
bars: 0,
previous: None,
current_period: None,
}
}
pub fn with_defaults() -> Self {
Self::new(
TwapAnchor::Daily {
start_offset_seconds: 0,
},
Source::Close,
TwapWeighting::PerBar,
)
}
fn restart(&mut self) {
self.sum = 0.0;
self.weight = 0.0;
self.bars = 0;
self.previous = None;
}
}
impl Indicator for AnchoredTwap {
fn name(&self) -> &str {
"twap"
}
fn warmup_period(&self) -> usize {
0
}
fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
match self.anchor {
TwapAnchor::Continuous => {}
TwapAnchor::Daily {
start_offset_seconds,
} => {
let period = (bar.timestamp - start_offset_seconds).div_euclid(86_400);
if self.current_period != Some(period) {
self.current_period = Some(period);
self.restart();
}
}
TwapAnchor::ManualTimestamp(anchor) => {
if bar.timestamp < anchor {
return None;
}
}
}
let price = self.source.extract(bar);
if !price.is_finite() {
return None;
}
match self.weighting {
TwapWeighting::PerBar => {
self.sum += price;
self.bars += 1;
}
TwapWeighting::ByDuration => {
if let Some((previous_timestamp, previous_price)) = self.previous {
let duration = (bar.timestamp - previous_timestamp).max(0) as f64;
self.sum += previous_price * duration;
self.weight += duration;
}
}
}
self.previous = Some((bar.timestamp, price));
let value = match self.weighting {
TwapWeighting::PerBar => self.sum / self.bars as f64,
// Nothing has stood for any time yet: the single observation is the average.
TwapWeighting::ByDuration if self.weight <= 0.0 => price,
TwapWeighting::ByDuration => self.sum / self.weight,
};
Some(IndicatorOutput::new(value))
}
fn reset(&mut self) {
self.restart();
self.current_period = None;
}
}