package binance import ( "encoding/json" "errors" "fmt" "time" "github.com/adshao/go-binance/v2" "github.com/valyala/fastjson" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/util" ) /* executionReport { "e": "executionReport", // Event type "E": 1499405658658, // Event time "s": "ETHBTC", // Symbol "c": "mUvoqJxFIILMdfAW5iGSOW", // Client order ID "S": "BUY", // Side "o": "LIMIT", // Order type "f": "GTC", // Time in force "q": "1.00000000", // Order quantity "p": "0.10264410", // Order price "P": "0.00000000", // Stop price "F": "0.00000000", // Iceberg quantity "g": -1, // OrderListId "C": null, // Original client order ID; This is the ID of the order being canceled "x": "NEW", // Current execution type "X": "NEW", // Current order status "r": "NONE", // Order reject reason; will be an error code. "i": 4293153, // Order ID "l": "0.00000000", // Last executed quantity "z": "0.00000000", // Cumulative filled quantity "L": "0.00000000", // Last executed price "n": "0", // Commission amount "N": null, // Commission asset "T": 1499405658657, // Transaction time "t": -1, // Trade ID "I": 8641984, // Ignore "w": true, // Is the order on the book? "m": false, // Is this trade the maker side? "M": false, // Ignore "O": 1499405658657, // Order creation time "Z": "0.00000000", // Cumulative quote asset transacted quantity "Y": "0.00000000", // Last quote asset transacted quantity (i.e. lastPrice * lastQty) "Q": "0.00000000" // Quote Order Qty } */ type ExecutionReportEvent struct { EventBase Symbol string `json:"s"` Side string `json:"S"` ClientOrderID string `json:"c"` OriginalClientOrderID string `json:"C"` OrderType string `json:"o"` OrderCreationTime int64 `json:"O"` TimeInForce string `json:"f"` IcebergQuantity string `json:"F"` OrderQuantity string `json:"q"` QuoteOrderQuantity string `json:"Q"` OrderPrice string `json:"p"` StopPrice string `json:"P"` IsOnBook bool `json:"w"` IsMaker bool `json:"m"` Ignore bool `json:"M"` CommissionAmount string `json:"n"` CommissionAsset string `json:"N"` CurrentExecutionType string `json:"x"` CurrentOrderStatus string `json:"X"` OrderID int64 `json:"i"` Ignored int64 `json:"I"` TradeID int64 `json:"t"` TransactionTime int64 `json:"T"` LastExecutedQuantity string `json:"l"` LastExecutedPrice string `json:"L"` CumulativeFilledQuantity string `json:"z"` CumulativeQuoteAssetTransactedQuantity string `json:"Z"` LastQuoteAssetTransactedQuantity string `json:"Y"` } func (e *ExecutionReportEvent) Order() (*types.Order, error) { switch e.CurrentExecutionType { case "NEW", "CANCELED", "REJECTED", "EXPIRED": case "REPLACED": case "TRADE": // For Order FILLED status. And the order has been completed. default: return nil, errors.New("execution report type is not for order") } orderCreationTime := time.Unix(0, e.OrderCreationTime*int64(time.Millisecond)) return &types.Order{ Exchange: types.ExchangeBinance, SubmitOrder: types.SubmitOrder{ Symbol: e.Symbol, ClientOrderID: e.ClientOrderID, Side: toGlobalSideType(binance.SideType(e.Side)), Type: toGlobalOrderType(binance.OrderType(e.OrderType)), Quantity: util.MustParseFloat(e.OrderQuantity), Price: util.MustParseFloat(e.OrderPrice), TimeInForce: e.TimeInForce, }, OrderID: uint64(e.OrderID), Status: toGlobalOrderStatus(binance.OrderStatusType(e.CurrentOrderStatus)), ExecutedQuantity: util.MustParseFloat(e.CumulativeFilledQuantity), CreationTime: types.Time(orderCreationTime), }, nil } func (e *ExecutionReportEvent) Trade() (*types.Trade, error) { if e.CurrentExecutionType != "TRADE" { return nil, errors.New("execution report is not a trade") } tt := time.Unix(0, e.TransactionTime*int64(time.Millisecond)) return &types.Trade{ ID: e.TradeID, Exchange: types.ExchangeBinance, Symbol: e.Symbol, OrderID: uint64(e.OrderID), Side: toGlobalSideType(binance.SideType(e.Side)), Price: util.MustParseFloat(e.LastExecutedPrice), Quantity: util.MustParseFloat(e.LastExecutedQuantity), QuoteQuantity: util.MustParseFloat(e.LastQuoteAssetTransactedQuantity), IsBuyer: e.Side == "BUY", IsMaker: e.IsMaker, Time: types.Time(tt), Fee: util.MustParseFloat(e.CommissionAmount), FeeCurrency: e.CommissionAsset, }, nil } /* balanceUpdate { "e": "balanceUpdate", //KLineEvent Type "E": 1573200697110, //KLineEvent Time "a": "BTC", //Asset "d": "100.00000000", //Balance Delta "T": 1573200697068 //Clear Time } */ type BalanceUpdateEvent struct { EventBase Asset string `json:"a"` Delta string `json:"d"` ClearTime int64 `json:"T"` } /* outboundAccountInfo { "e": "outboundAccountInfo", // KLineEvent type "E": 1499405658849, // KLineEvent time "m": 0, // Maker commission rate (bips) "t": 0, // Taker commission rate (bips) "b": 0, // Buyer commission rate (bips) "s": 0, // Seller commission rate (bips) "T": true, // Can trade? "W": true, // Can withdraw? "D": true, // Can deposit? "u": 1499405658848, // Time of last account update "B": [ // Balances array { "a": "LTC", // Asset "f": "17366.18538083", // Free amount "l": "0.00000000" // Locked amount }, { "a": "BTC", "f": "10537.85314051", "l": "2.19464093" }, { "a": "ETH", "f": "17902.35190619", "l": "0.00000000" }, { "a": "BNC", "f": "1114503.29769312", "l": "0.00000000" }, { "a": "NEO", "f": "0.00000000", "l": "0.00000000" } ], "P": [ // Account Permissions "SPOT" ] } */ type Balance struct { Asset string `json:"a"` Free string `json:"f"` Locked string `json:"l"` } type OutboundAccountPositionEvent struct { EventBase LastAccountUpdateTime int `json:"u"` Balances []Balance `json:"B,omitempty"` } type OutboundAccountInfoEvent struct { EventBase MakerCommissionRate int `json:"m"` TakerCommissionRate int `json:"t"` BuyerCommissionRate int `json:"b"` SellerCommissionRate int `json:"s"` CanTrade bool `json:"T"` CanWithdraw bool `json:"W"` CanDeposit bool `json:"D"` LastAccountUpdateTime int `json:"u"` Balances []Balance `json:"B,omitempty"` Permissions []string `json:"P,omitempty"` } type ResultEvent struct { Result interface{} `json:"result,omitempty"` ID int `json:"id"` } func ParseEvent(message string) (interface{}, error) { val, err := fastjson.Parse(message) if err != nil { return nil, err } eventType := string(val.GetStringBytes("e")) switch eventType { case "kline": var event KLineEvent err := json.Unmarshal([]byte(message), &event) return &event, err case "outboundAccountPosition": var event OutboundAccountPositionEvent err := json.Unmarshal([]byte(message), &event) return &event, err case "outboundAccountInfo": var event OutboundAccountInfoEvent err := json.Unmarshal([]byte(message), &event) return &event, err case "balanceUpdate": var event BalanceUpdateEvent err := json.Unmarshal([]byte(message), &event) return &event, err case "executionReport": var event ExecutionReportEvent err := json.Unmarshal([]byte(message), &event) return &event, err case "depthUpdate": return parseDepthEvent(val) default: id := val.GetInt("id") if id > 0 { return &ResultEvent{ID: id}, nil } } return nil, fmt.Errorf("unsupported message: %s", message) } type DepthEntry struct { PriceLevel string Quantity string } type DepthEvent struct { EventBase Symbol string `json:"s"` FirstUpdateID int64 `json:"U"` FinalUpdateID int64 `json:"u"` Bids []DepthEntry Asks []DepthEntry } func (e *DepthEvent) String() (o string) { o += fmt.Sprintf("Depth %s bid/ask = ", e.Symbol) if len(e.Bids) == 0 { o += "empty" } else { o += e.Bids[0].PriceLevel } o += "/" if len(e.Asks) == 0 { o += "empty" } else { o += e.Asks[0].PriceLevel } o += fmt.Sprintf(" %d ~ %d", e.FirstUpdateID, e.FinalUpdateID) return o } func (e *DepthEvent) OrderBook() (book types.SliceOrderBook, err error) { book.Symbol = e.Symbol for _, entry := range e.Bids { quantity, err := fixedpoint.NewFromString(entry.Quantity) if err != nil { log.WithError(err).Errorf("depth quantity parse error: %s", entry.Quantity) continue } price, err := fixedpoint.NewFromString(entry.PriceLevel) if err != nil { log.WithError(err).Errorf("depth price parse error: %s", entry.PriceLevel) continue } pv := types.PriceVolume{ Price: price, Volume: quantity, } book.Bids = book.Bids.Upsert(pv, true) } for _, entry := range e.Asks { quantity, err := fixedpoint.NewFromString(entry.Quantity) if err != nil { log.WithError(err).Errorf("depth quantity parse error: %s", entry.Quantity) continue } price, err := fixedpoint.NewFromString(entry.PriceLevel) if err != nil { log.WithError(err).Errorf("depth price parse error: %s", entry.PriceLevel) continue } pv := types.PriceVolume{ Price: price, Volume: quantity, } book.Asks = book.Asks.Upsert(pv, false) } return book, nil } func parseDepthEntry(val *fastjson.Value) (*DepthEntry, error) { arr, err := val.Array() if err != nil { return nil, err } if len(arr) < 2 { return nil, errors.New("incorrect depth entry element length") } return &DepthEntry{ PriceLevel: string(arr[0].GetStringBytes()), Quantity: string(arr[1].GetStringBytes()), }, nil } func parseDepthEvent(val *fastjson.Value) (*DepthEvent, error) { var err error var depth = &DepthEvent{ EventBase: EventBase{ Event: string(val.GetStringBytes("e")), Time: val.GetInt64("E"), }, Symbol: string(val.GetStringBytes("s")), FirstUpdateID: val.GetInt64("U"), FinalUpdateID: val.GetInt64("u"), } for _, ev := range val.GetArray("b") { entry, err2 := parseDepthEntry(ev) if err2 != nil { err = err2 continue } depth.Bids = append(depth.Bids, *entry) } for _, ev := range val.GetArray("a") { entry, err2 := parseDepthEntry(ev) if err2 != nil { err = err2 continue } depth.Asks = append(depth.Asks, *entry) } return depth, err } type KLine struct { StartTime int64 `json:"t"` EndTime int64 `json:"T"` Symbol string `json:"s"` Interval string `json:"i"` Open string `json:"o"` Close string `json:"c"` High string `json:"h"` Low string `json:"l"` Volume string `json:"v"` // base asset volume (like 10 BTC) QuoteVolume string `json:"q"` // quote asset volume TakerBaseVolume string `json:"V"` // taker buy base asset volume (like 10 BTC) TakerQuoteVolume string `json:"Q"` // taker buy quote asset volume (like 1000USDT) LastTradeID int `json:"L"` NumberOfTrades int64 `json:"n"` Closed bool `json:"x"` } type KLineEvent struct { EventBase Symbol string `json:"s"` KLine KLine `json:"k,omitempty"` } func (k *KLine) KLine() types.KLine { return types.KLine{ Exchange: "binance", Symbol: k.Symbol, Interval: types.Interval(k.Interval), StartTime: time.Unix(0, k.StartTime*int64(time.Millisecond)), EndTime: time.Unix(0, k.EndTime*int64(time.Millisecond)), Open: util.MustParseFloat(k.Open), Close: util.MustParseFloat(k.Close), High: util.MustParseFloat(k.High), Low: util.MustParseFloat(k.Low), Volume: util.MustParseFloat(k.Volume), QuoteVolume: util.MustParseFloat(k.TakerQuoteVolume), LastTradeID: uint64(k.LastTradeID), NumberOfTrades: uint64(k.NumberOfTrades), Closed: k.Closed, } } /* kline { "e": "kline", // KLineEvent type "E": 123456789, // KLineEvent time "s": "BNBBTC", // Symbol "k": { "t": 123400000, // Kline start time "T": 123460000, // Kline close time "s": "BNBBTC", // Symbol "i": "1m", // Interval "f": 100, // First trade ID "L": 200, // Last trade ID "o": "0.0010", // Open price "c": "0.0020", // Close price "h": "0.0025", // High price "l": "0.0015", // Low price "v": "1000", // Base asset volume "n": 100, // Number of trades "x": false, // Is this kline closed? "q": "1.0000", // Quote asset volume "V": "500", // Taker buy base asset volume "Q": "0.500", // Taker buy quote asset volume "B": "123456" // Ignore } } */ type EventBase struct { Event string `json:"e"` // event Time int64 `json:"E"` }