diff --git a/doc/topics/developing-strategy.md b/doc/topics/developing-strategy.md index dd01ca8f4..aed7dd915 100644 --- a/doc/topics/developing-strategy.md +++ b/doc/topics/developing-strategy.md @@ -16,11 +16,11 @@ In general, strategies are Go struct, defined in the Go package. To add your first strategy, the fastest way is to add it as a built-in strategy. -Simply edit `pkg/cmd/builtin.go` and import your strategy package there. +Simply edit `pkg/cmd/strategy/builtin.go` and import your strategy package there. When BBGO starts, the strategy will be imported as a package, and register its struct to the engine. -You can also create a new file called `pkg/cmd/builtin_short.go` and import your strategy package. +You can also create a new file called `pkg/cmd/strategy/short.go` and import your strategy package. ``` import ( diff --git a/pkg/bbgo/activeorderbook.go b/pkg/bbgo/activeorderbook.go index ba13b6293..75d28ecc7 100644 --- a/pkg/bbgo/activeorderbook.go +++ b/pkg/bbgo/activeorderbook.go @@ -105,18 +105,28 @@ func (b *ActiveOrderBook) GracefulCancel(ctx context.Context, ex types.Exchange) // verify the current open orders via the RESTful API log.Warnf("[ActiveOrderBook] using REStful API to verify active orders...") - openOrders, err := ex.QueryOpenOrders(ctx, b.Symbol) - if err != nil { - log.WithError(err).Errorf("can not query %s open orders", b.Symbol) - continue + + orders = b.Orders() + var symbols = map[string]struct{}{} + for _, order := range orders { + symbols[order.Symbol] = struct{}{} + } - openOrderStore := NewOrderStore(b.Symbol) - openOrderStore.Add(openOrders...) - for _, o := range orders { - // if it's not on the order book (open orders), we should remove it from our local side - if !openOrderStore.Exists(o.OrderID) { - b.Remove(o) + for symbol := range symbols { + openOrders, err := ex.QueryOpenOrders(ctx, symbol) + if err != nil { + log.WithError(err).Errorf("can not query %s open orders", symbol) + continue + } + + openOrderStore := NewOrderStore(symbol) + openOrderStore.Add(openOrders...) + for _, o := range orders { + // if it's not on the order book (open orders), we should remove it from our local side + if !openOrderStore.Exists(o.OrderID) { + b.Remove(o) + } } } } diff --git a/pkg/bbgo/tradecollector.go b/pkg/bbgo/tradecollector.go index 0c23798f3..8d145a5e5 100644 --- a/pkg/bbgo/tradecollector.go +++ b/pkg/bbgo/tradecollector.go @@ -2,6 +2,7 @@ package bbgo import ( "context" + "sync" "time" log "github.com/sirupsen/logrus" @@ -22,6 +23,8 @@ type TradeCollector struct { orderStore *OrderStore doneTrades map[types.TradeKey]struct{} + mu sync.Mutex + recoverCallbacks []func(trade types.Trade) tradeCallbacks []func(trade types.Trade, profit, netProfit fixedpoint.Value) @@ -100,11 +103,18 @@ func (c *TradeCollector) Recover(ctx context.Context, ex types.ExchangeTradeHist return nil } +func (c *TradeCollector) setDone(key types.TradeKey) { + c.mu.Lock() + c.doneTrades[key] = struct{}{} + c.mu.Unlock() +} + // Process filters the received trades and see if there are orders matching the trades // if we have the order in the order store, then the trade will be considered for the position. // profit will also be calculated. func (c *TradeCollector) Process() bool { positionChanged := false + c.tradeStore.Filter(func(trade types.Trade) bool { key := trade.Key() @@ -114,22 +124,28 @@ func (c *TradeCollector) Process() bool { } if c.orderStore.Exists(trade.OrderID) { - c.doneTrades[key] = struct{}{} - profit, netProfit, madeProfit := c.position.AddTrade(trade) - if madeProfit { - p := c.position.NewProfit(trade, profit, netProfit) - c.EmitTrade(trade, profit, netProfit) - c.EmitProfit(trade, &p) + c.setDone(key) + + if c.position != nil { + profit, netProfit, madeProfit := c.position.AddTrade(trade) + if madeProfit { + p := c.position.NewProfit(trade, profit, netProfit) + c.EmitTrade(trade, profit, netProfit) + c.EmitProfit(trade, &p) + } else { + c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero) + c.EmitProfit(trade, nil) + } + positionChanged = true } else { c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero) - c.EmitProfit(trade, nil) } - positionChanged = true return true } return false }) - if positionChanged { + + if positionChanged && c.position != nil { c.EmitPositionUpdate(c.position) } @@ -149,17 +165,22 @@ func (c *TradeCollector) processTrade(trade types.Trade) bool { return false } - profit, netProfit, madeProfit := c.position.AddTrade(trade) - if madeProfit { - p := c.position.NewProfit(trade, profit, netProfit) - c.EmitTrade(trade, profit, netProfit) - c.EmitProfit(trade, &p) + if c.position != nil { + profit, netProfit, madeProfit := c.position.AddTrade(trade) + if madeProfit { + p := c.position.NewProfit(trade, profit, netProfit) + c.EmitTrade(trade, profit, netProfit) + c.EmitProfit(trade, &p) + } else { + c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero) + c.EmitProfit(trade, nil) + } + c.EmitPositionUpdate(c.position) } else { c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero) - c.EmitProfit(trade, nil) } - c.EmitPositionUpdate(c.position) - c.doneTrades[key] = struct{}{} + + c.setDone(key) return true } return false diff --git a/pkg/cmd/import.go b/pkg/cmd/import.go new file mode 100644 index 000000000..6e60754e4 --- /dev/null +++ b/pkg/cmd/import.go @@ -0,0 +1,6 @@ +package cmd + +// import built-in strategies +import ( + _ "github.com/c9s/bbgo/pkg/cmd/strategy" +) diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index 0bfec7e66..27e22194a 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -62,15 +62,15 @@ var RootCmd = &cobra.Command{ } if cpuProfile != "" { - log.Infof("starting cpu profiler...") + log.Infof("starting cpu profiler, recording at %s", cpuProfile) cpuProfileFile, err = os.Create(cpuProfile) if err != nil { - log.Fatal("could not create CPU profile: ", err) + return errors.Wrap(err, "can not create file for CPU profile") } if err := pprof.StartCPUProfile(cpuProfileFile); err != nil { - log.Fatal("could not start CPU profile: ", err) + return errors.Wrap(err, "can not start CPU profile") } } diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index c6f1841d6..23b70438e 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -6,7 +6,6 @@ import ( "os" "os/exec" "path/filepath" - "runtime/pprof" "syscall" "github.com/pkg/errors" @@ -244,11 +243,6 @@ func run(cmd *cobra.Command, args []string) error { return err } - cpuProfile, err := cmd.Flags().GetString("cpu-profile") - if err != nil { - return err - } - if !setup { // if it's not setup, then the config file option is required. if len(configFile) == 0 { @@ -280,20 +274,6 @@ func run(cmd *cobra.Command, args []string) error { return err } - if cpuProfile != "" { - f, err := os.Create(cpuProfile) - if err != nil { - log.Fatal("could not create CPU profile: ", err) - } - defer f.Close() // error handling omitted for example - - if err := pprof.StartCPUProfile(f); err != nil { - log.Fatal("could not start CPU profile: ", err) - } - - defer pprof.StopCPUProfile() - } - return runConfig(ctx, cmd, userConfig) } diff --git a/pkg/cmd/builtin.go b/pkg/cmd/strategy/builtin.go similarity index 98% rename from pkg/cmd/builtin.go rename to pkg/cmd/strategy/builtin.go index 2bb81b931..d00c5f824 100644 --- a/pkg/cmd/builtin.go +++ b/pkg/cmd/strategy/builtin.go @@ -1,4 +1,4 @@ -package cmd +package strategy // import built-in strategies import ( diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 264044bc8..16944bf5a 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -168,7 +168,42 @@ func (e *Exchange) NewStream() types.Stream { return stream } +func (e *Exchange) QueryOrderTrades(ctx context.Context, q types.OrderQuery) ([]types.Trade, error) { + if q.OrderID == "" { + return nil, errors.New("max.QueryOrder: OrderID is required parameter") + } + + orderID, err := strconv.ParseInt(q.OrderID, 10, 64) + if err != nil { + return nil, err + } + + maxTrades, err := e.v3order.NewGetOrderTradesRequest().OrderID(uint64(orderID)).Do(ctx) + if err != nil { + return nil, err + } + + var trades []types.Trade + for _, t := range maxTrades { + localTrade, err := toGlobalTrade(t) + if err != nil { + log.WithError(err).Errorf("can not convert trade: %+v", t) + continue + } + + trades = append(trades, *localTrade) + } + + // ensure everything is sorted ascending + trades = types.SortTradesAscending(trades) + return trades, nil +} + func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.Order, error) { + if q.OrderID == "" { + return nil, errors.New("max.QueryOrder: OrderID is required parameter") + } + orderID, err := strconv.ParseInt(q.OrderID, 10, 64) if err != nil { return nil, err diff --git a/pkg/exchange/max/maxapi/v3/get_order_trades_request.go b/pkg/exchange/max/maxapi/v3/get_order_trades_request.go new file mode 100644 index 000000000..4ab982b02 --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/get_order_trades_request.go @@ -0,0 +1,19 @@ +package v3 + +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE + +import "github.com/c9s/requestgen" + +func (s *OrderService) NewGetOrderTradesRequest() *GetOrderTradesRequest { + return &GetOrderTradesRequest{client: s.Client} +} + +//go:generate GetRequest -url "/api/v3/order/trades" -type GetOrderTradesRequest -responseType []Trade +type GetOrderTradesRequest struct { + client requestgen.AuthenticatedAPIClient + + orderID *uint64 `param:"order_id,omitempty"` + clientOrderID *string `param:"client_oid,omitempty"` +} diff --git a/pkg/exchange/max/maxapi/v3/get_order_trades_request_requestgen.go b/pkg/exchange/max/maxapi/v3/get_order_trades_request_requestgen.go new file mode 100644 index 000000000..10bd1cd44 --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/get_order_trades_request_requestgen.go @@ -0,0 +1,165 @@ +// Code generated by "requestgen -method GET -url /api/v3/order/trades -type GetOrderTradesRequest -responseType []Trade"; DO NOT EDIT. + +package v3 + +import ( + "context" + "encoding/json" + "fmt" + "github.com/c9s/bbgo/pkg/exchange/max/maxapi" + "net/url" + "reflect" + "regexp" +) + +func (g *GetOrderTradesRequest) OrderID(orderID uint64) *GetOrderTradesRequest { + g.orderID = &orderID + return g +} + +func (g *GetOrderTradesRequest) ClientOrderID(clientOrderID string) *GetOrderTradesRequest { + g.clientOrderID = &clientOrderID + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetOrderTradesRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetOrderTradesRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check orderID field -> json key order_id + if g.orderID != nil { + orderID := *g.orderID + + // assign parameter of orderID + params["order_id"] = orderID + } else { + } + // check clientOrderID field -> json key client_oid + if g.clientOrderID != nil { + clientOrderID := *g.clientOrderID + + // assign parameter of clientOrderID + params["client_oid"] = clientOrderID + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetOrderTradesRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetOrderTradesRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetOrderTradesRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetOrderTradesRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetOrderTradesRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetOrderTradesRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetOrderTradesRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetOrderTradesRequest) Do(ctx context.Context) ([]max.Trade, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/api/v3/order/trades" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []max.Trade + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} diff --git a/pkg/types/currencies.go b/pkg/types/currencies.go index 3be262926..4e24c3a83 100644 --- a/pkg/types/currencies.go +++ b/pkg/types/currencies.go @@ -1,10 +1,12 @@ package types -import "math/big" +import ( + "math/big" -import "github.com/leekchan/accounting" + "github.com/leekchan/accounting" -import "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/fixedpoint" +) type Acc = accounting.Accounting @@ -24,6 +26,17 @@ var BNB = wrapper{accounting.Accounting{Symbol: "BNB ", Precision: 4}} var FiatCurrencies = []string{"USDC", "USDT", "USD", "TWD", "EUR", "GBP", "BUSD"} +var USDFiatCurrencies = []string{"USDT", "USDC", "USD", "BUSD"} + +func IsUSDFiatCurrency(currency string) bool { + for _, c := range USDFiatCurrencies { + if c == currency { + return true + } + } + return false +} + func IsFiatCurrency(currency string) bool { for _, c := range FiatCurrencies { if c == currency { diff --git a/pkg/types/exchange.go b/pkg/types/exchange.go index e026decb4..8ce73c283 100644 --- a/pkg/types/exchange.go +++ b/pkg/types/exchange.go @@ -88,6 +88,7 @@ type Exchange interface { // ExchangeOrderQueryService provides an interface for querying the order status via order ID or client order ID type ExchangeOrderQueryService interface { QueryOrder(ctx context.Context, q OrderQuery) (*Order, error) + QueryOrderTrades(ctx context.Context, q OrderQuery) ([]Trade, error) } type ExchangeTradeService interface { diff --git a/pkg/types/kline.go b/pkg/types/kline.go index 6906a1c97..5444c8e3d 100644 --- a/pkg/types/kline.go +++ b/pkg/types/kline.go @@ -214,11 +214,11 @@ func (k KLine) GetChange() fixedpoint.Value { func (k KLine) Color() string { if k.Direction() > 0 { - return GreenColor + return util.GreenColor } else if k.Direction() < 0 { - return RedColor + return util.RedColor } - return GrayColor + return util.GrayColor } func (k KLine) String() string { @@ -368,11 +368,11 @@ func (k KLineWindow) GetTrend() int { func (k KLineWindow) Color() string { if k.GetTrend() > 0 { - return GreenColor + return util.GreenColor } else if k.GetTrend() < 0 { - return RedColor + return util.RedColor } - return GrayColor + return util.GrayColor } // Mid price diff --git a/pkg/types/order.go b/pkg/types/order.go index dc4fced21..d67f83068 100644 --- a/pkg/types/order.go +++ b/pkg/types/order.go @@ -116,13 +116,13 @@ type SubmitOrder struct { Side SideType `json:"side" db:"side"` Type OrderType `json:"orderType" db:"order_type"` - Quantity fixedpoint.Value `json:"quantity" db:"quantity"` - Price fixedpoint.Value `json:"price" db:"price"` + Quantity fixedpoint.Value `json:"quantity" db:"quantity"` + Price fixedpoint.Value `json:"price" db:"price"` // AveragePrice is only used in back-test currently AveragePrice fixedpoint.Value `json:"averagePrice"` - StopPrice fixedpoint.Value `json:"stopPrice,omitempty" db:"stop_price"` + StopPrice fixedpoint.Value `json:"stopPrice,omitempty" db:"stop_price"` Market Market `json:"-" db:"-"` @@ -140,6 +140,39 @@ type SubmitOrder struct { Tag string `json:"tag" db:"-"` } +func (o *SubmitOrder) In() (fixedpoint.Value, string) { + switch o.Side { + case SideTypeBuy: + if o.AveragePrice.IsZero() { + return o.Quantity.Mul(o.Price), o.Market.QuoteCurrency + } else { + return o.Quantity.Mul(o.AveragePrice), o.Market.QuoteCurrency + } + + case SideTypeSell: + return o.Quantity, o.Market.BaseCurrency + + } + + return fixedpoint.Zero, "" +} + +func (o *SubmitOrder) Out() (fixedpoint.Value, string) { + switch o.Side { + case SideTypeBuy: + return o.Quantity, o.Market.BaseCurrency + + case SideTypeSell: + if o.AveragePrice.IsZero() { + return o.Quantity.Mul(o.Price), o.Market.QuoteCurrency + } else { + return o.Quantity.Mul(o.AveragePrice), o.Market.QuoteCurrency + } + } + + return fixedpoint.Zero, "" +} + func (o *SubmitOrder) String() string { switch o.Type { case OrderTypeMarket: diff --git a/pkg/types/orderbook.go b/pkg/types/orderbook.go index 8bae186c8..78d14ced3 100644 --- a/pkg/types/orderbook.go +++ b/pkg/types/orderbook.go @@ -114,12 +114,16 @@ func (b *MutexOrderBook) Update(update SliceOrderBook) { b.Unlock() } +//go:generate callbackgen -type StreamOrderBook // StreamOrderBook receives streaming data from websocket connection and // update the order book with mutex lock, so you can safely access it. type StreamOrderBook struct { *MutexOrderBook C sigchan.Chan + + updateCallbacks []func(update SliceOrderBook) + snapshotCallbacks []func(snapshot SliceOrderBook) } func NewStreamBook(symbol string) *StreamOrderBook { @@ -136,6 +140,7 @@ func (sb *StreamOrderBook) BindStream(stream Stream) { } sb.Load(book) + sb.EmitSnapshot(book) sb.C.Emit() }) @@ -145,6 +150,7 @@ func (sb *StreamOrderBook) BindStream(stream Stream) { } sb.Update(book) + sb.EmitUpdate(book) sb.C.Emit() }) } diff --git a/pkg/types/profit.go b/pkg/types/profit.go index 3bfb4e68a..73da1ed21 100644 --- a/pkg/types/profit.go +++ b/pkg/types/profit.go @@ -64,17 +64,17 @@ type Profit struct { } func (p *Profit) SlackAttachment() slack.Attachment { - var color = pnlColor(p.Profit) + var color = util.PnLColor(p.Profit) var title = fmt.Sprintf("%s PnL ", p.Symbol) - title += pnlEmojiMargin(p.Profit, p.ProfitMargin, defaultPnlLevelResolution) + " " - title += pnlSignString(p.Profit) + " " + p.QuoteCurrency + title += util.PnLEmojiMargin(p.Profit, p.ProfitMargin, util.DefaultPnLLevelResolution) + " " + title += util.PnLSignString(p.Profit) + " " + p.QuoteCurrency var fields []slack.AttachmentField if !p.NetProfit.IsZero() { fields = append(fields, slack.AttachmentField{ Title: "Net Profit", - Value: pnlSignString(p.NetProfit) + " " + p.QuoteCurrency, + Value: util.PnLSignString(p.NetProfit) + " " + p.QuoteCurrency, Short: true, }) } @@ -130,9 +130,9 @@ func (p *Profit) SlackAttachment() slack.Attachment { func (p *Profit) PlainText() string { var emoji string if !p.ProfitMargin.IsZero() { - emoji = pnlEmojiMargin(p.Profit, p.ProfitMargin, defaultPnlLevelResolution) + emoji = util.PnLEmojiMargin(p.Profit, p.ProfitMargin, util.DefaultPnLLevelResolution) } else { - emoji = pnlEmojiSimple(p.Profit) + emoji = util.PnLEmojiSimple(p.Profit) } return fmt.Sprintf("%s trade profit %s %s %s (%s), net profit =~ %s %s (%s)", @@ -145,62 +145,6 @@ func (p *Profit) PlainText() string { ) } -var lossEmoji = "🔥" -var profitEmoji = "💰" -var defaultPnlLevelResolution = fixedpoint.NewFromFloat(0.001) - -func pnlColor(pnl fixedpoint.Value) string { - if pnl.Sign() > 0 { - return GreenColor - } - return RedColor -} - -func pnlSignString(pnl fixedpoint.Value) string { - if pnl.Sign() > 0 { - return "+" + pnl.String() - } - return pnl.String() -} - -func pnlEmojiSimple(pnl fixedpoint.Value) string { - if pnl.Sign() < 0 { - return lossEmoji - } - - if pnl.IsZero() { - return "" - } - - return profitEmoji -} - -func pnlEmojiMargin(pnl, margin, resolution fixedpoint.Value) (out string) { - if margin.IsZero() { - return pnlEmojiSimple(pnl) - } - - if pnl.Sign() < 0 { - out = lossEmoji - level := (margin.Neg()).Div(resolution).Int() - for i := 1; i < level; i++ { - out += lossEmoji - } - return out - } - - if pnl.IsZero() { - return out - } - - out = profitEmoji - level := margin.Div(resolution).Int() - for i := 1; i < level; i++ { - out += profitEmoji - } - return out -} - type ProfitStats struct { Symbol string `json:"symbol"` QuoteCurrency string `json:"quoteCurrency"` @@ -303,8 +247,8 @@ func (s *ProfitStats) PlainText() string { } func (s *ProfitStats) SlackAttachment() slack.Attachment { - var color = pnlColor(s.AccumulatedPnL) - var title = fmt.Sprintf("%s Accumulated PnL %s %s", s.Symbol, pnlSignString(s.AccumulatedPnL), s.QuoteCurrency) + var color = util.PnLColor(s.AccumulatedPnL) + var title = fmt.Sprintf("%s Accumulated PnL %s %s", s.Symbol, util.PnLSignString(s.AccumulatedPnL), s.QuoteCurrency) since := time.Unix(s.AccumulatedSince, 0).Local() title += " Since " + since.Format(time.RFC822) @@ -314,7 +258,7 @@ func (s *ProfitStats) SlackAttachment() slack.Attachment { if !s.TodayPnL.IsZero() { fields = append(fields, slack.AttachmentField{ Title: "P&L Today", - Value: pnlSignString(s.TodayPnL) + " " + s.QuoteCurrency, + Value: util.PnLSignString(s.TodayPnL) + " " + s.QuoteCurrency, Short: true, }) } @@ -322,7 +266,7 @@ func (s *ProfitStats) SlackAttachment() slack.Attachment { if !s.TodayNetProfit.IsZero() { fields = append(fields, slack.AttachmentField{ Title: "Net Profit Today", - Value: pnlSignString(s.TodayNetProfit) + " " + s.QuoteCurrency, + Value: util.PnLSignString(s.TodayNetProfit) + " " + s.QuoteCurrency, Short: true, }) } @@ -330,7 +274,7 @@ func (s *ProfitStats) SlackAttachment() slack.Attachment { if !s.TodayGrossProfit.IsZero() { fields = append(fields, slack.AttachmentField{ Title: "Gross Profit Today", - Value: pnlSignString(s.TodayGrossProfit) + " " + s.QuoteCurrency, + Value: util.PnLSignString(s.TodayGrossProfit) + " " + s.QuoteCurrency, Short: true, }) } @@ -338,7 +282,7 @@ func (s *ProfitStats) SlackAttachment() slack.Attachment { if !s.TodayGrossLoss.IsZero() { fields = append(fields, slack.AttachmentField{ Title: "Gross Loss Today", - Value: pnlSignString(s.TodayGrossLoss) + " " + s.QuoteCurrency, + Value: util.PnLSignString(s.TodayGrossLoss) + " " + s.QuoteCurrency, Short: true, }) } @@ -346,28 +290,28 @@ func (s *ProfitStats) SlackAttachment() slack.Attachment { if !s.AccumulatedPnL.IsZero() { fields = append(fields, slack.AttachmentField{ Title: "Accumulated P&L", - Value: pnlSignString(s.AccumulatedPnL) + " " + s.QuoteCurrency, + Value: util.PnLSignString(s.AccumulatedPnL) + " " + s.QuoteCurrency, }) } if !s.AccumulatedGrossProfit.IsZero() { fields = append(fields, slack.AttachmentField{ Title: "Accumulated Gross Profit", - Value: pnlSignString(s.AccumulatedGrossProfit) + " " + s.QuoteCurrency, + Value: util.PnLSignString(s.AccumulatedGrossProfit) + " " + s.QuoteCurrency, }) } if !s.AccumulatedGrossLoss.IsZero() { fields = append(fields, slack.AttachmentField{ Title: "Accumulated Gross Loss", - Value: pnlSignString(s.AccumulatedGrossLoss) + " " + s.QuoteCurrency, + Value: util.PnLSignString(s.AccumulatedGrossLoss) + " " + s.QuoteCurrency, }) } if !s.AccumulatedNetProfit.IsZero() { fields = append(fields, slack.AttachmentField{ Title: "Accumulated Net Profit", - Value: pnlSignString(s.AccumulatedNetProfit) + " " + s.QuoteCurrency, + Value: util.PnLSignString(s.AccumulatedNetProfit) + " " + s.QuoteCurrency, }) } diff --git a/pkg/types/rbtree.go b/pkg/types/rbtree.go index 6064d742f..3eeae0d88 100644 --- a/pkg/types/rbtree.go +++ b/pkg/types/rbtree.go @@ -314,6 +314,9 @@ func (tree *RBTree) RotateRight(y *RBNode) { y.left = x.right if !x.right.isNil() { + if x.right == nil { + panic(fmt.Errorf("x.right is nil: node = %+v, left = %+v, right = %+v, parent = %+v", x, x.left, x.right, x.parent)) + } x.right.parent = y } diff --git a/pkg/types/side.go b/pkg/types/side.go index 46c916aed..4eef24665 100644 --- a/pkg/types/side.go +++ b/pkg/types/side.go @@ -5,6 +5,8 @@ import ( "strings" "github.com/pkg/errors" + + "github.com/c9s/bbgo/pkg/util" ) // SideType define side type of order @@ -74,14 +76,14 @@ func (side SideType) String() string { func (side SideType) Color() string { if side == SideTypeBuy { - return GreenColor + return util.GreenColor } if side == SideTypeSell { - return RedColor + return util.RedColor } - return GrayColor + return util.GrayColor } func SideToColorName(side SideType) string { diff --git a/pkg/types/streamorderbook_callbacks.go b/pkg/types/streamorderbook_callbacks.go new file mode 100644 index 000000000..ac7e3ed16 --- /dev/null +++ b/pkg/types/streamorderbook_callbacks.go @@ -0,0 +1,25 @@ +// Code generated by "callbackgen -type StreamOrderBook"; DO NOT EDIT. + +package types + +import () + +func (sb *StreamOrderBook) OnUpdate(cb func(update SliceOrderBook)) { + sb.updateCallbacks = append(sb.updateCallbacks, cb) +} + +func (sb *StreamOrderBook) EmitUpdate(update SliceOrderBook) { + for _, cb := range sb.updateCallbacks { + cb(update) + } +} + +func (sb *StreamOrderBook) OnSnapshot(cb func(snapshot SliceOrderBook)) { + sb.snapshotCallbacks = append(sb.snapshotCallbacks, cb) +} + +func (sb *StreamOrderBook) EmitSnapshot(snapshot SliceOrderBook) { + for _, cb := range sb.snapshotCallbacks { + cb(snapshot) + } +} diff --git a/pkg/types/color.go b/pkg/util/colors.go similarity index 85% rename from pkg/types/color.go rename to pkg/util/colors.go index ac8324aa2..b17b407eb 100644 --- a/pkg/types/color.go +++ b/pkg/util/colors.go @@ -1,4 +1,4 @@ -package types +package util const GreenColor = "#228B22" const RedColor = "#800000" diff --git a/pkg/util/emoji.go b/pkg/util/emoji.go new file mode 100644 index 000000000..354718252 --- /dev/null +++ b/pkg/util/emoji.go @@ -0,0 +1,63 @@ +package util + +import ( + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +var LossEmoji = "🔥" +var ProfitEmoji = "💰" +var DefaultPnLLevelResolution = fixedpoint.NewFromFloat(0.001) + +func PnLColor(pnl fixedpoint.Value) string { + if pnl.Sign() > 0 { + return GreenColor + } + return RedColor +} + +func PnLSignString(pnl fixedpoint.Value) string { + if pnl.Sign() > 0 { + return "+" + pnl.String() + } + return pnl.String() +} + + +func PnLEmojiSimple(pnl fixedpoint.Value) string { + if pnl.Sign() < 0 { + return LossEmoji + } + + if pnl.IsZero() { + return "" + } + + return ProfitEmoji +} + +func PnLEmojiMargin(pnl, margin, resolution fixedpoint.Value) (out string) { + if margin.IsZero() { + return PnLEmojiSimple(pnl) + } + + if pnl.Sign() < 0 { + out = LossEmoji + level := (margin.Neg()).Div(resolution).Int() + for i := 1; i < level; i++ { + out += LossEmoji + } + return out + } + + if pnl.IsZero() { + return out + } + + out = ProfitEmoji + level := margin.Div(resolution).Int() + for i := 1; i < level; i++ { + out += ProfitEmoji + } + return out +} +