rust-okx 0.3.0

Async Rust client for the OKX v5 REST API
Documentation
use crate::OkxClient;
use crate::model::InstType;
use crate::test_util::MockTransport;

#[tokio::test]
async fn get_instruments_builds_request_and_parses() {
    let body = r#"{"code":"0","msg":"","data":[
            {"instType":"SPOT","instId":"BTC-USDT","uly":"","instFamily":"",
             "baseCcy":"BTC","quoteCcy":"USDT","settleCcy":"","lotSz":"0.00000001",
             "tickSz":"0.1","minSz":"0.00001","state":"live"}]}"#;
    let mock = MockTransport::new(body);
    let client = OkxClient::with_transport(mock.clone()).build();

    let instruments = client
        .public_data()
        .get_instruments(InstType::Spot, None)
        .await
        .unwrap();

    assert_eq!(instruments.len(), 1);
    assert_eq!(instruments[0].inst_id, "BTC-USDT");
    assert_eq!(instruments[0].base_ccy, "BTC");
    assert_eq!(instruments[0].tick_sz.as_str(), "0.1");

    let req = mock.captured();
    assert_eq!(req.method, http::Method::GET);
    assert!(
        req.uri
            .ends_with("/api/v5/public/instruments?instType=SPOT")
    );
    assert!(!req.is_signed(), "public endpoint must not be signed");
}

#[tokio::test]
async fn get_system_time_parses_time() {
    let body = r#"{"code":"0","msg":"","data":[{"ts":"1597026383085"}]}"#;
    let mock = MockTransport::new(body);
    let client = OkxClient::with_transport(mock.clone()).build();

    let time = client.public_data().get_system_time().await.unwrap();
    assert_eq!(time[0].ts.as_str(), "1597026383085");

    let req = mock.captured();
    assert!(req.uri.ends_with("/api/v5/public/time"));
    assert_eq!(req.query(), None);
    assert!(!req.is_signed());
}

#[tokio::test]
async fn get_open_interest_uses_family_request() {
    let body = r#"{"code":"0","msg":"","data":[
            {"instType":"SWAP","instId":"BTC-USDT-SWAP","oi":"10","oiCcy":"1","ts":"1597026383085"}]}"#;
    let mock = MockTransport::new(body);
    let client = OkxClient::with_transport(mock.clone()).build();
    let request = super::InstrumentFamilyRequest::new(InstType::Swap).inst_id("BTC-USDT-SWAP");

    let rows = client
        .public_data()
        .get_open_interest(&request)
        .await
        .unwrap();
    assert_eq!(rows[0].oi.as_str(), "10");

    let req = mock.captured();
    assert_eq!(req.query(), Some("instType=SWAP&instId=BTC-USDT-SWAP"));
}

#[tokio::test]
async fn get_funding_rate_queries_instrument() {
    let body = r#"{"code":"0","msg":"","data":[
            {"instId":"BTC-USDT-SWAP","fundingRate":"0.0001","nextFundingRate":"0.0002",
             "fundingTime":"1597026383085","nextFundingTime":"1597030000000"}]}"#;
    let mock = MockTransport::new(body);
    let client = OkxClient::with_transport(mock.clone()).build();

    let rows = client
        .public_data()
        .get_funding_rate("BTC-USDT-SWAP")
        .await
        .unwrap();
    assert_eq!(rows[0].funding_rate.as_str(), "0.0001");

    let req = mock.captured();
    assert_eq!(req.query(), Some("instId=BTC-USDT-SWAP"));
}

#[tokio::test]
async fn get_funding_rate_history_uses_builder_query() {
    let body = r#"{"code":"0","msg":"","data":[
            {"instId":"BTC-USDT-SWAP","fundingRate":"0.0001","realizedRate":"0.0001",
             "fundingTime":"1597026383085","method":"current_period"}]}"#;
    let mock = MockTransport::new(body);
    let client = OkxClient::with_transport(mock.clone()).build();
    let request = super::FundingRateHistoryRequest::new("BTC-USDT-SWAP")
        .before("10")
        .limit(1);

    let rows = client
        .public_data()
        .get_funding_rate_history(&request)
        .await
        .unwrap();
    assert_eq!(rows[0].method, "current_period");

    let req = mock.captured();
    assert_eq!(req.query(), Some("instId=BTC-USDT-SWAP&before=10&limit=1"));
    assert!(!req.query().unwrap().contains("after"));
}

#[tokio::test]
async fn get_price_limit_queries_instrument() {
    let body = r#"{"code":"0","msg":"","data":[
            {"instId":"BTC-USDT-SWAP","buyLmt":"45000","sellLmt":"39000","ts":"1597026383085"}]}"#;
    let mock = MockTransport::new(body);
    let client = OkxClient::with_transport(mock.clone()).build();

    let rows = client
        .public_data()
        .get_price_limit("BTC-USDT-SWAP")
        .await
        .unwrap();
    assert_eq!(rows[0].buy_lmt.as_str(), "45000");

    let req = mock.captured();
    assert_eq!(req.query(), Some("instId=BTC-USDT-SWAP"));
}

#[tokio::test]
async fn get_mark_price_uses_family_request() {
    let body = r#"{"code":"0","msg":"","data":[
            {"instType":"SWAP","instId":"BTC-USDT-SWAP","markPx":"42000","ts":"1597026383085"}]}"#;
    let mock = MockTransport::new(body);
    let client = OkxClient::with_transport(mock.clone()).build();
    let request = super::InstrumentFamilyRequest::new(InstType::Swap).inst_family("BTC-USDT");

    let rows = client.public_data().get_mark_price(&request).await.unwrap();
    assert_eq!(rows[0].mark_px.as_str(), "42000");

    let req = mock.captured();
    assert_eq!(req.query(), Some("instType=SWAP&instFamily=BTC-USDT"));
}

#[tokio::test]
async fn get_delivery_exercise_history_uses_builder_query() {
    let body = r#"{"code":"0","msg":"","data":[
            {"instType":"FUTURES","instId":"BTC-USD-240628","px":"42000","type":"delivery","ts":"1597026383085"}]}"#;
    let mock = MockTransport::new(body);
    let client = OkxClient::with_transport(mock.clone()).build();
    let request = super::DeliveryExerciseHistoryRequest::new(InstType::Futures)
        .underlying("BTC-USD")
        .limit(1);

    let rows = client
        .public_data()
        .get_delivery_exercise_history(&request)
        .await
        .unwrap();
    assert_eq!(rows[0].exercise_type, "delivery");

    let req = mock.captured();
    assert_eq!(req.query(), Some("instType=FUTURES&uly=BTC-USD&limit=1"));
}

#[tokio::test]
async fn get_position_tiers_uses_builder_query() {
    let body = r#"{"code":"0","msg":"","data":[
            {"instType":"SWAP","tdMode":"cross","instId":"BTC-USDT-SWAP","tier":"1",
             "minSz":"0","maxSz":"100","imr":"0.1","mmr":"0.05"}]}"#;
    let mock = MockTransport::new(body);
    let client = OkxClient::with_transport(mock.clone()).build();
    let request = super::PositionTiersRequest::new(InstType::Swap, "cross").tier("1");

    let rows = client
        .public_data()
        .get_position_tiers(&request)
        .await
        .unwrap();
    assert_eq!(rows[0].tier, "1");

    let req = mock.captured();
    assert_eq!(req.query(), Some("instType=SWAP&tdMode=cross&tier=1"));
}

#[tokio::test]
async fn get_underlying_queries_inst_type() {
    let body = r#"{"code":"0","msg":"","data":["BTC-USD"]}"#;
    let mock = MockTransport::new(body);
    let client = OkxClient::with_transport(mock.clone()).build();

    let rows = client
        .public_data()
        .get_underlying(InstType::Swap)
        .await
        .unwrap();
    assert_eq!(rows[0], "BTC-USD");

    let req = mock.captured();
    assert_eq!(req.query(), Some("instType=SWAP"));
}

#[tokio::test]
async fn get_insurance_fund_uses_builder_query() {
    let body = r#"{"code":"0","msg":"","data":[
            {"instType":"SWAP","type":"regular_update","ccy":"USDT","amt":"100","ts":"1597026383085"}]}"#;
    let mock = MockTransport::new(body);
    let client = OkxClient::with_transport(mock.clone()).build();
    let request = super::InsuranceFundRequest::new(InstType::Swap)
        .fund_type("regular_update")
        .currency("USDT");

    let rows = client
        .public_data()
        .get_insurance_fund(&request)
        .await
        .unwrap();
    assert_eq!(rows[0].amt.as_str(), "100");

    let req = mock.captured();
    assert_eq!(
        req.query(),
        Some("instType=SWAP&type=regular_update&ccy=USDT")
    );
}

#[tokio::test]
async fn get_convert_contract_coin_uses_builder_query() {
    let body = r#"{"code":"0","msg":"","data":[
            {"instId":"BTC-USDT-SWAP","sz":"1","px":"42000","unit":"coin"}]}"#;
    let mock = MockTransport::new(body);
    let client = OkxClient::with_transport(mock.clone()).build();
    let request = super::ConvertContractCoinRequest::new("1", "BTC-USDT-SWAP", "1")
        .price("42000")
        .unit("coin");

    let rows = client
        .public_data()
        .get_convert_contract_coin(&request)
        .await
        .unwrap();
    assert_eq!(rows[0].unit, "coin");

    let req = mock.captured();
    assert_eq!(
        req.query(),
        Some("type=1&instId=BTC-USDT-SWAP&sz=1&px=42000&unit=coin")
    );
}

#[tokio::test]
async fn get_option_summary_uses_flexible_query() {
    let body = r#"{"code":"0","msg":"","data":[
            {"instType":"OPTION","instFamily":"BTC-USD","instId":"BTC-USD-240628-50000-C","delta":"0.5","ts":"1597026383085"}]}"#;
    let mock = MockTransport::new(body);
    let client = OkxClient::with_transport(mock.clone()).build();
    let request = super::OptionSummaryRequest::new().param("instFamily", "BTC-USD");

    let rows = client
        .public_data()
        .get_option_summary(&request)
        .await
        .unwrap();
    assert_eq!(rows[0].inst_family, "BTC-USD");

    let req = mock.captured();
    assert_eq!(req.query(), Some("instFamily=BTC-USD"));
}

#[tokio::test]
async fn get_estimated_price_queries_inst_id() {
    let body = r#"{"code":"0","msg":"","data":[
            {"instId":"BTC-USD-240628","px":"42000","ts":"1597026383085"}]}"#;
    let mock = MockTransport::new(body);
    let client = OkxClient::with_transport(mock.clone()).build();

    let rows = client
        .public_data()
        .get_estimated_price("BTC-USD-240628")
        .await
        .unwrap();
    assert_eq!(rows[0].px.as_str(), "42000");

    let req = mock.captured();
    assert_eq!(req.query(), Some("instId=BTC-USD-240628"));
}

#[tokio::test]
async fn get_discount_rate_interest_free_quota_omits_currency() {
    let body = r#"{"code":"0","msg":"","data":[{"ccy":"USDT","amt":"100"}]}"#;
    let mock = MockTransport::new(body);
    let client = OkxClient::with_transport(mock.clone()).build();

    let rows = client
        .public_data()
        .get_discount_rate_interest_free_quota(None)
        .await
        .unwrap();
    assert_eq!(rows[0].ccy, "USDT");

    let req = mock.captured();
    assert_eq!(req.query(), None);
}

#[tokio::test]
async fn public_edge_requests_use_flexible_query() {
    let body = r#"{"code":"0","msg":"","data":[
            {"instType":"OPTION","instFamily":"BTC-USD","instId":"BTC-USD-240628-50000-C","px":"10","ts":"1597026383085"}]}"#;

    let mock = MockTransport::new(body);
    let client = OkxClient::with_transport(mock.clone()).build();
    let request = super::InstrumentTickBandsRequest::new().param("instFamily", "BTC-USD");
    let rows = client
        .public_data()
        .get_instrument_tick_bands(&request)
        .await
        .unwrap();
    assert_eq!(rows[0].inst_id, "BTC-USD-240628-50000-C");
    assert_eq!(mock.captured().query(), Some("instFamily=BTC-USD"));

    let mock = MockTransport::new(body);
    let client = OkxClient::with_transport(mock.clone()).build();
    let request = super::PublicOptionTradesRequest::new().param("instFamily", "BTC-USD");
    client
        .public_data()
        .get_option_trades(&request)
        .await
        .unwrap();
    assert_eq!(mock.captured().query(), Some("instFamily=BTC-USD"));

    let mock = MockTransport::new(body);
    let client = OkxClient::with_transport(mock.clone()).build();
    let request = super::MarketDataHistoryRequest::new()
        .param("instId", "BTC-USDT")
        .param("period", "5m");
    client
        .public_data()
        .get_market_data_history(&request)
        .await
        .unwrap();
    assert_eq!(mock.captured().query(), Some("instId=BTC-USDT&period=5m"));
}

#[tokio::test]
async fn loan_quota_requests_are_public_and_flexible() {
    let body =
        r#"{"code":"0","msg":"","data":[{"ccy":"USDT","rate":"0.01","ts":"1597026383085"}]}"#;

    let mock = MockTransport::new(body);
    let client = OkxClient::with_transport(mock.clone()).build();
    let request = super::InterestRateLoanQuotaRequest::new().param("ccy", "USDT");
    let rows = client
        .public_data()
        .get_interest_rate_loan_quota(&request)
        .await
        .unwrap();
    assert_eq!(rows[0].rate.as_str(), "0.01");
    assert!(!mock.captured().is_signed());

    let mock = MockTransport::new(body);
    let client = OkxClient::with_transport(mock.clone()).build();
    let request = super::VipInterestRateLoanQuotaRequest::new().param("ccy", "USDT");
    client
        .public_data()
        .get_vip_interest_rate_loan_quota(&request)
        .await
        .unwrap();
    assert_eq!(mock.captured().query(), Some("ccy=USDT"));
    assert!(!mock.captured().is_signed());
}