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>
991 lines
26 KiB
Markdown
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)
|