mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-26 00:35:15 +00:00
abstract binance exchange
This commit is contained in:
parent
5665f6db97
commit
0ae851a085
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
||||
|
|
416
bbgo/kline.go
416
bbgo/kline.go
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
236
bbgo/parser.go
236
bbgo/parser.go
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
172
exchange/binance/exchange.go
Normal file
172
exchange/binance/exchange.go
Normal 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
238
exchange/binance/parse.go
Normal 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
151
exchange/binance/stream.go
Normal 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
22
exchange/binance/trade.go
Normal 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
415
types/kline.go
Normal 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: "",
|
||||
}
|
||||
}
|
||||
|
21
util/math.go
21
util/math.go
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user