mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-26 08:45:16 +00:00
Merge pull request #786 from c9s/strategy/pivotshort
strategy: pivotshort: resistance short
This commit is contained in:
commit
c8a4af99bc
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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{}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
2
pkg/bbgo/reflect_test.go
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
package bbgo
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
53
pkg/dynamic/call.go
Normal 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
29
pkg/dynamic/call_test.go
Normal 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
26
pkg/dynamic/field.go
Normal 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
54
pkg/dynamic/iterate.go
Normal 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
41
pkg/dynamic/merge.go
Normal 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
82
pkg/dynamic/merge_test.go
Normal 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
24
pkg/dynamic/typevalue.go
Normal 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
|
||||||
|
}
|
|
@ -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...")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"`
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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...")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
package types
|
|
||||||
|
|
||||||
type Subscriber interface {
|
|
||||||
Subscribe()
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user