add rsicross strategy

This commit is contained in:
c9s 2023-07-09 21:23:42 +08:00
parent 7c2de46273
commit 5c88abe72f
No known key found for this signature in database
GPG Key ID: 7385E7E464CB0A54
10 changed files with 311 additions and 113 deletions

53
config/rsicross.yaml Normal file
View File

@ -0,0 +1,53 @@
persistence:
json:
directory: var/data
redis:
host: 127.0.0.1
port: 6379
db: 0
sessions:
binance:
exchange: binance
envVarPrefix: binance
exchangeStrategies:
- on: binance
rsicross:
symbol: BTCUSDT
interval: 5m
fastWindow: 7
slowWindow: 12
quantity: 0.1
### RISK CONTROLS
## circuitBreakEMA is used for calculating the price for circuitBreak
# circuitBreakEMA:
# interval: 1m
# window: 14
## circuitBreakLossThreshold is the maximum loss threshold for realized+unrealized PnL
# circuitBreakLossThreshold: -10.0
## positionHardLimit is the maximum position limit
# positionHardLimit: 500.0
## maxPositionQuantity is the maximum quantity per order that could be controlled in positionHardLimit,
## this parameter is used with positionHardLimit togerther
# maxPositionQuantity: 10.0
backtest:
startTime: "2022-01-01"
endTime: "2022-02-01"
symbols:
- BTCUSDT
sessions: [binance]
# syncSecKLines: true
accounts:
binance:
makerFeeRate: 0.0%
takerFeeRate: 0.075%
balances:
BTC: 0.0
USDT: 10000.0

View File

@ -417,6 +417,8 @@ func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPos
return createdOrder, nil
}
log.WithError(err).Errorf("unable to submit order: %v", err)
log.Infof("reduce quantity and retry order")
return e.reduceQuantityAndSubmitOrder(ctx, price, *submitOrder)
}

View File

