Merge pull request #786 from c9s/strategy/pivotshort

strategy: pivotshort: resistance short
This commit is contained in:
Yo-An Lin 2022-06-30 21:54:47 +08:00 committed by GitHub
commit c8a4af99bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 672 additions and 388 deletions

View File

@ -39,18 +39,21 @@ exchangeStrategies:
# stopEMARange is the price range we allow short. # stopEMARange is the price range we allow short.
# Short-allowed price range = [current price] > [EMA] * (1 - [stopEMARange]) # Short-allowed price range = [current price] > [EMA] * (1 - [stopEMARange])
stopEMARange: 0% # Higher the stopEMARange than higher the chance to open a short
stopEMARange: 2%
stopEMA: stopEMA:
interval: 1h interval: 1h
window: 99 window: 99
bounceShort: resistanceShort:
enabled: false enabled: true
interval: 1h interval: 1h
window: 10 window: 8
quantity: 10.0 quantity: 10.0
# minDistance is used to ignore the place that is too near to the current price
minDistance: 3% minDistance: 3%
# stopLossPercentage: 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
@ -86,12 +89,15 @@ exchangeStrategies:
# you can grab a simple stats by the following SQL: # you can grab a simple stats by the following SQL:
# SELECT ((close - low) / close) AS shadow_ratio FROM binance_klines WHERE symbol = 'ETHUSDT' AND `interval` = '5m' AND start_time > '2022-01-01' ORDER BY shadow_ratio DESC LIMIT 20; # SELECT ((close - low) / close) AS shadow_ratio FROM binance_klines WHERE symbol = 'ETHUSDT' AND `interval` = '5m' AND start_time > '2022-01-01' ORDER BY shadow_ratio DESC LIMIT 20;
- lowerShadowTakeProfit: - lowerShadowTakeProfit:
interval: 30m
window: 99
ratio: 3% ratio: 3%
# (5) cumulatedVolumeTakeProfit is used to take profit when the cumulated quote volume from the klines exceeded a threshold # (5) cumulatedVolumeTakeProfit is used to take profit when the cumulated quote volume from the klines exceeded a threshold
- cumulatedVolumeTakeProfit: - cumulatedVolumeTakeProfit:
minQuoteVolume: 100_000_000 interval: 5m
window: 2 window: 2
minQuoteVolume: 200_000_000
backtest: backtest:
sessions: sessions:

View File

@ -13,6 +13,7 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/c9s/bbgo/pkg/datatype" "github.com/c9s/bbgo/pkg/datatype"
"github.com/c9s/bbgo/pkg/dynamic"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/service" "github.com/c9s/bbgo/pkg/service"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
@ -387,7 +388,7 @@ func (c *Config) GetSignature() string {
id := strategy.ID() id := strategy.ID()
ps = append(ps, id) ps = append(ps, id)
if symbol, ok := isSymbolBasedStrategy(reflect.ValueOf(strategy)); ok { if symbol, ok := dynamic.LookupSymbolField(reflect.ValueOf(strategy)); ok {
ps = append(ps, symbol) ps = append(ps, symbol)
} }
} }

View File

@ -3,9 +3,23 @@ package bbgo
import ( import (
"reflect" "reflect"
"github.com/c9s/bbgo/pkg/types" "github.com/pkg/errors"
"github.com/c9s/bbgo/pkg/dynamic"
) )
type ExitMethodSet []ExitMethod
func (s *ExitMethodSet) SetAndSubscribe(session *ExchangeSession, parent interface{}) {
for i := range *s {
m := (*s)[i]
// manually inherit configuration from strategy
m.Inherit(parent)
m.Subscribe(session)
}
}
type ExitMethod struct { type ExitMethod struct {
RoiStopLoss *RoiStopLoss `json:"roiStopLoss"` RoiStopLoss *RoiStopLoss `json:"roiStopLoss"`
ProtectiveStopLoss *ProtectiveStopLoss `json:"protectiveStopLoss"` ProtectiveStopLoss *ProtectiveStopLoss `json:"protectiveStopLoss"`
@ -14,35 +28,50 @@ type ExitMethod struct {
CumulatedVolumeTakeProfit *CumulatedVolumeTakeProfit `json:"cumulatedVolumeTakeProfit"` CumulatedVolumeTakeProfit *CumulatedVolumeTakeProfit `json:"cumulatedVolumeTakeProfit"`
} }
func (m *ExitMethod) Subscribe(session *ExchangeSession) { // Inherit is used for inheriting properties from the given strategy struct
// TODO: pull out this implementation as a simple function to reflect.go // for example, some exit method requires the default interval and symbol name from the strategy param object
rv := reflect.ValueOf(m) func (m *ExitMethod) Inherit(parent interface{}) {
rt := reflect.TypeOf(m) // we need to pass some information from the strategy configuration to the exit methods, like symbol, interval and window
rt := reflect.TypeOf(m).Elem()
rv = rv.Elem() rv := reflect.ValueOf(m).Elem()
rt = rt.Elem() for j := 0; j < rv.NumField(); j++ {
infType := reflect.TypeOf((*types.Subscriber)(nil)).Elem() if !rt.Field(j).IsExported() {
continue
argValues := toReflectValues(session)
for i := 0; i < rt.NumField(); i++ {
fieldType := rt.Field(i)
if fieldType.Type.Implements(infType) {
method := rv.Field(i).MethodByName("Subscribe")
method.Call(argValues)
} }
fieldValue := rv.Field(j)
if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() {
continue
}
dynamic.InheritStructValues(fieldValue.Interface(), parent)
}
}
func (m *ExitMethod) Subscribe(session *ExchangeSession) {
if err := dynamic.CallStructFieldsMethod(m, "Subscribe", session); err != nil {
panic(errors.Wrap(err, "dynamic Subscribe call failed"))
} }
} }
func (m *ExitMethod) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) { func (m *ExitMethod) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) {
if m.ProtectiveStopLoss != nil { if m.ProtectiveStopLoss != nil {
m.ProtectiveStopLoss.Bind(session, orderExecutor) m.ProtectiveStopLoss.Bind(session, orderExecutor)
} else if m.RoiStopLoss != nil { }
if m.RoiStopLoss != nil {
m.RoiStopLoss.Bind(session, orderExecutor) m.RoiStopLoss.Bind(session, orderExecutor)
} else if m.RoiTakeProfit != nil { }
if m.RoiTakeProfit != nil {
m.RoiTakeProfit.Bind(session, orderExecutor) m.RoiTakeProfit.Bind(session, orderExecutor)
} else if m.LowerShadowTakeProfit != nil { }
if m.LowerShadowTakeProfit != nil {
m.LowerShadowTakeProfit.Bind(session, orderExecutor) m.LowerShadowTakeProfit.Bind(session, orderExecutor)
} else if m.CumulatedVolumeTakeProfit != nil { }
if m.CumulatedVolumeTakeProfit != nil {
m.CumulatedVolumeTakeProfit.Bind(session, orderExecutor) m.CumulatedVolumeTakeProfit.Bind(session, orderExecutor)
} }
} }

