Skip to content

Commit

Permalink
Start adding Delta Exchange venue adapter
Browse files Browse the repository at this point in the history
- Add products
- Add order book streaming
- Add maker taker fees
  • Loading branch information
rupurt committed Nov 17, 2021
1 parent 4d8404e commit 45be053
Show file tree
Hide file tree
Showing 20 changed files with 591 additions and 38 deletions.
28 changes: 16 additions & 12 deletions .github/workflows/code_coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,20 @@ jobs:
env:
MIX_ENV: test
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
BINANCE_API_KEY: ../../secrets/ci/binance_api_key
BINANCE_API_SECRET: ../../secrets/ci/binance_api_secret
BITMEX_API_KEY: ../../secrets/ci/bitmex_api_key
BITMEX_API_SECRET: ../../secrets/ci/bitmex_api_secret
GDAX_API_KEY: ../../secrets/ci/gdax_api_key
GDAX_API_PASSPHRASE: ../../secrets/ci/gdax_api_passphrase
GDAX_API_SECRET: ../../secrets/ci/gdax_api_secret
OKEX_API_KEY: ../../secrets/ci/okex_api_key
OKEX_API_PASSPHRASE: ../../secrets/ci/okex_api_passphrase
OKEX_API_SECRET: ../../secrets/ci/okex_api_secret
DERIBIT_CLIENT_ID: ../../secrets/ci/deribit_client_id
DERIBIT_CLIENT_SECRET: ../../secrets/ci/deribit_client_secret
BINANCE_API_KEY: ./secrets/ci/binance_api_key
BINANCE_API_SECRET: ./secrets/ci/binance_api_secret
BITMEX_API_KEY: ./secrets/ci/bitmex_api_key
BITMEX_API_SECRET: ./secrets/ci/bitmex_api_secret
GDAX_API_KEY: ./secrets/ci/gdax_api_key
GDAX_API_PASSPHRASE: ./secrets/ci/gdax_api_passphrase
GDAX_API_SECRET: ./secrets/ci/gdax_api_secret
OKEX_API_KEY: ./secrets/ci/okex_api_key
OKEX_API_PASSPHRASE: ./secrets/ci/okex_api_passphrase
OKEX_API_SECRET: ./secrets/ci/okex_api_secret
DERIBIT_CLIENT_ID: ./secrets/ci/deribit_client_id
DERIBIT_CLIENT_SECRET: ./secrets/ci/deribit_client_secret
FTX_API_KEY: ./secrets/ci/ftx_api_key
FTX_API_SECRET: ./secrets/ci/ftx_api_secret
DELTA_EXCHANGE_API_KEY: ./secrets/ci/delta_exchange_api_key
DELTA_EXCHANGE_API_SECRET: ./secrets/ci/delta_exchange_api_secret
run: mix coveralls.github --umbrella
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ jobs:
DERIBIT_CLIENT_SECRET: ./secrets/ci/deribit_client_secret
FTX_API_KEY: ./secrets/ci/ftx_api_key
FTX_API_SECRET: ./secrets/ci/ftx_api_secret
DELTA_EXCHANGE_API_KEY: ./secrets/ci/delta_exchange_api_key
DELTA_EXCHANGE_API_SECRET: ./secrets/ci/delta_exchange_api_secret
run: |
mix tai.gen.migration
mix test
Expand Down
43 changes: 22 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,28 @@ Here's an example of an advisor that logs the spread between multiple products o

## Venues In Progress

| Venue | Live Order Book | Accounts | Orders | Products | Fees |
| -------- | :-------------: | :------: | :----: | :------: | :--: |
| Binance | [x] | [x] | [ ] | [x] | [x] |
| Deribit | [x] | [x] | [ ] | [x] | [x] |
| GDAX | [x] | [x] | [ ] | [x] | [x] |
| Huobi | [x] | [ ] | [ ] | [x] | [ ] |
| Bybit | [ ] | [ ] | [ ] | [x] | [ ] |
| bit.com | [ ] | [ ] | [ ] | [ ] | [ ] |
| Bitfinex | [ ] | [ ] | [ ] | [ ] | [ ] |
| Kraken | [ ] | [ ] | [ ] | [ ] | [ ] |
| CME | [ ] | [ ] | [ ] | [ ] | [ ] |
| Phemex | [ ] | [ ] | [ ] | [ ] | [ ] |
| BTSE | [ ] | [ ] | [ ] | [ ] | [ ] |
| KuCoin | [ ] | [ ] | [ ] | [ ] | [ ] |
| BitMax | [ ] | [ ] | [ ] | [ ] | [ ] |
| Bitget | [ ] | [ ] | [ ] | [ ] | [ ] |
| MEXC | [ ] | [ ] | [ ] | [ ] | [ ] |
| PrimeXBT | [ ] | [ ] | [ ] | [ ] | [ ] |
| Gate.io | [ ] | [ ] | [ ] | [ ] | [ ] |
| Coinflex | [ ] | [ ] | [ ] | [ ] | [ ] |
| bitFlyer | [ ] | [ ] | [ ] | [ ] | [ ] |
| Venue | Live Order Book | Accounts | Orders | Products | Fees |
| ----------------- | :-------------: | :------: | :----: | :------: | :--: |
| Binance | [x] | [x] | [ ] | [x] | [x] |
| Deribit | [x] | [x] | [ ] | [x] | [x] |
| GDAX | [x] | [x] | [ ] | [x] | [x] |
| Huobi | [x] | [ ] | [ ] | [x] | [ ] |
| Delta Exchange | [x] | [ ] | [ ] | [x] | [x] |
| Bybit | [ ] | [ ] | [ ] | [x] | [ ] |
| bit.com | [ ] | [ ] | [ ] | [ ] | [ ] |
| Bitfinex | [ ] | [ ] | [ ] | [ ] | [ ] |
| Kraken | [ ] | [ ] | [ ] | [ ] | [ ] |
| CME | [ ] | [ ] | [ ] | [ ] | [ ] |
| Phemex | [ ] | [ ] | [ ] | [ ] | [ ] |
| BTSE | [ ] | [ ] | [ ] | [ ] | [ ] |
| KuCoin | [ ] | [ ] | [ ] | [ ] | [ ] |
| BitMax | [ ] | [ ] | [ ] | [ ] | [ ] |
| Bitget | [ ] | [ ] | [ ] | [ ] | [ ] |
| MEXC | [ ] | [ ] | [ ] | [ ] | [ ] |
| PrimeXBT | [ ] | [ ] | [ ] | [ ] | [ ] |
| Gate.io | [ ] | [ ] | [ ] | [ ] | [ ] |
| Coinflex | [ ] | [ ] | [ ] | [ ] | [ ] |
| bitFlyer | [ ] | [ ] | [ ] | [ ] | [ ] |

## Install

Expand Down

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions apps/tai/lib/tai/venue_adapters/delta_exchange.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
defmodule Tai.VenueAdapters.DeltaExchange do
alias Tai.VenueAdapters.DeltaExchange.{
StreamSupervisor,
Products,
Accounts,
MakerTakerFees,
# Positions,
# CreateOrder,
# CancelOrder
}

@behaviour Tai.Venues.Adapter

def stream_supervisor, do: StreamSupervisor
defdelegate products(venue_id), to: Products
defdelegate accounts(venue_id, credential_id, credentials), to: Accounts
defdelegate maker_taker_fees(venue_id, credential_id, credentials), to: MakerTakerFees
def positions(_venue_id, _credential_id, _credentials), do: {:error, :not_implemented}
def create_order(_order, _credentials), do: {:error, :not_implemented}
def amend_order(_order, _attrs, _credentials), do: {:error, :not_supported}
def amend_bulk_orders(_orders_with_attrs, _credentials), do: {:error, :not_supported}
def cancel_order(_order, _credentials), do: {:error, :not_implemented}
end
12 changes: 12 additions & 0 deletions apps/tai/lib/tai/venue_adapters/delta_exchange/accounts.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
defmodule Tai.VenueAdapters.DeltaExchange.Accounts do
def accounts(_venue_id, _credential_id, _credentials) do
# venue_credentials = struct!(ExDeltaExchange.Credentials, credentials)

# with {:ok, balances} <- ExDeltaExchange.Wallet.Balances.get(venue_credentials) do
# accounts = balances |> Enum.map(&build(&1, venue_id, credential_id))
# {:ok, accounts}
# end

