mod common;
use common::{ACCOUNT_ID, mock_client, standard_headers};
use oanda_rs::models::{
CandleSpecification, CandlestickGranularity, InstrumentName, PricingComponent,
};
use serde_json::json;
use wiremock::matchers::{method, path, query_param};
use wiremock::{Mock, ResponseTemplate};
fn candles_body() -> serde_json::Value {
json!({
"instrument": "EUR_USD",
"granularity": "H1",
"candles": [
{
"time": "2024-06-14T12:00:00.000000000Z",
"mid": {"o": "1.07132", "h": "1.07240", "l": "1.07106", "c": "1.07223"},
"volume": 4713,
"complete": true
},
{
"time": "2024-06-14T13:00:00.000000000Z",
"bid": {"o": "1.07215", "h": "1.07220", "l": "1.07158", "c": "1.07198"},
"ask": {"o": "1.07230", "h": "1.07236", "l": "1.07172", "c": "1.07213"},
"volume": 3892,
"complete": false
}
]
})
}
#[tokio::test]
async fn instrument_candles_with_params() {
let (server, client) = mock_client().await;
standard_headers(
Mock::given(method("GET"))
.and(path("/instruments/EUR_USD/candles"))
.and(query_param("granularity", "H1"))
.and(query_param("count", "2"))
.and(query_param("price", "BMA"))
.and(query_param("smooth", "false"))
.and(query_param("weeklyAlignment", "Friday")),
)
.respond_with(ResponseTemplate::new(200).set_body_json(candles_body()))
.expect(1)
.mount(&server)
.await;
let candles = client
.candles("EUR_USD")
.granularity(CandlestickGranularity::H1)
.count(2)
.price(PricingComponent::BID.with_mid().with_ask())
.smooth(false)
.weekly_alignment(oanda_rs::models::WeeklyAlignment::Friday)
.send()
.await
.unwrap();
assert_eq!(candles.instrument, Some(InstrumentName::EurUsd));
assert_eq!(candles.granularity, Some(CandlestickGranularity::H1));
assert_eq!(candles.candles.len(), 2);
let first = &candles.candles[0];
assert_eq!(first.complete, Some(true));
assert_eq!(first.volume, Some(4713));
assert_eq!(
first.mid.as_ref().unwrap().o.unwrap().to_string(),
"1.07132"
);
assert!(candles.candles[1].bid.is_some());
}
#[tokio::test]
async fn account_scoped_candles_with_units() {
let (server, client) = mock_client().await;
standard_headers(
Mock::given(method("GET"))
.and(path(format!(
"/accounts/{ACCOUNT_ID}/instruments/EUR_USD/candles"
)))
.and(query_param("units", "1000")),
)
.respond_with(ResponseTemplate::new(200).set_body_json(candles_body()))
.expect(1)
.mount(&server)
.await;
let candles = client
.account_candles(ACCOUNT_ID, InstrumentName::EurUsd)
.units(1000)
.send()
.await
.unwrap();
assert_eq!(candles.candles.len(), 2);
}
#[tokio::test]
async fn latest_candles_specifications() {
let (server, client) = mock_client().await;
standard_headers(
Mock::given(method("GET"))
.and(path(format!("/accounts/{ACCOUNT_ID}/candles/latest")))
.and(query_param(
"candleSpecifications",
"EUR_USD:S10:BM,XAU_USD:M1",
)),
)
.respond_with(
ResponseTemplate::new(200).set_body_json(json!({"latestCandles": [candles_body()]})),
)
.expect(1)
.mount(&server)
.await;
let response = client
.latest_candles(
ACCOUNT_ID,
[
CandleSpecification::new("EUR_USD", CandlestickGranularity::S10)
.price(PricingComponent::BID.with_mid()),
CandleSpecification::new(InstrumentName::XauUsd, CandlestickGranularity::M1),
],
)
.send()
.await
.unwrap();
assert_eq!(response.latest_candles.len(), 1);
}
#[tokio::test]
async fn order_book_surfaces_link_header() {
let (server, client) = mock_client().await;
standard_headers(
Mock::given(method("GET"))
.and(path("/instruments/EUR_USD/orderBook"))
.and(query_param("time", "2024-06-14T12:00:00Z")),
)
.respond_with(
ResponseTemplate::new(200)
.insert_header(
"Link",
"<https://api-fxpractice.oanda.com/v3/instruments/EUR_USD/orderBook?time=123>; rel=\"next\"",
)
.set_body_json(json!({
"orderBook": {
"instrument": "EUR_USD",
"time": "2024-06-14T12:00:00Z",
"unixTime": "1718366400",
"price": "1.07132",
"bucketWidth": "0.0005",
"buckets": [
{"price": "1.07100", "longCountPercent": "0.2422", "shortCountPercent": "0.1902"}
]
}
})),
)
.expect(1)
.mount(&server)
.await;
let response = client
.instrument_order_book("EUR_USD")
.time("2024-06-14T12:00:00Z")
.send()
.await
.unwrap();
assert_eq!(response.order_book.buckets.len(), 1);
assert_eq!(
response.order_book.buckets[0]
.long_count_percent
.unwrap()
.to_string(),
"0.2422"
);
assert!(response.link.as_deref().unwrap().contains("rel=\"next\""));
}
#[tokio::test]
async fn position_book() {
let (server, client) = mock_client().await;
standard_headers(Mock::given(method("GET")).and(path("/instruments/EUR_USD/positionBook")))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"positionBook": {
"instrument": "EUR_USD",
"time": "2024-06-14T12:00:00Z",
"price": "1.07132",
"bucketWidth": "0.0005",
"buckets": [
{"price": "1.07100", "longCountPercent": "55.1", "shortCountPercent": "44.9"}
]
}
})))
.expect(1)
.mount(&server)
.await;
let response = client
.instrument_position_book(InstrumentName::EurUsd)
.send()
.await
.unwrap();
assert_eq!(response.position_book.buckets.len(), 1);
assert!(response.link.is_none());
}