1use serde::{Deserialize, Serialize};
4
5use super::{
6 shared::{
7 deserialize_utils::{deserialize_f64_or_none, empty_string_or_null_as_none},
8 traits::{
9 builder::JQuantsBuilder,
10 pagination::{HasPaginationKey, MergePage, Paginatable},
11 },
12 types::{
13 emergency_margin_trigger_division::EmergencyMarginTriggerDivision,
14 put_call_division::PutCallDivision,
15 },
16 },
17 JQuantsApiClient, JQuantsPlanClient,
18};
19
20#[derive(Clone, Serialize)]
22pub struct IndexOptionPricesBuilder {
23 #[serde(skip)]
24 client: JQuantsApiClient,
25
26 date: String,
28
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pagination_key: Option<String>,
32}
33
34impl JQuantsBuilder<IndexOptionPricesResponse> for IndexOptionPricesBuilder {
35 async fn send(self) -> Result<IndexOptionPricesResponse, crate::JQuantsError> {
36 self.send_ref().await
37 }
38
39 async fn send_ref(&self) -> Result<IndexOptionPricesResponse, crate::JQuantsError> {
40 self.client.inner.get("option/index_option", self).await
41 }
42}
43
44impl Paginatable<IndexOptionPricesResponse> for IndexOptionPricesBuilder {
45 fn pagination_key(mut self, pagination_key: impl Into<String>) -> Self {
46 self.pagination_key = Some(pagination_key.into());
47 self
48 }
49}
50
51impl IndexOptionPricesBuilder {
52 pub(crate) fn new(client: JQuantsApiClient, date: String) -> Self {
54 Self {
55 client,
56 date,
57 pagination_key: None,
58 }
59 }
60
61 pub fn date(mut self, date: impl Into<String>) -> Self {
63 self.date = date.into();
64 self
65 }
66
67 pub fn pagination_key(mut self, pagination_key: impl Into<String>) -> Self {
69 self.pagination_key = Some(pagination_key.into());
70 self
71 }
72}
73
74pub trait IndexOptionPricesApi: JQuantsPlanClient {
76 fn get_index_option_prices(&self, date: impl Into<String>) -> IndexOptionPricesBuilder {
80 IndexOptionPricesBuilder::new(self.get_api_client().clone(), date.into())
81 }
82}
83
84#[derive(Debug, Clone, PartialEq, Deserialize)]
88pub struct IndexOptionPricesResponse {
89 pub index_option: Vec<IndexOptionPriceItem>,
91 pub pagination_key: Option<String>,
93}
94
95impl HasPaginationKey for IndexOptionPricesResponse {
96 fn get_pagination_key(&self) -> Option<&str> {
97 self.pagination_key.as_deref()
98 }
99}
100
101impl MergePage for IndexOptionPricesResponse {
102 fn merge_page(
103 page: Result<Vec<Self>, crate::JQuantsError>,
104 ) -> Result<Self, crate::JQuantsError> {
105 let mut page = page?;
106 let mut merged = page.pop().unwrap();
107 for p in page {
108 merged.index_option.extend(p.index_option);
109 }
110 merged.pagination_key = None;
111
112 Ok(merged)
113 }
114}
115
116#[derive(Debug, Clone, PartialEq, Deserialize)]
118pub struct IndexOptionPriceItem {
119 #[serde(rename = "Date")]
121 pub date: String,
122
123 #[serde(rename = "Code")]
125 pub code: String,
126
127 #[serde(rename = "WholeDayOpen")]
129 pub whole_day_open: f64,
130
131 #[serde(rename = "WholeDayHigh")]
133 pub whole_day_high: f64,
134
135 #[serde(rename = "WholeDayLow")]
137 pub whole_day_low: f64,
138
139 #[serde(rename = "WholeDayClose")]
141 pub whole_day_close: f64,
142
143 #[serde(
145 rename = "NightSessionOpen",
146 deserialize_with = "deserialize_f64_or_none"
147 )]
148 pub night_session_open: Option<f64>,
149
150 #[serde(
152 rename = "NightSessionHigh",
153 deserialize_with = "deserialize_f64_or_none"
154 )]
155 pub night_session_high: Option<f64>,
156
157 #[serde(
159 rename = "NightSessionLow",
160 deserialize_with = "deserialize_f64_or_none"
161 )]
162 pub night_session_low: Option<f64>,
163
164 #[serde(
166 rename = "NightSessionClose",
167 deserialize_with = "deserialize_f64_or_none"
168 )]
169 pub night_session_close: Option<f64>,
170
171 #[serde(rename = "DaySessionOpen")]
173 pub day_session_open: f64,
174
175 #[serde(rename = "DaySessionHigh")]
177 pub day_session_high: f64,
178
179 #[serde(rename = "DaySessionLow")]
181 pub day_session_low: f64,
182
183 #[serde(rename = "DaySessionClose")]
185 pub day_session_close: f64,
186
187 #[serde(rename = "Volume")]
189 pub volume: f64,
190
191 #[serde(rename = "OpenInterest")]
193 pub open_interest: f64,
194
195 #[serde(rename = "TurnoverValue")]
197 pub turnover_value: f64,
198
199 #[serde(rename = "ContractMonth")]
201 pub contract_month: String,
202
203 #[serde(rename = "StrikePrice")]
205 pub strike_price: f64,
206
207 #[serde(
209 rename = "Volume(OnlyAuction)",
210 deserialize_with = "deserialize_f64_or_none"
211 )]
212 pub volume_only_auction: Option<f64>,
213
214 #[serde(
216 rename = "EmergencyMarginTriggerDivision",
217 deserialize_with = "empty_string_or_null_as_none"
218 )]
219 pub emergency_margin_trigger_division: Option<EmergencyMarginTriggerDivision>,
220
221 #[serde(rename = "PutCallDivision")]
223 pub put_call_division: PutCallDivision,
224
225 #[serde(
227 rename = "LastTradingDay",
228 deserialize_with = "empty_string_or_null_as_none"
229 )]
230 pub last_trading_day: Option<String>,
231
232 #[serde(
234 rename = "SpecialQuotationDay",
235 deserialize_with = "empty_string_or_null_as_none"
236 )]
237 pub special_quotation_day: Option<String>,
238
239 #[serde(
241 rename = "SettlementPrice",
242 deserialize_with = "deserialize_f64_or_none"
243 )]
244 pub settlement_price: Option<f64>,
245
246 #[serde(
248 rename = "TheoreticalPrice",
249 deserialize_with = "deserialize_f64_or_none"
250 )]
251 pub theoretical_price: Option<f64>,
252
253 #[serde(
255 rename = "BaseVolatility",
256 deserialize_with = "deserialize_f64_or_none"
257 )]
258 pub base_volatility: Option<f64>,
259
260 #[serde(
262 rename = "UnderlyingPrice",
263 deserialize_with = "deserialize_f64_or_none"
264 )]
265 pub underlying_price: Option<f64>,
266
267 #[serde(
269 rename = "ImpliedVolatility",
270 deserialize_with = "deserialize_f64_or_none"
271 )]
272 pub implied_volatility: Option<f64>,
273
274 #[serde(rename = "InterestRate", deserialize_with = "deserialize_f64_or_none")]
276 pub interest_rate: Option<f64>,
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 #[test]
284 fn test_deserialize_index_option_prices_response() {
285 let json_data = r#"
286 {
287 "index_option": [
288 {
289 "Date": "2023-03-22",
290 "Code": "130060018",
291 "WholeDayOpen": 0.0,
292 "WholeDayHigh": 0.0,
293 "WholeDayLow": 0.0,
294 "WholeDayClose": 0.0,
295 "NightSessionOpen": 0.0,
296 "NightSessionHigh": 0.0,
297 "NightSessionLow": 0.0,
298 "NightSessionClose": 0.0,
299 "DaySessionOpen": 0.0,
300 "DaySessionHigh": 0.0,
301 "DaySessionLow": 0.0,
302 "DaySessionClose": 0.0,
303 "Volume": 0.0,
304 "OpenInterest": 330.0,
305 "TurnoverValue": 0.0,
306 "ContractMonth": "2025-06",
307 "StrikePrice": 20000.0,
308 "Volume(OnlyAuction)": 0.0,
309 "EmergencyMarginTriggerDivision": "002",
310 "PutCallDivision": "1",
311 "LastTradingDay": "2025-06-12",
312 "SpecialQuotationDay": "2025-06-13",
313 "SettlementPrice": 980.0,
314 "TheoreticalPrice": 974.641,
315 "BaseVolatility": 17.93025,
316 "UnderlyingPrice": 27466.61,
317 "ImpliedVolatility": 23.1816,
318 "InterestRate": 0.2336
319 }
320 ],
321 "pagination_key": "value1.value2."
322 }
323 "#;
324
325 let response: IndexOptionPricesResponse = serde_json::from_str(json_data).unwrap();
326
327 let expected_announcement = vec![IndexOptionPriceItem {
328 date: "2023-03-22".to_string(),
329 code: "130060018".to_string(),
330 whole_day_open: 0.0,
331 whole_day_high: 0.0,
332 whole_day_low: 0.0,
333 whole_day_close: 0.0,
334 night_session_open: Some(0.0),
335 night_session_high: Some(0.0),
336 night_session_low: Some(0.0),
337 night_session_close: Some(0.0),
338 day_session_open: 0.0,
339 day_session_high: 0.0,
340 day_session_low: 0.0,
341 day_session_close: 0.0,
342 volume: 0.0,
343 open_interest: 330.0,
344 turnover_value: 0.0,
345 contract_month: "2025-06".to_string(),
346 strike_price: 20000.0,
347 volume_only_auction: Some(0.0),
348 emergency_margin_trigger_division: Some(EmergencyMarginTriggerDivision::Calculated),
349 put_call_division: PutCallDivision::Put,
350 last_trading_day: Some("2025-06-12".to_string()),
351 special_quotation_day: Some("2025-06-13".to_string()),
352 settlement_price: Some(980.0),
353 theoretical_price: Some(974.641),
354 base_volatility: Some(17.93025),
355 underlying_price: Some(27466.61),
356 implied_volatility: Some(23.1816),
357 interest_rate: Some(0.2336),
358 }];
359
360 let expected_response = IndexOptionPricesResponse {
361 index_option: expected_announcement,
362 pagination_key: Some("value1.value2.".to_string()),
363 };
364
365 pretty_assertions::assert_eq!(response, expected_response);
366 }
367
368 #[test]
369 fn test_deserialize_index_option_prices_response_with_missing_optional_fields() {
370 let json_data = r#"
371 {
372 "index_option": [
373 {
374 "Date": "2023-03-22",
375 "Code": "130060018",
376 "WholeDayOpen": 0.0,
377 "WholeDayHigh": 0.0,
378 "WholeDayLow": 0.0,
379 "WholeDayClose": 0.0,
380 "NightSessionOpen": "",
381 "NightSessionHigh": "",
382 "NightSessionLow": "",
383 "NightSessionClose": "",
384 "DaySessionOpen": 0.0,
385 "DaySessionHigh": 0.0,
386 "DaySessionLow": 0.0,
387 "DaySessionClose": 0.0,
388 "Volume": 0.0,
389 "OpenInterest": 0.0,
390 "TurnoverValue": 0.0,
391 "ContractMonth": "2025-06",
392 "StrikePrice": 0.0,
393 "Volume(OnlyAuction)": "",
394 "EmergencyMarginTriggerDivision": "",
395 "PutCallDivision": "1",
396 "LastTradingDay": "",
397 "SpecialQuotationDay": "",
398 "SettlementPrice": "",
399 "TheoreticalPrice": "",
400 "BaseVolatility": "",
401 "UnderlyingPrice": "",
402 "ImpliedVolatility": "",
403 "InterestRate": ""
404 }
405 ],
406 "pagination_key": "value1.value2."
407 }
408 "#;
409
410 let response: IndexOptionPricesResponse = serde_json::from_str(json_data).unwrap();
411
412 let expected_announcement = vec![IndexOptionPriceItem {
413 date: "2023-03-22".to_string(),
414 code: "130060018".to_string(),
415 whole_day_open: 0.0,
416 whole_day_high: 0.0,
417 whole_day_low: 0.0,
418 whole_day_close: 0.0,
419 night_session_open: None,
420 night_session_high: None,
421 night_session_low: None,
422 night_session_close: None,
423 day_session_open: 0.0,
424 day_session_high: 0.0,
425 day_session_low: 0.0,
426 day_session_close: 0.0,
427 volume: 0.0,
428 open_interest: 0.0,
429 turnover_value: 0.0,
430 contract_month: "2025-06".to_string(),
431 strike_price: 0.0,
432 volume_only_auction: None,
433 emergency_margin_trigger_division: None,
434 put_call_division: PutCallDivision::Put,
435 last_trading_day: None,
436 special_quotation_day: None,
437 settlement_price: None,
438 theoretical_price: None,
439 base_volatility: None,
440 underlying_price: None,
441 implied_volatility: None,
442 interest_rate: None,
443 }];
444
445 let expected_response = IndexOptionPricesResponse {
446 index_option: expected_announcement,
447 pagination_key: Some("value1.value2.".to_string()),
448 };
449
450 pretty_assertions::assert_eq!(response, expected_response);
451 }
452
453 #[test]
454 fn test_deserialize_index_option_prices_response_multiple_items() {
455 let json_data = r#"
456 {
457 "index_option": [
458 {
459 "Date": "2023-03-22",
460 "Code": "130060018",
461 "WholeDayOpen": 1000.0,
462 "WholeDayHigh": 1050.0,
463 "WholeDayLow": 990.0,
464 "WholeDayClose": 1025.0,
465 "NightSessionOpen": 1010.0,
466 "NightSessionHigh": 1040.0,
467 "NightSessionLow": 995.0,
468 "NightSessionClose": 1030.0,
469 "DaySessionOpen": 1025.0,
470 "DaySessionHigh": 1060.0,
471 "DaySessionLow": 1000.0,
472 "DaySessionClose": 1045.0,
473 "Volume": 1500.0,
474 "OpenInterest": 330.0,
475 "TurnoverValue": 1500000.0,
476 "ContractMonth": "2025-06",
477 "StrikePrice": 20000.0,
478 "Volume(OnlyAuction)": 500.0,
479 "EmergencyMarginTriggerDivision": "002",
480 "PutCallDivision": "1",
481 "LastTradingDay": "2025-06-12",
482 "SpecialQuotationDay": "2025-06-13",
483 "SettlementPrice": 980.0,
484 "TheoreticalPrice": 974.641,
485 "BaseVolatility": 17.93025,
486 "UnderlyingPrice": 27466.61,
487 "ImpliedVolatility": 23.1816,
488 "InterestRate": 0.2336
489 },
490 {
491 "Date": "2023-03-22",
492 "Code": "130060019",
493 "WholeDayOpen": 2000.0,
494 "WholeDayHigh": 2050.0,
495 "WholeDayLow": 1990.0,
496 "WholeDayClose": 2025.0,
497 "NightSessionOpen": 2010.0,
498 "NightSessionHigh": 2040.0,
499 "NightSessionLow": 1995.0,
500 "NightSessionClose": 2030.0,
501 "DaySessionOpen": 2025.0,
502 "DaySessionHigh": 2060.0,
503 "DaySessionLow": 2000.0,
504 "DaySessionClose": 2045.0,
505 "Volume": 2500.0,
506 "OpenInterest": 430.0,
507 "TurnoverValue": 2500000.0,
508 "ContractMonth": "2025-07",
509 "StrikePrice": 21000.0,
510 "Volume(OnlyAuction)": 600.0,
511 "EmergencyMarginTriggerDivision": "001",
512 "PutCallDivision": "2",
513 "LastTradingDay": "2025-07-12",
514 "SpecialQuotationDay": "2025-07-13",
515 "SettlementPrice": 1980.0,
516 "TheoreticalPrice": 1974.641,
517 "BaseVolatility": 18.93025,
518 "UnderlyingPrice": 27566.61,
519 "ImpliedVolatility": 24.1816,
520 "InterestRate": 0.2436
521 }
522 ],
523 "pagination_key": "value3.value4."
524 }
525 "#;
526
527 let response: IndexOptionPricesResponse = serde_json::from_str(json_data).unwrap();
528
529 let expected_announcement = vec![
530 IndexOptionPriceItem {
531 date: "2023-03-22".to_string(),
532 code: "130060018".to_string(),
533 whole_day_open: 1000.0,
534 whole_day_high: 1050.0,
535 whole_day_low: 990.0,
536 whole_day_close: 1025.0,
537 night_session_open: Some(1010.0),
538 night_session_high: Some(1040.0),
539 night_session_low: Some(995.0),
540 night_session_close: Some(1030.0),
541 day_session_open: 1025.0,
542 day_session_high: 1060.0,
543 day_session_low: 1000.0,
544 day_session_close: 1045.0,
545 volume: 1500.0,
546 open_interest: 330.0,
547 turnover_value: 1500000.0,
548 contract_month: "2025-06".to_string(),
549 strike_price: 20000.0,
550 volume_only_auction: Some(500.0),
551 emergency_margin_trigger_division: Some(EmergencyMarginTriggerDivision::Calculated),
552 put_call_division: PutCallDivision::Put,
553 last_trading_day: Some("2025-06-12".to_string()),
554 special_quotation_day: Some("2025-06-13".to_string()),
555 settlement_price: Some(980.0),
556 theoretical_price: Some(974.641),
557 base_volatility: Some(17.93025),
558 underlying_price: Some(27466.61),
559 implied_volatility: Some(23.1816),
560 interest_rate: Some(0.2336),
561 },
562 IndexOptionPriceItem {
563 date: "2023-03-22".to_string(),
564 code: "130060019".to_string(),
565 whole_day_open: 2000.0,
566 whole_day_high: 2050.0,
567 whole_day_low: 1990.0,
568 whole_day_close: 2025.0,
569 night_session_open: Some(2010.0),
570 night_session_high: Some(2040.0),
571 night_session_low: Some(1995.0),
572 night_session_close: Some(2030.0),
573 day_session_open: 2025.0,
574 day_session_high: 2060.0,
575 day_session_low: 2000.0,
576 day_session_close: 2045.0,
577 volume: 2500.0,
578 open_interest: 430.0,
579 turnover_value: 2500000.0,
580 contract_month: "2025-07".to_string(),
581 strike_price: 21000.0,
582 volume_only_auction: Some(600.0),
583 emergency_margin_trigger_division: Some(EmergencyMarginTriggerDivision::Triggered),
584 put_call_division: PutCallDivision::Call,
585 last_trading_day: Some("2025-07-12".to_string()),
586 special_quotation_day: Some("2025-07-13".to_string()),
587 settlement_price: Some(1980.0),
588 theoretical_price: Some(1974.641),
589 base_volatility: Some(18.93025),
590 underlying_price: Some(27566.61),
591 implied_volatility: Some(24.1816),
592 interest_rate: Some(0.2436),
593 },
594 ];
595
596 let expected_response = IndexOptionPricesResponse {
597 index_option: expected_announcement,
598 pagination_key: Some("value3.value4.".to_string()),
599 };
600
601 pretty_assertions::assert_eq!(response, expected_response);
602 }
603
604 #[test]
605 fn test_deserialize_index_option_prices_response_no_pagination_key() {
606 let json_data = r#"
607 {
608 "index_option": [
609 {
610 "Date": "2023-03-22",
611 "Code": "130060018",
612 "WholeDayOpen": 0.0,
613 "WholeDayHigh": 0.0,
614 "WholeDayLow": 0.0,
615 "WholeDayClose": 0.0,
616 "NightSessionOpen": 0.0,
617 "NightSessionHigh": 0.0,
618 "NightSessionLow": 0.0,
619 "NightSessionClose": 0.0,
620 "DaySessionOpen": 0.0,
621 "DaySessionHigh": 0.0,
622 "DaySessionLow": 0.0,
623 "DaySessionClose": 0.0,
624 "Volume": 0.0,
625 "OpenInterest": 330.0,
626 "TurnoverValue": 0.0,
627 "ContractMonth": "2025-06",
628 "StrikePrice": 20000.0,
629 "Volume(OnlyAuction)": 0.0,
630 "EmergencyMarginTriggerDivision": "003",
631 "PutCallDivision": "1",
632 "LastTradingDay": "2025-06-12",
633 "SpecialQuotationDay": "2025-06-13",
634 "SettlementPrice": 980.0,
635 "TheoreticalPrice": 974.641,
636 "BaseVolatility": 17.93025,
637 "UnderlyingPrice": 27466.61,
638 "ImpliedVolatility": 23.1816,
639 "InterestRate": 0.2336
640 }
641 ]
642 }
643 "#;
644
645 let response: IndexOptionPricesResponse = serde_json::from_str(json_data).unwrap();
646
647 let expected_announcement = vec![IndexOptionPriceItem {
648 date: "2023-03-22".to_string(),
649 code: "130060018".to_string(),
650 whole_day_open: 0.0,
651 whole_day_high: 0.0,
652 whole_day_low: 0.0,
653 whole_day_close: 0.0,
654 night_session_open: Some(0.0),
655 night_session_high: Some(0.0),
656 night_session_low: Some(0.0),
657 night_session_close: Some(0.0),
658 day_session_open: 0.0,
659 day_session_high: 0.0,
660 day_session_low: 0.0,
661 day_session_close: 0.0,
662 volume: 0.0,
663 open_interest: 330.0,
664 turnover_value: 0.0,
665 contract_month: "2025-06".to_string(),
666 strike_price: 20000.0,
667 volume_only_auction: Some(0.0),
668 emergency_margin_trigger_division: Some(EmergencyMarginTriggerDivision::Unknown(
669 "003".to_string(),
670 )),
671 put_call_division: PutCallDivision::Put,
672 last_trading_day: Some("2025-06-12".to_string()),
673 special_quotation_day: Some("2025-06-13".to_string()),
674 settlement_price: Some(980.0),
675 theoretical_price: Some(974.641),
676 base_volatility: Some(17.93025),
677 underlying_price: Some(27466.61),
678 implied_volatility: Some(23.1816),
679 interest_rate: Some(0.2336),
680 }];
681
682 let expected_response = IndexOptionPricesResponse {
683 index_option: expected_announcement,
684 pagination_key: None,
685 };
686
687 pretty_assertions::assert_eq!(response, expected_response);
688 }
689
690 #[test]
691 fn test_deserialize_index_option_prices_response_no_data() {
692 let json_data = r#"
693 {
694 "index_option": []
695 }
696 "#;
697
698 let response: IndexOptionPricesResponse = serde_json::from_str(json_data).unwrap();
699 let expected_response = IndexOptionPricesResponse {
700 index_option: vec![],
701 pagination_key: None,
702 };
703
704 pretty_assertions::assert_eq!(response, expected_response);
705 }
706}