Merge pull request #827 from c9s/strategy/pivotshort

strategy/pivotshort: improve quantity calculation for margin and futures
This commit is contained in:
Yo-An Lin 2022-07-14 18:16:48 +08:00 committed by GitHub
commit 191e00adeb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 128 additions and 46 deletions

29
pkg/exchange/util.go Normal file
View File

@ -0,0 +1,29 @@
package exchange
import "github.com/c9s/bbgo/pkg/types"
func GetSessionAttributes(exchange types.Exchange) (isMargin, isFutures, isIsolated bool, isolatedSymbol string) {
if marginExchange, ok := exchange.(types.MarginExchange); ok {
marginSettings := marginExchange.GetMarginSettings()
isMargin = marginSettings.IsMargin
if isMargin {
isIsolated = marginSettings.IsIsolatedMargin
if marginSettings.IsIsolatedMargin {
isolatedSymbol = marginSettings.IsolatedMarginSymbol
}
}
}
if futuresExchange, ok := exchange.(types.FuturesExchange); ok {
futuresSettings := futuresExchange.GetFuturesSettings()
isFutures = futuresSettings.IsFutures
if isFutures {
isIsolated = futuresSettings.IsIsolatedFutures
if futuresSettings.IsIsolatedFutures {
isolatedSymbol = futuresSettings.IsolatedFuturesSymbol
}
}
}
return isMargin, isFutures, isIsolated, isolatedSymbol
}

View File

