kiteconnect_async_wasm/models/auth/
margins.rs

1/*!
2Margin data structures for account balance and trading limits.
3
4Handles user margins, segment-wise balances, and fund information.
5*/
6
7use serde::{Deserialize, Serialize};
8
9/// Complete margin data from the `margins` API
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct MarginData {
12    /// Equity segment margins
13    pub equity: Option<SegmentMargin>,
14
15    /// Commodity segment margins
16    pub commodity: Option<SegmentMargin>,
17}
18
19/// Margin data for a specific trading segment
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct SegmentMargin {
22    /// Available cash margin
23    pub available: MarginFunds,
24
25    /// Utilised margin amounts
26    pub utilised: MarginUtilisation,
27
28    /// Net available margin (available - utilised)
29    pub net: f64,
30}
31
32/// Available margin funds breakdown
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct MarginFunds {
35    /// Available cash in the account
36    pub cash: f64,
37
38    /// Opening balance
39    pub opening_balance: f64,
40
41    /// Live balance (real-time)
42    pub live_balance: f64,
43
44    /// Additional margin from holdings/collateral
45    pub adhoc_margin: f64,
46
47    /// Collateral margin from pledged securities
48    pub collateral: f64,
49
50    /// Intraday payin
51    pub intraday_payin: f64,
52}
53
54impl MarginFunds {
55    /// Calculate total available margin
56    pub fn total(&self) -> f64 {
57        self.cash + self.adhoc_margin + self.collateral + self.intraday_payin
58    }
59
60    /// Check if sufficient funds are available
61    pub fn has_sufficient_funds(&self, required: f64) -> bool {
62        self.total() >= required
63    }
64
65    /// Get cash-only balance (excluding collateral/margins)
66    pub fn cash_only(&self) -> f64 {
67        self.cash
68    }
69}
70
71/// Margin utilisation breakdown
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct MarginUtilisation {
74    /// Debits from trades and charges
75    pub debits: f64,
76
77    /// Exposure margin utilised
78    pub exposure: f64,
79
80    /// M2M (Mark to Market) unrealised P&L
81    pub m2m_unrealised: f64,
82
83    /// M2M realised P&L
84    pub m2m_realised: f64,
85
86    /// Option premium
87    pub option_premium: f64,
88
89    /// Payout amount (funds on hold)
90    pub payout: f64,
91
92    /// SPAN margin utilised
93    pub span: f64,
94
95    /// Holding sales proceeds
96    pub holding_sales: f64,
97
98    /// Turnover charges
99    pub turnover: f64,
100
101    /// Liquid collateral utilised
102    pub liquid: f64,
103
104    /// Stock collateral utilised  
105    pub stock_collateral: f64,
106}
107
108impl MarginUtilisation {
109    /// Calculate total utilised margin
110    pub fn total(&self) -> f64 {
111        self.debits
112            + self.exposure
113            + self.option_premium
114            + self.payout
115            + self.span
116            + self.turnover
117            + self.liquid
118            + self.stock_collateral
119    }
120
121    /// Get total P&L (realised + unrealised)
122    pub fn total_pnl(&self) -> f64 {
123        self.m2m_realised + self.m2m_unrealised
124    }
125
126    /// Check if account has unrealised losses
127    pub fn has_unrealised_losses(&self) -> bool {
128        self.m2m_unrealised < 0.0
129    }
130}
131
132impl SegmentMargin {
133    /// Calculate actual net margin (available total - utilised total)
134    pub fn calculate_net(&self) -> f64 {
135        self.available.total() - self.utilised.total()
136    }
137
138    /// Check if margin is sufficient for a trade
139    pub fn can_place_order(&self, required_margin: f64) -> bool {
140        self.net >= required_margin
141    }
142
143    /// Get margin utilisation percentage
144    pub fn utilisation_percentage(&self) -> f64 {
145        let total_available = self.available.total();
146        if total_available > 0.0 {
147            (self.utilised.total() / total_available) * 100.0
148        } else {
149            0.0
150        }
151    }
152}
153
154impl MarginData {
155    /// Get margin for a specific segment
156    pub fn get_segment(&self, segment: TradingSegment) -> Option<&SegmentMargin> {
157        match segment {
158            TradingSegment::Equity => self.equity.as_ref(),
159            TradingSegment::Commodity => self.commodity.as_ref(),
160        }
161    }
162
163    /// Get total available cash across all segments
164    pub fn total_cash(&self) -> f64 {
165        let mut total = 0.0;
166
167        if let Some(equity) = &self.equity {
168            total += equity.available.cash;
169        }
170
171        if let Some(commodity) = &self.commodity {
172            total += commodity.available.cash;
173        }
174
175        total
176    }
177
178    /// Get combined net margin across segments
179    pub fn total_net_margin(&self) -> f64 {
180        let mut total = 0.0;
181
182        if let Some(equity) = &self.equity {
183            total += equity.net;
184        }
185
186        if let Some(commodity) = &self.commodity {
187            total += commodity.net;
188        }
189
190        total
191    }
192
193    /// Check if any segment has sufficient margin
194    pub fn has_sufficient_margin(&self, required: f64, segment: Option<TradingSegment>) -> bool {
195        match segment {
196            Some(seg) => self
197                .get_segment(seg)
198                .map(|margin| margin.can_place_order(required))
199                .unwrap_or(false),
200            None => {
201                // Check any segment
202                self.equity
203                    .as_ref()
204                    .map(|m| m.can_place_order(required))
205                    .unwrap_or(false)
206                    || self
207                        .commodity
208                        .as_ref()
209                        .map(|m| m.can_place_order(required))
210                        .unwrap_or(false)
211            }
212        }
213    }
214}
215
216/// Trading segments for margin segregation
217#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
218#[serde(rename_all = "lowercase")]
219pub enum TradingSegment {
220    Equity,
221    Commodity,
222}
223
224impl std::fmt::Display for TradingSegment {
225    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226        match self {
227            TradingSegment::Equity => write!(f, "equity"),
228            TradingSegment::Commodity => write!(f, "commodity"),
229        }
230    }
231}
232
233/// Fund transaction details
234#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct FundTransaction {
236    /// Transaction ID
237    pub id: String,
238
239    /// Transaction type (credit/debit)
240    pub transaction_type: String,
241
242    /// Amount
243    pub amount: f64,
244
245    /// Description/narration
246    pub description: String,
247
248    /// Transaction date
249    pub date: String,
250
251    /// Segment where transaction occurred
252    #[serde(default)]
253    pub segment: Option<String>,
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn test_margin_funds() {
262        let funds = MarginFunds {
263            cash: 10000.0,
264            opening_balance: 10000.0,
265            live_balance: 9500.0,
266            adhoc_margin: 2000.0,
267            collateral: 5000.0,
268            intraday_payin: 0.0,
269        };
270
271        assert_eq!(funds.total(), 17000.0);
272        assert!(funds.has_sufficient_funds(15000.0));
273        assert!(!funds.has_sufficient_funds(20000.0));
274        assert_eq!(funds.cash_only(), 10000.0);
275    }
276
277    #[test]
278    fn test_margin_utilisation() {
279        let utilisation = MarginUtilisation {
280            debits: 1000.0,
281            exposure: 2000.0,
282            m2m_unrealised: -500.0,
283            m2m_realised: 200.0,
284            option_premium: 0.0,
285            payout: 0.0,
286            span: 1500.0,
287            holding_sales: 0.0,
288            turnover: 50.0,
289            liquid: 0.0,
290            stock_collateral: 0.0,
291        };
292
293        assert_eq!(utilisation.total(), 4550.0);
294        assert_eq!(utilisation.total_pnl(), -300.0);
295        assert!(utilisation.has_unrealised_losses());
296    }
297
298    #[test]
299    fn test_segment_margin() {
300        let available = MarginFunds {
301            cash: 10000.0,
302            opening_balance: 10000.0,
303            live_balance: 9500.0,
304            adhoc_margin: 0.0,
305            collateral: 0.0,
306            intraday_payin: 0.0,
307        };
308
309        let utilised = MarginUtilisation {
310            debits: 2000.0,
311            exposure: 1000.0,
312            m2m_unrealised: 0.0,
313            m2m_realised: 0.0,
314            option_premium: 0.0,
315            payout: 0.0,
316            span: 0.0,
317            holding_sales: 0.0,
318            turnover: 0.0,
319            liquid: 0.0,
320            stock_collateral: 0.0,
321        };
322
323        let margin = SegmentMargin {
324            available,
325            utilised,
326            net: 7000.0,
327        };
328
329        assert_eq!(margin.calculate_net(), 7000.0);
330        assert!(margin.can_place_order(5000.0));
331        assert!(!margin.can_place_order(8000.0));
332        assert_eq!(margin.utilisation_percentage(), 30.0);
333    }
334
335    #[test]
336    fn test_margin_data() {
337        let equity_margin = SegmentMargin {
338            available: MarginFunds {
339                cash: 10000.0,
340                opening_balance: 10000.0,
341                live_balance: 9500.0,
342                adhoc_margin: 0.0,
343                collateral: 0.0,
344                intraday_payin: 0.0,
345            },
346            utilised: MarginUtilisation {
347                debits: 2000.0,
348                exposure: 0.0,
349                m2m_unrealised: 0.0,
350                m2m_realised: 0.0,
351                option_premium: 0.0,
352                payout: 0.0,
353                span: 0.0,
354                holding_sales: 0.0,
355                turnover: 0.0,
356                liquid: 0.0,
357                stock_collateral: 0.0,
358            },
359            net: 8000.0,
360        };
361
362        let margin_data = MarginData {
363            equity: Some(equity_margin),
364            commodity: None,
365        };
366
367        assert_eq!(margin_data.total_cash(), 10000.0);
368        assert_eq!(margin_data.total_net_margin(), 8000.0);
369        assert!(margin_data.has_sufficient_margin(5000.0, Some(TradingSegment::Equity)));
370        assert!(!margin_data.has_sufficient_margin(5000.0, Some(TradingSegment::Commodity)));
371    }
372}