mirror of
https://github.com/c9s/bbgo.git
synced 2024-09-20 08:11:08 +00:00
commit
072b808a6e
|
@ -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 (
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
6
pkg/cmd/import.go
Normal file
6
pkg/cmd/import.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package cmd
|
||||
|
||||
// import built-in strategies
|
||||
import (
|
||||
_ "github.com/c9s/bbgo/pkg/cmd/strategy"
|
||||
)
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package cmd
|
||||
package strategy
|
||||
|
||||
// import built-in strategies
|
||||
import (
|
|
@ -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
|
||||
|
|
19
pkg/exchange/max/maxapi/v3/get_order_trades_request.go
Normal file
19
pkg/exchange/max/maxapi/v3/get_order_trades_request.go
Normal 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"`
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
25
pkg/types/streamorderbook_callbacks.go
Normal file
25
pkg/types/streamorderbook_callbacks.go
Normal 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)
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package types
|
||||
package util
|
||||
|
||||
const GreenColor = "#228B22"
|
||||
const RedColor = "#800000"
|
63
pkg/util/emoji.go
Normal file
63
pkg/util/emoji.go
Normal 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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user