This repository provides a compact, idiomatic Go client for the cTrader OpenAPI proxy. It focuses on a small, testable public surface for:
- sending RPC-like requests (request/response mapped to protobuf messages),
- subscribing to and listening for streaming server events (push-style),
- a concurrency-safe event dispatcher and small adapters to wire typed handlers without reflection.
- an client-side request rate limiter to prevent server-side rate limiting.
Contents
- Quick highlights
- Installation (Go modules)
- Exported types & API summary
- Examples (connect/request, subscribe + listen, client events)
- Running tests
- License
- Typed, protobuf-backed request/response helpers.
- Event handler for both API and client events providing easy abstraction via subscribe/unsubscribe and listen functions.
- Client-side rate limiter to avoid server-side rate limiting.
- Small typed adapters (
SpawnAPIEventHandler,SpawnClientEventHandler) that adapt generic channels to typed callbacks (they perform runtime type assertions and will drop non-matching events). - Connection loss handling; recovering authenticated accounts and active subscriptions on reconnect.
This library uses Go modules. From your project directory you can add the dependency with:
go get github.com/linuskuehnle/ctraderopenapi@latestThen import the package in your code:
import "github.com/linuskuehnle/ctraderopenapi"To install a binary (if provided by this repository) or to run tooling:
go install github.com/linuskuehnle/ctraderopenapi@latestKey public types and functions (see types.go and api_client.go for
full comments and examples):
NewAPIClient(cred ApplicationCredentials, env Environment) (APIClient, error)— create a new client instance. CallWithConfigbeforeConnectto adjust runtime buffers/timeouts.
Certain requests (ProtoOANewOrderReq, ProtoOACancelOrderReq, ProtoOAAmendOrderReq,
ProtoOAAmendPositionSLTPReq, ProtoOAClosePositionReq) do not have explicit response
types defined in the cTrader OpenAPI specification. Instead, they are answered asynchronously via
ProtoOAExecutionEvent. This client library transparently maps these events back to their
originating requests, providing a synchronous request/response interface.
How it works:
- When you send a request with no explicit response type, you provide a custom response type
that is type-aliased to
ProtoOAExecutionEvent(e.g.,ProtoOANewOrderRes = ProtoOAExecutionEvent). - The client internally assigns each request a unique ID and tracks pending requests, allowing multiple concurrent requests with the same account ID to be properly correlated.
- When a
ProtoOAExecutionEventarrives from the server, the client matches it against pending requests to determine which request it corresponds to, then returns the event as the synchronized response. - If an error occurs, the server may respond with
ProtoOAErrorResinstead, which the client properly handles before attempting to match execution events.
Why map asynchronous events to synchronous requests:
The cTrader OpenAPI does not provide dedicated response types for order management operations,
instead returning generic ProtoOAErrorRes on errors or ProtoOAExecutionEvent on success.
This inconsistency (error uses one type, success uses another) makes error handling and response
correlation ambiguous. By internally managing this mapping and exposing a synchronous interface,
callers benefit from intuitive error handling and clear request/response pairing without needing
to subscribe to execution events or manually match incoming events to outgoing requests.
-
APIClient— main interface. Important methods:Connection management:
Connect() error/Disconnect()— establish or close the connection
Account authentication:
AuthenticateAccount(CtraderAccountId, AccessToken) (*ProtoOAAccountAuthRes, error)— authenticate with a specific cTrader account using an access token. Must be called before making account-specific requests or subscribing to account events.LogoutAccount(CtraderAccountId, bool) (*ProtoOAAccountLogoutRes, error)— logout from a cTrader account. The boolean parameter controls whether to wait for the server'sProtoOAAccountDisconnectEventconfirmation before returning.RefreshAccessToken(AccessToken, RefreshToken) (*ProtoOARefreshTokenRes, error)— refresh an expired access token using a refresh token, returns a new access token.
Request/Response:
SendRequest(RequestData) error— sends a protobuf-typed request and unmarshals the response into the provided response object.
Event subscriptions:
SubscribeAPIEvent(APIEventSubData)/UnsubscribeAPIEvent(...)— subscribe/unsubscribe for server-side subscription-based events.SubscribeClientEvent(SubscribableClientEventData)/UnsubscribeClientEvent(...)— subscribe/unsubscribe for client-side events.
Event listening:
ListenToAPIEvent(ctx, APIEventListenData)— register a long-running listener channel for push events. APIEventListenData includes optionalEventKeyDatafor fine-grained filtering (e.g., by account ID and symbol ID).ListenToClientEvent(ctx, ClientEventListenData)— listen for client-side events (connection loss, reconnect events, fatal errors).
Client event types: Fatal client errors, connection loss, reconnect success, and reconnect fail.
Error handling: Fatal (non-recoverable) client errors are emitted as
FatalErrorEvent. If a listener channel is registered, the error is sent to the channel and the client recovers by reconnecting. Without a listener, a fatal error raises a panic.Reconnection: The API Client automatically handles connection losses. Register listeners for
ConnectionLossEvent,ReconnectSuccessEvent, andReconnectFailEventusingListenToClientEvent. Previously subscribed API Events are automatically resubscribed beforeReconnectSuccessEventis emitted.Runtime configuration:
WithQueueBufferSize(int)updates the number of queued requests that may be buffered by the internal request queue before backpressure applies.WithTCPMessageBufferSize(int)updates the size of the channel used to receive inbound TCP messages from the network reader.WithRequestContextManagerIterationTimeout(time.Duration)updates interval used by the request context manager to periodically check for expired request contexts.WithRequestTimeout(time.Duration)updates the duration until a request roundtrip is aborted no matter if already sent or not.DisableConcurrentEventEmits()disables concurrent event emit; events are passed sequentially to event channels instead of being spawned in separate goroutines. This reduces overhead but may block the client if an event channel buffer is full.DisableDefaultRateLimiter()to disable the client-side rate limiter.
Rate limiting: The client enforces rate limits to prevent server-side throttling:
- 50 live requests per second (ProtoOALiveView subscriptions)
- 5 historical requests per second (ProtoOAHistoricalData requests)
If the server-side rate limit is still exceeded due to network divergence, the request is automatically re-enqueued, so callers do not need to handle rate limit errors.
-
ApplicationCredentials{ ClientId, ClientSecret }— credentials used by the application to authenticate with the OpenAPI. Validate withCheckError(). -
CtraderAccountId— thin typed alias over int64 for explicit account ID references. -
AccessToken— thin typed alias over string for access tokens obtained after authenticating an account. -
RefreshToken— thin typed alias over string for refresh tokens used to obtain new access tokens.
Several error types provide fine-grained control over error handling:
-
ResponseError— wraps server-side errors returned inProtoOAErrorRes. ContainsErrorCode,Description, and optionalMaintenanceEndTimestamp(if the server is undergoing maintenance). -
RequestContextManagerNodeNotIncludedError— internal error indicating a request context was not found. Typically not used by callers. -
UnexpectedMessageTypeError— raised when the server sends a message with an unexpected payload type. Includes the numeric message type for debugging. -
ProtoUnmarshalError— wraps errors during protobuf unmarshalling, including the context (e.g., "proto OA execution event") where the error occurred. -
Event helpers and adapters:
APIEventandClientEvent— marker interfaces for event types.CastToAPIEventType[T]andCastToClientEventType[T]— helpers to cast generic event types to concrete event types.ProtoOAExecutionEvent— accessible to listen to for specific ExecutionType values:DEPOSIT_WITHDRAW,BONUS_DEPOSIT_WITHDRAW, orSWAP. Other execution types are handled internally as responses to order management requests and are not emitted as standalone API events.SpawnAPIEventHandlerandSpawnClientEventHandler— start small goroutines that forward typed events to your handler, eliminating the need for manual type assertions.
-
Fine-grained event filtering:
APIEventListenDataandClientEventListenData— data structures for configuring event listeners.EventKeyData— optional parameter inAPIEventListenDatafor filtering events by subscription-specific criteria (e.g., account ID and symbol ID for spot events). When nil, all events of the specified type are delivered.KeyDataSpotEvent,KeyDataDepthEvent,KeyDataTrailingSLChangedEvent, etc. — concrete key data types for filtering specific event subtypes.
- Basic connect and simple request
package main
import (
"context"
"fmt"
"github.com/linuskuehnle/ctraderopenapi"
)
func main() {
cred := ctraderopenapi.ApplicationCredentials{ClientId: "id", ClientSecret: "secret"}
client, err := ctraderopenapi.NewAPIClient(cred, ctraderopenapi.Environment_Demo)
if err != nil {
panic(err)
}
if err := client.Connect(); err != nil {
panic(err)
}
defer client.Disconnect()
req := ctraderopenapi.ProtoOAVersionReq{}
var res ctraderopenapi.ProtoOAVersionRes
reqData := ctraderopenapi.RequestData{
Ctx: context.Background(),
Req: &req,
Res: &res,
}
if err := client.SendRequest(reqData); err != nil {
fmt.Println("request failed:", err)
return
}
fmt.Println("version:", res.GetVersion())
}- Sending an order with custom response type (no explicit response)
package main
import (
"context"
"fmt"
"github.com/linuskuehnle/ctraderopenapi"
)
func main() {
cred := ctraderopenapi.ApplicationCredentials{ClientId: "id", ClientSecret: "secret"}
client, err := ctraderopenapi.NewAPIClient(cred, ctraderopenapi.Environment_Demo)
if err != nil {
panic(err)
}
if err := client.Connect(); err != nil {
panic(err)
}
defer client.Disconnect()
// Authenticate with an account first
accountId := ctraderopenapi.CtraderAccountId(123456)
accessToken := ctraderopenapi.AccessToken("your-access-token")
if _, err := client.AuthenticateAccount(accountId, accessToken); err != nil {
panic(err)
}
// Send a new order - response is an execution event internally mapped to the request
orderReq := ctraderopenapi.ProtoOANewOrderReq{
// ... configure order details
}
var orderRes ctraderopenapi.ProtoOANewOrderRes
reqData := ctraderopenapi.RequestData{
Ctx: context.Background(),
Req: &orderReq,
Res: &orderRes, // This is actually a ProtoOAExecutionEvent (custom response type)
}
if err := client.SendRequest(reqData); err != nil {
fmt.Println("order request failed:", err)
return
}
fmt.Println("order execution event:", orderRes.GetExecutionType())
}- Account authentication and logout
package main
import (
"fmt"
"github.com/linuskuehnle/ctraderopenapi"
)
func main() {
cred := ctraderopenapi.ApplicationCredentials{ClientId: "id", ClientSecret: "secret"}
client, err := ctraderopenapi.NewAPIClient(cred, ctraderopenapi.Environment_Demo)
if err != nil {
panic(err)
}
if err := client.Connect(); err != nil {
panic(err)
}
defer client.Disconnect()
// Authenticate with a cTrader account
accountId := ctraderopenapi.CtraderAccountId(123456)
accessToken := ctraderopenapi.AccessToken("your-access-token")
authRes, err := client.AuthenticateAccount(accountId, accessToken)
if err != nil {
fmt.Println("authentication failed:", err)
return
}
fmt.Println("authenticated successfully:", authRes)
// Use the authenticated account to make requests or subscribe to events...
// Logout from the account (waitForConfirm=true waits for server confirmation)
if _, err := client.LogoutAccount(accountId, true); err != nil {
fmt.Println("logout failed:", err)
return
}
fmt.Println("logged out successfully")
}- Token refresh
package main
import (
"fmt"
"github.com/linuskuehnle/ctraderopenapi"
)
func main() {
cred := ctraderopenapi.ApplicationCredentials{ClientId: "id", ClientSecret: "secret"}
client, err := ctraderopenapi.NewAPIClient(cred, ctraderopenapi.Environment_Demo)
if err != nil {
panic(err)
}
if err := client.Connect(); err != nil {
panic(err)
}
defer client.Disconnect()
// Refresh an expired access token
expiredToken := ctraderopenapi.AccessToken("expired-token")
refreshToken := ctraderopenapi.RefreshToken("refresh-token")
refreshRes, err := client.RefreshAccessToken(expiredToken, refreshToken)
if err != nil {
fmt.Println("token refresh failed:", err)
return
}
// Use the new access token for subsequent requests
newAccessToken := ctraderopenapi.AccessToken(refreshRes.GetAccessToken())
fmt.Println("new access token:", newAccessToken)
}- Subscribe to spot events and handle them with the typed adapter
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
onEventCh := make(chan ctraderopenapi.APIEvent)
listenData := ctraderopenapi.APIEventListenData{
EventType: ctraderopenapi.APIEventType_Spots,
EventCh: onEventCh,
EventKeyData: &ctraderopenapi.KeyDataSpotEvent{
Ctid: ctraderopenapi.CtraderAccountId(123456),
SymbolId: 1,
Period: ctraderopenapi.ProtoOATrendbarPeriod_M1,
},
}
if err := client.ListenToAPIEvent(ctx, listenData); err != nil {
panic(err)
}
_ = ctraderopenapi.SpawnAPIEventHandler(ctx, onEventCh, func(e *ctraderopenapi.ProtoOASpotEvent) {
fmt.Println("spot event:", e)
})
sub := ctraderopenapi.APIEventSubData{
EventType: ctraderopenapi.EventType_Spots,
SubcriptionData: &ctraderopenapi.SpotEventData{
CtraderAccountId: ctraderopenapi.CtraderAccountId(123456),
SymbolIds: []int64{1, 2, 3},
},
}
if err := client.SubscribeAPIEvent(sub); err != nil {
panic(err)
}- Listen for client events; e.g. ReconnectSuccessEvent:
clientCh := make(chan ctraderopenapi.ClientEvent)
listenData := ctraderopenapi.ClientEventListenData{
EventType: ctraderopenapi.ClientEventType_ReconnectSuccessEvent,
EventCh: clientCh,
}
if err := client.ListenToClientEvent(context.Background(), listenData); err != nil {
panic(err)
}
ctraderopenapi.SpawnClientEventHandler(context.Background(), clientCh, func(e *ctraderopenapi.ReconnectSuccessEvent) {
fmt.Println("reconnected")
})I suggest registering all event listener channels before calling apiClient.Connect().
apiClient.Disconnect() unregisters all event listener channels; if you want to
reconnect and use the same listeners, explicitly register them again before the next Connect() call.
Authentication workflow:
- Create and connect the API client with application credentials
- Authenticate with a specific account using
AuthenticateAccount(accountId, accessToken) - Make account-specific requests or subscribe to events
- Optionally refresh access tokens with
RefreshAccessToken()if tokens expire (see APIEventProtoOAAccountsTokenInvalidatedEvent) - Logout with
LogoutAccount()when finished with the account
Some tests exercise live interactions and expect credentials via environment
variables or a .env file (see stubs_test.go). To run unit/integration
tests locally:
go test ./...To run a focused test and vet the code:
go vet ./...
go test ./... -run TestClientConnectDisconnectThis project is licensed under the Apache License 2.0 — see the LICENSE file for details.