Skip to content

✅ Stream Real-Time Symbol Ticks

Request: subscribe to real-time tick updates for one or more symbols. Receive Bid/Ask price updates through Go channels.

API Information:

  • Low-level API: MT5Account.OnSymbolTick(...) (from Go package github.com/MetaRPC/GoMT5/package/Helpers)
  • gRPC service: mt5_term_api.SubscriptionService
  • Proto definition: OnSymbolTick (defined in mt5-term-api-subscriptions.proto)

RPC

  • Service: mt5_term_api.SubscriptionService
  • Method: OnSymbolTick(OnSymbolTickRequest) → stream OnSymbolTickReply
  • Low‑level client (generated): SubscriptionServiceClient.OnSymbolTick(ctx, request, opts...)
package mt5

type MT5Account struct {
    // ...
}

// OnSymbolTick streams real-time tick data for a symbol.
// Returns two channels: data channel and error channel.
func (a *MT5Account) OnSymbolTick(
    ctx context.Context,
    req *pb.OnSymbolTickRequest,
) (<-chan *pb.OnSymbolTickData, <-chan error)

Request message:

OnSymbolTickRequest {
  repeated string SymbolNames = 1;  // Array of symbol names to stream
}

🔽 Input

Parameter Type Description
ctx context.Context Context for cancellation (cancel to stop stream)
req *pb.OnSymbolTickRequest Request with array of symbol names

⬆️ Output — Channels

Channel Type Description
Data Channel <-chan *pb.OnSymbolTickData Receives tick updates
Error Channel <-chan error Receives errors (closed on ctx cancel)

OnSymbolTickData fields:

Field Type Go Type Description
SymbolTick MrpcSubscriptionMqlTick *pb.MrpcSubscriptionMqlTick The tick (fields below)
TerminalInstanceGuidId string string Terminal instance that sent the tick

SymbolTick (MrpcSubscriptionMqlTick) fields — read them as tick.SymbolTick.Bid etc.:

Field Type Go Type Description
Symbol string string Symbol of this tick
Bid double float64 Current bid price
Ask double float64 Current ask price
Last double float64 Last deal price
Volume uint64 uint64 Volume
VolumeReal double float64 Volume with extended accuracy
Time google.protobuf.Timestamp *timestamppb.Timestamp Tick time (broker server time)
TimeUtc google.protobuf.Timestamp *timestamppb.Timestamp Tick time in UTC
TimeMsc int64 int64 Tick time in milliseconds (broker server time)
TimeMscUtc int64 int64 TimeMsc in UTC
Flags uint32 uint32 Tick flags (see MT5 documentation)

💬 Just the essentials

  • What it is. Real-time streaming of price updates via Go channels.
  • Why you need it. Monitor live prices, implement tick-based strategies, real-time dashboards.
  • Goroutines required. Must consume channels in goroutines to avoid blocking.

🎯 Purpose

Use it to:

  • Stream real-time tick data for symbols
  • Monitor live bid/ask prices and spreads
  • Implement tick-based trading strategies
  • Build real-time price dashboards
  • Track price movements and volatility
  • Set up price alerts and notifications

📚 Tutorial

For a detailed line-by-line explanation with examples, see: → OnSymbolTick - How it works


🧩 Notes & Tips

  • Automatic reconnection: Streaming methods restart the stream automatically on transient errors (Unavailable, terminal not found) via ExecuteStreamWithReconnect.
  • Default timeout: If context has no deadline, streams run indefinitely until cancelled.
  • Nil context: If you pass nil context, context.Background() is used automatically.
  • Channel buffering: Data channel is unbuffered, error channel is buffered (size 1).
  • Goroutine required: You MUST consume the channels in a separate goroutine to avoid blocking.
  • Context cancellation: Use context.WithCancel() or context.WithTimeout() to stop streaming.
  • Channel closure: Both channels close when context is cancelled or stream ends.
  • Symbol must exist: Use SymbolSelect to add symbol to Market Watch before streaming.

🔗 Usage Examples

1) Basic streaming with goroutine

package main

import (
    "github.com/google/uuid"
    "context"
    "fmt"
    "time"

    pb "github.com/MetaRPC/GoMT5/package"
    "github.com/MetaRPC/GoMT5/package/Helpers"
)

func main() {
    account, err := mt5.NewMT5Account(591129415, "password", "mt5.mrpc.pro:443", uuid.Nil)
    if err != nil {
        panic(err)
    }
    defer account.Close()

    // Connect first
    _, err = account.ConnectEx(context.Background(), &pb.ConnectExRequest{User: 591129415, Password: "password", MtClusterName: "MetaQuotes-Demo"})
    if err != nil {
        panic(err)
    }

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    dataChan, errChan := account.OnSymbolTick(ctx, &pb.OnSymbolTickRequest{
        SymbolNames: []string{"EURUSD"},
    })

    // Process in goroutine
    go func() {
        for {
            select {
            case tick := <-dataChan:
                if tick == nil {
                    return
                }
                fmt.Printf("[%s] EURUSD: Bid=%.5f, Ask=%.5f, Spread=%.5f\n",
                    time.Now().Format("15:04:05"),
                    tick.SymbolTick.Bid, tick.SymbolTick.Ask, tick.SymbolTick.Ask-tick.SymbolTick.Bid)

            case err := <-errChan:
                if err != nil {
                    fmt.Printf("Stream error: %v\n", err)
                    return
                }
            }
        }
    }()

    // Keep main running
    <-ctx.Done()
    fmt.Println("Stream stopped")
}

2) Streaming with context cancellation

func StreamWithCancellation(account *mt5.MT5Account) {
    ctx, cancel := context.WithCancel(context.Background())

    dataChan, errChan := account.OnSymbolTick(ctx, &pb.OnSymbolTickRequest{
        SymbolNames: []string{"EURUSD"},
    })

    // Start consuming
    go func() {
        for {
            select {
            case tick := <-dataChan:
                if tick == nil {
                    fmt.Println("Data channel closed")
                    return
                }
                fmt.Printf("Tick: Bid=%.5f, Ask=%.5f\n", tick.SymbolTick.Bid, tick.SymbolTick.Ask)

            case err := <-errChan:
                if err != nil {
                    fmt.Printf("Error: %v\n", err)
                    return
                }

            case <-ctx.Done():
                fmt.Println("Context cancelled")
                return
            }
        }
    }()

    // Cancel after 10 seconds
    time.Sleep(10 * time.Second)
    cancel()
    fmt.Println("Cancellation triggered")
}

3) Multiple symbol streaming

func StreamMultipleSymbols(account *mt5.MT5Account, symbols []string) {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    for _, symbol := range symbols {
        dataChan, errChan := account.OnSymbolTick(ctx, &pb.OnSymbolTickRequest{
            SymbolNames: []string{symbol},
        })

        // Launch goroutine for each symbol
        go func(sym string, data <-chan *pb.OnSymbolTickData, errs <-chan error) {
            for {
                select {
                case tick := <-data:
                    if tick == nil {
                        return
                    }
                    fmt.Printf("[%s] Bid=%.5f, Ask=%.5f\n", sym, tick.SymbolTick.Bid, tick.SymbolTick.Ask)

                case err := <-errs:
                    if err != nil {
                        fmt.Printf("[%s] Error: %v\n", sym, err)
                        return
                    }

                case <-ctx.Done():
                    return
                }
            }
        }(symbol, dataChan, errChan)
    }

    // Run for 30 seconds
    time.Sleep(30 * time.Second)
}

// Usage:
// StreamMultipleSymbols(account, []string{"EURUSD", "GBPUSD", "USDJPY"})

4) Tick aggregation and statistics

type TickStats struct {
    Symbol    string
    Count     int64
    LastBid   float64
    LastAsk   float64
    MinBid    float64
    MaxBid    float64
}

func AggregateTickStats(account *mt5.MT5Account, symbol string, duration time.Duration) {
    ctx, cancel := context.WithTimeout(context.Background(), duration)
    defer cancel()

    stats := &TickStats{
        Symbol: symbol,
        MinBid: 999999.0,
    }

    dataChan, errChan := account.OnSymbolTick(ctx, &pb.OnSymbolTickRequest{
        SymbolNames: []string{symbol},
    })

    for {
        select {
        case tick := <-dataChan:
            if tick == nil {
                break
            }
            stats.Count++
            stats.LastBid = tick.SymbolTick.Bid
            stats.LastAsk = tick.SymbolTick.Ask

            if tick.SymbolTick.Bid < stats.MinBid {
                stats.MinBid = tick.SymbolTick.Bid
            }
            if tick.SymbolTick.Bid > stats.MaxBid {
                stats.MaxBid = tick.SymbolTick.Bid
            }

        case err := <-errChan:
            if err != nil {
                fmt.Printf("Error: %v\n", err)
            }

        case <-ctx.Done():
            fmt.Printf("\n%s Statistics:\n", stats.Symbol)
            fmt.Printf("  Ticks received: %d\n", stats.Count)
            fmt.Printf("  Last Bid/Ask: %.5f / %.5f\n", stats.LastBid, stats.LastAsk)
            fmt.Printf("  Bid range: %.5f - %.5f\n", stats.MinBid, stats.MaxBid)
            return
        }
    }
}

