667 lines
22 KiB
Rust
667 lines
22 KiB
Rust
//! Position Lifecycle Integration Tests
|
||
//!
|
||
//! Comprehensive tests for position management covering:
|
||
//! - Position opening and closing
|
||
//! - Real-time PnL calculation and tracking
|
||
//! - Position updates from order fills
|
||
//! - Multi-symbol position management
|
||
//! - Position reconciliation
|
||
//! - Edge cases: zero positions, negative positions (shorts)
|
||
|
||
use anyhow::Result;
|
||
use std::sync::Arc;
|
||
use tonic::Request;
|
||
use trading_service::proto::trading::{
|
||
trading_service_server::TradingService,
|
||
SubmitOrderRequest, GetPositionsRequest, GetPortfolioSummaryRequest,
|
||
OrderSide, OrderType
|
||
};
|
||
use trading_service::{
|
||
state::TradingServiceState,
|
||
services::trading::TradingServiceImpl,
|
||
};
|
||
|
||
/// Setup test trading service instance
|
||
async fn setup_trading_service() -> Result<TradingServiceImpl> {
|
||
let state = Arc::new(TradingServiceState::new_for_testing().await?);
|
||
Ok(TradingServiceImpl::new(state))
|
||
}
|
||
|
||
// ============================================================================
|
||
// Position Opening Tests
|
||
// ============================================================================
|
||
|
||
#[tokio::test]
|
||
async fn test_open_long_position() -> Result<()> {
|
||
println!("\n=== Test: Open Long Position ===");
|
||
|
||
let service = setup_trading_service().await?;
|
||
let account_id = "position_test_001";
|
||
|
||
// Submit buy order to open long position
|
||
let mut metadata = std::collections::HashMap::new();
|
||
metadata.insert("time_in_force".to_string(), "GTC".to_string());
|
||
|
||
let buy_request = Request::new(SubmitOrderRequest {
|
||
account_id: account_id.to_string(),
|
||
symbol: "AAPL".to_string(),
|
||
side: OrderSide::Buy as i32,
|
||
order_type: OrderType::Market as i32,
|
||
quantity: 100.0,
|
||
price: None,
|
||
stop_price: None,
|
||
metadata,
|
||
});
|
||
|
||
let buy_response = service.submit_order(buy_request).await?;
|
||
let order_id = buy_response.into_inner().order_id;
|
||
println!(" Buy order submitted: {}", order_id);
|
||
|
||
// Get positions
|
||
let positions_request = Request::new(GetPositionsRequest {
|
||
account_id: Some(account_id.to_string()),
|
||
symbol: Some("AAPL".to_string()),
|
||
});
|
||
|
||
let positions_response = service.get_positions(positions_request).await?;
|
||
let positions = positions_response.into_inner();
|
||
|
||
println!(" Positions found: {}", positions.positions.len());
|
||
for pos in &positions.positions {
|
||
println!(" Symbol: {}, Quantity: {}", pos.symbol, pos.quantity);
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_open_short_position() -> Result<()> {
|
||
println!("\n=== Test: Open Short Position ===");
|
||
|
||
let service = setup_trading_service().await?;
|
||
let account_id = "position_test_002";
|
||
|
||
// Submit sell order to open short position
|
||
let mut metadata = std::collections::HashMap::new();
|
||
metadata.insert("time_in_force".to_string(), "GTC".to_string());
|
||
|
||
let sell_request = Request::new(SubmitOrderRequest {
|
||
account_id: account_id.to_string(),
|
||
symbol: "TSLA".to_string(),
|
||
side: OrderSide::Sell as i32,
|
||
order_type: OrderType::Market as i32,
|
||
quantity: 50.0,
|
||
price: None,
|
||
stop_price: None,
|
||
metadata,
|
||
});
|
||
|
||
let sell_response = service.submit_order(sell_request).await?;
|
||
let order_id = sell_response.into_inner().order_id;
|
||
println!(" Sell order submitted: {}", order_id);
|
||
|
||
// Get positions
|
||
let positions_request = Request::new(GetPositionsRequest {
|
||
account_id: Some(account_id.to_string()),
|
||
symbol: Some("TSLA".to_string()),
|
||
});
|
||
|
||
let positions_response = service.get_positions(positions_request).await?;
|
||
let positions = positions_response.into_inner();
|
||
|
||
println!(" Positions found: {}", positions.positions.len());
|
||
for pos in &positions.positions {
|
||
println!(" Symbol: {}, Quantity: {}", pos.symbol, pos.quantity);
|
||
// Short position should show negative quantity
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_open_multiple_positions() -> Result<()> {
|
||
println!("\n=== Test: Open Multiple Positions ===");
|
||
|
||
let service = setup_trading_service().await?;
|
||
let account_id = "position_test_003";
|
||
|
||
let symbols = vec![
|
||
("AAPL", 100.0),
|
||
("GOOGL", 50.0),
|
||
("MSFT", 75.0),
|
||
("NVDA", 25.0),
|
||
];
|
||
|
||
// Open positions in multiple symbols
|
||
for (symbol, quantity) in &symbols {
|
||
let mut metadata = std::collections::HashMap::new();
|
||
metadata.insert("time_in_force".to_string(), "GTC".to_string());
|
||
|
||
let request = Request::new(SubmitOrderRequest {
|
||
account_id: account_id.to_string(),
|
||
symbol: symbol.to_string(),
|
||
side: OrderSide::Buy as i32,
|
||
order_type: OrderType::Market as i32,
|
||
quantity: *quantity,
|
||
price: None,
|
||
stop_price: None,
|
||
metadata,
|
||
});
|
||
|
||
let response = service.submit_order(request).await?;
|
||
println!(" Position opened: {} - {} shares", symbol, quantity);
|
||
let _ = response.into_inner();
|
||
}
|
||
|
||
// Get all positions
|
||
let positions_request = Request::new(GetPositionsRequest {
|
||
account_id: Some(account_id.to_string()),
|
||
symbol: None, // All symbols
|
||
});
|
||
|
||
let positions_response = service.get_positions(positions_request).await?;
|
||
let positions = positions_response.into_inner();
|
||
|
||
println!(" Total positions: {}", positions.positions.len());
|
||
for pos in &positions.positions {
|
||
println!(" {}: {} shares", pos.symbol, pos.quantity);
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
// ============================================================================
|
||
// Position Closing Tests
|
||
// ============================================================================
|
||
|
||
#[tokio::test]
|
||
async fn test_close_long_position() -> Result<()> {
|
||
println!("\n=== Test: Close Long Position ===");
|
||
|
||
let service = setup_trading_service().await?;
|
||
let account_id = "position_test_004";
|
||
|
||
// 1. Open long position
|
||
let mut metadata = std::collections::HashMap::new();
|
||
metadata.insert("time_in_force".to_string(), "GTC".to_string());
|
||
|
||
let buy_request = Request::new(SubmitOrderRequest {
|
||
account_id: account_id.to_string(),
|
||
symbol: "AMD".to_string(),
|
||
side: OrderSide::Buy as i32,
|
||
order_type: OrderType::Market as i32,
|
||
quantity: 100.0,
|
||
price: None,
|
||
stop_price: None,
|
||
metadata: metadata.clone(),
|
||
});
|
||
|
||
let _ = service.submit_order(buy_request).await?;
|
||
println!(" 1. Long position opened: 100 AMD");
|
||
|
||
// 2. Close position
|
||
let sell_request = Request::new(SubmitOrderRequest {
|
||
account_id: account_id.to_string(),
|
||
symbol: "AMD".to_string(),
|
||
side: OrderSide::Sell as i32,
|
||
order_type: OrderType::Market as i32,
|
||
quantity: 100.0,
|
||
price: None,
|
||
stop_price: None,
|
||
metadata,
|
||
});
|
||
|
||
let _ = service.submit_order(sell_request).await?;
|
||
println!(" 2. Position closed: Sold 100 AMD");
|
||
|
||
// 3. Verify position is zero or removed
|
||
let positions_request = Request::new(GetPositionsRequest {
|
||
account_id: Some(account_id.to_string()),
|
||
symbol: Some("AMD".to_string()),
|
||
});
|
||
|
||
let positions_response = service.get_positions(positions_request).await?;
|
||
let positions = positions_response.into_inner();
|
||
|
||
println!(" 3. Positions after close: {}", positions.positions.len());
|
||
for pos in &positions.positions {
|
||
println!(" {}: {} shares", pos.symbol, pos.quantity);
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_partial_position_close() -> Result<()> {
|
||
println!("\n=== Test: Partial Position Close ===");
|
||
|
||
let service = setup_trading_service().await?;
|
||
let account_id = "position_test_005";
|
||
|
||
// 1. Open position with 200 shares
|
||
let mut metadata = std::collections::HashMap::new();
|
||
metadata.insert("time_in_force".to_string(), "GTC".to_string());
|
||
|
||
let buy_request = Request::new(SubmitOrderRequest {
|
||
account_id: account_id.to_string(),
|
||
symbol: "SPY".to_string(),
|
||
side: OrderSide::Buy as i32,
|
||
order_type: OrderType::Market as i32,
|
||
quantity: 200.0,
|
||
price: None,
|
||
stop_price: None,
|
||
metadata: metadata.clone(),
|
||
});
|
||
|
||
let _ = service.submit_order(buy_request).await?;
|
||
println!(" 1. Position opened: 200 SPY");
|
||
|
||
// 2. Partially close (sell 75 shares)
|
||
let sell_request = Request::new(SubmitOrderRequest {
|
||
account_id: account_id.to_string(),
|
||
symbol: "SPY".to_string(),
|
||
side: OrderSide::Sell as i32,
|
||
order_type: OrderType::Market as i32,
|
||
quantity: 75.0,
|
||
price: None,
|
||
stop_price: None,
|
||
metadata,
|
||
});
|
||
|
||
let _ = service.submit_order(sell_request).await?;
|
||
println!(" 2. Partially closed: Sold 75 SPY");
|
||
|
||
// 3. Check remaining position (should be ~125 shares)
|
||
let positions_request = Request::new(GetPositionsRequest {
|
||
account_id: Some(account_id.to_string()),
|
||
symbol: Some("SPY".to_string()),
|
||
});
|
||
|
||
let positions_response = service.get_positions(positions_request).await?;
|
||
let positions = positions_response.into_inner();
|
||
|
||
println!(" 3. Remaining position:");
|
||
for pos in &positions.positions {
|
||
println!(" {}: {} shares", pos.symbol, pos.quantity);
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
// ============================================================================
|
||
// PnL Tracking Tests
|
||
// ============================================================================
|
||
|
||
#[tokio::test]
|
||
async fn test_portfolio_summary() -> Result<()> {
|
||
println!("\n=== Test: Portfolio Summary ===");
|
||
|
||
let service = setup_trading_service().await?;
|
||
let account_id = "position_test_006";
|
||
|
||
// Open some positions
|
||
let symbols = vec![("AAPL", 50.0), ("GOOGL", 25.0)];
|
||
|
||
for (symbol, quantity) in &symbols {
|
||
let mut metadata = std::collections::HashMap::new();
|
||
metadata.insert("time_in_force".to_string(), "GTC".to_string());
|
||
|
||
let request = Request::new(SubmitOrderRequest {
|
||
account_id: account_id.to_string(),
|
||
symbol: symbol.to_string(),
|
||
side: OrderSide::Buy as i32,
|
||
order_type: OrderType::Market as i32,
|
||
quantity: *quantity,
|
||
price: None,
|
||
stop_price: None,
|
||
metadata,
|
||
});
|
||
|
||
let _ = service.submit_order(request).await?;
|
||
}
|
||
|
||
// Get portfolio summary
|
||
let summary_request = Request::new(GetPortfolioSummaryRequest {
|
||
account_id: account_id.to_string(),
|
||
});
|
||
|
||
let summary_response = service.get_portfolio_summary(summary_request).await?;
|
||
let summary = summary_response.into_inner();
|
||
|
||
println!("\n Portfolio Summary:");
|
||
println!(" ├─ Total Value: ${:.2}", summary.total_value);
|
||
println!(" ├─ Unrealized PnL: ${:.2}", summary.unrealized_pnl);
|
||
println!(" ├─ Realized PnL: ${:.2}", summary.realized_pnl);
|
||
println!(" ├─ Day PnL: ${:.2}", summary.day_pnl);
|
||
println!(" ├─ Buying Power: ${:.2}", summary.buying_power);
|
||
println!(" ├─ Margin Used: ${:.2}", summary.margin_used);
|
||
println!(" └─ Positions: {}", summary.positions.len());
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_unrealized_pnl_calculation() -> Result<()> {
|
||
println!("\n=== Test: Unrealized PnL Calculation ===");
|
||
|
||
let service = setup_trading_service().await?;
|
||
let account_id = "position_test_007";
|
||
|
||
// Open position
|
||
let mut metadata = std::collections::HashMap::new();
|
||
metadata.insert("time_in_force".to_string(), "GTC".to_string());
|
||
metadata.insert("entry_price".to_string(), "100.00".to_string());
|
||
|
||
let request = Request::new(SubmitOrderRequest {
|
||
account_id: account_id.to_string(),
|
||
symbol: "AAPL".to_string(),
|
||
side: OrderSide::Buy as i32,
|
||
order_type: OrderType::Limit as i32,
|
||
quantity: 100.0,
|
||
price: Some(100.00), // Entry price
|
||
stop_price: None,
|
||
metadata,
|
||
});
|
||
|
||
let _ = service.submit_order(request).await?;
|
||
println!(" Position opened at $100.00");
|
||
|
||
// Get position and check unrealized PnL
|
||
let positions_request = Request::new(GetPositionsRequest {
|
||
account_id: Some(account_id.to_string()),
|
||
symbol: Some("AAPL".to_string()),
|
||
});
|
||
|
||
let positions_response = service.get_positions(positions_request).await?;
|
||
let positions = positions_response.into_inner();
|
||
|
||
for pos in &positions.positions {
|
||
println!("\n Position Details:");
|
||
println!(" ├─ Symbol: {}", pos.symbol);
|
||
println!(" ├─ Quantity: {}", pos.quantity);
|
||
println!(" ├─ Average Price: ${:.2}", pos.average_price);
|
||
// Note: current_price not in proto, using market_value/quantity
|
||
let current_price = if pos.quantity != 0.0 { pos.market_value / pos.quantity.abs() } else { 0.0 };
|
||
println!(" ├─ Current Price: ${:.2}", current_price);
|
||
println!(" ├─ Unrealized PnL: ${:.2}", pos.unrealized_pnl);
|
||
println!(" └─ Total Value: ${:.2}", pos.market_value);
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_realized_pnl_on_close() -> Result<()> {
|
||
println!("\n=== Test: Realized PnL on Close ===");
|
||
|
||
let service = setup_trading_service().await?;
|
||
let account_id = "position_test_008";
|
||
|
||
// 1. Buy at specific price
|
||
let mut buy_metadata = std::collections::HashMap::new();
|
||
buy_metadata.insert("time_in_force".to_string(), "GTC".to_string());
|
||
|
||
let buy_request = Request::new(SubmitOrderRequest {
|
||
account_id: account_id.to_string(),
|
||
symbol: "MSFT".to_string(),
|
||
side: OrderSide::Buy as i32,
|
||
order_type: OrderType::Limit as i32,
|
||
quantity: 50.0,
|
||
price: Some(350.00),
|
||
stop_price: None,
|
||
metadata: buy_metadata,
|
||
});
|
||
|
||
let _ = service.submit_order(buy_request).await?;
|
||
println!(" 1. Bought 50 MSFT at $350.00");
|
||
|
||
// 2. Sell at different price
|
||
let mut sell_metadata = std::collections::HashMap::new();
|
||
sell_metadata.insert("time_in_force".to_string(), "GTC".to_string());
|
||
|
||
let sell_request = Request::new(SubmitOrderRequest {
|
||
account_id: account_id.to_string(),
|
||
symbol: "MSFT".to_string(),
|
||
side: OrderSide::Sell as i32,
|
||
order_type: OrderType::Limit as i32,
|
||
quantity: 50.0,
|
||
price: Some(360.00),
|
||
stop_price: None,
|
||
metadata: sell_metadata,
|
||
});
|
||
|
||
let _ = service.submit_order(sell_request).await?;
|
||
println!(" 2. Sold 50 MSFT at $360.00");
|
||
|
||
// 3. Check portfolio summary for realized PnL
|
||
let summary_request = Request::new(GetPortfolioSummaryRequest {
|
||
account_id: account_id.to_string(),
|
||
});
|
||
|
||
let summary_response = service.get_portfolio_summary(summary_request).await?;
|
||
let summary = summary_response.into_inner();
|
||
|
||
println!("\n Realized PnL: ${:.2}", summary.realized_pnl);
|
||
println!(" Expected: $500.00 (50 shares × $10 profit)");
|
||
|
||
Ok(())
|
||
}
|
||
|
||
// ============================================================================
|
||
// Position Reconciliation Tests
|
||
// ============================================================================
|
||
|
||
#[tokio::test]
|
||
async fn test_position_average_price_calculation() -> Result<()> {
|
||
println!("\n=== Test: Position Average Price Calculation ===");
|
||
|
||
let service = setup_trading_service().await?;
|
||
let account_id = "position_test_009";
|
||
|
||
// Buy in multiple lots at different prices
|
||
let lots = vec![
|
||
(50.0, 100.00),
|
||
(30.0, 105.00),
|
||
(20.0, 98.00),
|
||
];
|
||
|
||
for (quantity, price) in &lots {
|
||
let mut metadata = std::collections::HashMap::new();
|
||
metadata.insert("time_in_force".to_string(), "GTC".to_string());
|
||
|
||
let request = Request::new(SubmitOrderRequest {
|
||
account_id: account_id.to_string(),
|
||
symbol: "NVDA".to_string(),
|
||
side: OrderSide::Buy as i32,
|
||
order_type: OrderType::Limit as i32,
|
||
quantity: *quantity,
|
||
price: Some(*price),
|
||
stop_price: None,
|
||
metadata,
|
||
});
|
||
|
||
let _ = service.submit_order(request).await?;
|
||
println!(" Bought {} NVDA at ${:.2}", quantity, price);
|
||
}
|
||
|
||
// Check average entry price
|
||
let positions_request = Request::new(GetPositionsRequest {
|
||
account_id: Some(account_id.to_string()),
|
||
symbol: Some("NVDA".to_string()),
|
||
});
|
||
|
||
let positions_response = service.get_positions(positions_request).await?;
|
||
let positions = positions_response.into_inner();
|
||
|
||
for pos in &positions.positions {
|
||
let expected_avg = (50.0 * 100.00 + 30.0 * 105.00 + 20.0 * 98.00) / 100.0;
|
||
println!("\n Total Position: {} shares", pos.quantity);
|
||
println!(" Average Entry Price: ${:.2}", pos.average_price);
|
||
println!(" Expected: ${:.2}", expected_avg);
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_zero_position_after_equal_buys_sells() -> Result<()> {
|
||
println!("\n=== Test: Zero Position After Equal Buys/Sells ===");
|
||
|
||
let service = setup_trading_service().await?;
|
||
let account_id = "position_test_010";
|
||
|
||
let quantity = 100.0;
|
||
|
||
// Buy
|
||
let mut metadata = std::collections::HashMap::new();
|
||
metadata.insert("time_in_force".to_string(), "GTC".to_string());
|
||
|
||
let buy_request = Request::new(SubmitOrderRequest {
|
||
account_id: account_id.to_string(),
|
||
symbol: "AMD".to_string(),
|
||
side: OrderSide::Buy as i32,
|
||
order_type: OrderType::Market as i32,
|
||
quantity,
|
||
price: None,
|
||
stop_price: None,
|
||
metadata: metadata.clone(),
|
||
});
|
||
|
||
let _ = service.submit_order(buy_request).await?;
|
||
println!(" Bought {} AMD", quantity);
|
||
|
||
// Sell equal amount
|
||
let sell_request = Request::new(SubmitOrderRequest {
|
||
account_id: account_id.to_string(),
|
||
symbol: "AMD".to_string(),
|
||
side: OrderSide::Sell as i32,
|
||
order_type: OrderType::Market as i32,
|
||
quantity,
|
||
price: None,
|
||
stop_price: None,
|
||
metadata,
|
||
});
|
||
|
||
let _ = service.submit_order(sell_request).await?;
|
||
println!(" Sold {} AMD", quantity);
|
||
|
||
// Position should be zero
|
||
let positions_request = Request::new(GetPositionsRequest {
|
||
account_id: Some(account_id.to_string()),
|
||
symbol: Some("AMD".to_string()),
|
||
});
|
||
|
||
let positions_response = service.get_positions(positions_request).await?;
|
||
let positions = positions_response.into_inner();
|
||
|
||
println!("\n Positions: {}", positions.positions.len());
|
||
for pos in &positions.positions {
|
||
println!(" {}: {} shares (should be 0 or absent)", pos.symbol, pos.quantity);
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
// ============================================================================
|
||
// Edge Cases and Error Handling
|
||
// ============================================================================
|
||
|
||
#[tokio::test]
|
||
async fn test_get_positions_empty_account() -> Result<()> {
|
||
println!("\n=== Test: Get Positions - Empty Account ===");
|
||
|
||
let service = setup_trading_service().await?;
|
||
|
||
let request = Request::new(GetPositionsRequest {
|
||
account_id: Some("empty_account_999".to_string()),
|
||
symbol: None,
|
||
});
|
||
|
||
let response = service.get_positions(request).await?;
|
||
let positions = response.into_inner();
|
||
|
||
println!(" Positions for empty account: {}", positions.positions.len());
|
||
assert_eq!(positions.positions.len(), 0);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_get_positions_nonexistent_symbol() -> Result<()> {
|
||
println!("\n=== Test: Get Positions - Nonexistent Symbol ===");
|
||
|
||
let service = setup_trading_service().await?;
|
||
let account_id = "position_test_011";
|
||
|
||
// Open position in AAPL
|
||
let mut metadata = std::collections::HashMap::new();
|
||
metadata.insert("time_in_force".to_string(), "GTC".to_string());
|
||
|
||
let request = Request::new(SubmitOrderRequest {
|
||
account_id: account_id.to_string(),
|
||
symbol: "AAPL".to_string(),
|
||
side: OrderSide::Buy as i32,
|
||
order_type: OrderType::Market as i32,
|
||
quantity: 10.0,
|
||
price: None,
|
||
stop_price: None,
|
||
metadata,
|
||
});
|
||
|
||
let _ = service.submit_order(request).await?;
|
||
|
||
// Query for different symbol
|
||
let positions_request = Request::new(GetPositionsRequest {
|
||
account_id: Some(account_id.to_string()),
|
||
symbol: Some("NONEXISTENT".to_string()),
|
||
});
|
||
|
||
let positions_response = service.get_positions(positions_request).await?;
|
||
let positions = positions_response.into_inner();
|
||
|
||
println!(" Positions for nonexistent symbol: {}", positions.positions.len());
|
||
assert_eq!(positions.positions.len(), 0);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_position_with_fractional_shares() -> Result<()> {
|
||
println!("\n=== Test: Position with Fractional Shares ===");
|
||
|
||
let service = setup_trading_service().await?;
|
||
let account_id = "position_test_012";
|
||
|
||
let mut metadata = std::collections::HashMap::new();
|
||
metadata.insert("time_in_force".to_string(), "GTC".to_string());
|
||
metadata.insert("allow_fractional".to_string(), "true".to_string());
|
||
|
||
let request = Request::new(SubmitOrderRequest {
|
||
account_id: account_id.to_string(),
|
||
symbol: "TSLA".to_string(),
|
||
side: OrderSide::Buy as i32,
|
||
order_type: OrderType::Market as i32,
|
||
quantity: 15.75, // Fractional shares
|
||
price: None,
|
||
stop_price: None,
|
||
metadata,
|
||
});
|
||
|
||
let response = service.submit_order(request).await?;
|
||
println!(" Fractional order: {}", response.into_inner().order_id);
|
||
|
||
let positions_request = Request::new(GetPositionsRequest {
|
||
account_id: Some(account_id.to_string()),
|
||
symbol: Some("TSLA".to_string()),
|
||
});
|
||
|
||
let positions_response = service.get_positions(positions_request).await?;
|
||
let positions = positions_response.into_inner();
|
||
|
||
for pos in &positions.positions {
|
||
println!(" Position: {} shares of {}", pos.quantity, pos.symbol);
|
||
}
|
||
|
||
Ok(())
|
||
}
|