Merge pull request #418 from austin362667/refactor/futures-account

binance: add futures exchange api queries
This commit is contained in:
Yo-An Lin 2022-01-17 20:54:49 +08:00 committed by GitHub
commit 0e0525be99
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 522 additions and 102 deletions

38
config/funding.yaml Normal file
View File

@ -0,0 +1,38 @@
---
notifications:
slack:
defaultChannel: "dev-bbgo"
errorChannel: "bbgo-error"
# if you want to route channel by symbol
symbolChannels:
"^BTC": "btc"
"^ETH": "eth"
# object routing rules
routing:
trade: "$symbol"
order: "$symbol"
submitOrder: "$session" # not supported yet
pnL: "bbgo-pnl"
sessions:
binance:
exchange: binance
envVarPrefix: binance
futures: true
exchangeStrategies:
- on: binance
funding:
symbol: ETHUSDT
quantity: 0.0001
fundingRate:
high: 0.01%
supportDetection:
- interval: 1m
movingAverageType: EMA
movingAverageIntervalWindow:
interval: 15m
window: 60
minVolume: 8_000

View File

@ -30,6 +30,7 @@ package backtest
import (
"context"
"fmt"
"github.com/c9s/bbgo/pkg/cache"
"sync"
"time"
@ -68,7 +69,7 @@ type Exchange struct {
func NewExchange(sourceName types.ExchangeName, sourceExchange types.Exchange, srv *service.BacktestService, config *bbgo.Backtest) (*Exchange, error) {
ex := sourceExchange
markets, err := bbgo.LoadExchangeMarketsWithCache(context.Background(), ex)
markets, err := cache.LoadExchangeMarketsWithCache(context.Background(), ex)
if err != nil {
return nil, err
}

View File

@ -3,6 +3,7 @@ package bbgo
import (
"context"
"fmt"
"github.com/c9s/bbgo/pkg/cache"
"strings"
"time"
@ -295,7 +296,7 @@ func (session *ExchangeSession) Init(ctx context.Context, environ *Environment)
if util.SetEnvVarBool("DISABLE_MARKETS_CACHE", &disableMarketsCache); disableMarketsCache {
markets, err = session.Exchange.QueryMarkets(ctx)
} else {
markets, err = LoadExchangeMarketsWithCache(ctx, session.Exchange)
markets, err = cache.LoadExchangeMarketsWithCache(ctx, session.Exchange)
if err != nil {
return err
}

View File

@ -1,4 +1,4 @@
package bbgo
package cache
import (
"context"

View File

@ -1,4 +1,4 @@
package bbgo
package cache
import (
"os"

View File

@ -7,7 +7,7 @@ import (
_ "github.com/c9s/bbgo/pkg/strategy/emastop"
_ "github.com/c9s/bbgo/pkg/strategy/etf"
_ "github.com/c9s/bbgo/pkg/strategy/flashcrash"
_ "github.com/c9s/bbgo/pkg/strategy/xgap"
_ "github.com/c9s/bbgo/pkg/strategy/funding"
_ "github.com/c9s/bbgo/pkg/strategy/grid"
_ "github.com/c9s/bbgo/pkg/strategy/kline"
_ "github.com/c9s/bbgo/pkg/strategy/pricealert"
@ -18,6 +18,7 @@ import (
_ "github.com/c9s/bbgo/pkg/strategy/swing"
_ "github.com/c9s/bbgo/pkg/strategy/techsignal"
_ "github.com/c9s/bbgo/pkg/strategy/xbalance"
_ "github.com/c9s/bbgo/pkg/strategy/xgap"
_ "github.com/c9s/bbgo/pkg/strategy/xmaker"
_ "github.com/c9s/bbgo/pkg/strategy/xnav"
_ "github.com/c9s/bbgo/pkg/strategy/xpuremaker"

View File

@ -50,6 +50,42 @@ func toGlobalMarket(symbol binance.Symbol) types.Market {
return market
}
// TODO: Cuz it returns types.Market as well, merge following to the above function
func toGlobalFuturesMarket(symbol futures.Symbol) types.Market {
market := types.Market{
Symbol: symbol.Symbol,
LocalSymbol: symbol.Symbol,
PricePrecision: symbol.QuotePrecision,
VolumePrecision: symbol.BaseAssetPrecision,
QuoteCurrency: symbol.QuoteAsset,
BaseCurrency: symbol.BaseAsset,
}
if f := symbol.MinNotionalFilter(); f != nil {
market.MinNotional = util.MustParseFloat(f.Notional)
market.MinAmount = util.MustParseFloat(f.Notional)
}
// The LOT_SIZE filter defines the quantity (aka "lots" in auction terms) rules for a symbol.
// There are 3 parts:
// minQty defines the minimum quantity/icebergQty allowed.
// maxQty defines the maximum quantity/icebergQty allowed.
// stepSize defines the intervals that a quantity/icebergQty can be increased/decreased by.
if f := symbol.LotSizeFilter(); f != nil {
market.MinQuantity = util.MustParseFloat(f.MinQuantity)
market.MaxQuantity = util.MustParseFloat(f.MaxQuantity)
market.StepSize = util.MustParseFloat(f.StepSize)
}
if f := symbol.PriceFilter(); f != nil {
market.MaxPrice = util.MustParseFloat(f.MaxPrice)
market.MinPrice = util.MustParseFloat(f.MinPrice)
market.TickSize = util.MustParseFloat(f.TickSize)
}
return market
}
func toGlobalIsolatedUserAsset(userAsset binance.IsolatedUserAsset) types.IsolatedUserAsset {
return types.IsolatedUserAsset{
Asset: userAsset.Asset,
@ -81,40 +117,42 @@ func toGlobalIsolatedMarginAsset(asset binance.IsolatedMarginAsset) types.Isolat
}
}
func toGlobalIsolatedMarginAssets(assets []binance.IsolatedMarginAsset) (retAssets []types.IsolatedMarginAsset) {
for _, asset := range assets {
retAssets = append(retAssets, toGlobalIsolatedMarginAsset(asset))
func toGlobalIsolatedMarginAssets(assets []binance.IsolatedMarginAsset) (retAssets types.IsolatedMarginAssetMap) {
retMarginAssets := make(types.IsolatedMarginAssetMap)
for _, marginAsset := range assets {
retMarginAssets[marginAsset.Symbol] = toGlobalIsolatedMarginAsset(marginAsset)
}
return retAssets
return retMarginAssets
}
func toGlobalIsolatedMarginAccount(account *binance.IsolatedMarginAccount) *types.IsolatedMarginAccount {
return &types.IsolatedMarginAccount{
TotalAssetOfBTC: fixedpoint.MustNewFromString(account.TotalNetAssetOfBTC),
TotalLiabilityOfBTC: fixedpoint.MustNewFromString(account.TotalLiabilityOfBTC),
TotalNetAssetOfBTC: fixedpoint.MustNewFromString(account.TotalNetAssetOfBTC),
Assets: toGlobalIsolatedMarginAssets(account.Assets),
}
}
//func toGlobalIsolatedMarginAccount(account *binance.IsolatedMarginAccount) *types.IsolatedMarginAccount {
// return &types.IsolatedMarginAccount{
// TotalAssetOfBTC: fixedpoint.MustNewFromString(account.TotalNetAssetOfBTC),
// TotalLiabilityOfBTC: fixedpoint.MustNewFromString(account.TotalLiabilityOfBTC),
// TotalNetAssetOfBTC: fixedpoint.MustNewFromString(account.TotalNetAssetOfBTC),
// Assets: toGlobalIsolatedMarginAssets(account.Assets),
// }
//}
func toGlobalMarginUserAssets(userAssets []binance.UserAsset) (retAssets []types.MarginUserAsset) {
for _, asset := range userAssets {
retAssets = append(retAssets, types.MarginUserAsset{
Asset: asset.Asset,
Borrowed: fixedpoint.MustNewFromString(asset.Borrowed),
Free: fixedpoint.MustNewFromString(asset.Free),
Interest: fixedpoint.MustNewFromString(asset.Interest),
Locked: fixedpoint.MustNewFromString(asset.Locked),
NetAsset: fixedpoint.MustNewFromString(asset.NetAsset),
})
func toGlobalMarginUserAssets(assets []binance.UserAsset) types.MarginAssetMap {
retMarginAssets := make(types.MarginAssetMap)
for _, marginAsset := range assets {
retMarginAssets[marginAsset.Asset] = types.MarginUserAsset{
Asset: marginAsset.Asset,
Borrowed: fixedpoint.MustNewFromString(marginAsset.Borrowed),
Free: fixedpoint.MustNewFromString(marginAsset.Free),
Interest: fixedpoint.MustNewFromString(marginAsset.Interest),
Locked: fixedpoint.MustNewFromString(marginAsset.Locked),
NetAsset: fixedpoint.MustNewFromString(marginAsset.NetAsset),
}
}
return retAssets
return retMarginAssets
}
func toGlobalMarginAccount(account *binance.MarginAccount) *types.MarginAccount {
return &types.MarginAccount{
func toGlobalMarginAccountInfo(account *binance.MarginAccount) *types.MarginAccountInfo {
return &types.MarginAccountInfo{
BorrowEnabled: account.BorrowEnabled,
MarginLevel: fixedpoint.MustNewFromString(account.MarginLevel),
TotalAssetOfBTC: fixedpoint.MustNewFromString(account.TotalAssetOfBTC),
@ -122,15 +160,22 @@ func toGlobalMarginAccount(account *binance.MarginAccount) *types.MarginAccount
TotalNetAssetOfBTC: fixedpoint.MustNewFromString(account.TotalNetAssetOfBTC),
TradeEnabled: account.TradeEnabled,
TransferEnabled: account.TransferEnabled,
UserAssets: toGlobalMarginUserAssets(account.UserAssets),
Assets: toGlobalMarginUserAssets(account.UserAssets),
}
}
func toGlobalIsolatedMarginAccountInfo(account *binance.IsolatedMarginAccount) *types.IsolatedMarginAccountInfo {
return &types.IsolatedMarginAccountInfo{
TotalAssetOfBTC: fixedpoint.MustNewFromString(account.TotalAssetOfBTC),
TotalLiabilityOfBTC: fixedpoint.MustNewFromString(account.TotalLiabilityOfBTC),
TotalNetAssetOfBTC: fixedpoint.MustNewFromString(account.TotalNetAssetOfBTC),
Assets: toGlobalIsolatedMarginAssets(account.Assets),
}
}
func toGlobalFuturesAccountInfo(account *futures.Account) *types.FuturesAccountInfo {
return &types.FuturesAccountInfo{
Assets: toGlobalFuturesUserAssets(account.Assets),
FeeTier: account.FeeTier,
MaxWithdrawAmount: fixedpoint.MustNewFromString(account.MaxWithdrawAmount),
Positions: toGlobalFuturesPositions(account.Positions),
TotalInitialMargin: fixedpoint.MustNewFromString(account.TotalInitialMargin),
TotalMaintMargin: fixedpoint.MustNewFromString(account.TotalMaintMargin),
@ -170,23 +215,23 @@ func toGlobalFuturesPositions(futuresPositions []*futures.AccountPosition) types
return retFuturesPositions
}
func toGlobalFuturesUserAssets(assets []*futures.AccountAsset) (retAssets map[types.Asset]types.FuturesUserAsset) {
for _, asset := range assets {
// TODO: or modify to type FuturesAssetMap map[string]FuturesAssetMap
retAssets[types.Asset{Currency: asset.Asset}] = types.FuturesUserAsset{
Asset: asset.Asset,
InitialMargin: fixedpoint.MustNewFromString(asset.InitialMargin),
MaintMargin: fixedpoint.MustNewFromString(asset.MaintMargin),
MarginBalance: fixedpoint.MustNewFromString(asset.MarginBalance),
MaxWithdrawAmount: fixedpoint.MustNewFromString(asset.MaxWithdrawAmount),
OpenOrderInitialMargin: fixedpoint.MustNewFromString(asset.OpenOrderInitialMargin),
PositionInitialMargin: fixedpoint.MustNewFromString(asset.PositionInitialMargin),
UnrealizedProfit: fixedpoint.MustNewFromString(asset.UnrealizedProfit),
WalletBalance: fixedpoint.MustNewFromString(asset.WalletBalance),
func toGlobalFuturesUserAssets(assets []*futures.AccountAsset) (retAssets types.FuturesAssetMap) {
retFuturesAssets := make(types.FuturesAssetMap)
for _, futuresAsset := range assets {
retFuturesAssets[futuresAsset.Asset] = types.FuturesUserAsset{
Asset: futuresAsset.Asset,
InitialMargin: fixedpoint.MustNewFromString(futuresAsset.InitialMargin),
MaintMargin: fixedpoint.MustNewFromString(futuresAsset.MaintMargin),
MarginBalance: fixedpoint.MustNewFromString(futuresAsset.MarginBalance),
MaxWithdrawAmount: fixedpoint.MustNewFromString(futuresAsset.MaxWithdrawAmount),
OpenOrderInitialMargin: fixedpoint.MustNewFromString(futuresAsset.OpenOrderInitialMargin),
PositionInitialMargin: fixedpoint.MustNewFromString(futuresAsset.PositionInitialMargin),
UnrealizedProfit: fixedpoint.MustNewFromString(futuresAsset.UnrealizedProfit),
WalletBalance: fixedpoint.MustNewFromString(futuresAsset.WalletBalance),
}
}
return retAssets
return retFuturesAssets
}
func toGlobalTicker(stats *binance.PriceChangeStats) (*types.Ticker, error) {

View File

@ -169,6 +169,21 @@ func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[stri
}
func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) {
if e.IsFutures {
exchangeInfo, err := e.futuresClient.NewExchangeInfoService().Do(ctx)
if err != nil {
return nil, err
}
markets := types.MarketMap{}
for _, symbol := range exchangeInfo.Symbols {
markets[symbol.Symbol] = toGlobalFuturesMarket(symbol)
}
return markets, nil
}
exchangeInfo, err := e.Client.NewExchangeInfoService().Do(ctx)
if err != nil {
return nil, err
@ -198,16 +213,21 @@ func (e *Exchange) NewStream() types.Stream {
return stream
}
func (e *Exchange) QueryMarginAccount(ctx context.Context) (*types.MarginAccount, error) {
func (e *Exchange) QueryMarginAccount(ctx context.Context) (*types.Account, error) {
account, err := e.Client.NewGetMarginAccountService().Do(ctx)
if err != nil {
return nil, err
}
return toGlobalMarginAccount(account), nil
a := &types.Account{
AccountType: types.AccountTypeMargin,
MarginInfo: toGlobalMarginAccountInfo(account), // In binance GO api, Account define account info which mantain []*AccountAsset and []*AccountPosition.
}
return a, nil
}
func (e *Exchange) QueryIsolatedMarginAccount(ctx context.Context, symbols ...string) (*types.IsolatedMarginAccount, error) {
func (e *Exchange) QueryIsolatedMarginAccount(ctx context.Context, symbols ...string) (*types.Account, error) {
req := e.Client.NewGetIsolatedMarginAccountService()
if len(symbols) > 0 {
req.Symbols(symbols...)
@ -218,7 +238,12 @@ func (e *Exchange) QueryIsolatedMarginAccount(ctx context.Context, symbols ...st
return nil, err
}
return toGlobalIsolatedMarginAccount(account), nil
a := &types.Account{
AccountType: types.AccountTypeMargin,
IsolatedMarginInfo: toGlobalIsolatedMarginAccountInfo(account), // In binance GO api, Account define account info which mantain []*AccountAsset and []*AccountPosition.
}
return a, nil
}
func (e *Exchange) Withdrawal(ctx context.Context, asset string, amount fixedpoint.Value, address string, options *types.WithdrawalOptions) error {
@ -416,7 +441,7 @@ func (e *Exchange) PlatformFeeCurrency() string {
return BNB
}
func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) {
func (e *Exchange) QuerySpotAccount(ctx context.Context) (*types.Account, error) {
account, err := e.Client.NewGetAccountService().Do(ctx)
if err != nil {
return nil, err
@ -426,20 +451,68 @@ func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) {
for _, b := range account.Balances {
balances[b.Asset] = types.Balance{
Currency: b.Asset,
Available: fixedpoint.Must(fixedpoint.NewFromString(b.Free)),
Locked: fixedpoint.Must(fixedpoint.NewFromString(b.Locked)),
Available: fixedpoint.MustNewFromString(b.Free),
Locked: fixedpoint.MustNewFromString(b.Locked),
}
}
// binance use 15 -> 0.15%, so we convert it to 0.0015
a := &types.Account{
AccountType: types.AccountTypeSpot,
MakerCommission: fixedpoint.NewFromFloat(float64(account.MakerCommission) * 0.0001),
TakerCommission: fixedpoint.NewFromFloat(float64(account.TakerCommission) * 0.0001),
CanDeposit: account.CanDeposit, // if can transfer in asset
CanTrade: account.CanTrade, // if can trade
CanWithdraw: account.CanWithdraw, // if can transfer out asset
}
a.UpdateBalances(balances)
return a, nil
}
func (e *Exchange) QueryFuturesAccount(ctx context.Context) (*types.Account, error) {
account, err := e.futuresClient.NewGetAccountService().Do(ctx)
if err != nil {
return nil, err
}
accountBalances, err := e.futuresClient.NewGetBalanceService().Do(ctx)
if err != nil {
return nil, err
}
var balances = map[string]types.Balance{}
for _, b := range accountBalances {
balances[b.Asset] = types.Balance{
Currency: b.Asset,
Available: fixedpoint.Must(fixedpoint.NewFromString(b.AvailableBalance)),
}
}
a := &types.Account{
AccountType: types.AccountTypeFutures,
FuturesInfo: toGlobalFuturesAccountInfo(account), // In binance GO api, Account define account info which mantain []*AccountAsset and []*AccountPosition.
CanDeposit: account.CanDeposit, // if can transfer in asset
CanTrade: account.CanTrade, // if can trade
CanWithdraw: account.CanWithdraw, // if can transfer out asset
}
a.UpdateBalances(balances)
return a, nil
}
func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) {
var account *types.Account
var err error
if e.IsFutures {
account, err = e.QueryFuturesAccount(ctx)
} else if e.IsIsolatedMargin {
account, err = e.QueryIsolatedMarginAccount(ctx)
} else if e.IsMargin {
account, err = e.QueryMarginAccount(ctx)
} else {
account, err = e.QuerySpotAccount(ctx)
}
return account, err
}
func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) {
if e.IsMargin {
req := e.Client.NewListMarginOpenOrdersService().Symbol(symbol)
@ -516,7 +589,6 @@ func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since,
if err != nil {
return orders, err
}
return toGlobalFuturesOrders(binanceOrders)
}
@ -1032,9 +1104,8 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type
return trades, nil
} else if e.IsFutures {
var remoteTrades []*futures.AccountTrade
req := e.futuresClient.NewListAccountTradeService(). // IsIsolated(e.IsIsolatedFutures).
Symbol(symbol)
req := e.futuresClient.NewListAccountTradeService().
Symbol(symbol)
if options.Limit > 0 {
req.Limit(int(options.Limit))
} else {
@ -1053,7 +1124,7 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type
for _, t := range remoteTrades {
localTrade, err := toGlobalFuturesTrade(*t)
if err != nil {
log.WithError(err).Errorf("can not convert binance trade: %+v", t)
log.WithError(err).Errorf("can not convert binance futures trade: %+v", t)
continue
}
@ -1207,6 +1278,18 @@ func (e *Exchange) QueryFundingRateHistory(ctx context.Context, symbol string) (
}, nil
}
func (e *Exchange) QueryPositionRisk(ctx context.Context, symbol string) (*types.PositionRisk, error) {
futuresClient := binance.NewFuturesClient(e.key, e.secret)
// when symbol is set, only one position risk will be returned.
risks, err := futuresClient.NewGetPositionRiskService().Symbol(symbol).Do(ctx)
if err != nil {
return nil, err
}
return convertPositionRisk(risks[0])
}
func getLaunchDate() (time.Time, error) {
// binance launch date 12:00 July 14th, 2017
loc, err := time.LoadLocation("Asia/Shanghai")

View File

@ -719,6 +719,29 @@ func (e *OrderTradeUpdateEvent) OrderFutures() (*types.Order, error) {
}, nil
}
func (e *OrderTradeUpdateEvent) TradeFutures() (*types.Trade, error) {
if e.OrderTrade.CurrentExecutionType != "TRADE" {
return nil, errors.New("execution report is not a futures trade")
}
tt := time.Unix(0, e.OrderTrade.OrderTradeTime*int64(time.Millisecond))
return &types.Trade{
ID: uint64(e.OrderTrade.TradeId),
Exchange: types.ExchangeBinance,
Symbol: e.OrderTrade.Symbol,
OrderID: uint64(e.OrderTrade.OrderId),
Side: toGlobalSideType(binance.SideType(e.OrderTrade.Side)),
Price: float64(e.OrderTrade.LastFilledPrice),
Quantity: float64(e.OrderTrade.OrderLastFilledQuantity),
QuoteQuantity: float64(e.OrderTrade.OrderFilledAccumulatedQuantity),
IsBuyer: e.OrderTrade.Side == "BUY",
IsMaker: e.OrderTrade.IsMaker,
Time: types.Time(tt),
Fee: float64(e.OrderTrade.CommissionAmount),
FeeCurrency: e.OrderTrade.CommissionAsset,
}, nil
}
type AccountUpdate struct {
EventReasonType string `json:"m"`
Balances []*futures.Balance `json:"B,omitempty"`

View File

@ -247,33 +247,21 @@ func (s *Stream) handleOrderTradeUpdateEvent(e *OrderTradeUpdateEvent) {
case "NEW", "CANCELED", "EXPIRED":
order, err := e.OrderFutures()
if err != nil {
log.WithError(err).Error("order convert error")
log.WithError(err).Error("futures order convert error")
return
}
s.EmitOrderUpdate(*order)
case "TRADE":
// TODO
trade, err := e.TradeFutures()
if err != nil {
log.WithError(err).Error("futures trade convert error")
return
}
// trade, err := e.Trade()
// if err != nil {
// log.WithError(err).Error("trade convert error")
// return
// }
s.EmitTradeUpdate(*trade)
// stream.EmitTradeUpdate(*trade)
// order, err := e.OrderFutures()
// if err != nil {
// log.WithError(err).Error("order convert error")
// return
// }
// Update Order with FILLED event
// if order.Status == types.OrderStatusFilled {
// stream.EmitOrderUpdate(*order)
// }
case "CALCULATED - Liquidation Execution":
log.Infof("CALCULATED - Liquidation Execution not support yet.")
}

View File

@ -3,6 +3,7 @@ package service
import (
"context"
"errors"
"github.com/c9s/bbgo/pkg/cache"
"time"
log "github.com/sirupsen/logrus"
@ -25,17 +26,22 @@ type SyncService struct {
func (s *SyncService) SyncSessionSymbols(ctx context.Context, exchange types.Exchange, startTime time.Time, symbols ...string) error {
for _, symbol := range symbols {
log.Infof("syncing %s %s trades...", exchange.Name(), symbol)
if err := s.TradeService.Sync(ctx, exchange, symbol, startTime); err != nil {
markets, err := cache.LoadExchangeMarketsWithCache(ctx, exchange)
if err != nil {
return err
}
log.Infof("syncing %s %s orders...", exchange.Name(), symbol)
if err := s.OrderService.Sync(ctx, exchange, symbol, startTime); err != nil {
return err
if _, ok := markets[symbol]; ok {
if err := s.TradeService.Sync(ctx, exchange, symbol, startTime); err != nil {
return err
}
log.Infof("syncing %s %s orders...", exchange.Name(), symbol)
if err := s.OrderService.Sync(ctx, exchange, symbol, startTime); err != nil {
return err
}
}
}
log.Infof("syncing %s deposit records...", exchange.Name())
if err := s.DepositService.Sync(ctx, exchange); err != nil {
if err != ErrNotImplemented {

View File

@ -0,0 +1,211 @@
package funding
import (
"context"
"errors"
"fmt"
"github.com/c9s/bbgo/pkg/exchange/binance"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/sirupsen/logrus"
"math"
"strings"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/types"
)
const ID = "funding"
var log = logrus.WithField("strategy", ID)
func init() {
// Register the pointer of the strategy struct,
// so that bbgo knows what struct to be used to unmarshal the configs (YAML or JSON)
// Note: built-in strategies need to imported manually in the bbgo cmd package.
bbgo.RegisterStrategy(ID, &Strategy{})
}
type Strategy struct {
*bbgo.Notifiability
// These fields will be filled from the config file (it translates YAML to JSON)
Symbol string `json:"symbol"`
Market types.Market `json:"-"`
Quantity fixedpoint.Value `json:"quantity,omitempty"`
MaxExposurePosition fixedpoint.Value `json:"maxExposurePosition"`
//Interval types.Interval `json:"interval"`
FundingRate *struct {
High fixedpoint.Value `json:"high"`
Neutral fixedpoint.Value `json:"neutral"`
DiffThreshold fixedpoint.Value `json:"diffThreshold"`
} `json:"fundingRate"`
SupportDetection []struct {
Interval types.Interval `json:"interval"`
// MovingAverageType is the moving average indicator type that we want to use,
// it could be SMA or EWMA
MovingAverageType string `json:"movingAverageType"`
// MovingAverageInterval is the interval of k-lines for the moving average indicator to calculate,
// it could be "1m", "5m", "1h" and so on. note that, the moving averages are calculated from
// the k-line data we subscribed
//MovingAverageInterval types.Interval `json:"movingAverageInterval"`
//
//// MovingAverageWindow is the number of the window size of the moving average indicator.
//// The number of k-lines in the window. generally used window sizes are 7, 25 and 99 in the TradingView.
//MovingAverageWindow int `json:"movingAverageWindow"`
MovingAverageIntervalWindow types.IntervalWindow `json:"movingAverageIntervalWindow"`
MinVolume fixedpoint.Value `json:"minVolume"`
MinQuoteVolume fixedpoint.Value `json:"minQuoteVolume"`
} `json:"supportDetection"`
}
func (s *Strategy) ID() string {
return ID
}
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
// session.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{})
//session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{
// Interval: string(s.Interval),
//})
for _, detection := range s.SupportDetection {
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{
Interval: string(detection.Interval),
})
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{
Interval: string(detection.MovingAverageIntervalWindow.Interval),
})
}
}
func (s *Strategy) Validate() error {
if len(s.Symbol) == 0 {
return errors.New("symbol is required")
}
return nil
}
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
standardIndicatorSet, ok := session.StandardIndicatorSet(s.Symbol)
if !ok {
return fmt.Errorf("standardIndicatorSet is nil, symbol %s", s.Symbol)
}
//binanceExchange, ok := session.Exchange.(*binance.Exchange)
//if !ok {
// log.Error("exchange failed")
//}
if !session.Futures {
log.Error("futures not enabled in config for this strategy")
return nil
}
//if s.FundingRate != nil {
// go s.listenToFundingRate(ctx, binanceExchange)
//}
premiumIndex, err := session.Exchange.(*binance.Exchange).QueryPremiumIndex(ctx, s.Symbol)
if err != nil {
log.Error("exchange does not support funding rate api")
}
var ma types.Float64Indicator
for _, detection := range s.SupportDetection {
switch strings.ToLower(detection.MovingAverageType) {
case "sma":
ma = standardIndicatorSet.SMA(types.IntervalWindow{
Interval: detection.MovingAverageIntervalWindow.Interval,
Window: detection.MovingAverageIntervalWindow.Window,
})
case "ema", "ewma":
ma = standardIndicatorSet.EWMA(types.IntervalWindow{
Interval: detection.MovingAverageIntervalWindow.Interval,
Window: detection.MovingAverageIntervalWindow.Window,
})
default:
ma = standardIndicatorSet.EWMA(types.IntervalWindow{
Interval: detection.MovingAverageIntervalWindow.Interval,
Window: detection.MovingAverageIntervalWindow.Window,
})
}
}
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
// skip k-lines from other symbols
if kline.Symbol != s.Symbol {
return
}
for _, detection := range s.SupportDetection {
var lastMA = ma.Last()
closePriceF := kline.GetClose()
closePrice := fixedpoint.NewFromFloat(closePriceF)
// skip if the closed price is under the moving average
if closePrice.Float64() < lastMA {
log.Infof("skip %s closed price %f < last ma %f", s.Symbol, closePrice.Float64(), lastMA)
return
}
fundingRate := premiumIndex.LastFundingRate
if fundingRate >= s.FundingRate.High {
s.Notifiability.Notify("%s funding rate %s is too high! threshold %s",
s.Symbol,
fundingRate.Percentage(),
s.FundingRate.High.Percentage(),
)
} else {
log.Infof("skip funding rate is too low")
return
}
prettyBaseVolume := s.Market.BaseCurrencyFormatter()
prettyQuoteVolume := s.Market.QuoteCurrencyFormatter()
if detection.MinVolume > 0 && kline.Volume > detection.MinVolume.Float64() {
s.Notifiability.Notify("Detected %s %s resistance base volume %s > min base volume %s, quote volume %s",
s.Symbol, detection.Interval.String(),
prettyBaseVolume.FormatMoney(math.Round(kline.Volume)),
prettyBaseVolume.FormatMoney(math.Round(detection.MinVolume.Float64())),
prettyQuoteVolume.FormatMoney(math.Round(kline.QuoteVolume)),
)
s.Notifiability.Notify(kline)
baseBalance, ok := session.Account.Balance(s.Market.BaseCurrency)
if !ok {
return
}
if baseBalance.Available > 0 && baseBalance.Total() < s.MaxExposurePosition {
log.Infof("opening a short position")
_, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
Symbol: kline.Symbol,
Side: types.SideTypeSell,
Type: types.OrderTypeMarket,
Quantity: s.Quantity.Float64(),
})
if err != nil {
log.WithError(err).Error("submit order error")
}
}
} else if detection.MinQuoteVolume > 0 && kline.QuoteVolume > detection.MinQuoteVolume.Float64() {
s.Notifiability.Notify("Detected %s %s resistance quote volume %s > min quote volume %s, base volume %s",
s.Symbol, detection.Interval.String(),
prettyQuoteVolume.FormatMoney(math.Round(kline.QuoteVolume)),
prettyQuoteVolume.FormatMoney(math.Round(detection.MinQuoteVolume.Float64())),
prettyBaseVolume.FormatMoney(math.Round(kline.Volume)),
)
s.Notifiability.Notify(kline)
}
}
})
return nil
}

View File

@ -121,6 +121,9 @@ func (m AssetMap) SlackAttachment() slack.Attachment {
type BalanceMap map[string]Balance
type PositionMap map[string]Position
type IsolatedMarginAssetMap map[string]IsolatedMarginAsset
type MarginAssetMap map[string]MarginUserAsset
type FuturesAssetMap map[string]FuturesUserAsset
type FuturesPositionMap map[string]FuturesPosition
func (m BalanceMap) String() string {
@ -200,14 +203,17 @@ type AccountType string
const (
AccountTypeFutures = AccountType("futures")
AccountTypeMargin = AccountType("margin")
AccountTypeSpot = AccountType("spot")
)
type Account struct {
sync.Mutex `json:"-"`
AccountType AccountType `json:"accountType,omitempty"`
FuturesInfo *FuturesAccountInfo
AccountType AccountType `json:"accountType,omitempty"`
FuturesInfo *FuturesAccountInfo
MarginInfo *MarginAccountInfo
IsolatedMarginInfo *IsolatedMarginAccountInfo
MakerFeeRate fixedpoint.Value `json:"makerFeeRate,omitempty"`
TakerFeeRate fixedpoint.Value `json:"takerFeeRate,omitempty"`
@ -227,18 +233,35 @@ type Account struct {
type FuturesAccountInfo struct {
// Futures fields
Assets map[Asset]FuturesUserAsset `json:"assets"`
FeeTier int `json:"feeTier"`
MaxWithdrawAmount fixedpoint.Value `json:"maxWithdrawAmount"`
Positions FuturesPositionMap `json:"positions"`
TotalInitialMargin fixedpoint.Value `json:"totalInitialMargin"`
TotalMaintMargin fixedpoint.Value `json:"totalMaintMargin"`
TotalMarginBalance fixedpoint.Value `json:"totalMarginBalance"`
TotalOpenOrderInitialMargin fixedpoint.Value `json:"totalOpenOrderInitialMargin"`
TotalPositionInitialMargin fixedpoint.Value `json:"totalPositionInitialMargin"`
TotalUnrealizedProfit fixedpoint.Value `json:"totalUnrealizedProfit"`
TotalWalletBalance fixedpoint.Value `json:"totalWalletBalance"`
UpdateTime int64 `json:"updateTime"`
Assets FuturesAssetMap `json:"assets"`
Positions FuturesPositionMap `json:"positions"`
TotalInitialMargin fixedpoint.Value `json:"totalInitialMargin"`
TotalMaintMargin fixedpoint.Value `json:"totalMaintMargin"`
TotalMarginBalance fixedpoint.Value `json:"totalMarginBalance"`
TotalOpenOrderInitialMargin fixedpoint.Value `json:"totalOpenOrderInitialMargin"`
TotalPositionInitialMargin fixedpoint.Value `json:"totalPositionInitialMargin"`
TotalUnrealizedProfit fixedpoint.Value `json:"totalUnrealizedProfit"`
TotalWalletBalance fixedpoint.Value `json:"totalWalletBalance"`
UpdateTime int64 `json:"updateTime"`
}
type MarginAccountInfo struct {
// Margin fields
BorrowEnabled bool `json:"borrowEnabled"`
MarginLevel fixedpoint.Value `json:"marginLevel"`
TotalAssetOfBTC fixedpoint.Value `json:"totalAssetOfBtc"`
TotalLiabilityOfBTC fixedpoint.Value `json:"totalLiabilityOfBtc"`
TotalNetAssetOfBTC fixedpoint.Value `json:"totalNetAssetOfBtc"`
TradeEnabled bool `json:"tradeEnabled"`
TransferEnabled bool `json:"transferEnabled"`
Assets MarginAssetMap `json:"userAssets"`
}
type IsolatedMarginAccountInfo struct {
TotalAssetOfBTC fixedpoint.Value `json:"totalAssetOfBtc"`
TotalLiabilityOfBTC fixedpoint.Value `json:"totalLiabilityOfBtc"`
TotalNetAssetOfBTC fixedpoint.Value `json:"totalNetAssetOfBtc"`
Assets IsolatedMarginAssetMap `json:"userAssets"`
}
func NewAccount() *Account {

View File

@ -92,10 +92,10 @@ type MarginUserAsset struct {
// IsolatedMarginAccount defines isolated user assets of margin account
type IsolatedMarginAccount struct {
TotalAssetOfBTC fixedpoint.Value `json:"totalAssetOfBtc"`
TotalLiabilityOfBTC fixedpoint.Value `json:"totalLiabilityOfBtc"`
TotalNetAssetOfBTC fixedpoint.Value `json:"totalNetAssetOfBtc"`
Assets []IsolatedMarginAsset `json:"assets"`
TotalAssetOfBTC fixedpoint.Value `json:"totalAssetOfBtc"`
TotalLiabilityOfBTC fixedpoint.Value `json:"totalLiabilityOfBtc"`
TotalNetAssetOfBTC fixedpoint.Value `json:"totalNetAssetOfBtc"`
Assets IsolatedMarginAssetMap `json:"assets"`
}
// IsolatedMarginAsset defines isolated margin asset information, like margin level, liquidation price... etc