Merge pull request #657 from c9s/fix-issue-642

fix: bollmaker: fix short position order
This commit is contained in:
Yo-An Lin 2022-06-02 19:28:25 +08:00 committed by GitHub
commit 1d85fac209
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 150 additions and 83 deletions

View File

@ -0,0 +1,84 @@
package bollmaker
import (
"github.com/pkg/errors"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
)
type DynamicSpreadSettings struct {
Enabled bool `json:"enabled"`
// Window is the window of the SMAs of spreads
Window int `json:"window"`
// AskSpreadScale is used to define the ask spread range with the given percentage.
AskSpreadScale *bbgo.PercentageScale `json:"askSpreadScale"`
// BidSpreadScale is used to define the bid spread range with the given percentage.
BidSpreadScale *bbgo.PercentageScale `json:"bidSpreadScale"`
DynamicAskSpread *indicator.SMA
DynamicBidSpread *indicator.SMA
}
// Update dynamic spreads
func (ds *DynamicSpreadSettings) Update(kline types.KLine) {
if !ds.Enabled {
return
}
ampl := (kline.GetHigh().Float64() - kline.GetLow().Float64()) / kline.GetOpen().Float64()
switch kline.Direction() {
case types.DirectionUp:
ds.DynamicAskSpread.Update(ampl)
ds.DynamicBidSpread.Update(0)
case types.DirectionDown:
ds.DynamicBidSpread.Update(ampl)
ds.DynamicAskSpread.Update(0)
default:
ds.DynamicAskSpread.Update(0)
ds.DynamicBidSpread.Update(0)
}
}
// GetAskSpread returns current ask spread
func (ds *DynamicSpreadSettings) GetAskSpread() (askSpread float64, err error) {
if !ds.Enabled {
return 0, errors.New("dynamic spread is not enabled")
}
if ds.AskSpreadScale != nil && ds.DynamicAskSpread.Length() >= ds.Window {
askSpread, err = ds.AskSpreadScale.Scale(ds.DynamicAskSpread.Last())
if err != nil {
log.WithError(err).Errorf("can not calculate dynamicAskSpread")
return 0, err
}
return askSpread, nil
}
return 0, errors.New("incomplete dynamic spread settings or not enough data yet")
}
// GetBidSpread returns current dynamic bid spread
func (ds *DynamicSpreadSettings) GetBidSpread() (bidSpread float64, err error) {
if !ds.Enabled {
return 0, errors.New("dynamic spread is not enabled")
}
if ds.BidSpreadScale != nil && ds.DynamicBidSpread.Length() >= ds.Window {
bidSpread, err = ds.BidSpreadScale.Scale(ds.DynamicBidSpread.Last())
if err != nil {
log.WithError(err).Errorf("can not calculate dynamicBidSpread")
return 0, err
}
return bidSpread, nil
}
return 0, errors.New("incomplete dynamic spread settings or not enough data yet")
}

View File

