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

26 KiB

Broker Gateway Service API Reference

Version: 0.1.0 Protocol: gRPC (Protocol Buffers v3) Base URL: grpc://localhost:50056

Table of Contents


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

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

// 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

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

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

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

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

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

message CancelOrderRequest {
  string client_order_id = 1;           // Order to cancel (required)
  string account_id = 2;                // Account verification (required)
}

Response

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

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

message GetAccountStateRequest {
  string account_id = 1;                // AMP account identifier
}

Response

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

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

message GetPositionsRequest {
  string account_id = 1;                // AMP account identifier
  optional string symbol = 2;           // Filter by symbol (optional)
}

Response

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

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

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

message GetSessionStatusRequest {
  optional string session_id = 1;       // Optional: default to active session
}

Response

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

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

message StreamExecutionsRequest {
  optional string account_id = 1;       // Filter by account (optional)
  optional string symbol = 2;           // Filter by symbol (optional)
}

Response (Stream)

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

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

message HealthCheckRequest {}

Response

message HealthCheckResponse {
  bool healthy = 1;                     // True if service is healthy
  string message = 2;                   // Status message
  map<string, string> details = 3;      // Component health details
}

Example

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

enum OrderSide {
  ORDER_SIDE_UNSPECIFIED = 0;
  ORDER_SIDE_BUY = 1;
  ORDER_SIDE_SELL = 2;
}

OrderType

enum OrderType {
  ORDER_TYPE_UNSPECIFIED = 0;
  ORDER_TYPE_MARKET = 1;
  ORDER_TYPE_LIMIT = 2;
  ORDER_TYPE_STOP = 3;
  ORDER_TYPE_STOP_LIMIT = 4;
}

OrderStatus

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

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

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:

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

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

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

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: