mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-26 16:55:15 +00:00
Merge pull request #794 from c9s/strategy/pivotshort
strategy/pivotshort: fix resistance updater
This commit is contained in:
commit
d5fce91fb6
|
@ -54,6 +54,7 @@ exchangeStrategies:
|
||||||
|
|
||||||
# minDistance is used to ignore the place that is too near to the current price
|
# minDistance is used to ignore the place that is too near to the current price
|
||||||
minDistance: 3%
|
minDistance: 3%
|
||||||
|
groupDistance: 1%
|
||||||
|
|
||||||
# ratio is the ratio of the resistance price,
|
# ratio is the ratio of the resistance price,
|
||||||
# higher the ratio, lower the price
|
# higher the ratio, lower the price
|
||||||
|
|
|
@ -38,6 +38,10 @@ type BreakLow struct {
|
||||||
session *bbgo.ExchangeSession
|
session *bbgo.ExchangeSession
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *BreakLow) Subscribe(session *bbgo.ExchangeSession) {
|
||||||
|
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) {
|
func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) {
|
||||||
s.session = session
|
s.session = session
|
||||||
s.orderExecutor = orderExecutor
|
s.orderExecutor = orderExecutor
|
||||||
|
@ -102,7 +106,6 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener
|
||||||
|
|
||||||
// we need the price cross the break line or we do nothing
|
// we need the price cross the break line or we do nothing
|
||||||
if !(openPrice.Compare(breakPrice) > 0 && closePrice.Compare(breakPrice) < 0) {
|
if !(openPrice.Compare(breakPrice) > 0 && closePrice.Compare(breakPrice) < 0) {
|
||||||
log.Infof("%s kline is not between the break low price %f", kline.Symbol, breakPrice.Float64())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,18 +17,19 @@ type ResistanceShort struct {
|
||||||
|
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
|
|
||||||
MinDistance fixedpoint.Value `json:"minDistance"`
|
MinDistance fixedpoint.Value `json:"minDistance"`
|
||||||
NumLayers int `json:"numLayers"`
|
GroupDistance fixedpoint.Value `json:"groupDistance"`
|
||||||
LayerSpread fixedpoint.Value `json:"layerSpread"`
|
NumLayers int `json:"numLayers"`
|
||||||
Quantity fixedpoint.Value `json:"quantity"`
|
LayerSpread fixedpoint.Value `json:"layerSpread"`
|
||||||
Ratio fixedpoint.Value `json:"ratio"`
|
Quantity fixedpoint.Value `json:"quantity"`
|
||||||
|
Ratio fixedpoint.Value `json:"ratio"`
|
||||||
|
|
||||||
session *bbgo.ExchangeSession
|
session *bbgo.ExchangeSession
|
||||||
orderExecutor *bbgo.GeneralOrderExecutor
|
orderExecutor *bbgo.GeneralOrderExecutor
|
||||||
|
|
||||||
resistancePivot *indicator.Pivot
|
resistancePivot *indicator.Pivot
|
||||||
resistancePrices []float64
|
resistancePrices []float64
|
||||||
nextResistancePrice fixedpoint.Value
|
currentResistancePrice fixedpoint.Value
|
||||||
|
|
||||||
activeOrders *bbgo.ActiveOrderBook
|
activeOrders *bbgo.ActiveOrderBook
|
||||||
}
|
}
|
||||||
|
@ -39,6 +40,10 @@ func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbg
|
||||||
s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol)
|
s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol)
|
||||||
s.activeOrders.BindStream(session.UserDataStream)
|
s.activeOrders.BindStream(session.UserDataStream)
|
||||||
|
|
||||||
|
if s.GroupDistance.IsZero() {
|
||||||
|
s.GroupDistance = fixedpoint.NewFromFloat(0.01)
|
||||||
|
}
|
||||||
|
|
||||||
store, _ := session.MarketDataStore(s.Symbol)
|
store, _ := session.MarketDataStore(s.Symbol)
|
||||||
|
|
||||||
s.resistancePivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow}
|
s.resistancePivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow}
|
||||||
|
@ -56,6 +61,7 @@ func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbg
|
||||||
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) {
|
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) {
|
||||||
position := s.orderExecutor.Position()
|
position := s.orderExecutor.Position()
|
||||||
if position.IsOpened(kline.Close) {
|
if position.IsOpened(kline.Close) {
|
||||||
|
log.Infof("position is already opened, skip placing resistance orders")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,26 +69,51 @@ func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbg
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ResistanceShort) findNextResistancePriceAndPlaceOrders(closePrice fixedpoint.Value) {
|
func tail(arr []float64, length int) []float64 {
|
||||||
// if the close price is still lower than the resistance price, then we don't have to update
|
if len(arr) < length {
|
||||||
if closePrice.Compare(s.nextResistancePrice) <= 0 {
|
return arr
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return arr[len(arr)-1-length:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ResistanceShort) updateNextResistancePrice(closePrice fixedpoint.Value) bool {
|
||||||
minDistance := s.MinDistance.Float64()
|
minDistance := s.MinDistance.Float64()
|
||||||
lows := s.resistancePivot.Lows
|
groupDistance := s.GroupDistance.Float64()
|
||||||
resistancePrices := findPossibleResistancePrices(closePrice.Float64(), minDistance, lows)
|
resistancePrices := findPossibleResistancePrices(closePrice.Float64()*(1.0+minDistance), groupDistance, tail(s.resistancePivot.Lows, 6))
|
||||||
|
|
||||||
log.Infof("last price: %f, possible resistance prices: %+v", closePrice.Float64(), resistancePrices)
|
if len(resistancePrices) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("%s close price: %f, min distance: %f, possible resistance prices: %+v", s.Symbol, closePrice.Float64(), minDistance, resistancePrices)
|
||||||
|
|
||||||
|
nextResistancePrice := fixedpoint.NewFromFloat(resistancePrices[0])
|
||||||
|
|
||||||
|
// if currentResistancePrice is not set or the close price is already higher than the current resistance price,
|
||||||
|
// we should update the resistance price
|
||||||
|
// if the detected resistance price is lower than the current one, we should also update it too
|
||||||
|
if s.currentResistancePrice.IsZero() {
|
||||||
|
s.currentResistancePrice = nextResistancePrice
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSellPrice := s.currentResistancePrice.Mul(one.Add(s.Ratio))
|
||||||
|
if closePrice.Compare(currentSellPrice) > 0 ||
|
||||||
|
nextResistancePrice.Compare(currentSellPrice) < 0 {
|
||||||
|
s.currentResistancePrice = nextResistancePrice
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ResistanceShort) findNextResistancePriceAndPlaceOrders(closePrice fixedpoint.Value) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if len(resistancePrices) > 0 {
|
resistanceUpdated := s.updateNextResistancePrice(closePrice)
|
||||||
nextResistancePrice := fixedpoint.NewFromFloat(resistancePrices[0])
|
if resistanceUpdated {
|
||||||
if nextResistancePrice.Compare(s.nextResistancePrice) != 0 {
|
bbgo.Notify("Found next resistance price: %f, updating resistance order...", s.currentResistancePrice.Float64())
|
||||||
bbgo.Notify("Found next resistance price: %f", nextResistancePrice.Float64())
|
s.placeResistanceOrders(ctx, s.currentResistancePrice)
|
||||||
s.nextResistancePrice = nextResistancePrice
|
|
||||||
s.placeResistanceOrders(ctx, nextResistancePrice)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,6 +139,7 @@ func (s *ResistanceShort) placeResistanceOrders(ctx context.Context, resistanceP
|
||||||
|
|
||||||
log.Infof("placing resistance orders: resistance price = %f, layer quantity = %f, num of layers = %d", resistancePrice.Float64(), quantity.Float64(), numLayers)
|
log.Infof("placing resistance orders: resistance price = %f, layer quantity = %f, num of layers = %d", resistancePrice.Float64(), quantity.Float64(), numLayers)
|
||||||
|
|
||||||
|
var sellPriceStart = resistancePrice.Mul(fixedpoint.One.Add(s.Ratio))
|
||||||
var orderForms []types.SubmitOrder
|
var orderForms []types.SubmitOrder
|
||||||
for i := 0; i < numLayers; i++ {
|
for i := 0; i < numLayers; i++ {
|
||||||
balances := s.session.GetAccount().Balances()
|
balances := s.session.GetAccount().Balances()
|
||||||
|
@ -116,13 +148,11 @@ func (s *ResistanceShort) placeResistanceOrders(ctx context.Context, resistanceP
|
||||||
_ = quoteBalance
|
_ = quoteBalance
|
||||||
_ = baseBalance
|
_ = 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)))
|
spread := layerSpread.Mul(fixedpoint.NewFromInt(int64(i)))
|
||||||
price = price.Add(spread)
|
price := sellPriceStart.Mul(one.Add(spread))
|
||||||
log.Infof("price = %f", price.Float64())
|
log.Infof("price = %f", price.Float64())
|
||||||
|
|
||||||
log.Infof("placing bounce short order #%d: price = %f, quantity = %f", i, price.Float64(), quantity.Float64())
|
log.Infof("placing resistance short order #%d: price = %f, quantity = %f", i, price.Float64(), quantity.Float64())
|
||||||
|
|
||||||
orderForms = append(orderForms, types.SubmitOrder{
|
orderForms = append(orderForms, types.SubmitOrder{
|
||||||
Symbol: s.Symbol,
|
Symbol: s.Symbol,
|
||||||
|
@ -150,27 +180,73 @@ func (s *ResistanceShort) placeResistanceOrders(ctx context.Context, resistanceP
|
||||||
s.activeOrders.Add(createdOrders...)
|
s.activeOrders.Add(createdOrders...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func findPossibleResistancePrices(closePrice float64, minDistance float64, lows []float64) []float64 {
|
func findPossibleSupportPrices(closePrice float64, minDistance float64, lows []float64) []float64 {
|
||||||
// sort float64 in increasing order
|
return group(lower(lows, closePrice), minDistance)
|
||||||
// lower to higher prices
|
}
|
||||||
sort.Float64s(lows)
|
|
||||||
|
|
||||||
var resistancePrices []float64
|
func lower(arr []float64, x float64) []float64 {
|
||||||
for _, low := range lows {
|
sort.Float64s(arr)
|
||||||
if low < closePrice {
|
|
||||||
|
var rst []float64
|
||||||
|
for _, a := range arr {
|
||||||
|
// filter prices that are lower than the current closed price
|
||||||
|
if a > x {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
last := closePrice
|
rst = append(rst, a)
|
||||||
if len(resistancePrices) > 0 {
|
|
||||||
last = resistancePrices[len(resistancePrices)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (low / last) < (1.0 + minDistance) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
resistancePrices = append(resistancePrices, low)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return resistancePrices
|
return rst
|
||||||
|
}
|
||||||
|
|
||||||
|
func higher(arr []float64, x float64) []float64 {
|
||||||
|
sort.Float64s(arr)
|
||||||
|
|
||||||
|
var rst []float64
|
||||||
|
for _, a := range arr {
|
||||||
|
// filter prices that are lower than the current closed price
|
||||||
|
if a < x {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rst = append(rst, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rst
|
||||||
|
}
|
||||||
|
|
||||||
|
func group(arr []float64, minDistance float64) []float64 {
|
||||||
|
if len(arr) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var groups []float64
|
||||||
|
var grp = []float64{arr[0]}
|
||||||
|
for _, price := range arr {
|
||||||
|
avg := average(grp)
|
||||||
|
if (price / avg) > (1.0 + minDistance) {
|
||||||
|
groups = append(groups, avg)
|
||||||
|
grp = []float64{price}
|
||||||
|
} else {
|
||||||
|
grp = append(grp, price)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(grp) > 0 {
|
||||||
|
groups = append(groups, average(grp))
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups
|
||||||
|
}
|
||||||
|
|
||||||
|
func findPossibleResistancePrices(closePrice float64, minDistance float64, lows []float64) []float64 {
|
||||||
|
return group(higher(lows, closePrice), minDistance)
|
||||||
|
}
|
||||||
|
|
||||||
|
func average(arr []float64) float64 {
|
||||||
|
s := 0.0
|
||||||
|
for _, a := range arr {
|
||||||
|
s += a
|
||||||
|
}
|
||||||
|
return s / float64(len(arr))
|
||||||
}
|
}
|
||||||
|
|
28
pkg/strategy/pivotshort/resistance_test.go
Normal file
28
pkg/strategy/pivotshort/resistance_test.go
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
package pivotshort
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_findPossibleResistancePrices(t *testing.T) {
|
||||||
|
prices := findPossibleResistancePrices(19000.0, 0.01, []float64{
|
||||||
|
23020.0,
|
||||||
|
23040.0,
|
||||||
|
23060.0,
|
||||||
|
|
||||||
|
24020.0,
|
||||||
|
24040.0,
|
||||||
|
24060.0,
|
||||||
|
})
|
||||||
|
assert.Equal(t, []float64{23035, 24040}, prices)
|
||||||
|
|
||||||
|
|
||||||
|
prices = findPossibleResistancePrices(19000.0, 0.01, []float64{
|
||||||
|
23020.0,
|
||||||
|
23040.0,
|
||||||
|
23060.0,
|
||||||
|
})
|
||||||
|
assert.Equal(t, []float64{23035}, prices)
|
||||||
|
}
|
|
@ -30,6 +30,69 @@ type IntervalWindowSetting struct {
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SupportTakeProfit struct {
|
||||||
|
Symbol string
|
||||||
|
types.IntervalWindow
|
||||||
|
Ratio fixedpoint.Value `json:"ratio"`
|
||||||
|
|
||||||
|
pivot *indicator.Pivot
|
||||||
|
orderExecutor *bbgo.GeneralOrderExecutor
|
||||||
|
session *bbgo.ExchangeSession
|
||||||
|
activeOrders *bbgo.ActiveOrderBook
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SupportTakeProfit) Subscribe(session *bbgo.ExchangeSession) {
|
||||||
|
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SupportTakeProfit) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) {
|
||||||
|
s.session = session
|
||||||
|
s.orderExecutor = orderExecutor
|
||||||
|
s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol)
|
||||||
|
|
||||||
|
position := orderExecutor.Position()
|
||||||
|
symbol := position.Symbol
|
||||||
|
store, _ := session.MarketDataStore(symbol)
|
||||||
|
s.pivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow}
|
||||||
|
s.pivot.Bind(store)
|
||||||
|
preloadPivot(s.pivot, store)
|
||||||
|
|
||||||
|
session.UserDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) {
|
||||||
|
supportPrices := findPossibleSupportPrices(kline.Close.Float64(), 0.1, s.pivot.Lows)
|
||||||
|
// supportPrices are sorted in decreasing order
|
||||||
|
if len(supportPrices) == 0 {
|
||||||
|
log.Infof("support prices not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !position.IsOpened(kline.Close) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nextSupport := fixedpoint.NewFromFloat(supportPrices[0])
|
||||||
|
buyPrice := nextSupport.Mul(one.Add(s.Ratio))
|
||||||
|
quantity := position.GetQuantity()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
if err := orderExecutor.GracefulCancelActiveOrderBook(ctx, s.activeOrders); err != nil {
|
||||||
|
log.WithError(err).Errorf("cancel order failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
createdOrders, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
|
||||||
|
Symbol: symbol,
|
||||||
|
Type: types.OrderTypeLimitMaker,
|
||||||
|
Price: buyPrice,
|
||||||
|
Quantity: quantity,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Errorf("can not submit orders: %+v", createdOrders)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.activeOrders.Add(createdOrders...)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
type Strategy struct {
|
type Strategy struct {
|
||||||
Environment *bbgo.Environment
|
Environment *bbgo.Environment
|
||||||
Symbol string `json:"symbol"`
|
Symbol string `json:"symbol"`
|
||||||
|
@ -49,6 +112,8 @@ type Strategy struct {
|
||||||
// ResistanceShort is one of the entry method
|
// ResistanceShort is one of the entry method
|
||||||
ResistanceShort *ResistanceShort `json:"resistanceShort"`
|
ResistanceShort *ResistanceShort `json:"resistanceShort"`
|
||||||
|
|
||||||
|
SupportTakeProfit *SupportTakeProfit `json:"supportTakeProfit"`
|
||||||
|
|
||||||
ExitMethods bbgo.ExitMethodSet `json:"exits"`
|
ExitMethods bbgo.ExitMethodSet `json:"exits"`
|
||||||
|
|
||||||
session *bbgo.ExchangeSession
|
session *bbgo.ExchangeSession
|
||||||
|
@ -73,6 +138,12 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
|
||||||
|
|
||||||
if s.BreakLow != nil {
|
if s.BreakLow != nil {
|
||||||
dynamic.InheritStructValues(s.BreakLow, s)
|
dynamic.InheritStructValues(s.BreakLow, s)
|
||||||
|
s.BreakLow.Subscribe(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.SupportTakeProfit != nil {
|
||||||
|
dynamic.InheritStructValues(s.SupportTakeProfit, s)
|
||||||
|
s.SupportTakeProfit.Subscribe(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !bbgo.IsBackTesting {
|
if !bbgo.IsBackTesting {
|
||||||
|
|
|
@ -218,6 +218,10 @@ type FuturesPosition struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPositionFromMarket(market Market) *Position {
|
func NewPositionFromMarket(market Market) *Position {
|
||||||
|
if len(market.BaseCurrency) == 0 || len(market.QuoteCurrency) == 0 {
|
||||||
|
panic("logical exception: missing market information, base currency or quote currency is empty")
|
||||||
|
}
|
||||||
|
|
||||||
return &Position{
|
return &Position{
|
||||||
Symbol: market.Symbol,
|
Symbol: market.Symbol,
|
||||||
BaseCurrency: market.BaseCurrency,
|
BaseCurrency: market.BaseCurrency,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user