use serde_json::{json, Value};
use crate::api::{Trade, API};
use crate::client::Client;
use crate::errors::BybitError;
use crate::model::{
AmendOrderRequest, AmendOrderResponse, BatchAmendRequest, BatchAmendResponse, BatchCancelRequest, BatchCancelResponse, BatchPlaceRequest, BatchPlaceResponse, CancelOrderRequest, CancelOrderResponse, CancelallRequest, CancelallResponse, Category, OpenOrdersRequest, OpenOrdersResponse, OrderHistoryRequest, OrderHistoryResponse, OrderRequest, OrderResponse, OrderType, RequestType, Side, TradeHistoryRequest, TradeHistoryResponse
};
use crate::util::{build_json_request, build_request, date_to_milliseconds, generate_random_uid};
use std::borrow::Cow;
use std::collections::BTreeMap;
#[derive(Clone)]
pub struct Trader<'a> {
pub client: Client<'a>,
pub recv_window: u16,
}
pub enum Action<'a> {
Order(OrderRequest<'a>, bool),
Amend(AmendOrderRequest<'a>, bool),
Cancel(CancelOrderRequest<'a>, bool),
}
impl<'a> Trader<'_> {
pub async fn place_custom_order<'b>(
&self,
req: OrderRequest<'_>,
) -> Result<OrderResponse, BybitError> {
let action = Action::Order(req, false);
let parameters = Self::build_orders(action);
let request = build_json_request(¶meters);
let response: OrderResponse = self
.client
.post_signed(
API::Trade(Trade::Place),
self.recv_window.into(),
Some(request),
)
.await?;
Ok(response)
}
pub async fn place_futures_limit_order(
&self,
category: Category,
symbol: &str,
side: Side,
qty: f64,
price: f64,
mode: u8,
) -> Result<OrderResponse, BybitError> {
let mut parameters: BTreeMap<String, String> = BTreeMap::new();
let req = OrderRequest {
category,
symbol: Cow::Borrowed(symbol),
side,
qty,
order_type: OrderType::Limit,
position_idx: Some(mode),
order_link_id: Some(generate_random_uid(36).into()),
price: Some(price),
..Default::default()
};
parameters.insert("category".into(), req.category.as_str().into());
parameters.insert("symbol".into(), req.symbol.into_owned());
parameters.insert("orderType".into(), req.order_type.as_str().into());
parameters.insert("side".into(), req.side.as_str().into());
if let Some(v) = req.order_link_id {
parameters.insert("orderLinkId".into(), v.into());
}
parameters.insert("qty".into(), req.qty.to_string());
if let Some(v) = req.position_idx {
match v {
0 | 1 | 2 => {
parameters.insert("positionIdx".into(), v.to_string());
}
_ => return Err(BybitError::from("Invalid position index".to_string())),
}
}
if let Some(v) = req.price {
parameters.insert("price".into(), v.to_string());
}
parameters.insert("timeInForce".into(), "GTC".into());
let request = build_json_request(¶meters);
let response: OrderResponse = self
.client
.post_signed(
API::Trade(Trade::Place),
self.recv_window.into(),
Some(request),
)
.await?;
Ok(response)
}
pub async fn amend_order<'b>(
&self,
req: AmendOrderRequest<'_>,
) -> Result<AmendOrderResponse, BybitError> {
let action = Action::Amend(req, false);
let parameters = Self::build_orders(action);
let request = build_json_request(¶meters);
let response: AmendOrderResponse = self
.client
.post_signed(
API::Trade(Trade::Amend),
self.recv_window.into(),
Some(request),
)
.await?;
Ok(response)
}
pub async fn cancel_order<'b>(
&self,
req: CancelOrderRequest<'_>,
) -> Result<CancelOrderResponse, BybitError> {
let action = Action::Cancel(req, false);
let parameters = Self::build_orders(action);
let request = build_json_request(¶meters);
let response: CancelOrderResponse = self
.client
.post_signed(
API::Trade(Trade::Cancel),
self.recv_window.into(),
Some(request),
)
.await?;
Ok(response)
}
pub async fn get_open_orders<'b>(
&self,
req: OpenOrdersRequest<'_>,
) -> Result<OpenOrdersResponse, BybitError> {
let mut parameters: BTreeMap<String, String> = BTreeMap::new();
parameters.insert("category".into(), req.category.as_str().into());
parameters.insert("symbol".into(), req.symbol.into());
if let Some(base_coin) = req.base_coin {
parameters.insert("baseCoin".into(), base_coin.into());
}
if let Some(settle_coin) = req.settle_coin {
parameters.insert("settleCoin".into(), settle_coin.into());
}
if let Some(order_id) = req.order_id {
parameters.insert("orderId".into(), order_id.into());
}
if let Some(order_link_id) = req.order_link_id {
parameters.insert("orderLinkId".into(), order_link_id.into());
}
if let Some(open_only) = req.open_only {
if matches!(open_only, 0 | 1 | 2) {
parameters.insert("openOnly".into(), open_only.to_string().into());
}
}
if let Some(order_filter) = req.order_filter {
parameters.insert("orderFilter".into(), order_filter.into());
}
if let Some(limit) = req.limit {
parameters.insert("limit".into(), limit.to_string().into());
}
let request = build_request(¶meters);
let response: OpenOrdersResponse = self
.client
.get_signed(API::Trade(Trade::OpenOrders), 5000, Some(request))
.await?;
Ok(response)
}
pub async fn cancel_all_orders<'b>(
&self,
req: CancelallRequest<'_>,
) -> Result<CancelallResponse, BybitError> {
let mut parameters: BTreeMap<String, String> = BTreeMap::new();
parameters.insert("category".into(), req.category.as_str().into());
parameters.insert("symbol".into(), req.symbol.into());
if let Some(base_coin) = req.base_coin {
parameters.insert("baseCoin".into(), base_coin.into());
}
if let Some(settle_coin) = req.settle_coin {
parameters.insert("settleCoin".into(), settle_coin.into());
}
if let Some(order_filter) = req.order_filter {
parameters.insert("orderFilter".into(), order_filter.into());
}
if let Some(stop_order_type) = req.stop_order_type {
parameters.insert("stopOrderType".into(), stop_order_type.into());
}
let request = build_json_request(¶meters);
let response: CancelallResponse = self
.client
.post_signed(
API::Trade(Trade::CancelAll),
self.recv_window.into(),
Some(request),
)
.await?;
Ok(response)
}
pub async fn get_order_history<'b>(
&self,
req: OrderHistoryRequest<'_>,
) -> Result<OrderHistoryResponse, BybitError> {
let mut parameters: BTreeMap<String, String> = BTreeMap::new();
parameters.insert("category".into(), req.category.as_str().into());
req.symbol
.map(|symbol| parameters.insert("symbol".into(), symbol.into()));
req.base_coin
.map(|base_coin| parameters.insert("baseCoin".into(), base_coin.into()));
req.settle_coin
.map(|settle_coin| parameters.insert("settleCoin".into(), settle_coin.into()));
req.order_id
.map(|order_id| parameters.insert("orderId".into(), order_id.into()));
req.order_link_id
.map(|order_link_id| parameters.insert("orderLinkId".into(), order_link_id.into()));
req.order_filter
.map(|order_filter| parameters.insert("orderFilter".into(), order_filter.into()));
req.order_status
.map(|order_status| parameters.insert("orderStatus".into(), order_status.into()));
req.start_time
.and_then(|start_time| Some(date_to_milliseconds(start_time.as_ref())))
.map(|start_millis| parameters.insert("startTime".into(), start_millis.to_string()));
req.end_time
.and_then(|end_time| Some(date_to_milliseconds(end_time.as_ref())))
.map(|end_millis| parameters.insert("endTime".into(), end_millis.to_string()));
req.limit
.map(|limit| parameters.insert("limit".into(), limit.to_string()));
let request = build_request(¶meters);
let response: OrderHistoryResponse = self
.client
.get_signed(
API::Trade(Trade::History),
self.recv_window.into(),
Some(request),
)
.await?;
Ok(response)
}
pub async fn get_trade_history<'b>(
&self,
req: TradeHistoryRequest<'_>,
) -> Result<TradeHistoryResponse, BybitError> {
let mut parameters: BTreeMap<String, String> = BTreeMap::new();
parameters.insert("category".into(), req.category.as_str().into());
req.symbol
.map(|symbol| parameters.insert("symbol".into(), symbol.into()));
req.order_id
.map(|order_id| parameters.insert("orderId".into(), order_id.into()));
req.order_link_id
.map(|order_link_id| parameters.insert("orderLinkId".into(), order_link_id.into()));
req.base_coin
.map(|base_coin| parameters.insert("baseCoin".into(), base_coin.into()));
req.start_time
.and_then(|start_time| Some(date_to_milliseconds(start_time.as_ref())))
.map(|start_millis| {
parameters.insert("startTime".into(), start_millis.to_string())
});
req.end_time
.and_then(|end_time| Some(date_to_milliseconds(end_time.as_ref())))
.map(|end_millis| {
parameters.insert("endTime".into(), end_millis.to_string())
});
req.limit
.map(|limit| parameters.insert("limit".into(), limit.to_string()));
req.exec_type
.map(|exec_type| parameters.insert("execType".into(), exec_type.into()));
let request = build_request(¶meters);
let response: TradeHistoryResponse = self
.client
.get_signed(
API::Trade(Trade::TradeHistory),
self.recv_window.into(),
Some(request),
)
.await?;
Ok(response)
}
pub async fn batch_place_order<'b>(
&self,
req: BatchPlaceRequest<'_>,
) -> Result<BatchPlaceResponse, BybitError> {
let mut parameters: BTreeMap<String, Value> = BTreeMap::new();
match req.category {
Category::Linear | Category::Inverse | Category::Option => {
parameters.insert("category".into(), req.category.as_str().into());
}
_ => {
println!("Invalid category");
}
}
let mut requests_array: Vec<Value> = Vec::new();
for value in req.requests {
let action = Action::Order(value, true);
let order_object = Self::build_orders(action);
let built_orders = json!(order_object);
requests_array.push(built_orders);
}
parameters.insert("request".into(), Value::Array(requests_array));
let request = build_json_request(¶meters);
let response: BatchPlaceResponse = self
.client
.post_signed(
API::Trade(Trade::BatchPlace),
self.recv_window.into(),
Some(request),
)
.await?;
Ok(response)
}
pub async fn batch_amend_order<'b>(
&self,
req: BatchAmendRequest<'_>,
) -> Result<BatchAmendResponse, BybitError> {
let mut parameters: BTreeMap<String, Value> = BTreeMap::new();
match req.category {
Category::Linear | Category::Inverse | Category::Option => {
parameters.insert("category".into(), req.category.as_str().into());
}
_ => {
println!("Invalid category");
}
}
let mut requests_array: Vec<Value> = Vec::new();
for value in req.requests {
let action = Action::Amend(value, true);
let amend_object = Self::build_orders(action); let built_amends = json!(amend_object);
requests_array.push(built_amends);
}
parameters.insert("request".into(), Value::Array(requests_array));
let request = build_json_request(¶meters);
let response: BatchAmendResponse = self
.client
.post_signed(
API::Trade(Trade::BatchAmend),
self.recv_window.into(),
Some(request),
)
.await?;
Ok(response)
}
pub async fn batch_cancel_order<'b>(
&self,
req: BatchCancelRequest<'_>,
) -> Result<BatchCancelResponse, BybitError> {
let mut parameters: BTreeMap<String, Value> = BTreeMap::new();
match req.category {
Category::Linear | Category::Inverse | Category::Option => {
parameters.insert("category".into(), req.category.as_str().into());
}
_ => {
println!("Invalid category");
}
}
let mut requests_array: Vec<Value> = Vec::new();
for value in req.requests {
let action = Action::Cancel(value, true);
let cancel_object = Self::build_orders(action); let built_cancels = json!(cancel_object);
requests_array.push(built_cancels);
}
parameters.insert("request".into(), Value::Array(requests_array));
let request = build_json_request(¶meters);
let response: BatchCancelResponse = self
.client
.post_signed(
API::Trade(Trade::BatchCancel),
self.recv_window.into(),
Some(request),
)
.await?;
Ok(response)
}
pub async fn get_borrow_quota_spot(&self) {
todo!("This function has not yet been implemented");
}
pub async fn set_dcp_options(&self) {
todo!("This function has not yet been implemented");
}
pub fn build_orders<'b>(action: Action<'_>) -> BTreeMap<String, Value> {
let mut parameters: BTreeMap<String, Value> = BTreeMap::new();
match action {
Action::Order(req, batch) => {
if batch == false {
parameters.insert("category".into(), req.category.as_str().into());
}
parameters.insert("symbol".into(), req.symbol.into());
if let Some(leverage) = req.is_leverage {
if leverage {
parameters.insert("leverage".into(), 1.into());
}
}
parameters.insert("side".into(), req.side.as_str().into());
parameters.insert("orderType".into(), req.order_type.as_str().into());
parameters.insert("qty".into(), req.qty.to_string().into());
if let Some(market_unit) = req.market_unit {
parameters.insert("marketUnit".into(), market_unit.to_string().into());
}
if let Some(price) = req.price {
parameters.insert("price".into(), price.to_string().into());
}
if let Some(trigger_direction) = req.trigger_direction {
if trigger_direction {
parameters.insert("triggerDirection".into(), 1.into());
} else {
parameters.insert("triggerDirection".into(), 2.into());
}
}
if let Some(order_filter) = req.order_filter {
parameters.insert("orderFilter".into(), order_filter.into());
}
if let Some(trigger_price) = req.trigger_price {
parameters.insert("triggerPrice".into(), trigger_price.to_string().into());
}
if let Some(trigger) = req.trigger_by {
parameters.insert("triggerBy".into(), trigger.into());
}
if let Some(iv) = req.order_iv {
parameters.insert("orderIv".into(), iv.to_string().into());
}
if let Some(time_in_force) = req.time_in_force {
parameters.insert("timeInForce".into(), time_in_force.into());
}
if let Some(v) = req.position_idx {
match v {
0 | 1 | 2 => {
parameters.insert("positionIdx".into(), v.to_string().into());
}
_ => println!("Invalid position idx"),
}
}
if let Some(order_link_id) = req.order_link_id {
parameters.insert("orderLinkId".into(), order_link_id.into());
} else {
let uuid = generate_random_uid(36);
parameters.insert("orderLinkId".into(), uuid.into());
}
if let Some(price) = req.take_profit {
parameters.insert("takeProfit".into(), price.to_string().into());
}
if let Some(price) = req.stop_loss {
parameters.insert("stopLoss".into(), price.to_string().into());
}
if let Some(kind) = req.tp_trigger_by {
parameters.insert("tpTriggerBy".into(), kind.into());
}
if let Some(kind) = req.sl_trigger_by {
parameters.insert("slTriggerBy".into(), kind.into());
}
if let Some(reduce) = req.reduce_only {
parameters.insert("reduceOnly".into(), reduce.into());
}
if let Some(close) = req.close_on_trigger {
parameters.insert("closeOnTrigger".into(), close.into());
}
if let Some(v) = req.mmp {
parameters.insert("mmp".into(), v.into());
}
if let Some(v) = req.tpsl_mode {
parameters.insert("tpslMode".into(), v.into());
}
if let Some(v) = req.tp_limit_price {
parameters.insert("tpTriggerPrice".into(), v.to_string().into());
}
if let Some(v) = req.sl_limit_price {
parameters.insert("slTriggerPrice".into(), v.to_string().into());
}
if let Some(v) = req.tp_order_type {
parameters.insert("tpOrderType".into(), v.into());
}
if let Some(v) = req.sl_order_type {
parameters.insert("slOrderType".into(), v.into());
}
}
Action::Amend(req, batch) => {
if batch == false {
parameters.insert("category".into(), req.category.as_str().into());
}
parameters.insert("symbol".into(), req.symbol.into());
if let Some(v) = req.order_id {
parameters.insert("orderId".into(), v.into());
}
if let Some(v) = req.order_link_id {
parameters.insert("orderLinkId".into(), v.into());
}
if let Some(v) = req.order_iv {
parameters.insert("orderIv".into(), v.to_string().into());
}
if let Some(v) = req.trigger_price {
parameters.insert("triggerPrice".into(), v.to_string().into());
}
parameters.insert("qty".into(), req.qty.into());
if let Some(v) = req.price {
parameters.insert("price".into(), v.to_string().into());
}
if let Some(v) = req.tpsl_mode {
parameters.insert("tpslMode".into(), v.into());
}
if let Some(v) = req.take_profit {
parameters.insert("takeProfit".into(), v.to_string().into());
}
if let Some(v) = req.stop_loss {
parameters.insert("stopLoss".into(), v.to_string().into());
}
if let Some(v) = req.tp_trigger_by {
parameters.insert("tpTriggerBy".into(), v.into());
}
if let Some(v) = req.sl_trigger_by {
parameters.insert("slTriggerBy".into(), v.into());
}
if let Some(v) = req.trigger_by {
parameters.insert("triggerBy".into(), v.into());
}
if let Some(v) = req.tp_limit_price {
parameters.insert("tpLimitPrice".into(), v.to_string().into());
}
if let Some(v) = req.sl_limit_price {
parameters.insert("slLimitPrice".into(), v.to_string().into());
}
}
Action::Cancel(req, batch) => {
if batch == false {
parameters.insert("category".into(), req.category.as_str().into());
}
parameters.insert("symbol".into(), req.symbol.into());
if let Some(v) = req.order_id {
parameters.insert("orderId".into(), v.into());
}
if let Some(v) = req.order_link_id {
parameters.insert("orderLinkId".into(), v.into());
}
if let Some(v) = req.order_filter {
parameters.insert("orderFilter".into(), v.into());
}
}
}
parameters
}
}
pub fn build_ws_orders<'a>(orders: RequestType) -> Value {
let mut order_array = Vec::new();
match orders {
RequestType::Create(req) => {
for v in req.requests {
let action = Action::Order(v, false);
let order_object = Trader::build_orders(action); let built_order = json!(order_object);
order_array.push(built_order);
}
Value::Array(order_array)
}
RequestType::Amend(req) => {
for v in req.requests {
let action = Action::Amend(v, false);
let order_object = Trader::build_orders(action); let built_order = json!(order_object);
order_array.push(built_order);
}
Value::Array(order_array)
}
RequestType::Cancel(req) => {
for v in req.requests {
let action = Action::Cancel(v, false);
let order_object = Trader::build_orders(action); let built_order = json!(order_object);
order_array.push(built_order);
}
Value::Array(order_array)
}
}
}