tesser_execution/algorithm/
pegged.rs

1use 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
25/// Algorithm that repeatedly submits IOC limit orders pegged to the top of book.
26pub 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}