tesser_execution/algorithm/
iceberg.rs

1use anyhow::{bail, Result};
2use rust_decimal::Decimal;
3use serde::{Deserialize, Serialize};
4use tesser_core::{
5    Order, OrderRequest, OrderStatus, OrderType, Price, Quantity, Side, Signal, Tick, TimeInForce,
6};
7use tracing::info;
8use uuid::Uuid;
9
10use super::{AlgoStatus, ChildOrderAction, ChildOrderRequest, ExecutionAlgorithm};
11
12#[derive(Debug, Deserialize, Serialize)]
13struct ActiveChild {
14    order_id: String,
15    remaining: Quantity,
16}
17
18#[derive(Debug, Deserialize, Serialize)]
19struct IcebergState {
20    id: Uuid,
21    parent_signal: Signal,
22    status: String,
23    total_quantity: Quantity,
24    filled_quantity: Quantity,
25    display_quantity: Quantity,
26    limit_price: Price,
27    limit_offset_bps: Option<Decimal>,
28    next_child_seq: u32,
29    active_child: Option<ActiveChild>,
30}
31
32pub struct IcebergAlgorithm {
33    state: IcebergState,
34}
35
36impl IcebergAlgorithm {
37    pub fn new(
38        signal: Signal,
39        total_quantity: Quantity,
40        display_quantity: Quantity,
41        limit_price: Price,
42        limit_offset_bps: Option<Decimal>,
43    ) -> Result<Self> {
44        if total_quantity <= Decimal::ZERO {
45            bail!("iceberg total quantity must be positive");
46        }
47        if display_quantity <= Decimal::ZERO {
48            bail!("iceberg display quantity must be positive");
49        }
50        if limit_price <= Decimal::ZERO {
51            bail!("iceberg limit price must be positive");
52        }
53        Ok(Self {
54            state: IcebergState {
55                id: Uuid::new_v4(),
56                parent_signal: signal,
57                status: "Working".into(),
58                total_quantity,
59                filled_quantity: Decimal::ZERO,
60                display_quantity,
61                limit_price,
62                limit_offset_bps,
63                next_child_seq: 0,
64                active_child: None,
65            },
66        })
67    }
68
69    fn remaining_parent(&self) -> Quantity {
70        (self.state.total_quantity - self.state.filled_quantity).max(Decimal::ZERO)
71    }
72
73    fn build_limit_child(&mut self, quantity: Quantity) -> ChildOrderRequest {
74        self.state.next_child_seq += 1;
75        let price = self.adjusted_limit_price();
76        ChildOrderRequest {
77            parent_algo_id: self.state.id,
78            action: ChildOrderAction::Place(OrderRequest {
79                symbol: self.state.parent_signal.symbol,
80                side: self.state.parent_signal.kind.side(),
81                order_type: OrderType::Limit,
82                quantity,
83                price: Some(price),
84                trigger_price: None,
85                time_in_force: Some(TimeInForce::GoodTilCanceled),
86                client_order_id: Some(format!(
87                    "iceberg-{}-{}",
88                    self.state.id, self.state.next_child_seq
89                )),
90                take_profit: None,
91                stop_loss: None,
92                display_quantity: Some(self.state.display_quantity.min(quantity)),
93            }),
94        }
95    }
96
97    fn adjusted_limit_price(&self) -> Price {
98        let base = self.state.limit_price;
99        let offset = self
100            .state
101            .limit_offset_bps
102            .unwrap_or(Decimal::ZERO)
103            .max(Decimal::ZERO)
104            / Decimal::from(10_000);
105        match self.state.parent_signal.kind.side() {
106            Side::Buy => base * (Decimal::ONE + offset),
107            Side::Sell => base * (Decimal::ONE - offset),
108        }
109    }
110
111    fn maybe_spawn_slice(&mut self) -> Vec<ChildOrderRequest> {
112        if !matches!(self.status(), AlgoStatus::Working) {
113            return Vec::new();
114        }
115        if self.remaining_parent() <= Decimal::ZERO || self.state.active_child.is_some() {
116            return Vec::new();
117        }
118        let qty = self
119            .remaining_parent()
120            .min(self.state.display_quantity)
121            .max(Decimal::ZERO);
122        if qty <= Decimal::ZERO {
123            return Vec::new();
124        }
125        vec![self.build_limit_child(qty)]
126    }
127
128    fn complete_if_needed(&mut self) {
129        if self.remaining_parent() <= Decimal::ZERO {
130            self.state.status = "Completed".into();
131        }
132    }
133
134    fn sequence_from_client_id(&self, client_id: &str) -> Option<u32> {
135        let rest = client_id.strip_prefix("iceberg-")?;
136        let (id_part, seq_part) = rest.rsplit_once('-')?;
137        if id_part != self.state.id.to_string() {
138            return None;
139        }
140        seq_part.parse().ok()
141    }
142
143    fn is_active_status(status: OrderStatus) -> bool {
144        matches!(
145            status,
146            OrderStatus::PendingNew | OrderStatus::Accepted | OrderStatus::PartiallyFilled
147        )
148    }
149}
150
151impl ExecutionAlgorithm for IcebergAlgorithm {
152    fn kind(&self) -> &'static str {
153        "ICEBERG"
154    }
155
156    fn id(&self) -> &Uuid {
157        &self.state.id
158    }
159
160    fn status(&self) -> AlgoStatus {
161        match self.state.status.as_str() {
162            "Working" => AlgoStatus::Working,
163            "Completed" => AlgoStatus::Completed,
164            "Cancelled" => AlgoStatus::Cancelled,
165            other => AlgoStatus::Failed(other.to_string()),
166        }
167    }
168
169    fn start(&mut self) -> Result<Vec<ChildOrderRequest>> {
170        Ok(self.maybe_spawn_slice())
171    }
172
173    fn on_child_order_placed(&mut self, order: &Order) {
174        self.state.active_child = Some(ActiveChild {
175            order_id: order.id.clone(),
176            remaining: order.request.quantity.abs(),
177        });
178    }
179
180    fn on_fill(&mut self, fill: &tesser_core::Fill) -> Result<Vec<ChildOrderRequest>> {
181        self.state.filled_quantity += fill.fill_quantity;
182        if let Some(active) = self.state.active_child.as_mut() {
183            if active.order_id == fill.order_id {
184                active.remaining -= fill.fill_quantity;
185            }
186            if active.remaining <= Decimal::ZERO {
187                self.state.active_child = None;
188            }
189        }
190        self.complete_if_needed();
191        Ok(self.maybe_spawn_slice())
192    }
193
194    fn on_tick(&mut self, _tick: &Tick) -> Result<Vec<ChildOrderRequest>> {
195        Ok(Vec::new())
196    }
197
198    fn on_timer(&mut self) -> Result<Vec<ChildOrderRequest>> {
199        self.complete_if_needed();
200        Ok(Vec::new())
201    }
202
203    fn cancel(&mut self) -> Result<()> {
204        self.state.status = "Cancelled".into();
205        Ok(())
206    }
207
208    fn bind_child_order(&mut self, order: Order) -> Result<()> {
209        if !Self::is_active_status(order.status) {
210            return Ok(());
211        }
212        let Some(client_id) = order.request.client_order_id.as_deref() else {
213            return Ok(());
214        };
215        let Some(seq) = self.sequence_from_client_id(client_id) else {
216            return Ok(());
217        };
218
219        self.state.next_child_seq = self.state.next_child_seq.max(seq);
220        let remaining = (order.request.quantity.abs() - order.filled_quantity).max(Decimal::ZERO);
221        self.state.active_child = Some(ActiveChild {
222            order_id: order.id.clone(),
223            remaining,
224        });
225        if !matches!(self.status(), AlgoStatus::Working) {
226            self.state.status = "Working".into();
227        }
228
229        info!(
230            id = %self.state.id,
231            order_id = %order.id,
232            seq = seq,
233            remaining = %remaining,
234            "bound Iceberg child order after recovery"
235        );
236        Ok(())
237    }
238
239    fn state(&self) -> serde_json::Value {
240        serde_json::to_value(&self.state).expect("iceberg state serialization failed")
241    }
242
243    fn from_state(state: serde_json::Value) -> Result<Self>
244    where
245        Self: Sized,
246    {
247        let state: IcebergState = serde_json::from_value(state)?;
248        Ok(Self { state })
249    }
250}