abstract binance exchange

This commit is contained in:
c9s 2020-07-11 13:02:53 +08:00
parent 5665f6db97
commit 0ae851a085
12 changed files with 1013 additions and 1006 deletions

View File

@ -1,332 +0,0 @@
package bbgo
import (
"context"
"fmt"
"github.com/adshao/go-binance"
"github.com/c9s/bbgo/pkg/types"
"github.com/gorilla/websocket"
log "github.com/sirupsen/logrus"
"strconv"
"strings"
"time"
)
type SubscribeOptions struct {
Interval string
Depth string
}
func (o SubscribeOptions) String() string {
if len(o.Interval) > 0 {
return o.Interval
}
return o.Depth
}
type Subscription struct {
Symbol string
Channel string
Options SubscribeOptions
}
func (s *Subscription) String() string {
// binance uses lower case symbol name
return fmt.Sprintf("%s@%s_%s", strings.ToLower(s.Symbol), s.Channel, s.Options.String())
}
type StreamRequest struct {
// request ID is required
ID int `json:"id"`
Method string `json:"method"`
Params []string `json:"params"`
}
type PrivateStream struct {
Client *binance.Client
ListenKey string
Conn *websocket.Conn
Subscriptions []Subscription
}
func (s *PrivateStream) Subscribe(channel string, symbol string, options SubscribeOptions) {
s.Subscriptions = append(s.Subscriptions, Subscription{
Channel: channel,
Symbol: symbol,
Options: options,
})
}
func (s *PrivateStream) Connect(ctx context.Context, eventC chan interface{}) error {
url := "wss://stream.binance.com:9443/ws/" + s.ListenKey
conn, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
return err
}
log.Infof("[binance] websocket connected")
s.Conn = conn
var params []string
for _, subscription := range s.Subscriptions {
params = append(params, subscription.String())
}
log.Infof("[binance] subscribing channels: %+v", params)
err = conn.WriteJSON(StreamRequest{
Method: "SUBSCRIBE",
Params: params,
ID: 1,
})
if err != nil {
return err
}
go s.read(ctx, eventC)
return nil
}
func (s *PrivateStream) read(ctx context.Context, eventC chan interface{}) {
defer close(eventC)
ticker := time.NewTicker(30 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
err := s.Client.NewKeepaliveUserStreamService().ListenKey(s.ListenKey).Do(ctx)
if err != nil {
log.WithError(err).Error("listen key keep-alive error", err)
}
default:
if err := s.Conn.SetReadDeadline(time.Now().Add(15 * time.Second)); err != nil {
log.WithError(err).Errorf("set read deadline error: %s", err.Error())
}
mt, message, err := s.Conn.ReadMessage()
if err != nil {
log.WithError(err).Errorf("read error: %s", err.Error())
return
}
// skip non-text messages
if mt != websocket.TextMessage {
continue
}
log.Debugf("[binance] recv: %s", message)
e, err := parseEvent(string(message))
if err != nil {
log.WithError(err).Errorf("[binance] event parse error")
continue
}
eventC <- e
}
}
}
func (s *PrivateStream) Close() error {
log.Infof("[binance] closing user data stream...")
defer s.Conn.Close()
// use background context to close user stream
err := s.Client.NewCloseUserStreamService().ListenKey(s.ListenKey).Do(context.Background())
if err != nil {
log.WithError(err).Error("[binance] error close user data stream")
return err
}
return err
}
type BinanceExchange struct {
Client *binance.Client
}
func (e *BinanceExchange) QueryAveragePrice(ctx context.Context, symbol string) (float64, error) {
resp, err := e.Client.NewAveragePriceService().Symbol(symbol).Do(ctx)
if err != nil {
return 0, err
}
return MustParseFloat(resp.Price), nil
}
func (e *BinanceExchange) NewPrivateStream(ctx context.Context) (*PrivateStream, error) {
log.Infof("[binance] creating user data stream...")
listenKey, err := e.Client.NewStartUserStreamService().Do(ctx)
if err != nil {
return nil, err
}
log.Infof("[binance] user data stream created. listenKey: %s", listenKey)
return &PrivateStream{
Client: e.Client,
ListenKey: listenKey,
}, nil
}
func (e *BinanceExchange) SubmitOrder(ctx context.Context, order *Order) error {
/*
limit order example
order, err := Client.NewCreateOrderService().
Symbol(Symbol).
Side(side).
Type(binance.OrderTypeLimit).
TimeInForce(binance.TimeInForceTypeGTC).
Quantity(volumeString).
Price(priceString).
Do(ctx)
*/
req := e.Client.NewCreateOrderService().
Symbol(order.Symbol).
Side(order.Side).
Type(order.Type).
Quantity(order.VolumeStr)
if len(order.PriceStr) > 0 {
req.Price(order.PriceStr)
}
if len(order.TimeInForce) > 0 {
req.TimeInForce(order.TimeInForce)
}
retOrder, err := req.Do(ctx)
log.Infof("[binance] order created: %+v", retOrder)
return err
}
func (e *BinanceExchange) QueryKLines(ctx context.Context, symbol, interval string, limit int) ([]KLine, error) {
log.Infof("[binance] querying kline %s %s limit %d", symbol, interval, limit)
resp, err := e.Client.NewKlinesService().Symbol(symbol).Interval(interval).Limit(limit).Do(ctx)
if err != nil {
return nil, err
}
var kLines []KLine
for _, kline := range resp {
kLines = append(kLines, KLine{
Symbol: symbol,
Interval: interval,
StartTime: kline.OpenTime,
EndTime: kline.CloseTime,
Open: kline.Open,
Close: kline.Close,
High: kline.High,
Low: kline.Low,
Volume: kline.Volume,
QuoteVolume: kline.QuoteAssetVolume,
NumberOfTrades: kline.TradeNum,
})
}
return kLines, nil
}
func (e *BinanceExchange) QueryTrades(ctx context.Context, symbol string, startTime time.Time) (trades []types.Trade, err error) {
log.Infof("[binance] querying %s trades from %s", symbol, startTime)
var lastTradeID int64 = 0
for {
req := e.Client.NewListTradesService().
Limit(1000).
Symbol(symbol).
StartTime(startTime.UnixNano() / 1000000)
if lastTradeID > 0 {
req.FromID(lastTradeID)
}
bnTrades, err := req.Do(ctx)
if err != nil {
return nil, err
}
if len(bnTrades) <= 1 {
break
}
buyerOrSellerLabel := func(trade *binance.TradeV3) (o string) {
if trade.IsBuyer {
o = "BUYER"
} else {
o = "SELLER"
}
return o
}
makerOrTakerLabel := func(trade *binance.TradeV3) (o string) {
if trade.IsMaker {
o += "MAKER"
} else {
o += "TAKER"
}
return o
}
for _, t := range bnTrades {
// skip trade ID that is the same. however this should not happen
if t.ID == lastTradeID {
continue
}
var side string
if t.IsBuyer {
side = "BUY"
} else {
side = "SELL"
}
// trade time
tt := time.Unix(0, t.Time*1000000)
log.Infof("[binance] trade: %d %s % 4s price: % 13s volume: % 11s %6s % 5s %s", t.ID, t.Symbol, side, t.Price, t.Quantity, buyerOrSellerLabel(t), makerOrTakerLabel(t), tt)
price, err := strconv.ParseFloat(t.Price, 64)
if err != nil {
return nil, err
}
quantity, err := strconv.ParseFloat(t.Quantity, 64)
if err != nil {
return nil, err
}
fee, err := strconv.ParseFloat(t.Commission, 64)
if err != nil {
return nil, err
}
trades = append(trades, types.Trade{
ID: t.ID,
Price: price,
Volume: quantity,
Side: side,
IsBuyer: t.IsBuyer,
IsMaker: t.IsMaker,
Fee: fee,
FeeCurrency: t.CommissionAsset,
Time: tt,
})
lastTradeID = t.ID
}
}
return trades, nil
}

