Skip to content

Commit 4055ee5

Browse files
authoredOct 19, 2021
feat(chart): include OCO target and stop in plots / compress JS (rodrigo-brito#50)
1 parent 175a5ed commit 4055ee5

File tree

11 files changed

+384
-232
lines changed

11 files changed

+384
-232
lines changed
 

‎examples/backtesting/main.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,15 @@ func main() {
5555
exchange.WithDataFeed(csvFeed),
5656
)
5757

58-
chart := plot.NewChart(plot.WithIndicators(
58+
chart, err := plot.NewChart(plot.WithIndicators(
5959
indicator.EMA(8, "red"),
6060
indicator.EMA(21, "#000"),
6161
indicator.RSI(14, "purple"),
6262
indicator.Stoch(8, 3, "red", "blue"),
6363
))
64+
if err != nil {
65+
log.Fatal(err)
66+
}
6467

6568
bot, err := ninjabot.NewBot(
6669
ctx,

‎examples/strategies/ocosell.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ func (e *OCOSell) OnCandle(df *model.Dataframe, broker service.Broker) {
5555
}).Error(err)
5656
}
5757

58-
_, err = broker.CreateOrderOCO(model.SideTypeSell, df.Pair, size, closePrice*1.05, closePrice*0.95, closePrice*0.95)
58+
_, err = broker.CreateOrderOCO(model.SideTypeSell, df.Pair, size, closePrice*1.1, closePrice*0.95, closePrice*0.95)
5959
if err != nil {
6060
log.WithFields(map[string]interface{}{
6161
"pair": df.Pair,

‎exchange/exchange.go

+10-5
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import (
77
"strings"
88
"sync"
99

10-
"github.com/rodrigo-brito/ninjabot/service"
11-
1210
"github.com/rodrigo-brito/ninjabot/model"
11+
"github.com/rodrigo-brito/ninjabot/service"
1312

13+
"github.com/StudioSol/set"
1414
log "github.com/sirupsen/logrus"
1515
)
1616

@@ -27,7 +27,7 @@ type DataFeed struct {
2727

2828
type DataFeedSubscription struct {
2929
exchange service.Exchange
30-
Feeds []string
30+
Feeds *set.LinkedHashSetString
3131
DataFeeds map[string]*DataFeed
3232
SubscriptionsByDataFeed map[string][]Subscription
3333
SubscriptionsFinish []func()
@@ -43,6 +43,7 @@ type DataFeedConsumer func(model.Candle)
4343
func NewDataFeed(exchange service.Exchange) *DataFeedSubscription {
4444
return &DataFeedSubscription{
4545
exchange: exchange,
46+
Feeds: set.NewLinkedHashSetString(),
4647
DataFeeds: make(map[string]*DataFeed),
4748
SubscriptionsByDataFeed: make(map[string][]Subscription),
4849
}
@@ -59,7 +60,7 @@ func (d *DataFeedSubscription) pairTimeframeFromKey(key string) (pair, timeframe
5960

6061
func (d *DataFeedSubscription) Subscribe(pair, timeframe string, consumer DataFeedConsumer, onCandleClose bool) {
6162
key := d.feedKey(pair, timeframe)
62-
d.Feeds = append(d.Feeds, key)
63+
d.Feeds.Add(key)
6364
d.SubscriptionsByDataFeed[key] = append(d.SubscriptionsByDataFeed[key], Subscription{
6465
onCandleClose: onCandleClose,
6566
consumer: consumer,
@@ -74,6 +75,10 @@ func (d *DataFeedSubscription) Preload(pair, timeframe string, candles []model.C
7475
log.Infof("[SETUP] preloading %d candles for %s-%s", len(candles), pair, timeframe)
7576
key := d.feedKey(pair, timeframe)
7677
for _, candle := range candles {
78+
if !candle.Complete {
79+
continue
80+
}
81+
7782
for _, subscription := range d.SubscriptionsByDataFeed[key] {
7883
subscription.consumer(candle)
7984
}
@@ -82,7 +87,7 @@ func (d *DataFeedSubscription) Preload(pair, timeframe string, candles []model.C
8287

8388
func (d *DataFeedSubscription) Connect() {
8489
log.Infof("Connecting to the exchange.")
85-
for _, feed := range d.Feeds {
90+
for feed := range d.Feeds.Iter() {
8691
pair, timeframe := d.pairTimeframeFromKey(feed)
8792
ccandle, cerr := d.exchange.CandlesSubscription(context.Background(), pair, timeframe)
8893
d.DataFeeds[feed] = &DataFeed{

‎exchange/paperwallet.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ func (p *PaperWallet) OnCandle(candle model.Candle) {
149149
}
150150

151151
for i, order := range p.orders {
152-
if order.Status != model.OrderStatusTypeNew {
152+
if order.Pair != candle.Pair || order.Status != model.OrderStatusTypeNew {
153153
continue
154154
}
155155

@@ -168,6 +168,7 @@ func (p *PaperWallet) OnCandle(candle model.Candle) {
168168
walletValue := p.avgPrice[candle.Pair] * actualQty
169169

170170
p.volume[candle.Pair] += orderVolume
171+
p.orders[i].UpdatedAt = candle.Time
171172
p.orders[i].Status = model.OrderStatusTypeFilled
172173
p.avgPrice[candle.Pair] = (walletValue + orderVolume) / (actualQty + order.Quantity)
173174
p.assets[asset].Free = p.assets[asset].Free + order.Quantity
@@ -196,6 +197,7 @@ func (p *PaperWallet) OnCandle(candle model.Candle) {
196197
if groupOrder.GroupID != nil && *groupOrder.GroupID == *order.GroupID &&
197198
groupOrder.ExchangeID != order.ExchangeID {
198199
p.orders[j].Status = model.OrderStatusTypeCanceled
200+
p.orders[j].UpdatedAt = candle.Time
199201
break
200202
}
201203
}
@@ -270,6 +272,7 @@ func (p *PaperWallet) CreateOrderOCO(side model.SideType, pair string,
270272
Price: price,
271273
Quantity: size,
272274
GroupID: &groupID,
275+
RefPrice: p.lastCandle[pair].Close,
273276
}
274277

275278
stopOrder := model.Order{
@@ -284,6 +287,7 @@ func (p *PaperWallet) CreateOrderOCO(side model.SideType, pair string,
284287
Stop: &stop,
285288
Quantity: size,
286289
GroupID: &groupID,
290+
RefPrice: p.lastCandle[pair].Close,
287291
}
288292
p.orders = append(p.orders, limitMaker, stopOrder)
289293

‎go.mod

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ module github.com/rodrigo-brito/ninjabot
33
go 1.16
44

55
require (
6+
github.com/StudioSol/set v0.0.0-20211001132805-52fe71d0afcf
67
github.com/adshao/go-binance/v2 v2.3.2
78
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
9+
github.com/evanw/esbuild v0.13.7
810
github.com/gorilla/websocket v1.4.2 // indirect
911
github.com/jpillora/backoff v1.0.0
10-
github.com/kr/pretty v0.2.1 // indirect
12+
github.com/kr/pretty v0.3.0 // indirect
1113
github.com/markcheno/go-talib v0.0.0-20190307022042-cd53a9264d70
1214
github.com/olekukonko/tablewriter v0.0.5
1315
github.com/pkg/errors v0.9.1 // indirect
@@ -19,8 +21,6 @@ require (
1921
github.com/tidwall/gjson v1.9.2 // indirect
2022
github.com/urfave/cli/v2 v2.3.0
2123
github.com/xhit/go-str2duration/v2 v2.0.0
22-
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 // indirect
23-
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
2424
gopkg.in/tucnak/telebot.v2 v2.4.0
2525
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c // indirect
2626
)

‎go.sum

+16-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
2+
github.com/StudioSol/set v0.0.0-20211001132805-52fe71d0afcf h1:J0UFkeov8c7HXBx/9LT0k5NZgUMXQWnl9o3B15Qja5w=
3+
github.com/StudioSol/set v0.0.0-20211001132805-52fe71d0afcf/go.mod h1:gHtdo3acqCLmt09I95lUZsaYbD5EkgaXXb/+IRbU3jg=
24
github.com/adshao/go-binance/v2 v2.3.2 h1:qIXuCUDma9yrnQ1qP0PQZrHwA9d/4b8UaFxw+5TQKFU=
35
github.com/adshao/go-binance/v2 v2.3.2/go.mod h1:TfcBwfGtmRibSljDDR0XCaPkfBt1kc2N9lnNMYC3dCQ=
46
github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
@@ -8,9 +10,12 @@ github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dR
810
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
911
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
1012
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
13+
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
1114
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1215
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
1316
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
17+
github.com/evanw/esbuild v0.13.7 h1:ijdfXsbVKc70+JclIgYLSuyEgGj8nIpjGSO1KbULosk=
18+
github.com/evanw/esbuild v0.13.7/go.mod h1:GG+zjdi59yh3ehDn4ZWfPcATxjPDUH53iU4ZJbp7dkY=
1419
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
1520
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
1621
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
@@ -45,12 +50,14 @@ github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
4550
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
4651
github.com/klauspost/compress v1.13.1 h1:wXr2uRxZTJXHLly6qhJabee5JqIhTRoLBhDOA74hDEQ=
4752
github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
53+
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
4854
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
49-
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
50-
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
55+
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
56+
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
5157
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
52-
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
5358
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
59+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
60+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
5461
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
5562
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
5663
github.com/markcheno/go-talib v0.0.0-20190307022042-cd53a9264d70 h1:+iG37/Aw61Oc+ZJ4DSxQF2+K0e4ZiMidI7ytWuW4/cI=
@@ -70,6 +77,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
7077
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
7178
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
7279
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
80+
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
81+
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
7382
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
7483
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
7584
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
@@ -83,6 +92,7 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
8392
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
8493
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
8594
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
95+
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
8696
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
8797
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
8898
github.com/tidwall/btree v0.6.0/go.mod h1:TzIRzen6yHbibdSfK6t8QimqbUnoxUSrZfeW7Uob0q4=
@@ -117,8 +127,8 @@ github.com/xhit/go-str2duration/v2 v2.0.0 h1:uFtk6FWB375bP7ewQl+/1wBcn840GPhnySO
117127
github.com/xhit/go-str2duration/v2 v2.0.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
118128
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
119129
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
120-
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 h1:EZ2mChiOa8udjfp6rRmswTbtZN/QzUQp4ptM4rnjHvc=
121-
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
130+
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365 h1:6wSTsvPddg9gc/mVEEyk9oOAoxn+bT4Z9q1zx+4RwA4=
131+
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
122132
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
123133
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
124134
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -127,6 +137,7 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
127137
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
128138
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
129139
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
140+
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
130141
gopkg.in/tucnak/telebot.v2 v2.4.0 h1:nOeqOWnOAD3dzbKW+NRumd8zjj5vrWwSa0WRTxvgfag=
131142
gopkg.in/tucnak/telebot.v2 v2.4.0/go.mod h1:BgaIIx50PSRS9pG59JH+geT82cfvoJU/IaI5TJdN3v8=
132143
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

‎model/order.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@ type Order struct {
4848
GroupID *int64 `db:"group_id" json:"group_id"`
4949

5050
// Internal use (Plot)
51-
Profit float64 `json:"-"`
52-
Candle Candle `json:"-"`
51+
RefPrice float64 `json:"-"`
52+
Profit float64 `json:"-"`
53+
Candle Candle `json:"-"`
5354
}
5455

5556
func (o Order) String() string {

‎model/priorityqueue.go

+8
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@ func NewPriorityQueue(data []Item) *PriorityQueue {
2828
func (q *PriorityQueue) Push(item Item) {
2929
q.Lock()
3030
defer q.Unlock()
31+
3132
q.data = append(q.data, item)
3233
q.length++
3334
q.up(q.length - 1)
3435
}
3536
func (q *PriorityQueue) Pop() Item {
3637
q.Lock()
3738
defer q.Unlock()
39+
3840
if q.length == 0 {
3941
return nil
4042
}
@@ -47,15 +49,21 @@ func (q *PriorityQueue) Pop() Item {
4749
q.data = q.data[:len(q.data)-1]
4850
return top
4951
}
52+
5053
func (q *PriorityQueue) Peek() Item {
54+
q.Lock()
55+
defer q.Unlock()
56+
5157
if q.length == 0 {
5258
return nil
5359
}
5460
return q.data[0]
5561
}
62+
5663
func (q *PriorityQueue) Len() int {
5764
q.Lock()
5865
defer q.Unlock()
66+
5967
return q.length
6068
}
6169
func (q *PriorityQueue) down(pos int) {

‎ninjabot.go

+8-6
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ func (n *NinjaBot) processCandles() {
243243

244244
func (n *NinjaBot) Run(ctx context.Context) error {
245245
for _, pair := range n.settings.Pairs {
246+
pair := pair
246247
// setup and subscribe strategy to data feed (candles)
247248
strategyController := strategy.NewStrategyController(pair, n.strategy, n.orderController)
248249
strategyController.Start()
@@ -252,13 +253,14 @@ func (n *NinjaBot) Run(ctx context.Context) error {
252253
// TODO: include onCandleClose=false to improve precision in OCO orders (backtesting)
253254
n.dataFeed.Subscribe(pair, n.strategy.Timeframe(), n.onCandle, true)
254255

255-
// preload candles to warmup strategy
256-
candles, err := n.exchange.CandlesByLimit(ctx, pair, n.strategy.Timeframe(), n.strategy.WarmupPeriod()+1)
257-
if err != nil {
258-
return err
256+
if !n.backtest {
257+
// preload candles to warmup strategy
258+
candles, err := n.exchange.CandlesByLimit(ctx, pair, n.strategy.Timeframe(), n.strategy.WarmupPeriod()+1)
259+
if err != nil {
260+
return err
261+
}
262+
n.dataFeed.Preload(pair, n.strategy.Timeframe(), candles)
259263
}
260-
261-
n.dataFeed.Preload(pair, n.strategy.Timeframe(), candles)
262264
}
263265

264266
n.orderFeed.Start()

‎plot/assets/chart.js

+198-160
Original file line numberDiff line numberDiff line change
@@ -1,178 +1,216 @@
1+
const LIMIT_TYPE = "LIMIT";
2+
const MARKET_TYPE = "MARKET";
3+
const STOP_LOSS_TYPE = "STOP_LOSS";
4+
const LIMIT_MAKER_TYPE = "LIMIT_MAKER";
5+
6+
const SELL_SIDE = "SELL";
7+
const BUY_SIDE = "BUY";
8+
9+
const STATUS_FILLED = "FILLED";
10+
111
function unpack(rows, key) {
2-
return rows.map(function (row) {
3-
return row[key];
4-
});
12+
return rows.map(function (row) {
13+
return row[key];
14+
});
515
}
616

717
document.addEventListener("DOMContentLoaded", function () {
8-
const params = new URLSearchParams(window.location.search)
9-
const pair = params.get("pair") || ""
10-
fetch("/data?pair="+pair).
11-
then(data => data.json())
12-
.then(data => {
13-
const candleStickData = {
14-
name: "Candles",
15-
x: unpack(data.candles, "time"),
16-
close: unpack(data.candles, "close"),
17-
open: unpack(data.candles, "open"),
18-
low: unpack(data.candles, "low"),
19-
high: unpack(data.candles, "high"),
20-
type: "candlestick",
21-
xaxis: "x1",
22-
yaxis: "y1",
23-
};
18+
const params = new URLSearchParams(window.location.search);
19+
const pair = params.get("pair") || "";
20+
fetch("/data?pair=" + pair)
21+
.then((data) => data.json())
22+
.then((data) => {
23+
const candleStickData = {
24+
name: "Candles",
25+
x: unpack(data.candles, "time"),
26+
close: unpack(data.candles, "close"),
27+
open: unpack(data.candles, "open"),
28+
low: unpack(data.candles, "low"),
29+
high: unpack(data.candles, "high"),
30+
type: "candlestick",
31+
xaxis: "x1",
32+
yaxis: "y1",
33+
};
2434

25-
const points = [];
26-
const annotations = [];
27-
data.candles.forEach((candle) => {
28-
candle.orders.forEach(order => {
29-
const point = {
30-
time: candle.time,
31-
position: order.price,
32-
side: order.side,
33-
color: "green"
34-
}
35-
if (order.side === "SELL") {
36-
point.color = "red"
37-
}
38-
points.push(point);
35+
const points = [];
36+
const annotations = [];
37+
data.candles.forEach((candle) => {
38+
candle.orders
39+
.filter((o) => o.status === STATUS_FILLED)
40+
.forEach((order) => {
41+
const point = {
42+
time: candle.time,
43+
position: order.price,
44+
side: order.side,
45+
color: "green",
46+
};
47+
if (order.side === SELL_SIDE) {
48+
point.color = "red";
49+
}
50+
points.push(point);
3951

40-
const annotation = {
41-
x: candle.time,
42-
y: candle.low,
43-
xref: "x",
44-
yref: "y",
45-
xaxis: "x1",
46-
yaxis: "y1",
47-
text: "B",
48-
hovertext: `${order.time}
52+
const annotation = {
53+
x: candle.time,
54+
y: candle.low,
55+
xref: "x",
56+
yref: "y",
57+
xaxis: "x1",
58+
yaxis: "y1",
59+
text: "B",
60+
hovertext: `${order.updated_at}
4961
<br>ID: ${order.id}
5062
<br>Price: ${order.price.toLocaleString()}
51-
<br>Size: ${order.quantity.toPrecision(4).toLocaleString()}
52-
<br>Type: ${order.type}
53-
<br>${(order.profit && "Profit: " + (order.profit * 100).toPrecision(2).toLocaleString() + "%") || ""}`,
54-
showarrow: true,
55-
arrowcolor: "green",
56-
valign: "bottom",
57-
borderpad: 4,
58-
arrowhead: 2,
59-
ax: 0,
60-
ay: 20,
61-
font: {
62-
size: 12,
63-
color: "green",
64-
},
65-
};
63+
<br>Size: ${order.quantity
64+
.toPrecision(4)
65+
.toLocaleString()}<br>Type: ${order.type}<br>${
66+
(order.profit &&
67+
"Profit: " +
68+
(order.profit * 100).toPrecision(2).toLocaleString() +
69+
"%") ||
70+
""
71+
}`,
72+
showarrow: true,
73+
arrowcolor: "green",
74+
valign: "bottom",
75+
borderpad: 4,
76+
arrowhead: 2,
77+
ax: 0,
78+
ay: 20,
79+
font: {
80+
size: 12,
81+
color: "green",
82+
},
83+
};
6684

67-
if (order.side === "SELL") {
68-
annotation.font.color = "red";
69-
annotation.arrowcolor = "red";
70-
annotation.text = "S";
71-
annotation.y = candle.high;
72-
annotation.ay = -20;
73-
annotation.valign = "top";
74-
}
85+
if (order.side === SELL_SIDE) {
86+
annotation.font.color = "red";
87+
annotation.arrowcolor = "red";
88+
annotation.text = "S";
89+
annotation.y = candle.high;
90+
annotation.ay = -20;
91+
annotation.valign = "top";
92+
}
7593

76-
annotations.push(annotation);
77-
});
78-
});
94+
annotations.push(annotation);
95+
});
96+
});
7997

80-
const sellPoints = points.filter(p => p.side === "SELL");
81-
const buyPoints = points.filter(p => p.side === "BUY");
82-
const buyData = {
83-
name: "Buy Points",
84-
x: unpack(buyPoints, "time"),
85-
y: unpack(buyPoints, "position"),
86-
xaxis: "x1",
87-
yaxis: "y1",
88-
mode: 'markers',
89-
type: 'scatter',
90-
marker: {
91-
color: "green",
92-
}
93-
};
94-
const sellData = {
95-
name: "Sell Points",
96-
x: unpack(sellPoints, "time"),
97-
y: unpack(sellPoints, "position"),
98-
xaxis: "x1",
99-
yaxis: "y1",
100-
mode: 'markers',
101-
type: 'scatter',
102-
marker: {
103-
color: "red",
104-
}
98+
const shapes = data.shapes.map((s) => {
99+
return {
100+
type: "rect",
101+
xref: "x",
102+
yref: "y",
103+
x0: s.x0,
104+
y0: s.y0,
105+
x1: s.x1,
106+
y1: s.y1,
107+
line: {
108+
width: 0,
109+
},
110+
fillcolor: s.color,
105111
};
112+
});
106113

107-
const standaloneIndicators = data.indicators.reduce((total, indicator) => {
108-
if (!indicator.overlay) {
109-
return total + 1;
110-
}
111-
return total;
112-
}, 0);
114+
const sellPoints = points.filter((p) => p.side === SELL_SIDE);
115+
const buyPoints = points.filter((p) => p.side === BUY_SIDE);
116+
const buyData = {
117+
name: "Buy Points",
118+
x: unpack(buyPoints, "time"),
119+
y: unpack(buyPoints, "position"),
120+
xaxis: "x1",
121+
yaxis: "y1",
122+
mode: "markers",
123+
type: "scatter",
124+
marker: {
125+
color: "green",
126+
},
127+
};
128+
const sellData = {
129+
name: "Sell Points",
130+
x: unpack(sellPoints, "time"),
131+
y: unpack(sellPoints, "position"),
132+
xaxis: "x1",
133+
yaxis: "y1",
134+
mode: "markers",
135+
type: "scatter",
136+
marker: {
137+
color: "red",
138+
},
139+
};
113140

114-
let layout = {
115-
template: "ggplot2",
116-
dragmode: "zoom",
117-
margin: {
118-
t: 25,
119-
},
120-
showlegend: true,
121-
xaxis: {
122-
autorange: true,
123-
rangeslider: {visible: false},
124-
showline: true,
125-
anchor: standaloneIndicators > 0 ? "y2" : "y1"
126-
},
127-
yaxis: {
128-
domain: standaloneIndicators > 0 ? [0.5, 1] : [0, 1],
129-
autorange: true,
130-
mirror: true,
131-
showline: true,
132-
gridcolor: "#ddd"
133-
},
134-
hovermode: "x unified",
135-
annotations: annotations,
136-
};
141+
const standaloneIndicators = data.indicators.reduce(
142+
(total, indicator) => {
143+
if (!indicator.overlay) {
144+
return total + 1;
145+
}
146+
return total;
147+
},
148+
0
149+
);
137150

138-
let plotData = [candleStickData, buyData, sellData];
139-
const indicatorsHeight = 0.49/standaloneIndicators;
140-
let standaloneIndicatorIndex = 0;
141-
data.indicators.forEach((indicator) => {
142-
const axisNumber = standaloneIndicatorIndex+2;
143-
if (!indicator.overlay) {
144-
const heightStart = standaloneIndicatorIndex * indicatorsHeight;
145-
layout["yaxis"+axisNumber] = {
146-
title: indicator.name,
147-
domain: [heightStart, heightStart + indicatorsHeight],
148-
autorange: true,
149-
mirror: true,
150-
showline: true,
151-
linecolor: "black",
152-
gridcolor: "#ddd"
153-
};
154-
standaloneIndicatorIndex++;
155-
}
151+
let layout = {
152+
template: "ggplot2",
153+
dragmode: "zoom",
154+
margin: {
155+
t: 25,
156+
},
157+
showlegend: true,
158+
xaxis: {
159+
autorange: true,
160+
rangeslider: { visible: false },
161+
showline: true,
162+
anchor: standaloneIndicators > 0 ? "y2" : "y1",
163+
},
164+
yaxis: {
165+
domain: standaloneIndicators > 0 ? [0.5, 1] : [0, 1],
166+
autorange: true,
167+
mirror: true,
168+
showline: true,
169+
gridcolor: "#ddd",
170+
},
171+
hovermode: "x unified",
172+
annotations: annotations,
173+
shapes: shapes,
174+
};
156175

157-
indicator.metrics.forEach(metric => {
158-
const data = {
159-
title: indicator.name,
160-
name: indicator.name + (metric.name && " - " + metric.name),
161-
x: metric.time,
162-
y: metric.value,
163-
mode: metric.style,
164-
line: {
165-
color: metric.color,
166-
},
167-
xaxis: "x1",
168-
yaxis: "y1",
169-
};
170-
if (!indicator.overlay) {
171-
data.yaxis = "y"+axisNumber;
172-
}
173-
plotData.push(data);
174-
})
176+
let plotData = [candleStickData, buyData, sellData];
177+
const indicatorsHeight = 0.49 / standaloneIndicators;
178+
let standaloneIndicatorIndex = 0;
179+
data.indicators.forEach((indicator) => {
180+
const axisNumber = standaloneIndicatorIndex + 2;
181+
if (!indicator.overlay) {
182+
const heightStart = standaloneIndicatorIndex * indicatorsHeight;
183+
layout["yaxis" + axisNumber] = {
184+
title: indicator.name,
185+
domain: [heightStart, heightStart + indicatorsHeight],
186+
autorange: true,
187+
mirror: true,
188+
showline: true,
189+
linecolor: "black",
190+
gridcolor: "#ddd",
191+
};
192+
standaloneIndicatorIndex++;
193+
}
194+
195+
indicator.metrics.forEach((metric) => {
196+
const data = {
197+
title: indicator.name,
198+
name: indicator.name + (metric.name && " - " + metric.name),
199+
x: metric.time,
200+
y: metric.value,
201+
mode: metric.style,
202+
line: {
203+
color: metric.color,
204+
},
205+
xaxis: "x1",
206+
yaxis: "y1",
207+
};
208+
if (!indicator.overlay) {
209+
data.yaxis = "y" + axisNumber;
210+
}
211+
plotData.push(data);
175212
});
176-
Plotly.newPlot("graph", plotData, layout);
177-
})
213+
});
214+
Plotly.newPlot("graph", plotData, layout);
215+
});
178216
});

‎plot/chart.go

+128-48
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,27 @@ import (
1010
"time"
1111

1212
"github.com/rodrigo-brito/ninjabot/model"
13+
14+
"github.com/StudioSol/set"
15+
"github.com/evanw/esbuild/pkg/api"
1316
log "github.com/sirupsen/logrus"
1417
)
1518

16-
//go:embed assets
17-
var staticFiles embed.FS
19+
var (
20+
//go:embed assets
21+
staticFiles embed.FS
22+
)
1823

1924
type Chart struct {
2025
sync.Mutex
21-
port int
22-
candles map[string][]Candle
23-
dataframe map[string]*model.Dataframe
24-
orders map[string][]*Order
25-
indicators []Indicator
26+
port int
27+
debug bool
28+
candles map[string][]Candle
29+
dataframe map[string]*model.Dataframe
30+
ordersByPair map[string]*set.LinkedHashSetINT64
31+
orderByID map[int64]*Order
32+
indicators []Indicator
33+
scriptContent string
2634
}
2735

2836
type Candle struct {
@@ -35,14 +43,29 @@ type Candle struct {
3543
Orders []Order `json:"orders"`
3644
}
3745

46+
type Shape struct {
47+
StartX time.Time `json:"x0"`
48+
EndX time.Time `json:"x1"`
49+
StartY float64 `json:"y0"`
50+
EndY float64 `json:"y1"`
51+
Color string `json:"color"`
52+
}
53+
3854
type Order struct {
39-
ID int64 `json:"id"`
40-
Time time.Time `json:"time"`
41-
Price float64 `json:"price"`
42-
Quantity float64 `json:"quantity"`
43-
Type string `json:"type"`
44-
Side string `json:"side"`
45-
Profit float64 `json:"profit"`
55+
ID int64 `json:"id"`
56+
CreatedAt time.Time `json:"created_at"`
57+
UpdatedAt time.Time `json:"updated_at"`
58+
Status string `json:"status"`
59+
Price float64 `json:"price"`
60+
Quantity float64 `json:"quantity"`
61+
Type string `json:"type"`
62+
Side string `json:"side"`
63+
Profit float64 `json:"profit"`
64+
65+
// Only for OCO Orders
66+
Stop *float64 `json:"stop"`
67+
OCOGroup *int64 `json:"oco_group"`
68+
RefPrice float64 `json:"ref_price"`
4669
}
4770

4871
type indicatorMetric struct {
@@ -78,23 +101,24 @@ func (c *Chart) OnOrder(order model.Order) {
78101
c.Lock()
79102
defer c.Unlock()
80103

81-
if order.Status == model.OrderStatusTypeFilled {
82-
item := &Order{
83-
ID: order.ID,
84-
Time: order.UpdatedAt,
85-
Price: order.Price,
86-
Quantity: order.Quantity,
87-
Type: string(order.Type),
88-
Side: string(order.Side),
89-
Profit: order.Profit,
90-
}
104+
item := &Order{
105+
ID: order.ID,
106+
CreatedAt: order.CreatedAt,
107+
UpdatedAt: order.UpdatedAt,
108+
Status: string(order.Status),
109+
Price: order.Price,
110+
Quantity: order.Quantity,
111+
Type: string(order.Type),
112+
Side: string(order.Side),
113+
Profit: order.Profit,
114+
Stop: order.Stop,
115+
OCOGroup: order.GroupID,
116+
RefPrice: order.RefPrice,
117+
}
91118

92-
if order.Type == model.OrderTypeStopLoss || order.Type == model.OrderTypeStopLossLimit {
93-
item.Price = *order.Stop
94-
}
119+
c.ordersByPair[order.Pair].Add(order.ID)
120+
c.orderByID[order.ID] = item
95121

96-
c.orders[order.Pair] = append(c.orders[order.Pair], item)
97-
}
98122
}
99123

100124
func (c *Chart) OnCandle(candle model.Candle) {
@@ -119,6 +143,7 @@ func (c *Chart) OnCandle(candle model.Candle) {
119143
Pair: candle.Pair,
120144
Metadata: make(map[string]model.Series),
121145
}
146+
c.ordersByPair[candle.Pair] = set.NewLinkedHashSetINT64()
122147
}
123148

124149
c.dataframe[candle.Pair].Close = append(c.dataframe[candle.Pair].Close, candle.Close)
@@ -157,29 +182,50 @@ func (c *Chart) indicatorsByPair(pair string) []plotIndicator {
157182
}
158183

159184
func (c *Chart) candlesByPair(pair string) []Candle {
185+
candles := make([]Candle, len(c.candles[pair]))
160186
for i := range c.candles[pair] {
161-
for j, order := range c.orders[pair] {
162-
if order == nil {
163-
continue
164-
}
187+
candles[i] = c.candles[pair][i]
188+
for id := range c.ordersByPair[pair].Iter() {
189+
order := c.orderByID[id]
165190

166191
if i < len(c.candles[pair])-1 &&
167-
(order.Time.After(c.candles[pair][i].Time) &&
168-
order.Time.Before(c.candles[pair][i+1].Time)) ||
169-
order.Time.Equal(c.candles[pair][i].Time) {
170-
c.candles[pair][i].Orders = append(c.candles[pair][i].Orders, *order)
171-
c.orders[pair][j] = nil
192+
(order.UpdatedAt.After(c.candles[pair][i].Time) &&
193+
order.UpdatedAt.Before(c.candles[pair][i+1].Time)) ||
194+
order.UpdatedAt.Equal(c.candles[pair][i].Time) {
195+
candles[i].Orders = append(candles[i].Orders, *order)
172196
}
173197
}
174198
}
175199

176-
for _, order := range c.orders[pair] {
177-
if order != nil {
178-
log.Warnf("orders without candle data: %v", order)
200+
return candles
201+
}
202+
203+
func (c *Chart) shapesByPair(pair string) []Shape {
204+
shapes := make([]Shape, 0)
205+
for id := range c.ordersByPair[pair].Iter() {
206+
order := c.orderByID[id]
207+
208+
if order.Type != string(model.OrderTypeStopLoss) &&
209+
order.Type != string(model.OrderTypeLimitMaker) {
210+
continue
211+
}
212+
213+
shape := Shape{
214+
StartX: order.CreatedAt,
215+
EndX: order.UpdatedAt,
216+
StartY: order.RefPrice,
217+
EndY: order.Price,
218+
Color: "rgba(0, 255, 0, 0.3)",
219+
}
220+
221+
if order.Type == string(model.OrderTypeStopLoss) {
222+
shape.Color = "rgba(255, 0, 0, 0.3)"
179223
}
224+
225+
shapes = append(shapes, shape)
180226
}
181227

182-
return c.candles[pair]
228+
return shapes
183229
}
184230

185231
func (c *Chart) Start() error {
@@ -198,6 +244,11 @@ func (c *Chart) Start() error {
198244
pairs = append(pairs, pair)
199245
}
200246

247+
http.HandleFunc("/assets/chart.js", func(w http.ResponseWriter, req *http.Request) {
248+
w.Header().Set("Content-type", "application/javascript")
249+
fmt.Fprint(w, c.scriptContent)
250+
})
251+
201252
http.HandleFunc("/data", func(w http.ResponseWriter, req *http.Request) {
202253
pair := req.URL.Query().Get("pair")
203254
if pair == "" {
@@ -209,6 +260,7 @@ func (c *Chart) Start() error {
209260
err := json.NewEncoder(w).Encode(map[string]interface{}{
210261
"candles": c.candlesByPair(pair),
211262
"indicators": c.indicatorsByPair(pair),
263+
"shapes": c.shapesByPair(pair),
212264
})
213265
if err != nil {
214266
log.Error(err)
@@ -243,21 +295,49 @@ func WithPort(port int) Option {
243295
}
244296
}
245297

298+
// WithDebug starts chart without compress
299+
func WithDebug() Option {
300+
return func(chart *Chart) {
301+
chart.debug = true
302+
}
303+
}
304+
246305
func WithIndicators(indicators ...Indicator) Option {
247306
return func(chart *Chart) {
248307
chart.indicators = indicators
249308
}
250309
}
251310

252-
func NewChart(options ...Option) *Chart {
311+
func NewChart(options ...Option) (*Chart, error) {
253312
chart := &Chart{
254-
port: 8080,
255-
candles: make(map[string][]Candle),
256-
dataframe: make(map[string]*model.Dataframe),
257-
orders: make(map[string][]*Order),
313+
port: 8080,
314+
candles: make(map[string][]Candle),
315+
dataframe: make(map[string]*model.Dataframe),
316+
ordersByPair: make(map[string]*set.LinkedHashSetINT64),
317+
orderByID: make(map[int64]*Order),
258318
}
259319
for _, option := range options {
260320
option(chart)
261321
}
262-
return chart
322+
323+
content, err := staticFiles.ReadFile("assets/chart.js")
324+
if err != nil {
325+
return nil, err
326+
}
327+
328+
result := api.Transform(string(content), api.TransformOptions{
329+
Loader: api.LoaderJS,
330+
Target: api.ES2015,
331+
MinifySyntax: !chart.debug,
332+
MinifyIdentifiers: !chart.debug,
333+
MinifyWhitespace: !chart.debug,
334+
})
335+
336+
if len(result.Errors) > 0 {
337+
return nil, fmt.Errorf("chart script faild with: %v", result.Errors)
338+
}
339+
340+
chart.scriptContent = string(result.Code)
341+
342+
return chart, nil
263343
}

0 commit comments

Comments
 (0)
Please sign in to comment.