tesser_execution/algorithm/
pegged.rs1use anyhow::{anyhow, Result};
2use chrono::{DateTime, Duration, Utc};
3use rust_decimal::Decimal;
4use serde::{Deserialize, Serialize};
5use tesser_core::{
6 Order, OrderRequest, OrderStatus, OrderType, Quantity, Signal, Tick, TimeInForce,
7};
8use uuid::Uuid;
9
10use super::{AlgoStatus, ChildOrderRequest, ExecutionAlgorithm};
11
12#[derive(Debug, Deserialize, Serialize)]
13struct PeggedState {
14 id: Uuid,
15 parent_signal: Signal,
16 status: String,
17 total_quantity: Quantity,
18 filled_quantity: Quantity,
19 clip_size: Quantity,
20 offset_bps: Decimal,
21 refresh: Duration,
22 last_order_time: Option<DateTime<Utc>>,
23 last_peg_price: Option<Decimal>,
24 next_child_seq: u32,
25}
26
27pub struct PeggedBestAlgorithm {
29 state: PeggedState,
30}
31
32impl PeggedBestAlgorithm {
33 pub fn new(
34 signal: Signal,
35 total_quantity: Quantity,
36 offset_bps: Decimal,
37 clip_size: Option<Quantity>,
38 refresh: Duration,
39 ) -> Result<Self> {
40 if total_quantity <= Decimal::ZERO {
41 return Err(anyhow!("pegged algorithm requires positive quantity"));
42 }
43 if offset_bps < Decimal::ZERO {
44 return Err(anyhow!("offset must be non-negative"));
45 }
46 if refresh <= Duration::zero() {
47 return Err(anyhow!("refresh interval must be positive"));
48 }
49 let clip = clip_size
50 .unwrap_or(total_quantity)
51 .max(Decimal::ZERO)
52 .min(total_quantity);
53 Ok(Self {
54 state: PeggedState {
55 id: Uuid::new_v4(),
56 parent_signal: signal,
57 status: "Working".into(),
58 total_quantity,
59 filled_quantity: Decimal::ZERO,
60 clip_size: if clip <= Decimal::ZERO {
61 total_quantity
62 } else {
63 clip
64 },
65 offset_bps,
66 refresh,
67 last_order_time: None,
68 last_peg_price: None,
69 next_child_seq: 0,
70 },
71 })
72 }
73
74 fn remaining(&self) -> Quantity {
75 (self.state.total_quantity - self.state.filled_quantity).max(Decimal::ZERO)
76 }
77
78 fn peg_price(&self, tick_price: Decimal) -> Decimal {
79 let offset = self.state.offset_bps / Decimal::from(10_000);
80 match self.state.parent_signal.kind.side() {
81 tesser_core::Side::Buy => tick_price * (Decimal::ONE - offset),
82 tesser_core::Side::Sell => tick_price * (Decimal::ONE + offset),
83 }
84 }
85
86 fn should_refresh(&self, price: Decimal, now: DateTime<Utc>) -> bool {
87 if self.last_emit_elapsed(now) >= self.state.refresh {
88 return true;
89 }
90 self.state
91 .last_peg_price
92 .map(|prev| (prev - price).abs() > Decimal::ZERO)
93 .unwrap_or(true)
94 }
95
96 fn last_emit_elapsed(&self, now: DateTime<Utc>) -> Duration {
97 self.state
98 .last_order_time
99 .map(|ts| now.signed_duration_since(ts))
100 .unwrap_or(self.state.refresh)
101 }
102
103 fn build_child(&mut self, price: Decimal, qty: Quantity) -> ChildOrderRequest {
104 self.state.next_child_seq += 1;
105 ChildOrderRequest {
106 parent_algo_id: self.state.id,
107 order_request: OrderRequest {
108 symbol: self.state.parent_signal.symbol.clone(),
109 side: self.state.parent_signal.kind.side(),
110 order_type: OrderType::Limit,
111 quantity: qty,
112 price: Some(price),
113 trigger_price: None,
114 time_in_force: Some(TimeInForce::ImmediateOrCancel),
115 client_order_id: Some(format!(
116 "peg-{}-{}",
117 self.state.id, self.state.next_child_seq
118 )),
119 take_profit: None,
120 stop_loss: None,
121 display_quantity: None,
122 },
123 }
124 }
125
126 fn mark_completed(&mut self) {
127 if self.remaining() <= Decimal::ZERO {
128 self.state.status = "Completed".into();
129 }
130 }
131
132 fn sequence_from_client_id(&self, client_id: &str) -> Option<u32> {
133 let rest = client_id.strip_prefix("peg-")?;
134 let (id_part, seq_part) = rest.rsplit_once('-')?;
135 if id_part != self.state.id.to_string() {
136 return None;
137 }
138 seq_part.parse().ok()
139 }
140
141 fn is_active_status(status: OrderStatus) -> bool {
142 matches!(
143 status,
144 OrderStatus::PendingNew | OrderStatus::Accepted | OrderStatus::PartiallyFilled
145 )
146 }
147}
148
149impl ExecutionAlgorithm for PeggedBestAlgorithm {
150 fn kind(&self) -> &'static str {
151 "PEGGED_BEST"
152 }
153
154 fn id(&self) -> &Uuid {
155 &self.state.id
156 }
157
158 fn status(&self) -> AlgoStatus {
159 match self.state.status.as_str() {
160 "Working" => AlgoStatus::Working,
161 "Completed" => AlgoStatus::Completed,
162 "Cancelled" => AlgoStatus::Cancelled,
163 other => AlgoStatus::Failed(other.to_string()),
164 }
165 }
166
167 fn start(&mut self) -> Result<Vec<ChildOrderRequest>> {
168 Ok(Vec::new())
169 }
170
171 fn on_child_order_placed(&mut self, _order: &Order) {
172 self.state.last_order_time = Some(Utc::now());
173 }
174
175 fn on_fill(&mut self, fill: &tesser_core::Fill) -> Result<Vec<ChildOrderRequest>> {
176 self.state.filled_quantity += fill.fill_quantity;
177 self.mark_completed();
178 Ok(Vec::new())
179 }
180
181 fn on_tick(&mut self, tick: &Tick) -> Result<Vec<ChildOrderRequest>> {
182 if !matches!(self.status(), AlgoStatus::Working) {
183 return Ok(Vec::new());
184 }
185 let remaining = self.remaining();
186 if remaining <= Decimal::ZERO {
187 self.state.status = "Completed".into();
188 return Ok(Vec::new());
189 }
190 let now = Utc::now();
191 let price = self.peg_price(tick.price.max(Decimal::ZERO));
192 if !self.should_refresh(price, now) {
193 return Ok(Vec::new());
194 }
195 self.state.last_peg_price = Some(price);
196 let qty = remaining.min(self.state.clip_size);
197 Ok(vec![self.build_child(price, qty)])
198 }
199
200 fn on_timer(&mut self) -> Result<Vec<ChildOrderRequest>> {
201 self.mark_completed();
202 Ok(Vec::new())
203 }
204
205 fn cancel(&mut self) -> Result<()> {
206 self.state.status = "Cancelled".into();
207 Ok(())
208 }
209
210 fn bind_child_order(&mut self, order: Order) -> Result<()> {
211 if !Self::is_active_status(order.status) {
212 return Ok(());
213 }
214 let Some(client_id) = order.request.client_order_id.as_deref() else {
215 return Ok(());
216 };
217 if let Some(seq) = self.sequence_from_client_id(client_id) {
218 self.state.next_child_seq = self.state.next_child_seq.max(seq);
219 }
220 self.state.last_order_time = Some(order.updated_at);
221 if let Some(price) = order.request.price {
222 self.state.last_peg_price = Some(price);
223 }
224 if !matches!(self.status(), AlgoStatus::Working) {
225 self.state.status = "Working".into();
226 }
227 Ok(())
228 }
229
230 fn state(&self) -> serde_json::Value {
231 serde_json::to_value(&self.state).expect("pegged state serialization failed")
232 }
233
234 fn from_state(state: serde_json::Value) -> Result<Self>
235 where
236 Self: Sized,
237 {
238 let state: PeggedState = serde_json::from_value(state)?;
239 Ok(Self { state })
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use tesser_core::{Signal, SignalKind, Tick};
247
248 #[test]
249 fn emits_child_after_tick() {
250 let signal = Signal::new("BTCUSDT", SignalKind::EnterLong, 0.9);
251 let mut algo = PeggedBestAlgorithm::new(
252 signal,
253 Decimal::from(5),
254 Decimal::new(5, 1),
255 None,
256 Duration::seconds(1),
257 )
258 .unwrap();
259 let tick = Tick {
260 symbol: "BTCUSDT".into(),
261 price: Decimal::from(100),
262 size: Decimal::ONE,
263 side: tesser_core::Side::Buy,
264 exchange_timestamp: Utc::now(),
265 received_at: Utc::now(),
266 };
267 let orders = algo.on_tick(&tick).unwrap();
268 assert_eq!(orders.len(), 1);
269 assert!(orders[0].order_request.quantity > Decimal::ZERO);
270 }
271}