Merge pull request #169 from c9s/ftx/account

feature: support FTX account information api
This commit is contained in:
YC 2021-03-19 00:03:21 +08:00 committed by GitHub
commit 25dff00410
10 changed files with 339 additions and 13 deletions

View File

@ -47,8 +47,8 @@ type SimplePriceMatching struct {
Account *types.Account
MakerCommission int `json:"makerCommission"`
TakerCommission int `json:"takerCommission"`
MakerCommission float64 `json:"makerCommission"`
TakerCommission float64 `json:"takerCommission"`
tradeUpdateCallbacks []func(trade types.Trade)
orderUpdateCallbacks []func(order types.Order)
@ -205,9 +205,9 @@ func (m *SimplePriceMatching) newTradeFromOrder(order types.Order, isMaker bool)
// MAX uses 0.050% for maker and 0.15% for taker
var commission = DefaultFeeRate
if isMaker && m.Account.MakerCommission > 0 {
commission = 0.0001 * float64(m.Account.MakerCommission) // binance uses 10~15
commission = 0.0001 * m.Account.MakerCommission // binance uses 10~15
} else if m.Account.TakerCommission > 0 {
commission = 0.0001 * float64(m.Account.TakerCommission) // binance uses 10~15
commission = 0.0001 * m.Account.TakerCommission // binance uses 10~15
}
var fee float64

View File

@ -113,8 +113,8 @@ func (t Backtest) ParseStartTime() (time.Time, error) {
}
type BacktestAccount struct {
MakerCommission int `json:"makerCommission"`
TakerCommission int `json:"takerCommission"`
MakerCommission float64 `json:"makerCommission"`
TakerCommission float64 `json:"takerCommission"`
BuyerCommission int `json:"buyerCommission"`
SellerCommission int `json:"sellerCommission"`
Balances BacktestAccountBalanceMap `json:"balances" yaml:"balances"`

78
pkg/cmd/account.go Normal file
View File

@ -0,0 +1,78 @@
package cmd
import (
"context"
"fmt"
"os"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/c9s/bbgo/pkg/bbgo"
)
func init() {
accountCmd.Flags().String("session", "", "the exchange session name for querying information")
RootCmd.AddCommand(accountCmd)
}
// go run ./cmd/bbgo account --session=ftx --config=config/bbgo.yaml
var accountCmd = &cobra.Command{
Use: "account",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
configFile, err := cmd.Flags().GetString("config")
if err != nil {
return err
}
if len(configFile) == 0 {
return errors.New("--config option is required")
}
var userConfig *bbgo.Config
if _, err := os.Stat(configFile); err == nil {
// load successfully
userConfig, err = bbgo.Load(configFile, false)
if err != nil {
return err
}
} else if os.IsNotExist(err) {
// config file doesn't exist
userConfig = &bbgo.Config{}
} else {
// other error
return err
}
environ := bbgo.NewEnvironment()
if err := environ.ConfigureDatabase(ctx); err != nil {
return err
}
if err := environ.ConfigureExchangeSessions(userConfig); err != nil {
return err
}
sessionName, err := cmd.Flags().GetString("session")
if err != nil {
return err
}
session, ok := environ.Session(sessionName)
if !ok {
return fmt.Errorf("session %s not found", sessionName)
}
a, err := session.Exchange.QueryAccount(ctx)
if err != nil {
return err
}
log.Infof("account info: %+v", a)
return nil
},
}

View File

@ -378,8 +378,8 @@ func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) {
}
a := &types.Account{
MakerCommission: int(account.MakerCommission),
TakerCommission: int(account.TakerCommission),
MakerCommission: float64(account.MakerCommission),
TakerCommission: float64(account.TakerCommission),
}
a.UpdateBalances(balances)
return a, nil

View File

