#![cfg_attr(not(any(test, feature = "test-mode")), allow(dead_code))]
use chrono::{DateTime, NaiveDate, Utc};
use paft::Decimal;
use paft::domain::{AssetKind, Exchange, MarketState, ReportingPeriod};
use paft::fundamentals::analysis::{RecommendationAction, RecommendationGrade};
use paft::fundamentals::holders::{InsiderPosition, TransactionType};
use paft::fundamentals::profile::FundKind;
use paft::money::{Currency, MonetaryAmount, Money, Price, PriceAmount, QuantityAmount};
use rust_decimal::prelude::ToPrimitive;
use std::{fmt::Display, str::FromStr};
use crate::{
YfError,
core::{
currency_resolver::ResolvedCurrencyUnit,
yahoo_vocab::{first_parsed_yahoo_exchange, parse_yahoo_exchange, parse_yahoo_quote_type},
},
};
#[must_use]
pub fn decimal_from_f64(value: f64) -> Option<Decimal> {
value
.is_finite()
.then_some(value)
.and_then(|value| Decimal::try_from(value).ok())
}
#[must_use]
pub fn decimal_from_f32(value: f32) -> Option<Decimal> {
value
.is_finite()
.then_some(value)
.and_then(|value| Decimal::try_from(value).ok())
}
#[must_use]
pub fn money_from_f64(value: f64, currency: Currency) -> Option<Money> {
decimal_from_f64(value).and_then(|decimal| Money::new(decimal, currency).ok())
}
pub fn i64_to_money_with_currency(value: i64, currency: Currency) -> Result<Money, YfError> {
let decimal = rust_decimal::Decimal::from_i128_with_scale(i128::from(value), 0);
Ok(Money::new(decimal, currency)?)
}
pub fn u64_to_money_with_currency(value: u64, currency: Currency) -> Result<Money, YfError> {
let decimal = rust_decimal::Decimal::from_i128_with_scale(i128::from(value), 0);
Ok(Money::new(decimal, currency)?)
}
#[must_use]
pub fn money_from_f64_with_currency_str(value: f64, currency_str: Option<&str>) -> Option<Money> {
let unit = currency_str.and_then(ResolvedCurrencyUnit::from_code)?;
unit.money_from_f64(value)
}
#[must_use]
pub fn money_from_decimal_with_currency_str(
value: Decimal,
currency_str: Option<&str>,
) -> Option<Money> {
let unit = currency_str.and_then(ResolvedCurrencyUnit::from_code)?;
unit.money_from_decimal(value).ok()
}
#[must_use]
pub fn price_from_f64(value: f64, currency: Currency) -> Option<Price> {
decimal_from_f64(value).map(|decimal| Price::new(decimal, currency))
}
#[must_use]
pub fn price_amount_from_f64(value: f64) -> Option<PriceAmount> {
decimal_from_f64(value).map(PriceAmount::new)
}
#[must_use]
pub fn price_amount_from_f32(value: f32) -> Option<PriceAmount> {
decimal_from_f32(value).map(PriceAmount::new)
}
#[must_use]
pub fn price_from_f64_with_currency_str(value: f64, currency_str: Option<&str>) -> Option<Price> {
let unit = currency_str.and_then(ResolvedCurrencyUnit::from_code)?;
unit.price_from_f64(value)
}
#[must_use]
pub fn quantity_from_u64(value: u64) -> Option<QuantityAmount> {
QuantityAmount::from_decimal(Decimal::from(value)).ok()
}
#[must_use]
pub fn quantity_from_i64(value: i64) -> Option<QuantityAmount> {
let value = u64::try_from(value).ok()?;
quantity_from_u64(value)
}
pub trait CurrencyValue {
fn amount(&self) -> Decimal;
fn currency(&self) -> &Currency;
}
pub trait DecimalValue {
fn decimal_amount(&self) -> Decimal;
}
impl DecimalValue for Money {
fn decimal_amount(&self) -> Decimal {
self.amount()
}
}
impl DecimalValue for Price {
fn decimal_amount(&self) -> Decimal {
self.amount()
}
}
impl DecimalValue for MonetaryAmount {
fn decimal_amount(&self) -> Decimal {
self.amount()
}
}
impl DecimalValue for PriceAmount {
fn decimal_amount(&self) -> Decimal {
*self.as_decimal()
}
}
impl DecimalValue for QuantityAmount {
fn decimal_amount(&self) -> Decimal {
*self.as_decimal()
}
}
impl CurrencyValue for Money {
fn amount(&self) -> Decimal {
self.amount()
}
fn currency(&self) -> &Currency {
self.currency()
}
}
impl CurrencyValue for Price {
fn amount(&self) -> Decimal {
self.amount()
}
fn currency(&self) -> &Currency {
self.currency()
}
}
impl CurrencyValue for MonetaryAmount {
fn amount(&self) -> Decimal {
self.amount()
}
fn currency(&self) -> &Currency {
self.currency()
}
}
#[must_use]
pub fn f64_from_price_amount(value: &PriceAmount) -> Option<f64> {
value.as_decimal().to_f64()
}
#[must_use]
pub fn f64_from_currency_value(value: &impl CurrencyValue) -> Option<f64> {
value.amount().to_f64()
}
#[must_use]
pub fn money_to_f64(value: &impl DecimalValue) -> f64 {
value
.decimal_amount()
.to_f64()
.expect("decimal amount should fit in f64")
}
#[must_use]
pub fn money_to_currency_str(value: &impl CurrencyValue) -> Option<String> {
Some(value.currency().to_string())
}
pub fn i64_to_datetime(timestamp: i64) -> Result<DateTime<Utc>, YfError> {
DateTime::from_timestamp(timestamp, 0)
.ok_or_else(|| YfError::InvalidData(format!("invalid Unix timestamp: {timestamp}")))
}
pub fn i64_to_date(timestamp: i64) -> Result<NaiveDate, YfError> {
Ok(i64_to_datetime(timestamp)?.date_naive())
}
#[must_use]
pub const fn datetime_to_i64(dt: DateTime<Utc>) -> i64 {
dt.timestamp()
}
pub fn parse_exchange_str(s: &str) -> Result<Exchange, YfError> {
parse_yahoo_exchange(s)
}
#[must_use]
pub fn first_parsed_exchange<'a>(
candidates: impl IntoIterator<Item = Option<&'a str>>,
) -> Option<Exchange> {
first_parsed_yahoo_exchange(candidates)
}
#[must_use]
pub fn string_to_exchange(s: Option<String>) -> Option<Exchange> {
s.and_then(|s| parse_exchange_str(&s).ok())
}
#[must_use]
pub fn exchange_to_string(exchange: Option<Exchange>) -> Option<String> {
exchange.map(|e| e.to_string())
}
#[must_use]
pub fn string_to_market_state(s: Option<String>) -> Option<MarketState> {
s.and_then(|s| s.parse().ok())
}
#[must_use]
pub fn market_state_to_string(state: Option<MarketState>) -> Option<String> {
state.map(|s| s.to_string())
}
pub fn string_to_fund_kind(s: Option<String>) -> Result<Option<FundKind>, YfError> {
s.map_or(Ok(None), |s| {
let kind = match s.as_str() {
"Exchange Traded Fund" => Ok(FundKind::Etf),
"Mutual Fund" => Ok(FundKind::MutualFund),
"Index Fund" => Ok(FundKind::IndexFund),
"Closed-End Fund" => Ok(FundKind::ClosedEndFund),
"Money Market Fund" => Ok(FundKind::MoneyMarketFund),
"Hedge Fund" => Ok(FundKind::HedgeFund),
"Real Estate Investment Trust" => Ok(FundKind::Reit),
"Unit Investment Trust" => Ok(FundKind::UnitInvestmentTrust),
_ => parse_required_token(&s, "fund kind"),
}?;
Ok(Some(kind))
})
}
#[must_use]
pub fn fund_kind_to_string(kind: Option<FundKind>) -> Option<String> {
kind.map(|k| k.to_string())
}
pub fn string_to_insider_position(s: &str) -> Result<InsiderPosition, YfError> {
parse_required_token(s, "insider position")
}
pub fn string_to_transaction_type(s: &str) -> Result<TransactionType, YfError> {
parse_required_token(s, "insider transaction type")
}
pub fn string_to_period(s: &str) -> Result<ReportingPeriod, YfError> {
parse_required_token(s, "period")
}
pub fn string_to_recommendation_grade(s: &str) -> Result<RecommendationGrade, YfError> {
parse_required_token(s, "recommendation grade")
}
pub fn string_to_recommendation_action(s: &str) -> Result<RecommendationAction, YfError> {
parse_required_token(s, "recommendation action")
}
pub fn string_to_asset_kind(s: &str) -> Result<AssetKind, YfError> {
parse_yahoo_quote_type(s)
}
fn parse_required_token<T>(s: &str, name: &str) -> Result<T, YfError>
where
T: FromStr,
T::Err: Display,
{
let token = s.trim();
if token.is_empty() {
return Err(YfError::MissingData(format!("{name} missing")));
}
token
.parse()
.map_err(|err| YfError::InvalidData(format!("invalid {name} {s:?}: {err}")))
}