tesser_execution/algorithm/
twap.rs

1//! Time-Weighted Average Price (TWAP) execution algorithm.
2
3use 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/// Persistent state for the TWAP algorithm.
13#[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
32/// TWAP (Time-Weighted Average Price) execution algorithm.
33///
34/// This algorithm spreads an order over a specified time duration by breaking it
35/// into smaller slices executed at regular intervals. This helps minimize market
36/// impact and achieve a more representative average price.
37pub struct TwapAlgorithm {
38    state: TwapState,
39}
40
41impl TwapAlgorithm {
42    /// Create a new TWAP algorithm instance.
43    ///
44    /// # Arguments
45    /// * `signal` - The parent signal that triggered this algorithm
46    /// * `total_quantity` - Total quantity to be executed
47    /// * `duration` - Time period over which to spread the execution
48    /// * `num_slices` - Number of smaller orders to break the total into
49    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    /// Check if we should execute the next slice based on current time.
85    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    /// Calculate the quantity for the next slice.
93    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    /// Generate a child order request for the next slice.
113    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    /// Check if the algorithm should complete based on time or quantity.
137    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        // TWAP starts by waiting for the first timer tick, so no initial orders
179        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        // For a simple TWAP, we don't need special handling when orders are placed
197        // A more sophisticated version could track in-flight orders
198    }
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        // TWAP is time-driven, not tick-driven
216        Ok(vec![])
217    }
218
219    fn on_timer(&mut self) -> Result<Vec<ChildOrderRequest>> {
220        // Check if algorithm should complete
221        self.check_completion();
222
223        if !matches!(self.status(), AlgoStatus::Working) {
224            return Ok(vec![]);
225        }
226
227        // Check if we should execute a slice
228        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        // Update state for the new slice
238        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        // Zero duration should fail
293        let result = TwapAlgorithm::new(signal.clone(), Decimal::ONE, Duration::zero(), 10);
294        assert!(result.is_err());
295
296        // Zero slices should fail
297        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}