@ -222,7 +222,6 @@ func (trader *Trader) injectFieldsAndSubscribe(ctx context.Context) error {
// load and run Session strategies
for sessionName, strategies := range trader.exchangeStrategies {
var session = trader.environment.sessions[sessionName]
var orderExecutor = trader.getSessionOrderExecutor(sessionName)
for _, strategy := range strategies {
rs := reflect.ValueOf(strategy)
@ -237,10 +236,6 @@ func (trader *Trader) injectFieldsAndSubscribe(ctx context.Context) error {
return err
}
if err := dynamic.InjectField(rs, "OrderExecutor", orderExecutor, false); err != nil {
return errors.Wrapf(err, "failed to inject OrderExecutor on %T", strategy)
}
if defaulter, ok := strategy.(StrategyDefaulter); ok {
if err := defaulter.Defaults(); err != nil {
panic(err)
@ -441,7 +436,7 @@ func (trader *Trader) injectCommonServices(ctx context.Context, s interface{}) e
return fmt.Errorf("field Persistence is not a struct element, %s given", field)
}
if err := dynamic.InjectField(elem, "Facade", ps, true); err != nil {
if err := dynamic.InjectField(elem.Interface(), "Facade", ps, true); err != nil {
return err
}

View File

@ -27,6 +27,7 @@ import (
_ "github.com/c9s/bbgo/pkg/strategy/pricealert"
_ "github.com/c9s/bbgo/pkg/strategy/pricedrop"
_ "github.com/c9s/bbgo/pkg/strategy/rebalance"
_ "github.com/c9s/bbgo/pkg/strategy/rsicross"
_ "github.com/c9s/bbgo/pkg/strategy/rsmaker"
_ "github.com/c9s/bbgo/pkg/strategy/schedule"
_ "github.com/c9s/bbgo/pkg/strategy/scmaker"

View File

@ -3,22 +3,19 @@ package dynamic
import (
"fmt"
"reflect"
"testing"
"time"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/service"
"github.com/c9s/bbgo/pkg/types"
)
type testEnvironment struct {
startTime time.Time
}
func InjectField(rs reflect.Value, fieldName string, obj interface{}, pointerOnly bool) error {
func InjectField(target interface{}, fieldName string, obj interface{}, pointerOnly bool) error {
rs := reflect.ValueOf(target)
field := rs.FieldByName(fieldName)
if !field.IsValid() {
return nil
}
@ -131,96 +128,3 @@ func ParseStructAndInject(f interface{}, objects ...interface{}) error {
return nil
}
func Test_injectField(t *testing.T) {
type TT struct {
TradeService *service.TradeService
}
// only pointer object can be set.
var tt = &TT{}
// get the value of the pointer, or it can not be set.
var rv = reflect.ValueOf(tt).Elem()
_, ret := HasField(rv, "TradeService")
assert.True(t, ret)
ts := &service.TradeService{}
err := InjectField(rv, "TradeService", ts, true)
assert.NoError(t, err)
}
func Test_parseStructAndInject(t *testing.T) {
t.Run("skip nil", func(t *testing.T) {
ss := struct {
a int
Env *testEnvironment
}{
a: 1,
Env: nil,
}
err := ParseStructAndInject(&ss, nil)
assert.NoError(t, err)
assert.Nil(t, ss.Env)
})
t.Run("pointer", func(t *testing.T) {
ss := struct {
a int
Env *testEnvironment
}{
a: 1,
Env: nil,
}
err := ParseStructAndInject(&ss, &testEnvironment{})
assert.NoError(t, err)
assert.NotNil(t, ss.Env)
})
t.Run("composition", func(t *testing.T) {
type TT struct {
*service.TradeService
}
ss := TT{}
err := ParseStructAndInject(&ss, &service.TradeService{})
assert.NoError(t, err)
assert.NotNil(t, ss.TradeService)
})
t.Run("struct", func(t *testing.T) {
ss := struct {
a int
Env testEnvironment
}{
a: 1,
}
err := ParseStructAndInject(&ss, testEnvironment{
startTime: time.Now(),
})
assert.NoError(t, err)
assert.NotEqual(t, time.Time{}, ss.Env.startTime)
})
t.Run("interface/any", func(t *testing.T) {
ss := struct {
Any interface{} // anything
}{
Any: nil,
}
err := ParseStructAndInject(&ss, &testEnvironment{
startTime: time.Now(),
})
assert.NoError(t, err)
assert.NotNil(t, ss.Any)
})
t.Run("interface/stringer", func(t *testing.T) {
ss := struct {
Stringer types.Stringer // stringer interface
}{
Stringer: nil,
}
err := ParseStructAndInject(&ss, &types.Trade{})
assert.NoError(t, err)
assert.NotNil(t, ss.Stringer)
})
}

105
pkg/dynamic/inject_test.go Normal file
View File

@ -0,0 +1,105 @@
package dynamic
import (
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/service"
"github.com/c9s/bbgo/pkg/types"
)
func Test_injectField(t *testing.T) {
type TT struct {
TradeService *service.TradeService
}
// only pointer object can be set.
var tt = &TT{}
// get the value of the pointer, or it can not be set.
var rv = reflect.ValueOf(tt).Elem()
_, ret := HasField(rv, "TradeService")
assert.True(t, ret)
ts := &service.TradeService{}
err := InjectField(rv, "TradeService", ts, true)
assert.NoError(t, err)
}
func Test_parseStructAndInject(t *testing.T) {
t.Run("skip nil", func(t *testing.T) {
ss := struct {
a int
Env *testEnvironment
}{
a: 1,
Env: nil,
}
err := ParseStructAndInject(&ss, nil)
assert.NoError(t, err)
assert.Nil(t, ss.Env)
})
t.Run("pointer", func(t *testing.T) {
ss := struct {
a int
Env *testEnvironment
}{
a: 1,
Env: nil,
}
err := ParseStructAndInject(&ss, &testEnvironment{})
assert.NoError(t, err)
assert.NotNil(t, ss.Env)
})
t.Run("composition", func(t *testing.T) {
type TT struct {
*service.TradeService
}
ss := TT{}
err := ParseStructAndInject(&ss, &service.TradeService{})
assert.NoError(t, err)
assert.NotNil(t, ss.TradeService)
})
t.Run("struct", func(t *testing.T) {
ss := struct {
a int
Env testEnvironment
}{
a: 1,
}
err := ParseStructAndInject(&ss, testEnvironment{
startTime: time.Now(),
})
assert.NoError(t, err)
assert.NotEqual(t, time.Time{}, ss.Env.startTime)
})
t.Run("interface/any", func(t *testing.T) {
ss := struct {
Any interface{} // anything
}{
Any: nil,
}
err := ParseStructAndInject(&ss, &testEnvironment{
startTime: time.Now(),
})
assert.NoError(t, err)
assert.NotNil(t, ss.Any)
})
t.Run("interface/stringer", func(t *testing.T) {
ss := struct {
Stringer types.Stringer // stringer interface
}{
Stringer: nil,
}
err := ParseStructAndInject(&ss, &types.Trade{})
assert.NoError(t, err)
assert.NotNil(t, ss.Stringer)
})
}

View File

@ -20,7 +20,7 @@ type Strategy struct {
OrderExecutor *bbgo.GeneralOrderExecutor
}
func (s *Strategy) Setup(ctx context.Context, environ *bbgo.Environment, session *bbgo.ExchangeSession, market types.Market, strategyID, instanceID string) {
func (s *Strategy) Initialize(ctx context.Context, environ *bbgo.Environment, session *bbgo.ExchangeSession, market types.Market, strategyID, instanceID string) {
s.parent = ctx
s.ctx, s.cancel = context.WithCancel(ctx)
@ -53,6 +53,6 @@ func (s *Strategy) Setup(ctx context.Context, environ *bbgo.Environment, session
s.OrderExecutor.BindProfitStats(s.ProfitStats)
s.OrderExecutor.Bind()
s.OrderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) {
bbgo.Sync(ctx, s)
// bbgo.Sync(ctx, s)
})
}

View File

@ -3,9 +3,10 @@ package linregmaker
import (
"context"
"fmt"
"github.com/c9s/bbgo/pkg/risk/dynamicrisk"
"sync"
"github.com/c9s/bbgo/pkg/risk/dynamicrisk"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/util"
@ -221,20 +222,20 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
})
}
// Setup Exits
// Initialize Exits
s.ExitMethods.SetAndSubscribe(session, s)
// Setup dynamic spread
// Initialize dynamic spread
if s.DynamicSpread.IsEnabled() {
s.DynamicSpread.Initialize(s.Symbol, session)
}
// Setup dynamic exposure
// Initialize dynamic exposure
if s.DynamicExposure.IsEnabled() {
s.DynamicExposure.Initialize(s.Symbol, session)
}
// Setup dynamic quantities
// Initialize dynamic quantities
if len(s.DynamicQuantityIncrease) > 0 {
s.DynamicQuantityIncrease.Initialize(s.Symbol, session)
}

View File

@ -0,0 +1,137 @@
package rsicross
import (
"context"
"fmt"
"sync"
log "github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/risk/riskcontrol"
"github.com/c9s/bbgo/pkg/strategy/common"
"github.com/c9s/bbgo/pkg/types"
)
const ID = "rsicross"
func init() {
bbgo.RegisterStrategy(ID, &Strategy{})
}
type Strategy struct {
*common.Strategy
Environment *bbgo.Environment
Market types.Market
Symbol string `json:"symbol"`
Interval types.Interval `json:"interval"`
SlowWindow int `json:"slowWindow"`
FastWindow int `json:"fastWindow"`
bbgo.OpenPositionOptions
// risk related parameters
PositionHardLimit fixedpoint.Value `json:"positionHardLimit"`
MaxPositionQuantity fixedpoint.Value `json:"maxPositionQuantity"`
CircuitBreakLossThreshold fixedpoint.Value `json:"circuitBreakLossThreshold"`
CircuitBreakEMA types.IntervalWindow `json:"circuitBreakEMA"`
positionRiskControl *riskcontrol.PositionRiskControl
circuitBreakRiskControl *riskcontrol.CircuitBreakRiskControl
}
func (s *Strategy) ID() string {
return ID
}
func (s *Strategy) InstanceID() string {
return fmt.Sprintf("%s:%s:%s:%d-%d", ID, s.Symbol, s.Interval, s.FastWindow, s.SlowWindow)
}
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
}
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
s.Strategy = &common.Strategy{}
s.Strategy.Initialize(ctx, s.Environment, session, s.Market, ID, s.InstanceID())
if !s.PositionHardLimit.IsZero() && !s.MaxPositionQuantity.IsZero() {
log.Infof("positionHardLimit and maxPositionQuantity are configured, setting up PositionRiskControl...")
s.positionRiskControl = riskcontrol.NewPositionRiskControl(s.OrderExecutor, s.PositionHardLimit, s.MaxPositionQuantity)
}
if !s.CircuitBreakLossThreshold.IsZero() {
log.Infof("circuitBreakLossThreshold is configured, setting up CircuitBreakRiskControl...")
s.circuitBreakRiskControl = riskcontrol.NewCircuitBreakRiskControl(
s.Position,
session.Indicators(s.Symbol).EWMA(s.CircuitBreakEMA),
s.CircuitBreakLossThreshold,
s.ProfitStats)
}
fastRsi := session.Indicators(s.Symbol).RSI(types.IntervalWindow{Interval: s.Interval, Window: s.FastWindow})
slowRsi := session.Indicators(s.Symbol).RSI(types.IntervalWindow{Interval: s.Interval, Window: s.SlowWindow})
rsiCross := indicator.Cross(fastRsi, slowRsi)
rsiCross.OnUpdate(func(v float64) {
switch indicator.CrossType(v) {
case indicator.CrossOver:
opts := s.OpenPositionOptions
opts.Long = true
if price, ok := session.LastPrice(s.Symbol); ok {
opts.Price = price
}
// opts.Price = closePrice
opts.Tags = []string{"rsiCrossOver"}
if _, err := s.OrderExecutor.OpenPosition(ctx, opts); err != nil {
logErr(err, "unable to open position")
}
case indicator.CrossUnder:
if err := s.OrderExecutor.ClosePosition(ctx, fixedpoint.One); err != nil {
logErr(err, "failed to close position")
}
}
})
bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
})
return nil
}
func (s *Strategy) preloadKLines(inc *indicator.KLineStream, session *bbgo.ExchangeSession, symbol string, interval types.Interval) {
if store, ok := session.MarketDataStore(symbol); ok {
if kLinesData, ok := store.KLinesOfInterval(interval); ok {
for _, k := range *kLinesData {
inc.EmitUpdate(k)
}
}
}
}
func logErr(err error, msgAndArgs ...interface{}) bool {
if err == nil {
return false
}
if len(msgAndArgs) == 0 {
log.WithError(err).Error(err.Error())
} else if len(msgAndArgs) == 1 {
msg := msgAndArgs[0].(string)
log.WithError(err).Error(msg)
} else if len(msgAndArgs) > 1 {
msg := msgAndArgs[0].(string)
log.WithError(err).Errorf(msg, msgAndArgs[1:]...)
}
return true
}

View File

@ -101,7 +101,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
s.Strategy = &common.Strategy{}
s.Strategy.Setup(ctx, s.Environment, session, s.Market, ID, s.InstanceID())
s.Strategy.Initialize(ctx, s.Environment, session, s.Market, ID, s.InstanceID())
s.book = types.NewStreamBook(s.Symbol)
s.book.BindStream(session.UserDataStream)