kiteconnect_async_wasm/connect/
portfolio.rs

1//! # Portfolio Module
2//!
3//! This module provides comprehensive portfolio management capabilities for the KiteConnect API v1.0.3,
4//! offering both real-time portfolio tracking and detailed analysis of holdings and positions.
5//!
6//! ## Overview
7//!
8//! The portfolio module is the central component for managing your trading and investment portfolio.
9//! It provides access to holdings (long-term investments), positions (trading activities), margins,
10//! and portfolio analytics with both legacy JSON-based and modern strongly-typed APIs.
11//!
12//! ## Key Features
13//!
14//! ### 🔄 **Dual API Support**
15//! - **Legacy API**: Returns `JsonValue` for backward compatibility
16//! - **Typed API**: Returns structured types with compile-time safety (methods ending in `_typed`)
17//!
18//! ### 📊 **Comprehensive Portfolio Data**
19//! - **Holdings**: Long-term investments with P&L tracking
20//! - **Positions**: Intraday and overnight trading positions
21//! - **Margins**: Available funds and utilization across segments
22//! - **Analytics**: Portfolio summaries and performance metrics
23//!
24//! ### 💡 **Advanced Features**
25//! - **Real-time P&L**: Live profit/loss calculations
26//! - **Position Analysis**: Day vs overnight position tracking
27//! - **Risk Management**: Margin monitoring and limit checking
28//! - **Portfolio Conversion**: Convert positions between product types
29//!
30//! ## Available Methods
31//!
32//! ### Holdings Management
33//! - [`holdings()`](KiteConnect::holdings) / [`holdings_typed()`](KiteConnect::holdings_typed) - Get all stock holdings
34//! - Portfolio analysis and P&L tracking
35//! - T+1 quantity and sellable quantity calculations
36//!
37//! ### Positions Tracking
38//! - [`positions()`](KiteConnect::positions) / [`positions_typed()`](KiteConnect::positions_typed) - Get current positions
39//! - Day and net position separation
40//! - Real-time P&L and M2M calculations
41//!
42//! ### Margin Management
43//! - [`margins()`](KiteConnect::margins) / [`margins_typed()`](KiteConnect::margins_typed) - Get available margins
44//! - Segment-wise margin tracking
45//! - Utilization and available funds monitoring
46//!
47//! ## Usage Examples
48//!
49//! ### Holdings Analysis
50//! ```rust,no_run
51//! use kiteconnect_async_wasm::connect::KiteConnect;
52//!
53//! # #[tokio::main]
54//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
55//! let client = KiteConnect::new("api_key", "access_token");
56//!
57//! // Get all holdings (typed API - recommended)
58//! let holdings = client.holdings_typed().await?;
59//!
60//! let mut total_investment = 0.0;
61//! let mut total_value = 0.0;
62//! let mut total_pnl = 0.0;
63//!
64//! println!("Holdings Portfolio Analysis:");
65//! println!("============================");
66//!
67//! for holding in &holdings {
68//!     let investment = holding.investment_value();
69//!     let current_value = holding.market_value();
70//!     let pnl_pct = holding.pnl_percentage();
71//!     
72//!     total_investment += investment;
73//!     total_value += current_value;
74//!     total_pnl += holding.pnl;
75//!     
76//!     println!("📈 {}: {} shares", holding.trading_symbol, holding.quantity);
77//!     println!("   💰 Investment: ₹{:.2} | Current: ₹{:.2}", investment, current_value);
78//!     println!("   📊 P&L: ₹{:.2} ({:.2}%)", holding.pnl, pnl_pct);
79//!     
80//!     // Check trading availability
81//!     if holding.can_sell_today() {
82//!         println!("   ✅ Can sell {} shares today", holding.sellable_today());
83//!     }
84//!     
85//!     println!();
86//! }
87//!
88//! let overall_pnl_pct = (total_pnl / total_investment) * 100.0;
89//! println!("🎯 Portfolio Summary:");
90//! println!("   Total Investment: ₹{:.2}", total_investment);
91//! println!("   Current Value: ₹{:.2}", total_value);
92//! println!("   Total P&L: ₹{:.2} ({:.2}%)", total_pnl, overall_pnl_pct);
93//! # Ok(())
94//! # }
95//! ```
96//!
97//! ### Positions Monitoring
98//! ```rust,no_run
99//! use kiteconnect_async_wasm::connect::KiteConnect;
100//!
101//! # #[tokio::main]
102//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
103//! let client = KiteConnect::new("api_key", "access_token");
104//!
105//! // Get all positions (typed API)
106//! let positions = client.positions_typed().await?;
107//!
108//! println!("Active Trading Positions:");
109//! println!("========================");
110//!
111//! let mut day_pnl = 0.0;
112//! let mut total_pnl = 0.0;
113//!
114//! for position in &positions {
115//!     if !position.is_flat() {
116//!         let direction = if position.is_long() { "LONG" } else { "SHORT" };
117//!         let pnl_pct = position.pnl_percentage();
118//!         
119//!         day_pnl += position.day_pnl();
120//!         total_pnl += position.pnl;
121//!         
122//!         println!("📊 {}: {} {} shares",
123//!             position.trading_symbol,
124//!             position.abs_quantity(),
125//!             direction);
126//!         println!("   💵 Avg: ₹{:.2} | LTP: ₹{:.2}",
127//!             position.average_price, position.last_price);
128//!         println!("   📈 P&L: ₹{:.2} ({:.2}%)", position.pnl, pnl_pct);
129//!         
130//!         if position.is_day_position() {
131//!             println!("   🔄 Intraday position");
132//!         } else if position.is_overnight_position() {
133//!             println!("   🌙 Overnight position ({})", position.overnight_quantity);
134//!         }
135//!         
136//!         println!();
137//!     }
138//! }
139//!
140//! println!("📊 Trading Summary:");
141//! println!("   Day P&L: ₹{:.2}", day_pnl);
142//! println!("   Total P&L: ₹{:.2}", total_pnl);
143//! # Ok(())
144//! # }
145//! ```
146//!
147//! ### Margin Analysis
148//! ```rust,no_run
149//! use kiteconnect_async_wasm::connect::KiteConnect;
150//! use kiteconnect_async_wasm::models::auth::TradingSegment;
151//!
152//! # #[tokio::main]
153//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
154//! let client = KiteConnect::new("api_key", "access_token");
155//!
156//! // Get margin data (typed API)
157//! let margins = client.margins_typed(None).await?;
158//!
159//! println!("Margin Analysis:");
160//! println!("===============");
161//!
162//! if let Some(ref equity_margin) = margins.equity {
163//!     let available = equity_margin.available.cash;
164//!     let net = equity_margin.net;
165//!     let utilisation_pct = equity_margin.utilisation_percentage();
166//!     
167//!     println!("💰 Equity Segment:");
168//!     println!("   Available Cash: ₹{:.2}", available);
169//!     println!("   Net Margin: ₹{:.2}", net);
170//!     println!("   Utilisation: {:.1}%", utilisation_pct);
171//!     
172//!     // Check if sufficient margin for trading
173//!     let required_margin = 50000.0; // Example
174//!     if equity_margin.can_place_order(required_margin) {
175//!         println!("   ✅ Sufficient margin for ₹{:.0} order", required_margin);
176//!     } else {
177//!         println!("   ❌ Insufficient margin for ₹{:.0} order", required_margin);
178//!     }
179//! }
180//!
181//! if let Some(ref commodity_margin) = margins.commodity {
182//!     println!("🌾 Commodity Segment:");
183//!     println!("   Available Cash: ₹{:.2}", commodity_margin.available.cash);
184//!     println!("   Net Margin: ₹{:.2}", commodity_margin.net);
185//! }
186//!
187//! // Overall margin check
188//! let total_cash = margins.total_cash();
189//! let total_net = margins.total_net_margin();
190//! println!("🎯 Total Available: ₹{:.2} | Net: ₹{:.2}", total_cash, total_net);
191//! # Ok(())
192//! # }
193//! ```
194//!
195//! ### Portfolio Risk Analysis
196//! ```rust,no_run
197//! use kiteconnect_async_wasm::connect::KiteConnect;
198//!
199//! # #[tokio::main]
200//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
201//! let client = KiteConnect::new("api_key", "access_token");
202//!
203//! // Get holdings and positions for comprehensive analysis
204//! let (holdings, positions) = tokio::try_join!(
205//!     client.holdings_typed(),
206//!     client.positions_typed()
207//! )?;
208//!
209//! println!("Portfolio Risk Analysis:");
210//! println!("=======================");
211//!
212//! // Holdings analysis
213//! let profitable_holdings = holdings.iter().filter(|h| h.is_profitable()).count();
214//! let loss_holdings = holdings.iter().filter(|h| h.is_loss()).count();
215//! let holdings_win_rate = (profitable_holdings as f64 / holdings.len() as f64) * 100.0;
216//!
217//! println!("📊 Holdings (Long-term):");
218//! println!("   Total Holdings: {}", holdings.len());
219//! println!("   Profitable: {} | Loss-making: {}", profitable_holdings, loss_holdings);
220//! println!("   Win Rate: {:.1}%", holdings_win_rate);
221//!
222//! // Positions analysis
223//! let active_positions: Vec<_> = positions.iter().filter(|p| !p.is_flat()).collect();
224//! let profitable_positions = active_positions.iter().filter(|p| p.is_profitable()).count();
225//! let loss_positions = active_positions.iter().filter(|p| p.is_loss()).count();
226//!
227//! if !active_positions.is_empty() {
228//!     let positions_win_rate = (profitable_positions as f64 / active_positions.len() as f64) * 100.0;
229//!     
230//!     println!("📈 Active Positions (Trading):");
231//!     println!("   Active Positions: {}", active_positions.len());
232//!     println!("   Profitable: {} | Loss-making: {}", profitable_positions, loss_positions);
233//!     println!("   Win Rate: {:.1}%", positions_win_rate);
234//! }
235//!
236//! // Risk metrics
237//! let total_holdings_value: f64 = holdings.iter().map(|h| h.market_value()).sum();
238//! let total_position_exposure: f64 = active_positions.iter()
239//!     .map(|p| p.market_value())
240//!     .sum();
241//!
242//! println!("⚖️ Risk Exposure:");
243//! println!("   Holdings Value: ₹{:.2}", total_holdings_value);
244//! println!("   Position Exposure: ₹{:.2}", total_position_exposure);
245//! println!("   Total Exposure: ₹{:.2}", total_holdings_value + total_position_exposure);
246//! # Ok(())
247//! # }
248//! ```
249//!
250//! ## Data Models
251//!
252//! ### Holdings
253//! The [`Holding`] struct represents long-term investments with comprehensive tracking:
254//! - **Investment tracking**: Average price, current price, P&L calculations
255//! - **Quantity management**: Total, T+1, realised, and pledged quantities
256//! - **Trading availability**: Check what can be sold today vs tomorrow
257//! - **Portfolio analytics**: Market value, investment value, percentage returns
258//!
259//! ### Positions
260//! The [`Position`] struct represents active trading positions:
261//! - **Direction tracking**: Long vs short positions
262//! - **Day vs Net**: Separate intraday and overnight positions
263//! - **P&L breakdown**: Realised, unrealised, and M2M calculations
264//! - **Risk metrics**: Exposure, margin requirements
265//!
266//! ### Margins
267//! The [`MarginData`] struct provides fund information:
268//! - **Segment-wise**: Equity and commodity margins separately
269//! - **Available funds**: Cash, collateral, and total available
270//! - **Utilisation**: Used margin and exposure tracking
271//! - **Order capacity**: Check if sufficient margin for new orders
272//!
273//! ## Error Handling
274//!
275//! All methods return `Result<T>` with comprehensive error information:
276//!
277//! ```rust,no_run
278//! use kiteconnect_async_wasm::models::common::KiteError;
279//!
280//! # #[tokio::main]
281//! # async fn main() {
282//! # let client = kiteconnect_async_wasm::connect::KiteConnect::new("", "");
283//! match client.holdings_typed().await {
284//!     Ok(holdings) => {
285//!         println!("Portfolio loaded: {} holdings", holdings.len());
286//!         // Process holdings...
287//!     }
288//!     Err(KiteError::Authentication(msg)) => {
289//!         eprintln!("Authentication failed: {}", msg);
290//!         // Handle re-authentication
291//!     }
292//!     Err(KiteError::Api { status, message, .. }) => {
293//!         eprintln!("API Error {}: {}", status, message);
294//!         // Handle API errors
295//!     }
296//!     Err(e) => eprintln!("Other error: {}", e),
297//! }
298//! # }
299//! ```
300//!
301//! ## Performance Considerations
302//!
303//! ### Efficient Data Access
304//! - **Batch Operations**: Get holdings and positions together with `tokio::try_join!`
305//! - **Typed APIs**: Use `*_typed()` methods for better performance and type safety
306//! - **Selective Updates**: Update only necessary data for real-time monitoring
307//!
308//! ### Memory Usage
309//! - **Structured Data**: Typed APIs use less memory than JSON parsing
310//! - **Efficient Calculations**: Built-in helper methods reduce computation overhead
311//! - **Lazy Evaluation**: Calculate metrics only when needed
312//!
313//! ## Rate Limiting
314//!
315//! The module automatically handles rate limiting according to KiteConnect API guidelines:
316//! - **Portfolio APIs**: 3 requests per second for holdings, positions, margins
317//! - **Automatic Retry**: Built-in retry mechanism with exponential backoff
318//! - **Connection Pooling**: HTTP connections are reused for better performance
319//!
320//! ## Thread Safety
321//!
322//! All methods are thread-safe and can be called concurrently:
323//! ```rust,no_run
324//! # use kiteconnect_async_wasm::connect::KiteConnect;
325//! # #[tokio::main]
326//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
327//! # let client = KiteConnect::new("", "");
328//! // Concurrent portfolio data retrieval
329//! let (holdings, positions, margins) = tokio::try_join!(
330//!     client.holdings_typed(),
331//!     client.positions_typed(),
332//!     client.margins_typed(None)
333//! )?;
334//!
335//! // All data retrieved concurrently for maximum efficiency
336//! # Ok(())
337//! # }
338//! ```
339//!
340//! ## Migration from v1.0.2
341//!
342//! All existing methods continue to work. New typed methods provide enhanced features:
343//! - Replace `holdings()` with `holdings_typed()` for structured data
344//! - Use `positions_typed()` and `margins_typed()` for type safety
345//! - Legacy methods remain available for backward compatibility
346//! - Enhanced helper methods on all model structs for better analytics
347
348use crate::connect::endpoints::KiteEndpoint;
349use anyhow::Result;
350use serde_json::Value as JsonValue;
351// Import typed models for dual API support
352use crate::models::auth::MarginData;
353use crate::models::common::KiteResult;
354use crate::models::portfolio::{ConversionRequest, Holding, Position};
355use crate::models::orders::OrderMarginRequest;
356use crate::models::orders::{BasketMarginsData, OrderMarginResult};
357use crate::models::orders::{OrderChargesRequest, OrderChargesResult};
358
359use crate::connect::KiteConnect;
360
361impl KiteConnect {
362    // === LEGACY API METHODS (JSON responses) ===
363
364    /// Retrieves account balance and margin details
365    ///
366    /// Returns margin information for trading segments including available cash,
367    /// used margins, and available margins for different product types.
368    ///
369    /// # Arguments
370    ///
371    /// * `segment` - Optional trading segment ("equity" or "commodity"). If None, returns all segments
372    ///
373    /// # Returns
374    ///
375    /// A `Result<JsonValue>` containing margin data with fields like:
376    /// - `available` - Available margin for trading
377    /// - `utilised` - Currently utilized margin
378    /// - `net` - Net available margin
379    /// - `enabled` - Whether the segment is enabled
380    ///
381    /// # Errors
382    ///
383    /// Returns an error if the API request fails or the user is not authenticated.
384    ///
385    /// # Example
386    ///
387    /// ```rust,no_run
388    /// use kiteconnect_async_wasm::connect::KiteConnect;
389    ///
390    /// # #[tokio::main]
391    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
392    /// let client = KiteConnect::new("api_key", "access_token");
393    ///
394    /// // Get margins for all segments
395    /// let all_margins = client.margins(None).await?;
396    /// println!("All margins: {:?}", all_margins);
397    ///
398    /// // Get margins for specific segment
399    /// let equity_margins = client.margins(Some("equity".to_string())).await?;
400    /// println!("Equity available margin: {}",
401    ///     equity_margins["data"]["available"]["live_balance"]);
402    /// # Ok(())
403    /// # }
404    /// ```
405    pub async fn margins(&self, segment: Option<String>) -> Result<JsonValue> {
406        if let Some(segment) = segment {
407            let resp = self
408                .send_request_with_rate_limiting_and_retry(
409                    KiteEndpoint::MarginsSegment,
410                    &[&segment],
411                    None,
412                    None,
413                )
414                .await
415                .map_err(|e| anyhow::anyhow!("Get margins failed: {:?}", e))?;
416            self.raise_or_return_json(resp).await
417        } else {
418            let resp = self
419                .send_request_with_rate_limiting_and_retry(KiteEndpoint::Margins, &[], None, None)
420                .await
421                .map_err(|e| anyhow::anyhow!("Get margins failed: {:?}", e))?;
422            self.raise_or_return_json(resp).await
423        }
424    }
425
426    /// Get user profile details
427    pub async fn profile(&self) -> Result<JsonValue> {
428        let resp = self
429            .send_request_with_rate_limiting_and_retry(KiteEndpoint::Profile, &[], None, None)
430            .await
431            .map_err(|e| anyhow::anyhow!("Get profile failed: {:?}", e))?;
432        self.raise_or_return_json(resp).await
433    }
434
435    /// Retrieves the user's holdings (stocks held in demat account)
436    ///
437    /// Holdings represent stocks that are held in the user's demat account.
438    /// This includes information about quantity, average price, current market value,
439    /// profit/loss, and more.
440    ///
441    /// # Returns
442    ///
443    /// A `Result<JsonValue>` containing holdings data with fields like:
444    /// - `tradingsymbol` - Trading symbol of the instrument
445    /// - `quantity` - Total quantity held
446    /// - `average_price` - Average buying price
447    /// - `last_price` - Current market price
448    /// - `pnl` - Profit and loss
449    /// - `product` - Product type (CNC, MIS, etc.)
450    ///
451    /// # Errors
452    ///
453    /// Returns an error if the API request fails or the user is not authenticated.
454    ///
455    /// # Example
456    ///
457    /// ```rust,no_run
458    /// use kiteconnect_async_wasm::connect::KiteConnect;
459    ///
460    /// # #[tokio::main]
461    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
462    /// let client = KiteConnect::new("api_key", "access_token");
463    ///
464    /// let holdings = client.holdings().await?;
465    /// println!("Holdings: {:?}", holdings);
466    ///
467    /// // Access specific fields
468    /// if let Some(data) = holdings["data"].as_array() {
469    ///     for holding in data {
470    ///         println!("Symbol: {}, Quantity: {}",
471    ///             holding["tradingsymbol"], holding["quantity"]);
472    ///     }
473    /// }
474    /// # Ok(())
475    /// # }
476    /// ```
477    pub async fn holdings(&self) -> Result<JsonValue> {
478        let resp = self
479            .send_request_with_rate_limiting_and_retry(KiteEndpoint::Holdings, &[], None, None)
480            .await
481            .map_err(|e| anyhow::anyhow!("Get holdings failed: {:?}", e))?;
482        self.raise_or_return_json(resp).await
483    }
484
485    /// Retrieves the user's positions (open positions for the day)
486    ///
487    /// Positions represent open trading positions for the current trading day.
488    /// This includes both intraday and carry-forward positions with details about
489    /// profit/loss, margin requirements, and position status.
490    ///
491    /// # Returns
492    ///
493    /// A `Result<JsonValue>` containing positions data with fields like:
494    /// - `tradingsymbol` - Trading symbol of the instrument
495    /// - `quantity` - Net position quantity
496    /// - `buy_quantity` - Total buy quantity
497    /// - `sell_quantity` - Total sell quantity
498    /// - `average_price` - Average position price
499    /// - `pnl` - Realized and unrealized P&L
500    /// - `product` - Product type (MIS, CNC, NRML)
501    ///
502    /// # Errors
503    ///
504    /// Returns an error if the API request fails or the user is not authenticated.
505    ///
506    /// # Example
507    ///
508    /// ```rust,no_run
509    /// use kiteconnect_async_wasm::connect::KiteConnect;
510    ///
511    /// # #[tokio::main]
512    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
513    /// let client = KiteConnect::new("api_key", "access_token");
514    ///
515    /// let positions = client.positions().await?;
516    /// println!("Positions: {:?}", positions);
517    ///
518    /// // Check for open positions
519    /// if let Some(day_positions) = positions["data"]["day"].as_array() {
520    ///     for position in day_positions {
521    ///         if position["quantity"].as_i64().unwrap_or(0) != 0 {
522    ///             println!("Open position: {} qty {}",
523    ///                 position["tradingsymbol"], position["quantity"]);
524    ///         }
525    ///     }
526    /// }
527    /// # Ok(())
528    /// # }
529    /// ```
530    pub async fn positions(&self) -> Result<JsonValue> {
531        let resp = self
532            .send_request_with_rate_limiting_and_retry(KiteEndpoint::Positions, &[], None, None)
533            .await
534            .map_err(|e| anyhow::anyhow!("Get positions failed: {:?}", e))?;
535        self.raise_or_return_json(resp).await
536    }
537
538    /// Calculate margins for a list of orders
539    ///
540    /// POST /margins/orders (JSON body)
541    pub async fn calculate_order_margins(
542        &self,
543        orders: &[OrderMarginRequest],
544        compact: bool,
545    ) -> Result<JsonValue> {
546        let query = if compact { Some(vec![("mode", "compact")]) } else { None };
547        let resp = self
548            .send_json_with_rate_limiting_and_retry(
549                KiteEndpoint::CalculateOrderMargins,
550                &[],
551                query,
552                &orders,
553            )
554            .await
555            .map_err(|e| anyhow::anyhow!("Order margin calc failed: {:?}", e))?;
556        self.raise_or_return_json(resp).await
557    }
558
559    /// Calculate basket margins with optional consideration of positions
560    ///
561    /// POST /margins/basket (JSON body)
562    pub async fn calculate_basket_margins(
563         &self,
564         orders: &[OrderMarginRequest],
565         consider_positions: bool,
566         compact: bool,
567     ) -> Result<JsonValue> {
568        let mut query: Vec<(&str, &str)> = Vec::new();
569        if consider_positions {
570            query.push(("consider_positions", "true"));
571        }
572        if compact {
573            query.push(("mode", "compact"));
574        }
575        let query = if query.is_empty() { None } else { Some(query) };
576
577        let resp = self
578            .send_json_with_rate_limiting_and_retry(
579                KiteEndpoint::CalculateBasketMargins,
580                &[],
581                query,
582                &orders,
583            )
584            .await
585            .map_err(|e| anyhow::anyhow!("Basket margin calc failed: {:?}", e))?;
586        self.raise_or_return_json(resp).await
587    }
588
589    /// Calculate order-wise charges (virtual contract note)
590    ///
591    /// POST /charges/orders (JSON body)
592    pub async fn calculate_order_charges(
593        &self,
594        orders: &[OrderChargesRequest],
595    ) -> Result<JsonValue> {
596        let resp = self
597            .send_json_with_rate_limiting_and_retry(
598                KiteEndpoint::CalculateOrderCharges,
599                &[],
600                None,
601                &orders,
602            )
603            .await
604            .map_err(|e| anyhow::anyhow!("Order charges calc failed: {:?}", e))?;
605        self.raise_or_return_json(resp).await
606    }
607
608    // === TYPED API METHODS (v1.0.0) ===
609
610    /// Get user margins with typed response
611    ///
612    /// Returns strongly typed margin data instead of JsonValue.
613    ///
614    /// # Arguments
615    ///
616    /// * `segment` - Optional trading segment ("equity" or "commodity")
617    ///
618    /// # Returns
619    ///
620    /// A `KiteResult<MarginData>` containing typed margin information
621    ///
622    /// # Example
623    ///
624    /// ```rust,no_run
625    /// use kiteconnect_async_wasm::connect::KiteConnect;
626    ///
627    /// # #[tokio::main]
628    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
629    /// let client = KiteConnect::new("api_key", "access_token");
630    ///
631    /// let margins = client.margins_typed(None).await?;
632    /// println!("Available equity margin: {}", margins.equity.unwrap().available.cash);
633    /// # Ok(())
634    /// # }
635    /// ```
636    pub async fn margins_typed(&self, segment: Option<&str>) -> KiteResult<MarginData> {
637        if let Some(segment) = segment {
638            let resp = self
639                .send_request_with_rate_limiting_and_retry(
640                    KiteEndpoint::MarginsSegment,
641                    &[segment],
642                    None,
643                    None,
644                )
645                .await?;
646            let json_response = self.raise_or_return_json_typed(resp).await?;
647            self.parse_response(json_response)
648        } else {
649            let resp = self
650                .send_request_with_rate_limiting_and_retry(KiteEndpoint::Margins, &[], None, None)
651                .await?;
652            let json_response = self.raise_or_return_json_typed(resp).await?;
653            self.parse_response(json_response)
654        }
655    }
656
657    /// Get user holdings with typed response
658    ///
659    /// Returns a vector of strongly typed holding objects instead of JsonValue.
660    ///
661    /// # Returns
662    ///
663    /// A `KiteResult<Vec<Holding>>` containing typed holdings data
664    ///
665    /// # Example
666    ///
667    /// ```rust,no_run
668    /// use kiteconnect_async_wasm::connect::KiteConnect;
669    ///
670    /// # #[tokio::main]
671    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
672    /// let client = KiteConnect::new("api_key", "access_token");
673    ///
674    /// let holdings = client.holdings_typed().await?;
675    /// for holding in holdings {
676    ///     println!("Symbol: {}, Quantity: {}, P&L: {}",
677    ///         holding.trading_symbol, holding.quantity, holding.pnl);
678    /// }
679    /// # Ok(())
680    /// # }
681    /// ```
682    pub async fn holdings_typed(&self) -> KiteResult<Vec<Holding>> {
683        let resp = self
684            .send_request_with_rate_limiting_and_retry(KiteEndpoint::Holdings, &[], None, None)
685            .await?;
686        let json_response = self.raise_or_return_json_typed(resp).await?;
687
688        // Extract the "data" field and parse as Vec<Holding>
689        if let Some(data) = json_response.get("data") {
690            self.parse_response(data.clone())
691        } else {
692            // If no "data" field, try parsing the entire response
693            self.parse_response(json_response)
694        }
695    }
696
697    /// Get user positions with typed response
698    ///
699    /// Returns structured position data instead of JsonValue.
700    ///
701    /// # Returns
702    ///
703    /// A `KiteResult<Vec<Position>>` containing typed positions data
704    ///
705    /// # Example
706    ///
707    /// ```rust,no_run
708    /// use kiteconnect_async_wasm::connect::KiteConnect;
709    ///
710    /// # #[tokio::main]
711    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
712    /// let client = KiteConnect::new("api_key", "access_token");
713    ///
714    /// let positions = client.positions_typed().await?;
715    /// for position in &positions {
716    ///     if position.quantity != 0 {
717    ///         println!("Open position: {} qty {}",
718    ///             position.trading_symbol, position.quantity);
719    ///     }
720    /// }
721    /// # Ok(())
722    /// # }
723    /// ```
724    pub async fn positions_typed(&self) -> KiteResult<Vec<Position>> {
725        let resp = self
726            .send_request_with_rate_limiting_and_retry(KiteEndpoint::Positions, &[], None, None)
727            .await?;
728        let json_response = self.raise_or_return_json_typed(resp).await?;
729
730        // KiteConnect returns positions in nested structure: { "data": { "day": [...], "net": [...] } }
731        // We'll flatten both day and net positions into a single vector
732        let mut all_positions = Vec::new();
733
734        if let Some(data) = json_response.get("data") {
735            if let Some(day_positions) = data.get("day").and_then(|v| v.as_array()) {
736                for pos_json in day_positions {
737                    if let Ok(position) = self.parse_response::<Position>(pos_json.clone()) {
738                        all_positions.push(position);
739                    }
740                }
741            }
742
743            if let Some(net_positions) = data.get("net").and_then(|v| v.as_array()) {
744                for pos_json in net_positions {
745                    if let Ok(position) = self.parse_response::<Position>(pos_json.clone()) {
746                        all_positions.push(position);
747                    }
748                }
749            }
750        }
751
752        Ok(all_positions)
753    }
754
755    /// Convert positions between product types (typed)
756    ///
757    /// Converts a position from one product type to another (e.g., MIS to CNC).
758    ///
759    /// # Arguments
760    ///
761    /// * `request` - Conversion request details
762    ///
763    /// # Returns
764    ///
765    /// A `KiteResult<bool>` indicating success
766    ///
767    /// # Example
768    ///
769    /// ```rust,no_run
770    /// use kiteconnect_async_wasm::connect::KiteConnect;
771    /// use kiteconnect_async_wasm::models::portfolio::ConversionRequest;
772    /// use kiteconnect_async_wasm::models::common::{Exchange, Product, TransactionType};
773    ///
774    /// # #[tokio::main]
775    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
776    /// let client = KiteConnect::new("api_key", "access_token");
777    ///
778    /// let conversion = ConversionRequest {
779    ///     exchange: Exchange::NSE,
780    ///     trading_symbol: "RELIANCE".to_string(),
781    ///     transaction_type: TransactionType::BUY,
782    ///     quantity: 10,
783    ///     from_product: Product::MIS,
784    ///     to_product: Product::CNC,
785    /// };
786    ///
787    /// let success = client.convert_position_typed(&conversion).await?;
788    /// println!("Conversion successful: {}", success);
789    /// # Ok(())
790    /// # }
791    /// ```
792    pub async fn convert_position_typed(&self, request: &ConversionRequest) -> KiteResult<bool> {
793        let mut params = std::collections::HashMap::new();
794        let exchange_str = request.exchange.to_string();
795        let transaction_str = request.transaction_type.to_string();
796        let quantity_str = request.quantity.to_string();
797        let from_product_str = request.from_product.to_string();
798        let to_product_str = request.to_product.to_string();
799
800        params.insert("exchange", exchange_str.as_str());
801        params.insert("tradingsymbol", request.trading_symbol.as_str());
802        params.insert("transaction_type", transaction_str.as_str());
803        params.insert("quantity", quantity_str.as_str());
804        params.insert("old_product", from_product_str.as_str());
805        params.insert("new_product", to_product_str.as_str());
806
807        let resp = self
808            .send_request_with_rate_limiting_and_retry(
809                KiteEndpoint::ConvertPosition,
810                &[],
811                None,
812                Some(params),
813            )
814            .await?;
815        let json_response = self.raise_or_return_json_typed(resp).await?;
816
817        // Check if conversion was successful
818        Ok(json_response.get("status").and_then(|v| v.as_str()) == Some("success"))
819    }
820
821    // === TYPED margin calculation helpers ===
822
823    /// Calculate margins for a list of orders (typed)
824    pub async fn calculate_order_margins_typed(
825        &self,
826        orders: &[OrderMarginRequest],
827        compact: bool,
828    ) -> KiteResult<Vec<OrderMarginResult>> {
829        let query = if compact { Some(vec![("mode", "compact")]) } else { None };
830        let resp = self
831            .send_json_with_rate_limiting_and_retry(
832                KiteEndpoint::CalculateOrderMargins,
833                &[],
834                query,
835                &orders,
836            )
837            .await?;
838        let json_response = self.raise_or_return_json_typed(resp).await?;
839        let data = json_response["data"].clone();
840        self.parse_response(data)
841    }
842
843    /// Calculate basket margins (typed)
844    pub async fn calculate_basket_margins_typed(
845        &self,
846        orders: &[OrderMarginRequest],
847        consider_positions: bool,
848        compact: bool,
849    ) -> KiteResult<BasketMarginsData> {
850        let mut query: Vec<(&str, &str)> = Vec::new();
851        if consider_positions {
852            query.push(("consider_positions", "true"));
853        }
854        if compact {
855            query.push(("mode", "compact"));
856        }
857        let query = if query.is_empty() { None } else { Some(query) };
858
859        let resp = self
860            .send_json_with_rate_limiting_and_retry(
861                KiteEndpoint::CalculateBasketMargins,
862                &[],
863                query,
864                &orders,
865            )
866            .await?;
867        let json_response = self.raise_or_return_json_typed(resp).await?;
868        let data = json_response["data"].clone();
869        self.parse_response(data)
870    }
871
872    /// Calculate order-wise charges (typed)
873    pub async fn calculate_order_charges_typed(
874        &self,
875        orders: &[OrderChargesRequest],
876    ) -> KiteResult<Vec<OrderChargesResult>> {
877        let resp = self
878            .send_json_with_rate_limiting_and_retry(
879                KiteEndpoint::CalculateOrderCharges,
880                &[],
881                None,
882                &orders,
883            )
884            .await?;
885        let json_response = self.raise_or_return_json_typed(resp).await?;
886        let data = json_response["data"].clone();
887        self.parse_response(data)
888    }
889}