diff --git a/examples/backtesting/main.go b/examples/backtesting/main.go
index 19b6198a..b98c83fc 100644
--- a/examples/backtesting/main.go
+++ b/examples/backtesting/main.go
@@ -54,7 +54,10 @@ func main() {
exchange.WithDataFeed(csvFeed),
)
- chart := plot.NewChart()
+ chart := plot.NewChart(plot.WithIndicators(
+ plot.EMA(9, "red"),
+ plot.RSI(14, "purple"),
+ ))
bot, err := ninjabot.NewBot(
ctx,
diff --git a/plot/assets/chart.html b/plot/assets/chart.html
index 8ef8d473..f7bd5e23 100644
--- a/plot/assets/chart.html
+++ b/plot/assets/chart.html
@@ -10,9 +10,6 @@
Ninja Bot - Trade Results
-
- {{range $val := .Pairs}}
+ {{range $val := .pairs}}
- {{ $val }}
{{end}}
diff --git a/plot/assets/chart.js b/plot/assets/chart.js
index 1efe6944..31c2ce72 100644
--- a/plot/assets/chart.js
+++ b/plot/assets/chart.js
@@ -5,110 +5,116 @@ function unpack(rows, key) {
}
document.addEventListener("DOMContentLoaded", function () {
- const candleStickData = {
- name: "Candles",
- x: unpack(candles, "time"),
- close: unpack(candles, "close"),
- open: unpack(candles, "open"),
- low: unpack(candles, "low"),
- high: unpack(candles, "high"),
- type: "candlestick",
- xaxis: "x",
- yaxis: "y",
- };
+ const params = new URLSearchParams(window.location.search)
+ const pair = params.get("pair") || ""
+ fetch("/data?pair="+pair).
+ then(data => data.json())
+ .then(data => {
+ const candleStickData = {
+ name: "Candles",
+ x: unpack(data.candles, "time"),
+ close: unpack(data.candles, "close"),
+ open: unpack(data.candles, "open"),
+ low: unpack(data.candles, "low"),
+ high: unpack(data.candles, "high"),
+ type: "candlestick",
+ xaxis: "x",
+ yaxis: "y",
+ };
- const points = [];
- const annotations = [];
- candles.forEach((candle) => {
- candle.orders.forEach(order => {
- const point = {
- time: candle.time,
- position: order.price,
- side: order.side,
- color: "green"
- }
- if (order.side === "SELL") {
- point.color = "red"
- }
- points.push(point);
+ const points = [];
+ const annotations = [];
+ data.candles.forEach((candle) => {
+ candle.orders.forEach(order => {
+ const point = {
+ time: candle.time,
+ position: order.price,
+ side: order.side,
+ color: "green"
+ }
+ if (order.side === "SELL") {
+ point.color = "red"
+ }
+ points.push(point);
- const annotation = {
- x: candle.time,
- y: candle.low,
- xref: "x",
- yref: "y",
- text: "B",
- hovertext: `${order.time}
-
ID: ${order.id}
-
Price: ${order.price.toLocaleString()}
-
Size: ${order.quantity.toPrecision(4).toLocaleString()}
-
Type: ${order.type}
-
${(order.profit && "Profit: " + (order.profit * 100).toPrecision(2).toLocaleString() + "%") || ""}`,
- showarrow: true,
- arrowcolor: "green",
- valign: "bottom",
- borderpad: 4,
- arrowhead: 2,
- ax: 0,
- ay: 20,
- font: {
- size: 12,
- color: "green",
- },
- };
+ const annotation = {
+ x: candle.time,
+ y: candle.low,
+ xref: "x",
+ yref: "y",
+ text: "B",
+ hovertext: `${order.time}
+
ID: ${order.id}
+
Price: ${order.price.toLocaleString()}
+
Size: ${order.quantity.toPrecision(4).toLocaleString()}
+
Type: ${order.type}
+
${(order.profit && "Profit: " + (order.profit * 100).toPrecision(2).toLocaleString() + "%") || ""}`,
+ showarrow: true,
+ arrowcolor: "green",
+ valign: "bottom",
+ borderpad: 4,
+ arrowhead: 2,
+ ax: 0,
+ ay: 20,
+ font: {
+ size: 12,
+ color: "green",
+ },
+ };
- if (order.side === "SELL") {
- annotation.font.color = "red";
- annotation.arrowcolor = "red";
- annotation.text = "S";
- annotation.y = candle.high;
- annotation.ay = -20;
- annotation.valign = "top";
- }
- annotations.push(annotation);
+ if (order.side === "SELL") {
+ annotation.font.color = "red";
+ annotation.arrowcolor = "red";
+ annotation.text = "S";
+ annotation.y = candle.high;
+ annotation.ay = -20;
+ annotation.valign = "top";
+ }
+ annotations.push(annotation);
+ });
});
- });
- const sellPoints = points.filter(p => p.side === "SELL");
- const buyPoints = points.filter(p => p.side === "BUY");
- const buyData = {
- name: "Buy Points",
- x: unpack(buyPoints, "time"),
- y: unpack(buyPoints, "position"),
- mode: 'markers',
- type: 'scatter',
- marker: {
- color: "green",
- }
- };
- const sellData = {
- name: "Sell Points",
- x: unpack(sellPoints, "time"),
- y: unpack(sellPoints, "position"),
- mode: 'markers',
- type: 'scatter',
- marker: {
- color: "red",
- }
- };
+ const sellPoints = points.filter(p => p.side === "SELL");
+ const buyPoints = points.filter(p => p.side === "BUY");
+ const buyData = {
+ name: "Buy Points",
+ x: unpack(buyPoints, "time"),
+ y: unpack(buyPoints, "position"),
+ mode: 'markers',
+ type: 'scatter',
+ marker: {
+ color: "green",
+ }
+ };
+ const sellData = {
+ name: "Sell Points",
+ x: unpack(sellPoints, "time"),
+ y: unpack(sellPoints, "position"),
+ mode: 'markers',
+ type: 'scatter',
+ marker: {
+ color: "red",
+ }
+ };
- var layout = {
- dragmode: "pan",
- margin: {
- r: 10,
- t: 25,
- b: 40,
- l: 60,
- },
- showlegend: true,
- xaxis: {
- autorange: true
- },
- yaxis: {
- autorange: true
- },
- annotations: annotations,
- };
+ var layout = {
+ dragmode: "pan",
+ margin: {
+ r: 10,
+ t: 25,
+ b: 40,
+ l: 60,
+ },
+ showlegend: true,
+ xaxis: {
+ autorange: true
+ },
+ yaxis: {
+ autorange: true
+ },
+ annotations: annotations,
+ };
- Plotly.newPlot("graph", [candleStickData, buyData, sellData], layout);
+ Plotly.newPlot("graph", [candleStickData, buyData, sellData], layout);
+ })
});
diff --git a/plot/chart.go b/plot/chart.go
index 98751ffa..3199c2d7 100644
--- a/plot/chart.go
+++ b/plot/chart.go
@@ -2,6 +2,7 @@ package plot
import (
"embed"
+ "encoding/json"
"fmt"
"html/template"
"net/http"
@@ -17,9 +18,11 @@ var staticFiles embed.FS
type Chart struct {
sync.Mutex
- port int
- candles map[string][]Candle
- orders map[string][]*Order
+ port int
+ candles map[string][]Candle
+ dataframe map[string]*model.Dataframe
+ orders map[string][]*Order
+ indicators []Indicator
}
type Candle struct {
@@ -42,6 +45,20 @@ type Order struct {
Profit float64 `json:"profit"`
}
+type indicatorMetric struct {
+ Name string `json:"name"`
+ Time []time.Time `json:"time"`
+ Values []float64 `json:"value"`
+ Color string `json:"color"`
+ Style string `json:"style"`
+}
+
+type plotIndicator struct {
+ Name string `json:"name"`
+ Overlay bool `json:"overlay"`
+ Metrics []indicatorMetric `json:"metrics"`
+}
+
func (c *Chart) OnOrder(order model.Order) {
c.Lock()
defer c.Unlock()
@@ -69,7 +86,9 @@ func (c *Chart) OnCandle(candle model.Candle) {
c.Lock()
defer c.Unlock()
- if candle.Complete {
+ if candle.Complete && (len(c.candles[candle.Symbol]) == 0 ||
+ candle.Time.After(c.candles[candle.Symbol][len(c.candles[candle.Symbol])-1].Time)) {
+
c.candles[candle.Symbol] = append(c.candles[candle.Symbol], Candle{
Time: candle.Time,
Open: candle.Open,
@@ -79,10 +98,50 @@ func (c *Chart) OnCandle(candle model.Candle) {
Volume: candle.Volume,
Orders: make([]Order, 0),
})
+
+ if c.dataframe[candle.Symbol] == nil {
+ c.dataframe[candle.Symbol] = &model.Dataframe{
+ Pair: candle.Symbol,
+ Metadata: make(map[string]model.Series),
+ }
+ }
+
+ c.dataframe[candle.Symbol].Close = append(c.dataframe[candle.Symbol].Close, candle.Close)
+ c.dataframe[candle.Symbol].Open = append(c.dataframe[candle.Symbol].Open, candle.Open)
+ c.dataframe[candle.Symbol].High = append(c.dataframe[candle.Symbol].High, candle.High)
+ c.dataframe[candle.Symbol].Low = append(c.dataframe[candle.Symbol].Low, candle.Low)
+ c.dataframe[candle.Symbol].Volume = append(c.dataframe[candle.Symbol].Volume, candle.Volume)
+ c.dataframe[candle.Symbol].Time = append(c.dataframe[candle.Symbol].Time, candle.Time)
+ c.dataframe[candle.Symbol].LastUpdate = candle.Time
}
}
-func (c *Chart) CandlesByPair(pair string) []Candle {
+func (c *Chart) indicatorsByPair(pair string) []plotIndicator {
+ indicators := make([]plotIndicator, 0)
+ for _, i := range c.indicators {
+ i.Load(c.dataframe[pair])
+ indicator := plotIndicator{
+ Name: i.Name(),
+ Overlay: i.Overlay(),
+ Metrics: make([]indicatorMetric, 0),
+ }
+
+ for _, metric := range i.Metrics() {
+ indicator.Metrics = append(indicator.Metrics, indicatorMetric{
+ Name: metric.Name,
+ Values: metric.Values,
+ Time: metric.Time,
+ Color: metric.Color,
+ Style: metric.Style,
+ })
+ }
+
+ indicators = append(indicators, indicator)
+ }
+ return indicators
+}
+
+func (c *Chart) candlesByPair(pair string) []Candle {
for i := range c.candles[pair] {
for j, order := range c.orders[pair] {
if order == nil {
@@ -124,21 +183,26 @@ func (c *Chart) Start() error {
pairs = append(pairs, pair)
}
- http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+ http.HandleFunc("/data", func(w http.ResponseWriter, req *http.Request) {
pair := req.URL.Query().Get("pair")
if pair == "" {
pair = pairs[0]
}
- candles := c.CandlesByPair(pair)
+ w.Header().Set("Content-type", "text/json")
+ err := json.NewEncoder(w).Encode(map[string]interface{}{
+ "candles": c.candlesByPair(pair),
+ "indicators": c.indicatorsByPair(pair),
+ })
+ if err != nil {
+ log.Error(err)
+ }
+ })
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
w.Header().Add("Content-Type", "text/html")
- err := t.Execute(w, struct {
- Pairs []string
- Candles []Candle
- }{
- Pairs: pairs,
- Candles: candles,
+ err := t.Execute(w, map[string][]string{
+ "pairs": pairs,
})
if err != nil {
log.Error(err)
@@ -156,11 +220,18 @@ func WithPort(port int) Option {
}
}
+func WithIndicators(indicators ...Indicator) Option {
+ return func(chart *Chart) {
+ chart.indicators = indicators
+ }
+}
+
func NewChart(options ...Option) *Chart {
chart := &Chart{
- port: 8080,
- candles: make(map[string][]Candle),
- orders: make(map[string][]*Order),
+ port: 8080,
+ candles: make(map[string][]Candle),
+ dataframe: make(map[string]*model.Dataframe),
+ orders: make(map[string][]*Order),
}
for _, option := range options {
option(chart)
diff --git a/plot/indicator.go b/plot/indicator.go
new file mode 100644
index 00000000..58c9e87b
--- /dev/null
+++ b/plot/indicator.go
@@ -0,0 +1,103 @@
+package plot
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/markcheno/go-talib"
+
+ "github.com/rodrigo-brito/ninjabot/model"
+)
+
+type Metric struct {
+ Name string
+ Color string
+ Style string
+ Values model.Series
+ Time []time.Time
+}
+
+type Indicator interface {
+ Name() string
+ Overlay() bool
+ Metrics() []Metric
+ Load(dataframe *model.Dataframe)
+}
+
+func EMA(period int, color string) Indicator {
+ return &ema{
+ Period: period,
+ Color: color,
+ }
+}
+
+type ema struct {
+ Period int
+ Color string
+ Values model.Series
+ Time []time.Time
+}
+
+func (e ema) Name() string {
+ return fmt.Sprintf("EMA(%d)", e.Period)
+}
+
+func (e ema) Overlay() bool {
+ return true
+}
+
+func (e *ema) Load(dataframe *model.Dataframe) {
+ e.Values = talib.Ema(dataframe.Close, e.Period)
+ e.Time = dataframe.Time
+}
+
+func (e ema) Metrics() []Metric {
+ return []Metric{
+ {
+ Name: "value",
+ Style: "line",
+ Color: e.Color,
+ Values: e.Values,
+ Time: e.Time,
+ },
+ }
+}
+
+func RSI(period int, color string) Indicator {
+ return &ema{
+ Period: period,
+ Color: color,
+ }
+}
+
+type rsi struct {
+ Period int
+ Color string
+ Values model.Series
+ Time []time.Time
+}
+
+func (e rsi) Name() string {
+ return fmt.Sprintf("RSI(%d)", e.Period)
+}
+
+func (e rsi) Overlay() bool {
+ return false
+}
+
+func (e *rsi) Load(dataframe *model.Dataframe) {
+ e.Values = talib.Rsi(dataframe.Close, e.Period)
+ e.Time = dataframe.Time
+}
+
+func (e rsi) Metrics() []Metric {
+ return []Metric{
+ {
+ Name: "value",
+ Color: e.Color,
+ Style: "line",
+ Values: e.Values,
+ Time: e.Time,
+ },
+ }
+}