Merge branch 'main' into ftx/websocket-kline

This commit is contained in:
Jui-Nan Lin 2021-05-24 10:01:58 +08:00
commit 64387ed2cb
66 changed files with 1806 additions and 898 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

@ -111,6 +111,21 @@ func (b *LocalActiveOrderBook) NumOfAsks() int {
return b.Asks.Len()
}
func (b *LocalActiveOrderBook) Exists(order types.Order) bool {
switch order.Side {
case types.SideTypeBuy:
return b.Bids.Exists(order.OrderID)
case types.SideTypeSell:
return b.Asks.Exists(order.OrderID)
}
return false
}
func (b *LocalActiveOrderBook) Remove(order types.Order) bool {
switch order.Side {
case types.SideTypeBuy:

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

@ -25,6 +25,10 @@ type Position struct {
Quote fixedpoint.Value `json:"quote"`
AverageCost fixedpoint.Value `json:"averageCost"`
// ApproximateAverageCost adds the computed fee in quote in the average cost
// This is used for calculating net profit
ApproximateAverageCost fixedpoint.Value `json:"approximateAverageCost"`
ExchangeFeeRates map[types.ExchangeName]ExchangeFee `json:"exchangeFeeRates"`
sync.Mutex
@ -107,25 +111,26 @@ func (p *Position) BindStream(stream types.Stream) {
})
}
func (p *Position) AddTrades(trades []types.Trade) (fixedpoint.Value, bool) {
var totalProfitAmount fixedpoint.Value
func (p *Position) AddTrades(trades []types.Trade) (fixedpoint.Value, fixedpoint.Value, bool) {
var totalProfitAmount, totalNetProfit fixedpoint.Value
for _, trade := range trades {
if profitAmount, profit := p.AddTrade(trade); profit {
totalProfitAmount += profitAmount
if profit, netProfit, madeProfit := p.AddTrade(trade); madeProfit {
totalProfitAmount += profit
totalNetProfit += netProfit
}
}
return totalProfitAmount, totalProfitAmount != 0
return totalProfitAmount, totalNetProfit, totalProfitAmount != 0
}
func (p *Position) AddTrade(t types.Trade) (fixedpoint.Value, bool) {
func (p *Position) AddTrade(t types.Trade) (profit fixedpoint.Value, netProfit fixedpoint.Value, madeProfit bool) {
price := fixedpoint.NewFromFloat(t.Price)
quantity := fixedpoint.NewFromFloat(t.Quantity)
quoteQuantity := fixedpoint.NewFromFloat(t.QuoteQuantity)
fee := fixedpoint.NewFromFloat(t.Fee)
// calculated fee in quote (some exchange accounts may enable platform currency fee discount, like BNB)
var quoteFee fixedpoint.Value = 0
var feeInQuote fixedpoint.Value = 0
switch t.FeeCurrency {
@ -139,9 +144,9 @@ func (p *Position) AddTrade(t types.Trade) (fixedpoint.Value, bool) {
if p.ExchangeFeeRates != nil {
if exchangeFee, ok := p.ExchangeFeeRates[t.Exchange]; ok {
if t.IsMaker {
quoteFee += exchangeFee.MakerFeeRate.Mul(quoteQuantity)
feeInQuote += exchangeFee.MakerFeeRate.Mul(quoteQuantity)
} else {
quoteFee += exchangeFee.TakerFeeRate.Mul(quoteQuantity)
feeInQuote += exchangeFee.TakerFeeRate.Mul(quoteQuantity)
}
}
}
@ -156,50 +161,60 @@ func (p *Position) AddTrade(t types.Trade) (fixedpoint.Value, bool) {
case types.SideTypeBuy:
if p.Base < 0 {
// handling short-to-long position
// convert short position to long position
if p.Base+quantity > 0 {
closingProfit := (p.AverageCost - price).Mul(-p.Base) - quoteFee
profit = (p.AverageCost - price).Mul(-p.Base)
netProfit = (p.ApproximateAverageCost - price).Mul(-p.Base) - feeInQuote
p.Base += quantity
p.Quote -= quoteQuantity
p.AverageCost = price
return closingProfit, true
p.ApproximateAverageCost = price
return profit, netProfit, true
} else {
// covering short position
p.Base += quantity
p.Quote -= quoteQuantity
return (p.AverageCost - price).Mul(quantity) - quoteFee, true
profit = (p.AverageCost - price).Mul(quantity)
netProfit = (p.ApproximateAverageCost - price).Mul(quantity) - feeInQuote
return profit, netProfit, true
}
}
p.AverageCost = (p.AverageCost.Mul(p.Base) + quoteQuantity + quoteFee).Div(p.Base + quantity)
p.ApproximateAverageCost = (p.ApproximateAverageCost.Mul(p.Base) + quoteQuantity + feeInQuote).Div(p.Base + quantity)
p.AverageCost = (p.AverageCost.Mul(p.Base) + quoteQuantity).Div(p.Base + quantity)
p.Base += quantity
p.Quote -= quoteQuantity
return 0, false
return 0, 0, false
case types.SideTypeSell:
if p.Base > 0 {
// long-to-short
// convert long position to short position
if p.Base-quantity < 0 {
closingProfit := (price - p.AverageCost).Mul(p.Base) - quoteFee
profit = (price - p.AverageCost).Mul(p.Base)
netProfit = (price - p.ApproximateAverageCost).Mul(p.Base) - feeInQuote
p.Base -= quantity
p.Quote += quoteQuantity
p.AverageCost = price
return closingProfit, true
p.ApproximateAverageCost = price
return profit, netProfit, true
} else {
p.Base -= quantity
p.Quote += quoteQuantity
return (price - p.AverageCost).Mul(quantity) - quoteFee, true
profit = (price - p.AverageCost).Mul(quantity)
netProfit = (price - p.ApproximateAverageCost).Mul(quantity) - feeInQuote
return profit, netProfit, true
}
}
// handling short position, since Base here is negative we need to reverse the sign
p.AverageCost = (p.AverageCost.Mul(-p.Base) + quoteQuantity - quoteFee).Div(-p.Base + quantity)
p.ApproximateAverageCost = (p.ApproximateAverageCost.Mul(-p.Base) + quoteQuantity - feeInQuote).Div(-p.Base + quantity)
p.AverageCost = (p.AverageCost.Mul(-p.Base) + quoteQuantity).Div(-p.Base + quantity)
p.Base -= quantity
p.Quote += quoteQuantity
return 0, false
return 0, 0, false
}
return 0, false
return 0, 0, false
}

View File

@ -38,7 +38,7 @@ func TestPosition_ExchangeFeeRate_Short(t *testing.T) {
FeeCurrency: "BNB",
})
profit, madeProfit := pos.AddTrade(types.Trade{
_, netProfit, madeProfit := pos.AddTrade(types.Trade{
Exchange: types.ExchangeBinance,
Price: 2000.0,
Quantity: 10.0,
@ -51,7 +51,7 @@ func TestPosition_ExchangeFeeRate_Short(t *testing.T) {
expectedProfit := (averageCost-2000.0)*10.0 - (2000.0 * 10.0 * feeRate)
assert.True(t, madeProfit)
assert.Equal(t, fixedpoint.NewFromFloat(expectedProfit), profit)
assert.Equal(t, fixedpoint.NewFromFloat(expectedProfit), netProfit)
}
func TestPosition_ExchangeFeeRate_Long(t *testing.T) {
@ -83,7 +83,7 @@ func TestPosition_ExchangeFeeRate_Long(t *testing.T) {
FeeCurrency: "BNB",
})
profit, madeProfit := pos.AddTrade(types.Trade{
_, netProfit, madeProfit := pos.AddTrade(types.Trade{
Exchange: types.ExchangeBinance,
Price: 4000.0,
Quantity: 10.0,
@ -96,7 +96,7 @@ func TestPosition_ExchangeFeeRate_Long(t *testing.T) {
expectedProfit := (4000.0-averageCost)*10.0 - (4000.0 * 10.0 * feeRate)
assert.True(t, madeProfit)
assert.Equal(t, fixedpoint.NewFromFloat(expectedProfit), profit)
assert.Equal(t, fixedpoint.NewFromFloat(expectedProfit), netProfit)
}
func TestPosition(t *testing.T) {
@ -253,7 +253,7 @@ func TestPosition(t *testing.T) {
BaseCurrency: "BTC",
QuoteCurrency: "USDT",
}
profitAmount, profit := pos.AddTrades(testcase.trades)
profitAmount, _, profit := pos.AddTrades(testcase.trades)
assert.Equal(t, testcase.expectedQuote, pos.Quote, "expectedQuote")
assert.Equal(t, testcase.expectedBase, pos.Base, "expectedBase")

View File

@ -63,30 +63,9 @@ 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)
}
return pvs, err
}
func (e *TwapExecution) newBestPriceOrder() (orderForm types.SubmitOrder, err error) {
book := e.orderBook.Get()
sideBook, err := e.getSideBook()
if err != nil {
return orderForm, err
}
book := e.orderBook.Copy()
sideBook := book.SideBook(e.Side)
first, ok := sideBook.First()
if !ok {
@ -223,11 +202,8 @@ func (e *TwapExecution) newBestPriceOrder() (orderForm types.SubmitOrder, err er
}
func (e *TwapExecution) updateOrder(ctx context.Context) error {
sideBook, err := e.getSideBook()
if err != nil {
return err
}
book := e.orderBook.Copy()
sideBook := book.SideBook(e.Side)
first, ok := sideBook.First()
if !ok {
@ -319,7 +295,34 @@ func (e *TwapExecution) cancelActiveOrders(ctx context.Context) {
if err := e.Session.Exchange.CancelOrders(ctx, orders...); err != nil {
log.WithError(err).Errorf("can not cancel %s orders", e.Symbol)
}
time.Sleep(3 * time.Second)
select {
case <-ctx.Done():
return
case <-time.After(3 * time.Second):
}
// verify the current open orders via the RESTful API
if e.activeMakerOrders.NumOfOrders() > 0 {
log.Warnf("there are orders not cancelled, using REStful API to verify...")
openOrders, err := e.Session.Exchange.QueryOpenOrders(ctx, e.Symbol)
if err != nil {
log.WithError(err).Errorf("can not query %s open orders", e.Symbol)
continue
}
openOrderStore := NewOrderStore(e.Symbol)
openOrderStore.Add(openOrders...)
for _, o := range e.activeMakerOrders.Orders() {
// if it does not exist, we should remove it
if !openOrderStore.Exists(o.OrderID) {
e.activeMakerOrders.Remove(o)
}
}
}
}
if didCancel {

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

@ -73,7 +73,8 @@ func runSetup(baseCtx context.Context, userConfig *bbgo.Config, enableApiServer
cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM)
cancelTrading()
shutdownCtx, cancelShutdown := context.WithDeadline(ctx, time.Now().Add(30*time.Second))
// graceful period = 15 second
shutdownCtx, cancelShutdown := context.WithDeadline(ctx, time.Now().Add(15*time.Second))
log.Infof("shutting down...")
trader.Graceful.Shutdown(shutdownCtx)

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

@ -352,16 +352,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

@ -15,7 +15,7 @@ Accumulation/Distribution Indicator (A/D)
//go:generate callbackgen -type AD
type AD struct {
types.IntervalWindow
Values Float64Slice
Values types.Float64Slice
PrePrice float64
EndTime time.Time

View File

@ -29,10 +29,10 @@ type BOLL struct {
// times of Std, generally it's 2
K float64
SMA Float64Slice
StdDev Float64Slice
UpBand Float64Slice
DownBand Float64Slice
SMA types.Float64Slice
StdDev types.Float64Slice
UpBand types.Float64Slice
DownBand types.Float64Slice
EndTime time.Time

View File

@ -12,7 +12,7 @@ import (
//go:generate callbackgen -type EWMA
type EWMA struct {
types.IntervalWindow
Values Float64Slice
Values types.Float64Slice
LastOpenTime time.Time
UpdateCallbacks []func(value float64)

View File

@ -18,11 +18,11 @@ type MACD struct {
types.IntervalWindow // 9
ShortPeriod int // 12
LongPeriod int // 26
Values Float64Slice
Values types.Float64Slice
FastEWMA EWMA
SlowEWMA EWMA
SignalLine EWMA
Histogram Float64Slice
Histogram types.Float64Slice
EndTime time.Time

View File

@ -15,7 +15,7 @@ On-Balance Volume (OBV) Definition
//go:generate callbackgen -type OBV
type OBV struct {
types.IntervalWindow
Values Float64Slice
Values types.Float64Slice
PrePrice float64
EndTime time.Time

View File

@ -19,19 +19,19 @@ func Test_calculateOBV(t *testing.T) {
name string
kLines []types.KLine
window int
want Float64Slice
want types.Float64Slice
}{
{
name: "trivial_case",
kLines: buildKLines([]float64{0}, []float64{1}),
window: 0,
want: Float64Slice{1.0},
want: types.Float64Slice{1.0},
},
{
name: "easy_case",
kLines: buildKLines([]float64{3, 2, 1, 4}, []float64{3, 2, 2, 6}),
window: 0,
want: Float64Slice{3, 1, -1, 5},
want: types.Float64Slice{3, 1, -1, 5},
},
}

View File

@ -9,18 +9,12 @@ import (
"github.com/c9s/bbgo/pkg/types"
)
type Float64Slice []float64
func (s *Float64Slice) Push(v float64) {
*s = append(*s, v)
}
var zeroTime time.Time
//go:generate callbackgen -type SMA
type SMA struct {
types.IntervalWindow
Values Float64Slice
Values types.Float64Slice
EndTime time.Time
UpdateCallbacks []func(value float64)

83
pkg/indicator/stoch.go Normal file
View File

@ -0,0 +1,83 @@
package indicator
import (
"time"
"github.com/c9s/bbgo/pkg/types"
)
const DPeriod int = 3
/*
stoch implements stochastic oscillator indicator
Stochastic Oscillator
- https://www.investopedia.com/terms/s/stochasticoscillator.asp
*/
//go:generate callbackgen -type STOCH
type STOCH struct {
types.IntervalWindow
K types.Float64Slice
D types.Float64Slice
KLineWindow types.KLineWindow
EndTime time.Time
UpdateCallbacks []func(k float64, d float64)
}
func (inc *STOCH) update(kLine types.KLine) {
inc.KLineWindow.Add(kLine)
inc.KLineWindow.Truncate(inc.Window)
lowest := inc.KLineWindow.GetLow()
highest := inc.KLineWindow.GetHigh()
k := 100.0 * (kLine.Close - lowest) / (highest - lowest)
inc.K.Push(k)
d := inc.K.Tail(DPeriod).Mean()
inc.D.Push(d)
}
func (inc *STOCH) LastK() float64 {
if len(inc.K) == 0 {
return 0.0
}
return inc.K[len(inc.K)-1]
}
func (inc *STOCH) LastD() float64 {
if len(inc.K) == 0 {
return 0.0
}
return inc.D[len(inc.D)-1]
}
func (inc *STOCH) calculateAndUpdate(kLines []types.KLine) {
if len(kLines) < inc.Window || len(kLines) < DPeriod {
return
}
for i, k := range kLines {
if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) {
continue
}
inc.update(k)
inc.EmitUpdate(inc.LastK(), inc.LastD())
inc.EndTime = kLines[i].EndTime
}
}
func (inc *STOCH) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) {
if inc.Interval != interval {
return
}
inc.calculateAndUpdate(window)
}
func (inc *STOCH) Bind(updater KLineWindowUpdater) {
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
}

View File

@ -0,0 +1,15 @@
// Code generated by "callbackgen -type STOCH"; DO NOT EDIT.
package indicator
import ()
func (inc *STOCH) OnUpdate(cb func(k float64, d float64)) {
inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb)
}
func (inc *STOCH) EmitUpdate(k float64, d float64) {
for _, cb := range inc.UpdateCallbacks {
cb(k, d)
}
}

View File

@ -0,0 +1,67 @@
package indicator
import (
"math"
"testing"
"time"
"github.com/c9s/bbgo/pkg/types"
)
/*
python
import pandas as pd
import pandas_ta as ta
klines = ...
df = pd.DataFrame(klines, columns=['open', 'high', 'low', 'close', 'volume'])
print(df.ta.stoch(df['high'], df['low'], df['close'], k=14, d=3, smooth_k=1))
*/
func TestSTOCH_update(t *testing.T) {
open := []float64{8273.0, 8280.0, 8280.0, 8275.0, 8281.0, 8277.0, 8279.0, 8280.0, 8284.0, 8286.0, 8283.0, 8283.0, 8284.0, 8286.0, 8285.0, 8287.0, 8289.0, 8282.0, 8286.0, 8279.0, 8275.0, 8276.0, 8276.0, 8281.0, 8269.0, 8256.0, 8258.0, 8252.0, 8241.0, 8232.0, 8218.0, 8221.0, 8216.0, 8210.0, 8212.0, 8201.0, 8197.0, 8200.0, 8193.0, 8181.0, 8185.0, 8190.0, 8184.0, 8185.0, 8163.0, 8153.0, 8162.0, 8165.0, 8162.0, 8157.0, 8159.0, 8141.0, 8140.0, 8141.0, 8130.0, 8144.0, 8141.0, 8148.0, 8145.0, 8134.0, 8123.0, 8127.0, 8130.0, 8125.0, 8122.0, 8105.0, 8096.0, 8103.0, 8102.0, 8110.0, 8104.0, 8109.0, 8103.0, 8111.0, 8112.0, 8109.0, 8092.0, 8100.0, 8101.0, 8100.0, 8096.0, 8095.0, 8094.0, 8101.0, 8095.0, 8069.0, 8067.0, 8070.0, 8069.0, 8066.0, 8047.0, 8046.0, 8042.0, 8039.0, 8049.0, 8055.0, 8063.0, 8061.0, 8056.0, 8057.0, 8056.0, 8057.0, 8057.0, 8054.0, 8056.0, 8056.0, 8065.0, 8065.0, 8070.0, 8065.0, 8064.0, 8063.0, 8060.0, 8065.0, 8068.0, 8068.0, 8069.0, 8073.0, 8073.0, 8084.0, 8084.0, 8076.0, 8074.0, 8074.0, 8074.0, 8078.0, 8080.0, 8082.0, 8085.0, 8083.0, 8087.0, 8087.0, 8083.0, 8083.0, 8082.0, 8074.0, 8074.0, 8071.0, 8071.0, 8072.0, 8075.0, 8075.0, 8076.0, 8073.0, 8071.0, 8070.0, 8075.0, 8078.0, 8077.0, 8075.0, 8073.0, 8079.0, 8084.0, 8082.0, 8085.0, 8085.0, 8085.0, 8101.0, 8106.0, 8113.0, 8109.0, 8104.0, 8105.0, 8105.0, 8107.0, 8106.0, 8104.0, 8106.0, 8106.0, 8110.0, 8107.0, 8110.0, 8111.0, 8104.0, 8098.0, 8098.0, 8098.0, 8098.0, 8094.0, 8097.0, 8096.0, 8099.0, 8098.0, 8099.0, 8098.0, 8095.0, 8096.0, 8086.0, 8088.0, 8093.0, 8092.0, 8096.0, 8100.0, 8104.0, 8104.0, 8108.0, 8107.0, 8103.0, 8104.0, 8110.0, 8105.0, 8102.0, 8104.0, 8096.0, 8099.0, 8103.0, 8102.0, 8108.0, 8107.0, 8107.0, 8104.0, 8095.0, 8091.0, 8092.0, 8090.0, 8093.0, 8093.0, 8094.0, 8095.0, 8096.0, 8088.0, 8090.0, 8079.0, 8077.0, 8079.0, 8081.0, 8083.0, 8084.0, 8084.0, 8087.0, 8091.0, 8089.0, 8089.0, 8091.0, 8087.0, 8093.0, 8090.0, 8090.0, 8095.0, 8093.0, 8088.0, 8087.0, 8090.0, 8089.0, 8087.0, 8084.0, 8087.0, 8084.0, 8080.0, 8078.0, 8077.0, 8077.0, 8076.0, 8072.0, 8072.0, 8075.0, 8076.0, 8074.0, 8077.0, 8081.0, 8080.0, 8076.0, 8075.0, 8077.0, 8080.0, 8077.0, 8076.0, 8076.0, 8070.0, 8071.0, 8070.0, 8073.0, 8069.0, 8069.0, 8068.0, 8072.0, 8078.0, 8077.0, 8079.0, 8081.0, 8076.0, 8076.0, 8077.0, 8077.0, 8078.0, 8075.0, 8066.0, 8064.0, 8064.0, 8062.0, 8062.0, 8065.0, 8062.0, 8063.0, 8074.0, 8070.0, 8069.0, 8068.0, 8074.0, 8075.0}
high := []float64{8279.0, 8282.0, 8280.0, 8280.0, 8284.0, 8284.0, 8280.0, 8282.0, 8284.0, 8289.0, 8288.0, 8285.0, 8284.0, 8287.0, 8286.0, 8294.0, 8290.0, 8292.0, 8289.0, 8288.0, 8278.0, 8279.0, 8279.0, 8284.0, 8282.0, 8270.0, 8261.0, 8260.0, 8252.0, 8244.0, 8233.0, 8227.0, 8222.0, 8217.0, 8217.0, 8211.0, 8202.0, 8203.0, 8203.0, 8196.0, 8186.0, 8193.0, 8194.0, 8187.0, 8185.0, 8168.0, 8165.0, 8169.0, 8166.0, 8163.0, 8162.0, 8159.0, 8143.0, 8148.0, 8143.0, 8146.0, 8152.0, 8149.0, 8152.0, 8147.0, 8138.0, 8128.0, 8134.0, 8131.0, 8133.0, 8123.0, 8106.0, 8105.0, 8104.0, 8113.0, 8112.0, 8112.0, 8111.0, 8114.0, 8115.0, 8114.0, 8110.0, 8101.0, 8107.0, 8103.0, 8100.0, 8101.0, 8100.0, 8102.0, 8101.0, 8100.0, 8070.0, 8076.0, 8072.0, 8072.0, 8069.0, 8050.0, 8048.0, 8044.0, 8049.0, 8055.0, 8063.0, 8070.0, 8067.0, 8061.0, 8059.0, 8060.0, 8063.0, 8058.0, 8061.0, 8061.0, 8068.0, 8066.0, 8071.0, 8073.0, 8068.0, 8066.0, 8066.0, 8065.0, 8070.0, 8072.0, 8072.0, 8075.0, 8078.0, 8084.0, 8085.0, 8084.0, 8077.0, 8076.0, 8075.0, 8079.0, 8081.0, 8083.0, 8088.0, 8086.0, 8088.0, 8088.0, 8092.0, 8086.0, 8086.0, 8083.0, 8075.0, 8074.0, 8073.0, 8073.0, 8077.0, 8077.0, 8078.0, 8077.0, 8076.0, 8073.0, 8075.0, 8079.0, 8079.0, 8078.0, 8074.0, 8080.0, 8086.0, 8086.0, 8085.0, 8085.0, 8087.0, 8102.0, 8109.0, 8113.0, 8114.0, 8110.0, 8105.0, 8106.0, 8109.0, 8114.0, 8107.0, 8106.0, 8106.0, 8110.0, 8111.0, 8110.0, 8112.0, 8112.0, 8109.0, 8102.0, 8098.0, 8099.0, 8098.0, 8097.0, 8099.0, 8099.0, 8099.0, 8102.0, 8099.0, 8099.0, 8096.0, 8097.0, 8091.0, 8094.0, 8094.0, 8096.0, 8102.0, 8106.0, 8109.0, 8109.0, 8110.0, 8108.0, 8106.0, 8110.0, 8122.0, 8105.0, 8105.0, 8104.0, 8103.0, 8104.0, 8103.0, 8110.0, 8110.0, 8107.0, 8109.0, 8105.0, 8097.0, 8095.0, 8093.0, 8094.0, 8097.0, 8096.0, 8096.0, 8096.0, 8097.0, 8092.0, 8090.0, 8081.0, 8081.0, 8083.0, 8087.0, 8085.0, 8085.0, 8087.0, 8092.0, 8094.0, 8090.0, 8093.0, 8092.0, 8094.0, 8093.0, 8091.0, 8095.0, 8095.0, 8092.0, 8089.0, 8090.0, 8090.0, 8091.0, 8088.0, 8089.0, 8089.0, 8085.0, 8081.0, 8080.0, 8078.0, 8078.0, 8076.0, 8073.0, 8077.0, 8078.0, 8077.0, 8077.0, 8083.0, 8082.0, 8082.0, 8077.0, 8079.0, 8082.0, 8080.0, 8077.0, 8078.0, 8076.0, 8073.0, 8074.0, 8073.0, 8073.0, 8070.0, 8070.0, 8072.0, 8079.0, 8078.0, 8079.0, 8081.0, 8083.0, 8077.0, 8078.0, 8080.0, 8079.0, 8080.0, 8077.0, 8069.0, 8071.0, 8066.0, 8064.0, 8066.0, 8066.0, 8063.0, 8074.0, 8075.0, 8071.0, 8070.0, 8075.0, 8075.0}
low := []float64{8260.0, 8272.0, 8275.0, 8274.0, 8275.0, 8277.0, 8276.0, 8278.0, 8277.0, 8283.0, 8282.0, 8283.0, 8283.0, 8283.0, 8283.0, 8279.0, 8281.0, 8282.0, 8277.0, 8276.0, 8273.0, 8275.0, 8274.0, 8275.0, 8266.0, 8256.0, 8255.0, 8250.0, 8239.0, 8230.0, 8214.0, 8218.0, 8216.0, 8208.0, 8209.0, 8201.0, 8190.0, 8195.0, 8193.0, 8181.0, 8175.0, 8183.0, 8182.0, 8181.0, 8159.0, 8152.0, 8150.0, 8160.0, 8161.0, 8153.0, 8153.0, 8137.0, 8135.0, 8139.0, 8130.0, 8130.0, 8140.0, 8137.0, 8145.0, 8134.0, 8123.0, 8116.0, 8122.0, 8124.0, 8122.0, 8105.0, 8096.0, 8096.0, 8097.0, 8100.0, 8100.0, 8104.0, 8101.0, 8103.0, 8109.0, 8108.0, 8089.0, 8092.0, 8097.0, 8098.0, 8094.0, 8092.0, 8087.0, 8094.0, 8094.0, 8069.0, 8058.0, 8065.0, 8066.0, 8065.0, 8046.0, 8041.0, 8036.0, 8038.0, 8039.0, 8047.0, 8053.0, 8058.0, 8056.0, 8056.0, 8053.0, 8052.0, 8054.0, 8051.0, 8053.0, 8056.0, 8055.0, 8063.0, 8064.0, 8063.0, 8062.0, 8061.0, 8059.0, 8059.0, 8063.0, 8066.0, 8067.0, 8068.0, 8071.0, 8071.0, 8079.0, 8074.0, 8073.0, 8074.0, 8073.0, 8073.0, 8076.0, 8079.0, 8080.0, 8083.0, 8083.0, 8085.0, 8082.0, 8082.0, 8081.0, 8072.0, 8072.0, 8068.0, 8070.0, 8070.0, 8072.0, 8074.0, 8075.0, 8073.0, 8071.0, 8070.0, 8067.0, 8074.0, 8076.0, 8072.0, 8070.0, 8072.0, 8079.0, 8081.0, 8082.0, 8082.0, 8084.0, 8083.0, 8097.0, 8103.0, 8107.0, 8104.0, 8103.0, 8104.0, 8103.0, 8105.0, 8103.0, 8102.0, 8102.0, 8103.0, 8106.0, 8107.0, 8108.0, 8102.0, 8098.0, 8096.0, 8095.0, 8096.0, 8093.0, 8094.0, 8094.0, 8096.0, 8097.0, 8097.0, 8096.0, 8094.0, 8094.0, 8086.0, 8086.0, 8087.0, 8090.0, 8091.0, 8095.0, 8099.0, 8104.0, 8102.0, 8106.0, 8101.0, 8103.0, 8104.0, 8104.0, 8101.0, 8102.0, 8096.0, 8096.0, 8098.0, 8100.0, 8102.0, 8106.0, 8103.0, 8103.0, 8094.0, 8090.0, 8090.0, 8089.0, 8088.0, 8090.0, 8093.0, 8094.0, 8094.0, 8088.0, 8087.0, 8079.0, 8075.0, 8076.0, 8077.0, 8081.0, 8083.0, 8083.0, 8084.0, 8087.0, 8089.0, 8088.0, 8088.0, 8086.0, 8087.0, 8090.0, 8088.0, 8090.0, 8091.0, 8087.0, 8087.0, 8086.0, 8088.0, 8087.0, 8082.0, 8083.0, 8083.0, 8078.0, 8077.0, 8077.0, 8072.0, 8074.0, 8071.0, 8070.0, 8072.0, 8073.0, 8073.0, 8072.0, 8076.0, 8079.0, 8075.0, 8075.0, 8075.0, 8076.0, 8076.0, 8074.0, 8076.0, 8069.0, 8068.0, 8069.0, 8069.0, 8065.0, 8067.0, 8067.0, 8067.0, 8073.0, 8075.0, 8076.0, 8077.0, 8075.0, 8072.0, 8074.0, 8075.0, 8074.0, 8072.0, 8066.0, 8063.0, 8062.0, 8058.0, 8060.0, 8059.0, 8060.0, 8059.0, 8062.0, 8067.0, 8068.0, 8067.0, 8068.0, 8071.0}
close := []float64{8262.0, 8273.0, 8279.0, 8279.0, 8275.0, 8282.0, 8278.0, 8279.0, 8281.0, 8285.0, 8287.0, 8284.0, 8283.0, 8283.0, 8285.0, 8286.0, 8287.0, 8290.0, 8283.0, 8287.0, 8278.0, 8275.0, 8276.0, 8275.0, 8281.0, 8270.0, 8257.0, 8258.0, 8252.0, 8243.0, 8231.0, 8219.0, 8220.0, 8216.0, 8210.0, 8211.0, 8201.0, 8197.0, 8201.0, 8193.0, 8183.0, 8184.0, 8191.0, 8184.0, 8185.0, 8161.0, 8154.0, 8163.0, 8164.0, 8162.0, 8156.0, 8158.0, 8141.0, 8139.0, 8142.0, 8130.0, 8145.0, 8140.0, 8149.0, 8146.0, 8136.0, 8123.0, 8126.0, 8130.0, 8125.0, 8122.0, 8106.0, 8096.0, 8103.0, 8102.0, 8111.0, 8105.0, 8111.0, 8103.0, 8112.0, 8113.0, 8109.0, 8093.0, 8101.0, 8101.0, 8100.0, 8095.0, 8096.0, 8095.0, 8100.0, 8095.0, 8069.0, 8068.0, 8072.0, 8068.0, 8067.0, 8046.0, 8045.0, 8043.0, 8040.0, 8049.0, 8055.0, 8062.0, 8062.0, 8058.0, 8056.0, 8055.0, 8058.0, 8057.0, 8054.0, 8056.0, 8057.0, 8066.0, 8065.0, 8069.0, 8064.0, 8063.0, 8064.0, 8059.0, 8065.0, 8069.0, 8068.0, 8069.0, 8072.0, 8074.0, 8084.0, 8084.0, 8076.0, 8074.0, 8074.0, 8075.0, 8077.0, 8080.0, 8082.0, 8086.0, 8084.0, 8087.0, 8087.0, 8083.0, 8083.0, 8082.0, 8074.0, 8073.0, 8072.0, 8071.0, 8072.0, 8075.0, 8076.0, 8076.0, 8074.0, 8071.0, 8071.0, 8075.0, 8079.0, 8077.0, 8074.0, 8072.0, 8079.0, 8084.0, 8082.0, 8085.0, 8086.0, 8084.0, 8102.0, 8107.0, 8113.0, 8109.0, 8104.0, 8104.0, 8105.0, 8108.0, 8106.0, 8104.0, 8106.0, 8105.0, 8110.0, 8107.0, 8109.0, 8112.0, 8104.0, 8099.0, 8097.0, 8097.0, 8098.0, 8095.0, 8096.0, 8097.0, 8099.0, 8098.0, 8099.0, 8099.0, 8095.0, 8097.0, 8086.0, 8088.0, 8093.0, 8092.0, 8096.0, 8101.0, 8105.0, 8105.0, 8109.0, 8107.0, 8103.0, 8104.0, 8109.0, 8105.0, 8102.0, 8104.0, 8097.0, 8100.0, 8103.0, 8103.0, 8109.0, 8107.0, 8106.0, 8104.0, 8096.0, 8090.0, 8092.0, 8089.0, 8093.0, 8093.0, 8094.0, 8095.0, 8096.0, 8088.0, 8089.0, 8079.0, 8077.0, 8079.0, 8082.0, 8083.0, 8084.0, 8084.0, 8087.0, 8091.0, 8088.0, 8088.0, 8091.0, 8087.0, 8092.0, 8090.0, 8091.0, 8095.0, 8092.0, 8088.0, 8087.0, 8090.0, 8089.0, 8087.0, 8084.0, 8088.0, 8084.0, 8079.0, 8078.0, 8078.0, 8076.0, 8075.0, 8071.0, 8072.0, 8074.0, 8077.0, 8074.0, 8077.0, 8081.0, 8080.0, 8076.0, 8076.0, 8078.0, 8079.0, 8076.0, 8076.0, 8076.0, 8070.0, 8072.0, 8069.0, 8072.0, 8070.0, 8069.0, 8069.0, 8073.0, 8078.0, 8077.0, 8079.0, 8080.0, 8076.0, 8076.0, 8076.0, 8077.0, 8078.0, 8075.0, 8067.0, 8064.0, 8064.0, 8062.0, 8062.0, 8065.0, 8062.0, 8063.0, 8074.0, 8070.0, 8069.0, 8068.0, 8074.0}
buildKLines := func(open, high, low, close []float64) (kLines []types.KLine) {
for i := range high {
kLines = append(kLines, types.KLine{Open: open[i], High: high[i], Low: low[i], Close: close[i], EndTime: time.Now()})
}
return kLines
}
tests := []struct {
name string
kLines []types.KLine
window int
want_k float64
want_d float64
}{
{
name: "TXF1-1min_2016/1/4",
kLines: buildKLines(open, high, low, close),
window: 14,
want_k: 84.210526,
want_d: 59.888357,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
kd := STOCH{IntervalWindow: types.IntervalWindow{Window: tt.window}}
kd.calculateAndUpdate(tt.kLines)
got_k := kd.LastK()
diff_k := math.Trunc((got_k-tt.want_k)*100) / 100
if diff_k != 0 {
t.Errorf("%%K() = %v, want %v", got_k, tt.want_k)
}
got_d := kd.LastD()
diff_d := math.Trunc((got_d-tt.want_d)*100) / 100
if diff_d != 0 {
t.Errorf("%%D() = %v, want %v", got_d, tt.want_d)
}
})
}
}

View File

@ -18,7 +18,7 @@ Volume-Weighted Average Price (VWAP) Explained
//go:generate callbackgen -type VWAP
type VWAP struct {
types.IntervalWindow
Values Float64Slice
Values types.Float64Slice
WeightedSum float64
VolumeSum float64
EndTime time.Time

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

@ -391,9 +391,9 @@ func (s *Strategy) tradeUpdateHandler(trade types.Trade) {
return
}
profit, madeProfit := s.state.Position.AddTrade(trade)
profit, netProfit, madeProfit := s.state.Position.AddTrade(trade)
if madeProfit {
s.Notify("average cost profit: %f", profit.Float64())
s.Notify("%s average cost profit: %f, net profit =~ %f", s.Symbol, profit.Float64(), netProfit.Float64())
}
}
}

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,23 +442,18 @@ 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)
@ -542,7 +536,7 @@ func (s *Strategy) handleTradeUpdate(trade types.Trade) {
s.state.HedgePosition.AtomicAdd(q)
s.state.AccumulatedVolume.AtomicAdd(fixedpoint.NewFromFloat(trade.Quantity))
if profit, madeProfit := s.state.Position.AddTrade(trade); madeProfit {
if profit, netProfit, madeProfit := s.state.Position.AddTrade(trade); madeProfit {
s.state.AccumulatedPnL.AtomicAdd(profit)
if profit < 0 {
@ -552,17 +546,20 @@ func (s *Strategy) handleTradeUpdate(trade types.Trade) {
}
profitMargin := profit.DivFloat64(trade.QuoteQuantity)
netProfitMargin := netProfit.DivFloat64(trade.QuoteQuantity)
var since time.Time
if s.state.AccumulatedSince > 0 {
since = time.Unix(s.state.AccumulatedSince, 0).In(localTimeZone)
}
s.Notify("%s trade profit %s %f %s (%.3f%%), since %s accumulated net profit %f %s, accumulated loss %f %s",
s.Notify("%s trade profit %s %f %s (%.2f%%), net profit =~ %f %s (%.2f%%), since %s accumulated net profit %f %s, accumulated loss %f %s",
s.Symbol,
pnlEmoji(profit),
profit.Float64(), s.state.Position.QuoteCurrency,
profitMargin.Float64()*100.0,
netProfit.Float64(), s.state.Position.QuoteCurrency,
netProfitMargin.Float64()*100.0,
since.Format(time.RFC822),
s.state.AccumulatedPnL.Float64(), s.state.Position.QuoteCurrency,
s.state.AccumulatedLoss.Float64(), s.state.Position.QuoteCurrency)
@ -572,6 +569,10 @@ func (s *Strategy) handleTradeUpdate(trade types.Trade) {
}
s.lastPrice = trade.Price
if err := s.SaveState(); err != nil {
log.WithError(err).Error("save state error")
}
}
func (s *Strategy) Validate() error {
@ -590,6 +591,33 @@ func (s *Strategy) Validate() error {
return nil
}
func (s *Strategy) LoadState() error {
var state State
// load position
if err := s.Persistence.Load(&state, ID, s.Symbol, stateKey); err != nil {
if err != service.ErrPersistenceNotExists {
return err
}
s.state = &State{}
} else {
s.state = &state
log.Infof("state is restored: %+v", s.state)
}
return nil
}
func (s *Strategy) SaveState() error {
if err := s.Persistence.Save(s.state, ID, s.Symbol, stateKey); err != nil {
return err
} else {
log.Infof("state is saved => %+v", s.state)
}
return nil
}
func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.OrderExecutionRouter, sessions map[string]*bbgo.ExchangeSession) error {
if s.BollBandInterval == "" {
s.BollBandInterval = types.Interval1m
@ -671,20 +699,9 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order
s.groupID = max.GenerateGroupID(instanceID)
log.Infof("using group id %d from fnv(%s)", s.groupID, instanceID)
var state State
// load position
if err := s.Persistence.Load(&state, ID, s.Symbol, stateKey); err != nil {
if err != service.ErrPersistenceNotExists {
return err
}
s.state = &State{}
if err := s.LoadState(); err != nil {
return err
} else {
// loaded successfully
s.state = &state
log.Infof("state is restored: %+v", s.state)
s.Notify("%s position is restored => %f", s.Symbol, s.state.HedgePosition.Float64())
}
@ -772,8 +789,10 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order
close(s.stopC)
// wait for the quoter to stop
time.Sleep(s.UpdateInterval.Duration())
// ensure every order is cancelled
for s.activeMakerOrders.NumOfOrders() > 0 {
orders := s.activeMakerOrders.Orders()
log.Warnf("%d orders are not cancelled yet:", len(orders))
@ -781,18 +800,45 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order
if err := s.makerSession.Exchange.CancelOrders(ctx, s.activeMakerOrders.Orders()...); err != nil {
log.WithError(err).Errorf("can not cancel %s orders", s.Symbol)
continue
}
log.Warnf("waiting for orders to be cancelled...")
time.Sleep(3 * time.Second)
log.Infof("waiting for orders to be cancelled...")
select {
case <-time.After(3 * time.Second):
case <-ctx.Done():
break
}
// verify the current open orders via the RESTful API
if s.activeMakerOrders.NumOfOrders() > 0 {
log.Warnf("there are orders not cancelled, using REStful API to verify...")
openOrders, err := s.makerSession.Exchange.QueryOpenOrders(ctx, s.Symbol)
if err != nil {
log.WithError(err).Errorf("can not query %s open orders", s.Symbol)
continue
}
openOrderStore := bbgo.NewOrderStore(s.Symbol)
openOrderStore.Add(openOrders...)
for _, o := range s.activeMakerOrders.Orders() {
// if it does not exist, we should remove it
if !openOrderStore.Exists(o.OrderID) {
s.activeMakerOrders.Remove(o)
}
}
}
}
log.Info("all orders are cancelled successfully")
if err := s.Persistence.Save(s.state, ID, s.Symbol, stateKey); err != nil {
if err := s.SaveState(); err != nil {
log.WithError(err).Errorf("can not save state: %+v", s.state)
} else {
log.Infof("state is saved => %+v", s.state)
s.Notify("%s position is saved: position = %f", s.Symbol, s.state.HedgePosition.Float64())
s.Notify("%s position is saved: position = %f", s.Symbol, s.state.HedgePosition.Float64(), s.state.Position)
}
})

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"`

55
pkg/types/float_slice.go Normal file
View File

@ -0,0 +1,55 @@
package types
import "math"
type Float64Slice []float64
func (s *Float64Slice) Push(v float64) {
*s = append(*s, v)
}
func (s *Float64Slice) Pop(i int64) (v float64) {
v = (*s)[i]
*s = append((*s)[:i], (*s)[i+1:]...)
return v
}
func (s Float64Slice) Max() float64 {
m := -math.MaxFloat64
for _, v := range s {
m = math.Max(m, v)
}
return m
}
func (s Float64Slice) Min() float64 {
m := math.MaxFloat64
for _, v := range s {
m = math.Min(m, v)
}
return m
}
func (s Float64Slice) Sum() (sum float64) {
for _, v := range s {
sum += v
}
return sum
}
func (s Float64Slice) Mean() (mean float64) {
return s.Sum() / float64(len(s))
}
func (s Float64Slice) Tail(size int) Float64Slice {
length := len(s)
if length <= size {
win := make(Float64Slice, length)
copy(win, s)
return win
}
win := make(Float64Slice, size)
copy(win, s[length-size:])
return win
}

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 {
@ -256,7 +256,7 @@ func (k KLineWindow) GetClose() float64 {
}
func (k KLineWindow) GetHigh() float64 {
high := k.GetOpen()
high := k.First().GetHigh()
for _, line := range k {
high = math.Max(high, line.GetHigh())
}
@ -265,7 +265,7 @@ func (k KLineWindow) GetHigh() float64 {
}
func (k KLineWindow) GetLow() float64 {
low := k.GetOpen()
low := k.First().GetLow()
for _, line := range k {
low = math.Min(low, line.GetLow())
}
@ -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
return (k.GetHigh() + k.GetLow()) / 2.0
}
// 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()
@ -355,7 +356,7 @@ func (k KLineWindow) Tail(size int) KLineWindow {
}
win := make(KLineWindow, size)
copy(win, k[length-1-size:])
copy(win, k[length-size:])
return win
}

View File

@ -9,17 +9,20 @@ import (
func TestKLineWindow_Tail(t *testing.T) {
var win = KLineWindow{
{Open: 11600.0, Close: 11600.0, High: 11600.0, Low: 11600.0},
{Open: 11600.0, Close: 11600.0, High: 11600.0, Low: 11600.0},
{Open: 11700.0, Close: 11700.0, High: 11700.0, Low: 11700.0},
}
var win2 = win.Tail(1)
assert.Len(t, win2, 1)
assert.ElementsMatch(t, win2, win[1:])
var win3 = win.Tail(2)
assert.Len(t, win3, 2)
assert.ElementsMatch(t, win3, win)
var win4 = win.Tail(3)
assert.Len(t, win4, 2)
assert.ElementsMatch(t, win4, win)
}
func TestKLineWindow_Truncate(t *testing.T) {

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"
)
@ -145,14 +144,14 @@ func (o *SubmitOrder) SlackAttachment() slack.Attachment {
type Order struct {
SubmitOrder
Exchange ExchangeName `json:"exchange" db:"exchange"`
GID uint64 `json:"gid" db:"gid"`
OrderID uint64 `json:"orderID" db:"order_id"` // order id
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"`
Exchange ExchangeName `json:"exchange" db:"exchange"`
GID uint64 `json:"gid" db:"gid"`
OrderID uint64 `json:"orderID" db:"order_id"` // order id
Status OrderStatus `json:"status" db:"status"`
ExecutedQuantity float64 `json:"executedQuantity" db:"executed_quantity"`
IsWorking bool `json:"isWorking" db:"is_working"`
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
}

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

@ -0,0 +1,189 @@
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()
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()
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(limit int) OrderBook {
var book = NewRBOrderBook(b.Symbol)
book.Asks = b.Asks.CopyInorder(limit)
book.Bids = b.Bids.CopyInorderReverse(limit)
return book
}
func (b *RBTOrderBook) convertTreeToPriceVolumeSlice(tree *RBTree, limit int, descending bool) (pvs PriceVolumeSlice) {
if descending {
tree.InorderReverse(func(n *RBNode) bool {
pvs = append(pvs, PriceVolume{
Price: n.Key,
Volume: n.Value,
})
return !(limit > 0 && len(pvs) >= limit)
})
return pvs
}
tree.Inorder(func(n *RBNode) bool {
pvs = append(pvs, PriceVolume{
Price: n.Key,
Volume: n.Value,
})
return !(limit > 0 && len(pvs) >= limit)
})
return pvs
}
func (b *RBTOrderBook) SideBook(sideType SideType) PriceVolumeSlice {
switch sideType {
case SideTypeBuy:
return b.convertTreeToPriceVolumeSlice(b.Bids, 0, true)
case SideTypeSell:
return b.convertTreeToPriceVolumeSlice(b.Asks, 0, false)
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)
}
}

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

@ -0,0 +1,497 @@
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 == nil {
// prevent segmentation fault
y = Neel
}
}
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 {
if current.Parent == current.Parent.Parent.Left {
uncle := current.Parent.Parent.Right
if uncle.Color == Red {
current.Parent.Color = Black
uncle.Color = Black
current.Parent.Parent.Color = Red
current = current.Parent.Parent
} 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
current = current.Parent.Parent
} 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() *RBNode {
return tree.RightmostOf(tree.Root)
}
func (tree *RBTree) RightmostOf(current *RBNode) *RBNode {
if current == Neel {
return nil
}
for current.Right != Neel {
current = current.Right
}
return current
}
func (tree *RBTree) Leftmost() *RBNode {
return tree.LeftmostOf(tree.Root)
}
func (tree *RBTree) LeftmostOf(current *RBNode) *RBNode {
if current == Neel {
return nil
}
for current.Left != Neel {
current = current.Left
}
return current
}
func (tree *RBTree) Successor(current *RBNode) *RBNode {
if current == Neel {
return nil
}
if current.Right != Neel {
return tree.LeftmostOf(current.Right)
}
var newNode = current.Parent
for newNode != Neel && current == newNode.Right {
current = newNode
newNode = newNode.Parent
}
if newNode == Neel {
return nil
}
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 != Neel {
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 != Neel {
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(limit int) OrderBook {
var book SliceOrderBook
book.Symbol = b.Symbol
book.Bids = book.Bids.CopyDepth(limit)
book.Asks = book.Asks.CopyDepth(limit)
return &book
}
func (b *SliceOrderBook) Copy() OrderBook {
var book SliceOrderBook
book.Symbol = b.Symbol
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"
)
@ -59,12 +58,12 @@ type Trade struct {
QuoteQuantity float64 `json:"quoteQuantity" db:"quote_quantity"`
Symbol string `json:"symbol" db:"symbol"`
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"`
Fee float64 `json:"fee" db:"fee"`
FeeCurrency string `json:"feeCurrency" db:"fee_currency"`
Side SideType `json:"side" db:"side"`
IsBuyer bool `json:"isBuyer" db:"is_buyer"`
IsMaker bool `json:"isMaker" db:"is_maker"`
Time Time `json:"tradedAt" db:"traded_at"`
Fee float64 `json:"fee" db:"fee"`
FeeCurrency string `json:"feeCurrency" db:"fee_currency"`
IsMargin bool `json:"isMargin" db:"is_margin"`
IsIsolated bool `json:"isIsolated" db:"is_isolated"`
@ -74,12 +73,12 @@ type Trade struct {
}
func (trade Trade) String() string {
return fmt.Sprintf("TRADE %s %s %4s %s @ %s orderID %d %s amount %f",
return fmt.Sprintf("TRADE %s %s %4s %f @ %f orderID %d %s amount %f",
trade.Exchange.String(),
trade.Symbol,
trade.Side,
util.FormatFloat(trade.Quantity, 4),
util.FormatFloat(trade.Price, 3),
trade.Quantity,
trade.Price,
trade.OrderID,
trade.Time.Time().Format(time.StampMilli),
trade.QuoteQuantity)
@ -87,13 +86,13 @@ func (trade Trade) String() string {
// PlainText is used for telegram-styled messages
func (trade Trade) PlainText() string {
return fmt.Sprintf("Trade %s %s %s %s @ %s, amount %s",
return fmt.Sprintf("Trade %s %s %s %f @ %f, amount %f",
trade.Exchange.String(),
trade.Symbol,
trade.Side,
util.FormatFloat(trade.Quantity, 4),
util.FormatFloat(trade.Price, 3),
util.FormatFloat(trade.QuoteQuantity, 2))
trade.Quantity,
trade.Price,
trade.QuoteQuantity)
}
var slackTradeTextTemplate = ":handshake: {{ .Symbol }} {{ .Side }} Trade Execution @ {{ .Price }}"
@ -122,7 +121,7 @@ func (trade Trade) SlackAttachment() slack.Attachment {
{Title: "Liquidity", Value: liquidity, Short: true},
{Title: "Order ID", Value: strconv.FormatUint(trade.OrderID, 10), Short: true},
},
Footer: util.Render("trade time {{ . }}", trade.Time.Time().Format(time.RFC822)),
Footer: util.Render("trade time {{ . }}", trade.Time.Time().Format(time.StampMilli)),
}
}

View File

@ -3,8 +3,6 @@ package types
import (
"fmt"
"time"
"github.com/c9s/bbgo/pkg/datatype"
)
type Withdraw struct {
@ -16,12 +14,12 @@ type Withdraw struct {
AddressTag string `json:"addressTag"`
Status string `json:"status"`
TransactionID string `json:"transactionID" db:"txn_id"`
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"`
Network string `json:"network" db:"network"`
TransactionID string `json:"transactionID" db:"txn_id"`
TransactionFee float64 `json:"transactionFee" db:"txn_fee"`
TransactionFeeCurrency string `json:"transactionFeeCurrency" db:"txn_fee_currency"`
WithdrawOrderID string `json:"withdrawOrderId"`
ApplyTime Time `json:"applyTime" db:"time"`
Network string `json:"network" db:"network"`
}
func (w Withdraw) String() string {