tesser_execution/algorithm/
twap.rs1use anyhow::{anyhow, Result};
4use chrono::{DateTime, Duration, Utc};
5use rust_decimal::{prelude::FromPrimitive, Decimal};
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9use super::{AlgoStatus, ChildOrderRequest, ExecutionAlgorithm};
10use tesser_core::{Fill, Order, OrderRequest, OrderType, Quantity, Signal, Tick};
11
12#[derive(Debug, Deserialize, Serialize)]
14struct TwapState {
15 id: Uuid,
16 parent_signal: Signal,
17 status: String,
18
19 start_time: DateTime<Utc>,
20 end_time: DateTime<Utc>,
21
22 total_quantity: Quantity,
23 filled_quantity: Quantity,
24
25 num_slices: u32,
26 executed_slices: u32,
27
28 next_slice_time: DateTime<Utc>,
29 slice_interval: Duration,
30}
31
32pub struct TwapAlgorithm {
38 state: TwapState,
39}
40
41impl TwapAlgorithm {
42 pub fn new(
50 signal: Signal,
51 total_quantity: Quantity,
52 duration: Duration,
53 num_slices: u32,
54 ) -> Result<Self> {
55 if duration <= Duration::zero() || num_slices == 0 {
56 return Err(anyhow!("TWAP duration and slices must be positive"));
57 }
58
59 if total_quantity <= Decimal::ZERO {
60 return Err(anyhow!("TWAP total quantity must be positive"));
61 }
62
63 let now = Utc::now();
64 let slice_interval =
65 Duration::seconds((duration.num_seconds() as f64 / num_slices as f64).ceil() as i64);
66
67 Ok(Self {
68 state: TwapState {
69 id: Uuid::new_v4(),
70 parent_signal: signal,
71 status: "Working".to_string(),
72 start_time: now,
73 end_time: now + duration,
74 total_quantity,
75 filled_quantity: Decimal::ZERO,
76 num_slices,
77 executed_slices: 0,
78 next_slice_time: now,
79 slice_interval,
80 },
81 })
82 }
83
84 fn should_execute_slice(&self) -> bool {
86 let now = Utc::now();
87 now >= self.state.next_slice_time
88 && self.state.executed_slices < self.state.num_slices
89 && now < self.state.end_time
90 }
91
92 fn calculate_slice_quantity(&self) -> Quantity {
94 let remaining_qty = self.state.total_quantity - self.state.filled_quantity;
95 if remaining_qty <= Decimal::ZERO {
96 return Decimal::ZERO;
97 }
98
99 let remaining_slices = self.state.num_slices - self.state.executed_slices;
100 if remaining_slices == 0 {
101 return Decimal::ZERO;
102 }
103
104 let divisor = Decimal::from_u32(remaining_slices).unwrap_or(Decimal::ONE);
105 if divisor.is_zero() {
106 Decimal::ZERO
107 } else {
108 remaining_qty / divisor
109 }
110 }
111
112 fn create_slice_order(&self, slice_qty: Quantity) -> ChildOrderRequest {
114 ChildOrderRequest {
115 parent_algo_id: self.state.id,
116 order_request: OrderRequest {
117 symbol: self.state.parent_signal.symbol.clone(),
118 side: self.state.parent_signal.kind.side(),
119 order_type: OrderType::Market,
120 quantity: slice_qty,
121 price: None,
122 trigger_price: None,
123 time_in_force: None,
124 client_order_id: Some(format!(
125 "twap-{}-slice-{}",
126 self.state.id,
127 self.state.executed_slices + 1
128 )),
129 take_profit: None,
130 stop_loss: None,
131 display_quantity: None,
132 },
133 }
134 }
135
136 fn check_completion(&mut self) {
138 let now = Utc::now();
139
140 if now >= self.state.end_time {
141 self.state.status = "Completed".to_string();
142 tracing::info!(
143 id = %self.state.id,
144 "TWAP completed due to reaching end time"
145 );
146 } else if self.state.filled_quantity >= self.state.total_quantity {
147 self.state.status = "Completed".to_string();
148 tracing::info!(
149 id = %self.state.id,
150 filled = %self.state.filled_quantity,
151 total = %self.state.total_quantity,
152 "TWAP completed due to reaching total quantity"
153 );
154 }
155 }
156}
157
158impl ExecutionAlgorithm for TwapAlgorithm {
159 fn kind(&self) -> &'static str {
160 "TWAP"
161 }
162
163 fn id(&self) -> &Uuid {
164 &self.state.id
165 }
166
167 fn status(&self) -> AlgoStatus {
168 match self.state.status.as_str() {
169 "Working" => AlgoStatus::Working,
170 "Completed" => AlgoStatus::Completed,
171 "Cancelled" => AlgoStatus::Cancelled,
172 "Failed" => AlgoStatus::Failed("Generic failure".to_string()),
173 _ => AlgoStatus::Failed("Unknown state".to_string()),
174 }
175 }
176
177 fn start(&mut self) -> Result<Vec<ChildOrderRequest>> {
178 tracing::info!(
180 id = %self.state.id,
181 duration_secs = self.state.end_time.signed_duration_since(self.state.start_time).num_seconds(),
182 slices = self.state.num_slices,
183 total_qty = %self.state.total_quantity,
184 "TWAP algorithm started"
185 );
186 Ok(vec![])
187 }
188
189 fn on_child_order_placed(&mut self, order: &Order) {
190 tracing::debug!(
191 id = %self.state.id,
192 order_id = %order.id,
193 qty = %order.request.quantity,
194 "TWAP child order placed"
195 );
196 }
199
200 fn on_fill(&mut self, fill: &Fill) -> Result<Vec<ChildOrderRequest>> {
201 tracing::debug!(
202 id = %self.state.id,
203 fill_qty = %fill.fill_quantity,
204 fill_price = %fill.fill_price,
205 "TWAP received fill"
206 );
207
208 self.state.filled_quantity += fill.fill_quantity;
209 self.check_completion();
210
211 Ok(vec![])
212 }
213
214 fn on_tick(&mut self, _tick: &Tick) -> Result<Vec<ChildOrderRequest>> {
215 Ok(vec![])
217 }
218
219 fn on_timer(&mut self) -> Result<Vec<ChildOrderRequest>> {
220 self.check_completion();
222
223 if !matches!(self.status(), AlgoStatus::Working) {
224 return Ok(vec![]);
225 }
226
227 if !self.should_execute_slice() {
229 return Ok(vec![]);
230 }
231
232 let slice_qty = self.calculate_slice_quantity();
233 if slice_qty <= Decimal::ZERO {
234 return Ok(vec![]);
235 }
236
237 self.state.executed_slices += 1;
239 self.state.next_slice_time = Utc::now() + self.state.slice_interval;
240
241 tracing::debug!(
242 id = %self.state.id,
243 slice = self.state.executed_slices,
244 qty = %slice_qty,
245 next_slice_time = %self.state.next_slice_time,
246 "Executing TWAP slice"
247 );
248
249 let request = self.create_slice_order(slice_qty);
250 Ok(vec![request])
251 }
252
253 fn cancel(&mut self) -> Result<()> {
254 self.state.status = "Cancelled".to_string();
255 tracing::info!(id = %self.state.id, "TWAP algorithm cancelled");
256 Ok(())
257 }
258
259 fn state(&self) -> serde_json::Value {
260 serde_json::to_value(&self.state).expect("Failed to serialize TWAP state")
261 }
262
263 fn from_state(state_val: serde_json::Value) -> Result<Self>
264 where
265 Self: Sized,
266 {
267 let state: TwapState = serde_json::from_value(state_val)?;
268 Ok(Self { state })
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275 use tesser_core::SignalKind;
276
277 #[test]
278 fn test_twap_creation() {
279 let signal = Signal::new("BTCUSDT", SignalKind::EnterLong, 0.8);
280 let duration = Duration::minutes(30);
281 let twap = TwapAlgorithm::new(signal, Decimal::ONE, duration, 10).unwrap();
282
283 assert_eq!(twap.state.total_quantity, Decimal::ONE);
284 assert_eq!(twap.state.num_slices, 10);
285 assert_eq!(twap.status(), AlgoStatus::Working);
286 }
287
288 #[test]
289 fn test_twap_invalid_parameters() {
290 let signal = Signal::new("BTCUSDT", SignalKind::EnterLong, 0.8);
291
292 let result = TwapAlgorithm::new(signal.clone(), Decimal::ONE, Duration::zero(), 10);
294 assert!(result.is_err());
295
296 let result = TwapAlgorithm::new(signal, Decimal::ONE, Duration::minutes(30), 0);
298 assert!(result.is_err());
299 }
300
301 #[test]
302 fn test_slice_quantity_calculation() {
303 let signal = Signal::new("BTCUSDT", SignalKind::EnterLong, 0.8);
304 let twap = TwapAlgorithm::new(
305 signal,
306 Decimal::from_i32(10).unwrap(),
307 Duration::minutes(30),
308 5,
309 )
310 .unwrap();
311
312 let slice_qty = twap.calculate_slice_quantity();
313 assert_eq!(slice_qty, Decimal::from_i32(2).unwrap());
314 }
315}