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

🌐 English

자동매매 봇을 만들다 보면 전략 코드보다 주변 골격에 시간을 더 쓰게 됩니다. 설정을 어떻게 읽지? 상태는 어디서 보지? 알림은? 이 글은 실제로 돌아가는 Go 선물 신호봇을 만들며 정리한, 재사용 가능한 프레임워크 뼈대입니다.

핵심 원칙은 하나입니다. 최소 의존성. 설정은 ini 하나, 웹은 표준 net/http, 알림은 텔레그램 REST 호출. 무거운 프레임워크 없이 단일 실행 파일로 끝냅니다.

1. 설정 로더 — config.ini

봇 파라미터는 코드가 아니라 파일에 둡니다. 재빌드 없이 값을 바꾸기 위해서죠. INI는 사람이 편집하기 쉽고 섹션으로 묶기 좋습니다.

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) // 기본값을 안전한 쪽으로

	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

}

MustBool, MustInt 계열의 기본값이 핵심입니다. 키가 비어 있어도 봇이 죽지 않고, testnet 같은 위험한 값은 기본을 안전한 쪽(true)으로 둡니다. 민감한 값은 환경변수로 덮어쓸 여지도 남깁니다.

if v := os.Getenv("DASHBOARD_KEY"); v != "" {

	c.DashboardKey = v

}

API 키가 들어가는 config.ini는 절대 저장소에 커밋하지 마세요. config.example.ini만 커밋하고 실제 파일은 .gitignore에 넣습니다.

2. 동시성 모델 — 뮤텍스 대신 채널

여기가 설계의 심장입니다. 봇에는 여러 주체가 상태를 건드립니다. 매 틱 평가 루프, 웹 대시보드의 "강제 청산" 버튼, 설정 변경 요청... 뮤텍스로 방어하기 시작하면 데드락과 씨름하게 됩니다.

Go 관용구를 따릅니다. 공유 메모리를 잠그지 말고, 채널로 통신하라.

포지션·전략·설정을 엔진 goroutine 하나가 단독 소유하고, 외부 요청은 전부 명령 채널로 위임합니다.

type command struct {

	kind      cmdKind      // 강제진입 / 청산 / 설정변경 ...

	symbol    string

	replyErr  chan error   // 결과를 되돌려받을 채널

}

func (e *engine) run() {

	e.tick() // 시작 즉시 1회

	for {

		timer := time.NewTimer(e.loopInterval())

		select {

		case <-e.stop:

			return

		case c := <-e.cmds: // 외부 명령 우선 처리

			timer.Stop()

			e.handleCommand(c)

		case <-timer.C:

			if e.running {

				e.tick() // 주기적 시장 평가

			}

		}

	}

}

대시보드 핸들러는 상태를 직접 만지지 않습니다. 명령을 채널에 넣고 응답을 기다릴 뿐이죠.

ForceExitFn = func(sym string) error {

	r := make(chan error)

	e.cmds <- command{kind: cmdForceExit, symbol: sym, replyErr: r}

	return <-r // 엔진이 처리하고 결과를 돌려줄 때까지 블로킹

}

이 구조의 이점은 경쟁 상태가 원천적으로 없다는 것입니다. 모든 주문과 상태 변경이 한 goroutine에서 직렬로 처리되니까요. 트레이드오프는 있습니다. 엔진이 한 틱(여러 종목 조회로 수 초)을 도는 동안 들어온 명령은 틱이 끝난 뒤에야 처리되어 최대 한 틱만큼 지연됩니다. 매매 봇에서는 충분히 받아들일 만한 값입니다.

3. 알림 — 텔레그램은 그냥 HTTP

텔레그램 알림에 SDK는 필요 없습니다. 봇 토큰으로 sendMessage에 POST 한 번이면 됩니다. 토큰이 비어 있으면 조용히 로그만 남기도록 만들어 두면, 알림 설정 없이도 봇이 돌아갑니다.

4. 콘솔 제어와 진입점

Windows 콘솔 앱이라면 시작 시 창을 최소화하고, 로그는 파일과 표준출력에 동시에 씁니다.

log.SetOutput(io.MultiWriter(os.Stdout, logFile))

종료 처리도 매매 봇답게. Ctrl+C를 받아도 보유 포지션과 거래소에 걸린 손절 주문은 건드리지 않고 루프만 멈춥니다.

signal.Notify(sig, os.Interrupt, syscall.SIGTERM)

<-sig

log.Println("[종료] 보유 포지션은 유지됩니다")

close(e.stop)

정리

  • 설정은 파일로 빼되 안전한 기본값을 강제한다
  • 상태는 한 goroutine이 소유하고 채널로만 접근한다 — 뮤텍스 지옥 회피
  • 알림·웹은 표준 라이브러리로 충분하다 — 단일 실행 파일 유지
  • 종료는 포지션을 보존하는 방향으로 설계한다

전략을 갈아 끼워도 이 골격은 그대로 재사용됩니다. 프레임워크와 전략을 분리하는 것, 그게 오래 가는 봇의 조건입니다.

투자에는 원금 손실 위험이 있으며, 이 글은 특정 수익을 보장하지 않는 기술 설명입니다. 실거래 전 반드시 테스트넷에서 검증하세요.

댓글

이 블로그의 인기 게시물

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

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