diff --git a/config/funding.yaml b/config/funding.yaml new file mode 100644 index 000000000..0fcd433ed --- /dev/null +++ b/config/funding.yaml @@ -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 diff --git a/pkg/backtest/exchange.go b/pkg/backtest/exchange.go index 67e5a0554..a361ee2da 100644 --- a/pkg/backtest/exchange.go +++ b/pkg/backtest/exchange.go @@ -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 } diff --git a/pkg/bbgo/session.go b/pkg/bbgo/session.go index 5d78e0e83..168728797 100644 --- a/pkg/bbgo/session.go +++ b/pkg/bbgo/session.go @@ -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 } diff --git a/pkg/bbgo/cache.go b/pkg/cache/cache.go similarity index 99% rename from pkg/bbgo/cache.go rename to pkg/cache/cache.go index 65fa56fc3..5a5f0ccc2 100644 --- a/pkg/bbgo/cache.go +++ b/pkg/cache/cache.go @@ -1,4 +1,4 @@ -package bbgo +package cache import ( "context" diff --git a/pkg/bbgo/home.go b/pkg/cache/home.go similarity index 96% rename from pkg/bbgo/home.go rename to pkg/cache/home.go index f82ac21ce..c6480543e 100644 --- a/pkg/bbgo/home.go +++ b/pkg/cache/home.go @@ -1,4 +1,4 @@ -package bbgo +package cache import ( "os" diff --git a/pkg/cmd/builtin.go b/pkg/cmd/builtin.go index 8b0817d31..8af23e482 100644 --- a/pkg/cmd/builtin.go +++ b/pkg/cmd/builtin.go @@ -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" diff --git a/pkg/exchange/binance/convert.go b/pkg/exchange/binance/convert.go index 7235308c7..f6df8c48b 100644 --- a/pkg/exchange/binance/convert.go +++ b/pkg/exchange/binance/convert.go @@ -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) { diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index eee8618be..7ce0c7c23 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -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") diff --git a/pkg/exchange/binance/parse.go b/pkg/exchange/binance/parse.go index e18020a40..01f871f3c 100644 --- a/pkg/exchange/binance/parse.go +++ b/pkg/exchange/binance/parse.go @@ -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"` diff --git a/pkg/exchange/binance/stream.go b/pkg/exchange/binance/stream.go index dd774fbfa..17769ae5a 100644 --- a/pkg/exchange/binance/stream.go +++ b/pkg/exchange/binance/stream.go @@ -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.") } diff --git a/pkg/service/sync.go b/pkg/service/sync.go index 93f904f87..90401132c 100644 --- a/pkg/service/sync.go +++ b/pkg/service/sync.go @@ -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 { diff --git a/pkg/strategy/funding/strategy.go b/pkg/strategy/funding/strategy.go new file mode 100644 index 000000000..7e12d81c9 --- /dev/null +++ b/pkg/strategy/funding/strategy.go @@ -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 +} diff --git a/pkg/types/account.go b/pkg/types/account.go index 17c841086..29ddb762a 100644 --- a/pkg/types/account.go +++ b/pkg/types/account.go @@ -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 { diff --git a/pkg/types/margin.go b/pkg/types/margin.go index e8d661583..6931d168f 100644 --- a/pkg/types/margin.go +++ b/pkg/types/margin.go @@ -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