diff --git a/cmd/cli/flags.go b/cmd/cli/flags.go index 6145618c..e382ada3 100644 --- a/cmd/cli/flags.go +++ b/cmd/cli/flags.go @@ -46,7 +46,7 @@ func listProviderNames() []string { *providers = "" } if *cex { - *providers += ",deribit,delta" + *providers += ",deribit,delta,thalex" } if *dex { *providers += ",lyra,synquote,zeta" diff --git a/frontend/src/api/queries.ts b/frontend/src/api/queries.ts index a53b949e..f0378ec9 100644 --- a/frontend/src/api/queries.ts +++ b/frontend/src/api/queries.ts @@ -4,7 +4,7 @@ // SPDX-License-Identifier: MIT const classicOptionsQuery = `{ - rows(providers: ["Delta Exchange", "Deribit","Lyra::Arbitrum", "Lyra::Optimism", "Synquote", "Zeta"]) { + rows(providers: ["Delta Exchange", "Deribit", "Thalex", "Lyra::Arbitrum", "Lyra::Optimism", "Synquote", "Zeta"]) { date expiry provider diff --git a/frontend/src/const/filter_presets.ts b/frontend/src/const/filter_presets.ts index 92210bf7..3b3b6b62 100644 --- a/frontend/src/const/filter_presets.ts +++ b/frontend/src/const/filter_presets.ts @@ -32,7 +32,7 @@ const filterPresets: Record = { }, 'CEXes': { assets: { 'defaultValue': true }, - providers: { 'defaultValue': false, 'Deribit': true, 'Delta Exchange': true } + providers: { 'defaultValue': false, 'Deribit': true, 'Delta Exchange': true, 'Thalex': true } }, 'DEXes': { assets: { 'defaultValue': true }, @@ -53,14 +53,14 @@ const filterPresets: Record = { 'SOL': { assets: { 'defaultValue': false, 'SOL': true }, providers: { 'defaultValue': true }, - },//TODO separate L1 & L2 + }, 'L1 tokens': { assets: { 'defaultValue': false, 'BTC': true, 'ETH': true, 'SOL': true, 'TRX': true, 'LTC': true, 'BNB': true, }, providers: { 'defaultValue': true }, - },//TODO separate L1 & L2 + }, 'L2 tokens': { assets: { 'defaultValue': false, diff --git a/pkg/provider/deribit/deribit.go b/pkg/provider/deribit/deribit.go index 4bc31795..a61361bb 100644 --- a/pkg/provider/deribit/deribit.go +++ b/pkg/provider/deribit/deribit.go @@ -16,7 +16,9 @@ import ( "github.com/teal-finance/rainbow/pkg/rainbow" ) -var log = emo.NewZone("Deribit") +const name = "Deribit" + +var log = emo.NewZone(name) const baseURL = "https://www.deribit.com/options/" @@ -25,7 +27,7 @@ type Provider struct { } func (Provider) Name() string { - return "Deribit" + return name } // adaptiveMinSleepTime to rate limit the Deribit API. @@ -41,7 +43,7 @@ const maxBytesToRead = 2_000_000 func (p *Provider) Options() ([]rainbow.Option, error) { if p.ar.Name == "" { - p.ar = garcon.NewAdaptiveRate("Deribit", adaptiveMinSleepTime) + p.ar = garcon.NewAdaptiveRate(name, adaptiveMinSleepTime) } instruments, err := p.query("BTC") @@ -88,7 +90,7 @@ func (p *Provider) query(coin string) ([]instrument, error) { const api = "https://deribit.com/api/v2/public/get_instruments?currency=" const opts = "&expired=false&kind=option" url := api + coin + opts - log.Info("Deribit " + url) + log.Info(name + url) var result instrumentsResult err := p.ar.Get(coin, url, &result, maxBytesToRead) @@ -168,7 +170,7 @@ func (p *Provider) fillOptions(instruments []instrument, depth uint32) ([]rainbo apiurl := api + instruments[i].InstrumentName if err := p.ar.Get(instruments[i].InstrumentName, apiurl, &result); err != nil { lastError = err - log.Warn("Deribit book " + err.Error()) + log.Warn(name + " book " + err.Error()) } // API doc: https://docs.deribit.com/#public-get_index_price_names diff --git a/pkg/provider/providers.go b/pkg/provider/providers.go index 640ab8d2..079debc9 100644 --- a/pkg/provider/providers.go +++ b/pkg/provider/providers.go @@ -16,6 +16,7 @@ import ( "github.com/teal-finance/rainbow/pkg/provider/lyra" "github.com/teal-finance/rainbow/pkg/provider/synquote" "github.com/teal-finance/rainbow/pkg/provider/thales" + "github.com/teal-finance/rainbow/pkg/provider/thalex" "github.com/teal-finance/rainbow/pkg/rainbow" ) @@ -26,7 +27,8 @@ func AllProviders() []rainbow.Provider { return []rainbow.Provider{ &deribit.Provider{}, lyra.Provider{}, - synquote.Provider{}, // paused cause they changed layer and I didn't get the new API link + synquote.Provider{}, + &thalex.Provider{}, // zetamarkets.Provider{}, // paused platform due to current market conditions thales.Provider{}, // Thales = exotic options -> https://teal.finance/rainbow/exotic deltaexchange.Provider{}, // last because slow (rate limit) diff --git a/pkg/provider/thalex/thalex.go b/pkg/provider/thalex/thalex.go new file mode 100644 index 00000000..d266247d --- /dev/null +++ b/pkg/provider/thalex/thalex.go @@ -0,0 +1,186 @@ +// Copyright 2023 Teal.Finance/Rainbow contributors +// This file is part of Teal.Finance/Rainbow, +// a screener for DeFi options under the MIT License. +// SPDX-License-Identifier: MIT + +package thalex + +import ( + "strings" + "time" + + "github.com/teal-finance/emo" + "github.com/teal-finance/garcon" + "github.com/teal-finance/rainbow/pkg/rainbow" +) + +var log = emo.NewZone(name) + +const baseURL = "https://thalex.com/api/v2/public/" +const name = "Thalex" + +type Provider struct { + ar garcon.AdaptiveRate +} + +func (Provider) Name() string { + return name +} + +// adaptiveMinSleepTime to rate limit the Thalex API. +// https://thalex.com/docs/#section/API-description/Message-rates +// The maximum number of matching engine messages (buy, sell, amend, delete, etc.) per connection per second is 10. +// When the connection is set to non-persistent (private/set_cancel_on_disconnect), this limit is raised to 50. +const adaptiveMinSleepTime = 25 * time.Millisecond + +// Hour at which the options expires = 8:00 UTC. +const Hour = 8 + +// maxBytesToRead prevents wasting memory/CPU when receiving an abnormally huge response from Thalex API. +// we put the same param as Deribit +const maxBytesToRead = 2_000_000 + +func (p *Provider) Options() ([]rainbow.Option, error) { + if p.ar.Name == "" { + p.ar = garcon.NewAdaptiveRate(name, adaptiveMinSleepTime) + } + + instruments, err := p.query() + if err != nil { + return nil, err + } + + options, err := p.fillOptions(instruments) + if err != nil { + return nil, err + } + return options, nil +} + +func (p *Provider) query() ([]Instrument, error) { + const api = baseURL + "instruments" + url := api + log.Info(name + " " + url) + + var result instrumentsResult + err := p.ar.Get("", url, &result, maxBytesToRead) + if err != nil { + return nil, err + } + + return result.Result, nil +} + +func (p *Provider) fillOptions(instruments []Instrument) ([]rainbow.Option, error) { + options := make([]rainbow.Option, 0, len(instruments)) + var err error + + var result tickers + + for _, i := range instruments { + if i.Type != "option" { + continue + } + apiurl := baseURL + "ticker?instrument_name=" + i.InstrumentName + if err := p.ar.Get(i.InstrumentName, apiurl, &result); err != nil { + log.Warn(name + " book " + err.Error()) + } + + asset := getUnderlying(i.Underlying) + + o := rainbow.Option{ + Name: i.InstrumentName, + Type: strings.ToUpper(i.OptionType), + UnderlyingAsset: asset, + Asset: asset, + Strike: i.StrikePrice, + Expiry: i.ExpiryDate + " 08:00:00", + ExchangeType: "CEX", + Chain: "-", + Layer: "-", + LayerName: "-", + Provider: name, + UnderlyingQuote: i.BaseCurrency, + QuoteCurrency: "USD", + URL: "https://thalex.com/exchange/options?underlying=" + i.Product + "&expiration=" + i.ExpiryDate, + OpenInterest: result.Result.OpenInterest * result.Result.Index, + MarketIV: 100 * result.Result.Iv, + } + o.Bid = append(o.Bid, rainbow.Order{ + Price: result.Result.BestBidPrice, + Size: result.Result.BestBidAmount, + }) + o.Ask = append(o.Ask, rainbow.Order{ + Price: result.Result.BestAskPrice, + Size: result.Result.BestAskAmount, + }) + + options = append(options, o) + } + + return options, err + +} +func getUnderlying(u string) string { + coin := "" + switch u { + case "ETHUSD": + coin = "ETH" + case "BTCUSD": + coin = "BTC" + default: + log.Warn(name + " unknow underlying instrument " + u) + coin = "TTT" + } + return coin +} + +type tickers struct { + Result Ticker `json:"result"` +} + +type Ticker struct { + MarkPrice float64 `json:"mark_price"` + BestBidPrice float64 `json:"best_bid_price"` + BestBidAmount float64 `json:"best_bid_amount"` + BestAskPrice float64 `json:"best_ask_price"` + BestAskAmount float64 `json:"best_ask_amount"` + LastPrice int `json:"last_price"` + Iv float64 `json:"iv"` + Delta float64 `json:"delta"` + Volume24H float64 `json:"volume_24h"` + Value24H float64 `json:"value_24h"` + LowPrice24H float64 `json:"low_price_24h"` + HighPrice24H float64 `json:"high_price_24h"` + Change24H float64 `json:"change_24h"` + Index float64 `json:"index"` + Forward float64 `json:"forward"` + Funding_mark float64 `json:"funding_mark"` + Funding_rate float64 `json:"funding_rate"` + CollarLow float64 `json:"collar_low"` + CollarHigh float64 `json:"collar_high"` + OpenInterest float64 `json:"open_interest"` +} + +type instrumentsResult struct { + Result []Instrument `json:"result"` +} + +type Instrument struct { + InstrumentName string `json:"instrument_name"` + Product string `json:"product"` + TickSize float64 `json:"tick_size"` + VolumeTickSize float64 `json:"volume_tick_size"` + Underlying string `json:"underlying"` + Type string `json:"type"` + OptionType string `json:"option_type,omitempty"` + ExpiryDate string `json:"expiry_date"` + ExpirationTimestamp int `json:"expiration_timestamp"` + StrikePrice float64 `json:"strike_price,omitempty"` + BaseCurrency string `json:"base_currency"` + CreateTime float64 `json:"create_time"` + Legs []struct { + InstrumentName string `json:"instrument_name"` + Quantity int `json:"quantity"` + } `json:"legs,omitempty"` +} diff --git a/pkg/rainbow/option.go b/pkg/rainbow/option.go index b16fe3c8..6abfc619 100644 --- a/pkg/rainbow/option.go +++ b/pkg/rainbow/option.go @@ -16,11 +16,11 @@ import "fmt" // Work on the csv type Option struct { - Name string `json:"name"` // ASSET-DATE-Strike-OptionsType - Type string `json:"type"` // CALL / PUT // TODO add exotic like binary and perp(squeeth) - Asset string `json:"asset"` // ETH, BTC, SOL, ... the crypto we are exposed to - UnderlyingAsset string `json:"underlyingasset"` // sETH, WETH, sBTC, WBTC ... the actual asset token we track - + Name string `json:"name"` // ASSET-DATE-Strike-OptionsType + Type string `json:"type"` // CALL / PUT // TODO add exotic like binary and perp(squeeth) + Asset string `json:"asset"` // ETH, BTC, SOL, ... the crypto we are exposed to + UnderlyingAsset string `json:"underlyingasset"` // sETH, WETH, sBTC, WBTC ... the actual asset token we track + Strike float64 `json:"strike"` // Option strike Expiry string `json:"expiry"` // Expiry date in Format("2006-01-02 15:04:05") ExchangeType string `json:"exchange"` // CEX / DEX Chain string `json:"chain"` // Ethereum, Solana and "–" for CEX (Deribit) @@ -37,7 +37,6 @@ type Option struct { MarketIV float64 `json:"markIV"` // When it is present on the provider, we store their Market IV // see https://corporatefinanceinstitute.com/resources/knowledge/trading-investing/option-greeks/ Greeks TheGreeks `json:"greeks"` // Greeks measure the sensitivity of an option’s price to its the underlying determining parameters. - Strike float64 `json:"strike"` // OpenInterest float64 `json:"openinterest"` // ProtocolID string `json:"protocolID"` // when present log the ID of that instrument on the provider }