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, ChildOrderAction, ChildOrderRequest, ExecutionAlgorithm};
10use tesser_core::{Fill, Order, OrderRequest, OrderStatus, 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 action: ChildOrderAction::Place(OrderRequest {
117 symbol: self.state.parent_signal.symbol,
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 fn slice_from_client_id(&self, client_id: &str) -> Option<u32> {
158 let rest = client_id.strip_prefix("twap-")?;
159 let (id_part, slice_part) = rest.split_once("-slice-")?;
160 if id_part != self.state.id.to_string() {
161 return None;
162 }
163 slice_part.parse().ok()
164 }
165
166 fn is_active_status(status: OrderStatus) -> bool {
167 matches!(
168 status,
169 OrderStatus::PendingNew | OrderStatus::Accepted | OrderStatus::PartiallyFilled
170 )
171 }
172}
173
174impl ExecutionAlgorithm for TwapAlgorithm {
175 fn kind(&self) -> &'static str {
176 "TWAP"
177 }
178
179 fn id(&self) -> &Uuid {
180 &self.state.id
181 }
182
183 fn status(&self) -> AlgoStatus {
184 match self.state.status.as_str() {
185 "Working" => AlgoStatus::Working,
186 "Completed" => AlgoStatus::Completed,
187 "Cancelled" => AlgoStatus::Cancelled,
188 "Failed" => AlgoStatus::Failed("Generic failure".to_string()),
189 _ => AlgoStatus::Failed("Unknown state".to_string()),
190 }
191 }
192
193 fn start(&mut self) -> Result<Vec<ChildOrderRequest>> {
194 tracing::info!(
196 id = %self.state.id,
197 duration_secs = self.state.end_time.signed_duration_since(self.state.start_time).num_seconds(),
198 slices = self.state.num_slices,
199 total_qty = %self.state.total_quantity,
200 "TWAP algorithm started"
201 );
202 Ok(vec![])
203 }
204
205 fn on_child_order_placed(&mut self, order: &Order) {
206 tracing::debug!(
207 id = %self.state.id,
208 order_id = %order.id,
209 qty = %order.request.quantity,
210 "TWAP child order placed"
211 );
212 }
215
216 fn on_fill(&mut self, fill: &Fill) -> Result<Vec<ChildOrderRequest>> {
217 tracing::debug!(
218 id = %self.state.id,
219 fill_qty = %fill.fill_quantity,
220 fill_price = %fill.fill_price,
221 "TWAP received fill"
222 );
223
224 self.state.filled_quantity += fill.fill_quantity;
225 self.check_completion();
226
227 Ok(vec![])
228 }
229
230 fn on_tick(&mut self, _tick: &Tick) -> Result<Vec<ChildOrderRequest>> {
231 Ok(vec![])
233 }
234
235 fn on_timer(&mut self) -> Result<Vec<ChildOrderRequest>> {
236 self.check_completion();
238
239 if !matches!(self.status(), AlgoStatus::Working) {
240 return Ok(vec![]);
241 }
242
243 if !self.should_execute_slice() {
245 return Ok(vec![]);
246 }
247
248 let slice_qty = self.calculate_slice_quantity();
249 if slice_qty <= Decimal::ZERO {
250 return Ok(vec![]);
251 }
252
253 self.state.executed_slices += 1;
255 self.state.next_slice_time = Utc::now() + self.state.slice_interval;
256
257 tracing::debug!(
258 id = %self.state.id,
259 slice = self.state.executed_slices,
260 qty = %slice_qty,
261 next_slice_time = %self.state.next_slice_time,
262 "Executing TWAP slice"
263 );
264
265 let request = self.create_slice_order(slice_qty);
266 Ok(vec![request])
267 }
268
269 fn cancel(&mut self) -> Result<()> {
270 self.state.status = "Cancelled".to_string();
271 tracing::info!(id = %self.state.id, "TWAP algorithm cancelled");
272 Ok(())
273 }
274
275 fn bind_child_order(&mut self, order: Order) -> Result<()> {
276 if !Self::is_active_status(order.status) {
277 return Ok(());
278 }
279 let Some(client_id) = order.request.client_order_id.as_deref() else {
280 return Ok(());
281 };
282 let Some(slice_number) = self.slice_from_client_id(client_id) else {
283 return Ok(());
284 };
285
286 let previous = self.state.executed_slices;
287 if slice_number > previous {
288 self.state.executed_slices = slice_number;
289 }
290 self.state.next_slice_time = Utc::now() + self.state.slice_interval;
291 if !matches!(self.status(), AlgoStatus::Working) {
292 self.state.status = "Working".to_string();
293 }
294
295 tracing::info!(
296 id = %self.state.id,
297 slice = slice_number,
298 order_id = %order.id,
299 "bound TWAP child order after recovery"
300 );
301 Ok(())
302 }
303
304 fn state(&self) -> serde_json::Value {
305 serde_json::to_value(&self.state).expect("Failed to serialize TWAP state")
306 }
307
308 fn from_state(state_val: serde_json::Value) -> Result<Self>
309 where
310 Self: Sized,
311 {
312 let state: TwapState = serde_json::from_value(state_val)?;
313 tracing::debug!(
314 symbol = %state.parent_signal.symbol,
315 id = %state.id,
316 "Restored TWAP algorithm state"
317 );
318 Ok(Self { state })
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325 use tesser_core::SignalKind;
326
327 #[test]
328 fn test_twap_creation() {
329 let signal = Signal::new("BTCUSDT", SignalKind::EnterLong, 0.8);
330 let duration = Duration::minutes(30);
331 let twap = TwapAlgorithm::new(signal, Decimal::ONE, duration, 10).unwrap();
332
333 assert_eq!(twap.state.total_quantity, Decimal::ONE);
334 assert_eq!(twap.state.num_slices, 10);
335 assert_eq!(twap.status(), AlgoStatus::Working);
336 }
337
338 #[test]
339 fn test_twap_invalid_parameters() {
340 let signal = Signal::new("BTCUSDT", SignalKind::EnterLong, 0.8);
341
342 let result = TwapAlgorithm::new(signal.clone(), Decimal::ONE, Duration::zero(), 10);
344 assert!(result.is_err());
345
346 let result = TwapAlgorithm::new(signal, Decimal::ONE, Duration::minutes(30), 0);
348 assert!(result.is_err());
349 }
350
351 #[test]
352 fn test_slice_quantity_calculation() {
353 let signal = Signal::new("BTCUSDT", SignalKind::EnterLong, 0.8);
354 let twap = TwapAlgorithm::new(
355 signal,
356 Decimal::from_i32(10).unwrap(),
357 Duration::minutes(30),
358 5,
359 )
360 .unwrap();
361
362 let slice_qty = twap.calculate_slice_quantity();
363 assert_eq!(slice_qty, Decimal::from_i32(2).unwrap());
364 }
365}