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