🤖 Пишем торгового робота на Go: от котировок до сделок


На главную > Блог > Категория > 🤖 Пишем торгового робота на Go: от котировок до сделок

tradebot_go

Вступление: почему Go — идеальный выбор для торгового бота

Вы написали прототип бота на Python, но он тормозит при 1000 свечей? Устали от GIL и медленных циклов? Хотите создать лёгкого, быстрого и надёжного бота, который будет работать 24/7 без перезагрузки? Встречайте Go (Golang).

Go создавался в Google для высоконагруженных сетевых сервисов. Он компилируется в машинный код, имеет встроенную поддержку многопоточности (горутины), простой синтаксис и отличную экосистему для работы с HTTP и WebSocket. В этой статье мы создадим полноценного торгового робота, который:

  • 📡 Получает котировки в реальном времени через WebSocket Binance.
  • 📊 Рассчитывает технические индикаторы (SMA, RSI) с помощью библиотеки ta4go.
  • 📈 Генерирует сигналы на покупку/продажу.
  • 💸 Отправляет ордеры через Binance API (с симуляцией для тестов).
  • 🛡️ Управляет рисками (фиксированный риск, стоп-лосс, дневной лимит).
«Go — это язык, который позволяет вашему боту обрабатывать тысячи сделок в секунду, не теряя ни капли производительности. И при этом код остаётся читаемым».

1. Настройка проекта и установка зависимостей

Создаём проект


mkdir trading-bot-go
cd trading-bot-go
go mod init trading-bot

Устанавливаем библиотеки


go get github.com/gorilla/websocket
go get github.com/shopspring/decimal
go get github.com/markcheno/go-talib
go get github.com/go-resty/resty/v2
go get github.com/joho/godotenv
📦 Что за библиотеки:
  • gorilla/websocket — работа с WebSocket (котировки в реальном времени).
  • shopspring/decimal — точные вычисления с деньгами (чтобы не потерять сатоши).
  • markcheno/go-talib — 100+ технических индикаторов (SMA, RSI, MACD).
  • go-resty/resty — удобный HTTP-клиент для Binance API.
  • godotenv — загрузка API-ключей из .env файла.

2. Подключение к WebSocket Binance (реальные котировки)

Создадим модуль websocket_client.go, который будет получать поток цен BTC/USDT.


// websocket_client.go
package main

import (
    "encoding/json"
    "log"
    "github.com/gorilla/websocket"
)

type TradeEvent struct {
    Price  string `json:"p"`
    Volume string `json:"q"`
    Time   int64  `json:"T"`
}

type WebSocketClient struct {
    conn *websocket.Conn
}

func NewWebSocketClient() (*WebSocketClient, error) {
    url := "wss://stream.binance.com:9443/ws/btcusdt@trade"
    conn, _, err := websocket.DefaultDialer.Dial(url, nil)
    if err != nil {
        return nil, err
    }
    return &WebSocketClient{conn: conn}, nil
}

func (wsc *WebSocketClient) Listen(onTrade func(price float64)) {
    for {
        _, message, err := wsc.conn.ReadMessage()
        if err != nil {
            log.Println("WebSocket ошибка:", err)
            return
        }
        
        var trade TradeEvent
        if err := json.Unmarshal(message, &trade); err != nil {
            log.Println("Ошибка парсинга JSON:", err)
            continue
        }
        
        var price float64
        if _, err := fmt.Sscanf(trade.Price, "%f", &price); err == nil {
            onTrade(price)
        }
    }
}

3. Расчёт индикаторов (SMA и RSI) с ta4go

Создадим модуль strategy.go. Мы будем использовать библиотеку ta4go (форк go-talib для типизированных индикаторов).


// strategy.go
package main

import (
    "github.com/markcheno/go-talib"
)

type SMACrossoverStrategy struct {
    Prices        []float64
    ShortPeriod   int
    LongPeriod    int
    lastSignal    string
}

