DEV Community

Zmey56
Zmey56

Posted on

Step-by-step guide on how to create a DCA bot on Go using the Binance API

Introduction

There are many ways to invest in crypto. Some try to catch the "bottom" and go all-in, others trade based on candlesticks and indicators. And then there are those - a growing number - who use the DCA (Dollar-Cost Averaging) strategy, or simply put, averaging. The idea is simple: you buy cryptocurrency for a fixed amount at regular intervals - for example, once a day or once a week. It doesn't matter whether the market is up or down - you keep buying. In the long run, this helps smooth out volatility and reduce risk.

Why does it work? Because no one can predict the bottom with precision. But with DCA, you take emotions out of the equation and enter the market gradually, at average prices. This works especially well in a rising market - for instance, in Bitcoin's case, this strategy has outperformed "buy and hold" when entering at the peak.

Now - why Go? The answer is simple: if you've ever written anything in Go, you know the language is all about performance, simplicity, and concurrency. Need a bot that runs reliably 24/7, connects to the Binance API, tracks timing, and sends orders precisely? Go is a perfect fit. Low memory usage, high speed, ease of maintenance - exactly what a trading tool needs.

What We're Going to Build

Before we start coding, let's clarify what exactly our DCA bot will be capable of and how it works under the hood. Our goal isn't just a basic "quick and dirty" example, but a fully functional tool that can be developed, scaled, and safely used.

Multiple Trading Pairs Support

You'll be able to configure multiple coins - for example, simultaneously buying BTC, ETH, and SOL. This is convenient if you're building a diversified crypto portfolio and want to run averaging separately for each coin.

Flexible Purchase Scheduling

Want to buy every day at 10 AM? Or every Monday? Or even every hour? - No problem. The bot will use a built-in scheduler (via cron or time.Ticker) that lets you define the desired frequency for each trading pair.

Customizable Purchase Amount

You set the purchase amount yourself. It can be a fixed amount in USDT - for example, $50 for BTC, $20 for ETH, etc. The settings are stored in a config file, making them easy to adjust.

Balance Check and Logging

Before each purchase, the bot will check if there's enough USDT in your account. Everything that happens - successful trades, errors, insufficient funds, Binance API behavior - gets logged. If something goes wrong, you'll see it right away.

Minimal UI via CLI or Optional REST

You'll be able to launch and manage the bot through a CLI interface - running with parameters, viewing logs, checking current status. If desired, you can easily add a REST API for control via a browser or mobile app.

Project Architecture

To make everything work reliably and be easy to maintain, we'll break the project into several logical components:

Binance Client

  • Handles communication with the exchange: authentication, order placement, balance retrieval.

Scheduler

  • Task scheduler. Responsible for triggering purchases on time according to the defined schedule.

Order Executor

  • Core component: checks balance, places orders, logs the results.

Logger / Storage

  • Stores the history of all actions and errors. Can write to a file, stdout, or even a database.

Config & CLI

  • Easy configuration via .env/yaml/json files and management through the command line.

In the end, you'll have not just a script, but a foundation for a real microservice that you can extend with strategies, notifications, a web interface, and analytics. Built the right way - with tests, logs, and an architecture that can scale.

Environment Setup

Before the bot can start trading, we need to set up the environment: install Go, add dependencies, configure access to the Binance API, and prepare our configuration.

Installing Go and Initializing the Project

You'll need to have Go installed. I'm using version 1.24.2, but any recent version will do.

After installing Go, you can either clone the repository or create the project manually:

git clone https://github.com/Zmey56/dca-bot.git
cd dca-bot
Enter fullscreen mode Exit fullscreen mode

If you're starting the project from scratch:

mkdir dca-bot
cd dca-bot
go mod init github.com/yourusername/dca-bot
Enter fullscreen mode Exit fullscreen mode

Installing Dependencies

The project uses three main libraries:

go get github.com/adshao/go-binance/v2
go get github.com/robfig/cron/v3
go get github.com/joho/godotenv
Enter fullscreen mode Exit fullscreen mode

Here's what each is for:

  • go-binance/v2 - handles communication with Binance: balances, orders, price quotes.
  • cron/v3 - allows scheduling tasks (e.g., placing an order every 24 hours).
  • godotenv - safely loads environment variables (API keys and settings are stored in .env instead of being hardcoded).

If you already have a go.mod file, simply run:

go mod tidy

Working with .env and Binance API Keys

To connect to Binance, you'll need an API key and secret. You can get them from your Binance account settings.

Create a .env file in the root of the project and add the following:

BINANCE_API_KEY=your_api_key_here
BINANCE_SECRET_KEY=your_secret_key_here
BUY_AMOUNT=0.001
Enter fullscreen mode Exit fullscreen mode

Make sure to add .env to your .gitignore to prevent the keys from accidentally being committed to a public repository.

Connecting the Configuration and Binance Client

The project includes a module internal/binance with a ClientWrapper implementation. It wraps the official Binance client and provides convenient methods like GetBalance and CreateMarketOrder.

Client initialization looks like this:

import (
    "github.com/Zmey56/dca-bot/internal/binance"
)
Enter fullscreen mode Exit fullscreen mode
client := binance.NewClientWrapper(binance.NewBinanceClient())
Enter fullscreen mode Exit fullscreen mode

Now you can safely interact with the Binance API - no hardcoded keys, no violations of clean architecture principles.

Integration with the Binance API

At this point, our bot can already launch, read configuration from .env, and has a clear structure. Now it's time to connect to Binance so the bot can check balances and place orders. We'll do this using the prebuilt module internal/binance, which wraps the official go-binance/v2 library.

Creating the Binance Client

First, we need to initialize the Binance client. We have two constructors for this:

  • NewBinanceClient() - creates a raw client using your API keys;
  • NewClientWrapper() - wraps it into our custom interface with methods like GetBalance and CreateMarketOrder.

Here's how it looks:

client := binance.NewClientWrapper(binance.NewBinanceClient())
Enter fullscreen mode Exit fullscreen mode

Now client is our main tool for interacting with the exchange.

Getting Balance Information

Before making any purchases, the bot needs to check if there's enough available funds in the account. For example, checking the USDT balance:

balance, err := client.GetBalance(ctx, "USDT")
if err != nil {
    log.Fatalf("❌ Failed to fetch balance: %v", err)
}
log.Printf("💰 Available USDT balance: %.2f", balance)
Enter fullscreen mode Exit fullscreen mode

This method calls GET /api/v3/account, parses the list of assets, and returns the value as a float64. Simple and effective.

Sending a Market Order

Now for the most important part - making a purchase. We're sending a market order, which tells Binance: "Buy the coin right now at the current market price."

err = client.CreateMarketOrder(ctx, "BTCUSDT", 0.001)
if err != nil {
    log.Fatalf("❌ Error while placing order: %v", err)
}
log.Println("✅ Market order successfully placed")
Enter fullscreen mode Exit fullscreen mode

The quantity must be rounded to the correct number of decimal places. This is already handled inside the method using fmt.Sprintf("%.6f", quantity).

Handling Errors and Rate Limits

Binance imposes a rate limit on API calls per minute. If we exceed it, the API will return a Too many requests error (code -1003). The SDK doesn't expose a dedicated error type for this, so we handle it by checking the error text directly:

package binance

import (
    "log"
    "strings"
    "time"
)
func HandleBinanceError(err error) bool {
    if err == nil {
        return false
    }
    if strings.Contains(err.Error(), "Too many requests") || strings.Contains(err.Error(), "-1003") {
        log.Println("⚠️ Rate limit exceeded. Waiting 2 seconds...")
        time.Sleep(2 * time.Second)
        return true
    }
    log.Printf("❌ Binance API error: %s", err.Error())
    return false
}
Enter fullscreen mode Exit fullscreen mode

Usage in code:

err := client.CreateMarketOrder(ctx, "BTCUSDT", 0.001)
if binance.HandleBinanceError(err) {
    // you can try again
    err = client.CreateMarketOrder(ctx, "BTCUSDT", 0.001)
}
Enter fullscreen mode Exit fullscreen mode

Implementing DCA Logic

Now that we know how to work with the Binance API - getting the balance and sending orders - it's time to put everything together and implement the actual DCA logic: buying a selected coin on a schedule, for a specified amount, without crashing in the process.

Configuration: pair, amount, frequency

To let the bot know what to buy, how much, and when, we need a simple configuration. No YAML or databases for now - just set everything in .env, for example:

SYMBOL=BTCUSDT
BUY_AMOUNT=0.001
SCHEDULE=0 0 * * *   # Every day at 00:00 (cron)
Enter fullscreen mode Exit fullscreen mode

In Go, we read it like this:

symbol := os.Getenv("SYMBOL")
amount, _ := strconv.ParseFloat(os.Getenv("BUY_AMOUNT"), 64)
Enter fullscreen mode Exit fullscreen mode

The frequency can be set either via cron (robfig/cron/v3) or using time.Ticker if you want a simple interval (e.g. every 6 hours).

Main cycle: what the bot does at each trigger

Each time the scheduled trigger fires, the bot follows a simple flow:

Get the current price (optional, but useful for logs)

price, err := client.GetCurrentPrice(ctx, symbol)
if err == nil {
    log.Printf("📊 Current price of %s: %.2f", symbol, price)
}
Enter fullscreen mode Exit fullscreen mode

The GetCurrentPrice method can be implemented using NewListPricesService().Symbol(symbol) - see go-binance/v2 → Get Price.

Check balance

Before buying anything, make sure there's enough USDT available:

balance, err := client.GetBalance(ctx, "USDT")
if err != nil || balance < amount*price {
    log.Printf("⚠️ Not enough funds: %.2f USDT", balance)
    return
}
Enter fullscreen mode Exit fullscreen mode

Execute the order

If everything checks out - send a market order:

err = client.CreateMarketOrder(ctx, symbol, amount)
if err != nil {
    if binance.HandleBinanceError(err) {
        // retry if needed
    }
    log.Printf("❌ Error buying %s: %v", symbol, err)
    return
}
log.Printf("✅ Successfully purchased %f %s", amount, symbol)
Enter fullscreen mode Exit fullscreen mode

Logging

All key actions and errors are logged. Writing to file or stdout is enough for now. Later we can add CSV or SQLite support if needed for history.

Example: running on a schedule

We use github.com/robfig/cron/v3 to run the buy logic once a day:

import (
    "github.com/robfig/cron/v3"
)

func startScheduler(client binance.BinanceClient, symbol string, amount float64) {
    c := cron.New()
    _, err := c.AddFunc("0 10 * * *", func() {
        log.Println("🕒 Time to buy!")
        err := client.CreateMarketOrder(context.Background(), symbol, amount)
        if err != nil {
            log.Printf("❌ Purchase error: %v", err)
        } else {
            log.Println("✅ Order sent")
        }
    })
    if err != nil {
        log.Fatalf("Error adding cron job: %v", err)
    }
    c.Start()
}
Enter fullscreen mode Exit fullscreen mode

If you want something simpler - you can use time.Ticker:

ticker := time.NewTicker(24 * time.Hour)
for range ticker.C {
    log.Println("🕒 It's time to buy!")
    // purchase...
}
Enter fullscreen mode Exit fullscreen mode

Testing and debugging

Developing the bot is only half the job. To make sure it runs reliably and doesn't buy crypto randomly, we need to ensure that:

  • the logic works correctly,
  • everything can be tested in isolation,
  • and errors are easy to catch.

Simple Unit Tests with testing

First things first - basic unit tests for core business logic. For example, if you move the calculation of the buy amount or interval into a function, it's easy to test it with the standard library:

func TestSomething(t *testing.T) {
 result := CalculateXYZ(...)
 if result != expected {
  t.Errorf("expected %v, got %v", expected, result)
 }
}
Enter fullscreen mode Exit fullscreen mode

Test files are named something_test.go and live alongside the source files.

Mocks for the Binance API

Binance is an external system - we don't want to make real trades in our tests. That's why we declared an interface in internal/binance/interface.go:

type BinanceClient interface {
 GetBalance(ctx context.Context, asset string) (float64, error)
 CreateMarketOrder(ctx context.Context, symbol string, quantity float64) error
}
Enter fullscreen mode Exit fullscreen mode

Now we can mock this interface using Uber's mock library:

go install go.uber.org/mock/mockgen@latest 
Enter fullscreen mode Exit fullscreen mode

Generate the mock:

mockgen -source=internal/binance/interface.go -destination=internal/binance/mock_client.go -package=binance
Enter fullscreen mode Exit fullscreen mode

Then, in tests, we can use the fake implementation:

func TestDCAExecution(t *testing.T) {
 mock := NewMockBinanceClient(ctrl)
 mock.EXPECT().GetBalance(gomock.Any(), "USDT").Return(100.0, nil)
 mock.EXPECT().CreateMarketOrder(gomock.Any(), "BTCUSDT", 0.001).Return(nil)

 // Inject the mock instead of the real client
 err := DoDCA(mock)
 if err != nil {
  t.Fatalf("purchase execution failed: %v", err)
 }
}
Enter fullscreen mode Exit fullscreen mode

Logging to File and Console

During debugging, it's important to see what's happening. By default, everything is printed to the console with log.Println(), but you can easily add file output too:

logFile, err := os.OpenFile("dca.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
 log.Fatal(err)
}
log.SetOutput(io.MultiWriter(os.Stdout, logFile))
Enter fullscreen mode Exit fullscreen mode

Now all logs will go both to the terminal and to dca.log - handy for both production use and debugging.

Result and launch

At this point, we already have a working DCA bot that, on schedule, logs into Binance, checks the balance, and sends market orders. All that's left is to launch it properly, observe how it runs, and make sure we don't forget about security.

How to Run the Bot

The project is built like a standard Go application. The entry point is cmd/dca-bot/main.go.

Run via Terminal

go run ./cmd/dca-bot
Enter fullscreen mode Exit fullscreen mode

Or build a binary:

go build -o dca-bot ./cmd/dca-bot
./dca-bot
Enter fullscreen mode Exit fullscreen mode

Run as a Background Service

You can use systemd, supervisord, nohup, or simply:

nohup ./dca-bot > output.log 2>&1 &
Enter fullscreen mode Exit fullscreen mode

This way, the bot will run in the background and log everything to output.log.

Order Execution Logs

All actions are logged both to the console and to the file dca.log. For example:

🚀 Bot started
📅 Scheduler initialized
🕒 Time to buy!
📊 Current price BTCUSDT: 63784.12
💰 Available USDT balance: 25.00
✅ Bought 0.001 BTCUSDT
Enter fullscreen mode Exit fullscreen mode

If something goes wrong:

⚠️ Rate limit exceeded. Waiting for 2 seconds...
❌ Purchase error: request rate limit exceeded
Enter fullscreen mode Exit fullscreen mode

Logs are useful both in development and in production. You can easily set up log rotation using logrotate or configure log forwarding to Telegram/Slack - totally up to you.

Security: Keys and Limits

  1. API keys are stored in .env, not hardcoded - that's already good.
  2. .env is added to .gitignore, so it won't accidentally get pushed to GitHub.
  3. The bot does not store balances or draw charts, it simply acts as an "averaging" worker.
  4. To avoid getting banned by Binance:
  • we handle errors and use sleep when hitting rate limits;
  • we avoid unnecessary API calls;
  • you can use a proxy or an API key with limited permissions (e.g., trade-only).

Conclusion

In the end, we've built a minimalistic yet functional DCA bot in Go that does one simple thing - buys crypto on a schedule. It can connect to Binance, check balances, send orders, log activity, and run either manually or as a background service. Everything is written from scratch with clean architecture and room for expansion.

What Can Be Improved

If you want more than just scheduled buys - there's plenty of room to grow:

  • QFL (Quickfingers Luc) Strategy - the bot can buy not just by time, but in pullback zones;
  • Add MACD, EMA, or RSI - to enter only when the market sends a signal;
  • Telegram Notifications - know when a buy is made;
  • Purchase history in SQLite or CSV - to analyze performance later;
  • Visualization via Grafana or Prometheus - for dashboard lovers;
  • More tests and integrations - e.g., with testcontainers-go for CI-ready setups.

Useful Resources

Source Code on GitHub

You can find the complete bot code (and a bit more) in my repository. Everything is well structured: cmd, internal, tests, logic, .env - clone and run.

Alternative: Ready-Made Bots on Bitsgap

Want to try out DCA or other strategies (like Grid, Combo, or Trailing) but don't feel like writing code, dealing with APIs, or setting everything up manually? There's an easier way: just sign up on Bitsgap using my referral link

I personally use Bitsgap for part of my portfolio - it's convenient, visual, and helps you catch good entry points. Plus, you can try the PRO plan free for 7 days to see how everything works in real market conditions without taking unnecessary risks.

By signing up through my link, you'll also be supporting my next project - a free Telegram bot that provides DCA trading signals for manual execution. The more support it gets, the sooner it will be ready!!!

Top comments (0)