#[derive(Debug, Clone)]
pub struct UpstoxUrls {
pub rest_base: &'static str,
pub rest_hft: &'static str,
pub rest_v3: &'static str,
pub _ws_market_data: &'static str,
pub _ws_portfolio: &'static str,
}
impl UpstoxUrls {
pub const MAINNET: Self = Self {
rest_base: "https://api.upstox.com/v2",
rest_hft: "https://api-hft.upstox.com/v2",
rest_v3: "https://api.upstox.com/v3",
_ws_market_data: "wss://api.upstox.com/v2/feed/market-data-feed/protobuf",
_ws_portfolio: "wss://api.upstox.com/v2/feed/portfolio-stream-feed",
};
pub fn rest_url(&self, use_hft: bool) -> &str {
if use_hft {
self.rest_hft
} else {
self.rest_base
}
}
pub fn rest_v3_url(&self) -> &str {
self.rest_v3
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[allow(dead_code)]
pub enum UpstoxEndpoint {
LoginDialog,
LoginToken,
MarketQuoteLtp,
MarketQuoteQuotes,
MarketQuoteOhlc,
MarketQuotesMulti,
HistoricalCandleV2,
HistoricalCandleV3,
HistoricalDataV3,
IntradayCandleV2,
IntradayCandleV3,
OptionChain,
OptionContract,
OrderPlaceV2,
OrderPlaceV3,
OrderModify,
OrderCancel,
OrderDetails,
OrderBook,
OrderTrades,
TradeHistory,
MultiOrderPlace,
MultiOrderCancel,
GttPlace,
GttModify,
GttCancel,
GttOrders,
GttOrderDetails,
PositionsShortTerm,
HoldingsLongTerm,
MtfPositions,
ConvertPosition,
ExitAllPositions,
FundsAndMargin,
MarginRequirement,
TradeCharges,
TradePnl,
Brokerage,
UserProfile,
WsMarketDataAuthorize,
WsPortfolioAuthorize,
}
impl UpstoxEndpoint {
pub fn path(&self) -> String {
match self {
Self::LoginDialog => "/login/authorization/dialog".to_string(),
Self::LoginToken => "/login/authorization/token".to_string(),
Self::MarketQuoteLtp => "/market-quote/ltp".to_string(),
Self::MarketQuoteQuotes => "/market-quote/quotes".to_string(),
Self::MarketQuoteOhlc => "/market-quote/ohlc".to_string(),
Self::MarketQuotesMulti => "/market-quote/ltp".to_string(),
Self::HistoricalCandleV2 => "/historical-candle".to_string(),
Self::HistoricalCandleV3 => "/historical-candle".to_string(),
Self::HistoricalDataV3 => "/historical-candle".to_string(),
Self::IntradayCandleV2 => "/historical-candle/intraday".to_string(),
Self::IntradayCandleV3 => "/historical-candle/intraday".to_string(),
Self::OptionChain => "/option/chain".to_string(),
Self::OptionContract => "/option/contract".to_string(),
Self::OrderPlaceV2 => "/order/place".to_string(),
Self::OrderPlaceV3 => "/order/place".to_string(),
Self::OrderModify => "/order/modify".to_string(),
Self::OrderCancel => "/order/cancel".to_string(),
Self::OrderDetails => "/order/details".to_string(),
Self::OrderBook => "/order/details".to_string(),
Self::OrderTrades => "/order/trades".to_string(),
Self::TradeHistory => "/order/history".to_string(),
Self::MultiOrderPlace => "/order/multi/place".to_string(),
Self::MultiOrderCancel => "/order/multi/cancel".to_string(),
Self::GttPlace => "/order/gtt/place".to_string(),
Self::GttModify => "/order/gtt/modify".to_string(),
Self::GttCancel => "/order/gtt/cancel".to_string(),
Self::GttOrders => "/gtt/orders".to_string(),
Self::GttOrderDetails => "/gtt/order".to_string(),
Self::PositionsShortTerm => "/portfolio/short-term-positions".to_string(),
Self::HoldingsLongTerm => "/portfolio/long-term-holdings".to_string(),
Self::MtfPositions => "/portfolio/mtf-positions".to_string(),
Self::ConvertPosition => "/portfolio/convert-position".to_string(),
Self::ExitAllPositions => "/portfolio/positions".to_string(),
Self::FundsAndMargin => "/user/get-funds-and-margin".to_string(),
Self::MarginRequirement => "/charges/margin".to_string(),
Self::TradeCharges => "/trade/profit-loss/charges".to_string(),
Self::TradePnl => "/trade/profit-loss/data".to_string(),
Self::Brokerage => "/charges/brokerage".to_string(),
Self::UserProfile => "/user/profile".to_string(),
Self::WsMarketDataAuthorize => "/feed/market-data-feed/authorize".to_string(),
Self::WsPortfolioAuthorize => "/feed/portfolio-stream-feed/authorize".to_string(),
}
}
pub fn is_v3(&self) -> bool {
matches!(
self,
Self::HistoricalCandleV3
| Self::IntradayCandleV3
| Self::HistoricalDataV3
| Self::OrderPlaceV3
| Self::GttPlace
| Self::GttModify
| Self::GttCancel
| Self::MtfPositions
)
}
}
pub fn format_symbol(symbol: &crate::core::types::Symbol) -> String {
if symbol.quote.is_empty() {
format!("NSE_EQ|{}", symbol.base.to_uppercase())
} else if symbol.quote.contains('|') {
symbol.quote.clone()
} else {
format!("{}|{}", symbol.quote.to_uppercase(), symbol.base.to_uppercase())
}
}
pub fn _parse_symbol(api_symbol: &str) -> crate::core::types::Symbol {
if let Some((segment, identifier)) = api_symbol.split_once('|') {
crate::core::types::Symbol {
base: identifier.to_string(),
quote: segment.to_string(),
raw: Some(api_symbol.to_string()),
}
} else {
crate::core::types::Symbol {
base: api_symbol.to_string(),
quote: "NSE_EQ".to_string(),
raw: Some(api_symbol.to_string()),
}
}
}
pub fn map_kline_interval(interval: &str) -> crate::core::ExchangeResult<(&'static str, String)> {
let interval_lower = interval.to_lowercase();
if interval_lower.ends_with('m') {
let num = interval_lower.trim_end_matches('m');
Ok(("minutes", num.to_string()))
} else if interval_lower.ends_with('h') {
let num = interval_lower.trim_end_matches('h');
Ok(("hours", num.to_string()))
} else if interval_lower.ends_with('d') {
Ok(("days", "1".to_string()))
} else if interval_lower.ends_with('w') {
Ok(("weeks", "1".to_string()))
} else if interval_lower.ends_with("mo") {
Ok(("months", "1".to_string()))
} else {
Err(crate::core::ExchangeError::InvalidRequest(
format!("Invalid interval: {}", interval)
))
}
}