View File

@ -3,6 +3,8 @@ package bbgo
import ( import (
"context" "context"
log "github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
) )
@ -15,7 +17,10 @@ import (
// > SELECT start_time, `interval`, quote_volume, open, close FROM binance_klines WHERE symbol = 'ETHUSDT' AND `interval` = '5m' ORDER BY quote_volume DESC LIMIT 20; // > SELECT start_time, `interval`, quote_volume, open, close FROM binance_klines WHERE symbol = 'ETHUSDT' AND `interval` = '5m' ORDER BY quote_volume DESC LIMIT 20;
// //
type CumulatedVolumeTakeProfit struct { type CumulatedVolumeTakeProfit struct {
Symbol string `json:"symbol"`
types.IntervalWindow types.IntervalWindow
Ratio fixedpoint.Value `json:"ratio"` Ratio fixedpoint.Value `json:"ratio"`
MinQuoteVolume fixedpoint.Value `json:"minQuoteVolume"` MinQuoteVolume fixedpoint.Value `json:"minQuoteVolume"`
@ -32,7 +37,7 @@ func (s *CumulatedVolumeTakeProfit) Bind(session *ExchangeSession, orderExecutor
store, _ := session.MarketDataStore(position.Symbol) store, _ := session.MarketDataStore(position.Symbol)
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
if kline.Symbol != position.Symbol || kline.Interval != types.Interval1m { if kline.Symbol != position.Symbol || kline.Interval != s.Interval {
return return
} }
@ -46,25 +51,33 @@ func (s *CumulatedVolumeTakeProfit) Bind(session *ExchangeSession, orderExecutor
return return
} }
if klines, ok := store.KLinesOfInterval(s.Interval); ok { klines, ok := store.KLinesOfInterval(s.Interval)
var cbv = fixedpoint.Zero if !ok {
var cqv = fixedpoint.Zero log.Warnf("history kline not found")
for i := 0; i < s.Window; i++ { return
last := (*klines)[len(*klines)-1-i] }
cqv = cqv.Add(last.QuoteVolume)
cbv = cbv.Add(last.Volume)
}
if cqv.Compare(s.MinQuoteVolume) > 0 { if len(*klines) < s.Window {
Notify("%s TakeProfit triggered by cumulated volume (window: %d) %f > %f, price = %f", return
position.Symbol, }
s.Window,
cqv.Float64(),
s.MinQuoteVolume.Float64(), kline.Close.Float64())
_ = orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "cumulatedVolumeTakeProfit") var cbv = fixedpoint.Zero
return var cqv = fixedpoint.Zero
} for i := 0; i < s.Window; i++ {
last := (*klines)[len(*klines)-1-i]
cqv = cqv.Add(last.QuoteVolume)
cbv = cbv.Add(last.Volume)
}
if cqv.Compare(s.MinQuoteVolume) > 0 {
Notify("%s TakeProfit triggered by cumulated volume (window: %d) %f > %f, price = %f",
position.Symbol,
s.Window,
cqv.Float64(),
s.MinQuoteVolume.Float64(), kline.Close.Float64())
_ = orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "cumulatedVolumeTakeProfit")
return
} }
}) })
} }

View File

