Este guia descreve, de forma contínua e didática, como construir a primeira metade de um bot de trading em Go. Nesta etapa não há transações on-chain. O programa observa o preço em dólar de um token de referência, compara esse valor com limites configurados em um arquivo .env
e decide, de maneira simulada, quando compraria e quando venderia. O objetivo é validar lógica e parâmetros sem risco financeiro, além de introduzir a persistência do estado em SQLite para que o bot possa ser interrompido e retomado sem perder contexto.
Conceito e fluxo
O comportamento é simples. O aplicativo carrega as configurações do .env
, consulta periodicamente o preço em USD do token de referência por HTTP e imprime no console o valor atual. Se o preço observado estiver abaixo ou igual ao alvo de compra e ainda não houver posição aberta, considera-se uma entrada e o estado é salvo no banco com a informação do preço de entrada. Se, com posição aberta, o preço atingir o alvo de lucro, considera-se a saída e o estado é atualizado para indicar que não há mais posição. Em qualquer outro caso, o programa apenas aguarda. Como não há transações nesta fase, tratamos essas decisões como um simulador de gatilhos, útil para ajustar parâmetros e entender o ciclo de execução.
Preparação do ambiente
É necessário ter Go instalado em versão recente. Crie uma pasta para o projeto, inicialize um módulo e instale as dependências. O pacote godotenv
carrega variáveis do arquivo .env
e o driver modernc.org/sqlite
possibilita usar SQLite sem CGO, o que simplifica a portabilidade.
mkdir sushiswap-v3-go && cd sushiswap-v3-go
go mod init example.com/sushiswap-v3-go
go get github.com/joho/godotenv
go get modernc.org/sqlite
Configuração com .env
Crie um arquivo .env
na raiz do projeto. Ele concentra variáveis de rede e parâmetros de estratégia. Também indicamos DB_PATH
, o caminho do arquivo SQLite que manterá o estado.
CHAIN_ID=11155111
NETWORK=sepolia
INFURA_API_KEY=SUA_INFURA_KEY
TOKEN0_ADDRESS=0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14
TOKEN1_ADDRESS=0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238
QUOTE0_ADDRESS=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
WALLET=0xSUA_WALLET
PRIVATE_KEY=0xSUA_PRIVATE_KEY
ROUTER_ADDRESS=0x3b0aa7d38bf3c103bf02d1de2e37568cbed3d6e8
PRICE_TO_BUY=5000
AMOUNT_TO_BUY=0.1
PROFITABILITY=1.1
DB_PATH=./bot.db
A variável QUOTE0_ADDRESS
representa o token cujo preço em USD será observado; no exemplo, usa-se o WETH da mainnet por ser uma referência líquida e amplamente utilizada. As variáveis PRICE_TO_BUY
e PROFITABILITY
definem, respectivamente, o alvo de entrada e o multiplicador do alvo de lucro. As chaves de carteira e o endereço do router são armazenados desde já, embora só venham a ser utilizadas na fase seguinte, quando forem adicionadas transações reais. Recomenda-se manter o arquivo .env
fora do controle de versão.
Implementação do código
A seguir estão os arquivos Go, organizados por responsabilidade. Basta criar cada arquivo com o conteúdo indicado.
config.go
Este módulo lê e valida o .env
, convertendo valores numéricos e garantindo que os campos mínimos existam para a execução desta etapa. Se faltar algo essencial, a função sinaliza claramente o problema.
package main
import (
"fmt"
"os"
"strconv"
"github.com/joho/godotenv"
)
type Config struct {
ChainID int
Network string
InfuraAPIKey string
Token0Address string
Token1Address string
Quote0Address string
Wallet string
PrivateKey string
RouterAddress string
PriceToBuy float64
AmountToBuy string
Profitability float64
DBPath string
}
func LoadConfig() (*Config, error) {
_ = godotenv.Load()
var err error
cfg := &Config{}
if v := os.Getenv("CHAIN_ID"); v != "" {
if cfg.ChainID, err = strconv.Atoi(v); err != nil {
return nil, fmt.Errorf("CHAIN_ID inválido: %w", err)
}
} else {
cfg.ChainID = 11155111
}
cfg.Network = os.Getenv("NETWORK")
cfg.InfuraAPIKey = os.Getenv("INFURA_API_KEY")
cfg.Token0Address = os.Getenv("TOKEN0_ADDRESS")
cfg.Token1Address = os.Getenv("TOKEN1_ADDRESS")
cfg.Quote0Address = os.Getenv("QUOTE0_ADDRESS")
cfg.Wallet = os.Getenv("WALLET")
cfg.PrivateKey = os.Getenv("PRIVATE_KEY")
cfg.RouterAddress = os.Getenv("ROUTER_ADDRESS")
cfg.AmountToBuy = os.Getenv("AMOUNT_TO_BUY")
if v := os.Getenv("PRICE_TO_BUY"); v != "" {
if cfg.PriceToBuy, err = strconv.ParseFloat(v, 64); err != nil {
return nil, fmt.Errorf("PRICE_TO_BUY inválido: %w", err)
}
} else {
return nil, fmt.Errorf("PRICE_TO_BUY ausente")
}
if v := os.Getenv("PROFITABILITY"); v != "" {
if cfg.Profitability, err = strconv.ParseFloat(v, 64); err != nil {
return nil, fmt.Errorf("PROFITABILITY inválido: %w", err)
}
} else {
return nil, fmt.Errorf("PROFITABILITY ausente")
}
if cfg.Quote0Address == "" {
return nil, fmt.Errorf("QUOTE0_ADDRESS ausente")
}
if v := os.Getenv("DB_PATH"); v != "" {
cfg.DBPath = v
} else {
cfg.DBPath = "./bot.db"
}
return cfg, nil
}
database.go
Este módulo cria e inicializa o banco SQLite, além de oferecer funções para ler e salvar o estado do bot. O estado se restringe a duas informações: se há ou não posição aberta e qual foi o preço de entrada, quando aplicável. Ao iniciar pela primeira vez, uma linha padrão é criada para garantir que o acesso seja simples e previsível nas execuções subsequentes.
package main
import (
"database/sql"
"fmt"
_ "modernc.org/sqlite"
)
type BotState struct {
IsOpened bool
EntryPrice *float64
}
func InitDB(path string) (*sql.DB, error) {
db, err := sql.Open("sqlite", path)
if err != nil {
return nil, fmt.Errorf("erro abrindo sqlite: %w", err)
}
schema := `
CREATE TABLE IF NOT EXISTS bot_state (
id INTEGER PRIMARY KEY CHECK (id = 1),
is_opened INTEGER NOT NULL,
entry_price REAL
);
INSERT INTO bot_state (id, is_opened, entry_price)
SELECT 1, 0, NULL
WHERE NOT EXISTS (SELECT 1 FROM bot_state WHERE id = 1);
`
if _, err := db.Exec(schema); err != nil {
return nil, fmt.Errorf("erro criando schema: %w", err)
}
return db, nil
}
func LoadBotState(db *sql.DB) (*BotState, error) {
row := db.QueryRow(`SELECT is_opened, entry_price FROM bot_state WHERE id = 1`)
var isOpenedInt int
var entryPrice sql.NullFloat64
if err := row.Scan(&isOpenedInt, &entryPrice); err != nil {
return nil, fmt.Errorf("erro lendo estado: %w", err)
}
var ep *float64
if entryPrice.Valid {
v := entryPrice.Float64
ep = &v
}
return &BotState{
IsOpened: isOpenedInt == 1,
EntryPrice: ep,
}, nil
}
func SaveBotState(db *sql.DB, st *BotState) error {
var isOpenedInt int
if st.IsOpened {
isOpenedInt = 1
} else {
isOpenedInt = 0
}
var entry interface{}
if st.EntryPrice != nil {
entry = *st.EntryPrice
}
if _, err := db.Exec(
`UPDATE bot_state SET is_opened = ?, entry_price = ? WHERE id = 1`,
isOpenedInt, entry,
); err != nil {
return fmt.Errorf("erro salvando estado: %w", err)
}
return nil
}
prices.go
Esta unidade contém a função que consulta a API pública de preços. O retorno esperado é um número de ponto flutuante representando o preço em USD do token indicado. A configuração de timeout e a verificação do status HTTP ajudam a tornar o comportamento previsível.
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
func fetchPriceUSD(tokenAddress string) (float64, error) {
url := fmt.Sprintf("https://api.sushi.com/price/v1/%d/%s", 1, tokenAddress)
req, _ := http.NewRequest(http.MethodGet, url, nil)
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 10 * time.Second}
res, err := client.Do(req)
if err != nil {
return 0, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
body, _ := io.ReadAll(res.Body)
return 0, fmt.Errorf("pricing api status %d: %s", res.StatusCode, string(body))
}
var price float64
if err := json.NewDecoder(res.Body).Decode(&price); err != nil {
return 0, fmt.Errorf("falha ao decodificar preço: %w", err)
}
return price, nil
}
main.go
O arquivo principal integra as partes. Ao iniciar, o programa carrega a configuração, prepara o banco, recupera o estado e entra em um ciclo que consulta o preço, imprime o valor e executa a lógica de decisão, atualizando o SQLite sempre que houver mudanças de estado. Um comando para limpar a tela mantém o console com aparência de painel.
package main
import (
"fmt"
"log"
"time"
)
func monitor(cfg *Config) (float64, error) {
price, err := fetchPriceUSD(cfg.Quote0Address)
if err != nil {
return 0, err
}
fmt.Print("\033[2J\033[H")
fmt.Println("Price:", price)
return price, nil
}
func main() {
cfg, err := LoadConfig()
if err != nil {
log.Fatalf("config erro: %v", err)
}
db, err := InitDB(cfg.DBPath)
if err != nil {
log.Fatalf("db erro: %v", err)
}
defer db.Close()
state, err := LoadBotState(db)
if err != nil {
log.Fatalf("erro carregando estado: %v", err)
}
cycle := func() {
price, err := monitor(cfg)
if err != nil {
log.Printf("erro monitorando preço: %v", err)
return
}
if !state.IsOpened && price <= cfg.PriceToBuy {
state.IsOpened = true
state.EntryPrice = &price
if err := SaveBotState(db, state); err != nil {
log.Printf("erro salvando estado (swap in): %v", err)
}
fmt.Printf("Swap in (entry=%.6f)\n", price)
return
}
target := cfg.PriceToBuy * cfg.Profitability
if state.IsOpened && price >= target {
state.IsOpened = false
state.EntryPrice = nil
if err := SaveBotState(db, state); err != nil {
log.Printf("erro salvando estado (swap out): %v", err)
}
fmt.Printf("Swap out (target=%.6f)\n", target)
return
}
if state.IsOpened && state.EntryPrice != nil {
fmt.Printf("Wait... (opened at %.6f, target %.6f)\n", *state.EntryPrice, target)
return
}
fmt.Println("Wait...")
}
cycle()
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for range ticker.C {
cycle()
}
}
Execução
Com os arquivos criados e o .env
preenchido, execute o projeto a partir da raiz. O console exibirá o preço e a decisão correspondente do ciclo. Quando o preço atingir o alvo de compra, aparecerá a indicação de entrada e o estado será salvo. Quando alcançar o alvo de lucro com posição aberta, a saída será registrada. Se o programa for encerrado e reiniciado, a leitura do banco restaurará a situação anterior.
go run .
Encerramento
Com esta base consolidada, você tem um esqueleto funcional capaz de observar preço, tomar decisões simuladas e manter consistência entre execuções por meio de SQLite. Na Parte 2, em vez de montar e enviar a transação on-chain localmente, o fluxo de swap será executado via gRPC: o cliente em Go fará chamadas a um serviço gRPC responsável por cotar e processar a operação de compra e venda. Esse serviço receberá parâmetros como chain_id
, token_in
, token_out
, amount
, max_slippage
e wallet
, retornará métricas de pré-troca (por exemplo, o assumedAmountOut) e cuidará da preparação/execução da transação, incluindo roteamento, montagem do payload, assinatura e envio, além do retorno do hash e do recibo quando confirmada.
Do ponto de vista de arquitetura, o cliente Go apenas disciplina a orquestração: abre o canal gRPC, envia a requisição de Quote/Execute
, aguarda a resposta (com sucesso ou erro tratado) e registra estado/telemetria. O serviço gRPC concentra a lógica sensível: integração com o roteador (para encontrar a melhor rota), aplicação de slippage máxima, políticas de deadline e retries, idempotência por chave, auditoria, observabilidade e segurança de chaves (sem trafegar PRIVATE_KEY
pelo cliente). A assinatura pode ficar no servidor (HSM, KMS ou cofre) ou ser separada em um signer dedicado, mantendo o cliente leve e sem material sensível.
Finalmente, a evolução natural é: o bot em Go continua decidindo quando agir, persiste o estado no SQLite, e a execução do swap passa a ser um contrato gRPC bem definido com protobufs para cotação e execução, entregando robustez operacional, isolamento de segredos e padronização de erros/logs para produção.
Referências
Go. (2025, 25 fevereiro). The Go programming language specification. go.dev. https://go.dev/ref/spec (Go)
Go. (n.d.). Documentation. go.dev. https://go.dev/doc/ (Go)
gRPC. (2024, 25 novembro). Basics tutorial | Go. grpc.io. https://grpc.io/docs/languages/go/basics/ (gRPC)
gRPC. (2024, 25 novembro). Quick start | Go. grpc.io. https://grpc.io/docs/languages/go/quickstart/ (gRPC)
Protocol Buffers. (n.d.). Protocol buffer basics: Go. protobuf.dev. https://protobuf.dev/getting-started/gotutorial/ (protobuf.dev)
Equipe go-ethereum (Geth). (2024, 24 maio). Getting started with Geth. geth.ethereum.org. https://geth.ethereum.org/docs/getting-started (go-ethereum)
Go Ethereum. (n.d.). github.com/ethereum/go-ethereum (API em Go). pkg.go.dev. https://pkg.go.dev/github.com/ethereum/go-ethereum (Go Packages)
modernc.org. (2025, 11 agosto). sqlite package (driver SQLite sem CGO). pkg.go.dev. https://pkg.go.dev/modernc.org/sqlite (Go Packages)
Joho. (n.d.). joho/godotenv [Repositório]. GitHub. https://github.com/joho/godotenv (GitHub)
Sushi. (2025, 13 setembro). Pricing API documentation. docs.sushi.com. https://docs.sushi.com/api/examples/pricing (docs.sushi.com)
gRPC-Go. (n.d.). google.golang.org/grpc (pacote em Go). pkg.go.dev. https://pkg.go.dev/google.golang.org/grpc (Go Packages)
💡Curtiu?
Se quiser trocar ideia sobre IA, cloud e arquitetura, me segue nas redes:
Publico conteúdos técnicos direto do campo de batalha. E quando descubro uma ferramenta que economiza tempo e resolve bem, como essa, você fica sabendo também.
Top comments (0)