defmodule Binance do
alias Binance.Rest.HTTPClient
# Server
@doc """
Pings binance API. Returns `{:ok, %{}}` if successful, `{:error, reason}` otherwise
"""
def ping() do
HTTPClient.get_binance("/api/v3/ping")
end
@doc """
Get binance server time in unix epoch.
Returns `{:ok, time}` if successful, `{:error, reason}` otherwise
## Example
```
{:ok, 1515390701097}
```
"""
def get_server_time() do
case HTTPClient.get_binance("/api/v3/time") do
{:ok, %{"serverTime" => time}} -> {:ok, time}
err -> err
end
end
def get_exchange_info() do
case HTTPClient.get_binance("/api/v3/exchangeInfo") do
{:ok, data} -> {:ok, Binance.ExchangeInfo.new(data)}
err -> err
end
end
@doc """
Get historical trades
https://binance-docs.github.io/apidocs/spot/en/#old-trade-lookup
Returns `{:ok, [%Binance.HistoricalTrade{}]}` or `{:error, reason}`.
## Example
{:ok,
[%Binance.HistoricalTrade{
id: 192180149,
is_best_match: true,
is_buyer_maker: true,
price: "1.79878000",
qty: "55.50000000",
quote_qty: "99.83229000",
time: 1618341167715
}]
}
"""
def get_historical_trades(symbol, limit, from_id)
when is_binary(symbol) and is_integer(limit) do
arguments =
%{
symbol: symbol,
limit: limit
}
|> Map.merge(
unless(
is_nil(from_id),
do: %{fromId: from_id},
else: %{}
)
)
case HTTPClient.unsigned_request_binance(
"/api/v3/historicalTrades",
arguments,
:get
) do
{:ok, data} ->
{:ok, Enum.map(data, &Binance.HistoricalTrade.new(&1))}
{:error, err} ->
err
end
end
# Ticker
@doc """
Get all symbols and current prices listed in binance
Returns `{:ok, [%Binance.SymbolPrice{}]}` or `{:error, reason}`.
## Example
```
{:ok,
[%Binance.SymbolPrice{price: "0.07579300", symbol: "ETHBTC"},
%Binance.SymbolPrice{price: "0.01670200", symbol: "LTCBTC"},
%Binance.SymbolPrice{price: "0.00114550", symbol: "BNBBTC"},
%Binance.SymbolPrice{price: "0.00640000", symbol: "NEOBTC"},
%Binance.SymbolPrice{price: "0.00030000", symbol: "123456"},
%Binance.SymbolPrice{price: "0.04895000", symbol: "QTUMETH"},
...]}
```
"""
def get_all_prices() do
case HTTPClient.get_binance("/api/v3/ticker/price") do
{:ok, data} ->
{:ok, Enum.map(data, &Binance.SymbolPrice.new(&1))}
err ->
err
end
end
@doc """
Retrieves the current ticker information for the given trade pair.
Symbol can be a binance symbol in the form of `"ETHBTC"` or `%Binance.TradePair{}`.
Returns `{:ok, %Binance.Ticker{}}` or `{:error, reason}`
## Example
```
{:ok,
%Binance.Ticker{ask_price: "0.07548800", bid_price: "0.07542100",
close_time: 1515391124878, count: 661676, first_id: 16797673,
high_price: "0.07948000", last_id: 17459348, last_price: "0.07542000",
low_price: "0.06330000", open_price: "0.06593800", open_time: 1515304724878,
prev_close_price: "0.06593800", price_change: "0.00948200",
price_change_percent: "14.380", volume: "507770.18500000",
weighted_avg_price: "0.06946930"}}
```
"""
def get_ticker(%Binance.TradePair{} = symbol) do
case find_symbol(symbol) do
{:ok, binance_symbol} -> get_ticker(binance_symbol)
e -> e
end
end
def get_ticker(symbol) when is_binary(symbol) do
case HTTPClient.get_binance("/api/v3/ticker/24hr?symbol=#{symbol}") do
{:ok, data} -> {:ok, Binance.Ticker.new(data)}
err -> err
end
end
@doc """
Retrieves klines for a symbol, provided a given interval, e.g. "1h".
Function can also take in a 'limit' argument to reduce the number of intervals.
Returns `{:ok, [%Binance.Kline{}]` or `{:error, reason}`
## Example
```
{:ok,
[
%Binance.Kline{
close: "0.16527000",
close_time: 1617861599999,
high: "0.17100000",
ignore: "0",
low: "0.16352000",
number_of_trades: 16167,
open: "0.17088000",
open_time: 1617858000000,
quote_asset_volume: "7713624.32966000",
taker_buy_base_asset_volume: "22020677.70000000",
taker_buy_quote_asset_volume: "3668705.43042700",
volume: "46282422.20000000"
},
%Binance.Kline{
...
```
"""
def get_klines(symbol, interval, limit \\ 500) when is_binary(symbol) do
case HTTPClient.get_binance(
"/api/v3/klines?symbol=#{symbol}&interval=#{interval}&limit=#{limit}"
) do
{:ok, data} ->
{:ok, Enum.map(data, &Binance.Kline.new(&1))}
err ->
err
end
end
@doc """
Retrieves the bids & asks of the order book up to the depth for the given symbol
Returns `{:ok, %{bids: [...], asks: [...], lastUpdateId: 12345}}` or `{:error, reason}`
## Example
```
{:ok,
%Binance.OrderBook{
asks: [
["8400.00000000", "2.04078100", []],
["8405.35000000", "0.50354700", []],
["8406.00000000", "0.32769800", []],
["8406.33000000", "0.00239000", []],
["8406.51000000", "0.03241000", []]
],
bids: [
["8393.00000000", "0.20453200", []],
["8392.57000000", "0.02639000", []],
["8392.00000000", "1.40893300", []],
["8390.09000000", "0.07047100", []],
["8388.72000000", "0.04577400", []]
],
last_update_id: 113634395
}
}
```
"""
def get_depth(symbol, limit) do
case HTTPClient.get_binance("/api/v3/depth?symbol=#{symbol}&limit=#{limit}") do
{:ok, data} -> {:ok, Binance.OrderBook.new(data)}
err -> err
end
end
@doc """
Fetches system status.
Returns `{:ok, %Binance.SystemStatus{}}` or `{:error, reason}`.
"""
def get_system_status() do
case HTTPClient.get_binance("/sapi/v1/system/status") do
{:ok, data} -> {:ok, Binance.SystemStatus.new(data)}
error -> error
end
end
# Account
@doc """
Fetches user account from binance
Returns `{:ok, %Binance.Account{}}` or `{:error, reason}`.
In the case of a error on binance, for example with invalid parameters, `{:error, {:binance_error, %{code: code, msg: msg}}}` will be returned.
Please read https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#account-information-user_data to understand API
"""
def get_account() do
api_key = Application.get_env(:binance, :api_key)
secret_key = Application.get_env(:binance, :secret_key)
case HTTPClient.get_binance("/api/v3/account", %{}, secret_key, api_key) do
{:ok, data} -> {:ok, Binance.Account.new(data)}
error -> error
end
end
# User data streams
@doc """
Creates a socket listen key that later can be used as parameter to listen for
user related events.
Returns `{:ok, %Binance.DataStream{}}` or `{:error, reason}`.
## Example
```
{:ok,
%Binance.DataStream{
listen_key: "pqia91ma19a5s61cv6a81va65sdf19v8a65a1a5s61cv6a81va65sdf19v8a65a1"
}
}
```
For more context please read https://github.com/binance/binance-spot-api-docs/blob/master/user-data-stream.md#create-a-listenkey
"""
def create_listen_key() do
case HTTPClient.unsigned_request_binance("/api/v3/userDataStream", "", :post) do
{:ok, data} -> {:ok, Binance.DataStream.new(data)}
error -> error
end
end
@doc """
Socket listen key expires after 30 minutes withouth a pong response, this
allows keeping it alive.
Returns `{:ok, %{}}` or `{:error, reason}`.
For more context please read https://github.com/binance/binance-spot-api-docs/blob/master/user-data-stream.md#pingkeep-alive-a-listenkey
"""
def keep_alive_listen_key(key) do
case HTTPClient.unsigned_request_binance(
"/api/v3/userDataStream",
"listenKey=#{key}",
:put
) do
{:ok, data} -> {:ok, data}
error -> error
end
end
@doc """
Closes/disables the listen key. To be used when you stop listening to the
stream.
Returns `{:ok, %{}}` or `{:error, reason}`.
For more context please read https://github.com/binance/binance-spot-api-docs/blob/master/user-data-stream.md#close-a-listenkey
"""
def close_listen_key(key) do
case HTTPClient.unsigned_request_binance(
"/api/v3/userDataStream?listenKey=#{key}",
nil,
:delete
) do
{:ok, data} -> {:ok, data}
error -> error
end
end
# Order
@doc """
Creates a new order on binance
Returns `{:ok, %{}}` or `{:error, reason}`.
In the case of a error on binance, for example with invalid parameters, `{:error, {:binance_error, %{code: code, msg: msg}}}` will be returned.
Please read https://www.binance.com/restapipub.html#user-content-account-endpoints to understand all the parameters
"""
def create_order(
symbol,
side,
type,
quantity,
price \\ nil,
time_in_force \\ nil,
new_client_order_id \\ nil,
stop_price \\ nil,
iceberg_quantity \\ nil,
receiving_window \\ 1000,
timestamp \\ nil
) do
timestamp =
case timestamp do
# timestamp needs to be in milliseconds
nil ->
:os.system_time(:millisecond)
t ->
t
end
arguments =
%{
symbol: symbol,
side: side,
type: type,
quantity: quantity,
timestamp: timestamp,
recvWindow: receiving_window
}
|> Map.merge(
unless(
is_nil(new_client_order_id),
do: %{newClientOrderId: new_client_order_id},
else: %{}
)
)
|> Map.merge(
unless(is_nil(stop_price), do: %{stopPrice: format_price(stop_price)}, else: %{})
)
|> Map.merge(
unless(is_nil(iceberg_quantity), do: %{icebergQty: iceberg_quantity}, else: %{})
)
|> Map.merge(unless(is_nil(time_in_force), do: %{timeInForce: time_in_force}, else: %{}))
|> Map.merge(unless(is_nil(price), do: %{price: format_price(price)}, else: %{}))
case HTTPClient.signed_request_binance("/api/v3/order", arguments, :post) do
{:ok, %{"code" => code, "msg" => msg}} ->
{:error, {:binance_error, %{code: code, msg: msg}}}
data ->
data
end
end
@doc """
Creates a new **limit** **buy** order
Symbol can be a binance symbol in the form of `"ETHBTC"` or `%Binance.TradePair{}`.
Returns `{:ok, %{}}` or `{:error, reason}`
"""
def order_limit_buy(symbol, quantity, price, time_in_force \\ "GTC")
def order_limit_buy(
%Binance.TradePair{from: from, to: to} = symbol,
quantity,
price,
time_in_force
)
when is_number(quantity)
when is_number(price)
when is_binary(from)
when is_binary(to) do
case find_symbol(symbol) do
{:ok, binance_symbol} -> order_limit_buy(binance_symbol, quantity, price, time_in_force)
e -> e
end
end
def order_limit_buy(symbol, quantity, price, time_in_force)
when is_binary(symbol)
when is_number(quantity)
when is_number(price) do
create_order(symbol, "BUY", "LIMIT", quantity, price, time_in_force)
|> parse_order_response
end
@doc """
Creates a new **limit** **sell** order
Symbol can be a binance symbol in the form of `"ETHBTC"` or `%Binance.TradePair{}`.
Returns `{:ok, %{}}` or `{:error, reason}`
"""
def order_limit_sell(symbol, quantity, price, time_in_force \\ "GTC")
def order_limit_sell(
%Binance.TradePair{from: from, to: to} = symbol,
quantity,
price,
time_in_force
)
when is_number(quantity)
when is_number(price)
when is_binary(from)
when is_binary(to) do
case find_symbol(symbol) do
{:ok, binance_symbol} -> order_limit_sell(binance_symbol, quantity, price, time_in_force)
e -> e
end
end
def order_limit_sell(symbol, quantity, price, time_in_force)
when is_binary(symbol)
when is_number(quantity)
when is_number(price) do
create_order(symbol, "SELL", "LIMIT", quantity, price, time_in_force)
|> parse_order_response
end
@doc """
Creates a new **market** **buy** order
Symbol can be a binance symbol in the form of `"ETHBTC"` or `%Binance.TradePair{}`.
Returns `{:ok, %{}}` or `{:error, reason}`
"""
def order_market_buy(%Binance.TradePair{from: from, to: to} = symbol, quantity)
when is_number(quantity)
when is_binary(from)
when is_binary(to) do
case find_symbol(symbol) do
{:ok, binance_symbol} -> order_market_buy(binance_symbol, quantity)
e -> e
end
end
def order_market_buy(symbol, quantity)
when is_binary(symbol)
when is_number(quantity) do
create_order(symbol, "BUY", "MARKET", quantity)
end
@doc """
Creates a new **market** **sell** order
Symbol can be a binance symbol in the form of `"ETHBTC"` or `%Binance.TradePair{}`.
Returns `{:ok, %{}}` or `{:error, reason}`
"""
def order_market_sell(%Binance.TradePair{from: from, to: to} = symbol, quantity)
when is_number(quantity)
when is_binary(from)
when is_binary(to) do
case find_symbol(symbol) do
{:ok, binance_symbol} -> order_market_sell(binance_symbol, quantity)
e -> e
end
end
def order_market_sell(symbol, quantity)
when is_binary(symbol)
when is_number(quantity) do
create_order(symbol, "SELL", "MARKET", quantity)
end
defp parse_order_response({:ok, response}) do
{:ok, Binance.OrderResponse.new(response)}
end
defp parse_order_response({
:error,
{
:binance_error,
%{code: -2010, msg: "Account has insufficient balance for requested action."} = reason
}
}) do
{:error, %Binance.InsufficientBalanceError{reason: reason}}
end
# Misc
defp format_price(num) when is_float(num), do: :erlang.float_to_binary(num, [{:decimals, 8}])
defp format_price(num) when is_integer(num), do: inspect(num)
defp format_price(num) when is_binary(num), do: num
@doc """
Searches and normalizes the symbol as it is listed on binance.
To retrieve this information, a request to the binance API is done. The result is then **cached** to ensure the request is done only once.
Order of which symbol comes first, and case sensitivity does not matter.
Returns `{:ok, "SYMBOL"}` if successfully, or `{:error, reason}` otherwise.
## Examples
These 3 calls will result in the same result string:
```
find_symbol(%Binance.TradePair{from: "ETH", to: "REQ"})
```
```
find_symbol(%Binance.TradePair{from: "REQ", to: "ETH"})
```
```
find_symbol(%Binance.TradePair{from: "rEq", to: "eTH"})
```
Result: `{:ok, "REQETH"}`
"""
def find_symbol(%Binance.TradePair{from: from, to: to} = tp)
when is_binary(from)
when is_binary(to) do
case Binance.SymbolCache.get() do
# cache hit
{:ok, data} ->
from = String.upcase(from)
to = String.upcase(to)
found = Enum.filter(data, &Enum.member?([from <> to, to <> from], &1))
case Enum.count(found) do
1 -> {:ok, found |> List.first()}
0 -> {:error, :symbol_not_found}
end
# cache miss
{:error, :not_initialized} ->
case get_all_prices() do
{:ok, price_data} ->
price_data
|> Enum.map(fn x -> x.symbol end)
|> Binance.SymbolCache.store()
find_symbol(tp)
err ->
err
end
err ->
err
end
end
# Open orders
@doc """
Get all open orders, alternatively open orders by symbol
Returns `{:ok, [%Binance.Order{}]}` or `{:error, reason}`.
Weight: 1 for a single symbol; 40 when the symbol parameter is omitted
## Example
```
{:ok,
[%Binance.Order{price: "0.1", origQty: "1.0", executedQty: "0.0", ...},
%Binance.Order{...},
%Binance.Order{...},
%Binance.Order{...},
%Binance.Order{...},
%Binance.Order{...},
...]}
```
"""
def get_open_orders() do
api_key = Application.get_env(:binance, :api_key)
secret_key = Application.get_env(:binance, :secret_key)
case HTTPClient.get_binance("/api/v3/openOrders", %{}, secret_key, api_key) do
{:ok, data} -> {:ok, Enum.map(data, &Binance.Order.new(&1))}
err -> err
end
end
def get_open_orders(%Binance.TradePair{} = symbol) do
case find_symbol(symbol) do
{:ok, binance_symbol} -> get_open_orders(binance_symbol)
e -> e
end
end
def get_open_orders(symbol) when is_binary(symbol) do
api_key = Application.get_env(:binance, :api_key)
secret_key = Application.get_env(:binance, :secret_key)
case HTTPClient.get_binance("/api/v3/openOrders", %{:symbol => symbol}, secret_key, api_key) do
{:ok, data} -> {:ok, Enum.map(data, &Binance.Order.new(&1))}
err -> err
end
end
# Order
@doc """
Get order by symbol, timestamp and either orderId or origClientOrderId are mandatory
Returns `{:ok, [%Binance.Order{}]}` or `{:error, reason}`.
Weight: 1
## Example
```
{:ok, %Binance.Order{price: "0.1", origQty: "1.0", executedQty: "0.0", ...}}
```
Info: https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#query-order-user_data
"""
def get_order(
symbol,
timestamp,
order_id \\ nil,
orig_client_order_id \\ nil,
recv_window \\ nil
) do
case is_binary(symbol) do
true ->
fetch_order(symbol, timestamp, order_id, orig_client_order_id, recv_window)
false ->
case find_symbol(symbol) do
{:ok, binance_symbol} ->
fetch_order(binance_symbol, timestamp, order_id, orig_client_order_id, recv_window)
e ->
e
end
end
end
def fetch_order(symbol, timestamp, order_id, orig_client_order_id, recv_window)
when is_binary(symbol)
when is_integer(timestamp)
when is_integer(order_id) or is_binary(orig_client_order_id) do
api_key = Application.get_env(:binance, :api_key)
secret_key = Application.get_env(:binance, :secret_key)
arguments =
%{
symbol: symbol,
timestamp: timestamp
}
|> Map.merge(unless(is_nil(order_id), do: %{orderId: order_id}, else: %{}))
|> Map.merge(
unless(
is_nil(orig_client_order_id),
do: %{origClientOrderId: orig_client_order_id},
else: %{}
)
)
|> Map.merge(unless(is_nil(recv_window), do: %{recvWindow: recv_window}, else: %{}))
case HTTPClient.get_binance("/api/v3/order", arguments, secret_key, api_key) do
{:ok, data} -> {:ok, Binance.Order.new(data)}
err -> err
end
end
@doc """
Cancel an active order..
Symbol and either orderId or origClientOrderId must be sent.
Returns `{:ok, %Binance.Order{}}` or `{:error, reason}`.
Weight: 1
Info: https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#cancel-order-trade
"""
def cancel_order(
symbol,
timestamp,
order_id \\ nil,
orig_client_order_id \\ nil,
new_client_order_id \\ nil,
recv_window \\ nil
) do
case is_binary(symbol) do
true ->
cancel_order_(
symbol,
timestamp,
order_id,
orig_client_order_id,
new_client_order_id,
recv_window
)
false ->
case find_symbol(symbol) do
{:ok, binance_symbol} ->
cancel_order_(
binance_symbol,
timestamp,
order_id,
orig_client_order_id,
new_client_order_id,
recv_window
)
e ->
e
end
end
end
defp cancel_order_(
symbol,
timestamp,
order_id,
orig_client_order_id,
new_client_order_id,
recv_window
)
when is_binary(symbol)
when is_integer(timestamp)
when is_integer(order_id) or is_binary(orig_client_order_id) do
api_key = Application.get_env(:binance, :api_key)
secret_key = Application.get_env(:binance, :secret_key)
arguments =
%{
symbol: symbol,
timestamp: timestamp
}
|> Map.merge(unless(is_nil(order_id), do: %{orderId: order_id}, else: %{}))
|> Map.merge(
unless(
is_nil(orig_client_order_id),
do: %{origClientOrderId: orig_client_order_id},
else: %{}
)
)
|> Map.merge(
unless(is_nil(new_client_order_id),
do: %{newClientOrderId: new_client_order_id},
else: %{}
)
)
|> Map.merge(unless(is_nil(recv_window), do: %{recvWindow: recv_window}, else: %{}))
case HTTPClient.delete_binance("/api/v3/order", arguments, secret_key, api_key) do
{:ok, data} -> {:ok, Binance.Order.new(data)}
err -> err
end
end
end