@ -49,81 +49,6 @@ type BollingerSetting struct {
BandWidth float64 `json:"bandWidth"`
}
type DynamicSpreadSettings struct {
Enabled bool `json:"enabled"`
// Window is the window of the SMAs of spreads
Window int `json:"window"`
// AskSpreadScale is used to define the ask spread range with the given percentage.
AskSpreadScale *bbgo.PercentageScale `json:"askSpreadScale"`
// BidSpreadScale is used to define the bid spread range with the given percentage.
BidSpreadScale *bbgo.PercentageScale `json:"bidSpreadScale"`
DynamicAskSpread *indicator.SMA
DynamicBidSpread *indicator.SMA
}
// Update dynamic spreads
func (ds *DynamicSpreadSettings) Update(kline types.KLine) {
if !ds.Enabled {
return
}
ampl := (kline.GetHigh().Float64() - kline.GetLow().Float64()) / kline.GetOpen().Float64()
switch kline.Direction() {
case types.DirectionUp:
ds.DynamicAskSpread.Update(ampl)
ds.DynamicBidSpread.Update(0)
case types.DirectionDown:
ds.DynamicBidSpread.Update(ampl)
ds.DynamicAskSpread.Update(0)
default:
ds.DynamicAskSpread.Update(0)
ds.DynamicBidSpread.Update(0)
}
}
// GetAskSpread returns current ask spread
func (ds *DynamicSpreadSettings) GetAskSpread() (askSpread float64, err error) {
if !ds.Enabled {
return 0, errors.New("dynamic spread is not enabled")
}
if ds.AskSpreadScale != nil && ds.DynamicAskSpread.Length() >= ds.Window {
askSpread, err = ds.AskSpreadScale.Scale(ds.DynamicAskSpread.Last())
if err != nil {
log.WithError(err).Errorf("can not calculate dynamicAskSpread")
return 0, err
}
return askSpread, nil
}
return 0, errors.New("incomplete dynamic spread settings or not enough data yet")
}
// GetBidSpread returns current dynamic bid spread
func (ds *DynamicSpreadSettings) GetBidSpread() (bidSpread float64, err error) {
if !ds.Enabled {
return 0, errors.New("dynamic spread is not enabled")
}
if ds.BidSpreadScale != nil && ds.DynamicBidSpread.Length() >= ds.Window {
bidSpread, err = ds.BidSpreadScale.Scale(ds.DynamicBidSpread.Last())
if err != nil {
log.WithError(err).Errorf("can not calculate dynamicBidSpread")
return 0, err
}
return bidSpread, nil
}
return 0, errors.New("incomplete dynamic spread settings or not enough data yet")
}
type Strategy struct {
*bbgo.Graceful
*bbgo.Notifiability
@ -408,18 +333,22 @@ func (s *Strategy) placeOrders(ctx context.Context, orderExecutor bbgo.OrderExec
downBand := s.defaultBoll.LastDownBand()
upBand := s.defaultBoll.LastUpBand()
sma := s.defaultBoll.LastSMA()
log.Infof("bollinger band: up %f sma %f down %f", upBand, sma, downBand)
log.Infof("%s bollinger band: up %f sma %f down %f", s.Symbol, upBand, sma, downBand)
bandPercentage := calculateBandPercentage(upBand, downBand, sma, midPrice.Float64())
log.Infof("mid price band percentage: %v", bandPercentage)
log.Infof("%s mid price band percentage: %v", s.Symbol, bandPercentage)
maxExposurePosition, err := s.getCurrentAllowedExposurePosition(bandPercentage)
if err != nil {
log.WithError(err).Errorf("can not calculate CurrentAllowedExposurePosition")
log.WithError(err).Errorf("can not calculate %s CurrentAllowedExposurePosition", s.Symbol)
return
}
log.Infof("calculated max exposure position: %v", maxExposurePosition)
log.Infof("calculated %s max exposure position: %v", s.Symbol, maxExposurePosition)
if !s.Position.IsClosed() && !s.Position.IsDust(midPrice) {
log.Infof("current %s unrealized profit: %f %s", s.Symbol, s.Position.UnrealizedProfit(midPrice).Float64(), s.Market.QuoteCurrency)
}
canSell := true
canBuy := true
@ -429,7 +358,7 @@ func (s *Strategy) placeOrders(ctx context.Context, orderExecutor bbgo.OrderExec
}
if maxExposurePosition.Sign() > 0 {
if s.Long != nil && *s.Long && base.Sign() < 0 {
if s.hasLongSet() && base.Sign() < 0 {
canSell = false
} else if base.Compare(maxExposurePosition.Neg()) < 0 {
canSell = false
@ -502,11 +431,26 @@ func (s *Strategy) placeOrders(ctx context.Context, orderExecutor bbgo.OrderExec
canSell = false
}
if midPrice.Compare(s.Position.AverageCost.Mul(fixedpoint.One.Add(s.MinProfitSpread))) < 0 {
canSell = false
isLongPosition := s.Position.IsLong()
isShortPosition := s.Position.IsShort()
minProfitPrice := s.Position.AverageCost.Mul(fixedpoint.One.Add(s.MinProfitSpread))
if isShortPosition {
minProfitPrice = s.Position.AverageCost.Mul(fixedpoint.One.Sub(s.MinProfitSpread))
}
if s.Long != nil && *s.Long && base.Sub(sellOrder.Quantity).Sign() < 0 {
if isLongPosition {
// for long position if the current price is lower than the minimal profitable price then we should stop sell
if midPrice.Compare(minProfitPrice) < 0 {
canSell = false
}
} else if isShortPosition {
// for short position if the current price is higher than the minimal profitable price then we should stop buy
if midPrice.Compare(minProfitPrice) > 0 {
canBuy = false
}
}
if s.hasLongSet() && base.Sub(sellOrder.Quantity).Sign() < 0 {
canSell = false
}
@ -544,6 +488,14 @@ func (s *Strategy) placeOrders(ctx context.Context, orderExecutor bbgo.OrderExec
s.activeMakerOrders.Add(createdOrders...)
}
func (s *Strategy) hasLongSet() bool {
return s.Long != nil && *s.Long
}
func (s *Strategy) hasShortSet() bool {
return s.Short != nil && *s.Short
}
type PriceTrend string
const (

View File

@ -144,6 +144,13 @@ func (p *Position) NewClosePositionOrder(percentage fixedpoint.Value) *SubmitOrd
}
}
func (p *Position) IsDust(price fixedpoint.Value) bool {
base := p.GetBase()
return p.Market.IsDustQuantity(base, price)
}
// GetBase locks the mutex and return the base quantity
// The base quantity can be negative
func (p *Position) GetBase() (base fixedpoint.Value) {
p.Lock()
base = p.Base
@ -151,6 +158,18 @@ func (p *Position) GetBase() (base fixedpoint.Value) {
return base
}
func (p *Position) UnrealizedProfit(price fixedpoint.Value) fixedpoint.Value {
base := p.GetBase()
if p.IsLong() {
return price.Sub(p.AverageCost).Mul(base)
} else if p.IsShort() {
return p.AverageCost.Sub(price).Mul(base)
}
return fixedpoint.Zero
}
type FuturesPosition struct {
Symbol string `json:"symbol"`
BaseCurrency string `json:"baseCurrency"`
@ -221,6 +240,18 @@ func (p *Position) SetExchangeFeeRate(ex ExchangeName, exchangeFee ExchangeFee)
p.ExchangeFeeRates[ex] = exchangeFee
}
func (p *Position) IsShort() bool {
return p.Base.Sign() < 0
}
func (p *Position) IsLong() bool {
return p.Base.Sign() > 0
}
func (p *Position) IsClosed() bool {
return p.Base.Sign() == 0
}
func (p *Position) Type() PositionType {
if p.Base.Sign() > 0 {
return PositionLong