- 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
980 lines
33 KiB
Rust
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
|
|
}
|