1use bot_core::{AssetId, Balance, ClientOrderId, Fee, OrderSide, Price, Qty, StrategyId};
4use rust_decimal::Decimal;
5use std::collections::HashMap;
6
7#[derive(Debug, Clone)]
9pub struct Reservation {
10 pub asset: AssetId,
12 pub amount: Decimal,
14}
15
16pub struct InventoryLedger {
18 balances: HashMap<AssetId, AccountBalance>,
20
21 order_reservations: HashMap<ClientOrderId, Vec<Reservation>>,
23
24 strategy_allocations: HashMap<StrategyId, StrategyAllocation>,
26
27 strategy_reserved: HashMap<(StrategyId, AssetId), Decimal>,
29}
30
31#[derive(Debug, Clone, Default)]
32struct AccountBalance {
33 total: Decimal,
34 reserved: Decimal,
35}
36
37impl AccountBalance {
38 fn available(&self) -> Decimal {
39 self.total - self.reserved
40 }
41}
42
43#[derive(Debug, Clone)]
45pub struct StrategyAllocation {
46 pub strategy_id: StrategyId,
48 pub budgets: HashMap<AssetId, Decimal>,
50}
51
52impl InventoryLedger {
53 pub fn new() -> Self {
55 Self {
56 balances: HashMap::new(),
57 order_reservations: HashMap::new(),
58 strategy_allocations: HashMap::new(),
59 strategy_reserved: HashMap::new(),
60 }
61 }
62
63 pub fn set_balance(&mut self, asset: &AssetId, total: Decimal) {
65 let entry = self.balances.entry(asset.clone()).or_default();
66 entry.total = total;
67 }
68
69 pub fn balance(&self, asset: &AssetId) -> Balance {
71 if let Some(b) = self.balances.get(asset) {
72 Balance {
73 total: b.total,
74 available: b.available(),
75 reserved: b.reserved,
76 }
77 } else {
78 Balance::zero()
79 }
80 }
81
82 pub fn reserve(&mut self, order_id: &ClientOrderId, asset: &AssetId, amount: Decimal) -> bool {
85 let entry = self.balances.entry(asset.clone()).or_default();
86
87 if entry.available() < amount {
88 return false;
89 }
90
91 entry.reserved += amount;
92
93 let reservations = self.order_reservations.entry(order_id.clone()).or_default();
94 reservations.push(Reservation {
95 asset: asset.clone(),
96 amount,
97 });
98
99 true
100 }
101
102 pub fn release_order(&mut self, order_id: &ClientOrderId) {
104 if let Some(reservations) = self.order_reservations.remove(order_id) {
105 for res in reservations {
106 if let Some(balance) = self.balances.get_mut(&res.asset) {
107 balance.reserved = (balance.reserved - res.amount).max(Decimal::ZERO);
108 }
109 }
110 }
111 }
112
113 pub fn apply_fill(
115 &mut self,
116 side: OrderSide,
117 base_asset: &AssetId,
118 quote_asset: &AssetId,
119 qty: Qty,
120 price: Price,
121 fee: &Fee,
122 ) {
123 let notional = qty.0 * price.0;
124
125 match side {
126 OrderSide::Buy => {
127 let base_entry = self.balances.entry(base_asset.clone()).or_default();
129 let base_increase = if fee.asset == *base_asset {
130 qty.0 - fee.amount
131 } else {
132 qty.0
133 };
134 base_entry.total += base_increase;
135
136 let quote_entry = self.balances.entry(quote_asset.clone()).or_default();
137 let quote_decrease = if fee.asset == *quote_asset {
138 notional + fee.amount
139 } else {
140 notional
141 };
142 quote_entry.total = (quote_entry.total - quote_decrease).max(Decimal::ZERO);
143 }
144 OrderSide::Sell => {
145 let base_entry = self.balances.entry(base_asset.clone()).or_default();
147 let base_decrease = if fee.asset == *base_asset {
148 qty.0 + fee.amount
149 } else {
150 qty.0
151 };
152 base_entry.total = (base_entry.total - base_decrease).max(Decimal::ZERO);
153
154 let quote_entry = self.balances.entry(quote_asset.clone()).or_default();
155 let quote_increase = if fee.asset == *quote_asset {
156 notional - fee.amount
157 } else {
158 notional
159 };
160 quote_entry.total += quote_increase.max(Decimal::ZERO);
161 }
162 }
163 }
164
165 pub fn partial_release(
167 &mut self,
168 order_id: &ClientOrderId,
169 asset: &AssetId,
170 filled_fraction: Decimal,
171 ) {
172 if let Some(reservations) = self.order_reservations.get_mut(order_id) {
173 for res in reservations.iter_mut() {
174 if res.asset == *asset {
175 let release_amount = res.amount * filled_fraction;
176 res.amount -= release_amount;
177 if let Some(balance) = self.balances.get_mut(asset) {
178 balance.reserved = (balance.reserved - release_amount).max(Decimal::ZERO);
179 }
180 }
181 }
182 }
183 }
184
185 pub fn set_allocation(&mut self, allocation: StrategyAllocation) {
187 self.strategy_allocations
188 .insert(allocation.strategy_id.clone(), allocation);
189 }
190
191 pub fn check_strategy_budget(
193 &self,
194 strategy_id: &StrategyId,
195 asset: &AssetId,
196 amount: Decimal,
197 ) -> bool {
198 if let Some(allocation) = self.strategy_allocations.get(strategy_id) {
199 if let Some(&budget) = allocation.budgets.get(asset) {
200 let reserved = self
201 .strategy_reserved
202 .get(&(strategy_id.clone(), asset.clone()))
203 .copied()
204 .unwrap_or_default();
205 return reserved + amount <= budget;
206 }
207 }
208 true
210 }
211}
212
213impl Default for InventoryLedger {
214 fn default() -> Self {
215 Self::new()
216 }
217}