use derive_more::{AsRef, Display};
use reqwest::Url;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use serde_with::{NoneAsEmptyString, serde_as};
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum AccountFilter {
All,
AccountId(Uuid),
}
#[derive(
Debug, Display, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, AsRef,
)]
pub struct AccountNumber(String);
impl AccountNumber {
pub fn new(num: impl Into<String>) -> Result<Self, error::ParseAccountNumberError> {
let num = num.into();
if num.is_empty() || num.len() < 6 || num.len() > 12 {
Err(error::ParseAccountNumberError::BadLength)
} else if !num.chars().all(|c| c.is_ascii_digit()) {
Err(error::ParseAccountNumberError::InvalidFormat)
} else {
Ok(AccountNumber(num))
}
}
}
#[derive(Debug, Display, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, AsRef)]
pub struct Currency(String);
impl Currency {
pub fn new(code: impl AsRef<str>) -> Result<Self, error::ParseCurrencyError> {
let code = code.as_ref();
if code.len() != 3 || !code.chars().all(|c| c.is_ascii_alphabetic()) {
Err(error::ParseCurrencyError::InvalidFormat)
} else {
Ok(Currency(code.to_uppercase()))
}
}
}
impl<'de> Deserialize<'de> for Currency {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let code = String::deserialize(deserializer)?;
Currency::new(code).map_err(serde::de::Error::custom)
}
}
#[serde_as]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountHoldings {
pub account_id: Uuid,
pub account_number: AccountNumber,
#[serde_as(as = "NoneAsEmptyString")]
pub account_name: Option<String>,
pub account_type: AccountType,
pub currency: Currency,
pub summary: AccountSummary,
pub positions: Vec<Position>,
pub currency_positions: Vec<CurrencyPosition>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub enum AccountType {
#[doc(alias = "ISK")]
#[serde(rename = "ISK")]
InvestmentSavings,
#[doc(alias = "KF")]
#[serde(rename = "KF")]
CapitalInsurance,
#[doc(alias = "Depot")]
#[serde(rename = "Depot")]
Brokerage,
#[serde(rename = "SPAR")]
Savings,
#[serde(rename = "Credit")]
Credit,
#[serde(untagged)]
Other(String),
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountSummary {
pub total_market_value: Decimal,
pub available_for_purchase: Decimal,
pub total_value: Decimal,
pub currency: Currency,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Position {
pub instrument_name: String,
pub ticker: String,
pub orderbook_id: u64,
pub possible_orderbook_ids: Vec<u64>,
pub quantity: Decimal,
pub market_value: InstrumentValue,
pub unrealized_result: InstrumentValue,
pub unrealized_result_percent: Decimal,
pub instrument_currency: Currency,
pub fx_rate: Decimal,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CurrencyPosition {
pub currency_code: Currency,
pub balance: Decimal,
pub accrued_interest: Decimal,
pub available_for_purchase: Decimal,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InstrumentValue {
pub account_currency: Decimal,
pub instrument_currency: Decimal,
}
#[serde_as]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountIdentifiers {
pub account_id: Uuid,
pub account_number: AccountNumber,
#[serde_as(as = "NoneAsEmptyString")]
pub account_name: Option<String>,
pub account_type: String,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TradeTicketArgs {
pub side: TradeSide,
pub account_id: Option<Uuid>,
pub price: Option<Decimal>,
#[serde(flatten)]
pub volume: TradeVolume,
pub currency: TradeCurrency,
#[serde(flatten)]
pub instrument: Instrument,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum TradeVolume {
Amount(Decimal),
Quantity(Decimal),
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum TradeCurrency {
#[default]
Account,
Code(Currency),
}
impl Serialize for TradeCurrency {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::Account => serializer.serialize_none(),
Self::Code(code) => serializer.serialize_some(code),
}
}
}
impl<'de> Deserialize<'de> for TradeCurrency {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_option(TradeCurrencyVisitor)
}
}
struct TradeCurrencyVisitor;
impl<'de> serde::de::Visitor<'de> for TradeCurrencyVisitor {
type Value = TradeCurrency;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("an ISO 4217 code of length 3")
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(TradeCurrency::Account)
}
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: serde::Deserializer<'de>,
{
let code = Currency::deserialize(deserializer)?;
Ok(TradeCurrency::Code(code))
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Instrument {
Name(String),
OrderbookId(u64),
Ticker(String),
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default)]
pub enum TradeSide {
#[default]
Buy,
Sell,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct CreateTradeTicketResult {
pub url: Url,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InstrumentIdentifiers {
pub name: String,
pub orderbook_id: u64,
pub ticker: String,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WatchlistInfo {
pub list_id: u64,
pub name: String,
pub orderbook_count: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Watchlist {
pub list_id: u64,
pub name: String,
#[doc(alias = "instruments")]
pub items: Vec<InstrumentIdentifiers>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModifyWatchlistResult {
pub list_id: u64,
pub orderbook_ids: Vec<u64>,
}
pub mod error {
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum ParseAccountNumberError {
#[error("invalid account number format")]
InvalidFormat,
#[error("account number has a bad length")]
BadLength,
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum ParseCurrencyError {
#[error("invalid currency code")]
InvalidFormat,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn trade_currency_serialization() {
assert_eq!(
serde_json::to_string(&TradeCurrency::Account).unwrap(),
"null"
);
assert_eq!(
serde_json::from_str::<TradeCurrency>("null").unwrap(),
TradeCurrency::Account
);
assert_eq!(
serde_json::to_string(&TradeCurrency::Code(Currency::new("SEK").unwrap())).unwrap(),
"\"SEK\""
);
assert_eq!(
serde_json::from_str::<TradeCurrency>("\"USD\"").unwrap(),
TradeCurrency::Code(Currency::new("USD").unwrap())
);
assert!(serde_json::from_str::<TradeCurrency>("\"WRONG_LEN\"").is_err());
assert!(serde_json::from_str::<TradeCurrency>("\"\"").is_err());
}
#[test]
fn account_number_parsing() {
assert_eq!(
AccountNumber::new("1234567").unwrap(),
AccountNumber("1234567".to_string())
);
assert_eq!(
AccountNumber::new(""),
Err(error::ParseAccountNumberError::BadLength)
);
assert_eq!(
AccountNumber::new("283333a"),
Err(error::ParseAccountNumberError::InvalidFormat)
);
assert_eq!(
AccountNumber::new("2833333939393939392222222"),
Err(error::ParseAccountNumberError::BadLength)
);
}
#[test]
fn account_number_to_string() {
assert_eq!(AccountNumber("1111233".to_string()).to_string(), "1111233");
}
#[test]
fn currency_parsing() {
assert_eq!(Currency::new("USD").unwrap(), Currency("USD".to_string()));
assert_eq!(Currency::new("sek").unwrap(), Currency("SEK".to_string()));
assert_eq!(
Currency::new(""),
Err(error::ParseCurrencyError::InvalidFormat)
);
assert_eq!(
Currency::new("USDOLLAR"),
Err(error::ParseCurrencyError::InvalidFormat)
);
assert_eq!(
Currency::new("US2"),
Err(error::ParseCurrencyError::InvalidFormat)
);
}
#[test]
fn currency_to_string() {
assert_eq!(Currency("USD".to_string()).to_string(), "USD");
}
#[test]
fn currency_deserialization() {
let json = "\"usd\"";
let currency: Currency = serde_json::from_str(json).unwrap();
assert_eq!(currency, Currency("USD".to_string()));
let invalid_json = "\"US2\"";
assert!(serde_json::from_str::<Currency>(invalid_json).is_err());
}
}