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
If you're starting the project from scratch:
mkdir dca-bot
cd dca-bot
go mod init github.com/yourusername/dca-bot
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
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
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"
)
client := binance.NewClientWrapper(binance.NewBinanceClient())
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())
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)
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")
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
}
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)
}
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)
In Go, we read it like this:
symbol := os.Getenv("SYMBOL")
amount, _ := strconv.ParseFloat(os.Getenv("BUY_AMOUNT"), 64)
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)
}
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
}
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)
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()
}
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...
}
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)
}
}
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
}
Now we can mock this interface using Uber's mock library:
go install go.uber.org/mock/mockgen@latest
Generate the mock:
mockgen -source=internal/binance/interface.go -destination=internal/binance/mock_client.go -package=binance
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)
}
}
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))
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
Or build a binary:
go build -o dca-bot ./cmd/dca-bot
./dca-bot
Run as a Background Service
You can use systemd, supervisord, nohup, or simply:
nohup ./dca-bot > output.log 2>&1 &
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
If something goes wrong:
⚠️ Rate limit exceeded. Waiting for 2 seconds...
❌ Purchase error: request rate limit exceeded
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
- API keys are stored in .env, not hardcoded - that's already good.
- .env is added to .gitignore, so it won't accidentally get pushed to GitHub.
- The bot does not store balances or draw charts, it simply acts as an "averaging" worker.
- 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
- Binance API Documentation
- Official go-binance/v2 SDK
- cron/v3 Scheduler Library
- Mocking with GoMock (Uber)
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)