func NewSMACrossoverStrategy(shortPeriod, longPeriod int) *SMACrossoverStrategy {
    return &SMACrossoverStrategy{
        ShortPeriod: shortPeriod,
        LongPeriod:  longPeriod,
        Prices:      []float64{},
        lastSignal:  "",
    }
}

func (s *SMACrossoverStrategy) AddPrice(price float64) {
    s.Prices = append(s.Prices, price)
    if len(s.Prices) > s.LongPeriod+20 {
        s.Prices = s.Prices[1:]
    }
}

func (s *SMACrossoverStrategy) GetSignal() string {
    if len(s.Prices) < s.LongPeriod {
        return "HOLD"
    }
    
    smaShort := talib.Sma(s.Prices, s.ShortPeriod)
    smaLong := talib.Sma(s.Prices, s.LongPeriod)
    
    lastShort := smaShort[len(smaShort)-1]
    lastLong := smaLong[len(smaLong)-1]
    prevShort := smaShort[len(smaShort)-2]
    prevLong := smaLong[len(smaLong)-2]
    
    // Бычье пересечение (быстрая пересекает медленную снизу вверх)
    if prevShort <= prevLong && lastShort > lastLong && s.lastSignal != "BUY" {
        s.lastSignal = "BUY"
        return "BUY"
    }
    
    // Медвежье пересечение (быстрая пересекает медленную сверху вниз)
    if prevShort >= prevLong && lastShort < lastLong && s.lastSignal != "SELL" {
        s.lastSignal = "SELL"
        return "SELL"
    }
    
    return "HOLD"
}
📊 Добавляем RSI для фильтрации:

func (s *SMACrossoverStrategy) IsStrongBuy() bool {
    if len(s.Prices) < 30 {
        return false
    }
    rsi := talib.Rsi(s.Prices, 14)
    lastRSI := rsi[len(rsi)-1]
    return lastRSI > 50
}

4. Риск-менеджмент (фиксированный риск, дневной лимит)

// risk_manager.go
package main

import (
    "log"
    "time"
)

type RiskManager struct {
    MaxDailyLoss     float64
    CurrentDailyLoss float64
    LastResetDate     string
}

func NewRiskManager(maxDailyLoss float64) *RiskManager {
    return &RiskManager{
        MaxDailyLoss:     maxDailyLoss,
        CurrentDailyLoss: 0,
        LastResetDate:     time.Now().Format("2006-01-02"),
    }
}

func (rm *RiskManager) CanTrade(riskAmount float64) bool {
    // Сброс дневного счётчика
    today := time.Now().Format("2006-01-02")
    if today != rm.LastResetDate {
        rm.CurrentDailyLoss = 0
        rm.LastResetDate = today
        log.Println("Дневной лимит убытков сброшен")
    }
    
    if rm.CurrentDailyLoss+riskAmount > rm.MaxDailyLoss {
        log.Printf("❌ Риск: превышен дневной лимит убытков (%.2f / %.2f)", rm.CurrentDailyLoss+riskAmount, rm.MaxDailyLoss)
        return false
    }
    return true
}

func (rm *RiskManager) RegisterLoss(loss float64) {
    rm.CurrentDailyLoss += loss
    log.Printf("⚠️ Зафиксирован убыток %.2f. Дневной убыток: %.2f", loss, rm.CurrentDailyLoss)
}

5. Исполнение ордеров (Binance API с симуляцией)


// order_executor.go
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "log"
    "net/url"
    "time"
    "github.com/go-resty/resty/v2"
)

type OrderExecutor struct {
    ApiKey    string
    ApiSecret string
    PaperMode bool
    client    *resty.Client
}

func NewOrderExecutor(apiKey, apiSecret string, paperMode bool) *OrderExecutor {
    return &OrderExecutor{
        ApiKey:    apiKey,
        ApiSecret: apiSecret,
        PaperMode: paperMode,
        client:    resty.New(),
    }
}

