tesser_execution/algorithm/
iceberg.rs

1use 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}