fix and improve backtest

This commit is contained in:
c9s 2020-11-10 14:18:04 +08:00
parent f5b17193c5
commit 69a33b6400
9 changed files with 115 additions and 126 deletions

View File

@ -52,13 +52,13 @@ func NewExchange(sourceName types.ExchangeName, srv *service.BacktestService, co
panic(err) panic(err)
} }
balances := config.Account.Balances.BalanceMap()
account := &types.Account{ account := &types.Account{
MakerCommission: config.Account.MakerCommission, MakerCommission: config.Account.MakerCommission,
TakerCommission: config.Account.TakerCommission, TakerCommission: config.Account.TakerCommission,
AccountType: "SPOT", // currently not used AccountType: "SPOT", // currently not used
} }
balances := config.Account.Balances.BalanceMap()
account.UpdateBalances(balances) account.UpdateBalances(balances)
e := &Exchange{ e := &Exchange{
@ -93,19 +93,18 @@ func (e *Exchange) NewStream() types.Stream {
e.trades[trade.Symbol] = append(e.trades[trade.Symbol], trade) e.trades[trade.Symbol] = append(e.trades[trade.Symbol], trade)
}) })
for _, symbol := range e.config.Symbols { for symbol, market := range e.markets {
market, ok := e.markets[symbol] matching := &SimplePriceMatching{
if !ok {
panic(fmt.Errorf("market %s is undefined", symbol))
}
e.matchingBooks[symbol] = &SimplePriceMatching{
CurrentTime: e.startTime, CurrentTime: e.startTime,
Account: e.account, Account: e.account,
Market: market, Market: market,
MakerCommission: e.config.Account.MakerCommission, MakerCommission: e.config.Account.MakerCommission,
TakerCommission: e.config.Account.TakerCommission, TakerCommission: e.config.Account.TakerCommission,
} }
matching.OnTradeUpdate(e.stream.EmitTradeUpdate)
matching.OnOrderUpdate(e.stream.EmitOrderUpdate)
matching.OnBalanceUpdate(e.stream.EmitBalanceUpdate)
e.matchingBooks[symbol] = matching
} }
return e.stream return e.stream
@ -119,7 +118,6 @@ func (e Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder)
return nil, fmt.Errorf("matching engine is not initialized for symbol %s", symbol) return nil, fmt.Errorf("matching engine is not initialized for symbol %s", symbol)
} }
createdOrder, trade, err := matching.PlaceOrder(order) createdOrder, trade, err := matching.PlaceOrder(order)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
@ -49,7 +50,7 @@ type SimplePriceMatching struct {
tradeUpdateCallbacks []func(trade types.Trade) tradeUpdateCallbacks []func(trade types.Trade)
orderUpdateCallbacks []func(order types.Order) orderUpdateCallbacks []func(order types.Order)
accountUpdateCallbacks []func(balances types.BalanceMap) balanceUpdateCallbacks []func(balances types.BalanceMap)
} }
func (m *SimplePriceMatching) CancelOrder(o types.Order) (types.Order, error) { func (m *SimplePriceMatching) CancelOrder(o types.Order) (types.Order, error) {
@ -73,43 +74,43 @@ func (m *SimplePriceMatching) CancelOrder(o types.Order) (types.Order, error) {
case types.SideTypeSell: case types.SideTypeSell:
m.mu.Lock() m.mu.Lock()
var orders []types.Order var orders []types.Order
for _, order := range m.bidOrders { for _, order := range m.askOrders {
if o.OrderID == order.OrderID { if o.OrderID == order.OrderID {
found = true found = true
continue continue
} }
orders = append(orders, order) orders = append(orders, order)
} }
m.bidOrders = orders m.askOrders = orders
m.mu.Unlock() m.mu.Unlock()
} }
if !found { if !found {
logrus.Panicf("cancel order failed, order %d not found: %+v", o.OrderID, o)
return o, fmt.Errorf("cancel order failed, order %d not found: %+v", o.OrderID, o) return o, fmt.Errorf("cancel order failed, order %d not found: %+v", o.OrderID, o)
} }
switch o.Side { switch o.Side {
case types.SideTypeBuy: case types.SideTypeBuy:
if err := m.Account.UnlockBalance(m.Market.QuoteCurrency, o.Price*o.Quantity); err != nil { if err := m.Account.UnlockBalance(m.Market.QuoteCurrency, fixedpoint.NewFromFloat(o.Price*o.Quantity)); err != nil {
return o, err return o, err
} }
case types.SideTypeSell: case types.SideTypeSell:
if err := m.Account.UnlockBalance(m.Market.BaseCurrency, o.Quantity); err != nil { if err := m.Account.UnlockBalance(m.Market.BaseCurrency, fixedpoint.NewFromFloat(o.Quantity)); err != nil {
return o, err return o, err
} }
} }
o.Status = types.OrderStatusCanceled o.Status = types.OrderStatusCanceled
m.EmitOrderUpdate(o) m.EmitOrderUpdate(o)
m.EmitAccountUpdate(m.Account.Balances()) m.EmitBalanceUpdate(m.Account.Balances())
return o, nil return o, nil
} }
func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (closedOrders *types.Order, trades *types.Trade, err error) { func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (closedOrders *types.Order, trades *types.Trade, err error) {
// start from one
orderID := incOrderID()
// price for checking account balance // price for checking account balance
price := o.Price price := o.Price
@ -123,19 +124,21 @@ func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (closedOrders *typ
switch o.Side { switch o.Side {
case types.SideTypeBuy: case types.SideTypeBuy:
quote := price * o.Quantity quote := price * o.Quantity
if err := m.Account.LockBalance(m.Market.QuoteCurrency, quote); err != nil { if err := m.Account.LockBalance(m.Market.QuoteCurrency, fixedpoint.NewFromFloat(quote)); err != nil {
return nil, nil, err return nil, nil, err
} }
case types.SideTypeSell: case types.SideTypeSell:
baseQuantity := o.Quantity baseQuantity := o.Quantity
if err := m.Account.LockBalance(m.Market.BaseCurrency, baseQuantity); err != nil { if err := m.Account.LockBalance(m.Market.BaseCurrency, fixedpoint.NewFromFloat(baseQuantity)); err != nil {
return nil, nil, err return nil, nil, err
} }
} }
m.EmitAccountUpdate(m.Account.Balances()) m.EmitBalanceUpdate(m.Account.Balances())
// start from one
orderID := incOrderID()
order := m.newOrder(o, orderID) order := m.newOrder(o, orderID)
if o.Type == types.OrderTypeMarket { if o.Type == types.OrderTypeMarket {
@ -150,7 +153,7 @@ func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (closedOrders *typ
order.ExecutedQuantity = order.Quantity order.ExecutedQuantity = order.Quantity
order.Price = price order.Price = price
m.EmitOrderUpdate(order) m.EmitOrderUpdate(order)
m.EmitAccountUpdate(m.Account.Balances()) m.EmitBalanceUpdate(m.Account.Balances())
return &order, &trade, nil return &order, &trade, nil
} }
@ -177,10 +180,13 @@ func (m *SimplePriceMatching) executeTrade(trade types.Trade) {
var err error var err error
// execute trade, update account balances // execute trade, update account balances
if trade.IsBuyer { if trade.IsBuyer {
quote := trade.Price * trade.Quantity err = m.Account.UseLockedBalance(m.Market.QuoteCurrency, fixedpoint.NewFromFloat(trade.Price*trade.Quantity))
err = m.Account.UseLockedBalance(m.Market.QuoteCurrency, quote)
_ = m.Account.AddBalance(m.Market.BaseCurrency, fixedpoint.NewFromFloat(trade.Quantity))
} else { } else {
err = m.Account.UseLockedBalance(m.Market.BaseCurrency, trade.Quantity) err = m.Account.UseLockedBalance(m.Market.BaseCurrency, fixedpoint.NewFromFloat(trade.Quantity))
_ = m.Account.AddBalance(m.Market.QuoteCurrency, fixedpoint.NewFromFloat(trade.Quantity*trade.Price))
} }
if err != nil { if err != nil {
@ -188,7 +194,7 @@ func (m *SimplePriceMatching) executeTrade(trade types.Trade) {
} }
m.EmitTradeUpdate(trade) m.EmitTradeUpdate(trade)
m.EmitAccountUpdate(m.Account.Balances()) m.EmitBalanceUpdate(m.Account.Balances())
return return
} }
@ -391,69 +397,34 @@ func (m *SimplePriceMatching) SellToPrice(price fixedpoint.Value) (closedOrders
return closedOrders, trades return closedOrders, trades
} }
func emitTxn(stream *Stream, trades []types.Trade, orders []types.Order) { func (m *SimplePriceMatching) processKLine(kline types.KLine) {
for _, t := range trades {
stream.EmitTradeUpdate(t)
}
for _, o := range orders {
stream.EmitOrderUpdate(o)
}
}
func (m *SimplePriceMatching) processKLine(stream *Stream, kline types.KLine) {
m.CurrentTime = kline.EndTime m.CurrentTime = kline.EndTime
switch kline.GetTrend() { switch kline.GetTrend() {
case types.TrendDown: case types.TrendDown:
if kline.High > kline.Open { if kline.High > kline.Open {
orders, trades := m.BuyToPrice(fixedpoint.NewFromFloat(kline.High)) m.BuyToPrice(fixedpoint.NewFromFloat(kline.High))
emitTxn(stream, trades, orders)
} }
if kline.Low > kline.Close { if kline.Low > kline.Close {
orders, trades := m.SellToPrice(fixedpoint.NewFromFloat(kline.Low)) m.SellToPrice(fixedpoint.NewFromFloat(kline.Low))
emitTxn(stream, trades, orders)
} }
orders, trades := m.SellToPrice(fixedpoint.NewFromFloat(kline.Close))
emitTxn(stream, trades, orders) m.SellToPrice(fixedpoint.NewFromFloat(kline.Close))
case types.TrendUp: case types.TrendUp:
if kline.Low < kline.Open { if kline.Low < kline.Open {
orders, trades := m.SellToPrice(fixedpoint.NewFromFloat(kline.Low)) m.SellToPrice(fixedpoint.NewFromFloat(kline.Low))
emitTxn(stream, trades, orders)
} }
if kline.High > kline.Close { if kline.High > kline.Close {
orders, trades := m.BuyToPrice(fixedpoint.NewFromFloat(kline.High)) m.BuyToPrice(fixedpoint.NewFromFloat(kline.High))
emitTxn(stream, trades, orders)
} }
orders, trades := m.BuyToPrice(fixedpoint.NewFromFloat(kline.Close))
emitTxn(stream, trades, orders) m.BuyToPrice(fixedpoint.NewFromFloat(kline.Close))
} }
} }
type Matching struct {
Symbol string
Asks PriceOrderSlice
Bids PriceOrderSlice
OrderID uint64
CurrentTime time.Time
}
func (m *Matching) PlaceOrder(o types.SubmitOrder) {
var order = types.Order{
SubmitOrder: o,
Exchange: "backtest",
OrderID: m.OrderID,
Status: types.OrderStatusNew,
ExecutedQuantity: 0,
IsWorking: false,
CreationTime: m.CurrentTime,
UpdateTime: m.CurrentTime,
}
_ = order
}
func (m *SimplePriceMatching) newOrder(o types.SubmitOrder, orderID uint64) types.Order { func (m *SimplePriceMatching) newOrder(o types.SubmitOrder, orderID uint64) types.Order {
return types.Order{ return types.Order{
OrderID: orderID, OrderID: orderID,
@ -461,7 +432,7 @@ func (m *SimplePriceMatching) newOrder(o types.SubmitOrder, orderID uint64) type
Exchange: "backtest", Exchange: "backtest",
Status: types.OrderStatusNew, Status: types.OrderStatusNew,
ExecutedQuantity: 0, ExecutedQuantity: 0,
IsWorking: false, IsWorking: true,
CreationTime: m.CurrentTime, CreationTime: m.CurrentTime,
UpdateTime: m.CurrentTime, UpdateTime: m.CurrentTime,
} }

View File

@ -28,8 +28,8 @@ func TestSimplePriceMatching_LimitOrder(t *testing.T) {
} }
account.UpdateBalances(types.BalanceMap{ account.UpdateBalances(types.BalanceMap{
"USDT": {Currency: "USDT", Available: 1000000.0}, "USDT": {Currency: "USDT", Available: fixedpoint.NewFromFloat(1000000.0)},
"BTC": {Currency: "BTC", Available: 100.0}, "BTC": {Currency: "BTC", Available: fixedpoint.NewFromFloat(100.0)},
}) })
market := types.Market{ market := types.Market{
@ -71,6 +71,10 @@ func TestSimplePriceMatching_LimitOrder(t *testing.T) {
closedOrders, trades = engine.SellToPrice(fixedpoint.NewFromFloat(8000.0)) closedOrders, trades = engine.SellToPrice(fixedpoint.NewFromFloat(8000.0))
assert.Len(t, closedOrders, 1) assert.Len(t, closedOrders, 1)
assert.Len(t, trades, 1) assert.Len(t, trades, 1)
for _, trade := range trades {
assert.True(t, trade.IsBuyer)
}
for _, o := range closedOrders { for _, o := range closedOrders {
assert.Equal(t, types.SideTypeBuy, o.Side) assert.Equal(t, types.SideTypeBuy, o.Side)
} }
@ -89,7 +93,6 @@ func TestSimplePriceMatching_LimitOrder(t *testing.T) {
for _, o := range closedOrders { for _, o := range closedOrders {
assert.Equal(t, types.SideTypeSell, o.Side) assert.Equal(t, types.SideTypeSell, o.Side)
} }
for _, trade := range trades { for _, trade := range trades {
assert.Equal(t, types.SideTypeSell, trade.Side) assert.Equal(t, types.SideTypeSell, trade.Side)
} }

View File

@ -26,12 +26,12 @@ func (m *SimplePriceMatching) EmitOrderUpdate(order types.Order) {
} }
} }
func (m *SimplePriceMatching) OnAccountUpdate(cb func(balances types.BalanceMap)) { func (m *SimplePriceMatching) OnBalanceUpdate(cb func(balances types.BalanceMap)) {
m.accountUpdateCallbacks = append(m.accountUpdateCallbacks, cb) m.balanceUpdateCallbacks = append(m.balanceUpdateCallbacks, cb)
} }
func (m *SimplePriceMatching) EmitAccountUpdate(balances types.BalanceMap) { func (m *SimplePriceMatching) EmitBalanceUpdate(balances types.BalanceMap) {
for _, cb := range m.accountUpdateCallbacks { for _, cb := range m.balanceUpdateCallbacks {
cb(balances) cb(balances)
} }
} }

View File

@ -21,7 +21,8 @@ func (s *Stream) Connect(ctx context.Context) error {
loadedSymbols := map[string]struct{}{} loadedSymbols := map[string]struct{}{}
loadedIntervals := map[types.Interval]struct{}{ loadedIntervals := map[types.Interval]struct{}{
// 1m interval is required for the backtest matching engine // 1m interval is required for the backtest matching engine
types.Interval1m: struct{}{}, types.Interval1m: {},
types.Interval1d: {},
} }
for _, sub := range s.Subscriptions { for _, sub := range s.Subscriptions {
@ -56,7 +57,7 @@ func (s *Stream) Connect(ctx context.Context) error {
if !ok { if !ok {
log.Errorf("matching book of %s is not initialized", k.Symbol) log.Errorf("matching book of %s is not initialized", k.Symbol)
} }
matching.processKLine(s, k) matching.processKLine(k)
} }
s.EmitKLineClosed(k) s.EmitKLineClosed(k)

View File

@ -6,17 +6,27 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/util" "github.com/c9s/bbgo/pkg/fixedpoint"
) )
type Balance struct { type Balance struct {
Currency string `json:"currency"` Currency string `json:"currency"`
Available float64 `json:"available"` Available fixedpoint.Value `json:"available"`
Locked float64 `json:"locked"` Locked fixedpoint.Value `json:"locked"`
} }
type BalanceMap map[string]Balance type BalanceMap map[string]Balance
func (m BalanceMap) Print() {
for _, balance := range m {
if balance.Locked > 0 {
logrus.Infof(" %s: %f (locked %f)", balance.Currency, balance.Available.Float64(), balance.Locked.Float64())
} else {
logrus.Infof(" %s: %f", balance.Currency, balance.Available.Float64())
}
}
}
type Account struct { type Account struct {
sync.Mutex sync.Mutex
@ -53,7 +63,7 @@ func (a *Account) Balance(currency string) (balance Balance, ok bool) {
return balance, ok return balance, ok
} }
func (a *Account) AddBalance(currency string, fund float64) error { func (a *Account) AddBalance(currency string, fund fixedpoint.Value) error {
a.Lock() a.Lock()
defer a.Unlock() defer a.Unlock()
@ -72,7 +82,7 @@ func (a *Account) AddBalance(currency string, fund float64) error {
return nil return nil
} }
func (a *Account) UseLockedBalance(currency string, fund float64) error { func (a *Account) UseLockedBalance(currency string, fund fixedpoint.Value) error {
a.Lock() a.Lock()
defer a.Unlock() defer a.Unlock()
@ -83,24 +93,29 @@ func (a *Account) UseLockedBalance(currency string, fund float64) error {
return nil return nil
} }
return fmt.Errorf("trying to use more than locked: locked %f < want to use %f", balance.Locked, fund) return fmt.Errorf("trying to use more than locked: locked %f < want to use %f", balance.Locked.Float64(), fund.Float64())
} }
func (a *Account) UnlockBalance(currency string, unlocked float64) error { func (a *Account) UnlockBalance(currency string, unlocked fixedpoint.Value) error {
a.Lock() a.Lock()
defer a.Unlock() defer a.Unlock()
balance, ok := a.balances[currency] balance, ok := a.balances[currency]
if ok && balance.Locked >= unlocked { if !ok {
balance.Locked -= unlocked return fmt.Errorf("trying to unlocked inexisted balance: %s", currency)
balance.Available += unlocked
a.balances[currency] = balance
return nil
} }
return fmt.Errorf("trying to unlocked more than locked: locked %f < want to unlock %f", balance.Locked, unlocked) if unlocked > balance.Locked {
return fmt.Errorf("trying to unlocked more than locked %s: locked %f < want to unlock %f", currency, balance.Locked.Float64(), unlocked.Float64())
}
balance.Locked -= unlocked
balance.Available += unlocked
a.balances[currency] = balance
return nil
} }
func (a *Account) LockBalance(currency string, locked float64) error { func (a *Account) LockBalance(currency string, locked fixedpoint.Value) error {
a.Lock() a.Lock()
defer a.Unlock() defer a.Unlock()
@ -112,10 +127,10 @@ func (a *Account) LockBalance(currency string, locked float64) error {
return nil return nil
} }
return fmt.Errorf("insufficient available balance for lock %f", locked) return fmt.Errorf("insufficient available balance %s for lock: want to lock %f, available %f", currency, locked.Float64(), balance.Available.Float64())
} }
func (a *Account) UpdateBalances(balances map[string]Balance) { func (a *Account) UpdateBalances(balances BalanceMap) {
a.Lock() a.Lock()
defer a.Unlock() defer a.Unlock()
@ -138,8 +153,8 @@ func (a *Account) Print() {
defer a.Unlock() defer a.Unlock()
for _, balance := range a.balances { for _, balance := range a.balances {
if util.NotZero(balance.Available) { if balance.Available != 0 {
logrus.Infof("account balance %s %f", balance.Currency, balance.Available) logrus.Infof("account balance %s %f", balance.Currency, balance.Available.Float64())
} }
} }
} }

View File

@ -4,58 +4,59 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/fixedpoint"
) )
func TestAccountLockAndUnlock(t *testing.T) { func TestAccountLockAndUnlock(t *testing.T) {
a := NewAccount() a := NewAccount()
err := a.AddBalance("USDT", 1000.0) err := a.AddBalance("USDT", 1000)
assert.NoError(t, err) assert.NoError(t, err)
balance, ok := a.Balance("USDT") balance, ok := a.Balance("USDT")
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, balance.Available, 1000.0) assert.Equal(t, balance.Available, fixedpoint.Value(1000))
assert.Equal(t, balance.Locked, 0.0) assert.Equal(t, balance.Locked, fixedpoint.Value(0))
err = a.LockBalance("USDT", 100.0) err = a.LockBalance("USDT", fixedpoint.Value(100))
assert.NoError(t, err) assert.NoError(t, err)
balance, ok = a.Balance("USDT") balance, ok = a.Balance("USDT")
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, balance.Available, 900.0) assert.Equal(t, balance.Available, fixedpoint.Value(900))
assert.Equal(t, balance.Locked, 100.0) assert.Equal(t, balance.Locked, fixedpoint.Value(100))
err = a.UnlockBalance("USDT", 100.0) err = a.UnlockBalance("USDT", 100)
assert.NoError(t, err) assert.NoError(t, err)
balance, ok = a.Balance("USDT") balance, ok = a.Balance("USDT")
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, balance.Available, 1000.0) assert.Equal(t, balance.Available, fixedpoint.Value(1000))
assert.Equal(t, balance.Locked, 0.0) assert.Equal(t, balance.Locked, fixedpoint.Value(0))
} }
func TestAccountLockAndUse(t *testing.T) { func TestAccountLockAndUse(t *testing.T) {
a := NewAccount() a := NewAccount()
err := a.AddBalance("USDT", 1000.0) err := a.AddBalance("USDT", 1000)
assert.NoError(t, err) assert.NoError(t, err)
balance, ok := a.Balance("USDT") balance, ok := a.Balance("USDT")
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, balance.Available, 1000.0) assert.Equal(t, balance.Available, fixedpoint.Value(1000))
assert.Equal(t, balance.Locked, 0.0) assert.Equal(t, balance.Locked, fixedpoint.Value(0))
err = a.LockBalance("USDT", 100.0) err = a.LockBalance("USDT", 100)
assert.NoError(t, err) assert.NoError(t, err)
balance, ok = a.Balance("USDT") balance, ok = a.Balance("USDT")
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, balance.Available, 900.0) assert.Equal(t, balance.Available, fixedpoint.Value(900))
assert.Equal(t, balance.Locked, 100.0) assert.Equal(t, balance.Locked, fixedpoint.Value(100))
err = a.UseLockedBalance("USDT", 100.0) err = a.UseLockedBalance("USDT", 100)
assert.NoError(t, err) assert.NoError(t, err)
balance, ok = a.Balance("USDT") balance, ok = a.Balance("USDT")
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, balance.Available, 900.0) assert.Equal(t, balance.Available, fixedpoint.Value(900))
assert.Equal(t, balance.Locked, 0.0) assert.Equal(t, balance.Locked, fixedpoint.Value(0))
} }

