Merge pull request #868 from c9s/fixes

fix: many minor fixes
This commit is contained in:
Yo-An Lin 2022-08-09 12:11:38 +08:00 committed by GitHub
commit 072b808a6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 467 additions and 141 deletions

View File

@ -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. 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. 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 ( import (

View File

@ -105,18 +105,28 @@ func (b *ActiveOrderBook) GracefulCancel(ctx context.Context, ex types.Exchange)
// verify the current open orders via the RESTful API // verify the current open orders via the RESTful API
log.Warnf("[ActiveOrderBook] using REStful API to verify active orders...") log.Warnf("[ActiveOrderBook] using REStful API to verify active orders...")
openOrders, err := ex.QueryOpenOrders(ctx, b.Symbol)
if err != nil { orders = b.Orders()
log.WithError(err).Errorf("can not query %s open orders", b.Symbol) var symbols = map[string]struct{}{}
continue for _, order := range orders {
symbols[order.Symbol] = struct{}{}
} }
openOrderStore := NewOrderStore(b.Symbol) for symbol := range symbols {
openOrderStore.Add(openOrders...) openOrders, err := ex.QueryOpenOrders(ctx, symbol)
for _, o := range orders { if err != nil {
// if it's not on the order book (open orders), we should remove it from our local side log.WithError(err).Errorf("can not query %s open orders", symbol)
if !openOrderStore.Exists(o.OrderID) { continue
b.Remove(o) }
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)
}
} }
} }
} }

View File

