use crate::core::types::AccountType;
#[derive(Debug, Clone)]
pub struct FyersUrls {
pub rest_api: &'static str,
pub rest_data: &'static str,
pub ws_data: &'static str,
pub ws_order: &'static str,
pub ws_tbt: &'static str,
pub auth_base: &'static str,
}
impl FyersUrls {
pub const PRODUCTION: Self = Self {
rest_api: "https://api.fyers.in",
rest_data: "https://api-t1.fyers.in",
ws_data: "wss://api-t1.fyers.in/socket/v3/dataSock",
ws_order: "wss://api-t1.fyers.in/socket/v3/orderSock",
ws_tbt: "wss://rtsocket-api.fyers.in/versova",
auth_base: "https://api.fyers.in",
};
pub fn rest_url(&self, is_data_endpoint: bool) -> &str {
if is_data_endpoint {
self.rest_data
} else {
self.rest_api
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum FyersEndpoint {
GenerateAuthCode,
ValidateAuthCode,
GenerateToken,
Profile,
Funds,
Holdings,
Quotes,
Depth,
History,
MarketStatus,
SymbolMaster,
PlaceOrder,
PlaceOrderMulti,
ModifyOrder,
CancelOrder,
GetOrders,
GetOrderById,
Positions,
ConvertPosition,
Tradebook,
GenerateTpin,
EdisTransactions,
SubmitHoldings,
InquireTransaction,
NetPosition,
BasketOrders,
}
impl FyersEndpoint {
pub fn path(&self) -> &'static str {
match self {
Self::GenerateAuthCode => "/api/v3/generate-authcode",
Self::ValidateAuthCode => "/api/v3/validate-authcode",
Self::GenerateToken => "/api/v3/token",
Self::Profile => "/api/v3/profile",
Self::Funds => "/api/v3/funds",
Self::Holdings => "/api/v3/holdings",
Self::Quotes => "/data/quotes",
Self::Depth => "/data/depth/",
Self::History => "/data/history",
Self::MarketStatus => "/data/market-status",
Self::SymbolMaster => "/data/symbol-master",
Self::PlaceOrder => "/api/v3/orders",
Self::PlaceOrderMulti => "/api/v3/orders/multi",
Self::ModifyOrder => "/api/v3/orders",
Self::CancelOrder => "/api/v3/orders",
Self::GetOrders => "/api/v3/orders",
Self::GetOrderById => "/api/v3/orders",
Self::Positions => "/api/v3/positions",
Self::ConvertPosition => "/api/v3/positions",
Self::Tradebook => "/api/v3/tradebook",
Self::GenerateTpin => "/api/v3/edis/generate-tpin",
Self::EdisTransactions => "/api/v3/edis/transactions",
Self::SubmitHoldings => "/api/v3/edis/submit-holdings",
Self::InquireTransaction => "/api/v3/edis/inquire-transaction",
Self::NetPosition => "/api/v3/positions",
Self::BasketOrders => "/api/v3/orders/multi",
}
}
pub fn requires_auth(&self) -> bool {
match self {
Self::GenerateAuthCode
| Self::ValidateAuthCode
| Self::GenerateToken
| Self::MarketStatus => false,
_ => true,
}
}
pub fn method(&self) -> &'static str {
match self {
Self::GenerateAuthCode
| Self::ValidateAuthCode
| Self::GenerateToken
| Self::PlaceOrder
| Self::PlaceOrderMulti
| Self::BasketOrders
| Self::GenerateTpin
| Self::SubmitHoldings
| Self::InquireTransaction => "POST",
Self::ModifyOrder | Self::ConvertPosition => "PUT",
Self::CancelOrder => "DELETE",
_ => "GET",
}
}
pub fn is_data_endpoint(&self) -> bool {
matches!(
self,
Self::Quotes
| Self::Depth
| Self::History
| Self::MarketStatus
| Self::SymbolMaster
)
}
}
pub fn format_symbol(base: &str, quote: &str, _account_type: AccountType) -> String {
let exchange = if quote.is_empty() { "NSE" } else { quote };
if base.contains('-') {
return format!("{}:{}", exchange, base);
}
let up = base.to_uppercase();
let ends_with_option = (up.ends_with("CE") || up.ends_with("PE"))
&& up
.as_bytes()
.iter()
.rev()
.nth(2) .map(|b| b.is_ascii_digit())
.unwrap_or(false);
let is_fno = up.ends_with("FUT") || ends_with_option;
if is_fno {
format!("{}:{}", exchange, up)
} else {
format!("{}:{}-EQ", exchange, up)
}
}
pub fn map_kline_interval(interval: &str) -> String {
match interval {
"1m" => "1",
"2m" => "2",
"3m" => "3",
"5m" => "5",
"10m" => "10",
"15m" => "15",
"30m" => "30",
"45m" => "45",
"1h" => "60",
"2h" => "120",
"3h" => "180",
"4h" => "240",
"1d" => "1D",
"1w" => "1W",
"1M" => "1M",
_ => "60", }
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_symbol() {
assert_eq!(
format_symbol("SBIN", "NSE", AccountType::Spot),
"NSE:SBIN-EQ"
);
assert_eq!(
format_symbol("SENSEX", "BSE", AccountType::Spot),
"BSE:SENSEX-EQ"
);
assert_eq!(
format_symbol("NIFTY24JANFUT", "NSE", AccountType::Spot),
"NSE:NIFTY24JANFUT"
);
assert_eq!(
format_symbol("RELIANCE", "", AccountType::Spot),
"NSE:RELIANCE-EQ"
);
}
#[test]
fn test_map_kline_interval() {
assert_eq!(map_kline_interval("1m"), "1");
assert_eq!(map_kline_interval("5m"), "5");
assert_eq!(map_kline_interval("1h"), "60");
assert_eq!(map_kline_interval("1d"), "1D");
assert_eq!(map_kline_interval("1w"), "1W");
assert_eq!(map_kline_interval("1M"), "1M");
}
#[test]
fn test_endpoint_methods() {
assert_eq!(FyersEndpoint::PlaceOrder.method(), "POST");
assert_eq!(FyersEndpoint::ModifyOrder.method(), "PUT");
assert_eq!(FyersEndpoint::CancelOrder.method(), "DELETE");
assert_eq!(FyersEndpoint::Profile.method(), "GET");
}
#[test]
fn test_endpoint_auth() {
assert!(!FyersEndpoint::MarketStatus.requires_auth());
assert!(FyersEndpoint::Profile.requires_auth());
assert!(FyersEndpoint::PlaceOrder.requires_auth());
}
#[test]
fn test_data_endpoint() {
assert!(FyersEndpoint::Quotes.is_data_endpoint());
assert!(FyersEndpoint::History.is_data_endpoint());
assert!(!FyersEndpoint::PlaceOrder.is_data_endpoint());
assert!(!FyersEndpoint::Profile.is_data_endpoint());
}
}