kiteconnect_async_wasm/models/mutual_funds/
instruments.rs

1use chrono::NaiveDate;
2use serde::{Deserialize, Deserializer, Serialize};
3
4/// Convert CSV string fields into f64 during deserialization
5fn deserialize_string_to_f64<'de, D>(deserializer: D) -> Result<f64, D::Error>
6where
7    D: Deserializer<'de>,
8{
9    let s: String = Deserialize::deserialize(deserializer)?;
10    s.parse::<f64>().map_err(serde::de::Error::custom)
11}
12
13/// Mutual Fund instrument data
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct MFInstrument {
16    /// Trading symbol (unique identifier)
17    #[serde(rename = "tradingsymbol")]
18    pub trading_symbol: String,
19
20    /// AMC (Asset Management Company) code
21    pub amc: String,
22
23    /// Fund name
24    pub name: String,
25
26    /// Fund type (legacy alias, not present in CSV; prefer `dividend_type`/`scheme_type`)
27    #[serde(default)]
28    pub fund_type: Option<String>,
29
30    /// Fund category (equity, debt, hybrid, etc.)
31    pub plan: String,
32
33    /// Settlement type (T+1, T+3, etc.)
34    #[serde(rename = "settlement_type")]
35    pub settlement_type: String,
36
37    /// Minimum purchase amount
38    #[serde(
39        rename = "minimum_purchase_amount",
40        deserialize_with = "deserialize_string_to_f64"
41    )]
42    pub minimum_purchase_amount: f64,
43
44    /// Purchase amount multiple
45    #[serde(
46        rename = "purchase_amount_multiplier",
47        deserialize_with = "deserialize_string_to_f64"
48    )]
49    pub purchase_amount_multiplier: f64,
50
51    /// Minimum additional purchase amount
52    #[serde(
53        rename = "minimum_additional_purchase_amount",
54        deserialize_with = "deserialize_string_to_f64"
55    )]
56    pub minimum_additional_purchase_amount: f64,
57
58    /// Minimum redemption quantity
59    #[serde(
60        rename = "minimum_redemption_quantity",
61        deserialize_with = "deserialize_string_to_f64"
62    )]
63    pub minimum_redemption_quantity: f64,
64
65    /// Redemption quantity multiple
66    #[serde(
67        rename = "redemption_quantity_multiplier",
68        deserialize_with = "deserialize_string_to_f64"
69    )]
70    pub redemption_quantity_multiplier: f64,
71
72    /// Dividend reinvestment flag (growth/dividend)
73    #[serde(rename = "dividend_type")]
74    pub dividend_type: String,
75
76    /// Scheme type (equity, elss, etc.)
77    #[serde(rename = "scheme_type")]
78    pub scheme_type: String,
79
80    /// Last price (NAV)
81    #[serde(rename = "last_price", deserialize_with = "deserialize_string_to_f64")]
82    pub last_price: f64,
83
84    /// Last price date
85    #[serde(rename = "last_price_date")]
86    pub last_price_date: NaiveDate,
87}
88
89/// MF fund performance data
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct MFPerformance {
92    /// Trading symbol
93    #[serde(rename = "tradingsymbol")]
94    pub trading_symbol: String,
95
96    /// Fund name
97    pub name: String,
98
99    /// Current NAV
100    pub nav: f64,
101
102    /// NAV date
103    #[serde(rename = "nav_date")]
104    pub nav_date: NaiveDate,
105
106    /// 1 day return
107    #[serde(rename = "return_1d")]
108    pub return_1d: Option<f64>,
109
110    /// 1 week return
111    #[serde(rename = "return_1w")]
112    pub return_1w: Option<f64>,
113
114    /// 1 month return
115    #[serde(rename = "return_1m")]
116    pub return_1m: Option<f64>,
117
118    /// 3 months return
119    #[serde(rename = "return_3m")]
120    pub return_3m: Option<f64>,
121
122    /// 6 months return
123    #[serde(rename = "return_6m")]
124    pub return_6m: Option<f64>,
125
126    /// 1 year return
127    #[serde(rename = "return_1y")]
128    pub return_1y: Option<f64>,
129
130    /// 3 years return
131    #[serde(rename = "return_3y")]
132    pub return_3y: Option<f64>,
133
134    /// 5 years return
135    #[serde(rename = "return_5y")]
136    pub return_5y: Option<f64>,
137
138    /// Since inception return
139    #[serde(rename = "return_inception")]
140    pub return_inception: Option<f64>,
141}
142
143/// MF instrument search parameters
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct MFInstrumentSearch {
146    /// Search query (fund name or AMC)
147    pub query: String,
148
149    /// AMC filter
150    pub amc: Option<String>,
151
152    /// Fund type filter
153    pub fund_type: Option<String>,
154
155    /// Plan filter (growth, dividend)
156    pub plan: Option<String>,
157
158    /// Limit results
159    pub limit: Option<u32>,
160}
161
162impl MFInstrument {
163    /// Check if this is an equity fund
164    pub fn is_equity_fund(&self) -> bool {
165        self.plan.to_lowercase().contains("equity")
166    }
167
168    /// Check if this is a debt fund
169    pub fn is_debt_fund(&self) -> bool {
170        self.plan.to_lowercase().contains("debt")
171    }
172
173    /// Check if this is a hybrid fund
174    pub fn is_hybrid_fund(&self) -> bool {
175        self.plan.to_lowercase().contains("hybrid")
176    }
177
178    /// Check if this is a growth plan
179    pub fn is_growth_plan(&self) -> bool {
180        self.dividend_type.to_lowercase().contains("growth")
181    }
182
183    /// Check if this is a dividend plan
184    pub fn is_dividend_plan(&self) -> bool {
185        self.dividend_type.to_lowercase().contains("dividend")
186    }
187
188    /// Check if fund allows SIP
189    pub fn allows_sip(&self) -> bool {
190        self.minimum_additional_purchase_amount > 0.0
191    }
192
193    /// Get the settlement days
194    pub fn settlement_days(&self) -> u32 {
195        // Parse settlement type like "T+1", "T+3"
196        self.settlement_type
197            .chars()
198            .filter(|c| c.is_ascii_digit())
199            .collect::<String>()
200            .parse()
201            .unwrap_or(1)
202    }
203
204    /// Check if amount is valid for purchase
205    pub fn is_valid_purchase_amount(&self, amount: f64) -> bool {
206        if amount < self.minimum_purchase_amount {
207            return false;
208        }
209
210        let remainder = (amount - self.minimum_purchase_amount) % self.purchase_amount_multiplier;
211        remainder.abs() < 0.01 // Allow for floating point precision
212    }
213
214    /// Get next valid purchase amount
215    pub fn next_valid_purchase_amount(&self, amount: f64) -> f64 {
216        if amount <= self.minimum_purchase_amount {
217            return self.minimum_purchase_amount;
218        }
219
220        let excess = amount - self.minimum_purchase_amount;
221        let multiplier_count = (excess / self.purchase_amount_multiplier).ceil();
222        self.minimum_purchase_amount + (multiplier_count * self.purchase_amount_multiplier)
223    }
224}
225
226impl MFPerformance {
227    /// Get the best performing period return
228    pub fn best_return(&self) -> Option<f64> {
229        [
230            self.return_1d,
231            self.return_1w,
232            self.return_1m,
233            self.return_3m,
234            self.return_6m,
235            self.return_1y,
236            self.return_3y,
237            self.return_5y,
238            self.return_inception,
239        ]
240        .iter()
241        .filter_map(|&r| r)
242        .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
243    }
244
245    /// Get the worst performing period return
246    pub fn worst_return(&self) -> Option<f64> {
247        [
248            self.return_1d,
249            self.return_1w,
250            self.return_1m,
251            self.return_3m,
252            self.return_6m,
253            self.return_1y,
254            self.return_3y,
255            self.return_5y,
256            self.return_inception,
257        ]
258        .iter()
259        .filter_map(|&r| r)
260        .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
261    }
262
263    /// Check if fund is consistently performing (positive returns across periods)
264    pub fn is_consistently_positive(&self) -> bool {
265        [
266            self.return_1m,
267            self.return_3m,
268            self.return_6m,
269            self.return_1y,
270        ]
271        .iter()
272        .filter_map(|&r| r)
273        .all(|r| r > 0.0)
274    }
275
276    /// Get volatility indicator based on return spread
277    pub fn volatility_indicator(&self) -> Option<f64> {
278        match (self.best_return(), self.worst_return()) {
279            (Some(best), Some(worst)) => Some(best - worst),
280            _ => None,
281        }
282    }
283}
284
285impl MFInstrumentSearch {
286    /// Create a new search
287    pub fn new(query: String) -> Self {
288        Self {
289            query,
290            amc: None,
291            fund_type: None,
292            plan: None,
293            limit: None,
294        }
295    }
296
297    /// Filter by AMC
298    pub fn amc<S: Into<String>>(mut self, amc: S) -> Self {
299        self.amc = Some(amc.into());
300        self
301    }
302
303    /// Filter by fund type
304    pub fn fund_type<S: Into<String>>(mut self, fund_type: S) -> Self {
305        self.fund_type = Some(fund_type.into());
306        self
307    }
308
309    /// Filter by plan
310    pub fn plan<S: Into<String>>(mut self, plan: S) -> Self {
311        self.plan = Some(plan.into());
312        self
313    }
314
315    /// Limit results
316    pub fn limit(mut self, limit: u32) -> Self {
317        self.limit = Some(limit);
318        self
319    }
320
321    /// Search for equity funds only
322    pub fn equity_only(mut self) -> Self {
323        self.plan = Some("equity".to_string());
324        self
325    }
326
327    /// Search for debt funds only
328    pub fn debt_only(mut self) -> Self {
329        self.plan = Some("debt".to_string());
330        self
331    }
332}