tesser_execution/algorithm/
vwap.rs1use 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}