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};
355
356use crate::connect::KiteConnect;
357
358impl KiteConnect {
359    // === LEGACY API METHODS (JSON responses) ===
360
361    /// Retrieves account balance and margin details
362    ///
363    /// Returns margin information for trading segments including available cash,
364    /// used margins, and available margins for different product types.
365    ///
366    /// # Arguments
367    ///
368    /// * `segment` - Optional trading segment ("equity" or "commodity"). If None, returns all segments
369    ///
370    /// # Returns
371    ///
372    /// A `Result<JsonValue>` containing margin data with fields like:
373    /// - `available` - Available margin for trading
374    /// - `utilised` - Currently utilized margin
375    /// - `net` - Net available margin
376    /// - `enabled` - Whether the segment is enabled
377    ///
378    /// # Errors
379    ///
380    /// Returns an error if the API request fails or the user is not authenticated.
381    ///
382    /// # Example
383    ///
384    /// ```rust,no_run
385    /// use kiteconnect_async_wasm::connect::KiteConnect;
386    ///
387    /// # #[tokio::main]
388    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
389    /// let client = KiteConnect::new("api_key", "access_token");
390    ///
391    /// // Get margins for all segments
392    /// let all_margins = client.margins(None).await?;
393    /// println!("All margins: {:?}", all_margins);
394    ///
395    /// // Get margins for specific segment
396    /// let equity_margins = client.margins(Some("equity".to_string())).await?;
397    /// println!("Equity available margin: {}",
398    ///     equity_margins["data"]["available"]["live_balance"]);
399    /// # Ok(())
400    /// # }
401    /// ```
402    pub async fn margins(&self, segment: Option<String>) -> Result<JsonValue> {
403        if let Some(segment) = segment {
404            let resp = self
405                .send_request_with_rate_limiting_and_retry(
406                    KiteEndpoint::MarginsSegment,
407                    &[&segment],
408                    None,
409                    None,
410                )
411                .await
412                .map_err(|e| anyhow::anyhow!("Get margins failed: {:?}", e))?;
413            self.raise_or_return_json(resp).await
414        } else {
415            let resp = self
416                .send_request_with_rate_limiting_and_retry(KiteEndpoint::Margins, &[], None, None)
417                .await
418                .map_err(|e| anyhow::anyhow!("Get margins failed: {:?}", e))?;
419            self.raise_or_return_json(resp).await
420        }
421    }
422
423    /// Get user profile details
424    pub async fn profile(&self) -> Result<JsonValue> {
425        let resp = self
426            .send_request_with_rate_limiting_and_retry(KiteEndpoint::Profile, &[], None, None)
427            .await
428            .map_err(|e| anyhow::anyhow!("Get profile failed: {:?}", e))?;
429        self.raise_or_return_json(resp).await
430    }
431
432    /// Retrieves the user's holdings (stocks held in demat account)
433    ///
434    /// Holdings represent stocks that are held in the user's demat account.
435    /// This includes information about quantity, average price, current market value,
436    /// profit/loss, and more.
437    ///
438    /// # Returns
439    ///
440    /// A `Result<JsonValue>` containing holdings data with fields like:
441    /// - `tradingsymbol` - Trading symbol of the instrument
442    /// - `quantity` - Total quantity held
443    /// - `average_price` - Average buying price
444    /// - `last_price` - Current market price
445    /// - `pnl` - Profit and loss
446    /// - `product` - Product type (CNC, MIS, etc.)
447    ///
448    /// # Errors
449    ///
450    /// Returns an error if the API request fails or the user is not authenticated.
451    ///
452    /// # Example
453    ///
454    /// ```rust,no_run
455    /// use kiteconnect_async_wasm::connect::KiteConnect;
456    ///
457    /// # #[tokio::main]
458    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
459    /// let client = KiteConnect::new("api_key", "access_token");
460    ///
461    /// let holdings = client.holdings().await?;
462    /// println!("Holdings: {:?}", holdings);
463    ///
464    /// // Access specific fields
465    /// if let Some(data) = holdings["data"].as_array() {
466    ///     for holding in data {
467    ///         println!("Symbol: {}, Quantity: {}",
468    ///             holding["tradingsymbol"], holding["quantity"]);
469    ///     }
470    /// }
471    /// # Ok(())
472    /// # }
473    /// ```
474    pub async fn holdings(&self) -> Result<JsonValue> {
475        let resp = self
476            .send_request_with_rate_limiting_and_retry(KiteEndpoint::Holdings, &[], None, None)
477            .await
478            .map_err(|e| anyhow::anyhow!("Get holdings failed: {:?}", e))?;
479        self.raise_or_return_json(resp).await
480    }
481
482    /// Retrieves the user's positions (open positions for the day)
483    ///
484    /// Positions represent open trading positions for the current trading day.
485    /// This includes both intraday and carry-forward positions with details about
486    /// profit/loss, margin requirements, and position status.
487    ///
488    /// # Returns
489    ///
490    /// A `Result<JsonValue>` containing positions data with fields like:
491    /// - `tradingsymbol` - Trading symbol of the instrument
492    /// - `quantity` - Net position quantity
493    /// - `buy_quantity` - Total buy quantity
494    /// - `sell_quantity` - Total sell quantity
495    /// - `average_price` - Average position price
496    /// - `pnl` - Realized and unrealized P&L
497    /// - `product` - Product type (MIS, CNC, NRML)
498    ///
499    /// # Errors
500    ///
501    /// Returns an error if the API request fails or the user is not authenticated.
502    ///
503    /// # Example
504    ///
505    /// ```rust,no_run
506    /// use kiteconnect_async_wasm::connect::KiteConnect;
507    ///
508    /// # #[tokio::main]
509    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
510    /// let client = KiteConnect::new("api_key", "access_token");
511    ///
512    /// let positions = client.positions().await?;
513    /// println!("Positions: {:?}", positions);
514    ///
515    /// // Check for open positions
516    /// if let Some(day_positions) = positions["data"]["day"].as_array() {
517    ///     for position in day_positions {
518    ///         if position["quantity"].as_i64().unwrap_or(0) != 0 {
519    ///             println!("Open position: {} qty {}",
520    ///                 position["tradingsymbol"], position["quantity"]);
521    ///         }
522    ///     }
523    /// }
524    /// # Ok(())
525    /// # }
526    /// ```
527    pub async fn positions(&self) -> Result<JsonValue> {
528        let resp = self
529            .send_request_with_rate_limiting_and_retry(KiteEndpoint::Positions, &[], None, None)
530            .await
531            .map_err(|e| anyhow::anyhow!("Get positions failed: {:?}", e))?;
532        self.raise_or_return_json(resp).await
533    }
534
535    // === TYPED API METHODS (v1.0.0) ===
536
537    /// Get user margins with typed response
538    ///
539    /// Returns strongly typed margin data instead of JsonValue.
540    ///
541    /// # Arguments
542    ///
543    /// * `segment` - Optional trading segment ("equity" or "commodity")
544    ///
545    /// # Returns
546    ///
547    /// A `KiteResult<MarginData>` containing typed margin information
548    ///
549    /// # Example
550    ///
551    /// ```rust,no_run
552    /// use kiteconnect_async_wasm::connect::KiteConnect;
553    ///
554    /// # #[tokio::main]
555    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
556    /// let client = KiteConnect::new("api_key", "access_token");
557    ///
558    /// let margins = client.margins_typed(None).await?;
559    /// println!("Available equity margin: {}", margins.equity.unwrap().available.cash);
560    /// # Ok(())
561    /// # }
562    /// ```
563    pub async fn margins_typed(&self, segment: Option<&str>) -> KiteResult<MarginData> {
564        if let Some(segment) = segment {
565            let resp = self
566                .send_request_with_rate_limiting_and_retry(
567                    KiteEndpoint::MarginsSegment,
568                    &[segment],
569                    None,
570                    None,
571                )
572                .await?;
573            let json_response = self.raise_or_return_json_typed(resp).await?;
574            self.parse_response(json_response)
575        } else {
576            let resp = self
577                .send_request_with_rate_limiting_and_retry(KiteEndpoint::Margins, &[], None, None)
578                .await?;
579            let json_response = self.raise_or_return_json_typed(resp).await?;
580            self.parse_response(json_response)
581        }
582    }
583
584    /// Get user holdings with typed response
585    ///
586    /// Returns a vector of strongly typed holding objects instead of JsonValue.
587    ///
588    /// # Returns
589    ///
590    /// A `KiteResult<Vec<Holding>>` containing typed holdings data
591    ///
592    /// # Example
593    ///
594    /// ```rust,no_run
595    /// use kiteconnect_async_wasm::connect::KiteConnect;
596    ///
597    /// # #[tokio::main]
598    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
599    /// let client = KiteConnect::new("api_key", "access_token");
600    ///
601    /// let holdings = client.holdings_typed().await?;
602    /// for holding in holdings {
603    ///     println!("Symbol: {}, Quantity: {}, P&L: {}",
604    ///         holding.trading_symbol, holding.quantity, holding.pnl);
605    /// }
606    /// # Ok(())
607    /// # }
608    /// ```
609    pub async fn holdings_typed(&self) -> KiteResult<Vec<Holding>> {
610        let resp = self
611            .send_request_with_rate_limiting_and_retry(KiteEndpoint::Holdings, &[], None, None)
612            .await?;
613        let json_response = self.raise_or_return_json_typed(resp).await?;
614
615        // Extract the "data" field and parse as Vec<Holding>
616        if let Some(data) = json_response.get("data") {
617            self.parse_response(data.clone())
618        } else {
619            // If no "data" field, try parsing the entire response
620            self.parse_response(json_response)
621        }
622    }
623
624    /// Get user positions with typed response
625    ///
626    /// Returns structured position data instead of JsonValue.
627    ///
628    /// # Returns
629    ///
630    /// A `KiteResult<Vec<Position>>` containing typed positions data
631    ///
632    /// # Example
633    ///
634    /// ```rust,no_run
635    /// use kiteconnect_async_wasm::connect::KiteConnect;
636    ///
637    /// # #[tokio::main]
638    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
639    /// let client = KiteConnect::new("api_key", "access_token");
640    ///
641    /// let positions = client.positions_typed().await?;
642    /// for position in &positions {
643    ///     if position.quantity != 0 {
644    ///         println!("Open position: {} qty {}",
645    ///             position.trading_symbol, position.quantity);
646    ///     }
647    /// }
648    /// # Ok(())
649    /// # }
650    /// ```
651    pub async fn positions_typed(&self) -> KiteResult<Vec<Position>> {
652        let resp = self
653            .send_request_with_rate_limiting_and_retry(KiteEndpoint::Positions, &[], None, None)
654            .await?;
655        let json_response = self.raise_or_return_json_typed(resp).await?;
656
657        // KiteConnect returns positions in nested structure: { "data": { "day": [...], "net": [...] } }
658        // We'll flatten both day and net positions into a single vector
659        let mut all_positions = Vec::new();
660
661        if let Some(data) = json_response.get("data") {
662            if let Some(day_positions) = data.get("day").and_then(|v| v.as_array()) {
663                for pos_json in day_positions {
664                    if let Ok(position) = self.parse_response::<Position>(pos_json.clone()) {
665                        all_positions.push(position);
666                    }
667                }
668            }
669
670            if let Some(net_positions) = data.get("net").and_then(|v| v.as_array()) {
671                for pos_json in net_positions {
672                    if let Ok(position) = self.parse_response::<Position>(pos_json.clone()) {
673                        all_positions.push(position);
674                    }
675                }
676            }
677        }
678
679        Ok(all_positions)
680    }
681
682    /// Convert positions between product types (typed)
683    ///
684    /// Converts a position from one product type to another (e.g., MIS to CNC).
685    ///
686    /// # Arguments
687    ///
688    /// * `request` - Conversion request details
689    ///
690    /// # Returns
691    ///
692    /// A `KiteResult<bool>` indicating success
693    ///
694    /// # Example
695    ///
696    /// ```rust,no_run
697    /// use kiteconnect_async_wasm::connect::KiteConnect;
698    /// use kiteconnect_async_wasm::models::portfolio::ConversionRequest;
699    /// use kiteconnect_async_wasm::models::common::{Exchange, Product, TransactionType};
700    ///
701    /// # #[tokio::main]
702    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
703    /// let client = KiteConnect::new("api_key", "access_token");
704    ///
705    /// let conversion = ConversionRequest {
706    ///     exchange: Exchange::NSE,
707    ///     trading_symbol: "RELIANCE".to_string(),
708    ///     transaction_type: TransactionType::BUY,
709    ///     quantity: 10,
710    ///     from_product: Product::MIS,
711    ///     to_product: Product::CNC,
712    /// };
713    ///
714    /// let success = client.convert_position_typed(&conversion).await?;
715    /// println!("Conversion successful: {}", success);
716    /// # Ok(())
717    /// # }
718    /// ```
719    pub async fn convert_position_typed(&self, request: &ConversionRequest) -> KiteResult<bool> {
720        let mut params = std::collections::HashMap::new();
721        let exchange_str = request.exchange.to_string();
722        let transaction_str = request.transaction_type.to_string();
723        let quantity_str = request.quantity.to_string();
724        let from_product_str = request.from_product.to_string();
725        let to_product_str = request.to_product.to_string();
726
727        params.insert("exchange", exchange_str.as_str());
728        params.insert("tradingsymbol", request.trading_symbol.as_str());
729        params.insert("transaction_type", transaction_str.as_str());
730        params.insert("quantity", quantity_str.as_str());
731        params.insert("old_product", from_product_str.as_str());
732        params.insert("new_product", to_product_str.as_str());
733
734        let resp = self
735            .send_request_with_rate_limiting_and_retry(
736                KiteEndpoint::ConvertPosition,
737                &[],
738                None,
739                Some(params),
740            )
741            .await?;
742        let json_response = self.raise_or_return_json_typed(resp).await?;
743
744        // Check if conversion was successful
745        Ok(json_response.get("status").and_then(|v| v.as_str()) == Some("success"))
746    }
747}