Skip to content
Merged
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
6 changes: 5 additions & 1 deletion pkg/bbgo/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,10 @@ type EnvironmentConfig struct {
SyncBufferPeriod *types.Duration `json:"syncBufferPeriod"`
}

type ChartConfig struct {
Kind string `json:"kind" yaml:"kind"` // kline, supertrend, etc
Options bbgochart.PanelOptions `json:"options" yaml:"options"`
}
type Config struct {
Build *BuildConfig `json:"build,omitempty" yaml:"build,omitempty"`

Expand Down Expand Up @@ -391,7 +395,7 @@ type Config struct {

ProfilingConfig *ProfilingConfig `json:"profiling,omitempty" yaml:"profiling,omitempty"`

ChartConfig *bbgochart.PanelOptions `json:"chart,omitempty" yaml:"chart,omitempty"`
ChartConfig []ChartConfig `json:"chart,omitempty" yaml:"chart,omitempty"`
}

func (c *Config) Map() (map[string]interface{}, error) {
Expand Down
72 changes: 54 additions & 18 deletions pkg/chart/v1/indicator_series.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ var (
type LegendKind string

var (
LegendTop = LegendKind("legend_top")
LegendThin = LegendKind("legend_thin")
LegendLeft = LegendKind("legend_left")
LegendTop = LegendKind("top")
LegendThin = LegendKind("thin")
LegendLeft = LegendKind("left")
)

type IndicatorSeries interface {
chart.Series
SetLegend(kind LegendKind)
GetLegend() *LegendKind
GetTimeRange() (time.Time, time.Time)
GetValueRange() (float64, float64)
}

type BandSample struct {
Expand All @@ -35,16 +35,34 @@ type BandIndicatorSeries struct {
Name string
Options *PanelOptions

samples []BandSample
legendKind *LegendKind
samples []BandSample
}

func (bs *BandIndicatorSeries) SetLegend(kind LegendKind) {
bs.legendKind = &kind
func (bs *BandIndicatorSeries) GetTimeRange() (time.Time, time.Time) {
if len(bs.samples) == 0 {
return time.Time{}, time.Time{}
}
return bs.samples[0].Time, bs.samples[len(bs.samples)-1].Time
}

func (bs *BandIndicatorSeries) GetLegend() *LegendKind {
return bs.legendKind
func (bs *BandIndicatorSeries) GetValueRange() (float64, float64) {
if len(bs.samples) == 0 {
return 0., 0.
}
minValue, maxValue := 0.0, 0.0
for _, s := range bs.samples {
if s.Value == nil {
continue
}
if minValue == 0.0 && maxValue == 0.0 {
minValue = *s.Value
maxValue = *s.Value
} else if s.Value != nil {
minValue = min(minValue, *s.Value)
maxValue = max(maxValue, *s.Value)
}
}
return minValue, maxValue
}

func NewBandIndicatorSeries(name string, samples []BandSample, options *PanelOptions) *BandIndicatorSeries {
Expand Down Expand Up @@ -135,9 +153,8 @@ type PointSample struct {
type LineIndicatorSeries struct {
Name string

points []PointSample
options *PanelOptions
legendKind *LegendKind
points []PointSample
options *PanelOptions
}

func NewLineIndicatorSeries(name string, points []PointSample, options *PanelOptions) *LineIndicatorSeries {
Expand All @@ -153,12 +170,31 @@ func NewLineIndicatorSeries(name string, points []PointSample, options *PanelOpt
}
}

func (ls *LineIndicatorSeries) SetLegend(kind LegendKind) {
ls.legendKind = &kind
func (ls *LineIndicatorSeries) GetTimeRange() (time.Time, time.Time) {
if len(ls.points) == 0 {
return time.Time{}, time.Time{}
}
return ls.points[0].Time, ls.points[len(ls.points)-1].Time
}

func (ls *LineIndicatorSeries) GetLegend() *LegendKind {
return ls.legendKind
func (ls *LineIndicatorSeries) GetValueRange() (float64, float64) {
if len(ls.points) == 0 {
return 0., 0.
}
minValue, maxValue := 0.0, 0.0
for _, p := range ls.points {
if p.Value == nil {
continue
}
if minValue == 0.0 && maxValue == 0.0 {
minValue = *p.Value
maxValue = *p.Value
} else if p.Value != nil {
minValue = min(minValue, *p.Value)
maxValue = max(maxValue, *p.Value)
}
}
return minValue, maxValue
}

func (ls *LineIndicatorSeries) AddPoints(points ...PointSample) {
Expand Down
199 changes: 127 additions & 72 deletions pkg/chart/v1/panel.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"io"
"sort"
"time"

"github.com/c9s/bbgo/pkg/types"
"github.com/wcharczuk/go-chart/v2"
Expand All @@ -12,14 +13,13 @@ import (
type Panel struct {
*chart.Chart

Name string
Options *PanelOptions

klines []types.KLine
indicators []IndicatorSeries
}

func NewPanel(name string, options *PanelOptions) *Panel {
func NewPanel(options *PanelOptions) *Panel {
if options == nil {
options = &PanelOptions{
IncludeVolume: true,
Expand All @@ -30,9 +30,7 @@ func NewPanel(name string, options *PanelOptions) *Panel {
}

return &Panel{
Chart: &chart.Chart{},

Name: name,
Chart: &chart.Chart{},
Options: options,
}
}
Expand All @@ -48,78 +46,123 @@ func (p *Panel) AddKLines(klines []types.KLine) {
}

func (p *Panel) draw() {
var xAxis chart.XAxis
var yAxis chart.YAxis
candles := ConvertKLinesToCandles(p.klines)
if len(candles) == 0 {
logger.Warn("no candles to plot")
return

if len(candles) > 0 {
// setup x and y axis based on klines
symbol := p.klines[0].Symbol
interval := p.klines[0].Interval

// Calculate min and max values for price and volume
minY, maxY := FindPriceRange(candles)
maxY = maxY * (1 + p.Options.RangePadding)
minY = minY * (1 - p.Options.RangePadding)

minTime := candles[0].Time
maxTime := candles[len(candles)-1].Time
padDuration := interval.Duration()
xAxis = chart.XAxis{
ValueFormatter: chart.TimeValueFormatterWithFormat("01/02 15:04"),
Range: &chart.ContinuousRange{
Min: chart.TimeToFloat64(minTime.Add(-padDuration)),
Max: chart.TimeToFloat64(maxTime.Add(padDuration)),
},
}
yAxis = chart.YAxis{
Range: &chart.ContinuousRange{
Min: minY,
Max: maxY,
},
}

if p.Options.Title == "" {
p.Title = fmt.Sprintf("%s %s (%s ~ %s)",
symbol,
interval.String(),
minTime.Format("01/02 15:04"),
maxTime.Format("01/02 15:04"),
)
} else {
p.Title = p.Options.Title
}
} else {
// find time range and value range from indicators
var minTime, maxTime time.Time
var minY, maxY float64

for _, ind := range p.indicators {
indMinTime, indMaxTime := ind.GetTimeRange()
indMinValue, indMaxValue := ind.GetValueRange()
if minTime.IsZero() || indMinTime.Before(minTime) {
minTime = indMinTime
}
if maxTime.IsZero() || indMaxTime.After(maxTime) {
maxTime = indMaxTime
}
if minY == 0.0 || indMinValue < minY {
minY = indMinValue
}
if maxY == 0.0 || indMaxValue > maxY {
maxY = indMaxValue
}
}

p.Title = p.Options.Title
xAxis = chart.XAxis{
ValueFormatter: chart.TimeValueFormatterWithFormat("01/02 15:04"),
Range: &chart.ContinuousRange{
Min: chart.TimeToFloat64(minTime) - p.Options.XAxisPadding,
Max: chart.TimeToFloat64(maxTime) + p.Options.XAxisPadding,
},
}
yAxis = chart.YAxis{
Range: &chart.ContinuousRange{
Min: minY * (1 - p.Options.RangePadding),
Max: maxY * (1 + p.Options.RangePadding),
},
}
}

symbol := p.klines[0].Symbol
interval := p.klines[0].Interval

// Calculate min and max values for price and volume
minY, maxY := FindPriceRange(candles)
maxY = maxY * (1 + p.Options.RangePadding)
minY = minY * (1 - p.Options.RangePadding)

padDuration := interval.Duration()
startTime := candles[0].Time
endTime := candles[len(candles)-1].Time
p.Title = fmt.Sprintf("%s %s (%s ~ %s)",
symbol,
interval.String(),
startTime.Format("01/02 15:04"),
endTime.Format("01/02 15:04"),
)
p.Width = p.Options.Width
p.Height = p.Options.Height
p.XAxis = chart.XAxis{
ValueFormatter: chart.TimeValueFormatterWithFormat("01/02 15:04"),
Range: &chart.ContinuousRange{
Min: chart.TimeToFloat64(candles[0].Time.Add(-padDuration)),
Max: chart.TimeToFloat64(candles[len(candles)-1].Time.Add(padDuration)),
},
}
p.YAxis = chart.YAxis{
Range: &chart.ContinuousRange{
Min: minY,
Max: maxY,
},
}
p.Series = []chart.Series{
CandlestickSeries{
Candles: candles,
},
p.XAxis = xAxis
p.YAxis = yAxis

// add series
if len(p.klines) > 0 {
p.Series = append(p.Series, CandlestickSeries{Candles: candles})
if p.Options != nil && p.Options.IncludeVolume {
// draw volume
minVolume, maxVolume := FindVolumeRange(candles)
maxVolume = maxVolume * 3

yMinAdj := p.YAxis.Range.GetMin() - p.YAxis.Range.GetDelta()*0.6
yAxis.Range.SetMin(yMinAdj)
p.YAxisSecondary = chart.YAxis{
Range: &chart.ContinuousRange{
Min: minVolume,
Max: maxVolume,
},
}
p.Series = append(p.Series, VolumeSeries{Candles: candles})
}
}
for _, ind := range p.indicators {
p.Series = append(p.Series, ind)
legendKind := ind.GetLegend()
if legendKind != nil {
switch *legendKind {
case LegendTop:
p.Elements = append(p.Elements, chart.Legend(p.Chart))
case LegendLeft:
p.Elements = append(p.Elements, chart.LegendLeft(p.Chart))
case LegendThin:
p.Elements = append(p.Elements, chart.LegendThin(p.Chart))
default:
p.Elements = append(p.Elements, chart.Legend(p.Chart))
logger.Warnf("unknown legend kind %s, use default legend top", *legendKind)
}
}
}

if p.Options != nil && p.Options.IncludeVolume {
minVolume, maxVolume := FindVolumeRange(candles)
maxVolume = maxVolume * 3

p.YAxis.Range.SetMin(minY - (maxY-minY)*0.6)
p.Series = append(p.Series, VolumeSeries{Candles: candles})
p.YAxisSecondary = chart.YAxis{
Range: &chart.ContinuousRange{
Min: minVolume,
Max: maxVolume,
},
if p.Options.Legend != nil {
switch *p.Options.Legend {
case LegendTop:
p.Elements = append(p.Elements, chart.Legend(p.Chart))
case LegendLeft:
p.Elements = append(p.Elements, chart.LegendLeft(p.Chart))
case LegendThin:
p.Elements = append(p.Elements, chart.LegendThin(p.Chart))
default:
p.Elements = append(p.Elements, chart.Legend(p.Chart))
logger.Warnf("unknown legend kind %s, use default legend top", *p.Options.Legend)
}
}
}
Expand All @@ -134,15 +177,27 @@ func (p *Panel) Write(w io.Writer) error {
}

type PanelOptions struct {
IncludeVolume bool `json:"include_volume" yaml:"include_volume"`
RangePadding float64 `json:"range_padding" yaml:"range_padding"`
Width int `json:"width" yaml:"width"`
Height int `json:"height" yaml:"height"`
// general options
Title string `json:"title,omitempty" yaml:"title,omitempty"`
RangePadding float64 `json:"range_padding" yaml:"range_padding"`
XAxisPadding float64 `json:"x_axis_padding" yaml:"x_axis_padding"`
Width int `json:"width" yaml:"width"`
Height int `json:"height" yaml:"height"`
Legend *LegendKind `json:"legend" yaml:"legend"`

// kline options
IncludeVolume bool `json:"include_volume" yaml:"include_volume"`

// indicators options
Window int `json:"window" yaml:"window"`

// band indicators options
UpperBoundColor string `json:"upper_bound_color" yaml:"upper_bound_color"`
LowerBoundColor string `json:"lower_bound_color" yaml:"lower_bound_color"`
ValueColor string `json:"value_color" yaml:"value_color"`

// supertrend
Multiplier float64 `json:"multiplier" yaml:"multiplier"`
}

// ConvertKLinesToCandles converts a slice of KLine to a slice of Candle.
Expand Down
Loading
Loading