v1

package
v1.64.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 13, 2026 License: AGPL-3.0 Imports: 8 Imported by: 0

README

Chart API (v1)

This package provides a charting API for visualizing kline (candlestick) data with technical indicators, built on top of go-chart.

Overview

The chart API is designed to:

  1. Render candlestick charts from kline data
  2. Overlay multiple technical indicators as separate chart.Series
  3. Include volume as a secondary series
  4. Write charts as PNG to any io.Writer

Core Components

Panel

Panel is the top-level struct that manages the chart. It embeds *chart.Chart and holds klines plus any indicator series.

type Panel struct {
    *chart.Chart
    Name    string
    Options *PanelOptions
    // ...
}
PanelOptions
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"`

    // Colors for indicator series (CSS hex or named colors)
    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"`
}
Creating and Rendering a Chart
import (
    "log"
    "os"

    bbgochart "github.com/c9s/bbgo/pkg/chart/v1"
)

panel := bbgochart.NewPanel("BTCUSDT", &bbgochart.PanelOptions{
    IncludeVolume: true,
    RangePadding:  0.15,
    Width:         1200,
    Height:        600,
})

panel.AddKLines(klines)

f, err := os.Create("klines.png")
if err != nil {
    log.Fatal(err)
}
defer f.Close()

if err := panel.Write(f); err != nil {
    log.Fatal(err)
}

Write triggers the internal layout pass, which builds the series list (always CandlestickSeries, then any registered indicator series, and optionally VolumeSeries when IncludeVolume is true), then renders the chart as PNG to the given io.Writer.

Indicator Series

Indicators are added to the panel via AddIndicator, which accepts any value that satisfies chart.Series.

panel.AddIndicator(series)

Two built-in series types are provided.

LineIndicatorSeries

Renders a single line from a sequence of (time, value) points. Nil values break the line.

type PointSample struct {
    Time  time.Time
    Value *float64  // nil = gap in the line
}
line := bbgochart.NewLineIndicatorSeries("SMA-20", nil, &bbgochart.PanelOptions{
    ValueColor: "#0074D9",
})

v := 42000.0
line.AddPoints(bbgochart.PointSample{Time: t, Value: &v})

panel.AddIndicator(line)
BandIndicatorSeries

Renders up to three lines — upper bound, lower bound, and a middle value — from a sequence of BandSample points. Each field is nullable; nil fields are skipped for that line only.

type BandSample struct {
    Time                          time.Time
    UpperBound, LowerBound, Value *float64
}
band := bbgochart.NewBandIndicatorSeries("BOLL", nil, &bbgochart.PanelOptions{
    UpperBoundColor: "#2ECC40",  // green upper
    LowerBoundColor: "#FF4136",  // red lower
    ValueColor:      "#AAAAAA",  // gray midline
})

upper, mid, lower := 43000.0, 42000.0, 41000.0
band.AddSamples(bbgochart.BandSample{
    Time:       t,
    UpperBound: &upper,
    Value:      &mid,
    LowerBound: &lower,
})

panel.AddIndicator(band)

Example: Candlestick Chart with SMA and Bollinger Bands

package main

import (
    "log"
    "os"
    "time"

    bbgochart "github.com/c9s/bbgo/pkg/chart/v1"
    indicatorv2 "github.com/c9s/bbgo/pkg/indicator/v2"
    "github.com/c9s/bbgo/pkg/types"
)

func buildChart(klines []types.KLine) error {
    panel := bbgochart.NewPanel("BTCUSDT 1h", &bbgochart.PanelOptions{
        IncludeVolume: true,
        RangePadding:  0.15,
        Width:         1920,
        Height:        1080,
    })
    panel.AddKLines(klines)

    // --- SMA-20 via LineIndicatorSeries ---
    smaSeries := bbgochart.NewLineIndicatorSeries("SMA-20", nil, &bbgochart.PanelOptions{
        ValueColor: "#0074D9",
    })

    klineStream := &indicatorv2.KLineStream{}
    closeStream := indicatorv2.ClosePrices(klineStream)
    sma := indicatorv2.SMA(closeStream, 20)
    for _, k := range klines {
        klineStream.BackFill([]types.KLine{k})
        if sma.Len() > 0 {
            v := sma.Last(0)
            smaSeries.AddPoints(bbgochart.PointSample{
                Time:  k.StartTime.Time(),
                Value: &v,
            })
        }
    }
    panel.AddIndicator(smaSeries)

    // --- Bollinger Bands via BandIndicatorSeries ---
    bollSeries := bbgochart.NewBandIndicatorSeries("BOLL-20", nil, &bbgochart.PanelOptions{
        UpperBoundColor: "#2ECC40",
        LowerBoundColor: "#FF4136",
        ValueColor:      "#AAAAAA",
    })

    klineStream2 := &indicatorv2.KLineStream{}
    boll := indicatorv2.BOLL(indicatorv2.ClosePrices(klineStream2), 20, 2.0)
    for _, k := range klines {
        klineStream2.BackFill([]types.KLine{k})
        if boll.UpBand.Len() > 0 {
            up := boll.UpBand.Last(0)
            mid := boll.Last(0)
            dn := boll.DownBand.Last(0)
            bollSeries.AddSamples(bbgochart.BandSample{
                Time:       k.StartTime.Time(),
                UpperBound: &up,
                Value:      &mid,
                LowerBound: &dn,
            })
        }
    }
    panel.AddIndicator(bollSeries)

    f, err := os.Create("chart.png")
    if err != nil {
        return err
    }
    defer f.Close()
    return panel.Write(f)
}

