go-etoro

Go clients for the eToro public API, generated from the specs eToro publishes.
| Directory |
Package |
API |
Generator |
rest/ |
etororest |
REST, https://public-api.etoro.com |
oapi-codegen |
ws/ |
etorows |
WebSocket, wss://ws.etoro.com/ws |
gen-asyncapi |
Everything in rest/ and ws/ except gen.go and the generator configs is generated. Don't edit it by hand, regenerate it.
Install
go get github.com/psyb0t/go-etoro
REST
Every endpoint in the spec has a typed method on ClientWithResponses. eToro authenticates with two headers, x-api-key and x-user-key, taken from the API key section of your account settings.
package main
import (
"context"
"net/http"
"os"
"github.com/psyb0t/go-etoro/rest"
)
func main() {
auth := etororest.WithRequestEditorFn(func(_ context.Context, req *http.Request) error {
req.Header.Set("x-api-key", os.Getenv("ETORO_API_KEY"))
req.Header.Set("x-user-key", os.Getenv("ETORO_USER_KEY"))
return nil
})
client, err := etororest.NewClientWithResponses("https://public-api.etoro.com", auth)
if err != nil {
panic(err)
}
res, err := client.GetBalancesWithResponse(context.Background())
if err != nil {
panic(err)
}
_ = res.JSON200
}
WebSocket
etorows is a typed client for the WebSocket API. It sends the authenticate, subscribe and unsubscribe requests, and decodes the instrument rate, candle and market status pushes, the order and position updates, and the operation responses. Every message is checked against eToro's schema both ways.
package main
import (
"context"
"crypto/rand"
"log/slog"
"os"
"github.com/psyb0t/gen-asyncapi/asyncapirt/gorillaws"
"github.com/psyb0t/go-etoro/ws"
)
type handler struct {
etorows.UnimplementedHandler
}
func (handler) HandleInstrumentRateMessage(
ctx context.Context,
envelope etorows.InstrumentRateEnvelope,
) error {
for _, item := range envelope.Messages {
rate, err := item.ContentValue()
if err != nil {
return err
}
slog.InfoContext(ctx, "rate", "topic", item.Topic, "ask", *rate.Ask)
}
return nil
}
func main() {
ctx := context.Background()
conn, err := gorillaws.Dial(ctx, etorows.ServerEtoroURL)
if err != nil {
panic(err)
}
client := etorows.NewClient(conn)
defer client.Close()
err = client.Authenticate(ctx, etorows.AuthenticateEnvelope{
ID: rand.Text(),
Operation: "Authenticate",
Data: etorows.AuthenticateData{
APIKey: os.Getenv("ETORO_API_KEY"),
UserKey: os.Getenv("ETORO_USER_KEY"),
},
})
if err != nil {
panic(err)
}
err = client.Subscribe(ctx, etorows.SubscribeEnvelope{
ID: rand.Text(),
Operation: "Subscribe",
Data: etorows.SubscribeData{Topics: []string{"instrument:1001"}},
})
if err != nil {
panic(err)
}
if err := client.Run(ctx, handler{}); err != nil {
panic(err)
}
}
Push frames are envelopes whose items carry a type and a JSON-encoded content string; ContentValue() decodes and checks it. Rate values arrive as strings on the wire ("1.25"), so the InstrumentRate fields are strings too.
Pushes are told apart by their type; both order update types go to HandleOrderUpdateMessage. An envelope with a type the spec doesn't list fails with asyncapirt.ErrUnknownMessage.
Regenerating
eToro's specs are not committed. make generate downloads the current OpenAPI and AsyncAPI documents into the gitignored api/ directory and regenerates both packages:
make generate
Each package's gen.go declares its own steps, so make generate is just go generate ./...:
rest/gen.go downloads openapi.json and runs oapi-codegen with rest/config.yaml.
ws/gen.go downloads asyncapi.json, runs scripts/match-wire-types.sh, and runs gen-asyncapi with ws/config.yaml. The match step turns the number fields of every schema whose description says its values arrive as JSON strings, such as InstrumentRate, into strings, so real frames decode.
Development
Every command runs in the Dockerfile.dev image. The host needs only Docker and Make.
make help # list targets
make dep # go mod tidy + vendor
make lint # shellcheck, shfmt, go fix, golangci-lint
make test # go test -race
make sec # gitleaks, govulncheck, semgrep
make generate # download the specs and regenerate
License
MIT. The generated code comes from eToro's published API specifications. The specifications themselves are not redistributed here.