use crate::core::types::AccountType;
#[derive(Debug, Clone)]
pub struct OandaUrls {
pub rest_url: &'static str,
pub stream_url: &'static str,
}
impl OandaUrls {
pub const LIVE: Self = Self {
rest_url: "https://api-fxtrade.oanda.com",
stream_url: "https://stream-fxtrade.oanda.com",
};
pub const PRACTICE: Self = Self {
rest_url: "https://api-fxpractice.oanda.com",
stream_url: "https://stream-fxpractice.oanda.com",
};
pub fn rest_url(&self, _account_type: AccountType) -> &str {
self.rest_url
}
pub fn stream_url(&self, _account_type: AccountType) -> &str {
self.stream_url
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum OandaEndpoint {
ListAccounts,
GetAccount(String),
GetAccountSummary(String),
GetInstruments(String),
PollAccountChanges(String),
GetPricing(String),
StreamPricing(String),
GetLatestCandles(String),
GetCandles(String),
CreateOrder(String),
ListOrders(String),
ListPendingOrders(String),
GetOrder { account_id: String, order_id: String },
CancelOrder { account_id: String, order_id: String },
AmendOrder { account_id: String, order_id: String },
ListTrades(String),
ListOpenTrades(String),
GetTrade { account_id: String, trade_id: String },
CloseTrade { account_id: String, trade_id: String },
ListPositions(String),
ListOpenPositions(String),
GetPosition { account_id: String, instrument: String },
ClosePosition { account_id: String, instrument: String },
StreamTransactions(String),
}
impl OandaEndpoint {
pub fn path(&self) -> String {
match self {
Self::ListAccounts => "/v3/accounts".to_string(),
Self::GetAccount(account_id) => format!("/v3/accounts/{}", account_id),
Self::GetAccountSummary(account_id) => format!("/v3/accounts/{}/summary", account_id),
Self::GetInstruments(account_id) => format!("/v3/accounts/{}/instruments", account_id),
Self::PollAccountChanges(account_id) => format!("/v3/accounts/{}/changes", account_id),
Self::GetPricing(account_id) => format!("/v3/accounts/{}/pricing", account_id),
Self::StreamPricing(account_id) => format!("/v3/accounts/{}/pricing/stream", account_id),
Self::GetLatestCandles(account_id) => format!("/v3/accounts/{}/candles/latest", account_id),
Self::GetCandles(instrument) => format!("/v3/instruments/{}/candles", instrument),
Self::CreateOrder(account_id) => format!("/v3/accounts/{}/orders", account_id),
Self::ListOrders(account_id) => format!("/v3/accounts/{}/orders", account_id),
Self::ListPendingOrders(account_id) => format!("/v3/accounts/{}/pendingOrders", account_id),
Self::GetOrder { account_id, order_id } => format!("/v3/accounts/{}/orders/{}", account_id, order_id),
Self::CancelOrder { account_id, order_id } => format!("/v3/accounts/{}/orders/{}/cancel", account_id, order_id),
Self::AmendOrder { account_id, order_id } => format!("/v3/accounts/{}/orders/{}", account_id, order_id),
Self::ListTrades(account_id) => format!("/v3/accounts/{}/trades", account_id),
Self::ListOpenTrades(account_id) => format!("/v3/accounts/{}/openTrades", account_id),
Self::GetTrade { account_id, trade_id } => format!("/v3/accounts/{}/trades/{}", account_id, trade_id),
Self::CloseTrade { account_id, trade_id } => format!("/v3/accounts/{}/trades/{}/close", account_id, trade_id),
Self::ListPositions(account_id) => format!("/v3/accounts/{}/positions", account_id),
Self::ListOpenPositions(account_id) => format!("/v3/accounts/{}/openPositions", account_id),
Self::GetPosition { account_id, instrument } => format!("/v3/accounts/{}/positions/{}", account_id, instrument),
Self::ClosePosition { account_id, instrument } => format!("/v3/accounts/{}/positions/{}/close", account_id, instrument),
Self::StreamTransactions(account_id) => format!("/v3/accounts/{}/transactions/stream", account_id),
}
}
pub fn requires_auth(&self) -> bool {
true
}
pub fn method(&self) -> &'static str {
match self {
Self::CreateOrder(_) => "POST",
Self::CancelOrder { .. }
| Self::AmendOrder { .. }
| Self::CloseTrade { .. }
| Self::ClosePosition { .. } => "PUT",
_ => "GET",
}
}
}
pub fn format_symbol(base: &str, quote: &str) -> String {
format!("{}_{}", base.to_uppercase(), quote.to_uppercase())
}
pub fn parse_symbol(s: &str) -> Option<(String, String)> {
if let Some((base, quote)) = s.split_once('_') {
Some((base.to_string(), quote.to_string()))
} else {
None
}
}
pub fn map_granularity(interval: &str) -> &'static str {
match interval {
"5s" => "S5",
"10s" => "S10",
"15s" => "S15",
"30s" => "S30",
"1m" => "M1",
"2m" => "M2",
"4m" => "M4",
"5m" => "M5",
"10m" => "M10",
"15m" => "M15",
"30m" => "M30",
"1h" => "H1",
"2h" => "H2",
"3h" => "H3",
"4h" => "H4",
"6h" => "H6",
"8h" => "H8",
"12h" => "H12",
"1d" => "D",
"1w" => "W",
"1M" => "M",
_ => "H1", }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_symbol() {
assert_eq!(format_symbol("EUR", "USD"), "EUR_USD");
assert_eq!(format_symbol("eur", "usd"), "EUR_USD");
assert_eq!(format_symbol("GBP", "JPY"), "GBP_JPY");
assert_eq!(format_symbol("XAU", "USD"), "XAU_USD");
}
#[test]
fn test_parse_symbol() {
assert_eq!(parse_symbol("EUR_USD"), Some(("EUR".to_string(), "USD".to_string())));
assert_eq!(parse_symbol("GBP_JPY"), Some(("GBP".to_string(), "JPY".to_string())));
assert_eq!(parse_symbol("INVALID"), None);
}
#[test]
fn test_map_granularity() {
assert_eq!(map_granularity("1m"), "M1");
assert_eq!(map_granularity("1h"), "H1");
assert_eq!(map_granularity("1d"), "D");
assert_eq!(map_granularity("1w"), "W");
assert_eq!(map_granularity("invalid"), "H1");
}
#[test]
fn test_endpoint_path() {
let endpoint = OandaEndpoint::GetAccount("001-011-5838423-001".to_string());
assert_eq!(endpoint.path(), "/v3/accounts/001-011-5838423-001");
let endpoint = OandaEndpoint::GetCandles("EUR_USD".to_string());
assert_eq!(endpoint.path(), "/v3/instruments/EUR_USD/candles");
}
#[test]
fn test_endpoint_method() {
assert_eq!(OandaEndpoint::ListAccounts.method(), "GET");
assert_eq!(OandaEndpoint::CreateOrder("123".to_string()).method(), "POST");
assert_eq!(OandaEndpoint::CancelOrder { account_id: "123".to_string(), order_id: "456".to_string() }.method(), "PUT");
}
}