mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-25 16:25:16 +00:00
Merge pull request #657 from c9s/fix-issue-642
fix: bollmaker: fix short position order
This commit is contained in:
commit
1d85fac209
84
pkg/strategy/bollmaker/dynamic_spread.go
Normal file
84
pkg/strategy/bollmaker/dynamic_spread.go
Normal 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")
|
||||
}
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user