use serde::Serialize;
use serde_json::Value;
use crate::client::api_request::ApiRequest;
use crate::client::decode::decode_value;
use crate::client::http_client::HttpClient;
use crate::error::TigerError;
use crate::model::contract::Contract;
use crate::model::order::{Order, OrderRequest};
use crate::model::position::Position;
use crate::model::trade::{
AggregateAssets, AnalyticsAsset, Asset, EstimateTradableQuantity, ForexOrderResult,
FundDetails, FundingHistoryItem, ManagedAccount, OptionExerciseCheckResult,
OptionExercisePositionPageResult, OptionExerciseRecordPageResult, OrderIdResult,
PlaceOrderResult, PositionTransferDetail, PositionTransferExternalRecord,
PositionTransferRecord, PreviewResult, PrimeAsset, SegmentFund, SegmentFundAvailableItem,
SegmentFundHistoryItem, Transaction,
};
use crate::model::trade_requests::{
AggregateAssetsRequest, AnalyticsAssetRequest, AssetsRequest, DerivativeContractsRequest,
EstimateTradableQuantityRequest, ForexOrderRequest, FundDetailsRequest, FundingHistoryRequest,
GetOrderRequest, ManagedAccountsRequest, OptionExerciseCancelRequest,
OptionExerciseCheckRequest, OptionExerciseRecordsRequest, OptionExercisePositionRequest,
OptionExerciseSubmitRequest, OrderTransactionsRequest, OrdersRequest,
PositionTransferDetailRequest, PositionTransferExternalRecordsRequest,
PositionTransferRecordsRequest, PositionTransferRequest, PositionsRequest, SegmentFundRequest,
};
pub struct TradeClient<'a> {
http_client: &'a HttpClient,
account: String,
secret_key: Option<String>,
}
impl<'a> TradeClient<'a> {
pub fn new(http_client: &'a HttpClient, account: impl Into<String>) -> Self {
Self {
http_client,
account: account.into(),
secret_key: None,
}
}
pub fn with_secret_key(
http_client: &'a HttpClient,
account: impl Into<String>,
secret_key: impl Into<String>,
) -> Self {
Self {
http_client,
account: account.into(),
secret_key: Some(secret_key.into()),
}
}
async fn call_optional<T, P>(&self, method: &str, params: P) -> Result<Option<T>, TigerError>
where
T: serde::de::DeserializeOwned,
P: Serialize,
{
let biz = serde_json::to_string(¶ms)
.map_err(|e| TigerError::Config(format!("serialize biz params failed: {}", e)))?;
let req = ApiRequest::new(method, biz);
let resp = self.http_client.execute_request(&req).await?;
match resp.data {
None => Ok(None),
Some(v) if v.is_null() => Ok(None),
Some(v) => {
let parsed: T = decode_value(v)?;
Ok(Some(parsed))
}
}
}
async fn call_into_items<T, P>(&self, method: &str, params: P) -> Result<Vec<T>, TigerError>
where
T: serde::de::DeserializeOwned,
P: Serialize,
{
let biz = serde_json::to_string(¶ms)
.map_err(|e| TigerError::Config(format!("serialize biz params failed: {}", e)))?;
let req = ApiRequest::new(method, biz);
let resp = self.http_client.execute_request(&req).await?;
let Some(v) = resp.data else { return Ok(Vec::new()); };
if v.is_null() {
return Ok(Vec::new());
}
let raw = match &v {
Value::String(s) => serde_json::from_str::<Value>(s).unwrap_or(v.clone()),
_ => v.clone(),
};
if let Value::Object(map) = &raw {
if let Some(items) = map.get("items") {
if items.is_null() {
return Ok(Vec::new());
}
return serde_json::from_value::<Vec<T>>(items.clone())
.map_err(|e| TigerError::Config(format!("decode items failed: {}", e)));
}
}
if raw.is_array() {
return serde_json::from_value::<Vec<T>>(raw)
.map_err(|e| TigerError::Config(format!("decode array data failed: {}", e)));
}
Ok(Vec::new())
}
pub async fn get_contract(&self, symbol: &str, sec_type: &str) -> Result<Vec<Contract>, TigerError> {
self.call_into_items(
"contract",
serde_json::json!({
"account": self.account,
"symbol": symbol,
"sec_type": sec_type,
}),
)
.await
}
pub async fn get_contracts(
&self,
symbols: &[&str],
sec_type: &str,
) -> Result<Vec<Contract>, TigerError> {
self.call_into_items(
"contracts",
serde_json::json!({
"account": self.account,
"symbols": symbols,
"sec_type": sec_type,
}),
)
.await
}
pub async fn get_quote_contract(
&self,
symbol: &str,
sec_type: &str,
expiry: &str,
) -> Result<Vec<Contract>, TigerError> {
self.call_into_items(
"quote_contract",
serde_json::json!({
"account": self.account,
"symbols": [symbol],
"sec_type": sec_type,
"expiry": expiry,
}),
)
.await
}
pub async fn preview_order(
&self,
order: OrderRequest,
) -> Result<Option<PreviewResult>, TigerError> {
let mut order = order;
order.account = Some(self.account.clone());
self.call_optional("preview_order", order).await
}
pub async fn place_order(
&self,
order: OrderRequest,
) -> Result<Option<PlaceOrderResult>, TigerError> {
let mut order = order;
order.account = Some(self.account.clone());
self.call_optional("place_order", order).await
}
pub async fn modify_order(
&self,
id: i64,
order: OrderRequest,
) -> Result<Option<OrderIdResult>, TigerError> {
let mut order = order;
order.account = Some(self.account.clone());
order.id = Some(id);
self.call_optional("modify_order", order).await
}
pub async fn cancel_order(&self, id: i64) -> Result<Option<OrderIdResult>, TigerError> {
self.call_optional(
"cancel_order",
serde_json::json!({ "account": self.account, "id": id }),
)
.await
}
pub async fn get_orders(&self, req: OrdersRequest) -> Result<Vec<Order>, TigerError> {
let mut req = req;
if req.account.is_none() {
req.account = Some(self.account.clone());
}
self.call_into_items("orders", req).await
}
pub async fn get_active_orders(&self, req: OrdersRequest) -> Result<Vec<Order>, TigerError> {
let mut req = req;
if req.account.is_none() {
req.account = Some(self.account.clone());
}
self.call_into_items("active_orders", req).await
}
pub async fn get_inactive_orders(&self, req: OrdersRequest) -> Result<Vec<Order>, TigerError> {
let mut req = req;
if req.account.is_none() {
req.account = Some(self.account.clone());
}
self.call_into_items("inactive_orders", req).await
}
pub async fn get_filled_orders(&self, req: OrdersRequest) -> Result<Vec<Order>, TigerError> {
let mut req = req;
if req.account.is_none() {
req.account = Some(self.account.clone());
}
self.call_into_items("filled_orders", req).await
}
pub async fn get_order(&self, req: GetOrderRequest) -> Result<Option<Order>, TigerError> {
let mut req = req;
if req.account.is_none() {
req.account = Some(self.account.clone());
}
if req.id.unwrap_or(0) == 0 && req.order_id.unwrap_or(0) == 0 {
return Ok(None);
}
self.call_optional("order_no", req).await
}
pub async fn get_order_transactions(
&self,
req: OrderTransactionsRequest,
) -> Result<Vec<Transaction>, TigerError> {
let mut req = req;
if req.account.is_none() {
req.account = Some(self.account.clone());
}
self.call_into_items("order_transactions", req).await
}
pub async fn get_positions(&self, req: PositionsRequest) -> Result<Vec<Position>, TigerError> {
let mut req = req;
if req.account.is_none() {
req.account = Some(self.account.clone());
}
self.call_into_items("positions", req).await
}
pub async fn get_assets(&self, req: AssetsRequest) -> Result<Vec<Asset>, TigerError> {
let mut req = req;
if req.account.is_none() {
req.account = Some(self.account.clone());
}
self.call_into_items("assets", req).await
}
pub async fn get_prime_assets(&self, req: AssetsRequest) -> Result<Option<PrimeAsset>, TigerError> {
let mut req = req;
if req.account.is_none() {
req.account = Some(self.account.clone());
}
self.call_optional("prime_assets", req).await
}
pub async fn get_managed_accounts(
&self,
req: ManagedAccountsRequest,
) -> Result<Vec<ManagedAccount>, TigerError> {
let mut req = req;
if req.account.is_none() {
req.account = Some(self.account.clone());
}
self.call_into_items("accounts", req).await
}
pub async fn get_derivative_contracts(
&self,
req: DerivativeContractsRequest,
) -> Result<Vec<Contract>, TigerError> {
let mut req = req;
if req.account.is_none() {
req.account = Some(self.account.clone());
}
self.call_into_items("quote_contract", req).await
}
pub async fn get_analytics_asset(
&self,
req: AnalyticsAssetRequest,
) -> Result<Vec<AnalyticsAsset>, TigerError> {
let mut req = req;
if req.account.is_none() {
req.account = Some(self.account.clone());
}
self.call_into_items("analytics_asset", req).await
}
pub async fn get_aggregate_assets(
&self,
req: AggregateAssetsRequest,
) -> Result<Option<AggregateAssets>, TigerError> {
let mut req = req;
if req.account.is_none() {
req.account = Some(self.account.clone());
}
self.call_optional("aggregate_assets", req).await
}
pub async fn get_estimate_tradable_quantity(
&self,
req: EstimateTradableQuantityRequest,
) -> Result<Option<EstimateTradableQuantity>, TigerError> {
let mut req = req;
if req.account.is_none() {
req.account = Some(self.account.clone());
}
self.call_optional("estimate_tradable_quantity", req).await
}
pub async fn place_forex_order(
&self,
req: ForexOrderRequest,
) -> Result<Option<ForexOrderResult>, TigerError> {
let mut req = req;
if req.account.is_none() {
req.account = Some(self.account.clone());
}
self.call_optional("place_forex_order", req).await
}
pub async fn get_segment_fund_available(
&self,
req: SegmentFundRequest,
) -> Result<Vec<SegmentFundAvailableItem>, TigerError> {
let mut req = req;
if req.account.is_none() {
req.account = Some(self.account.clone());
}
self.call_into_items("segment_fund_available", req).await
}
pub async fn get_segment_fund_history(
&self,
req: SegmentFundRequest,
) -> Result<Vec<SegmentFundHistoryItem>, TigerError> {
let mut req = req;
if req.account.is_none() {
req.account = Some(self.account.clone());
}
self.call_into_items("segment_fund_history", req).await
}
pub async fn transfer_segment_fund(
&self,
req: SegmentFundRequest,
) -> Result<Option<SegmentFund>, TigerError> {
let mut req = req;
if req.account.is_none() {
req.account = Some(self.account.clone());
}
self.call_optional("transfer_segment_fund", req).await
}
pub async fn cancel_segment_fund(
&self,
req: SegmentFundRequest,
) -> Result<Option<SegmentFund>, TigerError> {
let mut req = req;
if req.account.is_none() {
req.account = Some(self.account.clone());
}
self.call_optional("cancel_segment_fund", req).await
}
pub async fn get_fund_details(
&self,
req: FundDetailsRequest,
) -> Result<Vec<FundDetails>, TigerError> {
let mut req = req;
if req.account.is_none() {
req.account = Some(self.account.clone());
}
self.call_into_items("fund_details", req).await
}
pub async fn get_funding_history(
&self,
req: FundingHistoryRequest,
) -> Result<Vec<FundingHistoryItem>, TigerError> {
let mut req = req;
if req.account.is_none() {
req.account = Some(self.account.clone());
}
self.call_into_items("transfer_fund", req).await
}
pub async fn transfer_position(
&self,
req: PositionTransferRequest,
) -> Result<Option<PositionTransferRecord>, TigerError> {
self.call_optional("position_transfer", req).await
}
pub async fn get_position_transfer_records(
&self,
req: PositionTransferRecordsRequest,
) -> Result<Vec<PositionTransferRecord>, TigerError> {
let mut req = req;
if req.account_id.is_none() {
req.account_id = Some(self.account.clone());
}
self.call_into_items("position_transfer_records", req).await
}
pub async fn get_position_transfer_detail(
&self,
req: PositionTransferDetailRequest,
) -> Result<Option<PositionTransferDetail>, TigerError> {
let mut req = req;
if req.account_id.is_none() {
req.account_id = Some(self.account.clone());
}
self.call_optional("position_transfer_detail", req).await
}
pub async fn get_position_transfer_external_records(
&self,
req: PositionTransferExternalRecordsRequest,
) -> Result<Vec<PositionTransferExternalRecord>, TigerError> {
let mut req = req;
if req.account_id.is_none() {
req.account_id = Some(self.account.clone());
}
self.call_into_items("position_transfer_external_records", req).await
}
pub async fn option_exercise_check(
&self,
mut req: OptionExerciseCheckRequest,
) -> Result<Option<OptionExerciseCheckResult>, TigerError> {
if req.account.is_none() {
req.account = Some(self.account.clone());
}
if req.secret_key.is_none() {
req.secret_key = self.secret_key.clone();
}
self.call_optional("option_exercise_check", req).await
}
pub async fn get_option_exercise_positions(
&self,
mut req: OptionExercisePositionRequest,
) -> Result<Option<OptionExercisePositionPageResult>, TigerError> {
if req.account.is_none() {
req.account = Some(self.account.clone());
}
if req.secret_key.is_none() {
req.secret_key = self.secret_key.clone();
}
self.call_optional("option_exercise_position", req).await
}
pub async fn submit_option_exercise(
&self,
mut req: OptionExerciseSubmitRequest,
) -> Result<Option<bool>, TigerError> {
if req.account.is_none() {
req.account = Some(self.account.clone());
}
if req.secret_key.is_none() {
req.secret_key = self.secret_key.clone();
}
self.call_optional("option_exercise_submit", req).await
}
pub async fn get_option_exercise_records(
&self,
mut req: OptionExerciseRecordsRequest,
) -> Result<Option<OptionExerciseRecordPageResult>, TigerError> {
if req.account.is_none() {
req.account = Some(self.account.clone());
}
if req.secret_key.is_none() {
req.secret_key = self.secret_key.clone();
}
self.call_optional("option_exercise_record", req).await
}
pub async fn cancel_option_exercise(
&self,
mut req: OptionExerciseCancelRequest,
) -> Result<Option<bool>, TigerError> {
if req.account.is_none() {
req.account = Some(self.account.clone());
}
if req.secret_key.is_none() {
req.secret_key = self.secret_key.clone();
}
self.call_optional("option_exercise_cancel", req).await
}
}
#[cfg(test)]
mod tests;