kiteconnect_async_wasm/connect/
portfolio.rs

1//! # Portfolio Module
2//! 
3//! This module contains portfolio-related methods for the KiteConnect API.
4
5use serde_json::Value as JsonValue;
6use anyhow::Result;
7use crate::connect::endpoints::KiteEndpoint;
8// Import typed models for dual API support
9use crate::models::common::KiteResult;
10use crate::models::portfolio::{Holding, Position, ConversionRequest};
11use crate::models::auth::MarginData;
12
13use crate::connect::KiteConnect;
14
15impl KiteConnect {
16    // === LEGACY API METHODS (JSON responses) ===
17    
18    /// Retrieves account balance and margin details
19    /// 
20    /// Returns margin information for trading segments including available cash,
21    /// used margins, and available margins for different product types.
22    /// 
23    /// # Arguments
24    /// 
25    /// * `segment` - Optional trading segment ("equity" or "commodity"). If None, returns all segments
26    /// 
27    /// # Returns
28    /// 
29    /// A `Result<JsonValue>` containing margin data with fields like:
30    /// - `available` - Available margin for trading
31    /// - `utilised` - Currently utilized margin
32    /// - `net` - Net available margin
33    /// - `enabled` - Whether the segment is enabled
34    /// 
35    /// # Errors
36    /// 
37    /// Returns an error if the API request fails or the user is not authenticated.
38    /// 
39    /// # Example
40    /// 
41    /// ```rust,no_run
42    /// use kiteconnect_async_wasm::connect::KiteConnect;
43    /// 
44    /// # #[tokio::main]
45    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
46    /// let client = KiteConnect::new("api_key", "access_token");
47    /// 
48    /// // Get margins for all segments
49    /// let all_margins = client.margins(None).await?;
50    /// println!("All margins: {:?}", all_margins);
51    /// 
52    /// // Get margins for specific segment
53    /// let equity_margins = client.margins(Some("equity".to_string())).await?;
54    /// println!("Equity available margin: {}", 
55    ///     equity_margins["data"]["available"]["live_balance"]);
56    /// # Ok(())
57    /// # }
58    /// ```
59    pub async fn margins(&self, segment: Option<String>) -> Result<JsonValue> {
60        if let Some(segment) = segment {
61            let resp = self.send_request_with_rate_limiting_and_retry(
62                KiteEndpoint::MarginsSegment, 
63                &[&segment],
64                None,
65                None
66            ).await.map_err(|e| anyhow::anyhow!("Get margins failed: {:?}", e))?;
67            self.raise_or_return_json(resp).await
68        } else {
69            let resp = self.send_request_with_rate_limiting_and_retry(
70                KiteEndpoint::Margins, 
71                &[],
72                None,
73                None
74            ).await.map_err(|e| anyhow::anyhow!("Get margins failed: {:?}", e))?;
75            self.raise_or_return_json(resp).await
76        }
77    }
78
79    /// Get user profile details
80    pub async fn profile(&self) -> Result<JsonValue> {
81        let resp = self.send_request_with_rate_limiting_and_retry(
82            KiteEndpoint::Profile, 
83            &[],
84            None,
85            None
86        ).await.map_err(|e| anyhow::anyhow!("Get profile failed: {:?}", e))?;
87        self.raise_or_return_json(resp).await
88    }
89
90    /// Retrieves the user's holdings (stocks held in demat account)
91    /// 
92    /// Holdings represent stocks that are held in the user's demat account.
93    /// This includes information about quantity, average price, current market value,
94    /// profit/loss, and more.
95    /// 
96    /// # Returns
97    /// 
98    /// A `Result<JsonValue>` containing holdings data with fields like:
99    /// - `tradingsymbol` - Trading symbol of the instrument
100    /// - `quantity` - Total quantity held
101    /// - `average_price` - Average buying price
102    /// - `last_price` - Current market price
103    /// - `pnl` - Profit and loss
104    /// - `product` - Product type (CNC, MIS, etc.)
105    /// 
106    /// # Errors
107    /// 
108    /// Returns an error if the API request fails or the user is not authenticated.
109    /// 
110    /// # Example
111    /// 
112    /// ```rust,no_run
113    /// use kiteconnect_async_wasm::connect::KiteConnect;
114    /// 
115    /// # #[tokio::main]
116    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
117    /// let client = KiteConnect::new("api_key", "access_token");
118    /// 
119    /// let holdings = client.holdings().await?;
120    /// println!("Holdings: {:?}", holdings);
121    /// 
122    /// // Access specific fields
123    /// if let Some(data) = holdings["data"].as_array() {
124    ///     for holding in data {
125    ///         println!("Symbol: {}, Quantity: {}", 
126    ///             holding["tradingsymbol"], holding["quantity"]);
127    ///     }
128    /// }
129    /// # Ok(())
130    /// # }
131    /// ```
132    pub async fn holdings(&self) -> Result<JsonValue> {
133        let resp = self.send_request_with_rate_limiting_and_retry(
134            KiteEndpoint::Holdings, 
135            &[],
136            None,
137            None
138        ).await.map_err(|e| anyhow::anyhow!("Get holdings failed: {:?}", e))?;
139        self.raise_or_return_json(resp).await
140    }
141
142    /// Retrieves the user's positions (open positions for the day)
143    /// 
144    /// Positions represent open trading positions for the current trading day.
145    /// This includes both intraday and carry-forward positions with details about
146    /// profit/loss, margin requirements, and position status.
147    /// 
148    /// # Returns
149    /// 
150    /// A `Result<JsonValue>` containing positions data with fields like:
151    /// - `tradingsymbol` - Trading symbol of the instrument
152    /// - `quantity` - Net position quantity
153    /// - `buy_quantity` - Total buy quantity
154    /// - `sell_quantity` - Total sell quantity
155    /// - `average_price` - Average position price
156    /// - `pnl` - Realized and unrealized P&L
157    /// - `product` - Product type (MIS, CNC, NRML)
158    /// 
159    /// # Errors
160    /// 
161    /// Returns an error if the API request fails or the user is not authenticated.
162    /// 
163    /// # Example
164    /// 
165    /// ```rust,no_run
166    /// use kiteconnect_async_wasm::connect::KiteConnect;
167    /// 
168    /// # #[tokio::main]
169    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
170    /// let client = KiteConnect::new("api_key", "access_token");
171    /// 
172    /// let positions = client.positions().await?;
173    /// println!("Positions: {:?}", positions);
174    /// 
175    /// // Check for open positions
176    /// if let Some(day_positions) = positions["data"]["day"].as_array() {
177    ///     for position in day_positions {
178    ///         if position["quantity"].as_i64().unwrap_or(0) != 0 {
179    ///             println!("Open position: {} qty {}", 
180    ///                 position["tradingsymbol"], position["quantity"]);
181    ///         }
182    ///     }
183    /// }
184    /// # Ok(())
185    /// # }
186    /// ```
187    pub async fn positions(&self) -> Result<JsonValue> {
188        let resp = self.send_request_with_rate_limiting_and_retry(
189            KiteEndpoint::Positions, 
190            &[],
191            None,
192            None
193        ).await.map_err(|e| anyhow::anyhow!("Get positions failed: {:?}", e))?;
194        self.raise_or_return_json(resp).await
195    }
196
197    // === TYPED API METHODS (v1.0.0) ===
198    
199    /// Get user margins with typed response
200    /// 
201    /// Returns strongly typed margin data instead of JsonValue.
202    /// 
203    /// # Arguments
204    /// 
205    /// * `segment` - Optional trading segment ("equity" or "commodity")
206    /// 
207    /// # Returns
208    /// 
209    /// A `KiteResult<MarginData>` containing typed margin information
210    /// 
211    /// # Example
212    /// 
213    /// ```rust,no_run
214    /// use kiteconnect_async_wasm::connect::KiteConnect;
215    /// 
216    /// # #[tokio::main]
217    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
218    /// let client = KiteConnect::new("api_key", "access_token");
219    /// 
220    /// let margins = client.margins_typed(None).await?;
221    /// println!("Available equity margin: {}", margins.equity.unwrap().available.cash);
222    /// # Ok(())
223    /// # }
224    /// ```
225    pub async fn margins_typed(&self, segment: Option<&str>) -> KiteResult<MarginData> {
226        if let Some(segment) = segment {
227            let resp = self.send_request_with_rate_limiting_and_retry(
228                KiteEndpoint::MarginsSegment, 
229                &[segment],
230                None,
231                None
232            ).await?;
233            let json_response = self.raise_or_return_json_typed(resp).await?;
234            self.parse_response(json_response)
235        } else {
236            let resp = self.send_request_with_rate_limiting_and_retry(
237                KiteEndpoint::Margins, 
238                &[],
239                None,
240                None
241            ).await?;
242            let json_response = self.raise_or_return_json_typed(resp).await?;
243            self.parse_response(json_response)
244        }
245    }
246
247    /// Get user holdings with typed response
248    /// 
249    /// Returns a vector of strongly typed holding objects instead of JsonValue.
250    /// 
251    /// # Returns
252    /// 
253    /// A `KiteResult<Vec<Holding>>` containing typed holdings data
254    /// 
255    /// # Example
256    /// 
257    /// ```rust,no_run
258    /// use kiteconnect_async_wasm::connect::KiteConnect;
259    /// 
260    /// # #[tokio::main]
261    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
262    /// let client = KiteConnect::new("api_key", "access_token");
263    /// 
264    /// let holdings = client.holdings_typed().await?;
265    /// for holding in holdings {
266    ///     println!("Symbol: {}, Quantity: {}, P&L: {}", 
267    ///         holding.trading_symbol, holding.quantity, holding.pnl);
268    /// }
269    /// # Ok(())
270    /// # }
271    /// ```
272    pub async fn holdings_typed(&self) -> KiteResult<Vec<Holding>> {
273        let resp = self.send_request_with_rate_limiting_and_retry(
274            KiteEndpoint::Holdings, 
275            &[],
276            None,
277            None
278        ).await?;
279        let json_response = self.raise_or_return_json_typed(resp).await?;
280        
281        // Extract the "data" field and parse as Vec<Holding>
282        if let Some(data) = json_response.get("data") {
283            self.parse_response(data.clone())
284        } else {
285            // If no "data" field, try parsing the entire response
286            self.parse_response(json_response)
287        }
288    }
289
290    /// Get user positions with typed response
291    /// 
292    /// Returns structured position data instead of JsonValue.
293    /// 
294    /// # Returns
295    /// 
296    /// A `KiteResult<Vec<Position>>` containing typed positions data
297    /// 
298    /// # Example
299    /// 
300    /// ```rust,no_run
301    /// use kiteconnect_async_wasm::connect::KiteConnect;
302    /// 
303    /// # #[tokio::main]
304    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
305    /// let client = KiteConnect::new("api_key", "access_token");
306    /// 
307    /// let positions = client.positions_typed().await?;
308    /// for position in &positions {
309    ///     if position.quantity != 0 {
310    ///         println!("Open position: {} qty {}", 
311    ///             position.trading_symbol, position.quantity);
312    ///     }
313    /// }
314    /// # Ok(())
315    /// # }
316    /// ```
317    pub async fn positions_typed(&self) -> KiteResult<Vec<Position>> {
318        let resp = self.send_request_with_rate_limiting_and_retry(
319            KiteEndpoint::Positions, 
320            &[],
321            None,
322            None
323        ).await?;
324        let json_response = self.raise_or_return_json_typed(resp).await?;
325        
326        // KiteConnect returns positions in nested structure: { "data": { "day": [...], "net": [...] } }
327        // We'll flatten both day and net positions into a single vector
328        let mut all_positions = Vec::new();
329        
330        if let Some(data) = json_response.get("data") {
331            if let Some(day_positions) = data.get("day").and_then(|v| v.as_array()) {
332                for pos_json in day_positions {
333                    if let Ok(position) = self.parse_response::<Position>(pos_json.clone()) {
334                        all_positions.push(position);
335                    }
336                }
337            }
338            
339            if let Some(net_positions) = data.get("net").and_then(|v| v.as_array()) {
340                for pos_json in net_positions {
341                    if let Ok(position) = self.parse_response::<Position>(pos_json.clone()) {
342                        all_positions.push(position);
343                    }
344                }
345            }
346        }
347        
348        Ok(all_positions)
349    }
350
351    /// Convert positions between product types (typed)
352    /// 
353    /// Converts a position from one product type to another (e.g., MIS to CNC).
354    /// 
355    /// # Arguments
356    /// 
357    /// * `request` - Conversion request details
358    /// 
359    /// # Returns
360    /// 
361    /// A `KiteResult<bool>` indicating success
362    /// 
363    /// # Example
364    /// 
365    /// ```rust,no_run
366    /// use kiteconnect_async_wasm::connect::KiteConnect;
367    /// use kiteconnect_async_wasm::models::portfolio::ConversionRequest;
368    /// use kiteconnect_async_wasm::models::common::{Exchange, Product, TransactionType};
369    /// 
370    /// # #[tokio::main]
371    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
372    /// let client = KiteConnect::new("api_key", "access_token");
373    /// 
374    /// let conversion = ConversionRequest {
375    ///     exchange: Exchange::NSE,
376    ///     trading_symbol: "RELIANCE".to_string(),
377    ///     transaction_type: TransactionType::BUY,
378    ///     quantity: 10,
379    ///     from_product: Product::MIS,
380    ///     to_product: Product::CNC,
381    /// };
382    /// 
383    /// let success = client.convert_position_typed(&conversion).await?;
384    /// println!("Conversion successful: {}", success);
385    /// # Ok(())
386    /// # }
387    /// ```
388    pub async fn convert_position_typed(&self, request: &ConversionRequest) -> KiteResult<bool> {
389        let mut params = std::collections::HashMap::new();
390        let exchange_str = request.exchange.to_string();
391        let transaction_str = request.transaction_type.to_string();
392        let quantity_str = request.quantity.to_string();
393        let from_product_str = request.from_product.to_string();
394        let to_product_str = request.to_product.to_string();
395        
396        params.insert("exchange", exchange_str.as_str());
397        params.insert("tradingsymbol", request.trading_symbol.as_str());
398        params.insert("transaction_type", transaction_str.as_str());
399        params.insert("quantity", quantity_str.as_str());
400        params.insert("old_product", from_product_str.as_str());
401        params.insert("new_product", to_product_str.as_str());
402
403        let resp = self.send_request_with_rate_limiting_and_retry(
404            KiteEndpoint::ConvertPosition, 
405            &[],
406            None,
407            Some(params)
408        ).await?;
409        let json_response = self.raise_or_return_json_typed(resp).await?;
410        
411        // Check if conversion was successful
412        Ok(json_response.get("status").and_then(|v| v.as_str()) == Some("success"))
413    }
414}