Skip to content

Commit

Permalink
Add support for binance options (#96)
Browse files Browse the repository at this point in the history
* binance: add binance european options endpoint

* binance: empty content type for binance-e get req

* binance: finish the adaption for options insts

* binance: fix default stream

* binance: allow dispatch req in svc

* binance: finish the adaption for options trade

* binance: finish the adaption for options depth

* binance: finish the adaptation for options candles

* binance: fix the stream name bugs

* binance: fix the format of trade

* binance: finish the adaptation of sub option order

* binance: finish the adaptation for options trading

* binance: allow dispatching main/sub streams

* binance: add example for subscriptiong options ws

* binance: add CHANNEL env
  • Loading branch information
Nouzan authored Feb 11, 2024
1 parent caa5463 commit c96b647
Show file tree
Hide file tree
Showing 42 changed files with 1,502 additions and 290 deletions.
5 changes: 4 additions & 1 deletion examples/examples/binance_candle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,19 @@ async fn main() -> anyhow::Result<()> {
let endpoint = match endpoint.as_str() {
"binance-u" => Binance::usd_margin_futures(),
"binance-s" => Binance::spot(),
"binance-e" => Binance::european_options(),
_ => anyhow::bail!("unsupported"),
};

let inst = std::env::var("INST").unwrap_or_else(|_| String::from("btcusdt"));

let mut binance = endpoint
.connect_exc()
.into_rate_limited(200, Duration::from_secs(60))
.into_fetch_candles_forward(1000);
let mut stream = binance
.fetch_candles_range(
"btcusdt",
&inst,
Period::minutes(UtcOffset::UTC, 1),
datetime!(2020-06-27 00:00:00 +08:00)..,
)
Expand Down
1 change: 1 addition & 0 deletions examples/examples/binance_fetch_instruments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ async fn main() -> anyhow::Result<()> {
let endpoint = match endpoint.as_str() {
"binance-u" => Binance::usd_margin_futures(),
"binance-s" => Binance::spot(),
"binance-e" => Binance::european_options(),
_ => anyhow::bail!("unsupported"),
};

Expand Down
1 change: 1 addition & 0 deletions examples/examples/binance_orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ async fn main() -> anyhow::Result<()> {
let mut endpoint = match endpoint.as_str() {
"binance-u" => Binance::usd_margin_futures(),
"binance-s" => Binance::spot(),
"binance-e" => Binance::european_options(),
_ => anyhow::bail!("unsupported"),
};

Expand Down
1 change: 1 addition & 0 deletions examples/examples/binance_ticker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ async fn main() -> anyhow::Result<()> {
let mut endpoint = match endpoint.as_str() {
"binance-u" => Binance::usd_margin_futures(),
"binance-s" => Binance::spot(),
"binance-e" => Binance::european_options(),
_ => anyhow::bail!("unsupported"),
};

Expand Down
6 changes: 6 additions & 0 deletions examples/examples/exc_trading.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ impl From<MarginOp> for exc_binance::MarginOp {
enum Exchange {
BinanceU,
BinanceS,
BinanceE,
Okx,
}

Expand Down Expand Up @@ -226,6 +227,11 @@ async fn main() -> anyhow::Result<()> {
let exc = Binance::usd_margin_futures().private(key).connect_exc();
env.execute(exc, inst, execs).await?;
}
Exchange::BinanceE => {
let key = serde_json::from_str(&args.key)?;
let exc = Binance::european_options().private(key).connect_exc();
env.execute(exc, inst, execs).await?;
}
Exchange::BinanceS => {
let key = serde_json::from_str(&args.key)?;
let options = match (args.buy_margin, args.sell_margin) {
Expand Down
4 changes: 2 additions & 2 deletions exc-binance/examples/binance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ async fn main() -> anyhow::Result<()> {
.connect();
api.ready().await?;
let mut stream = api
.call(Request::Ws(WsRequest::subscribe(Name::agg_trade(
.call(Request::Ws(WsRequest::subscribe_stream(Name::agg_trade(
"btcusdt",
))))
.await?
Expand Down Expand Up @@ -59,7 +59,7 @@ async fn main() -> anyhow::Result<()> {
count += 1;
api.ready().await?;
match api
.call(Request::Ws(WsRequest::subscribe(Name::agg_trade(
.call(Request::Ws(WsRequest::subscribe_stream(Name::agg_trade(
"btcusdt",
))))
.await
Expand Down
44 changes: 44 additions & 0 deletions exc-binance/examples/binance_options.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use std::time::Duration;

use exc_binance::{
websocket::{protocol::frame::Name, request::WsRequest},
Binance, Request,
};
use futures::StreamExt;
use tower::{Service, ServiceExt};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::fmt()
.with_writer(std::io::stderr)
.with_env_filter(tracing_subscriber::EnvFilter::new(
std::env::var("RUST_LOG").unwrap_or_else(|_| "error,binance_options=debug".into()),
))
.init();
let channel = std::env::var("CHANNEL").unwrap_or("index".to_string());
let inst = std::env::var("INST")?;
let mut api = Binance::european_options()
.ws_keep_alive_timeout(Duration::from_secs(30))
.connect();
api.ready().await?;
let mut stream = api
.call(Request::Ws(WsRequest::subscribe_stream(
Name::new(&channel).with_inst(&inst),
)))
.await?
.into_stream::<serde_json::Value>()
.unwrap()
.boxed();
while let Some(data) = stream.next().await {
match data {
Ok(data) => {
tracing::info!("data={data:#?}");
}
Err(err) => {
tracing::error!("error={err}");
break;
}
}
}
Ok(())
}
2 changes: 2 additions & 0 deletions exc-binance/examples/binance_trading_directly.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ async fn main() -> anyhow::Result<()> {
symbol: symbol.to_string(),
order_id: Some(id),
orig_client_order_id: None,
client_order_id: None,
},
}))
.await?
Expand All @@ -74,6 +75,7 @@ async fn main() -> anyhow::Result<()> {
symbol: symbol.to_string(),
order_id: Some(id),
orig_client_order_id: None,
client_order_id: None,
},
}))
.await?
Expand Down
4 changes: 2 additions & 2 deletions exc-binance/examples/binance_ws_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ async fn main() -> anyhow::Result<()> {
.connect();
ServiceExt::<WsRequest>::ready(&mut api).await?;
let mut stream = api
.call(WsRequest::subscribe(Name::agg_trade("btcusdt")))
.call(WsRequest::subscribe_stream(Name::agg_trade("btcusdt")))
.await?
.into_stream::<AggTrade>()
.unwrap()
Expand All @@ -49,7 +49,7 @@ async fn main() -> anyhow::Result<()> {
count += 1;
ServiceExt::<WsRequest>::ready(&mut api).await?;
match api
.call(WsRequest::subscribe(Name::agg_trade("btcusdt")))
.call(WsRequest::subscribe_stream(Name::agg_trade("btcusdt")))
.await
{
Ok(resp) => {
Expand Down
10 changes: 10 additions & 0 deletions exc-binance/src/endpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ impl Endpoint {
}
}

/// Endpoint for European options.
pub fn european_options() -> Self {
Self {
key: None,
http: (RestEndpoint::EuropeanOptions, HttpEndpoint::default()),
ws: BinanceWebsocketApi::european_options(),
buffer: CAP,
}
}

/// Set websocket keep-alive timeout.
pub fn ws_keep_alive_timeout(&mut self, timeout: Duration) -> &mut Self {
self.ws.keep_alive_timeout(timeout);
Expand Down
6 changes: 6 additions & 0 deletions exc-binance/src/http/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ pub enum RestError {
/// Missing date for futures.
#[error("missing date for futures")]
MissingDateForFutures,
/// Invalid date for options.
#[error("invalid date for options")]
InvalidDateForOptions,
/// Missing base asset for options.
#[error("missing base asset for options")]
MissingBaseAssetForOptions,
/// Unknown contract type.
#[error("unknown contract type: {0:?}")]
UnknownContractType(String),
Expand Down
40 changes: 25 additions & 15 deletions exc-binance/src/http/request/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ impl Rest for ListSubAccounts {

fn to_path(&self, endpoint: &RestEndpoint) -> Result<String, RestError> {
match endpoint {
RestEndpoint::UsdMarginFutures => Err(RestError::UnsupportedEndpoint(anyhow::anyhow!(
"`ListSubAccounts` only available on `binance-s`"
))),
RestEndpoint::UsdMarginFutures | RestEndpoint::EuropeanOptions => {
Err(RestError::UnsupportedEndpoint(anyhow::anyhow!(
"`ListSubAccounts` only available on `binance-s`"
)))
}
RestEndpoint::Spot(_options) => Ok("/sapi/v1/sub-account/list".to_string()),
}
}
Expand Down Expand Up @@ -68,9 +70,11 @@ impl Rest for GetSubAccountAssets {

fn to_path(&self, endpoint: &RestEndpoint) -> Result<String, RestError> {
match endpoint {
RestEndpoint::UsdMarginFutures => Err(RestError::UnsupportedEndpoint(anyhow::anyhow!(
"`GetSubAccountAssets` only available on `binance-s`"
))),
RestEndpoint::UsdMarginFutures | RestEndpoint::EuropeanOptions => {
Err(RestError::UnsupportedEndpoint(anyhow::anyhow!(
"`GetSubAccountAssets` only available on `binance-s`"
)))
}
RestEndpoint::Spot(_options) => Ok("/sapi/v3/sub-account/assets".to_string()),
}
}
Expand Down Expand Up @@ -107,9 +111,11 @@ impl Rest for GetSubAccountMargin {

fn to_path(&self, endpoint: &RestEndpoint) -> Result<String, RestError> {
match endpoint {
RestEndpoint::UsdMarginFutures => Err(RestError::UnsupportedEndpoint(anyhow::anyhow!(
"`GetSubAccountMargin` only available on `binance-s`"
))),
RestEndpoint::UsdMarginFutures | RestEndpoint::EuropeanOptions => {
Err(RestError::UnsupportedEndpoint(anyhow::anyhow!(
"`GetSubAccountMargin` only available on `binance-s`"
)))
}
RestEndpoint::Spot(_options) => Ok("/sapi/v1/sub-account/margin/account".to_string()),
}
}
Expand Down Expand Up @@ -165,9 +171,11 @@ impl Rest for GetSubAccountFutures {

fn to_path(&self, endpoint: &RestEndpoint) -> Result<String, RestError> {
match endpoint {
RestEndpoint::UsdMarginFutures => Err(RestError::UnsupportedEndpoint(anyhow::anyhow!(
"`GetSubAccountFutures` only available on `binance-s`"
))),
RestEndpoint::UsdMarginFutures | RestEndpoint::EuropeanOptions => {
Err(RestError::UnsupportedEndpoint(anyhow::anyhow!(
"`GetSubAccountFutures` only available on `binance-s`"
)))
}
RestEndpoint::Spot(_options) => Ok("/sapi/v2/sub-account/futures/account".to_string()),
}
}
Expand Down Expand Up @@ -223,9 +231,11 @@ impl Rest for GetSubAccountFuturesPositions {

fn to_path(&self, endpoint: &RestEndpoint) -> Result<String, RestError> {
match endpoint {
RestEndpoint::UsdMarginFutures => Err(RestError::UnsupportedEndpoint(anyhow::anyhow!(
"`GetSubAccountFuturesPositions` only available on `binance-s`"
))),
RestEndpoint::UsdMarginFutures | RestEndpoint::EuropeanOptions => {
Err(RestError::UnsupportedEndpoint(anyhow::anyhow!(
"`GetSubAccountFuturesPositions` only available on `binance-s`"
)))
}
RestEndpoint::Spot(_options) => {
Ok("/sapi/v2/sub-account/futures/positionRisk".to_string())
}
Expand Down
1 change: 1 addition & 0 deletions exc-binance/src/http/request/candle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ impl Rest for QueryCandles {
match endpoint {
RestEndpoint::UsdMarginFutures => Ok(format!("/fapi/v1/klines?{qs}")),
RestEndpoint::Spot(_) => Ok(format!("/api/v3/klines?{qs}")),
RestEndpoint::EuropeanOptions => Ok(format!("/eapi/v1/klines?{qs}")),
}
}

Expand Down
1 change: 1 addition & 0 deletions exc-binance/src/http/request/instrument.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ impl Rest for ExchangeInfo {
match endpoint {
RestEndpoint::UsdMarginFutures => Ok("/fapi/v1/exchangeInfo".to_string()),
RestEndpoint::Spot(_) => Ok("/api/v3/exchangeInfo".to_string()),
RestEndpoint::EuropeanOptions => Ok("/eapi/v1/exchangeInfo".to_string()),
}
}

Expand Down
2 changes: 2 additions & 0 deletions exc-binance/src/http/request/listen_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ impl Rest for CurrentListenKey {
fn to_path(&self, endpoint: &super::RestEndpoint) -> Result<String, RestError> {
match endpoint {
RestEndpoint::UsdMarginFutures => Ok("/fapi/v1/listenKey".to_string()),
RestEndpoint::EuropeanOptions => Ok("/eapi/v1/listenKey".to_string()),
RestEndpoint::Spot(options) => {
if options.margin.is_some() {
Ok("/sapi/v1/userDataStream".to_string())
Expand Down Expand Up @@ -50,6 +51,7 @@ impl Rest for DeleteListenKey {
fn to_path(&self, endpoint: &super::RestEndpoint) -> Result<String, RestError> {
match endpoint {
RestEndpoint::UsdMarginFutures => Ok("/fapi/v1/listenKey".to_string()),
RestEndpoint::EuropeanOptions => Ok("/eapi/v1/listenKey".to_string()),
RestEndpoint::Spot(options) => {
if options.margin.is_some() {
Ok("/sapi/v1/userDataStream".to_string())
Expand Down
22 changes: 17 additions & 5 deletions exc-binance/src/http/request/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,13 +169,16 @@ pub enum RestEndpoint {
/// Spot.
/// Set it to `true` to enable margin trading.
Spot(SpotOptions),
/// European options.
EuropeanOptions,
}

impl fmt::Display for RestEndpoint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UsdMarginFutures => write!(f, "binance-u"),
Self::Spot(_) => write!(f, "binance-s"),
Self::EuropeanOptions => write!(f, "binance-e"),
}
}
}
Expand All @@ -186,6 +189,7 @@ impl RestEndpoint {
match self {
Self::UsdMarginFutures => "https://fapi.binance.com",
Self::Spot(_) => "https://api.binance.com",
Self::EuropeanOptions => "https://eapi.binance.com",
}
}
}
Expand Down Expand Up @@ -235,11 +239,19 @@ impl<T: Rest> RestRequest<T> {
} else {
hyper::Body::from(serde_urlencoded::to_string(&value)?)
};
let mut request = Request::builder()
.method(self.payload.method(endpoint)?)
.uri(uri)
.header("content-type", "application/x-www-form-urlencoded")
.body(body)?;
let method = self.payload.method(endpoint)?;

let mut builder = Request::builder().method(method.clone()).uri(uri);

// FIXME: This is required by binance european options for now.
if !matches!(
(endpoint, method),
(RestEndpoint::EuropeanOptions, Method::GET)
) {
builder = builder.header("content-type", "application/x-www-form-urlencoded");
}

let mut request = builder.body(body)?;
let headers = request.headers_mut();
if let Some(key) = key {
if self.payload.need_apikey() {
Expand Down
Loading

0 comments on commit c96b647

Please sign in to comment.