@ -8,16 +8,29 @@ import (
) )
type LowerShadowTakeProfit struct { type LowerShadowTakeProfit struct {
Ratio fixedpoint.Value `json:"ratio"` // inherit from the strategy
types.IntervalWindow
// inherit from the strategy
Symbol string `json:"symbol"`
Ratio fixedpoint.Value `json:"ratio"`
session *ExchangeSession session *ExchangeSession
orderExecutor *GeneralOrderExecutor orderExecutor *GeneralOrderExecutor
} }
func (s *LowerShadowTakeProfit) Subscribe(session *ExchangeSession) {
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
}
func (s *LowerShadowTakeProfit) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) { func (s *LowerShadowTakeProfit) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) {
s.session = session s.session = session
s.orderExecutor = orderExecutor s.orderExecutor = orderExecutor
stdIndicatorSet, _ := session.StandardIndicatorSet(s.Symbol)
ewma := stdIndicatorSet.EWMA(s.IntervalWindow)
position := orderExecutor.Position() position := orderExecutor.Position()
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
if kline.Symbol != position.Symbol || kline.Interval != types.Interval1m { if kline.Symbol != position.Symbol || kline.Interval != types.Interval1m {
@ -38,6 +51,11 @@ func (s *LowerShadowTakeProfit) Bind(session *ExchangeSession, orderExecutor *Ge
return return
} }
// skip close price higher than the ewma
if closePrice.Float64() > ewma.Last() {
return
}
if kline.GetLowerShadowHeight().Div(kline.Close).Compare(s.Ratio) > 0 { if kline.GetLowerShadowHeight().Div(kline.Close).Compare(s.Ratio) > 0 {
Notify("%s TakeProfit triggered by shadow ratio %f, price = %f", Notify("%s TakeProfit triggered by shadow ratio %f, price = %f",
position.Symbol, position.Symbol,

View File

@ -3,18 +3,40 @@ package bbgo
import ( import (
"context" "context"
"sync" "sync"
"time"
"github.com/sirupsen/logrus"
) )
var graceful = &Graceful{}
//go:generate callbackgen -type Graceful //go:generate callbackgen -type Graceful
type Graceful struct { type Graceful struct {
shutdownCallbacks []func(ctx context.Context, wg *sync.WaitGroup) shutdownCallbacks []func(ctx context.Context, wg *sync.WaitGroup)
} }
// Shutdown is a blocking call to emit all shutdown callbacks at the same time.
func (g *Graceful) Shutdown(ctx context.Context) { func (g *Graceful) Shutdown(ctx context.Context) {
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(len(g.shutdownCallbacks)) wg.Add(len(g.shutdownCallbacks))
go g.EmitShutdown(ctx, &wg) // for each shutdown callback, we give them 10 second
shtCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
go g.EmitShutdown(shtCtx, &wg)
wg.Wait() wg.Wait()
cancel()
}
func OnShutdown(f func(ctx context.Context, wg *sync.WaitGroup)) {
graceful.OnShutdown(f)
}
func Shutdown() {
logrus.Infof("shutting down...")
ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second)
graceful.Shutdown(ctx)
cancel()
} }

View File

@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/dynamic"
"github.com/c9s/bbgo/pkg/service" "github.com/c9s/bbgo/pkg/service"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
) )
@ -22,7 +23,7 @@ func Test_injectField(t *testing.T) {
// get the value of the pointer, or it can not be set. // get the value of the pointer, or it can not be set.
var rv = reflect.ValueOf(tt).Elem() var rv = reflect.ValueOf(tt).Elem()
_, ret := hasField(rv, "TradeService") _, ret := dynamic.HasField(rv, "TradeService")
assert.True(t, ret) assert.True(t, ret)
ts := &service.TradeService{} ts := &service.TradeService{}

View File

@ -6,6 +6,7 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/dynamic"
"github.com/c9s/bbgo/pkg/service" "github.com/c9s/bbgo/pkg/service"
) )
@ -106,10 +107,10 @@ func Sync(obj interface{}) {
} }
func loadPersistenceFields(obj interface{}, id string, persistence service.PersistenceService) error { func loadPersistenceFields(obj interface{}, id string, persistence service.PersistenceService) error {
return iterateFieldsByTag(obj, "persistence", func(tag string, field reflect.StructField, value reflect.Value) error { return dynamic.IterateFieldsByTag(obj, "persistence", func(tag string, field reflect.StructField, value reflect.Value) error {
log.Debugf("[loadPersistenceFields] loading value into field %v, tag = %s, original value = %v", field, tag, value) log.Debugf("[loadPersistenceFields] loading value into field %v, tag = %s, original value = %v", field, tag, value)
newValueInf := newTypeValueInterface(value.Type()) newValueInf := dynamic.NewTypeValueInterface(value.Type())
// inf := value.Interface() // inf := value.Interface()
store := persistence.NewStore("state", id, tag) store := persistence.NewStore("state", id, tag)
if err := store.Load(&newValueInf); err != nil { if err := store.Load(&newValueInf); err != nil {
@ -134,7 +135,7 @@ func loadPersistenceFields(obj interface{}, id string, persistence service.Persi
} }
func storePersistenceFields(obj interface{}, id string, persistence service.PersistenceService) error { func storePersistenceFields(obj interface{}, id string, persistence service.PersistenceService) error {
return iterateFieldsByTag(obj, "persistence", func(tag string, ft reflect.StructField, fv reflect.Value) error { return dynamic.IterateFieldsByTag(obj, "persistence", func(tag string, ft reflect.StructField, fv reflect.Value) error {
log.Debugf("[storePersistenceFields] storing value from field %v, tag = %s, original value = %v", ft, tag, fv) log.Debugf("[storePersistenceFields] storing value from field %v, tag = %s, original value = %v", ft, tag, fv)
inf := fv.Interface() inf := fv.Interface()

View File

@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/dynamic"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/service" "github.com/c9s/bbgo/pkg/service"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
@ -23,7 +24,6 @@ func (s *TestStructWithoutInstanceID) ID() string {
type TestStruct struct { type TestStruct struct {
*Environment *Environment
*Graceful
Position *types.Position `persistence:"position"` Position *types.Position `persistence:"position"`
Integer int64 `persistence:"integer"` Integer int64 `persistence:"integer"`
@ -83,7 +83,7 @@ func Test_loadPersistenceFields(t *testing.T) {
t.Run(psName+"/nil", func(t *testing.T) { t.Run(psName+"/nil", func(t *testing.T) {
var b *TestStruct = nil var b *TestStruct = nil
err := loadPersistenceFields(b, "test-nil", ps) err := loadPersistenceFields(b, "test-nil", ps)
assert.Equal(t, errCanNotIterateNilPointer, err) assert.Equal(t, dynamic.ErrCanNotIterateNilPointer, err)
}) })
t.Run(psName+"/pointer-field", func(t *testing.T) { t.Run(psName+"/pointer-field", func(t *testing.T) {

View File

@ -1,9 +1,9 @@
package bbgo package bbgo
import ( import (
"errors"
"fmt"
"reflect" "reflect"
"github.com/c9s/bbgo/pkg/dynamic"
) )
type InstanceIDProvider interface { type InstanceIDProvider interface {
@ -19,7 +19,7 @@ func callID(obj interface{}) string {
return ret[0].String() return ret[0].String()
} }
if symbol, ok := isSymbolBasedStrategy(sv); ok { if symbol, ok := dynamic.LookupSymbolField(sv); ok {
m := sv.MethodByName("ID") m := sv.MethodByName("ID")
ret := m.Call(nil) ret := m.Call(nil)
return ret[0].String() + ":" + symbol return ret[0].String() + ":" + symbol
@ -31,90 +31,3 @@ func callID(obj interface{}) string {
return ret[0].String() + ":" return ret[0].String() + ":"
} }
func isSymbolBasedStrategy(rs reflect.Value) (string, bool) {
if rs.Kind() == reflect.Ptr {
rs = rs.Elem()
}
field := rs.FieldByName("Symbol")
if !field.IsValid() {
return "", false
}
if field.Kind() != reflect.String {
return "", false
}
return field.String(), true
}
func hasField(rs reflect.Value, fieldName string) (field reflect.Value, ok bool) {
field = rs.FieldByName(fieldName)
return field, field.IsValid()
}
type StructFieldIterator func(tag string, ft reflect.StructField, fv reflect.Value) error
var errCanNotIterateNilPointer = errors.New("can not iterate struct on a nil pointer")
func iterateFieldsByTag(obj interface{}, tagName string, cb StructFieldIterator) error {
sv := reflect.ValueOf(obj)
st := reflect.TypeOf(obj)
if st.Kind() != reflect.Ptr {
return fmt.Errorf("f should be a pointer of a struct, %s given", st)
}
// for pointer, check if it's nil
if sv.IsNil() {
return errCanNotIterateNilPointer
}
// solve the reference
st = st.Elem()
sv = sv.Elem()
if st.Kind() != reflect.Struct {
return fmt.Errorf("f should be a struct, %s given", st)
}
for i := 0; i < sv.NumField(); i++ {
fv := sv.Field(i)
ft := st.Field(i)
// skip unexported fields
if !st.Field(i).IsExported() {
continue
}
tag, ok := ft.Tag.Lookup(tagName)
if !ok {
continue
}
if err := cb(tag, ft, fv); err != nil {
return err
}
}
return nil
}
// https://github.com/xiaojun207/go-base-utils/blob/master/utils/Clone.go
func newTypeValueInterface(typ reflect.Type) interface{} {
if typ.Kind() == reflect.Ptr {
typ = typ.Elem()
dst := reflect.New(typ).Elem()
return dst.Addr().Interface()
}
dst := reflect.New(typ)
return dst.Interface()
}
func toReflectValues(args ...interface{}) (values []reflect.Value) {
for _, arg := range args {
values = append(values, reflect.ValueOf(arg))
}
return values
}

2
pkg/bbgo/reflect_test.go Normal file
View File

@ -0,0 +1,2 @@
package bbgo

View File

@ -10,6 +10,7 @@ import (
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
"github.com/c9s/bbgo/pkg/dynamic"
"github.com/c9s/bbgo/pkg/interact" "github.com/c9s/bbgo/pkg/interact"
) )
@ -72,8 +73,6 @@ type Trader struct {
exchangeStrategies map[string][]SingleExchangeStrategy exchangeStrategies map[string][]SingleExchangeStrategy
logger Logger logger Logger
Graceful Graceful
} }
func NewTrader(environ *Environment) *Trader { func NewTrader(environ *Environment) *Trader {
@ -201,7 +200,7 @@ func (trader *Trader) RunSingleExchangeStrategy(ctx context.Context, strategy Si
return errors.Wrapf(err, "failed to inject OrderExecutor on %T", strategy) return errors.Wrapf(err, "failed to inject OrderExecutor on %T", strategy)
} }
if symbol, ok := isSymbolBasedStrategy(rs); ok { if symbol, ok := dynamic.LookupSymbolField(rs); ok {
log.Infof("found symbol based strategy from %s", rs.Type()) log.Infof("found symbol based strategy from %s", rs.Type())
market, ok := session.Market(symbol) market, ok := session.Market(symbol)
@ -394,7 +393,7 @@ func (trader *Trader) injectCommonServices(s interface{}) error {
// a special injection for persistence selector: // a special injection for persistence selector:
// if user defined the selector, the facade pointer will be nil, hence we need to update the persistence facade pointer // if user defined the selector, the facade pointer will be nil, hence we need to update the persistence facade pointer
sv := reflect.ValueOf(s).Elem() sv := reflect.ValueOf(s).Elem()
if field, ok := hasField(sv, "Persistence"); ok { if field, ok := dynamic.HasField(sv, "Persistence"); ok {
// the selector is set, but we need to update the facade pointer // the selector is set, but we need to update the facade pointer
if !field.IsNil() { if !field.IsNil() {
elem := field.Elem() elem := field.Elem()
@ -415,7 +414,6 @@ func (trader *Trader) injectCommonServices(s interface{}) error {
} }
return parseStructAndInject(s, return parseStructAndInject(s,
&trader.Graceful,
&trader.logger, &trader.logger,
Notification, Notification,
trader.environment.TradeService, trader.environment.TradeService,

View File

@ -443,9 +443,7 @@ var BacktestCmd = &cobra.Command{
cmdutil.WaitForSignal(runCtx, syscall.SIGINT, syscall.SIGTERM) cmdutil.WaitForSignal(runCtx, syscall.SIGINT, syscall.SIGTERM)
log.Infof("shutting down trader...") log.Infof("shutting down trader...")
shutdownCtx, cancelShutdown := context.WithDeadline(runCtx, time.Now().Add(10*time.Second)) bbgo.Shutdown()
trader.Graceful.Shutdown(shutdownCtx)
cancelShutdown()
// put the logger back to print the pnl // put the logger back to print the pnl
log.SetLevel(log.InfoLevel) log.SetLevel(log.InfoLevel)

View File

@ -8,7 +8,6 @@ import (
"path/filepath" "path/filepath"
"runtime/pprof" "runtime/pprof"
"syscall" "syscall"
"time"
"github.com/pkg/errors" "github.com/pkg/errors"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -78,12 +77,7 @@ func runSetup(baseCtx context.Context, userConfig *bbgo.Config, enableApiServer
cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM) cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM)
cancelTrading() cancelTrading()
// graceful period = 15 second bbgo.Shutdown()
shutdownCtx, cancelShutdown := context.WithDeadline(ctx, time.Now().Add(15*time.Second))
log.Infof("shutting down...")
trader.Graceful.Shutdown(shutdownCtx)
cancelShutdown()
return nil return nil
} }
@ -216,10 +210,7 @@ func runConfig(basectx context.Context, cmd *cobra.Command, userConfig *bbgo.Con
cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM) cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM)
cancelTrading() cancelTrading()
log.Infof("shutting down...") bbgo.Shutdown()
shutdownCtx, cancelShutdown := context.WithDeadline(ctx, time.Now().Add(30*time.Second))
trader.Graceful.Shutdown(shutdownCtx)
cancelShutdown()
if err := trader.SaveState(); err != nil { if err := trader.SaveState(); err != nil {
log.WithError(err).Errorf("can not save strategy states") log.WithError(err).Errorf("can not save strategy states")

53
pkg/dynamic/call.go Normal file
View File

@ -0,0 +1,53 @@
package dynamic
import (
"errors"
"reflect"
)
// CallStructFieldsMethod iterates field from the given struct object
// check if the field object implements the interface, if it's implemented, then we call a specific method
func CallStructFieldsMethod(m interface{}, method string, args ...interface{}) error {
rv := reflect.ValueOf(m)
rt := reflect.TypeOf(m)
if rt.Kind() != reflect.Ptr {
return errors.New("the given object needs to be a pointer")
}
rv = rv.Elem()
rt = rt.Elem()
if rt.Kind() != reflect.Struct {
return errors.New("the given object needs to be struct")
}
argValues := ToReflectValues(args...)
for i := 0; i < rt.NumField(); i++ {
fieldType := rt.Field(i)
fieldValue := rv.Field(i)
// skip non-exported fields
if !fieldType.IsExported() {
continue
}
if fieldType.Type.Kind() == reflect.Ptr && fieldValue.IsNil() {
continue
}
methodType, ok := fieldType.Type.MethodByName(method)
if !ok {
continue
}
if len(argValues) < methodType.Type.NumIn() {
// return fmt.Errorf("method %v require %d args, %d given", methodType, methodType.Type.NumIn(), len(argValues))
}
refMethod := fieldValue.MethodByName(method)
refMethod.Call(argValues)
}
return nil
}

29
pkg/dynamic/call_test.go Normal file
View File

@ -0,0 +1,29 @@
package dynamic
import (
"testing"
"github.com/stretchr/testify/assert"
)
type callTest struct {
ChildCall1 *childCall1
ChildCall2 *childCall2
}
type childCall1 struct{}
func (c *childCall1) Subscribe(a int) {}
type childCall2 struct{}
func (c *childCall2) Subscribe(a int) {}
func TestCallStructFieldsMethod(t *testing.T) {
c := &callTest{
ChildCall1: &childCall1{},
ChildCall2: &childCall2{},
}
err := CallStructFieldsMethod(c, "Subscribe", 10)
assert.NoError(t, err)
}

26
pkg/dynamic/field.go Normal file
View File

@ -0,0 +1,26 @@
package dynamic
import "reflect"
func HasField(rs reflect.Value, fieldName string) (field reflect.Value, ok bool) {
field = rs.FieldByName(fieldName)
return field, field.IsValid()
}
func LookupSymbolField(rs reflect.Value) (string, bool) {
if rs.Kind() == reflect.Ptr {
rs = rs.Elem()
}
field := rs.FieldByName("Symbol")
if !field.IsValid() {
return "", false
}
if field.Kind() != reflect.String {
return "", false
}
return field.String(), true
}

54
pkg/dynamic/iterate.go Normal file
View File

@ -0,0 +1,54 @@
package dynamic
import (
"errors"
"fmt"
"reflect"
)
type StructFieldIterator func(tag string, ft reflect.StructField, fv reflect.Value) error
var ErrCanNotIterateNilPointer = errors.New("can not iterate struct on a nil pointer")
func IterateFieldsByTag(obj interface{}, tagName string, cb StructFieldIterator) error {
sv := reflect.ValueOf(obj)
st := reflect.TypeOf(obj)
if st.Kind() != reflect.Ptr {
return fmt.Errorf("f should be a pointer of a struct, %s given", st)
}
// for pointer, check if it's nil
if sv.IsNil() {
return ErrCanNotIterateNilPointer
}
// solve the reference
st = st.Elem()
sv = sv.Elem()
if st.Kind() != reflect.Struct {
return fmt.Errorf("f should be a struct, %s given", st)
}
for i := 0; i < sv.NumField(); i++ {
fv := sv.Field(i)
ft := st.Field(i)
// skip unexported fields
if !st.Field(i).IsExported() {
continue
}
tag, ok := ft.Tag.Lookup(tagName)
if !ok {
continue
}
if err := cb(tag, ft, fv); err != nil {
return err
}
}
return nil
}

41
pkg/dynamic/merge.go Normal file
View File

@ -0,0 +1,41 @@
package dynamic
import "reflect"
// 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 InheritStructValues(dst, src interface{}) {
if dst == nil {
return
}
rtA := reflect.TypeOf(dst)
srcStructType := reflect.TypeOf(src)
rtA = rtA.Elem()
srcStructType = srcStructType.Elem()
for i := 0; i < rtA.NumField(); i++ {
fieldType := rtA.Field(i)
fieldName := fieldType.Name
if !fieldType.IsExported() {
continue
}
// if there is a field with the same name
fieldSrcType, found := srcStructType.FieldByName(fieldName)
if !found {
continue
}
// ensure that the type is the same
if fieldSrcType.Type == fieldType.Type {
srcValue := reflect.ValueOf(src).Elem().FieldByName(fieldName)
dstValue := reflect.ValueOf(dst).Elem().FieldByName(fieldName)
if (fieldType.Type.Kind() == reflect.Ptr && dstValue.IsNil()) || dstValue.IsZero() {
dstValue.Set(srcValue)
}
}
}
}

82
pkg/dynamic/merge_test.go Normal file
View File

@ -0,0 +1,82 @@
package dynamic
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
type TestStrategy struct {
Symbol string `json:"symbol"`
Interval string `json:"interval"`
BaseQuantity fixedpoint.Value `json:"baseQuantity"`
MaxAssetQuantity fixedpoint.Value `json:"maxAssetQuantity"`
MinDropPercentage fixedpoint.Value `json:"minDropPercentage"`
}
func Test_reflectMergeStructFields(t *testing.T) {
t.Run("zero value", func(t *testing.T) {
a := &TestStrategy{Symbol: "BTCUSDT"}
b := &struct{ Symbol string }{Symbol: ""}
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"}
InheritStructValues(b, a)
assert.Equal(t, "ETHUSDT", b.Symbol, "should be the original value")
})
t.Run("zero embedded struct", func(t *testing.T) {
iw := types.IntervalWindow{Interval: types.Interval1h, Window: 30}
a := &struct {
types.IntervalWindow
Symbol string
}{
IntervalWindow: iw,
Symbol: "BTCUSDT",
}
b := &struct {
Symbol string
types.IntervalWindow
}{}
InheritStructValues(b, a)
assert.Equal(t, iw, b.IntervalWindow)
assert.Equal(t, "BTCUSDT", b.Symbol)
})
t.Run("non-zero embedded struct", func(t *testing.T) {
iw := types.IntervalWindow{Interval: types.Interval1h, Window: 30}
a := &struct {
types.IntervalWindow
}{
IntervalWindow: iw,
}
b := &struct {
types.IntervalWindow
}{
IntervalWindow: types.IntervalWindow{Interval: types.Interval5m, Window: 9},
}
InheritStructValues(b, a)
assert.Equal(t, types.IntervalWindow{Interval: types.Interval5m, Window: 9}, b.IntervalWindow)
})
t.Run("skip different type but the same name", func(t *testing.T) {
a := &struct {
A float64
}{
A: 1.99,
}
b := &struct {
A string
}{}
InheritStructValues(b, a)
assert.Equal(t, "", b.A)
assert.Equal(t, 1.99, a.A)
})
}

24
pkg/dynamic/typevalue.go Normal file
View File

@ -0,0 +1,24 @@
package dynamic
import "reflect"
// https://github.com/xiaojun207/go-base-utils/blob/master/utils/Clone.go
func NewTypeValueInterface(typ reflect.Type) interface{} {
if typ.Kind() == reflect.Ptr {
typ = typ.Elem()
dst := reflect.New(typ).Elem()
return dst.Addr().Interface()
}
dst := reflect.New(typ)
return dst.Interface()
}
// ToReflectValues convert the go objects into reflect.Value slice
func ToReflectValues(args ...interface{}) (values []reflect.Value) {
for i := range args {
arg := args[i]
values = append(values, reflect.ValueOf(arg))
}
return values
}

View File

@ -41,9 +41,6 @@ type Strategy struct {
// This field will be injected automatically since we defined the Symbol field. // This field will be injected automatically since we defined the Symbol field.
*bbgo.StandardIndicatorSet *bbgo.StandardIndicatorSet
// Graceful let you define the graceful shutdown handler
*bbgo.Graceful
// Market stores the configuration of the market, for example, VolumePrecision, PricePrecision, MinLotSize... etc // Market stores the configuration of the market, for example, VolumePrecision, PricePrecision, MinLotSize... etc
// This field will be injected automatically since we defined the Symbol field. // This field will be injected automatically since we defined the Symbol field.
types.Market types.Market
@ -350,7 +347,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.profitOrders.BindStream(session.UserDataStream) s.profitOrders.BindStream(session.UserDataStream)
// setup graceful shutting down handler // setup graceful shutting down handler
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
// call Done to notify the main process. // call Done to notify the main process.
defer wg.Done() defer wg.Done()
log.Infof("canceling active orders...") log.Infof("canceling active orders...")

View File

@ -49,7 +49,6 @@ type BollingerSetting struct {
} }
type Strategy struct { type Strategy struct {
*bbgo.Graceful
*bbgo.Persistence *bbgo.Persistence
Environment *bbgo.Environment Environment *bbgo.Environment
@ -616,7 +615,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
// s.book = types.NewStreamBook(s.Symbol) // s.book = types.NewStreamBook(s.Symbol)
// s.book.BindStreamForBackground(session.MarketDataStream) // s.book.BindStreamForBackground(session.MarketDataStream)
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
_ = s.orderExecutor.GracefulCancel(ctx) _ = s.orderExecutor.GracefulCancel(ctx)

View File

@ -47,7 +47,6 @@ func (b BudgetPeriod) Duration() time.Duration {
// Strategy is the Dollar-Cost-Average strategy // Strategy is the Dollar-Cost-Average strategy
type Strategy struct { type Strategy struct {
*bbgo.Graceful
Environment *bbgo.Environment Environment *bbgo.Environment
Symbol string `json:"symbol"` Symbol string `json:"symbol"`

View File

@ -25,7 +25,6 @@ func init() {
} }
type Strategy struct { type Strategy struct {
*bbgo.Graceful
SourceExchangeName string `json:"sourceExchange"` SourceExchangeName string `json:"sourceExchange"`
@ -217,7 +216,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.place(ctx, orderExecutor, session, indicator, closePrice) s.place(ctx, orderExecutor, session, indicator, closePrice)
}) })
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
log.Infof("canceling trailingstop order...") log.Infof("canceling trailingstop order...")
s.clear(ctx, orderExecutor) s.clear(ctx, orderExecutor)
@ -261,7 +260,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se
s.place(ctx, &orderExecutor, session, indicator, closePrice) s.place(ctx, &orderExecutor, session, indicator, closePrice)
}) })
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
log.Infof("canceling trailingstop order...") log.Infof("canceling trailingstop order...")
s.clear(ctx, &orderExecutor) s.clear(ctx, &orderExecutor)

View File

@ -51,7 +51,6 @@ type Strategy struct {
KLineEndTime types.Time KLineEndTime types.Time
*bbgo.Environment *bbgo.Environment
*bbgo.Graceful
bbgo.StrategyController bbgo.StrategyController
activeMakerOrders *bbgo.ActiveOrderBook activeMakerOrders *bbgo.ActiveOrderBook
@ -1221,7 +1220,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
} }
} }
}) })
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
log.Infof("canceling active orders...") log.Infof("canceling active orders...")
s.CancelAll(ctx) s.CancelAll(ctx)

View File

@ -49,10 +49,6 @@ type Strategy struct {
// This field will be injected automatically since we defined the Symbol field. // This field will be injected automatically since we defined the Symbol field.
*bbgo.StandardIndicatorSet *bbgo.StandardIndicatorSet
// Graceful shutdown function
*bbgo.Graceful
// --------------------------
// ewma is the exponential weighted moving average indicator // ewma is the exponential weighted moving average indicator
ewma *indicator.EWMA ewma *indicator.EWMA
} }
@ -114,7 +110,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol)
s.activeOrders.BindStream(session.UserDataStream) s.activeOrders.BindStream(session.UserDataStream)
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
log.Infof("canceling active orders...") log.Infof("canceling active orders...")

View File

@ -31,7 +31,6 @@ type IntervalWindowSetting struct {
} }
type Strategy struct { type Strategy struct {
*bbgo.Graceful
*bbgo.Persistence *bbgo.Persistence
Environment *bbgo.Environment Environment *bbgo.Environment

View File

@ -45,8 +45,6 @@ type State struct {
} }
type Strategy struct { type Strategy struct {
*bbgo.Graceful `json:"-" yaml:"-"`
*bbgo.Persistence *bbgo.Persistence
// OrderExecutor is an interface for submitting order. // OrderExecutor is an interface for submitting order.
@ -621,7 +619,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
}) })
s.tradeCollector.BindStream(session.UserDataStream) s.tradeCollector.BindStream(session.UserDataStream)
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
if err := s.SaveState(); err != nil { if err := s.SaveState(); err != nil {

View File

@ -10,6 +10,7 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/dynamic"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
@ -47,8 +48,10 @@ type BreakLow struct {
StopEMA *types.IntervalWindow `json:"stopEMA"` StopEMA *types.IntervalWindow `json:"stopEMA"`
} }
type BounceShort struct { type ResistanceShort struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
Symbol string `json:"-"`
Market types.Market `json:"-"`
types.IntervalWindow types.IntervalWindow
@ -57,20 +60,129 @@ type BounceShort struct {
LayerSpread fixedpoint.Value `json:"layerSpread"` LayerSpread fixedpoint.Value `json:"layerSpread"`
Quantity fixedpoint.Value `json:"quantity"` Quantity fixedpoint.Value `json:"quantity"`
Ratio fixedpoint.Value `json:"ratio"` 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 { func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) {
CatBounceRatio fixedpoint.Value `json:"catBounceRatio"` s.session = session
NumLayers int `json:"numLayers"` s.orderExecutor = orderExecutor
TotalQuantity fixedpoint.Value `json:"totalQuantity"`
Quantity fixedpoint.Value `json:"quantity"` position := orderExecutor.Position()
MarginSideEffect types.MarginOrderSideEffectType `json:"marginOrderSideEffect"` 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 { type Strategy struct {
*bbgo.Graceful
Environment *bbgo.Environment Environment *bbgo.Environment
Symbol string `json:"symbol"` Symbol string `json:"symbol"`
Market types.Market Market types.Market
@ -83,23 +195,22 @@ type Strategy struct {
ProfitStats *types.ProfitStats `persistence:"profit_stats"` ProfitStats *types.ProfitStats `persistence:"profit_stats"`
TradeStats *types.TradeStats `persistence:"trade_stats"` TradeStats *types.TradeStats `persistence:"trade_stats"`
// BreakLow is one of the entry method
BreakLow BreakLow `json:"breakLow"` BreakLow BreakLow `json:"breakLow"`
BounceShort *BounceShort `json:"bounceShort"` // ResistanceShort is one of the entry method
ResistanceShort *ResistanceShort `json:"resistanceShort"`
Entry Entry `json:"entry"` ExitMethods bbgo.ExitMethodSet `json:"exits"`
ExitMethods []bbgo.ExitMethod `json:"exits"`
session *bbgo.ExchangeSession session *bbgo.ExchangeSession
orderExecutor *bbgo.GeneralOrderExecutor orderExecutor *bbgo.GeneralOrderExecutor
stopLossPrice fixedpoint.Value
lastLow fixedpoint.Value lastLow fixedpoint.Value
pivot *indicator.Pivot pivot *indicator.Pivot
resistancePivot *indicator.Pivot resistancePivot *indicator.Pivot
stopEWMA *indicator.EWMA stopEWMA *indicator.EWMA
pivotLowPrices []fixedpoint.Value pivotLowPrices []fixedpoint.Value
resistancePrices []float64
currentBounceShortPrice fixedpoint.Value currentBounceShortPrice fixedpoint.Value
// StrategyController // StrategyController
@ -114,55 +225,16 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m}) session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m})
if s.BounceShort != nil && s.BounceShort.Enabled { if s.ResistanceShort != nil && s.ResistanceShort.Enabled {
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.BounceShort.Interval}) dynamic.InheritStructValues(s.ResistanceShort, s)
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.ResistanceShort.Interval})
} }
if !bbgo.IsBackTesting { if !bbgo.IsBackTesting {
session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{}) session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{})
} }
}
func (s *Strategy) useQuantityOrBaseBalance(quantity fixedpoint.Value) fixedpoint.Value { s.ExitMethods.SetAndSubscribe(session, s)
balance, hasBalance := s.session.Account.Balance(s.Market.BaseCurrency)
if hasBalance {
if quantity.IsZero() {
bbgo.Notify("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
}
func (s *Strategy) placeLimitSell(ctx context.Context, price, quantity fixedpoint.Value, tag string) {
_, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
Symbol: s.Symbol,
Price: price,
Side: types.SideTypeSell,
Type: types.OrderTypeLimit,
Quantity: quantity,
MarginSideEffect: types.SideEffectTypeMarginBuy,
Tag: tag,
})
}
func (s *Strategy) placeMarketSell(ctx context.Context, quantity fixedpoint.Value, tag string) {
_, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
Symbol: s.Symbol,
Side: types.SideTypeSell,
Type: types.OrderTypeMarket,
Quantity: quantity,
MarginSideEffect: types.SideEffectTypeMarginBuy,
Tag: tag,
})
} }
func (s *Strategy) InstanceID() string { func (s *Strategy) InstanceID() string {
@ -225,8 +297,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.pivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow} s.pivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow}
s.pivot.Bind(store) s.pivot.Bind(store)
preloadPivot(s.pivot, store)
lastKLine := s.preloadPivot(s.pivot, store)
// update pivot low data // update pivot low data
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
@ -247,11 +318,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.pivotLowPrices = append(s.pivotLowPrices, s.lastLow) s.pivotLowPrices = append(s.pivotLowPrices, s.lastLow)
}) })
if s.BounceShort != nil && s.BounceShort.Enabled {
s.resistancePivot = &indicator.Pivot{IntervalWindow: s.BounceShort.IntervalWindow}
s.resistancePivot.Bind(store)
}
if s.BreakLow.StopEMA != nil { if s.BreakLow.StopEMA != nil {
s.stopEWMA = standardIndicator.EWMA(*s.BreakLow.StopEMA) s.stopEWMA = standardIndicator.EWMA(*s.BreakLow.StopEMA)
} }
@ -260,36 +326,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
method.Bind(session, s.orderExecutor) method.Bind(session, s.orderExecutor)
} }
if s.BounceShort != nil && s.BounceShort.Enabled { if s.ResistanceShort != nil && s.ResistanceShort.Enabled {
if s.resistancePivot != nil { s.ResistanceShort.Bind(session, s.orderExecutor)
s.preloadPivot(s.resistancePivot, store)
}
session.UserDataStream.OnStart(func() {
if lastKLine == nil {
return
}
if s.resistancePivot != nil {
lows := s.resistancePivot.Lows
minDistance := s.BounceShort.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)
}
}
}
})
} }
// Always check whether you can open a short position or not // Always check whether you can open a short position or not
@ -364,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.BounceShort == nil || !s.BounceShort.Enabled {
return
}
if kline.Symbol != s.Symbol || kline.Interval != s.BounceShort.Interval {
return
}
if s.resistancePivot != nil {
closePrice := kline.Close.Float64()
minDistance := s.BounceShort.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 { if !bbgo.IsBackTesting {
// use market trade to submit short order // use market trade to submit short order
session.MarketDataStream.OnMarketTrade(func(trade types.Trade) { session.MarketDataStream.OnMarketTrade(func(trade types.Trade) {
@ -405,7 +409,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
}) })
} }
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
_, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String()) _, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String())
wg.Done() wg.Done()
}) })
@ -423,46 +427,6 @@ func (s *Strategy) findHigherPivotLow(price fixedpoint.Value) (fixedpoint.Value,
return price, false return price, false
} }
func (s *Strategy) placeBounceSellOrders(ctx context.Context, resistancePrice fixedpoint.Value) {
futuresMode := s.session.Futures || s.session.IsolatedFutures
totalQuantity := s.BounceShort.Quantity
numLayers := s.BounceShort.NumLayers
if numLayers == 0 {
numLayers = 1
}
numLayersF := fixedpoint.NewFromInt(int64(numLayers))
layerSpread := s.BounceShort.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.BounceShort.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) { func (s *Strategy) placeOrder(ctx context.Context, price fixedpoint.Value, quantity fixedpoint.Value) {
_, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
Symbol: s.Symbol, Symbol: s.Symbol,
@ -473,26 +437,51 @@ func (s *Strategy) placeOrder(ctx context.Context, price fixedpoint.Value, quant
}) })
} }
func (s *Strategy) preloadPivot(pivot *indicator.Pivot, store *bbgo.MarketDataStore) *types.KLine { func (s *Strategy) useQuantityOrBaseBalance(quantity fixedpoint.Value) fixedpoint.Value {
klines, ok := store.KLinesOfInterval(pivot.Interval) balance, hasBalance := s.session.Account.Balance(s.Market.BaseCurrency)
if !ok {
return nil if hasBalance {
if quantity.IsZero() {
bbgo.Notify("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)
}
} }
last := (*klines)[len(*klines)-1] if quantity.IsZero() {
log.Debugf("updating pivot indicator: %d klines", len(*klines)) log.Errorf("quantity is zero, can not submit sell order, please check settings")
for i := pivot.Window; i < len(*klines); i++ {
pivot.Update((*klines)[0 : i+1])
} }
log.Infof("found %s %v previous lows: %v", s.Symbol, pivot.IntervalWindow, pivot.Lows) return quantity
log.Infof("found %s %v previous highs: %v", s.Symbol, pivot.IntervalWindow, pivot.Highs) }
return &last
func (s *Strategy) placeLimitSell(ctx context.Context, price, quantity fixedpoint.Value, tag string) {
_, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
Symbol: s.Symbol,
Price: price,
Side: types.SideTypeSell,
Type: types.OrderTypeLimit,
Quantity: quantity,
MarginSideEffect: types.SideEffectTypeMarginBuy,
Tag: tag,
})
}
func (s *Strategy) placeMarketSell(ctx context.Context, quantity fixedpoint.Value, tag string) {
_, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
Symbol: s.Symbol,
Side: types.SideTypeSell,
Type: types.OrderTypeMarket,
Quantity: quantity,
MarginSideEffect: types.SideEffectTypeMarginBuy,
Tag: tag,
})
} }
func findPossibleResistancePrices(closePrice float64, minDistance float64, lows []float64) []float64 { func findPossibleResistancePrices(closePrice float64, minDistance float64, lows []float64) []float64 {
// sort float64 in increasing order // sort float64 in increasing order
// lower to higher prices
sort.Float64s(lows) sort.Float64s(lows)
var resistancePrices []float64 var resistancePrices []float64
@ -514,3 +503,21 @@ func findPossibleResistancePrices(closePrice float64, minDistance float64, lows
return resistancePrices return resistancePrices
} }
func preloadPivot(pivot *indicator.Pivot, store *bbgo.MarketDataStore) *types.KLine {
klines, ok := store.KLinesOfInterval(pivot.Interval)
if !ok {
return nil
}
last := (*klines)[len(*klines)-1]
log.Debugf("updating pivot indicator: %d klines", len(*klines))
for i := pivot.Window; i < len(*klines); i++ {
pivot.Update((*klines)[0 : i+1])
}
log.Debugf("found %v previous lows: %v", pivot.IntervalWindow, pivot.Lows)
log.Debugf("found %v previous highs: %v", pivot.IntervalWindow, pivot.Highs)
return &last
}

View File

@ -29,9 +29,6 @@ func init() {
} }
type Strategy struct { type Strategy struct {
*bbgo.Graceful
*bbgo.Notifiability
Environment *bbgo.Environment Environment *bbgo.Environment
StandardIndicatorSet *bbgo.StandardIndicatorSet StandardIndicatorSet *bbgo.StandardIndicatorSet
Market types.Market Market types.Market

View File

@ -30,7 +30,6 @@ func init() {
} }
type Strategy struct { type Strategy struct {
*bbgo.Graceful
*bbgo.Persistence *bbgo.Persistence
Environment *bbgo.Environment Environment *bbgo.Environment
@ -391,7 +390,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
}) })
// Graceful shutdown // Graceful shutdown
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
close(s.stopC) close(s.stopC)