@ -2,6 +2,7 @@ package bbgo
import ( import (
"context" "context"
"sync"
"time" "time"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -22,6 +23,8 @@ type TradeCollector struct {
orderStore *OrderStore orderStore *OrderStore
doneTrades map[types.TradeKey]struct{} doneTrades map[types.TradeKey]struct{}
mu sync.Mutex
recoverCallbacks []func(trade types.Trade) recoverCallbacks []func(trade types.Trade)
tradeCallbacks []func(trade types.Trade, profit, netProfit fixedpoint.Value) 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 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 // 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. // if we have the order in the order store, then the trade will be considered for the position.
// profit will also be calculated. // profit will also be calculated.
func (c *TradeCollector) Process() bool { func (c *TradeCollector) Process() bool {
positionChanged := false positionChanged := false
c.tradeStore.Filter(func(trade types.Trade) bool { c.tradeStore.Filter(func(trade types.Trade) bool {
key := trade.Key() key := trade.Key()
@ -114,22 +124,28 @@ func (c *TradeCollector) Process() bool {
} }
if c.orderStore.Exists(trade.OrderID) { if c.orderStore.Exists(trade.OrderID) {
c.doneTrades[key] = struct{}{} c.setDone(key)
profit, netProfit, madeProfit := c.position.AddTrade(trade)
if madeProfit { if c.position != nil {
p := c.position.NewProfit(trade, profit, netProfit) profit, netProfit, madeProfit := c.position.AddTrade(trade)
c.EmitTrade(trade, profit, netProfit) if madeProfit {
c.EmitProfit(trade, &p) 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 { } else {
c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero) c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero)
c.EmitProfit(trade, nil)
} }
positionChanged = true
return true return true
} }
return false return false
}) })
if positionChanged {
if positionChanged && c.position != nil {
c.EmitPositionUpdate(c.position) c.EmitPositionUpdate(c.position)
} }
@ -149,17 +165,22 @@ func (c *TradeCollector) processTrade(trade types.Trade) bool {
return false return false
} }
profit, netProfit, madeProfit := c.position.AddTrade(trade) if c.position != nil {
if madeProfit { profit, netProfit, madeProfit := c.position.AddTrade(trade)
p := c.position.NewProfit(trade, profit, netProfit) if madeProfit {
c.EmitTrade(trade, profit, netProfit) p := c.position.NewProfit(trade, profit, netProfit)
c.EmitProfit(trade, &p) 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 { } else {
c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero) 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 true
} }
return false return false

6
pkg/cmd/import.go Normal file
View File

@ -0,0 +1,6 @@
package cmd
// import built-in strategies
import (
_ "github.com/c9s/bbgo/pkg/cmd/strategy"
)

View File

@ -62,15 +62,15 @@ var RootCmd = &cobra.Command{
} }
if cpuProfile != "" { if cpuProfile != "" {
log.Infof("starting cpu profiler...") log.Infof("starting cpu profiler, recording at %s", cpuProfile)
cpuProfileFile, err = os.Create(cpuProfile) cpuProfileFile, err = os.Create(cpuProfile)
if err != nil { 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 { if err := pprof.StartCPUProfile(cpuProfileFile); err != nil {
log.Fatal("could not start CPU profile: ", err) return errors.Wrap(err, "can not start CPU profile")
} }
} }

View File

@ -6,7 +6,6 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"runtime/pprof"
"syscall" "syscall"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -244,11 +243,6 @@ func run(cmd *cobra.Command, args []string) error {
return err return err
} }
cpuProfile, err := cmd.Flags().GetString("cpu-profile")
if err != nil {
return err
}
if !setup { if !setup {
// if it's not setup, then the config file option is required. // if it's not setup, then the config file option is required.
if len(configFile) == 0 { if len(configFile) == 0 {
@ -280,20 +274,6 @@ func run(cmd *cobra.Command, args []string) error {
return err 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) return runConfig(ctx, cmd, userConfig)
} }

View File

@ -1,4 +1,4 @@
package cmd package strategy
// import built-in strategies // import built-in strategies
import ( import (

View File

@ -168,7 +168,42 @@ func (e *Exchange) NewStream() types.Stream {
return 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) { 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) orderID, err := strconv.ParseInt(q.OrderID, 10, 64)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -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"`
}

View File

@ -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
}

View File

@ -1,10 +1,12 @@
package types 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 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 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 { func IsFiatCurrency(currency string) bool {
for _, c := range FiatCurrencies { for _, c := range FiatCurrencies {
if c == currency { if c == currency {

View File

@ -88,6 +88,7 @@ type Exchange interface {
// ExchangeOrderQueryService provides an interface for querying the order status via order ID or client order ID // ExchangeOrderQueryService provides an interface for querying the order status via order ID or client order ID
type ExchangeOrderQueryService interface { type ExchangeOrderQueryService interface {
QueryOrder(ctx context.Context, q OrderQuery) (*Order, error) QueryOrder(ctx context.Context, q OrderQuery) (*Order, error)
QueryOrderTrades(ctx context.Context, q OrderQuery) ([]Trade, error)
} }
type ExchangeTradeService interface { type ExchangeTradeService interface {

View File

@ -214,11 +214,11 @@ func (k KLine) GetChange() fixedpoint.Value {
func (k KLine) Color() string { func (k KLine) Color() string {
if k.Direction() > 0 { if k.Direction() > 0 {
return GreenColor return util.GreenColor
} else if k.Direction() < 0 { } else if k.Direction() < 0 {
return RedColor return util.RedColor
} }
return GrayColor return util.GrayColor
} }
func (k KLine) String() string { func (k KLine) String() string {
@ -368,11 +368,11 @@ func (k KLineWindow) GetTrend() int {
func (k KLineWindow) Color() string { func (k KLineWindow) Color() string {
if k.GetTrend() > 0 { if k.GetTrend() > 0 {
return GreenColor return util.GreenColor
} else if k.GetTrend() < 0 { } else if k.GetTrend() < 0 {
return RedColor return util.RedColor
} }
return GrayColor return util.GrayColor
} }
// Mid price // Mid price

View File

@ -116,13 +116,13 @@ type SubmitOrder struct {
Side SideType `json:"side" db:"side"` Side SideType `json:"side" db:"side"`
Type OrderType `json:"orderType" db:"order_type"` Type OrderType `json:"orderType" db:"order_type"`
Quantity fixedpoint.Value `json:"quantity" db:"quantity"` Quantity fixedpoint.Value `json:"quantity" db:"quantity"`
Price fixedpoint.Value `json:"price" db:"price"` Price fixedpoint.Value `json:"price" db:"price"`
// AveragePrice is only used in back-test currently // AveragePrice is only used in back-test currently
AveragePrice fixedpoint.Value `json:"averagePrice"` 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:"-"` Market Market `json:"-" db:"-"`
@ -140,6 +140,39 @@ type SubmitOrder struct {
Tag string `json:"tag" db:"-"` 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 { func (o *SubmitOrder) String() string {
switch o.Type { switch o.Type {
case OrderTypeMarket: case OrderTypeMarket:

View File

@ -114,12 +114,16 @@ func (b *MutexOrderBook) Update(update SliceOrderBook) {
b.Unlock() b.Unlock()
} }
//go:generate callbackgen -type StreamOrderBook
// StreamOrderBook receives streaming data from websocket connection and // StreamOrderBook receives streaming data from websocket connection and
// update the order book with mutex lock, so you can safely access it. // update the order book with mutex lock, so you can safely access it.
type StreamOrderBook struct { type StreamOrderBook struct {
*MutexOrderBook *MutexOrderBook
C sigchan.Chan C sigchan.Chan
updateCallbacks []func(update SliceOrderBook)
snapshotCallbacks []func(snapshot SliceOrderBook)
} }
func NewStreamBook(symbol string) *StreamOrderBook { func NewStreamBook(symbol string) *StreamOrderBook {
@ -136,6 +140,7 @@ func (sb *StreamOrderBook) BindStream(stream Stream) {
} }
sb.Load(book) sb.Load(book)
sb.EmitSnapshot(book)
sb.C.Emit() sb.C.Emit()
}) })
@ -145,6 +150,7 @@ func (sb *StreamOrderBook) BindStream(stream Stream) {
} }
sb.Update(book) sb.Update(book)
sb.EmitUpdate(book)
sb.C.Emit() sb.C.Emit()
}) })
} }

View File

@ -64,17 +64,17 @@ type Profit struct {
} }
func (p *Profit) SlackAttachment() slack.Attachment { 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) var title = fmt.Sprintf("%s PnL ", p.Symbol)
title += pnlEmojiMargin(p.Profit, p.ProfitMargin, defaultPnlLevelResolution) + " " title += util.PnLEmojiMargin(p.Profit, p.ProfitMargin, util.DefaultPnLLevelResolution) + " "
title += pnlSignString(p.Profit) + " " + p.QuoteCurrency title += util.PnLSignString(p.Profit) + " " + p.QuoteCurrency
var fields []slack.AttachmentField var fields []slack.AttachmentField
if !p.NetProfit.IsZero() { if !p.NetProfit.IsZero() {
fields = append(fields, slack.AttachmentField{ fields = append(fields, slack.AttachmentField{
Title: "Net Profit", Title: "Net Profit",
Value: pnlSignString(p.NetProfit) + " " + p.QuoteCurrency, Value: util.PnLSignString(p.NetProfit) + " " + p.QuoteCurrency,
Short: true, Short: true,
}) })
} }
@ -130,9 +130,9 @@ func (p *Profit) SlackAttachment() slack.Attachment {
func (p *Profit) PlainText() string { func (p *Profit) PlainText() string {
var emoji string var emoji string
if !p.ProfitMargin.IsZero() { if !p.ProfitMargin.IsZero() {
emoji = pnlEmojiMargin(p.Profit, p.ProfitMargin, defaultPnlLevelResolution) emoji = util.PnLEmojiMargin(p.Profit, p.ProfitMargin, util.DefaultPnLLevelResolution)
} else { } 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)", 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 { type ProfitStats struct {
Symbol string `json:"symbol"` Symbol string `json:"symbol"`
QuoteCurrency string `json:"quoteCurrency"` QuoteCurrency string `json:"quoteCurrency"`
@ -303,8 +247,8 @@ func (s *ProfitStats) PlainText() string {
} }
func (s *ProfitStats) SlackAttachment() slack.Attachment { func (s *ProfitStats) SlackAttachment() slack.Attachment {
var color = pnlColor(s.AccumulatedPnL) var color = util.PnLColor(s.AccumulatedPnL)
var title = fmt.Sprintf("%s Accumulated PnL %s %s", s.Symbol, pnlSignString(s.AccumulatedPnL), s.QuoteCurrency) var title = fmt.Sprintf("%s Accumulated PnL %s %s", s.Symbol, util.PnLSignString(s.AccumulatedPnL), s.QuoteCurrency)
since := time.Unix(s.AccumulatedSince, 0).Local() since := time.Unix(s.AccumulatedSince, 0).Local()
title += " Since " + since.Format(time.RFC822) title += " Since " + since.Format(time.RFC822)
@ -314,7 +258,7 @@ func (s *ProfitStats) SlackAttachment() slack.Attachment {
if !s.TodayPnL.IsZero() { if !s.TodayPnL.IsZero() {
fields = append(fields, slack.AttachmentField{ fields = append(fields, slack.AttachmentField{
Title: "P&L Today", Title: "P&L Today",
Value: pnlSignString(s.TodayPnL) + " " + s.QuoteCurrency, Value: util.PnLSignString(s.TodayPnL) + " " + s.QuoteCurrency,
Short: true, Short: true,
}) })
} }
@ -322,7 +266,7 @@ func (s *ProfitStats) SlackAttachment() slack.Attachment {
if !s.TodayNetProfit.IsZero() { if !s.TodayNetProfit.IsZero() {
fields = append(fields, slack.AttachmentField{ fields = append(fields, slack.AttachmentField{
Title: "Net Profit Today", Title: "Net Profit Today",
Value: pnlSignString(s.TodayNetProfit) + " " + s.QuoteCurrency, Value: util.PnLSignString(s.TodayNetProfit) + " " + s.QuoteCurrency,
Short: true, Short: true,
}) })
} }
@ -330,7 +274,7 @@ func (s *ProfitStats) SlackAttachment() slack.Attachment {
if !s.TodayGrossProfit.IsZero() { if !s.TodayGrossProfit.IsZero() {
fields = append(fields, slack.AttachmentField{ fields = append(fields, slack.AttachmentField{
Title: "Gross Profit Today", Title: "Gross Profit Today",
Value: pnlSignString(s.TodayGrossProfit) + " " + s.QuoteCurrency, Value: util.PnLSignString(s.TodayGrossProfit) + " " + s.QuoteCurrency,
Short: true, Short: true,
}) })
} }
@ -338,7 +282,7 @@ func (s *ProfitStats) SlackAttachment() slack.Attachment {
if !s.TodayGrossLoss.IsZero() { if !s.TodayGrossLoss.IsZero() {
fields = append(fields, slack.AttachmentField{ fields = append(fields, slack.AttachmentField{
Title: "Gross Loss Today", Title: "Gross Loss Today",
Value: pnlSignString(s.TodayGrossLoss) + " " + s.QuoteCurrency, Value: util.PnLSignString(s.TodayGrossLoss) + " " + s.QuoteCurrency,
Short: true, Short: true,
}) })
} }
@ -346,28 +290,28 @@ func (s *ProfitStats) SlackAttachment() slack.Attachment {
if !s.AccumulatedPnL.IsZero() { if !s.AccumulatedPnL.IsZero() {
fields = append(fields, slack.AttachmentField{ fields = append(fields, slack.AttachmentField{
Title: "Accumulated P&L", Title: "Accumulated P&L",
Value: pnlSignString(s.AccumulatedPnL) + " " + s.QuoteCurrency, Value: util.PnLSignString(s.AccumulatedPnL) + " " + s.QuoteCurrency,
}) })
} }
if !s.AccumulatedGrossProfit.IsZero() { if !s.AccumulatedGrossProfit.IsZero() {
fields = append(fields, slack.AttachmentField{ fields = append(fields, slack.AttachmentField{
Title: "Accumulated Gross Profit", Title: "Accumulated Gross Profit",
Value: pnlSignString(s.AccumulatedGrossProfit) + " " + s.QuoteCurrency, Value: util.PnLSignString(s.AccumulatedGrossProfit) + " " + s.QuoteCurrency,
}) })
} }
if !s.AccumulatedGrossLoss.IsZero() { if !s.AccumulatedGrossLoss.IsZero() {
fields = append(fields, slack.AttachmentField{ fields = append(fields, slack.AttachmentField{
Title: "Accumulated Gross Loss", Title: "Accumulated Gross Loss",
Value: pnlSignString(s.AccumulatedGrossLoss) + " " + s.QuoteCurrency, Value: util.PnLSignString(s.AccumulatedGrossLoss) + " " + s.QuoteCurrency,
}) })
} }
if !s.AccumulatedNetProfit.IsZero() { if !s.AccumulatedNetProfit.IsZero() {
fields = append(fields, slack.AttachmentField{ fields = append(fields, slack.AttachmentField{
Title: "Accumulated Net Profit", Title: "Accumulated Net Profit",
Value: pnlSignString(s.AccumulatedNetProfit) + " " + s.QuoteCurrency, Value: util.PnLSignString(s.AccumulatedNetProfit) + " " + s.QuoteCurrency,
}) })
} }

View File

@ -314,6 +314,9 @@ func (tree *RBTree) RotateRight(y *RBNode) {
y.left = x.right y.left = x.right
if !x.right.isNil() { 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 x.right.parent = y
} }

View File

@ -5,6 +5,8 @@ import (
"strings" "strings"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/c9s/bbgo/pkg/util"
) )
// SideType define side type of order // SideType define side type of order
@ -74,14 +76,14 @@ func (side SideType) String() string {
func (side SideType) Color() string { func (side SideType) Color() string {
if side == SideTypeBuy { if side == SideTypeBuy {
return GreenColor return util.GreenColor
} }
if side == SideTypeSell { if side == SideTypeSell {
return RedColor return util.RedColor
} }
return GrayColor return util.GrayColor
} }
func SideToColorName(side SideType) string { func SideToColorName(side SideType) string {

View File

@ -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)
}
}

View File

@ -1,4 +1,4 @@
package types package util
const GreenColor = "#228B22" const GreenColor = "#228B22"
const RedColor = "#800000" const RedColor = "#800000"

63
pkg/util/emoji.go Normal file
View File

@ -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
}