1use std::convert::Infallible;
2use std::fmt;
3use std::num::NonZeroU32;
4use std::str::FromStr;
5
6use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
7use thiserror::Error;
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
10pub struct IndexId(Box<str>);
12
13impl IndexId {
14 pub fn as_str(&self) -> &str {
16 self.0.as_ref()
17 }
18}
19
20impl fmt::Display for IndexId {
21 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22 f.write_str(self.as_str())
23 }
24}
25
26impl AsRef<str> for IndexId {
27 fn as_ref(&self) -> &str {
28 self.as_str()
29 }
30}
31
32impl From<&IndexId> for IndexId {
33 fn from(value: &IndexId) -> Self {
34 value.clone()
35 }
36}
37
38#[derive(Debug, Error, Clone, PartialEq, Eq)]
39pub enum ParseIndexError {
41 #[error("index id must not be empty")]
43 EmptyIndexId,
44 #[error("index short name must not be empty")]
46 EmptyShortName,
47 #[error("invalid index date range: from={from} is after till={till}")]
49 InvalidDateRange {
50 from: NaiveDate,
52 till: NaiveDate,
54 },
55}
56
57impl From<Infallible> for ParseIndexError {
58 fn from(value: Infallible) -> Self {
59 match value {}
60 }
61}
62
63#[derive(Debug, Error, Clone, PartialEq, Eq)]
64pub enum ParseHistoryDatesError {
66 #[error("invalid history dates range: from={from} is after till={till}")]
68 InvalidDateRange {
69 from: NaiveDate,
71 till: NaiveDate,
73 },
74}
75
76#[derive(Debug, Error, Clone, PartialEq, Eq)]
77pub enum ParseHistoryRecordError {
79 #[error(transparent)]
81 InvalidBoardId(#[from] ParseBoardIdError),
82 #[error(transparent)]
84 InvalidSecId(#[from] ParseSecIdError),
85 #[error("history numtrades must not be negative, got {0}")]
87 NegativeNumTrades(i64),
88 #[error("history volume must not be negative, got {0}")]
90 NegativeVolume(i64),
91}
92
93#[derive(Debug, Error, Clone, PartialEq, Eq)]
94pub enum ParseTurnoverError {
96 #[error("turnover name must not be empty")]
98 EmptyName,
99 #[error("turnover id must be positive, got {0}")]
101 NonPositiveId(i64),
102 #[error("turnover id is out of range for u32, got {0}")]
104 IdOutOfRange(i64),
105 #[error("turnover numtrades must not be negative, got {0}")]
107 NegativeNumTrades(i64),
108 #[error("turnover title must not be empty")]
110 EmptyTitle,
111}
112
113#[derive(Debug, Error, Clone, PartialEq, Eq)]
114pub enum ParseSecStatError {
116 #[error(transparent)]
118 InvalidSecId(#[from] ParseSecIdError),
119 #[error(transparent)]
121 InvalidBoardId(#[from] ParseBoardIdError),
122 #[error("secstats voltoday must not be negative, got {0}")]
124 NegativeVolToday(i64),
125 #[error("secstats numtrades must not be negative, got {0}")]
127 NegativeNumTrades(i64),
128}
129
130#[derive(Debug, Error, Clone, PartialEq, Eq)]
131pub enum ParseSiteNewsError {
133 #[error("sitenews id must be positive, got {0}")]
135 NonPositiveId(i64),
136 #[error("sitenews id is out of range for u64, got {0}")]
138 IdOutOfRange(i64),
139 #[error("sitenews tag must not be empty")]
141 EmptyTag,
142 #[error("sitenews title must not be empty")]
144 EmptyTitle,
145}
146
147#[derive(Debug, Error, Clone, PartialEq, Eq)]
148pub enum ParseEventError {
150 #[error("events id must be positive, got {0}")]
152 NonPositiveId(i64),
153 #[error("events id is out of range for u64, got {0}")]
155 IdOutOfRange(i64),
156 #[error("events tag must not be empty")]
158 EmptyTag,
159 #[error("events title must not be empty")]
161 EmptyTitle,
162}
163
164#[derive(Debug, Error, Clone, PartialEq, Eq)]
165pub enum ParseIndexAnalyticsError {
167 #[error(transparent)]
169 InvalidIndexId(#[from] ParseIndexError),
170 #[error("ticker is invalid: {0}")]
172 InvalidTicker(ParseSecIdError),
173 #[error("secid is invalid: {0}")]
175 InvalidSecId(ParseSecIdError),
176 #[error("shortnames must not be empty")]
178 EmptyShortnames,
179 #[error("weight must be finite")]
181 NonFiniteWeight,
182 #[error("weight must not be negative")]
184 NegativeWeight,
185 #[error("tradingsession must be 1, 2 or 3, got {0}")]
187 InvalidTradingsession(i64),
188}
189
190#[derive(Debug, Error, Clone, PartialEq, Eq)]
191pub enum ParseEngineError {
193 #[error("engine id must be positive, got {0}")]
195 NonPositiveId(i64),
196 #[error("engine id is out of range for u32, got {0}")]
198 IdOutOfRange(i64),
199 #[error(transparent)]
201 InvalidName(#[from] ParseEngineNameError),
202 #[error("engine title must not be empty")]
204 EmptyTitle,
205}
206
207#[derive(Debug, Error, Clone, PartialEq, Eq)]
208pub enum ParseEngineNameError {
210 #[error("engine name must not be empty")]
212 Empty,
213 #[error("engine name must not contain '/'")]
215 ContainsSlash,
216}
217
218impl From<Infallible> for ParseEngineNameError {
219 fn from(value: Infallible) -> Self {
220 match value {}
221 }
222}
223
224#[derive(Debug, Clone, PartialEq, Eq, Hash)]
225pub struct EngineName(Box<str>);
227
228impl EngineName {
229 pub fn as_str(&self) -> &str {
231 self.0.as_ref()
232 }
233}
234
235impl fmt::Display for EngineName {
236 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237 f.write_str(self.as_str())
238 }
239}
240
241impl AsRef<str> for EngineName {
242 fn as_ref(&self) -> &str {
243 self.as_str()
244 }
245}
246
247impl From<&EngineName> for EngineName {
248 fn from(value: &EngineName) -> Self {
249 value.clone()
250 }
251}
252
253impl TryFrom<String> for EngineName {
254 type Error = ParseEngineNameError;
255
256 fn try_from(value: String) -> Result<Self, Self::Error> {
257 Self::try_from(value.as_str())
258 }
259}
260
261impl TryFrom<&str> for EngineName {
262 type Error = ParseEngineNameError;
263
264 fn try_from(value: &str) -> Result<Self, Self::Error> {
265 let value = value.trim();
266 if value.is_empty() {
267 return Err(ParseEngineNameError::Empty);
268 }
269 if value.contains('/') {
270 return Err(ParseEngineNameError::ContainsSlash);
271 }
272 Ok(Self(value.to_owned().into_boxed_str()))
273 }
274}
275
276impl FromStr for EngineName {
277 type Err = ParseEngineNameError;
278
279 fn from_str(value: &str) -> Result<Self, Self::Err> {
280 Self::try_from(value)
281 }
282}
283
284#[derive(Debug, Error, Clone, PartialEq, Eq)]
285pub enum ParseMarketError {
287 #[error("market id must be positive, got {0}")]
289 NonPositiveId(i64),
290 #[error("market id is out of range for u32, got {0}")]
292 IdOutOfRange(i64),
293 #[error(transparent)]
295 InvalidName(#[from] ParseMarketNameError),
296 #[error("market title must not be empty")]
298 EmptyTitle,
299}
300
301#[derive(Debug, Error, Clone, PartialEq, Eq)]
302pub enum ParseBoardError {
304 #[error("board id must be positive, got {0}")]
306 NonPositiveId(i64),
307 #[error("board id is out of range for u32, got {0}")]
309 IdOutOfRange(i64),
310 #[error("board_group_id must not be negative, got {0}")]
312 NegativeBoardGroupId(i64),
313 #[error("board_group_id is out of range for u32, got {0}")]
315 BoardGroupIdOutOfRange(i64),
316 #[error(transparent)]
318 InvalidBoardId(#[from] ParseBoardIdError),
319 #[error("board title must not be empty")]
321 EmptyTitle,
322 #[error("is_traded must be 0 or 1, got {0}")]
324 InvalidIsTraded(i64),
325}
326
327#[derive(Debug, Error, Clone, PartialEq, Eq)]
328pub enum ParseSecurityError {
330 #[error(transparent)]
332 InvalidSecId(#[from] ParseSecIdError),
333 #[error("security shortname must not be empty")]
335 EmptyShortname,
336 #[error("security secname must not be empty")]
338 EmptySecname,
339 #[error("security status must not be empty")]
341 EmptyStatus,
342}
343
344#[derive(Debug, Error, Clone, PartialEq, Eq)]
345pub enum ParseSecurityBoardError {
347 #[error(transparent)]
349 InvalidEngine(#[from] ParseEngineNameError),
350 #[error(transparent)]
352 InvalidMarket(#[from] ParseMarketNameError),
353 #[error(transparent)]
355 InvalidBoardId(#[from] ParseBoardIdError),
356 #[error("is_primary must be 0 or 1, got {0}")]
358 InvalidIsPrimary(i64),
359}
360
361#[derive(Debug, Error, Clone, PartialEq)]
362pub enum ParseSecuritySnapshotError {
364 #[error(transparent)]
366 InvalidSecId(#[from] ParseSecIdError),
367 #[error("lot size must not be negative, got {0}")]
369 NegativeLotSize(i64),
370 #[error("lot size is out of range for u32, got {0}")]
372 LotSizeOutOfRange(i64),
373 #[error("last must be finite")]
375 NonFiniteLast(f64),
376}
377
378#[derive(Debug, Error, Clone, PartialEq, Eq)]
379pub enum ParseCandleError {
381 #[error("invalid candle datetime range: begin={begin} is after end={end}")]
383 InvalidDateRange {
384 begin: NaiveDateTime,
386 end: NaiveDateTime,
388 },
389 #[error("candle volume must not be negative, got {0}")]
391 NegativeVolume(i64),
392}
393
394#[derive(Debug, Error, Clone, PartialEq, Eq)]
395pub enum ParseCandleIntervalError {
397 #[error("invalid candle interval code, got {0}")]
399 InvalidCode(i64),
400}
401
402#[derive(Debug, Error, Clone, PartialEq, Eq)]
403pub enum ParseCandleQueryError {
405 #[error("invalid candle query datetime range: from={from} is after till={till}")]
407 InvalidDateRange {
408 from: NaiveDateTime,
410 till: NaiveDateTime,
412 },
413}
414
415#[derive(Debug, Error, Clone, PartialEq, Eq)]
416pub enum ParseCandleBorderError {
418 #[error("invalid candle borders range: begin={begin} is after end={end}")]
420 InvalidDateRange {
421 begin: NaiveDateTime,
423 end: NaiveDateTime,
425 },
426 #[error(transparent)]
428 InvalidInterval(#[from] ParseCandleIntervalError),
429 #[error("board_group_id must not be negative, got {0}")]
431 NegativeBoardGroupId(i64),
432 #[error("board_group_id is out of range for u32, got {0}")]
434 BoardGroupIdOutOfRange(i64),
435}
436
437#[derive(Debug, Error, Clone, PartialEq, Eq)]
438pub enum ParseTradeError {
440 #[error("trade number must be positive, got {0}")]
442 NonPositiveTradeNo(i64),
443 #[error("trade number is out of range for u64, got {0}")]
445 TradeNoOutOfRange(i64),
446 #[error("trade quantity must not be negative, got {0}")]
448 NegativeQuantity(i64),
449}
450
451#[derive(Debug, Error, Clone, PartialEq, Eq)]
452pub enum ParseOrderbookError {
454 #[error("orderbook side must be 'B' or 'S', got '{0}'")]
456 InvalidSide(Box<str>),
457 #[error("orderbook price must be present")]
459 MissingPrice,
460 #[error("orderbook price must not be negative")]
462 NegativePrice,
463 #[error("orderbook quantity must be present")]
465 MissingQuantity,
466 #[error("orderbook quantity must not be negative, got {0}")]
468 NegativeQuantity(i64),
469}
470
471#[derive(Debug, Error, Clone, PartialEq, Eq)]
472pub enum ParseSecIdError {
474 #[error("secid must not be empty")]
476 Empty,
477 #[error("secid must not contain '/'")]
479 ContainsSlash,
480}
481
482impl From<Infallible> for ParseSecIdError {
483 fn from(value: Infallible) -> Self {
484 match value {}
485 }
486}
487
488#[derive(Debug, Clone, PartialEq, Eq, Hash)]
489pub struct SecId(Box<str>);
491
492impl SecId {
493 pub fn as_str(&self) -> &str {
495 self.0.as_ref()
496 }
497}
498
499impl fmt::Display for SecId {
500 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
501 f.write_str(self.as_str())
502 }
503}
504
505impl AsRef<str> for SecId {
506 fn as_ref(&self) -> &str {
507 self.as_str()
508 }
509}
510
511impl From<&SecId> for SecId {
512 fn from(value: &SecId) -> Self {
513 value.clone()
514 }
515}
516
517impl TryFrom<String> for SecId {
518 type Error = ParseSecIdError;
519
520 fn try_from(value: String) -> Result<Self, Self::Error> {
521 Self::try_from(value.as_str())
522 }
523}
524
525impl TryFrom<&str> for SecId {
526 type Error = ParseSecIdError;
527
528 fn try_from(value: &str) -> Result<Self, Self::Error> {
529 let value = value.trim();
530 if value.is_empty() {
531 return Err(ParseSecIdError::Empty);
532 }
533 if value.contains('/') {
534 return Err(ParseSecIdError::ContainsSlash);
535 }
536 Ok(Self(value.to_owned().into_boxed_str()))
537 }
538}
539
540impl FromStr for SecId {
541 type Err = ParseSecIdError;
542
543 fn from_str(value: &str) -> Result<Self, Self::Err> {
544 Self::try_from(value)
545 }
546}
547
548#[derive(Debug, Error, Clone, PartialEq, Eq)]
549pub enum ParseBoardIdError {
551 #[error("boardid must not be empty")]
553 Empty,
554 #[error("boardid must not contain '/'")]
556 ContainsSlash,
557}
558
559impl From<Infallible> for ParseBoardIdError {
560 fn from(value: Infallible) -> Self {
561 match value {}
562 }
563}
564
565#[derive(Debug, Clone, PartialEq, Eq, Hash)]
566pub struct BoardId(Box<str>);
568
569impl BoardId {
570 pub fn as_str(&self) -> &str {
572 self.0.as_ref()
573 }
574}
575
576impl fmt::Display for BoardId {
577 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
578 f.write_str(self.as_str())
579 }
580}
581
582impl AsRef<str> for BoardId {
583 fn as_ref(&self) -> &str {
584 self.as_str()
585 }
586}
587
588impl From<&BoardId> for BoardId {
589 fn from(value: &BoardId) -> Self {
590 value.clone()
591 }
592}
593
594impl TryFrom<String> for BoardId {
595 type Error = ParseBoardIdError;
596
597 fn try_from(value: String) -> Result<Self, Self::Error> {
598 Self::try_from(value.as_str())
599 }
600}
601
602impl TryFrom<&str> for BoardId {
603 type Error = ParseBoardIdError;
604
605 fn try_from(value: &str) -> Result<Self, Self::Error> {
606 let value = value.trim();
607 if value.is_empty() {
608 return Err(ParseBoardIdError::Empty);
609 }
610 if value.contains('/') {
611 return Err(ParseBoardIdError::ContainsSlash);
612 }
613 Ok(Self(value.to_owned().into_boxed_str()))
614 }
615}
616
617impl FromStr for BoardId {
618 type Err = ParseBoardIdError;
619
620 fn from_str(value: &str) -> Result<Self, Self::Err> {
621 Self::try_from(value)
622 }
623}
624
625#[derive(Debug, Error, Clone, PartialEq, Eq)]
626pub enum ParseMarketNameError {
628 #[error("market name must not be empty")]
630 Empty,
631 #[error("market name must not contain '/'")]
633 ContainsSlash,
634}
635
636impl From<Infallible> for ParseMarketNameError {
637 fn from(value: Infallible) -> Self {
638 match value {}
639 }
640}
641
642#[derive(Debug, Clone, PartialEq, Eq, Hash)]
643pub struct MarketName(Box<str>);
645
646impl MarketName {
647 pub fn as_str(&self) -> &str {
649 self.0.as_ref()
650 }
651}
652
653impl fmt::Display for MarketName {
654 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
655 f.write_str(self.as_str())
656 }
657}
658
659impl AsRef<str> for MarketName {
660 fn as_ref(&self) -> &str {
661 self.as_str()
662 }
663}
664
665impl From<&MarketName> for MarketName {
666 fn from(value: &MarketName) -> Self {
667 value.clone()
668 }
669}
670
671impl TryFrom<String> for MarketName {
672 type Error = ParseMarketNameError;
673
674 fn try_from(value: String) -> Result<Self, Self::Error> {
675 Self::try_from(value.as_str())
676 }
677}
678
679impl TryFrom<&str> for MarketName {
680 type Error = ParseMarketNameError;
681
682 fn try_from(value: &str) -> Result<Self, Self::Error> {
683 let value = value.trim();
684 if value.is_empty() {
685 return Err(ParseMarketNameError::Empty);
686 }
687 if value.contains('/') {
688 return Err(ParseMarketNameError::ContainsSlash);
689 }
690 Ok(Self(value.to_owned().into_boxed_str()))
691 }
692}
693
694impl FromStr for MarketName {
695 type Err = ParseMarketNameError;
696
697 fn from_str(value: &str) -> Result<Self, Self::Err> {
698 Self::try_from(value)
699 }
700}
701
702#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
703pub struct EngineId(u32);
705
706impl EngineId {
707 pub fn get(self) -> u32 {
709 self.0
710 }
711}
712
713impl fmt::Display for EngineId {
714 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
715 write!(f, "{}", self.0)
716 }
717}
718
719#[derive(Debug, Clone, PartialEq, Eq)]
720pub struct Engine {
722 id: EngineId,
723 name: EngineName,
724 title: Box<str>,
725}
726
727impl Engine {
728 pub fn try_new(id: i64, name: String, title: String) -> Result<Self, ParseEngineError> {
730 if id <= 0 {
731 return Err(ParseEngineError::NonPositiveId(id));
732 }
733 let id = u32::try_from(id)
734 .map(EngineId)
735 .map_err(|_| ParseEngineError::IdOutOfRange(id))?;
736
737 let name = EngineName::try_from(name)?;
738
739 let title = title.trim();
740 if title.is_empty() {
741 return Err(ParseEngineError::EmptyTitle);
742 }
743
744 Ok(Self {
745 id,
746 name,
747 title: title.to_owned().into_boxed_str(),
748 })
749 }
750
751 pub fn id(&self) -> EngineId {
753 self.id
754 }
755
756 pub fn name(&self) -> &EngineName {
758 &self.name
759 }
760
761 pub fn title(&self) -> &str {
763 self.title.as_ref()
764 }
765}
766
767#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
768pub struct MarketId(u32);
770
771impl MarketId {
772 pub fn get(self) -> u32 {
774 self.0
775 }
776}
777
778impl fmt::Display for MarketId {
779 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
780 write!(f, "{}", self.0)
781 }
782}
783
784#[derive(Debug, Clone, PartialEq, Eq)]
785pub struct Market {
787 id: MarketId,
788 name: MarketName,
789 title: Box<str>,
790}
791
792impl Market {
793 pub fn try_new(id: i64, name: String, title: String) -> Result<Self, ParseMarketError> {
795 if id <= 0 {
796 return Err(ParseMarketError::NonPositiveId(id));
797 }
798 let id = u32::try_from(id)
799 .map(MarketId)
800 .map_err(|_| ParseMarketError::IdOutOfRange(id))?;
801
802 let name = MarketName::try_from(name)?;
803
804 let title = title.trim();
805 if title.is_empty() {
806 return Err(ParseMarketError::EmptyTitle);
807 }
808
809 Ok(Self {
810 id,
811 name,
812 title: title.to_owned().into_boxed_str(),
813 })
814 }
815
816 pub fn id(&self) -> MarketId {
818 self.id
819 }
820
821 pub fn name(&self) -> &MarketName {
823 &self.name
824 }
825
826 pub fn title(&self) -> &str {
828 self.title.as_ref()
829 }
830}
831
832#[derive(Debug, Clone, PartialEq, Eq)]
833pub struct Board {
835 id: u32,
836 board_group_id: u32,
837 boardid: BoardId,
838 title: Box<str>,
839 is_traded: bool,
840}
841
842impl Board {
843 pub fn try_new(
845 id: i64,
846 board_group_id: i64,
847 boardid: String,
848 title: String,
849 is_traded: i64,
850 ) -> Result<Self, ParseBoardError> {
851 if id <= 0 {
852 return Err(ParseBoardError::NonPositiveId(id));
853 }
854 let id = u32::try_from(id).map_err(|_| ParseBoardError::IdOutOfRange(id))?;
855
856 if board_group_id < 0 {
857 return Err(ParseBoardError::NegativeBoardGroupId(board_group_id));
858 }
859 let board_group_id = u32::try_from(board_group_id)
860 .map_err(|_| ParseBoardError::BoardGroupIdOutOfRange(board_group_id))?;
861
862 let boardid = BoardId::try_from(boardid)?;
863
864 let title = title.trim();
865 if title.is_empty() {
866 return Err(ParseBoardError::EmptyTitle);
867 }
868
869 let is_traded = match is_traded {
870 0 => false,
871 1 => true,
872 other => return Err(ParseBoardError::InvalidIsTraded(other)),
873 };
874
875 Ok(Self {
876 id,
877 board_group_id,
878 boardid,
879 title: title.to_owned().into_boxed_str(),
880 is_traded,
881 })
882 }
883
884 pub fn id(&self) -> u32 {
886 self.id
887 }
888
889 pub fn board_group_id(&self) -> u32 {
891 self.board_group_id
892 }
893
894 pub fn boardid(&self) -> &BoardId {
896 &self.boardid
897 }
898
899 pub fn title(&self) -> &str {
901 self.title.as_ref()
902 }
903
904 pub fn is_traded(&self) -> bool {
906 self.is_traded
907 }
908}
909
910#[derive(Debug, Clone, PartialEq, Eq, Hash)]
911pub struct SecurityBoard {
913 engine: EngineName,
914 market: MarketName,
915 boardid: BoardId,
916 is_primary: bool,
917}
918
919impl SecurityBoard {
920 pub fn try_new(
922 engine: String,
923 market: String,
924 boardid: String,
925 is_primary: i64,
926 ) -> Result<Self, ParseSecurityBoardError> {
927 let engine = EngineName::try_from(engine)?;
928 let market = MarketName::try_from(market)?;
929 let boardid = BoardId::try_from(boardid)?;
930 let is_primary = match is_primary {
931 0 => false,
932 1 => true,
933 other => return Err(ParseSecurityBoardError::InvalidIsPrimary(other)),
934 };
935
936 Ok(Self {
937 engine,
938 market,
939 boardid,
940 is_primary,
941 })
942 }
943
944 pub fn engine(&self) -> &EngineName {
946 &self.engine
947 }
948
949 pub fn market(&self) -> &MarketName {
951 &self.market
952 }
953
954 pub fn boardid(&self) -> &BoardId {
956 &self.boardid
957 }
958
959 pub fn is_primary(&self) -> bool {
961 self.is_primary
962 }
963}
964
965#[derive(Debug, Clone, PartialEq, Eq)]
966pub struct Security {
968 secid: SecId,
969 shortname: Box<str>,
970 secname: Box<str>,
971 status: Box<str>,
972}
973
974impl Security {
975 pub fn try_new(
977 secid: String,
978 shortname: String,
979 secname: String,
980 status: String,
981 ) -> Result<Self, ParseSecurityError> {
982 let secid = SecId::try_from(secid)?;
983
984 let shortname = shortname.trim();
985 if shortname.is_empty() {
986 return Err(ParseSecurityError::EmptyShortname);
987 }
988
989 let secname = secname.trim();
990 if secname.is_empty() {
991 return Err(ParseSecurityError::EmptySecname);
992 }
993
994 let status = status.trim();
995 if status.is_empty() {
996 return Err(ParseSecurityError::EmptyStatus);
997 }
998
999 Ok(Self {
1000 secid,
1001 shortname: shortname.to_owned().into_boxed_str(),
1002 secname: secname.to_owned().into_boxed_str(),
1003 status: status.to_owned().into_boxed_str(),
1004 })
1005 }
1006
1007 pub fn secid(&self) -> &SecId {
1009 &self.secid
1010 }
1011
1012 pub fn shortname(&self) -> &str {
1014 self.shortname.as_ref()
1015 }
1016
1017 pub fn secname(&self) -> &str {
1019 self.secname.as_ref()
1020 }
1021
1022 pub fn status(&self) -> &str {
1024 self.status.as_ref()
1025 }
1026}
1027
1028#[derive(Debug, Clone, PartialEq)]
1029pub struct SecuritySnapshot {
1031 secid: SecId,
1032 lot_size: Option<u32>,
1033 last: Option<f64>,
1034}
1035
1036impl SecuritySnapshot {
1037 pub fn try_new(
1039 secid: String,
1040 lot_size: Option<i64>,
1041 last: Option<f64>,
1042 ) -> Result<Self, ParseSecuritySnapshotError> {
1043 let secid = SecId::try_from(secid).map_err(ParseSecuritySnapshotError::InvalidSecId)?;
1044 let lot_size = match lot_size {
1045 None => None,
1046 Some(raw) if raw < 0 => return Err(ParseSecuritySnapshotError::NegativeLotSize(raw)),
1047 Some(raw) => Some(
1048 u32::try_from(raw)
1049 .map_err(|_| ParseSecuritySnapshotError::LotSizeOutOfRange(raw))?,
1050 ),
1051 };
1052 Self::try_from_parts(secid, lot_size, last)
1053 }
1054
1055 pub(crate) fn try_from_parts(
1057 secid: SecId,
1058 lot_size: Option<u32>,
1059 last: Option<f64>,
1060 ) -> Result<Self, ParseSecuritySnapshotError> {
1061 if let Some(last) = last
1063 && !last.is_finite()
1064 {
1065 return Err(ParseSecuritySnapshotError::NonFiniteLast(last));
1066 }
1067
1068 Ok(Self {
1069 secid,
1070 lot_size,
1071 last,
1072 })
1073 }
1074
1075 pub fn secid(&self) -> &SecId {
1077 &self.secid
1078 }
1079
1080 pub fn lot_size(&self) -> Option<u32> {
1082 self.lot_size
1083 }
1084
1085 pub fn last(&self) -> Option<f64> {
1087 self.last
1088 }
1089}
1090
1091#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1092pub enum CandleInterval {
1094 Minute1,
1096 Minute10,
1098 Hour1,
1100 Day1,
1102 Week1,
1104 Month1,
1106 Quarter1,
1108}
1109
1110impl CandleInterval {
1111 pub fn as_str(self) -> &'static str {
1113 match self {
1114 Self::Minute1 => "1",
1115 Self::Minute10 => "10",
1116 Self::Hour1 => "60",
1117 Self::Day1 => "24",
1118 Self::Week1 => "7",
1119 Self::Month1 => "31",
1120 Self::Quarter1 => "4",
1121 }
1122 }
1123}
1124
1125impl TryFrom<i64> for CandleInterval {
1126 type Error = ParseCandleIntervalError;
1127
1128 fn try_from(value: i64) -> Result<Self, Self::Error> {
1129 match value {
1130 1 => Ok(Self::Minute1),
1131 10 => Ok(Self::Minute10),
1132 60 => Ok(Self::Hour1),
1133 24 => Ok(Self::Day1),
1134 7 => Ok(Self::Week1),
1135 31 => Ok(Self::Month1),
1136 4 => Ok(Self::Quarter1),
1137 other => Err(ParseCandleIntervalError::InvalidCode(other)),
1138 }
1139 }
1140}
1141
1142#[derive(Debug, Clone, PartialEq, Eq)]
1143pub struct CandleBorder {
1145 begin: NaiveDateTime,
1146 end: NaiveDateTime,
1147 interval: CandleInterval,
1148 board_group_id: u32,
1149}
1150
1151impl CandleBorder {
1152 pub fn try_new(
1154 begin: NaiveDateTime,
1155 end: NaiveDateTime,
1156 interval: i64,
1157 board_group_id: i64,
1158 ) -> Result<Self, ParseCandleBorderError> {
1159 if begin > end {
1160 return Err(ParseCandleBorderError::InvalidDateRange { begin, end });
1161 }
1162
1163 let interval = CandleInterval::try_from(interval)?;
1164 if board_group_id < 0 {
1165 return Err(ParseCandleBorderError::NegativeBoardGroupId(board_group_id));
1166 }
1167 let board_group_id = u32::try_from(board_group_id)
1168 .map_err(|_| ParseCandleBorderError::BoardGroupIdOutOfRange(board_group_id))?;
1169
1170 Ok(Self {
1171 begin,
1172 end,
1173 interval,
1174 board_group_id,
1175 })
1176 }
1177
1178 pub fn begin(&self) -> NaiveDateTime {
1180 self.begin
1181 }
1182
1183 pub fn end(&self) -> NaiveDateTime {
1185 self.end
1186 }
1187
1188 pub fn interval(&self) -> CandleInterval {
1190 self.interval
1191 }
1192
1193 pub fn board_group_id(&self) -> u32 {
1195 self.board_group_id
1196 }
1197}
1198
1199#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1200pub struct CandleQuery {
1202 from: Option<NaiveDateTime>,
1203 till: Option<NaiveDateTime>,
1204 interval: Option<CandleInterval>,
1205}
1206
1207impl CandleQuery {
1208 pub fn try_new(
1210 from: Option<NaiveDateTime>,
1211 till: Option<NaiveDateTime>,
1212 interval: Option<CandleInterval>,
1213 ) -> Result<Self, ParseCandleQueryError> {
1214 if let (Some(from), Some(till)) = (from, till)
1215 && from > till
1216 {
1217 return Err(ParseCandleQueryError::InvalidDateRange { from, till });
1218 }
1219
1220 Ok(Self {
1221 from,
1222 till,
1223 interval,
1224 })
1225 }
1226
1227 pub fn from(&self) -> Option<NaiveDateTime> {
1229 self.from
1230 }
1231
1232 pub fn till(&self) -> Option<NaiveDateTime> {
1234 self.till
1235 }
1236
1237 pub fn interval(&self) -> Option<CandleInterval> {
1239 self.interval
1240 }
1241
1242 pub fn with_from(self, from: NaiveDateTime) -> Result<Self, ParseCandleQueryError> {
1244 Self::try_new(Some(from), self.till, self.interval)
1245 }
1246
1247 pub fn with_till(self, till: NaiveDateTime) -> Result<Self, ParseCandleQueryError> {
1249 Self::try_new(self.from, Some(till), self.interval)
1250 }
1251
1252 pub fn with_interval(mut self, interval: CandleInterval) -> Self {
1254 self.interval = Some(interval);
1255 self
1256 }
1257}
1258
1259#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
1260pub struct Pagination {
1262 pub start: Option<u32>,
1264 pub limit: Option<NonZeroU32>,
1266}
1267
1268impl Pagination {
1269 pub fn with_start(mut self, start: u32) -> Self {
1271 self.start = Some(start);
1272 self
1273 }
1274
1275 pub fn with_limit(mut self, limit: NonZeroU32) -> Self {
1277 self.limit = Some(limit);
1278 self
1279 }
1280}
1281
1282#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1283pub enum PageRequest {
1288 #[default]
1290 FirstPage,
1291 Page(Pagination),
1293 All {
1295 page_limit: NonZeroU32,
1297 },
1298}
1299
1300impl PageRequest {
1301 pub fn first_page() -> Self {
1303 Self::FirstPage
1304 }
1305
1306 pub fn page(pagination: Pagination) -> Self {
1308 Self::Page(pagination)
1309 }
1310
1311 pub fn all(page_limit: NonZeroU32) -> Self {
1313 Self::All { page_limit }
1314 }
1315}
1316
1317#[derive(Debug, Clone, PartialEq)]
1318pub struct Candle {
1320 begin: NaiveDateTime,
1321 end: NaiveDateTime,
1322 open: Option<f64>,
1323 close: Option<f64>,
1324 high: Option<f64>,
1325 low: Option<f64>,
1326 value: Option<f64>,
1327 volume: Option<u64>,
1328}
1329
1330#[derive(Debug, Clone, Copy, PartialEq)]
1331pub struct CandleOhlcv {
1333 open: Option<f64>,
1334 close: Option<f64>,
1335 high: Option<f64>,
1336 low: Option<f64>,
1337 value: Option<f64>,
1338 volume: Option<i64>,
1339}
1340
1341impl CandleOhlcv {
1342 pub fn new(
1344 open: Option<f64>,
1345 close: Option<f64>,
1346 high: Option<f64>,
1347 low: Option<f64>,
1348 value: Option<f64>,
1349 volume: Option<i64>,
1350 ) -> Self {
1351 Self {
1352 open,
1353 close,
1354 high,
1355 low,
1356 value,
1357 volume,
1358 }
1359 }
1360}
1361
1362impl Candle {
1363 pub fn try_new(
1365 begin: NaiveDateTime,
1366 end: NaiveDateTime,
1367 ohlcv: CandleOhlcv,
1368 ) -> Result<Self, ParseCandleError> {
1369 if begin > end {
1370 return Err(ParseCandleError::InvalidDateRange { begin, end });
1371 }
1372
1373 let volume = match ohlcv.volume {
1374 None => None,
1375 Some(raw) if raw >= 0 => Some(raw as u64),
1376 Some(raw) => return Err(ParseCandleError::NegativeVolume(raw)),
1377 };
1378
1379 Ok(Self {
1380 begin,
1381 end,
1382 open: ohlcv.open,
1383 close: ohlcv.close,
1384 high: ohlcv.high,
1385 low: ohlcv.low,
1386 value: ohlcv.value,
1387 volume,
1388 })
1389 }
1390
1391 pub fn begin(&self) -> NaiveDateTime {
1393 self.begin
1394 }
1395
1396 pub fn end(&self) -> NaiveDateTime {
1398 self.end
1399 }
1400
1401 pub fn open(&self) -> Option<f64> {
1403 self.open
1404 }
1405
1406 pub fn close(&self) -> Option<f64> {
1408 self.close
1409 }
1410
1411 pub fn high(&self) -> Option<f64> {
1413 self.high
1414 }
1415
1416 pub fn low(&self) -> Option<f64> {
1418 self.low
1419 }
1420
1421 pub fn value(&self) -> Option<f64> {
1423 self.value
1424 }
1425
1426 pub fn volume(&self) -> Option<u64> {
1428 self.volume
1429 }
1430}
1431
1432#[derive(Debug, Clone, PartialEq)]
1433pub struct Trade {
1435 tradeno: u64,
1436 tradetime: NaiveTime,
1437 price: Option<f64>,
1438 quantity: Option<u64>,
1439 value: Option<f64>,
1440}
1441
1442impl Trade {
1443 pub fn try_new(
1445 tradeno: i64,
1446 tradetime: NaiveTime,
1447 price: Option<f64>,
1448 quantity: Option<i64>,
1449 value: Option<f64>,
1450 ) -> Result<Self, ParseTradeError> {
1451 if tradeno <= 0 {
1452 return Err(ParseTradeError::NonPositiveTradeNo(tradeno));
1453 }
1454 let tradeno =
1455 u64::try_from(tradeno).map_err(|_| ParseTradeError::TradeNoOutOfRange(tradeno))?;
1456
1457 let quantity = match quantity {
1458 None => None,
1459 Some(raw) if raw >= 0 => Some(raw as u64),
1460 Some(raw) => return Err(ParseTradeError::NegativeQuantity(raw)),
1461 };
1462
1463 Ok(Self {
1464 tradeno,
1465 tradetime,
1466 price,
1467 quantity,
1468 value,
1469 })
1470 }
1471
1472 pub fn tradeno(&self) -> u64 {
1474 self.tradeno
1475 }
1476
1477 pub fn tradetime(&self) -> NaiveTime {
1479 self.tradetime
1480 }
1481
1482 pub fn price(&self) -> Option<f64> {
1484 self.price
1485 }
1486
1487 pub fn quantity(&self) -> Option<u64> {
1489 self.quantity
1490 }
1491
1492 pub fn value(&self) -> Option<f64> {
1494 self.value
1495 }
1496}
1497
1498#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1499pub enum BuySell {
1501 Buy,
1503 Sell,
1505}
1506
1507impl BuySell {
1508 pub fn as_str(self) -> &'static str {
1510 match self {
1511 Self::Buy => "B",
1512 Self::Sell => "S",
1513 }
1514 }
1515}
1516
1517impl TryFrom<String> for BuySell {
1518 type Error = ParseOrderbookError;
1519
1520 fn try_from(value: String) -> Result<Self, Self::Error> {
1521 let value = value.trim();
1522 match value {
1523 "B" => Ok(Self::Buy),
1524 "S" => Ok(Self::Sell),
1525 _ => Err(ParseOrderbookError::InvalidSide(
1526 value.to_owned().into_boxed_str(),
1527 )),
1528 }
1529 }
1530}
1531
1532#[derive(Debug, Clone, PartialEq)]
1533pub struct OrderbookLevel {
1535 buy_sell: BuySell,
1536 price: f64,
1537 quantity: u64,
1538}
1539
1540impl OrderbookLevel {
1541 pub fn try_new(
1543 buy_sell: String,
1544 price: Option<f64>,
1545 quantity: Option<i64>,
1546 ) -> Result<Self, ParseOrderbookError> {
1547 let buy_sell = BuySell::try_from(buy_sell)?;
1548
1549 let Some(price) = price else {
1550 return Err(ParseOrderbookError::MissingPrice);
1551 };
1552 if price.is_sign_negative() {
1553 return Err(ParseOrderbookError::NegativePrice);
1554 }
1555
1556 let Some(quantity) = quantity else {
1557 return Err(ParseOrderbookError::MissingQuantity);
1558 };
1559 let quantity = match quantity {
1560 raw if raw >= 0 => raw as u64,
1561 raw => return Err(ParseOrderbookError::NegativeQuantity(raw)),
1562 };
1563
1564 Ok(Self {
1565 buy_sell,
1566 price,
1567 quantity,
1568 })
1569 }
1570
1571 pub fn buy_sell(&self) -> BuySell {
1573 self.buy_sell
1574 }
1575
1576 pub fn price(&self) -> f64 {
1578 self.price
1579 }
1580
1581 pub fn quantity(&self) -> u64 {
1583 self.quantity
1584 }
1585}
1586
1587impl TryFrom<String> for IndexId {
1588 type Error = ParseIndexError;
1589
1590 fn try_from(value: String) -> Result<Self, Self::Error> {
1591 Self::try_from(value.as_str())
1592 }
1593}
1594
1595impl TryFrom<&str> for IndexId {
1596 type Error = ParseIndexError;
1597
1598 fn try_from(value: &str) -> Result<Self, Self::Error> {
1599 let value = value.trim();
1600 if value.is_empty() {
1601 return Err(ParseIndexError::EmptyIndexId);
1602 }
1603 Ok(Self(value.to_owned().into_boxed_str()))
1604 }
1605}
1606
1607impl FromStr for IndexId {
1608 type Err = ParseIndexError;
1609
1610 fn from_str(value: &str) -> Result<Self, Self::Err> {
1611 Self::try_from(value)
1612 }
1613}
1614
1615#[derive(Debug, Clone, PartialEq, Eq)]
1616pub struct Index {
1618 id: IndexId,
1619 short_name: Box<str>,
1620 from: Option<NaiveDate>,
1621 till: Option<NaiveDate>,
1622}
1623
1624#[derive(Debug, Clone, PartialEq, Eq)]
1625pub struct HistoryDates {
1627 from: NaiveDate,
1628 till: NaiveDate,
1629}
1630
1631impl HistoryDates {
1632 pub fn try_new(from: NaiveDate, till: NaiveDate) -> Result<Self, ParseHistoryDatesError> {
1634 if from > till {
1635 return Err(ParseHistoryDatesError::InvalidDateRange { from, till });
1636 }
1637 Ok(Self { from, till })
1638 }
1639
1640 pub fn from(&self) -> NaiveDate {
1642 self.from
1643 }
1644
1645 pub fn till(&self) -> NaiveDate {
1647 self.till
1648 }
1649}
1650
1651#[derive(Debug, Clone, PartialEq)]
1652pub struct HistoryRecord {
1654 boardid: BoardId,
1655 tradedate: NaiveDate,
1656 secid: SecId,
1657 numtrades: Option<u64>,
1658 value: Option<f64>,
1659 open: Option<f64>,
1660 low: Option<f64>,
1661 high: Option<f64>,
1662 close: Option<f64>,
1663 volume: Option<u64>,
1664}
1665
1666#[derive(Debug, Clone, PartialEq)]
1667pub struct Turnover {
1669 name: Box<str>,
1670 id: u32,
1671 valtoday: Option<f64>,
1672 valtoday_usd: Option<f64>,
1673 numtrades: Option<u64>,
1674 updatetime: NaiveDateTime,
1675 title: Box<str>,
1676}
1677
1678impl Turnover {
1679 pub fn try_new(
1681 name: String,
1682 id: i64,
1683 valtoday: Option<f64>,
1684 valtoday_usd: Option<f64>,
1685 numtrades: Option<i64>,
1686 updatetime: NaiveDateTime,
1687 title: String,
1688 ) -> Result<Self, ParseTurnoverError> {
1689 let name = name.trim();
1690 if name.is_empty() {
1691 return Err(ParseTurnoverError::EmptyName);
1692 }
1693
1694 if id <= 0 {
1695 return Err(ParseTurnoverError::NonPositiveId(id));
1696 }
1697 let id = u32::try_from(id).map_err(|_| ParseTurnoverError::IdOutOfRange(id))?;
1698
1699 let numtrades = match numtrades {
1700 None => None,
1701 Some(raw) if raw >= 0 => Some(raw as u64),
1702 Some(raw) => return Err(ParseTurnoverError::NegativeNumTrades(raw)),
1703 };
1704
1705 let title = title.trim();
1706 if title.is_empty() {
1707 return Err(ParseTurnoverError::EmptyTitle);
1708 }
1709
1710 Ok(Self {
1711 name: name.to_owned().into_boxed_str(),
1712 id,
1713 valtoday,
1714 valtoday_usd,
1715 numtrades,
1716 updatetime,
1717 title: title.to_owned().into_boxed_str(),
1718 })
1719 }
1720
1721 pub fn name(&self) -> &str {
1723 self.name.as_ref()
1724 }
1725
1726 pub fn id(&self) -> u32 {
1728 self.id
1729 }
1730
1731 pub fn valtoday(&self) -> Option<f64> {
1733 self.valtoday
1734 }
1735
1736 pub fn valtoday_usd(&self) -> Option<f64> {
1738 self.valtoday_usd
1739 }
1740
1741 pub fn numtrades(&self) -> Option<u64> {
1743 self.numtrades
1744 }
1745
1746 pub fn updatetime(&self) -> NaiveDateTime {
1748 self.updatetime
1749 }
1750
1751 pub fn title(&self) -> &str {
1753 self.title.as_ref()
1754 }
1755}
1756
1757#[derive(Debug, Clone, PartialEq)]
1758pub struct SecStat {
1760 secid: SecId,
1761 boardid: BoardId,
1762 voltoday: Option<u64>,
1763 valtoday: Option<f64>,
1764 highbid: Option<f64>,
1765 lowoffer: Option<f64>,
1766 lastoffer: Option<f64>,
1767 lastbid: Option<f64>,
1768 open: Option<f64>,
1769 low: Option<f64>,
1770 high: Option<f64>,
1771 last: Option<f64>,
1772 numtrades: Option<u64>,
1773 waprice: Option<f64>,
1774}
1775
1776#[derive(Debug, Clone, PartialEq, Eq)]
1777pub struct SiteNews {
1779 id: u64,
1780 tag: Box<str>,
1781 title: Box<str>,
1782 published_at: NaiveDateTime,
1783 modified_at: NaiveDateTime,
1784}
1785
1786impl SiteNews {
1787 pub fn try_new(
1789 id: i64,
1790 tag: String,
1791 title: String,
1792 published_at: NaiveDateTime,
1793 modified_at: NaiveDateTime,
1794 ) -> Result<Self, ParseSiteNewsError> {
1795 if id <= 0 {
1796 return Err(ParseSiteNewsError::NonPositiveId(id));
1797 }
1798 let id = u64::try_from(id).map_err(|_| ParseSiteNewsError::IdOutOfRange(id))?;
1799
1800 let tag = tag.trim();
1801 if tag.is_empty() {
1802 return Err(ParseSiteNewsError::EmptyTag);
1803 }
1804 let title = title.trim();
1805 if title.is_empty() {
1806 return Err(ParseSiteNewsError::EmptyTitle);
1807 }
1808
1809 Ok(Self {
1810 id,
1811 tag: tag.to_owned().into_boxed_str(),
1812 title: title.to_owned().into_boxed_str(),
1813 published_at,
1814 modified_at,
1815 })
1816 }
1817
1818 pub fn id(&self) -> u64 {
1820 self.id
1821 }
1822
1823 pub fn tag(&self) -> &str {
1825 self.tag.as_ref()
1826 }
1827
1828 pub fn title(&self) -> &str {
1830 self.title.as_ref()
1831 }
1832
1833 pub fn published_at(&self) -> NaiveDateTime {
1835 self.published_at
1836 }
1837
1838 pub fn modified_at(&self) -> NaiveDateTime {
1840 self.modified_at
1841 }
1842}
1843
1844#[derive(Debug, Clone, PartialEq, Eq)]
1845pub struct Event {
1847 id: u64,
1848 tag: Box<str>,
1849 title: Box<str>,
1850 from: Option<NaiveDateTime>,
1851 modified_at: NaiveDateTime,
1852}
1853
1854impl Event {
1855 pub fn try_new(
1857 id: i64,
1858 tag: String,
1859 title: String,
1860 from: Option<NaiveDateTime>,
1861 modified_at: NaiveDateTime,
1862 ) -> Result<Self, ParseEventError> {
1863 if id <= 0 {
1864 return Err(ParseEventError::NonPositiveId(id));
1865 }
1866 let id = u64::try_from(id).map_err(|_| ParseEventError::IdOutOfRange(id))?;
1867
1868 let tag = tag.trim();
1869 if tag.is_empty() {
1870 return Err(ParseEventError::EmptyTag);
1871 }
1872 let title = title.trim();
1873 if title.is_empty() {
1874 return Err(ParseEventError::EmptyTitle);
1875 }
1876
1877 Ok(Self {
1878 id,
1879 tag: tag.to_owned().into_boxed_str(),
1880 title: title.to_owned().into_boxed_str(),
1881 from,
1882 modified_at,
1883 })
1884 }
1885
1886 pub fn id(&self) -> u64 {
1888 self.id
1889 }
1890
1891 pub fn tag(&self) -> &str {
1893 self.tag.as_ref()
1894 }
1895
1896 pub fn title(&self) -> &str {
1898 self.title.as_ref()
1899 }
1900
1901 pub fn from(&self) -> Option<NaiveDateTime> {
1903 self.from
1904 }
1905
1906 pub fn modified_at(&self) -> NaiveDateTime {
1908 self.modified_at
1909 }
1910}
1911
1912pub(crate) struct SecStatInput {
1917 pub(crate) secid: String,
1918 pub(crate) boardid: String,
1919 pub(crate) voltoday: Option<i64>,
1920 pub(crate) valtoday: Option<f64>,
1921 pub(crate) highbid: Option<f64>,
1922 pub(crate) lowoffer: Option<f64>,
1923 pub(crate) lastoffer: Option<f64>,
1924 pub(crate) lastbid: Option<f64>,
1925 pub(crate) open: Option<f64>,
1926 pub(crate) low: Option<f64>,
1927 pub(crate) high: Option<f64>,
1928 pub(crate) last: Option<f64>,
1929 pub(crate) numtrades: Option<i64>,
1930 pub(crate) waprice: Option<f64>,
1931}
1932
1933impl SecStat {
1934 pub(crate) fn try_new(input: SecStatInput) -> Result<Self, ParseSecStatError> {
1936 let SecStatInput {
1937 secid,
1938 boardid,
1939 voltoday,
1940 valtoday,
1941 highbid,
1942 lowoffer,
1943 lastoffer,
1944 lastbid,
1945 open,
1946 low,
1947 high,
1948 last,
1949 numtrades,
1950 waprice,
1951 } = input;
1952
1953 let secid = SecId::try_from(secid)?;
1954 let boardid = BoardId::try_from(boardid)?;
1955
1956 let voltoday = match voltoday {
1957 None => None,
1958 Some(raw) if raw >= 0 => Some(raw as u64),
1959 Some(raw) => return Err(ParseSecStatError::NegativeVolToday(raw)),
1960 };
1961
1962 let numtrades = match numtrades {
1963 None => None,
1964 Some(raw) if raw >= 0 => Some(raw as u64),
1965 Some(raw) => return Err(ParseSecStatError::NegativeNumTrades(raw)),
1966 };
1967
1968 Ok(Self {
1969 secid,
1970 boardid,
1971 voltoday,
1972 valtoday,
1973 highbid,
1974 lowoffer,
1975 lastoffer,
1976 lastbid,
1977 open,
1978 low,
1979 high,
1980 last,
1981 numtrades,
1982 waprice,
1983 })
1984 }
1985
1986 pub fn secid(&self) -> &SecId {
1988 &self.secid
1989 }
1990
1991 pub fn boardid(&self) -> &BoardId {
1993 &self.boardid
1994 }
1995
1996 pub fn voltoday(&self) -> Option<u64> {
1998 self.voltoday
1999 }
2000
2001 pub fn valtoday(&self) -> Option<f64> {
2003 self.valtoday
2004 }
2005
2006 pub fn highbid(&self) -> Option<f64> {
2008 self.highbid
2009 }
2010
2011 pub fn lowoffer(&self) -> Option<f64> {
2013 self.lowoffer
2014 }
2015
2016 pub fn lastoffer(&self) -> Option<f64> {
2018 self.lastoffer
2019 }
2020
2021 pub fn lastbid(&self) -> Option<f64> {
2023 self.lastbid
2024 }
2025
2026 pub fn open(&self) -> Option<f64> {
2028 self.open
2029 }
2030
2031 pub fn low(&self) -> Option<f64> {
2033 self.low
2034 }
2035
2036 pub fn high(&self) -> Option<f64> {
2038 self.high
2039 }
2040
2041 pub fn last(&self) -> Option<f64> {
2043 self.last
2044 }
2045
2046 pub fn numtrades(&self) -> Option<u64> {
2048 self.numtrades
2049 }
2050
2051 pub fn waprice(&self) -> Option<f64> {
2053 self.waprice
2054 }
2055}
2056
2057pub(crate) struct HistoryRecordInput {
2059 pub(crate) boardid: String,
2060 pub(crate) tradedate: NaiveDate,
2061 pub(crate) secid: String,
2062 pub(crate) numtrades: Option<i64>,
2063 pub(crate) value: Option<f64>,
2064 pub(crate) open: Option<f64>,
2065 pub(crate) low: Option<f64>,
2066 pub(crate) high: Option<f64>,
2067 pub(crate) close: Option<f64>,
2068 pub(crate) volume: Option<i64>,
2069}
2070
2071impl HistoryRecord {
2072 pub(crate) fn try_new(input: HistoryRecordInput) -> Result<Self, ParseHistoryRecordError> {
2074 let HistoryRecordInput {
2075 boardid,
2076 tradedate,
2077 secid,
2078 numtrades,
2079 value,
2080 open,
2081 low,
2082 high,
2083 close,
2084 volume,
2085 } = input;
2086
2087 let boardid = BoardId::try_from(boardid)?;
2088 let secid = SecId::try_from(secid)?;
2089
2090 let numtrades = match numtrades {
2091 None => None,
2092 Some(raw) if raw >= 0 => Some(raw as u64),
2093 Some(raw) => return Err(ParseHistoryRecordError::NegativeNumTrades(raw)),
2094 };
2095
2096 let volume = match volume {
2097 None => None,
2098 Some(raw) if raw >= 0 => Some(raw as u64),
2099 Some(raw) => return Err(ParseHistoryRecordError::NegativeVolume(raw)),
2100 };
2101
2102 Ok(Self {
2103 boardid,
2104 tradedate,
2105 secid,
2106 numtrades,
2107 value,
2108 open,
2109 low,
2110 high,
2111 close,
2112 volume,
2113 })
2114 }
2115
2116 pub fn boardid(&self) -> &BoardId {
2118 &self.boardid
2119 }
2120
2121 pub fn tradedate(&self) -> NaiveDate {
2123 self.tradedate
2124 }
2125
2126 pub fn secid(&self) -> &SecId {
2128 &self.secid
2129 }
2130
2131 pub fn numtrades(&self) -> Option<u64> {
2133 self.numtrades
2134 }
2135
2136 pub fn value(&self) -> Option<f64> {
2138 self.value
2139 }
2140
2141 pub fn open(&self) -> Option<f64> {
2143 self.open
2144 }
2145
2146 pub fn low(&self) -> Option<f64> {
2148 self.low
2149 }
2150
2151 pub fn high(&self) -> Option<f64> {
2153 self.high
2154 }
2155
2156 pub fn close(&self) -> Option<f64> {
2158 self.close
2159 }
2160
2161 pub fn volume(&self) -> Option<u64> {
2163 self.volume
2164 }
2165}
2166
2167impl Index {
2168 pub fn try_new(
2170 id: String,
2171 short_name: String,
2172 from: Option<NaiveDate>,
2173 till: Option<NaiveDate>,
2174 ) -> Result<Self, ParseIndexError> {
2175 let id = IndexId::try_from(id)?;
2176 let short_name = short_name.trim();
2177 if short_name.is_empty() {
2178 return Err(ParseIndexError::EmptyShortName);
2179 }
2180 if let (Some(from_date), Some(till_date)) = (from, till)
2181 && from_date > till_date
2182 {
2183 return Err(ParseIndexError::InvalidDateRange {
2184 from: from_date,
2185 till: till_date,
2186 });
2187 }
2188
2189 Ok(Self {
2190 id,
2191 short_name: short_name.to_owned().into_boxed_str(),
2192 from,
2193 till,
2194 })
2195 }
2196
2197 pub fn id(&self) -> &IndexId {
2199 &self.id
2200 }
2201
2202 pub fn short_name(&self) -> &str {
2204 self.short_name.as_ref()
2205 }
2206
2207 pub fn from(&self) -> Option<NaiveDate> {
2209 self.from
2210 }
2211
2212 pub fn till(&self) -> Option<NaiveDate> {
2214 self.till
2215 }
2216
2217 pub fn is_active_on(&self, date: NaiveDate) -> bool {
2219 self.from.is_none_or(|from| from <= date) && self.till.is_none_or(|till| date <= till)
2220 }
2221}
2222
2223#[derive(Debug, Clone, PartialEq)]
2224pub struct IndexAnalytics {
2226 indexid: IndexId,
2227 tradedate: NaiveDate,
2228 ticker: SecId,
2229 shortnames: Box<str>,
2230 secid: SecId,
2231 weight: f64,
2232 tradingsession: u8,
2233 trade_session_date: NaiveDate,
2234}
2235
2236pub(crate) struct IndexAnalyticsInput {
2238 pub(crate) indexid: String,
2239 pub(crate) tradedate: NaiveDate,
2240 pub(crate) ticker: String,
2241 pub(crate) shortnames: String,
2242 pub(crate) secid: String,
2243 pub(crate) weight: f64,
2244 pub(crate) tradingsession: i64,
2245 pub(crate) trade_session_date: NaiveDate,
2246}
2247
2248impl IndexAnalytics {
2249 pub(crate) fn try_new(input: IndexAnalyticsInput) -> Result<Self, ParseIndexAnalyticsError> {
2251 let IndexAnalyticsInput {
2252 indexid,
2253 tradedate,
2254 ticker,
2255 shortnames,
2256 secid,
2257 weight,
2258 tradingsession,
2259 trade_session_date,
2260 } = input;
2261
2262 let indexid = IndexId::try_from(indexid)?;
2263 let ticker = SecId::try_from(ticker).map_err(ParseIndexAnalyticsError::InvalidTicker)?;
2264 let secid = SecId::try_from(secid).map_err(ParseIndexAnalyticsError::InvalidSecId)?;
2265
2266 let shortnames = shortnames.trim();
2267 if shortnames.is_empty() {
2268 return Err(ParseIndexAnalyticsError::EmptyShortnames);
2269 }
2270 if !weight.is_finite() {
2271 return Err(ParseIndexAnalyticsError::NonFiniteWeight);
2272 }
2273 if weight.is_sign_negative() {
2274 return Err(ParseIndexAnalyticsError::NegativeWeight);
2275 }
2276 if !(1..=3).contains(&tradingsession) {
2277 return Err(ParseIndexAnalyticsError::InvalidTradingsession(
2278 tradingsession,
2279 ));
2280 }
2281
2282 Ok(Self {
2283 indexid,
2284 tradedate,
2285 ticker,
2286 shortnames: shortnames.to_owned().into_boxed_str(),
2287 secid,
2288 weight,
2289 tradingsession: tradingsession as u8,
2290 trade_session_date,
2291 })
2292 }
2293
2294 pub fn indexid(&self) -> &IndexId {
2296 &self.indexid
2297 }
2298
2299 pub fn tradedate(&self) -> NaiveDate {
2301 self.tradedate
2302 }
2303
2304 pub fn ticker(&self) -> &SecId {
2306 &self.ticker
2307 }
2308
2309 pub fn shortnames(&self) -> &str {
2311 self.shortnames.as_ref()
2312 }
2313
2314 pub fn secid(&self) -> &SecId {
2316 &self.secid
2317 }
2318
2319 pub fn weight(&self) -> f64 {
2321 self.weight
2322 }
2323
2324 pub fn tradingsession(&self) -> u8 {
2326 self.tradingsession
2327 }
2328
2329 pub fn trade_session_date(&self) -> NaiveDate {
2331 self.trade_session_date
2332 }
2333}
2334
2335pub fn actual_indexes(indexes: &[Index]) -> impl Iterator<Item = &Index> {
2337 let latest_till = indexes.iter().filter_map(Index::till).max();
2338 indexes
2339 .iter()
2340 .filter(move |index| index.till() == latest_till)
2341}