Designing a Trading Signal Bot Framework in Go
Building automated trading bots, you'll find that most of your time goes into the infrastructure around the strategy, not the strategy itself. Where do I load config? How do I check bot status? What about alerts? This article captures a reusable framework skeleton distilled from actually building a live Go futures signal bot.
One core principle guides it all: minimal dependencies. Config from a single ini file, web server from the standard net/http, Telegram alerts via plain HTTP calls. No heavy frameworks—just a single executable.
1. Config Loader — config.ini
Bot parameters live in files, not code. This lets you tweak values without rebuilding. INI format is human-friendly and groups settings neatly.
type AppConfig struct {
APIKey, APISecret string
Testnet bool
LoopIntervalSec int
NotionalPerEntry float64
Leverage int
DashboardPort int
DashboardKey string
}
func loadConfig(path string) (*AppConfig, error) {
f, err := ini.Load(path)
if err != nil {
return nil, err
}
c := &AppConfig{}
bi := f.Section("binance")
c.APIKey = bi.Key("api_key").String()
c.Testnet = bi.Key("testnet").MustBool(true) // safe default
tr := f.Section("trading")
c.LoopIntervalSec = tr.Key("loop_interval_sec").MustInt(10)
c.Leverage = tr.Key("leverage").MustInt(5)
c.DashboardPort = f.Section("dashboard").Key("port").MustInt(9333)
return c, nil
}
The magic is defaults. Methods like MustBool and MustInt ensure the bot won't crash on missing values, and dangerous settings like testnet default to the safe option (true). Sensitive values can be overridden by environment variables:
if v := os.Getenv("DASHBOARD_KEY"); v != "" {
c.DashboardKey = v
}
Never commit your actual
config.iniwith API keys to version control. Commit onlyconfig.example.iniand add the real file to.gitignore.
2. Concurrency Model — Channels, Not Mutexes
This is where the framework's heart beats. Multiple actors touch bot state: the evaluation loop each tick, the web dashboard's "force exit" button, config change requests... Defending with mutexes leads to deadlock hell.
Follow Go idiom: don't lock shared memory; communicate over channels.
One engine goroutine owns all state—positions, strategy, config—and all external requests are delegated through a command channel.
type command struct {
kind cmdKind // force entry / exit / config change ...
symbol string
replyErr chan error // return result to this channel
}
func (e *engine) run() {
e.tick() // first tick immediately
for {
timer := time.NewTimer(e.loopInterval())
select {
case <-e.stop:
return
case c := <-e.cmds: // process external commands first
timer.Stop()
e.handleCommand(c)
case <-timer.C:
if e.running {
e.tick() // periodic market evaluation
}
}
}
}
The dashboard handler never touches state directly. It pushes a command to the channel and waits for the response:
ForceExitFn = func(sym string) error {
r := make(chan error)
e.cmds <- command{kind: cmdForceExit, symbol: sym, replyErr: r}
return <-r // blocks until engine processes and returns
}
The beauty of this design: no race conditions by construction. All order execution and state changes happen serially in one goroutine. The tradeoff: while the engine runs one tick (several seconds for multi-symbol fetches), commands queue and execute after the tick completes—maximum latency is one tick interval, which trading bots can easily live with.
3. Notifications — Telegram Is Just HTTP
You don't need an SDK for Telegram alerts. One POST to sendMessage with your bot token is enough. If the token is blank, silently log and keep the bot running; no alerting infrastructure required.
4. Console Control and Entry Point
For Windows console apps, minimize the window at startup and log to both stdout and file simultaneously:
log.SetOutput(io.MultiWriter(os.Stdout, logFile))
Handle shutdown the trading-bot way: when you hit Ctrl+C, stop the loop but leave positions and stop-loss orders on the exchange untouched:
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
<-sig
log.Println("[shutdown] positions preserved")
close(e.stop)
Summary
- Config lives in files with safe defaults—rebuild-free tweaking
- One goroutine owns state; access only through channels—no mutex deadlock hell
- Alerts and web use stdlib—keep it one executable
- Shutdown preserves positions—designed for real trading
Swap out strategies, reuse the skeleton. Separating framework from logic is what makes a bot survive.
Investment carries risk of principal loss. This is a technical explanation, not investment advice. Always validate on testnet before live trading.
댓글
댓글 쓰기