{:ok, []}
end
end
13 changes: 13 additions & 0 deletions apps/tai/lib/tai/venue_adapters/delta_exchange/maker_taker_fees.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
defmodule Tai.VenueAdapters.DeltaExchange.MakerTakerFees do
def maker_taker_fees(_venue_id, _credential_id, _credentials) do
# venue_credentials = struct!(ExDeltaExchange.Credentials, credentials)

# with {:ok, account} <- ExDeltaExchange.Account.Show.get(venue_credentials) do
# maker = account.maker_fee |> Tai.Utils.Decimal.cast!()
# taker = account.taker_fee |> Tai.Utils.Decimal.cast!()
# {:ok, {maker, taker}}
# end

{:ok, nil}
end
end
57 changes: 57 additions & 0 deletions apps/tai/lib/tai/venue_adapters/delta_exchange/product.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
defmodule Tai.VenueAdapters.DeltaExchange.Product do
@type venue :: Tai.Venue.id()
@type product :: Tai.Venues.Product.t()
@type venue_product :: ExDeltaExchange.Product.t()

@spec build(venue_product, venue) :: product
def build(%ExDeltaExchange.Product{} = venue_product, venue_id) do
%Tai.Venues.Product{
venue_id: venue_id,
symbol: venue_product.symbol |> to_symbol,
venue_symbol: venue_product.symbol,
alias: nil,
base: venue_product.contract_unit_currency |> downcase_and_atom(),
quote: venue_product.quoting_asset.symbol |> downcase_and_atom(),
venue_base: venue_product.contract_unit_currency,
venue_quote: venue_product.quoting_asset.symbol,
status: venue_product |> status(),
type: venue_product |> type(),
collateral: false,
price_increment: venue_product.tick_size |> Decimal.new(),
size_increment: venue_product.tick_size |> Decimal.new(),
min_price: venue_product.tick_size |> Decimal.new(),
min_size: venue_product.tick_size |> Decimal.new(),
value: venue_product.contract_value |> Decimal.new(),
value_side: :base,
maker_fee: venue_product.maker_commission_rate |> Decimal.new(),
taker_fee: venue_product.taker_commission_rate |> Decimal.new(),
is_quanto: venue_product.is_quanto,
is_inverse: false
}
end

def to_symbol(venue_product_name), do: venue_product_name |> downcase_and_atom()

def status(venue_product) do
case venue_product.trading_status do
"operational" -> :trading
_ -> :unknown
end
end

defp type(venue_product) do
cond do
venue_product.contract_type == "call_options" -> :option
venue_product.contract_type == "put_options" -> :option
venue_product.contract_type == "futures" -> :future
venue_product.contract_type == "perpetual_futures" -> :swap
venue_product.contract_type == "interest_rate_swaps" -> :swap
venue_product.contract_type == "spreads" -> :swap
venue_product.contract_type == "spot" -> :spot
String.starts_with?(venue_product.symbol, "MV-") -> :move
true -> :unknown
end
end

defp downcase_and_atom(str), do: str |> String.downcase() |> String.to_atom()
end
10 changes: 10 additions & 0 deletions apps/tai/lib/tai/venue_adapters/delta_exchange/products.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
defmodule Tai.VenueAdapters.DeltaExchange.Products do
alias Tai.VenueAdapters.DeltaExchange

def products(venue_id) do
with {:ok, venue_products} <- ExDeltaExchange.Products.List.get() do
products = venue_products |> Enum.map(& DeltaExchange.Product.build(&1, venue_id))
{:ok, products}
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
defmodule Tai.VenueAdapters.DeltaExchange.Stream.Connection do
use Tai.Venues.Streams.ConnectionAdapter
require Logger
alias Tai.VenueAdapters.DeltaExchange.Stream

@type stream :: Tai.Venues.Stream.t()
@type venue_id :: Tai.Venue.id()
@type credential_id :: Tai.Venue.credential_id()
@type credential :: Tai.Venue.credential()

@spec start_link(
endpoint: String.t(),
stream: stream,
credential: {credential_id, credential} | nil
) :: {:ok, pid} | {:error, term}
def start_link(endpoint: endpoint, stream: stream, credential: credential) do
routes = %{
order_books: stream.venue.id |> Stream.RouteOrderBooks.to_name(),
}

