FTX: Add REST subaccount support (#653)

* FTX: Add REST API subaccount support

* Add API key check to GetSubaccounts

* Fix missing comment and expand to rest of the codebase

* Address glorious nits

* Address various nits

* Fix ZB typo

https://www.zb.com/api#hsptccieyyqomlp
This commit is contained in:
Adrian Gallagher
2021-03-29 16:06:30 +11:00
committed by GitHub
parent fe3d0e9ed1
commit 2855e68bac
16 changed files with 296 additions and 29 deletions

View File

@@ -39,7 +39,8 @@ var (
// ErrNilArguments is a common error response to highlight that nils were passed in
// when they should not have been
ErrNilArguments = errors.New("received nil argument(s)")
ErrNilEvent = errors.New("nil event received")
// ErrNilEvent is a common error for whenever a nil event occurs when it shouldn't have
ErrNilEvent = errors.New("nil event received")
)
// EventHandler interface implements required GetTime() & Pair() return

View File

@@ -2,6 +2,7 @@ package base
import "errors"
// Error vars related to strategies and invalid config settings
var (
ErrCustomSettingsUnsupported = errors.New("custom settings not supported")
ErrSimultaneousProcessingNotSupported = errors.New("does not support simultaneous processing and could not be loaded")

View File

@@ -46,7 +46,7 @@ type Storage struct {
// FiatExchangeMarkets defines an interface to access FX data for fiat
// currency rates
fiatExchangeMarkets *forexprovider.ForexProviders
// CurrencyAnalysis defines a full market analysis suite to receieve and
// CurrencyAnalysis defines a full market analysis suite to receive and
// define different fiat currencies, cryptocurrencies and markets
currencyAnalysis *coinmarketcap.Coinmarketcap
// Path defines the main folder to dump and find currency JSON

View File

@@ -73,21 +73,21 @@ func TestHoldings(t *testing.T) {
}
if u.Accounts[0].ID != "1337" {
t.Errorf("expecting 1337 but receieved %s", u.Accounts[0].ID)
t.Errorf("expecting 1337 but received %s", u.Accounts[0].ID)
}
if u.Accounts[0].Currencies[0].CurrencyName != currency.BTC {
t.Errorf("expecting BTC but receieved %s",
t.Errorf("expecting BTC but received %s",
u.Accounts[0].Currencies[0].CurrencyName)
}
if u.Accounts[0].Currencies[0].TotalValue != 100 {
t.Errorf("expecting 100 but receieved %f",
t.Errorf("expecting 100 but received %f",
u.Accounts[0].Currencies[0].TotalValue)
}
if u.Accounts[0].Currencies[0].Hold != 20 {
t.Errorf("expecting 20 but receieved %f",
t.Errorf("expecting 20 but received %f",
u.Accounts[0].Currencies[0].Hold)
}

View File

@@ -7,6 +7,7 @@ import (
)
var (
// ErrNotSupported is an error for an unsupported asset type
ErrNotSupported = errors.New("received unsupported asset type")
)

View File

@@ -2459,12 +2459,12 @@ func TestSetExchangeOrderExecutionLimits(t *testing.T) {
err = limit.Conforms(0.000001, 0.1, order.Limit)
if !errors.Is(err, order.ErrAmountBelowMin) {
t.Fatalf("expected %v, but receieved %v", order.ErrAmountBelowMin, err)
t.Fatalf("expected %v, but received %v", order.ErrAmountBelowMin, err)
}
err = limit.Conforms(0.01, 1, order.Limit)
if !errors.Is(err, order.ErrPriceBelowMin) {
t.Fatalf("expected %v, but receieved %v", order.ErrPriceBelowMin, err)
t.Fatalf("expected %v, but received %v", order.ErrPriceBelowMin, err)
}
}

View File

@@ -15,6 +15,7 @@ import (
"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"
@@ -90,6 +91,10 @@ const (
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"
@@ -114,7 +119,12 @@ const (
)
var (
errStartTimeCannotBeAfterEndTime = errors.New("start timestamp cannot be after end timestamp")
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
@@ -403,17 +413,17 @@ func (f *FTX) GetCoins() ([]WalletCoinsData, error) {
}
// GetBalances gets balances of the account
func (f *FTX) GetBalances() ([]BalancesData, error) {
func (f *FTX) GetBalances() ([]WalletBalance, error) {
resp := struct {
Data []BalancesData `json:"result"`
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() (AllWalletAccountData, error) {
func (f *FTX) GetAllWalletBalances() (AllWalletBalances, error) {
resp := struct {
Data AllWalletAccountData `json:"result"`
Data AllWalletBalances `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getAllWalletBalances, nil, &resp)
}
@@ -1105,3 +1115,104 @@ func (f *FTX) GetOTCQuoteStatus(marketName, quoteID string) ([]QuoteStatusData,
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
}
// DeleteSubaccountName 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
}

View File

@@ -1,6 +1,7 @@
package ftx
import (
"errors"
"log"
"os"
"reflect"
@@ -1450,3 +1451,126 @@ func TestParsingWSOBData2(t *testing.T) {
t.Error(err)
}
}
func TestGetSubaccounts(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() {
t.Skip("skipping test, api keys not set")
}
_, err := f.GetSubaccounts()
if err != nil {
t.Error(err)
}
}
func TestCreateSubaccount(t *testing.T) {
t.Parallel()
_, err := f.CreateSubaccount("")
if !errors.Is(err, errSubaccountNameMustBeSpecified) {
t.Errorf("expected %v, but received: %s", errSubaccountNameMustBeSpecified, err)
}
if !areTestAPIKeysSet() || !canManipulateRealOrders {
t.Skip("skipping test, either api keys or canManipulateRealOrders isn't set")
}
_, err = f.CreateSubaccount("subzero")
if err != nil {
t.Fatal(err)
}
if err = f.DeleteSubaccount("subzero"); err != nil {
t.Error(err)
}
}
func TestUpdateSubaccountName(t *testing.T) {
t.Parallel()
_, err := f.UpdateSubaccountName("", "")
if !errors.Is(err, errSubaccountUpdateNameInvalid) {
t.Errorf("expected %v, but received: %s", errSubaccountUpdateNameInvalid, err)
}
if !areTestAPIKeysSet() || !canManipulateRealOrders {
t.Skip("skipping test, either api keys or canManipulateRealOrders isn't set")
}
_, err = f.CreateSubaccount("subzero")
if err != nil {
t.Fatal(err)
}
_, err = f.UpdateSubaccountName("subzero", "bizzlebot")
if err != nil {
t.Fatal(err)
}
if err := f.DeleteSubaccount("bizzlebot"); err != nil {
t.Error(err)
}
}
func TestDeleteSubaccountName(t *testing.T) {
t.Parallel()
if err := f.DeleteSubaccount(""); !errors.Is(err, errSubaccountNameMustBeSpecified) {
t.Errorf("expected %v, but received: %s", errSubaccountNameMustBeSpecified, err)
}
if !areTestAPIKeysSet() || !canManipulateRealOrders {
t.Skip("skipping test, either api keys or canManipulateRealOrders isn't set")
}
_, err := f.CreateSubaccount("subzero")
if err != nil {
t.Fatal(err)
}
if err := f.DeleteSubaccount("subzero"); err != nil {
t.Error(err)
}
}
func TestSubaccountBalances(t *testing.T) {
t.Parallel()
_, err := f.SubaccountBalances("")
if !errors.Is(err, errSubaccountNameMustBeSpecified) {
t.Errorf("expected %s, but received: %s", errSubaccountNameMustBeSpecified, err)
}
if !areTestAPIKeysSet() {
t.Skip("skipping test, api keys not set")
}
_, err = f.SubaccountBalances("non-existent")
if err == nil {
t.Error("expecting non-existent subaccount to return an error")
}
_, err = f.CreateSubaccount("subzero")
if err != nil {
t.Fatal(err)
}
_, err = f.SubaccountBalances("subzero")
if err != nil {
t.Error(err)
}
if err := f.DeleteSubaccount("subzero"); err != nil {
t.Error(err)
}
}
func TestSubaccountTransfer(t *testing.T) {
tt := []struct {
Coin currency.Code
Source string
Destination string
Size float64
ErrExpected error
}{
{ErrExpected: errCoinMustBeSpecified},
{Coin: currency.BTC, ErrExpected: errSubaccountTransferSizeGreaterThanZero},
{Coin: currency.BTC, Size: 420, ErrExpected: errSubaccountTransferSourceDestinationMustNotBeEqual},
}
for x := range tt {
_, err := f.SubaccountTransfer(tt[x].Coin, tt[x].Source, tt[x].Destination, tt[x].Size)
if !errors.Is(err, tt[x].ErrExpected) {
t.Errorf("expected %s, but received: %s", tt[x].ErrExpected, err)
}
}
if !areTestAPIKeysSet() || !canManipulateRealOrders {
t.Skip("skipping test, either api keys or canManipulateRealOrders isn't set")
}
_, err := f.SubaccountTransfer(currency.BTC, "", "test", 0.1)
if err != nil {
t.Error(err)
}
}

View File

@@ -234,18 +234,18 @@ type WalletCoinsData struct {
Name string `json:"name"`
}
// BalancesData stores balances data
type BalancesData struct {
Coin string `json:"coin"`
Free float64 `json:"free"`
Total float64 `json:"total"`
// WalletBalance stores balances data
type WalletBalance struct {
Coin string `json:"coin"`
Free float64 `json:"free"`
Total float64 `json:"total"`
AvailableWithoutBorrow float64 `json:"availableWithoutBorrow"`
USDValue float64 `json:"usdValue"`
SpotBorrow float64 `json:"spotBorrow"`
}
// AllWalletAccountData stores account data on all WalletCoins
type AllWalletAccountData struct {
Main []BalancesData `json:"main"`
BattleRoyale []BalancesData `json:"Battle Royale"`
}
// AllWalletBalances stores all the user's account balances
type AllWalletBalances map[string][]WalletBalance
// DepositData stores deposit address data
type DepositData struct {
@@ -771,3 +771,31 @@ type QuoteStatusData struct {
type AcceptQuote struct {
Success bool `json:"success"`
}
// Subaccount stores subaccount data
type Subaccount struct {
Nickname string `json:"nickname"`
Special bool `json:"special"`
Deletable bool `json:"deletable"`
Editable bool `json:"editable"`
Competition bool `json:"competition"`
}
// SubaccountBalance stores the user's subaccount balance
type SubaccountBalance struct {
Coin string `json:"coin"`
Free float64 `json:"free"`
Total float64 `json:"total"`
SpotBorrow float64 `json:"spotBorrow"`
AvailableWithoutBorrow float64 `json:"availableWithoutBorrow"`
}
// SubaccountTransferStatus stores the subaccount transfer details
type SubaccountTransferStatus struct {
ID int64 `json:"id"`
Coin string `json:"coin"`
Size float64 `json:"size"`
Time time.Time `json:"time"`
Notes string `json:"notes"`
Status string `json:"status"`
}

View File

@@ -444,7 +444,7 @@ func (h *IntervalRangeHolder) VerifyResultsHaveData(c []Candle) error {
return nil
}
// Set is a simple helper function to set the time twice
// CreateIntervalTime is a simple helper function to set the time twice
func CreateIntervalTime(tt time.Time) IntervalTime {
return IntervalTime{
Time: tt,

View File

@@ -39,6 +39,7 @@ const (
)
var (
// ErrMissingCandleData is an error for missing candle data
ErrMissingCandleData = errors.New("missing candle data")
// SupportedIntervals is a list of all supported intervals
SupportedIntervals = []Interval{

View File

@@ -534,7 +534,7 @@ type BatchOrderData struct {
Status string `json:"status"`
OrderTag string `json:"order_tag"`
OrderID string `json:"order_id"`
DateTimeReceived string `json:"dateTimeReceieved"`
DateTimeReceived string `json:"dateTimeReceived"`
OrderEvents []struct {
OrderPlaced FuturesOrderData `json:"orderPlaced"`
ReduceOnly bool `json:"reduceOnly"`

View File

@@ -859,7 +859,7 @@ func TestOrderBookPartialChecksumCalculator(t *testing.T) {
calculatedChecksum := o.CalculatePartialOrderbookChecksum(&dataResponse)
if calculatedChecksum != dataResponse.Checksum {
t.Errorf("Expected %v, Receieved %v", dataResponse.Checksum, calculatedChecksum)
t.Errorf("Expected %v, received %v", dataResponse.Checksum, calculatedChecksum)
}
}

View File

@@ -1672,7 +1672,7 @@ func TestOrderBookPartialChecksumCalculator(t *testing.T) {
calculatedChecksum := o.CalculatePartialOrderbookChecksum(&dataResponse)
if calculatedChecksum != dataResponse.Checksum {
t.Errorf("Expected %v, Receieved %v", dataResponse.Checksum, calculatedChecksum)
t.Errorf("Expected %v, received %v", dataResponse.Checksum, calculatedChecksum)
}
}

View File

@@ -436,7 +436,7 @@ func (z *ZB) Withdraw(currency, address, safepassword string, amount, fees float
vals.Set("fees", strconv.FormatFloat(fees, 'f', -1, 64))
vals.Set("itransfer", strconv.FormatBool(itransfer))
vals.Set("method", "withdraw")
vals.Set("recieveAddr", address)
vals.Set("receiveAddr", address)
vals.Set("safePwd", safepassword)
var resp response

View File

@@ -254,7 +254,7 @@ func TestValidateFiat(t *testing.T) {
err := test.request.Validate(test.validate)
if err != nil {
if test.output.(error).Error() != err.Error() {
t.Fatalf("Test Name %s expecting error [%s] but receieved [%s]", test.name, test.output.(error).Error(), err)
t.Fatalf("Test Name %s expecting error [%s] but received [%s]", test.name, test.output.(error).Error(), err)
}
}
})