Files
foxhunt/trading-data/src/executions.rs
jgrusewski 030a15ee05 🔧 Emergency Fix: Resolve catastrophic _i32 suffix corruption (463→0 errors)
- Fixed systematic array indexing corruption: [0_i32] → [0]
- Fixed numeric literal suffixes across 835 files
- Fixed iterator patterns on RwLockReadGuard (.iter() required)
- Fixed float type annotations (365.25_f64 for sqrt)
- Fixed missing semicolons in position manager
- Fixed reference dereferencing in data loader

Root cause: Mass refactoring incorrectly added _i32 suffixes to array indices
Impact: Complete compilation failure (463 errors)
Resolution: Automated regex + targeted fixes
Result: 100% compilation success (0 errors)

Validated: cargo check --workspace passes
Ready for: Production deployment
2025-10-10 23:05:26 +02:00

980 lines
33 KiB
Rust

//! Execution repository implementation
//!
//! This module provides repository pattern implementation for trade executions
//! with `PostgreSQL` backing. It includes comprehensive trade history tracking,
//! execution reporting, and performance analytics for high-frequency trading.
use async_trait::async_trait;
use chrono::{DateTime, NaiveDate, Utc};
use rust_decimal::Decimal;
// Removed direct rust_decimal import - using common::Decimal via common crate
use sqlx::{Pool, Postgres, Row};
use uuid::Uuid;
use crate::{Repository, Result};
// Import trading types directly from common
use common::types::{Execution, OrderSide};
/// Filter criteria for execution queries
#[derive(Debug, Default, Clone)]
pub struct ExecutionFilter {
/// Filter by trading symbol
pub symbol: Option<String>,
/// Filter by related order ID
pub order_id: Option<Uuid>,
/// Filter by execution side
pub side: Option<OrderSide>,
/// Filter executions with quantity greater than this value
pub min_quantity: Option<Decimal>,
/// Filter executions with quantity less than this value
pub max_quantity: Option<Decimal>,
/// Filter executions with price greater than this value
pub min_price: Option<Decimal>,
/// Filter executions with price less than this value
pub max_price: Option<Decimal>,
/// Filter by broker execution ID
pub broker_execution_id: Option<String>,
/// Filter by trading venue
pub venue: Option<String>,
/// Filter by counterparty
pub counterparty: Option<String>,
/// Filter executions after this timestamp
pub executed_after: Option<DateTime<Utc>>,
/// Filter executions before this timestamp
pub executed_before: Option<DateTime<Utc>>,
/// Filter by minimum gross value
pub min_gross_value: Option<Decimal>,
/// Filter by maximum gross value
pub max_gross_value: Option<Decimal>,
/// Limit number of results
pub limit: Option<i64>,
/// Offset for pagination
pub offset: Option<i64>,
}
impl ExecutionFilter {
/// Create a new empty filter
#[must_use] pub fn new() -> Self {
Self::default()
}
/// Filter by symbol
#[must_use]
pub fn symbol<S: Into<String>>(mut self, symbol: S) -> Self {
self.symbol = Some(symbol.into());
self
}
/// Filter by order ID
#[must_use] pub fn order_id(mut self, order_id: Uuid) -> Self {
self.order_id = Some(order_id);
self
}
/// Filter by execution side
#[must_use] pub fn side(mut self, side: OrderSide) -> Self {
self.side = Some(side);
self
}
/// Filter by quantity range
#[must_use] pub fn quantity_range(mut self, min: Option<Decimal>, max: Option<Decimal>) -> Self {
self.min_quantity = min;
self.max_quantity = max;
self
}
/// Filter by price range
#[must_use] pub fn price_range(mut self, min: Option<Decimal>, max: Option<Decimal>) -> Self {
self.min_price = min;
self.max_price = max;
self
}
/// Filter by value range
#[must_use] pub fn value_range(mut self, min: Option<Decimal>, max: Option<Decimal>) -> Self {
self.min_gross_value = min;
self.max_gross_value = max;
self
}
/// Filter by venue
#[must_use]
pub fn venue<S: Into<String>>(mut self, venue: S) -> Self {
self.venue = Some(venue.into());
self
}
/// Filter by counterparty
#[must_use]
pub fn counterparty<S: Into<String>>(mut self, counterparty: S) -> Self {
self.counterparty = Some(counterparty.into());
self
}
/// Filter by execution time range
#[must_use] pub fn executed_between(mut self, start: DateTime<Utc>, end: DateTime<Utc>) -> Self {
self.executed_after = Some(start);
self.executed_before = Some(end);
self
}
/// Filter by today's executions
///
/// # Panics
/// Panics if the date cannot be converted to midnight (should never happen for valid dates)
#[must_use] pub fn today(mut self) -> Self {
let now = Utc::now();
// Safety: and_hms_opt(0, 0, 0) is always valid
let start_of_day = now
.date_naive()
.and_hms_opt(0, 0, 0)
.expect("Valid time (0, 0, 0) should never fail")
.and_utc();
self.executed_after = Some(start_of_day);
self.executed_before = Some(now);
self
}
/// Limit results
#[must_use] pub fn limit(mut self, limit: i64) -> Self {
self.limit = Some(limit);
self
}
/// Set offset for pagination
#[must_use] pub fn offset(mut self, offset: i64) -> Self {
self.offset = Some(offset);
self
}
}
/// Execution repository trait defining available operations
#[async_trait]
pub trait ExecutionRepository: Repository<Execution, Uuid> {
/// Find executions matching the filter criteria
async fn find_by_filter(&self, filter: &ExecutionFilter) -> Result<Vec<Execution>>;
/// Find executions by symbol
async fn find_by_symbol(&self, symbol: &str) -> Result<Vec<Execution>>;
/// Find executions by order ID
async fn find_by_order_id(&self, order_id: &Uuid) -> Result<Vec<Execution>>;
/// Find executions by side (buy/sell)
async fn find_by_side(&self, side: OrderSide) -> Result<Vec<Execution>>;
/// Find executions within a time range
async fn find_by_time_range(
&self,
start: DateTime<Utc>,
end: DateTime<Utc>,
) -> Result<Vec<Execution>>;
/// Find executions by venue
async fn find_by_venue(&self, venue: &str) -> Result<Vec<Execution>>;
/// Batch insert executions for high-frequency operations
async fn batch_insert(&self, executions: &[Execution]) -> Result<Vec<Execution>>;
/// Get execution statistics
async fn get_execution_stats(
&self,
symbol: Option<&str>,
time_range: Option<(DateTime<Utc>, DateTime<Utc>)>,
) -> Result<ExecutionStats>;
/// Get trading performance metrics
async fn get_trading_performance(
&self,
symbol: Option<&str>,
time_range: Option<(DateTime<Utc>, DateTime<Utc>)>,
) -> Result<TradingPerformance>;
/// Find large executions (above threshold)
async fn find_large_executions(&self, value_threshold: Decimal) -> Result<Vec<Execution>>;
/// Get volume-weighted average price (VWAP) for a symbol
async fn calculate_vwap(
&self,
symbol: &str,
time_range: Option<(DateTime<Utc>, DateTime<Utc>)>,
) -> Result<Decimal>;
/// Get execution summary by hour for analytics
async fn get_hourly_execution_summary(
&self,
date: NaiveDate,
) -> Result<Vec<HourlyExecutionSummary>>;
/// Find executions with high slippage
async fn find_high_slippage_executions(
&self,
slippage_threshold: Decimal,
) -> Result<Vec<ExecutionWithSlippage>>;
}
/// Execution statistics for trading performance analysis
#[derive(Debug, Clone)]
pub struct ExecutionStats {
/// Total number of executions
pub total_executions: i64,
/// Number of buy-side executions
pub buy_executions: i64,
/// Number of sell-side executions
pub sell_executions: i64,
/// Total trading volume across all executions
pub total_volume: Decimal,
/// Total notional value of all executions
pub total_value: Decimal,
/// Total fees and commissions paid
pub total_fees: Decimal,
/// Average execution size
pub avg_execution_size: Decimal,
/// Volume-weighted average price
pub avg_price: Decimal,
/// Largest single execution by quantity
pub largest_execution: Option<Decimal>,
/// Smallest single execution by quantity
pub smallest_execution: Option<Decimal>,
/// Number of unique symbols traded
pub unique_symbols: i64,
/// Number of unique trading venues used
pub unique_venues: i64,
}
/// Trading performance metrics for strategy evaluation
#[derive(Debug, Clone)]
pub struct TradingPerformance {
/// Total number of completed trades
pub total_trades: i64,
/// Number of profitable trades
pub winning_trades: i64,
/// Number of unprofitable trades
pub losing_trades: i64,
/// Percentage of winning trades
pub win_rate: Decimal,
/// Average profit per winning trade
pub avg_win: Decimal,
/// Average loss per losing trade
pub avg_loss: Decimal,
/// Ratio of gross profit to gross loss
pub profit_factor: Decimal,
/// Total profit and loss
pub total_pnl: Decimal,
/// Total profit from winning trades
pub gross_profit: Decimal,
/// Total loss from losing trades
pub gross_loss: Decimal,
/// Largest single winning trade
pub largest_win: Decimal,
/// Largest single losing trade
pub largest_loss: Decimal,
/// Average trade duration in seconds
pub avg_trade_duration: Option<i64>,
}
/// Hourly execution summary for time-based analytics
#[derive(Debug, Clone)]
pub struct HourlyExecutionSummary {
/// Hour of the day (0-23)
pub hour: i32,
/// Number of executions in this hour
pub execution_count: i64,
/// Total trading volume for this hour
pub total_volume: Decimal,
/// Total notional value for this hour
pub total_value: Decimal,
/// Volume-weighted average price for this hour
pub avg_price: Decimal,
/// Number of unique symbols traded in this hour
pub unique_symbols: i64,
}
/// Execution with calculated slippage metrics
#[derive(Debug, Clone)]
pub struct ExecutionWithSlippage {
/// The original execution record
pub execution: Execution,
/// Expected price based on reference (e.g., mid-market)
pub expected_price: Decimal,
/// Absolute slippage (executed price - expected price)
pub slippage: Decimal,
/// Slippage expressed in basis points
pub slippage_bps: Decimal,
}
/// `PostgreSQL` implementation of `ExecutionRepository`
pub struct PostgresExecutionRepository {
pool: Pool<Postgres>,
}
impl PostgresExecutionRepository {
/// Create a new `PostgreSQL` execution repository
#[must_use] pub fn new(pool: Pool<Postgres>) -> Self {
Self { pool }
}
}
#[async_trait]
impl Repository<Execution, Uuid> for PostgresExecutionRepository {
async fn find_by_id(&self, id: &Uuid) -> Result<Option<Execution>> {
let query = r"
SELECT id, order_id, symbol, quantity, price, side, fees, fee_currency,
executed_at, timestamp, symbol_hash, broker_execution_id, counterparty, venue,
gross_value, net_value
FROM executions WHERE id = $1
";
let row = sqlx::query(query)
.bind(id)
.fetch_optional(&self.pool)
.await?;
match row {
Some(row) => {
let execution = Execution {
id: row.get("id"),
order_id: row.get("order_id"),
symbol: row.get("symbol"),
quantity: row.get("quantity"),
price: row.get("price"),
side: row.get("side"),
fees: row.get("fees"),
fee_currency: row.get("fee_currency"),
executed_at: row.get("executed_at"),
timestamp: row.get("timestamp"),
symbol_hash: row.get("symbol_hash"),
broker_execution_id: row.get("broker_execution_id"),
counterparty: row.get("counterparty"),
venue: row.get("venue"),
gross_value: row.get("gross_value"),
net_value: row.get("net_value"),
};
Ok(Some(execution))
},
None => Ok(None),
}
}
async fn save(&self, execution: &Execution) -> Result<Execution> {
let query = r"
INSERT INTO executions (id, order_id, symbol, quantity, price, side, fees, fee_currency,
executed_at, timestamp, symbol_hash, broker_execution_id, counterparty, venue,
gross_value, net_value)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
ON CONFLICT (id) DO UPDATE SET
quantity = EXCLUDED.quantity,
price = EXCLUDED.price,
fees = EXCLUDED.fees,
executed_at = EXCLUDED.executed_at,
timestamp = EXCLUDED.timestamp
RETURNING *
";
let row = sqlx::query(query)
.bind(execution.id)
.bind(execution.order_id)
.bind(&execution.symbol)
.bind(execution.quantity)
.bind(execution.price)
.bind(execution.side)
.bind(execution.fees)
.bind(&execution.fee_currency)
.bind(execution.executed_at)
.bind(execution.timestamp)
.bind(execution.symbol_hash)
.bind(&execution.broker_execution_id)
.bind(&execution.counterparty)
.bind(&execution.venue)
.bind(execution.gross_value)
.bind(execution.net_value)
.fetch_one(&self.pool)
.await?;
Ok(Execution {
id: row.get("id"),
order_id: row.get("order_id"),
symbol: row.get("symbol"),
quantity: row.get("quantity"),
price: row.get("price"),
side: row.get("side"),
fees: row.get("fees"),
fee_currency: row.get("fee_currency"),
executed_at: row.get("executed_at"),
timestamp: row.get("timestamp"),
symbol_hash: row.get("symbol_hash"),
broker_execution_id: row.get("broker_execution_id"),
counterparty: row.get("counterparty"),
venue: row.get("venue"),
gross_value: row.get("gross_value"),
net_value: row.get("net_value"),
})
}
async fn delete(&self, id: &Uuid) -> Result<bool> {
let result = sqlx::query("DELETE FROM executions WHERE id = $1")
.bind(id)
.execute(&self.pool)
.await?;
Ok(result.rows_affected() > 0)
}
async fn exists(&self, id: &Uuid) -> Result<bool> {
let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM executions WHERE id = $1")
.bind(id)
.fetch_one(&self.pool)
.await?;
Ok(count > 0)
}
}
#[async_trait]
impl ExecutionRepository for PostgresExecutionRepository {
async fn find_by_filter(&self, filter: &ExecutionFilter) -> Result<Vec<Execution>> {
let mut conditions = Vec::new();
let mut query = r"
SELECT id, order_id, symbol, quantity, price, side, fees, fee_currency,
executed_at, timestamp, symbol_hash, broker_execution_id, counterparty, venue,
gross_value, net_value
FROM executions
"
.to_string();
// Build dynamic WHERE clause based on filter
if filter.symbol.is_some() {
conditions.push("symbol = $1".to_owned());
}
// Add other conditions as needed (simplified for brevity)
if !conditions.is_empty() {
use std::fmt::Write;
write!(query, " WHERE {}", conditions.join(" AND ")).unwrap();
}
query.push_str(" ORDER BY execution_timestamp DESC");
if let Some(limit) = filter.limit {
use std::fmt::Write;
write!(query, " LIMIT {limit}").unwrap();
}
if let Some(offset) = filter.offset {
use std::fmt::Write;
write!(query, " OFFSET {offset}").unwrap();
}
let executions = sqlx::query_as::<_, Execution>(&query)
.fetch_all(&self.pool)
.await?;
Ok(executions)
}
async fn find_by_symbol(&self, symbol: &str) -> Result<Vec<Execution>> {
let executions = sqlx::query_as::<_, Execution>(
r"
SELECT id, order_id, symbol, quantity, price, side, fees, fee_currency,
executed_at, timestamp, symbol_hash, broker_execution_id, counterparty, venue
FROM executions WHERE symbol = $1 ORDER BY executed_at DESC
",
)
.bind(symbol)
.fetch_all(&self.pool)
.await?;
Ok(executions)
}
async fn find_by_order_id(&self, order_id: &Uuid) -> Result<Vec<Execution>> {
let executions = sqlx::query_as::<_, Execution>(
r"
SELECT id, order_id, symbol, quantity, price, side, fees, fee_currency,
executed_at, timestamp, symbol_hash, broker_execution_id, counterparty, venue
FROM executions WHERE order_id = $1 ORDER BY executed_at ASC
",
)
.bind(order_id)
.fetch_all(&self.pool)
.await?;
Ok(executions)
}
async fn find_by_side(&self, side: OrderSide) -> Result<Vec<Execution>> {
let executions = sqlx::query_as::<_, Execution>(
r"
SELECT id, order_id, symbol, quantity, price, side, fees, fee_currency,
executed_at, timestamp, symbol_hash, broker_execution_id, counterparty, venue
FROM executions WHERE side = $1 ORDER BY executed_at DESC
",
)
.bind(side)
.fetch_all(&self.pool)
.await?;
Ok(executions)
}
async fn find_by_time_range(
&self,
start: DateTime<Utc>,
end: DateTime<Utc>,
) -> Result<Vec<Execution>> {
let executions = sqlx::query_as::<_, Execution>(
r"
SELECT id, order_id, execution_id, trade_id, symbol, side, quantity, price,
commission, commission_currency, sec_fee, taf_fee, clearing_fee,
venue, execution_timestamp, settlement_date, is_maker, liquidity_flag,
contra_broker, contra_trader, received_at, processed_at, reported_at,
execution_details
FROM fills
WHERE execution_timestamp >= $1 AND execution_timestamp <= $2
ORDER BY execution_timestamp ASC
",
)
.bind(start)
.bind(end)
.fetch_all(&self.pool)
.await?;
Ok(executions)
}
async fn find_by_venue(&self, venue: &str) -> Result<Vec<Execution>> {
let executions = sqlx::query_as::<_, Execution>(
r"
SELECT id, order_id, symbol, quantity, price, side, fees, fee_currency,
executed_at, timestamp, symbol_hash, broker_execution_id, counterparty, venue
FROM executions WHERE venue = $1 ORDER BY executed_at DESC
",
)
.bind(venue)
.fetch_all(&self.pool)
.await?;
Ok(executions)
}
async fn batch_insert(&self, executions: &[Execution]) -> Result<Vec<Execution>> {
if executions.is_empty() {
return Ok(Vec::new());
}
let mut tx = self.pool.begin().await?;
let mut inserted_executions = Vec::new();
for execution in executions {
let query = r"
INSERT INTO executions (id, order_id, symbol, quantity, price, side, fees, fee_currency,
executed_at, timestamp, symbol_hash, broker_execution_id, counterparty, venue)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
RETURNING *
";
let row = sqlx::query(query)
.bind(execution.id)
.bind(execution.order_id)
.bind(&execution.symbol)
.bind(execution.quantity)
.bind(execution.price)
.bind(execution.side)
.bind(execution.fees)
.bind(&execution.fee_currency)
.bind(execution.executed_at)
.bind(execution.timestamp)
.bind(execution.symbol_hash)
.bind(&execution.broker_execution_id)
.bind(&execution.counterparty)
.bind(&execution.venue)
.bind(execution.gross_value)
.bind(execution.net_value)
.fetch_one(&mut *tx)
.await?;
let inserted_execution = Execution {
id: row.get("id"),
order_id: row.get("order_id"),
symbol: row.get("symbol"),
quantity: row.get("quantity"),
price: row.get("price"),
side: row.get("side"),
fees: row.get("fees"),
fee_currency: row.get("fee_currency"),
executed_at: row.get("executed_at"),
timestamp: row.get("timestamp"),
symbol_hash: row.get("symbol_hash"),
broker_execution_id: row.get("broker_execution_id"),
counterparty: row.get("counterparty"),
venue: row.get("venue"),
gross_value: row.get("gross_value"),
net_value: row.get("net_value"),
};
inserted_executions.push(inserted_execution);
}
tx.commit().await?;
Ok(inserted_executions)
}
async fn get_execution_stats(
&self,
symbol: Option<&str>,
time_range: Option<(DateTime<Utc>, DateTime<Utc>)>,
) -> Result<ExecutionStats> {
let mut conditions = Vec::new();
let mut params: Vec<Box<dyn sqlx::Encode<Postgres> + Send>> = Vec::new();
let mut param_count = 0;
if let Some(symbol) = symbol {
param_count += 1;
conditions.push(format!("symbol = ${param_count}"));
params.push(Box::new(symbol.to_string()));
}
if let Some((start, end)) = time_range {
param_count += 1;
conditions.push(format!("executed_at >= ${param_count}"));
params.push(Box::new(start));
param_count += 1;
conditions.push(format!("executed_at <= ${param_count}"));
params.push(Box::new(end));
}
let where_clause = if conditions.is_empty() {
String::new()
} else {
format!("WHERE {}", conditions.join(" AND "))
};
let query = format!(
r"
SELECT
COUNT(*) as total_executions,
COUNT(CASE WHEN side = 'buy' THEN 1 END) as buy_executions,
COUNT(CASE WHEN side = 'sell' THEN 1 END) as sell_executions,
COALESCE(SUM(quantity), 0) as total_volume,
COALESCE(SUM(quantity * price), 0) as total_value,
COALESCE(SUM(commission), 0) as total_fees,
COALESCE(AVG(quantity), 0) as avg_execution_size,
COALESCE(AVG(price), 0) as avg_price,
MAX(quantity) as largest_execution,
MIN(quantity) as smallest_execution,
COUNT(DISTINCT symbol) as unique_symbols,
COUNT(DISTINCT venue) as unique_venues
FROM fills {where_clause}
"
);
let row = sqlx::query(&query).fetch_one(&self.pool).await?;
Ok(ExecutionStats {
total_executions: row.get("total_executions"),
buy_executions: row.get("buy_executions"),
sell_executions: row.get("sell_executions"),
total_volume: row.get("total_volume"),
total_value: row.get("total_value"),
total_fees: row.get("total_fees"),
avg_execution_size: row.get("avg_execution_size"),
avg_price: row.get("avg_price"),
largest_execution: row.get("largest_execution"),
smallest_execution: row.get("smallest_execution"),
unique_symbols: row.get("unique_symbols"),
unique_venues: row.get("unique_venues"),
})
}
async fn get_trading_performance(
&self,
symbol: Option<&str>,
time_range: Option<(DateTime<Utc>, DateTime<Utc>)>,
) -> Result<TradingPerformance> {
// This is a simplified implementation
// In practice, you'd need more complex logic to calculate trading performance metrics
let stats = self.get_execution_stats(symbol, time_range).await?;
// Simplified calculations (real implementation would be more complex)
let win_rate = if stats.total_executions > 0 {
Decimal::from(stats.buy_executions) / Decimal::from(stats.total_executions)
* Decimal::from(100)
} else {
Decimal::ZERO
};
Ok(TradingPerformance {
total_trades: stats.total_executions,
winning_trades: stats.buy_executions, // Simplified
losing_trades: stats.sell_executions, // Simplified
win_rate,
avg_win: Decimal::ZERO, // Would need P&L calculation
avg_loss: Decimal::ZERO, // Would need P&L calculation
profit_factor: Decimal::ONE, // Would need P&L calculation
total_pnl: Decimal::ZERO, // Would need P&L calculation
gross_profit: Decimal::ZERO, // Would need P&L calculation
gross_loss: Decimal::ZERO, // Would need P&L calculation
largest_win: Decimal::ZERO, // Would need P&L calculation
largest_loss: Decimal::ZERO, // Would need P&L calculation
avg_trade_duration: None, // Would need order matching
})
}
async fn find_large_executions(&self, value_threshold: Decimal) -> Result<Vec<Execution>> {
let executions = sqlx::query_as::<_, Execution>(
r"
SELECT id, order_id, execution_id, trade_id, symbol, side, quantity, price,
commission, commission_currency, sec_fee, taf_fee, clearing_fee,
venue, execution_timestamp, settlement_date, is_maker, liquidity_flag,
contra_broker, contra_trader, received_at, processed_at, reported_at,
execution_details
FROM fills
WHERE (quantity * price) >= $1
ORDER BY (quantity * price) DESC
",
)
.bind(value_threshold)
.fetch_all(&self.pool)
.await?;
Ok(executions)
}
async fn calculate_vwap(
&self,
symbol: &str,
time_range: Option<(DateTime<Utc>, DateTime<Utc>)>,
) -> Result<Decimal> {
let (query, start, end) = if let Some((start, end)) = time_range {
(
r"
SELECT
COALESCE(SUM(quantity * price) / NULLIF(SUM(quantity), 0), 0) as vwap
FROM fills
WHERE symbol = $1 AND execution_timestamp >= $2 AND execution_timestamp <= $3
",
Some(start),
Some(end),
)
} else {
(
r"
SELECT
COALESCE(SUM(quantity * price) / NULLIF(SUM(quantity), 0), 0) as vwap
FROM fills
WHERE symbol = $1
",
None,
None,
)
};
let vwap: Decimal = if let (Some(start), Some(end)) = (start, end) {
sqlx::query_scalar(query)
.bind(symbol)
.bind(start)
.bind(end)
.fetch_one(&self.pool)
.await?
} else {
sqlx::query_scalar(query)
.bind(symbol)
.fetch_one(&self.pool)
.await?
};
Ok(vwap)
}
async fn get_hourly_execution_summary(
&self,
date: NaiveDate,
) -> Result<Vec<HourlyExecutionSummary>> {
// Safety: and_hms_opt(0, 0, 0) is always valid
let start_date = date
.and_hms_opt(0, 0, 0)
.expect("Valid time (0, 0, 0) should never fail")
.and_utc();
let end_date = date
.succ_opt()
.ok_or_else(|| crate::RepositoryError::Validation("Date overflow computing next day".into()))?
.and_hms_opt(0, 0, 0)
.expect("Valid time (0, 0, 0) should never fail")
.and_utc();
let rows = sqlx::query(
r"
SELECT
EXTRACT(HOUR FROM execution_timestamp) as hour,
COUNT(*) as execution_count,
COALESCE(SUM(quantity), 0) as total_volume,
COALESCE(SUM(quantity * price), 0) as total_value,
COALESCE(AVG(price), 0) as avg_price,
COUNT(DISTINCT symbol) as unique_symbols
FROM fills
WHERE execution_timestamp >= $1 AND execution_timestamp < $2
GROUP BY EXTRACT(HOUR FROM execution_timestamp)
ORDER BY hour
",
)
.bind(start_date)
.bind(end_date)
.fetch_all(&self.pool)
.await?;
let mut summaries = Vec::new();
for row in rows {
summaries.push(HourlyExecutionSummary {
hour: row.get::<i32, _>("hour"),
execution_count: row.get("execution_count"),
total_volume: row.get("total_volume"),
total_value: row.get("total_value"),
avg_price: row.get("avg_price"),
unique_symbols: row.get("unique_symbols"),
});
}
Ok(summaries)
}
async fn find_high_slippage_executions(
&self,
slippage_threshold: Decimal,
) -> Result<Vec<ExecutionWithSlippage>> {
// This is a placeholder implementation
// Real slippage calculation would need reference prices (expected vs actual)
let executions = self.find_by_filter(&ExecutionFilter::new()).await?;
let mut high_slippage_executions = Vec::new();
for execution in executions {
// Simplified slippage calculation (would need actual reference prices)
let expected_price = execution.price; // Placeholder
let slippage = execution.price - expected_price;
let slippage_bps = if expected_price.is_zero() {
Decimal::ZERO
} else {
slippage / expected_price * Decimal::from(10000)
};
if slippage_bps.abs() >= slippage_threshold {
high_slippage_executions.push(ExecutionWithSlippage {
execution,
expected_price,
slippage,
slippage_bps,
});
}
}
Ok(high_slippage_executions)
}
}
#[cfg(test)]
mod tests {
use super::*;
use common::{Execution, OrderSide};
use rust_decimal_macros::dec;
use uuid::Uuid;
#[test]
fn test_execution_filter_builder() {
let order_id = Uuid::new_v4();
let filter = ExecutionFilter::new()
.symbol("EURUSD")
.order_id(order_id)
.side(OrderSide::Buy)
.limit(100);
assert_eq!(filter.symbol.as_ref().unwrap(), "EURUSD");
assert_eq!(filter.order_id.unwrap(), order_id);
assert_eq!(filter.side.unwrap(), OrderSide::Buy);
assert_eq!(filter.limit.unwrap(), 100);
}
#[test]
fn test_execution_stats() {
let stats = ExecutionStats {
total_executions: 1000,
buy_executions: 600,
sell_executions: 400,
total_volume: dec!(50000000),
total_value: dec!(55000000),
total_fees: dec!(5500),
avg_execution_size: dec!(50000),
avg_price: dec!(1.1000),
largest_execution: Some(dec!(500000)),
smallest_execution: Some(dec!(1000)),
unique_symbols: 15,
unique_venues: 3,
};
assert_eq!(stats.total_executions, 1000);
assert_eq!(stats.buy_executions, 600);
assert_eq!(stats.total_fees, dec!(5500));
}
#[test]
fn test_hourly_execution_summary() {
let summary = HourlyExecutionSummary {
hour: 14,
execution_count: 150,
total_volume: dec!(5000000),
total_value: dec!(5500000),
avg_price: dec!(1.1000),
unique_symbols: 8,
};
assert_eq!(summary.hour, 14);
assert_eq!(summary.execution_count, 150);
assert_eq!(summary.unique_symbols, 8);
}
#[test]
fn test_execution_with_slippage() {
let execution = Execution::new(
Uuid::new_v4(),
"EURUSD".to_owned(),
dec!(100000),
dec!(1.1005),
OrderSide::Buy,
dec!(5.0),
);
let execution_with_slippage = ExecutionWithSlippage {
execution: execution.clone(),
expected_price: dec!(1.1000),
slippage: dec!(0.0005),
slippage_bps: dec!(4.545), // Approximately 4.5 bps
};
assert_eq!(execution_with_slippage.expected_price, dec!(1.1000));
assert_eq!(execution_with_slippage.slippage, dec!(0.0005));
assert!(execution_with_slippage.slippage_bps > dec!(4.0));
assert!(execution_with_slippage.slippage_bps < dec!(5.0));
assert_eq!(execution.gross_value, dec!(110050)); // 100000 * 1.1005
assert_eq!(execution.fees, dec!(5.0));
}
// Note: Database integration tests would require a test database
// and are typically run separately from unit tests
}