Helper Functions

// Convert a price value to a canvas Y coordinate
func YValueToCanvas(r chart.Range, b chart.Box, v float64) int

// Convert a time float64 to a canvas X coordinate
func XValueToCanvas(r chart.Range, b chart.Box, v float64) int

// Convert []types.KLine to []Candle
func ConvertKLinesToCandles(kLines []types.KLine) []Candle

// Return the min/max price across a slice of candles
func FindPriceRange(candles []Candle) (min, max float64)

// Return the min/max volume across a slice of candles
func FindVolumeRange(candles []Candle) (min, max float64)

Design Philosophy

  1. IndicatorSeries as the integration point: AddIndicator accepts the IndicatorSeries interface, which extends go-chart's chart.Series with GetTimeRange() (time.Time, time.Time) and GetValueRange() (float64, float64). These extra methods let the panel compute axes when rendering indicator-only charts (no klines). The two built-in types — LineIndicatorSeries and BandIndicatorSeries — satisfy this interface, and any external type can too.
  2. Data accumulation before layout: AddKLines, AddPoints, and AddSamples collect raw data upfront. Write triggers an internal draw() pass that assembles the full series list, axes, and ranges in a single shot. When klines are present, axes derive from their price/time range; when the panel contains only indicators, axes derive from GetTimeRange/GetValueRange across all registered indicator series.
  3. Nullable values for natural gaps: PointSample.Value and the three fields of BandSample are *float64. A nil value breaks the line at that point rather than zero-filling or interpolating, which correctly handles indicator warm-up periods and missing data.
  4. Shared options struct: PanelOptions carries panel-level settings (dimensions, padding, volume toggle, title, x-axis padding, legend kind) and indicator color configuration. Passing the same struct to both NewPanel and the indicator series constructors avoids a proliferation of separate config types.
  5. Composability over inheritance: Multiple indicators of any IndicatorSeries-compatible type can be overlaid on a single panel. The panel stores []IndicatorSeries and delegates rendering entirely to each series' own Render method.

References

Documentation

Index

Constants

View Source
const (
	LegendTop  = LegendKind("top")
	LegendThin = LegendKind("thin")
	LegendLeft = LegendKind("left")
)

Variables

This section is empty.

Functions

func FindPriceRange

func FindPriceRange(candles []Candle) (float64, float64)

FindPriceRange calculates the Y-axis range for the chart.

func FindVolumeRange

func FindVolumeRange(candles []Candle) (float64, float64)

func XValueToCanvas

func XValueToCanvas(r chart.Range, b chart.Box, v float64) int

func YValueToCanvas

func YValueToCanvas(r chart.Range, b chart.Box, v float64) int

Types

type BandIndicatorSeries

type BandIndicatorSeries struct {
	Name    string
	Options *PanelOptions
	// contains filtered or unexported fields
}

func NewBandIndicatorSeries

func NewBandIndicatorSeries(name string, samples []BandSample, options *PanelOptions) *BandIndicatorSeries

func (*BandIndicatorSeries) AddSamples

func (s *BandIndicatorSeries) AddSamples(samples ...BandSample)

func (*BandIndicatorSeries) GetName

func (bs *BandIndicatorSeries) GetName() string

Implement chart.Series interface for BandIndicatorSeries.

func (*BandIndicatorSeries) GetStyle

func (bs *BandIndicatorSeries) GetStyle() chart.Style

func (*BandIndicatorSeries) GetTimeRange

func (bs *BandIndicatorSeries) GetTimeRange() (time.Time, time.Time)

func (*BandIndicatorSeries) GetValueRange

func (bs *BandIndicatorSeries) GetValueRange() (float64, float64)

func (*BandIndicatorSeries) GetYAxis

func (bs *BandIndicatorSeries) GetYAxis() chart.YAxisType

func (*BandIndicatorSeries) Render

func (bs *BandIndicatorSeries) Render(r chart.Renderer, b chart.Box, xRange, yRange chart.Range, style chart.Style)

func (*BandIndicatorSeries) Validate

func (bs *BandIndicatorSeries) Validate() error

