#![cfg(all(feature = "domain", feature = "market"))]
use chrono::{DateTime, NaiveDate};
use chrono_tz::Tz;
use paft::market::MarketError;
use paft::prelude::{
Action, AssetKind, Candle, Currency, Exchange, HistoryMeta, HistoryRequest, HistoryResponse,
Instrument, Interval, IsoCurrency, MarketState, Ohlc, OhlcPriceBasis, Price, PriceAmount,
PriceBasis, QuantityAmount, Quote, QuoteUpdate, Range, SearchRequest,
};
use paft_decimal::Decimal;
use std::num::NonZeroU32;
use std::str::FromStr;
const fn usd() -> Currency {
Currency::Iso(IsoCurrency::USD)
}
fn amount(value: impl Into<Decimal>) -> PriceAmount {
PriceAmount::new(value.into())
}
fn quantity(value: impl Into<Decimal>) -> QuantityAmount {
QuantityAmount::from_decimal(value.into()).unwrap()
}
const fn date(year: i32, month: u32, day: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(year, month, day).unwrap()
}
fn ohlc(open: &str, high: &str, low: &str, close: &str) -> Ohlc {
Ohlc::new(
amount(Decimal::from_str(open).unwrap()),
amount(Decimal::from_str(high).unwrap()),
amount(Decimal::from_str(low).unwrap()),
amount(Decimal::from_str(close).unwrap()),
)
}
#[cfg(all(feature = "domain", feature = "market"))]
#[test]
fn end_to_end_workflow() {
let _search_req = SearchRequest::builder("AAPL")
.kind(AssetKind::Equity)
.limit(10)
.build()
.unwrap();
let instrument =
Instrument::from_symbol_and_exchange("AAPL", Exchange::NASDAQ, AssetKind::Equity)
.expect("valid instrument");
let inst_symbol = instrument.symbol.as_str();
assert_eq!(inst_symbol, "AAPL");
assert_eq!(instrument.kind, AssetKind::Equity);
let _history_req = HistoryRequest::builder()
.range(Range::D1)
.interval(Interval::D1)
.include_actions(true)
.prefer_adjusted_prices(true)
.build()
.unwrap();
let candle = Candle {
ts: DateTime::from_timestamp(1_640_995_200, 0).unwrap(),
currency: usd(),
ohlc: ohlc("100.0", "110.0", "95.0", "105.0"),
close_unadj: None,
volume: Some(quantity(1_000_000)),
provider: (),
};
let action = Action::Dividend {
date: date(2022, 1, 1),
amount: Price::new(Decimal::from_str("0.5").unwrap(), usd()),
};
let meta = HistoryMeta {
timezone: Some("America/New_York".parse::<Tz>().unwrap()),
utc_offset_seconds: Some(-18000),
};
let history_response = HistoryResponse {
candles: vec![candle],
actions: vec![action],
price_basis: OhlcPriceBasis::uniform(PriceBasis::provider_latest_adjusted()),
meta: Some(meta),
provider: (),
};
let quote = Quote {
instrument: instrument.clone(),
name: Some("Apple Inc.".to_string()),
currency: usd(),
price: Some(amount(105)),
previous_close: Some(amount(100)),
day_volume: None,
market_state: Some(MarketState::Regular),
as_of: None,
bid: None,
ask: None,
provider: (),
};
let quote_update = QuoteUpdate {
instrument: instrument.clone(),
currency: usd(),
price: Some(amount(106)),
previous_close: Some(amount(100)),
volume: None,
ts: DateTime::from_timestamp(1_640_995_260, 0).unwrap(),
provider: (),
};
let inst_symbol = instrument.symbol.as_str();
assert_eq!(quote.instrument.symbol.as_str(), inst_symbol);
assert_eq!(quote_update.instrument.symbol.as_str(), inst_symbol);
assert_eq!(history_response.candles[0].ohlc.close, quote.price.unwrap());
}
#[cfg(all(feature = "domain", feature = "market"))]
#[test]
fn error_handling_workflow() {
let result = SearchRequest::builder("").limit(0).build();
assert!(result.is_err());
if result == Err(MarketError::EmptySearchQuery) {
} else {
panic!("Expected EmptySearchQuery error, got: {result:?}");
}
let result = HistoryRequest::builder()
.period(
DateTime::from_timestamp(2000, 0).unwrap(),
DateTime::from_timestamp(1000, 0).unwrap(),
) .build();
assert!(result.is_err());
if let Err(MarketError::InvalidPeriod { start, end }) = result {
assert_eq!(start, 2_000_000);
assert_eq!(end, 1_000_000);
} else {
panic!("Expected InvalidPeriod error, got: {result:?}");
}
let result = HistoryRequest::builder()
.range(Range::D1)
.period(
DateTime::from_timestamp(1000, 0).unwrap(),
DateTime::from_timestamp(2000, 0).unwrap(),
) .build();
assert!(result.is_ok());
let req = result.unwrap();
assert_eq!(req.range(), None);
let (s, e) = req.period().unwrap();
assert_eq!(s.timestamp(), 1000);
assert_eq!(e.timestamp(), 2000);
}
#[cfg(all(feature = "domain", feature = "market"))]
#[test]
fn serialization_workflow() {
let search_req = SearchRequest::builder("AAPL")
.kind(AssetKind::Equity)
.build()
.unwrap();
let search_json = serde_json::to_string(&search_req).unwrap();
let deserialized_search: SearchRequest = serde_json::from_str(&search_json).unwrap();
assert_eq!(search_req, deserialized_search);
let history_req = HistoryRequest::builder()
.range(Range::D1)
.interval(Interval::D1)
.include_actions(true)
.prefer_adjusted_prices(true)
.build()
.unwrap();
let history_json = serde_json::to_string(&history_req).unwrap();
let deserialized_history: HistoryRequest = serde_json::from_str(&history_json).unwrap();
assert_eq!(history_req, deserialized_history);
let quote = Quote {
instrument: Instrument::from_symbol("AAPL", AssetKind::Equity).unwrap(),
name: Some("Apple Inc.".to_string()),
currency: usd(),
price: Some(amount(150)),
previous_close: Some(amount(Decimal::from(1475) / Decimal::from(10))),
day_volume: None,
market_state: Some(MarketState::Regular),
as_of: None,
bid: None,
ask: None,
provider: (),
};
let quote_json = serde_json::to_string("e).unwrap();
let deserialized_quote: Quote = serde_json::from_str("e_json).unwrap();
assert_eq!(quote, deserialized_quote);
}
#[cfg(all(feature = "domain", feature = "market"))]
#[test]
fn asset_kind_workflow() {
let asset_kinds = [
AssetKind::Equity,
AssetKind::Crypto,
AssetKind::Fund,
AssetKind::Index,
AssetKind::Forex,
AssetKind::Bond,
AssetKind::Commodity,
AssetKind::Option,
AssetKind::Future,
AssetKind::REIT,
AssetKind::Warrant,
AssetKind::Convertible,
AssetKind::NFT,
AssetKind::PerpetualFuture,
AssetKind::LeveragedToken,
AssetKind::LPToken,
AssetKind::LST,
AssetKind::RWA,
];
for asset_kind in asset_kinds {
let instrument = Instrument::from_symbol_and_exchange(
"TEST",
Exchange::try_from_str("TEST").unwrap(),
asset_kind.clone(),
)
.expect("valid instrument for asset kind");
assert_eq!(instrument.kind, asset_kind);
let json = serde_json::to_string(&instrument).unwrap();
let deserialized: Instrument = serde_json::from_str(&json).unwrap();
assert_eq!(instrument, deserialized);
}
}
#[cfg(all(feature = "domain", feature = "market"))]
#[test]
fn interval_and_range_workflow() {
let intervals = [
Interval::I1m,
Interval::I5m,
Interval::I15m,
Interval::I30m,
Interval::I1h,
Interval::D1,
Interval::W1,
Interval::M1,
];
let ranges = [
Range::D1,
Range::D5,
Range::M1,
Range::M3,
Range::M6,
Range::Y1,
Range::Y2,
Range::Y5,
Range::Y10,
Range::Ytd,
Range::Max,
];
for interval in intervals {
let history_req = HistoryRequest::builder()
.range(Range::D1)
.interval(interval)
.build()
.unwrap();
let json = serde_json::to_string(&history_req).unwrap();
let deserialized: HistoryRequest = serde_json::from_str(&json).unwrap();
assert_eq!(history_req, deserialized);
}
for range in ranges {
let history_req = HistoryRequest::builder()
.range(range)
.interval(Interval::D1)
.build()
.unwrap();
let json = serde_json::to_string(&history_req).unwrap();
let deserialized: HistoryRequest = serde_json::from_str(&json).unwrap();
assert_eq!(history_req, deserialized);
}
}
#[cfg(all(feature = "domain", feature = "market"))]
#[test]
fn action_types_workflow() {
let actions = [
Action::Dividend {
date: date(2022, 1, 1),
amount: Price::new(Decimal::from_str("0.5").unwrap(), usd()),
},
Action::Split {
date: date(2022, 1, 1),
numerator: NonZeroU32::new(2).unwrap(),
denominator: NonZeroU32::new(1).unwrap(),
},
Action::CapitalGain {
date: date(2022, 1, 1),
gain: Price::new(Decimal::from_str("1.0").unwrap(), usd()),
},
];
for action in actions {
let json = serde_json::to_string(&action).unwrap();
let deserialized: Action = serde_json::from_str(&json).unwrap();
assert_eq!(action, deserialized);
let history_response = HistoryResponse {
candles: vec![],
actions: vec![action],
price_basis: OhlcPriceBasis::raw(),
meta: None,
provider: (),
};
let json = serde_json::to_string(&history_response).unwrap();
let deserialized: HistoryResponse = serde_json::from_str(&json).unwrap();
assert_eq!(history_response, deserialized);
}
}