Files
foxhunt/services/broker_gateway_service/tests/integration_tests.rs
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

909 lines
30 KiB
Rust

//! Integration Tests for Broker Gateway Service
//!
//! End-to-end tests covering:
//! - Order routing (Market, Limit, Stop orders)
//! - Execution report handling
//! - Error scenarios (timeouts, rejections, disconnects)
//! - Reconnection logic
//! - Position tracking
//!
//! Requires: Mock FIX server (tests/mock_fix_server.rs)
//! Target: 80%+ integration coverage, <50ms latency
mod mock_fix_server;
use anyhow::Result;
use mock_fix_server::{MockFIXServer, SessionState};
use std::time::{Duration, Instant};
use tokio::time::sleep;
// ============================================================================
// Order Routing Tests (8 tests)
// ============================================================================
mod order_routing {
use super::*;
#[tokio::test]
async fn test_submit_market_order_success() -> Result<()> {
println!("\n=== Test: Submit Market Order ===");
// Start mock FIX server
let server = MockFIXServer::start().await?;
println!("Mock FIX server started on port {}", server.port);
// Accept connection in background
let server_clone = server.clone();
tokio::spawn(async move {
server_clone.accept_connection().await.ok();
});
sleep(Duration::from_millis(50)).await;
// Connect client and send Logon
let mut client = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", server.port))
.await?;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let logon = "8=FIX.4.2|9=120|35=A|34=1|49=FOXHUNT_CLIENT|56=CQG|98=0|108=30|10=123|";
client.write_all(logon.as_bytes()).await?;
sleep(Duration::from_millis(100)).await;
// Send NewOrderSingle (Market order)
let order = "8=FIX.4.2|9=180|35=D|34=2|49=FOXHUNT_CLIENT|56=CQG|\
11=ORDER_MKT_001|1=ACCT001|55=ES|54=1|38=10|40=1|59=0|21=1|10=234|";
client.write_all(order.as_bytes()).await?;
// Read ExecutionReport response
let mut buf = vec![0u8; 2048];
sleep(Duration::from_millis(100)).await;
let n = client.read(&mut buf).await?;
let response = String::from_utf8_lossy(&buf[..n]);
println!("Response: {}", response);
assert!(response.contains("35=8")); // ExecutionReport
assert!(response.contains("11=ORDER_MKT_001")); // ClOrdID
assert!(response.contains("150=0")); // ExecType=New
// Verify server received the order
let messages = server.get_received_messages().await;
assert_eq!(messages.len(), 2); // Logon + NewOrderSingle
assert_eq!(messages[1].msg_type, "D");
assert_eq!(messages[1].get_field(55), Some("ES"));
Ok(())
}
#[tokio::test]
async fn test_submit_limit_order_success() -> Result<()> {
println!("\n=== Test: Submit Limit Order ===");
let server = MockFIXServer::start().await?;
let server_clone = server.clone();
tokio::spawn(async move {
server_clone.accept_connection().await.ok();
});
sleep(Duration::from_millis(50)).await;
let mut client = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", server.port))
.await?;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
// Logon
let logon = "8=FIX.4.2|9=120|35=A|34=1|49=CLIENT|56=CQG|98=0|108=30|10=123|";
client.write_all(logon.as_bytes()).await?;
sleep(Duration::from_millis(100)).await;
// NewOrderSingle (Limit order with price)
let order = "8=FIX.4.2|9=200|35=D|34=2|49=CLIENT|56=CQG|\
11=ORDER_LMT_002|1=ACCT001|55=NQ|54=2|38=5|40=2|44=18500.50|59=0|21=1|10=123|";
client.write_all(order.as_bytes()).await?;
let mut buf = vec![0u8; 2048];
sleep(Duration::from_millis(100)).await;
let n = client.read(&mut buf).await?;
let response = String::from_utf8_lossy(&buf[..n]);
assert!(response.contains("11=ORDER_LMT_002"));
assert!(response.contains("55=NQ"));
let messages = server.get_received_messages().await;
let order_msg = &messages[1];
assert_eq!(order_msg.get_field(40), Some("2")); // OrdType=Limit
assert_eq!(order_msg.get_field(44), Some("18500.50")); // Price
Ok(())
}
#[tokio::test]
async fn test_submit_stop_order_success() -> Result<()> {
println!("\n=== Test: Submit Stop Order ===");
let server = MockFIXServer::start().await?;
let server_clone = server.clone();
tokio::spawn(async move {
server_clone.accept_connection().await.ok();
});
sleep(Duration::from_millis(50)).await;
let mut client = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", server.port))
.await?;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let logon = "8=FIX.4.2|9=120|35=A|34=1|49=CLIENT|56=CQG|98=0|108=30|10=123|";
client.write_all(logon.as_bytes()).await?;
sleep(Duration::from_millis(100)).await;
// NewOrderSingle (Stop order with stop price)
let order = "8=FIX.4.2|9=200|35=D|34=2|49=CLIENT|56=CQG|\
11=ORDER_STP_003|1=ACCT001|55=ES|54=1|38=10|40=3|99=5795.00|59=0|21=1|10=234|";
client.write_all(order.as_bytes()).await?;
let mut buf = vec![0u8; 2048];
sleep(Duration::from_millis(100)).await;
let n = client.read(&mut buf).await?;
let response = String::from_utf8_lossy(&buf[..n]);
assert!(response.contains("11=ORDER_STP_003"));
let messages = server.get_received_messages().await;
let order_msg = &messages[1];
assert_eq!(order_msg.get_field(40), Some("3")); // OrdType=Stop
assert_eq!(order_msg.get_field(99), Some("5795.00")); // StopPx
Ok(())
}
#[tokio::test]
async fn test_cancel_order_success() -> Result<()> {
println!("\n=== Test: Cancel Order ===");
let server = MockFIXServer::start().await?;
let server_clone = server.clone();
tokio::spawn(async move {
server_clone.accept_connection().await.ok();
});
sleep(Duration::from_millis(50)).await;
let mut client = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", server.port))
.await?;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let logon = "8=FIX.4.2|9=120|35=A|34=1|49=CLIENT|56=CQG|98=0|108=30|10=123|";
client.write_all(logon.as_bytes()).await?;
sleep(Duration::from_millis(100)).await;
// Send OrderCancelRequest
let cancel = "8=FIX.4.2|9=150|35=F|34=2|49=CLIENT|56=CQG|\
11=CANCEL_001|37=BROKER123|41=ORDER_ORIG|55=ES|54=1|10=089|";
client.write_all(cancel.as_bytes()).await?;
sleep(Duration::from_millis(100)).await;
let messages = server.get_received_messages().await;
assert_eq!(messages.len(), 2); // Logon + Cancel
assert_eq!(messages[1].msg_type, "F"); // OrderCancelRequest
assert_eq!(messages[1].get_field(11), Some("CANCEL_001"));
assert_eq!(messages[1].get_field(41), Some("ORDER_ORIG")); // OrigClOrdID
Ok(())
}
#[tokio::test]
async fn test_modify_order_price() -> Result<()> {
println!("\n=== Test: Modify Order Price ===");
let server = MockFIXServer::start().await?;
let server_clone = server.clone();
tokio::spawn(async move {
server_clone.accept_connection().await.ok();
});
sleep(Duration::from_millis(50)).await;
let mut client = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", server.port))
.await?;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let logon = "8=FIX.4.2|9=120|35=A|34=1|49=CLIENT|56=CQG|98=0|108=30|10=123|";
client.write_all(logon.as_bytes()).await?;
sleep(Duration::from_millis(100)).await;
// Send OrderCancelReplaceRequest (modify price)
let modify = "8=FIX.4.2|9=180|35=G|34=2|49=CLIENT|56=CQG|\
11=MODIFY_001|37=BROKER123|41=ORDER_ORIG|55=ES|54=1|38=10|40=2|44=5801.00|10=145|";
client.write_all(modify.as_bytes()).await?;
sleep(Duration::from_millis(100)).await;
let messages = server.get_received_messages().await;
assert_eq!(messages[1].msg_type, "G"); // OrderCancelReplaceRequest
assert_eq!(messages[1].get_field(44), Some("5801.00")); // New price
Ok(())
}
#[tokio::test]
async fn test_order_submission_latency() -> Result<()> {
println!("\n=== Test: Order Submission Latency (<50ms) ===");
let server = MockFIXServer::start().await?;
server.set_latency(10).await; // Simulate 10ms network latency
let server_clone = server.clone();
tokio::spawn(async move {
server_clone.accept_connection().await.ok();
});
sleep(Duration::from_millis(50)).await;
let mut client = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", server.port))
.await?;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let logon = "8=FIX.4.2|9=120|35=A|34=1|49=CLIENT|56=CQG|98=0|108=30|10=123|";
client.write_all(logon.as_bytes()).await?;
sleep(Duration::from_millis(150)).await; // Wait for logon
// Measure order submission latency
let order = "8=FIX.4.2|9=180|35=D|34=2|49=CLIENT|56=CQG|\
11=ORDER_LAT_001|1=ACCT001|55=ES|54=1|38=10|40=1|59=0|21=1|10=234|";
let start = Instant::now();
client.write_all(order.as_bytes()).await?;
let mut buf = vec![0u8; 2048];
let n = client.read(&mut buf).await?;
let latency = start.elapsed();
let response = String::from_utf8_lossy(&buf[..n]);
assert!(response.contains("35=8")); // ExecutionReport received
println!("Order submission latency: {:?}", latency);
assert!(
latency < Duration::from_millis(50),
"Latency {}ms exceeds 50ms target",
latency.as_millis()
);
Ok(())
}
#[tokio::test]
async fn test_bulk_order_submission() -> Result<()> {
println!("\n=== Test: Bulk Order Submission (100 orders) ===");
let server = MockFIXServer::start().await?;
let server_clone = server.clone();
tokio::spawn(async move {
server_clone.accept_connection().await.ok();
});
sleep(Duration::from_millis(50)).await;
let mut client = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", server.port))
.await?;
use tokio::io::AsyncWriteExt;
let logon = "8=FIX.4.2|9=120|35=A|34=1|49=CLIENT|56=CQG|98=0|108=30|10=123|";
client.write_all(logon.as_bytes()).await?;
sleep(Duration::from_millis(100)).await;
// Submit 100 orders
for i in 0..100 {
let order = format!(
"8=FIX.4.2|9=180|35=D|34={}|49=CLIENT|56=CQG|\
11=ORDER_BULK_{}|1=ACCT001|55=ES|54=1|38=10|40=1|59=0|21=1|10=234|",
i + 2,
i
);
client.write_all(order.as_bytes()).await?;
sleep(Duration::from_millis(5)).await; // Small delay between orders
}
sleep(Duration::from_millis(500)).await;
let messages = server.get_received_messages().await;
assert!(messages.len() >= 100); // At least 100 orders + Logon
Ok(())
}
#[tokio::test]
async fn test_order_validation_fails() -> Result<()> {
println!("\n=== Test: Order Validation Failures ===");
let server = MockFIXServer::start().await?;
let server_clone = server.clone();
tokio::spawn(async move {
server_clone.accept_connection().await.ok();
});
sleep(Duration::from_millis(50)).await;
let mut client = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", server.port))
.await?;
use tokio::io::AsyncWriteExt;
let logon = "8=FIX.4.2|9=120|35=A|34=1|49=CLIENT|56=CQG|98=0|108=30|10=123|";
client.write_all(logon.as_bytes()).await?;
sleep(Duration::from_millis(100)).await;
// Send order with empty symbol (should fail validation)
let invalid_order = "8=FIX.4.2|9=150|35=D|34=2|49=CLIENT|56=CQG|\
11=ORDER_INVALID|1=ACCT001|55=|54=1|38=10|40=1|10=234|";
client.write_all(invalid_order.as_bytes()).await?;
sleep(Duration::from_millis(100)).await;
// In production, server would reject this order
let messages = server.get_received_messages().await;
let order_msg = &messages[1];
assert_eq!(order_msg.get_field(55), Some("")); // Empty symbol
Ok(())
}
}
// ============================================================================
// Execution Report Tests (6 tests)
// ============================================================================
mod execution_reports {
use super::*;
#[tokio::test]
async fn test_execution_report_new_order() -> Result<()> {
println!("\n=== Test: ExecutionReport (New Order) ===");
let server = MockFIXServer::start().await?;
let server_clone = server.clone();
tokio::spawn(async move {
server_clone.accept_connection().await.ok();
});
sleep(Duration::from_millis(50)).await;
let mut client = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", server.port))
.await?;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let logon = "8=FIX.4.2|9=120|35=A|34=1|49=CLIENT|56=CQG|98=0|108=30|10=123|";
client.write_all(logon.as_bytes()).await?;
sleep(Duration::from_millis(100)).await;
let order = "8=FIX.4.2|9=180|35=D|34=2|49=CLIENT|56=CQG|\
11=ORDER_NEW_001|1=ACCT001|55=ES|54=1|38=10|40=1|59=0|21=1|10=234|";
client.write_all(order.as_bytes()).await?;
let mut buf = vec![0u8; 2048];
sleep(Duration::from_millis(100)).await;
let n = client.read(&mut buf).await?;
let response = String::from_utf8_lossy(&buf[..n]);
// Parse ExecutionReport
assert!(response.contains("35=8")); // ExecutionReport
assert!(response.contains("150=0")); // ExecType=New
assert!(response.contains("39=0")); // OrdStatus=New
assert!(response.contains("11=ORDER_NEW_001")); // ClOrdID
Ok(())
}
#[tokio::test]
async fn test_execution_report_fill() -> Result<()> {
println!("\n=== Test: ExecutionReport (Fill) ===");
let server = MockFIXServer::start().await?;
let server_clone = server.clone();
tokio::spawn(async move {
server_clone.accept_connection().await.ok();
});
sleep(Duration::from_millis(50)).await;
// Note: Fill report would be sent asynchronously by server
// For this test, we just verify the format
let fill_msg = mock_fix_server::FIXMessage::create_execution_report_fill(
"ORDER_FILL_001",
"BROKER123",
"ES",
"1",
"10",
"5800.25",
10,
"CQG",
"CLIENT",
);
assert!(fill_msg.contains("150=F")); // ExecType=Fill
assert!(fill_msg.contains("39=2")); // OrdStatus=Filled
assert!(fill_msg.contains("32=10")); // LastQty
assert!(fill_msg.contains("31=5800.25")); // LastPx
assert!(fill_msg.contains("14=10")); // CumQty
assert!(fill_msg.contains("6=5800.25")); // AvgPx
Ok(())
}
#[tokio::test]
async fn test_execution_report_partial_fill() -> Result<()> {
println!("\n=== Test: ExecutionReport (Partial Fill) ===");
// Partial fill: LastQty < OrderQty, LeavesQty > 0
let partial_fill = format!(
"8=FIX.4.2|9=250|35=8|34=5|49=CQG|56=CLIENT|\
37=BROKER123|11=ORDER_PARTIAL|17=EXEC_PART|150=F|39=1|\
55=ES|54=1|38=10|32=5|31=5800.00|151=5|14=5|6=5800.00|10=234|"
);
assert!(partial_fill.contains("39=1")); // OrdStatus=PartiallyFilled
assert!(partial_fill.contains("32=5")); // LastQty (5 of 10)
assert!(partial_fill.contains("151=5")); // LeavesQty (5 remaining)
assert!(partial_fill.contains("14=5")); // CumQty (5 filled so far)
Ok(())
}
#[tokio::test]
async fn test_execution_report_reject() -> Result<()> {
println!("\n=== Test: ExecutionReport (Reject) ===");
let server = MockFIXServer::start().await?;
server
.reject_next_order("Insufficient margin".to_string())
.await;
let server_clone = server.clone();
tokio::spawn(async move {
server_clone.accept_connection().await.ok();
});
sleep(Duration::from_millis(50)).await;
let mut client = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", server.port))
.await?;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let logon = "8=FIX.4.2|9=120|35=A|34=1|49=CLIENT|56=CQG|98=0|108=30|10=123|";
client.write_all(logon.as_bytes()).await?;
sleep(Duration::from_millis(100)).await;
let order = "8=FIX.4.2|9=180|35=D|34=2|49=CLIENT|56=CQG|\
11=ORDER_REJECT|1=ACCT001|55=ES|54=1|38=10|40=1|59=0|21=1|10=234|";
client.write_all(order.as_bytes()).await?;
let mut buf = vec![0u8; 2048];
sleep(Duration::from_millis(100)).await;
let n = client.read(&mut buf).await?;
let response = String::from_utf8_lossy(&buf[..n]);
assert!(response.contains("150=8")); // ExecType=Rejected
assert!(response.contains("39=8")); // OrdStatus=Rejected
assert!(response.contains("Insufficient margin"));
Ok(())
}
#[tokio::test]
async fn test_execution_report_cancel() -> Result<()> {
println!("\n=== Test: ExecutionReport (Cancel) ===");
let cancel_exec = format!(
"8=FIX.4.2|9=200|35=8|34=10|49=CQG|56=CLIENT|\
37=BROKER456|11=CANCEL_EXEC|17=EXEC_CANCEL|150=4|39=4|\
55=ES|54=1|38=10|32=0|151=0|14=0|10=145|"
);
assert!(cancel_exec.contains("150=4")); // ExecType=Canceled
assert!(cancel_exec.contains("39=4")); // OrdStatus=Canceled
assert!(cancel_exec.contains("32=0")); // LastQty=0 (no fill)
assert!(cancel_exec.contains("151=0")); // LeavesQty=0
Ok(())
}
#[tokio::test]
async fn test_multiple_fills_same_order() -> Result<()> {
println!("\n=== Test: Multiple Fills (Same Order) ===");
// Scenario: Order for 10 contracts fills in 2 parts (5 + 5)
// First fill: 5 contracts @ 5800.00
let fill1 = format!(
"8=FIX.4.2|9=250|35=8|34=5|49=CQG|56=CLIENT|\
37=BROKER789|11=ORDER_MULTI|17=EXEC_001|150=F|39=1|\
55=ES|54=1|38=10|32=5|31=5800.00|151=5|14=5|6=5800.00|10=234|"
);
// Second fill: 5 contracts @ 5800.50 (different price)
let fill2 = format!(
"8=FIX.4.2|9=250|35=8|34=6|49=CQG|56=CLIENT|\
37=BROKER789|11=ORDER_MULTI|17=EXEC_002|150=F|39=2|\
55=ES|54=1|38=10|32=5|31=5800.50|151=0|14=10|6=5800.25|10=145|"
);
// Validate first fill
assert!(fill1.contains("32=5")); // LastQty
assert!(fill1.contains("14=5")); // CumQty
assert!(fill1.contains("39=1")); // PartiallyFilled
// Validate second fill
assert!(fill2.contains("32=5")); // LastQty
assert!(fill2.contains("14=10")); // CumQty (total)
assert!(fill2.contains("6=5800.25")); // AvgPx (weighted avg)
assert!(fill2.contains("39=2")); // Filled
Ok(())
}
}
// ============================================================================
// Error Handling Tests (10 tests) - Simplified stubs
// ============================================================================
mod error_handling {
use super::*;
#[tokio::test]
async fn test_fix_connection_timeout() -> Result<()> {
println!("\n=== Test: FIX Connection Timeout ===");
// Attempt to connect to non-existent server
let result = tokio::time::timeout(
Duration::from_secs(2),
tokio::net::TcpStream::connect("127.0.0.1:9999"),
)
.await;
assert!(result.is_err() || result.unwrap().is_err());
Ok(())
}
#[tokio::test]
async fn test_fix_logon_rejection() -> Result<()> {
println!("\n=== Test: FIX Logon Rejection ===");
// Mock server could reject Logon with invalid credentials
// In production, would receive Logout message
let logout_msg = "8=FIX.4.2|9=100|35=5|34=1|49=CQG|56=CLIENT|58=Invalid credentials|10=123|";
assert!(logout_msg.contains("35=5")); // Logout
assert!(logout_msg.contains("Invalid credentials"));
Ok(())
}
#[tokio::test]
async fn test_order_reject_insufficient_margin() -> Result<()> {
println!("\n=== Test: Order Reject (Insufficient Margin) ===");
let reject = mock_fix_server::FIXMessage::create_execution_report_reject(
"ORDER_MARGIN",
"ES",
"Insufficient margin",
5,
"CQG",
"CLIENT",
);
assert!(reject.contains("150=8")); // Rejected
assert!(reject.contains("Insufficient margin"));
Ok(())
}
#[tokio::test]
async fn test_sequence_gap_recovery() -> Result<()> {
println!("\n=== Test: Sequence Gap Recovery ===");
// Receive message with seq_num=10, expecting 5 → gap
// Should send ResendRequest
let resend_request = "8=FIX.4.2|9=80|35=2|34=5|49=CLIENT|56=CQG|7=5|16=10|10=123|";
assert!(resend_request.contains("35=2")); // ResendRequest
assert!(resend_request.contains("7=5")); // BeginSeqNo
assert!(resend_request.contains("16=10")); // EndSeqNo
Ok(())
}
#[tokio::test]
async fn test_heartbeat_timeout_reconnect() -> Result<()> {
println!("\n=== Test: Heartbeat Timeout Reconnect ===");
// Simulate no heartbeat for 60s → trigger reconnect
let last_heartbeat = Instant::now() - Duration::from_secs(65);
let timeout_threshold = Duration::from_secs(60);
assert!(last_heartbeat.elapsed() > timeout_threshold);
// Reconnect logic would trigger here
Ok(())
}
#[tokio::test]
async fn test_invalid_execution_report_ignored() -> Result<()> {
println!("\n=== Test: Invalid ExecutionReport Ignored ===");
// ExecutionReport with missing required fields
let invalid_exec = "8=FIX.4.2|9=100|35=8|34=15|49=CQG|56=CLIENT|10=089|"; // Missing ClOrdID, ExecType
// Parser should reject this message
assert!(invalid_exec.contains("35=8"));
assert!(!invalid_exec.contains("11=")); // No ClOrdID
Ok(())
}
#[tokio::test]
async fn test_database_write_failure_retry() -> Result<()> {
println!("\n=== Test: Database Write Failure Retry ===");
// Simulate database write failure + retry logic
let mut retry_count = 0;
let max_retries = 3;
while retry_count < max_retries {
// Simulate write failure
retry_count += 1;
sleep(Duration::from_millis(10)).await;
}
assert_eq!(retry_count, max_retries);
Ok(())
}
#[tokio::test]
async fn test_redis_unavailable_degraded_mode() -> Result<()> {
println!("\n=== Test: Redis Unavailable (Degraded Mode) ===");
// Simulate Redis connection failure
// Service should fall back to database-only mode
let redis_available = false;
if !redis_available {
// Fallback to database
println!("Redis unavailable, using database-only mode");
}
assert!(!redis_available);
Ok(())
}
#[tokio::test]
async fn test_grpc_timeout_trading_service() -> Result<()> {
println!("\n=== Test: gRPC Timeout (Trading Service) ===");
// Simulate gRPC timeout when notifying Trading Service
let result = tokio::time::timeout(Duration::from_millis(100), async {
sleep(Duration::from_millis(200)).await;
})
.await;
assert!(result.is_err()); // Timeout
Ok(())
}
#[tokio::test]
async fn test_circuit_breaker_active_rejects_orders() -> Result<()> {
println!("\n=== Test: Circuit Breaker Active ===");
// Simulate circuit breaker activation
let circuit_breaker_active = true;
if circuit_breaker_active {
// Reject all new orders
println!("Circuit breaker active - rejecting order");
}
assert!(circuit_breaker_active);
Ok(())
}
}
// ============================================================================
// Reconnection Tests (5 tests) - Simplified stubs
// ============================================================================
mod reconnection {
use super::*;
#[tokio::test]
async fn test_reconnect_after_disconnect() -> Result<()> {
println!("\n=== Test: Reconnect After Disconnect ===");
let server = MockFIXServer::start().await?;
// Initial connection
let server_clone = server.clone();
tokio::spawn(async move {
server_clone.accept_connection().await.ok();
});
sleep(Duration::from_millis(100)).await;
assert_eq!(server.get_session_state().await, SessionState::LoggedIn);
// Disconnect
server.disconnect().await;
sleep(Duration::from_millis(50)).await;
assert_eq!(server.get_session_state().await, SessionState::Disconnected);
// Reconnect would happen here in production
Ok(())
}
#[tokio::test]
async fn test_reconnect_exponential_backoff() -> Result<()> {
println!("\n=== Test: Exponential Backoff ===");
let mut delay = Duration::from_secs(1);
for attempt in 0..5 {
println!("Retry attempt {}: delay {:?}", attempt, delay);
sleep(Duration::from_millis(10)).await; // Simulate retry
delay *= 2; // Exponential backoff
}
assert_eq!(delay, Duration::from_secs(32)); // 1 → 2 → 4 → 8 → 16 → 32
Ok(())
}
#[tokio::test]
async fn test_reconnect_sequence_recovery() -> Result<()> {
println!("\n=== Test: Sequence Recovery on Reconnect ===");
// After reconnect, restore sequences from database
let sender_seq = 50u64;
let target_seq = 45u64;
// Send Logon with last known sequences
let logon = format!(
"8=FIX.4.2|9=120|35=A|34={}|49=CLIENT|56=CQG|98=0|108=30|141=N|10=123|",
sender_seq
);
assert!(logon.contains(&format!("34={}", sender_seq)));
assert!(logon.contains("141=N")); // Don't reset sequences
Ok(())
}
#[tokio::test]
async fn test_reconnect_max_retries_exceeded() -> Result<()> {
println!("\n=== Test: Max Retries Exceeded ===");
let max_retries = 5;
let mut attempt = 0;
while attempt < max_retries {
// Simulate failed connection
attempt += 1;
sleep(Duration::from_millis(10)).await;
}
assert_eq!(attempt, max_retries);
println!("Max retries exceeded, giving up");
Ok(())
}
#[tokio::test]
async fn test_reconnect_order_state_recovery() -> Result<()> {
println!("\n=== Test: Order State Recovery on Reconnect ===");
// After reconnect, send OrderStatusRequest for open orders
let status_request = "8=FIX.4.2|9=120|35=H|34=51|49=CLIENT|56=CQG|\
37=BROKER123|11=ORDER_OPEN|55=ES|54=1|10=123|";
assert!(status_request.contains("35=H")); // OrderStatusRequest
assert!(status_request.contains("11=ORDER_OPEN"));
Ok(())
}
}
// ============================================================================
// Position Tracking Tests (4 tests) - Simplified stubs
// ============================================================================
mod position_tracking {
use super::*;
#[tokio::test]
async fn test_position_update_on_fill() -> Result<()> {
println!("\n=== Test: Position Update on Fill ===");
// BUY 10 contracts → position = +10
let mut position = 0.0;
position += 10.0;
assert_eq!(position, 10.0);
// SELL 5 contracts → position = +5
position -= 5.0;
assert_eq!(position, 5.0);
Ok(())
}
#[tokio::test]
async fn test_position_redis_cache_consistency() -> Result<()> {
println!("\n=== Test: Redis Cache Consistency ===");
// Simulate Redis HINCRBYFLOAT for position update
let mut cached_position = 10.0;
cached_position += 5.0; // Buy 5 more
assert_eq!(cached_position, 15.0);
// Database should match Redis
let db_position = 15.0;
assert_eq!(cached_position, db_position);
Ok(())
}
#[tokio::test]
async fn test_position_reconciliation_on_reconnect() -> Result<()> {
println!("\n=== Test: Position Reconciliation ===");
// After reconnect, query broker for current positions
// Compare with local cache/database
let broker_position = 20.0;
let local_position = 15.0;
if (broker_position - local_position).abs() > 0.01 {
println!(
"Position mismatch: broker={}, local={}",
broker_position, local_position
);
// Reconcile to broker's position
}
Ok(())
}
#[tokio::test]
async fn test_position_flattening_on_close() -> Result<()> {
println!("\n=== Test: Position Flattening ===");
let mut position = 10.0; // Long 10 contracts
// Flatten position: SELL 10 contracts
position -= 10.0;
assert_eq!(position, 0.0);
// Redis cache should be cleared or set to 0
Ok(())
}
}