Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ require (
github.com/gertd/go-pluralize v0.2.1
github.com/gin-contrib/cors v1.7.2
github.com/gin-gonic/gin v1.10.0
github.com/gizak/termui/v3 v3.1.0
github.com/go-redis/redis/v8 v8.11.5
github.com/go-sql-driver/mysql v1.10.0
github.com/gofrs/flock v0.8.1
Expand Down Expand Up @@ -117,10 +118,12 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/common v0.55.0 // indirect
Expand Down
7 changes: 7 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc=
github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gota/gota v0.10.1/go.mod h1:NZLQccXn0rABmkXjsaugRY6l+UH2dDZSgIgF8E2ipmA=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
Expand Down Expand Up @@ -395,6 +397,7 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
Expand All @@ -414,6 +417,8 @@ github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceT
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
Expand All @@ -433,6 +438,8 @@ github.com/muesli/kmeans v0.3.0/go.mod h1:eNyybq0tX9/iBEP6EMU4Y7dpmGK0uEhODdZpnG
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840=
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
Expand Down
123 changes: 123 additions & 0 deletions pkg/cmd/dashboard.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package cmd

import (
"context"
"errors"
"log"
"strings"
"time"

"github.com/c9s/bbgo/pkg/cmd/widget"
"github.com/c9s/bbgo/pkg/exchange"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
ui "github.com/gizak/termui/v3"
"github.com/spf13/cobra"
)

func init() {
orderFlowCmd.Flags().String("exchange", "", "exchange name")
orderFlowCmd.MarkFlagRequired("exchange")
orderFlowCmd.Flags().String("symbol", "", "trading pair symbol")
orderFlowCmd.MarkFlagRequired("symbol")
orderFlowCmd.Flags().Float64("aggregate", 0, "aggregate price unit; 0 disables aggregation")
dashboardCmd.AddCommand(orderFlowCmd)
RootCmd.AddCommand(dashboardCmd)
}

var dashboardCmd = &cobra.Command{
Use: "dashboard",
Short: "start the dashboard",
RunE: func(cmd *cobra.Command, args []string) error {
return nil
},
}

var orderFlowCmd = &cobra.Command{
Use: "orderflow",
Short: "start the order flow dashboard",
RunE: func(cmd *cobra.Command, args []string) error {
if err := ui.Init(); err != nil {
log.Fatalf("failed to initialize termui: %v", err)
}
defer ui.Close()

exName, err := cmd.Flags().GetString("exchange")
if err != nil {
return err
}

symbol, err := cmd.Flags().GetString("symbol")
if err != nil {
return err
}
if len(exName) == 0 || len(symbol) == 0 {
return errors.New("--exchange and --symbol are required")
}
aggregate, err := cmd.Flags().GetFloat64("aggregate")
if err != nil {
return err
}
aggregateUnit := fixedpoint.NewFromFloat(aggregate)

// create a new public trade stream
exMin, err := exchange.NewWithEnvVarPrefix(types.ExchangeName(exName), strings.ToUpper(exName))
ex, ok := exMin.(types.Exchange)
if !ok {
return errors.New("exchange does not implement types.Exchange")
}
stream := ex.NewStream()
stream.Subscribe(types.MarketTradeChannel, symbol, types.SubscribeOptions{})

w := widget.NewOrderFlowWidget()
w.Title = " Order Flow Delta (Total Volume Normalization) "
stream.OnMarketTrade(types.TradeWith(symbol, func(trade types.Trade) {
trade.Price = aggregatePrice(trade.Price, aggregateUnit)
w.UpdateTrade(trade)
}))
stream.SetPublicOnly()
// start receiving market trades
if err := stream.Connect(context.Background()); err != nil {
return err
}

w.BorderStyle = ui.NewStyle(ui.ColorCyan)
w.TitleStyle = ui.NewStyle(ui.ColorWhite, ui.ColorClear, ui.ModifierBold)

termWidth, termHeight := ui.TerminalDimensions()
w.SetRect(0, 0, termWidth, termHeight)
ui.Render(w)

// refresh ticker
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()

uiEvents := ui.PollEvents()

for {
select {
case <-ticker.C:
ui.Render(w)
case e := <-uiEvents:
switch e.Type {
case ui.KeyboardEvent:
if e.ID == "q" || e.ID == "<C-c>" {
return nil
}
w.HandleKeyboardEvent(e)
case ui.ResizeEvent:
payload := e.Payload.(ui.Resize)
w.SetRect(0, 0, payload.Width, payload.Height)
ui.Render(w)
}
}
}
},
}

func aggregatePrice(price, aggregateUnit fixedpoint.Value) fixedpoint.Value {
if aggregateUnit.Sign() <= 0 {
return price
}
return price.Div(aggregateUnit).Floor().Mul(aggregateUnit)
}
70 changes: 70 additions & 0 deletions pkg/cmd/dashboard_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package cmd

import (
"testing"

"github.com/c9s/bbgo/pkg/fixedpoint"
testifyassert "github.com/stretchr/testify/assert"
)

func TestAggregatePrice(t *testing.T) {
price := fixedpoint.MustNewFromString("63773.91")

tests := []struct {
name string
unit fixedpoint.Value
want fixedpoint.Value
}{
{
name: "zero disables aggregation",
unit: fixedpoint.Zero,
want: fixedpoint.MustNewFromString("63773.91"),
},
{
name: "negative disables aggregation",
unit: fixedpoint.MustNewFromString("-1"),
want: fixedpoint.MustNewFromString("63773.91"),
},
{
name: "one tenth",
unit: fixedpoint.MustNewFromString("0.1"),
want: fixedpoint.MustNewFromString("63773.9"),
},
{
name: "cent",
unit: fixedpoint.MustNewFromString("0.01"),
want: fixedpoint.MustNewFromString("63773.91"),
},
{
name: "one",
unit: fixedpoint.One,
want: fixedpoint.MustNewFromString("63773"),
},
{
name: "five",
unit: fixedpoint.NewFromInt(5),
want: fixedpoint.MustNewFromString("63770"),
},
{
name: "ten",
unit: fixedpoint.NewFromInt(10),
want: fixedpoint.MustNewFromString("63770"),
},
{
name: "larger fractional step",
unit: fixedpoint.MustNewFromString("2.5"),
want: fixedpoint.MustNewFromString("63772.5"),
},
{
name: "sub-unit floors down, not to nearest",
unit: fixedpoint.MustNewFromString("0.5"),
want: fixedpoint.MustNewFromString("63773.5"),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
testifyassert.Equal(t, tt.want, aggregatePrice(price, tt.unit))
})
}
}
Loading
Loading