rust trading project : api 요청 구조

wangki·2025년 3월 16일

trading-algorithm

목록 보기
2/9

개요

https://velog.io/@wang_ki/rust-algorithm-trading-project 이 후 처음으로 개발 내용에 대해서 작성한다.
많이는 개발하지 못했지만 꾸준히 개발 중이다.

binance로 api 요청을 하는 구조에 대해서 작성하겠다.

내용

Hexagonal architexture

헥사고날 아키텍처로 구조를 구성했다.

위 그림과 같이 큰 튼을 구성했다.

port

pub trait UserPort {
   async fn get_account_balance(&self) -> anyhow::Result<String>;
   async fn get_trade_fee(&self) -> anyhow::Result<String>;
}

pub trait CommonPort {
   async fn get_kline(&self) -> anyhow::Result<Klines>;
}

// adapter는 raw date(json string)를 넘기도록 한다.

pub trait TradePort {
   async fn order_position(&self) -> anyhow::Result<()>;
   async fn change_leverage(&self, symbol: &str, leverage: i32) -> anyhow::Result<String>;
}

여러 포트를 만들었다.
port를 만든 이유는 확장성을 고려하여 여러 거래소로부터 데이터를 가져올 수 있도록 하기 위함이다.
1. UserPort
유저 정보를 받아오는데 필요한 인터페이스를 정의한 trait이다.
2. CommonPort
signature가 필요 없는 가격, 볼륨과 같은 일반적인 데이터 인터페이스를 정의한 trait이다.
3. TradePort
주문과 관련된 인터페이스를 정의한 trait이다.

adapter

pub struct BinanceTrade {
    secret_key: String,
    api_key: String,
}

impl BinanceTrade {
    pub fn new() -> Self {
        let api_key = env::var("BINANCE_API_KEY").expect("fail to get api_key");
        let secret_key = env::var("BINANCE_SECRET_KEY").expect("fail to get secret_key");

        BinanceTrade {
            secret_key, 
            api_key, 
        }
    }
}

impl TradePort for BinanceTrade {
    async fn order_position(&self) -> anyhow::Result<()> {
        Ok(())
    }
    
    async fn change_leverage(&self, symbol: &str, leverage: i32) -> anyhow::Result<String> {
        let binance_request = BinanceRequest::new(BaseUrl::future, TradeEndpoint::Leverage { symbol: symbol.to_string(), leverage });
        
        let text = utils::request_with_signature(
            "post", 
            self.secret_key.clone(), 
            self.api_key.clone(), 
            binance_request
            )
            .await?;

        Ok(text)
    }
}

secret_keyapi_key를 필드로 정의하였다.
TradePort를 구현하여 실제 binance와의 통신을 정의하였다.
request_with_signature의 경우 공통 메서드로 api요청을 할 수 있도록 만들었다.

model

모델을 정의하며 고민이 많았다.
url을 통해 요청을 하는데 수많은 baseurl
endpoint, 그리고 파라미터, signature등등 고려할 것이 많았다. 이 모든것을 하드코딩으로 하게 되면 여러 거래소에 대해서 뿐만아니라 유지보수, 사용성 측면에서도 문제가 있을거라고 생각했다. rust의 강력한 enum type을 활용하여 문제를 해결하려고 했다.

pub struct BinanceRequest<T> 
where
    T: Endpoint, 
{
    pub base_url: BaseUrl,
    pub endpoint_url: T,
}

BinanceReqeust 구조체를 생성하여 endpoint_url을 제네릭타입으로 설정하였다. base_url의 경우 많아야 2가지 정도만 있기때문에

#[derive(Debug, Clone, Copy)]
pub enum BaseUrl {
    future, 
    spot,
}
  1. BaseUrl
    BaseUrl enum을 만들었다. From trait를 구현하여 enum의 필드값을 Into를 활용해 String값으로 손쉽게 변경하고자 하였다.

    impl From<BaseUrl> for String {
        fn from(value: BaseUrl) -> Self {
            match value {
                BaseUrl::future => "https://fapi.binance.com".to_string(),
                BaseUrl::spot => "https://api.binance.com".to_string(),
            }
        }
    }

    BinanceRequest 구조체의 내부 메서드를 정의할 때

    
    impl<T> BinanceRequest<T> 
    where 
        T: Endpoint + Into<String>,
    {
        pub fn new(base_url: BaseUrl, endpoint_url: T) -> Self {
            BinanceRequest {
                base_url,
                endpoint_url,
            }
        }
    
        pub fn base_url(&self) -> String {
            self.base_url.into()
        }
    
        pub fn query(&self) -> String {
            self.endpoint_url.query()
        }
    
        pub fn query_with_timestamp(&self) -> String {
            self.endpoint_url.query_with_tiemstamp()
        }
    }

    base_url 메서드 내부에서 into를 활용하였다.

  2. EndpointUrl
    BinanceRequest의 필드인 endpoint_urlT타입을 가지는 제네릭 타입이다.
    T의 trait bound는 T: Endpoint + Into<String>이다.
    Endpoint trait을 구현한 예시를 보겠다.

    
    pub enum TradeEndpoint {
      Order,
      Leverage{
          symbol: String,
          leverage: i32,
      },
    }
    
    // trait bound로 가야할 것 같은데?
    
    impl Endpoint for TradeEndpoint {
      fn query(&self) -> String {
          match self {
              TradeEndpoint::Leverage { symbol, leverage } => {
                  query!(symbol, leverage)                
              }, 
              TradeEndpoint::Order => {
                  String::new()
              }
          }
      }
    }
    
    impl From<TradeEndpoint> for String {
      fn from(value: TradeEndpoint) -> Self {
          match value {
              TradeEndpoint::Order => "/fapi/v1/order".to_string(),
              TradeEndpoint::Leverage { symbol: _, leverage: _ } => {
                  format!("fapi/v1/leverage")
              },
          }
      }
    }
    
    // Endpoint trait bound를 활용하기 위함.
    // 
    pub trait Endpoint {
      fn query(&self) -> String;
      fn query_with_tiemstamp(&self) -> String {
          let timestamp = SystemTime::now()
              .duration_since(UNIX_EPOCH).unwrap()
              .as_millis()
              .to_string();
    
          format!("{}&timestamp={}", self.query(), timestamp)
      }
    }
    

    FromEndpoint trait을 구현하였다.

    trait bound를 만족하는 경우 손쉽게 원하는 api 요청을 할 수 있도록 설계하였다.

결론

adapterdomain 영역에서 사용한다.
주문, 조회, 등 여러 기능을 가진 api 요청 구조 작업을 완료 후 실제 알고리즘을 개발할 예정이다. 쉽지 않지만 그래도 할 수 있을 것 같은 기분이 든다.

0개의 댓글