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.ini with API keys to version control. Commit only config.example.ini and 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.

댓글

이 블로그의 인기 게시물

한국투자증권 KIS API로 실시간 시세 받기 (WebSocket 실전)

파이썬으로 업비트 API 연동하기 — 시세 조회부터 주문까지 기초

Go로 자동매매 신호봇 프레임워크 설계하기