Files
gocryptotrader/exchanges/ftx/ftx.go
Scott 5ea5245afb Improvement: Subsystem separation (#664)
* Initial codes for a trade tracker

* Moving everything in a broken fashion

* Removes tradetracker. Removes some errors for subsystems

* Cleans up some subsystems, renames stuttering types. Removes some global Bot usage

* More basic subsystem renaming and file moving

* Removes engine dependency from events,ntpserver,ordermanager,comms manager

* Exports eventManager, fixes rpcserver. puts rpcserver back for now

* Removes redundant error message, further removes engine dependencies

* experimental end of day interface usage

* adds ability to build the application

* Withdraw and event manager handling

* cleans up apiserver and communications manager

* Cleans up some start/setup processes. Though should separate

* More consistency with Setup Start Stop IsRunning funcs

* Final consistency pass before testing phase

* Fixes engine tests. Fixes stop nil issue

* api server tests

* Communications manager testing

* Connection manager tests and nilsubsystem error

* End of day currencypairsyncer tests

* Adds databaseconnection/databaseconnection_test.go

* Adds withdrawal manager tests

* Deposit address testing. Moved orderbook sync first as its more important

* Adds test for event manager

* More full eventmanager testing

* Adds testfile. Enables skipped test.

* ntp manager tests

* Adds ordermanager tests, Extracts a whole new subsystem from engine and fanangles import cycles

* Adds websocket routine manager tests

* Basic portfolio manager testing

* Fixes issue with currency pair sync startup

* Fixes issue with event manager startup

* Starts the order manager before backtester starts

* Fixes fee tests. Expands testing. Doesnt fix races

* Fixes most test races

* Resolves data races

* Fixes subsystem test issues

* currency pair syncer coverage tests

* Refactors portfolio. Fixes tests. Withdraw validation

Portfolio didn't need to exist with a portfolio manager. Now the porfolio manager
is in charge how the portfolio is handled and all portfolio functions are attached
to the base instead of just exported at the package level

Withdrawal validation occurred at the exchange level when it can just be run at the
withdrawal manager level. All withdrawal requests go through that endpoint

* lint -fix

* golang lint fixes

* lints and comments everything

* Updates GCT logo, adds documentation for some subsystems

* More documentation and more logo updates

* Fixes backtesting and apiserver errors encountered

* Fixes errors and typos from reviewing

* More minor fixes

* Changes %h verb to %w

* reverbs to %s

* Humbly begins reverting to more flat engine package

The main reasoning for this is that the subsystem split doesn't make sense
in a golang environment. The subsystems are only meant to be used with engine
and so by placing them in a non-engine area, it does not work and is
inconsistent with the rest of the application's package layout.

This will begin salvaging the changes made by reverting to a flat
engine package, but maintaining the consistent designs introduced.
Further, I will look to remove any TestMains and decrease the scope
of testing to be more local and decrease the issues that have been
caused from our style of testing.

* Manages to re-flatten things. Everything is within its own file

* mini fixes

* Fixes tests and data races and lints

* Updates docs tool for engine to create filename readmes

* os -> ioutil

* remove err

* Appveyor version increase test

* Removes tCleanup as its unsupported on appveyor

* Adds stuff that I thought was in previous merge master commit

* Removes cancel from test

* Fixes really fun test-exclusive data race

* minor nit fixes

* niterinos

* docs gen

* rm;rf test

* Remove typoline. expands startstop helper. Splits apiserver

* Removes accidental folder

* Uses update instead of replace for order upsert

* addresses nits. Renames files. Regenerates documentation.

* lint and removal of comments

* Add new test for default scenario

* Fixes typo

* regen docs
2021-05-31 10:17:12 +10:00

1219 lines
40 KiB
Go

package ftx
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
)
// FTX is the overarching type across this package
type FTX struct {
exchange.Base
}
const (
ftxAPIURL = "https://ftx.com/api"
// Public endpoints
getMarkets = "/markets"
getMarket = "/markets/"
getOrderbook = "/markets/%s/orderbook?depth=%s"
getTrades = "/markets/%s/trades"
getHistoricalData = "/markets/%s/candles"
getFutures = "/futures"
getFuture = "/futures/"
getFutureStats = "/futures/%s/stats"
getFundingRates = "/funding_rates"
getIndexWeights = "/indexes/%s/weights"
getAllWalletBalances = "/wallet/all_balances"
// Authenticated endpoints
getAccountInfo = "/account"
getPositions = "/positions"
setLeverage = "/account/leverage"
getCoins = "/wallet/coins"
getBalances = "/wallet/balances"
getDepositAddress = "/wallet/deposit_address/"
getDepositHistory = "/wallet/deposits"
getWithdrawalHistory = "/wallet/withdrawals"
withdrawRequest = "/wallet/withdrawals"
getOpenOrders = "/orders"
getOrderHistory = "/orders/history"
getOpenTriggerOrders = "/conditional_orders"
getTriggerOrderTriggers = "/conditional_orders/%s/triggers"
getTriggerOrderHistory = "/conditional_orders/history"
placeOrder = "/orders"
placeTriggerOrder = "/conditional_orders"
modifyOrder = "/orders/%s/modify"
modifyOrderByClientID = "/orders/by_client_id/%s/modify"
modifyTriggerOrder = "/conditional_orders/%s/modify"
getOrderStatus = "/orders/"
getOrderStatusByClientID = "/orders/by_client_id/"
deleteOrder = "/orders/"
deleteOrderByClientID = "/orders/by_client_id/"
cancelTriggerOrder = "/conditional_orders/"
getFills = "/fills"
getFundingPayments = "/funding_payments"
getLeveragedTokens = "/lt/tokens"
getTokenInfo = "/lt/"
getLTBalances = "/lt/balances"
getLTCreations = "/lt/creations"
requestLTCreation = "/lt/%s/create"
getLTRedemptions = "/lt/redemptions"
requestLTRedemption = "/lt/%s/redeem"
getListQuotes = "/options/requests"
getMyQuotesRequests = "/options/my_requests"
createQuoteRequest = "/options/requests"
deleteQuote = "/options/requests/"
endpointQuote = "/options/requests/%s/quotes"
getMyQuotes = "/options/my_quotes"
deleteMyQuote = "/options/quotes/"
acceptQuote = "/options/quotes/%s/accept"
getOptionsInfo = "/options/account_info"
getOptionsPositions = "/options/positions"
getPublicOptionsTrades = "/options/trades"
getOptionsFills = "/options/fills"
requestOTCQuote = "/otc/quotes"
getOTCQuoteStatus = "/otc/quotes/"
acceptOTCQuote = "/otc/quotes/%s/accept"
subaccounts = "/subaccounts"
subaccountsUpdateName = "/subaccounts/update_name"
subaccountsBalance = "/subaccounts/%s/balances"
subaccountsTransfer = "/subaccounts/transfer"
// Margin Endpoints
marginBorrowRates = "/spot_margin/borrow_rates"
marginLendingRates = "/spot_margin/lending_rates"
dailyBorrowedAmounts = "/spot_margin/borrow_summary"
marginMarketInfo = "/spot_margin/market_info?market=%s"
marginBorrowHistory = "/spot_margin/borrow_history"
marginLendHistory = "/spot_margin/lending_history"
marginLendingOffers = "/spot_margin/offers"
marginLendingInfo = "/spot_margin/lending_info"
submitLendingOrder = "/spot_margin/offers"
// Other Consts
trailingStopOrderType = "trailingStop"
takeProfitOrderType = "takeProfit"
closedStatus = "closed"
spotString = "spot"
futuresString = "future"
ratePeriod = time.Second
rateLimit = 30
)
var (
errStartTimeCannotBeAfterEndTime = errors.New("start timestamp cannot be after end timestamp")
errSubaccountNameMustBeSpecified = errors.New("a subaccount name must be specified")
errSubaccountUpdateNameInvalid = errors.New("invalid subaccount old/new name")
errCoinMustBeSpecified = errors.New("a coin must be specified")
errSubaccountTransferSizeGreaterThanZero = errors.New("transfer size must be greater than 0")
errSubaccountTransferSourceDestinationMustNotBeEqual = errors.New("subaccount transfer source and destination must not be the same value")
)
// GetMarkets gets market data
func (f *FTX) GetMarkets() ([]MarketData, error) {
resp := struct {
Data []MarketData `json:"result"`
}{}
return resp.Data, f.SendHTTPRequest(exchange.RestSpot, getMarkets, &resp)
}
// GetMarket gets market data for a provided asset type
func (f *FTX) GetMarket(marketName string) (MarketData, error) {
resp := struct {
Data MarketData `json:"result"`
}{}
return resp.Data, f.SendHTTPRequest(exchange.RestSpot, getMarket+marketName,
&resp)
}
// GetOrderbook gets orderbook for a given market with a given depth (default depth 20)
func (f *FTX) GetOrderbook(marketName string, depth int64) (OrderbookData, error) {
result := struct {
Data TempOBData `json:"result"`
}{}
strDepth := "20" // If we send a zero value we get zero asks from the
// endpoint
if depth != 0 {
strDepth = strconv.FormatInt(depth, 10)
}
var resp OrderbookData
err := f.SendHTTPRequest(exchange.RestSpot, fmt.Sprintf(getOrderbook, marketName, strDepth), &result)
if err != nil {
return resp, err
}
resp.MarketName = marketName
for x := range result.Data.Asks {
resp.Asks = append(resp.Asks, OData{
Price: result.Data.Asks[x][0],
Size: result.Data.Asks[x][1],
})
}
for y := range result.Data.Bids {
resp.Bids = append(resp.Bids, OData{
Price: result.Data.Bids[y][0],
Size: result.Data.Bids[y][1],
})
}
return resp, nil
}
// GetTrades gets trades based on the conditions specified
func (f *FTX) GetTrades(marketName string, startTime, endTime, limit int64) ([]TradeData, error) {
if marketName == "" {
return nil, errors.New("a market pair must be specified")
}
params := url.Values{}
if limit != 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if startTime > 0 && endTime > 0 {
if startTime >= (endTime) {
return nil, errStartTimeCannotBeAfterEndTime
}
params.Set("start_time", strconv.FormatInt(startTime, 10))
params.Set("end_time", strconv.FormatInt(endTime, 10))
}
resp := struct {
Data []TradeData `json:"result"`
}{}
endpoint := common.EncodeURLValues(fmt.Sprintf(getTrades, marketName), params)
return resp.Data, f.SendHTTPRequest(exchange.RestSpot, endpoint, &resp)
}
// GetHistoricalData gets historical OHLCV data for a given market pair
func (f *FTX) GetHistoricalData(marketName, timeInterval, limit string, startTime, endTime time.Time) ([]OHLCVData, error) {
if marketName == "" {
return nil, errors.New("a market pair must be specified")
}
if timeInterval == "" {
return nil, errors.New("a time interval must be specified")
}
params := url.Values{}
params.Set("resolution", timeInterval)
if limit != "" {
params.Set("limit", limit)
}
if !startTime.IsZero() && !endTime.IsZero() {
if startTime.After(endTime) {
return nil, errStartTimeCannotBeAfterEndTime
}
params.Set("start_time", strconv.FormatInt(startTime.Unix(), 10))
params.Set("end_time", strconv.FormatInt(endTime.Unix(), 10))
}
resp := struct {
Data []OHLCVData `json:"result"`
}{}
endpoint := common.EncodeURLValues(fmt.Sprintf(getHistoricalData, marketName), params)
return resp.Data, f.SendHTTPRequest(exchange.RestSpot, endpoint, &resp)
}
// GetFutures gets data on futures
func (f *FTX) GetFutures() ([]FuturesData, error) {
resp := struct {
Data []FuturesData `json:"result"`
}{}
return resp.Data, f.SendHTTPRequest(exchange.RestSpot, getFutures, &resp)
}
// GetFuture gets data on a given future
func (f *FTX) GetFuture(futureName string) (FuturesData, error) {
resp := struct {
Data FuturesData `json:"result"`
}{}
return resp.Data, f.SendHTTPRequest(exchange.RestSpot, getFuture+futureName, &resp)
}
// GetFutureStats gets data on a given future's stats
func (f *FTX) GetFutureStats(futureName string) (FutureStatsData, error) {
resp := struct {
Data FutureStatsData `json:"result"`
}{}
return resp.Data, f.SendHTTPRequest(exchange.RestSpot, fmt.Sprintf(getFutureStats, futureName), &resp)
}
// GetFundingRates gets data on funding rates
func (f *FTX) GetFundingRates(startTime, endTime time.Time, future string) ([]FundingRatesData, error) {
resp := struct {
Data []FundingRatesData `json:"result"`
}{}
params := url.Values{}
if !startTime.IsZero() && !endTime.IsZero() {
if startTime.After(endTime) {
return resp.Data, errStartTimeCannotBeAfterEndTime
}
params.Set("start_time", strconv.FormatInt(startTime.Unix(), 10))
params.Set("end_time", strconv.FormatInt(endTime.Unix(), 10))
}
if future != "" {
params.Set("future", future)
}
endpoint := common.EncodeURLValues(getFundingRates, params)
return resp.Data, f.SendHTTPRequest(exchange.RestSpot, endpoint, &resp)
}
// GetIndexWeights gets index weights
func (f *FTX) GetIndexWeights(index string) (IndexWeights, error) {
var resp IndexWeights
return resp, f.SendHTTPRequest(exchange.RestSpot, fmt.Sprintf(getIndexWeights, index), &resp)
}
// SendHTTPRequest sends an unauthenticated HTTP request
func (f *FTX) SendHTTPRequest(ep exchange.URL, path string, result interface{}) error {
endpoint, err := f.API.Endpoints.GetURL(ep)
if err != nil {
return err
}
return f.SendPayload(context.Background(), &request.Item{
Method: http.MethodGet,
Path: endpoint + path,
Result: result,
Verbose: f.Verbose,
HTTPDebugging: f.HTTPDebugging,
HTTPRecording: f.HTTPRecording,
})
}
// GetMarginBorrowRates gets borrowing rates for margin trading
func (f *FTX) GetMarginBorrowRates() ([]MarginFundingData, error) {
r := struct {
Data []MarginFundingData `json:"result"`
}{}
return r.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, marginBorrowRates, nil, &r)
}
// GetMarginLendingRates gets lending rates for margin trading
func (f *FTX) GetMarginLendingRates() ([]MarginFundingData, error) {
r := struct {
Data []MarginFundingData `json:"result"`
}{}
return r.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, marginLendingRates, nil, &r)
}
// MarginDailyBorrowedAmounts gets daily borrowed amounts for margin
func (f *FTX) MarginDailyBorrowedAmounts() ([]MarginDailyBorrowStats, error) {
r := struct {
Data []MarginDailyBorrowStats `json:"result"`
}{}
return r.Data, f.SendHTTPRequest(exchange.RestSpot, dailyBorrowedAmounts, &r)
}
// GetMarginMarketInfo gets margin market data
func (f *FTX) GetMarginMarketInfo(market string) ([]MarginMarketInfo, error) {
r := struct {
Data []MarginMarketInfo `json:"result"`
}{}
return r.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, fmt.Sprintf(marginMarketInfo, market), nil, &r)
}
// GetMarginBorrowHistory gets margin borrowing history
func (f *FTX) GetMarginBorrowHistory() ([]MarginTransactionHistoryData, error) {
r := struct {
Data []MarginTransactionHistoryData `json:"result"`
}{}
return r.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, marginBorrowHistory, nil, &r)
}
// GetMarginLendingHistory gets margin lending history
func (f *FTX) GetMarginLendingHistory() ([]MarginTransactionHistoryData, error) {
r := struct {
Data []MarginTransactionHistoryData `json:"result"`
}{}
return r.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, marginLendHistory, nil, &r)
}
// GetMarginLendingOffers gets margin lending offers
func (f *FTX) GetMarginLendingOffers() ([]LendingOffersData, error) {
r := struct {
Data []LendingOffersData `json:"result"`
}{}
return r.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, marginLendingOffers, nil, &r)
}
// GetLendingInfo gets margin lending info
func (f *FTX) GetLendingInfo() ([]LendingInfoData, error) {
r := struct {
Data []LendingInfoData `json:"result"`
}{}
return r.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, marginLendingInfo, nil, &r)
}
// SubmitLendingOffer submits an offer for margin lending
func (f *FTX) SubmitLendingOffer(coin string, size, rate float64) error {
resp := struct {
Result string `json:"result"`
Success bool `json:"success"`
}{}
req := make(map[string]interface{})
req["coin"] = strings.ToUpper(coin)
req["size"] = size
req["rate"] = rate
if err := f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodPost, marginLendingOffers, req, &resp); err != nil {
return err
}
if !resp.Success {
return errors.New(resp.Result)
}
return nil
}
// GetAccountInfo gets account info
func (f *FTX) GetAccountInfo() (AccountInfoData, error) {
resp := struct {
Data AccountInfoData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getAccountInfo, nil, &resp)
}
// GetPositions gets the users positions
func (f *FTX) GetPositions() ([]PositionData, error) {
resp := struct {
Data []PositionData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getPositions, nil, &resp)
}
// ChangeAccountLeverage changes default leverage used by account
func (f *FTX) ChangeAccountLeverage(leverage float64) error {
req := make(map[string]interface{})
req["leverage"] = leverage
return f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodPost, setLeverage, req, nil)
}
// GetCoins gets coins' data in the account wallet
func (f *FTX) GetCoins() ([]WalletCoinsData, error) {
resp := struct {
Data []WalletCoinsData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getCoins, nil, &resp)
}
// GetBalances gets balances of the account
func (f *FTX) GetBalances() ([]WalletBalance, error) {
resp := struct {
Data []WalletBalance `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getBalances, nil, &resp)
}
// GetAllWalletBalances gets all wallets' balances
func (f *FTX) GetAllWalletBalances() (AllWalletBalances, error) {
resp := struct {
Data AllWalletBalances `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getAllWalletBalances, nil, &resp)
}
// FetchDepositAddress gets deposit address for a given coin
func (f *FTX) FetchDepositAddress(coin string) (DepositData, error) {
resp := struct {
Data DepositData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getDepositAddress+strings.ToUpper(coin), nil, &resp)
}
// FetchDepositHistory gets deposit history
func (f *FTX) FetchDepositHistory() ([]TransactionData, error) {
resp := struct {
Data []TransactionData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getDepositHistory, nil, &resp)
}
// FetchWithdrawalHistory gets withdrawal history
func (f *FTX) FetchWithdrawalHistory() ([]TransactionData, error) {
resp := struct {
Data []TransactionData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getWithdrawalHistory, nil, &resp)
}
// Withdraw sends a withdrawal request
func (f *FTX) Withdraw(coin, address, tag, password, code string, size float64) (TransactionData, error) {
req := make(map[string]interface{})
req["coin"] = strings.ToUpper(coin)
req["address"] = address
req["size"] = size
if code != "" {
req["code"] = code
}
if tag != "" {
req["tag"] = tag
}
if password != "" {
req["password"] = password
}
resp := struct {
Data TransactionData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodPost, withdrawRequest, req, &resp)
}
// GetOpenOrders gets open orders
func (f *FTX) GetOpenOrders(marketName string) ([]OrderData, error) {
params := url.Values{}
if marketName != "" {
params.Set("market", marketName)
}
resp := struct {
Data []OrderData `json:"result"`
}{}
endpoint := common.EncodeURLValues(getOpenOrders, params)
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, endpoint, nil, &resp)
}
// FetchOrderHistory gets order history
func (f *FTX) FetchOrderHistory(marketName string, startTime, endTime time.Time, limit string) ([]OrderData, error) {
resp := struct {
Data []OrderData `json:"result"`
}{}
params := url.Values{}
if marketName != "" {
params.Set("market", marketName)
}
if !startTime.IsZero() && !endTime.IsZero() {
if startTime.After(endTime) {
return resp.Data, errStartTimeCannotBeAfterEndTime
}
params.Set("start_time", strconv.FormatInt(startTime.Unix(), 10))
params.Set("end_time", strconv.FormatInt(endTime.Unix(), 10))
}
if limit != "" {
params.Set("limit", limit)
}
endpoint := common.EncodeURLValues(getOrderHistory, params)
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, endpoint, nil, &resp)
}
// GetOpenTriggerOrders gets trigger orders that are currently open
func (f *FTX) GetOpenTriggerOrders(marketName, orderType string) ([]TriggerOrderData, error) {
params := url.Values{}
if marketName != "" {
params.Set("market", marketName)
}
if orderType != "" {
params.Set("type", orderType)
}
resp := struct {
Data []TriggerOrderData `json:"result"`
}{}
endpoint := common.EncodeURLValues(getOpenTriggerOrders, params)
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, endpoint, nil, &resp)
}
// GetTriggerOrderTriggers gets trigger orders that are currently open
func (f *FTX) GetTriggerOrderTriggers(orderID string) ([]TriggerData, error) {
resp := struct {
Data []TriggerData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, fmt.Sprintf(getTriggerOrderTriggers, orderID), nil, &resp)
}
// GetTriggerOrderHistory gets trigger orders that are currently open
func (f *FTX) GetTriggerOrderHistory(marketName string, startTime, endTime time.Time, side, orderType, limit string) ([]TriggerOrderData, error) {
params := url.Values{}
if marketName != "" {
params.Set("market", marketName)
}
if !startTime.IsZero() && !endTime.IsZero() {
if startTime.After(endTime) {
return nil, errStartTimeCannotBeAfterEndTime
}
params.Set("start_time", strconv.FormatInt(startTime.Unix(), 10))
params.Set("end_time", strconv.FormatInt(endTime.Unix(), 10))
}
if side != "" {
params.Set("side", side)
}
if orderType != "" {
params.Set("type", orderType)
}
if limit != "" {
params.Set("limit", limit)
}
resp := struct {
Data []TriggerOrderData `json:"result"`
}{}
endpoint := common.EncodeURLValues(getTriggerOrderHistory, params)
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, endpoint, nil, &resp)
}
// Order places an order
func (f *FTX) Order(marketName, side, orderType, reduceOnly, ioc, postOnly, clientID string, price, size float64) (OrderData, error) {
req := make(map[string]interface{})
req["market"] = marketName
req["side"] = side
req["price"] = price
req["type"] = orderType
req["size"] = size
if reduceOnly != "" {
req["reduceOnly"] = reduceOnly
}
if ioc != "" {
req["ioc"] = ioc
}
if postOnly != "" {
req["postOnly"] = postOnly
}
if clientID != "" {
req["clientId"] = clientID
}
resp := struct {
Data OrderData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodPost, placeOrder, req, &resp)
}
// TriggerOrder places an order
func (f *FTX) TriggerOrder(marketName, side, orderType, reduceOnly, retryUntilFilled string, size, triggerPrice, orderPrice, trailValue float64) (TriggerOrderData, error) {
req := make(map[string]interface{})
req["market"] = marketName
req["side"] = side
req["type"] = orderType
req["size"] = size
if reduceOnly != "" {
req["reduceOnly"] = reduceOnly
}
if retryUntilFilled != "" {
req["retryUntilFilled"] = retryUntilFilled
}
if orderType == order.Stop.Lower() || orderType == "" {
req["triggerPrice"] = triggerPrice
req["orderPrice"] = orderPrice
}
if orderType == trailingStopOrderType {
req["trailValue"] = trailValue
}
if orderType == takeProfitOrderType {
req["triggerPrice"] = triggerPrice
req["orderPrice"] = orderPrice
}
resp := struct {
Data TriggerOrderData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodPost, placeTriggerOrder, req, &resp)
}
// ModifyPlacedOrder modifies a placed order
func (f *FTX) ModifyPlacedOrder(orderID, clientID string, price, size float64) (OrderData, error) {
req := make(map[string]interface{})
req["price"] = price
req["size"] = size
if clientID != "" {
req["clientID"] = clientID
}
resp := struct {
Data OrderData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodPost, fmt.Sprintf(modifyOrder, orderID), req, &resp)
}
// ModifyOrderByClientID modifies a placed order via clientOrderID
func (f *FTX) ModifyOrderByClientID(clientOrderID, clientID string, price, size float64) (OrderData, error) {
req := make(map[string]interface{})
req["price"] = price
req["size"] = size
if clientID != "" {
req["clientID"] = clientID
}
resp := struct {
Data OrderData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodPost, fmt.Sprintf(modifyOrderByClientID, clientOrderID), req, &resp)
}
// ModifyTriggerOrder modifies an existing trigger order
// Choices for ordertype include stop, trailingStop, takeProfit
func (f *FTX) ModifyTriggerOrder(orderID, orderType string, size, triggerPrice, orderPrice, trailValue float64) (TriggerOrderData, error) {
req := make(map[string]interface{})
req["size"] = size
if orderType == order.Stop.Lower() || orderType == "" {
req["triggerPrice"] = triggerPrice
req["orderPrice"] = orderPrice
}
if orderType == trailingStopOrderType {
req["trailValue"] = trailValue
}
if orderType == takeProfitOrderType {
req["triggerPrice"] = triggerPrice
req["orderPrice"] = orderPrice
}
resp := struct {
Data TriggerOrderData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodPost, fmt.Sprintf(modifyTriggerOrder, orderID), req, &resp)
}
// GetOrderStatus gets the order status of a given orderID
func (f *FTX) GetOrderStatus(orderID string) (OrderData, error) {
resp := struct {
Data OrderData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getOrderStatus+orderID, nil, &resp)
}
// GetOrderStatusByClientID gets the order status of a given clientOrderID
func (f *FTX) GetOrderStatusByClientID(clientOrderID string) (OrderData, error) {
resp := struct {
Data OrderData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getOrderStatusByClientID+clientOrderID, nil, &resp)
}
// DeleteOrder deletes an order
func (f *FTX) DeleteOrder(orderID string) (string, error) {
resp := struct {
Result string `json:"result"`
Success bool `json:"success"`
}{}
if err := f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodDelete, deleteOrder+orderID, nil, &resp); err != nil {
return "", err
}
if !resp.Success {
return resp.Result, errors.New("delete order request by ID unsuccessful")
}
return resp.Result, nil
}
// DeleteOrderByClientID deletes an order
func (f *FTX) DeleteOrderByClientID(clientID string) (string, error) {
resp := struct {
Result string `json:"result"`
Success bool `json:"success"`
}{}
if err := f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodDelete, deleteOrderByClientID+clientID, nil, &resp); err != nil {
return "", err
}
if !resp.Success {
return resp.Result, errors.New("delete order request by client ID unsuccessful")
}
return resp.Result, nil
}
// DeleteTriggerOrder deletes an order
func (f *FTX) DeleteTriggerOrder(orderID string) (string, error) {
resp := struct {
Result string `json:"result"`
Success bool `json:"success"`
}{}
if err := f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodDelete, cancelTriggerOrder+orderID, nil, &resp); err != nil {
return "", err
}
if !resp.Success {
return resp.Result, errors.New("delete trigger order request unsuccessful")
}
return resp.Result, nil
}
// GetFills gets fills' data
func (f *FTX) GetFills(market, limit string, startTime, endTime time.Time) ([]FillsData, error) {
resp := struct {
Data []FillsData `json:"result"`
}{}
params := url.Values{}
if market != "" {
params.Set("market", market)
}
if limit != "" {
params.Set("limit", limit)
}
if !startTime.IsZero() && !endTime.IsZero() {
if startTime.After(endTime) {
return resp.Data, errStartTimeCannotBeAfterEndTime
}
params.Set("start_time", strconv.FormatInt(startTime.Unix(), 10))
params.Set("end_time", strconv.FormatInt(endTime.Unix(), 10))
}
endpoint := common.EncodeURLValues(getFills, params)
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, endpoint, nil, &resp)
}
// GetFundingPayments gets funding payments
func (f *FTX) GetFundingPayments(startTime, endTime time.Time, future string) ([]FundingPaymentsData, error) {
resp := struct {
Data []FundingPaymentsData `json:"result"`
}{}
params := url.Values{}
if !startTime.IsZero() && !endTime.IsZero() {
if startTime.After(endTime) {
return resp.Data, errStartTimeCannotBeAfterEndTime
}
params.Set("start_time", strconv.FormatInt(startTime.Unix(), 10))
params.Set("end_time", strconv.FormatInt(endTime.Unix(), 10))
}
if future != "" {
params.Set("future", future)
}
endpoint := common.EncodeURLValues(getFundingPayments, params)
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, endpoint, nil, &resp)
}
// ListLeveragedTokens lists leveraged tokens
func (f *FTX) ListLeveragedTokens() ([]LeveragedTokensData, error) {
resp := struct {
Data []LeveragedTokensData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getLeveragedTokens, nil, &resp)
}
// GetTokenInfo gets token info
func (f *FTX) GetTokenInfo(tokenName string) ([]LeveragedTokensData, error) {
resp := struct {
Data []LeveragedTokensData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getTokenInfo+tokenName, nil, &resp)
}
// ListLTBalances gets leveraged tokens' balances
func (f *FTX) ListLTBalances() ([]LTBalanceData, error) {
resp := struct {
Data []LTBalanceData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getLTBalances, nil, &resp)
}
// ListLTCreations lists the leveraged tokens' creation requests
func (f *FTX) ListLTCreations() ([]LTCreationData, error) {
resp := struct {
Data []LTCreationData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getLTCreations, nil, &resp)
}
// RequestLTCreation sends a request to create a leveraged token
func (f *FTX) RequestLTCreation(tokenName string, size float64) (RequestTokenCreationData, error) {
req := make(map[string]interface{})
req["size"] = size
resp := struct {
Data RequestTokenCreationData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodPost, fmt.Sprintf(requestLTCreation, tokenName), req, &resp)
}
// ListLTRedemptions lists the leveraged tokens' redemption requests
func (f *FTX) ListLTRedemptions() ([]LTRedemptionData, error) {
resp := struct {
Data []LTRedemptionData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getLTRedemptions, nil, &resp)
}
// RequestLTRedemption sends a request to redeem a leveraged token
func (f *FTX) RequestLTRedemption(tokenName string, size float64) (LTRedemptionRequestData, error) {
req := make(map[string]interface{})
req["size"] = size
resp := struct {
Data LTRedemptionRequestData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodPost, fmt.Sprintf(requestLTRedemption, tokenName), req, &resp)
}
// GetQuoteRequests gets a list of quote requests
func (f *FTX) GetQuoteRequests() ([]QuoteRequestData, error) {
resp := struct {
Data []QuoteRequestData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getListQuotes, nil, &resp)
}
// GetYourQuoteRequests gets a list of your quote requests
func (f *FTX) GetYourQuoteRequests() ([]PersonalQuotesData, error) {
resp := struct {
Data []PersonalQuotesData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getMyQuotesRequests, nil, &resp)
}
// CreateQuoteRequest sends a request to create a quote
func (f *FTX) CreateQuoteRequest(underlying, optionType, side string, expiry int64, requestExpiry string, strike, size, limitPrice, counterParyID float64, hideLimitPrice bool) (CreateQuoteRequestData, error) {
req := make(map[string]interface{})
req["underlying"] = strings.ToUpper(underlying)
req["type"] = optionType
req["side"] = side
req["strike"] = strike
req["expiry"] = expiry
req["size"] = size
if limitPrice != 0 {
req["limitPrice"] = limitPrice
}
if requestExpiry != "" {
req["requestExpiry"] = requestExpiry
}
if counterParyID != 0 {
req["counterParyID"] = counterParyID
}
req["hideLimitPrice"] = hideLimitPrice
resp := struct {
Data CreateQuoteRequestData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodPost, createQuoteRequest, req, &resp)
}
// DeleteQuote sends request to cancel a quote
func (f *FTX) DeleteQuote(requestID string) (CancelQuoteRequestData, error) {
resp := struct {
Data CancelQuoteRequestData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodDelete, deleteQuote+requestID, nil, &resp)
}
// GetQuotesForYourQuote gets a list of quotes for your quote
func (f *FTX) GetQuotesForYourQuote(requestID string) (QuoteForQuoteData, error) {
var resp QuoteForQuoteData
return resp, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, fmt.Sprintf(endpointQuote, requestID), nil, &resp)
}
// MakeQuote makes a quote for a quote
func (f *FTX) MakeQuote(requestID, price string) ([]QuoteForQuoteData, error) {
params := url.Values{}
params.Set("price", price)
resp := struct {
Data []QuoteForQuoteData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodPost, fmt.Sprintf(endpointQuote, requestID), nil, &resp)
}
// MyQuotes gets a list of my quotes for quotes
func (f *FTX) MyQuotes() ([]QuoteForQuoteData, error) {
resp := struct {
Data []QuoteForQuoteData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getMyQuotes, nil, &resp)
}
// DeleteMyQuote deletes my quote for quotes
func (f *FTX) DeleteMyQuote(quoteID string) ([]QuoteForQuoteData, error) {
resp := struct {
Data []QuoteForQuoteData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodDelete, deleteMyQuote+quoteID, nil, &resp)
}
// AcceptQuote accepts the quote for quote
func (f *FTX) AcceptQuote(quoteID string) ([]QuoteForQuoteData, error) {
resp := struct {
Data []QuoteForQuoteData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodPost, fmt.Sprintf(acceptQuote, quoteID), nil, &resp)
}
// GetAccountOptionsInfo gets account's options' info
func (f *FTX) GetAccountOptionsInfo() (AccountOptionsInfoData, error) {
resp := struct {
Data AccountOptionsInfoData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getOptionsInfo, nil, &resp)
}
// GetOptionsPositions gets options' positions
func (f *FTX) GetOptionsPositions() ([]OptionsPositionsData, error) {
resp := struct {
Data []OptionsPositionsData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getOptionsPositions, nil, &resp)
}
// GetPublicOptionsTrades gets options' trades from public
func (f *FTX) GetPublicOptionsTrades(startTime, endTime time.Time, limit string) ([]OptionsTradesData, error) {
params := url.Values{}
if !startTime.IsZero() && !endTime.IsZero() {
if startTime.After(endTime) {
return nil, errStartTimeCannotBeAfterEndTime
}
params.Set("start_time", strconv.FormatInt(startTime.Unix(), 10))
params.Set("end_time", strconv.FormatInt(endTime.Unix(), 10))
}
if limit != "" {
params.Set("limit", limit)
}
resp := struct {
Data []OptionsTradesData `json:"result"`
}{}
endpoint := common.EncodeURLValues(getPublicOptionsTrades, params)
return resp.Data, f.SendHTTPRequest(exchange.RestSpot, endpoint, &resp)
}
// GetOptionsFills gets fills data for options
func (f *FTX) GetOptionsFills(startTime, endTime time.Time, limit string) ([]OptionFillsData, error) {
resp := struct {
Data []OptionFillsData `json:"result"`
}{}
req := make(map[string]interface{})
if !startTime.IsZero() && !endTime.IsZero() {
req["start_time"] = strconv.FormatInt(startTime.Unix(), 10)
req["end_time"] = strconv.FormatInt(endTime.Unix(), 10)
if startTime.After(endTime) {
return resp.Data, errStartTimeCannotBeAfterEndTime
}
}
if limit != "" {
req["limit"] = limit
}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getOptionsFills, req, &resp)
}
// SendAuthHTTPRequest sends an authenticated request
func (f *FTX) SendAuthHTTPRequest(ep exchange.URL, method, path string, data, result interface{}) error {
endpoint, err := f.API.Endpoints.GetURL(ep)
if err != nil {
return err
}
ts := strconv.FormatInt(time.Now().UnixNano()/1000000, 10)
var body io.Reader
var hmac, payload []byte
if data != nil {
payload, err = json.Marshal(data)
if err != nil {
return err
}
body = bytes.NewBuffer(payload)
sigPayload := ts + method + "/api" + path + string(payload)
hmac = crypto.GetHMAC(crypto.HashSHA256, []byte(sigPayload), []byte(f.API.Credentials.Secret))
} else {
sigPayload := ts + method + "/api" + path
hmac = crypto.GetHMAC(crypto.HashSHA256, []byte(sigPayload), []byte(f.API.Credentials.Secret))
}
headers := make(map[string]string)
headers["FTX-KEY"] = f.API.Credentials.Key
headers["FTX-SIGN"] = crypto.HexEncodeToString(hmac)
headers["FTX-TS"] = ts
headers["Content-Type"] = "application/json"
return f.SendPayload(context.Background(), &request.Item{
Method: method,
Path: endpoint + path,
Headers: headers,
Body: body,
Result: result,
AuthRequest: true,
Verbose: f.Verbose,
HTTPDebugging: f.HTTPDebugging,
HTTPRecording: f.HTTPRecording,
})
}
// GetFee returns an estimate of fee based on type of transaction
func (f *FTX) GetFee(feeBuilder *exchange.FeeBuilder) (float64, error) {
var fee float64
switch feeBuilder.FeeType {
case exchange.OfflineTradeFee:
fee = getOfflineTradeFee(feeBuilder)
default:
feeData, err := f.GetAccountInfo()
if err != nil {
return 0, err
}
switch feeBuilder.IsMaker {
case true:
fee = feeData.MakerFee * feeBuilder.Amount * feeBuilder.PurchasePrice
case false:
fee = feeData.TakerFee * feeBuilder.Amount * feeBuilder.PurchasePrice
}
if fee < 0 {
fee = 0
}
}
return fee, nil
}
// getOfflineTradeFee calculates the worst case-scenario trading fee
func getOfflineTradeFee(feeBuilder *exchange.FeeBuilder) float64 {
if feeBuilder.IsMaker {
return 0.0002 * feeBuilder.PurchasePrice * feeBuilder.Amount
}
return 0.0007 * feeBuilder.PurchasePrice * feeBuilder.Amount
}
func (f *FTX) compatibleOrderVars(orderSide, orderStatus, orderType string, amount, filledAmount, avgFillPrice float64) (OrderVars, error) {
var resp OrderVars
switch orderSide {
case order.Buy.Lower():
resp.Side = order.Buy
case order.Sell.Lower():
resp.Side = order.Sell
}
switch orderStatus {
case strings.ToLower(order.New.String()):
resp.Status = order.New
case strings.ToLower(order.Open.String()):
resp.Status = order.Open
case closedStatus:
if filledAmount != 0 && filledAmount != amount {
resp.Status = order.PartiallyCancelled
}
if filledAmount == 0 {
resp.Status = order.Cancelled
}
if filledAmount == amount {
resp.Status = order.Filled
}
}
var feeBuilder exchange.FeeBuilder
feeBuilder.PurchasePrice = avgFillPrice
feeBuilder.Amount = amount
resp.OrderType = order.Market
if strings.EqualFold(orderType, order.Limit.String()) {
resp.OrderType = order.Limit
feeBuilder.IsMaker = true
}
fee, err := f.GetFee(&feeBuilder)
if err != nil {
return resp, err
}
resp.Fee = fee
return resp, nil
}
// RequestForQuotes requests for otc quotes
func (f *FTX) RequestForQuotes(base, quote string, amount float64) (RequestQuoteData, error) {
resp := struct {
Data RequestQuoteData `json:"result"`
}{}
req := make(map[string]interface{})
req["fromCoin"] = strings.ToUpper(base)
req["toCoin"] = strings.ToUpper(quote)
req["size"] = amount
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodPost, requestOTCQuote, req, &resp)
}
// GetOTCQuoteStatus gets quote status of a quote
func (f *FTX) GetOTCQuoteStatus(marketName, quoteID string) ([]QuoteStatusData, error) {
resp := struct {
Data []QuoteStatusData `json:"result"`
}{}
params := url.Values{}
params.Set("market", marketName)
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getOTCQuoteStatus+quoteID, params, &resp)
}
// AcceptOTCQuote requests for otc quotes
func (f *FTX) AcceptOTCQuote(quoteID string) error {
return f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodPost, fmt.Sprintf(acceptOTCQuote, quoteID), nil, nil)
}
// GetSubaccounts returns the users subaccounts
func (f *FTX) GetSubaccounts() ([]Subaccount, error) {
resp := struct {
Data []Subaccount `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, subaccounts, nil, &resp)
}
// CreateSubaccount creates a new subaccount
func (f *FTX) CreateSubaccount(name string) (*Subaccount, error) {
if name == "" {
return nil, errSubaccountNameMustBeSpecified
}
d := make(map[string]string)
d["nickname"] = name
resp := struct {
Data Subaccount `json:"result"`
}{}
if err := f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodPost, subaccounts, d, &resp); err != nil {
return nil, err
}
return &resp.Data, nil
}
// UpdateSubaccountName updates an existing subaccount name
func (f *FTX) UpdateSubaccountName(oldName, newName string) (*Subaccount, error) {
if oldName == "" || newName == "" || oldName == newName {
return nil, errSubaccountUpdateNameInvalid
}
d := make(map[string]string)
d["nickname"] = oldName
d["newNickname"] = newName
resp := struct {
Data Subaccount `json:"result"`
}{}
if err := f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodPost, subaccountsUpdateName, d, &resp); err != nil {
return nil, err
}
return &resp.Data, nil
}
// DeleteSubaccount deletes the specified subaccount name
func (f *FTX) DeleteSubaccount(name string) error {
if name == "" {
return errSubaccountNameMustBeSpecified
}
d := make(map[string]string)
d["nickname"] = name
resp := struct {
Data Subaccount `json:"result"`
}{}
return f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodDelete, subaccounts, d, &resp)
}
// SubaccountBalances returns the user's subaccount balances
func (f *FTX) SubaccountBalances(name string) ([]SubaccountBalance, error) {
if name == "" {
return nil, errSubaccountNameMustBeSpecified
}
resp := struct {
Data []SubaccountBalance `json:"result"`
}{}
if err := f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, fmt.Sprintf(subaccountsBalance, name), nil, &resp); err != nil {
return nil, err
}
return resp.Data, nil
}
// SubaccountTransfer transfers a desired coin to the specified subaccount
func (f *FTX) SubaccountTransfer(coin currency.Code, source, destination string, size float64) (*SubaccountTransferStatus, error) {
if coin.IsEmpty() {
return nil, errCoinMustBeSpecified
}
if size <= 0 {
return nil, errSubaccountTransferSizeGreaterThanZero
}
if source == destination {
return nil, errSubaccountTransferSourceDestinationMustNotBeEqual
}
d := make(map[string]interface{})
d["coin"] = coin.Upper().String()
d["size"] = size
if source == "" {
source = "main"
}
d["source"] = source
if destination == "" {
destination = "main"
}
d["destination"] = destination
resp := struct {
Data SubaccountTransferStatus `json:"result"`
}{}
if err := f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodPost, subaccountsTransfer, d, &resp); err != nil {
return nil, err
}
return &resp.Data, nil
}