feature: add heikinashi support

This commit is contained in:
zenix 2022-06-15 16:32:04 +09:00
parent 7225a597f2
commit f5007752b2
9 changed files with 177 additions and 65 deletions

View File

@ -4,6 +4,7 @@ sessions:
exchange: binance exchange: binance
futures: true futures: true
envVarPrefix: binance envVarPrefix: binance
useHeikinAshi: false
exchangeStrategies: exchangeStrategies:

View File

@ -2,6 +2,7 @@
sessions: sessions:
binance: binance:
exchange: binance exchange: binance
useHeikinAshi: true
envVarPrefix: binance envVarPrefix: binance
exchangeStrategies: exchangeStrategies:
@ -11,10 +12,11 @@ exchangeStrategies:
symbol: BNBBUSD symbol: BNBBUSD
backtest: backtest:
startTime: "2022-01-02" startTime: "2022-06-14"
endTime: "2022-01-19" endTime: "2022-06-15"
symbols: symbols:
- BNBBUSD - BNBBUSD
sessions: [binance]
account: account:
binance: binance:
balances: balances:

View File

@ -33,6 +33,8 @@ import (
"sync" "sync"
"time" "time"
"github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/cache" "github.com/c9s/bbgo/pkg/cache"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -42,6 +44,8 @@ import (
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
) )
var log = logrus.WithField("cmd", "backtest")
var ErrUnimplemented = errors.New("unimplemented method") var ErrUnimplemented = errors.New("unimplemented method")
type Exchange struct { type Exchange struct {
@ -53,7 +57,7 @@ type Exchange struct {
account *types.Account account *types.Account
config *bbgo.Backtest config *bbgo.Backtest
userDataStream, marketDataStream *Stream UserDataStream, MarketDataStream types.StandardStreamEmitter
trades map[string][]types.Trade trades map[string][]types.Trade
tradesMutex sync.Mutex tradesMutex sync.Mutex
@ -147,12 +151,14 @@ func (e *Exchange) _addMatchingBook(symbol string, market types.Market) {
} }
func (e *Exchange) NewStream() types.Stream { func (e *Exchange) NewStream() types.Stream {
return &Stream{exchange: e} return &types.BacktestStream{
StandardStreamEmitter: &types.StandardStream{},
}
} }
func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error) { func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error) {
if e.userDataStream == nil { if e.UserDataStream == nil {
return createdOrders, fmt.Errorf("SubmitOrders should be called after userDataStream been initialized") return createdOrders, fmt.Errorf("SubmitOrders should be called after UserDataStream been initialized")
} }
for _, order := range orders { for _, order := range orders {
symbol := order.Symbol symbol := order.Symbol
@ -175,7 +181,7 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder
e.addClosedOrder(*createdOrder) e.addClosedOrder(*createdOrder)
} }
e.userDataStream.EmitOrderUpdate(*createdOrder) e.UserDataStream.EmitOrderUpdate(*createdOrder)
} }
} }
@ -201,8 +207,8 @@ func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since,
} }
func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) error { func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) error {
if e.userDataStream == nil { if e.UserDataStream == nil {
return fmt.Errorf("CancelOrders should be called after userDataStream been initialized") return fmt.Errorf("CancelOrders should be called after UserDataStream been initialized")
} }
for _, order := range orders { for _, order := range orders {
matching, ok := e.matchingBook(order.Symbol) matching, ok := e.matchingBook(order.Symbol)
@ -214,7 +220,7 @@ func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) erro
return err return err
} }
e.userDataStream.EmitOrderUpdate(canceledOrder) e.UserDataStream.EmitOrderUpdate(canceledOrder)
} }
return nil return nil
@ -297,15 +303,15 @@ func (e *Exchange) matchingBook(symbol string) (*SimplePriceMatching, bool) {
} }
func (e *Exchange) InitMarketData() { func (e *Exchange) InitMarketData() {
e.userDataStream.OnTradeUpdate(func(trade types.Trade) { e.UserDataStream.OnTradeUpdate(func(trade types.Trade) {
e.addTrade(trade) e.addTrade(trade)
}) })
e.matchingBooksMutex.Lock() e.matchingBooksMutex.Lock()
for _, matching := range e.matchingBooks { for _, matching := range e.matchingBooks {
matching.OnTradeUpdate(e.userDataStream.EmitTradeUpdate) matching.OnTradeUpdate(e.UserDataStream.EmitTradeUpdate)
matching.OnOrderUpdate(e.userDataStream.EmitOrderUpdate) matching.OnOrderUpdate(e.UserDataStream.EmitOrderUpdate)
matching.OnBalanceUpdate(e.userDataStream.EmitBalanceUpdate) matching.OnBalanceUpdate(e.UserDataStream.EmitBalanceUpdate)
} }
e.matchingBooksMutex.Unlock() e.matchingBooksMutex.Unlock()
} }
@ -324,7 +330,7 @@ func (e *Exchange) SubscribeMarketData(extraIntervals ...types.Interval) (chan t
} }
// collect subscriptions // collect subscriptions
for _, sub := range e.marketDataStream.Subscriptions { for _, sub := range e.MarketDataStream.GetSubscriptions() {
loadedSymbols[sub.Symbol] = struct{}{} loadedSymbols[sub.Symbol] = struct{}{}
switch sub.Channel { switch sub.Channel {
@ -370,11 +376,11 @@ func (e *Exchange) ConsumeKLine(k types.KLine) {
matching.processKLine(k) matching.processKLine(k)
} }
e.marketDataStream.EmitKLineClosed(k) e.MarketDataStream.EmitKLineClosed(k)
} }
func (e *Exchange) CloseMarketData() error { func (e *Exchange) CloseMarketData() error {
if err := e.marketDataStream.Close(); err != nil { if err := e.MarketDataStream.Close(); err != nil {
log.WithError(err).Error("stream close error") log.WithError(err).Error("stream close error")
return err return err
} }

View File

@ -1,41 +0,0 @@
package backtest
import (
"context"
"github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/types"
)
var log = logrus.WithField("cmd", "backtest")
type Stream struct {
types.StandardStream
exchange *Exchange
}
func (s *Stream) Connect(ctx context.Context) error {
if s.PublicOnly {
if s.exchange.marketDataStream != nil {
panic("you should not set up more than 1 market data stream in back-test")
}
s.exchange.marketDataStream = s
} else {
// assign user data stream back
if s.exchange.userDataStream != nil {
panic("you should not set up more than 1 user data stream in back-test")
}
s.exchange.userDataStream = s
}
s.EmitConnect()
s.EmitStart()
return nil
}
func (s *Stream) Close() error {
return nil
}

View File

@ -213,6 +213,8 @@ type ExchangeSession struct {
Exchange types.Exchange `json:"-" yaml:"-"` Exchange types.Exchange `json:"-" yaml:"-"`
UseHeikinAshi bool `json:"useHeikinAshi,omitempty" yaml:"useHeikinAshi,omitempty"`
// Trades collects the executed trades from the exchange // Trades collects the executed trades from the exchange
// map: symbol -> []trade // map: symbol -> []trade
Trades map[string]*types.TradeSlice `json:"-" yaml:"-"` Trades map[string]*types.TradeSlice `json:"-" yaml:"-"`
@ -346,6 +348,12 @@ func (session *ExchangeSession) Init(ctx context.Context, environ *Environment)
} }
} }
if session.UseHeikinAshi {
session.MarketDataStream = &types.HeikinAshiStream{
StandardStreamEmitter: session.MarketDataStream.(types.StandardStreamEmitter),
}
}
// query and initialize the balances // query and initialize the balances
if !session.PublicOnly { if !session.PublicOnly {
account, err := session.Exchange.QueryAccount(ctx) account, err := session.Exchange.QueryAccount(ctx)
@ -400,13 +408,23 @@ func (session *ExchangeSession) Init(ctx context.Context, environ *Environment)
} }
// update last prices // update last prices
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { if session.UseHeikinAshi {
if _, ok := session.startPrices[kline.Symbol]; !ok { session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
session.startPrices[kline.Symbol] = kline.Open if _, ok := session.startPrices[kline.Symbol]; !ok {
} session.startPrices[kline.Symbol] = kline.Open
}
session.lastPrices[kline.Symbol] = kline.Close session.lastPrices[kline.Symbol] = session.MarketDataStream.(*types.HeikinAshiStream).LastOrigin[kline.Symbol][kline.Interval].Close
}) })
} else {
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
if _, ok := session.startPrices[kline.Symbol]; !ok {
session.startPrices[kline.Symbol] = kline.Open
}
session.lastPrices[kline.Symbol] = kline.Close
})
}
session.MarketDataStream.OnMarketTrade(func(trade types.Trade) { session.MarketDataStream.OnMarketTrade(func(trade types.Trade) {
session.lastPrices[trade.Symbol] = trade.Price session.lastPrices[trade.Symbol] = trade.Price

View File

@ -255,7 +255,11 @@ var BacktestCmd = &cobra.Command{
if err != nil { if err != nil {
return errors.Wrap(err, "failed to create backtest exchange") return errors.Wrap(err, "failed to create backtest exchange")
} }
environ.AddExchange(name.String(), backtestExchange) session := environ.AddExchange(name.String(), backtestExchange)
exchangeFromConfig := userConfig.Sessions[name.String()]
if exchangeFromConfig != nil {
session.UseHeikinAshi = exchangeFromConfig.UseHeikinAshi
}
} }
if err := environ.Init(ctx); err != nil { if err := environ.Init(ctx); err != nil {
@ -640,6 +644,8 @@ func confirmation(s string) bool {
func toExchangeSources(sessions map[string]*bbgo.ExchangeSession, extraIntervals ...types.Interval) (exchangeSources []backtest.ExchangeDataSource, err error) { func toExchangeSources(sessions map[string]*bbgo.ExchangeSession, extraIntervals ...types.Interval) (exchangeSources []backtest.ExchangeDataSource, err error) {
for _, session := range sessions { for _, session := range sessions {
exchange := session.Exchange.(*backtest.Exchange) exchange := session.Exchange.(*backtest.Exchange)
exchange.UserDataStream = session.UserDataStream.(types.StandardStreamEmitter)
exchange.MarketDataStream = session.MarketDataStream.(types.StandardStreamEmitter)
exchange.InitMarketData() exchange.InitMarketData()
c, err := exchange.SubscribeMarketData(extraIntervals...) c, err := exchange.SubscribeMarketData(extraIntervals...)

View File

@ -0,0 +1,19 @@
package types
import (
"context"
)
type BacktestStream struct {
StandardStreamEmitter
}
func (s *BacktestStream) Connect(ctx context.Context) error {
s.EmitConnect()
s.EmitStart()
return nil
}
func (s *BacktestStream) Close() error {
return nil
}

View File

@ -0,0 +1,72 @@
package types
import (
"github.com/c9s/bbgo/pkg/fixedpoint"
)
var Four fixedpoint.Value = fixedpoint.NewFromInt(4)
type HeikinAshiStream struct {
StandardStreamEmitter
lastAshi map[string]map[Interval]*KLine
LastOrigin map[string]map[Interval]*KLine
}
func (s *HeikinAshiStream) EmitKLineClosed(kline KLine) {
ashi := kline
if s.lastAshi == nil {
s.lastAshi = make(map[string]map[Interval]*KLine)
s.LastOrigin = make(map[string]map[Interval]*KLine)
}
if s.lastAshi[kline.Symbol] == nil {
s.lastAshi[kline.Symbol] = make(map[Interval]*KLine)
s.LastOrigin[kline.Symbol] = make(map[Interval]*KLine)
}
lastAshi := s.lastAshi[kline.Symbol][kline.Interval]
if lastAshi == nil {
ashi.Close = kline.Close.Add(kline.High).
Add(kline.Low).
Add(kline.Open).
Div(Four)
// High and Low are the same
s.lastAshi[kline.Symbol][kline.Interval] = &ashi
s.LastOrigin[kline.Symbol][kline.Interval] = &kline
} else {
ashi.Close = kline.Close.Add(kline.High).
Add(kline.Low).
Add(kline.Open).
Div(Four)
ashi.Open = lastAshi.Open.Add(lastAshi.Close).Div(Two)
// High and Low are the same
s.lastAshi[kline.Symbol][kline.Interval] = &ashi
s.LastOrigin[kline.Symbol][kline.Interval] = &kline
}
s.StandardStreamEmitter.EmitKLineClosed(ashi)
}
// No writeback to lastAshi
func (s *HeikinAshiStream) EmitKLine(kline KLine) {
ashi := kline
if s.lastAshi == nil {
s.lastAshi = make(map[string]map[Interval]*KLine)
}
if s.lastAshi[kline.Symbol] == nil {
s.lastAshi[kline.Symbol] = make(map[Interval]*KLine)
}
lastAshi := s.lastAshi[kline.Symbol][kline.Interval]
if lastAshi == nil {
ashi.Close = kline.Close.Add(kline.High).
Add(kline.Low).
Add(kline.Open).
Div(Four)
} else {
ashi.Close = kline.Close.Add(kline.High).
Add(kline.Low).
Add(kline.Open).
Div(Four)
ashi.Open = lastAshi.Open.Add(lastAshi.Close).Div(Two)
}
s.StandardStreamEmitter.EmitKLine(ashi)
}
var _ StandardStreamEmitter = &HeikinAshiStream{}

View File

@ -28,7 +28,9 @@ type Stream interface {
StandardStreamEventHub StandardStreamEventHub
Subscribe(channel Channel, symbol string, options SubscribeOptions) Subscribe(channel Channel, symbol string, options SubscribeOptions)
GetSubscriptions() []Subscription
SetPublicOnly() SetPublicOnly()
GetPublicOnly() bool
Connect(ctx context.Context) error Connect(ctx context.Context) error
Close() error Close() error
} }
@ -104,6 +106,25 @@ type StandardStream struct {
FuturesPositionSnapshotCallbacks []func(futuresPositions FuturesPositionMap) FuturesPositionSnapshotCallbacks []func(futuresPositions FuturesPositionMap)
} }
type StandardStreamEmitter interface {
Stream
EmitStart()
EmitConnect()
EmitDisconnect()
EmitTradeUpdate(Trade)
EmitOrderUpdate(Order)
EmitBalanceSnapshot(BalanceMap)
EmitBalanceUpdate(BalanceMap)
EmitKLineClosed(KLine)
EmitKLine(KLine)
EmitBookUpdate(SliceOrderBook)
EmitBookTickerUpdate(BookTicker)
EmitBookSnapshot(SliceOrderBook)
EmitMarketTrade(Trade)
EmitFuturesPositionUpdate(FuturesPositionMap)
EmitFuturesPositionSnapshot(FuturesPositionMap)
}
func NewStandardStream() StandardStream { func NewStandardStream() StandardStream {
return StandardStream{ return StandardStream{
ReconnectC: make(chan struct{}, 1), ReconnectC: make(chan struct{}, 1),
@ -115,6 +136,10 @@ func (s *StandardStream) SetPublicOnly() {
s.PublicOnly = true s.PublicOnly = true
} }
func (s *StandardStream) GetPublicOnly() bool {
return s.PublicOnly
}
func (s *StandardStream) SetEndpointCreator(creator EndpointCreator) { func (s *StandardStream) SetEndpointCreator(creator EndpointCreator) {
s.endpointCreator = creator s.endpointCreator = creator
} }
@ -254,6 +279,10 @@ func (s *StandardStream) ping(ctx context.Context, conn *websocket.Conn, cancel
} }
} }
func (s *StandardStream) GetSubscriptions() []Subscription {
return s.Subscriptions
}
func (s *StandardStream) Subscribe(channel Channel, symbol string, options SubscribeOptions) { func (s *StandardStream) Subscribe(channel Channel, symbol string, options SubscribeOptions) {
s.Subscriptions = append(s.Subscriptions, Subscription{ s.Subscriptions = append(s.Subscriptions, Subscription{
Channel: channel, Channel: channel,