use std::convert::Infallible;
use std::fmt;
use std::num::NonZeroU32;
use std::str::FromStr;
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct IndexId(Box<str>);
impl IndexId {
pub fn as_str(&self) -> &str {
self.0.as_ref()
}
}
impl fmt::Display for IndexId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl AsRef<str> for IndexId {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl From<&IndexId> for IndexId {
fn from(value: &IndexId) -> Self {
value.clone()
}
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ParseIndexError {
#[error("index id must not be empty")]
EmptyIndexId,
#[error("index short name must not be empty")]
EmptyShortName,
#[error("invalid index date range: from={from} is after till={till}")]
InvalidDateRange {
from: NaiveDate,
till: NaiveDate,
},
}
impl From<Infallible> for ParseIndexError {
fn from(value: Infallible) -> Self {
match value {}
}
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ParseHistoryDatesError {
#[error("invalid history dates range: from={from} is after till={till}")]
InvalidDateRange {
from: NaiveDate,
till: NaiveDate,
},
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ParseHistoryRecordError {
#[error(transparent)]
InvalidBoardId(#[from] ParseBoardIdError),
#[error(transparent)]
InvalidSecId(#[from] ParseSecIdError),
#[error("history numtrades must not be negative, got {0}")]
NegativeNumTrades(i64),
#[error("history volume must not be negative, got {0}")]
NegativeVolume(i64),
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ParseTurnoverError {
#[error("turnover name must not be empty")]
EmptyName,
#[error("turnover id must be positive, got {0}")]
NonPositiveId(i64),
#[error("turnover id is out of range for u32, got {0}")]
IdOutOfRange(i64),
#[error("turnover numtrades must not be negative, got {0}")]
NegativeNumTrades(i64),
#[error("turnover title must not be empty")]
EmptyTitle,
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ParseSecStatError {
#[error(transparent)]
InvalidSecId(#[from] ParseSecIdError),
#[error(transparent)]
InvalidBoardId(#[from] ParseBoardIdError),
#[error("secstats voltoday must not be negative, got {0}")]
NegativeVolToday(i64),
#[error("secstats numtrades must not be negative, got {0}")]
NegativeNumTrades(i64),
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ParseSiteNewsError {
#[error("sitenews id must be positive, got {0}")]
NonPositiveId(i64),
#[error("sitenews id is out of range for u64, got {0}")]
IdOutOfRange(i64),
#[error("sitenews tag must not be empty")]
EmptyTag,
#[error("sitenews title must not be empty")]
EmptyTitle,
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ParseEventError {
#[error("events id must be positive, got {0}")]
NonPositiveId(i64),
#[error("events id is out of range for u64, got {0}")]
IdOutOfRange(i64),
#[error("events tag must not be empty")]
EmptyTag,
#[error("events title must not be empty")]
EmptyTitle,
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ParseIndexAnalyticsError {
#[error(transparent)]
InvalidIndexId(#[from] ParseIndexError),
#[error("ticker is invalid: {0}")]
InvalidTicker(ParseSecIdError),
#[error("secid is invalid: {0}")]
InvalidSecId(ParseSecIdError),
#[error("shortnames must not be empty")]
EmptyShortnames,
#[error("weight must be finite")]
NonFiniteWeight,
#[error("weight must not be negative")]
NegativeWeight,
#[error("tradingsession must be 1, 2 or 3, got {0}")]
InvalidTradingsession(i64),
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ParseEngineError {
#[error("engine id must be positive, got {0}")]
NonPositiveId(i64),
#[error("engine id is out of range for u32, got {0}")]
IdOutOfRange(i64),
#[error(transparent)]
InvalidName(#[from] ParseEngineNameError),
#[error("engine title must not be empty")]
EmptyTitle,
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ParseEngineNameError {
#[error("engine name must not be empty")]
Empty,
#[error("engine name must not contain '/'")]
ContainsSlash,
}
impl From<Infallible> for ParseEngineNameError {
fn from(value: Infallible) -> Self {
match value {}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct EngineName(Box<str>);
impl EngineName {
pub fn as_str(&self) -> &str {
self.0.as_ref()
}
}
impl fmt::Display for EngineName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl AsRef<str> for EngineName {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl From<&EngineName> for EngineName {
fn from(value: &EngineName) -> Self {
value.clone()
}
}
impl TryFrom<String> for EngineName {
type Error = ParseEngineNameError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::try_from(value.as_str())
}
}
impl TryFrom<&str> for EngineName {
type Error = ParseEngineNameError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let value = value.trim();
if value.is_empty() {
return Err(ParseEngineNameError::Empty);
}
if value.contains('/') {
return Err(ParseEngineNameError::ContainsSlash);
}
Ok(Self(value.to_owned().into_boxed_str()))
}
}
impl FromStr for EngineName {
type Err = ParseEngineNameError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::try_from(value)
}
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ParseMarketError {
#[error("market id must be positive, got {0}")]
NonPositiveId(i64),
#[error("market id is out of range for u32, got {0}")]
IdOutOfRange(i64),
#[error(transparent)]
InvalidName(#[from] ParseMarketNameError),
#[error("market title must not be empty")]
EmptyTitle,
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ParseBoardError {
#[error("board id must be positive, got {0}")]
NonPositiveId(i64),
#[error("board id is out of range for u32, got {0}")]
IdOutOfRange(i64),
#[error("board_group_id must not be negative, got {0}")]
NegativeBoardGroupId(i64),
#[error("board_group_id is out of range for u32, got {0}")]
BoardGroupIdOutOfRange(i64),
#[error(transparent)]
InvalidBoardId(#[from] ParseBoardIdError),
#[error("board title must not be empty")]
EmptyTitle,
#[error("is_traded must be 0 or 1, got {0}")]
InvalidIsTraded(i64),
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ParseSecurityError {
#[error(transparent)]
InvalidSecId(#[from] ParseSecIdError),
#[error("security shortname must not be empty")]
EmptyShortname,
#[error("security secname must not be empty")]
EmptySecname,
#[error("security status must not be empty")]
EmptyStatus,
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ParseSecurityBoardError {
#[error(transparent)]
InvalidEngine(#[from] ParseEngineNameError),
#[error(transparent)]
InvalidMarket(#[from] ParseMarketNameError),
#[error(transparent)]
InvalidBoardId(#[from] ParseBoardIdError),
#[error("is_primary must be 0 or 1, got {0}")]
InvalidIsPrimary(i64),
}
#[derive(Debug, Error, Clone, PartialEq)]
pub enum ParseSecuritySnapshotError {
#[error(transparent)]
InvalidSecId(#[from] ParseSecIdError),
#[error("lot size must not be negative, got {0}")]
NegativeLotSize(i64),
#[error("lot size is out of range for u32, got {0}")]
LotSizeOutOfRange(i64),
#[error("last must be finite")]
NonFiniteLast(f64),
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ParseCandleError {
#[error("invalid candle datetime range: begin={begin} is after end={end}")]
InvalidDateRange {
begin: NaiveDateTime,
end: NaiveDateTime,
},
#[error("candle volume must not be negative, got {0}")]
NegativeVolume(i64),
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ParseCandleIntervalError {
#[error("invalid candle interval code, got {0}")]
InvalidCode(i64),
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ParseCandleQueryError {
#[error("invalid candle query datetime range: from={from} is after till={till}")]
InvalidDateRange {
from: NaiveDateTime,
till: NaiveDateTime,
},
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ParseCandleBorderError {
#[error("invalid candle borders range: begin={begin} is after end={end}")]
InvalidDateRange {
begin: NaiveDateTime,
end: NaiveDateTime,
},
#[error(transparent)]
InvalidInterval(#[from] ParseCandleIntervalError),
#[error("board_group_id must not be negative, got {0}")]
NegativeBoardGroupId(i64),
#[error("board_group_id is out of range for u32, got {0}")]
BoardGroupIdOutOfRange(i64),
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ParseTradeError {
#[error("trade number must be positive, got {0}")]
NonPositiveTradeNo(i64),
#[error("trade number is out of range for u64, got {0}")]
TradeNoOutOfRange(i64),
#[error("trade quantity must not be negative, got {0}")]
NegativeQuantity(i64),
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ParseOrderbookError {
#[error("orderbook side must be 'B' or 'S', got '{0}'")]
InvalidSide(Box<str>),
#[error("orderbook price must be present")]
MissingPrice,
#[error("orderbook price must not be negative")]
NegativePrice,
#[error("orderbook quantity must be present")]
MissingQuantity,
#[error("orderbook quantity must not be negative, got {0}")]
NegativeQuantity(i64),
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ParseSecIdError {
#[error("secid must not be empty")]
Empty,
#[error("secid must not contain '/'")]
ContainsSlash,
}
impl From<Infallible> for ParseSecIdError {
fn from(value: Infallible) -> Self {
match value {}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SecId(Box<str>);
impl SecId {
pub fn as_str(&self) -> &str {
self.0.as_ref()
}
}
impl fmt::Display for SecId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl AsRef<str> for SecId {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl From<&SecId> for SecId {
fn from(value: &SecId) -> Self {
value.clone()
}
}
impl TryFrom<String> for SecId {
type Error = ParseSecIdError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::try_from(value.as_str())
}
}
impl TryFrom<&str> for SecId {
type Error = ParseSecIdError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let value = value.trim();
if value.is_empty() {
return Err(ParseSecIdError::Empty);
}
if value.contains('/') {
return Err(ParseSecIdError::ContainsSlash);
}
Ok(Self(value.to_owned().into_boxed_str()))
}
}
impl FromStr for SecId {
type Err = ParseSecIdError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::try_from(value)
}
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ParseBoardIdError {
#[error("boardid must not be empty")]
Empty,
#[error("boardid must not contain '/'")]
ContainsSlash,
}
impl From<Infallible> for ParseBoardIdError {
fn from(value: Infallible) -> Self {
match value {}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct BoardId(Box<str>);
impl BoardId {
pub fn as_str(&self) -> &str {
self.0.as_ref()
}
}
impl fmt::Display for BoardId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl AsRef<str> for BoardId {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl From<&BoardId> for BoardId {
fn from(value: &BoardId) -> Self {
value.clone()
}
}
impl TryFrom<String> for BoardId {
type Error = ParseBoardIdError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::try_from(value.as_str())
}
}
impl TryFrom<&str> for BoardId {
type Error = ParseBoardIdError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let value = value.trim();
if value.is_empty() {
return Err(ParseBoardIdError::Empty);
}
if value.contains('/') {
return Err(ParseBoardIdError::ContainsSlash);
}
Ok(Self(value.to_owned().into_boxed_str()))
}
}
impl FromStr for BoardId {
type Err = ParseBoardIdError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::try_from(value)
}
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ParseMarketNameError {
#[error("market name must not be empty")]
Empty,
#[error("market name must not contain '/'")]
ContainsSlash,
}
impl From<Infallible> for ParseMarketNameError {
fn from(value: Infallible) -> Self {
match value {}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct MarketName(Box<str>);
impl MarketName {
pub fn as_str(&self) -> &str {
self.0.as_ref()
}
}
impl fmt::Display for MarketName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl AsRef<str> for MarketName {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl From<&MarketName> for MarketName {
fn from(value: &MarketName) -> Self {
value.clone()
}
}
impl TryFrom<String> for MarketName {
type Error = ParseMarketNameError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::try_from(value.as_str())
}
}
impl TryFrom<&str> for MarketName {
type Error = ParseMarketNameError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let value = value.trim();
if value.is_empty() {
return Err(ParseMarketNameError::Empty);
}
if value.contains('/') {
return Err(ParseMarketNameError::ContainsSlash);
}
Ok(Self(value.to_owned().into_boxed_str()))
}
}
impl FromStr for MarketName {
type Err = ParseMarketNameError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::try_from(value)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct EngineId(u32);
impl EngineId {
pub fn get(self) -> u32 {
self.0
}
}
impl fmt::Display for EngineId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Engine {
id: EngineId,
name: EngineName,
title: Box<str>,
}
impl Engine {
pub fn try_new(id: i64, name: String, title: String) -> Result<Self, ParseEngineError> {
if id <= 0 {
return Err(ParseEngineError::NonPositiveId(id));
}
let id = u32::try_from(id)
.map(EngineId)
.map_err(|_| ParseEngineError::IdOutOfRange(id))?;
let name = EngineName::try_from(name)?;
let title = title.trim();
if title.is_empty() {
return Err(ParseEngineError::EmptyTitle);
}
Ok(Self {
id,
name,
title: title.to_owned().into_boxed_str(),
})
}
pub fn id(&self) -> EngineId {
self.id
}
pub fn name(&self) -> &EngineName {
&self.name
}
pub fn title(&self) -> &str {
self.title.as_ref()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct MarketId(u32);
impl MarketId {
pub fn get(self) -> u32 {
self.0
}
}
impl fmt::Display for MarketId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Market {
id: MarketId,
name: MarketName,
title: Box<str>,
}
impl Market {
pub fn try_new(id: i64, name: String, title: String) -> Result<Self, ParseMarketError> {
if id <= 0 {
return Err(ParseMarketError::NonPositiveId(id));
}
let id = u32::try_from(id)
.map(MarketId)
.map_err(|_| ParseMarketError::IdOutOfRange(id))?;
let name = MarketName::try_from(name)?;
let title = title.trim();
if title.is_empty() {
return Err(ParseMarketError::EmptyTitle);
}
Ok(Self {
id,
name,
title: title.to_owned().into_boxed_str(),
})
}
pub fn id(&self) -> MarketId {
self.id
}
pub fn name(&self) -> &MarketName {
&self.name
}
pub fn title(&self) -> &str {
self.title.as_ref()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Board {
id: u32,
board_group_id: u32,
boardid: BoardId,
title: Box<str>,
is_traded: bool,
}
impl Board {
pub fn try_new(
id: i64,
board_group_id: i64,
boardid: String,
title: String,
is_traded: i64,
) -> Result<Self, ParseBoardError> {
if id <= 0 {
return Err(ParseBoardError::NonPositiveId(id));
}
let id = u32::try_from(id).map_err(|_| ParseBoardError::IdOutOfRange(id))?;
if board_group_id < 0 {
return Err(ParseBoardError::NegativeBoardGroupId(board_group_id));
}
let board_group_id = u32::try_from(board_group_id)
.map_err(|_| ParseBoardError::BoardGroupIdOutOfRange(board_group_id))?;
let boardid = BoardId::try_from(boardid)?;
let title = title.trim();
if title.is_empty() {
return Err(ParseBoardError::EmptyTitle);
}
let is_traded = match is_traded {
0 => false,
1 => true,
other => return Err(ParseBoardError::InvalidIsTraded(other)),
};
Ok(Self {
id,
board_group_id,
boardid,
title: title.to_owned().into_boxed_str(),
is_traded,
})
}
pub fn id(&self) -> u32 {
self.id
}
pub fn board_group_id(&self) -> u32 {
self.board_group_id
}
pub fn boardid(&self) -> &BoardId {
&self.boardid
}
pub fn title(&self) -> &str {
self.title.as_ref()
}
pub fn is_traded(&self) -> bool {
self.is_traded
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SecurityBoard {
engine: EngineName,
market: MarketName,
boardid: BoardId,
is_primary: bool,
}
impl SecurityBoard {
pub fn try_new(
engine: String,
market: String,
boardid: String,
is_primary: i64,
) -> Result<Self, ParseSecurityBoardError> {
let engine = EngineName::try_from(engine)?;
let market = MarketName::try_from(market)?;
let boardid = BoardId::try_from(boardid)?;
let is_primary = match is_primary {
0 => false,
1 => true,
other => return Err(ParseSecurityBoardError::InvalidIsPrimary(other)),
};
Ok(Self {
engine,
market,
boardid,
is_primary,
})
}
pub fn engine(&self) -> &EngineName {
&self.engine
}
pub fn market(&self) -> &MarketName {
&self.market
}
pub fn boardid(&self) -> &BoardId {
&self.boardid
}
pub fn is_primary(&self) -> bool {
self.is_primary
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Security {
secid: SecId,
shortname: Box<str>,
secname: Box<str>,
status: Box<str>,
}
impl Security {
pub fn try_new(
secid: String,
shortname: String,
secname: String,
status: String,
) -> Result<Self, ParseSecurityError> {
let secid = SecId::try_from(secid)?;
let shortname = shortname.trim();
if shortname.is_empty() {
return Err(ParseSecurityError::EmptyShortname);
}
let secname = secname.trim();
if secname.is_empty() {
return Err(ParseSecurityError::EmptySecname);
}
let status = status.trim();
if status.is_empty() {
return Err(ParseSecurityError::EmptyStatus);
}
Ok(Self {
secid,
shortname: shortname.to_owned().into_boxed_str(),
secname: secname.to_owned().into_boxed_str(),
status: status.to_owned().into_boxed_str(),
})
}
pub fn secid(&self) -> &SecId {
&self.secid
}
pub fn shortname(&self) -> &str {
self.shortname.as_ref()
}
pub fn secname(&self) -> &str {
self.secname.as_ref()
}
pub fn status(&self) -> &str {
self.status.as_ref()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SecuritySnapshot {
secid: SecId,
lot_size: Option<u32>,
last: Option<f64>,
}
impl SecuritySnapshot {
pub fn try_new(
secid: String,
lot_size: Option<i64>,
last: Option<f64>,
) -> Result<Self, ParseSecuritySnapshotError> {
let secid = SecId::try_from(secid).map_err(ParseSecuritySnapshotError::InvalidSecId)?;
let lot_size = match lot_size {
None => None,
Some(raw) if raw < 0 => return Err(ParseSecuritySnapshotError::NegativeLotSize(raw)),
Some(raw) => Some(
u32::try_from(raw)
.map_err(|_| ParseSecuritySnapshotError::LotSizeOutOfRange(raw))?,
),
};
Self::try_from_parts(secid, lot_size, last)
}
pub(crate) fn try_from_parts(
secid: SecId,
lot_size: Option<u32>,
last: Option<f64>,
) -> Result<Self, ParseSecuritySnapshotError> {
if let Some(last) = last
&& !last.is_finite()
{
return Err(ParseSecuritySnapshotError::NonFiniteLast(last));
}
Ok(Self {
secid,
lot_size,
last,
})
}
pub fn secid(&self) -> &SecId {
&self.secid
}
pub fn lot_size(&self) -> Option<u32> {
self.lot_size
}
pub fn last(&self) -> Option<f64> {
self.last
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CandleInterval {
Minute1,
Minute10,
Hour1,
Day1,
Week1,
Month1,
Quarter1,
}
impl CandleInterval {
pub fn as_str(self) -> &'static str {
match self {
Self::Minute1 => "1",
Self::Minute10 => "10",
Self::Hour1 => "60",
Self::Day1 => "24",
Self::Week1 => "7",
Self::Month1 => "31",
Self::Quarter1 => "4",
}
}
}
impl TryFrom<i64> for CandleInterval {
type Error = ParseCandleIntervalError;
fn try_from(value: i64) -> Result<Self, Self::Error> {
match value {
1 => Ok(Self::Minute1),
10 => Ok(Self::Minute10),
60 => Ok(Self::Hour1),
24 => Ok(Self::Day1),
7 => Ok(Self::Week1),
31 => Ok(Self::Month1),
4 => Ok(Self::Quarter1),
other => Err(ParseCandleIntervalError::InvalidCode(other)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CandleBorder {
begin: NaiveDateTime,
end: NaiveDateTime,
interval: CandleInterval,
board_group_id: u32,
}
impl CandleBorder {
pub fn try_new(
begin: NaiveDateTime,
end: NaiveDateTime,
interval: i64,
board_group_id: i64,
) -> Result<Self, ParseCandleBorderError> {
if begin > end {
return Err(ParseCandleBorderError::InvalidDateRange { begin, end });
}
let interval = CandleInterval::try_from(interval)?;
if board_group_id < 0 {
return Err(ParseCandleBorderError::NegativeBoardGroupId(board_group_id));
}
let board_group_id = u32::try_from(board_group_id)
.map_err(|_| ParseCandleBorderError::BoardGroupIdOutOfRange(board_group_id))?;
Ok(Self {
begin,
end,
interval,
board_group_id,
})
}
pub fn begin(&self) -> NaiveDateTime {
self.begin
}
pub fn end(&self) -> NaiveDateTime {
self.end
}
pub fn interval(&self) -> CandleInterval {
self.interval
}
pub fn board_group_id(&self) -> u32 {
self.board_group_id
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct CandleQuery {
from: Option<NaiveDateTime>,
till: Option<NaiveDateTime>,
interval: Option<CandleInterval>,
}
impl CandleQuery {
pub fn try_new(
from: Option<NaiveDateTime>,
till: Option<NaiveDateTime>,
interval: Option<CandleInterval>,
) -> Result<Self, ParseCandleQueryError> {
if let (Some(from), Some(till)) = (from, till)
&& from > till
{
return Err(ParseCandleQueryError::InvalidDateRange { from, till });
}
Ok(Self {
from,
till,
interval,
})
}
pub fn from(&self) -> Option<NaiveDateTime> {
self.from
}
pub fn till(&self) -> Option<NaiveDateTime> {
self.till
}
pub fn interval(&self) -> Option<CandleInterval> {
self.interval
}
pub fn with_from(self, from: NaiveDateTime) -> Result<Self, ParseCandleQueryError> {
Self::try_new(Some(from), self.till, self.interval)
}
pub fn with_till(self, till: NaiveDateTime) -> Result<Self, ParseCandleQueryError> {
Self::try_new(self.from, Some(till), self.interval)
}
pub fn with_interval(mut self, interval: CandleInterval) -> Self {
self.interval = Some(interval);
self
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct Pagination {
pub start: Option<u32>,
pub limit: Option<NonZeroU32>,
}
impl Pagination {
pub fn with_start(mut self, start: u32) -> Self {
self.start = Some(start);
self
}
pub fn with_limit(mut self, limit: NonZeroU32) -> Self {
self.limit = Some(limit);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum PageRequest {
#[default]
FirstPage,
Page(Pagination),
All {
page_limit: NonZeroU32,
},
}
impl PageRequest {
pub fn first_page() -> Self {
Self::FirstPage
}
pub fn page(pagination: Pagination) -> Self {
Self::Page(pagination)
}
pub fn all(page_limit: NonZeroU32) -> Self {
Self::All { page_limit }
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Candle {
begin: NaiveDateTime,
end: NaiveDateTime,
open: Option<f64>,
close: Option<f64>,
high: Option<f64>,
low: Option<f64>,
value: Option<f64>,
volume: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CandleOhlcv {
open: Option<f64>,
close: Option<f64>,
high: Option<f64>,
low: Option<f64>,
value: Option<f64>,
volume: Option<i64>,
}
impl CandleOhlcv {
pub fn new(
open: Option<f64>,
close: Option<f64>,
high: Option<f64>,
low: Option<f64>,
value: Option<f64>,
volume: Option<i64>,
) -> Self {
Self {
open,
close,
high,
low,
value,
volume,
}
}
}
impl Candle {
pub fn try_new(
begin: NaiveDateTime,
end: NaiveDateTime,
ohlcv: CandleOhlcv,
) -> Result<Self, ParseCandleError> {
if begin > end {
return Err(ParseCandleError::InvalidDateRange { begin, end });
}
let volume = match ohlcv.volume {
None => None,
Some(raw) if raw >= 0 => Some(raw as u64),
Some(raw) => return Err(ParseCandleError::NegativeVolume(raw)),
};
Ok(Self {
begin,
end,
open: ohlcv.open,
close: ohlcv.close,
high: ohlcv.high,
low: ohlcv.low,
value: ohlcv.value,
volume,
})
}
pub fn begin(&self) -> NaiveDateTime {
self.begin
}
pub fn end(&self) -> NaiveDateTime {
self.end
}
pub fn open(&self) -> Option<f64> {
self.open
}
pub fn close(&self) -> Option<f64> {
self.close
}
pub fn high(&self) -> Option<f64> {
self.high
}
pub fn low(&self) -> Option<f64> {
self.low
}
pub fn value(&self) -> Option<f64> {
self.value
}
pub fn volume(&self) -> Option<u64> {
self.volume
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Trade {
tradeno: u64,
tradetime: NaiveTime,
price: Option<f64>,
quantity: Option<u64>,
value: Option<f64>,
}
impl Trade {
pub fn try_new(
tradeno: i64,
tradetime: NaiveTime,
price: Option<f64>,
quantity: Option<i64>,
value: Option<f64>,
) -> Result<Self, ParseTradeError> {
if tradeno <= 0 {
return Err(ParseTradeError::NonPositiveTradeNo(tradeno));
}
let tradeno =
u64::try_from(tradeno).map_err(|_| ParseTradeError::TradeNoOutOfRange(tradeno))?;
let quantity = match quantity {
None => None,
Some(raw) if raw >= 0 => Some(raw as u64),
Some(raw) => return Err(ParseTradeError::NegativeQuantity(raw)),
};
Ok(Self {
tradeno,
tradetime,
price,
quantity,
value,
})
}
pub fn tradeno(&self) -> u64 {
self.tradeno
}
pub fn tradetime(&self) -> NaiveTime {
self.tradetime
}
pub fn price(&self) -> Option<f64> {
self.price
}
pub fn quantity(&self) -> Option<u64> {
self.quantity
}
pub fn value(&self) -> Option<f64> {
self.value
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BuySell {
Buy,
Sell,
}
impl BuySell {
pub fn as_str(self) -> &'static str {
match self {
Self::Buy => "B",
Self::Sell => "S",
}
}
}
impl TryFrom<String> for BuySell {
type Error = ParseOrderbookError;
fn try_from(value: String) -> Result<Self, Self::Error> {
let value = value.trim();
match value {
"B" => Ok(Self::Buy),
"S" => Ok(Self::Sell),
_ => Err(ParseOrderbookError::InvalidSide(
value.to_owned().into_boxed_str(),
)),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct OrderbookLevel {
buy_sell: BuySell,
price: f64,
quantity: u64,
}
impl OrderbookLevel {
pub fn try_new(
buy_sell: String,
price: Option<f64>,
quantity: Option<i64>,
) -> Result<Self, ParseOrderbookError> {
let buy_sell = BuySell::try_from(buy_sell)?;
let Some(price) = price else {
return Err(ParseOrderbookError::MissingPrice);
};
if price.is_sign_negative() {
return Err(ParseOrderbookError::NegativePrice);
}
let Some(quantity) = quantity else {
return Err(ParseOrderbookError::MissingQuantity);
};
let quantity = match quantity {
raw if raw >= 0 => raw as u64,
raw => return Err(ParseOrderbookError::NegativeQuantity(raw)),
};
Ok(Self {
buy_sell,
price,
quantity,
})
}
pub fn buy_sell(&self) -> BuySell {
self.buy_sell
}
pub fn price(&self) -> f64 {
self.price
}
pub fn quantity(&self) -> u64 {
self.quantity
}
}
impl TryFrom<String> for IndexId {
type Error = ParseIndexError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::try_from(value.as_str())
}
}
impl TryFrom<&str> for IndexId {
type Error = ParseIndexError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let value = value.trim();
if value.is_empty() {
return Err(ParseIndexError::EmptyIndexId);
}
Ok(Self(value.to_owned().into_boxed_str()))
}
}
impl FromStr for IndexId {
type Err = ParseIndexError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::try_from(value)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Index {
id: IndexId,
short_name: Box<str>,
from: Option<NaiveDate>,
till: Option<NaiveDate>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HistoryDates {
from: NaiveDate,
till: NaiveDate,
}
impl HistoryDates {
pub fn try_new(from: NaiveDate, till: NaiveDate) -> Result<Self, ParseHistoryDatesError> {
if from > till {
return Err(ParseHistoryDatesError::InvalidDateRange { from, till });
}
Ok(Self { from, till })
}
pub fn from(&self) -> NaiveDate {
self.from
}
pub fn till(&self) -> NaiveDate {
self.till
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct HistoryRecord {
boardid: BoardId,
tradedate: NaiveDate,
secid: SecId,
numtrades: Option<u64>,
value: Option<f64>,
open: Option<f64>,
low: Option<f64>,
high: Option<f64>,
close: Option<f64>,
volume: Option<u64>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Turnover {
name: Box<str>,
id: u32,
valtoday: Option<f64>,
valtoday_usd: Option<f64>,
numtrades: Option<u64>,
updatetime: NaiveDateTime,
title: Box<str>,
}
impl Turnover {
pub fn try_new(
name: String,
id: i64,
valtoday: Option<f64>,
valtoday_usd: Option<f64>,
numtrades: Option<i64>,
updatetime: NaiveDateTime,
title: String,
) -> Result<Self, ParseTurnoverError> {
let name = name.trim();
if name.is_empty() {
return Err(ParseTurnoverError::EmptyName);
}
if id <= 0 {
return Err(ParseTurnoverError::NonPositiveId(id));
}
let id = u32::try_from(id).map_err(|_| ParseTurnoverError::IdOutOfRange(id))?;
let numtrades = match numtrades {
None => None,
Some(raw) if raw >= 0 => Some(raw as u64),
Some(raw) => return Err(ParseTurnoverError::NegativeNumTrades(raw)),
};
let title = title.trim();
if title.is_empty() {
return Err(ParseTurnoverError::EmptyTitle);
}
Ok(Self {
name: name.to_owned().into_boxed_str(),
id,
valtoday,
valtoday_usd,
numtrades,
updatetime,
title: title.to_owned().into_boxed_str(),
})
}
pub fn name(&self) -> &str {
self.name.as_ref()
}
pub fn id(&self) -> u32 {
self.id
}
pub fn valtoday(&self) -> Option<f64> {
self.valtoday
}
pub fn valtoday_usd(&self) -> Option<f64> {
self.valtoday_usd
}
pub fn numtrades(&self) -> Option<u64> {
self.numtrades
}
pub fn updatetime(&self) -> NaiveDateTime {
self.updatetime
}
pub fn title(&self) -> &str {
self.title.as_ref()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SecStat {
secid: SecId,
boardid: BoardId,
voltoday: Option<u64>,
valtoday: Option<f64>,
highbid: Option<f64>,
lowoffer: Option<f64>,
lastoffer: Option<f64>,
lastbid: Option<f64>,
open: Option<f64>,
low: Option<f64>,
high: Option<f64>,
last: Option<f64>,
numtrades: Option<u64>,
waprice: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SiteNews {
id: u64,
tag: Box<str>,
title: Box<str>,
published_at: NaiveDateTime,
modified_at: NaiveDateTime,
}
impl SiteNews {
pub fn try_new(
id: i64,
tag: String,
title: String,
published_at: NaiveDateTime,
modified_at: NaiveDateTime,
) -> Result<Self, ParseSiteNewsError> {
if id <= 0 {
return Err(ParseSiteNewsError::NonPositiveId(id));
}
let id = u64::try_from(id).map_err(|_| ParseSiteNewsError::IdOutOfRange(id))?;
let tag = tag.trim();
if tag.is_empty() {
return Err(ParseSiteNewsError::EmptyTag);
}
let title = title.trim();
if title.is_empty() {
return Err(ParseSiteNewsError::EmptyTitle);
}
Ok(Self {
id,
tag: tag.to_owned().into_boxed_str(),
title: title.to_owned().into_boxed_str(),
published_at,
modified_at,
})
}
pub fn id(&self) -> u64 {
self.id
}
pub fn tag(&self) -> &str {
self.tag.as_ref()
}
pub fn title(&self) -> &str {
self.title.as_ref()
}
pub fn published_at(&self) -> NaiveDateTime {
self.published_at
}
pub fn modified_at(&self) -> NaiveDateTime {
self.modified_at
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Event {
id: u64,
tag: Box<str>,
title: Box<str>,
from: Option<NaiveDateTime>,
modified_at: NaiveDateTime,
}
impl Event {
pub fn try_new(
id: i64,
tag: String,
title: String,
from: Option<NaiveDateTime>,
modified_at: NaiveDateTime,
) -> Result<Self, ParseEventError> {
if id <= 0 {
return Err(ParseEventError::NonPositiveId(id));
}
let id = u64::try_from(id).map_err(|_| ParseEventError::IdOutOfRange(id))?;
let tag = tag.trim();
if tag.is_empty() {
return Err(ParseEventError::EmptyTag);
}
let title = title.trim();
if title.is_empty() {
return Err(ParseEventError::EmptyTitle);
}
Ok(Self {
id,
tag: tag.to_owned().into_boxed_str(),
title: title.to_owned().into_boxed_str(),
from,
modified_at,
})
}
pub fn id(&self) -> u64 {
self.id
}
pub fn tag(&self) -> &str {
self.tag.as_ref()
}
pub fn title(&self) -> &str {
self.title.as_ref()
}
pub fn from(&self) -> Option<NaiveDateTime> {
self.from
}
pub fn modified_at(&self) -> NaiveDateTime {
self.modified_at
}
}
pub(crate) struct SecStatInput {
pub(crate) secid: String,
pub(crate) boardid: String,
pub(crate) voltoday: Option<i64>,
pub(crate) valtoday: Option<f64>,
pub(crate) highbid: Option<f64>,
pub(crate) lowoffer: Option<f64>,
pub(crate) lastoffer: Option<f64>,
pub(crate) lastbid: Option<f64>,
pub(crate) open: Option<f64>,
pub(crate) low: Option<f64>,
pub(crate) high: Option<f64>,
pub(crate) last: Option<f64>,
pub(crate) numtrades: Option<i64>,
pub(crate) waprice: Option<f64>,
}
impl SecStat {
pub(crate) fn try_new(input: SecStatInput) -> Result<Self, ParseSecStatError> {
let SecStatInput {
secid,
boardid,
voltoday,
valtoday,
highbid,
lowoffer,
lastoffer,
lastbid,
open,
low,
high,
last,
numtrades,
waprice,
} = input;
let secid = SecId::try_from(secid)?;
let boardid = BoardId::try_from(boardid)?;
let voltoday = match voltoday {
None => None,
Some(raw) if raw >= 0 => Some(raw as u64),
Some(raw) => return Err(ParseSecStatError::NegativeVolToday(raw)),
};
let numtrades = match numtrades {
None => None,
Some(raw) if raw >= 0 => Some(raw as u64),
Some(raw) => return Err(ParseSecStatError::NegativeNumTrades(raw)),
};
Ok(Self {
secid,
boardid,
voltoday,
valtoday,
highbid,
lowoffer,
lastoffer,
lastbid,
open,
low,
high,
last,
numtrades,
waprice,
})
}
pub fn secid(&self) -> &SecId {
&self.secid
}
pub fn boardid(&self) -> &BoardId {
&self.boardid
}
pub fn voltoday(&self) -> Option<u64> {
self.voltoday
}
pub fn valtoday(&self) -> Option<f64> {
self.valtoday
}
pub fn highbid(&self) -> Option<f64> {
self.highbid
}
pub fn lowoffer(&self) -> Option<f64> {
self.lowoffer
}
pub fn lastoffer(&self) -> Option<f64> {
self.lastoffer
}
pub fn lastbid(&self) -> Option<f64> {
self.lastbid
}
pub fn open(&self) -> Option<f64> {
self.open
}
pub fn low(&self) -> Option<f64> {
self.low
}
pub fn high(&self) -> Option<f64> {
self.high
}
pub fn last(&self) -> Option<f64> {
self.last
}
pub fn numtrades(&self) -> Option<u64> {
self.numtrades
}
pub fn waprice(&self) -> Option<f64> {
self.waprice
}
}
pub(crate) struct HistoryRecordInput {
pub(crate) boardid: String,
pub(crate) tradedate: NaiveDate,
pub(crate) secid: String,
pub(crate) numtrades: Option<i64>,
pub(crate) value: Option<f64>,
pub(crate) open: Option<f64>,
pub(crate) low: Option<f64>,
pub(crate) high: Option<f64>,
pub(crate) close: Option<f64>,
pub(crate) volume: Option<i64>,
}
impl HistoryRecord {
pub(crate) fn try_new(input: HistoryRecordInput) -> Result<Self, ParseHistoryRecordError> {
let HistoryRecordInput {
boardid,
tradedate,
secid,
numtrades,
value,
open,
low,
high,
close,
volume,
} = input;
let boardid = BoardId::try_from(boardid)?;
let secid = SecId::try_from(secid)?;
let numtrades = match numtrades {
None => None,
Some(raw) if raw >= 0 => Some(raw as u64),
Some(raw) => return Err(ParseHistoryRecordError::NegativeNumTrades(raw)),
};
let volume = match volume {
None => None,
Some(raw) if raw >= 0 => Some(raw as u64),
Some(raw) => return Err(ParseHistoryRecordError::NegativeVolume(raw)),
};
Ok(Self {
boardid,
tradedate,
secid,
numtrades,
value,
open,
low,
high,
close,
volume,
})
}
pub fn boardid(&self) -> &BoardId {
&self.boardid
}
pub fn tradedate(&self) -> NaiveDate {
self.tradedate
}
pub fn secid(&self) -> &SecId {
&self.secid
}
pub fn numtrades(&self) -> Option<u64> {
self.numtrades
}
pub fn value(&self) -> Option<f64> {
self.value
}
pub fn open(&self) -> Option<f64> {
self.open
}
pub fn low(&self) -> Option<f64> {
self.low
}
pub fn high(&self) -> Option<f64> {
self.high
}
pub fn close(&self) -> Option<f64> {
self.close
}
pub fn volume(&self) -> Option<u64> {
self.volume
}
}
impl Index {
pub fn try_new(
id: String,
short_name: String,
from: Option<NaiveDate>,
till: Option<NaiveDate>,
) -> Result<Self, ParseIndexError> {
let id = IndexId::try_from(id)?;
let short_name = short_name.trim();
if short_name.is_empty() {
return Err(ParseIndexError::EmptyShortName);
}
if let (Some(from_date), Some(till_date)) = (from, till)
&& from_date > till_date
{
return Err(ParseIndexError::InvalidDateRange {
from: from_date,
till: till_date,
});
}
Ok(Self {
id,
short_name: short_name.to_owned().into_boxed_str(),
from,
till,
})
}
pub fn id(&self) -> &IndexId {
&self.id
}
pub fn short_name(&self) -> &str {
self.short_name.as_ref()
}
pub fn from(&self) -> Option<NaiveDate> {
self.from
}
pub fn till(&self) -> Option<NaiveDate> {
self.till
}
pub fn is_active_on(&self, date: NaiveDate) -> bool {
self.from.is_none_or(|from| from <= date) && self.till.is_none_or(|till| date <= till)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct IndexAnalytics {
indexid: IndexId,
tradedate: NaiveDate,
ticker: SecId,
shortnames: Box<str>,
secid: SecId,
weight: f64,
tradingsession: u8,
trade_session_date: NaiveDate,
}
pub(crate) struct IndexAnalyticsInput {
pub(crate) indexid: String,
pub(crate) tradedate: NaiveDate,
pub(crate) ticker: String,
pub(crate) shortnames: String,
pub(crate) secid: String,
pub(crate) weight: f64,
pub(crate) tradingsession: i64,
pub(crate) trade_session_date: NaiveDate,
}
impl IndexAnalytics {
pub(crate) fn try_new(input: IndexAnalyticsInput) -> Result<Self, ParseIndexAnalyticsError> {
let IndexAnalyticsInput {
indexid,
tradedate,
ticker,
shortnames,
secid,
weight,
tradingsession,
trade_session_date,
} = input;
let indexid = IndexId::try_from(indexid)?;
let ticker = SecId::try_from(ticker).map_err(ParseIndexAnalyticsError::InvalidTicker)?;
let secid = SecId::try_from(secid).map_err(ParseIndexAnalyticsError::InvalidSecId)?;
let shortnames = shortnames.trim();
if shortnames.is_empty() {
return Err(ParseIndexAnalyticsError::EmptyShortnames);
}
if !weight.is_finite() {
return Err(ParseIndexAnalyticsError::NonFiniteWeight);
}
if weight.is_sign_negative() {
return Err(ParseIndexAnalyticsError::NegativeWeight);
}
if !(1..=3).contains(&tradingsession) {
return Err(ParseIndexAnalyticsError::InvalidTradingsession(
tradingsession,
));
}
Ok(Self {
indexid,
tradedate,
ticker,
shortnames: shortnames.to_owned().into_boxed_str(),
secid,
weight,
tradingsession: tradingsession as u8,
trade_session_date,
})
}
pub fn indexid(&self) -> &IndexId {
&self.indexid
}
pub fn tradedate(&self) -> NaiveDate {
self.tradedate
}
pub fn ticker(&self) -> &SecId {
&self.ticker
}
pub fn shortnames(&self) -> &str {
self.shortnames.as_ref()
}
pub fn secid(&self) -> &SecId {
&self.secid
}
pub fn weight(&self) -> f64 {
self.weight
}
pub fn tradingsession(&self) -> u8 {
self.tradingsession
}
pub fn trade_session_date(&self) -> NaiveDate {
self.trade_session_date
}
}
pub fn actual_indexes(indexes: &[Index]) -> impl Iterator<Item = &Index> {
let latest_till = indexes.iter().filter_map(Index::till).max();
indexes
.iter()
.filter(move |index| index.till() == latest_till)
}