mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-26 08:45:16 +00:00
Merge pull request #252 from c9s/feature/rbt
feature: improve orderbook interface
This commit is contained in:
commit
499816040a
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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())
|
||||
})
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
package types
|
||||
|
||||
const Green = "#228B22"
|
||||
const Red = "#800000"
|
||||
const GreenColor = "#228B22"
|
||||
const RedColor = "#800000"
|
||||
const GrayColor = "#f0f0f0"
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)},
|
||||
|
|
118
pkg/types/price_volume_slice.go
Normal file
118
pkg/types/price_volume_slice.go
Normal 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
184
pkg/types/rbtorderbook.go
Normal 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
|
||||
})
|
||||
}
|
25
pkg/types/rbtorderbook_callbacks.go
Normal file
25
pkg/types/rbtorderbook_callbacks.go
Normal 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
470
pkg/types/rbtree.go
Normal 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
22
pkg/types/rbtree_node.go
Normal 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
87
pkg/types/rbtree_test.go
Normal 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)
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
196
pkg/types/sliceorderbook.go
Normal 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
|
||||
}
|
25
pkg/types/sliceorderbook_callbacks.go
Normal file
25
pkg/types/sliceorderbook_callbacks.go
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package datatype
|
||||
package types
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
|
@ -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"`
|
||||
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user