@ -13,6 +13,7 @@ import (
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
exchange2 "github.com/c9s/bbgo/pkg/exchange"
"github.com/c9s/bbgo/pkg/exchange/batch"
"github.com/c9s/bbgo/pkg/types"
)
@ -25,7 +26,7 @@ func (s *BacktestService) SyncKLineByInterval(ctx context.Context, exchange type
log.Infof("synchronizing %s klines with interval %s: %s <=> %s", exchange.Name(), interval, startTime, endTime)
// TODO: use isFutures here
_, _, isIsolated, isolatedSymbol := getExchangeAttributes(exchange)
_, _, isIsolated, isolatedSymbol := exchange2.GetSessionAttributes(exchange)
// override symbol if isolatedSymbol is not empty
if isIsolated && len(isolatedSymbol) > 0 {
symbol = isolatedSymbol

View File

@ -7,6 +7,7 @@ import (
sq "github.com/Masterminds/squirrel"
"github.com/jmoiron/sqlx"
"github.com/c9s/bbgo/pkg/exchange"
"github.com/c9s/bbgo/pkg/exchange/batch"
"github.com/c9s/bbgo/pkg/types"
)
@ -17,7 +18,7 @@ type DepositService struct {
// Sync syncs the withdraw records into db
func (s *DepositService) Sync(ctx context.Context, ex types.Exchange, startTime time.Time) error {
isMargin, isFutures, isIsolated, _ := getExchangeAttributes(ex)
isMargin, isFutures, isIsolated, _ := exchange.GetSessionAttributes(ex)
if isMargin || isFutures || isIsolated {
// only works in spot
return nil

View File

@ -10,6 +10,7 @@ import (
"github.com/jmoiron/sqlx"
log "github.com/sirupsen/logrus"
exchange2 "github.com/c9s/bbgo/pkg/exchange"
"github.com/c9s/bbgo/pkg/exchange/batch"
"github.com/c9s/bbgo/pkg/types"
)
@ -19,7 +20,7 @@ type OrderService struct {
}
func (s *OrderService) Sync(ctx context.Context, exchange types.Exchange, symbol string, startTime time.Time) error {
isMargin, isFutures, isIsolated, isolatedSymbol := getExchangeAttributes(exchange)
isMargin, isFutures, isIsolated, isolatedSymbol := exchange2.GetSessionAttributes(exchange)
// override symbol if isolatedSymbol is not empty
if isIsolated && len(isolatedSymbol) > 0 {
symbol = isolatedSymbol

View File

@ -10,6 +10,7 @@ import (
sq "github.com/Masterminds/squirrel"
"github.com/jmoiron/sqlx"
exchange2 "github.com/c9s/bbgo/pkg/exchange"
"github.com/c9s/bbgo/pkg/exchange/batch"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
@ -29,7 +30,7 @@ func (s *RewardService) Sync(ctx context.Context, exchange types.Exchange, start
return ErrExchangeRewardServiceNotImplemented
}
isMargin, isFutures, _, _ := getExchangeAttributes(exchange)
isMargin, isFutures, _, _ := exchange2.GetSessionAttributes(exchange)
if isMargin || isFutures {
return nil
}

View File

@ -11,6 +11,7 @@ import (
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
exchange2 "github.com/c9s/bbgo/pkg/exchange"
"github.com/c9s/bbgo/pkg/exchange/batch"
"github.com/c9s/bbgo/pkg/types"
)
@ -53,7 +54,7 @@ func NewTradeService(db *sqlx.DB) *TradeService {
}
func (s *TradeService) Sync(ctx context.Context, exchange types.Exchange, symbol string, startTime time.Time) error {
isMargin, isFutures, isIsolated, isolatedSymbol := getExchangeAttributes(exchange)
isMargin, isFutures, isIsolated, isolatedSymbol := exchange2.GetSessionAttributes(exchange)
// override symbol if isolatedSymbol is not empty
if isIsolated && len(isolatedSymbol) > 0 {
symbol = isolatedSymbol
@ -412,28 +413,3 @@ func SelectLastTrades(ex types.ExchangeName, symbol string, isMargin, isFutures,
Limit(limit)
}
func getExchangeAttributes(exchange types.Exchange) (isMargin, isFutures, isIsolated bool, isolatedSymbol string) {
if marginExchange, ok := exchange.(types.MarginExchange); ok {
marginSettings := marginExchange.GetMarginSettings()
isMargin = marginSettings.IsMargin
if isMargin {
isIsolated = marginSettings.IsIsolatedMargin
if marginSettings.IsIsolatedMargin {
isolatedSymbol = marginSettings.IsolatedMarginSymbol
}
}
}
if futuresExchange, ok := exchange.(types.FuturesExchange); ok {
futuresSettings := futuresExchange.GetFuturesSettings()
isFutures = futuresSettings.IsFutures
if isFutures {
isIsolated = futuresSettings.IsIsolatedFutures
if futuresSettings.IsIsolatedFutures {
isolatedSymbol = futuresSettings.IsolatedFuturesSymbol
}
}
}
return isMargin, isFutures, isIsolated, isolatedSymbol
}

View File

@ -7,6 +7,7 @@ import (
sq "github.com/Masterminds/squirrel"
"github.com/jmoiron/sqlx"
"github.com/c9s/bbgo/pkg/exchange"
"github.com/c9s/bbgo/pkg/exchange/batch"
"github.com/c9s/bbgo/pkg/types"
)
@ -17,7 +18,7 @@ type WithdrawService struct {
// Sync syncs the withdrawal records into db
func (s *WithdrawService) Sync(ctx context.Context, ex types.Exchange, startTime time.Time) error {
isMargin, isFutures, isIsolated, _ := getExchangeAttributes(ex)
isMargin, isFutures, isIsolated, _ := exchange.GetSessionAttributes(ex)
if isMargin || isFutures || isIsolated {
// only works in spot
return nil

View File

@ -2,10 +2,12 @@ package pivotshort
import (
"context"
"fmt"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/risk"
"github.com/c9s/bbgo/pkg/types"
)
@ -25,6 +27,7 @@ type BreakLow struct {
// limit sell price = breakLowPrice * (1 + BounceRatio)
BounceRatio fixedpoint.Value `json:"bounceRatio"`
Leverage fixedpoint.Value `json:"leverage"`
Quantity fixedpoint.Value `json:"quantity"`
StopEMARange fixedpoint.Value `json:"stopEMARange"`
StopEMA *types.IntervalWindow `json:"stopEMA"`
@ -63,8 +66,8 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener
position := orderExecutor.Position()
symbol := position.Symbol
store, _ := session.MarketDataStore(symbol)
standardIndicator, _ := session.StandardIndicatorSet(symbol)
store, _ := session.MarketDataStore(s.Symbol)
standardIndicator, _ := session.StandardIndicatorSet(s.Symbol)
s.lastLow = fixedpoint.Zero
@ -168,7 +171,15 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener
// graceful cancel all active orders
_ = orderExecutor.GracefulCancel(ctx)
quantity := s.useQuantityOrBaseBalance(s.Quantity)
quantity, err := useQuantityOrBaseBalance(s.session, s.Market, closePrice, s.Quantity, s.Leverage)
if err != nil {
log.WithError(err).Errorf("quantity calculation error")
}
if quantity.IsZero() {
return
}
if s.MarketOrder {
bbgo.Notify("%s price %f breaks the previous low %f with ratio %f, submitting market sell to open a short position", symbol, kline.Close.Float64(), previousLow.Float64(), s.Ratio.Float64())
_, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
@ -204,24 +215,70 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener
}
}
func (s *BreakLow) useQuantityOrBaseBalance(quantity fixedpoint.Value) fixedpoint.Value {
if s.session.Margin || s.session.IsolatedMargin || s.session.Futures || s.session.IsolatedFutures {
return quantity
func useQuantityOrBaseBalance(session *bbgo.ExchangeSession, market types.Market, price, quantity, leverage fixedpoint.Value) (fixedpoint.Value, error) {
usingLeverage := session.Margin || session.IsolatedMargin || session.Futures || session.IsolatedFutures
if usingLeverage {
if !quantity.IsZero() {
return quantity, nil
}
balance, hasBalance := s.session.Account.Balance(s.Market.BaseCurrency)
if leverage.IsZero() {
leverage = fixedpoint.NewFromInt(3)
}
// quantity is zero, we need to calculate the quantity
baseBalance, _ := session.Account.Balance(market.BaseCurrency)
quoteBalance, _ := session.Account.Balance(market.QuoteCurrency)
// calculate the quantity automatically
if session.Margin || session.IsolatedMargin {
baseBalanceValue := baseBalance.Total().Mul(price)
accountValue := baseBalanceValue.Add(quoteBalance.Total())
if session.IsolatedMargin {
originLeverage := leverage
leverage = fixedpoint.Max(leverage, fixedpoint.NewFromInt(10))
log.Infof("using isolated margin, maxLeverage=10 originalLeverage=%f currentLeverage=%f",
originLeverage.Float64(),
leverage.Float64())
}
// spot margin use the equity value, so we use the total quote balance here
maxPositionQuantity := risk.CalculateMaxPosition(price, accountValue, leverage)
log.Infof("margin leverage: calculated maxPositionQuantity=%f price=%f accountValue=%f %s leverage=%f",
maxPositionQuantity.Float64(),
price.Float64(),
accountValue.Float64(),
market.QuoteCurrency,
leverage.Float64())
return maxPositionQuantity, nil
}
if session.Futures || session.IsolatedFutures {
// TODO: get mark price here
maxPositionQuantity := risk.CalculateMaxPosition(price, quoteBalance.Available, leverage)
requiredPositionCost := risk.CalculatePositionCost(price, price, maxPositionQuantity, leverage, types.SideTypeSell)
if quoteBalance.Available.Compare(requiredPositionCost) < 0 {
return maxPositionQuantity, fmt.Errorf("available margin %f %s is not enough, can not submit order", quoteBalance.Available.Float64(), market.QuoteCurrency)
}
return maxPositionQuantity, nil
}
}
// For spot, we simply sell the base currency
balance, hasBalance := session.Account.Balance(market.BaseCurrency)
if hasBalance {
if quantity.IsZero() {
bbgo.Notify("sell quantity is not set, submitting sell with all base balance: %s", balance.Available.String())
log.Warnf("sell quantity is not set, submitting sell with all base balance: %s", balance.Available.String())
quantity = balance.Available
} else {
quantity = fixedpoint.Min(quantity, balance.Available)
}
}
if quantity.IsZero() {
log.Errorf("quantity is zero, can not submit sell order, please check settings")
}
return quantity
return quantity, fmt.Errorf("quantity is zero, can not submit sell order, please check your settings")
}

View File

@ -37,6 +37,10 @@ func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbg
s.session = session
s.orderExecutor = orderExecutor
s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol)
s.activeOrders.OnFilled(func(o types.Order) {
// reset resistance price
s.currentResistancePrice = fixedpoint.Zero
})
s.activeOrders.BindStream(session.UserDataStream)
if s.GroupDistance.IsZero() {
@ -112,7 +116,10 @@ func (s *ResistanceShort) updateResistanceOrders(closePrice fixedpoint.Value) {
ctx := context.Background()
resistanceUpdated := s.updateCurrentResistancePrice(closePrice)
if resistanceUpdated {
bbgo.Notify("%s Found next resistance price at %f, updating resistance order...", s.Symbol, s.currentResistancePrice.Float64())
bbgo.Notify("Found next %s resistance price at %f, updating resistance orders...", s.Symbol, s.currentResistancePrice.Float64())
s.placeResistanceOrders(ctx, s.currentResistancePrice)
} else if s.activeOrders.NumOfOrders() == 0 && !s.currentResistancePrice.IsZero() {
bbgo.Notify("There is no %s resistance open order, re-placing resistance orders at %f...", s.Symbol, s.currentResistancePrice.Float64())
s.placeResistanceOrders(ctx, s.currentResistancePrice)
}
}

View File

@ -155,7 +155,11 @@ type Strategy struct {
// pivot interval and window
types.IntervalWindow
Leverage fixedpoint.Value `json:"leverage"`
Quantity fixedpoint.Value `json:"quantity"`
// persistence fields
Position *types.Position `persistence:"position"`
ProfitStats *types.ProfitStats `persistence:"profit_stats"`
TradeStats *types.TradeStats `persistence:"trade_stats"`
@ -177,7 +181,6 @@ type Strategy struct {
bbgo.StrategyController
}
func (s *Strategy) ID() string {
return ID
}
@ -236,6 +239,11 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.TradeStats = types.NewTradeStats(s.Symbol)
}
if s.Leverage.IsZero() {
// the default leverage is 3x
s.Leverage = fixedpoint.NewFromInt(3)
}
// StrategyController
s.Status = types.StrategyStatusRunning