type BandSample

type BandSample struct {
	Time                          time.Time
	UpperBound, LowerBound, Value *float64
}

type Candle

type Candle struct {
	Time   time.Time
	Open   float64
	High   float64
	Low    float64
	Close  float64
	Volume float64
}

func ConvertKLinesToCandles

func ConvertKLinesToCandles(kLines []types.KLine) []Candle

ConvertKLinesToCandles converts a slice of KLine to a slice of Candle.

type CandlestickSeries

type CandlestickSeries struct {
	Candles []Candle
}

func (*CandlestickSeries) GetFirstValues

func (ts *CandlestickSeries) GetFirstValues() (x, y float64)

func (*CandlestickSeries) GetLastValues

func (ts *CandlestickSeries) GetLastValues() (x, y float64)

func (*CandlestickSeries) GetName

func (cs *CandlestickSeries) GetName() string

Implement chart.Series interface for CandlestickSeries.

func (*CandlestickSeries) GetStyle

func (cs *CandlestickSeries) GetStyle() chart.Style

func (*CandlestickSeries) GetValue

func (cs *CandlestickSeries) GetValue(index int) float64

func (CandlestickSeries) GetValueFormatters

func (cs CandlestickSeries) GetValueFormatters() (x, y chart.ValueFormatter)

func (CandlestickSeries) GetXAxisValue

func (cs CandlestickSeries) GetXAxisValue(index int) float64

func (*CandlestickSeries) GetYAxis

func (cs *CandlestickSeries) GetYAxis() chart.YAxisType

func (*CandlestickSeries) Len

func (cs *CandlestickSeries) Len() int

func (*CandlestickSeries) Render

func (cs *CandlestickSeries) Render(
	r chart.Renderer, canvasBox chart.Box, xrange, yrange chart.Range, style chart.Style,
)

func (*CandlestickSeries) Validate

func (cs *CandlestickSeries) Validate() error

Validate implements chart.Series interface for CandlestickSeries.

func (*CandlestickSeries) XValues

func (cs *CandlestickSeries) XValues() []float64

XValues returns all X axis values for the series.

type ColumnIndicatorSeries

type ColumnIndicatorSeries struct {
	Name    string
	Options *PanelOptions
	// contains filtered or unexported fields
}

ColumnIndicatorSeries draws histogram columns along the x-axis. Useful for momentum indicators like TTM Squeeze momentum.

func NewColumnIndicatorSeries

func NewColumnIndicatorSeries(name string, samples []ColumnSample, options *PanelOptions) *ColumnIndicatorSeries

func (*ColumnIndicatorSeries) AddSamples

func (cs *ColumnIndicatorSeries) AddSamples(samples ...ColumnSample)

func (*ColumnIndicatorSeries) GetName

func (cs *ColumnIndicatorSeries) GetName() string

func (*ColumnIndicatorSeries) GetStyle

func (cs *ColumnIndicatorSeries) GetStyle() chart.Style

func (*ColumnIndicatorSeries) GetTimeRange

func (cs *ColumnIndicatorSeries) GetTimeRange() (time.Time, time.Time)

func (*ColumnIndicatorSeries) GetValueRange

func (cs *ColumnIndicatorSeries) GetValueRange() (float64, float64)

func (*ColumnIndicatorSeries) GetYAxis

func (cs *ColumnIndicatorSeries) GetYAxis() chart.YAxisType

func (*ColumnIndicatorSeries) Render

func (cs *ColumnIndicatorSeries) Render(r chart.Renderer, b chart.Box, xRange, yRange chart.Range, style chart.Style)

func (*ColumnIndicatorSeries) Validate

func (cs *ColumnIndicatorSeries) Validate() error

type ColumnSample

type ColumnSample struct {
	Time  time.Time
	Value float64
	Color drawing.Color
}

ColumnSample represents a single column in a histogram chart.

type DotIndicatorSeries

type DotIndicatorSeries struct {
	Name    string
	Options *PanelOptions
	// contains filtered or unexported fields
}

DotIndicatorSeries draws dots at given x-y coordinates. Useful for visualizing squeeze states in TTM Squeeze indicator.

func NewDotIndicatorSeries

func NewDotIndicatorSeries(name string, samples []DotSample, options *PanelOptions) *DotIndicatorSeries

func (*DotIndicatorSeries) AddSamples

func (ds *DotIndicatorSeries) AddSamples(samples ...DotSample)

func (*DotIndicatorSeries) GetName

func (ds *DotIndicatorSeries) GetName() string

func (*DotIndicatorSeries) GetStyle

func (ds *DotIndicatorSeries) GetStyle() chart.Style

func (*DotIndicatorSeries) GetTimeRange

func (ds *DotIndicatorSeries) GetTimeRange() (time.Time, time.Time)

func (*DotIndicatorSeries) GetValueRange

