mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +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.
|
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 (
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
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 != "" {
|
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package cmd
|
package strategy
|
||||||
|
|
||||||
// import built-in strategies
|
// import built-in strategies
|
||||||
import (
|
import (
|
|
@ -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
|
||||||
|
|
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
|
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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
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 GreenColor = "#228B22"
|
||||||
const RedColor = "#800000"
|
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