// Usage:
// AggregateTickStats(account, "EURUSD", 60*time.Second)

5) Price alert system

func PriceAlertMonitor(account *mt5.MT5Account, symbol string, targetPrice float64, above bool) {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    dataChan, errChan := account.OnSymbolTick(ctx, &pb.OnSymbolTickRequest{
        SymbolNames: []string{symbol},
    })

    fmt.Printf("Monitoring %s for price %s %.5f\n",
        symbol,
        map[bool]string{true: "above", false: "below"}[above],
        targetPrice)

    go func() {
        for {
            select {
            case tick := <-dataChan:
                if tick == nil {
                    return
                }

                triggered := false
                if above && tick.SymbolTick.Ask >= targetPrice {
                    triggered = true
                } else if !above && tick.SymbolTick.Bid <= targetPrice {
                    triggered = true
                }

                if triggered {
                    fmt.Printf("\n🔔 ALERT: %s price reached %.5f (Bid=%.5f, Ask=%.5f)\n",
                        symbol, targetPrice, tick.SymbolTick.Bid, tick.SymbolTick.Ask)
                    cancel() // Stop monitoring
                    return
                }

            case err := <-errChan:
                if err != nil {
                    fmt.Printf("Error: %v\n", err)
                    return
                }

            case <-ctx.Done():
                return
            }
        }
    }()

    <-ctx.Done()
}

// Usage:
// PriceAlertMonitor(account, "EURUSD", 1.11000, true) // Alert when above 1.11000

6) Real-time spread monitoring

func MonitorSpread(account *mt5.MT5Account, symbol string, maxSpreadPips float64) {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    dataChan, errChan := account.OnSymbolTick(ctx, &pb.OnSymbolTickRequest{
        SymbolNames: []string{symbol},
    })

    go func() {
        for {
            select {
            case tick := <-dataChan:
                if tick == nil {
                    return
                }

                spread := tick.SymbolTick.Ask - tick.SymbolTick.Bid
                spreadPips := spread * 10000 // For 5-digit quotes

                if spreadPips > maxSpreadPips {
                    fmt.Printf("⚠️  High spread alert: %.1f pips (Bid=%.5f, Ask=%.5f)\n",
                        spreadPips, tick.SymbolTick.Bid, tick.SymbolTick.Ask)
                } else {
                    fmt.Printf("[%s] Spread: %.1f pips\n",
                        time.Now().Format("15:04:05"), spreadPips)
                }

            case err := <-errChan:
                if err != nil {
                    fmt.Printf("Error: %v\n", err)
                    return
                }

            case <-ctx.Done():
                return
            }
        }
    }()

    time.Sleep(60 * time.Second)
    cancel()
}

// Usage:
// MonitorSpread(account, "EURUSD", 2.0) // Alert if spread > 2 pips

🔧 Common Patterns

Stream with graceful shutdown

func StreamWithShutdown(account *mt5.MT5Account, symbol string) {
    ctx, cancel := context.WithCancel(context.Background())

    dataChan, errChan := account.OnSymbolTick(ctx, &pb.OnSymbolTickRequest{
        SymbolNames: []string{symbol},
    })

    done := make(chan struct{})

    go func() {
        defer close(done)
        for {
            select {
            case tick := <-dataChan:
                if tick == nil {
                    return
                }
                // Process tick...

            case err := <-errChan:
                if err != nil {
                    return
                }

            case <-ctx.Done():
                fmt.Println("Shutting down gracefully...")
                return
            }
        }
    }()

    // Wait for signal (Ctrl+C, etc)
    // ...then cancel
    cancel()
    <-done // Wait for goroutine to finish
}

📚 See Also