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}