Skip to content

linuskuehnle/ctraderopenapi

Repository files navigation

ctrader — Go client for cTrader OpenAPI

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

Quick highlights

  • 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.

Installation

This library uses Go modules. From your project directory you can add the dependency with:

go get github.com/linuskuehnle/ctraderopenapi@latest

Then 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@latest

Exported types & API summary

Key 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. Call WithConfig before Connect to adjust runtime buffers/timeouts.

Custom response types for execution requests

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 ProtoOAExecutionEvent arrives 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 ProtoOAErrorRes instead, 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's ProtoOAAccountDisconnectEvent confirmation 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 optional EventKeyData for 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, and ReconnectFailEvent using ListenToClientEvent. Previously subscribed API Events are automatically resubscribed before ReconnectSuccessEvent is 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 with CheckError().

  • 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.

Error types

Several error types provide fine-grained control over error handling:

  • ResponseError — wraps server-side errors returned in ProtoOAErrorRes. Contains ErrorCode, Description, and optional MaintenanceEndTimestamp (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:

    • APIEvent and ClientEvent — marker interfaces for event types.
    • CastToAPIEventType[T] and CastToClientEventType[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, or SWAP. Other execution types are handled internally as responses to order management requests and are not emitted as standalone API events.
    • SpawnAPIEventHandler and SpawnClientEventHandler — start small goroutines that forward typed events to your handler, eliminating the need for manual type assertions.
  • Fine-grained event filtering:

    • APIEventListenData and ClientEventListenData — data structures for configuring event listeners.
    • EventKeyData — optional parameter in APIEventListenData for 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.

Examples

  1. 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())
}
  1. 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())
}
  1. 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")
}
  1. 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)
}
  1. 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)
}
  1. 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:

  1. Create and connect the API client with application credentials
  2. Authenticate with a specific account using AuthenticateAccount(accountId, accessToken)
  3. Make account-specific requests or subscribe to events
  4. Optionally refresh access tokens with RefreshAccessToken() if tokens expire (see APIEvent ProtoOAAccountsTokenInvalidatedEvent)
  5. Logout with LogoutAccount() when finished with the account

Running tests

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 TestClientConnectDisconnect

License

This project is licensed under the Apache License 2.0 — see the LICENSE file for details.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages