Learn technical analysis by building my trading bot (part 1)

I bought my first Bitcoin two years ago when it was around $3000 and has keep trading everyday since then. I love trading, it’s exciting to wake up in the morning and check my balance.

Everything is not going well, i burned out my account several times and never learned a thing. I’m still having fun while trading but it’s like an expensive hobby but not a business…

It’s time to get serious.

What is my plan?

As a technical person, i focus on technical analysis first (they’re both technical!). Last few weeks, i’ve read a lot of articles on that topic and joined any course i found, there’s no end of them. They give so many concepts, patterns that i can’t memorize them all. And even if i can, i don’t know how to use them. I definitely need a better way to learn.

Eventually i decided to learn it the programmer’s way: Build a trading bot from scratch. I hope it could helps me gain a better understanding of technical analysis. Here is the plan

First thing first…

What is Technical Analysis?

This is the definition i got from investopedia

Technical analysis is a trading discipline employed to evaluate investments and identify trading opportunities by analyzing statistical trends gathered from trading activity, such as price movement and volume.

In short, we use past price action to predict price movement. How to do that is coming later. Let’s start by defining data structures to represent these historic data.

mô hình nến

Data Point

The image above is an example of candletick chart, which is commonly used in technical analysis. A candle has 4 data:

There’s also volume data at the bottom of the chart. These values are aggregated during a single time period and identified by start timestamp. We will keep them in DataPoint struct:

type DataPoint struct {
	StartTime  time.Time
	OpenPrice  num.Decimal
	ClosePrice num.Decimal
	HighPrice  num.Decimal
	LowPrice   num.Decimal
	Volume     num.Decimal

with num.Decimal is the interface:

type Decimal interface {
	Add(addend Decimal) Decimal
	Sub(subtrahend Decimal) Decimal
	Mul(factor Decimal) Decimal
	Div(denominator Decimal) Decimal
	Compare(other Decimal) int
	Float() float64

We could implement num.Decimal using float64 if we need high performance. Otherwise, if we need decimal with arbitrary precision, we could use math/big.Float.

Below is the simple implement, which is a thin wrapper around float64. For precision decimal implement and unit tests, go here

type simpleDecimal struct {
	fl float64

func (*simpleDecimal) cast(d Decimal) *simpleDecimal {
	if sd, ok := d.(*simpleDecimal); ok {
		return sd
	panic("incompatible decimal implement")

func (d *simpleDecimal) Add(addend Decimal) Decimal {
	return &simpleDecimal{d.fl + d.cast(addend).fl}

// Sub, Mul, Div are similar ...

func (d *simpleDecimal) Compare(other Decimal) int {
	o := d.cast(other).fl
	if d.fl > o {
		return 1
	} else if d.fl < o {
		return -1
	return 0

func (d *simpleDecimal) Float() float64 {
	return d.fl

func (d *simpleDecimal) String() string {
	return fmt.Sprintf("%f", d.fl)

Time Series

Usually, we need a series of data points to analysis. These data points are taken at a same period described as chart time frame.

Time frame is an important variable we must consider when studying chart. It ranges from minute to daily or weekly, depend on the trader’s personal trading style.

Our TimeSeries struct will look like this:

type TimeSeries struct {
	candles   []*DataPoint
	timeframe time.Duration

func NewTimeSeries(timeframe time.Duration) (t *TimeSeries) {
	return &TimeSeries{[]*DataPoint{}, timeframe}

We use Append to add a DataPoint at the end of the series and At to get a DataPoint by index. Append method makes sure the series is in time order.

func (ts *TimeSeries) Append(point *DataPoint) bool {
	if point == nil {
		panic("candle cannot be nil")

	if len(ts.data) == 0 || point.StartTime.After(ts.data[len(ts.data)-1].StartTime) {
		ts.data = append(ts.data, point)
		return true

	return false

func (ts *TimeSeries) At(index int) (*DataPoint, error) {
	if index < 0 || index >= len(ts.data) {
		return nil, ErrIndexOutOfRange

	return ts.data[index], nil

That’s it for today.

In the next few parts of the series, we will use our DataPoints and TimeSeries to calculate some indicators.