use std::collections::HashMap;
use async_trait::async_trait;
use serde_json::{json, Value};
use reqwest;
use crate::core::{
timestamp_seconds, timestamp_millis, AccountInfo, AccountType, Balance, ExchangeError,
ExchangeId, ExchangeResult, ExchangeType, FundingRate, HttpClient, Kline, Order, OrderBook,
OrderSide, OrderType, OrderStatus, TimeInForce, Position, Price, Symbol, Ticker,
OrderRequest, CancelRequest, CancelScope,
BalanceQuery, PositionQuery, PositionModification,
OrderHistoryFilter, PlaceOrderResponse, FeeInfo,
AmendRequest, OrderResult,
};
use crate::core::traits::{Account, AmendOrder, BatchOrders, ExchangeIdentity, MarketData, Positions, Trading};
use crate::core::types::SymbolInfo;
use super::auth::FyersAuth;
use super::endpoints::{format_symbol, map_kline_interval, FyersEndpoint, FyersUrls};
use super::parser::FyersParser;
pub struct FyersConnector {
http: HttpClient,
auth: FyersAuth,
urls: FyersUrls,
}
impl FyersConnector {
pub fn new(auth: FyersAuth) -> ExchangeResult<Self> {
let http = HttpClient::new(30_000)?; let urls = FyersUrls::PRODUCTION;
Ok(Self { http, auth, urls })
}
pub fn from_env() -> ExchangeResult<Self> {
let auth = FyersAuth::from_env();
Self::new(auth)
}
pub fn with_token(
app_id: impl Into<String>,
app_secret: impl Into<String>,
access_token: impl Into<String>,
) -> ExchangeResult<Self> {
let auth = FyersAuth::with_token(app_id, app_secret, access_token);
Self::new(auth)
}
async fn get(
&self,
endpoint: FyersEndpoint,
params: HashMap<String, String>,
) -> ExchangeResult<Value> {
let base_url = self.urls.rest_url(endpoint.is_data_endpoint());
let path = endpoint.path();
let query = if params.is_empty() {
String::new()
} else {
let qs: Vec<String> = params
.iter()
.map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
.collect();
format!("?{}", qs.join("&"))
};
let url = format!("{}{}{}", base_url, path, query);
let mut headers = HashMap::new();
if endpoint.requires_auth() {
self.auth.sign_headers(&mut headers);
}
let response = self.http.get(&url, &headers).await?;
Ok(response)
}
async fn post(
&self,
endpoint: FyersEndpoint,
body: Value,
) -> ExchangeResult<Value> {
let base_url = self.urls.rest_url(endpoint.is_data_endpoint());
let path = endpoint.path();
let url = format!("{}{}", base_url, path);
let mut headers = HashMap::new();
if endpoint.requires_auth() {
self.auth.sign_headers(&mut headers);
}
headers.insert("Content-Type".to_string(), "application/json".to_string());
let response = self.http.post(&url, &body, &headers).await?;
Ok(response)
}
async fn put(
&self,
endpoint: FyersEndpoint,
body: Value,
) -> ExchangeResult<Value> {
let base_url = self.urls.rest_url(endpoint.is_data_endpoint());
let path = endpoint.path();
let url = format!("{}{}", base_url, path);
let mut headers = HashMap::new();
if endpoint.requires_auth() {
self.auth.sign_headers(&mut headers);
}
headers.insert("Content-Type".to_string(), "application/json".to_string());
let response = self.http.put(&url, &body, &headers).await?;
Ok(response)
}
async fn delete(
&self,
endpoint: FyersEndpoint,
params: HashMap<String, String>,
_body: Value,
) -> ExchangeResult<Value> {
let base_url = self.urls.rest_url(endpoint.is_data_endpoint());
let path = endpoint.path();
let query = if params.is_empty() {
String::new()
} else {
let qs: Vec<String> = params
.iter()
.map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
.collect();
format!("?{}", qs.join("&"))
};
let url = format!("{}{}{}", base_url, path, query);
let mut headers = HashMap::new();
if endpoint.requires_auth() {
self.auth.sign_headers(&mut headers);
}
headers.insert("Content-Type".to_string(), "application/json".to_string());
let response = self.http.delete(&url, &HashMap::new(), &headers).await?;
Ok(response)
}
pub async fn position_conversion(&self, body: Value) -> ExchangeResult<Value> {
self.put(FyersEndpoint::ConvertPosition, body).await
}
pub async fn place_basket_orders(&self, orders: Value) -> ExchangeResult<Value> {
self.post(FyersEndpoint::BasketOrders, orders).await
}
pub async fn get_net_position(&self) -> ExchangeResult<Value> {
self.get(FyersEndpoint::NetPosition, HashMap::new()).await
}
}
impl ExchangeIdentity for FyersConnector {
fn exchange_id(&self) -> ExchangeId {
ExchangeId::Fyers
}
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 FyersConnector {
async fn get_price(&self, symbol: Symbol, account_type: AccountType) -> ExchangeResult<Price> {
let symbol_str = format_symbol(&symbol.base, &symbol.quote, account_type);
let mut params = HashMap::new();
params.insert("symbols".to_string(), symbol_str.clone());
let response = self.get(FyersEndpoint::Quotes, params).await?;
FyersParser::parse_ltp(&response, &symbol_str)
}
async fn get_orderbook(
&self,
symbol: Symbol,
_depth: Option<u16>,
account_type: AccountType,
) -> ExchangeResult<OrderBook> {
let symbol_str = format_symbol(&symbol.base, &symbol.quote, account_type);
let mut params = HashMap::new();
params.insert("symbol".to_string(), symbol_str.clone());
params.insert("ohlcv_flag".to_string(), "1".to_string());
let response = self.get(FyersEndpoint::Depth, params).await?;
FyersParser::parse_orderbook(&response, &symbol_str)
}
async fn get_klines(
&self,
symbol: Symbol,
interval: &str,
limit: Option<u16>,
account_type: AccountType,
_end_time: Option<i64>,
) -> ExchangeResult<Vec<Kline>> {
let symbol_str = format_symbol(&symbol.base, &symbol.quote, account_type);
let resolution = map_kline_interval(interval);
let now = timestamp_seconds();
let limit = limit.unwrap_or(100) as u64;
let candle_seconds = match interval {
"1m" => 60,
"5m" => 300,
"15m" => 900,
"1h" => 3600,
"1d" => 86400,
_ => 3600,
};
let range_from = now - (limit * candle_seconds);
let mut params = HashMap::new();
params.insert("symbol".to_string(), symbol_str);
params.insert("resolution".to_string(), resolution);
params.insert("date_format".to_string(), "0".to_string()); params.insert("range_from".to_string(), range_from.to_string());
params.insert("range_to".to_string(), now.to_string());
let response = self.get(FyersEndpoint::History, params).await?;
FyersParser::parse_klines(&response)
}
async fn get_ticker(
&self,
symbol: Symbol,
account_type: AccountType,
) -> ExchangeResult<Ticker> {
let symbol_str = format_symbol(&symbol.base, &symbol.quote, account_type);
let mut params = HashMap::new();
params.insert("symbols".to_string(), symbol_str.clone());
let response = self.get(FyersEndpoint::Quotes, params).await?;
FyersParser::parse_ticker(&response, &symbol_str)
}
async fn ping(&self) -> ExchangeResult<()> {
let _response = self.get(FyersEndpoint::MarketStatus, HashMap::new()).await?;
Ok(())
}
async fn get_exchange_info(&self, account_type: AccountType) -> ExchangeResult<Vec<SymbolInfo>> {
let base_url = self.urls.rest_url(true); let url = format!(
"{}/data/symbol-master?exchange=NSE&segment=CM",
base_url
);
let mut headers = HashMap::new();
if FyersEndpoint::SymbolMaster.requires_auth() {
self.auth.sign_headers(&mut headers);
}
let client = reqwest::Client::new();
let mut req = client.get(&url);
for (k, v) in &headers {
req = req.header(k.as_str(), v.as_str());
}
let response = req.send().await
.map_err(|e| ExchangeError::Network(format!("Request failed: {}", e)))?;
let csv_text = response.text().await
.map_err(|e| ExchangeError::Network(format!("Failed to read text: {}", 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() < 4 {
continue;
}
let symbol = cols[1].trim().trim_matches('"').to_string();
let segment = cols[3].trim();
if segment != "CM" {
continue;
}
let display_symbol = if let Some(colon_pos) = symbol.find(':') {
symbol[colon_pos + 1..].to_string()
} else {
symbol.clone()
};
let tick_size = cols.get(6)
.and_then(|s| s.trim().parse::<f64>().ok())
.filter(|&v| v > 0.0);
infos.push(SymbolInfo {
symbol: display_symbol.clone(),
base_asset: display_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,
});
}
Ok(infos)
}
}
#[async_trait]
impl Trading for FyersConnector {
async fn place_order(&self, req: OrderRequest) -> ExchangeResult<PlaceOrderResponse> {
let symbol = req.symbol.clone();
let side = req.side;
let quantity = req.quantity;
let account_type = req.account_type;
let time_in_force = req.time_in_force;
let symbol_str = format_symbol(&symbol.base, &symbol.quote, account_type);
let fyers_side: i32 = match side {
OrderSide::Buy => 1,
OrderSide::Sell => -1,
};
let validity = match time_in_force {
TimeInForce::Ioc => "IOC",
_ => "DAY",
};
match req.order_type {
OrderType::Market => {
let body = json!({
"symbol": symbol_str,
"qty": quantity as i64,
"type": 2, "side": fyers_side,
"productType": "INTRADAY",
"limitPrice": 0,
"stopPrice": 0,
"validity": validity,
"disclosedQty": 0,
"offlineOrder": false
});
let response = self.post(FyersEndpoint::PlaceOrder, body).await?;
FyersParser::parse_order(&response).map(PlaceOrderResponse::Simple)
}
OrderType::Limit { price } => {
let body = json!({
"symbol": symbol_str,
"qty": quantity as i64,
"type": 1, "side": fyers_side,
"productType": "INTRADAY",
"limitPrice": price,
"stopPrice": 0,
"validity": validity,
"disclosedQty": 0,
"offlineOrder": false
});
let response = self.post(FyersEndpoint::PlaceOrder, body).await?;
FyersParser::parse_order(&response).map(PlaceOrderResponse::Simple)
}
OrderType::Ioc { price } => {
let (order_type_code, limit_price) = if let Some(p) = price {
(1i32, p)
} else {
(2i32, 0.0)
};
let body = json!({
"symbol": symbol_str,
"qty": quantity as i64,
"type": order_type_code,
"side": fyers_side,
"productType": "INTRADAY",
"limitPrice": limit_price,
"stopPrice": 0,
"validity": "IOC",
"disclosedQty": 0,
"offlineOrder": false
});
let response = self.post(FyersEndpoint::PlaceOrder, body).await?;
FyersParser::parse_order(&response).map(PlaceOrderResponse::Simple)
}
OrderType::StopMarket { stop_price } => {
let body = json!({
"symbol": symbol_str,
"qty": quantity as i64,
"type": 4, "side": fyers_side,
"productType": "INTRADAY",
"limitPrice": 0,
"stopPrice": stop_price,
"validity": validity,
"disclosedQty": 0,
"offlineOrder": false
});
let response = self.post(FyersEndpoint::PlaceOrder, body).await?;
FyersParser::parse_order(&response).map(PlaceOrderResponse::Simple)
}
OrderType::StopLimit { stop_price, limit_price } => {
let body = json!({
"symbol": symbol_str,
"qty": quantity as i64,
"type": 3, "side": fyers_side,
"productType": "INTRADAY",
"limitPrice": limit_price,
"stopPrice": stop_price,
"validity": validity,
"disclosedQty": 0,
"offlineOrder": false
});
let response = self.post(FyersEndpoint::PlaceOrder, body).await?;
FyersParser::parse_order(&response).map(PlaceOrderResponse::Simple)
}
OrderType::Bracket { price, take_profit, stop_loss } => {
let entry_price = price.ok_or_else(|| {
ExchangeError::InvalidRequest(
"Bracket orders on Fyers require an entry price".to_string(),
)
})?;
let body = json!({
"symbol": symbol_str,
"qty": quantity as i64,
"type": 1, "side": fyers_side,
"productType": "BO",
"limitPrice": entry_price,
"stopPrice": 0,
"validity": "DAY",
"disclosedQty": 0,
"offlineOrder": false,
"stopLoss": stop_loss,
"takeProfit": take_profit
});
let response = self.post(FyersEndpoint::PlaceOrder, body).await?;
FyersParser::parse_order(&response).map(PlaceOrderResponse::Simple)
}
other => Err(ExchangeError::UnsupportedOperation(
format!("{:?} order type not supported on Fyers API v3", other)
)),
}
}
async fn get_order_history(
&self,
filter: OrderHistoryFilter,
_account_type: AccountType,
) -> ExchangeResult<Vec<Order>> {
let response = self.get(FyersEndpoint::GetOrders, HashMap::new()).await?;
let all_orders = FyersParser::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(sym) = &filter.symbol {
let sym_str = format_symbol(&sym.base, &sym.quote, AccountType::Spot);
o.symbol == sym_str || o.symbol.contains(&sym.base)
} else {
true
}
})
.filter(|o| {
if let Some(status) = &filter.status {
&o.status == status
} 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 body = json!({
"id": order_id
});
let response = self.delete(FyersEndpoint::CancelOrder, HashMap::new(), body).await?;
FyersParser::parse_order(&response)
}
_ => 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(FyersEndpoint::GetOrders, HashMap::new()).await?;
let orders = FyersParser::parse_orders(&response)?;
orders
.into_iter()
.find(|o| o.id == order_id)
.ok_or_else(|| ExchangeError::NotFound(format!("Order {} not found", order_id)))
}
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(FyersEndpoint::GetOrders, HashMap::new()).await?;
let mut orders = FyersParser::parse_orders(&response)?;
orders.retain(|o| {
matches!(
o.status,
crate::core::types::OrderStatus::Open | crate::core::types::OrderStatus::PartiallyFilled
)
});
if let Some(sym) = symbol {
let symbol_str = format_symbol(&sym.base, &sym.quote, account_type);
orders.retain(|o| o.symbol == symbol_str);
}
Ok(orders)
}
}
#[async_trait]
impl AmendOrder for FyersConnector {
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 mut body_map = serde_json::Map::new();
body_map.insert("id".to_string(), json!(req.order_id));
if let Some(price) = req.fields.price {
body_map.insert("limitPrice".to_string(), json!(price));
}
if let Some(qty) = req.fields.quantity {
body_map.insert("qty".to_string(), json!(qty as i64));
}
if let Some(trigger) = req.fields.trigger_price {
body_map.insert("stopPrice".to_string(), json!(trigger));
}
let body = serde_json::Value::Object(body_map);
let _response = self.put(FyersEndpoint::ModifyOrder, body).await?;
let orders_response = self.get(FyersEndpoint::GetOrders, HashMap::new()).await?;
let orders = FyersParser::parse_orders(&orders_response)?;
orders
.into_iter()
.find(|o| o.id == req.order_id)
.ok_or_else(|| ExchangeError::NotFound(format!("Order {} not found after amend", req.order_id)))
}
}
#[async_trait]
impl BatchOrders for FyersConnector {
async fn place_orders_batch(
&self,
orders: Vec<OrderRequest>,
) -> ExchangeResult<Vec<OrderResult>> {
if orders.len() > self.max_batch_place_size() {
return Err(ExchangeError::InvalidRequest(format!(
"Fyers batch order limit is {}; {} orders provided",
self.max_batch_place_size(),
orders.len()
)));
}
let mut order_jsons = Vec::with_capacity(orders.len());
for req in &orders {
let symbol_str = format_symbol(&req.symbol.base, &req.symbol.quote, req.account_type);
let fyers_side: i32 = match req.side {
OrderSide::Buy => 1,
OrderSide::Sell => -1,
};
let validity = match req.time_in_force {
TimeInForce::Ioc => "IOC",
_ => "DAY",
};
let (order_type_code, limit_price, stop_price) = match &req.order_type {
OrderType::Market => (2i32, 0.0, 0.0),
OrderType::Limit { price } => (1i32, *price, 0.0),
OrderType::StopMarket { stop_price } => (4i32, 0.0, *stop_price),
OrderType::StopLimit { stop_price, limit_price } => (3i32, *limit_price, *stop_price),
_ => {
return Err(ExchangeError::UnsupportedOperation(
format!("{:?} not supported in batch orders on Fyers", req.order_type)
));
}
};
order_jsons.push(json!({
"symbol": symbol_str,
"qty": req.quantity as i64,
"type": order_type_code,
"side": fyers_side,
"productType": "INTRADAY",
"limitPrice": limit_price,
"stopPrice": stop_price,
"validity": validity,
"disclosedQty": 0,
"offlineOrder": false
}));
}
let body = json!(order_jsons);
let response = self
.post(FyersEndpoint::PlaceOrderMulti, body)
.await?;
let results_arr = response
.get("data")
.and_then(|d| d.as_array())
.ok_or_else(|| ExchangeError::Parse("Missing 'data' in batch response".to_string()))?;
let results: Vec<OrderResult> = results_arr
.iter()
.zip(orders.iter())
.map(|(item, req)| {
let success = item.get("s").and_then(|s| s.as_str()) == Some("ok");
let order_id = item.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string();
let error = if !success {
item.get("message").and_then(|m| m.as_str()).map(|s| s.to_string())
} else {
None
};
let order = if success && !order_id.is_empty() {
Some(Order {
id: order_id,
client_order_id: req.client_order_id.clone(),
symbol: format_symbol(&req.symbol.base, &req.symbol.quote, req.account_type),
side: req.side,
order_type: req.order_type.clone(),
status: OrderStatus::New,
price: match &req.order_type {
OrderType::Limit { price } => Some(*price),
OrderType::StopLimit { limit_price, .. } => Some(*limit_price),
_ => None,
},
stop_price: match &req.order_type {
OrderType::StopMarket { stop_price } => Some(*stop_price),
OrderType::StopLimit { stop_price, .. } => Some(*stop_price),
_ => None,
},
quantity: req.quantity,
filled_quantity: 0.0,
average_price: None,
commission: None,
commission_asset: None,
created_at: timestamp_millis() as i64,
updated_at: None,
time_in_force: req.time_in_force,
})
} else {
None
};
OrderResult {
order,
client_order_id: req.client_order_id.clone(),
success,
error,
error_code: if !success {
item.get("code").and_then(|c| c.as_i64()).map(|c| c as i32)
} else {
None
},
}
})
.collect();
Ok(results)
}
async fn cancel_orders_batch(
&self,
_order_ids: Vec<String>,
_symbol: Option<&str>,
_account_type: AccountType,
) -> ExchangeResult<Vec<OrderResult>> {
Err(ExchangeError::UnsupportedOperation(
"Fyers does not support native batch order cancellation".to_string(),
))
}
fn max_batch_place_size(&self) -> usize {
10 }
fn max_batch_cancel_size(&self) -> usize {
0 }
}
#[async_trait]
impl Account for FyersConnector {
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(FyersEndpoint::Funds, HashMap::new()).await?;
FyersParser::parse_balance(&response)
}
async fn get_account_info(&self, _account_type: AccountType) -> ExchangeResult<AccountInfo> {
let response = self.get(FyersEndpoint::Profile, HashMap::new()).await?;
FyersParser::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 FyersConnector {
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(FyersEndpoint::Positions, HashMap::new()).await?;
let mut positions = FyersParser::parse_positions(&response)?;
if let Some(sym) = symbol {
let symbol_str = format_symbol(&sym.base, &sym.quote, account_type);
positions.retain(|p| p.symbol == symbol_str);
}
Ok(positions)
}
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 applicable for Indian F&O market".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 is product-specific in Indian markets. Use productType in orders.".to_string(),
))
}
_ => Err(ExchangeError::UnsupportedOperation(
format!("{:?} not supported on {:?}", req, self.exchange_id())
)),
}
}
}
impl FyersConnector {
pub async fn get_holdings(&self) -> ExchangeResult<Value> {
self.get(FyersEndpoint::Holdings, HashMap::new()).await
}
pub async fn get_tradebook(&self) -> ExchangeResult<Value> {
self.get(FyersEndpoint::Tradebook, HashMap::new()).await
}
pub async fn convert_position(
&self,
symbol: &str,
position_side: i32,
convert_qty: f64,
convert_from: &str,
convert_to: &str,
) -> ExchangeResult<Value> {
let body = json!({
"symbol": symbol,
"positionSide": position_side,
"convertQty": convert_qty as i64,
"convertFrom": convert_from,
"convertTo": convert_to
});
self.put(FyersEndpoint::ConvertPosition, body).await
}
pub async fn modify_order(
&self,
order_id: &str,
order_type: Option<i32>,
limit_price: Option<f64>,
quantity: Option<i64>,
) -> ExchangeResult<Value> {
let mut body_map = serde_json::Map::new();
body_map.insert("id".to_string(), json!(order_id));
if let Some(t) = order_type {
body_map.insert("type".to_string(), json!(t));
}
if let Some(p) = limit_price {
body_map.insert("limitPrice".to_string(), json!(p));
}
if let Some(q) = quantity {
body_map.insert("qty".to_string(), json!(q));
}
let body = Value::Object(body_map);
self.put(FyersEndpoint::ModifyOrder, body).await
}
pub async fn exchange_auth_code(&mut self, auth_code: &str) -> ExchangeResult<String> {
let body = json!(self.auth.prepare_token_request(auth_code));
let response = self.post(FyersEndpoint::ValidateAuthCode, body).await?;
let access_token = FyersParser::parse_access_token(&response)?;
self.auth.set_access_token(access_token.clone());
Ok(access_token)
}
pub fn get_authorization_url(&self, redirect_uri: &str, state: Option<&str>) -> String {
self.auth.get_authorization_url(redirect_uri, state)
}
}