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