diff --git a/Cargo.toml b/Cargo.toml index 6856e0e2a..58bcef197 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -137,6 +137,7 @@ members = [ "tests/load_tests", "foxhunt-deploy", "web-gateway", + "ctrader-openapi", ] exclude = [ "performance-tests", @@ -391,6 +392,7 @@ storage = { path = "storage" } market-data = { path = "market-data" } config = { path = "config" } database = { path = "database" } +ctrader-openapi = { path = "ctrader-openapi" } [features] default = [] diff --git a/ctrader-openapi/Cargo.toml b/ctrader-openapi/Cargo.toml new file mode 100644 index 000000000..6aa7a175a --- /dev/null +++ b/ctrader-openapi/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "ctrader-openapi" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +publish.workspace = true +description = "cTrader Open API client — Protobuf over TCP+TLS for order routing, account queries, and market data" + +[dependencies] +tokio = { workspace = true, features = ["net", "sync", "time", "rt", "macros"] } +tokio-native-tls.workspace = true +native-tls = { version = "0.2", features = ["vendored"] } +tokio-util = { workspace = true, features = ["codec"] } +prost.workspace = true +bytes.workspace = true +reqwest = { workspace = true, features = ["json"] } +tracing.workspace = true +thiserror.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +chrono.workspace = true +uuid.workspace = true + +[build-dependencies] +prost-build.workspace = true + +[dev-dependencies] +tokio = { workspace = true, features = ["test-util"] } + +[lints] +workspace = true diff --git a/ctrader-openapi/src/config.rs b/ctrader-openapi/src/config.rs new file mode 100644 index 000000000..30d3b187e --- /dev/null +++ b/ctrader-openapi/src/config.rs @@ -0,0 +1,76 @@ +//! cTrader Open API configuration. + +use serde::{Deserialize, Serialize}; + +/// Target environment (demo vs. live). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum CTraderEnvironment { + /// `demo.ctraderapi.com:5035` + Demo, + /// `live.ctraderapi.com:5035` + Live, +} + +impl CTraderEnvironment { + /// TCP host for the environment. + pub const fn host(&self) -> &'static str { + match self { + Self::Demo => "demo.ctraderapi.com", + Self::Live => "live.ctraderapi.com", + } + } + + /// TCP port (both envs use 5035). + pub const fn port(&self) -> u16 { + 5035 + } +} + +impl Default for CTraderEnvironment { + fn default() -> Self { + Self::Demo + } +} + +/// Configuration for a cTrader Open API connection. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CTraderConfig { + /// OAuth2 application client ID. + pub client_id: String, + + /// OAuth2 application client secret. + pub client_secret: String, + + /// OAuth2 access token (obtained via auth code or refresh flow). + pub access_token: String, + + /// cTID trader account ID. + pub account_id: i64, + + /// Demo or Live environment. + #[serde(default)] + pub environment: CTraderEnvironment, + + /// Heartbeat interval in seconds (cTrader requires ≤ 10s). + #[serde(default = "default_heartbeat_interval")] + pub heartbeat_interval_secs: u64, + + /// Per-request timeout in milliseconds. + #[serde(default = "default_request_timeout")] + pub request_timeout_ms: u64, + + /// Maximum reconnection attempts before giving up. + #[serde(default = "default_max_reconnect")] + pub max_reconnect_attempts: u32, +} + +const fn default_heartbeat_interval() -> u64 { + 10 +} +const fn default_request_timeout() -> u64 { + 5000 +} +const fn default_max_reconnect() -> u32 { + 5 +} diff --git a/ctrader-openapi/src/error.rs b/ctrader-openapi/src/error.rs new file mode 100644 index 000000000..0a5a47c0d --- /dev/null +++ b/ctrader-openapi/src/error.rs @@ -0,0 +1,84 @@ +//! cTrader Open API error types. + +use std::fmt; + +/// Errors from the cTrader Open API client. +#[derive(Debug, thiserror::Error)] +pub enum CTraderError { + /// TCP or TLS connection failed. + #[error("connection failed: {0}")] + ConnectionFailed(String), + + /// TLS handshake error. + #[error("TLS error: {0}")] + TlsError(String), + + /// OAuth2 token exchange failed. + #[error("OAuth error: {0}")] + OAuthError(String), + + /// Application-level authentication rejected. + #[error("application auth failed: {0}")] + AppAuthFailed(String), + + /// Account-level authentication rejected. + #[error("account auth failed: {0}")] + AccountAuthFailed(String), + + /// Protobuf encode/decode error. + #[error("protocol error: {0}")] + ProtocolError(String), + + /// Codec framing error. + #[error("codec error: {0}")] + CodecError(String), + + /// Rate limit exceeded (50 req/s or 5 req/s for historical). + #[error("rate limit exceeded for {bucket}")] + RateLimitExceeded { + /// Which bucket was exhausted. + bucket: RateLimitBucket, + }, + + /// Request timed out waiting for a response. + #[error("request timed out after {0:?}")] + Timeout(std::time::Duration), + + /// Server returned an error payload. + #[error("server error {code}: {description}")] + ServerError { + /// cTrader error code. + code: String, + /// Human-readable description. + description: String, + }, + + /// Client is not connected. + #[error("not connected")] + NotConnected, + + /// Symbol not found in the cached symbol list. + #[error("unknown symbol: {0}")] + UnknownSymbol(String), +} + +/// Rate-limit bucket categories. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RateLimitBucket { + /// Standard messages: 50 per second. + NonHistorical, + /// Historical data requests: 5 per second. + Historical, +} + +impl fmt::Display for RateLimitBucket { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NonHistorical => f.write_str("non-historical (50/s)"), + Self::Historical => f.write_str("historical (5/s)"), + } + } +} + +/// Convenience alias used throughout the crate. +pub type Result = std::result::Result; diff --git a/ctrader-openapi/src/lib.rs b/ctrader-openapi/src/lib.rs new file mode 100644 index 000000000..a5f0d3cf9 --- /dev/null +++ b/ctrader-openapi/src/lib.rs @@ -0,0 +1,25 @@ +//! cTrader Open API client for Rust. +//! +//! Provides a production-grade async client for the cTrader Open API protocol +//! (Protobuf over TCP+TLS), targeting ICMarkets broker connectivity. +//! +//! # Architecture +//! +//! - **config** — connection configuration (environment, credentials, timeouts) +//! - **error** — error types for all failure modes +//! - **proto** — compiled protobuf message types +//! - **codec** — length-delimited protobuf wire codec +//! - **connection** — TCP+TLS transport with heartbeat +//! - **auth** — OAuth2 token exchange + protobuf auth sequence +//! - **dispatch** — request/response correlation and event broadcasting +//! - **rate_limiter** — token-bucket rate limiter (50/s + 5/s historical) +//! - **orders** — order request builders and type conversions +//! - **symbols** — symbol name → ID resolution cache +//! - **account** — account info and position reconciliation +//! - **client** — high-level `CTraderClient` API + +pub mod config; +pub mod error; + +pub use config::{CTraderConfig, CTraderEnvironment}; +pub use error::{CTraderError, Result};