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