#![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, Money, Quote, QuoteUpdate, Range, SearchRequest, Symbol,
};
use paft_money::Decimal;
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::try_new(
"AAPL",
AssetKind::Equity,
Some("BBG000B9XRY4"),
None,
Some(Exchange::NASDAQ),
)
.expect("valid instrument");
assert_eq!(instrument.symbol().as_str(), "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: Money::new(
Decimal::from_str("100.0").unwrap(),
Currency::Iso(IsoCurrency::USD),
)
.unwrap(),
high: Money::new(
Decimal::from_str("110.0").unwrap(),
Currency::Iso(IsoCurrency::USD),
)
.unwrap(),
low: Money::new(
Decimal::from_str("95.0").unwrap(),
Currency::Iso(IsoCurrency::USD),
)
.unwrap(),
close: Money::new(
Decimal::from_str("105.0").unwrap(),
Currency::Iso(IsoCurrency::USD),
)
.unwrap(),
close_unadj: None,
volume: Some(1_000_000),
};
let action = Action::Dividend {
ts: DateTime::from_timestamp(1_640_995_200, 0).unwrap(),
amount: Money::new(
Decimal::from_str("0.5").unwrap(),
Currency::Iso(IsoCurrency::USD),
)
.unwrap(),
};
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),
};
let quote = Quote {
symbol: Symbol::new("AAPL").unwrap(),
shortname: Some("Apple Inc.".to_string()),
price: Some(Money::new(Decimal::from(105), Currency::Iso(IsoCurrency::USD)).unwrap()),
previous_close: Some(
Money::new(Decimal::from(100), Currency::Iso(IsoCurrency::USD)).unwrap(),
),
day_volume: None,
exchange: Some(Exchange::NASDAQ),
market_state: Some(MarketState::Regular),
};
let quote_update = QuoteUpdate {
symbol: Symbol::new("AAPL").unwrap(),
price: Some(Money::new(Decimal::from(106), Currency::Iso(IsoCurrency::USD)).unwrap()),
previous_close: Some(
Money::new(Decimal::from(100), Currency::Iso(IsoCurrency::USD)).unwrap(),
),
volume: None,
ts: DateTime::from_timestamp(1_640_995_260, 0).unwrap(),
};
assert_eq!(quote.symbol.as_str(), instrument.symbol().as_str());
assert_eq!(quote_update.symbol.as_str(), instrument.symbol().as_str());
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, 2000);
assert_eq!(end, 1000);
} 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 {
symbol: Symbol::new("AAPL").unwrap(),
shortname: Some("Apple Inc.".to_string()),
price: Some(Money::new(Decimal::from(150), Currency::Iso(IsoCurrency::USD)).unwrap()),
previous_close: Some(
Money::new(
Decimal::from(1475) / Decimal::from(10),
Currency::Iso(IsoCurrency::USD),
)
.unwrap(),
),
day_volume: None,
exchange: Some(Exchange::NASDAQ),
market_state: Some(MarketState::Regular),
};
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::try_new(
"TEST",
asset_kind,
Some("BBG000B9XRY4"),
None,
Some(Exchange::try_from_str("TEST").unwrap()),
)
.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: Money::new(
Decimal::from_str("0.5").unwrap(),
Currency::Iso(IsoCurrency::USD),
)
.unwrap(),
},
Action::Split {
ts: DateTime::from_timestamp(1_640_995_200, 0).unwrap(),
numerator: 2,
denominator: 1,
},
Action::CapitalGain {
ts: DateTime::from_timestamp(1_640_995_200, 0).unwrap(),
gain: Money::new(
Decimal::from_str("1.0").unwrap(),
Currency::Iso(IsoCurrency::USD),
)
.unwrap(),
},
];
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,
};
let json = serde_json::to_string(&history_response).unwrap();
let deserialized: HistoryResponse = serde_json::from_str(&json).unwrap();
assert_eq!(history_response, deserialized);
}
}