View File

@ -134,7 +134,6 @@ func (control *TrailingStopControl) GenerateStopOrder(quantity fixedpoint.Value)
type Strategy struct { type Strategy struct {
*bbgo.Persistence `json:"-"` *bbgo.Persistence `json:"-"`
*bbgo.Environment `json:"-"` *bbgo.Environment `json:"-"`
*bbgo.Graceful `json:"-"`
session *bbgo.ExchangeSession session *bbgo.ExchangeSession
@ -582,7 +581,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
} }
}) })
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
// Cancel trailing stop order // Cancel trailing stop order

View File

@ -30,7 +30,6 @@ func init() {
} }
type Strategy struct { type Strategy struct {
*bbgo.Graceful
*bbgo.Persistence *bbgo.Persistence
Environment *bbgo.Environment Environment *bbgo.Environment
@ -377,7 +376,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
} }
}() }()
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
close(s.stopC) close(s.stopC)

View File

@ -135,8 +135,6 @@ func (a *Address) UnmarshalJSON(body []byte) error {
} }
type Strategy struct { type Strategy struct {
*bbgo.Graceful
Interval types.Duration `json:"interval"` Interval types.Duration `json:"interval"`
Addresses map[string]Address `json:"addresses"` Addresses map[string]Address `json:"addresses"`
@ -342,7 +340,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se
s.State = s.newDefaultState() s.State = s.newDefaultState()
} }
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
}) })

View File

@ -57,7 +57,6 @@ func (s *State) Reset() {
} }
type Strategy struct { type Strategy struct {
*bbgo.Graceful
*bbgo.Persistence *bbgo.Persistence
Symbol string `json:"symbol"` Symbol string `json:"symbol"`
@ -193,7 +192,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se
} }
} }
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
close(s.stopC) close(s.stopC)

View File

@ -33,7 +33,6 @@ func init() {
} }
type Strategy struct { type Strategy struct {
*bbgo.Graceful
*bbgo.Persistence *bbgo.Persistence
Environment *bbgo.Environment Environment *bbgo.Environment
@ -879,7 +878,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order
} }
}() }()
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
close(s.stopC) close(s.stopC)

View File

@ -58,7 +58,6 @@ func (s *State) Reset() {
} }
type Strategy struct { type Strategy struct {
*bbgo.Graceful
*bbgo.Persistence *bbgo.Persistence
*bbgo.Environment *bbgo.Environment
@ -180,7 +179,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se
return err return err
} }
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
s.SaveState() s.SaveState()

View File

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

View File

@ -1,5 +0,0 @@
package types
type Subscriber interface {
Subscribe()
}