@@ -540,14 +540,14 @@ impl DirectClient {
540540 ///
541541 /// gRPC: `BetaThetaTerminal/GetStockHistoryOhlc`
542542 ///
543- /// `interval` is in milliseconds (e.g. `" 60000" ` for 1-minute bars, `" 300000" ` for 5-minute).
543+ /// `interval` is in milliseconds (e.g. `60000` for 1-minute bars, `300000` for 5-minute).
544544 fn stock_history_ohlc( symbol: & str , date: & str , interval: & str ) -> Vec <OhlcTick >;
545545 grpc: get_stock_history_ohlc;
546546 request: StockHistoryOhlcRequest ;
547547 query: StockHistoryOhlcRequestQuery {
548548 symbol: symbol. to_string( ) ,
549549 date: Some ( date. to_string( ) ) ,
550- interval: interval . to_string ( ) ,
550+ interval: normalize_interval ( interval ) ,
551551 start_time: None ,
552552 end_time: None ,
553553 venue: None ,
@@ -566,7 +566,7 @@ impl DirectClient {
566566 ///
567567 /// Uses `start_date`/`end_date` instead of single `date`.
568568 ///
569- /// `interval` is in milliseconds (e.g. `" 60000" ` for 1-minute bars, `" 300000" ` for 5-minute).
569+ /// `interval` is in milliseconds (e.g. `60000` for 1-minute bars, `300000` for 5-minute).
570570 fn stock_history_ohlc_range(
571571 symbol: & str , start_date: & str , end_date: & str , interval: & str
572572 ) -> Vec <OhlcTick >;
@@ -575,7 +575,7 @@ impl DirectClient {
575575 query: StockHistoryOhlcRequestQuery {
576576 symbol: symbol. to_string( ) ,
577577 date: None ,
578- interval: interval . to_string ( ) ,
578+ interval: normalize_interval ( interval ) ,
579579 start_time: None ,
580580 end_time: None ,
581581 venue: None ,
@@ -613,14 +613,14 @@ impl DirectClient {
613613 ///
614614 /// gRPC: `BetaThetaTerminal/GetStockHistoryQuote`
615615 ///
616- /// `interval` is in milliseconds (e.g. `" 60000" ` for 1-minute bars, `" 300000" ` for 5-minute).
616+ /// `interval` is in milliseconds (e.g. `60000` for 1-minute bars, `300000` for 5-minute).
617617 fn stock_history_quote( symbol: & str , date: & str , interval: & str ) -> Vec <QuoteTick >;
618618 grpc: get_stock_history_quote;
619619 request: StockHistoryQuoteRequest ;
620620 query: StockHistoryQuoteRequestQuery {
621621 symbol: symbol. to_string( ) ,
622622 date: Some ( date. to_string( ) ) ,
623- interval: interval . to_string ( ) ,
623+ interval: normalize_interval ( interval ) ,
624624 start_time: None ,
625625 end_time: None ,
626626 venue: None ,
@@ -667,14 +667,14 @@ impl DirectClient {
667667 ///
668668 /// gRPC: `BetaThetaTerminal/GetStockHistoryQuote`
669669 ///
670- /// `interval` is in milliseconds (e.g. `" 60000" ` for 1-minute bars, `" 300000" ` for 5-minute).
670+ /// `interval` is in milliseconds (e.g. `60000` for 1-minute bars, `300000` for 5-minute).
671671 fn stock_history_quote_stream( symbol: & str , date: & str , interval: & str ; handler: F ) -> QuoteTick ;
672672 grpc: get_stock_history_quote;
673673 request: StockHistoryQuoteRequest ;
674674 query: StockHistoryQuoteRequestQuery {
675675 symbol: symbol. to_string( ) ,
676676 date: Some ( date. to_string( ) ) ,
677- interval: interval . to_string ( ) ,
677+ interval: normalize_interval ( interval ) ,
678678 start_time: None ,
679679 end_time: None ,
680680 venue: None ,
@@ -1104,7 +1104,7 @@ impl DirectClient {
11041104 ///
11051105 /// gRPC: `BetaThetaTerminal/GetOptionHistoryOhlc`
11061106 ///
1107- /// `interval` is in milliseconds (e.g. `" 60000" ` for 1-minute bars, `" 300000" ` for 5-minute).
1107+ /// `interval` is in milliseconds (e.g. `60000` for 1-minute bars, `300000` for 5-minute).
11081108 fn option_history_ohlc(
11091109 symbol: & str , expiration: & str , strike: & str , right: & str ,
11101110 date: & str , interval: & str
@@ -1115,7 +1115,7 @@ impl DirectClient {
11151115 contract_spec: contract_spec!( symbol, expiration, strike, right) ,
11161116 date: Some ( date. to_string( ) ) ,
11171117 expiration: expiration. to_string( ) ,
1118- interval: interval . to_string ( ) ,
1118+ interval: normalize_interval ( interval ) ,
11191119 start_time: None ,
11201120 end_time: None ,
11211121 strike_range: None ,
@@ -1157,7 +1157,7 @@ impl DirectClient {
11571157 ///
11581158 /// gRPC: `BetaThetaTerminal/GetOptionHistoryQuote`
11591159 ///
1160- /// `interval` is in milliseconds (e.g. `" 60000" ` for 1-minute bars, `" 300000" ` for 5-minute).
1160+ /// `interval` is in milliseconds (e.g. `60000` for 1-minute bars, `300000` for 5-minute).
11611161 fn option_history_quote(
11621162 symbol: & str , expiration: & str , strike: & str , right: & str ,
11631163 date: & str , interval: & str
@@ -1170,7 +1170,7 @@ impl DirectClient {
11701170 expiration: expiration. to_string( ) ,
11711171 start_time: None ,
11721172 end_time: None ,
1173- interval: interval . to_string ( ) ,
1173+ interval: normalize_interval ( interval ) ,
11741174 max_dte: None ,
11751175 strike_range: None ,
11761176 start_date: None ,
@@ -1230,7 +1230,7 @@ impl DirectClient {
12301230 expiration: expiration. to_string( ) ,
12311231 start_time: None ,
12321232 end_time: None ,
1233- interval: interval . to_string ( ) ,
1233+ interval: normalize_interval ( interval ) ,
12341234 max_dte: None ,
12351235 strike_range: None ,
12361236 start_date: None ,
@@ -1340,7 +1340,7 @@ impl DirectClient {
13401340 expiration: expiration. to_string( ) ,
13411341 start_time: None ,
13421342 end_time: None ,
1343- interval: interval . to_string ( ) ,
1343+ interval: normalize_interval ( interval ) ,
13441344 annual_dividend: None ,
13451345 rate_type: None ,
13461346 rate_value: None ,
@@ -1401,7 +1401,7 @@ impl DirectClient {
14011401 expiration: expiration. to_string( ) ,
14021402 start_time: None ,
14031403 end_time: None ,
1404- interval: interval . to_string ( ) ,
1404+ interval: normalize_interval ( interval ) ,
14051405 annual_dividend: None ,
14061406 rate_type: None ,
14071407 rate_value: None ,
@@ -1462,7 +1462,7 @@ impl DirectClient {
14621462 expiration: expiration. to_string( ) ,
14631463 start_time: None ,
14641464 end_time: None ,
1465- interval: interval . to_string ( ) ,
1465+ interval: normalize_interval ( interval ) ,
14661466 annual_dividend: None ,
14671467 rate_type: None ,
14681468 rate_value: None ,
@@ -1523,7 +1523,7 @@ impl DirectClient {
15231523 expiration: expiration. to_string( ) ,
15241524 start_time: None ,
15251525 end_time: None ,
1526- interval: interval . to_string ( ) ,
1526+ interval: normalize_interval ( interval ) ,
15271527 annual_dividend: None ,
15281528 rate_type: None ,
15291529 rate_value: None ,
@@ -1584,7 +1584,7 @@ impl DirectClient {
15841584 expiration: expiration. to_string( ) ,
15851585 start_time: None ,
15861586 end_time: None ,
1587- interval: interval . to_string ( ) ,
1587+ interval: normalize_interval ( interval ) ,
15881588 annual_dividend: None ,
15891589 rate_type: None ,
15901590 rate_value: None ,
@@ -1792,7 +1792,7 @@ impl DirectClient {
17921792 symbol: symbol. to_string( ) ,
17931793 start_date: start_date. to_string( ) ,
17941794 end_date: end_date. to_string( ) ,
1795- interval: interval . to_string ( ) ,
1795+ interval: normalize_interval ( interval ) ,
17961796 start_time: None ,
17971797 end_time: None ,
17981798 } ;
@@ -1817,7 +1817,7 @@ impl DirectClient {
18171817 symbol: symbol. to_string( ) ,
18181818 start_time: None ,
18191819 end_time: None ,
1820- interval: interval . to_string ( ) ,
1820+ interval: normalize_interval ( interval ) ,
18211821 start_date: None ,
18221822 end_date: None ,
18231823 } ;
@@ -1980,6 +1980,33 @@ impl DirectClient {
19801980// Private helpers
19811981// ═══════════════════════════════════════════════════════════════════════
19821982
1983+ /// Normalize an interval string to the `HH:MM:SS.mmm` format the MDDS server expects.
1984+ ///
1985+ /// Users pass milliseconds as a string (e.g. `"60000"` for 1-minute bars).
1986+ /// The server expects `HH:MM:SS.mmm`. If the input is already in that format
1987+ /// (contains `:`), it's passed through unchanged.
1988+ ///
1989+ /// Examples: `"60000"` -> `"00:01:00.000"`, `"900000"` -> `"00:15:00.000"`, `"0"` -> `"00:00:00.000"`
1990+ fn normalize_interval ( interval : & str ) -> String {
1991+ // If it already contains `:`, assume it's in HH:MM:SS format -- pass through.
1992+ if interval. contains ( ':' ) {
1993+ return interval. to_string ( ) ;
1994+ }
1995+ // Try parsing as milliseconds.
1996+ match interval. parse :: < u32 > ( ) {
1997+ Ok ( ms) => {
1998+ let total_secs = ms / 1000 ;
1999+ let millis = ms % 1000 ;
2000+ let h = total_secs / 3600 ;
2001+ let m = ( total_secs % 3600 ) / 60 ;
2002+ let s = total_secs % 60 ;
2003+ format ! ( "{h:02}:{m:02}:{s:02}.{millis:03}" )
2004+ }
2005+ // Not a number and not HH:MM:SS -- pass through and let the server reject it.
2006+ Err ( _) => interval. to_string ( ) ,
2007+ }
2008+ }
2009+
19832010/// Validate that a date string is in YYYYMMDD format (exactly 8 ASCII digits).
19842011fn validate_date ( date : & str ) -> Result < ( ) , Error > {
19852012 if date. len ( ) != 8 || !date. bytes ( ) . all ( |b| b. is_ascii_digit ( ) ) {
0 commit comments