func (ds *DotIndicatorSeries) GetValueRange() (float64, float64)

func (*DotIndicatorSeries) GetYAxis

func (ds *DotIndicatorSeries) GetYAxis() chart.YAxisType

func (*DotIndicatorSeries) Render

func (ds *DotIndicatorSeries) Render(r chart.Renderer, b chart.Box, xRange, yRange chart.Range, style chart.Style)

func (*DotIndicatorSeries) Validate

func (ds *DotIndicatorSeries) Validate() error

type DotSample

type DotSample struct {
	Time  time.Time
	Y     float64
	Color drawing.Color
}

DotSample represents a single dot on the chart.

type IndicatorSeries

type IndicatorSeries interface {
	chart.Series
	GetTimeRange() (time.Time, time.Time)
	GetValueRange() (float64, float64)
}

type LegendKind

type LegendKind string

type LineIndicatorSeries

type LineIndicatorSeries struct {
	Name string
	// contains filtered or unexported fields
}

func NewLineIndicatorSeries

func NewLineIndicatorSeries(name string, points []PointSample, options *PanelOptions) *LineIndicatorSeries

func (*LineIndicatorSeries) AddPoints

func (ls *LineIndicatorSeries) AddPoints(points ...PointSample)

func (*LineIndicatorSeries) GetName

func (ls *LineIndicatorSeries) GetName() string

Implement chart.Series interface for LineIndicatorSeries.

func (*LineIndicatorSeries) GetStyle

func (ls *LineIndicatorSeries) GetStyle() chart.Style

func (*LineIndicatorSeries) GetTimeRange

func (ls *LineIndicatorSeries) GetTimeRange() (time.Time, time.Time)

func (*LineIndicatorSeries) GetValueRange

func (ls *LineIndicatorSeries) GetValueRange() (float64, float64)

func (*LineIndicatorSeries) GetYAxis

func (ls *LineIndicatorSeries) GetYAxis() chart.YAxisType

func (*LineIndicatorSeries) Render

func (ls *LineIndicatorSeries) Render(r chart.Renderer, b chart.Box, xRange, yRange chart.Range, style chart.Style)

func (*LineIndicatorSeries) Validate

func (ls *LineIndicatorSeries) Validate() error

type Panel

type Panel struct {
	*chart.Chart

	Options *PanelOptions
	// contains filtered or unexported fields
}

func NewPanel

func NewPanel(options *PanelOptions) *Panel

func (*Panel) AddIndicator

func (p *Panel) AddIndicator(indicator IndicatorSeries)

func (*Panel) AddKLines

func (p *Panel) AddKLines(klines []types.KLine)

func (*Panel) Write

func (p *Panel) Write(w io.Writer) error

type PanelOptions

type PanelOptions struct {
	// 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"`

	// column indicators options
	ColumnWidth float64 `json:"column_width" yaml:"column_width"`
	ColumnGap   float64 `json:"column_gap" yaml:"column_gap"`     // gap ratio between columns, ex: 0.15 -> gap width = 0.15 * column width
	ColumnAlpha uint8   `json:"column_alpha" yaml:"column_alpha"` // alpha value for column colors (0-255), default 200

	// dot indicators options
	DotRadius float64 `json:"dot_radius" yaml:"dot_radius"`

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

type PointSample

type PointSample struct {
	Time  time.Time
	Value *float64
}

type VolumeSeries

type VolumeSeries struct {
	Candles []Candle
}

func (*VolumeSeries) GetFirstValues

func (vs *VolumeSeries) GetFirstValues() (x, y float64)

func (*VolumeSeries) GetLastValues

func (vs *VolumeSeries) GetLastValues() (x, y float64)

func (*VolumeSeries) GetName

func (vs *VolumeSeries) GetName() string

func (*VolumeSeries) GetStyle

func (vs *VolumeSeries) GetStyle() chart.Style

func (*VolumeSeries) GetValue

func (vs *VolumeSeries) GetValue(index int) float64

func (*VolumeSeries) GetValueFormatters

func (vs *VolumeSeries) GetValueFormatters() (x, y chart.ValueFormatter)

func (*VolumeSeries) GetXAxisValue

func (vs *VolumeSeries) GetXAxisValue(index int) float64

func (*VolumeSeries) GetYAxis

func (vs *VolumeSeries) GetYAxis() chart.YAxisType

func (*VolumeSeries) Len

func (vs *VolumeSeries) Len() int

func (*VolumeSeries) Render

func (vs *VolumeSeries) Render(r chart.Renderer, canvasBox chart.Box, xrange, yrange chart.Range, style chart.Style)

func (*VolumeSeries) Validate

func (vs *VolumeSeries) Validate() error

func (*VolumeSeries) XValues

func (vs *VolumeSeries) XValues() []float64

XValues returns all X axis values for the series.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL