Skip to main content

dhan_rs/types/
conditional.rs

1#![allow(missing_docs)]
2//! Conditional Trigger types.
3
4use serde::{Deserialize, Serialize};
5
6use crate::types::enums::*;
7
8// ---------------------------------------------------------------------------
9// Multi Order
10// ---------------------------------------------------------------------------
11
12/// Exchange segments accepted by the REST multi-order endpoint.
13///
14/// This is deliberately separate from [`ExchangeSegment`]. `NSE_COMM` is
15/// documented for this JSON API, but Dhan has not documented a corresponding
16/// standard market-feed binary segment code.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18#[allow(non_camel_case_types)]
19pub enum MultiOrderExchangeSegment {
20    NSE_EQ,
21    NSE_FNO,
22    NSE_COMM,
23    BSE_EQ,
24    BSE_FNO,
25    MCX_COMM,
26}
27
28/// Product types accepted specifically by the REST multi-order operation.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30#[allow(non_camel_case_types)]
31pub enum MultiOrderProductType {
32    CNC,
33    INTRADAY,
34    MARGIN,
35    MTF,
36}
37
38/// One order in a multi-order request.
39///
40/// Used in [`MultiOrderRequest`] for `POST /v2/alerts/multi/orders`.
41#[derive(Debug, Clone, Serialize)]
42#[serde(rename_all = "camelCase")]
43pub struct MultiOrderItemRequest {
44    /// Caller-selected identifier for correlating a response to this item.
45    pub sequence: String,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub correlation_id: Option<String>,
48    pub transaction_type: TransactionType,
49    pub exchange_segment: MultiOrderExchangeSegment,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub product_type: Option<MultiOrderProductType>,
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub order_type: Option<OrderType>,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub validity: Option<Validity>,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub security_id: Option<String>,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub quantity: Option<u32>,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub after_market_order: Option<bool>,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub amo_time: Option<AmoTime>,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub price: Option<f64>,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub trigger_price: Option<f64>,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub disclosed_quantity: Option<u32>,
70}
71
72/// Request body for placing a batch of orders without a condition.
73#[derive(Debug, Clone, Serialize)]
74#[serde(rename_all = "camelCase")]
75pub struct MultiOrderRequest {
76    pub dhan_client_id: String,
77    pub orders: Vec<MultiOrderItemRequest>,
78}
79
80impl MultiOrderRequest {
81    /// Validate the documented batch and correlation fields before sending.
82    pub fn validate(&self) -> std::result::Result<(), &'static str> {
83        if self.dhan_client_id.trim().is_empty() {
84            return Err("multi order dhan_client_id cannot be empty");
85        }
86        if self.orders.is_empty() {
87            return Err("multi order orders cannot be empty");
88        }
89        if self.orders.iter().any(|order| {
90            order
91                .correlation_id
92                .as_ref()
93                .is_some_and(|value| value.chars().count() > 30)
94        }) {
95            return Err("multi order correlation_id cannot exceed 30 characters");
96        }
97        if self.orders.iter().any(|order| {
98            order.quantity.is_some_and(|value| value > i32::MAX as u32)
99                || order
100                    .disclosed_quantity
101                    .is_some_and(|value| value > i32::MAX as u32)
102        }) {
103            return Err("multi order quantities cannot exceed the documented int32 range");
104        }
105        Ok(())
106    }
107}
108
109/// Response for one item in a multi-order response.
110#[derive(Debug, Clone, Deserialize)]
111#[serde(rename_all = "camelCase")]
112pub struct MultiOrderItemResponse {
113    pub order_id: Option<String>,
114    pub sequence: Option<String>,
115    pub order_status: Option<String>,
116}
117
118/// Response from placing a batch of orders.
119#[derive(Debug, Clone, Deserialize)]
120pub struct MultiOrderResponse {
121    #[serde(default)]
122    pub orders: Vec<MultiOrderItemResponse>,
123}
124
125// ---------------------------------------------------------------------------
126// Alert Condition
127// ---------------------------------------------------------------------------
128
129/// Condition configuration for a conditional trigger.
130#[derive(Debug, Clone, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase")]
132pub struct AlertCondition {
133    /// Type of comparison (e.g. `TECHNICAL_WITH_VALUE`).
134    pub comparison_type: String,
135    /// Exchange where condition is evaluated.
136    pub exchange_segment: ExchangeSegment,
137    /// Security ID of the instrument.
138    pub security_id: String,
139    /// Technical indicator name (e.g. `SMA_5`, `LTP`).
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub indicator_name: Option<String>,
142    /// Timeframe for indicator evaluation (`DAY`, `ONE_MIN`, `FIVE_MIN`, `FIFTEEN_MIN`).
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub time_frame: Option<String>,
145    /// Condition operator (e.g. `CROSSING_UP`, `GREATER_THAN`).
146    pub operator: String,
147    /// Value to compare indicator/price against.
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub comparing_value: Option<serde_json::Value>,
150    /// Second indicator name for indicator-vs-indicator comparisons.
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub comparing_indicator_name: Option<String>,
153    /// Alert expiry date (YYYY-MM-DD). Defaults to 1 year.
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub exp_date: Option<String>,
156    /// Trigger frequency (e.g. `ONCE`).
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub frequency: Option<String>,
159    /// User-provided note.
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub user_note: Option<String>,
162}
163
164// ---------------------------------------------------------------------------
165// Alert Order
166// ---------------------------------------------------------------------------
167
168/// Order to execute when the alert condition is met.
169#[derive(Debug, Clone, Serialize, Deserialize)]
170#[serde(rename_all = "camelCase")]
171pub struct AlertOrder {
172    pub transaction_type: TransactionType,
173    pub exchange_segment: ExchangeSegment,
174    pub product_type: ProductType,
175    pub order_type: OrderType,
176    pub security_id: String,
177    pub quantity: u64,
178    pub validity: Validity,
179    /// Price at which order is placed (as string in API).
180    pub price: String,
181    /// Disclosed quantity.
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub disc_quantity: Option<String>,
184    /// Trigger price for SL/SL-M.
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub trigger_price: Option<String>,
187}
188
189// ---------------------------------------------------------------------------
190// Place / Modify Conditional Trigger
191// ---------------------------------------------------------------------------
192
193/// Request body for placing or modifying a conditional trigger.
194///
195/// Used by `POST /v2/alerts/orders` and `PUT /v2/alerts/orders/{alertId}`.
196#[derive(Debug, Clone, Serialize)]
197#[serde(rename_all = "camelCase")]
198pub struct ConditionalTriggerRequest {
199    pub dhan_client_id: String,
200    /// Alert ID (only for modify requests).
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub alert_id: Option<String>,
203    pub condition: AlertCondition,
204    pub orders: Vec<AlertOrder>,
205}
206
207// ---------------------------------------------------------------------------
208// Conditional Trigger Response
209// ---------------------------------------------------------------------------
210
211/// Response from placing, modifying, or deleting a conditional trigger.
212#[derive(Debug, Clone, Deserialize)]
213#[serde(rename_all = "camelCase")]
214pub struct ConditionalTriggerResponse {
215    pub alert_id: String,
216    pub alert_status: String,
217}
218
219// ---------------------------------------------------------------------------
220// Conditional Trigger Detail
221// ---------------------------------------------------------------------------
222
223/// Full conditional trigger detail as returned by get endpoints.
224#[derive(Debug, Clone, Deserialize)]
225#[serde(rename_all = "camelCase")]
226pub struct ConditionalTriggerDetail {
227    pub alert_id: Option<String>,
228    pub alert_status: Option<String>,
229    pub created_time: Option<String>,
230    pub triggered_time: Option<String>,
231    #[serde(default)]
232    pub last_price: Option<serde_json::Value>,
233    pub condition: Option<AlertCondition>,
234    #[serde(default)]
235    pub orders: Vec<AlertOrder>,
236}