View File

@ -34,21 +34,21 @@ func (stream *StandardStream) EmitOrderUpdate(order Order) {
} }
} }
func (stream *StandardStream) OnBalanceSnapshot(cb func(balances map[string]Balance)) { func (stream *StandardStream) OnBalanceSnapshot(cb func(balances BalanceMap)) {
stream.balanceSnapshotCallbacks = append(stream.balanceSnapshotCallbacks, cb) stream.balanceSnapshotCallbacks = append(stream.balanceSnapshotCallbacks, cb)
} }
func (stream *StandardStream) EmitBalanceSnapshot(balances map[string]Balance) { func (stream *StandardStream) EmitBalanceSnapshot(balances BalanceMap) {
for _, cb := range stream.balanceSnapshotCallbacks { for _, cb := range stream.balanceSnapshotCallbacks {
cb(balances) cb(balances)
} }
} }
func (stream *StandardStream) OnBalanceUpdate(cb func(balances map[string]Balance)) { func (stream *StandardStream) OnBalanceUpdate(cb func(balances BalanceMap)) {
stream.balanceUpdateCallbacks = append(stream.balanceUpdateCallbacks, cb) stream.balanceUpdateCallbacks = append(stream.balanceUpdateCallbacks, cb)
} }
func (stream *StandardStream) EmitBalanceUpdate(balances map[string]Balance) { func (stream *StandardStream) EmitBalanceUpdate(balances BalanceMap) {
for _, cb := range stream.balanceUpdateCallbacks { for _, cb := range stream.balanceUpdateCallbacks {
cb(balances) cb(balances)
} }
@ -101,9 +101,9 @@ type StandardStreamEventHub interface {
OnOrderUpdate(cb func(order Order)) OnOrderUpdate(cb func(order Order))
OnBalanceSnapshot(cb func(balances map[string]Balance)) OnBalanceSnapshot(cb func(balances BalanceMap))
OnBalanceUpdate(cb func(balances map[string]Balance)) OnBalanceUpdate(cb func(balances BalanceMap))
OnKLineClosed(cb func(kline KLine)) OnKLineClosed(cb func(kline KLine))

View File

@ -31,9 +31,9 @@ type StandardStream struct {
orderUpdateCallbacks []func(order Order) orderUpdateCallbacks []func(order Order)
// balance snapshot callbacks // balance snapshot callbacks
balanceSnapshotCallbacks []func(balances map[string]Balance) balanceSnapshotCallbacks []func(balances BalanceMap)
balanceUpdateCallbacks []func(balances map[string]Balance) balanceUpdateCallbacks []func(balances BalanceMap)
kLineClosedCallbacks []func(kline KLine) kLineClosedCallbacks []func(kline KLine)