kiteconnect_async_wasm/models/gtt/
triggers.rs

1use serde::{Deserialize, Serialize};
2use chrono::{DateTime, Utc};
3use crate::models::common::{Exchange, Product, TransactionType, OrderType, GttStatus};
4
5/// GTT trigger condition
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct GTTCondition {
8    /// Exchange
9    pub exchange: Exchange,
10    
11    /// Trading symbol
12    #[serde(rename = "tradingsymbol")]
13    pub trading_symbol: String,
14    
15    /// Trigger values (price levels)
16    #[serde(rename = "trigger_values")]
17    pub trigger_values: Vec<f64>,
18    
19    /// Last price (when condition was created)
20    #[serde(rename = "last_price")]
21    pub last_price: f64,
22}
23
24/// GTT order parameters for execution when triggered
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct GTTOrderParams {
27    /// Exchange
28    pub exchange: Exchange,
29    
30    /// Trading symbol
31    #[serde(rename = "tradingsymbol")]
32    pub trading_symbol: String,
33    
34    /// Transaction type
35    #[serde(rename = "transaction_type")]
36    pub transaction_type: TransactionType,
37    
38    /// Order type
39    #[serde(rename = "order_type")]
40    pub order_type: OrderType,
41    
42    /// Product type
43    pub product: Product,
44    
45    /// Quantity
46    pub quantity: u32,
47    
48    /// Price (for limit orders)
49    pub price: f64,
50    
51    /// Result (order ID when triggered, if successful)
52    pub result: Option<GTTOrderResult>,
53}
54
55/// GTT order execution result
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct GTTOrderResult {
58    /// Order ID
59    #[serde(rename = "order_id")]
60    pub order_id: String,
61    
62    /// Rejection reason (if order was rejected)
63    #[serde(rename = "rejection_reason")]
64    pub rejection_reason: Option<String>,
65}
66
67/// GTT trigger type enumeration
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
69#[serde(rename_all = "lowercase")]
70pub enum GTTTriggerType {
71    /// Single trigger (one-time)
72    Single,
73    /// Two-leg trigger (OCO - One Cancels Other)
74    #[serde(rename = "two-leg")]
75    TwoLeg,
76}
77
78/// GTT data structure
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct GTT {
81    /// GTT ID
82    pub id: u32,
83    
84    /// User ID
85    #[serde(rename = "user_id")]
86    pub user_id: String,
87    
88    /// Parent trigger (if this is part of a multi-leg GTT)
89    #[serde(rename = "parent_trigger")]
90    pub parent_trigger: Option<u32>,
91    
92    /// GTT type
93    #[serde(rename = "type")]
94    pub gtt_type: GTTTriggerType,
95    
96    /// Created timestamp
97    #[serde(rename = "created_at")]
98    pub created_at: DateTime<Utc>,
99    
100    /// Updated timestamp
101    #[serde(rename = "updated_at")]
102    pub updated_at: DateTime<Utc>,
103    
104    /// Expires at (optional expiry)
105    #[serde(rename = "expires_at")]
106    pub expires_at: Option<DateTime<Utc>>,
107    
108    /// GTT status
109    pub status: GttStatus,
110    
111    /// Condition
112    pub condition: GTTCondition,
113    
114    /// Orders to be placed when triggered
115    pub orders: Vec<GTTOrderParams>,
116    
117    /// Metadata
118    pub meta: Option<serde_json::Value>,
119}
120
121/// GTT creation parameters
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct GTTCreateParams {
124    /// GTT type
125    #[serde(rename = "type")]
126    pub gtt_type: GTTTriggerType,
127    
128    /// Condition
129    pub condition: GTTCondition,
130    
131    /// Orders to execute when triggered
132    pub orders: Vec<GTTOrderParams>,
133    
134    /// Expiry time (optional)
135    #[serde(rename = "expires_at", skip_serializing_if = "Option::is_none")]
136    pub expires_at: Option<DateTime<Utc>>,
137}
138
139/// GTT modification parameters
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct GTTModifyParams {
142    /// GTT ID
143    #[serde(skip_serializing)]
144    pub gtt_id: u32,
145    
146    /// New condition
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub condition: Option<GTTCondition>,
149    
150    /// New orders
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub orders: Option<Vec<GTTOrderParams>>,
153    
154    /// New expiry time
155    #[serde(rename = "expires_at", skip_serializing_if = "Option::is_none")]
156    pub expires_at: Option<DateTime<Utc>>,
157}
158
159/// GTT response
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct GTTResponse {
162    /// GTT ID
163    pub id: u32,
164}
165
166/// GTTs collection
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct GTTs {
169    /// List of GTTs
170    pub triggers: Vec<GTT>,
171}
172
173impl GTT {
174    /// Check if GTT is active
175    pub fn is_active(&self) -> bool {
176        self.status == GttStatus::Active
177    }
178    
179    /// Check if GTT is triggered
180    pub fn is_triggered(&self) -> bool {
181        self.status == GttStatus::Triggered
182    }
183    
184    /// Check if GTT is disabled
185    pub fn is_disabled(&self) -> bool {
186        self.status == GttStatus::Disabled
187    }
188    
189    /// Check if GTT is expired
190    pub fn is_expired(&self) -> bool {
191        self.status == GttStatus::Expired
192    }
193    
194    /// Check if GTT is cancelled
195    pub fn is_cancelled(&self) -> bool {
196        self.status == GttStatus::Cancelled
197    }
198    
199    /// Check if GTT is rejected
200    pub fn is_rejected(&self) -> bool {
201        self.status == GttStatus::Rejected
202    }
203    
204    /// Check if GTT has expired based on expiry time
205    pub fn has_time_expired(&self) -> bool {
206        if let Some(expires_at) = self.expires_at {
207            chrono::Utc::now() > expires_at
208        } else {
209            false
210        }
211    }
212    
213    /// Get time remaining until expiry
214    pub fn time_to_expiry(&self) -> Option<chrono::Duration> {
215        self.expires_at.map(|expires_at| expires_at - chrono::Utc::now())
216    }
217    
218    /// Check if this is a single trigger GTT
219    pub fn is_single_trigger(&self) -> bool {
220        self.gtt_type == GTTTriggerType::Single
221    }
222    
223    /// Check if this is a two-leg (OCO) GTT
224    pub fn is_two_leg(&self) -> bool {
225        self.gtt_type == GTTTriggerType::TwoLeg
226    }
227    
228    /// Get all trigger values
229    pub fn trigger_values(&self) -> &[f64] {
230        &self.condition.trigger_values
231    }
232    
233    /// Check if current price would trigger the GTT
234    pub fn would_trigger(&self, current_price: f64) -> bool {
235        let last_price = self.condition.last_price;
236        
237        self.condition.trigger_values.iter().any(|&trigger_price| {
238            // Check if price has crossed the trigger level
239            (last_price <= trigger_price && current_price >= trigger_price) ||
240            (last_price >= trigger_price && current_price <= trigger_price)
241        })
242    }
243    
244    /// Get the number of orders to be executed
245    pub fn order_count(&self) -> usize {
246        self.orders.len()
247    }
248    
249    /// Check if any orders were successfully placed
250    pub fn has_successful_orders(&self) -> bool {
251        self.orders.iter().any(|order| {
252            order.result.as_ref()
253                .map(|result| !result.order_id.is_empty())
254                .unwrap_or(false)
255        })
256    }
257    
258    /// Get successful order IDs
259    pub fn successful_order_ids(&self) -> Vec<&str> {
260        self.orders
261            .iter()
262            .filter_map(|order| {
263                order.result.as_ref()
264                    .and_then(|result| {
265                        if !result.order_id.is_empty() {
266                            Some(result.order_id.as_str())
267                        } else {
268                            None
269                        }
270                    })
271            })
272            .collect()
273    }
274    
275    /// Get failed orders with rejection reasons
276    pub fn failed_orders(&self) -> Vec<(&GTTOrderParams, &str)> {
277        self.orders
278            .iter()
279            .filter_map(|order| {
280                order.result.as_ref()
281                    .and_then(|result| {
282                        result.rejection_reason.as_ref()
283                            .map(|reason| (order, reason.as_str()))
284                    })
285            })
286            .collect()
287    }
288}
289
290impl GTTCreateParams {
291    /// Create a single trigger GTT
292    pub fn single(
293        condition: GTTCondition,
294        order: GTTOrderParams,
295    ) -> Self {
296        Self {
297            gtt_type: GTTTriggerType::Single,
298            condition,
299            orders: vec![order],
300            expires_at: None,
301        }
302    }
303    
304    /// Create a two-leg (OCO) GTT
305    pub fn two_leg(
306        condition: GTTCondition,
307        orders: Vec<GTTOrderParams>,
308    ) -> Self {
309        Self {
310            gtt_type: GTTTriggerType::TwoLeg,
311            condition,
312            orders,
313            expires_at: None,
314        }
315    }
316    
317    /// Set expiry time
318    pub fn expires_at(mut self, expires_at: DateTime<Utc>) -> Self {
319        self.expires_at = Some(expires_at);
320        self
321    }
322    
323    /// Validate GTT parameters
324    pub fn validate(&self) -> Result<(), String> {
325        if self.condition.trigger_values.is_empty() {
326            return Err("At least one trigger value is required".to_string());
327        }
328        
329        if self.orders.is_empty() {
330            return Err("At least one order is required".to_string());
331        }
332        
333        match self.gtt_type {
334            GTTTriggerType::Single => {
335                if self.orders.len() > 1 {
336                    return Err("Single trigger GTT can have only one order".to_string());
337                }
338                if self.condition.trigger_values.len() > 1 {
339                    return Err("Single trigger GTT can have only one trigger value".to_string());
340                }
341            }
342            GTTTriggerType::TwoLeg => {
343                if self.orders.len() != 2 {
344                    return Err("Two-leg GTT must have exactly two orders".to_string());
345                }
346                if self.condition.trigger_values.len() != 2 {
347                    return Err("Two-leg GTT must have exactly two trigger values".to_string());
348                }
349            }
350        }
351        
352        Ok(())
353    }
354}
355
356impl GTTs {
357    /// Get active GTTs
358    pub fn active_gtts(&self) -> Vec<&GTT> {
359        self.triggers.iter().filter(|gtt| gtt.is_active()).collect()
360    }
361    
362    /// Get triggered GTTs
363    pub fn triggered_gtts(&self) -> Vec<&GTT> {
364        self.triggers.iter().filter(|gtt| gtt.is_triggered()).collect()
365    }
366    
367    /// Get expired GTTs
368    pub fn expired_gtts(&self) -> Vec<&GTT> {
369        self.triggers.iter().filter(|gtt| gtt.is_expired()).collect()
370    }
371    
372    /// Get GTTs for a specific symbol
373    pub fn gtts_for_symbol(&self, symbol: &str) -> Vec<&GTT> {
374        self.triggers
375            .iter()
376            .filter(|gtt| gtt.condition.trading_symbol == symbol)
377            .collect()
378    }
379    
380    /// Find GTT by ID
381    pub fn find_gtt(&self, gtt_id: u32) -> Option<&GTT> {
382        self.triggers.iter().find(|gtt| gtt.id == gtt_id)
383    }
384    
385    /// Get GTTs expiring soon
386    pub fn expiring_soon(&self, hours: i64) -> Vec<&GTT> {
387        let threshold = chrono::Utc::now() + chrono::Duration::hours(hours);
388        
389        self.active_gtts()
390            .into_iter()
391            .filter(|gtt| {
392                gtt.expires_at
393                    .map(|expires_at| expires_at <= threshold)
394                    .unwrap_or(false)
395            })
396            .collect()
397    }
398}