use std::collections::HashMap;
use async_trait::async_trait;
use serde_json::{json, Value};
use crate::core::{
HttpClient,
ExchangeId, AccountType, Symbol, ExchangeType,
ExchangeError, ExchangeResult,
Kline, Ticker, OrderBook,
Order, OrderSide, OrderType, OrderStatus, TimeInForce, Price,
Balance, AccountInfo, Position, FundingRate,
OrderRequest, CancelRequest, CancelScope,
BalanceQuery, PositionQuery, PositionModification,
OrderHistoryFilter, PlaceOrderResponse, FeeInfo,
AmendRequest,
};
use crate::core::traits::{
ExchangeIdentity, MarketData, Trading, Account, Positions, AmendOrder,
};
use crate::core::types::SymbolInfo;
use crate::core::utils::precision::PrecisionCache;
use super::endpoints::{ZerodhaEndpoints, ZerodhaEndpoint, format_symbol};
use super::auth::ZerodhaAuth;
use super::parser::ZerodhaParser;
pub struct ZerodhaConnector {
http: HttpClient,
auth: ZerodhaAuth,
endpoints: ZerodhaEndpoints,
precision: PrecisionCache,
}
impl ZerodhaConnector {
pub fn new(auth: ZerodhaAuth) -> ExchangeResult<Self> {
let http = HttpClient::new(30_000)?; let endpoints = ZerodhaEndpoints::default();
Ok(Self {
http,
auth,
endpoints,
precision: PrecisionCache::new(),
})
}
pub fn from_env() -> Self {
let auth = ZerodhaAuth::from_env();
Self::new(auth.clone()).unwrap_or_else(|_| {
Self {
http: HttpClient::new(30_000).expect("HTTP client creation should not fail with valid timeout"),
auth,
endpoints: ZerodhaEndpoints::default(),
precision: PrecisionCache::new(),
}
})
}
async fn get(
&self,
endpoint: ZerodhaEndpoint,
params: HashMap<String, String>,
) -> ExchangeResult<Value> {
let base_url = self.endpoints.rest_base;
let path = endpoint.path();
let query = if params.is_empty() {
String::new()
} else {
let qs: Vec<String> = params.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect();
format!("?{}", qs.join("&"))
};
let url = format!("{}{}{}", base_url, path, query);
let mut headers = HashMap::new();
self.auth.sign_headers(&mut headers);
let response = self.http.get(&url, &headers).await?;
Ok(response)
}
async fn post(
&self,
endpoint: ZerodhaEndpoint,
body: HashMap<String, String>,
) -> ExchangeResult<Value> {
let base_url = self.endpoints.rest_base;
let path = endpoint.path();
let url = format!("{}{}", base_url, path);
let mut headers = HashMap::new();
self.auth.sign_headers(&mut headers);
headers.insert("Content-Type".to_string(), "application/x-www-form-urlencoded".to_string());
let json_body = json!(body);
let response = self.http.post(&url, &json_body, &headers).await?;
Ok(response)
}
async fn put(
&self,
endpoint: ZerodhaEndpoint,
body: HashMap<String, String>,
) -> ExchangeResult<Value> {
let base_url = self.endpoints.rest_base;
let path = endpoint.path();
let url = format!("{}{}", base_url, path);
let mut headers = HashMap::new();
self.auth.sign_headers(&mut headers);
headers.insert("Content-Type".to_string(), "application/x-www-form-urlencoded".to_string());
let json_body = json!(body);
let response = self.http.put(&url, &json_body, &headers).await?;
Ok(response)
}
async fn delete(
&self,
endpoint: ZerodhaEndpoint,
) -> ExchangeResult<Value> {
let base_url = self.endpoints.rest_base;
let path = endpoint.path();
let url = format!("{}{}", base_url, path);
let mut headers = HashMap::new();
self.auth.sign_headers(&mut headers);
let response = self.http.delete(&url, &HashMap::new(), &headers).await?;
Ok(response)
}
pub async fn gtt_list(&self) -> ExchangeResult<Value> {
self.get(ZerodhaEndpoint::GetGtts, HashMap::new()).await
}
pub async fn gtt_delete(&self, trigger_id: u64) -> ExchangeResult<Value> {
self.delete(ZerodhaEndpoint::DeleteGtt(trigger_id)).await
}
pub async fn gtt_modify(
&self,
trigger_id: u64,
body: HashMap<String, String>,
) -> ExchangeResult<Value> {
self.put(ZerodhaEndpoint::ModifyGtt(trigger_id), body).await
}
pub async fn get_instruments_master(&self, exchange: Option<&str>) -> ExchangeResult<Value> {
let endpoint = match exchange {
Some(ex) => ZerodhaEndpoint::InstrumentsExchange(ex.to_uppercase()),
None => ZerodhaEndpoint::Instruments,
};
self.get(endpoint, HashMap::new()).await
}
pub async fn place_basket_orders(
&self,
orders: Vec<HashMap<String, String>>,
) -> ExchangeResult<Value> {
let base_url = self.endpoints.rest_base;
let path = ZerodhaEndpoint::BasketOrders.path();
let url = format!("{}{}", base_url, path);
let mut headers = HashMap::new();
self.auth.sign_headers(&mut headers);
headers.insert("Content-Type".to_string(), "application/json".to_string());
let body = serde_json::Value::Array(
orders.into_iter().map(|o| json!(o)).collect()
);
let response = self.http.post(&url, &body, &headers).await?;
Ok(response)
}
}
impl ExchangeIdentity for ZerodhaConnector {
fn exchange_id(&self) -> ExchangeId {
ExchangeId::Zerodha
}
fn is_testnet(&self) -> bool {
false }
fn supported_account_types(&self) -> Vec<AccountType> {
vec![AccountType::Spot]
}
fn exchange_type(&self) -> ExchangeType {
ExchangeType::Cex
}
}
#[async_trait]
impl MarketData for ZerodhaConnector {
async fn get_price(&self, symbol: Symbol, _account_type: AccountType) -> ExchangeResult<Price> {
let symbol_key = format_symbol(&symbol);
let mut params = HashMap::new();
params.insert("i".to_string(), symbol_key.clone());
let response = self.get(ZerodhaEndpoint::QuoteLtp, params).await?;
ZerodhaParser::parse_ltp(&response, &symbol_key)
}
async fn get_ticker(&self, symbol: Symbol, _account_type: AccountType) -> ExchangeResult<Ticker> {
let symbol_key = format_symbol(&symbol);
let mut params = HashMap::new();
params.insert("i".to_string(), symbol_key.clone());
let response = self.get(ZerodhaEndpoint::Quote, params).await?;
ZerodhaParser::parse_quote(&response, &symbol_key)
}
async fn get_orderbook(
&self,
symbol: Symbol,
_depth: Option<u16>,
_account_type: AccountType,
) -> ExchangeResult<OrderBook> {
let symbol_key = format_symbol(&symbol);
let mut params = HashMap::new();
params.insert("i".to_string(), symbol_key.clone());
let response = self.get(ZerodhaEndpoint::Quote, params).await?;
ZerodhaParser::parse_orderbook(&response, &symbol_key)
}
async fn get_klines(
&self,
symbol: Symbol,
interval: &str,
_limit: Option<u16>,
_account_type: AccountType,
_end_time: Option<i64>,
) -> ExchangeResult<Vec<Kline>> {
let _ = (symbol, interval);
Err(ExchangeError::UnsupportedOperation(
"Historical data requires instrument_token. Use get_instruments() first to map symbols to tokens.".to_string()
))
}
async fn ping(&self) -> ExchangeResult<()> {
let _response = self.get(ZerodhaEndpoint::UserProfile, HashMap::new()).await?;
Ok(())
}
async fn get_exchange_info(&self, account_type: AccountType) -> ExchangeResult<Vec<SymbolInfo>> {
let url = format!("{}{}", self.endpoints.rest_base, ZerodhaEndpoint::InstrumentsExchange("NSE".to_string()).path());
let mut headers = HashMap::new();
self.auth.sign_headers(&mut headers);
let bytes = self.http.get_bytes(&url).await?;
let csv_text = String::from_utf8(bytes)
.map_err(|e| ExchangeError::Parse(format!("Invalid UTF-8 in instruments CSV: {}", e)))?;
let mut infos = Vec::new();
for (i, line) in csv_text.lines().enumerate() {
if i == 0 {
continue; }
let cols: Vec<&str> = line.split(',').collect();
if cols.len() < 12 {
continue;
}
let symbol = cols[2].trim().to_string();
let instrument_type = cols[9].trim();
let exchange = cols[11].trim();
if instrument_type != "EQ" || exchange != "NSE" {
continue;
}
let tick_size = cols.get(7)
.and_then(|s| s.trim().parse::<f64>().ok())
.filter(|&v| v > 0.0);
infos.push(SymbolInfo {
symbol: symbol.clone(),
base_asset: symbol,
quote_asset: "INR".to_string(),
status: "TRADING".to_string(),
price_precision: 2,
quantity_precision: 0,
min_quantity: Some(1.0),
max_quantity: None,
tick_size,
step_size: Some(1.0),
min_notional: None,
account_type,
});
}
self.precision.load_from_symbols(&infos);
Ok(infos)
}
}
#[async_trait]
impl Trading for ZerodhaConnector {
async fn place_order(&self, req: OrderRequest) -> ExchangeResult<PlaceOrderResponse> {
let symbol = req.symbol.clone();
let side = req.side;
let quantity = req.quantity;
let time_in_force = req.time_in_force;
let symbol_key = format_symbol(&symbol);
let parts: Vec<&str> = symbol_key.split(':').collect();
if parts.len() != 2 {
return Err(ExchangeError::Parse(format!("Invalid symbol format: {}", symbol_key)));
}
let exchange = parts[0];
let tradingsymbol = parts[1];
let validity = match time_in_force {
TimeInForce::Ioc => "IOC",
_ => "DAY",
};
match req.order_type {
OrderType::Fok { .. } => {
return Err(ExchangeError::UnsupportedOperation(
"FOK orders not supported by Zerodha Kite Connect".to_string(),
));
}
OrderType::Market => {
let mut body = HashMap::new();
body.insert("exchange".to_string(), exchange.to_string());
body.insert("tradingsymbol".to_string(), tradingsymbol.to_string());
body.insert("transaction_type".to_string(), match side {
OrderSide::Buy => "BUY",
OrderSide::Sell => "SELL",
}.to_string());
body.insert("quantity".to_string(), self.precision.qty(tradingsymbol, quantity));
body.insert("order_type".to_string(), "MARKET".to_string());
body.insert("product".to_string(), "CNC".to_string());
body.insert("validity".to_string(), validity.to_string());
let response = self.post(ZerodhaEndpoint::PlaceOrder("regular".to_string()), body).await?;
ZerodhaParser::parse_order(&response).map(PlaceOrderResponse::Simple)
}
OrderType::Limit { price } => {
let mut body = HashMap::new();
body.insert("exchange".to_string(), exchange.to_string());
body.insert("tradingsymbol".to_string(), tradingsymbol.to_string());
body.insert("transaction_type".to_string(), match side {
OrderSide::Buy => "BUY",
OrderSide::Sell => "SELL",
}.to_string());
body.insert("quantity".to_string(), self.precision.qty(tradingsymbol, quantity));
body.insert("order_type".to_string(), "LIMIT".to_string());
body.insert("price".to_string(), self.precision.price(tradingsymbol, price));
body.insert("product".to_string(), "CNC".to_string());
body.insert("validity".to_string(), validity.to_string());
let response = self.post(ZerodhaEndpoint::PlaceOrder("regular".to_string()), body).await?;
ZerodhaParser::parse_order(&response).map(PlaceOrderResponse::Simple)
}
OrderType::Ioc { price } => {
let mut body = HashMap::new();
body.insert("exchange".to_string(), exchange.to_string());
body.insert("tradingsymbol".to_string(), tradingsymbol.to_string());
body.insert("transaction_type".to_string(), match side {
OrderSide::Buy => "BUY",
OrderSide::Sell => "SELL",
}.to_string());
body.insert("quantity".to_string(), self.precision.qty(tradingsymbol, quantity));
body.insert("product".to_string(), "CNC".to_string());
body.insert("validity".to_string(), "IOC".to_string());
if let Some(p) = price {
body.insert("order_type".to_string(), "LIMIT".to_string());
body.insert("price".to_string(), self.precision.price(tradingsymbol, p));
} else {
body.insert("order_type".to_string(), "MARKET".to_string());
}
let response = self.post(ZerodhaEndpoint::PlaceOrder("regular".to_string()), body).await?;
ZerodhaParser::parse_order(&response).map(PlaceOrderResponse::Simple)
}
OrderType::StopMarket { stop_price } => {
let mut body = HashMap::new();
body.insert("exchange".to_string(), exchange.to_string());
body.insert("tradingsymbol".to_string(), tradingsymbol.to_string());
body.insert("transaction_type".to_string(), match side {
OrderSide::Buy => "BUY",
OrderSide::Sell => "SELL",
}.to_string());
body.insert("quantity".to_string(), self.precision.qty(tradingsymbol, quantity));
body.insert("order_type".to_string(), "SL-M".to_string());
body.insert("trigger_price".to_string(), self.precision.price(tradingsymbol, stop_price));
body.insert("product".to_string(), "CNC".to_string());
body.insert("validity".to_string(), validity.to_string());
let response = self.post(ZerodhaEndpoint::PlaceOrder("regular".to_string()), body).await?;
ZerodhaParser::parse_order(&response).map(PlaceOrderResponse::Simple)
}
OrderType::StopLimit { stop_price, limit_price } => {
let mut body = HashMap::new();
body.insert("exchange".to_string(), exchange.to_string());
body.insert("tradingsymbol".to_string(), tradingsymbol.to_string());
body.insert("transaction_type".to_string(), match side {
OrderSide::Buy => "BUY",
OrderSide::Sell => "SELL",
}.to_string());
body.insert("quantity".to_string(), self.precision.qty(tradingsymbol, quantity));
body.insert("order_type".to_string(), "SL".to_string());
body.insert("price".to_string(), self.precision.price(tradingsymbol, limit_price));
body.insert("trigger_price".to_string(), self.precision.price(tradingsymbol, stop_price));
body.insert("product".to_string(), "CNC".to_string());
body.insert("validity".to_string(), validity.to_string());
let response = self.post(ZerodhaEndpoint::PlaceOrder("regular".to_string()), body).await?;
ZerodhaParser::parse_order(&response).map(PlaceOrderResponse::Simple)
}
OrderType::Bracket { price, stop_loss, .. } => {
let entry_price = price.ok_or_else(|| {
ExchangeError::InvalidRequest(
"Bracket orders on Zerodha (Cover Order) require an entry price".to_string(),
)
})?;
let mut body = HashMap::new();
body.insert("exchange".to_string(), exchange.to_string());
body.insert("tradingsymbol".to_string(), tradingsymbol.to_string());
body.insert("transaction_type".to_string(), match side {
OrderSide::Buy => "BUY",
OrderSide::Sell => "SELL",
}.to_string());
body.insert("quantity".to_string(), self.precision.qty(tradingsymbol, quantity));
body.insert("order_type".to_string(), "LIMIT".to_string());
body.insert("price".to_string(), self.precision.price(tradingsymbol, entry_price));
body.insert("trigger_price".to_string(), self.precision.price(tradingsymbol, stop_loss));
body.insert("product".to_string(), "MIS".to_string()); body.insert("validity".to_string(), "DAY".to_string());
let response = self.post(ZerodhaEndpoint::PlaceOrder("co".to_string()), body).await?;
ZerodhaParser::parse_order(&response).map(PlaceOrderResponse::Simple)
}
OrderType::Iceberg { price, display_quantity } => {
let total_qty = quantity as u32;
let display_qty = display_quantity as u32;
let legs = total_qty.div_ceil(display_qty);
let legs = legs.clamp(2, 10);
let mut body = HashMap::new();
body.insert("exchange".to_string(), exchange.to_string());
body.insert("tradingsymbol".to_string(), tradingsymbol.to_string());
body.insert("transaction_type".to_string(), match side {
OrderSide::Buy => "BUY",
OrderSide::Sell => "SELL",
}.to_string());
body.insert("quantity".to_string(), self.precision.qty(tradingsymbol, quantity));
body.insert("order_type".to_string(), "LIMIT".to_string());
body.insert("price".to_string(), self.precision.price(tradingsymbol, price));
body.insert("product".to_string(), "CNC".to_string());
body.insert("validity".to_string(), "DAY".to_string());
body.insert("iceberg_legs".to_string(), legs.to_string());
body.insert("iceberg_quantity".to_string(), self.precision.qty(tradingsymbol, display_quantity));
let response = self.post(ZerodhaEndpoint::PlaceOrder("iceberg".to_string()), body).await?;
ZerodhaParser::parse_order(&response).map(PlaceOrderResponse::Simple)
}
other => Err(ExchangeError::UnsupportedOperation(
format!("{:?} order type not supported on Zerodha Kite Connect", other)
)),
}
}
async fn get_order_history(
&self,
filter: OrderHistoryFilter,
_account_type: AccountType,
) -> ExchangeResult<Vec<Order>> {
let response = self.get(ZerodhaEndpoint::GetOrders, HashMap::new()).await?;
let all_orders = ZerodhaParser::parse_orders(&response)?;
let filtered: Vec<Order> = all_orders
.into_iter()
.filter(|o| {
!matches!(o.status, OrderStatus::Open | OrderStatus::New | OrderStatus::PartiallyFilled)
})
.filter(|o| {
if let Some(status) = &filter.status {
&o.status == status
} else {
true
}
})
.filter(|o| {
if let Some(sym) = &filter.symbol {
let sym_str = format_symbol(sym);
let parts: Vec<&str> = sym_str.split(':').collect();
let trading_sym = if parts.len() == 2 { parts[1] } else { sym_str.as_str() };
o.symbol == trading_sym || o.symbol == sym_str
} else {
true
}
})
.take(filter.limit.unwrap_or(500) as usize)
.collect();
Ok(filtered)
}
async fn cancel_order(&self, req: CancelRequest) -> ExchangeResult<Order> {
match req.scope {
CancelScope::Single { ref order_id } => {
let _symbol = req.symbol.as_ref()
.ok_or_else(|| ExchangeError::InvalidRequest("Symbol required for cancel".into()))?
.clone();
let _account_type = req.account_type;
let _response = self.delete(
ZerodhaEndpoint::CancelOrder("regular".to_string(), order_id.to_string())
).await?;
Ok(Order {
id: order_id.to_string(),
client_order_id: None,
symbol: String::new(),
side: OrderSide::Buy,
order_type: OrderType::Market,
status: OrderStatus::Canceled,
price: None,
stop_price: None,
quantity: 0.0,
filled_quantity: 0.0,
average_price: None,
commission: None,
commission_asset: None,
created_at: 0,
updated_at: Some(crate::core::timestamp_millis() as i64),
time_in_force: TimeInForce::Gtc,
})
}
_ => Err(ExchangeError::UnsupportedOperation(
format!("{:?} cancel scope not supported on {:?}", req.scope, self.exchange_id())
)),
}
}
async fn get_order(
&self,
_symbol: &str,
order_id: &str,
_account_type: AccountType,
) -> ExchangeResult<Order> {
let _symbol_parts: Vec<&str> = _symbol.split('/').collect();
let _symbol = if _symbol_parts.len() == 2 {
crate::core::Symbol::new(_symbol_parts[0], _symbol_parts[1])
} else {
crate::core::Symbol { base: _symbol.to_string(), quote: String::new(), raw: Some(_symbol.to_string()) }
};
let response = self.get(
ZerodhaEndpoint::GetOrder(order_id.to_string()),
HashMap::new()
).await?;
ZerodhaParser::parse_order(&response)
}
async fn get_open_orders(
&self,
_symbol: Option<&str>,
_account_type: AccountType,
) -> ExchangeResult<Vec<Order>> {
let _symbol_str = _symbol;
let _symbol: Option<crate::core::Symbol> = _symbol_str.map(|s| {
let parts: Vec<&str> = s.split('/').collect();
if parts.len() == 2 {
crate::core::Symbol::new(parts[0], parts[1])
} else {
crate::core::Symbol { base: s.to_string(), quote: String::new(), raw: Some(s.to_string()) }
}
});
let response = self.get(ZerodhaEndpoint::GetOrders, HashMap::new()).await?;
let all_orders = ZerodhaParser::parse_orders(&response)?;
Ok(all_orders.into_iter()
.filter(|o| matches!(o.status, OrderStatus::Open))
.collect())
}
}
#[async_trait]
impl Account for ZerodhaConnector {
async fn get_balance(&self, query: BalanceQuery) -> ExchangeResult<Vec<Balance>> {
let _asset = query.asset.clone();
let _account_type = query.account_type;
let response = self.get(ZerodhaEndpoint::GetMargins, HashMap::new()).await?;
ZerodhaParser::parse_balance(&response)
}
async fn get_account_info(&self, _account_type: AccountType) -> ExchangeResult<AccountInfo> {
let response = self.get(ZerodhaEndpoint::UserProfile, HashMap::new()).await?;
ZerodhaParser::parse_account_info(&response)
}
async fn get_fees(&self, _symbol: Option<&str>) -> ExchangeResult<FeeInfo> {
Err(ExchangeError::UnsupportedOperation(
"get_fees not yet implemented".to_string()
))
}
}
#[async_trait]
impl Positions for ZerodhaConnector {
async fn get_positions(&self, query: PositionQuery) -> ExchangeResult<Vec<Position>> {
let _symbol = query.symbol.clone();
let _account_type = query.account_type;
let response = self.get(ZerodhaEndpoint::Positions, HashMap::new()).await?;
ZerodhaParser::parse_positions(&response)
}
async fn get_funding_rate(
&self,
_symbol: &str,
_account_type: AccountType,
) -> ExchangeResult<FundingRate> {
let _symbol_str = _symbol;
let _symbol = {
let parts: Vec<&str> = _symbol_str.split('/').collect();
if parts.len() == 2 {
crate::core::Symbol::new(parts[0], parts[1])
} else {
crate::core::Symbol { base: _symbol_str.to_string(), quote: String::new(), raw: Some(_symbol_str.to_string()) }
}
};
Err(ExchangeError::UnsupportedOperation(
"Funding rates not supported for stock broker".to_string()
))
}
async fn modify_position(&self, req: PositionModification) -> ExchangeResult<()> {
match req {
PositionModification::SetLeverage { symbol: ref _symbol, leverage: _leverage, account_type: _account_type } => {
let _symbol = _symbol.clone();
Err(ExchangeError::UnsupportedOperation(
"Leverage setting not supported. Use product types (MIS/NRML) instead".to_string()
))
}
_ => Err(ExchangeError::UnsupportedOperation(
format!("{:?} not supported on {:?}", req, self.exchange_id())
)),
}
}
}
#[async_trait]
impl AmendOrder for ZerodhaConnector {
async fn amend_order(&self, req: AmendRequest) -> ExchangeResult<Order> {
if req.fields.price.is_none()
&& req.fields.quantity.is_none()
&& req.fields.trigger_price.is_none()
{
return Err(ExchangeError::InvalidRequest(
"At least one of price, quantity, or trigger_price must be provided".to_string(),
));
}
let symbol_key = format_symbol(&req.symbol);
let amend_tradingsymbol: &str = symbol_key.split(':').nth(1).unwrap_or(&symbol_key);
let mut body = HashMap::new();
if let Some(price) = req.fields.price {
body.insert("price".to_string(), self.precision.price(amend_tradingsymbol, price));
}
if let Some(qty) = req.fields.quantity {
body.insert("quantity".to_string(), self.precision.qty(amend_tradingsymbol, qty));
}
if let Some(trigger) = req.fields.trigger_price {
body.insert("trigger_price".to_string(), self.precision.price(amend_tradingsymbol, trigger));
}
let (variety, order_id) = if let Some(sep) = req.order_id.find(':') {
let v = req.order_id[..sep].to_string();
let id = req.order_id[sep + 1..].to_string();
(v, id)
} else {
("regular".to_string(), req.order_id.clone())
};
let _response = self
.put(ZerodhaEndpoint::ModifyOrder(variety, order_id.clone()), body)
.await?;
let updated = self
.get(ZerodhaEndpoint::GetOrder(order_id), HashMap::new())
.await?;
ZerodhaParser::parse_order(&updated)
}
}
impl ZerodhaConnector {
pub async fn get_holdings(&self) -> ExchangeResult<Value> {
self.get(ZerodhaEndpoint::Holdings, HashMap::new()).await
}
pub async fn convert_position(
&self,
exchange: &str,
tradingsymbol: &str,
transaction_type: &str,
quantity: f64,
old_product: &str,
new_product: &str,
) -> ExchangeResult<Value> {
let mut body = HashMap::new();
body.insert("exchange".to_string(), exchange.to_string());
body.insert("tradingsymbol".to_string(), tradingsymbol.to_string());
body.insert("transaction_type".to_string(), transaction_type.to_string());
body.insert("quantity".to_string(), quantity.to_string());
body.insert("old_product".to_string(), old_product.to_string());
body.insert("new_product".to_string(), new_product.to_string());
self.put(ZerodhaEndpoint::ConvertPosition, body).await
}
}