pivotshort: refactor ResistanceShort entry method

This commit is contained in:
c9s 2022-06-30 18:29:02 +08:00
parent bfcbf8566e
commit 3e6b975c2c
No known key found for this signature in database
GPG Key ID: 7385E7E464CB0A54
6 changed files with 144 additions and 130 deletions

View File

@ -46,12 +46,14 @@ exchangeStrategies:
window: 99
resistanceShort:
enabled: false
enabled: true
interval: 1h
window: 10
window: 8
quantity: 10.0
# minDistance is used to ignore the place that is too near to the current price
minDistance: 3%
# stopLossPercentage: 1%
# ratio is the ratio of the resistance price,
# higher the ratio, lower the price

View File

@ -44,7 +44,7 @@ func (m *ExitMethod) Inherit(parent interface{}) {
continue
}
dynamic.MergeStructValues(fieldValue.Interface(), parent)
dynamic.InheritStructValues(fieldValue.Interface(), parent)
}
}

View File

@ -2,9 +2,9 @@ package dynamic
import "reflect"
// MergeStructValues merges the field value from the source struct to the dest struct.
// InheritStructValues merges the field value from the source struct to the dest struct.
// Only fields with the same type and the same name will be updated.
func MergeStructValues(dst, src interface{}) {
func InheritStructValues(dst, src interface{}) {
if dst == nil {
return
}

View File

@ -21,14 +21,14 @@ func Test_reflectMergeStructFields(t *testing.T) {
t.Run("zero value", func(t *testing.T) {
a := &TestStrategy{Symbol: "BTCUSDT"}
b := &struct{ Symbol string }{Symbol: ""}
MergeStructValues(b, a)
InheritStructValues(b, a)
assert.Equal(t, "BTCUSDT", b.Symbol)
})
t.Run("non-zero value", func(t *testing.T) {
a := &TestStrategy{Symbol: "BTCUSDT"}
b := &struct{ Symbol string }{Symbol: "ETHUSDT"}
MergeStructValues(b, a)
InheritStructValues(b, a)
assert.Equal(t, "ETHUSDT", b.Symbol, "should be the original value")
})
@ -45,7 +45,7 @@ func Test_reflectMergeStructFields(t *testing.T) {
Symbol string
types.IntervalWindow
}{}
MergeStructValues(b, a)
InheritStructValues(b, a)
assert.Equal(t, iw, b.IntervalWindow)
assert.Equal(t, "BTCUSDT", b.Symbol)
})
@ -62,7 +62,7 @@ func Test_reflectMergeStructFields(t *testing.T) {
}{
IntervalWindow: types.IntervalWindow{Interval: types.Interval5m, Window: 9},
}
MergeStructValues(b, a)
InheritStructValues(b, a)
assert.Equal(t, types.IntervalWindow{Interval: types.Interval5m, Window: 9}, b.IntervalWindow)
})
@ -75,7 +75,7 @@ func Test_reflectMergeStructFields(t *testing.T) {
b := &struct {
A string
}{}
MergeStructValues(b, a)
InheritStructValues(b, a)
assert.Equal(t, "", b.A)
assert.Equal(t, 1.99, a.A)
})

View File

