1#![allow(missing_docs)]
2use std::collections::HashMap;
6
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8
9macro_rules! string_wire_enum {
10 ($name:ident { $($variant:ident => $wire:literal,)+ }) => {
11 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
12 pub enum $name {
13 $($variant,)+
14 Other(String),
16 }
17
18 impl $name {
19 pub fn as_str(&self) -> &str {
20 match self {
21 $(Self::$variant => $wire,)+
22 Self::Other(value) => value,
23 }
24 }
25
26 fn from_wire(value: String) -> Self {
27 match value.as_str() {
28 $($wire => Self::$variant,)+
29 _ => Self::Other(value),
30 }
31 }
32 }
33
34 impl Serialize for $name {
35 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
36 where
37 S: Serializer,
38 {
39 serializer.serialize_str(self.as_str())
40 }
41 }
42
43 impl<'de> Deserialize<'de> for $name {
44 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
45 where
46 D: Deserializer<'de>,
47 {
48 String::deserialize(deserializer).map(Self::from_wire)
49 }
50 }
51 };
52}
53
54fn null_to_default<'de, D, T>(deserializer: D) -> std::result::Result<T, D::Error>
55where
56 D: Deserializer<'de>,
57 T: Deserialize<'de> + Default,
58{
59 Option::<T>::deserialize(deserializer).map(Option::unwrap_or_default)
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
63pub enum RollingExchangeSegment {
64 #[serde(rename = "NSE_EQ")]
65 NseEq,
66 #[serde(rename = "NSE_FNO")]
67 NseFno,
68 #[serde(rename = "BSE_EQ")]
69 BseEq,
70 #[serde(rename = "BSE_FNO")]
71 BseFno,
72 #[serde(rename = "MCX_COMM")]
73 McxComm,
74 #[serde(rename = "IDX_I")]
75 Index,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
79pub enum RollingInterval {
80 #[serde(rename = "1")]
81 OneMinute,
82 #[serde(rename = "5")]
83 FiveMinutes,
84 #[serde(rename = "15")]
85 FifteenMinutes,
86 #[serde(rename = "25")]
87 TwentyFiveMinutes,
88 #[serde(rename = "60")]
89 SixtyMinutes,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
93#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
94pub enum RollingInstrument {
95 Index,
96 Futidx,
97 Optidx,
98 Equity,
99 Futstk,
100 Optstk,
101 Futcom,
102 Optfut,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
106#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
107pub enum RollingExpiryFlag {
108 Month,
109 Week,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114#[repr(u8)]
115pub enum RollingExpiryCode {
116 First = 1,
117 Second = 2,
118 Third = 3,
119}
120
121impl Serialize for RollingExpiryCode {
122 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
123 where
124 S: Serializer,
125 {
126 serializer.serialize_u8(*self as u8)
127 }
128}
129
130impl<'de> Deserialize<'de> for RollingExpiryCode {
131 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
132 where
133 D: Deserializer<'de>,
134 {
135 match u8::deserialize(deserializer)? {
136 1 => Ok(Self::First),
137 2 => Ok(Self::Second),
138 3 => Ok(Self::Third),
139 value => Err(serde::de::Error::custom(format!(
140 "invalid rolling expiry code {value}; expected 1, 2, or 3"
141 ))),
142 }
143 }
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
147#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
148pub enum RollingOptionType {
149 Call,
150 Put,
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
154#[serde(rename_all = "lowercase")]
155pub enum RollingDataField {
156 Open,
157 High,
158 Low,
159 Close,
160 Iv,
161 Volume,
162 Strike,
163 Oi,
164 Spot,
165}
166
167#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
169#[serde(rename_all = "camelCase")]
170pub struct RollingOptionRequest {
171 pub exchange_segment: RollingExchangeSegment,
172 pub interval: RollingInterval,
173 pub security_id: u64,
174 pub instrument: RollingInstrument,
175 pub expiry_flag: RollingExpiryFlag,
176 pub expiry_code: RollingExpiryCode,
177 pub strike: String,
179 pub drv_option_type: RollingOptionType,
180 pub required_data: Vec<RollingDataField>,
181 pub from_date: String,
183 pub to_date: String,
185}
186
187impl RollingOptionRequest {
188 pub(crate) fn validate(&self) -> std::result::Result<(), &'static str> {
189 if self.required_data.is_empty() {
190 return Err("rolling option required_data cannot be empty");
191 }
192 if self.strike.trim().is_empty()
193 || self.from_date.trim().is_empty()
194 || self.to_date.trim().is_empty()
195 {
196 return Err("rolling option strike and date fields cannot be empty");
197 }
198 Ok(())
199 }
200}
201
202#[derive(Debug, Clone, PartialEq, Deserialize)]
204pub struct RollingOptionSeries {
205 #[serde(default, deserialize_with = "null_to_default")]
206 pub iv: Vec<f64>,
207 #[serde(default, deserialize_with = "null_to_default")]
208 pub oi: Vec<i64>,
209 #[serde(default, deserialize_with = "null_to_default")]
210 pub strike: Vec<f64>,
211 #[serde(default, deserialize_with = "null_to_default")]
212 pub spot: Vec<f64>,
213 #[serde(default, deserialize_with = "null_to_default")]
214 pub open: Vec<f64>,
215 #[serde(default, deserialize_with = "null_to_default")]
216 pub high: Vec<f64>,
217 #[serde(default, deserialize_with = "null_to_default")]
218 pub low: Vec<f64>,
219 #[serde(default, deserialize_with = "null_to_default")]
220 pub close: Vec<f64>,
221 #[serde(default, deserialize_with = "null_to_default")]
222 pub volume: Vec<i64>,
223 #[serde(default, deserialize_with = "null_to_default")]
224 pub timestamp: Vec<i64>,
225}
226
227#[derive(Debug, Clone, PartialEq, Deserialize)]
228pub struct RollingOptionData {
229 #[serde(default)]
230 pub ce: Option<RollingOptionSeries>,
231 #[serde(default)]
232 pub pe: Option<RollingOptionSeries>,
233}
234
235#[derive(Debug, Clone, PartialEq, Deserialize)]
236pub struct RollingOptionResponse {
237 pub data: RollingOptionData,
238 #[serde(default)]
239 pub status: Option<String>,
240}
241
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
243pub enum TechnicalExchangeSegment {
244 #[serde(rename = "NSE_EQ")]
245 NseEq,
246 #[serde(rename = "IDX_I")]
247 Index,
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
251#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
252pub enum TechnicalInstrument {
253 Index,
254 Equity,
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
258pub enum TechnicalTimeframe {
259 #[serde(rename = "1")]
260 OneMinute,
261 #[serde(rename = "5")]
262 FiveMinutes,
263 #[serde(rename = "15")]
264 FifteenMinutes,
265 #[serde(rename = "D")]
266 Daily,
267}
268
269string_wire_enum!(TechnicalIndicator {
270 Sma5 => "SMA_5",
271 Sma10 => "SMA_10",
272 Sma20 => "SMA_20",
273 Sma50 => "SMA_50",
274 Sma100 => "SMA_100",
275 Sma200 => "SMA_200",
276 Ema5 => "EMA_5",
277 Ema10 => "EMA_10",
278 Ema20 => "EMA_20",
279 Ema50 => "EMA_50",
280 Ema100 => "EMA_100",
281 Ema200 => "EMA_200",
282 Rsi14 => "RSI_14",
283 MacdHist => "MACD_HIST",
284 Stoch => "STOCH",
285 StochRsi14 => "STOCHRSI_14",
286 Atr14 => "ATR_14",
287 Adx14 => "ADX_14",
288 UltimateOscillator => "UO",
289 RateOfChange => "ROC",
290 WilliamsR => "WILLR",
291 PivotClassic => "PIVOT_CLASSIC",
292 PivotFibonacci => "PIVOT_FIBONACCI",
293 PivotCamarilla => "PIVOT_CAMARILLA",
294});
295
296#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
298#[serde(rename_all = "camelCase")]
299pub struct TechnicalMetricsRequest {
300 pub security_id: String,
301 pub exchange_segment: TechnicalExchangeSegment,
302 pub instrument: TechnicalInstrument,
303 pub timeframe: TechnicalTimeframe,
304 pub indicators: Vec<TechnicalIndicator>,
305}
306
307impl TechnicalMetricsRequest {
308 pub(crate) fn validate(&self) -> std::result::Result<(), &'static str> {
309 if self.security_id.trim().is_empty() {
310 return Err("technical metrics security_id cannot be empty");
311 }
312 if self.indicators.is_empty()
313 || self
314 .indicators
315 .iter()
316 .any(|value| value.as_str().trim().is_empty())
317 {
318 return Err("technical metrics indicators must contain non-empty values");
319 }
320 Ok(())
321 }
322}
323
324#[derive(Debug, Clone, PartialEq, Deserialize)]
327#[serde(rename_all = "camelCase")]
328pub struct TechnicalMetricsResponse {
329 pub security_id: String,
330 pub timeframe: String,
331 #[serde(default)]
332 pub data: HashMap<String, serde_json::Value>,
333}
334
335#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
336pub enum MarketMoverExchangeSegment {
337 #[serde(rename = "NSE_FNO")]
338 NseFno,
339 #[serde(rename = "BSE_FNO")]
340 BseFno,
341 #[serde(rename = "NSE_COMM")]
342 NseComm,
343 #[serde(rename = "MCX_COMM")]
344 McxComm,
345 #[serde(rename = "NSE_EQ")]
346 NseEq,
347 #[serde(rename = "BSE_EQ")]
348 BseEq,
349}
350
351#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
352#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
353pub enum MarketMoverInstrument {
354 Optidx,
355 Optstk,
356 Optfut,
357 Futidx,
358 Futstk,
359 Futcom,
360 Equity,
361}
362
363#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
364#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
365pub enum MarketMoverCategory {
366 HighestOi,
367 OiGainers,
368 OiLosers,
369 TopVolume,
370 PriceGainers,
371 PriceLosers,
372}
373
374string_wire_enum!(MarketMoverUniverse {
375 All => "ALL",
376 FnoStocks => "FNO_STOCKS",
377 Nifty50 => "NIFTY_50",
378 NiftyBank => "NIFTY_BANK",
379 Finnifty => "FINNIFTY",
380 IndiaVix => "INDIA_VIX",
381 NiftyMidcap => "NIFTY_MIDCAP",
382 NiftyNext50 => "NIFTY_NEXT_50",
383 NiftySmallcap50 => "NIFTY_SMALLCAP_50",
384 NiftyMidCap50 => "NIFTY_MID_CAP_50",
385 Nifty100 => "NIFTY_100",
386 Nifty200 => "NIFTY_200",
387 Nifty500 => "NIFTY_500",
388 NiftyMidcap100 => "NIFTY_MIDCAP_100",
389 NiftyMidcap150 => "NIFTY_MIDCAP_150",
390 NiftySmallcap100 => "NIFTY_SMALLCAP_100",
391 NiftySmallcap250 => "NIFTY_SMALLCAP_250",
392 NiftyMicrocap250 => "NIFTY_MICROCAP_250",
393 NiftyAuto => "NIFTY_AUTO",
394 NiftyPrivateBank => "NIFTY_PRIVATE_BANK",
395 NiftyFmcg => "NIFTY_FMCG",
396 NiftyEnergy => "NIFTY_ENERGY",
397 NiftyInfra => "NIFTY_INFRA",
398 NiftyIt => "NIFTY_IT",
399 NiftyMedia => "NIFTY_MEDIA",
400 NiftyMetal => "NIFTY_METAL",
401 NiftyMnc => "NIFTY_MNC",
402 NiftyPharma => "NIFTY_PHARMA",
403 NiftyPsuBank => "NIFTY_PSU_BANK",
404 NiftyRealty => "NIFTY_REALTY",
405 NiftyServiceSector => "NIFTY_SERVICE_SECTOR",
406 NiftyConsumption => "NIFTY_CUNSUMPTION",
407 GiftNifty => "GIFT_NIFTY",
408 Sensex => "SENSEX",
409 Bse100 => "BSE_100",
410 Bse200 => "BSE_200",
411 Bse500 => "BSE_500",
412 Bse150Midcap => "BSE_150_MIDCAP",
413 Bse250Smallcap => "BSE_250_SMALLCAP",
414 Bse250LargeMid => "BSE_250_LARGE_MID",
415 Bse400MidSmall => "BSE_400_MID_SMALL",
416 BseBankex => "BSE_BANKEX",
417 BseAuto => "BSE_AUTO",
418 BseCapitalGoods => "BSE_CAPITAL_GOODS",
419 BseConsumerDurables => "BSE_CONSUMER_DURABLES",
420 BseEnergy => "BSE_ENERGY",
421 BseFinance => "BSE_FINANCE",
422 BseFmcg => "BSE_FMCG",
423 BseHealthcare => "BSE_HEALTHCARE",
424 BseIndiaMfg => "BSE_INDIA_MFG",
425 BseIndustrials => "BSE_INDUSTRIALS",
426 BseIpo => "BSE_IPO",
427 BseIt => "BSE_IT",
428 BseMetals => "BSE_METALS",
429 BseOilAndGas => "BSE_OIL_AND_GAS",
430 BsePower => "BSE_POWER",
431 BsePsu => "BSE_PSU",
432 BseTelecom => "BSE_TELECOM",
433});
434
435#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
437#[serde(rename_all = "camelCase")]
438pub struct MarketMoversRequest {
439 pub exchange_segment: MarketMoverExchangeSegment,
440 pub instrument: Vec<MarketMoverInstrument>,
441 pub category: MarketMoverCategory,
442 #[serde(skip_serializing_if = "Option::is_none")]
443 pub expiry: Option<String>,
444 #[serde(skip_serializing_if = "Option::is_none")]
445 pub universe: Option<MarketMoverUniverse>,
446 pub limit: u8,
448}
449
450impl MarketMoversRequest {
451 pub(crate) fn validate(&self) -> std::result::Result<(), &'static str> {
452 if self.instrument.is_empty() {
453 return Err("market movers instrument list cannot be empty");
454 }
455 if !(1..=100).contains(&self.limit) {
456 return Err("market movers limit must be between 1 and 100");
457 }
458 let group = self.instrument[0].group();
459 if self.instrument.iter().any(|value| value.group() != group) {
460 return Err("market movers instrument values must belong to one instrument group");
461 }
462 match group {
463 MarketMoverInstrumentGroup::Equity if self.universe.is_none() => {
464 return Err("market movers equity requests require universe");
465 }
466 MarketMoverInstrumentGroup::Options | MarketMoverInstrumentGroup::Futures
467 if self
468 .expiry
469 .as_deref()
470 .is_none_or(|value| value.trim().is_empty()) =>
471 {
472 return Err("market movers derivative requests require expiry");
473 }
474 _ => {}
475 }
476 Ok(())
477 }
478}
479
480#[derive(Debug, Clone, Copy, PartialEq, Eq)]
481enum MarketMoverInstrumentGroup {
482 Options,
483 Futures,
484 Equity,
485}
486
487impl MarketMoverInstrument {
488 fn group(self) -> MarketMoverInstrumentGroup {
489 match self {
490 Self::Optidx | Self::Optstk | Self::Optfut => MarketMoverInstrumentGroup::Options,
491 Self::Futidx | Self::Futstk | Self::Futcom => MarketMoverInstrumentGroup::Futures,
492 Self::Equity => MarketMoverInstrumentGroup::Equity,
493 }
494 }
495}
496
497#[derive(Debug, Clone, PartialEq, Deserialize)]
498#[serde(rename_all = "camelCase")]
499pub struct MarketMoverInstrumentDetail {
500 #[serde(default)]
501 pub security_id: Option<String>,
502 #[serde(default)]
503 pub exchange_segment: Option<String>,
504 #[serde(default)]
505 pub trading_symbol: Option<String>,
506 #[serde(default)]
507 pub display_name: Option<String>,
508 #[serde(default)]
509 pub instrument: Option<String>,
510 #[serde(default)]
511 pub expiry: Option<String>,
512 #[serde(default)]
513 pub strike_price: Option<f64>,
514 #[serde(default)]
515 pub tick_size: Option<f64>,
516 #[serde(default)]
517 pub lot_size: Option<i32>,
518 #[serde(default)]
519 pub ltp: Option<f64>,
520 #[serde(default)]
521 pub change: Option<f64>,
522 #[serde(default)]
523 pub change_percent: Option<f64>,
524 #[serde(default)]
525 pub volume: Option<i64>,
526 #[serde(default)]
527 pub traded_value: Option<f64>,
528 #[serde(default)]
529 pub underlying_security_id: Option<String>,
530 #[serde(default)]
531 pub underlying_ltp: Option<f64>,
532 #[serde(default)]
533 pub premium_discount: Option<f64>,
534 #[serde(default)]
535 pub premium_discount_percent: Option<f64>,
536 #[serde(default)]
537 pub open_interest: Option<i64>,
538 #[serde(default)]
539 pub open_interest_change: Option<i64>,
540 #[serde(default)]
541 pub open_interest_change_percent: Option<f64>,
542 #[serde(default)]
543 pub put_call_ratio: Option<f64>,
544}
545
546#[derive(Debug, Clone, PartialEq, Deserialize)]
547#[serde(rename_all = "camelCase")]
548pub struct MarketMoversResponse {
549 pub exchange_segment: String,
550 pub category: String,
551 #[serde(default)]
552 pub data: Vec<MarketMoverInstrumentDetail>,
553}
554
555#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
556pub enum FundamentalExchangeSegment {
557 #[serde(rename = "NSE_EQ")]
558 NseEq,
559 #[serde(rename = "BSE_EQ")]
560 BseEq,
561}
562
563#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
564#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
565pub enum FundamentalMetricSection {
566 Co,
567 Ratios,
568 Shp,
569}
570
571#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
572#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
573pub enum CompanyInstrument {
574 Equity,
575}
576
577#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
579#[serde(rename_all = "camelCase")]
580pub struct CompanyInfoRequest {
581 pub security_id: String,
582 pub exchange_segment: FundamentalExchangeSegment,
583 pub instrument: CompanyInstrument,
584 pub metrics: Vec<FundamentalMetricSection>,
585}
586
587impl CompanyInfoRequest {
588 pub(crate) fn validate(&self) -> std::result::Result<(), &'static str> {
589 if self.security_id.trim().is_empty() {
590 return Err("company info security_id cannot be empty");
591 }
592 if self.metrics.is_empty() {
593 return Err("company info metrics cannot be empty");
594 }
595 Ok(())
596 }
597}
598
599#[derive(Debug, Clone, PartialEq, Deserialize)]
602#[serde(rename_all = "camelCase")]
603pub struct CompanyInfoResponse {
604 pub security_id: String,
605 #[serde(default)]
606 pub data: HashMap<String, serde_json::Value>,
607}
608
609#[cfg(test)]
610mod tests {
611 use super::*;
612
613 #[test]
614 fn rolling_option_request_matches_documented_wire_names() {
615 let request = RollingOptionRequest {
616 exchange_segment: RollingExchangeSegment::NseFno,
617 interval: RollingInterval::OneMinute,
618 security_id: 13,
619 instrument: RollingInstrument::Optidx,
620 expiry_flag: RollingExpiryFlag::Month,
621 expiry_code: RollingExpiryCode::First,
622 strike: "ATM".into(),
623 drv_option_type: RollingOptionType::Call,
624 required_data: vec![RollingDataField::Open, RollingDataField::Iv],
625 from_date: "2026-01-01".into(),
626 to_date: "2026-01-31".into(),
627 };
628
629 let value = serde_json::to_value(request).unwrap();
630 assert_eq!(value["exchangeSegment"], "NSE_FNO");
631 assert_eq!(value["interval"], "1");
632 assert_eq!(value["instrument"], "OPTIDX");
633 assert_eq!(value["expiryFlag"], "MONTH");
634 assert_eq!(value["drvOptionType"], "CALL");
635 assert_eq!(value["requiredData"], serde_json::json!(["open", "iv"]));
636 }
637
638 #[test]
639 fn rolling_series_tolerates_omitted_and_null_arrays() {
640 let response: RollingOptionResponse = serde_json::from_value(serde_json::json!({
641 "data": {
642 "ce": { "open": [1.0], "iv": null },
643 "pe": null
644 }
645 }))
646 .unwrap();
647
648 let ce = response.data.ce.unwrap();
649 assert_eq!(ce.open, vec![1.0]);
650 assert!(ce.iv.is_empty());
651 assert!(ce.close.is_empty());
652 assert!(response.data.pe.is_none());
653 }
654
655 #[test]
656 fn data_requests_serialize_exact_enums() {
657 let technical = TechnicalMetricsRequest {
658 security_id: "1333".into(),
659 exchange_segment: TechnicalExchangeSegment::NseEq,
660 instrument: TechnicalInstrument::Equity,
661 timeframe: TechnicalTimeframe::Daily,
662 indicators: vec![TechnicalIndicator::Rsi14],
663 };
664 assert_eq!(
665 serde_json::to_value(technical).unwrap(),
666 serde_json::json!({
667 "securityId": "1333",
668 "exchangeSegment": "NSE_EQ",
669 "instrument": "EQUITY",
670 "timeframe": "D",
671 "indicators": ["RSI_14"]
672 })
673 );
674
675 let movers = MarketMoversRequest {
676 exchange_segment: MarketMoverExchangeSegment::NseFno,
677 instrument: vec![MarketMoverInstrument::Optidx],
678 category: MarketMoverCategory::HighestOi,
679 expiry: Some("2026-08-27".into()),
680 universe: Some(MarketMoverUniverse::Nifty50),
681 limit: 20,
682 };
683 let movers = serde_json::to_value(movers).unwrap();
684 assert_eq!(movers["category"], "HIGHEST_OI");
685 assert_eq!(movers["instrument"], serde_json::json!(["OPTIDX"]));
686 assert_eq!(movers["expiry"], "2026-08-27");
687 }
688
689 #[test]
690 fn request_validation_rejects_documented_limit_violations() {
691 let invalid = MarketMoversRequest {
692 exchange_segment: MarketMoverExchangeSegment::NseEq,
693 instrument: vec![MarketMoverInstrument::Equity],
694 category: MarketMoverCategory::TopVolume,
695 expiry: None,
696 universe: Some(MarketMoverUniverse::All),
697 limit: 0,
698 };
699 assert!(invalid.validate().is_err());
700
701 let invalid = TechnicalMetricsRequest {
702 security_id: "1333".into(),
703 exchange_segment: TechnicalExchangeSegment::NseEq,
704 instrument: TechnicalInstrument::Equity,
705 timeframe: TechnicalTimeframe::Daily,
706 indicators: Vec::new(),
707 };
708 assert!(invalid.validate().is_err());
709 }
710}