kiteconnect_async_wasm/models/gtt/
triggers.rs

1use crate::models::common::{Exchange, GttStatus, OrderType, Product, TransactionType};
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
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
216            .map(|expires_at| expires_at - chrono::Utc::now())
217    }
218
219    /// Check if this is a single trigger GTT
220    pub fn is_single_trigger(&self) -> bool {
221        self.gtt_type == GTTTriggerType::Single
222    }
223
224    /// Check if this is a two-leg (OCO) GTT
225    pub fn is_two_leg(&self) -> bool {
226        self.gtt_type == GTTTriggerType::TwoLeg
227    }
228
229    /// Get all trigger values
230    pub fn trigger_values(&self) -> &[f64] {
231        &self.condition.trigger_values
232    }
233
234    /// Check if current price would trigger the GTT
235    pub fn would_trigger(&self, current_price: f64) -> bool {
236        let last_price = self.condition.last_price;
237
238        self.condition.trigger_values.iter().any(|&trigger_price| {
239            // Check if price has crossed the trigger level
240            (last_price <= trigger_price && current_price >= trigger_price)
241                || (last_price >= trigger_price && current_price <= trigger_price)
242        })
243    }
244
245    /// Get the number of orders to be executed
246    pub fn order_count(&self) -> usize {
247        self.orders.len()
248    }
249
250    /// Check if any orders were successfully placed
251    pub fn has_successful_orders(&self) -> bool {
252        self.orders.iter().any(|order| {
253            order
254                .result
255                .as_ref()
256                .map(|result| !result.order_id.is_empty())
257                .unwrap_or(false)
258        })
259    }
260
261    /// Get successful order IDs
262    pub fn successful_order_ids(&self) -> Vec<&str> {
263        self.orders
264            .iter()
265            .filter_map(|order| {
266                order.result.as_ref().and_then(|result| {
267                    if !result.order_id.is_empty() {
268                        Some(result.order_id.as_str())
269                    } else {
270                        None
271                    }
272                })
273            })
274            .collect()
275    }
276
277    /// Get failed orders with rejection reasons
278    pub fn failed_orders(&self) -> Vec<(&GTTOrderParams, &str)> {
279        self.orders
280            .iter()
281            .filter_map(|order| {
282                order.result.as_ref().and_then(|result| {
283                    result
284                        .rejection_reason
285                        .as_ref()
286                        .map(|reason| (order, reason.as_str()))
287                })
288            })
289            .collect()
290    }
291}
292
293impl GTTCreateParams {
294    /// Create a single trigger GTT
295    pub fn single(condition: GTTCondition, order: GTTOrderParams) -> 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(condition: GTTCondition, orders: Vec<GTTOrderParams>) -> Self {
306        Self {
307            gtt_type: GTTTriggerType::TwoLeg,
308            condition,
309            orders,
310            expires_at: None,
311        }
312    }
313
314    /// Set expiry time
315    pub fn expires_at(mut self, expires_at: DateTime<Utc>) -> Self {
316        self.expires_at = Some(expires_at);
317        self
318    }
319
320    /// Validate GTT parameters
321    pub fn validate(&self) -> Result<(), String> {
322        if self.condition.trigger_values.is_empty() {
323            return Err("At least one trigger value is required".to_string());
324        }
325
326        if self.orders.is_empty() {
327            return Err("At least one order is required".to_string());
328        }
329
330        match self.gtt_type {
331            GTTTriggerType::Single => {
332                if self.orders.len() > 1 {
333                    return Err("Single trigger GTT can have only one order".to_string());
334                }
335                if self.condition.trigger_values.len() > 1 {
336                    return Err("Single trigger GTT can have only one trigger value".to_string());
337                }
338            }
339            GTTTriggerType::TwoLeg => {
340                if self.orders.len() != 2 {
341                    return Err("Two-leg GTT must have exactly two orders".to_string());
342                }
343                if self.condition.trigger_values.len() != 2 {
344                    return Err("Two-leg GTT must have exactly two trigger values".to_string());
345                }
346            }
347        }
348
349        Ok(())
350    }
351}
352
353impl GTTs {
354    /// Get active GTTs
355    pub fn active_gtts(&self) -> Vec<&GTT> {
356        self.triggers.iter().filter(|gtt| gtt.is_active()).collect()
357    }
358
359    /// Get triggered GTTs
360    pub fn triggered_gtts(&self) -> Vec<&GTT> {
361        self.triggers
362            .iter()
363            .filter(|gtt| gtt.is_triggered())
364            .collect()
365    }
366
367    /// Get expired GTTs
368    pub fn expired_gtts(&self) -> Vec<&GTT> {
369        self.triggers
370            .iter()
371            .filter(|gtt| gtt.is_expired())
372            .collect()
373    }
374
375    /// Get GTTs for a specific symbol
376    pub fn gtts_for_symbol(&self, symbol: &str) -> Vec<&GTT> {
377        self.triggers
378            .iter()
379            .filter(|gtt| gtt.condition.trading_symbol == symbol)
380            .collect()
381    }
382
383    /// Find GTT by ID
384    pub fn find_gtt(&self, gtt_id: u32) -> Option<&GTT> {
385        self.triggers.iter().find(|gtt| gtt.id == gtt_id)
386    }
387
388    /// Get GTTs expiring soon
389    pub fn expiring_soon(&self, hours: i64) -> Vec<&GTT> {
390        let threshold = chrono::Utc::now() + chrono::Duration::hours(hours);
391
392        self.active_gtts()
393            .into_iter()
394            .filter(|gtt| {
395                gtt.expires_at
396                    .map(|expires_at| expires_at <= threshold)
397                    .unwrap_or(false)
398            })
399            .collect()
400    }
401}