use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::{debug, info};
use crate::client::KuCoinClient;
use crate::error::Result;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountOverview {
pub available_balance: f64,
pub order_margin: Option<f64>,
pub position_margin: Option<f64>,
pub unrealised_pnl: Option<f64>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PositionInfo {
pub current_qty: i32,
pub symbol: String,
pub avg_entry_price: Option<f64>,
pub unrealised_pnl: Option<f64>,
pub realised_pnl: Option<f64>,
pub leverage: Option<f64>,
pub is_open: Option<bool>,
pub mark_price: Option<f64>,
pub mark_value: Option<f64>,
pub maintenance_margin: Option<f64>,
}
impl PositionInfo {
pub const fn is_flat(&self) -> bool {
self.current_qty == 0
}
pub const fn is_long(&self) -> bool {
self.current_qty > 0
}
pub const fn is_short(&self) -> bool {
self.current_qty < 0
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FundingRecord {
pub id: Option<String>,
pub symbol: String,
pub time_point: Option<i64>,
pub funding_rate: Option<f64>,
pub mark_price: Option<f64>,
pub position_qty: Option<i32>,
pub position_cost: Option<f64>,
pub funding: Option<f64>,
pub settlement: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RiskLimitLevel {
pub symbol: String,
pub level: u32,
pub max_risk_limit: Option<f64>,
pub min_risk_limit: Option<f64>,
pub max_leverage: Option<u32>,
pub initial_margin: Option<f64>,
pub maint_margin_rate: Option<f64>,
}
impl KuCoinClient {
pub async fn get_balance(&self, currency: &str) -> Result<f64> {
let overview: AccountOverview = self
.get("/api/v1/account-overview", &[("currency", currency)])
.await?;
debug!(
balance = overview.available_balance,
currency, "got balance"
);
Ok(overview.available_balance)
}
pub async fn get_account_overview(&self, currency: &str) -> Result<AccountOverview> {
self.get("/api/v1/account-overview", &[("currency", currency)])
.await
}
pub async fn get_position(&self, symbol: &str) -> Result<PositionInfo> {
self.get("/api/v1/position", &[("symbol", symbol)]).await
}
pub async fn get_all_positions(&self) -> Result<Vec<PositionInfo>> {
self.get("/api/v1/positions", &[]).await
}
pub async fn set_auto_deposit(&self, symbol: &str, auto_deposit: bool) -> Result<()> {
self.post::<serde_json::Value>(
"/api/v1/position/changeAutoDeposit",
&json!({ "symbol": symbol, "autoDeposit": auto_deposit }),
)
.await?;
info!(symbol, auto_deposit, "auto-deposit updated");
Ok(())
}
pub async fn set_risk_limit_level(&self, symbol: &str, level: u32) -> Result<()> {
let resp: serde_json::Value = self
.post(
"/api/v1/position/risk-limit-level/change",
&json!({ "symbol": symbol, "level": level }),
)
.await?;
info!(symbol, level, resp = %resp, "risk limit level updated");
Ok(())
}
pub async fn get_risk_limit_levels(&self, symbol: &str) -> Result<Vec<RiskLimitLevel>> {
self.get(&format!("/api/v1/contracts/risk-limit/{symbol}"), &[])
.await
}
pub async fn get_funding_history(
&self,
symbol: &str,
max_count: u32,
) -> Result<Vec<FundingRecord>> {
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct Page {
data_list: Vec<FundingRecord>,
}
let limit = max_count.min(100).to_string();
let page: Page = self
.get(
"/api/v1/funding-history",
&[("symbol", symbol), ("maxCount", &limit)],
)
.await?;
Ok(page.data_list)
}
pub async fn transfer_to_main(&self, currency: &str, amount: f64) -> Result<TransferResponse> {
let resp: TransferResponse = self
.post(
"/api/v1/transfer-out",
&json!({
"currency": currency,
"amount": amount,
"recAccountType": "MAIN",
}),
)
.await?;
info!(currency, amount, apply_id = %resp.apply_id, "transfer Futures→Main initiated");
Ok(resp)
}
pub async fn transfer_to_futures(
&self,
currency: &str,
amount: f64,
) -> Result<TransferResponse> {
let resp: TransferResponse = self
.post(
"/api/v1/transfer-in",
&json!({
"currency": currency,
"amount": amount,
"payAccountType": "MAIN",
}),
)
.await?;
info!(currency, amount, apply_id = %resp.apply_id, "transfer Main→Futures initiated");
Ok(resp)
}
pub async fn get_transfer_list(
&self,
currency: Option<&str>,
transfer_type: Option<&str>,
max_count: u32,
) -> Result<Vec<TransferRecord>> {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Page {
items: Vec<TransferRecord>,
}
let limit = max_count.min(50).to_string();
let mut params: Vec<(&str, &str)> = vec![("maxCount", &limit)];
if let Some(c) = currency {
params.push(("currency", c));
}
if let Some(t) = transfer_type {
params.push(("type", t));
}
let page: Page = self.get("/api/v1/transfer-list", ¶ms).await?;
Ok(page.items)
}
pub async fn add_position_margin(
&self,
symbol: &str,
margin: f64,
direction: &str, ) -> Result<()> {
let resp: serde_json::Value = self
.post(
"/api/v1/position/changeMargin",
&json!({
"symbol": symbol,
"margin": margin,
"direction": direction,
}),
)
.await?;
info!(symbol, margin, direction, resp = %resp, "position margin updated");
Ok(())
}
pub async fn get_account_overview_all(&self) -> Result<Vec<AccountOverview>> {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Wrapper {
summary: Vec<AccountOverview>,
}
let w: Wrapper = self.get("/api/v2/account-overview-all", &[]).await?;
debug!(count = w.summary.len(), "fetched all account overviews");
Ok(w.summary)
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TransferResponse {
pub apply_id: String,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TransferRecord {
pub apply_id: Option<String>,
pub currency: String,
pub status: Option<String>,
#[serde(rename = "type")]
pub transfer_type: Option<String>,
pub amount: Option<f64>,
pub created_at: Option<i64>,
}