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 central_contract_month_flag::CentralContractMonthFlag,
14 emergency_margin_trigger_division::EmergencyMarginTriggerDivision,
15 futures_code::FuturesCode,
16 },
17 },
18 JQuantsApiClient, JQuantsPlanClient,
19};
20
21#[derive(Clone, Serialize)]
23pub struct FuturesPricesBuilder {
24 #[serde(skip)]
25 client: JQuantsApiClient,
26
27 #[serde(skip_serializing_if = "Option::is_none")]
29 category: Option<FuturesCode>,
30
31 date: String,
33
34 #[serde(skip_serializing_if = "Option::is_none")]
36 central_contract_month_flag: Option<String>,
37
38 #[serde(skip_serializing_if = "Option::is_none")]
40 pagination_key: Option<String>,
41}
42
43impl JQuantsBuilder<FuturesPricesResponse> for FuturesPricesBuilder {
44 async fn send(self) -> Result<FuturesPricesResponse, crate::JQuantsError> {
45 self.send_ref().await
46 }
47
48 async fn send_ref(&self) -> Result<FuturesPricesResponse, crate::JQuantsError> {
49 self.client.inner.get("derivatives/futures", self).await
50 }
51}
52
53impl Paginatable<FuturesPricesResponse> for FuturesPricesBuilder {
54 fn pagination_key(mut self, pagination_key: impl Into<String>) -> Self {
55 self.pagination_key = Some(pagination_key.into());
56 self
57 }
58}
59
60impl FuturesPricesBuilder {
61 pub(crate) fn new(client: JQuantsApiClient, date: String) -> Self {
63 Self {
64 client,
65 category: None,
66 date,
67 central_contract_month_flag: None,
68 pagination_key: None,
69 }
70 }
71
72 pub fn category(mut self, category: impl Into<FuturesCode>) -> Self {
74 self.category = Some(category.into());
75 self
76 }
77
78 pub fn date(mut self, date: impl Into<String>) -> Self {
80 self.date = date.into();
81 self
82 }
83
84 pub fn central_contract_month_flag(mut self, flag: impl Into<String>) -> Self {
86 self.central_contract_month_flag = Some(flag.into());
87 self
88 }
89
90 pub fn pagination_key(mut self, pagination_key: impl Into<String>) -> Self {
92 self.pagination_key = Some(pagination_key.into());
93 self
94 }
95}
96
97pub trait FuturesPricesApi: JQuantsPlanClient {
99 fn get_futures_prices(&self, date: impl Into<String>) -> FuturesPricesBuilder {
103 FuturesPricesBuilder::new(self.get_api_client().clone(), date.into())
104 }
105}
106
107#[derive(Debug, Clone, PartialEq, Deserialize)]
111pub struct FuturesPricesResponse {
112 pub futures: Vec<FuturesPricesItem>,
114 pub pagination_key: Option<String>,
116}
117
118impl HasPaginationKey for FuturesPricesResponse {
119 fn get_pagination_key(&self) -> Option<&str> {
120 self.pagination_key.as_deref()
121 }
122}
123
124impl MergePage for FuturesPricesResponse {
125 fn merge_page(
126 page: Result<Vec<Self>, crate::JQuantsError>,
127 ) -> Result<Self, crate::JQuantsError> {
128 let mut page = page?;
129 let mut merged = page.pop().unwrap();
130 for p in page {
131 merged.futures.extend(p.futures);
132 }
133 merged.pagination_key = None;
134
135 Ok(merged)
136 }
137}
138
139#[derive(Debug, Clone, PartialEq, Deserialize)]
141pub struct FuturesPricesItem {
142 #[serde(rename = "Code")]
144 pub code: String,
145
146 #[serde(rename = "DerivativesProductCategory")]
148 pub derivatives_product_category: String,
149
150 #[serde(rename = "Date")]
152 pub date: String,
153
154 #[serde(rename = "WholeDayOpen")]
156 pub whole_day_open: f64,
157
158 #[serde(rename = "WholeDayHigh")]
160 pub whole_day_high: f64,
161
162 #[serde(rename = "WholeDayLow")]
164 pub whole_day_low: f64,
165
166 #[serde(rename = "WholeDayClose")]
168 pub whole_day_close: f64,
169
170 #[serde(
172 rename = "MorningSessionOpen",
173 deserialize_with = "deserialize_f64_or_none"
174 )]
175 pub morning_session_open: Option<f64>,
176
177 #[serde(
179 rename = "MorningSessionHigh",
180 deserialize_with = "deserialize_f64_or_none"
181 )]
182 pub morning_session_high: Option<f64>,
183
184 #[serde(
186 rename = "MorningSessionLow",
187 deserialize_with = "deserialize_f64_or_none"
188 )]
189 pub morning_session_low: Option<f64>,
190
191 #[serde(
193 rename = "MorningSessionClose",
194 deserialize_with = "deserialize_f64_or_none"
195 )]
196 pub morning_session_close: Option<f64>,
197
198 #[serde(
200 rename = "NightSessionOpen",
201 deserialize_with = "deserialize_f64_or_none"
202 )]
203 pub night_session_open: Option<f64>,
204
205 #[serde(
207 rename = "NightSessionHigh",
208 deserialize_with = "deserialize_f64_or_none"
209 )]
210 pub night_session_high: Option<f64>,
211
212 #[serde(
214 rename = "NightSessionLow",
215 deserialize_with = "deserialize_f64_or_none"
216 )]
217 pub night_session_low: Option<f64>,
218
219 #[serde(
221 rename = "NightSessionClose",
222 deserialize_with = "deserialize_f64_or_none"
223 )]
224 pub night_session_close: Option<f64>,
225
226 #[serde(rename = "DaySessionOpen")]
228 pub day_session_open: f64,
229
230 #[serde(rename = "DaySessionHigh")]
232 pub day_session_high: f64,
233
234 #[serde(rename = "DaySessionLow")]
236 pub day_session_low: f64,
237
238 #[serde(rename = "DaySessionClose")]
240 pub day_session_close: f64,
241
242 #[serde(rename = "Volume")]
244 pub volume: f64,
245
246 #[serde(rename = "OpenInterest")]
248 pub open_interest: f64,
249
250 #[serde(rename = "TurnoverValue")]
252 pub turnover_value: f64,
253
254 #[serde(rename = "ContractMonth")]
256 pub contract_month: String,
257
258 #[serde(
260 rename = "Volume(OnlyAuction)",
261 deserialize_with = "deserialize_f64_or_none"
262 )]
263 pub volume_only_auction: Option<f64>,
264
265 #[serde(rename = "EmergencyMarginTriggerDivision")]
267 pub emergency_margin_trigger_division: EmergencyMarginTriggerDivision,
268
269 #[serde(
271 rename = "LastTradingDay",
272 deserialize_with = "empty_string_or_null_as_none"
273 )]
274 pub last_trading_day: Option<String>,
275
276 #[serde(
278 rename = "SpecialQuotationDay",
279 deserialize_with = "empty_string_or_null_as_none"
280 )]
281 pub special_quotation_day: Option<String>,
282
283 #[serde(
285 rename = "SettlementPrice",
286 deserialize_with = "deserialize_f64_or_none"
287 )]
288 pub settlement_price: Option<f64>,
289
290 #[serde(
292 rename = "CentralContractMonthFlag",
293 deserialize_with = "empty_string_or_null_as_none"
294 )]
295 pub central_contract_month_flag: Option<CentralContractMonthFlag>,
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301
302 #[test]
303 fn test_deserialize_futures_prices_response() {
304 let json_data = r#"
305 {
306 "futures": [
307 {
308 "Code": "169090005",
309 "DerivativesProductCategory": "TOPIXF",
310 "Date": "2024-07-23",
311 "WholeDayOpen": 2825.5,
312 "WholeDayHigh": 2853.0,
313 "WholeDayLow": 2825.5,
314 "WholeDayClose": 2829.0,
315 "MorningSessionOpen": "",
316 "MorningSessionHigh": "",
317 "MorningSessionLow": "",
318 "MorningSessionClose": "",
319 "NightSessionOpen": 2825.5,
320 "NightSessionHigh": 2850.0,
321 "NightSessionLow": 2825.5,
322 "NightSessionClose": 2845.0,
323 "DaySessionOpen": 2850.5,
324 "DaySessionHigh": 2853.0,
325 "DaySessionLow": 2826.0,
326 "DaySessionClose": 2829.0,
327 "Volume": 42910.0,
328 "OpenInterest": 479812.0,
329 "TurnoverValue": 1217918971856.0,
330 "ContractMonth": "2024-09",
331 "Volume(OnlyAuction)": 40405.0,
332 "EmergencyMarginTriggerDivision": "002",
333 "LastTradingDay": "2024-09-12",
334 "SpecialQuotationDay": "2024-09-13",
335 "SettlementPrice": 2829.0,
336 "CentralContractMonthFlag": "1"
337 }
338 ],
339 "pagination_key": "value1.value2."
340 }
341 "#;
342
343 let response: FuturesPricesResponse = serde_json::from_str(json_data).unwrap();
344
345 let expected_futures = vec![FuturesPricesItem {
346 code: "169090005".to_string(),
347 derivatives_product_category: "TOPIXF".to_string(),
348 date: "2024-07-23".to_string(),
349 whole_day_open: 2825.5,
350 whole_day_high: 2853.0,
351 whole_day_low: 2825.5,
352 whole_day_close: 2829.0,
353 morning_session_open: None,
354 morning_session_high: None,
355 morning_session_low: None,
356 morning_session_close: None,
357 night_session_open: Some(2825.5),
358 night_session_high: Some(2850.0),
359 night_session_low: Some(2825.5),
360 night_session_close: Some(2845.0),
361 day_session_open: 2850.5,
362 day_session_high: 2853.0,
363 day_session_low: 2826.0,
364 day_session_close: 2829.0,
365 volume: 42910.0,
366 open_interest: 479812.0,
367 turnover_value: 1217918971856.0,
368 contract_month: "2024-09".to_string(),
369 volume_only_auction: Some(40405.0),
370 emergency_margin_trigger_division: EmergencyMarginTriggerDivision::Calculated,
371 last_trading_day: Some("2024-09-12".to_string()),
372 special_quotation_day: Some("2024-09-13".to_string()),
373 settlement_price: Some(2829.0),
374 central_contract_month_flag: Some(CentralContractMonthFlag::CentralContractMonth),
375 }];
376
377 let expected_response = FuturesPricesResponse {
378 futures: expected_futures,
379 pagination_key: Some("value1.value2.".to_string()),
380 };
381
382 pretty_assertions::assert_eq!(response, expected_response);
383 }
384
385 #[test]
386 fn test_deserialize_futures_prices_response_with_missing_optional_fields() {
387 let json_data = r#"
388 {
389 "futures": [
390 {
391 "Code": "169090005",
392 "DerivativesProductCategory": "TOPIXF",
393 "Date": "2024-07-23",
394 "WholeDayOpen": 2825.5,
395 "WholeDayHigh": 2853.0,
396 "WholeDayLow": 2825.5,
397 "WholeDayClose": 2829.0,
398 "MorningSessionOpen": "",
399 "MorningSessionHigh": "",
400 "MorningSessionLow": "",
401 "MorningSessionClose": "",
402 "NightSessionOpen": "",
403 "NightSessionHigh": "",
404 "NightSessionLow": "",
405 "NightSessionClose": "",
406 "DaySessionOpen": 2850.5,
407 "DaySessionHigh": 2853.0,
408 "DaySessionLow": 2826.0,
409 "DaySessionClose": 2829.0,
410 "Volume": 42910.0,
411 "OpenInterest": 479812.0,
412 "TurnoverValue": 1217918971856.0,
413 "ContractMonth": "2024-09",
414 "Volume(OnlyAuction)": "",
415 "EmergencyMarginTriggerDivision": "002",
416 "LastTradingDay": "",
417 "SpecialQuotationDay": "",
418 "SettlementPrice": "",
419 "CentralContractMonthFlag": ""
420 }
421 ],
422 "pagination_key": "value1.value2."
423 }
424 "#;
425
426 let response: FuturesPricesResponse = serde_json::from_str(json_data).unwrap();
427
428 let expected_futures = vec![FuturesPricesItem {
429 code: "169090005".to_string(),
430 derivatives_product_category: "TOPIXF".to_string(),
431 date: "2024-07-23".to_string(),
432 whole_day_open: 2825.5,
433 whole_day_high: 2853.0,
434 whole_day_low: 2825.5,
435 whole_day_close: 2829.0,
436 morning_session_open: None,
437 morning_session_high: None,
438 morning_session_low: None,
439 morning_session_close: None,
440 night_session_open: None,
441 night_session_high: None,
442 night_session_low: None,
443 night_session_close: None,
444 day_session_open: 2850.5,
445 day_session_high: 2853.0,
446 day_session_low: 2826.0,
447 day_session_close: 2829.0,
448 volume: 42910.0,
449 open_interest: 479812.0,
450 turnover_value: 1217918971856.0,
451 contract_month: "2024-09".to_string(),
452 volume_only_auction: None,
453 emergency_margin_trigger_division: EmergencyMarginTriggerDivision::Calculated,
454 last_trading_day: None,
455 special_quotation_day: None,
456 settlement_price: None,
457 central_contract_month_flag: None,
458 }];
459
460 let expected_response = FuturesPricesResponse {
461 futures: expected_futures,
462 pagination_key: Some("value1.value2.".to_string()),
463 };
464
465 pretty_assertions::assert_eq!(response, expected_response);
466 }
467
468 #[test]
469 fn test_deserialize_futures_prices_response_multiple_items() {
470 let json_data = r#"
471 {
472 "futures": [
473 {
474 "Code": "169090005",
475 "DerivativesProductCategory": "TOPIXF",
476 "Date": "2024-07-23",
477 "WholeDayOpen": 2825.5,
478 "WholeDayHigh": 2853.0,
479 "WholeDayLow": 2825.5,
480 "WholeDayClose": 2829.0,
481 "MorningSessionOpen": "",
482 "MorningSessionHigh": "",
483 "MorningSessionLow": "",
484 "MorningSessionClose": "",
485 "NightSessionOpen": 2825.5,
486 "NightSessionHigh": 2850.0,
487 "NightSessionLow": 2825.5,
488 "NightSessionClose": 2845.0,
489 "DaySessionOpen": 2850.5,
490 "DaySessionHigh": 2853.0,
491 "DaySessionLow": 2826.0,
492 "DaySessionClose": 2829.0,
493 "Volume": 42910.0,
494 "OpenInterest": 479812.0,
495 "TurnoverValue": 1217918971856.0,
496 "ContractMonth": "2024-09",
497 "Volume(OnlyAuction)": 40405.0,
498 "EmergencyMarginTriggerDivision": "002",
499 "LastTradingDay": "2024-09-12",
500 "SpecialQuotationDay": "2024-09-13",
501 "SettlementPrice": 2829.0,
502 "CentralContractMonthFlag": "1"
503 },
504 {
505 "Code": "169090006",
506 "DerivativesProductCategory": "NK225F",
507 "Date": "2024-07-24",
508 "WholeDayOpen": 3000.0,
509 "WholeDayHigh": 3050.0,
510 "WholeDayLow": 2950.0,
511 "WholeDayClose": 3025.0,
512 "MorningSessionOpen": 3010.0,
513 "MorningSessionHigh": 3040.0,
514 "MorningSessionLow": 2955.0,
515 "MorningSessionClose": 3030.0,
516 "NightSessionOpen": 3025.5,
517 "NightSessionHigh": 3050.0,
518 "NightSessionLow": 3000.0,
519 "NightSessionClose": 3045.0,
520 "DaySessionOpen": 3050.5,
521 "DaySessionHigh": 3053.0,
522 "DaySessionLow": 3006.0,
523 "DaySessionClose": 3029.0,
524 "Volume": 52910.0,
525 "OpenInterest": 579812.0,
526 "TurnoverValue": 1317918971856.0,
527 "ContractMonth": "2024-10",
528 "Volume(OnlyAuction)": 50405.0,
529 "EmergencyMarginTriggerDivision": "001",
530 "LastTradingDay": "2024-10-12",
531 "SpecialQuotationDay": "2024-10-13",
532 "SettlementPrice": 3029.0,
533 "CentralContractMonthFlag": "0"
534 }
535 ],
536 "pagination_key": "value3.value4."
537 }
538 "#;
539
540 let response: FuturesPricesResponse = serde_json::from_str(json_data).unwrap();
541
542 let expected_futures = vec![
543 FuturesPricesItem {
544 code: "169090005".to_string(),
545 derivatives_product_category: "TOPIXF".to_string(),
546 date: "2024-07-23".to_string(),
547 whole_day_open: 2825.5,
548 whole_day_high: 2853.0,
549 whole_day_low: 2825.5,
550 whole_day_close: 2829.0,
551 morning_session_open: None,
552 morning_session_high: None,
553 morning_session_low: None,
554 morning_session_close: None,
555 night_session_open: Some(2825.5),
556 night_session_high: Some(2850.0),
557 night_session_low: Some(2825.5),
558 night_session_close: Some(2845.0),
559 day_session_open: 2850.5,
560 day_session_high: 2853.0,
561 day_session_low: 2826.0,
562 day_session_close: 2829.0,
563 volume: 42910.0,
564 open_interest: 479812.0,
565 turnover_value: 1217918971856.0,
566 contract_month: "2024-09".to_string(),
567 volume_only_auction: Some(40405.0),
568 emergency_margin_trigger_division: EmergencyMarginTriggerDivision::Calculated,
569 last_trading_day: Some("2024-09-12".to_string()),
570 special_quotation_day: Some("2024-09-13".to_string()),
571 settlement_price: Some(2829.0),
572 central_contract_month_flag: Some(CentralContractMonthFlag::CentralContractMonth),
573 },
574 FuturesPricesItem {
575 code: "169090006".to_string(),
576 derivatives_product_category: "NK225F".to_string(),
577 date: "2024-07-24".to_string(),
578 whole_day_open: 3000.0,
579 whole_day_high: 3050.0,
580 whole_day_low: 2950.0,
581 whole_day_close: 3025.0,
582 morning_session_open: Some(3010.0),
583 morning_session_high: Some(3040.0),
584 morning_session_low: Some(2955.0),
585 morning_session_close: Some(3030.0),
586 night_session_open: Some(3025.5),
587 night_session_high: Some(3050.0),
588 night_session_low: Some(3000.0),
589 night_session_close: Some(3045.0),
590 day_session_open: 3050.5,
591 day_session_high: 3053.0,
592 day_session_low: 3006.0,
593 day_session_close: 3029.0,
594 volume: 52910.0,
595 open_interest: 579812.0,
596 turnover_value: 1317918971856.0,
597 contract_month: "2024-10".to_string(),
598 volume_only_auction: Some(50405.0),
599 emergency_margin_trigger_division: EmergencyMarginTriggerDivision::Triggered,
600 last_trading_day: Some("2024-10-12".to_string()),
601 special_quotation_day: Some("2024-10-13".to_string()),
602 settlement_price: Some(3029.0),
603 central_contract_month_flag: Some(CentralContractMonthFlag::Others),
604 },
605 ];
606
607 let expected_response = FuturesPricesResponse {
608 futures: expected_futures,
609 pagination_key: Some("value3.value4.".to_string()),
610 };
611
612 pretty_assertions::assert_eq!(response, expected_response);
613 }
614
615 #[test]
616 fn test_deserialize_futures_prices_response_no_pagination_key() {
617 let json_data = r#"
618 {
619 "futures": [
620 {
621 "Code": "169090005",
622 "DerivativesProductCategory": "TOPIXF",
623 "Date": "2024-07-23",
624 "WholeDayOpen": 2825.5,
625 "WholeDayHigh": 2853.0,
626 "WholeDayLow": 2825.5,
627 "WholeDayClose": 2829.0,
628 "MorningSessionOpen": "",
629 "MorningSessionHigh": "",
630 "MorningSessionLow": "",
631 "MorningSessionClose": "",
632 "NightSessionOpen": 2825.5,
633 "NightSessionHigh": 2850.0,
634 "NightSessionLow": 2825.5,
635 "NightSessionClose": 2845.0,
636 "DaySessionOpen": 2850.5,
637 "DaySessionHigh": 2853.0,
638 "DaySessionLow": 2826.0,
639 "DaySessionClose": 2829.0,
640 "Volume": 42910.0,
641 "OpenInterest": 479812.0,
642 "TurnoverValue": 1217918971856.0,
643 "ContractMonth": "2024-09",
644 "Volume(OnlyAuction)": 40405.0,
645 "EmergencyMarginTriggerDivision": "002",
646 "LastTradingDay": "2024-09-12",
647 "SpecialQuotationDay": "2024-09-13",
648 "SettlementPrice": 2829.0,
649 "CentralContractMonthFlag": "1"
650 }
651 ]
652 }
653 "#;
654
655 let response: FuturesPricesResponse = serde_json::from_str(json_data).unwrap();
656
657 let expected_futures = vec![FuturesPricesItem {
658 code: "169090005".to_string(),
659 derivatives_product_category: "TOPIXF".to_string(),
660 date: "2024-07-23".to_string(),
661 whole_day_open: 2825.5,
662 whole_day_high: 2853.0,
663 whole_day_low: 2825.5,
664 whole_day_close: 2829.0,
665 morning_session_open: None,
666 morning_session_high: None,
667 morning_session_low: None,
668 morning_session_close: None,
669 night_session_open: Some(2825.5),
670 night_session_high: Some(2850.0),
671 night_session_low: Some(2825.5),
672 night_session_close: Some(2845.0),
673 day_session_open: 2850.5,
674 day_session_high: 2853.0,
675 day_session_low: 2826.0,
676 day_session_close: 2829.0,
677 volume: 42910.0,
678 open_interest: 479812.0,
679 turnover_value: 1217918971856.0,
680 contract_month: "2024-09".to_string(),
681 volume_only_auction: Some(40405.0),
682 emergency_margin_trigger_division: EmergencyMarginTriggerDivision::Calculated,
683 last_trading_day: Some("2024-09-12".to_string()),
684 special_quotation_day: Some("2024-09-13".to_string()),
685 settlement_price: Some(2829.0),
686 central_contract_month_flag: Some(CentralContractMonthFlag::CentralContractMonth),
687 }];
688
689 let expected_response = FuturesPricesResponse {
690 futures: expected_futures,
691 pagination_key: None,
692 };
693
694 pretty_assertions::assert_eq!(response, expected_response);
695 }
696
697 #[test]
698 fn test_deserialize_futures_prices_response_no_data() {
699 let json_data = r#"
700 {
701 "futures": []
702 }
703 "#;
704
705 let response: FuturesPricesResponse = serde_json::from_str(json_data).unwrap();
706 let expected_response = FuturesPricesResponse {
707 futures: vec![],
708 pagination_key: None,
709 };
710
711 pretty_assertions::assert_eq!(response, expected_response);
712 }
713}