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}