diff --git a/pkg/exchange/util.go b/pkg/exchange/util.go new file mode 100644 index 000000000..639103722 --- /dev/null +++ b/pkg/exchange/util.go @@ -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 +} diff --git a/pkg/service/backtest.go b/pkg/service/backtest.go index 256514fcf..7b43f8d03 100644 --- a/pkg/service/backtest.go +++ b/pkg/service/backtest.go @@ -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 diff --git a/pkg/service/deposit.go b/pkg/service/deposit.go index b892cdb81..6b49cce13 100644 --- a/pkg/service/deposit.go +++ b/pkg/service/deposit.go @@ -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 diff --git a/pkg/service/order.go b/pkg/service/order.go index 4780bfc0d..8cdff47ec 100644 --- a/pkg/service/order.go +++ b/pkg/service/order.go @@ -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 diff --git a/pkg/service/reward.go b/pkg/service/reward.go index ef28f0ef8..bf657a601 100644 --- a/pkg/service/reward.go +++ b/pkg/service/reward.go @@ -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 } diff --git a/pkg/service/trade.go b/pkg/service/trade.go index cc7e1480e..ae8379fa0 100644 --- a/pkg/service/trade.go +++ b/pkg/service/trade.go @@ -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 -} diff --git a/pkg/service/withdraw.go b/pkg/service/withdraw.go index af0a0df83..f8448d0e0 100644 --- a/pkg/service/withdraw.go +++ b/pkg/service/withdraw.go @@ -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 diff --git a/pkg/strategy/pivotshort/breaklow.go b/pkg/strategy/pivotshort/breaklow.go index 84678a0bf..753caa6f9 100644 --- a/pkg/strategy/pivotshort/breaklow.go +++ b/pkg/strategy/pivotshort/breaklow.go @@ -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 + } + + 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 + } + } - balance, hasBalance := s.session.Account.Balance(s.Market.BaseCurrency) + // 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") } diff --git a/pkg/strategy/pivotshort/resistance.go b/pkg/strategy/pivotshort/resistance.go index 8e35b72a5..760c76f3a 100644 --- a/pkg/strategy/pivotshort/resistance.go +++ b/pkg/strategy/pivotshort/resistance.go @@ -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) } } diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index 8756cbd5d..9711cd99f 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -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