# Broker Gateway Service API Reference **Version**: 0.1.0 **Protocol**: gRPC (Protocol Buffers v3) **Base URL**: `grpc://localhost:50056` ## Table of Contents - [Overview](#overview) - [Authentication](#authentication) - [Service Methods](#service-methods) - [RouteOrder](#routeorder) - [CancelOrder](#cancelorder) - [GetAccountState](#getaccountstate) - [GetPositions](#getpositions) - [GetSessionStatus](#getsessionstatus) - [StreamExecutions](#streamexecutions) - [HealthCheck](#healthcheck) - [Data Types](#data-types) - [Error Codes](#error-codes) - [Rate Limits](#rate-limits) - [Client Examples](#client-examples) --- ## Overview The Broker Gateway Service provides a gRPC API for order routing, execution management, and account state queries. All methods use Protocol Buffers for serialization. ### Protocol Definition The complete protobuf definition is available at `proto/broker_gateway.proto`. ### Connection ```rust use tonic::transport::Channel; use broker_gateway::broker_gateway_service_client::BrokerGatewayServiceClient; // Connect to service let channel = Channel::from_static("http://localhost:50056") .connect() .await?; let mut client = BrokerGatewayServiceClient::new(channel); ``` --- ## Authentication **MVP**: No authentication (internal service) **Phase 2**: mTLS client certificates + API keys ```rust // Phase 2: TLS with client cert use tonic::transport::{Certificate, ClientTlsConfig, Identity}; let cert = std::fs::read("client-cert.pem")?; let key = std::fs::read("client-key.pem")?; let identity = Identity::from_pem(cert, key); let ca_cert = std::fs::read("ca-cert.pem")?; let ca = Certificate::from_pem(ca_cert); let tls = ClientTlsConfig::new() .identity(identity) .ca_certificate(ca) .domain_name("broker-gateway.foxhunt.local"); let channel = Channel::from_static("https://broker-gateway:50056") .tls_config(tls)? .connect() .await?; let mut client = BrokerGatewayServiceClient::new(channel); ``` --- ## Service Methods ### RouteOrder Submit a new order to the broker. #### Request ```protobuf message RouteOrderRequest { string symbol = 1; // ES, NQ, YM, RTY, etc. OrderSide side = 2; // BUY or SELL double quantity = 3; // Number of contracts (must be > 0) OrderType order_type = 4; // MARKET, LIMIT, STOP, STOP_LIMIT optional double price = 5; // Required for LIMIT orders optional double stop_price = 6; // Required for STOP orders string account_id = 7; // AMP account identifier map metadata = 8; // Optional metadata (strategy, model_name) } ``` #### Response ```protobuf message RouteOrderResponse { string broker_order_id = 1; // Broker's OrderID (filled after ack) string client_order_id = 2; // Our ClOrdID (UUID) OrderStatus status = 3; // PENDING_SUBMIT, SUBMITTED, etc. int64 submitted_at = 4; // Timestamp (nanoseconds) string message = 5; // Success/error message } ``` #### Example: Market Order ```rust use broker_gateway::*; let request = RouteOrderRequest { symbol: "ES".to_string(), side: OrderSide::Buy as i32, quantity: 10.0, order_type: OrderType::Market as i32, price: None, stop_price: None, account_id: "ACCT_001".to_string(), metadata: [ ("strategy".to_string(), "momentum".to_string()), ("model".to_string(), "dqn_v2".to_string()), ] .iter() .cloned() .collect(), }; let response = client.route_order(request).await?; let order = response.into_inner(); println!("Order submitted:"); println!(" Client Order ID: {}", order.client_order_id); println!(" Status: {:?}", OrderStatus::try_from(order.status)?); println!(" Submitted At: {}", order.submitted_at); // Output: // Order submitted: // Client Order ID: 550e8400-e29b-41d4-a716-446655440000 // Status: PENDING_SUBMIT // Submitted At: 1704812400000000000 ``` #### Example: Limit Order ```rust let request = RouteOrderRequest { symbol: "NQ".to_string(), side: OrderSide::Sell as i32, quantity: 5.0, order_type: OrderType::Limit as i32, price: Some(18500.50), // Required for LIMIT stop_price: None, account_id: "ACCT_001".to_string(), metadata: HashMap::new(), }; let response = client.route_order(request).await?; ``` #### Example: Stop-Limit Order ```rust let request = RouteOrderRequest { symbol: "ES".to_string(), side: OrderSide::Buy as i32, quantity: 3.0, order_type: OrderType::StopLimit as i32, price: Some(5805.00), // Limit price (buy at 5805 after stop triggered) stop_price: Some(5800.00), // Stop price (trigger at 5800) account_id: "ACCT_001".to_string(), metadata: HashMap::new(), }; let response = client.route_order(request).await?; ``` #### Validation Rules | Field | Validation | |-------|------------| | `symbol` | Non-empty, uppercase, valid futures contract | | `quantity` | Must be > 0 | | `price` | Required if `order_type = LIMIT` or `STOP_LIMIT` | | `stop_price` | Required if `order_type = STOP` or `STOP_LIMIT` | | `account_id` | Non-empty | #### Error Codes | Code | Description | |------|-------------| | `INVALID_ARGUMENT` | Missing required field or invalid value | | `FAILED_PRECONDITION` | FIX session not active (Phase 2) | | `RESOURCE_EXHAUSTED` | Rate limit exceeded | | `INTERNAL` | Database error or unexpected failure | --- ### CancelOrder Cancel an existing order. #### Request ```protobuf message CancelOrderRequest { string client_order_id = 1; // Order to cancel (required) string account_id = 2; // Account verification (required) } ``` #### Response ```protobuf message CancelOrderResponse { bool success = 1; // True if cancel request accepted string message = 2; // Confirmation message or error OrderStatus new_status = 3; // CANCEL_PENDING or CANCELLED } ``` #### Example ```rust let request = CancelOrderRequest { client_order_id: "550e8400-e29b-41d4-a716-446655440000".to_string(), account_id: "ACCT_001".to_string(), }; let response = client.cancel_order(request).await?; let cancel = response.into_inner(); if cancel.success { println!("Cancel request accepted: {}", cancel.message); println!("New status: {:?}", OrderStatus::try_from(cancel.new_status)?); } else { println!("Cancel request failed: {}", cancel.message); } // Output: // Cancel request accepted: Cancel request queued (MVP: no FIX send). Order: 550e8400-... // New status: CANCEL_PENDING ``` #### Cancellable States Only orders in these states can be cancelled: - `PENDING_SUBMIT` - `SUBMITTED` - `PARTIALLY_FILLED` #### Error Codes | Code | Description | |------|-------------| | `NOT_FOUND` | Order not found or account_id mismatch | | `FAILED_PRECONDITION` | Order already filled, cancelled, or rejected | | `INTERNAL` | Database error | --- ### GetAccountState Retrieve current account balance and margin information. #### Request ```protobuf message GetAccountStateRequest { string account_id = 1; // AMP account identifier } ``` #### Response ```protobuf message GetAccountStateResponse { string account_id = 1; double cash_balance = 2; // Cash balance (USD) double equity = 3; // Cash + unrealized P&L double margin_used = 4; // Margin locked by open positions double margin_available = 5; // Available margin for new positions double buying_power = 6; // Margin available * leverage double unrealized_pnl = 7; // Unrealized profit/loss double realized_pnl = 8; // Realized profit/loss (today) int64 last_updated = 9; // Timestamp (nanoseconds) } ``` #### Example ```rust let request = GetAccountStateRequest { account_id: "ACCT_001".to_string(), }; let response = client.get_account_state(request).await?; let state = response.into_inner(); println!("Account: {}", state.account_id); println!("Cash Balance: ${:.2}", state.cash_balance); println!("Equity: ${:.2}", state.equity); println!("Margin Used: ${:.2}", state.margin_used); println!("Margin Available: ${:.2}", state.margin_available); println!("Buying Power: ${:.2}", state.buying_power); println!("Unrealized P&L: ${:.2}", state.unrealized_pnl); println!("Realized P&L: ${:.2}", state.realized_pnl); // Output: // Account: ACCT_001 // Cash Balance: $100000.00 // Equity: $100000.00 // Margin Used: $0.00 // Margin Available: $100000.00 // Buying Power: $400000.00 // Unrealized P&L: $0.00 // Realized P&L: $0.00 ``` #### MVP Behavior Returns placeholder data (cash_balance = $100,000, 4x leverage). **Phase 2**: Queries CQG broker via FIX `CollateralInquiry` (MsgType=BB). #### Error Codes | Code | Description | |------|-------------| | `NOT_FOUND` | Account not found | | `UNAVAILABLE` | Broker connection unavailable (Phase 2) | | `INTERNAL` | Database error | --- ### GetPositions Retrieve current open positions. #### Request ```protobuf message GetPositionsRequest { string account_id = 1; // AMP account identifier optional string symbol = 2; // Filter by symbol (optional) } ``` #### Response ```protobuf message GetPositionsResponse { repeated Position positions = 1; // List of positions double total_equity = 2; // Total account equity double total_exposure = 3; // Sum of abs(position_value) double leverage_ratio = 4; // total_exposure / total_equity int64 timestamp = 5; // Timestamp (nanoseconds) } message Position { string symbol = 1; // ES, NQ, etc. double quantity = 2; // Positive = long, negative = short double average_price = 3; // Average entry price double market_value = 4; // quantity * current_price double unrealized_pnl = 5; // (current_price - avg_price) * quantity } ``` #### Example: All Positions ```rust let request = GetPositionsRequest { account_id: "ACCT_001".to_string(), symbol: None, // All symbols }; let response = client.get_positions(request).await?; let positions_response = response.into_inner(); println!("Total Equity: ${:.2}", positions_response.total_equity); println!("Total Exposure: ${:.2}", positions_response.total_exposure); println!("Leverage: {:.2}x", positions_response.leverage_ratio); println!("\nPositions:"); for pos in &positions_response.positions { println!(" {} x {} @ ${:.2} | Market: ${:.2} | P&L: ${:.2}", pos.symbol, pos.quantity, pos.average_price, pos.market_value, pos.unrealized_pnl ); } // Output: // Total Equity: $102350.00 // Total Exposure: $290000.00 // Leverage: 2.83x // // Positions: // ES x 10 @ $5800.00 | Market: $58050.00 | P&L: $250.00 // NQ x -5 @ $18500.00 | Market: $-92000.00 | P&L: $-100.00 ``` #### Example: Single Symbol ```rust let request = GetPositionsRequest { account_id: "ACCT_001".to_string(), symbol: Some("ES".to_string()), }; let response = client.get_positions(request).await?; // Returns only ES positions ``` #### MVP Behavior Returns empty positions list. **Phase 2**: Queries CQG broker via FIX `RequestForPositions` (MsgType=AN). #### Error Codes | Code | Description | |------|-------------| | `NOT_FOUND` | Account not found | | `UNAVAILABLE` | Broker connection unavailable (Phase 2) | | `INTERNAL` | Database error | --- ### GetSessionStatus Retrieve FIX session status and health metrics. #### Request ```protobuf message GetSessionStatusRequest { optional string session_id = 1; // Optional: default to active session } ``` #### Response ```protobuf message GetSessionStatusResponse { string session_id = 1; // Session identifier (e.g., "FOXHUNT-CQG") SessionState state = 2; // DISCONNECTED, CONNECTED, ACTIVE, etc. int64 sender_seq_num = 3; // Current outgoing MsgSeqNum int64 target_seq_num = 4; // Expected incoming MsgSeqNum int64 last_heartbeat_sent = 5; // Timestamp (nanoseconds) int64 last_heartbeat_received = 6; // Timestamp (nanoseconds) double heartbeat_rtt_ms = 7; // Round-trip time (milliseconds) int64 connected_at = 8; // Connection timestamp (nanoseconds) map details = 9; // Additional info } enum SessionState { SESSION_STATE_DISCONNECTED = 0; SESSION_STATE_CONNECTED = 1; SESSION_STATE_LOGGING_IN = 2; SESSION_STATE_ACTIVE = 3; SESSION_STATE_LOGGING_OUT = 4; } ``` #### Example ```rust let request = GetSessionStatusRequest { session_id: None, // Use default session }; let response = client.get_session_status(request).await?; let status = response.into_inner(); println!("Session: {}", status.session_id); println!("State: {:?}", SessionState::try_from(status.state)?); println!("Sender Seq: {}", status.sender_seq_num); println!("Target Seq: {}", status.target_seq_num); println!("Heartbeat RTT: {:.2} ms", status.heartbeat_rtt_ms); // Output: // Session: FOXHUNT-CQG // State: ACTIVE // Sender Seq: 5432 // Target Seq: 5398 // Heartbeat RTT: 23.45 ms ``` #### MVP Behavior Returns simulated session state (ACTIVE, sequence = 1). **Phase 2**: Returns actual FIX session metrics. #### Error Codes | Code | Description | |------|-------------| | `NOT_FOUND` | Session ID not found | | `INTERNAL` | Database error | --- ### StreamExecutions Stream real-time execution reports from the broker. #### Request ```protobuf message StreamExecutionsRequest { optional string account_id = 1; // Filter by account (optional) optional string symbol = 2; // Filter by symbol (optional) } ``` #### Response (Stream) ```protobuf message ExecutionEvent { string execution_id = 1; // ExecID (Tag 17) string broker_order_id = 2; // OrderID (Tag 37) string client_order_id = 3; // ClOrdID (Tag 11) string symbol = 4; OrderSide side = 5; ExecutionType exec_type = 6; // NEW, TRADE, CANCELED, REJECTED OrderStatus order_status = 7; // Order status after this execution double last_qty = 8; // Quantity filled (Tag 32) double last_price = 9; // Fill price (Tag 31) double cum_qty = 10; // Total filled (Tag 14) double avg_price = 11; // Average fill price (Tag 6) int64 transact_time = 12; // Execution timestamp optional string text = 13; // Reject reason (if applicable) } enum ExecutionType { EXECUTION_TYPE_UNSPECIFIED = 0; EXECUTION_TYPE_NEW = 1; // Order accepted by broker EXECUTION_TYPE_TRADE = 2; // Partial or full fill EXECUTION_TYPE_CANCELED = 3; // Order canceled EXECUTION_TYPE_REJECTED = 4; // Order rejected } ``` #### Example ```rust use tokio_stream::StreamExt; let request = StreamExecutionsRequest { account_id: Some("ACCT_001".to_string()), symbol: None, // All symbols }; let mut stream = client.stream_executions(request).await?.into_inner(); println!("Streaming executions..."); while let Some(event) = stream.next().await { match event { Ok(exec) => { println!("\n--- Execution Event ---"); println!("Execution ID: {}", exec.execution_id); println!("Order ID: {} (Broker: {})", exec.client_order_id, exec.broker_order_id); println!("Symbol: {} {} {}", exec.symbol, if exec.side == OrderSide::Buy as i32 { "BUY" } else { "SELL" }, exec.last_qty); println!("Exec Type: {:?}", ExecutionType::try_from(exec.exec_type)?); println!("Order Status: {:?}", OrderStatus::try_from(exec.order_status)?); if exec.exec_type == ExecutionType::Trade as i32 { println!("Fill: {} @ ${:.2}", exec.last_qty, exec.last_price); println!("Cumulative: {} @ ${:.2} avg", exec.cum_qty, exec.avg_price); } if let Some(text) = exec.text { println!("Message: {}", text); } } Err(e) => { eprintln!("Stream error: {}", e); break; } } } // Output: // Streaming executions... // // --- Execution Event --- // Execution ID: EXEC_789 // Order ID: 550e8400-... (Broker: BROKER_123) // Symbol: ES BUY 10 // Exec Type: TRADE // Order Status: FILLED // Fill: 10 @ $5800.25 // Cumulative: 10 @ $5800.25 avg ``` #### MVP Behavior Stream closes immediately (no executions). **Phase 2**: Streams real-time FIX `ExecutionReport` messages. #### Error Codes | Code | Description | |------|-------------| | `UNAVAILABLE` | FIX session not active (Phase 2) | | `INTERNAL` | Stream initialization failed | --- ### HealthCheck Check service health (database, FIX session). #### Request ```protobuf message HealthCheckRequest {} ``` #### Response ```protobuf message HealthCheckResponse { bool healthy = 1; // True if service is healthy string message = 2; // Status message map details = 3; // Component health details } ``` #### Example ```rust let request = HealthCheckRequest {}; let response = client.health_check(request).await?; let health = response.into_inner(); println!("Service Healthy: {}", health.healthy); println!("Message: {}", health.message); println!("Details:"); for (key, value) in &health.details { println!(" {}: {}", key, value); } // Output: // Service Healthy: true // Message: Broker Gateway Service is healthy (MVP mode) // Details: // database: true // mvp_mode: true // fix_session: not_implemented ``` #### Error Codes Always returns `OK`. Check `healthy` field in response. --- ## Data Types ### Enums #### OrderSide ```protobuf enum OrderSide { ORDER_SIDE_UNSPECIFIED = 0; ORDER_SIDE_BUY = 1; ORDER_SIDE_SELL = 2; } ``` #### OrderType ```protobuf enum OrderType { ORDER_TYPE_UNSPECIFIED = 0; ORDER_TYPE_MARKET = 1; ORDER_TYPE_LIMIT = 2; ORDER_TYPE_STOP = 3; ORDER_TYPE_STOP_LIMIT = 4; } ``` #### OrderStatus ```protobuf enum OrderStatus { ORDER_STATUS_UNSPECIFIED = 0; ORDER_STATUS_PENDING_SUBMIT = 1; // Order created, not sent ORDER_STATUS_SUBMITTED = 2; // Sent to broker ORDER_STATUS_PARTIALLY_FILLED = 3; // Partially filled ORDER_STATUS_FILLED = 4; // Fully filled ORDER_STATUS_CANCEL_PENDING = 5; // Cancel request sent ORDER_STATUS_CANCELLED = 6; // Cancelled by broker ORDER_STATUS_REJECTED = 7; // Rejected by broker } ``` #### ExecutionType ```protobuf enum ExecutionType { EXECUTION_TYPE_UNSPECIFIED = 0; EXECUTION_TYPE_NEW = 1; // Order accepted EXECUTION_TYPE_TRADE = 2; // Fill (partial or full) EXECUTION_TYPE_CANCELED = 3; // Order canceled EXECUTION_TYPE_REJECTED = 4; // Order rejected } ``` #### SessionState ```protobuf enum SessionState { SESSION_STATE_DISCONNECTED = 0; SESSION_STATE_CONNECTED = 1; SESSION_STATE_LOGGING_IN = 2; SESSION_STATE_ACTIVE = 3; SESSION_STATE_LOGGING_OUT = 4; } ``` --- ## Error Codes ### gRPC Status Codes | Code | HTTP | Description | Retry | |------|------|-------------|-------| | `OK` | 200 | Success | - | | `INVALID_ARGUMENT` | 400 | Invalid request parameters | No | | `NOT_FOUND` | 404 | Resource not found | No | | `ALREADY_EXISTS` | 409 | Duplicate order ID | No | | `FAILED_PRECONDITION` | 400 | Order not cancellable | No | | `RESOURCE_EXHAUSTED` | 429 | Rate limit exceeded | Yes (backoff) | | `UNAVAILABLE` | 503 | Service unavailable | Yes (backoff) | | `INTERNAL` | 500 | Internal server error | Yes (limited) | | `DEADLINE_EXCEEDED` | 504 | Request timeout | Yes (once) | ### Error Details Errors include structured details in metadata: ```rust use tonic::{Code, Status}; // Example error response let status = Status::new( Code::InvalidArgument, "Price is required for LIMIT orders" ); // Client error handling match client.route_order(request).await { Ok(response) => { /* ... */ }, Err(e) => { match e.code() { Code::InvalidArgument => { eprintln!("Validation error: {}", e.message()); // Don't retry } Code::Unavailable => { eprintln!("Service unavailable: {}", e.message()); // Retry with backoff } _ => { eprintln!("Unexpected error: {} ({})", e.message(), e.code()); } } } } ``` --- ## Rate Limits ### MVP No rate limits. ### Phase 2 | Method | Limit | Window | |--------|-------|--------| | RouteOrder | 100 req/sec | Per account | | CancelOrder | 50 req/sec | Per account | | GetAccountState | 10 req/sec | Per account | | GetPositions | 10 req/sec | Per account | | GetSessionStatus | 5 req/sec | Global | | StreamExecutions | 1 connection | Per account | Rate limit exceeded returns `RESOURCE_EXHAUSTED` (HTTP 429). --- ## Client Examples ### Complete Order Lifecycle ```rust use broker_gateway::broker_gateway_service_client::BrokerGatewayServiceClient; use broker_gateway::*; use tonic::transport::Channel; use tokio_stream::StreamExt; #[tokio::main] async fn main() -> Result<(), Box> { // 1. Connect to service let channel = Channel::from_static("http://localhost:50056") .connect() .await?; let mut client = BrokerGatewayServiceClient::new(channel); // 2. Stream executions in background let mut stream_client = client.clone(); tokio::spawn(async move { let request = StreamExecutionsRequest { account_id: Some("ACCT_001".to_string()), symbol: None, }; let mut stream = stream_client.stream_executions(request).await.unwrap().into_inner(); while let Some(event) = stream.next().await { if let Ok(exec) = event { println!("Execution: {} {} @ ${}", exec.symbol, exec.last_qty, exec.last_price); } } }); // 3. Submit market order let request = RouteOrderRequest { symbol: "ES".to_string(), side: OrderSide::Buy as i32, quantity: 10.0, order_type: OrderType::Market as i32, price: None, stop_price: None, account_id: "ACCT_001".to_string(), metadata: HashMap::new(), }; let response = client.route_order(request).await?; let order = response.into_inner(); println!("Order submitted: {}", order.client_order_id); // 4. Wait 5 seconds (simulating fill delay) tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; // 5. Get updated positions let request = GetPositionsRequest { account_id: "ACCT_001".to_string(), symbol: Some("ES".to_string()), }; let response = client.get_positions(request).await?; let positions = response.into_inner(); for pos in positions.positions { println!("Position: {} x {}", pos.symbol, pos.quantity); } Ok(()) } ``` ### Error Handling with Retries ```rust use std::time::Duration; use tokio::time::sleep; async fn route_order_with_retry( client: &mut BrokerGatewayServiceClient, request: RouteOrderRequest, max_retries: u32, ) -> Result { let mut attempt = 0; let mut delay = Duration::from_millis(100); loop { match client.route_order(request.clone()).await { Ok(response) => return Ok(response.into_inner()), Err(e) => { let should_retry = matches!( e.code(), Code::Unavailable | Code::DeadlineExceeded | Code::ResourceExhausted ); if should_retry && attempt < max_retries { println!("Retry attempt {}/{} after {:?}", attempt + 1, max_retries, delay); sleep(delay).await; attempt += 1; delay = Duration::min(delay * 2, Duration::from_secs(10)); } else { return Err(e); } } } } } ``` ### Bulk Order Submission ```rust async fn submit_bulk_orders( client: &mut BrokerGatewayServiceClient, orders: Vec, ) -> Vec> { let mut results = Vec::new(); for order in orders { let result = match client.route_order(order.clone()).await { Ok(response) => Ok(response.into_inner().client_order_id), Err(e) => Err(e.message().to_string()), }; results.push(result); } results } // Example usage let orders = vec![ RouteOrderRequest { symbol: "ES".to_string(), /* ... */ }, RouteOrderRequest { symbol: "NQ".to_string(), /* ... */ }, RouteOrderRequest { symbol: "YM".to_string(), /* ... */ }, ]; let results = submit_bulk_orders(&mut client, orders).await; for (i, result) in results.iter().enumerate() { match result { Ok(order_id) => println!("Order {} submitted: {}", i, order_id), Err(e) => println!("Order {} failed: {}", i, e), } } ``` --- ## Changelog ### v0.1.0 (2025-01-09) **MVP Release**: - gRPC API endpoints (7 methods) - Database persistence - Request validation - Health checks **Limitations**: - No FIX protocol (placeholder responses) - No real broker connectivity - StreamExecutions closes immediately ### v0.2.0 (Planned - Phase 2) **Full FIX Integration**: - FIX 4.2/4.4 protocol implementation - CQG broker connectivity - Real-time execution streaming - Position reconciliation - Session management (LOGON, LOGOUT, Heartbeat) - Sequence number tracking --- ## Support For issues or questions: - **Internal**: Slack #broker-gateway-support - **Email**: ops@foxhunt.trading - **Runbook**: See [TROUBLESHOOTING.md](TROUBLESHOOTING.md)