state = %Tai.Venues.Streams.ConnectionAdapter.State{
venue: stream.venue.id,
routes: routes,
channels: stream.venue.channels,
credential: credential,
order_books: stream.order_books,
quote_depth: stream.venue.quote_depth,
heartbeat_interval: stream.venue.stream_heartbeat_interval,
heartbeat_timeout: stream.venue.stream_heartbeat_timeout,
opts: stream.venue.opts
}

name = process_name(stream.venue.id)
WebSockex.start_link(endpoint, __MODULE__, state, name: name)
end

@impl true
def subscribe(:init, state) do
send(self(), {:subscribe, :orderbook})
{:ok, state}
end

@subscribe_request %{
"type" => "subscribe",
"payload" => %{}
}
@order_books_chunk_count 100
@impl true
def subscribe(:orderbook, state) do
state.order_books
|> Enum.map(& &1.venue_symbol)
|> Enum.chunk_every(@order_books_chunk_count)
|> Enum.each(fn chunk_symbols ->
payload = %{
"channels" => [%{
"name" => "l2_orderbook",
"symbols" => chunk_symbols
}]
}
msg = @subscribe_request |> Map.put("payload", payload)
send(self(), {:send_msg, msg})
end)

{:ok, state}
end

@impl true
def on_msg(%{"type" => "l2_orderbook"} = msg, received_at, state) do
msg |> forward(:order_books, received_at, state)
{:ok, state}
end

@impl true
def on_msg(%{"type" => "subscriptions"}, _received_at, state) do
{:ok, state}
end

@impl true
def on_msg(%{"error" => %{"code" => code, "context" => context}}, _received_at, state) do
case code do
429 ->
%{"limit_reset_in" => limit_reset_in} = context
Logger.error "Too many requests limit_reset_in=#{limit_reset_in}"

c ->
Logger.warn "Unhandled error code=#{c}, context=#{inspect(context)}"
end

{:ok, state}
end

defp forward(msg, to, received_at, state) do
state.routes
|> Map.fetch!(to)
|> GenServer.cast({msg, received_at})
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
defmodule Tai.VenueAdapters.DeltaExchange.Stream.ProcessOrderBook do
use GenServer
alias Tai.Markets.OrderBook

defmodule State do
@type venue_id :: Tai.Venue.id()
@type product_symbol :: Tai.Venues.Product.symbol()
@type t :: %State{
venue: venue_id,
symbol: product_symbol,
table: %{optional(String.t()) => number}
}

@enforce_keys ~w[venue symbol table]a
defstruct ~w[venue symbol table]a
end

@type venue_id :: Tai.Venue.id()
@type product :: Tai.Venues.Product.t()
@type venue_symbol :: Tai.Venues.Product.venue_symbol()
@type state :: State.t()

@spec start_link(product) :: GenServer.on_start()
def start_link(product) do
state = %State{venue: product.venue_id, symbol: product.symbol, table: %{}}
name = to_name(product.venue_id, product.venue_symbol)

GenServer.start_link(__MODULE__, state, name: name)
end

@spec to_name(venue_id, venue_symbol) :: atom
def to_name(venue, symbol), do: :"#{__MODULE__}_#{venue}_#{symbol}"

@impl true
def init(state), do: {:ok, state}

@impl true
def handle_cast({:snapshot, data, received_at}, state) do
{data, received_at, state}
|> build_change_set()
|> OrderBook.replace()

{:noreply, state}
end

defp build_change_set({{bids, asks}, received_at, state}) do
normalized_bids = bids |> normalize_changes(:bid)
normalized_asks = asks |> normalize_changes(:ask)

%OrderBook.ChangeSet{
venue: state.venue,
symbol: state.symbol,
last_received_at: received_at,
changes: Enum.concat(normalized_bids, normalized_asks)
}
end

defp normalize_changes(data, :bid), do: data |> normalize_side(:bid)
defp normalize_changes(data, :ask), do: data |> normalize_side(:ask)

defp normalize_side(data, side) do
data
|> Enum.map(fn
%{"limit_price" => raw_price, "size" => size} ->
{price, _} = Float.parse(raw_price)
{:upsert, side, price, size}
end)
end
end
Loading

0 comments on commit 45be053

Please sign in to comment.