mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-22 23:05:15 +00:00
Merge branch 'main' into ftx/websocket-kline
This commit is contained in:
commit
64387ed2cb
|
@ -308,7 +308,6 @@ import (
|
|||
_ "github.com/c9s/bbgo/pkg/strategy/buyandhold"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/flashcrash"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/grid"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/mirrormaker"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/pricealert"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/support"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/swing"
|
||||
|
|
|
@ -48,11 +48,11 @@ var rootCmd = &cobra.Command{
|
|||
stream.SetPublicOnly()
|
||||
stream.Subscribe(types.BookChannel, symbol, types.SubscribeOptions{})
|
||||
|
||||
stream.OnBookSnapshot(func(book types.OrderBook) {
|
||||
stream.OnBookSnapshot(func(book types.SliceOrderBook) {
|
||||
// log.Infof("book snapshot: %+v", book)
|
||||
})
|
||||
|
||||
stream.OnBookUpdate(func(book types.OrderBook) {
|
||||
stream.OnBookUpdate(func(book types.SliceOrderBook) {
|
||||
// log.Infof("book update: %+v", book)
|
||||
})
|
||||
|
||||
|
@ -67,7 +67,7 @@ var rootCmd = &cobra.Command{
|
|||
return
|
||||
|
||||
case <-streambook.C:
|
||||
book := streambook.Get()
|
||||
book := streambook.Copy()
|
||||
|
||||
if valid, err := book.IsValid(); !valid {
|
||||
log.Errorf("order book is invalid, error: %v", err)
|
||||
|
|
|
@ -9,7 +9,6 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/datatype"
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
@ -237,7 +236,7 @@ func (m *SimplePriceMatching) newTradeFromOrder(order types.Order, isMaker bool)
|
|||
Side: order.Side,
|
||||
IsBuyer: order.Side == types.SideTypeBuy,
|
||||
IsMaker: isMaker,
|
||||
Time: datatype.Time(m.CurrentTime),
|
||||
Time: types.Time(m.CurrentTime),
|
||||
Fee: fee,
|
||||
FeeCurrency: feeCurrency,
|
||||
}
|
||||
|
@ -439,7 +438,7 @@ func (m *SimplePriceMatching) newOrder(o types.SubmitOrder, orderID uint64) type
|
|||
Status: types.OrderStatusNew,
|
||||
ExecutedQuantity: 0,
|
||||
IsWorking: true,
|
||||
CreationTime: datatype.Time(m.CurrentTime),
|
||||
UpdateTime: datatype.Time(m.CurrentTime),
|
||||
CreationTime: types.Time(m.CurrentTime),
|
||||
UpdateTime: types.Time(m.CurrentTime),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
_ "github.com/c9s/bbgo/pkg/strategy/flashcrash"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/gap"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/grid"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/mirrormaker"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/pricealert"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/schedule"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/support"
|
||||
|
|
|
@ -49,10 +49,10 @@ var orderbookCmd = &cobra.Command{
|
|||
s := ex.NewStream()
|
||||
s.SetPublicOnly()
|
||||
s.Subscribe(types.BookChannel, symbol, types.SubscribeOptions{})
|
||||
s.OnBookSnapshot(func(book types.OrderBook) {
|
||||
s.OnBookSnapshot(func(book types.SliceOrderBook) {
|
||||
log.Infof("orderbook snapshot: %s", book.String())
|
||||
})
|
||||
s.OnBookUpdate(func(book types.OrderBook) {
|
||||
s.OnBookUpdate(func(book types.SliceOrderBook) {
|
||||
log.Infof("orderbook update: %s", book.String())
|
||||
})
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -8,7 +8,6 @@ import (
|
|||
"github.com/adshao/go-binance/v2"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/datatype"
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
"github.com/c9s/bbgo/pkg/util"
|
||||
|
@ -150,8 +149,8 @@ func ToGlobalOrder(binanceOrder *binance.Order, isMargin bool) (*types.Order, er
|
|||
OrderID: uint64(binanceOrder.OrderID),
|
||||
Status: toGlobalOrderStatus(binanceOrder.Status),
|
||||
ExecutedQuantity: util.MustParseFloat(binanceOrder.ExecutedQuantity),
|
||||
CreationTime: datatype.Time(millisecondTime(binanceOrder.Time)),
|
||||
UpdateTime: datatype.Time(millisecondTime(binanceOrder.UpdateTime)),
|
||||
CreationTime: types.Time(millisecondTime(binanceOrder.Time)),
|
||||
UpdateTime: types.Time(millisecondTime(binanceOrder.UpdateTime)),
|
||||
IsMargin: isMargin,
|
||||
IsIsolated: binanceOrder.IsIsolated,
|
||||
}, nil
|
||||
|
@ -208,7 +207,7 @@ func ToGlobalTrade(t binance.TradeV3, isMargin bool) (*types.Trade, error) {
|
|||
IsMaker: t.IsMaker,
|
||||
Fee: fee,
|
||||
FeeCurrency: t.CommissionAsset,
|
||||
Time: datatype.Time(millisecondTime(t.Time)),
|
||||
Time: types.Time(millisecondTime(t.Time)),
|
||||
IsMargin: isMargin,
|
||||
IsIsolated: t.IsIsolated,
|
||||
}, nil
|
||||
|
|
|
@ -14,7 +14,6 @@ import (
|
|||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/datatype"
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
"github.com/c9s/bbgo/pkg/util"
|
||||
|
@ -283,7 +282,7 @@ func (e *Exchange) QueryWithdrawHistory(ctx context.Context, asset string, since
|
|||
txIDs[d.TxID] = struct{}{}
|
||||
allWithdraws = append(allWithdraws, types.Withdraw{
|
||||
Exchange: types.ExchangeBinance,
|
||||
ApplyTime: datatype.Time(time.Unix(0, d.ApplyTime*int64(time.Millisecond))),
|
||||
ApplyTime: types.Time(time.Unix(0, d.ApplyTime*int64(time.Millisecond))),
|
||||
Asset: d.Asset,
|
||||
Amount: d.Amount,
|
||||
Address: d.Address,
|
||||
|
@ -356,7 +355,7 @@ func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since,
|
|||
txIDs[d.TxID] = struct{}{}
|
||||
allDeposits = append(allDeposits, types.Deposit{
|
||||
Exchange: types.ExchangeBinance,
|
||||
Time: datatype.Time(time.Unix(0, d.InsertTime*int64(time.Millisecond))),
|
||||
Time: types.Time(time.Unix(0, d.InsertTime*int64(time.Millisecond))),
|
||||
Asset: d.Asset,
|
||||
Amount: d.Amount,
|
||||
Address: d.Address,
|
||||
|
|
|
@ -9,7 +9,6 @@ import (
|
|||
"github.com/adshao/go-binance/v2"
|
||||
"github.com/valyala/fastjson"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/datatype"
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
"github.com/c9s/bbgo/pkg/util"
|
||||
|
@ -126,7 +125,7 @@ func (e *ExecutionReportEvent) Order() (*types.Order, error) {
|
|||
OrderID: uint64(e.OrderID),
|
||||
Status: toGlobalOrderStatus(binance.OrderStatusType(e.CurrentOrderStatus)),
|
||||
ExecutedQuantity: util.MustParseFloat(e.CumulativeFilledQuantity),
|
||||
CreationTime: datatype.Time(orderCreationTime),
|
||||
CreationTime: types.Time(orderCreationTime),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -147,7 +146,7 @@ func (e *ExecutionReportEvent) Trade() (*types.Trade, error) {
|
|||
QuoteQuantity: util.MustParseFloat(e.LastQuoteAssetTransactedQuantity),
|
||||
IsBuyer: e.Side == "BUY",
|
||||
IsMaker: e.IsMaker,
|
||||
Time: datatype.Time(tt),
|
||||
Time: types.Time(tt),
|
||||
Fee: util.MustParseFloat(e.CommissionAmount),
|
||||
FeeCurrency: e.CommissionAsset,
|
||||
}, nil
|
||||
|
@ -320,7 +319,7 @@ type DepthEvent struct {
|
|||
Asks []DepthEntry
|
||||
}
|
||||
|
||||
func (e *DepthEvent) OrderBook() (book types.OrderBook, err error) {
|
||||
func (e *DepthEvent) OrderBook() (book types.SliceOrderBook, err error) {
|
||||
book.Symbol = e.Symbol
|
||||
|
||||
for _, entry := range e.Bids {
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/datatype"
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
@ -52,8 +51,8 @@ func toGlobalOrder(r order) (types.Order, error) {
|
|||
OrderID: uint64(r.ID),
|
||||
Status: "",
|
||||
ExecutedQuantity: r.FilledSize,
|
||||
CreationTime: datatype.Time(r.CreatedAt.Time),
|
||||
UpdateTime: datatype.Time(r.CreatedAt.Time),
|
||||
CreationTime: types.Time(r.CreatedAt.Time),
|
||||
UpdateTime: types.Time(r.CreatedAt.Time),
|
||||
}
|
||||
|
||||
// `new` (accepted but not processed yet), `open`, or `closed` (filled or cancelled)
|
||||
|
@ -93,7 +92,7 @@ func toGlobalDeposit(input depositHistory) (types.Deposit, error) {
|
|||
d := types.Deposit{
|
||||
GID: 0,
|
||||
Exchange: types.ExchangeFTX,
|
||||
Time: datatype.Time(t.Time),
|
||||
Time: types.Time(t.Time),
|
||||
Amount: input.Size,
|
||||
Asset: toGlobalCurrency(input.Coin),
|
||||
TransactionID: input.TxID,
|
||||
|
@ -126,7 +125,7 @@ func toGlobalTrade(f fill) (types.Trade, error) {
|
|||
Side: f.Side,
|
||||
IsBuyer: f.Side == types.SideTypeBuy,
|
||||
IsMaker: f.Liquidity == "maker",
|
||||
Time: datatype.Time(f.Time.Time),
|
||||
Time: types.Time(f.Time.Time),
|
||||
Fee: f.Fee,
|
||||
FeeCurrency: f.FeeCurrency,
|
||||
IsMargin: false,
|
||||
|
|
|
@ -13,7 +13,6 @@ import (
|
|||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/datatype"
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
@ -467,7 +466,7 @@ func TestExchange_QueryDepositHistory(t *testing.T) {
|
|||
assert.Len(t, dh, 1)
|
||||
assert.Equal(t, types.Deposit{
|
||||
Exchange: types.ExchangeFTX,
|
||||
Time: datatype.Time(actualConfirmedTime),
|
||||
Time: types.Time(actualConfirmedTime),
|
||||
Amount: 99.0,
|
||||
Asset: "TUSD",
|
||||
TransactionID: "0x8078356ae4b06a036d64747546c274af19581f1c78c510b60505798a7ffcaf1",
|
||||
|
@ -610,7 +609,7 @@ func TestExchange_QueryTrades(t *testing.T) {
|
|||
Side: types.SideTypeSell,
|
||||
IsBuyer: false,
|
||||
IsMaker: true,
|
||||
Time: datatype.Time(actualConfirmedTime),
|
||||
Time: types.Time(actualConfirmedTime),
|
||||
Fee: -0.0033625,
|
||||
FeeCurrency: "USD",
|
||||
IsMargin: false,
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/datatype"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
|
@ -55,8 +54,8 @@ func Test_messageHandler_handleMessage(t *testing.T) {
|
|||
OrderID: 36379,
|
||||
Status: types.OrderStatusFilled,
|
||||
ExecutedQuantity: 1.0,
|
||||
CreationTime: datatype.Time(mustParseDatetime("2021-03-28T06:12:50.991447+00:00")),
|
||||
UpdateTime: datatype.Time(mustParseDatetime("2021-03-28T06:12:50.991447+00:00")),
|
||||
CreationTime: types.Time(mustParseDatetime("2021-03-28T06:12:50.991447+00:00")),
|
||||
UpdateTime: types.Time(mustParseDatetime("2021-03-28T06:12:50.991447+00:00")),
|
||||
}, order)
|
||||
})
|
||||
h.handleMessage(input)
|
||||
|
@ -104,7 +103,7 @@ func Test_messageHandler_handleMessage(t *testing.T) {
|
|||
Side: types.SideTypeBuy,
|
||||
IsBuyer: true,
|
||||
IsMaker: false,
|
||||
Time: datatype.Time(mustParseDatetime("2021-03-28T06:12:34.702926+00:00")),
|
||||
Time: types.Time(mustParseDatetime("2021-03-28T06:12:34.702926+00:00")),
|
||||
Fee: 0.00153917575,
|
||||
FeeCurrency: "USD",
|
||||
IsMargin: false,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -8,7 +8,6 @@ import (
|
|||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/datatype"
|
||||
"github.com/c9s/bbgo/pkg/exchange/max/maxapi"
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
|
@ -201,8 +200,8 @@ func toGlobalOrder(maxOrder max.Order) (*types.Order, error) {
|
|||
OrderID: maxOrder.ID,
|
||||
Status: toGlobalOrderStatus(maxOrder.State, executedVolume, remainingVolume),
|
||||
ExecutedQuantity: executedVolume.Float64(),
|
||||
CreationTime: datatype.Time(maxOrder.CreatedAt),
|
||||
UpdateTime: datatype.Time(maxOrder.CreatedAt),
|
||||
CreationTime: types.Time(maxOrder.CreatedAt),
|
||||
UpdateTime: types.Time(maxOrder.CreatedAt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -246,7 +245,7 @@ func toGlobalTrade(t max.Trade) (*types.Trade, error) {
|
|||
Fee: fee,
|
||||
FeeCurrency: toGlobalCurrency(t.FeeCurrency),
|
||||
QuoteQuantity: quoteQuantity,
|
||||
Time: datatype.Time(mts),
|
||||
Time: types.Time(mts),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ import (
|
|||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/datatype"
|
||||
maxapi "github.com/c9s/bbgo/pkg/exchange/max/maxapi"
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
|
@ -610,7 +609,7 @@ func (e *Exchange) QueryWithdrawHistory(ctx context.Context, asset string, since
|
|||
txIDs[d.TxID] = struct{}{}
|
||||
withdraw := types.Withdraw{
|
||||
Exchange: types.ExchangeMax,
|
||||
ApplyTime: datatype.Time(time.Unix(d.CreatedAt, 0)),
|
||||
ApplyTime: types.Time(time.Unix(d.CreatedAt, 0)),
|
||||
Asset: toGlobalCurrency(d.Currency),
|
||||
Amount: util.MustParseFloat(d.Amount),
|
||||
Address: "",
|
||||
|
@ -682,7 +681,7 @@ func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since,
|
|||
|
||||
allDeposits = append(allDeposits, types.Deposit{
|
||||
Exchange: types.ExchangeMax,
|
||||
Time: datatype.Time(time.Unix(d.CreatedAt, 0)),
|
||||
Time: types.Time(time.Unix(d.CreatedAt, 0)),
|
||||
Amount: util.MustParseFloat(d.Amount),
|
||||
Asset: toGlobalCurrency(d.Currency),
|
||||
Address: "", // not supported
|
||||
|
|
|
@ -174,7 +174,7 @@ func (e *BookEvent) Time() time.Time {
|
|||
return time.Unix(0, e.Timestamp*int64(time.Millisecond))
|
||||
}
|
||||
|
||||
func (e *BookEvent) OrderBook() (snapshot types.OrderBook, err error) {
|
||||
func (e *BookEvent) OrderBook() (snapshot types.SliceOrderBook, err error) {
|
||||
snapshot.Symbol = strings.ToUpper(e.Market)
|
||||
|
||||
for _, bid := range e.Bids {
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/datatype"
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
@ -112,7 +111,7 @@ func (reward Reward) Reward() (*types.Reward, error) {
|
|||
State: reward.State,
|
||||
Note: reward.Note,
|
||||
Spent: false,
|
||||
CreatedAt: datatype.Time(reward.CreatedAt),
|
||||
CreatedAt: types.Time(reward.CreatedAt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ import (
|
|||
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/datatype"
|
||||
max "github.com/c9s/bbgo/pkg/exchange/max/maxapi"
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
|
@ -226,7 +225,7 @@ func convertWebSocketTrade(t max.TradeUpdate) (*types.Trade, error) {
|
|||
Fee: fee,
|
||||
FeeCurrency: toGlobalCurrency(t.FeeCurrency),
|
||||
QuoteQuantity: quoteQuantity,
|
||||
Time: datatype.Time(mts),
|
||||
Time: types.Time(mts),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -257,6 +256,6 @@ func toGlobalOrderUpdate(u max.OrderUpdate) (*types.Order, error) {
|
|||
OrderID: u.ID,
|
||||
Status: toGlobalOrderStatus(u.State, executedVolume, remainingVolume),
|
||||
ExecutedQuantity: executedVolume.Float64(),
|
||||
CreationTime: datatype.Time(time.Unix(0, u.CreatedAtMs*int64(time.Millisecond))),
|
||||
CreationTime: types.Time(time.Unix(0, u.CreatedAtMs*int64(time.Millisecond))),
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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
83
pkg/indicator/stoch.go
Normal 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)
|
||||
}
|
15
pkg/indicator/stoch_callbacks.go
Normal file
15
pkg/indicator/stoch_callbacks.go
Normal 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)
|
||||
}
|
||||
}
|
67
pkg/indicator/stoch_test.go
Normal file
67
pkg/indicator/stoch_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/datatype"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
|
@ -24,7 +23,7 @@ func TestDepositService(t *testing.T) {
|
|||
|
||||
err = service.Insert(types.Deposit{
|
||||
Exchange: types.ExchangeMax,
|
||||
Time: datatype.Time(time.Now()),
|
||||
Time: types.Time(time.Now()),
|
||||
Amount: 0.001,
|
||||
Asset: "BTC",
|
||||
Address: "test",
|
||||
|
|
|
@ -8,7 +8,6 @@ import (
|
|||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/datatype"
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
@ -34,7 +33,7 @@ func TestRewardService_InsertAndQueryUnspent(t *testing.T) {
|
|||
Quantity: 1,
|
||||
State: "done",
|
||||
Spent: false,
|
||||
CreatedAt: datatype.Time(time.Now()),
|
||||
CreatedAt: types.Time(time.Now()),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
@ -52,7 +51,7 @@ func TestRewardService_InsertAndQueryUnspent(t *testing.T) {
|
|||
Quantity: 1000000,
|
||||
State: "done",
|
||||
Spent: false,
|
||||
CreatedAt: datatype.Time(time.Now()),
|
||||
CreatedAt: types.Time(time.Now()),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
@ -99,7 +98,7 @@ func TestRewardService_AggregateUnspentCurrencyPosition(t *testing.T) {
|
|||
Quantity: 1,
|
||||
State: "done",
|
||||
Spent: false,
|
||||
CreatedAt: datatype.Time(now),
|
||||
CreatedAt: types.Time(now),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
@ -111,7 +110,7 @@ func TestRewardService_AggregateUnspentCurrencyPosition(t *testing.T) {
|
|||
Quantity: 2,
|
||||
State: "done",
|
||||
Spent: false,
|
||||
CreatedAt: datatype.Time(now),
|
||||
CreatedAt: types.Time(now),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
@ -123,7 +122,7 @@ func TestRewardService_AggregateUnspentCurrencyPosition(t *testing.T) {
|
|||
Quantity: 1000000,
|
||||
State: "done",
|
||||
Spent: false,
|
||||
CreatedAt: datatype.Time(now),
|
||||
CreatedAt: types.Time(now),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/datatype"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
|
@ -30,7 +29,7 @@ func TestWithdrawService(t *testing.T) {
|
|||
TransactionID: "01",
|
||||
TransactionFee: 0.0001,
|
||||
Network: "omni",
|
||||
ApplyTime: datatype.Time(time.Now()),
|
||||
ApplyTime: types.Time(time.Now()),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
|
|
@ -234,8 +234,8 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se
|
|||
continue
|
||||
}
|
||||
|
||||
sourceBook := s.sourceBook.Get()
|
||||
book := s.tradingBook.Get()
|
||||
sourceBook := s.sourceBook.Copy()
|
||||
book := s.tradingBook.Copy()
|
||||
bestBid, hasBid := book.BestBid()
|
||||
bestAsk, hasAsk := book.BestAsk()
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,310 +0,0 @@
|
|||
package mirrormaker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/bbgo"
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
const ID = "mirrormaker"
|
||||
|
||||
var defaultMargin = fixedpoint.NewFromFloat(0.01)
|
||||
|
||||
var defaultQuantity = fixedpoint.NewFromFloat(0.001)
|
||||
|
||||
var log = logrus.WithField("strategy", ID)
|
||||
|
||||
func init() {
|
||||
bbgo.RegisterStrategy(ID, &Strategy{})
|
||||
}
|
||||
|
||||
type Strategy struct {
|
||||
*bbgo.Graceful
|
||||
*bbgo.Persistence
|
||||
|
||||
Symbol string `json:"symbol"`
|
||||
SourceExchange string `json:"sourceExchange"`
|
||||
MakerExchange string `json:"makerExchange"`
|
||||
|
||||
UpdateInterval time.Duration `json:"updateInterval"`
|
||||
Margin fixedpoint.Value `json:"margin"`
|
||||
BidMargin fixedpoint.Value `json:"bidMargin"`
|
||||
AskMargin fixedpoint.Value `json:"askMargin"`
|
||||
Quantity fixedpoint.Value `json:"quantity"`
|
||||
QuantityMultiplier fixedpoint.Value `json:"quantityMultiplier"`
|
||||
|
||||
NumLayers int `json:"numLayers"`
|
||||
Pips int `json:"pips"`
|
||||
|
||||
makerSession *bbgo.ExchangeSession
|
||||
sourceSession *bbgo.ExchangeSession
|
||||
|
||||
sourceMarket types.Market
|
||||
makerMarket types.Market
|
||||
|
||||
book *types.StreamOrderBook
|
||||
activeMakerOrders *bbgo.LocalActiveOrderBook
|
||||
|
||||
orderStore *bbgo.OrderStore
|
||||
|
||||
Position fixedpoint.Value
|
||||
lastPrice float64
|
||||
|
||||
stopC chan struct{}
|
||||
}
|
||||
|
||||
func (s *Strategy) ID() string {
|
||||
return ID
|
||||
}
|
||||
|
||||
func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) {
|
||||
sourceSession, ok := sessions[s.SourceExchange]
|
||||
if !ok {
|
||||
panic(fmt.Errorf("source exchange %s is not defined", s.SourceExchange))
|
||||
}
|
||||
|
||||
log.Infof("subscribing %s from %s", s.Symbol, s.SourceExchange)
|
||||
sourceSession.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{})
|
||||
}
|
||||
|
||||
func (s *Strategy) updateQuote(ctx context.Context) {
|
||||
if err := s.makerSession.Exchange.CancelOrders(ctx, s.activeMakerOrders.Orders()...); err != nil {
|
||||
log.WithError(err).Errorf("can not cancel orders")
|
||||
return
|
||||
}
|
||||
|
||||
// avoid unlock issue
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
sourceBook := s.book.Get()
|
||||
if len(sourceBook.Bids) == 0 || len(sourceBook.Asks) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
bestBidPrice := sourceBook.Bids[0].Price
|
||||
bestAskPrice := sourceBook.Asks[0].Price
|
||||
log.Infof("best bid price %f, best ask price: %f", bestBidPrice.Float64(), bestAskPrice.Float64())
|
||||
|
||||
bidQuantity := s.Quantity
|
||||
bidPrice := bestBidPrice.MulFloat64(1.0 - s.BidMargin.Float64())
|
||||
|
||||
askQuantity := s.Quantity
|
||||
askPrice := bestAskPrice.MulFloat64(1.0 + s.AskMargin.Float64())
|
||||
|
||||
log.Infof("quote bid price: %f ask price: %f", bidPrice.Float64(), askPrice.Float64())
|
||||
|
||||
var submitOrders []types.SubmitOrder
|
||||
|
||||
balances := s.makerSession.Account.Balances()
|
||||
makerQuota := &bbgo.QuotaTransaction{}
|
||||
if b, ok := balances[s.makerMarket.BaseCurrency]; ok {
|
||||
makerQuota.BaseAsset.Add(b.Available)
|
||||
}
|
||||
if b, ok := balances[s.makerMarket.QuoteCurrency]; ok {
|
||||
makerQuota.QuoteAsset.Add(b.Available)
|
||||
}
|
||||
|
||||
hedgeBalances := s.sourceSession.Account.Balances()
|
||||
hedgeQuota := &bbgo.QuotaTransaction{}
|
||||
if b, ok := hedgeBalances[s.sourceMarket.BaseCurrency]; ok {
|
||||
hedgeQuota.BaseAsset.Add(b.Available)
|
||||
}
|
||||
if b, ok := hedgeBalances[s.sourceMarket.QuoteCurrency]; ok {
|
||||
hedgeQuota.QuoteAsset.Add(b.Available)
|
||||
}
|
||||
|
||||
log.Infof("maker quota: %+v", makerQuota)
|
||||
log.Infof("hedge quota: %+v", hedgeQuota)
|
||||
|
||||
for i := 0; i < s.NumLayers; i++ {
|
||||
// bid orders
|
||||
if makerQuota.QuoteAsset.Lock(bidQuantity.Mul(bidPrice)) && hedgeQuota.BaseAsset.Lock(bidQuantity) {
|
||||
// if we bought, then we need to sell the base from the hedge session
|
||||
submitOrders = append(submitOrders, types.SubmitOrder{
|
||||
Symbol: s.Symbol,
|
||||
Type: types.OrderTypeLimit,
|
||||
Side: types.SideTypeBuy,
|
||||
Price: bidPrice.Float64(),
|
||||
Quantity: bidQuantity.Float64(),
|
||||
TimeInForce: "GTC",
|
||||
})
|
||||
|
||||
makerQuota.Commit()
|
||||
hedgeQuota.Commit()
|
||||
} else {
|
||||
makerQuota.Rollback()
|
||||
hedgeQuota.Rollback()
|
||||
}
|
||||
|
||||
// ask orders
|
||||
if makerQuota.BaseAsset.Lock(askQuantity) && hedgeQuota.QuoteAsset.Lock(askQuantity.Mul(askPrice)) {
|
||||
// if we bought, then we need to sell the base from the hedge session
|
||||
submitOrders = append(submitOrders, types.SubmitOrder{
|
||||
Symbol: s.Symbol,
|
||||
Type: types.OrderTypeLimit,
|
||||
Side: types.SideTypeSell,
|
||||
Price: askPrice.Float64(),
|
||||
Quantity: askQuantity.Float64(),
|
||||
TimeInForce: "GTC",
|
||||
})
|
||||
makerQuota.Commit()
|
||||
hedgeQuota.Commit()
|
||||
} else {
|
||||
makerQuota.Rollback()
|
||||
hedgeQuota.Rollback()
|
||||
}
|
||||
|
||||
bidPrice -= fixedpoint.NewFromFloat(s.makerMarket.TickSize * float64(s.Pips))
|
||||
askPrice += fixedpoint.NewFromFloat(s.makerMarket.TickSize * float64(s.Pips))
|
||||
|
||||
askQuantity = askQuantity.Mul(s.QuantityMultiplier)
|
||||
bidQuantity = bidQuantity.Mul(s.QuantityMultiplier)
|
||||
}
|
||||
|
||||
if len(submitOrders) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
makerOrderExecutor := &bbgo.ExchangeOrderExecutor{Session: s.makerSession}
|
||||
makerOrders, err := makerOrderExecutor.SubmitOrders(ctx, submitOrders...)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("order submit error")
|
||||
return
|
||||
}
|
||||
|
||||
s.activeMakerOrders.Add(makerOrders...)
|
||||
s.orderStore.Add(makerOrders...)
|
||||
}
|
||||
|
||||
func (s *Strategy) handleTradeUpdate(trade types.Trade) {
|
||||
log.Infof("received trade %+v", trade)
|
||||
if s.orderStore.Exists(trade.OrderID) {
|
||||
log.Infof("identified trade %d with an existing order: %d", trade.ID, trade.OrderID)
|
||||
|
||||
q := fixedpoint.NewFromFloat(trade.Quantity)
|
||||
if trade.Side == types.SideTypeSell {
|
||||
q = -q
|
||||
}
|
||||
|
||||
s.Position.AtomicAdd(q)
|
||||
|
||||
pos := s.Position.AtomicLoad()
|
||||
log.Warnf("position changed: %f", pos.Float64())
|
||||
|
||||
s.lastPrice = trade.Price
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, sessions map[string]*bbgo.ExchangeSession) error {
|
||||
if s.UpdateInterval == 0 {
|
||||
s.UpdateInterval = time.Second
|
||||
}
|
||||
|
||||
if s.NumLayers == 0 {
|
||||
s.NumLayers = 1
|
||||
}
|
||||
|
||||
if s.BidMargin == 0 {
|
||||
if s.Margin != 0 {
|
||||
s.BidMargin = s.Margin
|
||||
} else {
|
||||
s.BidMargin = defaultMargin
|
||||
}
|
||||
}
|
||||
|
||||
if s.AskMargin == 0 {
|
||||
if s.Margin != 0 {
|
||||
s.AskMargin = s.Margin
|
||||
} else {
|
||||
s.AskMargin = defaultMargin
|
||||
}
|
||||
}
|
||||
|
||||
if s.Quantity == 0 {
|
||||
s.Quantity = defaultQuantity
|
||||
}
|
||||
|
||||
sourceSession, ok := sessions[s.SourceExchange]
|
||||
if !ok {
|
||||
return fmt.Errorf("source exchange session %s is not defined", s.SourceExchange)
|
||||
}
|
||||
|
||||
s.sourceSession = sourceSession
|
||||
|
||||
makerSession, ok := sessions[s.MakerExchange]
|
||||
if !ok {
|
||||
return fmt.Errorf("maker exchange session %s is not defined", s.MakerExchange)
|
||||
}
|
||||
|
||||
s.makerSession = makerSession
|
||||
|
||||
s.sourceMarket, ok = s.sourceSession.Market(s.Symbol)
|
||||
if !ok {
|
||||
return fmt.Errorf("source session market %s is not defined", s.Symbol)
|
||||
}
|
||||
|
||||
s.makerMarket, ok = s.makerSession.Market(s.Symbol)
|
||||
if !ok {
|
||||
return fmt.Errorf("maker session market %s is not defined", s.Symbol)
|
||||
}
|
||||
|
||||
s.book = types.NewStreamBook(s.Symbol)
|
||||
s.book.BindStream(s.sourceSession.Stream)
|
||||
|
||||
s.makerSession.Stream.OnTradeUpdate(s.handleTradeUpdate)
|
||||
|
||||
s.activeMakerOrders = bbgo.NewLocalActiveOrderBook()
|
||||
s.activeMakerOrders.BindStream(s.makerSession.Stream)
|
||||
|
||||
s.orderStore = bbgo.NewOrderStore(s.Symbol)
|
||||
s.orderStore.BindStream(s.makerSession.Stream)
|
||||
|
||||
s.stopC = make(chan struct{})
|
||||
|
||||
if err := s.Persistence.Load(&s.Position, "position"); err != nil {
|
||||
log.WithError(err).Warnf("can not load position")
|
||||
} else {
|
||||
log.Infof("position is loaded successfully, position=%f", s.Position.Float64())
|
||||
}
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(s.UpdateInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
|
||||
case <-s.stopC:
|
||||
return
|
||||
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
case <-ticker.C:
|
||||
s.updateQuote(ctx)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
close(s.stopC)
|
||||
|
||||
if err := s.Persistence.Save(&s.Position, "position"); err != nil {
|
||||
log.WithError(err).Error("persistence save error")
|
||||
}
|
||||
|
||||
if err := s.makerSession.Exchange.CancelOrders(ctx, s.activeMakerOrders.Orders()...); err != nil {
|
||||
log.WithError(err).Errorf("can not cancel orders")
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -184,11 +184,7 @@ func (s *Strategy) updateQuote(ctx context.Context, orderExecutionRouter bbgo.Or
|
|||
return
|
||||
}
|
||||
|
||||
sourceBook := s.book.Get()
|
||||
if len(sourceBook.Bids) == 0 || len(sourceBook.Asks) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
sourceBook := s.book.Copy()
|
||||
if valid, err := sourceBook.IsValid(); !valid {
|
||||
log.WithError(err).Errorf("%s invalid order book, skip quoting: %v", s.Symbol, err)
|
||||
return
|
||||
|
@ -265,8 +261,11 @@ func (s *Strategy) updateQuote(ctx context.Context, orderExecutionRouter bbgo.Or
|
|||
return
|
||||
}
|
||||
|
||||
bestBidPrice := sourceBook.Bids[0].Price
|
||||
bestAskPrice := sourceBook.Asks[0].Price
|
||||
bestBid, _ := sourceBook.BestBid()
|
||||
bestBidPrice := bestBid.Price
|
||||
|
||||
bestAsk, _ := sourceBook.BestAsk()
|
||||
bestAskPrice := bestAsk.Price
|
||||
log.Infof("%s book ticker: best ask / best bid = %f / %f", s.Symbol, bestAskPrice.Float64(), bestBidPrice.Float64())
|
||||
|
||||
var submitOrders []types.SubmitOrder
|
||||
|
@ -335,7 +334,7 @@ func (s *Strategy) updateQuote(ctx context.Context, orderExecutionRouter bbgo.Or
|
|||
}
|
||||
|
||||
accumulativeBidQuantity += bidQuantity
|
||||
bidPrice := aggregatePrice(sourceBook.Bids, accumulativeBidQuantity)
|
||||
bidPrice := aggregatePrice(sourceBook.SideBook(types.SideTypeBuy), accumulativeBidQuantity)
|
||||
bidPrice = bidPrice.MulFloat64(1.0 - bidMargin.Float64())
|
||||
|
||||
if i > 0 && pips > 0 {
|
||||
|
@ -382,7 +381,7 @@ func (s *Strategy) updateQuote(ctx context.Context, orderExecutionRouter bbgo.Or
|
|||
}
|
||||
accumulativeAskQuantity += askQuantity
|
||||
|
||||
askPrice := aggregatePrice(sourceBook.Asks, accumulativeAskQuantity)
|
||||
askPrice := aggregatePrice(sourceBook.SideBook(types.SideTypeSell), accumulativeAskQuantity)
|
||||
askPrice = askPrice.MulFloat64(1.0 + askMargin.Float64())
|
||||
if i > 0 && pips > 0 {
|
||||
askPrice -= pips.MulFloat64(s.makerMarket.TickSize)
|
||||
|
@ -443,25 +442,20 @@ func (s *Strategy) Hedge(ctx context.Context, pos fixedpoint.Value) {
|
|||
}
|
||||
|
||||
lastPrice := s.lastPrice
|
||||
sourceBook := s.book.Get()
|
||||
sourceBook := s.book.Copy()
|
||||
switch side {
|
||||
|
||||
case types.SideTypeBuy:
|
||||
if len(sourceBook.Asks) > 0 {
|
||||
if pv, ok := sourceBook.Asks.First(); ok {
|
||||
lastPrice = pv.Price.Float64()
|
||||
}
|
||||
if bestAsk, ok := sourceBook.BestAsk(); ok {
|
||||
lastPrice = bestAsk.Price.Float64()
|
||||
}
|
||||
|
||||
case types.SideTypeSell:
|
||||
if len(sourceBook.Bids) > 0 {
|
||||
if pv, ok := sourceBook.Bids.First(); ok {
|
||||
lastPrice = pv.Price.Float64()
|
||||
if bestBid, ok := sourceBook.BestBid(); ok {
|
||||
lastPrice = bestBid.Price.Float64()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
notional := quantity.MulFloat64(lastPrice)
|
||||
if notional.Float64() <= s.sourceMarket.MinNotional {
|
||||
log.Warnf("%s %f less than min notional, skipping", s.Symbol, notional.Float64())
|
||||
|
@ -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 {
|
||||
if err := s.LoadState(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.state = &State{}
|
||||
} 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)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -110,7 +110,7 @@ func (s *Strategy) update(orderExecutor bbgo.OrderExecutor, session *bbgo.Exchan
|
|||
|
||||
func (s *Strategy) updateOrders(orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession, side types.SideType) {
|
||||
var book = s.book.Copy()
|
||||
var pvs = book.PriceVolumesBySide(side)
|
||||
var pvs = book.SideBook(side)
|
||||
if pvs == nil || len(pvs) == 0 {
|
||||
log.Warnf("empty side: %s", side)
|
||||
return
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
package types
|
||||
|
||||
const Green = "#228B22"
|
||||
const Red = "#800000"
|
||||
const GreenColor = "#228B22"
|
||||
const RedColor = "#800000"
|
||||
const GrayColor = "#f0f0f0"
|
||||
|
|
|
@ -2,8 +2,6 @@ package types
|
|||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/datatype"
|
||||
)
|
||||
|
||||
type DepositStatus string
|
||||
|
@ -26,7 +24,7 @@ const (
|
|||
type Deposit struct {
|
||||
GID int64 `json:"gid" db:"gid"`
|
||||
Exchange ExchangeName `json:"exchange" db:"exchange"`
|
||||
Time datatype.Time `json:"time" db:"time"`
|
||||
Time Time `json:"time" db:"time"`
|
||||
Amount float64 `json:"amount" db:"amount"`
|
||||
Asset string `json:"asset" db:"asset"`
|
||||
Address string `json:"address" db:"address"`
|
||||
|
|
55
pkg/types/float_slice.go
Normal file
55
pkg/types/float_slice.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -9,7 +9,6 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
"github.com/slack-go/slack"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/datatype"
|
||||
"github.com/c9s/bbgo/pkg/util"
|
||||
)
|
||||
|
||||
|
@ -151,8 +150,8 @@ type Order struct {
|
|||
Status OrderStatus `json:"status" db:"status"`
|
||||
ExecutedQuantity float64 `json:"executedQuantity" db:"executed_quantity"`
|
||||
IsWorking bool `json:"isWorking" db:"is_working"`
|
||||
CreationTime datatype.Time `json:"creationTime" db:"created_at"`
|
||||
UpdateTime datatype.Time `json:"updateTime" db:"updated_at"`
|
||||
CreationTime Time `json:"creationTime" db:"created_at"`
|
||||
UpdateTime Time `json:"updateTime" db:"updated_at"`
|
||||
|
||||
IsMargin bool `json:"isMargin" db:"is_margin"`
|
||||
IsIsolated bool `json:"isIsolated" db:"is_isolated"`
|
||||
|
|
|
@ -2,12 +2,10 @@ package types
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/sigchan"
|
||||
)
|
||||
|
@ -21,301 +19,43 @@ func (p PriceVolume) String() string {
|
|||
return fmt.Sprintf("PriceVolume{ price: %f, volume: %f }", p.Price.Float64(), p.Volume.Float64())
|
||||
}
|
||||
|
||||
type PriceVolumeSlice []PriceVolume
|
||||
|
||||
func (slice PriceVolumeSlice) Len() int { return len(slice) }
|
||||
func (slice PriceVolumeSlice) Less(i, j int) bool { return slice[i].Price < slice[j].Price }
|
||||
func (slice PriceVolumeSlice) Swap(i, j int) { slice[i], slice[j] = slice[j], slice[i] }
|
||||
|
||||
// Trim removes the pairs that volume = 0
|
||||
func (slice PriceVolumeSlice) Trim() (pvs PriceVolumeSlice) {
|
||||
for _, pv := range slice {
|
||||
if pv.Volume > 0 {
|
||||
pvs = append(pvs, pv)
|
||||
}
|
||||
}
|
||||
|
||||
return pvs
|
||||
}
|
||||
|
||||
func (slice PriceVolumeSlice) CopyDepth(depth int) PriceVolumeSlice {
|
||||
if depth > len(slice) {
|
||||
return slice.Copy()
|
||||
}
|
||||
|
||||
var s = make(PriceVolumeSlice, depth, depth)
|
||||
copy(s, slice[:depth])
|
||||
return s
|
||||
}
|
||||
|
||||
func (slice PriceVolumeSlice) Copy() PriceVolumeSlice {
|
||||
var s = make(PriceVolumeSlice, len(slice), len(slice))
|
||||
copy(s, slice)
|
||||
return s
|
||||
}
|
||||
|
||||
func (slice PriceVolumeSlice) Second() (PriceVolume, bool) {
|
||||
if len(slice) > 1 {
|
||||
return slice[1], true
|
||||
}
|
||||
return PriceVolume{}, false
|
||||
}
|
||||
|
||||
func (slice PriceVolumeSlice) First() (PriceVolume, bool) {
|
||||
if len(slice) > 0 {
|
||||
return slice[0], true
|
||||
}
|
||||
return PriceVolume{}, false
|
||||
}
|
||||
|
||||
func (slice PriceVolumeSlice) IndexByVolumeDepth(requiredVolume fixedpoint.Value) int {
|
||||
var tv int64 = 0
|
||||
for x, el := range slice {
|
||||
tv += el.Volume.Int64()
|
||||
if tv >= requiredVolume.Int64() {
|
||||
return x
|
||||
}
|
||||
}
|
||||
|
||||
// not deep enough
|
||||
return -1
|
||||
}
|
||||
|
||||
func (slice PriceVolumeSlice) InsertAt(idx int, pv PriceVolume) PriceVolumeSlice {
|
||||
rear := append([]PriceVolume{}, slice[idx:]...)
|
||||
newSlice := append(slice[:idx], pv)
|
||||
return append(newSlice, rear...)
|
||||
}
|
||||
|
||||
func (slice PriceVolumeSlice) Remove(price fixedpoint.Value, descending bool) PriceVolumeSlice {
|
||||
matched, idx := slice.Find(price, descending)
|
||||
if matched.Price != price {
|
||||
return slice
|
||||
}
|
||||
|
||||
return append(slice[:idx], slice[idx+1:]...)
|
||||
}
|
||||
|
||||
// FindPriceVolumePair finds the pair by the given price, this function is a read-only
|
||||
// operation, so we use the value receiver to avoid copy value from the pointer
|
||||
// If the price is not found, it will return the index where the price can be inserted at.
|
||||
// true for descending (bid orders), false for ascending (ask orders)
|
||||
func (slice PriceVolumeSlice) Find(price fixedpoint.Value, descending bool) (pv PriceVolume, idx int) {
|
||||
idx = sort.Search(len(slice), func(i int) bool {
|
||||
if descending {
|
||||
return slice[i].Price <= price
|
||||
}
|
||||
return slice[i].Price >= price
|
||||
})
|
||||
|
||||
if idx >= len(slice) || slice[idx].Price != price {
|
||||
return pv, idx
|
||||
}
|
||||
|
||||
pv = slice[idx]
|
||||
|
||||
return pv, idx
|
||||
}
|
||||
|
||||
func (slice PriceVolumeSlice) Upsert(pv PriceVolume, descending bool) PriceVolumeSlice {
|
||||
if len(slice) == 0 {
|
||||
return append(slice, pv)
|
||||
}
|
||||
|
||||
price := pv.Price
|
||||
_, idx := slice.Find(price, descending)
|
||||
if idx >= len(slice) || slice[idx].Price != price {
|
||||
return slice.InsertAt(idx, pv)
|
||||
}
|
||||
|
||||
slice[idx].Volume = pv.Volume
|
||||
return slice
|
||||
}
|
||||
|
||||
//go:generate callbackgen -type OrderBook
|
||||
type OrderBook struct {
|
||||
Symbol string
|
||||
Bids PriceVolumeSlice
|
||||
Asks PriceVolumeSlice
|
||||
|
||||
loadCallbacks []func(book *OrderBook)
|
||||
updateCallbacks []func(book *OrderBook)
|
||||
bidsChangeCallbacks []func(pvs PriceVolumeSlice)
|
||||
asksChangeCallbacks []func(pvs PriceVolumeSlice)
|
||||
}
|
||||
|
||||
func (b *OrderBook) Spread() (fixedpoint.Value, bool) {
|
||||
bestBid, ok := b.BestBid()
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
bestAsk, ok := b.BestAsk()
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return bestAsk.Price - bestBid.Price, true
|
||||
}
|
||||
|
||||
func (b *OrderBook) BestBid() (PriceVolume, bool) {
|
||||
if len(b.Bids) == 0 {
|
||||
return PriceVolume{}, false
|
||||
}
|
||||
|
||||
return b.Bids[0], true
|
||||
}
|
||||
|
||||
func (b *OrderBook) BestAsk() (PriceVolume, bool) {
|
||||
if len(b.Asks) == 0 {
|
||||
return PriceVolume{}, false
|
||||
}
|
||||
|
||||
return b.Asks[0], true
|
||||
}
|
||||
|
||||
func (b *OrderBook) IsValid() (bool, error) {
|
||||
bid, hasBid := b.BestBid()
|
||||
ask, hasAsk := b.BestAsk()
|
||||
|
||||
if !hasBid {
|
||||
return false, errors.New("empty bids")
|
||||
}
|
||||
|
||||
if !hasAsk {
|
||||
return false, errors.New("empty asks")
|
||||
}
|
||||
|
||||
if bid.Price > ask.Price {
|
||||
return false, fmt.Errorf("bid price %f > ask price %f", bid.Price.Float64(), ask.Price.Float64())
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (b *OrderBook) PriceVolumesBySide(side SideType) PriceVolumeSlice {
|
||||
switch side {
|
||||
|
||||
case SideTypeBuy:
|
||||
return b.Bids
|
||||
|
||||
case SideTypeSell:
|
||||
return b.Asks
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *OrderBook) CopyDepth(depth int) (book OrderBook) {
|
||||
book = *b
|
||||
book.Bids = book.Bids.CopyDepth(depth)
|
||||
book.Asks = book.Asks.CopyDepth(depth)
|
||||
return book
|
||||
}
|
||||
|
||||
func (b *OrderBook) Copy() (book OrderBook) {
|
||||
book = *b
|
||||
book.Bids = book.Bids.Copy()
|
||||
book.Asks = book.Asks.Copy()
|
||||
return book
|
||||
}
|
||||
|
||||
func (b *OrderBook) updateAsks(pvs PriceVolumeSlice) {
|
||||
for _, pv := range pvs {
|
||||
if pv.Volume == 0 {
|
||||
b.Asks = b.Asks.Remove(pv.Price, false)
|
||||
} else {
|
||||
b.Asks = b.Asks.Upsert(pv, false)
|
||||
}
|
||||
}
|
||||
|
||||
b.EmitAsksChange(b.Asks)
|
||||
}
|
||||
|
||||
func (b *OrderBook) updateBids(pvs PriceVolumeSlice) {
|
||||
for _, pv := range pvs {
|
||||
if pv.Volume == 0 {
|
||||
b.Bids = b.Bids.Remove(pv.Price, true)
|
||||
} else {
|
||||
b.Bids = b.Bids.Upsert(pv, true)
|
||||
}
|
||||
}
|
||||
|
||||
b.EmitBidsChange(b.Bids)
|
||||
}
|
||||
|
||||
func (b *OrderBook) update(book OrderBook) {
|
||||
b.updateBids(book.Bids)
|
||||
b.updateAsks(book.Asks)
|
||||
}
|
||||
|
||||
func (b *OrderBook) Reset() {
|
||||
b.Bids = nil
|
||||
b.Asks = nil
|
||||
}
|
||||
|
||||
func (b *OrderBook) Load(book OrderBook) {
|
||||
b.Reset()
|
||||
b.update(book)
|
||||
b.EmitLoad(b)
|
||||
}
|
||||
|
||||
func (b *OrderBook) Update(book OrderBook) {
|
||||
b.update(book)
|
||||
b.EmitUpdate(b)
|
||||
}
|
||||
|
||||
func (b *OrderBook) Print() {
|
||||
fmt.Printf(b.String())
|
||||
}
|
||||
|
||||
func (b *OrderBook) String() string {
|
||||
sb := strings.Builder{}
|
||||
|
||||
sb.WriteString("BOOK ")
|
||||
sb.WriteString(b.Symbol)
|
||||
sb.WriteString("\n")
|
||||
|
||||
if len(b.Asks) > 0 {
|
||||
sb.WriteString("ASKS:\n")
|
||||
for i := len(b.Asks) - 1; i >= 0; i-- {
|
||||
sb.WriteString("- ASK: ")
|
||||
sb.WriteString(b.Asks[i].String())
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.Bids) > 0 {
|
||||
sb.WriteString("BIDS:\n")
|
||||
for _, bid := range b.Bids {
|
||||
sb.WriteString("- BID: ")
|
||||
sb.WriteString(bid.String())
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
type OrderBook interface {
|
||||
Spread() (fixedpoint.Value, bool)
|
||||
BestAsk() (PriceVolume, bool)
|
||||
BestBid() (PriceVolume, bool)
|
||||
Reset()
|
||||
Load(book SliceOrderBook)
|
||||
Update(book SliceOrderBook)
|
||||
Copy() OrderBook
|
||||
CopyDepth(depth int) OrderBook
|
||||
SideBook(sideType SideType) PriceVolumeSlice
|
||||
IsValid() (bool, error)
|
||||
}
|
||||
|
||||
type MutexOrderBook struct {
|
||||
sync.Mutex
|
||||
|
||||
*OrderBook
|
||||
Symbol string
|
||||
OrderBook OrderBook
|
||||
}
|
||||
|
||||
func NewMutexOrderBook(symbol string) *MutexOrderBook {
|
||||
var book OrderBook = NewSliceOrderBook(symbol)
|
||||
|
||||
if v, _ := strconv.ParseBool(os.Getenv("ENABLE_RBT_ORDERBOOK")); v {
|
||||
book = NewRBOrderBook(symbol)
|
||||
}
|
||||
|
||||
return &MutexOrderBook{
|
||||
OrderBook: &OrderBook{Symbol: symbol},
|
||||
Symbol: symbol,
|
||||
OrderBook: book,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *MutexOrderBook) Load(book OrderBook) {
|
||||
func (b *MutexOrderBook) Load(book SliceOrderBook) {
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
|
||||
b.OrderBook.Reset()
|
||||
b.OrderBook.update(book)
|
||||
b.EmitLoad(b.OrderBook)
|
||||
b.OrderBook.Load(book)
|
||||
b.Unlock()
|
||||
}
|
||||
|
||||
func (b *MutexOrderBook) Reset() {
|
||||
|
@ -330,18 +70,16 @@ func (b *MutexOrderBook) CopyDepth(depth int) OrderBook {
|
|||
return b.OrderBook.CopyDepth(depth)
|
||||
}
|
||||
|
||||
func (b *MutexOrderBook) Get() OrderBook {
|
||||
func (b *MutexOrderBook) Copy() OrderBook {
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
return b.OrderBook.Copy()
|
||||
}
|
||||
|
||||
func (b *MutexOrderBook) Update(update OrderBook) {
|
||||
func (b *MutexOrderBook) Update(update SliceOrderBook) {
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
|
||||
b.OrderBook.update(update)
|
||||
b.EmitUpdate(b.OrderBook)
|
||||
b.OrderBook.Update(update)
|
||||
b.Unlock()
|
||||
}
|
||||
|
||||
// StreamOrderBook receives streaming data from websocket connection and
|
||||
|
@ -360,8 +98,8 @@ func NewStreamBook(symbol string) *StreamOrderBook {
|
|||
}
|
||||
|
||||
func (sb *StreamOrderBook) BindStream(stream Stream) {
|
||||
stream.OnBookSnapshot(func(book OrderBook) {
|
||||
if sb.Symbol != book.Symbol {
|
||||
stream.OnBookSnapshot(func(book SliceOrderBook) {
|
||||
if sb.MutexOrderBook.Symbol != book.Symbol {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -369,8 +107,8 @@ func (sb *StreamOrderBook) BindStream(stream Stream) {
|
|||
sb.C.Emit()
|
||||
})
|
||||
|
||||
stream.OnBookUpdate(func(book OrderBook) {
|
||||
if sb.Symbol != book.Symbol {
|
||||
stream.OnBookUpdate(func(book SliceOrderBook) {
|
||||
if sb.MutexOrderBook.Symbol != book.Symbol {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
// Code generated by "callbackgen -type OrderBook"; DO NOT EDIT.
|
||||
|
||||
package types
|
||||
|
||||
func (b *OrderBook) OnLoad(cb func(book *OrderBook)) {
|
||||
b.loadCallbacks = append(b.loadCallbacks, cb)
|
||||
}
|
||||
|
||||
func (b *OrderBook) EmitLoad(book *OrderBook) {
|
||||
for _, cb := range b.loadCallbacks {
|
||||
cb(book)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *OrderBook) OnUpdate(cb func(book *OrderBook)) {
|
||||
b.updateCallbacks = append(b.updateCallbacks, cb)
|
||||
}
|
||||
|
||||
func (b *OrderBook) EmitUpdate(book *OrderBook) {
|
||||
for _, cb := range b.updateCallbacks {
|
||||
cb(book)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *OrderBook) OnBidsChange(cb func(pvs PriceVolumeSlice)) {
|
||||
b.bidsChangeCallbacks = append(b.bidsChangeCallbacks, cb)
|
||||
}
|
||||
|
||||
func (b *OrderBook) EmitBidsChange(pvs PriceVolumeSlice) {
|
||||
for _, cb := range b.bidsChangeCallbacks {
|
||||
cb(pvs)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *OrderBook) OnAsksChange(cb func(pvs PriceVolumeSlice)) {
|
||||
b.asksChangeCallbacks = append(b.asksChangeCallbacks, cb)
|
||||
}
|
||||
|
||||
func (b *OrderBook) EmitAsksChange(pvs PriceVolumeSlice) {
|
||||
for _, cb := range b.asksChangeCallbacks {
|
||||
cb(pvs)
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -8,8 +9,95 @@ import (
|
|||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
)
|
||||
|
||||
func prepareOrderBookBenchmarkData() (asks, bids PriceVolumeSlice) {
|
||||
for p := 0.0; p < 1000.0; p++ {
|
||||
asks = append(asks, PriceVolume{fixedpoint.NewFromFloat(1000 + p), fixedpoint.NewFromFloat(1)})
|
||||
bids = append(bids, PriceVolume{fixedpoint.NewFromFloat(1000 - 0.1 - p), fixedpoint.NewFromFloat(1)})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func BenchmarkOrderBook_Load(b *testing.B) {
|
||||
var asks, bids = prepareOrderBookBenchmarkData()
|
||||
for p := 0.0; p < 1000.0; p++ {
|
||||
asks = append(asks, PriceVolume{fixedpoint.NewFromFloat(1000 + p), fixedpoint.NewFromFloat(1)})
|
||||
bids = append(bids, PriceVolume{fixedpoint.NewFromFloat(1000 - 0.1 - p), fixedpoint.NewFromFloat(1)})
|
||||
}
|
||||
|
||||
b.Run("RBTOrderBook", func(b *testing.B) {
|
||||
book := NewRBOrderBook("ETHUSDT")
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, ask := range asks {
|
||||
book.Asks.Upsert(ask.Price, ask.Volume)
|
||||
}
|
||||
for _, bid := range bids {
|
||||
book.Bids.Upsert(bid.Price, bid.Volume)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("OrderBook", func(b *testing.B) {
|
||||
book := &SliceOrderBook{}
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, ask := range asks {
|
||||
book.Asks = book.Asks.Upsert(ask, false)
|
||||
}
|
||||
for _, bid := range bids {
|
||||
book.Bids = book.Bids.Upsert(bid, true)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkOrderBook_UpdateAndInsert(b *testing.B) {
|
||||
var asks, bids = prepareOrderBookBenchmarkData()
|
||||
for p := 0.0; p < 1000.0; p += 2 {
|
||||
asks = append(asks, PriceVolume{fixedpoint.NewFromFloat(1000 + p), fixedpoint.NewFromFloat(1)})
|
||||
bids = append(bids, PriceVolume{fixedpoint.NewFromFloat(1000 - 0.1 - p), fixedpoint.NewFromFloat(1)})
|
||||
}
|
||||
|
||||
rbBook := NewRBOrderBook("ETHUSDT")
|
||||
for _, ask := range asks {
|
||||
rbBook.Asks.Upsert(ask.Price, ask.Volume)
|
||||
}
|
||||
for _, bid := range bids {
|
||||
rbBook.Bids.Upsert(bid.Price, bid.Volume)
|
||||
}
|
||||
|
||||
b.Run("RBTOrderBook", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
var price = fixedpoint.NewFromFloat(rand.Float64() * 2000.0)
|
||||
if price >= fixedpoint.NewFromFloat(1000) {
|
||||
rbBook.Asks.Upsert(price, fixedpoint.NewFromFloat(1))
|
||||
} else {
|
||||
rbBook.Bids.Upsert(price, fixedpoint.NewFromFloat(1))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
sliceBook := &SliceOrderBook{}
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, ask := range asks {
|
||||
sliceBook.Asks = sliceBook.Asks.Upsert(ask, false)
|
||||
}
|
||||
for _, bid := range bids {
|
||||
sliceBook.Bids = sliceBook.Bids.Upsert(bid, true)
|
||||
}
|
||||
}
|
||||
b.Run("OrderBook", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
var price = fixedpoint.NewFromFloat(rand.Float64() * 2000.0)
|
||||
if price >= fixedpoint.NewFromFloat(1000) {
|
||||
sliceBook.Asks = sliceBook.Asks.Upsert(PriceVolume{Price: price, Volume: fixedpoint.NewFromFloat(1)}, false)
|
||||
} else {
|
||||
sliceBook.Bids = sliceBook.Bids.Upsert(PriceVolume{Price: price, Volume: fixedpoint.NewFromFloat(1)}, true)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestOrderBook_IsValid(t *testing.T) {
|
||||
ob := OrderBook{
|
||||
ob := SliceOrderBook{
|
||||
Bids: PriceVolumeSlice{
|
||||
{fixedpoint.NewFromFloat(100.0), fixedpoint.NewFromFloat(1.5)},
|
||||
{fixedpoint.NewFromFloat(90.0), fixedpoint.NewFromFloat(2.5)},
|
||||
|
|
118
pkg/types/price_volume_slice.go
Normal file
118
pkg/types/price_volume_slice.go
Normal file
|
@ -0,0 +1,118 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
)
|
||||
|
||||
type PriceVolumeSlice []PriceVolume
|
||||
|
||||
func (slice PriceVolumeSlice) Len() int { return len(slice) }
|
||||
func (slice PriceVolumeSlice) Less(i, j int) bool { return slice[i].Price < slice[j].Price }
|
||||
func (slice PriceVolumeSlice) Swap(i, j int) { slice[i], slice[j] = slice[j], slice[i] }
|
||||
|
||||
// Trim removes the pairs that volume = 0
|
||||
func (slice PriceVolumeSlice) Trim() (pvs PriceVolumeSlice) {
|
||||
for _, pv := range slice {
|
||||
if pv.Volume > 0 {
|
||||
pvs = append(pvs, pv)
|
||||
}
|
||||
}
|
||||
|
||||
return pvs
|
||||
}
|
||||
|
||||
func (slice PriceVolumeSlice) CopyDepth(depth int) PriceVolumeSlice {
|
||||
if depth > len(slice) {
|
||||
return slice.Copy()
|
||||
}
|
||||
|
||||
var s = make(PriceVolumeSlice, depth, depth)
|
||||
copy(s, slice[:depth])
|
||||
return s
|
||||
}
|
||||
|
||||
func (slice PriceVolumeSlice) Copy() PriceVolumeSlice {
|
||||
var s = make(PriceVolumeSlice, len(slice), len(slice))
|
||||
copy(s, slice)
|
||||
return s
|
||||
}
|
||||
|
||||
func (slice PriceVolumeSlice) Second() (PriceVolume, bool) {
|
||||
if len(slice) > 1 {
|
||||
return slice[1], true
|
||||
}
|
||||
return PriceVolume{}, false
|
||||
}
|
||||
|
||||
func (slice PriceVolumeSlice) First() (PriceVolume, bool) {
|
||||
if len(slice) > 0 {
|
||||
return slice[0], true
|
||||
}
|
||||
return PriceVolume{}, false
|
||||
}
|
||||
|
||||
func (slice PriceVolumeSlice) IndexByVolumeDepth(requiredVolume fixedpoint.Value) int {
|
||||
var tv int64 = 0
|
||||
for x, el := range slice {
|
||||
tv += el.Volume.Int64()
|
||||
if tv >= requiredVolume.Int64() {
|
||||
return x
|
||||
}
|
||||
}
|
||||
|
||||
// not deep enough
|
||||
return -1
|
||||
}
|
||||
|
||||
func (slice PriceVolumeSlice) InsertAt(idx int, pv PriceVolume) PriceVolumeSlice {
|
||||
rear := append([]PriceVolume{}, slice[idx:]...)
|
||||
newSlice := append(slice[:idx], pv)
|
||||
return append(newSlice, rear...)
|
||||
}
|
||||
|
||||
func (slice PriceVolumeSlice) Remove(price fixedpoint.Value, descending bool) PriceVolumeSlice {
|
||||
matched, idx := slice.Find(price, descending)
|
||||
if matched.Price != price {
|
||||
return slice
|
||||
}
|
||||
|
||||
return append(slice[:idx], slice[idx+1:]...)
|
||||
}
|
||||
|
||||
// Find finds the pair by the given price, this function is a read-only
|
||||
// operation, so we use the value receiver to avoid copy value from the pointer
|
||||
// If the price is not found, it will return the index where the price can be inserted at.
|
||||
// true for descending (bid orders), false for ascending (ask orders)
|
||||
func (slice PriceVolumeSlice) Find(price fixedpoint.Value, descending bool) (pv PriceVolume, idx int) {
|
||||
idx = sort.Search(len(slice), func(i int) bool {
|
||||
if descending {
|
||||
return slice[i].Price <= price
|
||||
}
|
||||
return slice[i].Price >= price
|
||||
})
|
||||
|
||||
if idx >= len(slice) || slice[idx].Price != price {
|
||||
return pv, idx
|
||||
}
|
||||
|
||||
pv = slice[idx]
|
||||
|
||||
return pv, idx
|
||||
}
|
||||
|
||||
func (slice PriceVolumeSlice) Upsert(pv PriceVolume, descending bool) PriceVolumeSlice {
|
||||
if len(slice) == 0 {
|
||||
return append(slice, pv)
|
||||
}
|
||||
|
||||
price := pv.Price
|
||||
_, idx := slice.Find(price, descending)
|
||||
if idx >= len(slice) || slice[idx].Price != price {
|
||||
return slice.InsertAt(idx, pv)
|
||||
}
|
||||
|
||||
slice[idx].Volume = pv.Volume
|
||||
return slice
|
||||
}
|
189
pkg/types/rbtorderbook.go
Normal file
189
pkg/types/rbtorderbook.go
Normal 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
|
||||
})
|
||||
}
|
25
pkg/types/rbtorderbook_callbacks.go
Normal file
25
pkg/types/rbtorderbook_callbacks.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
// Code generated by "callbackgen -type RBTOrderBook"; DO NOT EDIT.
|
||||
|
||||
package types
|
||||
|
||||
import ()
|
||||
|
||||
func (b *RBTOrderBook) OnLoad(cb func(book *RBTOrderBook)) {
|
||||
b.loadCallbacks = append(b.loadCallbacks, cb)
|
||||
}
|
||||
|
||||
func (b *RBTOrderBook) EmitLoad(book *RBTOrderBook) {
|
||||
for _, cb := range b.loadCallbacks {
|
||||
cb(book)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *RBTOrderBook) OnUpdate(cb func(book *RBTOrderBook)) {
|
||||
b.updateCallbacks = append(b.updateCallbacks, cb)
|
||||
}
|
||||
|
||||
func (b *RBTOrderBook) EmitUpdate(book *RBTOrderBook) {
|
||||
for _, cb := range b.updateCallbacks {
|
||||
cb(book)
|
||||
}
|
||||
}
|
497
pkg/types/rbtree.go
Normal file
497
pkg/types/rbtree.go
Normal 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
22
pkg/types/rbtree_node.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package types
|
||||
|
||||
import "github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
|
||||
// Color is the RB Tree color
|
||||
type Color bool
|
||||
|
||||
const (
|
||||
Red = Color(false)
|
||||
Black = Color(true)
|
||||
)
|
||||
|
||||
/*
|
||||
RBNode
|
||||
A red node always has black children.
|
||||
A black node may have red or black children
|
||||
*/
|
||||
type RBNode struct {
|
||||
Left, Right, Parent *RBNode
|
||||
Color Color
|
||||
Key, Value fixedpoint.Value
|
||||
}
|
87
pkg/types/rbtree_test.go
Normal file
87
pkg/types/rbtree_test.go
Normal file
|
@ -0,0 +1,87 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTree_CopyInorder(t *testing.T) {
|
||||
tree := NewRBTree()
|
||||
for i := 1.0; i < 10.0; i += 1.0 {
|
||||
tree.Insert(fixedpoint.NewFromFloat(i*100.0), fixedpoint.NewFromFloat(i))
|
||||
}
|
||||
|
||||
newTree := tree.CopyInorder(3)
|
||||
assert.Equal(t, 3, newTree.Size())
|
||||
|
||||
newTree.Print()
|
||||
|
||||
node1 := newTree.Search(fixedpoint.NewFromFloat(100.0))
|
||||
assert.NotNil(t, node1)
|
||||
|
||||
node2 := newTree.Search(fixedpoint.NewFromFloat(200.0))
|
||||
assert.NotNil(t, node2)
|
||||
|
||||
node3 := newTree.Search(fixedpoint.NewFromFloat(300.0))
|
||||
assert.NotNil(t, node3)
|
||||
|
||||
node4 := newTree.Search(fixedpoint.NewFromFloat(400.0))
|
||||
assert.Nil(t, node4)
|
||||
}
|
||||
|
||||
func TestTree_Copy(t *testing.T) {
|
||||
tree := NewRBTree()
|
||||
tree.Insert(fixedpoint.NewFromFloat(3000.0), fixedpoint.NewFromFloat(1.0))
|
||||
assert.NotNil(t, tree.Root)
|
||||
|
||||
tree.Insert(fixedpoint.NewFromFloat(4000.0), fixedpoint.NewFromFloat(2.0))
|
||||
tree.Insert(fixedpoint.NewFromFloat(2000.0), fixedpoint.NewFromFloat(3.0))
|
||||
|
||||
newTree := tree.Copy()
|
||||
node1 := newTree.Search(fixedpoint.NewFromFloat(2000.0))
|
||||
assert.NotNil(t, node1)
|
||||
assert.Equal(t, fixedpoint.NewFromFloat(2000.0), node1.Key)
|
||||
assert.Equal(t, fixedpoint.NewFromFloat(3.0), node1.Value)
|
||||
|
||||
node2 := newTree.Search(fixedpoint.NewFromFloat(3000.0))
|
||||
assert.NotNil(t, node2)
|
||||
assert.Equal(t, fixedpoint.NewFromFloat(3000.0), node2.Key)
|
||||
assert.Equal(t, fixedpoint.NewFromFloat(1.0), node2.Value)
|
||||
|
||||
node3 := newTree.Search(fixedpoint.NewFromFloat(4000.0))
|
||||
assert.NotNil(t, node3)
|
||||
assert.Equal(t, fixedpoint.NewFromFloat(4000.0), node3.Key)
|
||||
assert.Equal(t, fixedpoint.NewFromFloat(2.0), node3.Value)
|
||||
}
|
||||
|
||||
func TestTree(t *testing.T) {
|
||||
tree := NewRBTree()
|
||||
tree.Insert(fixedpoint.NewFromFloat(3000.0), fixedpoint.NewFromFloat(10.0))
|
||||
assert.NotNil(t, tree.Root)
|
||||
|
||||
tree.Insert(fixedpoint.NewFromFloat(4000.0), fixedpoint.NewFromFloat(10.0))
|
||||
tree.Insert(fixedpoint.NewFromFloat(2000.0), fixedpoint.NewFromFloat(10.0))
|
||||
|
||||
// root is always black
|
||||
assert.Equal(t, fixedpoint.NewFromFloat(3000.0), tree.Root.Key)
|
||||
assert.Equal(t, Black, tree.Root.Color)
|
||||
|
||||
assert.Equal(t, fixedpoint.NewFromFloat(2000.0), tree.Root.Left.Key)
|
||||
assert.Equal(t, Red, tree.Root.Left.Color)
|
||||
|
||||
assert.Equal(t, fixedpoint.NewFromFloat(4000.0), tree.Root.Right.Key)
|
||||
assert.Equal(t, Red, tree.Root.Right.Color)
|
||||
|
||||
// should rotate
|
||||
tree.Insert(fixedpoint.NewFromFloat(1500.0), fixedpoint.NewFromFloat(10.0))
|
||||
tree.Insert(fixedpoint.NewFromFloat(1000.0), fixedpoint.NewFromFloat(10.0))
|
||||
|
||||
deleted := tree.Delete(fixedpoint.NewFromFloat(1000.0))
|
||||
assert.True(t, deleted)
|
||||
|
||||
deleted = tree.Delete(fixedpoint.NewFromFloat(1500.0))
|
||||
assert.True(t, deleted)
|
||||
|
||||
}
|
|
@ -3,7 +3,6 @@ package types
|
|||
import (
|
||||
"time"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/datatype"
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
)
|
||||
|
||||
|
@ -30,7 +29,7 @@ type Reward struct {
|
|||
Spent bool `json:"spent" db:"spent"`
|
||||
|
||||
// Unix timestamp in seconds
|
||||
CreatedAt datatype.Time `json:"created_at" db:"created_at"`
|
||||
CreatedAt Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
type RewardSlice []Reward
|
||||
|
|
|
@ -74,14 +74,14 @@ func (side SideType) String() string {
|
|||
|
||||
func (side SideType) Color() string {
|
||||
if side == SideTypeBuy {
|
||||
return Green
|
||||
return GreenColor
|
||||
}
|
||||
|
||||
if side == SideTypeSell {
|
||||
return Red
|
||||
return RedColor
|
||||
}
|
||||
|
||||
return "#f0f0f0"
|
||||
return GrayColor
|
||||
}
|
||||
|
||||
func SideToColorName(side SideType) string {
|
||||
|
|
196
pkg/types/sliceorderbook.go
Normal file
196
pkg/types/sliceorderbook.go
Normal file
|
@ -0,0 +1,196 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// SliceOrderBook is a general order book structure which could be used
|
||||
// for RESTful responses and websocket stream parsing
|
||||
//go:generate callbackgen -type SliceOrderBook
|
||||
type SliceOrderBook struct {
|
||||
Symbol string
|
||||
Bids PriceVolumeSlice
|
||||
Asks PriceVolumeSlice
|
||||
|
||||
loadCallbacks []func(book *SliceOrderBook)
|
||||
updateCallbacks []func(book *SliceOrderBook)
|
||||
}
|
||||
|
||||
func NewSliceOrderBook(symbol string) *SliceOrderBook {
|
||||
return &SliceOrderBook{
|
||||
Symbol: symbol,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *SliceOrderBook) Spread() (fixedpoint.Value, bool) {
|
||||
bestBid, ok := b.BestBid()
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
bestAsk, ok := b.BestAsk()
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return bestAsk.Price - bestBid.Price, true
|
||||
}
|
||||
|
||||
func (b *SliceOrderBook) BestBid() (PriceVolume, bool) {
|
||||
if len(b.Bids) == 0 {
|
||||
return PriceVolume{}, false
|
||||
}
|
||||
|
||||
return b.Bids[0], true
|
||||
}
|
||||
|
||||
func (b *SliceOrderBook) BestAsk() (PriceVolume, bool) {
|
||||
if len(b.Asks) == 0 {
|
||||
return PriceVolume{}, false
|
||||
}
|
||||
|
||||
return b.Asks[0], true
|
||||
}
|
||||
|
||||
func (b *SliceOrderBook) SideBook(sideType SideType) PriceVolumeSlice {
|
||||
switch sideType {
|
||||
|
||||
case SideTypeBuy:
|
||||
return b.Bids
|
||||
|
||||
case SideTypeSell:
|
||||
return b.Asks
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (b *SliceOrderBook) IsValid() (bool, error) {
|
||||
bid, hasBid := b.BestBid()
|
||||
ask, hasAsk := b.BestAsk()
|
||||
|
||||
if !hasBid {
|
||||
return false, errors.New("empty bids")
|
||||
}
|
||||
|
||||
if !hasAsk {
|
||||
return false, errors.New("empty asks")
|
||||
}
|
||||
|
||||
if bid.Price > ask.Price {
|
||||
return false, fmt.Errorf("bid price %f > ask price %f", bid.Price.Float64(), ask.Price.Float64())
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (b *SliceOrderBook) PriceVolumesBySide(side SideType) PriceVolumeSlice {
|
||||
switch side {
|
||||
|
||||
case SideTypeBuy:
|
||||
return b.Bids
|
||||
|
||||
case SideTypeSell:
|
||||
return b.Asks
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *SliceOrderBook) updateAsks(pvs PriceVolumeSlice) {
|
||||
for _, pv := range pvs {
|
||||
if pv.Volume == 0 {
|
||||
b.Asks = b.Asks.Remove(pv.Price, false)
|
||||
} else {
|
||||
b.Asks = b.Asks.Upsert(pv, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *SliceOrderBook) updateBids(pvs PriceVolumeSlice) {
|
||||
for _, pv := range pvs {
|
||||
if pv.Volume == 0 {
|
||||
b.Bids = b.Bids.Remove(pv.Price, true)
|
||||
} else {
|
||||
b.Bids = b.Bids.Upsert(pv, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *SliceOrderBook) load(book SliceOrderBook) {
|
||||
b.Reset()
|
||||
b.update(book)
|
||||
}
|
||||
|
||||
func (b *SliceOrderBook) update(book SliceOrderBook) {
|
||||
b.updateBids(book.Bids)
|
||||
b.updateAsks(book.Asks)
|
||||
}
|
||||
|
||||
func (b *SliceOrderBook) Reset() {
|
||||
b.Bids = nil
|
||||
b.Asks = nil
|
||||
}
|
||||
|
||||
func (b *SliceOrderBook) Load(book SliceOrderBook) {
|
||||
b.load(book)
|
||||
b.EmitLoad(b)
|
||||
}
|
||||
|
||||
func (b *SliceOrderBook) Update(book SliceOrderBook) {
|
||||
b.update(book)
|
||||
b.EmitUpdate(b)
|
||||
}
|
||||
|
||||
func (b *SliceOrderBook) Print() {
|
||||
fmt.Printf(b.String())
|
||||
}
|
||||
|
||||
func (b *SliceOrderBook) String() string {
|
||||
sb := strings.Builder{}
|
||||
|
||||
sb.WriteString("BOOK ")
|
||||
sb.WriteString(b.Symbol)
|
||||
sb.WriteString("\n")
|
||||
|
||||
if len(b.Asks) > 0 {
|
||||
sb.WriteString("ASKS:\n")
|
||||
for i := len(b.Asks) - 1; i >= 0; i-- {
|
||||
sb.WriteString("- ASK: ")
|
||||
sb.WriteString(b.Asks[i].String())
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.Bids) > 0 {
|
||||
sb.WriteString("BIDS:\n")
|
||||
for _, bid := range b.Bids {
|
||||
sb.WriteString("- BID: ")
|
||||
sb.WriteString(bid.String())
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (b *SliceOrderBook) CopyDepth(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
|
||||
}
|
25
pkg/types/sliceorderbook_callbacks.go
Normal file
25
pkg/types/sliceorderbook_callbacks.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
// Code generated by "callbackgen -type SliceOrderBook"; DO NOT EDIT.
|
||||
|
||||
package types
|
||||
|
||||
import ()
|
||||
|
||||
func (b *SliceOrderBook) OnLoad(cb func(book *SliceOrderBook)) {
|
||||
b.loadCallbacks = append(b.loadCallbacks, cb)
|
||||
}
|
||||
|
||||
func (b *SliceOrderBook) EmitLoad(book *SliceOrderBook) {
|
||||
for _, cb := range b.loadCallbacks {
|
||||
cb(book)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *SliceOrderBook) OnUpdate(cb func(book *SliceOrderBook)) {
|
||||
b.updateCallbacks = append(b.updateCallbacks, cb)
|
||||
}
|
||||
|
||||
func (b *SliceOrderBook) EmitUpdate(book *SliceOrderBook) {
|
||||
for _, cb := range b.updateCallbacks {
|
||||
cb(book)
|
||||
}
|
||||
}
|
|
@ -94,21 +94,21 @@ func (stream *StandardStream) EmitKLine(kline KLine) {
|
|||
}
|
||||
}
|
||||
|
||||
func (stream *StandardStream) OnBookUpdate(cb func(book OrderBook)) {
|
||||
func (stream *StandardStream) OnBookUpdate(cb func(book SliceOrderBook)) {
|
||||
stream.bookUpdateCallbacks = append(stream.bookUpdateCallbacks, cb)
|
||||
}
|
||||
|
||||
func (stream *StandardStream) EmitBookUpdate(book OrderBook) {
|
||||
func (stream *StandardStream) EmitBookUpdate(book SliceOrderBook) {
|
||||
for _, cb := range stream.bookUpdateCallbacks {
|
||||
cb(book)
|
||||
}
|
||||
}
|
||||
|
||||
func (stream *StandardStream) OnBookSnapshot(cb func(book OrderBook)) {
|
||||
func (stream *StandardStream) OnBookSnapshot(cb func(book SliceOrderBook)) {
|
||||
stream.bookSnapshotCallbacks = append(stream.bookSnapshotCallbacks, cb)
|
||||
}
|
||||
|
||||
func (stream *StandardStream) EmitBookSnapshot(book OrderBook) {
|
||||
func (stream *StandardStream) EmitBookSnapshot(book SliceOrderBook) {
|
||||
for _, cb := range stream.bookSnapshotCallbacks {
|
||||
cb(book)
|
||||
}
|
||||
|
@ -133,7 +133,7 @@ type StandardStreamEventHub interface {
|
|||
|
||||
OnKLine(cb func(kline KLine))
|
||||
|
||||
OnBookUpdate(cb func(book OrderBook))
|
||||
OnBookUpdate(cb func(book SliceOrderBook))
|
||||
|
||||
OnBookSnapshot(cb func(book OrderBook))
|
||||
OnBookSnapshot(cb func(book SliceOrderBook))
|
||||
}
|
||||
|
|
|
@ -44,9 +44,9 @@ type StandardStream struct {
|
|||
|
||||
kLineCallbacks []func(kline KLine)
|
||||
|
||||
bookUpdateCallbacks []func(book OrderBook)
|
||||
bookUpdateCallbacks []func(book SliceOrderBook)
|
||||
|
||||
bookSnapshotCallbacks []func(book OrderBook)
|
||||
bookSnapshotCallbacks []func(book SliceOrderBook)
|
||||
}
|
||||
|
||||
func (stream *StandardStream) Subscribe(channel Channel, symbol string, options SubscribeOptions) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package datatype
|
||||
package types
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
|
@ -9,7 +9,6 @@ import (
|
|||
|
||||
"github.com/slack-go/slack"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/datatype"
|
||||
"github.com/c9s/bbgo/pkg/util"
|
||||
)
|
||||
|
||||
|
@ -62,7 +61,7 @@ type Trade struct {
|
|||
Side SideType `json:"side" db:"side"`
|
||||
IsBuyer bool `json:"isBuyer" db:"is_buyer"`
|
||||
IsMaker bool `json:"isMaker" db:"is_maker"`
|
||||
Time datatype.Time `json:"tradedAt" db:"traded_at"`
|
||||
Time Time `json:"tradedAt" db:"traded_at"`
|
||||
Fee float64 `json:"fee" db:"fee"`
|
||||
FeeCurrency string `json:"feeCurrency" db:"fee_currency"`
|
||||
|
||||
|
@ -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)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,8 +3,6 @@ package types
|
|||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/datatype"
|
||||
)
|
||||
|
||||
type Withdraw struct {
|
||||
|
@ -20,7 +18,7 @@ type Withdraw struct {
|
|||
TransactionFee float64 `json:"transactionFee" db:"txn_fee"`
|
||||
TransactionFeeCurrency string `json:"transactionFeeCurrency" db:"txn_fee_currency"`
|
||||
WithdrawOrderID string `json:"withdrawOrderId"`
|
||||
ApplyTime datatype.Time `json:"applyTime" db:"time"`
|
||||
ApplyTime Time `json:"applyTime" db:"time"`
|
||||
Network string `json:"network" db:"network"`
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user