@ -10,6 +10,7 @@ import (
"github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/dynamic"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
@ -48,7 +49,9 @@ type BreakLow struct {
}
type ResistanceShort struct {
Enabled bool `json:"enabled"`
Enabled bool `json:"enabled"`
Symbol string `json:"-"`
Market types.Market `json:"-"`
types.IntervalWindow
@ -57,15 +60,126 @@ type ResistanceShort struct {
LayerSpread fixedpoint.Value `json:"layerSpread"`
Quantity fixedpoint.Value `json:"quantity"`
Ratio fixedpoint.Value `json:"ratio"`
session *bbgo.ExchangeSession
orderExecutor *bbgo.GeneralOrderExecutor
resistancePivot *indicator.Pivot
resistancePrices []float64
nextResistancePrice fixedpoint.Value
resistanceOrders []types.Order
}
type Entry struct {
CatBounceRatio fixedpoint.Value `json:"catBounceRatio"`
NumLayers int `json:"numLayers"`
TotalQuantity fixedpoint.Value `json:"totalQuantity"`
func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) {
s.session = session
s.orderExecutor = orderExecutor
Quantity fixedpoint.Value `json:"quantity"`
MarginSideEffect types.MarginOrderSideEffectType `json:"marginOrderSideEffect"`
position := orderExecutor.Position()
symbol := position.Symbol
store, _ := session.MarketDataStore(symbol)
s.resistancePivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow}
s.resistancePivot.Bind(store)
// preload history kline data to the resistance pivot indicator
// we use the last kline to find the higher lows
lastKLine := preloadPivot(s.resistancePivot, store)
// use the last kline from the history before we get the next closed kline
s.findNextResistancePriceAndPlaceOrders(lastKLine.Close)
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
if kline.Symbol != s.Symbol || kline.Interval != s.Interval {
return
}
s.findNextResistancePriceAndPlaceOrders(kline.Close)
})
}
func (s *ResistanceShort) findNextResistancePriceAndPlaceOrders(closePrice fixedpoint.Value) {
position := s.orderExecutor.Position()
if position.IsOpened(closePrice) {
return
}
minDistance := s.MinDistance.Float64()
lows := s.resistancePivot.Lows
resistancePrices := findPossibleResistancePrices(closePrice.Float64(), minDistance, lows)
log.Infof("last price: %f, possible resistance prices: %+v", closePrice.Float64(), resistancePrices)
ctx := context.Background()
if len(resistancePrices) > 0 {
nextResistancePrice := fixedpoint.NewFromFloat(resistancePrices[0])
if nextResistancePrice.Compare(s.nextResistancePrice) != 0 {
s.nextResistancePrice = nextResistancePrice
s.placeResistanceOrders(ctx, nextResistancePrice)
}
}
}
func (s *ResistanceShort) placeResistanceOrders(ctx context.Context, resistancePrice fixedpoint.Value) {
futuresMode := s.session.Futures || s.session.IsolatedFutures
_ = futuresMode
totalQuantity := s.Quantity
numLayers := s.NumLayers
if numLayers == 0 {
numLayers = 1
}
numLayersF := fixedpoint.NewFromInt(int64(numLayers))
layerSpread := s.LayerSpread
quantity := totalQuantity.Div(numLayersF)
if err := s.orderExecutor.CancelOrders(ctx, s.resistanceOrders...); err != nil {
log.WithError(err).Errorf("can not cancel resistance orders: %+v", s.resistanceOrders)
}
s.resistanceOrders = nil
log.Infof("placing resistance orders: resistance price = %f, layer quantity = %f, num of layers = %d", resistancePrice.Float64(), quantity.Float64(), numLayers)
var orderForms []types.SubmitOrder
for i := 0; i < numLayers; i++ {
balances := s.session.GetAccount().Balances()
quoteBalance := balances[s.Market.QuoteCurrency]
baseBalance := balances[s.Market.BaseCurrency]
_ = quoteBalance
_ = baseBalance
// price = (resistance_price * (1.0 + ratio)) * ((1.0 + layerSpread) * i)
price := resistancePrice.Mul(fixedpoint.One.Add(s.Ratio))
spread := layerSpread.Mul(fixedpoint.NewFromInt(int64(i)))
price = price.Add(spread)
log.Infof("price = %f", price.Float64())
log.Infof("placing bounce short order #%d: price = %f, quantity = %f", i, price.Float64(), quantity.Float64())
orderForms = append(orderForms, types.SubmitOrder{
Symbol: s.Symbol,
Side: types.SideTypeSell,
Type: types.OrderTypeLimitMaker,
Price: price,
Quantity: quantity,
Tag: "resistanceShort",
})
// TODO: fix futures mode later
/*
if futuresMode {
if quantity.Mul(price).Compare(quoteBalance.Available) <= 0 {
}
}
*/
}
createdOrders, err := s.orderExecutor.SubmitOrders(ctx, orderForms...)
if err != nil {
log.WithError(err).Errorf("can not place resistance order")
}
s.resistanceOrders = createdOrders
}
type Strategy struct {
@ -81,11 +195,12 @@ type Strategy struct {
ProfitStats *types.ProfitStats `persistence:"profit_stats"`
TradeStats *types.TradeStats `persistence:"trade_stats"`
// BreakLow is one of the entry method
BreakLow BreakLow `json:"breakLow"`
// ResistanceShort is one of the entry method
ResistanceShort *ResistanceShort `json:"resistanceShort"`
Entry Entry `json:"entry"`
ExitMethods bbgo.ExitMethodSet `json:"exits"`
session *bbgo.ExchangeSession
@ -96,7 +211,6 @@ type Strategy struct {
resistancePivot *indicator.Pivot
stopEWMA *indicator.EWMA
pivotLowPrices []fixedpoint.Value
resistancePrices []float64
currentBounceShortPrice fixedpoint.Value
// StrategyController
@ -112,6 +226,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m})
if s.ResistanceShort != nil && s.ResistanceShort.Enabled {
dynamic.InheritStructValues(s.ResistanceShort, s)
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.ResistanceShort.Interval})
}
@ -182,8 +297,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.pivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow}
s.pivot.Bind(store)
lastKLine := preloadPivot(s.pivot, store)
preloadPivot(s.pivot, store)
// update pivot low data
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
@ -204,11 +318,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.pivotLowPrices = append(s.pivotLowPrices, s.lastLow)
})
if s.ResistanceShort != nil && s.ResistanceShort.Enabled {
s.resistancePivot = &indicator.Pivot{IntervalWindow: s.ResistanceShort.IntervalWindow}
s.resistancePivot.Bind(store)
}
if s.BreakLow.StopEMA != nil {
s.stopEWMA = standardIndicator.EWMA(*s.BreakLow.StopEMA)
}
@ -218,35 +327,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
}
if s.ResistanceShort != nil && s.ResistanceShort.Enabled {
if s.resistancePivot != nil {
preloadPivot(s.resistancePivot, store)
}
session.UserDataStream.OnStart(func() {
if lastKLine == nil {
return
}
if s.resistancePivot != nil {
lows := s.resistancePivot.Lows
minDistance := s.ResistanceShort.MinDistance.Float64()
closePrice := lastKLine.Close.Float64()
s.resistancePrices = findPossibleResistancePrices(closePrice, minDistance, lows)
log.Infof("last price: %f, possible resistance prices: %+v", closePrice, s.resistancePrices)
if len(s.resistancePrices) > 0 {
resistancePrice := fixedpoint.NewFromFloat(s.resistancePrices[0])
if resistancePrice.Compare(s.currentBounceShortPrice) != 0 {
log.Infof("updating resistance price... possible resistance prices: %+v", s.resistancePrices)
_ = s.orderExecutor.GracefulCancel(ctx)
s.currentBounceShortPrice = resistancePrice
s.placeBounceSellOrders(ctx, s.currentBounceShortPrice)
}
}
}
})
s.ResistanceShort.Bind(session, s.orderExecutor)
}
// Always check whether you can open a short position or not
@ -321,40 +402,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
}
})
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
// StrategyController
if s.Status != types.StrategyStatusRunning {
return
}
if s.ResistanceShort == nil || !s.ResistanceShort.Enabled {
return
}
if kline.Symbol != s.Symbol || kline.Interval != s.ResistanceShort.Interval {
return
}
if s.resistancePivot != nil {
closePrice := kline.Close.Float64()
minDistance := s.ResistanceShort.MinDistance.Float64()
lows := s.resistancePivot.Lows
s.resistancePrices = findPossibleResistancePrices(closePrice, minDistance, lows)
if len(s.resistancePrices) > 0 {
resistancePrice := fixedpoint.NewFromFloat(s.resistancePrices[0])
if resistancePrice.Compare(s.currentBounceShortPrice) != 0 {
log.Infof("updating resistance price... possible resistance prices: %+v", s.resistancePrices)
_ = s.orderExecutor.GracefulCancel(ctx)
s.currentBounceShortPrice = resistancePrice
s.placeBounceSellOrders(ctx, s.currentBounceShortPrice)
}
}
}
})
if !bbgo.IsBackTesting {
// use market trade to submit short order
session.MarketDataStream.OnMarketTrade(func(trade types.Trade) {
@ -380,46 +427,6 @@ func (s *Strategy) findHigherPivotLow(price fixedpoint.Value) (fixedpoint.Value,
return price, false
}
func (s *Strategy) placeBounceSellOrders(ctx context.Context, resistancePrice fixedpoint.Value) {
futuresMode := s.session.Futures || s.session.IsolatedFutures
totalQuantity := s.ResistanceShort.Quantity
numLayers := s.ResistanceShort.NumLayers
if numLayers == 0 {
numLayers = 1
}
numLayersF := fixedpoint.NewFromInt(int64(numLayers))
layerSpread := s.ResistanceShort.LayerSpread
quantity := totalQuantity.Div(numLayersF)
log.Infof("placing bounce short orders: resistance price = %f, layer quantity = %f, num of layers = %d", resistancePrice.Float64(), quantity.Float64(), numLayers)
for i := 0; i < numLayers; i++ {
balances := s.session.GetAccount().Balances()
quoteBalance := balances[s.Market.QuoteCurrency]
baseBalance := balances[s.Market.BaseCurrency]
// price = (resistance_price * (1.0 + ratio)) * ((1.0 + layerSpread) * i)
price := resistancePrice.Mul(fixedpoint.One.Add(s.ResistanceShort.Ratio))
spread := layerSpread.Mul(fixedpoint.NewFromInt(int64(i)))
price = price.Add(spread)
log.Infof("price = %f", price.Float64())
log.Infof("placing bounce short order #%d: price = %f, quantity = %f", i, price.Float64(), quantity.Float64())
if futuresMode {
if quantity.Mul(price).Compare(quoteBalance.Available) <= 0 {
s.placeOrder(ctx, price, quantity)
}
} else {
if quantity.Compare(baseBalance.Available) <= 0 {
s.placeOrder(ctx, price, quantity)
}
}
}
}
func (s *Strategy) placeOrder(ctx context.Context, price fixedpoint.Value, quantity fixedpoint.Value) {
_, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
Symbol: s.Symbol,
@ -474,6 +481,7 @@ func (s *Strategy) placeMarketSell(ctx context.Context, quantity fixedpoint.Valu
func findPossibleResistancePrices(closePrice float64, minDistance float64, lows []float64) []float64 {
// sort float64 in increasing order
// lower to higher prices
sort.Float64s(lows)
var resistancePrices []float64

View File

@ -273,6 +273,10 @@ func (p *Position) IsClosed() bool {
return p.Base.Sign() == 0
}
func (p *Position) IsOpened(currentPrice fixedpoint.Value) bool {
return p.IsClosed() || !p.IsDust(currentPrice)
}
func (p *Position) Type() PositionType {
if p.Base.Sign() > 0 {
return PositionLong