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}