func (oe *OrderExecutor) PlaceOrder(symbol, side string, quantity float64) (string, error) {
    if oe.PaperMode {
        log.Printf("📝 [PAPER] %s %s %.5f %s", side, symbol, quantity, "по рыночной цене")
        return "paper_order_id", nil
    }
    
    // Реальная отправка ордера на Binance
    timestamp := time.Now().UnixMilli()
    params := url.Values{}
    params.Set("symbol", symbol)
    params.Set("side", side)
    params.Set("type", "MARKET")
    params.Set("quantity", fmt.Sprintf("%.5f", quantity))
    params.Set("timestamp", fmt.Sprintf("%d", timestamp))
    
    queryString := params.Encode()
    signature := oe.signRequest(queryString)
    
    resp, err := oe.client.R().
        SetHeader("X-MBX-APIKEY", oe.ApiKey).
        SetQueryParams(params).
        SetQueryParam("signature", signature).
        Post("https://api.binance.com/api/v3/order")
    
    if err != nil {
        return "", err
    }
    
    log.Printf("✅ Реальный ордер отправлен: %s", resp.String())
    return resp.String(), nil
}

func (oe *OrderExecutor) signRequest(queryString string) string {
    h := hmac.New(sha256.New, []byte(oe.ApiSecret))
    h.Write([]byte(queryString))
    return hex.EncodeToString(h.Sum(nil))
}

6. Главный цикл (main.go) — собираем всё вместе


// main.go
package main

import (
    "log"
    "os"
    "time"
    "github.com/joho/godotenv"
)

const (
    RISK_PER_TRADE = 100.0      // риск на сделку в USDT
    MAX_DAILY_LOSS = 500.0      // дневной лимит убытков
    SHORT_PERIOD   = 10
    LONG_PERIOD    = 30
)

var currentPosition struct {
    Open    bool
    Side    string
    Entry   float64
    Quantity float64
}

func main() {
    // Загрузка .env файла
    godotenv.Load()
    apiKey := os.Getenv("BINANCE_API_KEY")
    apiSecret := os.Getenv("BINANCE_API_SECRET")
    
    // Инициализация модулей
    riskManager := NewRiskManager(MAX_DAILY_LOSS)
    strategy := NewSMACrossoverStrategy(SHORT_PERIOD, LONG_PERIOD)
    orderExecutor := NewOrderExecutor(apiKey, apiSecret, true) // true = paper trading
    
    // Подключение к WebSocket
    wsClient, err := NewWebSocketClient()
    if err != nil {
        log.Fatal("Ошибка подключения к WebSocket:", err)
    }
    
    log.Println("✅ Бот запущен. Ожидание сигналов...")
    
    wsClient.Listen(func(price float64) {
        // Обновляем стратегию новой ценой
        strategy.AddPrice(price)
        signal := strategy.GetSignal()
        
        log.Printf("Цена: %.2f, Сигнал: %s", price, signal)
        
        // Если нет открытой позиции и сигнал не HOLD
        if !currentPosition.Open && signal != "HOLD" {
            // Проверяем риск
            if !riskManager.CanTrade(RISK_PER_TRADE) {
                return
            }
            
            // Рассчитываем размер позиции (примерный, фиксированный)
            quantity := 0.01 // 0.01 BTC для примера
            if signal == "BUY" {
                orderExecutor.PlaceOrder("BTCUSDT", "BUY", quantity)
                currentPosition.Open = true
                currentPosition.Side = "BUY"
                currentPosition.Entry = price
                currentPosition.Quantity = quantity
            } else if signal == "SELL" {
                orderExecutor.PlaceOrder("BTCUSDT", "SELL", quantity)
                currentPosition.Open = true
                currentPosition.Side = "SELL"
                currentPosition.Entry = price
                currentPosition.Quantity = quantity
            }
        } else if currentPosition.Open && signal != "HOLD" {
            // Если позиция открыта, и сигнал противоположный → закрываем
            if (currentPosition.Side == "BUY" && signal == "SELL") ||
               (currentPosition.Side == "SELL" && signal == "BUY") {
                orderExecutor.PlaceOrder("BTCUSDT", currentPosition.Side == "BUY" ? "SELL" : "BUY", currentPosition.Quantity)
                currentPosition.Open = false
            }
        }
    })
}