@ -53,7 +53,7 @@ func (e *Exchange) Name() types.ExchangeName {
}
func (e *Exchange) PlatformFeeCurrency() string {
panic("implement me")
return toGlobalCurrency("FTT")
}
func (e *Exchange) NewStream() types.Stream {
@ -65,7 +65,27 @@ func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) {
}
func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) {
panic("implement me")
resp, err := e.newRest().Account(ctx)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("ftx returns querying balances failure")
}
bps := fixedpoint.NewFromFloat(10000)
a := &types.Account{
MakerCommission: fixedpoint.NewFromFloat(resp.Result.MakerFee).Mul(bps).Float64(),
TakerCommission: fixedpoint.NewFromFloat(resp.Result.TakerFee).Mul(bps).Float64(),
}
balances, err := e.QueryAccountBalances(ctx)
if err != nil {
return nil, err
}
a.UpdateBalances(balances)
return a, nil
}
func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) {

View File

@ -12,6 +12,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
func TestExchange_QueryAccountBalances(t *testing.T) {
@ -280,3 +281,90 @@ func TestExchange_QueryClosedOrders(t *testing.T) {
}
})
}
func TestExchange_QueryAccount(t *testing.T) {
balanceResp := `
{
"result": [
{
"availableWithoutBorrow": 19.47458865,
"coin": "USD",
"free": 19.48085209,
"spotBorrow": 0.0,
"total": 1094.66405065,
"usdValue": 1094.664050651561
}
],
"success": true
}
`
accountInfoResp := `
{
"success": true,
"result": {
"backstopProvider": true,
"collateral": 3568181.02691129,
"freeCollateral": 1786071.456884368,
"initialMarginRequirement": 0.12222384240257728,
"leverage": 10,
"liquidating": false,
"maintenanceMarginRequirement": 0.07177992558058484,
"makerFee": 0.0002,
"marginFraction": 0.5588433331419503,
"openMarginFraction": 0.2447194090423075,
"takerFee": 0.0005,
"totalAccountValue": 3568180.98341129,
"totalPositionSize": 6384939.6992,
"username": "user@domain.com",
"positions": [
{
"cost": -31.7906,
"entryPrice": 138.22,
"future": "ETH-PERP",
"initialMarginRequirement": 0.1,
"longOrderSize": 1744.55,
"maintenanceMarginRequirement": 0.04,
"netSize": -0.23,
"openSize": 1744.32,
"realizedPnl": 3.39441714,
"shortOrderSize": 1732.09,
"side": "sell",
"size": 0.23,
"unrealizedPnl": 0
}
]
}
}
`
returnBalance := false
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if returnBalance {
fmt.Fprintln(w, balanceResp)
return
}
returnBalance = true
fmt.Fprintln(w, accountInfoResp)
}))
defer ts.Close()
ex := NewExchange("", "", "")
serverURL, err := url.Parse(ts.URL)
assert.NoError(t, err)
ex.restEndpoint = serverURL
resp, err := ex.QueryAccount(context.Background())
assert.NoError(t, err)
b, ok := resp.Balance("USD")
assert.True(t, ok)
expected := types.Balance{
Currency: "USD",
Available: fixedpoint.MustNewFromString("19.48085209"),
Locked: fixedpoint.MustNewFromString("1094.66405065"),
}
expected.Locked = expected.Locked.Sub(expected.Available)
assert.Equal(t, expected, b)
assert.Equal(t, 0.0002*10000, resp.MakerCommission)
assert.Equal(t, 0.0005*10000, resp.TakerCommission)
}

View File

@ -21,6 +21,7 @@ import (
type restRequest struct {
*balanceRequest
*orderRequest
*accountRequest
key, secret string
// Optional sub-account name
@ -42,6 +43,7 @@ func newRestRequest(c *http.Client, baseURL *url.URL) *restRequest {
baseURL: baseURL,
}
r.accountRequest = &accountRequest{restRequest: r}
r.balanceRequest = &balanceRequest{restRequest: r}
r.orderRequest = &orderRequest{restRequest: r}
return r

View File

@ -0,0 +1,47 @@
package ftx
import (
"context"
"encoding/json"
"fmt"
)
type accountRequest struct {
*restRequest
}
func (r *accountRequest) Account(ctx context.Context) (accountResponse, error) {
resp, err := r.
Method("GET").
ReferenceURL("api/account").
DoAuthenticatedRequest(ctx)
if err != nil {
return accountResponse{}, err
}
var a accountResponse
if err := json.Unmarshal(resp.Body, &a); err != nil {
return accountResponse{}, fmt.Errorf("failed to unmarshal account response body to json: %w", err)
}
return a, nil
}
func (r *accountRequest) Positions(ctx context.Context) (positionsResponse, error) {
resp, err := r.
Method("GET").
ReferenceURL("api/positions").
DoAuthenticatedRequest(ctx)
if err != nil {
return positionsResponse{}, err
}
var p positionsResponse
if err := json.Unmarshal(resp.Body, &p); err != nil {
return positionsResponse{}, fmt.Errorf("failed to unmarshal position response body to json: %w", err)
}
return p, nil
}

View File

@ -2,6 +2,96 @@ package ftx
import "time"
/*
{
"success": true,
"result": {
"backstopProvider": true,
"collateral": 3568181.02691129,
"freeCollateral": 1786071.456884368,
"initialMarginRequirement": 0.12222384240257728,
"leverage": 10,
"liquidating": false,
"maintenanceMarginRequirement": 0.07177992558058484,
"makerFee": 0.0002,
"marginFraction": 0.5588433331419503,
"openMarginFraction": 0.2447194090423075,
"takerFee": 0.0005,
"totalAccountValue": 3568180.98341129,
"totalPositionSize": 6384939.6992,
"username": "user@domain.com",
"positions": [
{
"cost": -31.7906,
"entryPrice": 138.22,
"future": "ETH-PERP",
"initialMarginRequirement": 0.1,
"longOrderSize": 1744.55,
"maintenanceMarginRequirement": 0.04,
"netSize": -0.23,
"openSize": 1744.32,
"realizedPnl": 3.39441714,
"shortOrderSize": 1732.09,
"side": "sell",
"size": 0.23,
"unrealizedPnl": 0
}
]
}
}
*/
type accountResponse struct {
Success bool `json:"success"`
Result account `json:"result"`
}
type account struct {
MakerFee float64 `json:"makerFee"`
TakerFee float64 `json:"takerFee"`
}
type positionsResponse struct {
Success bool `json:"success"`
Result []position `json:"result"`
}
/*
{
"cost": -31.7906,
"entryPrice": 138.22,
"estimatedLiquidationPrice": 152.1,
"future": "ETH-PERP",
"initialMarginRequirement": 0.1,
"longOrderSize": 1744.55,
"maintenanceMarginRequirement": 0.04,
"netSize": -0.23,
"openSize": 1744.32,
"realizedPnl": 3.39441714,
"shortOrderSize": 1732.09,
"side": "sell",
"size": 0.23,
"unrealizedPnl": 0,
"collateralUsed": 3.17906
}
*/
type position struct {
Cost float64 `json:"cost"`
EntryPrice float64 `json:"entryPrice"`
EstimatedLiquidationPrice float64 `json:"estimatedLiquidationPrice"`
Future string `json:"future"`
InitialMarginRequirement float64 `json:"initialMarginRequirement"`
LongOrderSize float64 `json:"longOrderSize"`
MaintenanceMarginRequirement float64 `json:"maintenanceMarginRequirement"`
NetSize float64 `json:"netSize"`
OpenSize float64 `json:"openSize"`
RealizedPnl float64 `json:"realizedPnl"`
ShortOrderSize float64 `json:"shortOrderSize"`
Side string `json:"Side"`
Size float64 `json:"size"`
UnrealizedPnl float64 `json:"unrealizedPnl"`
CollateralUsed float64 `json:"collateralUsed"`
}
type balances struct {
Success bool `json:"success"`

View File

@ -102,9 +102,10 @@ func (m BalanceMap) Print() {
type Account struct {
sync.Mutex `json:"-"`
MakerCommission int `json:"makerCommission,omitempty"`
TakerCommission int `json:"takerCommission,omitempty"`
AccountType string `json:"accountType,omitempty"`
// bps. 0.15% fee will be 15.
MakerCommission float64 `json:"makerCommission,omitempty"`
TakerCommission float64 `json:"takerCommission,omitempty"`
AccountType string `json:"accountType,omitempty"`
balances BalanceMap
}