Files
foxhunt/services/broker_gateway_service/docs/API.md
jgrusewski 00ef9e2866 Wave 15: Complete FactoredAction migration to 45-action system
Major Changes:
- Migrated from 3-action TradingAction to 45-action FactoredAction
- 45 actions: 5 exposure × 3 order types × 3 urgency levels
- Absolute exposure model (target positions -1.0 to +1.0)
- Transaction cost differentiation (Market 0.15%, LimitMaker 0.05%, IoC 0.10%)
- Fixed action diversity threshold (1.11% → 0.5% for 45-action space)

Bug Fixes:
- Bug #15: Incomplete FactoredAction integration (code existed but unused)
- Bug #16: Runtime crash in action diversity checking (hardcoded 3-action match)

Code Changes (13 files, ~464 lines):
- ml/src/dqn/action_space.rs: Core FactoredAction + 4 helper methods
- ml/src/trainers/dqn.rs: Action diversity refactored (3→45 dynamic)
- ml/src/dqn/reward.rs: calculate_reward() signature updated
- ml/src/dqn/portfolio_tracker.rs: execute_action() absolute exposure
- ml/src/dqn/dqn.rs: WorkingDQN action selection migrated
- ml/tests/*.rs: 9 test files updated with FactoredAction assertions

Test Results:
- 1-epoch smoke test: 100% action diversity (45/45 actions, 80.2s)
- 10-epoch production: 87.8% readiness (79/90 scorecard, 14.0 min)
- Loss convergence: 96.9% reduction (119K → 3.6K)
- Action diversity: 100% → 44% (healthy specialization)
- Checkpoint reliability: 12/12 files saved (100%)
- DQN tests: 195/195 passing (100%)
- ML baseline: 1,514/1,515 passing (99.93%)

Production Status:  CERTIFIED (87.8% readiness)
Go/No-Go:  GO FOR 100-EPOCH PRODUCTION TRAINING

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 23:27:02 +01:00

991 lines
26 KiB
Markdown

# 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<string, string> 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<string, string> 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<string, string> 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<dyn std::error::Error>> {
// 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<Channel>,
request: RouteOrderRequest,
max_retries: u32,
) -> Result<RouteOrderResponse, tonic::Status> {
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<Channel>,
orders: Vec<RouteOrderRequest>,
) -> Vec<Result<String, String>> {
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)