#![cfg(all(feature = "domain", feature = "market"))]
use chrono::DateTime;
use chrono_tz::Tz;
use iso_currency::Currency as IsoCurrency;
use paft::market::MarketError;
use paft::prelude::{
Action, AssetKind, Candle, Currency, Exchange, HistoryMeta, HistoryRequest, HistoryResponse,
Instrument, Interval, MarketState, Price, Quote, QuoteUpdate, Range, SearchRequest,
};
use paft_decimal::Decimal;
use std::num::NonZeroU32;
use std::str::FromStr;
#[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)
.auto_adjust(true)
.build()
.unwrap();
let candle = Candle {
ts: DateTime::from_timestamp(1_640_995_200, 0).unwrap(),
open: Price::new(
Decimal::from_str("100.0").unwrap(),
Currency::Iso(IsoCurrency::USD),
),
high: Price::new(
Decimal::from_str("110.0").unwrap(),
Currency::Iso(IsoCurrency::USD),
),
low: Price::new(
Decimal::from_str("95.0").unwrap(),
Currency::Iso(IsoCurrency::USD),
),
close: Price::new(
Decimal::from_str("105.0").unwrap(),
Currency::Iso(IsoCurrency::USD),
),
close_unadj: None,
volume: Some(1_000_000),
provider: (),
};
let action = Action::Dividend {
ts: DateTime::from_timestamp(1_640_995_200, 0).unwrap(),
amount: Price::new(
Decimal::from_str("0.5").unwrap(),
Currency::Iso(IsoCurrency::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],
adjusted: true,
meta: Some(meta),
provider: (),
};
let quote = Quote {
instrument: instrument.clone(),
name: Some("Apple Inc.".to_string()),
price: Some(Price::new(
Decimal::from(105),
Currency::Iso(IsoCurrency::USD),
)),
previous_close: Some(Price::new(
Decimal::from(100),
Currency::Iso(IsoCurrency::USD),
)),
day_volume: None,
market_state: Some(MarketState::Regular),
as_of: None,
bid: None,
ask: None,
provider: (),
};
let quote_update = QuoteUpdate {
instrument: instrument.clone(),
price: Some(Price::new(
Decimal::from(106),
Currency::Iso(IsoCurrency::USD),
)),
previous_close: Some(Price::new(
Decimal::from(100),
Currency::Iso(IsoCurrency::USD),
)),
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].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)
.auto_adjust(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()),
price: Some(Price::new(
Decimal::from(150),
Currency::Iso(IsoCurrency::USD),
)),
previous_close: Some(Price::new(
Decimal::from(1475) / Decimal::from(10),
Currency::Iso(IsoCurrency::USD),
)),
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 {
ts: DateTime::from_timestamp(1_640_995_200, 0).unwrap(),
amount: Price::new(
Decimal::from_str("0.5").unwrap(),
Currency::Iso(IsoCurrency::USD),
),
},
Action::Split {
ts: DateTime::from_timestamp(1_640_995_200, 0).unwrap(),
numerator: NonZeroU32::new(2).unwrap(),
denominator: NonZeroU32::new(1).unwrap(),
},
Action::CapitalGain {
ts: DateTime::from_timestamp(1_640_995_200, 0).unwrap(),
gain: Price::new(
Decimal::from_str("1.0").unwrap(),
Currency::Iso(IsoCurrency::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],
adjusted: false,
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);
}
}