tesser_execution/algorithm/
vwap.rs

1use anyhow::{bail, Result};
2use chrono::{DateTime, Duration, Utc};
3use rust_decimal::{prelude::FromPrimitive, Decimal};
4use serde::{Deserialize, Serialize};
5use tesser_core::{Order, OrderRequest, OrderType, Quantity, Signal, Tick};
6use uuid::Uuid;
7
8use super::{AlgoStatus, ChildOrderAction, ChildOrderRequest, ExecutionAlgorithm};
9
10#[derive(Debug, Deserialize, Serialize)]
11struct VwapState {
12    id: Uuid,
13    parent_signal: Signal,
14    status: String,
15    total_quantity: Quantity,
16    filled_quantity: Quantity,
17    participation_rate: Option<Decimal>,
18    start_time: DateTime<Utc>,
19    end_time: DateTime<Utc>,
20    observed_volume: Quantity,
21    min_slice: Quantity,
22    next_child_seq: u32,
23}
24
25pub struct VwapAlgorithm {
26    state: VwapState,
27}
28
29impl VwapAlgorithm {
30    pub fn new(
31        signal: Signal,
32        total_quantity: Quantity,
33        duration: Duration,
34        participation_rate: Option<Decimal>,
35    ) -> Result<Self> {
36        if total_quantity <= Decimal::ZERO {
37            bail!("VWAP total quantity must be positive");
38        }
39        if duration <= Duration::zero() {
40            bail!("VWAP duration must be positive");
41        }
42
43        let now = Utc::now();
44        let min_slice = (total_quantity * Decimal::new(5, 2)).max(Decimal::new(1, 3));
45        Ok(Self {
46            state: VwapState {
47                id: Uuid::new_v4(),
48                parent_signal: signal,
49                status: "Working".into(),
50                total_quantity,
51                filled_quantity: Decimal::ZERO,
52                participation_rate,
53                start_time: now,
54                end_time: now + duration,
55                observed_volume: Decimal::ZERO,
56                min_slice,
57                next_child_seq: 0,
58            },
59        })
60    }
61
62    fn schedule_target(&self, now: DateTime<Utc>) -> Quantity {
63        let total_window = self
64            .state
65            .end_time
66            .signed_duration_since(self.state.start_time);
67        let elapsed = now.signed_duration_since(self.state.start_time);
68        if total_window.num_milliseconds() <= 0 {
69            return self.state.total_quantity;
70        }
71        let total_ms = Decimal::from_i64(total_window.num_milliseconds()).unwrap_or(Decimal::ONE);
72        let elapsed_ms = Decimal::from_i64(elapsed.num_milliseconds()).unwrap_or(Decimal::ZERO);
73        let mut progress = if total_ms.is_zero() {
74            Decimal::ONE
75        } else {
76            elapsed_ms / total_ms
77        };
78        if progress < Decimal::ZERO {
79            progress = Decimal::ZERO;
80        } else if progress > Decimal::ONE {
81            progress = Decimal::ONE;
82        }
83        let mut target = self.state.total_quantity * progress;
84        if let Some(rate) = self.state.participation_rate {
85            let clamped = rate.max(Decimal::ZERO);
86            target = target.min(self.state.observed_volume * clamped);
87        }
88        target.min(self.state.total_quantity)
89    }
90
91    fn build_market_child(&mut self, quantity: Quantity) -> ChildOrderRequest {
92        self.state.next_child_seq += 1;
93        ChildOrderRequest {
94            parent_algo_id: self.state.id,
95            action: ChildOrderAction::Place(OrderRequest {
96                symbol: self.state.parent_signal.symbol,
97                side: self.state.parent_signal.kind.side(),
98                order_type: OrderType::Market,
99                quantity,
100                price: None,
101                trigger_price: None,
102                time_in_force: None,
103                client_order_id: Some(format!(
104                    "vwap-{}-{}",
105                    self.state.id, self.state.next_child_seq
106                )),
107                take_profit: None,
108                stop_loss: None,
109                display_quantity: None,
110            }),
111        }
112    }
113
114    fn check_completion(&mut self) {
115        if self.state.filled_quantity >= self.state.total_quantity {
116            self.state.status = "Completed".into();
117            return;
118        }
119        if Utc::now() >= self.state.end_time {
120            self.state.status = "Completed".into();
121        }
122    }
123}
124
125impl ExecutionAlgorithm for VwapAlgorithm {
126    fn kind(&self) -> &'static str {
127        "VWAP"
128    }
129
130    fn id(&self) -> &Uuid {
131        &self.state.id
132    }
133
134    fn status(&self) -> AlgoStatus {
135        match self.state.status.as_str() {
136            "Working" => AlgoStatus::Working,
137            "Completed" => AlgoStatus::Completed,
138            "Cancelled" => AlgoStatus::Cancelled,
139            other => AlgoStatus::Failed(other.to_string()),
140        }
141    }
142
143    fn start(&mut self) -> Result<Vec<ChildOrderRequest>> {
144        Ok(Vec::new())
145    }
146
147    fn on_child_order_placed(&mut self, _order: &Order) {}
148
149    fn on_fill(&mut self, fill: &tesser_core::Fill) -> Result<Vec<ChildOrderRequest>> {
150        self.state.filled_quantity += fill.fill_quantity;
151        self.check_completion();
152        Ok(Vec::new())
153    }
154
155    fn on_tick(&mut self, tick: &Tick) -> Result<Vec<ChildOrderRequest>> {
156        self.state.observed_volume += tick.size.max(Decimal::ZERO);
157        if !matches!(self.status(), AlgoStatus::Working) {
158            return Ok(Vec::new());
159        }
160        let now = Utc::now();
161        let target = self.schedule_target(now);
162        let deficit = target - self.state.filled_quantity;
163        if deficit <= self.state.min_slice {
164            return Ok(Vec::new());
165        }
166        let qty = deficit.min(self.state.total_quantity - self.state.filled_quantity);
167        if qty <= Decimal::ZERO {
168            return Ok(Vec::new());
169        }
170        Ok(vec![self.build_market_child(qty)])
171    }
172
173    fn on_timer(&mut self) -> Result<Vec<ChildOrderRequest>> {
174        self.check_completion();
175        Ok(Vec::new())
176    }
177
178    fn cancel(&mut self) -> Result<()> {
179        self.state.status = "Cancelled".into();
180        Ok(())
181    }
182
183    fn state(&self) -> serde_json::Value {
184        serde_json::to_value(&self.state).expect("vwap state serialization failed")
185    }
186
187    fn from_state(state: serde_json::Value) -> Result<Self>
188    where
189        Self: Sized,
190    {
191        let state: VwapState = serde_json::from_value(state)?;
192        Ok(Self { state })
193    }
194}