Merge pull request #252 from c9s/feature/rbt

feature: improve orderbook interface
This commit is contained in:
Yo-An Lin 2021-05-22 16:51:21 +08:00 committed by GitHub
commit 499816040a
48 changed files with 1362 additions and 800 deletions

View File

@ -308,7 +308,6 @@ import (
_ "github.com/c9s/bbgo/pkg/strategy/buyandhold"
_ "github.com/c9s/bbgo/pkg/strategy/flashcrash"
_ "github.com/c9s/bbgo/pkg/strategy/grid"
_ "github.com/c9s/bbgo/pkg/strategy/mirrormaker"
_ "github.com/c9s/bbgo/pkg/strategy/pricealert"
_ "github.com/c9s/bbgo/pkg/strategy/support"
_ "github.com/c9s/bbgo/pkg/strategy/swing"

View File

@ -48,11 +48,11 @@ var rootCmd = &cobra.Command{
stream.SetPublicOnly()
stream.Subscribe(types.BookChannel, symbol, types.SubscribeOptions{})
stream.OnBookSnapshot(func(book types.OrderBook) {
stream.OnBookSnapshot(func(book types.SliceOrderBook) {
// log.Infof("book snapshot: %+v", book)
})
stream.OnBookUpdate(func(book types.OrderBook) {
stream.OnBookUpdate(func(book types.SliceOrderBook) {
// log.Infof("book update: %+v", book)
})
@ -67,7 +67,7 @@ var rootCmd = &cobra.Command{
return
case <-streambook.C:
book := streambook.Get()
book := streambook.Copy()
if valid, err := book.IsValid(); !valid {
log.Errorf("order book is invalid, error: %v", err)

View File

@ -9,7 +9,6 @@ import (
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/datatype"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
@ -237,7 +236,7 @@ func (m *SimplePriceMatching) newTradeFromOrder(order types.Order, isMaker bool)
Side: order.Side,
IsBuyer: order.Side == types.SideTypeBuy,
IsMaker: isMaker,
Time: datatype.Time(m.CurrentTime),
Time: types.Time(m.CurrentTime),
Fee: fee,
FeeCurrency: feeCurrency,
}
@ -439,7 +438,7 @@ func (m *SimplePriceMatching) newOrder(o types.SubmitOrder, orderID uint64) type
Status: types.OrderStatusNew,
ExecutedQuantity: 0,
IsWorking: true,
CreationTime: datatype.Time(m.CurrentTime),
UpdateTime: datatype.Time(m.CurrentTime),
CreationTime: types.Time(m.CurrentTime),
UpdateTime: types.Time(m.CurrentTime),
}
}

View File

@ -42,7 +42,7 @@ func (store *MarketDataStore) KLinesOfInterval(interval types.Interval) (kLines
return kLines, ok
}
func (store *MarketDataStore) handleOrderBookUpdate(book types.OrderBook) {
func (store *MarketDataStore) handleOrderBookUpdate(book types.SliceOrderBook) {
if book.Symbol != store.Symbol {
return
}
@ -52,7 +52,7 @@ func (store *MarketDataStore) handleOrderBookUpdate(book types.OrderBook) {
store.EmitOrderBookUpdate(store.orderBook)
}
func (store *MarketDataStore) handleOrderBookSnapshot(book types.OrderBook) {
func (store *MarketDataStore) handleOrderBookSnapshot(book types.SliceOrderBook) {
if book.Symbol != store.Symbol {
return
}

View File

@ -64,24 +64,13 @@ func (e *TwapExecution) connectUserData(ctx context.Context) {
}
func (e *TwapExecution) getSideBook() (pvs types.PriceVolumeSlice, err error) {
book := e.orderBook.Get()
switch e.Side {
case types.SideTypeSell:
pvs = book.Asks
case types.SideTypeBuy:
pvs = book.Bids
default:
err = fmt.Errorf("invalid side type: %+v", e.Side)
}
book := e.orderBook.Copy()
pvs = book.SideBook(e.Side)
return pvs, err
}
func (e *TwapExecution) newBestPriceOrder() (orderForm types.SubmitOrder, err error) {
book := e.orderBook.Get()
book := e.orderBook.Copy()
sideBook, err := e.getSideBook()
if err != nil {

View File

@ -7,7 +7,6 @@ import (
_ "github.com/c9s/bbgo/pkg/strategy/flashcrash"
_ "github.com/c9s/bbgo/pkg/strategy/gap"
_ "github.com/c9s/bbgo/pkg/strategy/grid"
_ "github.com/c9s/bbgo/pkg/strategy/mirrormaker"
_ "github.com/c9s/bbgo/pkg/strategy/pricealert"
_ "github.com/c9s/bbgo/pkg/strategy/schedule"
_ "github.com/c9s/bbgo/pkg/strategy/support"

View File

@ -49,10 +49,10 @@ var orderbookCmd = &cobra.Command{
s := ex.NewStream()
s.SetPublicOnly()
s.Subscribe(types.BookChannel, symbol, types.SubscribeOptions{})
s.OnBookSnapshot(func(book types.OrderBook) {
s.OnBookSnapshot(func(book types.SliceOrderBook) {
log.Infof("orderbook snapshot: %s", book.String())
})
s.OnBookUpdate(func(book types.OrderBook) {
s.OnBookUpdate(func(book types.SliceOrderBook) {
log.Infof("orderbook update: %s", book.String())
})

View File

@ -8,7 +8,6 @@ import (
"github.com/adshao/go-binance/v2"
"github.com/pkg/errors"
"github.com/c9s/bbgo/pkg/datatype"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/util"
@ -150,8 +149,8 @@ func ToGlobalOrder(binanceOrder *binance.Order, isMargin bool) (*types.Order, er
OrderID: uint64(binanceOrder.OrderID),
Status: toGlobalOrderStatus(binanceOrder.Status),
ExecutedQuantity: util.MustParseFloat(binanceOrder.ExecutedQuantity),
CreationTime: datatype.Time(millisecondTime(binanceOrder.Time)),
UpdateTime: datatype.Time(millisecondTime(binanceOrder.UpdateTime)),
CreationTime: types.Time(millisecondTime(binanceOrder.Time)),
UpdateTime: types.Time(millisecondTime(binanceOrder.UpdateTime)),
IsMargin: isMargin,
IsIsolated: binanceOrder.IsIsolated,
}, nil
@ -208,7 +207,7 @@ func ToGlobalTrade(t binance.TradeV3, isMargin bool) (*types.Trade, error) {
IsMaker: t.IsMaker,
Fee: fee,
FeeCurrency: t.CommissionAsset,
Time: datatype.Time(millisecondTime(t.Time)),
Time: types.Time(millisecondTime(t.Time)),
IsMargin: isMargin,
IsIsolated: t.IsIsolated,
}, nil

View File

@ -14,7 +14,6 @@ import (
"github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/datatype"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/util"
@ -283,7 +282,7 @@ func (e *Exchange) QueryWithdrawHistory(ctx context.Context, asset string, since
txIDs[d.TxID] = struct{}{}
allWithdraws = append(allWithdraws, types.Withdraw{
Exchange: types.ExchangeBinance,
ApplyTime: datatype.Time(time.Unix(0, d.ApplyTime*int64(time.Millisecond))),
ApplyTime: types.Time(time.Unix(0, d.ApplyTime*int64(time.Millisecond))),
Asset: d.Asset,
Amount: d.Amount,
Address: d.Address,
@ -356,7 +355,7 @@ func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since,
txIDs[d.TxID] = struct{}{}
allDeposits = append(allDeposits, types.Deposit{
Exchange: types.ExchangeBinance,
Time: datatype.Time(time.Unix(0, d.InsertTime*int64(time.Millisecond))),
Time: types.Time(time.Unix(0, d.InsertTime*int64(time.Millisecond))),
Asset: d.Asset,
Amount: d.Amount,
Address: d.Address,

View File

@ -9,7 +9,6 @@ import (
"github.com/adshao/go-binance/v2"
"github.com/valyala/fastjson"
"github.com/c9s/bbgo/pkg/datatype"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/util"
@ -126,7 +125,7 @@ func (e *ExecutionReportEvent) Order() (*types.Order, error) {
OrderID: uint64(e.OrderID),
Status: toGlobalOrderStatus(binance.OrderStatusType(e.CurrentOrderStatus)),
ExecutedQuantity: util.MustParseFloat(e.CumulativeFilledQuantity),
CreationTime: datatype.Time(orderCreationTime),
CreationTime: types.Time(orderCreationTime),
}, nil
}
@ -147,7 +146,7 @@ func (e *ExecutionReportEvent) Trade() (*types.Trade, error) {
QuoteQuantity: util.MustParseFloat(e.LastQuoteAssetTransactedQuantity),
IsBuyer: e.Side == "BUY",
IsMaker: e.IsMaker,
Time: datatype.Time(tt),
Time: types.Time(tt),
Fee: util.MustParseFloat(e.CommissionAmount),
FeeCurrency: e.CommissionAsset,
}, nil
@ -320,7 +319,7 @@ type DepthEvent struct {
Asks []DepthEntry
}
func (e *DepthEvent) OrderBook() (book types.OrderBook, err error) {
func (e *DepthEvent) OrderBook() (book types.SliceOrderBook, err error) {
book.Symbol = e.Symbol
for _, entry := range e.Bids {

View File

@ -7,7 +7,6 @@ import (
log "github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/datatype"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
@ -52,8 +51,8 @@ func toGlobalOrder(r order) (types.Order, error) {
OrderID: uint64(r.ID),
Status: "",
ExecutedQuantity: r.FilledSize,
CreationTime: datatype.Time(r.CreatedAt.Time),
UpdateTime: datatype.Time(r.CreatedAt.Time),
CreationTime: types.Time(r.CreatedAt.Time),
UpdateTime: types.Time(r.CreatedAt.Time),
}
// `new` (accepted but not processed yet), `open`, or `closed` (filled or cancelled)
@ -93,7 +92,7 @@ func toGlobalDeposit(input depositHistory) (types.Deposit, error) {
d := types.Deposit{
GID: 0,
Exchange: types.ExchangeFTX,
Time: datatype.Time(t.Time),
Time: types.Time(t.Time),
Amount: input.Size,
Asset: toGlobalCurrency(input.Coin),
TransactionID: input.TxID,
@ -126,7 +125,7 @@ func toGlobalTrade(f fill) (types.Trade, error) {
Side: f.Side,
IsBuyer: f.Side == types.SideTypeBuy,
IsMaker: f.Liquidity == "maker",
Time: datatype.Time(f.Time.Time),
Time: types.Time(f.Time.Time),
Fee: f.Fee,
FeeCurrency: f.FeeCurrency,
IsMargin: false,

View File

@ -13,7 +13,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/datatype"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
@ -467,7 +466,7 @@ func TestExchange_QueryDepositHistory(t *testing.T) {
assert.Len(t, dh, 1)
assert.Equal(t, types.Deposit{
Exchange: types.ExchangeFTX,
Time: datatype.Time(actualConfirmedTime),
Time: types.Time(actualConfirmedTime),
Amount: 99.0,
Asset: "TUSD",
TransactionID: "0x8078356ae4b06a036d64747546c274af19581f1c78c510b60505798a7ffcaf1",
@ -610,7 +609,7 @@ func TestExchange_QueryTrades(t *testing.T) {
Side: types.SideTypeSell,
IsBuyer: false,
IsMaker: true,
Time: datatype.Time(actualConfirmedTime),
Time: types.Time(actualConfirmedTime),
Fee: -0.0033625,
FeeCurrency: "USD",
IsMargin: false,

View File

@ -6,7 +6,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/datatype"
"github.com/c9s/bbgo/pkg/types"
)
@ -55,8 +54,8 @@ func Test_messageHandler_handleMessage(t *testing.T) {
OrderID: 36379,
Status: types.OrderStatusFilled,
ExecutedQuantity: 1.0,
CreationTime: datatype.Time(mustParseDatetime("2021-03-28T06:12:50.991447+00:00")),
UpdateTime: datatype.Time(mustParseDatetime("2021-03-28T06:12:50.991447+00:00")),
CreationTime: types.Time(mustParseDatetime("2021-03-28T06:12:50.991447+00:00")),
UpdateTime: types.Time(mustParseDatetime("2021-03-28T06:12:50.991447+00:00")),
}, order)
})
h.handleMessage(input)
@ -104,7 +103,7 @@ func Test_messageHandler_handleMessage(t *testing.T) {
Side: types.SideTypeBuy,
IsBuyer: true,
IsMaker: false,
Time: datatype.Time(mustParseDatetime("2021-03-28T06:12:34.702926+00:00")),
Time: types.Time(mustParseDatetime("2021-03-28T06:12:34.702926+00:00")),
Fee: 0.00153917575,
FeeCurrency: "USD",
IsMargin: false,

View File

@ -351,16 +351,16 @@ func checksumString(bids, asks [][]json.Number) string {
var errUnmatchedChecksum = fmt.Errorf("unmatched checksum")
func toGlobalOrderBook(r orderBookResponse) (types.OrderBook, error) {
func toGlobalOrderBook(r orderBookResponse) (types.SliceOrderBook, error) {
bids, err := toPriceVolumeSlice(r.Bids)
if err != nil {
return types.OrderBook{}, fmt.Errorf("can't convert bids to priceVolumeSlice: %w", err)
return types.SliceOrderBook{}, fmt.Errorf("can't convert bids to priceVolumeSlice: %w", err)
}
asks, err := toPriceVolumeSlice(r.Asks)
if err != nil {
return types.OrderBook{}, fmt.Errorf("can't convert asks to priceVolumeSlice: %w", err)
return types.SliceOrderBook{}, fmt.Errorf("can't convert asks to priceVolumeSlice: %w", err)
}
return types.OrderBook{
return types.SliceOrderBook{
// ex. BTC/USDT
Symbol: toGlobalSymbol(strings.ToUpper(r.Market)),
Bids: bids,

View File

@ -8,7 +8,6 @@ import (
"github.com/pkg/errors"
"github.com/c9s/bbgo/pkg/datatype"
"github.com/c9s/bbgo/pkg/exchange/max/maxapi"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
@ -201,8 +200,8 @@ func toGlobalOrder(maxOrder max.Order) (*types.Order, error) {
OrderID: maxOrder.ID,
Status: toGlobalOrderStatus(maxOrder.State, executedVolume, remainingVolume),
ExecutedQuantity: executedVolume.Float64(),
CreationTime: datatype.Time(maxOrder.CreatedAt),
UpdateTime: datatype.Time(maxOrder.CreatedAt),
CreationTime: types.Time(maxOrder.CreatedAt),
UpdateTime: types.Time(maxOrder.CreatedAt),
}, nil
}
@ -246,7 +245,7 @@ func toGlobalTrade(t max.Trade) (*types.Trade, error) {
Fee: fee,
FeeCurrency: toGlobalCurrency(t.FeeCurrency),
QuoteQuantity: quoteQuantity,
Time: datatype.Time(mts),
Time: types.Time(mts),
}, nil
}

View File

@ -13,7 +13,6 @@ import (
"github.com/sirupsen/logrus"
"golang.org/x/time/rate"
"github.com/c9s/bbgo/pkg/datatype"
maxapi "github.com/c9s/bbgo/pkg/exchange/max/maxapi"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
@ -610,7 +609,7 @@ func (e *Exchange) QueryWithdrawHistory(ctx context.Context, asset string, since
txIDs[d.TxID] = struct{}{}
withdraw := types.Withdraw{
Exchange: types.ExchangeMax,
ApplyTime: datatype.Time(time.Unix(d.CreatedAt, 0)),
ApplyTime: types.Time(time.Unix(d.CreatedAt, 0)),
Asset: toGlobalCurrency(d.Currency),
Amount: util.MustParseFloat(d.Amount),
Address: "",
@ -682,7 +681,7 @@ func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since,
allDeposits = append(allDeposits, types.Deposit{
Exchange: types.ExchangeMax,
Time: datatype.Time(time.Unix(d.CreatedAt, 0)),
Time: types.Time(time.Unix(d.CreatedAt, 0)),
Amount: util.MustParseFloat(d.Amount),
Asset: toGlobalCurrency(d.Currency),
Address: "", // not supported

View File

@ -174,7 +174,7 @@ func (e *BookEvent) Time() time.Time {
return time.Unix(0, e.Timestamp*int64(time.Millisecond))
}
func (e *BookEvent) OrderBook() (snapshot types.OrderBook, err error) {
func (e *BookEvent) OrderBook() (snapshot types.SliceOrderBook, err error) {
snapshot.Symbol = strings.ToUpper(e.Market)
for _, bid := range e.Bids {

View File

@ -6,7 +6,6 @@ import (
"fmt"
"strings"
"github.com/c9s/bbgo/pkg/datatype"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
@ -112,7 +111,7 @@ func (reward Reward) Reward() (*types.Reward, error) {
State: reward.State,
Note: reward.Note,
Spent: false,
CreatedAt: datatype.Time(reward.CreatedAt),
CreatedAt: types.Time(reward.CreatedAt),
}, nil
}

View File

@ -8,7 +8,6 @@ import (
"github.com/gorilla/websocket"
"github.com/c9s/bbgo/pkg/datatype"
max "github.com/c9s/bbgo/pkg/exchange/max/maxapi"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
@ -226,7 +225,7 @@ func convertWebSocketTrade(t max.TradeUpdate) (*types.Trade, error) {
Fee: fee,
FeeCurrency: toGlobalCurrency(t.FeeCurrency),
QuoteQuantity: quoteQuantity,
Time: datatype.Time(mts),
Time: types.Time(mts),
}, nil
}
@ -257,6 +256,6 @@ func toGlobalOrderUpdate(u max.OrderUpdate) (*types.Order, error) {
OrderID: u.ID,
Status: toGlobalOrderStatus(u.State, executedVolume, remainingVolume),
ExecutedQuantity: executedVolume.Float64(),
CreationTime: datatype.Time(time.Unix(0, u.CreatedAtMs*int64(time.Millisecond))),
CreationTime: types.Time(time.Unix(0, u.CreatedAtMs*int64(time.Millisecond))),
}, nil
}

View File

@ -7,7 +7,6 @@ import (
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/datatype"
"github.com/c9s/bbgo/pkg/types"
)
@ -24,7 +23,7 @@ func TestDepositService(t *testing.T) {
err = service.Insert(types.Deposit{
Exchange: types.ExchangeMax,
Time: datatype.Time(time.Now()),
Time: types.Time(time.Now()),
Amount: 0.001,
Asset: "BTC",
Address: "test",

View File

@ -8,7 +8,6 @@ import (
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/datatype"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
@ -34,7 +33,7 @@ func TestRewardService_InsertAndQueryUnspent(t *testing.T) {
Quantity: 1,
State: "done",
Spent: false,
CreatedAt: datatype.Time(time.Now()),
CreatedAt: types.Time(time.Now()),
})
assert.NoError(t, err)
@ -52,7 +51,7 @@ func TestRewardService_InsertAndQueryUnspent(t *testing.T) {
Quantity: 1000000,
State: "done",
Spent: false,
CreatedAt: datatype.Time(time.Now()),
CreatedAt: types.Time(time.Now()),
})
assert.NoError(t, err)
@ -99,7 +98,7 @@ func TestRewardService_AggregateUnspentCurrencyPosition(t *testing.T) {
Quantity: 1,
State: "done",
Spent: false,
CreatedAt: datatype.Time(now),
CreatedAt: types.Time(now),
})
assert.NoError(t, err)
@ -111,7 +110,7 @@ func TestRewardService_AggregateUnspentCurrencyPosition(t *testing.T) {
Quantity: 2,
State: "done",
Spent: false,
CreatedAt: datatype.Time(now),
CreatedAt: types.Time(now),
})
assert.NoError(t, err)
@ -123,7 +122,7 @@ func TestRewardService_AggregateUnspentCurrencyPosition(t *testing.T) {
Quantity: 1000000,
State: "done",
Spent: false,
CreatedAt: datatype.Time(now),
CreatedAt: types.Time(now),
})
assert.NoError(t, err)

View File

@ -7,7 +7,6 @@ import (
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/datatype"
"github.com/c9s/bbgo/pkg/types"
)
@ -30,7 +29,7 @@ func TestWithdrawService(t *testing.T) {
TransactionID: "01",
TransactionFee: 0.0001,
Network: "omni",
ApplyTime: datatype.Time(time.Now()),
ApplyTime: types.Time(time.Now()),
})
assert.NoError(t, err)

View File

@ -234,8 +234,8 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se
continue
}
sourceBook := s.sourceBook.Get()
book := s.tradingBook.Get()
sourceBook := s.sourceBook.Copy()
book := s.tradingBook.Copy()
bestBid, hasBid := book.BestBid()
bestAsk, hasAsk := book.BestAsk()

View File

@ -1,310 +0,0 @@
package mirrormaker
import (
"context"
"fmt"
"sync"
"time"
"github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
const ID = "mirrormaker"
var defaultMargin = fixedpoint.NewFromFloat(0.01)
var defaultQuantity = fixedpoint.NewFromFloat(0.001)
var log = logrus.WithField("strategy", ID)
func init() {
bbgo.RegisterStrategy(ID, &Strategy{})
}
type Strategy struct {
*bbgo.Graceful
*bbgo.Persistence
Symbol string `json:"symbol"`
SourceExchange string `json:"sourceExchange"`
MakerExchange string `json:"makerExchange"`
UpdateInterval time.Duration `json:"updateInterval"`
Margin fixedpoint.Value `json:"margin"`
BidMargin fixedpoint.Value `json:"bidMargin"`
AskMargin fixedpoint.Value `json:"askMargin"`
Quantity fixedpoint.Value `json:"quantity"`
QuantityMultiplier fixedpoint.Value `json:"quantityMultiplier"`
NumLayers int `json:"numLayers"`
Pips int `json:"pips"`
makerSession *bbgo.ExchangeSession
sourceSession *bbgo.ExchangeSession
sourceMarket types.Market
makerMarket types.Market
book *types.StreamOrderBook
activeMakerOrders *bbgo.LocalActiveOrderBook
orderStore *bbgo.OrderStore
Position fixedpoint.Value
lastPrice float64
stopC chan struct{}
}
func (s *Strategy) ID() string {
return ID
}
func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) {
sourceSession, ok := sessions[s.SourceExchange]
if !ok {
panic(fmt.Errorf("source exchange %s is not defined", s.SourceExchange))
}
log.Infof("subscribing %s from %s", s.Symbol, s.SourceExchange)
sourceSession.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{})
}
func (s *Strategy) updateQuote(ctx context.Context) {
if err := s.makerSession.Exchange.CancelOrders(ctx, s.activeMakerOrders.Orders()...); err != nil {
log.WithError(err).Errorf("can not cancel orders")
return
}
// avoid unlock issue
time.Sleep(100 * time.Millisecond)
sourceBook := s.book.Get()
if len(sourceBook.Bids) == 0 || len(sourceBook.Asks) == 0 {
return
}
bestBidPrice := sourceBook.Bids[0].Price
bestAskPrice := sourceBook.Asks[0].Price
log.Infof("best bid price %f, best ask price: %f", bestBidPrice.Float64(), bestAskPrice.Float64())
bidQuantity := s.Quantity
bidPrice := bestBidPrice.MulFloat64(1.0 - s.BidMargin.Float64())
askQuantity := s.Quantity
askPrice := bestAskPrice.MulFloat64(1.0 + s.AskMargin.Float64())
log.Infof("quote bid price: %f ask price: %f", bidPrice.Float64(), askPrice.Float64())
var submitOrders []types.SubmitOrder
balances := s.makerSession.Account.Balances()
makerQuota := &bbgo.QuotaTransaction{}
if b, ok := balances[s.makerMarket.BaseCurrency]; ok {
makerQuota.BaseAsset.Add(b.Available)
}
if b, ok := balances[s.makerMarket.QuoteCurrency]; ok {
makerQuota.QuoteAsset.Add(b.Available)
}
hedgeBalances := s.sourceSession.Account.Balances()
hedgeQuota := &bbgo.QuotaTransaction{}
if b, ok := hedgeBalances[s.sourceMarket.BaseCurrency]; ok {
hedgeQuota.BaseAsset.Add(b.Available)
}
if b, ok := hedgeBalances[s.sourceMarket.QuoteCurrency]; ok {
hedgeQuota.QuoteAsset.Add(b.Available)
}
log.Infof("maker quota: %+v", makerQuota)
log.Infof("hedge quota: %+v", hedgeQuota)
for i := 0; i < s.NumLayers; i++ {
// bid orders
if makerQuota.QuoteAsset.Lock(bidQuantity.Mul(bidPrice)) && hedgeQuota.BaseAsset.Lock(bidQuantity) {
// if we bought, then we need to sell the base from the hedge session
submitOrders = append(submitOrders, types.SubmitOrder{
Symbol: s.Symbol,
Type: types.OrderTypeLimit,
Side: types.SideTypeBuy,
Price: bidPrice.Float64(),
Quantity: bidQuantity.Float64(),
TimeInForce: "GTC",
})
makerQuota.Commit()
hedgeQuota.Commit()
} else {
makerQuota.Rollback()
hedgeQuota.Rollback()
}
// ask orders
if makerQuota.BaseAsset.Lock(askQuantity) && hedgeQuota.QuoteAsset.Lock(askQuantity.Mul(askPrice)) {
// if we bought, then we need to sell the base from the hedge session
submitOrders = append(submitOrders, types.SubmitOrder{
Symbol: s.Symbol,
Type: types.OrderTypeLimit,
Side: types.SideTypeSell,
Price: askPrice.Float64(),
Quantity: askQuantity.Float64(),
TimeInForce: "GTC",
})
makerQuota.Commit()
hedgeQuota.Commit()
} else {
makerQuota.Rollback()
hedgeQuota.Rollback()
}
bidPrice -= fixedpoint.NewFromFloat(s.makerMarket.TickSize * float64(s.Pips))
askPrice += fixedpoint.NewFromFloat(s.makerMarket.TickSize * float64(s.Pips))
askQuantity = askQuantity.Mul(s.QuantityMultiplier)
bidQuantity = bidQuantity.Mul(s.QuantityMultiplier)
}
if len(submitOrders) == 0 {
return
}
makerOrderExecutor := &bbgo.ExchangeOrderExecutor{Session: s.makerSession}
makerOrders, err := makerOrderExecutor.SubmitOrders(ctx, submitOrders...)
if err != nil {
log.WithError(err).Errorf("order submit error")
return
}
s.activeMakerOrders.Add(makerOrders...)
s.orderStore.Add(makerOrders...)
}
func (s *Strategy) handleTradeUpdate(trade types.Trade) {
log.Infof("received trade %+v", trade)
if s.orderStore.Exists(trade.OrderID) {
log.Infof("identified trade %d with an existing order: %d", trade.ID, trade.OrderID)
q := fixedpoint.NewFromFloat(trade.Quantity)
if trade.Side == types.SideTypeSell {
q = -q
}
s.Position.AtomicAdd(q)
pos := s.Position.AtomicLoad()
log.Warnf("position changed: %f", pos.Float64())
s.lastPrice = trade.Price
}
}
func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, sessions map[string]*bbgo.ExchangeSession) error {
if s.UpdateInterval == 0 {
s.UpdateInterval = time.Second
}
if s.NumLayers == 0 {
s.NumLayers = 1
}
if s.BidMargin == 0 {
if s.Margin != 0 {
s.BidMargin = s.Margin
} else {
s.BidMargin = defaultMargin
}
}
if s.AskMargin == 0 {
if s.Margin != 0 {
s.AskMargin = s.Margin
} else {
s.AskMargin = defaultMargin
}
}
if s.Quantity == 0 {
s.Quantity = defaultQuantity
}
sourceSession, ok := sessions[s.SourceExchange]
if !ok {
return fmt.Errorf("source exchange session %s is not defined", s.SourceExchange)
}
s.sourceSession = sourceSession
makerSession, ok := sessions[s.MakerExchange]
if !ok {
return fmt.Errorf("maker exchange session %s is not defined", s.MakerExchange)
}
s.makerSession = makerSession
s.sourceMarket, ok = s.sourceSession.Market(s.Symbol)
if !ok {
return fmt.Errorf("source session market %s is not defined", s.Symbol)
}
s.makerMarket, ok = s.makerSession.Market(s.Symbol)
if !ok {
return fmt.Errorf("maker session market %s is not defined", s.Symbol)
}
s.book = types.NewStreamBook(s.Symbol)
s.book.BindStream(s.sourceSession.Stream)
s.makerSession.Stream.OnTradeUpdate(s.handleTradeUpdate)
s.activeMakerOrders = bbgo.NewLocalActiveOrderBook()
s.activeMakerOrders.BindStream(s.makerSession.Stream)
s.orderStore = bbgo.NewOrderStore(s.Symbol)
s.orderStore.BindStream(s.makerSession.Stream)
s.stopC = make(chan struct{})
if err := s.Persistence.Load(&s.Position, "position"); err != nil {
log.WithError(err).Warnf("can not load position")
} else {
log.Infof("position is loaded successfully, position=%f", s.Position.Float64())
}
go func() {
ticker := time.NewTicker(s.UpdateInterval)
defer ticker.Stop()
for {
select {
case <-s.stopC:
return
case <-ctx.Done():
return
case <-ticker.C:
s.updateQuote(ctx)
}
}
}()
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
close(s.stopC)
if err := s.Persistence.Save(&s.Position, "position"); err != nil {
log.WithError(err).Error("persistence save error")
}
if err := s.makerSession.Exchange.CancelOrders(ctx, s.activeMakerOrders.Orders()...); err != nil {
log.WithError(err).Errorf("can not cancel orders")
}
})
return nil
}

View File

@ -184,11 +184,7 @@ func (s *Strategy) updateQuote(ctx context.Context, orderExecutionRouter bbgo.Or
return
}
sourceBook := s.book.Get()
if len(sourceBook.Bids) == 0 || len(sourceBook.Asks) == 0 {
return
}
sourceBook := s.book.Copy()
if valid, err := sourceBook.IsValid(); !valid {
log.WithError(err).Errorf("%s invalid order book, skip quoting: %v", s.Symbol, err)
return
@ -265,8 +261,11 @@ func (s *Strategy) updateQuote(ctx context.Context, orderExecutionRouter bbgo.Or
return
}
bestBidPrice := sourceBook.Bids[0].Price
bestAskPrice := sourceBook.Asks[0].Price
bestBid, _ := sourceBook.BestBid()
bestBidPrice := bestBid.Price
bestAsk, _ := sourceBook.BestAsk()
bestAskPrice := bestAsk.Price
log.Infof("%s book ticker: best ask / best bid = %f / %f", s.Symbol, bestAskPrice.Float64(), bestBidPrice.Float64())
var submitOrders []types.SubmitOrder
@ -335,7 +334,7 @@ func (s *Strategy) updateQuote(ctx context.Context, orderExecutionRouter bbgo.Or
}
accumulativeBidQuantity += bidQuantity
bidPrice := aggregatePrice(sourceBook.Bids, accumulativeBidQuantity)
bidPrice := aggregatePrice(sourceBook.SideBook(types.SideTypeBuy), accumulativeBidQuantity)
bidPrice = bidPrice.MulFloat64(1.0 - bidMargin.Float64())
if i > 0 && pips > 0 {
@ -382,7 +381,7 @@ func (s *Strategy) updateQuote(ctx context.Context, orderExecutionRouter bbgo.Or
}
accumulativeAskQuantity += askQuantity
askPrice := aggregatePrice(sourceBook.Asks, accumulativeAskQuantity)
askPrice := aggregatePrice(sourceBook.SideBook(types.SideTypeSell), accumulativeAskQuantity)
askPrice = askPrice.MulFloat64(1.0 + askMargin.Float64())
if i > 0 && pips > 0 {
askPrice -= pips.MulFloat64(s.makerMarket.TickSize)
@ -443,25 +442,20 @@ func (s *Strategy) Hedge(ctx context.Context, pos fixedpoint.Value) {
}
lastPrice := s.lastPrice
sourceBook := s.book.Get()
sourceBook := s.book.Copy()
switch side {
case types.SideTypeBuy:
if len(sourceBook.Asks) > 0 {
if pv, ok := sourceBook.Asks.First(); ok {
lastPrice = pv.Price.Float64()
}
if bestAsk, ok := sourceBook.BestAsk(); ok {
lastPrice = bestAsk.Price.Float64()
}
case types.SideTypeSell:
if len(sourceBook.Bids) > 0 {
if pv, ok := sourceBook.Bids.First(); ok {
lastPrice = pv.Price.Float64()
if bestBid, ok := sourceBook.BestBid(); ok {
lastPrice = bestBid.Price.Float64()
}
}
}
notional := quantity.MulFloat64(lastPrice)
if notional.Float64() <= s.sourceMarket.MinNotional {
log.Warnf("%s %f less than min notional, skipping", s.Symbol, notional.Float64())

View File

@ -110,7 +110,7 @@ func (s *Strategy) update(orderExecutor bbgo.OrderExecutor, session *bbgo.Exchan
func (s *Strategy) updateOrders(orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession, side types.SideType) {
var book = s.book.Copy()
var pvs = book.PriceVolumesBySide(side)
var pvs = book.SideBook(side)
if pvs == nil || len(pvs) == 0 {
log.Warnf("empty side: %s", side)
return

View File

@ -1,4 +1,5 @@
package types
const Green = "#228B22"
const Red = "#800000"
const GreenColor = "#228B22"
const RedColor = "#800000"
const GrayColor = "#f0f0f0"

View File

@ -2,8 +2,6 @@ package types
import (
"time"
"github.com/c9s/bbgo/pkg/datatype"
)
type DepositStatus string
@ -26,7 +24,7 @@ const (
type Deposit struct {
GID int64 `json:"gid" db:"gid"`
Exchange ExchangeName `json:"exchange" db:"exchange"`
Time datatype.Time `json:"time" db:"time"`
Time Time `json:"time" db:"time"`
Amount float64 `json:"amount" db:"amount"`
Asset string `json:"asset" db:"asset"`
Address string `json:"address" db:"address"`

View File

@ -177,11 +177,11 @@ func (k KLine) String() string {
func (k KLine) Color() string {
if k.Direction() > 0 {
return Green
return GreenColor
} else if k.Direction() < 0 {
return Red
return RedColor
}
return "#f0f0f0"
return GrayColor
}
func (k KLine) SlackAttachment() slack.Attachment {
@ -313,25 +313,26 @@ func (k KLineWindow) GetTrend() int {
func (k KLineWindow) Color() string {
if k.GetTrend() > 0 {
return Green
return GreenColor
} else if k.GetTrend() < 0 {
return Red
return RedColor
}
return "#f0f0f0"
return GrayColor
}
// Mid price
func (k KLineWindow) Mid() float64 {
return k.GetHigh() - k.GetLow()/2
}
// green candle with open and close near high price
// BounceUp returns true if it's green candle with open and close near high price
func (k KLineWindow) BounceUp() bool {
mid := k.Mid()
trend := k.GetTrend()
return trend > 0 && k.GetOpen() > mid && k.GetClose() > mid
}
// red candle with open and close near low price
// BounceDown returns true red candle with open and close near low price
func (k KLineWindow) BounceDown() bool {
mid := k.Mid()
trend := k.GetTrend()

View File

@ -9,7 +9,6 @@ import (
"github.com/pkg/errors"
"github.com/slack-go/slack"
"github.com/c9s/bbgo/pkg/datatype"
"github.com/c9s/bbgo/pkg/util"
)
@ -151,8 +150,8 @@ type Order struct {
Status OrderStatus `json:"status" db:"status"`
ExecutedQuantity float64 `json:"executedQuantity" db:"executed_quantity"`
IsWorking bool `json:"isWorking" db:"is_working"`
CreationTime datatype.Time `json:"creationTime" db:"created_at"`
UpdateTime datatype.Time `json:"updateTime" db:"updated_at"`
CreationTime Time `json:"creationTime" db:"created_at"`
UpdateTime Time `json:"updateTime" db:"updated_at"`
IsMargin bool `json:"isMargin" db:"is_margin"`
IsIsolated bool `json:"isIsolated" db:"is_isolated"`

View File

@ -2,12 +2,10 @@ package types
import (
"fmt"
"sort"
"strings"
"os"
"strconv"
"sync"
"github.com/pkg/errors"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/sigchan"
)
@ -21,301 +19,43 @@ func (p PriceVolume) String() string {
return fmt.Sprintf("PriceVolume{ price: %f, volume: %f }", p.Price.Float64(), p.Volume.Float64())
}
type PriceVolumeSlice []PriceVolume
func (slice PriceVolumeSlice) Len() int { return len(slice) }
func (slice PriceVolumeSlice) Less(i, j int) bool { return slice[i].Price < slice[j].Price }
func (slice PriceVolumeSlice) Swap(i, j int) { slice[i], slice[j] = slice[j], slice[i] }
// Trim removes the pairs that volume = 0
func (slice PriceVolumeSlice) Trim() (pvs PriceVolumeSlice) {
for _, pv := range slice {
if pv.Volume > 0 {
pvs = append(pvs, pv)
}
}
return pvs
}
func (slice PriceVolumeSlice) CopyDepth(depth int) PriceVolumeSlice {
if depth > len(slice) {
return slice.Copy()
}
var s = make(PriceVolumeSlice, depth, depth)
copy(s, slice[:depth])
return s
}
func (slice PriceVolumeSlice) Copy() PriceVolumeSlice {
var s = make(PriceVolumeSlice, len(slice), len(slice))
copy(s, slice)
return s
}
func (slice PriceVolumeSlice) Second() (PriceVolume, bool) {
if len(slice) > 1 {
return slice[1], true
}
return PriceVolume{}, false
}
func (slice PriceVolumeSlice) First() (PriceVolume, bool) {
if len(slice) > 0 {
return slice[0], true
}
return PriceVolume{}, false
}
func (slice PriceVolumeSlice) IndexByVolumeDepth(requiredVolume fixedpoint.Value) int {
var tv int64 = 0
for x, el := range slice {
tv += el.Volume.Int64()
if tv >= requiredVolume.Int64() {
return x
}
}
// not deep enough
return -1
}
func (slice PriceVolumeSlice) InsertAt(idx int, pv PriceVolume) PriceVolumeSlice {
rear := append([]PriceVolume{}, slice[idx:]...)
newSlice := append(slice[:idx], pv)
return append(newSlice, rear...)
}
func (slice PriceVolumeSlice) Remove(price fixedpoint.Value, descending bool) PriceVolumeSlice {
matched, idx := slice.Find(price, descending)
if matched.Price != price {
return slice
}
return append(slice[:idx], slice[idx+1:]...)
}
// FindPriceVolumePair finds the pair by the given price, this function is a read-only
// operation, so we use the value receiver to avoid copy value from the pointer
// If the price is not found, it will return the index where the price can be inserted at.
// true for descending (bid orders), false for ascending (ask orders)
func (slice PriceVolumeSlice) Find(price fixedpoint.Value, descending bool) (pv PriceVolume, idx int) {
idx = sort.Search(len(slice), func(i int) bool {
if descending {
return slice[i].Price <= price
}
return slice[i].Price >= price
})
if idx >= len(slice) || slice[idx].Price != price {
return pv, idx
}
pv = slice[idx]
return pv, idx
}
func (slice PriceVolumeSlice) Upsert(pv PriceVolume, descending bool) PriceVolumeSlice {
if len(slice) == 0 {
return append(slice, pv)
}
price := pv.Price
_, idx := slice.Find(price, descending)
if idx >= len(slice) || slice[idx].Price != price {
return slice.InsertAt(idx, pv)
}
slice[idx].Volume = pv.Volume
return slice
}
//go:generate callbackgen -type OrderBook
type OrderBook struct {
Symbol string
Bids PriceVolumeSlice
Asks PriceVolumeSlice
loadCallbacks []func(book *OrderBook)
updateCallbacks []func(book *OrderBook)
bidsChangeCallbacks []func(pvs PriceVolumeSlice)
asksChangeCallbacks []func(pvs PriceVolumeSlice)
}
func (b *OrderBook) Spread() (fixedpoint.Value, bool) {
bestBid, ok := b.BestBid()
if !ok {
return 0, false
}
bestAsk, ok := b.BestAsk()
if !ok {
return 0, false
}
return bestAsk.Price - bestBid.Price, true
}
func (b *OrderBook) BestBid() (PriceVolume, bool) {
if len(b.Bids) == 0 {
return PriceVolume{}, false
}
return b.Bids[0], true
}
func (b *OrderBook) BestAsk() (PriceVolume, bool) {
if len(b.Asks) == 0 {
return PriceVolume{}, false
}
return b.Asks[0], true
}
func (b *OrderBook) IsValid() (bool, error) {
bid, hasBid := b.BestBid()
ask, hasAsk := b.BestAsk()
if !hasBid {
return false, errors.New("empty bids")
}
if !hasAsk {
return false, errors.New("empty asks")
}
if bid.Price > ask.Price {
return false, fmt.Errorf("bid price %f > ask price %f", bid.Price.Float64(), ask.Price.Float64())
}
return true, nil
}
func (b *OrderBook) PriceVolumesBySide(side SideType) PriceVolumeSlice {
switch side {
case SideTypeBuy:
return b.Bids
case SideTypeSell:
return b.Asks
}
return nil
}
func (b *OrderBook) CopyDepth(depth int) (book OrderBook) {
book = *b
book.Bids = book.Bids.CopyDepth(depth)
book.Asks = book.Asks.CopyDepth(depth)
return book
}
func (b *OrderBook) Copy() (book OrderBook) {
book = *b
book.Bids = book.Bids.Copy()
book.Asks = book.Asks.Copy()
return book
}
func (b *OrderBook) updateAsks(pvs PriceVolumeSlice) {
for _, pv := range pvs {
if pv.Volume == 0 {
b.Asks = b.Asks.Remove(pv.Price, false)
} else {
b.Asks = b.Asks.Upsert(pv, false)
}
}
b.EmitAsksChange(b.Asks)
}
func (b *OrderBook) updateBids(pvs PriceVolumeSlice) {
for _, pv := range pvs {
if pv.Volume == 0 {
b.Bids = b.Bids.Remove(pv.Price, true)
} else {
b.Bids = b.Bids.Upsert(pv, true)
}
}
b.EmitBidsChange(b.Bids)
}
func (b *OrderBook) update(book OrderBook) {
b.updateBids(book.Bids)
b.updateAsks(book.Asks)
}
func (b *OrderBook) Reset() {
b.Bids = nil
b.Asks = nil
}
func (b *OrderBook) Load(book OrderBook) {
b.Reset()
b.update(book)
b.EmitLoad(b)
}
func (b *OrderBook) Update(book OrderBook) {
b.update(book)
b.EmitUpdate(b)
}
func (b *OrderBook) Print() {
fmt.Printf(b.String())
}
func (b *OrderBook) String() string {
sb := strings.Builder{}
sb.WriteString("BOOK ")
sb.WriteString(b.Symbol)
sb.WriteString("\n")
if len(b.Asks) > 0 {
sb.WriteString("ASKS:\n")
for i := len(b.Asks) - 1; i >= 0; i-- {
sb.WriteString("- ASK: ")
sb.WriteString(b.Asks[i].String())
sb.WriteString("\n")
}
}
if len(b.Bids) > 0 {
sb.WriteString("BIDS:\n")
for _, bid := range b.Bids {
sb.WriteString("- BID: ")
sb.WriteString(bid.String())
sb.WriteString("\n")
}
}
return sb.String()
type OrderBook interface {
Spread() (fixedpoint.Value, bool)
BestAsk() (PriceVolume, bool)
BestBid() (PriceVolume, bool)
Reset()
Load(book SliceOrderBook)
Update(book SliceOrderBook)
Copy() OrderBook
CopyDepth(depth int) OrderBook
SideBook(sideType SideType) PriceVolumeSlice
IsValid() (bool, error)
}
type MutexOrderBook struct {
sync.Mutex
*OrderBook
Symbol string
OrderBook OrderBook
}
func NewMutexOrderBook(symbol string) *MutexOrderBook {
var book OrderBook = NewSliceOrderBook(symbol)
if v, _ := strconv.ParseBool(os.Getenv("ENABLE_RBT_ORDERBOOK")); v {
book = NewRBOrderBook(symbol)
}
return &MutexOrderBook{
OrderBook: &OrderBook{Symbol: symbol},
Symbol: symbol,
OrderBook: book,
}
}
func (b *MutexOrderBook) Load(book OrderBook) {
func (b *MutexOrderBook) Load(book SliceOrderBook) {
b.Lock()
defer b.Unlock()
b.OrderBook.Reset()
b.OrderBook.update(book)
b.EmitLoad(b.OrderBook)
b.OrderBook.Load(book)
b.Unlock()
}
func (b *MutexOrderBook) Reset() {
@ -330,18 +70,16 @@ func (b *MutexOrderBook) CopyDepth(depth int) OrderBook {
return b.OrderBook.CopyDepth(depth)
}
func (b *MutexOrderBook) Get() OrderBook {
func (b *MutexOrderBook) Copy() OrderBook {
b.Lock()
defer b.Unlock()
return b.OrderBook.Copy()
}
func (b *MutexOrderBook) Update(update OrderBook) {
func (b *MutexOrderBook) Update(update SliceOrderBook) {
b.Lock()
defer b.Unlock()
b.OrderBook.update(update)
b.EmitUpdate(b.OrderBook)
b.OrderBook.Update(update)
b.Unlock()
}
// StreamOrderBook receives streaming data from websocket connection and
@ -360,8 +98,8 @@ func NewStreamBook(symbol string) *StreamOrderBook {
}
func (sb *StreamOrderBook) BindStream(stream Stream) {
stream.OnBookSnapshot(func(book OrderBook) {
if sb.Symbol != book.Symbol {
stream.OnBookSnapshot(func(book SliceOrderBook) {
if sb.MutexOrderBook.Symbol != book.Symbol {
return
}
@ -369,8 +107,8 @@ func (sb *StreamOrderBook) BindStream(stream Stream) {
sb.C.Emit()
})
stream.OnBookUpdate(func(book OrderBook) {
if sb.Symbol != book.Symbol {
stream.OnBookUpdate(func(book SliceOrderBook) {
if sb.MutexOrderBook.Symbol != book.Symbol {
return
}

View File

@ -1,43 +0,0 @@
// Code generated by "callbackgen -type OrderBook"; DO NOT EDIT.
package types
func (b *OrderBook) OnLoad(cb func(book *OrderBook)) {
b.loadCallbacks = append(b.loadCallbacks, cb)
}
func (b *OrderBook) EmitLoad(book *OrderBook) {
for _, cb := range b.loadCallbacks {
cb(book)
}
}
func (b *OrderBook) OnUpdate(cb func(book *OrderBook)) {
b.updateCallbacks = append(b.updateCallbacks, cb)
}
func (b *OrderBook) EmitUpdate(book *OrderBook) {
for _, cb := range b.updateCallbacks {
cb(book)
}
}
func (b *OrderBook) OnBidsChange(cb func(pvs PriceVolumeSlice)) {
b.bidsChangeCallbacks = append(b.bidsChangeCallbacks, cb)
}
func (b *OrderBook) EmitBidsChange(pvs PriceVolumeSlice) {
for _, cb := range b.bidsChangeCallbacks {
cb(pvs)
}
}
func (b *OrderBook) OnAsksChange(cb func(pvs PriceVolumeSlice)) {
b.asksChangeCallbacks = append(b.asksChangeCallbacks, cb)
}
func (b *OrderBook) EmitAsksChange(pvs PriceVolumeSlice) {
for _, cb := range b.asksChangeCallbacks {
cb(pvs)
}
}

View File

@ -1,6 +1,7 @@
package types
import (
"math/rand"
"testing"
"github.com/stretchr/testify/assert"
@ -8,8 +9,95 @@ import (
"github.com/c9s/bbgo/pkg/fixedpoint"
)
func prepareOrderBookBenchmarkData() (asks, bids PriceVolumeSlice) {
for p := 0.0; p < 1000.0; p++ {
asks = append(asks, PriceVolume{fixedpoint.NewFromFloat(1000 + p), fixedpoint.NewFromFloat(1)})
bids = append(bids, PriceVolume{fixedpoint.NewFromFloat(1000 - 0.1 - p), fixedpoint.NewFromFloat(1)})
}
return
}
func BenchmarkOrderBook_Load(b *testing.B) {
var asks, bids = prepareOrderBookBenchmarkData()
for p := 0.0; p < 1000.0; p++ {
asks = append(asks, PriceVolume{fixedpoint.NewFromFloat(1000 + p), fixedpoint.NewFromFloat(1)})
bids = append(bids, PriceVolume{fixedpoint.NewFromFloat(1000 - 0.1 - p), fixedpoint.NewFromFloat(1)})
}
b.Run("RBTOrderBook", func(b *testing.B) {
book := NewRBOrderBook("ETHUSDT")
for i := 0; i < b.N; i++ {
for _, ask := range asks {
book.Asks.Upsert(ask.Price, ask.Volume)
}
for _, bid := range bids {
book.Bids.Upsert(bid.Price, bid.Volume)
}
}
})
b.Run("OrderBook", func(b *testing.B) {
book := &SliceOrderBook{}
for i := 0; i < b.N; i++ {
for _, ask := range asks {
book.Asks = book.Asks.Upsert(ask, false)
}
for _, bid := range bids {
book.Bids = book.Bids.Upsert(bid, true)
}
}
})
}
func BenchmarkOrderBook_UpdateAndInsert(b *testing.B) {
var asks, bids = prepareOrderBookBenchmarkData()
for p := 0.0; p < 1000.0; p += 2 {
asks = append(asks, PriceVolume{fixedpoint.NewFromFloat(1000 + p), fixedpoint.NewFromFloat(1)})
bids = append(bids, PriceVolume{fixedpoint.NewFromFloat(1000 - 0.1 - p), fixedpoint.NewFromFloat(1)})
}
rbBook := NewRBOrderBook("ETHUSDT")
for _, ask := range asks {
rbBook.Asks.Upsert(ask.Price, ask.Volume)
}
for _, bid := range bids {
rbBook.Bids.Upsert(bid.Price, bid.Volume)
}
b.Run("RBTOrderBook", func(b *testing.B) {
for i := 0; i < b.N; i++ {
var price = fixedpoint.NewFromFloat(rand.Float64() * 2000.0)
if price >= fixedpoint.NewFromFloat(1000) {
rbBook.Asks.Upsert(price, fixedpoint.NewFromFloat(1))
} else {
rbBook.Bids.Upsert(price, fixedpoint.NewFromFloat(1))
}
}
})
sliceBook := &SliceOrderBook{}
for i := 0; i < b.N; i++ {
for _, ask := range asks {
sliceBook.Asks = sliceBook.Asks.Upsert(ask, false)
}
for _, bid := range bids {
sliceBook.Bids = sliceBook.Bids.Upsert(bid, true)
}
}
b.Run("OrderBook", func(b *testing.B) {
for i := 0; i < b.N; i++ {
var price = fixedpoint.NewFromFloat(rand.Float64() * 2000.0)
if price >= fixedpoint.NewFromFloat(1000) {
sliceBook.Asks = sliceBook.Asks.Upsert(PriceVolume{Price: price, Volume: fixedpoint.NewFromFloat(1)}, false)
} else {
sliceBook.Bids = sliceBook.Bids.Upsert(PriceVolume{Price: price, Volume: fixedpoint.NewFromFloat(1)}, true)
}
}
})
}
func TestOrderBook_IsValid(t *testing.T) {
ob := OrderBook{
ob := SliceOrderBook{
Bids: PriceVolumeSlice{
{fixedpoint.NewFromFloat(100.0), fixedpoint.NewFromFloat(1.5)},
{fixedpoint.NewFromFloat(90.0), fixedpoint.NewFromFloat(2.5)},

View File

@ -0,0 +1,118 @@
package types
import (
"sort"
"github.com/c9s/bbgo/pkg/fixedpoint"
)
type PriceVolumeSlice []PriceVolume
func (slice PriceVolumeSlice) Len() int { return len(slice) }
func (slice PriceVolumeSlice) Less(i, j int) bool { return slice[i].Price < slice[j].Price }
func (slice PriceVolumeSlice) Swap(i, j int) { slice[i], slice[j] = slice[j], slice[i] }
// Trim removes the pairs that volume = 0
func (slice PriceVolumeSlice) Trim() (pvs PriceVolumeSlice) {
for _, pv := range slice {
if pv.Volume > 0 {
pvs = append(pvs, pv)
}
}
return pvs
}
func (slice PriceVolumeSlice) CopyDepth(depth int) PriceVolumeSlice {
if depth > len(slice) {
return slice.Copy()
}
var s = make(PriceVolumeSlice, depth, depth)
copy(s, slice[:depth])
return s
}
func (slice PriceVolumeSlice) Copy() PriceVolumeSlice {
var s = make(PriceVolumeSlice, len(slice), len(slice))
copy(s, slice)
return s
}
func (slice PriceVolumeSlice) Second() (PriceVolume, bool) {
if len(slice) > 1 {
return slice[1], true
}
return PriceVolume{}, false
}
func (slice PriceVolumeSlice) First() (PriceVolume, bool) {
if len(slice) > 0 {
return slice[0], true
}
return PriceVolume{}, false
}
func (slice PriceVolumeSlice) IndexByVolumeDepth(requiredVolume fixedpoint.Value) int {
var tv int64 = 0
for x, el := range slice {
tv += el.Volume.Int64()
if tv >= requiredVolume.Int64() {
return x
}
}
// not deep enough
return -1
}
func (slice PriceVolumeSlice) InsertAt(idx int, pv PriceVolume) PriceVolumeSlice {
rear := append([]PriceVolume{}, slice[idx:]...)
newSlice := append(slice[:idx], pv)
return append(newSlice, rear...)
}
func (slice PriceVolumeSlice) Remove(price fixedpoint.Value, descending bool) PriceVolumeSlice {
matched, idx := slice.Find(price, descending)
if matched.Price != price {
return slice
}
return append(slice[:idx], slice[idx+1:]...)
}
// Find finds the pair by the given price, this function is a read-only
// operation, so we use the value receiver to avoid copy value from the pointer
// If the price is not found, it will return the index where the price can be inserted at.
// true for descending (bid orders), false for ascending (ask orders)
func (slice PriceVolumeSlice) Find(price fixedpoint.Value, descending bool) (pv PriceVolume, idx int) {
idx = sort.Search(len(slice), func(i int) bool {
if descending {
return slice[i].Price <= price
}
return slice[i].Price >= price
})
if idx >= len(slice) || slice[idx].Price != price {
return pv, idx
}
pv = slice[idx]
return pv, idx
}
func (slice PriceVolumeSlice) Upsert(pv PriceVolume, descending bool) PriceVolumeSlice {
if len(slice) == 0 {
return append(slice, pv)
}
price := pv.Price
_, idx := slice.Find(price, descending)
if idx >= len(slice) || slice[idx].Price != price {
return slice.InsertAt(idx, pv)
}
slice[idx].Volume = pv.Volume
return slice
}

184
pkg/types/rbtorderbook.go Normal file
View File

@ -0,0 +1,184 @@
package types
import (
"fmt"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/pkg/errors"
)
//go:generate callbackgen -type RBTOrderBook
type RBTOrderBook struct {
Symbol string
Bids *RBTree
Asks *RBTree
loadCallbacks []func(book *RBTOrderBook)
updateCallbacks []func(book *RBTOrderBook)
}
func NewRBOrderBook(symbol string) *RBTOrderBook {
return &RBTOrderBook{
Symbol: symbol,
Bids: NewRBTree(),
Asks: NewRBTree(),
}
}
func (b *RBTOrderBook) BestBid() (PriceVolume, bool) {
right := b.Bids.Rightmost(b.Bids.Root)
if right != nil {
return PriceVolume{Price: right.Key, Volume: right.Value}, true
}
return PriceVolume{}, false
}
func (b *RBTOrderBook) BestAsk() (PriceVolume, bool) {
left := b.Asks.Leftmost(b.Bids.Root)
if left != nil {
return PriceVolume{Price: left.Key, Volume: left.Value}, true
}
return PriceVolume{}, false
}
func (b *RBTOrderBook) Spread() (fixedpoint.Value, bool) {
bestBid, ok := b.BestBid()
if !ok {
return 0, false
}
bestAsk, ok := b.BestAsk()
if !ok {
return 0, false
}
return bestAsk.Price - bestBid.Price, true
}
func (b *RBTOrderBook) IsValid() (bool, error) {
bid, hasBid := b.BestBid()
ask, hasAsk := b.BestAsk()
if !hasBid {
return false, errors.New("empty bids")
}
if !hasAsk {
return false, errors.New("empty asks")
}
if bid.Price > ask.Price {
return false, fmt.Errorf("bid price %f > ask price %f", bid.Price.Float64(), ask.Price.Float64())
}
return true, nil
}
func (b *RBTOrderBook) Load(book SliceOrderBook) {
b.Reset()
b.update(book)
b.EmitLoad(b)
}
func (b *RBTOrderBook) Update(book SliceOrderBook) {
b.update(book)
b.EmitUpdate(b)
}
func (b *RBTOrderBook) Reset() {
b.Bids = NewRBTree()
b.Asks = NewRBTree()
}
func (b *RBTOrderBook) updateAsks(pvs PriceVolumeSlice) {
for _, pv := range pvs {
if pv.Volume == 0 {
b.Asks.Delete(pv.Price)
} else {
b.Asks.Upsert(pv.Price, pv.Volume)
}
}
}
func (b *RBTOrderBook) updateBids(pvs PriceVolumeSlice) {
for _, pv := range pvs {
if pv.Volume == 0 {
b.Bids.Delete(pv.Price)
} else {
b.Bids.Upsert(pv.Price, pv.Volume)
}
}
}
func (b *RBTOrderBook) update(book SliceOrderBook) {
b.updateBids(book.Bids)
b.updateAsks(book.Asks)
}
func (b *RBTOrderBook) load(book SliceOrderBook) {
b.Reset()
b.updateBids(book.Bids)
b.updateAsks(book.Asks)
}
func (b *RBTOrderBook) Copy() OrderBook {
var book = NewRBOrderBook(b.Symbol)
book.Asks = b.Asks.Copy()
book.Bids = b.Bids.Copy()
return book
}
func (b *RBTOrderBook) CopyDepth(depth int) OrderBook {
var book = NewRBOrderBook(b.Symbol)
book.Asks = b.Asks.CopyInorder(depth)
book.Bids = b.Bids.CopyInorder(depth)
return book
}
func (b *RBTOrderBook) convertTreeToPriceVolumeSlice(tree *RBTree, descending bool) (pvs PriceVolumeSlice) {
if descending {
tree.InorderReverse(func(n *RBNode) bool {
pvs = append(pvs, PriceVolume{
Price: n.Key,
Volume: n.Value,
})
return true
})
return pvs
}
tree.Inorder(func(n *RBNode) bool {
pvs = append(pvs, PriceVolume{
Price: n.Key,
Volume: n.Value,
})
return true
})
return pvs
}
func (b *RBTOrderBook) SideBook(sideType SideType) PriceVolumeSlice {
switch sideType {
case SideTypeBuy:
return b.convertTreeToPriceVolumeSlice(b.Bids, false)
case SideTypeSell:
return b.convertTreeToPriceVolumeSlice(b.Asks, true)
default:
return nil
}
}
func (b *RBTOrderBook) Print() {
b.Asks.Inorder(func(n *RBNode) bool {
fmt.Printf("ask: %f x %f", n.Key.Float64(), n.Value.Float64())
return true
})
b.Bids.InorderReverse(func(n *RBNode) bool {
fmt.Printf("bid: %f x %f", n.Key.Float64(), n.Value.Float64())
return true
})
}

View File

@ -0,0 +1,25 @@
// Code generated by "callbackgen -type RBTOrderBook"; DO NOT EDIT.
package types
import ()
func (b *RBTOrderBook) OnLoad(cb func(book *RBTOrderBook)) {
b.loadCallbacks = append(b.loadCallbacks, cb)
}
func (b *RBTOrderBook) EmitLoad(book *RBTOrderBook) {
for _, cb := range b.loadCallbacks {
cb(book)
}
}
func (b *RBTOrderBook) OnUpdate(cb func(book *RBTOrderBook)) {
b.updateCallbacks = append(b.updateCallbacks, cb)
}
func (b *RBTOrderBook) EmitUpdate(book *RBTOrderBook) {
for _, cb := range b.updateCallbacks {
cb(book)
}
}

470
pkg/types/rbtree.go Normal file
View File

@ -0,0 +1,470 @@
package types
import (
"fmt"
"github.com/c9s/bbgo/pkg/fixedpoint"
)
var Neel = &RBNode{
Color: Black,
}
type RBTree struct {
Root *RBNode
size int
}
func NewRBTree() *RBTree {
var root = Neel
root.Parent = Neel
return &RBTree{
Root: root,
}
}
func (tree *RBTree) Delete(key fixedpoint.Value) bool {
var del = tree.Search(key)
if del == nil {
return false
}
// y = the node to be deleted
// x (the child of the deleted node)
var x, y *RBNode
if del.Left == Neel || del.Right == Neel {
y = del
} else {
y = tree.Successor(del)
}
if y.Left != Neel {
x = y.Left
} else {
x = y.Right
}
x.Parent = y.Parent
if y.Parent == Neel {
tree.Root = x
} else if y == y.Parent.Left {
y.Parent.Left = x
} else {
y.Parent.Right = x
}
if y != del {
del.Key = y.Key
}
if y.Color == Black {
tree.DeleteFixup(x)
}
tree.size--
return true
}
func (tree *RBTree) DeleteFixup(current *RBNode) {
for current != tree.Root && current.Color == Black {
if current == current.Parent.Left {
sibling := current.Parent.Right
if sibling.Color == Red {
sibling.Color = Black
current.Parent.Color = Red
tree.RotateLeft(current.Parent)
sibling = current.Parent.Right
}
// if both are black nodes
if sibling.Left.Color == Black && sibling.Right.Color == Black {
sibling.Color = Red
current = current.Parent
} else {
// only one of the child is black
if sibling.Right.Color == Black {
sibling.Left.Color = Black
sibling.Color = Red
tree.RotateRight(sibling)
sibling = current.Parent.Right
}
sibling.Color = current.Parent.Color
current.Parent.Color = Black
sibling.Right.Color = Black
tree.RotateLeft(current.Parent)
current = tree.Root
}
} else { // if current is right child
sibling := current.Parent.Left
if sibling.Color == Red {
sibling.Color = Black
current.Parent.Color = Red
tree.RotateRight(current.Parent)
sibling = current.Parent.Left
}
if sibling.Left.Color == Black && sibling.Right.Color == Black {
sibling.Color = Red
current = current.Parent
} else { // if only one of child is Black
// the left child of sibling is black, and right child is red
if sibling.Left.Color == Black {
sibling.Right.Color = Black
sibling.Color = Red
tree.RotateLeft(sibling)
sibling = current.Parent.Left
}
sibling.Color = current.Parent.Color
current.Parent.Color = Black
sibling.Left.Color = Black
tree.RotateRight(current.Parent)
current = tree.Root
}
}
}
current.Color = Black
}
func (tree *RBTree) Upsert(key, val fixedpoint.Value) {
var y = Neel
var x = tree.Root
var node = &RBNode{
Key: key,
Value: val,
Color: Red,
}
for x != Neel {
y = x
if node.Key == x.Key {
// found node, skip insert and fix
x.Value = val
return
} else if node.Key < x.Key {
x = x.Left
} else {
x = x.Right
}
}
node.Parent = y
if y == Neel {
tree.Root = node
} else if node.Key < y.Key {
y.Left = node
} else {
y.Right = node
}
node.Left = Neel
node.Right = Neel
node.Color = Red
tree.InsertFixup(node)
}
func (tree *RBTree) Insert(key, val fixedpoint.Value) {
var y = Neel
var x = tree.Root
var node = &RBNode{
Key: key,
Value: val,
Color: Red,
}
for x != Neel {
y = x
if node.Key < x.Key {
x = x.Left
} else {
x = x.Right
}
}
node.Parent = y
if y == Neel {
tree.Root = node
} else if node.Key < y.Key {
y.Left = node
} else {
y.Right = node
}
node.Left = Neel
node.Right = Neel
node.Color = Red
tree.size++
tree.InsertFixup(node)
}
func (tree *RBTree) Search(key fixedpoint.Value) *RBNode {
var current = tree.Root
for current != Neel && key != current.Key {
if key < current.Key {
current = current.Left
} else {
current = current.Right
}
}
// convert Neel to real nil
if current == Neel {
return nil
}
return current
}
func (tree *RBTree) Size() int {
return tree.size
}
func (tree *RBTree) InsertFixup(current *RBNode) {
// A red node can't have a red parent, we need to fix it up
for current.Parent.Color == Red {
grandParent := current.Parent.Parent
if current.Parent == grandParent.Left {
uncle := grandParent.Right
if uncle.Color == Red {
current.Parent.Color = Black
uncle.Color = Black
grandParent.Color = Red
current = grandParent
} else { // if uncle is black
if current == current.Parent.Right {
current = current.Parent
tree.RotateLeft(current)
}
current.Parent.Color = Black
current.Parent.Parent.Color = Red
tree.RotateRight(current.Parent.Parent)
}
} else {
uncle := current.Parent.Parent.Left
if uncle.Color == Red {
current.Parent.Color = Black
uncle.Color = Black
current.Parent.Parent.Color = Red
} else {
if current == current.Parent.Left {
current = current.Parent
tree.RotateRight(current)
}
current.Parent.Color = Black
current.Parent.Parent.Color = Red
tree.RotateLeft(current.Parent.Parent)
}
}
}
// ensure that root is black
tree.Root.Color = Black
}
// RotateLeft
// x is the axes of rotation, y is the node that will be replace x's position.
// we need to:
// 1. move y's left child to the x's right child
// 2. change y's parent to x's parent
// 3. change x's parent to y
func (tree *RBTree) RotateLeft(x *RBNode) {
var y = x.Right
x.Right = y.Left
if y.Left != Neel {
y.Left.Parent = x
}
y.Parent = x.Parent
if x.Parent == Neel {
tree.Root = y
} else if x == x.Parent.Left {
x.Parent.Left = y
} else {
x.Parent.Right = y
}
y.Left = x
x.Parent = y
}
func (tree *RBTree) RotateRight(y *RBNode) {
x := y.Left
y.Left = x.Right
if x.Right != Neel {
x.Right.Parent = y
}
x.Parent = y.Parent
if y.Parent == Neel {
tree.Root = x
} else if y == y.Parent.Left {
y.Parent.Left = x
} else {
y.Parent.Right = x
}
x.Right = y
y.Parent = x
}
func (tree *RBTree) Rightmost(current *RBNode) *RBNode {
for current.Right != Neel {
current = current.Right
}
return current
}
func (tree *RBTree) Leftmost(current *RBNode) *RBNode {
for current.Left != Neel {
current = current.Left
}
return current
}
func (tree *RBTree) Successor(current *RBNode) *RBNode {
if current.Right != Neel {
return tree.Leftmost(current.Right)
}
var newNode = current.Parent
for newNode != Neel && current == newNode.Right {
current = newNode
newNode = newNode.Parent
}
return newNode
}
func (tree *RBTree) Preorder(cb func(n *RBNode)) {
tree.PreorderOf(tree.Root, cb)
}
func (tree *RBTree) PreorderOf(current *RBNode, cb func(n *RBNode)) {
if current != Neel {
cb(current)
tree.PreorderOf(current.Left, cb)
tree.PreorderOf(current.Right, cb)
}
}
// Inorder traverses the tree in ascending order
func (tree *RBTree) Inorder(cb func(n *RBNode) bool) {
tree.InorderOf(tree.Root, cb)
}
func (tree *RBTree) InorderOf(current *RBNode, cb func(n *RBNode) bool) {
if current != Neel {
tree.InorderOf(current.Left, cb)
if !cb(current) {
return
}
tree.InorderOf(current.Right, cb)
}
}
// InorderReverse traverses the tree in descending order
func (tree *RBTree) InorderReverse(cb func(n *RBNode) bool) {
tree.InorderReverseOf(tree.Root, cb)
}
func (tree *RBTree) InorderReverseOf(current *RBNode, cb func(n *RBNode) bool) {
if current != nil {
tree.InorderReverseOf(current.Right, cb)
if !cb(current) {
return
}
tree.InorderReverseOf(current.Left, cb)
}
}
func (tree *RBTree) Postorder(cb func(n *RBNode) bool) {
tree.PostorderOf(tree.Root, cb)
}
func (tree *RBTree) PostorderOf(current *RBNode, cb func(n *RBNode) bool) {
if current != nil {
tree.PostorderOf(current.Left, cb)
tree.PostorderOf(current.Right, cb)
if !cb(current) {
return
}
}
}
func copyNode(node *RBNode) *RBNode {
if node == Neel {
return Neel
}
newNode := *node
newNode.Left = copyNode(node.Left)
newNode.Right = copyNode(node.Right)
return &newNode
}
func (tree *RBTree) CopyInorderReverse(limit int) *RBTree {
cnt := 0
newTree := NewRBTree()
tree.InorderReverse(func(n *RBNode) bool {
if cnt >= limit {
return false
}
newTree.Insert(n.Key, n.Value)
cnt++
return true
})
return newTree
}
func (tree *RBTree) CopyInorder(limit int) *RBTree {
cnt := 0
newTree := NewRBTree()
tree.Inorder(func(n *RBNode) bool {
if cnt >= limit {
return false
}
newTree.Insert(n.Key, n.Value)
cnt++
return true
})
return newTree
}
func (tree *RBTree) Print() {
tree.Inorder(func(n *RBNode) bool {
fmt.Printf("%f -> %f\n", n.Key.Float64(), n.Value.Float64())
return true
})
}
func (tree *RBTree) Copy() *RBTree {
newTree := NewRBTree()
newTree.Root = copyNode(tree.Root)
return newTree
}

22
pkg/types/rbtree_node.go Normal file
View File

@ -0,0 +1,22 @@
package types
import "github.com/c9s/bbgo/pkg/fixedpoint"
// Color is the RB Tree color
type Color bool
const (
Red = Color(false)
Black = Color(true)
)
/*
RBNode
A red node always has black children.
A black node may have red or black children
*/
type RBNode struct {
Left, Right, Parent *RBNode
Color Color
Key, Value fixedpoint.Value
}

87
pkg/types/rbtree_test.go Normal file
View File

@ -0,0 +1,87 @@
package types
import (
"testing"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/stretchr/testify/assert"
)
func TestTree_CopyInorder(t *testing.T) {
tree := NewRBTree()
for i := 1.0; i < 10.0; i += 1.0 {
tree.Insert(fixedpoint.NewFromFloat(i*100.0), fixedpoint.NewFromFloat(i))
}
newTree := tree.CopyInorder(3)
assert.Equal(t, 3, newTree.Size())
newTree.Print()
node1 := newTree.Search(fixedpoint.NewFromFloat(100.0))
assert.NotNil(t, node1)
node2 := newTree.Search(fixedpoint.NewFromFloat(200.0))
assert.NotNil(t, node2)
node3 := newTree.Search(fixedpoint.NewFromFloat(300.0))
assert.NotNil(t, node3)
node4 := newTree.Search(fixedpoint.NewFromFloat(400.0))
assert.Nil(t, node4)
}
func TestTree_Copy(t *testing.T) {
tree := NewRBTree()
tree.Insert(fixedpoint.NewFromFloat(3000.0), fixedpoint.NewFromFloat(1.0))
assert.NotNil(t, tree.Root)
tree.Insert(fixedpoint.NewFromFloat(4000.0), fixedpoint.NewFromFloat(2.0))
tree.Insert(fixedpoint.NewFromFloat(2000.0), fixedpoint.NewFromFloat(3.0))
newTree := tree.Copy()
node1 := newTree.Search(fixedpoint.NewFromFloat(2000.0))
assert.NotNil(t, node1)
assert.Equal(t, fixedpoint.NewFromFloat(2000.0), node1.Key)
assert.Equal(t, fixedpoint.NewFromFloat(3.0), node1.Value)
node2 := newTree.Search(fixedpoint.NewFromFloat(3000.0))
assert.NotNil(t, node2)
assert.Equal(t, fixedpoint.NewFromFloat(3000.0), node2.Key)
assert.Equal(t, fixedpoint.NewFromFloat(1.0), node2.Value)
node3 := newTree.Search(fixedpoint.NewFromFloat(4000.0))
assert.NotNil(t, node3)
assert.Equal(t, fixedpoint.NewFromFloat(4000.0), node3.Key)
assert.Equal(t, fixedpoint.NewFromFloat(2.0), node3.Value)
}
func TestTree(t *testing.T) {
tree := NewRBTree()
tree.Insert(fixedpoint.NewFromFloat(3000.0), fixedpoint.NewFromFloat(10.0))
assert.NotNil(t, tree.Root)
tree.Insert(fixedpoint.NewFromFloat(4000.0), fixedpoint.NewFromFloat(10.0))
tree.Insert(fixedpoint.NewFromFloat(2000.0), fixedpoint.NewFromFloat(10.0))
// root is always black
assert.Equal(t, fixedpoint.NewFromFloat(3000.0), tree.Root.Key)
assert.Equal(t, Black, tree.Root.Color)
assert.Equal(t, fixedpoint.NewFromFloat(2000.0), tree.Root.Left.Key)
assert.Equal(t, Red, tree.Root.Left.Color)
assert.Equal(t, fixedpoint.NewFromFloat(4000.0), tree.Root.Right.Key)
assert.Equal(t, Red, tree.Root.Right.Color)
// should rotate
tree.Insert(fixedpoint.NewFromFloat(1500.0), fixedpoint.NewFromFloat(10.0))
tree.Insert(fixedpoint.NewFromFloat(1000.0), fixedpoint.NewFromFloat(10.0))
deleted := tree.Delete(fixedpoint.NewFromFloat(1000.0))
assert.True(t, deleted)
deleted = tree.Delete(fixedpoint.NewFromFloat(1500.0))
assert.True(t, deleted)
}

View File

@ -3,7 +3,6 @@ package types
import (
"time"
"github.com/c9s/bbgo/pkg/datatype"
"github.com/c9s/bbgo/pkg/fixedpoint"
)
@ -30,7 +29,7 @@ type Reward struct {
Spent bool `json:"spent" db:"spent"`
// Unix timestamp in seconds
CreatedAt datatype.Time `json:"created_at" db:"created_at"`
CreatedAt Time `json:"created_at" db:"created_at"`
}
type RewardSlice []Reward

View File

@ -74,14 +74,14 @@ func (side SideType) String() string {
func (side SideType) Color() string {
if side == SideTypeBuy {
return Green
return GreenColor
}
if side == SideTypeSell {
return Red
return RedColor
}
return "#f0f0f0"
return GrayColor
}
func SideToColorName(side SideType) string {

196
pkg/types/sliceorderbook.go Normal file
View File

@ -0,0 +1,196 @@
package types
import (
"fmt"
"strings"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/pkg/errors"
)
// SliceOrderBook is a general order book structure which could be used
// for RESTful responses and websocket stream parsing
//go:generate callbackgen -type SliceOrderBook
type SliceOrderBook struct {
Symbol string
Bids PriceVolumeSlice
Asks PriceVolumeSlice
loadCallbacks []func(book *SliceOrderBook)
updateCallbacks []func(book *SliceOrderBook)
}
func NewSliceOrderBook(symbol string) *SliceOrderBook {
return &SliceOrderBook{
Symbol: symbol,
}
}
func (b *SliceOrderBook) Spread() (fixedpoint.Value, bool) {
bestBid, ok := b.BestBid()
if !ok {
return 0, false
}
bestAsk, ok := b.BestAsk()
if !ok {
return 0, false
}
return bestAsk.Price - bestBid.Price, true
}
func (b *SliceOrderBook) BestBid() (PriceVolume, bool) {
if len(b.Bids) == 0 {
return PriceVolume{}, false
}
return b.Bids[0], true
}
func (b *SliceOrderBook) BestAsk() (PriceVolume, bool) {
if len(b.Asks) == 0 {
return PriceVolume{}, false
}
return b.Asks[0], true
}
func (b *SliceOrderBook) SideBook(sideType SideType) PriceVolumeSlice {
switch sideType {
case SideTypeBuy:
return b.Bids
case SideTypeSell:
return b.Asks
default:
return nil
}
}
func (b *SliceOrderBook) IsValid() (bool, error) {
bid, hasBid := b.BestBid()
ask, hasAsk := b.BestAsk()
if !hasBid {
return false, errors.New("empty bids")
}
if !hasAsk {
return false, errors.New("empty asks")
}
if bid.Price > ask.Price {
return false, fmt.Errorf("bid price %f > ask price %f", bid.Price.Float64(), ask.Price.Float64())
}
return true, nil
}
func (b *SliceOrderBook) PriceVolumesBySide(side SideType) PriceVolumeSlice {
switch side {
case SideTypeBuy:
return b.Bids
case SideTypeSell:
return b.Asks
}
return nil
}
func (b *SliceOrderBook) updateAsks(pvs PriceVolumeSlice) {
for _, pv := range pvs {
if pv.Volume == 0 {
b.Asks = b.Asks.Remove(pv.Price, false)
} else {
b.Asks = b.Asks.Upsert(pv, false)
}
}
}
func (b *SliceOrderBook) updateBids(pvs PriceVolumeSlice) {
for _, pv := range pvs {
if pv.Volume == 0 {
b.Bids = b.Bids.Remove(pv.Price, true)
} else {
b.Bids = b.Bids.Upsert(pv, true)
}
}
}
func (b *SliceOrderBook) load(book SliceOrderBook) {
b.Reset()
b.update(book)
}
func (b *SliceOrderBook) update(book SliceOrderBook) {
b.updateBids(book.Bids)
b.updateAsks(book.Asks)
}
func (b *SliceOrderBook) Reset() {
b.Bids = nil
b.Asks = nil
}
func (b *SliceOrderBook) Load(book SliceOrderBook) {
b.load(book)
b.EmitLoad(b)
}
func (b *SliceOrderBook) Update(book SliceOrderBook) {
b.update(book)
b.EmitUpdate(b)
}
func (b *SliceOrderBook) Print() {
fmt.Printf(b.String())
}
func (b *SliceOrderBook) String() string {
sb := strings.Builder{}
sb.WriteString("BOOK ")
sb.WriteString(b.Symbol)
sb.WriteString("\n")
if len(b.Asks) > 0 {
sb.WriteString("ASKS:\n")
for i := len(b.Asks) - 1; i >= 0; i-- {
sb.WriteString("- ASK: ")
sb.WriteString(b.Asks[i].String())
sb.WriteString("\n")
}
}
if len(b.Bids) > 0 {
sb.WriteString("BIDS:\n")
for _, bid := range b.Bids {
sb.WriteString("- BID: ")
sb.WriteString(bid.String())
sb.WriteString("\n")
}
}
return sb.String()
}
func (b *SliceOrderBook) CopyDepth(depth int) OrderBook {
var book SliceOrderBook
book = *b
book.Bids = book.Bids.CopyDepth(depth)
book.Asks = book.Asks.CopyDepth(depth)
return &book
}
func (b *SliceOrderBook) Copy() OrderBook {
var book SliceOrderBook
book = *b
book.Bids = book.Bids.Copy()
book.Asks = book.Asks.Copy()
return &book
}

View File

@ -0,0 +1,25 @@
// Code generated by "callbackgen -type SliceOrderBook"; DO NOT EDIT.
package types
import ()
func (b *SliceOrderBook) OnLoad(cb func(book *SliceOrderBook)) {
b.loadCallbacks = append(b.loadCallbacks, cb)
}
func (b *SliceOrderBook) EmitLoad(book *SliceOrderBook) {
for _, cb := range b.loadCallbacks {
cb(book)
}
}
func (b *SliceOrderBook) OnUpdate(cb func(book *SliceOrderBook)) {
b.updateCallbacks = append(b.updateCallbacks, cb)
}
func (b *SliceOrderBook) EmitUpdate(book *SliceOrderBook) {
for _, cb := range b.updateCallbacks {
cb(book)
}
}

View File

@ -94,21 +94,21 @@ func (stream *StandardStream) EmitKLine(kline KLine) {
}
}
func (stream *StandardStream) OnBookUpdate(cb func(book OrderBook)) {
func (stream *StandardStream) OnBookUpdate(cb func(book SliceOrderBook)) {
stream.bookUpdateCallbacks = append(stream.bookUpdateCallbacks, cb)
}
func (stream *StandardStream) EmitBookUpdate(book OrderBook) {
func (stream *StandardStream) EmitBookUpdate(book SliceOrderBook) {
for _, cb := range stream.bookUpdateCallbacks {
cb(book)
}
}
func (stream *StandardStream) OnBookSnapshot(cb func(book OrderBook)) {
func (stream *StandardStream) OnBookSnapshot(cb func(book SliceOrderBook)) {
stream.bookSnapshotCallbacks = append(stream.bookSnapshotCallbacks, cb)
}
func (stream *StandardStream) EmitBookSnapshot(book OrderBook) {
func (stream *StandardStream) EmitBookSnapshot(book SliceOrderBook) {
for _, cb := range stream.bookSnapshotCallbacks {
cb(book)
}
@ -133,7 +133,7 @@ type StandardStreamEventHub interface {
OnKLine(cb func(kline KLine))
OnBookUpdate(cb func(book OrderBook))
OnBookUpdate(cb func(book SliceOrderBook))
OnBookSnapshot(cb func(book OrderBook))
OnBookSnapshot(cb func(book SliceOrderBook))
}

View File

@ -44,9 +44,9 @@ type StandardStream struct {
kLineCallbacks []func(kline KLine)
bookUpdateCallbacks []func(book OrderBook)
bookUpdateCallbacks []func(book SliceOrderBook)
bookSnapshotCallbacks []func(book OrderBook)
bookSnapshotCallbacks []func(book SliceOrderBook)
}
func (stream *StandardStream) Subscribe(channel Channel, symbol string, options SubscribeOptions) {

View File

@ -1,4 +1,4 @@
package datatype
package types
import (
"database/sql/driver"

View File

@ -9,7 +9,6 @@ import (
"github.com/slack-go/slack"
"github.com/c9s/bbgo/pkg/datatype"
"github.com/c9s/bbgo/pkg/util"
)
@ -62,7 +61,7 @@ type Trade struct {
Side SideType `json:"side" db:"side"`
IsBuyer bool `json:"isBuyer" db:"is_buyer"`
IsMaker bool `json:"isMaker" db:"is_maker"`
Time datatype.Time `json:"tradedAt" db:"traded_at"`
Time Time `json:"tradedAt" db:"traded_at"`
Fee float64 `json:"fee" db:"fee"`
FeeCurrency string `json:"feeCurrency" db:"fee_currency"`

View File

@ -3,8 +3,6 @@ package types
import (
"fmt"
"time"
"github.com/c9s/bbgo/pkg/datatype"
)
type Withdraw struct {
@ -20,7 +18,7 @@ type Withdraw struct {
TransactionFee float64 `json:"transactionFee" db:"txn_fee"`
TransactionFeeCurrency string `json:"transactionFeeCurrency" db:"txn_fee_currency"`
WithdrawOrderID string `json:"withdrawOrderId"`
ApplyTime datatype.Time `json:"applyTime" db:"time"`
ApplyTime Time `json:"applyTime" db:"time"`
Network string `json:"network" db:"network"`
}