From 57a78777df606c090eaf2b86587ade4788d57c6c Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 20 May 2021 01:32:26 +0800 Subject: [PATCH 01/38] move Time type to types.Time --- pkg/backtest/matching.go | 7 +++---- pkg/exchange/binance/convert.go | 7 +++---- pkg/exchange/binance/exchange.go | 5 ++--- pkg/exchange/binance/parse.go | 5 ++--- pkg/exchange/ftx/convert.go | 9 ++++----- pkg/exchange/ftx/exchange_test.go | 5 ++--- pkg/exchange/ftx/stream_message_handler_test.go | 7 +++---- pkg/exchange/max/convert.go | 7 +++---- pkg/exchange/max/exchange.go | 5 ++--- pkg/exchange/max/maxapi/reward.go | 3 +-- pkg/exchange/max/stream.go | 5 ++--- pkg/service/deposit_test.go | 3 +-- pkg/service/reward_test.go | 11 +++++------ pkg/service/withdraw_test.go | 3 +-- pkg/types/deposit.go | 4 +--- pkg/types/order.go | 17 ++++++++--------- pkg/types/reward.go | 3 +-- pkg/{datatype => types}/time.go | 2 +- pkg/types/trade.go | 13 ++++++------- pkg/types/withdraw.go | 14 ++++++-------- 20 files changed, 57 insertions(+), 78 deletions(-) rename pkg/{datatype => types}/time.go (98%) diff --git a/pkg/backtest/matching.go b/pkg/backtest/matching.go index 3bc208459..825d34381 100644 --- a/pkg/backtest/matching.go +++ b/pkg/backtest/matching.go @@ -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), } } diff --git a/pkg/exchange/binance/convert.go b/pkg/exchange/binance/convert.go index 10aba5ef3..5060704d0 100644 --- a/pkg/exchange/binance/convert.go +++ b/pkg/exchange/binance/convert.go @@ -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 diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index e0f92297c..cabad7bf2 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -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, diff --git a/pkg/exchange/binance/parse.go b/pkg/exchange/binance/parse.go index 110237a09..7923f8955 100644 --- a/pkg/exchange/binance/parse.go +++ b/pkg/exchange/binance/parse.go @@ -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 diff --git a/pkg/exchange/ftx/convert.go b/pkg/exchange/ftx/convert.go index 9b886e44c..362faf5c9 100644 --- a/pkg/exchange/ftx/convert.go +++ b/pkg/exchange/ftx/convert.go @@ -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, diff --git a/pkg/exchange/ftx/exchange_test.go b/pkg/exchange/ftx/exchange_test.go index 246429569..a87bbe927 100644 --- a/pkg/exchange/ftx/exchange_test.go +++ b/pkg/exchange/ftx/exchange_test.go @@ -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, diff --git a/pkg/exchange/ftx/stream_message_handler_test.go b/pkg/exchange/ftx/stream_message_handler_test.go index 95b3658a0..3d532ae22 100644 --- a/pkg/exchange/ftx/stream_message_handler_test.go +++ b/pkg/exchange/ftx/stream_message_handler_test.go @@ -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, diff --git a/pkg/exchange/max/convert.go b/pkg/exchange/max/convert.go index 1481cc319..3c9f6a474 100644 --- a/pkg/exchange/max/convert.go +++ b/pkg/exchange/max/convert.go @@ -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 } diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 42c718afc..df9ec4240 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -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 diff --git a/pkg/exchange/max/maxapi/reward.go b/pkg/exchange/max/maxapi/reward.go index 188afae90..aceea0301 100644 --- a/pkg/exchange/max/maxapi/reward.go +++ b/pkg/exchange/max/maxapi/reward.go @@ -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 } diff --git a/pkg/exchange/max/stream.go b/pkg/exchange/max/stream.go index 330a07cf1..0845a5083 100644 --- a/pkg/exchange/max/stream.go +++ b/pkg/exchange/max/stream.go @@ -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 } diff --git a/pkg/service/deposit_test.go b/pkg/service/deposit_test.go index 8519098dc..2bd97b0b1 100644 --- a/pkg/service/deposit_test.go +++ b/pkg/service/deposit_test.go @@ -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", diff --git a/pkg/service/reward_test.go b/pkg/service/reward_test.go index ef8f13475..36661c468 100644 --- a/pkg/service/reward_test.go +++ b/pkg/service/reward_test.go @@ -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) diff --git a/pkg/service/withdraw_test.go b/pkg/service/withdraw_test.go index 68ececc18..80c8d3b8e 100644 --- a/pkg/service/withdraw_test.go +++ b/pkg/service/withdraw_test.go @@ -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) diff --git a/pkg/types/deposit.go b/pkg/types/deposit.go index 03a29724c..615ff90f1 100644 --- a/pkg/types/deposit.go +++ b/pkg/types/deposit.go @@ -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"` diff --git a/pkg/types/order.go b/pkg/types/order.go index aef9c587a..8a9fae151 100644 --- a/pkg/types/order.go +++ b/pkg/types/order.go @@ -9,7 +9,6 @@ import ( "github.com/pkg/errors" "github.com/slack-go/slack" - "github.com/c9s/bbgo/pkg/datatype" "github.com/c9s/bbgo/pkg/util" ) @@ -145,14 +144,14 @@ func (o *SubmitOrder) SlackAttachment() slack.Attachment { type Order struct { SubmitOrder - Exchange ExchangeName `json:"exchange" db:"exchange"` - GID uint64 `json:"gid" db:"gid"` - OrderID uint64 `json:"orderID" db:"order_id"` // order id - Status OrderStatus `json:"status" db:"status"` - ExecutedQuantity float64 `json:"executedQuantity" db:"executed_quantity"` - IsWorking bool `json:"isWorking" db:"is_working"` - CreationTime datatype.Time `json:"creationTime" db:"created_at"` - UpdateTime datatype.Time `json:"updateTime" db:"updated_at"` + Exchange ExchangeName `json:"exchange" db:"exchange"` + GID uint64 `json:"gid" db:"gid"` + OrderID uint64 `json:"orderID" db:"order_id"` // order id + Status OrderStatus `json:"status" db:"status"` + ExecutedQuantity float64 `json:"executedQuantity" db:"executed_quantity"` + IsWorking bool `json:"isWorking" db:"is_working"` + CreationTime Time `json:"creationTime" db:"created_at"` + UpdateTime Time `json:"updateTime" db:"updated_at"` IsMargin bool `json:"isMargin" db:"is_margin"` IsIsolated bool `json:"isIsolated" db:"is_isolated"` diff --git a/pkg/types/reward.go b/pkg/types/reward.go index 9e262d2e7..a645e96e3 100644 --- a/pkg/types/reward.go +++ b/pkg/types/reward.go @@ -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 diff --git a/pkg/datatype/time.go b/pkg/types/time.go similarity index 98% rename from pkg/datatype/time.go rename to pkg/types/time.go index 10e27c594..f531a209f 100644 --- a/pkg/datatype/time.go +++ b/pkg/types/time.go @@ -1,4 +1,4 @@ -package datatype +package types import ( "database/sql/driver" diff --git a/pkg/types/trade.go b/pkg/types/trade.go index ba4852d32..082020e2a 100644 --- a/pkg/types/trade.go +++ b/pkg/types/trade.go @@ -9,7 +9,6 @@ import ( "github.com/slack-go/slack" - "github.com/c9s/bbgo/pkg/datatype" "github.com/c9s/bbgo/pkg/util" ) @@ -59,12 +58,12 @@ type Trade struct { QuoteQuantity float64 `json:"quoteQuantity" db:"quote_quantity"` Symbol string `json:"symbol" db:"symbol"` - Side SideType `json:"side" db:"side"` - IsBuyer bool `json:"isBuyer" db:"is_buyer"` - IsMaker bool `json:"isMaker" db:"is_maker"` - Time datatype.Time `json:"tradedAt" db:"traded_at"` - Fee float64 `json:"fee" db:"fee"` - FeeCurrency string `json:"feeCurrency" db:"fee_currency"` + Side SideType `json:"side" db:"side"` + IsBuyer bool `json:"isBuyer" db:"is_buyer"` + IsMaker bool `json:"isMaker" db:"is_maker"` + Time Time `json:"tradedAt" db:"traded_at"` + Fee float64 `json:"fee" db:"fee"` + FeeCurrency string `json:"feeCurrency" db:"fee_currency"` IsMargin bool `json:"isMargin" db:"is_margin"` IsIsolated bool `json:"isIsolated" db:"is_isolated"` diff --git a/pkg/types/withdraw.go b/pkg/types/withdraw.go index d3ea2501d..a579a8ef4 100644 --- a/pkg/types/withdraw.go +++ b/pkg/types/withdraw.go @@ -3,8 +3,6 @@ package types import ( "fmt" "time" - - "github.com/c9s/bbgo/pkg/datatype" ) type Withdraw struct { @@ -16,12 +14,12 @@ type Withdraw struct { AddressTag string `json:"addressTag"` Status string `json:"status"` - TransactionID string `json:"transactionID" db:"txn_id"` - TransactionFee float64 `json:"transactionFee" db:"txn_fee"` - TransactionFeeCurrency string `json:"transactionFeeCurrency" db:"txn_fee_currency"` - WithdrawOrderID string `json:"withdrawOrderId"` - ApplyTime datatype.Time `json:"applyTime" db:"time"` - Network string `json:"network" db:"network"` + TransactionID string `json:"transactionID" db:"txn_id"` + TransactionFee float64 `json:"transactionFee" db:"txn_fee"` + TransactionFeeCurrency string `json:"transactionFeeCurrency" db:"txn_fee_currency"` + WithdrawOrderID string `json:"withdrawOrderId"` + ApplyTime Time `json:"applyTime" db:"time"` + Network string `json:"network" db:"network"` } func (w Withdraw) String() string { From d14137b878bf326f0a225ab6b91a2c736f40795c Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 20 May 2021 23:55:56 +0800 Subject: [PATCH 02/38] add rbtree functions --- pkg/rbtbook/node.go | 117 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 pkg/rbtbook/node.go diff --git a/pkg/rbtbook/node.go b/pkg/rbtbook/node.go new file mode 100644 index 000000000..2cb4117c3 --- /dev/null +++ b/pkg/rbtbook/node.go @@ -0,0 +1,117 @@ +package rbtbook + +import "github.com/c9s/bbgo/pkg/fixedpoint" + +type Color bool + +const ( + Red = Color(false) + Black = Color(true) +) + +type Tree struct { + Root *Node +} + +func (t *Tree) UpdateOrInsert(key, val fixedpoint.Value) { + var y *Node = nil + var x = t.Root + var node = &Node{ + Price: key, + Volume: val, + } + + for x != nil { + y = x + + // matched an existing node + if node.Price == x.Price { + x.Volume = val + return + } else if node.Price < x.Price { + x = x.Left + } else { + x = x.Right + } + } + + node.Parent = y + + if y == nil { + t.Root = node + } else if node.Price < y.Price { + y.Left = node + } else { + y.Right = node + } + + node.Left = nil + node.Right = nil + node.Color = Red +} + +func InsertFixup(tree *Tree, node *Node) { + +} + +/* +Node +A red node always has black children. +A black node may have red or black children +*/ +type Node struct { + Left, Right, Parent *Node + Color Color + Price fixedpoint.Value + Volume fixedpoint.Value +} + +// 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 RotateLeft(tree *Tree, x *Node) { + var y = x.Right + x.Right = y.Left + + if y.Left != nil { + y.Left.Parent = x + } + + y.Parent = x.Parent + + if x.Parent == nil { + 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 RotateRight(tree *Tree, y *Node) { + x := y.Left + y.Left = x.Right + + if x.Right != nil { + x.Right.Parent = y + } + + x.Parent = y.Parent + + if y.Parent == nil { + tree.Root = x + } else if y == y.Parent.Left { + y.Parent.Left = x + } else { + y.Parent.Right = x + } + + x.Right = y + y.Parent = x +} From edf8902b287f91a4822da12b183a14138eb69b53 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 21 May 2021 01:36:58 +0800 Subject: [PATCH 03/38] implement rbtree delete --- pkg/rbtbook/node.go | 98 +------------ pkg/rbtbook/tree.go | 294 +++++++++++++++++++++++++++++++++++++++ pkg/rbtbook/tree_test.go | 37 +++++ 3 files changed, 332 insertions(+), 97 deletions(-) create mode 100644 pkg/rbtbook/tree.go create mode 100644 pkg/rbtbook/tree_test.go diff --git a/pkg/rbtbook/node.go b/pkg/rbtbook/node.go index 2cb4117c3..287d06c80 100644 --- a/pkg/rbtbook/node.go +++ b/pkg/rbtbook/node.go @@ -9,51 +9,6 @@ const ( Black = Color(true) ) -type Tree struct { - Root *Node -} - -func (t *Tree) UpdateOrInsert(key, val fixedpoint.Value) { - var y *Node = nil - var x = t.Root - var node = &Node{ - Price: key, - Volume: val, - } - - for x != nil { - y = x - - // matched an existing node - if node.Price == x.Price { - x.Volume = val - return - } else if node.Price < x.Price { - x = x.Left - } else { - x = x.Right - } - } - - node.Parent = y - - if y == nil { - t.Root = node - } else if node.Price < y.Price { - y.Left = node - } else { - y.Right = node - } - - node.Left = nil - node.Right = nil - node.Color = Red -} - -func InsertFixup(tree *Tree, node *Node) { - -} - /* Node A red node always has black children. @@ -62,56 +17,5 @@ A black node may have red or black children type Node struct { Left, Right, Parent *Node Color Color - Price fixedpoint.Value - Volume fixedpoint.Value -} - -// 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 RotateLeft(tree *Tree, x *Node) { - var y = x.Right - x.Right = y.Left - - if y.Left != nil { - y.Left.Parent = x - } - - y.Parent = x.Parent - - if x.Parent == nil { - 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 RotateRight(tree *Tree, y *Node) { - x := y.Left - y.Left = x.Right - - if x.Right != nil { - x.Right.Parent = y - } - - x.Parent = y.Parent - - if y.Parent == nil { - tree.Root = x - } else if y == y.Parent.Left { - y.Parent.Left = x - } else { - y.Parent.Right = x - } - - x.Right = y - y.Parent = x + Key, Value fixedpoint.Value } diff --git a/pkg/rbtbook/tree.go b/pkg/rbtbook/tree.go new file mode 100644 index 000000000..a97bbcc2c --- /dev/null +++ b/pkg/rbtbook/tree.go @@ -0,0 +1,294 @@ +package rbtbook + +import ( + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +type Tree struct { + Root, Neel *Node +} + +func NewTree() *Tree { + var neel = &Node{ + Color: Black, + } + var root = neel + root.Parent = neel + + return &Tree{ + Root: root, + Neel: neel, + } +} + +func (tree *Tree) 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 *Node + + if del.Left == tree.Neel || del.Right == tree.Neel { + y = del + } else { + y = tree.Successor(del) + } + + if y.Left != tree.Neel { + x = y.Left + } else { + x = y.Right + } + + x.Parent = y.Parent + + if y.Parent == tree.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) + } + + return true +} + +func (tree *Tree) DeleteFixup(current *Node) { + 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 *Tree) Insert(key, val fixedpoint.Value) { + var y = tree.Neel + var x = tree.Root + var node = &Node{ + Key: key, + Value: val, + Color: Red, + } + + for x != tree.Neel { + y = x + + if node.Key < x.Key { + x = x.Left + } else { + x = x.Right + } + } + + node.Parent = y + + if y == tree.Neel { + tree.Root = node + } else if node.Key < y.Key { + y.Left = node + } else { + y.Right = node + } + + node.Left = tree.Neel + node.Right = tree.Neel + node.Color = Red + + tree.InsertFixup(node) +} + +func (tree *Tree) Search(key fixedpoint.Value) *Node { + var current = tree.Root + for current != nil && key != current.Key { + if key < current.Key { + current = current.Left + } else { + current = current.Right + } + } + return current +} + +func (tree *Tree) InsertFixup(current *Node) { + // A red node can't have a red parent, we need to fix it up + for current.Parent.Color == Red { + grandParent := current.Parent.Parent + if current.Parent == grandParent.Left { + uncle := grandParent.Right + if uncle.Color == Red { + current.Parent.Color = Black + uncle.Color = Black + grandParent.Color = Red + current = grandParent + } else { // if uncle is black + if current == current.Parent.Right { + current = current.Parent + tree.RotateLeft(current) + } + + current.Parent.Color = Black + current.Parent.Parent.Color = Red + tree.RotateRight(current.Parent.Parent) + } + } else { + uncle := current.Parent.Parent.Left + if uncle.Color == Red { + current.Parent.Color = Black + uncle.Color = Black + current.Parent.Parent.Color = Red + } else { + + if current == current.Parent.Left { + current = current.Parent + tree.RotateRight(current) + } + + current.Parent.Color = Black + current.Parent.Parent.Color = Red + tree.RotateLeft(current.Parent.Parent) + } + } + } + + // ensure that root is black + tree.Root.Color = Black +} + +// RotateLeft +// x is the axes of rotation, y is the node that will be replace x's position. +// we need to: +// 1. move y's left child to the x's right child +// 2. change y's parent to x's parent +// 3. change x's parent to y +func (tree *Tree) RotateLeft(x *Node) { + var y = x.Right + x.Right = y.Left + + if y.Left != nil { + y.Left.Parent = x + } + + y.Parent = x.Parent + + if x.Parent == nil { + 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 *Tree) RotateRight(y *Node) { + x := y.Left + y.Left = x.Right + + if x.Right != nil { + x.Right.Parent = y + } + + x.Parent = y.Parent + + if y.Parent == nil { + 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 *Tree) LeftMost(current *Node) *Node { + for current.Left != nil { + current = current.Left + } + + return current +} + +func (tree *Tree) Successor(current *Node) *Node { + if current.Right != nil { + return tree.LeftMost(current.Right) + } + + var newNode = current.Parent + for newNode != nil && current == newNode.Right { + current = newNode + newNode = newNode.Parent + } + + return newNode +} diff --git a/pkg/rbtbook/tree_test.go b/pkg/rbtbook/tree_test.go new file mode 100644 index 000000000..41044ef32 --- /dev/null +++ b/pkg/rbtbook/tree_test.go @@ -0,0 +1,37 @@ +package rbtbook + +import ( + "testing" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/stretchr/testify/assert" +) + +func TestTree(t *testing.T) { + tree := NewTree() + 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) +} From be646fbac2a6496ce4a9df18aee2a564f7d375ab Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 21 May 2021 01:44:50 +0800 Subject: [PATCH 04/38] move rbtree to types package --- pkg/types/color.go | 5 ++-- pkg/types/kline.go | 6 ++--- pkg/{rbtbook/tree.go => types/rbtree.go} | 26 +++++++++---------- pkg/{rbtbook/node.go => types/rbtree_node.go} | 2 +- .../tree_test.go => types/rbtree_test.go} | 4 +-- pkg/types/side.go | 6 ++--- 6 files changed, 25 insertions(+), 24 deletions(-) rename pkg/{rbtbook/tree.go => types/rbtree.go} (90%) rename pkg/{rbtbook/node.go => types/rbtree_node.go} (95%) rename pkg/{rbtbook/tree_test.go => types/rbtree_test.go} (96%) diff --git a/pkg/types/color.go b/pkg/types/color.go index fa714b5cc..ac8324aa2 100644 --- a/pkg/types/color.go +++ b/pkg/types/color.go @@ -1,4 +1,5 @@ package types -const Green = "#228B22" -const Red = "#800000" +const GreenColor = "#228B22" +const RedColor = "#800000" +const GrayColor = "#f0f0f0" diff --git a/pkg/types/kline.go b/pkg/types/kline.go index b45bb67d0..15e13c827 100644 --- a/pkg/types/kline.go +++ b/pkg/types/kline.go @@ -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 { diff --git a/pkg/rbtbook/tree.go b/pkg/types/rbtree.go similarity index 90% rename from pkg/rbtbook/tree.go rename to pkg/types/rbtree.go index a97bbcc2c..6aea02f97 100644 --- a/pkg/rbtbook/tree.go +++ b/pkg/types/rbtree.go @@ -1,27 +1,27 @@ -package rbtbook +package types import ( "github.com/c9s/bbgo/pkg/fixedpoint" ) -type Tree struct { +type RBTree struct { Root, Neel *Node } -func NewTree() *Tree { +func NewRBTree() *RBTree { var neel = &Node{ Color: Black, } var root = neel root.Parent = neel - return &Tree{ + return &RBTree{ Root: root, Neel: neel, } } -func (tree *Tree) Delete(key fixedpoint.Value) bool { +func (tree *RBTree) Delete(key fixedpoint.Value) bool { var del = tree.Search(key) if del == nil { return false @@ -64,7 +64,7 @@ func (tree *Tree) Delete(key fixedpoint.Value) bool { return true } -func (tree *Tree) DeleteFixup(current *Node) { +func (tree *RBTree) DeleteFixup(current *Node) { for current != tree.Root && current.Color == Black { if current == current.Parent.Left { sibling := current.Parent.Right @@ -128,7 +128,7 @@ func (tree *Tree) DeleteFixup(current *Node) { current.Color = Black } -func (tree *Tree) Insert(key, val fixedpoint.Value) { +func (tree *RBTree) Insert(key, val fixedpoint.Value) { var y = tree.Neel var x = tree.Root var node = &Node{ @@ -164,7 +164,7 @@ func (tree *Tree) Insert(key, val fixedpoint.Value) { tree.InsertFixup(node) } -func (tree *Tree) Search(key fixedpoint.Value) *Node { +func (tree *RBTree) Search(key fixedpoint.Value) *Node { var current = tree.Root for current != nil && key != current.Key { if key < current.Key { @@ -176,7 +176,7 @@ func (tree *Tree) Search(key fixedpoint.Value) *Node { return current } -func (tree *Tree) InsertFixup(current *Node) { +func (tree *RBTree) InsertFixup(current *Node) { // A red node can't have a red parent, we need to fix it up for current.Parent.Color == Red { grandParent := current.Parent.Parent @@ -227,7 +227,7 @@ func (tree *Tree) InsertFixup(current *Node) { // 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 *Tree) RotateLeft(x *Node) { +func (tree *RBTree) RotateLeft(x *Node) { var y = x.Right x.Right = y.Left @@ -249,7 +249,7 @@ func (tree *Tree) RotateLeft(x *Node) { x.Parent = y } -func (tree *Tree) RotateRight(y *Node) { +func (tree *RBTree) RotateRight(y *Node) { x := y.Left y.Left = x.Right @@ -271,7 +271,7 @@ func (tree *Tree) RotateRight(y *Node) { y.Parent = x } -func (tree *Tree) LeftMost(current *Node) *Node { +func (tree *RBTree) LeftMost(current *Node) *Node { for current.Left != nil { current = current.Left } @@ -279,7 +279,7 @@ func (tree *Tree) LeftMost(current *Node) *Node { return current } -func (tree *Tree) Successor(current *Node) *Node { +func (tree *RBTree) Successor(current *Node) *Node { if current.Right != nil { return tree.LeftMost(current.Right) } diff --git a/pkg/rbtbook/node.go b/pkg/types/rbtree_node.go similarity index 95% rename from pkg/rbtbook/node.go rename to pkg/types/rbtree_node.go index 287d06c80..3a3b28930 100644 --- a/pkg/rbtbook/node.go +++ b/pkg/types/rbtree_node.go @@ -1,4 +1,4 @@ -package rbtbook +package types import "github.com/c9s/bbgo/pkg/fixedpoint" diff --git a/pkg/rbtbook/tree_test.go b/pkg/types/rbtree_test.go similarity index 96% rename from pkg/rbtbook/tree_test.go rename to pkg/types/rbtree_test.go index 41044ef32..7684c7fb2 100644 --- a/pkg/rbtbook/tree_test.go +++ b/pkg/types/rbtree_test.go @@ -1,4 +1,4 @@ -package rbtbook +package types import ( "testing" @@ -8,7 +8,7 @@ import ( ) func TestTree(t *testing.T) { - tree := NewTree() + tree := NewRBTree() tree.Insert(fixedpoint.NewFromFloat(3000.0), fixedpoint.NewFromFloat(10.0)) assert.NotNil(t, tree.Root) diff --git a/pkg/types/side.go b/pkg/types/side.go index 492ebbdda..46c916aed 100644 --- a/pkg/types/side.go +++ b/pkg/types/side.go @@ -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 { From d9308166725c19eb13b156fcd4ea093458dde491 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 21 May 2021 02:15:31 +0800 Subject: [PATCH 05/38] define RBOrderBook --- pkg/types/orderbook.go | 24 ++++++++++++++--- pkg/types/orderbook_callbacks.go | 22 ++------------- pkg/types/rbtree.go | 46 ++++++++++++++++++++++++-------- pkg/types/rbtree_node.go | 6 ++--- 4 files changed, 60 insertions(+), 38 deletions(-) diff --git a/pkg/types/orderbook.go b/pkg/types/orderbook.go index 6df288aa5..99acd23f2 100644 --- a/pkg/types/orderbook.go +++ b/pkg/types/orderbook.go @@ -132,16 +132,32 @@ func (slice PriceVolumeSlice) Upsert(pv PriceVolume, descending bool) PriceVolum return slice } +//go:generate callbackgen -type RBOrderBook +type RBOrderBook struct { + Symbol string + Bids *RBTree + Asks *RBTree + + loadCallbacks []func(book *RBOrderBook) + updateCallbacks []func(book *RBOrderBook) +} + +func (b *RBOrderBook) BestBid() (PriceVolume, bool) { + return PriceVolume{}, true +} + +func (b *RBOrderBook) BestAsk() (PriceVolume, bool) { + return PriceVolume{}, true +} + //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) + loadCallbacks []func(book *OrderBook) + updateCallbacks []func(book *OrderBook) } func (b *OrderBook) Spread() (fixedpoint.Value, bool) { diff --git a/pkg/types/orderbook_callbacks.go b/pkg/types/orderbook_callbacks.go index 292c06419..a3e532c60 100644 --- a/pkg/types/orderbook_callbacks.go +++ b/pkg/types/orderbook_callbacks.go @@ -2,6 +2,8 @@ package types +import () + func (b *OrderBook) OnLoad(cb func(book *OrderBook)) { b.loadCallbacks = append(b.loadCallbacks, cb) } @@ -21,23 +23,3 @@ func (b *OrderBook) EmitUpdate(book *OrderBook) { 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) - } -} diff --git a/pkg/types/rbtree.go b/pkg/types/rbtree.go index 6aea02f97..794513a86 100644 --- a/pkg/types/rbtree.go +++ b/pkg/types/rbtree.go @@ -5,11 +5,11 @@ import ( ) type RBTree struct { - Root, Neel *Node + Root, Neel *RBNode } func NewRBTree() *RBTree { - var neel = &Node{ + var neel = &RBNode{ Color: Black, } var root = neel @@ -29,7 +29,7 @@ func (tree *RBTree) Delete(key fixedpoint.Value) bool { // y = the node to be deleted // x (the child of the deleted node) - var x, y *Node + var x, y *RBNode if del.Left == tree.Neel || del.Right == tree.Neel { y = del @@ -64,7 +64,7 @@ func (tree *RBTree) Delete(key fixedpoint.Value) bool { return true } -func (tree *RBTree) DeleteFixup(current *Node) { +func (tree *RBTree) DeleteFixup(current *RBNode) { for current != tree.Root && current.Color == Black { if current == current.Parent.Left { sibling := current.Parent.Right @@ -131,7 +131,7 @@ func (tree *RBTree) DeleteFixup(current *Node) { func (tree *RBTree) Insert(key, val fixedpoint.Value) { var y = tree.Neel var x = tree.Root - var node = &Node{ + var node = &RBNode{ Key: key, Value: val, Color: Red, @@ -164,7 +164,7 @@ func (tree *RBTree) Insert(key, val fixedpoint.Value) { tree.InsertFixup(node) } -func (tree *RBTree) Search(key fixedpoint.Value) *Node { +func (tree *RBTree) Search(key fixedpoint.Value) *RBNode { var current = tree.Root for current != nil && key != current.Key { if key < current.Key { @@ -176,7 +176,7 @@ func (tree *RBTree) Search(key fixedpoint.Value) *Node { return current } -func (tree *RBTree) InsertFixup(current *Node) { +func (tree *RBTree) InsertFixup(current *RBNode) { // A red node can't have a red parent, we need to fix it up for current.Parent.Color == Red { grandParent := current.Parent.Parent @@ -227,7 +227,7 @@ func (tree *RBTree) InsertFixup(current *Node) { // 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 *Node) { +func (tree *RBTree) RotateLeft(x *RBNode) { var y = x.Right x.Right = y.Left @@ -249,7 +249,7 @@ func (tree *RBTree) RotateLeft(x *Node) { x.Parent = y } -func (tree *RBTree) RotateRight(y *Node) { +func (tree *RBTree) RotateRight(y *RBNode) { x := y.Left y.Left = x.Right @@ -271,7 +271,7 @@ func (tree *RBTree) RotateRight(y *Node) { y.Parent = x } -func (tree *RBTree) LeftMost(current *Node) *Node { +func (tree *RBTree) LeftMost(current *RBNode) *RBNode { for current.Left != nil { current = current.Left } @@ -279,7 +279,7 @@ func (tree *RBTree) LeftMost(current *Node) *Node { return current } -func (tree *RBTree) Successor(current *Node) *Node { +func (tree *RBTree) Successor(current *RBNode) *RBNode { if current.Right != nil { return tree.LeftMost(current.Right) } @@ -292,3 +292,27 @@ func (tree *RBTree) Successor(current *Node) *Node { return newNode } + +func (tree *RBTree) Preorder(current *RBNode, cb func(n *RBNode)) { + if current != nil { + cb(current) + tree.Preorder(current.Left, cb) + tree.Preorder(current.Right, cb) + } +} + +func (tree *RBTree) Inorder(current *RBNode, cb func(n *RBNode)) { + if current != nil { + tree.Preorder(current.Left, cb) + cb(current) + tree.Preorder(current.Right, cb) + } +} + +func (tree *RBTree) Postorder(current *RBNode, cb func(n *RBNode)) { + if current != nil { + tree.Preorder(current.Left, cb) + tree.Preorder(current.Right, cb) + cb(current) + } +} diff --git a/pkg/types/rbtree_node.go b/pkg/types/rbtree_node.go index 3a3b28930..2b89cbbbf 100644 --- a/pkg/types/rbtree_node.go +++ b/pkg/types/rbtree_node.go @@ -10,12 +10,12 @@ const ( ) /* -Node +RBNode A red node always has black children. A black node may have red or black children */ -type Node struct { - Left, Right, Parent *Node +type RBNode struct { + Left, Right, Parent *RBNode Color Color Key, Value fixedpoint.Value } From d2003bbc3d0b245db7ac16537ba0224346e8f2de Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 21 May 2021 02:17:40 +0800 Subject: [PATCH 06/38] remove unused emit function calls --- pkg/types/orderbook.go | 4 ---- pkg/types/rbtree_node.go | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/pkg/types/orderbook.go b/pkg/types/orderbook.go index 99acd23f2..73dd3f240 100644 --- a/pkg/types/orderbook.go +++ b/pkg/types/orderbook.go @@ -244,8 +244,6 @@ func (b *OrderBook) updateAsks(pvs PriceVolumeSlice) { b.Asks = b.Asks.Upsert(pv, false) } } - - b.EmitAsksChange(b.Asks) } func (b *OrderBook) updateBids(pvs PriceVolumeSlice) { @@ -256,8 +254,6 @@ func (b *OrderBook) updateBids(pvs PriceVolumeSlice) { b.Bids = b.Bids.Upsert(pv, true) } } - - b.EmitBidsChange(b.Bids) } func (b *OrderBook) update(book OrderBook) { diff --git a/pkg/types/rbtree_node.go b/pkg/types/rbtree_node.go index 2b89cbbbf..1da12a2e6 100644 --- a/pkg/types/rbtree_node.go +++ b/pkg/types/rbtree_node.go @@ -2,6 +2,7 @@ package types import "github.com/c9s/bbgo/pkg/fixedpoint" +// Color is the RB Tree color type Color bool const ( From 31f9920ddcdf1c44dcc57406c48ec3366c25569b Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 21 May 2021 02:17:57 +0800 Subject: [PATCH 07/38] fix func comment --- pkg/types/orderbook.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/types/orderbook.go b/pkg/types/orderbook.go index 73dd3f240..f9fce11a3 100644 --- a/pkg/types/orderbook.go +++ b/pkg/types/orderbook.go @@ -96,7 +96,7 @@ func (slice PriceVolumeSlice) Remove(price fixedpoint.Value, descending bool) Pr return append(slice[:idx], slice[idx+1:]...) } -// FindPriceVolumePair finds the pair by the given price, this function is a read-only +// 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) From f6229515acfcfaaee0615777140dc4a474268ae7 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 21 May 2021 02:18:45 +0800 Subject: [PATCH 08/38] fix color ref --- pkg/types/kline.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/types/kline.go b/pkg/types/kline.go index 15e13c827..1459fd900 100644 --- a/pkg/types/kline.go +++ b/pkg/types/kline.go @@ -313,11 +313,11 @@ 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 } func (k KLineWindow) Mid() float64 { From 94fb0e320e3dd3b1a687d43d7222b42183d2fa9d Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 21 May 2021 12:31:18 +0800 Subject: [PATCH 09/38] implement RBTree orderbook benchmark --- pkg/types/kline.go | 5 ++- pkg/types/orderbook.go | 8 ++++ pkg/types/orderbook_test.go | 88 +++++++++++++++++++++++++++++++++++++ pkg/types/rbtree.go | 52 +++++++++++++++++++++- 4 files changed, 149 insertions(+), 4 deletions(-) diff --git a/pkg/types/kline.go b/pkg/types/kline.go index 1459fd900..16a122472 100644 --- a/pkg/types/kline.go +++ b/pkg/types/kline.go @@ -320,18 +320,19 @@ func (k KLineWindow) Color() string { return GrayColor } +// Mid price func (k KLineWindow) Mid() float64 { return k.GetHigh() - k.GetLow()/2 } -// green candle with open and close near high price +// BounceUp returns true if it's green candle with open and close near high price func (k KLineWindow) BounceUp() bool { mid := k.Mid() trend := k.GetTrend() return trend > 0 && k.GetOpen() > mid && k.GetClose() > mid } -// red candle with open and close near low price +// BounceDown returns true red candle with open and close near low price func (k KLineWindow) BounceDown() bool { mid := k.Mid() trend := k.GetTrend() diff --git a/pkg/types/orderbook.go b/pkg/types/orderbook.go index f9fce11a3..34a351a6c 100644 --- a/pkg/types/orderbook.go +++ b/pkg/types/orderbook.go @@ -142,6 +142,14 @@ type RBOrderBook struct { updateCallbacks []func(book *RBOrderBook) } +func NewRBOrderBook(symbol string) *RBOrderBook { + return &RBOrderBook{ + Symbol: symbol, + Bids: NewRBTree(), + Asks: NewRBTree(), + } +} + func (b *RBOrderBook) BestBid() (PriceVolume, bool) { return PriceVolume{}, true } diff --git a/pkg/types/orderbook_test.go b/pkg/types/orderbook_test.go index a342e82fa..c565607f8 100644 --- a/pkg/types/orderbook_test.go +++ b/pkg/types/orderbook_test.go @@ -1,6 +1,7 @@ package types import ( + "math/rand" "testing" "github.com/stretchr/testify/assert" @@ -8,6 +9,93 @@ 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("RBOrderBook", 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 := &OrderBook{} + 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("RBOrderBook", 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 := &OrderBook{} + 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{ Bids: PriceVolumeSlice{ diff --git a/pkg/types/rbtree.go b/pkg/types/rbtree.go index 794513a86..3208d84a0 100644 --- a/pkg/types/rbtree.go +++ b/pkg/types/rbtree.go @@ -128,6 +128,46 @@ func (tree *RBTree) DeleteFixup(current *RBNode) { current.Color = Black } +func (tree *RBTree) Upsert(key, val fixedpoint.Value) { + var y = tree.Neel + var x = tree.Root + var node = &RBNode{ + Key: key, + Value: val, + Color: Red, + } + + for x != tree.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 == tree.Neel { + tree.Root = node + } else if node.Key < y.Key { + y.Left = node + } else { + y.Right = node + } + + node.Left = tree.Neel + node.Right = tree.Neel + node.Color = Red + + tree.InsertFixup(node) +} + func (tree *RBTree) Insert(key, val fixedpoint.Value) { var y = tree.Neel var x = tree.Root @@ -271,7 +311,15 @@ func (tree *RBTree) RotateRight(y *RBNode) { y.Parent = x } -func (tree *RBTree) LeftMost(current *RBNode) *RBNode { +func (tree *RBTree) Rightmost(current *RBNode) *RBNode { + for current.Right != nil { + current = current.Right + } + + return current +} + +func (tree *RBTree) Leftmost(current *RBNode) *RBNode { for current.Left != nil { current = current.Left } @@ -281,7 +329,7 @@ func (tree *RBTree) LeftMost(current *RBNode) *RBNode { func (tree *RBTree) Successor(current *RBNode) *RBNode { if current.Right != nil { - return tree.LeftMost(current.Right) + return tree.Leftmost(current.Right) } var newNode = current.Parent From 09d68057c5f8a1c6a541cbe2924a835a8f99bdbd Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 21 May 2021 12:32:47 +0800 Subject: [PATCH 10/38] move price volume slice to a separated file --- pkg/types/orderbook.go | 112 ------------------------------ pkg/types/price_volume_slice.go | 118 ++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 112 deletions(-) create mode 100644 pkg/types/price_volume_slice.go diff --git a/pkg/types/orderbook.go b/pkg/types/orderbook.go index 34a351a6c..4a2de2057 100644 --- a/pkg/types/orderbook.go +++ b/pkg/types/orderbook.go @@ -2,7 +2,6 @@ package types import ( "fmt" - "sort" "strings" "sync" @@ -21,117 +20,6 @@ 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:]...) -} - -// 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 -} - //go:generate callbackgen -type RBOrderBook type RBOrderBook struct { Symbol string diff --git a/pkg/types/price_volume_slice.go b/pkg/types/price_volume_slice.go new file mode 100644 index 000000000..d31166734 --- /dev/null +++ b/pkg/types/price_volume_slice.go @@ -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 +} From c58e252ff24e67166e978451832045426f394b53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=82=8B=E3=81=BF?= Date: Sat, 22 May 2021 03:24:09 +0800 Subject: [PATCH 11/38] Add stochastic oscillator indicator --- pkg/indicator/kd.go | 83 +++++++++++++++++++++++++++++++++++ pkg/indicator/kd_callbacks.go | 15 +++++++ 2 files changed, 98 insertions(+) create mode 100644 pkg/indicator/kd.go create mode 100644 pkg/indicator/kd_callbacks.go diff --git a/pkg/indicator/kd.go b/pkg/indicator/kd.go new file mode 100644 index 000000000..3ba50b1bb --- /dev/null +++ b/pkg/indicator/kd.go @@ -0,0 +1,83 @@ +package indicator + +import ( + "time" + + "github.com/c9s/bbgo/pkg/types" +) + +const DPeriod int = 3 + +/* +kd implements stochastic oscillator indicator + +Stochastic Oscillator +- https://www.investopedia.com/terms/s/stochasticoscillator.asp +*/ +//go:generate callbackgen -type KD +type KD struct { + types.IntervalWindow + K Float64Slice + D Float64Slice + + KLineWindow types.KLineWindow + + EndTime time.Time + UpdateCallbacks []func(k float64, d float64) +} + +func (inc *KD) 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 *KD) LastK() float64 { + if len(inc.K) == 0 { + return 0.0 + } + return inc.K[len(inc.K)-1] +} + +func (inc *KD) LastD() float64 { + if len(inc.K) == 0 { + return 0.0 + } + return inc.D[len(inc.D)-1] +} + +func (inc *KD) 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 *KD) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.calculateAndUpdate(window) +} + +func (inc *KD) Bind(updater KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} diff --git a/pkg/indicator/kd_callbacks.go b/pkg/indicator/kd_callbacks.go new file mode 100644 index 000000000..fdfce2193 --- /dev/null +++ b/pkg/indicator/kd_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type KD"; DO NOT EDIT. + +package indicator + +import () + +func (K *KD) OnUpdate(cb func(k, d float64)) { + K.UpdateCallbacks = append(K.UpdateCallbacks, cb) +} + +func (K *KD) EmitUpdate(k, d float64) { + for _, cb := range K.UpdateCallbacks { + cb(k, d) + } +} From b82fbbb2ab28e8d85ff9332c8f814b90d6dbdeb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=82=8B=E3=81=BF?= Date: Sat, 22 May 2021 03:28:25 +0800 Subject: [PATCH 12/38] Add pop, max, min, sum, mean and tail methods to Float64Slice --- pkg/indicator/sma.go | 47 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/pkg/indicator/sma.go b/pkg/indicator/sma.go index a44c6e27e..a268ffe27 100644 --- a/pkg/indicator/sma.go +++ b/pkg/indicator/sma.go @@ -2,6 +2,7 @@ package indicator import ( "fmt" + "math" "time" log "github.com/sirupsen/logrus" @@ -15,6 +16,52 @@ 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-1-size:]) + return win +} + var zeroTime time.Time //go:generate callbackgen -type SMA From ec6cbb05aafe366373b36d80471d78061f4a1551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=82=8B=E3=81=BF?= Date: Sat, 22 May 2021 05:00:27 +0800 Subject: [PATCH 13/38] Add kd_test.go --- pkg/indicator/kd_test.go | 66 ++++++++++++++++++++++++++++++++++++++++ pkg/indicator/sma.go | 2 +- 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 pkg/indicator/kd_test.go diff --git a/pkg/indicator/kd_test.go b/pkg/indicator/kd_test.go new file mode 100644 index 000000000..53a07e753 --- /dev/null +++ b/pkg/indicator/kd_test.go @@ -0,0 +1,66 @@ +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 TestKD_update(t *testing.T) { + 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(high, low, close []float64) (kLines []types.KLine) { + for i := range high { + kLines = append(kLines, types.KLine{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(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 := KD{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) + } + }) + } +} diff --git a/pkg/indicator/sma.go b/pkg/indicator/sma.go index a268ffe27..c1250683f 100644 --- a/pkg/indicator/sma.go +++ b/pkg/indicator/sma.go @@ -58,7 +58,7 @@ func (s Float64Slice) Tail(size int) Float64Slice { } win := make(Float64Slice, size) - copy(win, s[length-1-size:]) + copy(win, s[length-size:]) return win } From 50d96f1276bb01b75915e0624379639998db6831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=82=8B=E3=81=BF?= Date: Sat, 22 May 2021 05:22:38 +0800 Subject: [PATCH 14/38] Fix KLineWindow.Tail --- pkg/types/kline.go | 2 +- pkg/types/kline_test.go | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/types/kline.go b/pkg/types/kline.go index b45bb67d0..db76aa8f2 100644 --- a/pkg/types/kline.go +++ b/pkg/types/kline.go @@ -355,7 +355,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 } diff --git a/pkg/types/kline_test.go b/pkg/types/kline_test.go index 3e7af5f07..9cb0c5ac9 100644 --- a/pkg/types/kline_test.go +++ b/pkg/types/kline_test.go @@ -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) { From 25f76235e9f7a43b2edf529d2409be89d1896d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=82=8B=E3=81=BF?= Date: Sat, 22 May 2021 05:26:27 +0800 Subject: [PATCH 15/38] Fix GetHigh, GetLow and Mid --- pkg/types/kline.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/types/kline.go b/pkg/types/kline.go index db76aa8f2..ff5224f8e 100644 --- a/pkg/types/kline.go +++ b/pkg/types/kline.go @@ -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()) } @@ -321,7 +321,7 @@ func (k KLineWindow) Color() string { } 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 From b9ced0955d86a8c5132a3318222b324446b6b221 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=82=8B=E3=81=BF?= Date: Sat, 22 May 2021 05:36:38 +0800 Subject: [PATCH 16/38] Fix test --- pkg/indicator/kd_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/indicator/kd_test.go b/pkg/indicator/kd_test.go index 53a07e753..fba011689 100644 --- a/pkg/indicator/kd_test.go +++ b/pkg/indicator/kd_test.go @@ -18,13 +18,14 @@ 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 TestKD_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(high, low, close []float64) (kLines []types.KLine) { + buildKLines := func(open, high, low, close []float64) (kLines []types.KLine) { for i := range high { - kLines = append(kLines, types.KLine{High: high[i], Low: low[i], Close: close[i], EndTime: time.Now()}) + kLines = append(kLines, types.KLine{Open: open[i], High: high[i], Low: low[i], Close: close[i], EndTime: time.Now()}) } return kLines } @@ -38,7 +39,7 @@ func TestKD_update(t *testing.T) { }{ { name: "TXF1-1min_2016/1/4", - kLines: buildKLines(high, low, close), + kLines: buildKLines(open, high, low, close), window: 14, want_k: 84.210526, want_d: 59.888357, From 0377a7321e92253d32d0cbd92dfc7d7a44a2ed45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=82=8B=E3=81=BF?= Date: Sat, 22 May 2021 05:52:10 +0800 Subject: [PATCH 17/38] Rename KD to STOCH --- pkg/indicator/kd_callbacks.go | 15 --------------- pkg/indicator/{kd.go => stoch.go} | 18 +++++++++--------- pkg/indicator/stoch_callbacks.go | 15 +++++++++++++++ pkg/indicator/{kd_test.go => stoch_test.go} | 4 ++-- 4 files changed, 26 insertions(+), 26 deletions(-) delete mode 100644 pkg/indicator/kd_callbacks.go rename pkg/indicator/{kd.go => stoch.go} (72%) create mode 100644 pkg/indicator/stoch_callbacks.go rename pkg/indicator/{kd_test.go => stoch_test.go} (99%) diff --git a/pkg/indicator/kd_callbacks.go b/pkg/indicator/kd_callbacks.go deleted file mode 100644 index fdfce2193..000000000 --- a/pkg/indicator/kd_callbacks.go +++ /dev/null @@ -1,15 +0,0 @@ -// Code generated by "callbackgen -type KD"; DO NOT EDIT. - -package indicator - -import () - -func (K *KD) OnUpdate(cb func(k, d float64)) { - K.UpdateCallbacks = append(K.UpdateCallbacks, cb) -} - -func (K *KD) EmitUpdate(k, d float64) { - for _, cb := range K.UpdateCallbacks { - cb(k, d) - } -} diff --git a/pkg/indicator/kd.go b/pkg/indicator/stoch.go similarity index 72% rename from pkg/indicator/kd.go rename to pkg/indicator/stoch.go index 3ba50b1bb..24a853be3 100644 --- a/pkg/indicator/kd.go +++ b/pkg/indicator/stoch.go @@ -9,13 +9,13 @@ import ( const DPeriod int = 3 /* -kd implements stochastic oscillator indicator +stoch implements stochastic oscillator indicator Stochastic Oscillator - https://www.investopedia.com/terms/s/stochasticoscillator.asp */ -//go:generate callbackgen -type KD -type KD struct { +//go:generate callbackgen -type STOCH +type STOCH struct { types.IntervalWindow K Float64Slice D Float64Slice @@ -26,7 +26,7 @@ type KD struct { UpdateCallbacks []func(k float64, d float64) } -func (inc *KD) update(kLine types.KLine) { +func (inc *STOCH) update(kLine types.KLine) { inc.KLineWindow.Add(kLine) inc.KLineWindow.Truncate(inc.Window) @@ -40,21 +40,21 @@ func (inc *KD) update(kLine types.KLine) { inc.D.Push(d) } -func (inc *KD) LastK() float64 { +func (inc *STOCH) LastK() float64 { if len(inc.K) == 0 { return 0.0 } return inc.K[len(inc.K)-1] } -func (inc *KD) LastD() float64 { +func (inc *STOCH) LastD() float64 { if len(inc.K) == 0 { return 0.0 } return inc.D[len(inc.D)-1] } -func (inc *KD) calculateAndUpdate(kLines []types.KLine) { +func (inc *STOCH) calculateAndUpdate(kLines []types.KLine) { if len(kLines) < inc.Window || len(kLines) < DPeriod { return } @@ -70,7 +70,7 @@ func (inc *KD) calculateAndUpdate(kLines []types.KLine) { } } -func (inc *KD) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { +func (inc *STOCH) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { if inc.Interval != interval { return } @@ -78,6 +78,6 @@ func (inc *KD) handleKLineWindowUpdate(interval types.Interval, window types.KLi inc.calculateAndUpdate(window) } -func (inc *KD) Bind(updater KLineWindowUpdater) { +func (inc *STOCH) Bind(updater KLineWindowUpdater) { updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) } diff --git a/pkg/indicator/stoch_callbacks.go b/pkg/indicator/stoch_callbacks.go new file mode 100644 index 000000000..dcc07e76a --- /dev/null +++ b/pkg/indicator/stoch_callbacks.go @@ -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) + } +} diff --git a/pkg/indicator/kd_test.go b/pkg/indicator/stoch_test.go similarity index 99% rename from pkg/indicator/kd_test.go rename to pkg/indicator/stoch_test.go index fba011689..d84876cd7 100644 --- a/pkg/indicator/kd_test.go +++ b/pkg/indicator/stoch_test.go @@ -17,7 +17,7 @@ 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 TestKD_update(t *testing.T) { +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} @@ -48,7 +48,7 @@ func TestKD_update(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - kd := KD{IntervalWindow: types.IntervalWindow{Window: tt.window}} + kd := STOCH{IntervalWindow: types.IntervalWindow{Window: tt.window}} kd.calculateAndUpdate(tt.kLines) got_k := kd.LastK() From 56b2c8845bd0e937cfc3b0491c7d2280ced16a68 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 22 May 2021 11:36:58 +0800 Subject: [PATCH 18/38] fix preorder, postorder and inorder --- examples/binance-book/main.go | 4 +- pkg/bbgo/marketdatastore.go | 6 +- pkg/cmd/orderbook.go | 4 +- pkg/exchange/binance/parse.go | 2 +- pkg/exchange/ftx/websocket_messages.go | 8 +- pkg/exchange/max/maxapi/public_parser.go | 2 +- pkg/types/orderbook.go | 228 +++-------------------- pkg/types/orderbook_callbacks.go | 25 --- pkg/types/orderbook_test.go | 14 +- pkg/types/rbtorderbook.go | 132 +++++++++++++ pkg/types/rbtree.go | 30 ++- pkg/types/sliceorderbook.go | 174 +++++++++++++++++ pkg/types/standardstream_callbacks.go | 12 +- pkg/types/stream.go | 4 +- 14 files changed, 377 insertions(+), 268 deletions(-) delete mode 100644 pkg/types/orderbook_callbacks.go create mode 100644 pkg/types/rbtorderbook.go create mode 100644 pkg/types/sliceorderbook.go diff --git a/examples/binance-book/main.go b/examples/binance-book/main.go index 570bb8aa2..f59f4db79 100644 --- a/examples/binance-book/main.go +++ b/examples/binance-book/main.go @@ -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) }) diff --git a/pkg/bbgo/marketdatastore.go b/pkg/bbgo/marketdatastore.go index 801bc6d90..f85341e4a 100644 --- a/pkg/bbgo/marketdatastore.go +++ b/pkg/bbgo/marketdatastore.go @@ -32,7 +32,7 @@ func (store *MarketDataStore) SetKLineWindows(windows map[types.Interval]types.K store.KLineWindows = windows } -func (store *MarketDataStore) OrderBook() types.OrderBook { +func (store *MarketDataStore) OrderBook() types.SliceOrderBook { return store.orderBook.Copy() } @@ -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 } diff --git a/pkg/cmd/orderbook.go b/pkg/cmd/orderbook.go index 82be530da..d89e5af0b 100644 --- a/pkg/cmd/orderbook.go +++ b/pkg/cmd/orderbook.go @@ -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()) }) diff --git a/pkg/exchange/binance/parse.go b/pkg/exchange/binance/parse.go index 7923f8955..b669b0b7b 100644 --- a/pkg/exchange/binance/parse.go +++ b/pkg/exchange/binance/parse.go @@ -319,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 { diff --git a/pkg/exchange/ftx/websocket_messages.go b/pkg/exchange/ftx/websocket_messages.go index 6ae213f7f..b943bb38f 100644 --- a/pkg/exchange/ftx/websocket_messages.go +++ b/pkg/exchange/ftx/websocket_messages.go @@ -351,16 +351,16 @@ func checksumString(bids, asks [][]json.Number) string { var errUnmatchedChecksum = fmt.Errorf("unmatched checksum") -func toGlobalOrderBook(r orderBookResponse) (types.OrderBook, error) { +func toGlobalOrderBook(r orderBookResponse) (types.SliceOrderBook, error) { bids, err := toPriceVolumeSlice(r.Bids) if err != nil { - return types.OrderBook{}, fmt.Errorf("can't convert bids to priceVolumeSlice: %w", err) + return types.SliceOrderBook{}, fmt.Errorf("can't convert bids to priceVolumeSlice: %w", err) } asks, err := toPriceVolumeSlice(r.Asks) if err != nil { - return types.OrderBook{}, fmt.Errorf("can't convert asks to priceVolumeSlice: %w", err) + return types.SliceOrderBook{}, fmt.Errorf("can't convert asks to priceVolumeSlice: %w", err) } - return types.OrderBook{ + return types.SliceOrderBook{ // ex. BTC/USDT Symbol: toGlobalSymbol(strings.ToUpper(r.Market)), Bids: bids, diff --git a/pkg/exchange/max/maxapi/public_parser.go b/pkg/exchange/max/maxapi/public_parser.go index e1a0d0ae8..39a055fa6 100644 --- a/pkg/exchange/max/maxapi/public_parser.go +++ b/pkg/exchange/max/maxapi/public_parser.go @@ -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 { diff --git a/pkg/types/orderbook.go b/pkg/types/orderbook.go index 4a2de2057..5e885376a 100644 --- a/pkg/types/orderbook.go +++ b/pkg/types/orderbook.go @@ -2,11 +2,8 @@ package types import ( "fmt" - "strings" "sync" - "github.com/pkg/errors" - "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/sigchan" ) @@ -20,236 +17,55 @@ func (p PriceVolume) String() string { return fmt.Sprintf("PriceVolume{ price: %f, volume: %f }", p.Price.Float64(), p.Volume.Float64()) } -//go:generate callbackgen -type RBOrderBook -type RBOrderBook struct { - Symbol string - Bids *RBTree - Asks *RBTree - - loadCallbacks []func(book *RBOrderBook) - updateCallbacks []func(book *RBOrderBook) -} - -func NewRBOrderBook(symbol string) *RBOrderBook { - return &RBOrderBook{ - Symbol: symbol, - Bids: NewRBTree(), - Asks: NewRBTree(), - } -} - -func (b *RBOrderBook) BestBid() (PriceVolume, bool) { - return PriceVolume{}, true -} - -func (b *RBOrderBook) BestAsk() (PriceVolume, bool) { - return PriceVolume{}, true -} - -//go:generate callbackgen -type OrderBook -type OrderBook struct { - Symbol string - Bids PriceVolumeSlice - Asks PriceVolumeSlice - - loadCallbacks []func(book *OrderBook) - updateCallbacks []func(book *OrderBook) -} - -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) - } - } -} - -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) - } - } -} - -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) } type MutexOrderBook struct { sync.Mutex - *OrderBook + *SliceOrderBook } func NewMutexOrderBook(symbol string) *MutexOrderBook { return &MutexOrderBook{ - OrderBook: &OrderBook{Symbol: symbol}, + SliceOrderBook: &SliceOrderBook{Symbol: symbol}, } } -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.SliceOrderBook.Load(book) + b.Unlock() } func (b *MutexOrderBook) Reset() { b.Lock() - b.OrderBook.Reset() + b.SliceOrderBook.Reset() b.Unlock() } -func (b *MutexOrderBook) CopyDepth(depth int) OrderBook { +func (b *MutexOrderBook) CopyDepth(depth int) SliceOrderBook { b.Lock() defer b.Unlock() - return b.OrderBook.CopyDepth(depth) + return b.SliceOrderBook.CopyDepth(depth) } -func (b *MutexOrderBook) Get() OrderBook { +func (b *MutexOrderBook) Get() SliceOrderBook { b.Lock() defer b.Unlock() - return b.OrderBook.Copy() + return b.SliceOrderBook.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.SliceOrderBook.Update(update) + b.Unlock() } // StreamOrderBook receives streaming data from websocket connection and @@ -268,7 +84,7 @@ func NewStreamBook(symbol string) *StreamOrderBook { } func (sb *StreamOrderBook) BindStream(stream Stream) { - stream.OnBookSnapshot(func(book OrderBook) { + stream.OnBookSnapshot(func(book SliceOrderBook) { if sb.Symbol != book.Symbol { return } @@ -277,7 +93,7 @@ func (sb *StreamOrderBook) BindStream(stream Stream) { sb.C.Emit() }) - stream.OnBookUpdate(func(book OrderBook) { + stream.OnBookUpdate(func(book SliceOrderBook) { if sb.Symbol != book.Symbol { return } diff --git a/pkg/types/orderbook_callbacks.go b/pkg/types/orderbook_callbacks.go deleted file mode 100644 index a3e532c60..000000000 --- a/pkg/types/orderbook_callbacks.go +++ /dev/null @@ -1,25 +0,0 @@ -// Code generated by "callbackgen -type OrderBook"; DO NOT EDIT. - -package types - -import () - -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) - } -} diff --git a/pkg/types/orderbook_test.go b/pkg/types/orderbook_test.go index c565607f8..098343775 100644 --- a/pkg/types/orderbook_test.go +++ b/pkg/types/orderbook_test.go @@ -24,7 +24,7 @@ func BenchmarkOrderBook_Load(b *testing.B) { bids = append(bids, PriceVolume{fixedpoint.NewFromFloat(1000 - 0.1 - p), fixedpoint.NewFromFloat(1)}) } - b.Run("RBOrderBook", func(b *testing.B) { + b.Run("RBTOrderBook", func(b *testing.B) { book := NewRBOrderBook("ETHUSDT") for i := 0; i < b.N; i++ { for _, ask := range asks { @@ -36,8 +36,8 @@ func BenchmarkOrderBook_Load(b *testing.B) { } }) - b.Run("OrderBook", func(b *testing.B) { - book := &OrderBook{} + b.Run("SliceOrderBook", func(b *testing.B) { + book := &SliceOrderBook{} for i := 0; i < b.N; i++ { for _, ask := range asks { book.Asks = book.Asks.Upsert(ask, false) @@ -64,7 +64,7 @@ func BenchmarkOrderBook_UpdateAndInsert(b *testing.B) { rbBook.Bids.Upsert(bid.Price, bid.Volume) } - b.Run("RBOrderBook", func(b *testing.B) { + 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) { @@ -75,7 +75,7 @@ func BenchmarkOrderBook_UpdateAndInsert(b *testing.B) { } }) - sliceBook := &OrderBook{} + sliceBook := &SliceOrderBook{} for i := 0; i < b.N; i++ { for _, ask := range asks { sliceBook.Asks = sliceBook.Asks.Upsert(ask, false) @@ -84,7 +84,7 @@ func BenchmarkOrderBook_UpdateAndInsert(b *testing.B) { sliceBook.Bids = sliceBook.Bids.Upsert(bid, true) } } - b.Run("OrderBook", func(b *testing.B) { + b.Run("SliceOrderBook", func(b *testing.B) { for i := 0; i < b.N; i++ { var price = fixedpoint.NewFromFloat(rand.Float64() * 2000.0) if price >= fixedpoint.NewFromFloat(1000) { @@ -97,7 +97,7 @@ func BenchmarkOrderBook_UpdateAndInsert(b *testing.B) { } 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)}, diff --git a/pkg/types/rbtorderbook.go b/pkg/types/rbtorderbook.go new file mode 100644 index 000000000..c54c545b4 --- /dev/null +++ b/pkg/types/rbtorderbook.go @@ -0,0 +1,132 @@ +package types + +import ( + "fmt" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/pkg/errors" +) + +//go:generate callbackgen -type RBTOrderBook +type RBTOrderBook struct { + Symbol string + Bids *RBTree + Asks *RBTree + + loadCallbacks []func(book *RBTOrderBook) + updateCallbacks []func(book *RBTOrderBook) +} + +func NewRBOrderBook(symbol string) *RBTOrderBook { + return &RBTOrderBook{ + Symbol: symbol, + Bids: NewRBTree(), + Asks: NewRBTree(), + } +} + +func (b *RBTOrderBook) BestBid() (PriceVolume, bool) { + right := b.Bids.Rightmost(b.Bids.Root) + if right != nil { + return PriceVolume{Price: right.Key, Volume: right.Value}, true + } + return PriceVolume{}, false +} + +func (b *RBTOrderBook) BestAsk() (PriceVolume, bool) { + left := b.Asks.Leftmost(b.Bids.Root) + if left != nil { + return PriceVolume{Price: left.Key, Volume: left.Value}, true + } + return PriceVolume{}, false +} + +func (b *RBTOrderBook) Spread() (fixedpoint.Value, bool) { + bestBid, ok := b.BestBid() + if !ok { + return 0, false + } + + bestAsk, ok := b.BestAsk() + if !ok { + return 0, false + } + + return bestAsk.Price - bestBid.Price, true +} + +func (b *RBTOrderBook) IsValid() (bool, error) { + bid, hasBid := b.BestBid() + ask, hasAsk := b.BestAsk() + + if !hasBid { + return false, errors.New("empty bids") + } + + if !hasAsk { + return false, errors.New("empty asks") + } + + if bid.Price > ask.Price { + return false, fmt.Errorf("bid price %f > ask price %f", bid.Price.Float64(), ask.Price.Float64()) + } + + return true, nil +} + +func (b *RBTOrderBook) Load(book SliceOrderBook) { + b.Reset() + b.update(book) + b.EmitLoad(b) +} + +func (b *RBTOrderBook) Update(book SliceOrderBook) { + b.update(book) + b.EmitUpdate(b) +} + +func (b *RBTOrderBook) Reset() { + b.Bids = NewRBTree() + b.Asks = NewRBTree() +} + +func (b *RBTOrderBook) updateAsks(pvs PriceVolumeSlice) { + for _, pv := range pvs { + if pv.Volume == 0 { + b.Asks.Delete(pv.Price) + } else { + b.Asks.Upsert(pv.Price, pv.Volume) + } + } +} + +func (b *RBTOrderBook) updateBids(pvs PriceVolumeSlice) { + for _, pv := range pvs { + if pv.Volume == 0 { + b.Bids.Delete(pv.Price) + } else { + b.Bids.Upsert(pv.Price, pv.Volume) + } + } +} + +func (b *RBTOrderBook) update(book SliceOrderBook) { + b.updateBids(book.Bids) + b.updateAsks(book.Asks) +} + +func (b *RBTOrderBook) load(book SliceOrderBook) { + b.Reset() + b.updateBids(book.Bids) + b.updateAsks(book.Asks) +} + +func (b *RBTOrderBook) Print() { + b.Bids.PostorderOf(b.Bids.Root, func(n *RBNode) { + fmt.Printf("bid: %f x %f", n.Key.Float64(), n.Value.Float64()) + }) + + b.Asks.PostorderOf(b.Asks.Root, func(n *RBNode) { + fmt.Printf("ask: %f x %f", n.Key.Float64(), n.Value.Float64()) + }) +} diff --git a/pkg/types/rbtree.go b/pkg/types/rbtree.go index 3208d84a0..5c6c91cff 100644 --- a/pkg/types/rbtree.go +++ b/pkg/types/rbtree.go @@ -341,26 +341,38 @@ func (tree *RBTree) Successor(current *RBNode) *RBNode { return newNode } -func (tree *RBTree) Preorder(current *RBNode, cb func(n *RBNode)) { +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 != nil { cb(current) - tree.Preorder(current.Left, cb) - tree.Preorder(current.Right, cb) + tree.PreorderOf(current.Left, cb) + tree.PreorderOf(current.Right, cb) } } -func (tree *RBTree) Inorder(current *RBNode, cb func(n *RBNode)) { +func (tree *RBTree) Inorder(cb func(n *RBNode)) { + tree.InorderOf(tree.Root, cb) +} + +func (tree *RBTree) InorderOf(current *RBNode, cb func(n *RBNode)) { if current != nil { - tree.Preorder(current.Left, cb) + tree.InorderOf(current.Left, cb) cb(current) - tree.Preorder(current.Right, cb) + tree.InorderOf(current.Right, cb) } } -func (tree *RBTree) Postorder(current *RBNode, cb func(n *RBNode)) { +func (tree *RBTree) Postorder(cb func(n *RBNode)) { + tree.PostorderOf(tree.Root, cb) +} + +func (tree *RBTree) PostorderOf(current *RBNode, cb func(n *RBNode)) { if current != nil { - tree.Preorder(current.Left, cb) - tree.Preorder(current.Right, cb) + tree.PostorderOf(current.Left, cb) + tree.PostorderOf(current.Right, cb) cb(current) } } diff --git a/pkg/types/sliceorderbook.go b/pkg/types/sliceorderbook.go new file mode 100644 index 000000000..70a45da15 --- /dev/null +++ b/pkg/types/sliceorderbook.go @@ -0,0 +1,174 @@ +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 (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) 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) CopyDepth(depth int) (book SliceOrderBook) { + book = *b + book.Bids = book.Bids.CopyDepth(depth) + book.Asks = book.Asks.CopyDepth(depth) + return book +} + +func (b *SliceOrderBook) Copy() (book SliceOrderBook) { + book = *b + book.Bids = book.Bids.Copy() + book.Asks = book.Asks.Copy() + return book +} + +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() +} diff --git a/pkg/types/standardstream_callbacks.go b/pkg/types/standardstream_callbacks.go index 1d779564d..0f334fd21 100644 --- a/pkg/types/standardstream_callbacks.go +++ b/pkg/types/standardstream_callbacks.go @@ -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)) } diff --git a/pkg/types/stream.go b/pkg/types/stream.go index c6a727c6e..a2c0ddec5 100644 --- a/pkg/types/stream.go +++ b/pkg/types/stream.go @@ -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) { From fd710d533fa65a222b81acec17b990aecccdb448 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 22 May 2021 12:18:08 +0800 Subject: [PATCH 19/38] implement tree copy method --- examples/binance-book/main.go | 2 +- pkg/bbgo/twap_order_executor.go | 4 +- pkg/strategy/gap/strategy.go | 4 +- pkg/strategy/mirrormaker/main.go | 2 +- pkg/strategy/xmaker/strategy.go | 4 +- pkg/types/orderbook.go | 2 +- pkg/types/rbtree.go | 71 +++++++++++++++++++++++--------- pkg/types/rbtree_test.go | 26 ++++++++++++ 8 files changed, 86 insertions(+), 29 deletions(-) diff --git a/examples/binance-book/main.go b/examples/binance-book/main.go index f59f4db79..257e2eadc 100644 --- a/examples/binance-book/main.go +++ b/examples/binance-book/main.go @@ -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) diff --git a/pkg/bbgo/twap_order_executor.go b/pkg/bbgo/twap_order_executor.go index 9ed90a51f..cecfcea28 100644 --- a/pkg/bbgo/twap_order_executor.go +++ b/pkg/bbgo/twap_order_executor.go @@ -64,7 +64,7 @@ func (e *TwapExecution) connectUserData(ctx context.Context) { } func (e *TwapExecution) getSideBook() (pvs types.PriceVolumeSlice, err error) { - book := e.orderBook.Get() + book := e.orderBook.Copy() switch e.Side { case types.SideTypeSell: @@ -81,7 +81,7 @@ func (e *TwapExecution) getSideBook() (pvs types.PriceVolumeSlice, err error) { } func (e *TwapExecution) newBestPriceOrder() (orderForm types.SubmitOrder, err error) { - book := e.orderBook.Get() + book := e.orderBook.Copy() sideBook, err := e.getSideBook() if err != nil { diff --git a/pkg/strategy/gap/strategy.go b/pkg/strategy/gap/strategy.go index 248adbaac..cae7e4a86 100644 --- a/pkg/strategy/gap/strategy.go +++ b/pkg/strategy/gap/strategy.go @@ -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() diff --git a/pkg/strategy/mirrormaker/main.go b/pkg/strategy/mirrormaker/main.go index e641b62cc..8d14fe572 100644 --- a/pkg/strategy/mirrormaker/main.go +++ b/pkg/strategy/mirrormaker/main.go @@ -83,7 +83,7 @@ func (s *Strategy) updateQuote(ctx context.Context) { // avoid unlock issue time.Sleep(100 * time.Millisecond) - sourceBook := s.book.Get() + sourceBook := s.book.Copy() if len(sourceBook.Bids) == 0 || len(sourceBook.Asks) == 0 { return } diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index 704d79fa9..fd9091f34 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -184,7 +184,7 @@ func (s *Strategy) updateQuote(ctx context.Context, orderExecutionRouter bbgo.Or return } - sourceBook := s.book.Get() + sourceBook := s.book.Copy() if len(sourceBook.Bids) == 0 || len(sourceBook.Asks) == 0 { return } @@ -443,7 +443,7 @@ 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: diff --git a/pkg/types/orderbook.go b/pkg/types/orderbook.go index 5e885376a..0ea9d2e02 100644 --- a/pkg/types/orderbook.go +++ b/pkg/types/orderbook.go @@ -56,7 +56,7 @@ func (b *MutexOrderBook) CopyDepth(depth int) SliceOrderBook { return b.SliceOrderBook.CopyDepth(depth) } -func (b *MutexOrderBook) Get() SliceOrderBook { +func (b *MutexOrderBook) Copy() SliceOrderBook { b.Lock() defer b.Unlock() return b.SliceOrderBook.Copy() diff --git a/pkg/types/rbtree.go b/pkg/types/rbtree.go index 5c6c91cff..3a9e2a721 100644 --- a/pkg/types/rbtree.go +++ b/pkg/types/rbtree.go @@ -4,20 +4,20 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" ) +var Neel = &RBNode{ + Color: Black, +} + type RBTree struct { - Root, Neel *RBNode + Root *RBNode } func NewRBTree() *RBTree { - var neel = &RBNode{ - Color: Black, - } - var root = neel - root.Parent = neel + var root = Neel + root.Parent = Neel return &RBTree{ Root: root, - Neel: neel, } } @@ -31,13 +31,13 @@ func (tree *RBTree) Delete(key fixedpoint.Value) bool { // x (the child of the deleted node) var x, y *RBNode - if del.Left == tree.Neel || del.Right == tree.Neel { + if del.Left == Neel || del.Right == Neel { y = del } else { y = tree.Successor(del) } - if y.Left != tree.Neel { + if y.Left != Neel { x = y.Left } else { x = y.Right @@ -45,7 +45,7 @@ func (tree *RBTree) Delete(key fixedpoint.Value) bool { x.Parent = y.Parent - if y.Parent == tree.Neel { + if y.Parent == Neel { tree.Root = x } else if y == y.Parent.Left { y.Parent.Left = x @@ -129,7 +129,7 @@ func (tree *RBTree) DeleteFixup(current *RBNode) { } func (tree *RBTree) Upsert(key, val fixedpoint.Value) { - var y = tree.Neel + var y = Neel var x = tree.Root var node = &RBNode{ Key: key, @@ -137,7 +137,7 @@ func (tree *RBTree) Upsert(key, val fixedpoint.Value) { Color: Red, } - for x != tree.Neel { + for x != Neel { y = x if node.Key == x.Key { @@ -153,7 +153,7 @@ func (tree *RBTree) Upsert(key, val fixedpoint.Value) { node.Parent = y - if y == tree.Neel { + if y == Neel { tree.Root = node } else if node.Key < y.Key { y.Left = node @@ -161,15 +161,15 @@ func (tree *RBTree) Upsert(key, val fixedpoint.Value) { y.Right = node } - node.Left = tree.Neel - node.Right = tree.Neel + node.Left = Neel + node.Right = Neel node.Color = Red tree.InsertFixup(node) } func (tree *RBTree) Insert(key, val fixedpoint.Value) { - var y = tree.Neel + var y = Neel var x = tree.Root var node = &RBNode{ Key: key, @@ -177,7 +177,7 @@ func (tree *RBTree) Insert(key, val fixedpoint.Value) { Color: Red, } - for x != tree.Neel { + for x != Neel { y = x if node.Key < x.Key { @@ -189,7 +189,7 @@ func (tree *RBTree) Insert(key, val fixedpoint.Value) { node.Parent = y - if y == tree.Neel { + if y == Neel { tree.Root = node } else if node.Key < y.Key { y.Left = node @@ -197,8 +197,8 @@ func (tree *RBTree) Insert(key, val fixedpoint.Value) { y.Right = node } - node.Left = tree.Neel - node.Right = tree.Neel + node.Left = Neel + node.Right = Neel node.Color = Red tree.InsertFixup(node) @@ -353,6 +353,7 @@ func (tree *RBTree) PreorderOf(current *RBNode, cb func(n *RBNode)) { } } +// Inorder traverses the tree in ascending order func (tree *RBTree) Inorder(cb func(n *RBNode)) { tree.InorderOf(tree.Root, cb) } @@ -365,6 +366,19 @@ func (tree *RBTree) InorderOf(current *RBNode, cb func(n *RBNode)) { } } +// InorderReverse traverses the tree in descending order +func (tree *RBTree) InorderReverse(cb func(n *RBNode)) { + tree.InorderReverseOf(tree.Root, cb) +} + +func (tree *RBTree) InorderReverseOf(current *RBNode, cb func(n *RBNode)) { + if current != nil { + tree.InorderOf(current.Right, cb) + cb(current) + tree.InorderOf(current.Left, cb) + } +} + func (tree *RBTree) Postorder(cb func(n *RBNode)) { tree.PostorderOf(tree.Root, cb) } @@ -376,3 +390,20 @@ func (tree *RBTree) PostorderOf(current *RBNode, cb func(n *RBNode)) { cb(current) } } + +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) Copy() *RBTree { + newTree := NewRBTree() + newTree.Root = copyNode(tree.Root) + return newTree +} diff --git a/pkg/types/rbtree_test.go b/pkg/types/rbtree_test.go index 7684c7fb2..4ebe5cea9 100644 --- a/pkg/types/rbtree_test.go +++ b/pkg/types/rbtree_test.go @@ -7,6 +7,31 @@ import ( "github.com/stretchr/testify/assert" ) +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)) @@ -34,4 +59,5 @@ func TestTree(t *testing.T) { deleted = tree.Delete(fixedpoint.NewFromFloat(1500.0)) assert.True(t, deleted) + } From cca37d309a4a528c56f9c71a594d6b0d977bcf05 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 22 May 2021 14:57:14 +0800 Subject: [PATCH 20/38] fix rbtree iteration --- pkg/types/rbtorderbook.go | 6 ++- pkg/types/rbtree.go | 105 ++++++++++++++++++++++++++++++-------- pkg/types/rbtree_test.go | 24 +++++++++ 3 files changed, 111 insertions(+), 24 deletions(-) diff --git a/pkg/types/rbtorderbook.go b/pkg/types/rbtorderbook.go index c54c545b4..4fafc6064 100644 --- a/pkg/types/rbtorderbook.go +++ b/pkg/types/rbtorderbook.go @@ -122,11 +122,13 @@ func (b *RBTOrderBook) load(book SliceOrderBook) { } func (b *RBTOrderBook) Print() { - b.Bids.PostorderOf(b.Bids.Root, func(n *RBNode) { + b.Bids.PostorderOf(b.Bids.Root, func(n *RBNode) bool { fmt.Printf("bid: %f x %f", n.Key.Float64(), n.Value.Float64()) + return true }) - b.Asks.PostorderOf(b.Asks.Root, func(n *RBNode) { + b.Asks.PostorderOf(b.Asks.Root, func(n *RBNode) bool { fmt.Printf("ask: %f x %f", n.Key.Float64(), n.Value.Float64()) + return true }) } diff --git a/pkg/types/rbtree.go b/pkg/types/rbtree.go index 3a9e2a721..2ededb49b 100644 --- a/pkg/types/rbtree.go +++ b/pkg/types/rbtree.go @@ -1,6 +1,8 @@ package types import ( + "fmt" + "github.com/c9s/bbgo/pkg/fixedpoint" ) @@ -10,6 +12,8 @@ var Neel = &RBNode{ type RBTree struct { Root *RBNode + + size int } func NewRBTree() *RBTree { @@ -61,6 +65,8 @@ func (tree *RBTree) Delete(key fixedpoint.Value) bool { tree.DeleteFixup(x) } + tree.size-- + return true } @@ -200,22 +206,33 @@ func (tree *RBTree) Insert(key, val fixedpoint.Value) { 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 != nil && key != current.Key { + 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 { @@ -271,13 +288,13 @@ func (tree *RBTree) RotateLeft(x *RBNode) { var y = x.Right x.Right = y.Left - if y.Left != nil { + if y.Left != Neel { y.Left.Parent = x } y.Parent = x.Parent - if x.Parent == nil { + if x.Parent == Neel { tree.Root = y } else if x == x.Parent.Left { x.Parent.Left = y @@ -293,13 +310,13 @@ func (tree *RBTree) RotateRight(y *RBNode) { x := y.Left y.Left = x.Right - if x.Right != nil { + if x.Right != Neel { x.Right.Parent = y } x.Parent = y.Parent - if y.Parent == nil { + if y.Parent == Neel { tree.Root = x } else if y == y.Parent.Left { y.Parent.Left = x @@ -312,7 +329,7 @@ func (tree *RBTree) RotateRight(y *RBNode) { } func (tree *RBTree) Rightmost(current *RBNode) *RBNode { - for current.Right != nil { + for current.Right != Neel { current = current.Right } @@ -320,7 +337,7 @@ func (tree *RBTree) Rightmost(current *RBNode) *RBNode { } func (tree *RBTree) Leftmost(current *RBNode) *RBNode { - for current.Left != nil { + for current.Left != Neel { current = current.Left } @@ -328,12 +345,12 @@ func (tree *RBTree) Leftmost(current *RBNode) *RBNode { } func (tree *RBTree) Successor(current *RBNode) *RBNode { - if current.Right != nil { + if current.Right != Neel { return tree.Leftmost(current.Right) } var newNode = current.Parent - for newNode != nil && current == newNode.Right { + for newNode != Neel && current == newNode.Right { current = newNode newNode = newNode.Parent } @@ -346,7 +363,7 @@ func (tree *RBTree) Preorder(cb func(n *RBNode)) { } func (tree *RBTree) PreorderOf(current *RBNode, cb func(n *RBNode)) { - if current != nil { + if current != Neel { cb(current) tree.PreorderOf(current.Left, cb) tree.PreorderOf(current.Right, cb) @@ -354,40 +371,46 @@ func (tree *RBTree) PreorderOf(current *RBNode, cb func(n *RBNode)) { } // Inorder traverses the tree in ascending order -func (tree *RBTree) Inorder(cb func(n *RBNode)) { +func (tree *RBTree) Inorder(cb func(n *RBNode) bool) { tree.InorderOf(tree.Root, cb) } -func (tree *RBTree) InorderOf(current *RBNode, cb func(n *RBNode)) { - if current != nil { +func (tree *RBTree) InorderOf(current *RBNode, cb func(n *RBNode) bool) { + if current != Neel { tree.InorderOf(current.Left, cb) - cb(current) + if !cb(current) { + return + } tree.InorderOf(current.Right, cb) } } // InorderReverse traverses the tree in descending order -func (tree *RBTree) InorderReverse(cb func(n *RBNode)) { +func (tree *RBTree) InorderReverse(cb func(n *RBNode) bool) { tree.InorderReverseOf(tree.Root, cb) } -func (tree *RBTree) InorderReverseOf(current *RBNode, cb func(n *RBNode)) { +func (tree *RBTree) InorderReverseOf(current *RBNode, cb func(n *RBNode) bool) { if current != nil { - tree.InorderOf(current.Right, cb) - cb(current) - tree.InorderOf(current.Left, cb) + tree.InorderReverseOf(current.Right, cb) + if !cb(current) { + return + } + tree.InorderReverseOf(current.Left, cb) } } -func (tree *RBTree) Postorder(cb func(n *RBNode)) { +func (tree *RBTree) Postorder(cb func(n *RBNode) bool) { tree.PostorderOf(tree.Root, cb) } -func (tree *RBTree) PostorderOf(current *RBNode, cb func(n *RBNode)) { +func (tree *RBTree) PostorderOf(current *RBNode, cb func(n *RBNode) bool) { if current != nil { tree.PostorderOf(current.Left, cb) tree.PostorderOf(current.Right, cb) - cb(current) + if !cb(current) { + return + } } } @@ -402,6 +425,44 @@ func copyNode(node *RBNode) *RBNode { 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) diff --git a/pkg/types/rbtree_test.go b/pkg/types/rbtree_test.go index 4ebe5cea9..bc2170075 100644 --- a/pkg/types/rbtree_test.go +++ b/pkg/types/rbtree_test.go @@ -7,6 +7,30 @@ import ( "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)) From 8acada76a9186b73213a77d41fa708f047028ba9 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 22 May 2021 16:32:29 +0800 Subject: [PATCH 21/38] replace sliceorderbook with orderbook interface --- README.md | 1 - pkg/bbgo/marketdatastore.go | 2 +- pkg/bbgo/twap_order_executor.go | 13 +- pkg/cmd/builtin.go | 1 - pkg/strategy/mirrormaker/main.go | 310 ---------------------------- pkg/strategy/xmaker/strategy.go | 28 +-- pkg/strategy/xpuremaker/strategy.go | 2 +- pkg/types/orderbook.go | 36 +++- pkg/types/orderbook_test.go | 4 +- pkg/types/rbtorderbook.go | 58 +++++- pkg/types/sliceorderbook.go | 50 +++-- 11 files changed, 131 insertions(+), 374 deletions(-) delete mode 100644 pkg/strategy/mirrormaker/main.go diff --git a/README.md b/README.md index 6a01d718c..48c721b13 100644 --- a/README.md +++ b/README.md @@ -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" diff --git a/pkg/bbgo/marketdatastore.go b/pkg/bbgo/marketdatastore.go index f85341e4a..ef0ecebb3 100644 --- a/pkg/bbgo/marketdatastore.go +++ b/pkg/bbgo/marketdatastore.go @@ -32,7 +32,7 @@ func (store *MarketDataStore) SetKLineWindows(windows map[types.Interval]types.K store.KLineWindows = windows } -func (store *MarketDataStore) OrderBook() types.SliceOrderBook { +func (store *MarketDataStore) OrderBook() types.OrderBook { return store.orderBook.Copy() } diff --git a/pkg/bbgo/twap_order_executor.go b/pkg/bbgo/twap_order_executor.go index cecfcea28..c1440a9e1 100644 --- a/pkg/bbgo/twap_order_executor.go +++ b/pkg/bbgo/twap_order_executor.go @@ -65,18 +65,7 @@ func (e *TwapExecution) connectUserData(ctx context.Context) { func (e *TwapExecution) getSideBook() (pvs types.PriceVolumeSlice, err error) { book := e.orderBook.Copy() - - 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) - } - + pvs = book.SideBook(e.Side) return pvs, err } diff --git a/pkg/cmd/builtin.go b/pkg/cmd/builtin.go index 77052b761..092bfc705 100644 --- a/pkg/cmd/builtin.go +++ b/pkg/cmd/builtin.go @@ -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" diff --git a/pkg/strategy/mirrormaker/main.go b/pkg/strategy/mirrormaker/main.go deleted file mode 100644 index 8d14fe572..000000000 --- a/pkg/strategy/mirrormaker/main.go +++ /dev/null @@ -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.Copy() - 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 -} diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index fd9091f34..6106b995f 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -185,10 +185,6 @@ func (s *Strategy) updateQuote(ctx context.Context, orderExecutionRouter bbgo.Or } sourceBook := s.book.Copy() - if len(sourceBook.Bids) == 0 || len(sourceBook.Asks) == 0 { - return - } - 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) @@ -447,19 +446,14 @@ func (s *Strategy) Hedge(ctx context.Context, pos fixedpoint.Value) { 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) diff --git a/pkg/strategy/xpuremaker/strategy.go b/pkg/strategy/xpuremaker/strategy.go index 711680fb8..ebbd2792f 100644 --- a/pkg/strategy/xpuremaker/strategy.go +++ b/pkg/strategy/xpuremaker/strategy.go @@ -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 diff --git a/pkg/types/orderbook.go b/pkg/types/orderbook.go index 0ea9d2e02..8b7f30feb 100644 --- a/pkg/types/orderbook.go +++ b/pkg/types/orderbook.go @@ -2,6 +2,8 @@ package types import ( "fmt" + "os" + "strconv" "sync" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -24,47 +26,59 @@ type OrderBook interface { 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 - *SliceOrderBook + 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{ - SliceOrderBook: &SliceOrderBook{Symbol: symbol}, + Symbol: symbol, + OrderBook: book, } } func (b *MutexOrderBook) Load(book SliceOrderBook) { b.Lock() - b.SliceOrderBook.Load(book) + b.OrderBook.Load(book) b.Unlock() } func (b *MutexOrderBook) Reset() { b.Lock() - b.SliceOrderBook.Reset() + b.OrderBook.Reset() b.Unlock() } -func (b *MutexOrderBook) CopyDepth(depth int) SliceOrderBook { +func (b *MutexOrderBook) CopyDepth(depth int) OrderBook { b.Lock() defer b.Unlock() - return b.SliceOrderBook.CopyDepth(depth) + return b.OrderBook.CopyDepth(depth) } -func (b *MutexOrderBook) Copy() SliceOrderBook { +func (b *MutexOrderBook) Copy() OrderBook { b.Lock() defer b.Unlock() - return b.SliceOrderBook.Copy() + return b.OrderBook.Copy() } func (b *MutexOrderBook) Update(update SliceOrderBook) { b.Lock() - b.SliceOrderBook.Update(update) + b.OrderBook.Update(update) b.Unlock() } @@ -85,7 +99,7 @@ func NewStreamBook(symbol string) *StreamOrderBook { func (sb *StreamOrderBook) BindStream(stream Stream) { stream.OnBookSnapshot(func(book SliceOrderBook) { - if sb.Symbol != book.Symbol { + if sb.MutexOrderBook.Symbol != book.Symbol { return } @@ -94,7 +108,7 @@ func (sb *StreamOrderBook) BindStream(stream Stream) { }) stream.OnBookUpdate(func(book SliceOrderBook) { - if sb.Symbol != book.Symbol { + if sb.MutexOrderBook.Symbol != book.Symbol { return } diff --git a/pkg/types/orderbook_test.go b/pkg/types/orderbook_test.go index 098343775..d10515acf 100644 --- a/pkg/types/orderbook_test.go +++ b/pkg/types/orderbook_test.go @@ -36,7 +36,7 @@ func BenchmarkOrderBook_Load(b *testing.B) { } }) - b.Run("SliceOrderBook", func(b *testing.B) { + b.Run("OrderBook", func(b *testing.B) { book := &SliceOrderBook{} for i := 0; i < b.N; i++ { for _, ask := range asks { @@ -84,7 +84,7 @@ func BenchmarkOrderBook_UpdateAndInsert(b *testing.B) { sliceBook.Bids = sliceBook.Bids.Upsert(bid, true) } } - b.Run("SliceOrderBook", func(b *testing.B) { + 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) { diff --git a/pkg/types/rbtorderbook.go b/pkg/types/rbtorderbook.go index 4fafc6064..b9870041b 100644 --- a/pkg/types/rbtorderbook.go +++ b/pkg/types/rbtorderbook.go @@ -121,14 +121,64 @@ func (b *RBTOrderBook) load(book SliceOrderBook) { b.updateAsks(book.Asks) } -func (b *RBTOrderBook) Print() { - b.Bids.PostorderOf(b.Bids.Root, func(n *RBNode) bool { - fmt.Printf("bid: %f x %f", n.Key.Float64(), n.Value.Float64()) +func (b *RBTOrderBook) Copy() OrderBook { + var book = NewRBOrderBook(b.Symbol) + book.Asks = b.Asks.Copy() + book.Bids = b.Bids.Copy() + return book +} + +func (b *RBTOrderBook) CopyDepth(depth int) OrderBook { + var book = NewRBOrderBook(b.Symbol) + book.Asks = b.Asks.CopyInorder(depth) + book.Bids = b.Bids.CopyInorder(depth) + return book +} + +func (b *RBTOrderBook) convertTreeToPriceVolumeSlice(tree *RBTree, descending bool) (pvs PriceVolumeSlice) { + if descending { + tree.InorderReverse(func(n *RBNode) bool { + pvs = append(pvs, PriceVolume{ + Price: n.Key, + Volume: n.Value, + }) + return true + }) + return pvs + } + + tree.Inorder(func(n *RBNode) bool { + pvs = append(pvs, PriceVolume{ + Price: n.Key, + Volume: n.Value, + }) return true }) + return pvs +} - b.Asks.PostorderOf(b.Asks.Root, func(n *RBNode) bool { +func (b *RBTOrderBook) SideBook(sideType SideType) PriceVolumeSlice { + switch sideType { + + case SideTypeBuy: + return b.convertTreeToPriceVolumeSlice(b.Bids, false) + + case SideTypeSell: + return b.convertTreeToPriceVolumeSlice(b.Asks, true) + + default: + return nil + } +} + +func (b *RBTOrderBook) Print() { + b.Asks.Inorder(func(n *RBNode) bool { fmt.Printf("ask: %f x %f", n.Key.Float64(), n.Value.Float64()) return true }) + + b.Bids.InorderReverse(func(n *RBNode) bool { + fmt.Printf("bid: %f x %f", n.Key.Float64(), n.Value.Float64()) + return true + }) } diff --git a/pkg/types/sliceorderbook.go b/pkg/types/sliceorderbook.go index 70a45da15..f17dd5389 100644 --- a/pkg/types/sliceorderbook.go +++ b/pkg/types/sliceorderbook.go @@ -20,6 +20,12 @@ type SliceOrderBook struct { 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 { @@ -50,6 +56,20 @@ func (b *SliceOrderBook) BestAsk() (PriceVolume, bool) { 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() @@ -82,20 +102,6 @@ func (b *SliceOrderBook) PriceVolumesBySide(side SideType) PriceVolumeSlice { return nil } -func (b *SliceOrderBook) CopyDepth(depth int) (book SliceOrderBook) { - book = *b - book.Bids = book.Bids.CopyDepth(depth) - book.Asks = book.Asks.CopyDepth(depth) - return book -} - -func (b *SliceOrderBook) Copy() (book SliceOrderBook) { - book = *b - book.Bids = book.Bids.Copy() - book.Asks = book.Asks.Copy() - return book -} - func (b *SliceOrderBook) updateAsks(pvs PriceVolumeSlice) { for _, pv := range pvs { if pv.Volume == 0 { @@ -172,3 +178,19 @@ func (b *SliceOrderBook) String() string { return sb.String() } + +func (b *SliceOrderBook) CopyDepth(depth int) OrderBook { + var book SliceOrderBook + book = *b + book.Bids = book.Bids.CopyDepth(depth) + book.Asks = book.Asks.CopyDepth(depth) + return &book +} + +func (b *SliceOrderBook) Copy() OrderBook { + var book SliceOrderBook + book = *b + book.Bids = book.Bids.Copy() + book.Asks = book.Asks.Copy() + return &book +} From 6df72d54a8a3d8e153b58c8fbbe03d51aee72172 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 22 May 2021 16:47:34 +0800 Subject: [PATCH 22/38] add callbacks --- pkg/types/rbtorderbook_callbacks.go | 25 +++++++++++++++++++++++++ pkg/types/sliceorderbook_callbacks.go | 25 +++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 pkg/types/rbtorderbook_callbacks.go create mode 100644 pkg/types/sliceorderbook_callbacks.go diff --git a/pkg/types/rbtorderbook_callbacks.go b/pkg/types/rbtorderbook_callbacks.go new file mode 100644 index 000000000..6f26f44b7 --- /dev/null +++ b/pkg/types/rbtorderbook_callbacks.go @@ -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) + } +} diff --git a/pkg/types/sliceorderbook_callbacks.go b/pkg/types/sliceorderbook_callbacks.go new file mode 100644 index 000000000..43aef7617 --- /dev/null +++ b/pkg/types/sliceorderbook_callbacks.go @@ -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) + } +} From cca3284140558dcdc3910c2b47d16dda6cd96049 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 22 May 2021 17:17:37 +0800 Subject: [PATCH 23/38] separate net profit and profit --- pkg/bbgo/position.go | 37 ++++++++++++++++++++------------- pkg/bbgo/position_test.go | 6 +++--- pkg/strategy/grid/strategy.go | 4 ++-- pkg/strategy/xmaker/strategy.go | 5 +++-- 4 files changed, 30 insertions(+), 22 deletions(-) diff --git a/pkg/bbgo/position.go b/pkg/bbgo/position.go index 3e62e13f0..570a8c684 100644 --- a/pkg/bbgo/position.go +++ b/pkg/bbgo/position.go @@ -107,18 +107,19 @@ 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) @@ -158,16 +159,19 @@ func (p *Position) AddTrade(t types.Trade) (fixedpoint.Value, bool) { if p.Base < 0 { // handling short-to-long position if p.Base+quantity > 0 { - closingProfit := (p.AverageCost - price).Mul(-p.Base) - quoteFee + profit = (p.AverageCost - price).Mul(-p.Base) + netProfit = profit - quoteFee p.Base += quantity p.Quote -= quoteQuantity p.AverageCost = price - return closingProfit, true + 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 = profit - quoteFee + return profit, netProfit, true } } @@ -175,21 +179,24 @@ func (p *Position) AddTrade(t types.Trade) (fixedpoint.Value, bool) { p.Base += quantity p.Quote -= quoteQuantity - return 0, false + return 0, 0, false case types.SideTypeSell: if p.Base > 0 { // long-to-short if p.Base-quantity < 0 { - closingProfit := (price - p.AverageCost).Mul(p.Base) - quoteFee + profit = (price - p.AverageCost).Mul(p.Base) + netProfit = profit - quoteFee p.Base -= quantity p.Quote += quoteQuantity p.AverageCost = price - return closingProfit, true + 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 = profit - quoteFee + return profit, netProfit, true } } @@ -198,8 +205,8 @@ func (p *Position) AddTrade(t types.Trade) (fixedpoint.Value, bool) { p.Base -= quantity p.Quote += quoteQuantity - return 0, false + return 0, 0, false } - return 0, false + return 0, 0, false } diff --git a/pkg/bbgo/position_test.go b/pkg/bbgo/position_test.go index 19d708ffd..41f178517 100644 --- a/pkg/bbgo/position_test.go +++ b/pkg/bbgo/position_test.go @@ -38,7 +38,7 @@ func TestPosition_ExchangeFeeRate_Short(t *testing.T) { FeeCurrency: "BNB", }) - profit, madeProfit := pos.AddTrade(types.Trade{ + profit, _, madeProfit := pos.AddTrade(types.Trade{ Exchange: types.ExchangeBinance, Price: 2000.0, Quantity: 10.0, @@ -83,7 +83,7 @@ func TestPosition_ExchangeFeeRate_Long(t *testing.T) { FeeCurrency: "BNB", }) - profit, madeProfit := pos.AddTrade(types.Trade{ + profit, _, madeProfit := pos.AddTrade(types.Trade{ Exchange: types.ExchangeBinance, Price: 4000.0, Quantity: 10.0, @@ -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") diff --git a/pkg/strategy/grid/strategy.go b/pkg/strategy/grid/strategy.go index 461cd9007..d087e4856 100644 --- a/pkg/strategy/grid/strategy.go +++ b/pkg/strategy/grid/strategy.go @@ -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()) } } } diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index 6106b995f..44d8d5ff9 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -536,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,11 +552,12 @@ func (s *Strategy) handleTradeUpdate(trade types.Trade) { 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 (%.3f%%), net profit =~ %f %s, 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, since.Format(time.RFC822), s.state.AccumulatedPnL.Float64(), s.state.Position.QuoteCurrency, s.state.AccumulatedLoss.Float64(), s.state.Position.QuoteCurrency) From 0a908e5dda902ce0c0656a1247ccb763d9cc31af Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 22 May 2021 17:43:53 +0800 Subject: [PATCH 24/38] fix position test for net profit --- pkg/bbgo/position_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/bbgo/position_test.go b/pkg/bbgo/position_test.go index 41f178517..54f71065d 100644 --- a/pkg/bbgo/position_test.go +++ b/pkg/bbgo/position_test.go @@ -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) { From 289227e5f35a6a8a2ad9003a59d09b7587ac318f Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 22 May 2021 17:44:07 +0800 Subject: [PATCH 25/38] add exists method for active book --- pkg/bbgo/active_book.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pkg/bbgo/active_book.go b/pkg/bbgo/active_book.go index e91edc05b..e773ad785 100644 --- a/pkg/bbgo/active_book.go +++ b/pkg/bbgo/active_book.go @@ -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: From 9b9643e1f99ef37534312242c98d415bd66d3093 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 22 May 2021 17:44:20 +0800 Subject: [PATCH 26/38] improve order cancellation mechanisim --- pkg/bbgo/twap_order_executor.go | 29 ++++++++++- pkg/cmd/run.go | 3 +- pkg/strategy/xmaker/strategy.go | 85 ++++++++++++++++++++++++++------- 3 files changed, 97 insertions(+), 20 deletions(-) diff --git a/pkg/bbgo/twap_order_executor.go b/pkg/bbgo/twap_order_executor.go index c1440a9e1..d4cc9e6c3 100644 --- a/pkg/bbgo/twap_order_executor.go +++ b/pkg/bbgo/twap_order_executor.go @@ -308,7 +308,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 { diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index fcfbd066a..d03590a35 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -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) diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index 44d8d5ff9..58905992a 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -567,6 +567,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 { @@ -585,6 +589,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 @@ -666,20 +697,9 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.groupID = max.GenerateGroupID(instanceID) log.Infof("using group id %d from fnv(%s)", s.groupID, instanceID) - var state State - - // load position - if err := s.Persistence.Load(&state, ID, s.Symbol, stateKey); err != nil { - if err != service.ErrPersistenceNotExists { - return err - } - - s.state = &State{} + if err := s.LoadState(); err != nil { + return err } else { - // loaded successfully - s.state = &state - - log.Infof("state is restored: %+v", s.state) s.Notify("%s position is restored => %f", s.Symbol, s.state.HedgePosition.Float64()) } @@ -767,8 +787,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)) @@ -776,18 +798,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) } }) From 1531f2bb1b319b74ca68c52142cd0a390ef17460 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 22 May 2021 18:11:32 +0800 Subject: [PATCH 27/38] fix rbtree insertion and rotation --- pkg/types/rbtorderbook.go | 6 ++++-- pkg/types/rbtree.go | 45 +++++++++++++++++++++++++++++---------- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/pkg/types/rbtorderbook.go b/pkg/types/rbtorderbook.go index b9870041b..e9bb26b61 100644 --- a/pkg/types/rbtorderbook.go +++ b/pkg/types/rbtorderbook.go @@ -26,18 +26,20 @@ func NewRBOrderBook(symbol string) *RBTOrderBook { } func (b *RBTOrderBook) BestBid() (PriceVolume, bool) { - right := b.Bids.Rightmost(b.Bids.Root) + 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(b.Bids.Root) + left := b.Asks.Leftmost() if left != nil { return PriceVolume{Price: left.Key, Volume: left.Value}, true } + return PriceVolume{}, false } diff --git a/pkg/types/rbtree.go b/pkg/types/rbtree.go index 2ededb49b..bdf463ebe 100644 --- a/pkg/types/rbtree.go +++ b/pkg/types/rbtree.go @@ -39,6 +39,10 @@ func (tree *RBTree) Delete(key fixedpoint.Value) bool { y = del } else { y = tree.Successor(del) + if y == nil { + // prevent segmentation fault + y = Neel + } } if y.Left != Neel { @@ -236,14 +240,13 @@ func (tree *RBTree) Size() int { func (tree *RBTree) InsertFixup(current *RBNode) { // A red node can't have a red parent, we need to fix it up for current.Parent.Color == Red { - grandParent := current.Parent.Parent - if current.Parent == grandParent.Left { - uncle := grandParent.Right + if current.Parent == current.Parent.Parent.Left { + uncle := current.Parent.Parent.Right if uncle.Color == Red { current.Parent.Color = Black uncle.Color = Black - grandParent.Color = Red - current = grandParent + current.Parent.Parent.Color = Red + current = current.Parent.Parent } else { // if uncle is black if current == current.Parent.Right { current = current.Parent @@ -260,8 +263,8 @@ func (tree *RBTree) InsertFixup(current *RBNode) { 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) @@ -328,25 +331,41 @@ func (tree *RBTree) RotateRight(y *RBNode) { y.Parent = x } -func (tree *RBTree) Rightmost(current *RBNode) *RBNode { +func (tree *RBTree) Rightmost() *RBNode { + return tree.RightmostOf(tree.Root) +} + +func (tree *RBTree) RightmostOf(current *RBNode) *RBNode { for current.Right != Neel { current = current.Right } + if current == Neel { + return nil + } + return current } -func (tree *RBTree) Leftmost(current *RBNode) *RBNode { +func (tree *RBTree) Leftmost() *RBNode { + return tree.LeftmostOf(tree.Root) +} + +func (tree *RBTree) LeftmostOf(current *RBNode) *RBNode { for current.Left != Neel { current = current.Left } + if current == Neel { + return nil + } + return current } func (tree *RBTree) Successor(current *RBNode) *RBNode { if current.Right != Neel { - return tree.Leftmost(current.Right) + return tree.LeftmostOf(current.Right) } var newNode = current.Parent @@ -355,6 +374,10 @@ func (tree *RBTree) Successor(current *RBNode) *RBNode { newNode = newNode.Parent } + if newNode == Neel { + return nil + } + return newNode } @@ -391,7 +414,7 @@ func (tree *RBTree) InorderReverse(cb func(n *RBNode) bool) { } func (tree *RBTree) InorderReverseOf(current *RBNode, cb func(n *RBNode) bool) { - if current != nil { + if current != Neel { tree.InorderReverseOf(current.Right, cb) if !cb(current) { return @@ -405,7 +428,7 @@ func (tree *RBTree) Postorder(cb func(n *RBNode) bool) { } func (tree *RBTree) PostorderOf(current *RBNode, cb func(n *RBNode) bool) { - if current != nil { + if current != Neel { tree.PostorderOf(current.Left, cb) tree.PostorderOf(current.Right, cb) if !cb(current) { From 2052d05bb3b2733de384189fcfa4c6744a997a42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=82=8B=E3=81=BF?= Date: Sat, 22 May 2021 20:20:48 +0800 Subject: [PATCH 28/38] Move Float64Slice to types --- pkg/indicator/ad.go | 2 +- pkg/indicator/boll.go | 8 +++--- pkg/indicator/ewma.go | 2 +- pkg/indicator/macd.go | 4 +-- pkg/indicator/obv.go | 2 +- pkg/indicator/obv_test.go | 6 ++--- pkg/indicator/sma.go | 55 +-------------------------------------- pkg/indicator/stoch.go | 4 +-- pkg/indicator/vwap.go | 2 +- pkg/types/float_slice.go | 55 +++++++++++++++++++++++++++++++++++++++ 10 files changed, 71 insertions(+), 69 deletions(-) create mode 100644 pkg/types/float_slice.go diff --git a/pkg/indicator/ad.go b/pkg/indicator/ad.go index 3023a1bc7..0928e9975 100644 --- a/pkg/indicator/ad.go +++ b/pkg/indicator/ad.go @@ -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 diff --git a/pkg/indicator/boll.go b/pkg/indicator/boll.go index 47fe9e282..6131d7b67 100644 --- a/pkg/indicator/boll.go +++ b/pkg/indicator/boll.go @@ -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 diff --git a/pkg/indicator/ewma.go b/pkg/indicator/ewma.go index 11a97e990..897f92040 100644 --- a/pkg/indicator/ewma.go +++ b/pkg/indicator/ewma.go @@ -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) diff --git a/pkg/indicator/macd.go b/pkg/indicator/macd.go index 05aff9181..2910a51a9 100644 --- a/pkg/indicator/macd.go +++ b/pkg/indicator/macd.go @@ -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 diff --git a/pkg/indicator/obv.go b/pkg/indicator/obv.go index 2ca18a63f..cccbcdc27 100644 --- a/pkg/indicator/obv.go +++ b/pkg/indicator/obv.go @@ -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 diff --git a/pkg/indicator/obv_test.go b/pkg/indicator/obv_test.go index 2d7f45b11..473c8a290 100644 --- a/pkg/indicator/obv_test.go +++ b/pkg/indicator/obv_test.go @@ -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}, }, } diff --git a/pkg/indicator/sma.go b/pkg/indicator/sma.go index c1250683f..aa5a4a557 100644 --- a/pkg/indicator/sma.go +++ b/pkg/indicator/sma.go @@ -2,7 +2,6 @@ package indicator import ( "fmt" - "math" "time" log "github.com/sirupsen/logrus" @@ -10,64 +9,12 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -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 -} - 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) diff --git a/pkg/indicator/stoch.go b/pkg/indicator/stoch.go index 24a853be3..9204f9427 100644 --- a/pkg/indicator/stoch.go +++ b/pkg/indicator/stoch.go @@ -17,8 +17,8 @@ Stochastic Oscillator //go:generate callbackgen -type STOCH type STOCH struct { types.IntervalWindow - K Float64Slice - D Float64Slice + K types.Float64Slice + D types.Float64Slice KLineWindow types.KLineWindow diff --git a/pkg/indicator/vwap.go b/pkg/indicator/vwap.go index 885c64d61..e7fcf0791 100644 --- a/pkg/indicator/vwap.go +++ b/pkg/indicator/vwap.go @@ -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 diff --git a/pkg/types/float_slice.go b/pkg/types/float_slice.go new file mode 100644 index 000000000..f3c6f65f9 --- /dev/null +++ b/pkg/types/float_slice.go @@ -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 +} From 0061e51dc9676a0c2e967efe3c6e60360d631d62 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 23 May 2021 00:17:12 +0800 Subject: [PATCH 29/38] fix rbtree copy depth --- pkg/types/rbtorderbook.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/types/rbtorderbook.go b/pkg/types/rbtorderbook.go index e9bb26b61..e68a6fc2f 100644 --- a/pkg/types/rbtorderbook.go +++ b/pkg/types/rbtorderbook.go @@ -130,10 +130,10 @@ func (b *RBTOrderBook) Copy() OrderBook { return book } -func (b *RBTOrderBook) CopyDepth(depth int) OrderBook { +func (b *RBTOrderBook) CopyDepth(limit int) OrderBook { var book = NewRBOrderBook(b.Symbol) - book.Asks = b.Asks.CopyInorder(depth) - book.Bids = b.Bids.CopyInorder(depth) + book.Asks = b.Asks.CopyInorder(limit) + book.Bids = b.Bids.CopyInorderReverse(limit) return book } From 7a653affa6d8293e62655f5667d7c9a35e78f73e Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 23 May 2021 00:17:21 +0800 Subject: [PATCH 30/38] slice orderbook: do not copy book callbacks --- pkg/types/sliceorderbook.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/types/sliceorderbook.go b/pkg/types/sliceorderbook.go index f17dd5389..e65ac1cf8 100644 --- a/pkg/types/sliceorderbook.go +++ b/pkg/types/sliceorderbook.go @@ -179,17 +179,17 @@ func (b *SliceOrderBook) String() string { return sb.String() } -func (b *SliceOrderBook) CopyDepth(depth int) OrderBook { +func (b *SliceOrderBook) CopyDepth(limit int) OrderBook { var book SliceOrderBook - book = *b - book.Bids = book.Bids.CopyDepth(depth) - book.Asks = book.Asks.CopyDepth(depth) + 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 = *b + book.Symbol = b.Symbol book.Bids = book.Bids.Copy() book.Asks = book.Asks.Copy() return &book From 9fa10ee1fd658df14d5dc3a3901979c668293e38 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 23 May 2021 00:42:27 +0800 Subject: [PATCH 31/38] fix rbtree price volume order --- pkg/types/rbtorderbook.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pkg/types/rbtorderbook.go b/pkg/types/rbtorderbook.go index e68a6fc2f..5150ed92e 100644 --- a/pkg/types/rbtorderbook.go +++ b/pkg/types/rbtorderbook.go @@ -137,15 +137,17 @@ func (b *RBTOrderBook) CopyDepth(limit int) OrderBook { return book } -func (b *RBTOrderBook) convertTreeToPriceVolumeSlice(tree *RBTree, descending bool) (pvs PriceVolumeSlice) { +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 true + + return !(limit > 0 && len(pvs) >= limit) }) + return pvs } @@ -154,7 +156,8 @@ func (b *RBTOrderBook) convertTreeToPriceVolumeSlice(tree *RBTree, descending bo Price: n.Key, Volume: n.Value, }) - return true + + return !(limit > 0 && len(pvs) >= limit) }) return pvs } @@ -163,10 +166,10 @@ func (b *RBTOrderBook) SideBook(sideType SideType) PriceVolumeSlice { switch sideType { case SideTypeBuy: - return b.convertTreeToPriceVolumeSlice(b.Bids, false) + return b.convertTreeToPriceVolumeSlice(b.Bids, 0, true) case SideTypeSell: - return b.convertTreeToPriceVolumeSlice(b.Asks, true) + return b.convertTreeToPriceVolumeSlice(b.Asks, 0, false) default: return nil From 9efb45b133e0a2a37f0545f139cd7cbc3e7e6332 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 23 May 2021 00:42:44 +0800 Subject: [PATCH 32/38] reduce side book copy --- pkg/bbgo/twap_order_executor.go | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/pkg/bbgo/twap_order_executor.go b/pkg/bbgo/twap_order_executor.go index d4cc9e6c3..b4aa0933b 100644 --- a/pkg/bbgo/twap_order_executor.go +++ b/pkg/bbgo/twap_order_executor.go @@ -63,19 +63,9 @@ func (e *TwapExecution) connectUserData(ctx context.Context) { } } -func (e *TwapExecution) getSideBook() (pvs types.PriceVolumeSlice, err error) { - book := e.orderBook.Copy() - pvs = book.SideBook(e.Side) - return pvs, err -} - func (e *TwapExecution) newBestPriceOrder() (orderForm types.SubmitOrder, err error) { book := e.orderBook.Copy() - - sideBook, err := e.getSideBook() - if err != nil { - return orderForm, err - } + sideBook := book.SideBook(e.Side) first, ok := sideBook.First() if !ok { @@ -212,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 { From d2e299a68a53a0437baf265d6f80e9f07a9d1b01 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 23 May 2021 00:42:57 +0800 Subject: [PATCH 33/38] improve position comment --- pkg/bbgo/position.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/bbgo/position.go b/pkg/bbgo/position.go index 570a8c684..b792b91ee 100644 --- a/pkg/bbgo/position.go +++ b/pkg/bbgo/position.go @@ -157,7 +157,7 @@ func (p *Position) AddTrade(t types.Trade) (profit fixedpoint.Value, netProfit f case types.SideTypeBuy: if p.Base < 0 { - // handling short-to-long position + // convert short position to long position if p.Base+quantity > 0 { profit = (p.AverageCost - price).Mul(-p.Base) netProfit = profit - quoteFee @@ -183,7 +183,7 @@ func (p *Position) AddTrade(t types.Trade) (profit fixedpoint.Value, netProfit f case types.SideTypeSell: if p.Base > 0 { - // long-to-short + // convert long position to short position if p.Base-quantity < 0 { profit = (price - p.AverageCost).Mul(p.Base) netProfit = profit - quoteFee From 9c70e36e1b6e14f773512c74d942a45d8f1ef424 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 23 May 2021 01:05:11 +0800 Subject: [PATCH 34/38] save average cost with feeInQuote in the ApproximateAverageCost --- pkg/bbgo/position.go | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/pkg/bbgo/position.go b/pkg/bbgo/position.go index b792b91ee..886936057 100644 --- a/pkg/bbgo/position.go +++ b/pkg/bbgo/position.go @@ -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 @@ -126,7 +130,7 @@ func (p *Position) AddTrade(t types.Trade) (profit fixedpoint.Value, netProfit f 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 { @@ -140,9 +144,9 @@ func (p *Position) AddTrade(t types.Trade) (profit fixedpoint.Value, netProfit f 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) } } } @@ -160,22 +164,24 @@ func (p *Position) AddTrade(t types.Trade) (profit fixedpoint.Value, netProfit f // convert short position to long position if p.Base+quantity > 0 { profit = (p.AverageCost - price).Mul(-p.Base) - netProfit = profit - quoteFee + netProfit = (p.ApproximateAverageCost - price).Mul(-p.Base) - feeInQuote p.Base += quantity p.Quote -= quoteQuantity p.AverageCost = price + p.ApproximateAverageCost = price return profit, netProfit, true } else { // covering short position p.Base += quantity p.Quote -= quoteQuantity profit = (p.AverageCost - price).Mul(quantity) - netProfit = profit - quoteFee + 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 @@ -186,22 +192,24 @@ func (p *Position) AddTrade(t types.Trade) (profit fixedpoint.Value, netProfit f // convert long position to short position if p.Base-quantity < 0 { profit = (price - p.AverageCost).Mul(p.Base) - netProfit = profit - quoteFee + netProfit = (price - p.ApproximateAverageCost).Mul(p.Base) - feeInQuote p.Base -= quantity p.Quote += quoteQuantity p.AverageCost = price + p.ApproximateAverageCost = price return profit, netProfit, true } else { p.Base -= quantity p.Quote += quoteQuantity profit = (price - p.AverageCost).Mul(quantity) - netProfit = profit - quoteFee + 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 From de768296f19bbe41220137363883a8d6676aba38 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 23 May 2021 01:12:16 +0800 Subject: [PATCH 35/38] fix rbtree memory error, check neel --- pkg/types/rbtree.go | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pkg/types/rbtree.go b/pkg/types/rbtree.go index bdf463ebe..69fd8f58d 100644 --- a/pkg/types/rbtree.go +++ b/pkg/types/rbtree.go @@ -336,14 +336,14 @@ func (tree *RBTree) Rightmost() *RBNode { } func (tree *RBTree) RightmostOf(current *RBNode) *RBNode { - for current.Right != Neel { - current = current.Right - } - if current == Neel { return nil } + for current.Right != Neel { + current = current.Right + } + return current } @@ -352,18 +352,22 @@ func (tree *RBTree) Leftmost() *RBNode { } func (tree *RBTree) LeftmostOf(current *RBNode) *RBNode { - for current.Left != Neel { - current = current.Left - } - 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) } From 117b26840e191dfbf1275c401186205ff57186af Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 23 May 2021 01:17:20 +0800 Subject: [PATCH 36/38] show net profit margin percentage --- pkg/strategy/xmaker/strategy.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index 58905992a..9a29e81fe 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -546,18 +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%%), net profit =~ %f %s, 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) From fbe850b3645bbbda50c703b85f0f12809be4feaa Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 23 May 2021 01:19:26 +0800 Subject: [PATCH 37/38] improve floating number formatting --- pkg/types/trade.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/types/trade.go b/pkg/types/trade.go index 082020e2a..e02087898 100644 --- a/pkg/types/trade.go +++ b/pkg/types/trade.go @@ -73,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) @@ -86,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 }}" From 956ef71a4811df763b7876fb7f79d39ca7371b07 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 23 May 2021 01:29:41 +0800 Subject: [PATCH 38/38] use stamp time with milliseconds --- pkg/types/trade.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/types/trade.go b/pkg/types/trade.go index e02087898..936f8aa98 100644 --- a/pkg/types/trade.go +++ b/pkg/types/trade.go @@ -121,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)), } }