View File

@ -1,8 +1,10 @@
package bbgo
import "github.com/c9s/bbgo/pkg/types"
type TradingContext struct {
KLineWindowSize int
KLineWindows map[string]KLineWindow
KLineWindows map[string]types.KLineWindow
Symbol string
@ -20,7 +22,7 @@ func (c *TradingContext) SetCurrentPrice(price float64) {
c.ProfitAndLossCalculator.SetCurrentPrice(price)
}
func (c *TradingContext) AddKLine(kline KLine) KLineWindow {
func (c *TradingContext) AddKLine(kline types.KLine) types.KLineWindow {
var klineWindow = c.KLineWindows[kline.Interval]
klineWindow.Add(kline)

View File

@ -1,422 +1,12 @@
package bbgo
import (
"fmt"
"github.com/slack-go/slack"
"math"
"strconv"
"github.com/c9s/bbgo/pkg/types"
)
type KLineEvent struct {
EventBase
Symbol string `json:"s"`
KLine *KLine `json:"k,omitempty"`
Symbol string `json:"s"`
KLine *types.KLine `json:"k,omitempty"`
}
type KLine struct {
StartTime int64 `json:"t"`
EndTime int64 `json:"T"`
Symbol string `json:"s"`
Interval string `json:"i"`
Open string `json:"o"`
Close string `json:"c"`
High string `json:"h"`
Low string `json:"l"`
Volume string `json:"V"` // taker buy base asset volume (like 10 BTC)
QuoteVolume string `json:"Q"` // taker buy quote asset volume (like 1000USDT)
LastTradeID int `json:"L"`
NumberOfTrades int64 `json:"n"`
Closed bool `json:"x"`
}
func (k KLine) Mid() float64 {
return (k.GetHigh() + k.GetLow()) / 2
}
// green candle with open and close near high price
func (k KLine) BounceUp() bool {
mid := k.Mid()
trend := k.GetTrend()
return trend > 0 && k.GetOpen() > mid && k.GetClose() > mid
}
// red candle with open and close near low price
func (k KLine) BounceDown() bool {
mid := k.Mid()
trend := k.GetTrend()
return trend > 0 && k.GetOpen() < mid && k.GetClose() < mid
}
func (k KLine) GetTrend() int {
o := k.GetOpen()
c := k.GetClose()
if c > o {
return 1
} else if c < o {
return -1
}
return 0
}
func (k KLine) GetHigh() float64 {
return MustParseFloat(k.High)
}
func (k KLine) GetLow() float64 {
return MustParseFloat(k.Low)
}
func (k KLine) GetOpen() float64 {
return MustParseFloat(k.Open)
}
func (k KLine) GetClose() float64 {
return MustParseFloat(k.Close)
}
func (k KLine) GetMaxChange() float64 {
return k.GetHigh() - k.GetLow()
}
// GetThickness returns the thickness of the kline. 1 => thick, 0.1 => thin
func (k KLine) GetThickness() float64 {
return math.Abs(k.GetChange()) / math.Abs(k.GetMaxChange())
}
func (k KLine) GetUpperShadowRatio() float64 {
return k.GetUpperShadowHeight() / math.Abs(k.GetMaxChange())
}
func (k KLine) GetUpperShadowHeight() float64 {
high := k.GetHigh()
if k.GetOpen() > k.GetClose() {
return high - k.GetOpen()
}
return high - k.GetClose()
}
func (k KLine) GetLowerShadowRatio() float64 {
return k.GetLowerShadowHeight() / math.Abs(k.GetMaxChange())
}
func (k KLine) GetLowerShadowHeight() float64 {
low := k.GetLow()
if k.GetOpen() < k.GetClose() {
return k.GetOpen() - low
}
return k.GetClose() - low
}
// GetBody returns the height of the candle real body
func (k KLine) GetBody() float64 {
return k.GetChange()
}
func (k KLine) GetChange() float64 {
return k.GetClose() - k.GetOpen()
}
func (k KLine) String() string {
return fmt.Sprintf("%s %s Open: % 14s Close: % 14s High: % 14s Low: % 14s Volume: % 15s Change: % 11f Max Change: % 11f", k.Symbol, k.Interval, k.Open, k.Close, k.High, k.Low, k.Volume, k.GetChange(), k.GetMaxChange())
}
func (k KLine) Color() string {
if k.GetTrend() > 0 {
return Green
} else if k.GetTrend() < 0 {
return Red
}
return "#f0f0f0"
}
func (k KLine) SlackAttachment() slack.Attachment {
return slack.Attachment{
Text: "KLine",
Color: k.Color(),
Fields: []slack.AttachmentField{
{
Title: "Open",
Value: k.Open,
Short: true,
},
{
Title: "Close",
Value: k.Close,
Short: true,
},
{
Title: "High",
Value: k.High,
Short: true,
},
{
Title: "Low",
Value: k.Low,
Short: true,
},
{
Title: "Mid",
Value: formatFloat(k.Mid(), 2),
Short: true,
},
{
Title: "Change",
Value: formatFloat(k.GetChange(), 2),
Short: true,
},
{
Title: "Max Change",
Value: formatFloat(k.GetMaxChange(), 2),
Short: true,
},
{
Title: "Thickness",
Value: formatFloat(k.GetThickness(), 4),
Short: true,
},
{
Title: "UpperShadowRatio",
Value: formatFloat(k.GetUpperShadowRatio(), 4),
Short: true,
},
{
Title: "LowerShadowRatio",
Value: formatFloat(k.GetLowerShadowRatio(), 4),
Short: true,
},
},
Footer: "",
FooterIcon: "",
}
}
type KLineWindow []KLine
func (k KLineWindow) Len() int {
return len(k)
}
func (k KLineWindow) GetOpen() float64 {
return k[0].GetOpen()
}
func (k KLineWindow) GetClose() float64 {
end := len(k) - 1
return k[end].GetClose()
}
func (k KLineWindow) GetHigh() float64 {
high := k.GetOpen()
for _, line := range k {
val := line.GetHigh()
if val > high {
high = val
}
}
return high
}
func (k KLineWindow) GetLow() float64 {
low := k.GetOpen()
for _, line := range k {
val := line.GetLow()
if val < low {
low = val
}
}
return low
}
func (k KLineWindow) GetChange() float64 {
return k.GetClose() - k.GetOpen()
}
func (k KLineWindow) GetMaxChange() float64 {
return k.GetHigh() - k.GetLow()
}
func (k KLineWindow) AllDrop() bool {
for _, n := range k {
if n.GetTrend() >= 0 {
return false
}
}
return true
}
func (k KLineWindow) AllRise() bool {
for _, n := range k {
if n.GetTrend() <= 0 {
return false
}
}
return true
}
func (k KLineWindow) GetTrend() int {
o := k.GetOpen()
c := k.GetClose()
if c > o {
return 1
} else if c < o {
return -1
}
return 0
}
func (k KLineWindow) Color() string {
if k.GetTrend() > 0 {
return Green
} else if k.GetTrend() < 0 {
return Red
}
return "#f0f0f0"
}
func (k KLineWindow) Mid() float64 {
return k.GetHigh() - k.GetLow()/2
}
// green candle with open and close near high price
func (k KLineWindow) BounceUp() bool {
mid := k.Mid()
trend := k.GetTrend()
return trend > 0 && k.GetOpen() > mid && k.GetClose() > mid
}
// red candle with open and close near low price
func (k KLineWindow) BounceDown() bool {
mid := k.Mid()
trend := k.GetTrend()
return trend > 0 && k.GetOpen() < mid && k.GetClose() < mid
}
func (k *KLineWindow) Add(line KLine) {
*k = append(*k, line)
}
func (k KLineWindow) Take(size int) KLineWindow {
return k[:size]
}
func (k KLineWindow) Tail(size int) KLineWindow {
if len(k) <= size {
return k[:]
}
return k[len(k)-size:]
}
func (k *KLineWindow) Truncate(size int) {
if len(*k) <= size {
return
}
end := len(*k) - 1
start := end - size
if start < 0 {
start = 0
}
*k = (*k)[end-5 : end]
}
func (k KLineWindow) GetBody() float64 {
return k.GetChange()
}
func (k KLineWindow) GetThickness() float64 {
return math.Abs(k.GetChange()) / math.Abs(k.GetMaxChange())
}
func (k KLineWindow) GetUpperShadowRatio() float64 {
return k.GetUpperShadowHeight() / math.Abs(k.GetMaxChange())
}
func (k KLineWindow) GetUpperShadowHeight() float64 {
high := k.GetHigh()
if k.GetOpen() > k.GetClose() {
return high - k.GetOpen()
}
return high - k.GetClose()
}
func (k KLineWindow) GetLowerShadowRatio() float64 {
return k.GetLowerShadowHeight() / math.Abs(k.GetMaxChange())
}
func (k KLineWindow) GetLowerShadowHeight() float64 {
low := k.GetLow()
if k.GetOpen() < k.GetClose() {
return k.GetOpen() - low
}
return k.GetClose() - low
}
func (k KLineWindow) SlackAttachment() slack.Attachment {
return slack.Attachment{
Text: "KLine",
Color: k.Color(),
Fields: []slack.AttachmentField{
{
Title: "Open",
Value: formatFloat(k.GetOpen(), 2),
Short: true,
},
{
Title: "Close",
Value: formatFloat(k.GetClose(), 2),
Short: true,
},
{
Title: "High",
Value: formatFloat(k.GetHigh(), 2),
Short: true,
},
{
Title: "Low",
Value: formatFloat(k.GetLow(), 2),
Short: true,
},
{
Title: "Mid",
Value: formatFloat(k.Mid(), 2),
Short: true,
},
{
Title: "Change",
Value: formatFloat(k.GetChange(), 2),
Short: true,
},
{
Title: "Max Change",
Value: formatFloat(k.GetMaxChange(), 2),
Short: true,
},
{
Title: "Thickness",
Value: formatFloat(k.GetThickness(), 4),
Short: true,
},
{
Title: "UpperShadowRatio",
Value: formatFloat(k.GetUpperShadowRatio(), 4),
Short: true,
},
{
Title: "LowerShadowRatio",
Value: formatFloat(k.GetLowerShadowRatio(), 4),
Short: true,
},
},
Footer: "",
FooterIcon: "",
}
}
func formatFloat(val float64, prec int) string {
return strconv.FormatFloat(val, 'f', prec, 64)
}

View File

@ -4,6 +4,7 @@ import (
"fmt"
"github.com/adshao/go-binance"
"github.com/c9s/bbgo/pkg/bbgo/types"
"github.com/c9s/bbgo/pkg/util"
"github.com/slack-go/slack"
"math"
"strconv"
@ -70,7 +71,7 @@ func (d *KLineDetector) SlackAttachment() slack.Attachment {
if d.EnableMinThickness && NotZero(d.MinThickness) {
fields = append(fields, slack.AttachmentField{
Title: "MinThickness",
Value: formatFloat(d.MinThickness, 4),
Value: util.FormatFloat(d.MinThickness, 4),
Short: true,
})
}
@ -78,7 +79,7 @@ func (d *KLineDetector) SlackAttachment() slack.Attachment {
if d.EnableMaxShadowRatio && NotZero(d.MaxShadowRatio) {
fields = append(fields, slack.AttachmentField{
Title: "MaxShadowRatio",
Value: formatFloat(d.MaxShadowRatio, 4),
Value: util.FormatFloat(d.MaxShadowRatio, 4),
Short: true,
})
}

View File

@ -1,238 +1,2 @@
package bbgo
import (
"encoding/json"
"errors"
"fmt"
"github.com/c9s/bbgo/pkg/types"
"time"
"github.com/valyala/fastjson"
)
/*
executionReport
{
"e": "executionReport", // KLineEvent type
"E": 1499405658658, // KLineEvent time
"s": "ETHBTC", // Symbol
"c": "mUvoqJxFIILMdfAW5iGSOW", // Client order ID
"S": "BUY", // Side
"o": "LIMIT", // Order type
"f": "GTC", // Time in force
"q": "1.00000000", // Order quantity
"p": "0.10264410", // Order price
"P": "0.00000000", // Stop price
"F": "0.00000000", // Iceberg quantity
"g": -1, // OrderListId
"C": null, // Original client order ID; This is the ID of the order being canceled
"x": "NEW", // Current execution type
"X": "NEW", // Current order status
"r": "NONE", // Order reject reason; will be an error code.
"i": 4293153, // Order ID
"l": "0.00000000", // Last executed quantity
"z": "0.00000000", // Cumulative filled quantity
"L": "0.00000000", // Last executed price
"n": "0", // Commission amount
"N": null, // Commission asset
"T": 1499405658657, // Transaction time
"t": -1, // Trade ID
"I": 8641984, // Ignore
"w": true, // Is the order on the book?
"m": false, // Is this trade the maker side?
"M": false, // Ignore
"O": 1499405658657, // Order creation time
"Z": "0.00000000", // Cumulative quote asset transacted quantity
"Y": "0.00000000", // Last quote asset transacted quantity (i.e. lastPrice * lastQty)
"Q": "0.00000000" // Quote Order Qty
}
*/
type BinanceExecutionReportEvent struct {
EventBase
Symbol string `json:"s"`
ClientOrderID string `json:"c"`
Side string `json:"S"`
OrderType string `json:"o"`
TimeInForce string `json:"f"`
OrderQuantity string `json:"q"`
OrderPrice string `json:"p"`
StopPrice string `json:"P"`
IsOnBook bool `json:"w"`
IsMaker bool `json:"m"`
CommissionAmount string `json:"n"`
CommissionAsset string `json:"N"`
CurrentExecutionType string `json:"x"`
CurrentOrderStatus string `json:"X"`
OrderID int `json:"i"`
TradeID int64 `json:"t"`
TransactionTime int64 `json:"T"`
LastExecutedQuantity string `json:"l"`
CumulativeFilledQuantity string `json:"z"`
LastExecutedPrice string `json:"L"`
OrderCreationTime int `json:"O"`
}
func (e *BinanceExecutionReportEvent) Trade() (*types.Trade, error) {
if e.CurrentExecutionType != "TRADE" {
return nil, errors.New("execution report is not a trade")
}
tt := time.Unix(0, e.TransactionTime/1000000)
return &types.Trade{
ID: e.TradeID,
Symbol: e.Symbol,
Price: MustParseFloat(e.LastExecutedPrice),
Volume: MustParseFloat(e.LastExecutedQuantity),
IsBuyer: e.Side == "BUY",
IsMaker: e.IsMaker,
Time: tt,
Fee: MustParseFloat(e.CommissionAmount),
FeeCurrency: e.CommissionAsset,
}, nil
}
/*
balanceUpdate
{
"e": "balanceUpdate", //KLineEvent Type
"E": 1573200697110, //KLineEvent Time
"a": "BTC", //Asset
"d": "100.00000000", //Balance Delta
"T": 1573200697068 //Clear Time
}
*/
type BalanceUpdateEvent struct {
EventBase
Asset string `json:"a"`
Delta string `json:"d"`
ClearTime int64 `json:"T"`
}
/*
outboundAccountInfo
{
"e": "outboundAccountInfo", // KLineEvent type
"E": 1499405658849, // KLineEvent time
"m": 0, // Maker commission rate (bips)
"t": 0, // Taker commission rate (bips)
"b": 0, // Buyer commission rate (bips)
"s": 0, // Seller commission rate (bips)
"T": true, // Can trade?
"W": true, // Can withdraw?
"D": true, // Can deposit?
"u": 1499405658848, // Time of last account update
"B": [ // Balances array
{
"a": "LTC", // Asset
"f": "17366.18538083", // Free amount
"l": "0.00000000" // Locked amount
},
{
"a": "BTC",
"f": "10537.85314051",
"l": "2.19464093"
},
{
"a": "ETH",
"f": "17902.35190619",
"l": "0.00000000"
},
{
"a": "BNC",
"f": "1114503.29769312",
"l": "0.00000000"
},
{
"a": "NEO",
"f": "0.00000000",
"l": "0.00000000"
}
],
"P": [ // Account Permissions
"SPOT"
]
}
*/
type Balance struct {
Asset string `json:"a"`
Free string `json:"f"`
Locked string `json:"l"`
}
type OutboundAccountInfoEvent struct {
EventBase
MakerCommissionRate int `json:"m"`
TakerCommissionRate int `json:"t"`
BuyerCommissionRate int `json:"b"`
SellerCommissionRate int `json:"s"`
CanTrade bool `json:"T"`
CanWithdraw bool `json:"W"`
CanDeposit bool `json:"D"`
LastAccountUpdateTime int `json:"u"`
Balances []Balance `json:"B,omitempty"`
Permissions []string `json:"P,omitempty"`
}
type ResultEvent struct {
Result interface{} `json:"result,omitempty"`
ID int `json:"id"`
}
func parseEvent(message string) (interface{}, error) {
val, err := fastjson.Parse(message)
if err != nil {
return nil, err
}
eventType := string(val.GetStringBytes("e"))
switch eventType {
case "kline":
var event KLineEvent
err := json.Unmarshal([]byte(message), &event)
return &event, err
case "outboundAccountInfo", "outboundAccountPosition":
var event OutboundAccountInfoEvent
err := json.Unmarshal([]byte(message), &event)
return &event, err
case "balanceUpdate":
var event BalanceUpdateEvent
err := json.Unmarshal([]byte(message), &event)
return &event, err
case "executionReport":
var event BinanceExecutionReportEvent
err := json.Unmarshal([]byte(message), &event)
return &event, err
default:
id := val.GetInt("id")
if id > 0 {
return &ResultEvent{ID: id}, nil
}
}
return nil, fmt.Errorf("unsupported message: %s", message)
}

View File

@ -3,6 +3,7 @@ package bbgo
import (
"context"
"fmt"
"github.com/c9s/bbgo/pkg/exchange/binance"
"github.com/c9s/bbgo/pkg/slack/slackstyle"
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/util"
@ -20,7 +21,7 @@ type Trader struct {
// Context is trading Context
Context *TradingContext
Exchange *BinanceExchange
Exchange *binance.Exchange
Slack *slack.Client
@ -75,7 +76,7 @@ func (t *Trader) Errorf(err error, format string, args ...interface{}) {
}
}
func (t *Trader) ReportTrade(e *BinanceExecutionReportEvent, trade *types.Trade) {
func (t *Trader) ReportTrade(e *binance.ExecutionReportEvent, trade *types.Trade) {
var color = ""
if trade.IsBuyer {
color = "#228B22"

View File

@ -0,0 +1,172 @@
package binance
import (
"context"
"github.com/adshao/go-binance"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/types"
"github.com/sirupsen/logrus"
"strconv"
"time"
)
type Exchange struct {
Client *binance.Client
}
func (e *Exchange) QueryAveragePrice(ctx context.Context, symbol string) (float64, error) {
resp, err := e.Client.NewAveragePriceService().Symbol(symbol).Do(ctx)
if err != nil {
return 0, err
}
return bbgo.MustParseFloat(resp.Price), nil
}
func (e *Exchange) NewPrivateStream(ctx context.Context) (*PrivateStream, error) {
logrus.Infof("[binance] creating user data stream...")
listenKey, err := e.Client.NewStartUserStreamService().Do(ctx)
if err != nil {
return nil, err
}
logrus.Infof("[binance] user data stream created. listenKey: %s", listenKey)
return &PrivateStream{
Client: e.Client,
ListenKey: listenKey,
}, nil
}
func (e *Exchange) SubmitOrder(ctx context.Context, order *bbgo.Order) error {
/*
limit order example
order, err := Client.NewCreateOrderService().
Symbol(Symbol).
Side(side).
Type(binance.OrderTypeLimit).
TimeInForce(binance.TimeInForceTypeGTC).
Quantity(volumeString).
Price(priceString).
Do(ctx)
*/
req := e.Client.NewCreateOrderService().
Symbol(order.Symbol).
Side(order.Side).
Type(order.Type).
Quantity(order.VolumeStr)
if len(order.PriceStr) > 0 {
req.Price(order.PriceStr)
}
if len(order.TimeInForce) > 0 {
req.TimeInForce(order.TimeInForce)
}
retOrder, err := req.Do(ctx)
logrus.Infof("[binance] order created: %+v", retOrder)
return err
}
func (e *Exchange) QueryKLines(ctx context.Context, symbol, interval string, limit int) ([]types.KLine, error) {
logrus.Infof("[binance] querying kline %s %s limit %d", symbol, interval, limit)
resp, err := e.Client.NewKlinesService().Symbol(symbol).Interval(interval).Limit(limit).Do(ctx)
if err != nil {
return nil, err
}
var kLines []types.KLine
for _, kline := range resp {
kLines = append(kLines, types.KLine{
Symbol: symbol,
Interval: interval,
StartTime: kline.OpenTime,
EndTime: kline.CloseTime,
Open: kline.Open,
Close: kline.Close,
High: kline.High,
Low: kline.Low,
Volume: kline.Volume,
QuoteVolume: kline.QuoteAssetVolume,
NumberOfTrades: kline.TradeNum,
})
}
return kLines, nil
}
func (e *Exchange) QueryTrades(ctx context.Context, symbol string, startTime time.Time) (trades []types.Trade, err error) {
logrus.Infof("[binance] querying %s trades from %s", symbol, startTime)
var lastTradeID int64 = 0
for {
req := e.Client.NewListTradesService().
Limit(1000).
Symbol(symbol).
StartTime(startTime.UnixNano() / 1000000)
if lastTradeID > 0 {
req.FromID(lastTradeID)
}
bnTrades, err := req.Do(ctx)
if err != nil {
return nil, err
}
if len(bnTrades) <= 1 {
break
}
for _, t := range bnTrades {
// skip trade ID that is the same. however this should not happen
if t.ID == lastTradeID {
continue
}
var side string
if t.IsBuyer {
side = "BUY"
} else {
side = "SELL"
}
// trade time
tt := time.Unix(0, t.Time*1000000)
logrus.Infof("[binance] trade: %d %s % 4s price: % 13s volume: % 11s %6s % 5s %s", t.ID, t.Symbol, side, t.Price, t.Quantity, BuyerOrSellerLabel(t), MakerOrTakerLabel(t), tt)
price, err := strconv.ParseFloat(t.Price, 64)
if err != nil {
return nil, err
}
quantity, err := strconv.ParseFloat(t.Quantity, 64)
if err != nil {
return nil, err
}
fee, err := strconv.ParseFloat(t.Commission, 64)
if err != nil {
return nil, err
}
trades = append(trades, types.Trade{
ID: t.ID,
Price: price,
Volume: quantity,
Side: side,
IsBuyer: t.IsBuyer,
IsMaker: t.IsMaker,
Fee: fee,
FeeCurrency: t.CommissionAsset,
Time: tt,
})
lastTradeID = t.ID
}
}
return trades, nil
}

238
exchange/binance/parse.go Normal file
View File

@ -0,0 +1,238 @@
package binance
import (
"encoding/json"
"errors"
"fmt"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/types"
"github.com/valyala/fastjson"
"time"
)
/*
executionReport
{
"e": "executionReport", // KLineEvent type
"E": 1499405658658, // KLineEvent time
"s": "ETHBTC", // Symbol
"c": "mUvoqJxFIILMdfAW5iGSOW", // Client order ID
"S": "BUY", // Side
"o": "LIMIT", // Order type
"f": "GTC", // Time in force
"q": "1.00000000", // Order quantity
"p": "0.10264410", // Order price
"P": "0.00000000", // Stop price
"F": "0.00000000", // Iceberg quantity
"g": -1, // OrderListId
"C": null, // Original client order ID; This is the ID of the order being canceled
"x": "NEW", // Current execution type
"X": "NEW", // Current order status
"r": "NONE", // Order reject reason; will be an error code.
"i": 4293153, // Order ID
"l": "0.00000000", // Last executed quantity
"z": "0.00000000", // Cumulative filled quantity
"L": "0.00000000", // Last executed price
"n": "0", // Commission amount
"N": null, // Commission asset
"T": 1499405658657, // Transaction time
"t": -1, // Trade ID
"I": 8641984, // Ignore
"w": true, // Is the order on the book?
"m": false, // Is this trade the maker side?
"M": false, // Ignore
"O": 1499405658657, // Order creation time
"Z": "0.00000000", // Cumulative quote asset transacted quantity
"Y": "0.00000000", // Last quote asset transacted quantity (i.e. lastPrice * lastQty)
"Q": "0.00000000" // Quote Order Qty
}
*/
type ExecutionReportEvent struct {
bbgo.EventBase
Symbol string `json:"s"`
ClientOrderID string `json:"c"`
Side string `json:"S"`
OrderType string `json:"o"`
TimeInForce string `json:"f"`
OrderQuantity string `json:"q"`
OrderPrice string `json:"p"`
StopPrice string `json:"P"`
IsOnBook bool `json:"w"`
IsMaker bool `json:"m"`
CommissionAmount string `json:"n"`
CommissionAsset string `json:"N"`
CurrentExecutionType string `json:"x"`
CurrentOrderStatus string `json:"X"`
OrderID int `json:"i"`
TradeID int64 `json:"t"`
TransactionTime int64 `json:"T"`
LastExecutedQuantity string `json:"l"`
CumulativeFilledQuantity string `json:"z"`
LastExecutedPrice string `json:"L"`
OrderCreationTime int `json:"O"`
}
func (e *ExecutionReportEvent) Trade() (*types.Trade, error) {
if e.CurrentExecutionType != "TRADE" {
return nil, errors.New("execution report is not a trade")
}
tt := time.Unix(0, e.TransactionTime/1000000)
return &types.Trade{
ID: e.TradeID,
Symbol: e.Symbol,
Price: bbgo.MustParseFloat(e.LastExecutedPrice),
Volume: bbgo.MustParseFloat(e.LastExecutedQuantity),
IsBuyer: e.Side == "BUY",
IsMaker: e.IsMaker,
Time: tt,
Fee: bbgo.MustParseFloat(e.CommissionAmount),
FeeCurrency: e.CommissionAsset,
}, nil
}
/*
balanceUpdate
{
"e": "balanceUpdate", //KLineEvent Type
"E": 1573200697110, //KLineEvent Time
"a": "BTC", //Asset
"d": "100.00000000", //Balance Delta
"T": 1573200697068 //Clear Time
}
*/
type BalanceUpdateEvent struct {
bbgo.EventBase
Asset string `json:"a"`
Delta string `json:"d"`
ClearTime int64 `json:"T"`
}
/*
outboundAccountInfo
{
"e": "outboundAccountInfo", // KLineEvent type
"E": 1499405658849, // KLineEvent time
"m": 0, // Maker commission rate (bips)
"t": 0, // Taker commission rate (bips)
"b": 0, // Buyer commission rate (bips)
"s": 0, // Seller commission rate (bips)
"T": true, // Can trade?
"W": true, // Can withdraw?
"D": true, // Can deposit?
"u": 1499405658848, // Time of last account update
"B": [ // Balances array
{
"a": "LTC", // Asset
"f": "17366.18538083", // Free amount
"l": "0.00000000" // Locked amount
},
{
"a": "BTC",
"f": "10537.85314051",
"l": "2.19464093"
},
{
"a": "ETH",
"f": "17902.35190619",
"l": "0.00000000"
},
{
"a": "BNC",
"f": "1114503.29769312",
"l": "0.00000000"
},
{
"a": "NEO",
"f": "0.00000000",
"l": "0.00000000"
}
],
"P": [ // Account Permissions
"SPOT"
]
}
*/
type Balance struct {
Asset string `json:"a"`
Free string `json:"f"`
Locked string `json:"l"`
}
type OutboundAccountInfoEvent struct {
bbgo.EventBase
MakerCommissionRate int `json:"m"`
TakerCommissionRate int `json:"t"`
BuyerCommissionRate int `json:"b"`
SellerCommissionRate int `json:"s"`
CanTrade bool `json:"T"`
CanWithdraw bool `json:"W"`
CanDeposit bool `json:"D"`
LastAccountUpdateTime int `json:"u"`
Balances []Balance `json:"B,omitempty"`
Permissions []string `json:"P,omitempty"`
}
type ResultEvent struct {
Result interface{} `json:"result,omitempty"`
ID int `json:"id"`
}
func ParseEvent(message string) (interface{}, error) {
val, err := fastjson.Parse(message)
if err != nil {
return nil, err
}
eventType := string(val.GetStringBytes("e"))
switch eventType {
case "kline":
var event bbgo.KLineEvent
err := json.Unmarshal([]byte(message), &event)
return &event, err
case "outboundAccountInfo", "outboundAccountPosition":
var event OutboundAccountInfoEvent
err := json.Unmarshal([]byte(message), &event)
return &event, err
case "balanceUpdate":
var event BalanceUpdateEvent
err := json.Unmarshal([]byte(message), &event)
return &event, err
case "executionReport":
var event ExecutionReportEvent
err := json.Unmarshal([]byte(message), &event)
return &event, err
default:
id := val.GetInt("id")
if id > 0 {
return &ResultEvent{ID: id}, nil
}
}
return nil, fmt.Errorf("unsupported message: %s", message)
}

151
exchange/binance/stream.go Normal file
View File

@ -0,0 +1,151 @@
package binance
import (
"context"
"fmt"
"github.com/adshao/go-binance"
"github.com/gorilla/websocket"
"github.com/sirupsen/logrus"
"strings"
"time"
)
type SubscribeOptions struct {
Interval string
Depth string
}
func (o SubscribeOptions) String() string {
if len(o.Interval) > 0 {
return o.Interval
}
return o.Depth
}
type Subscription struct {
Symbol string
Channel string
Options SubscribeOptions
}
func (s *Subscription) String() string {
// binance uses lower case symbol name
return fmt.Sprintf("%s@%s_%s", strings.ToLower(s.Symbol), s.Channel, s.Options.String())
}
type StreamRequest struct {
// request ID is required
ID int `json:"id"`
Method string `json:"method"`
Params []string `json:"params"`
}
type PrivateStream struct {
Client *binance.Client
ListenKey string
Conn *websocket.Conn
Subscriptions []Subscription
}
func (s *PrivateStream) Subscribe(channel string, symbol string, options SubscribeOptions) {
s.Subscriptions = append(s.Subscriptions, Subscription{
Channel: channel,
Symbol: symbol,
Options: options,
})
}
func (s *PrivateStream) Connect(ctx context.Context, eventC chan interface{}) error {
url := "wss://stream.binance.com:9443/ws/" + s.ListenKey
conn, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
return err
}
logrus.Infof("[binance] websocket connected")
s.Conn = conn
var params []string
for _, subscription := range s.Subscriptions {
params = append(params, subscription.String())
}
logrus.Infof("[binance] subscribing channels: %+v", params)
err = conn.WriteJSON(StreamRequest{
Method: "SUBSCRIBE",
Params: params,
ID: 1,
})
if err != nil {
return err
}
go s.read(ctx, eventC)
return nil
}
func (s *PrivateStream) read(ctx context.Context, eventC chan interface{}) {
defer close(eventC)
ticker := time.NewTicker(30 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
err := s.Client.NewKeepaliveUserStreamService().ListenKey(s.ListenKey).Do(ctx)
if err != nil {
logrus.WithError(err).Error("listen key keep-alive error", err)
}
default:
if err := s.Conn.SetReadDeadline(time.Now().Add(15 * time.Second)); err != nil {
logrus.WithError(err).Errorf("set read deadline error: %s", err.Error())
}
mt, message, err := s.Conn.ReadMessage()
if err != nil {
logrus.WithError(err).Errorf("read error: %s", err.Error())
return
}
// skip non-text messages
if mt != websocket.TextMessage {
continue
}
logrus.Debugf("[binance] recv: %s", message)
e, err := ParseEvent(string(message))
if err != nil {
logrus.WithError(err).Errorf("[binance] event parse error")
continue
}
eventC <- e
}
}
}
func (s *PrivateStream) Close() error {
logrus.Infof("[binance] closing user data stream...")
defer s.Conn.Close()
// use background context to close user stream
err := s.Client.NewCloseUserStreamService().ListenKey(s.ListenKey).Do(context.Background())
if err != nil {
logrus.WithError(err).Error("[binance] error close user data stream")
return err
}
return err
}

22
exchange/binance/trade.go Normal file
View File

@ -0,0 +1,22 @@
package binance
import "github.com/adshao/go-binance"
func BuyerOrSellerLabel(trade *binance.TradeV3) (o string) {
if trade.IsBuyer {
o = "BUYER"
} else {
o = "SELLER"
}
return o
}
func MakerOrTakerLabel(trade *binance.TradeV3) (o string) {
if trade.IsMaker {
o += "MAKER"
} else {
o += "TAKER"
}
return o
}

415
types/kline.go Normal file
View File

@ -0,0 +1,415 @@
package types
import (
"fmt"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/util"
"github.com/slack-go/slack"
"math"
)
// KLine uses binance's kline as the standard structure
type KLine struct {
StartTime int64 `json:"t"`
EndTime int64 `json:"T"`
Symbol string `json:"s"`
Interval string `json:"i"`
Open string `json:"o"`
Close string `json:"c"`
High string `json:"h"`
Low string `json:"l"`
Volume string `json:"V"` // taker buy base asset volume (like 10 BTC)
QuoteVolume string `json:"Q"` // taker buy quote asset volume (like 1000USDT)
LastTradeID int `json:"L"`
NumberOfTrades int64 `json:"n"`
Closed bool `json:"x"`
}
func (k KLine) Mid() float64 {
return (k.GetHigh() + k.GetLow()) / 2
}
// green candle with open and close near high price
func (k KLine) BounceUp() bool {
mid := k.Mid()
trend := k.GetTrend()
return trend > 0 && k.GetOpen() > mid && k.GetClose() > mid
}
// red candle with open and close near low price
func (k KLine) BounceDown() bool {
mid := k.Mid()
trend := k.GetTrend()
return trend > 0 && k.GetOpen() < mid && k.GetClose() < mid
}
func (k KLine) GetTrend() int {
o := k.GetOpen()
c := k.GetClose()
if c > o {
return 1
} else if c < o {
return -1
}
return 0
}
func (k KLine) GetHigh() float64 {
return bbgo.MustParseFloat(k.High)
}
func (k KLine) GetLow() float64 {
return bbgo.MustParseFloat(k.Low)
}
func (k KLine) GetOpen() float64 {
return bbgo.MustParseFloat(k.Open)
}
func (k KLine) GetClose() float64 {
return bbgo.MustParseFloat(k.Close)
}
func (k KLine) GetMaxChange() float64 {
return k.GetHigh() - k.GetLow()
}
// GetThickness returns the thickness of the kline. 1 => thick, 0.1 => thin
func (k KLine) GetThickness() float64 {
return math.Abs(k.GetChange()) / math.Abs(k.GetMaxChange())
}
func (k KLine) GetUpperShadowRatio() float64 {
return k.GetUpperShadowHeight() / math.Abs(k.GetMaxChange())
}
func (k KLine) GetUpperShadowHeight() float64 {
high := k.GetHigh()
if k.GetOpen() > k.GetClose() {
return high - k.GetOpen()
}
return high - k.GetClose()
}
func (k KLine) GetLowerShadowRatio() float64 {
return k.GetLowerShadowHeight() / math.Abs(k.GetMaxChange())
}
func (k KLine) GetLowerShadowHeight() float64 {
low := k.GetLow()
if k.GetOpen() < k.GetClose() {
return k.GetOpen() - low
}
return k.GetClose() - low
}
// GetBody returns the height of the candle real body
func (k KLine) GetBody() float64 {
return k.GetChange()
}
func (k KLine) GetChange() float64 {
return k.GetClose() - k.GetOpen()
}
func (k KLine) String() string {
return fmt.Sprintf("%s %s Open: % 14s Close: % 14s High: % 14s Low: % 14s Volume: % 15s Change: % 11f Max Change: % 11f", k.Symbol, k.Interval, k.Open, k.Close, k.High, k.Low, k.Volume, k.GetChange(), k.GetMaxChange())
}
func (k KLine) Color() string {
if k.GetTrend() > 0 {
return bbgo.Green
} else if k.GetTrend() < 0 {
return bbgo.Red
}
return "#f0f0f0"
}
func (k KLine) SlackAttachment() slack.Attachment {
return slack.Attachment{
Text: "KLine",
Color: k.Color(),
Fields: []slack.AttachmentField{
{
Title: "Open",
Value: k.Open,
Short: true,
},
{
Title: "Close",
Value: k.Close,
Short: true,
},
{
Title: "High",
Value: k.High,
Short: true,
},
{
Title: "Low",
Value: k.Low,
Short: true,
},
{
Title: "Mid",
Value: util.FormatFloat(k.Mid(), 2),
Short: true,
},
{
Title: "Change",
Value: util.FormatFloat(k.GetChange(), 2),
Short: true,
},
{
Title: "Max Change",
Value: util.FormatFloat(k.GetMaxChange(), 2),
Short: true,
},
{
Title: "Thickness",
Value: util.FormatFloat(k.GetThickness(), 4),
Short: true,
},
{
Title: "UpperShadowRatio",
Value: util.FormatFloat(k.GetUpperShadowRatio(), 4),
Short: true,
},
{
Title: "LowerShadowRatio",
Value: util.FormatFloat(k.GetLowerShadowRatio(), 4),
Short: true,
},
},
Footer: "",
FooterIcon: "",
}
}
type KLineWindow []KLine
func (k KLineWindow) Len() int {
return len(k)
}
func (k KLineWindow) GetOpen() float64 {
return k[0].GetOpen()
}
func (k KLineWindow) GetClose() float64 {
end := len(k) - 1
return k[end].GetClose()
}
func (k KLineWindow) GetHigh() float64 {
high := k.GetOpen()
for _, line := range k {
val := line.GetHigh()
if val > high {
high = val
}
}
return high
}
func (k KLineWindow) GetLow() float64 {
low := k.GetOpen()
for _, line := range k {
val := line.GetLow()
if val < low {
low = val
}
}
return low
}
func (k KLineWindow) GetChange() float64 {
return k.GetClose() - k.GetOpen()
}
func (k KLineWindow) GetMaxChange() float64 {
return k.GetHigh() - k.GetLow()
}
func (k KLineWindow) AllDrop() bool {
for _, n := range k {
if n.GetTrend() >= 0 {
return false
}
}
return true
}
func (k KLineWindow) AllRise() bool {
for _, n := range k {
if n.GetTrend() <= 0 {
return false
}
}
return true
}
func (k KLineWindow) GetTrend() int {
o := k.GetOpen()
c := k.GetClose()
if c > o {
return 1
} else if c < o {
return -1
}
return 0
}
func (k KLineWindow) Color() string {
if k.GetTrend() > 0 {
return bbgo.Green
} else if k.GetTrend() < 0 {
return bbgo.Red
}
return "#f0f0f0"
}
func (k KLineWindow) Mid() float64 {
return k.GetHigh() - k.GetLow()/2
}
// green candle with open and close near high price
func (k KLineWindow) BounceUp() bool {
mid := k.Mid()
trend := k.GetTrend()
return trend > 0 && k.GetOpen() > mid && k.GetClose() > mid
}
// red candle with open and close near low price
func (k KLineWindow) BounceDown() bool {
mid := k.Mid()
trend := k.GetTrend()
return trend > 0 && k.GetOpen() < mid && k.GetClose() < mid
}
func (k *KLineWindow) Add(line KLine) {
*k = append(*k, line)
}
func (k KLineWindow) Take(size int) KLineWindow {
return k[:size]
}
func (k KLineWindow) Tail(size int) KLineWindow {
if len(k) <= size {
return k[:]
}
return k[len(k)-size:]
}
func (k *KLineWindow) Truncate(size int) {
if len(*k) <= size {
return
}
end := len(*k) - 1
start := end - size
if start < 0 {
start = 0
}
*k = (*k)[end-5 : end]
}
func (k KLineWindow) GetBody() float64 {
return k.GetChange()
}
func (k KLineWindow) GetThickness() float64 {
return math.Abs(k.GetChange()) / math.Abs(k.GetMaxChange())
}
func (k KLineWindow) GetUpperShadowRatio() float64 {
return k.GetUpperShadowHeight() / math.Abs(k.GetMaxChange())
}
func (k KLineWindow) GetUpperShadowHeight() float64 {
high := k.GetHigh()
if k.GetOpen() > k.GetClose() {
return high - k.GetOpen()
}
return high - k.GetClose()
}
func (k KLineWindow) GetLowerShadowRatio() float64 {
return k.GetLowerShadowHeight() / math.Abs(k.GetMaxChange())
}
func (k KLineWindow) GetLowerShadowHeight() float64 {
low := k.GetLow()
if k.GetOpen() < k.GetClose() {
return k.GetOpen() - low
}
return k.GetClose() - low
}
func (k KLineWindow) SlackAttachment() slack.Attachment {
return slack.Attachment{
Text: "KLine",
Color: k.Color(),
Fields: []slack.AttachmentField{
{
Title: "Open",
Value: util.FormatFloat(k.GetOpen(), 2),
Short: true,
},
{
Title: "Close",
Value: util.FormatFloat(k.GetClose(), 2),
Short: true,
},
{
Title: "High",
Value: util.FormatFloat(k.GetHigh(), 2),
Short: true,
},
{
Title: "Low",
Value: util.FormatFloat(k.GetLow(), 2),
Short: true,
},
{
Title: "Mid",
Value: util.FormatFloat(k.Mid(), 2),
Short: true,
},
{
Title: "Change",
Value: util.FormatFloat(k.GetChange(), 2),
Short: true,
},
{
Title: "Max Change",
Value: util.FormatFloat(k.GetMaxChange(), 2),
Short: true,
},
{
Title: "Thickness",
Value: util.FormatFloat(k.GetThickness(), 4),
Short: true,
},
{
Title: "UpperShadowRatio",
Value: util.FormatFloat(k.GetUpperShadowRatio(), 4),
Short: true,
},
{
Title: "LowerShadowRatio",
Value: util.FormatFloat(k.GetLowerShadowRatio(), 4),
Short: true,
},
},
Footer: "",
FooterIcon: "",
}
}

View File

@ -1,7 +1,6 @@
package util
import (
"math"
"strconv"
)
@ -18,22 +17,6 @@ func Pow10(n int64) int64 {
return Pow10Table[n]
}
var NegPow10Table = [MaxDigits + 1]float64{
1, 1e-1, 1e-2, 1e-3, 1e-4, 1e-5, 1e-6, 1e-7, 1e-8, 1e-9, 1e-10, 1e-11, 1e-12, 1e-13, 1e-14, 1e-15, 1e-16, 1e-17, 1e-18,
}
func NegPow10(n int64) float64 {
if n < 0 || n > MaxDigits {
return 0.0
}
return NegPow10Table[n]
}
func Float64ToStr(input float64) string {
return strconv.FormatFloat(input, 'f', -1, 64)
}
func Float64ToInt64(input float64) int64 {
// eliminate rounding error for IEEE754 floating points
return int64(math.Round(input))
func FormatFloat(val float64, prec int) string {
return strconv.FormatFloat(val, 'f', prec, 64)
}