7. Добавляем трейлинг-стоп (продвинутая версия)

Добавим простой трейлинг-стоп для защиты прибыли.


type Position struct {
    Open          bool
    Side          string
    Entry         float64
    Quantity      float64
    HighestPrice  float64  // для лонга
    LowestPrice   float64  // для шорта
    TrailingStop  float64  // процент для стопа (0.02 = 2%)
}

func (p *Position) UpdateTrailing(currentPrice float64) bool {
    if p.Side == "BUY" {
        if currentPrice > p.HighestPrice {
            p.HighestPrice = currentPrice
        }
        stopPrice := p.HighestPrice * (1 - p.TrailingStop)
        if currentPrice <= stopPrice {
            return true // стоп сработал
        }
    } else if p.Side == "SELL" {
        if currentPrice < p.LowestPrice {
            p.LowestPrice = currentPrice
        }
        stopPrice := p.LowestPrice * (1 + p.TrailingStop)
        if currentPrice >= stopPrice {
            return true
        }
    }
    return false
}

8. Сборка и запуск

Создаём файл .env с API-ключами

BINANCE_API_KEY=ваш_api_ключ
BINANCE_API_SECRET=ваш_секретный_ключ

Запуск в режиме симуляции (paper trading):


go run .

Сборка исполняемого файла (для запуска на сервере):


go build -o trading-bot .
./trading-bot
⚠️ Перед запуском на реальные деньги:
  • Протестируйте бота в режиме paper trading минимум 2-4 недели.
  • Убедитесь, что обработка ошибок и переподключение к WebSocket работают надёжно.
  • Добавьте логирование всех сделок в файл (например, с помощью zap или logrus).
  • Начните с минимальной суммы ($10-20).
  • Используйте стоп-лосс в каждой сделке.

9. Как улучшить бота (следующие шаги)

  • 🔹 Добавьте несколько индикаторов — RSI, MACD, Bollinger Bands для фильтрации.
  • 🔹 Внедрите мультивалютность — запустите горутины для BTC, ETH, SOL одновременно.
  • 🔹 Добавьте Telegram-уведомления — чтобы получать оповещения о сделках.
  • 🔹 Создайте веб-интерфейс для мониторинга (HTML + WebSocket).
  • 🔹 Перепишите модуль индикаторов на каналах — для потоковой обработки.
  • 🔹 Добавьте бэктестинг — читайте исторические данные из CSV и прогоняйте ту же стратегию.

Заключение: ваш Go-бот готов к бою

Вы создали каркас профессионального торгового бота на Go. Он получает котировки в реальном времени через WebSocket, рассчитывает индикаторы, генерирует сигналы и отправляет ордеры. Этот бот может работать неделями без перезагрузки, обрабатывая тысячи тиков в секунду. Go даёт вам производительность C++ и безопасность типов, не жертвуя читаемостью.

Конечно, бота нужно дорабатывать: добавлять обработку ошибок, логирование, стоп-лоссы, несколько торговых пар. Но фундамент заложен. И теперь вы можете развивать его в любую сторону.

Запустите бота в режиме симуляции на демо-счёте, понаблюдайте за его работой. Затем постепенно добавляйте функционал. И когда почувствуете уверенность — переходите на реальные деньги, начиная с минимальных сумм.

И помните: даже самый быстрый бот не спасёт от плохой стратегии. Но он даст вам инструмент, который позволит тестировать идеи быстрее, чем конкуренты. А в трейдинге скорость итераций — это половина успеха.

«Go — это язык, который позволяет вашему боту быть одновременно быстрым, как гепард, и надёжным, как швейцарские часы. А горутины делают параллельную обработку данных простой, как приготовление кофе».

 

Дата размещения статьи: 07-06-2026 в 12:50:04