Files
foxhunt/market-data/src/indicators.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

684 lines
22 KiB
Rust

//! # Technical Indicators Module
//!
//! This module provides repository abstractions and implementations for storing,
//! retrieving, and managing technical indicator data. It supports various indicator
//! types including moving averages, oscillators, and volume-based indicators.
//!
//! ## Features
//!
//! - Storage and retrieval of multiple indicator types
//! - Batch operations for efficient data processing
//! - Historical data queries with time range filtering
//! - Statistical analysis of indicator values
//! - Data cleanup and maintenance operations
//!
//! ## Usage
//!
//! ```rust
//! use market_data::indicators::{IndicatorRepository, PostgresIndicatorRepository};
//! use market_data::models::{TechnicalIndicator, IndicatorType};
//! use sqlx::PgPool;
//!
//! # async fn example(pool: PgPool) -> Result<(), Box<dyn std::error::Error>> {
//! let repo = PostgresIndicatorRepository::new(pool);
//!
//! // Get latest RSI for a symbol
//! let rsi = repo.get_latest_indicator("AAPL", IndicatorType::Rsi).await?;
//!
//! // Get indicator history
//! let start = Utc::now() - Duration::days(30);
//! let end = Utc::now();
//! let history = repo.get_indicator_history("AAPL", IndicatorType::Rsi, start, end).await?;
//! # Ok(())
//! # }
//! ```
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use sqlx::{PgPool, Row};
use std::collections::HashMap;
use crate::{
error::{MarketDataError, MarketDataResult},
models::{IndicatorType, TechnicalIndicator},
};
/// Repository trait for technical indicator data operations
///
/// This trait defines the interface for storing, retrieving, and managing
/// technical indicator data. Implementations should provide efficient
/// data access patterns optimized for time-series queries.
///
/// The trait supports:
/// - Individual and batch storage operations
///
/// - Historical data retrieval with time filtering
/// - Multi-symbol and multi-indicator queries
///
/// - Statistical analysis and data maintenance
#[async_trait]
pub trait IndicatorRepository {
/// Store a single technical indicator
///
/// Stores a technical indicator value in the repository. If an indicator
/// with the same symbol, type, and timestamp already exists, it will be updated.
///
/// # Arguments
///
/// * `indicator` - The technical indicator to store
///
/// # Returns
///
/// `Ok(())` on success, or a `MarketDataError` if the operation fails
///
/// # Errors
///
/// - `MarketDataError::InvalidSymbol` if the symbol is invalid
/// - `MarketDataError::Database` if the database operation fails
async fn store_indicator(&self, indicator: &TechnicalIndicator) -> MarketDataResult<()>;
/// Store multiple technical indicators in a batch
///
/// Efficiently stores multiple indicators in a single transaction.
///
/// This is optimized for bulk data loading and reduces database overhead.
///
/// # Arguments
///
/// * `indicators` - Slice of technical indicators to store
///
/// # Returns
///
/// `Ok(())` on success, or a `MarketDataError` if any operation fails
///
/// # Errors
///
/// - `MarketDataError::InvalidSymbol` if any symbol is invalid
/// - `MarketDataError::Database` if the database transaction fails
async fn store_indicators(&self, indicators: &[TechnicalIndicator]) -> MarketDataResult<()>;
/// Get the latest indicator value for a symbol and type
///
/// Retrieves the most recent indicator value for the specified symbol
/// and indicator type, ordered by timestamp.
///
/// # Arguments
///
/// * `symbol` - Trading symbol to query
/// * `indicator_type` - Type of technical indicator
///
/// # Returns
///
/// `Some(indicator)` if found, `None` if no data exists, or a `MarketDataError`
///
/// # Errors
///
/// - `MarketDataError::InvalidSymbol` if the symbol is invalid
/// - `MarketDataError::Database` if the query fails
async fn get_latest_indicator(
&self,
symbol: &str,
indicator_type: IndicatorType,
) -> MarketDataResult<Option<TechnicalIndicator>>;
/// Get indicator history for a symbol and type within a time range
///
/// Retrieves historical indicator values within the specified time range,
/// ordered chronologically. This is useful for backtesting and analysis.
///
/// # Arguments
///
/// * `symbol` - Trading symbol to query
/// * `indicator_type` - Type of technical indicator
///
/// * `from` - Start of time range (inclusive)
/// * `to` - End of time range (inclusive)
///
/// # Returns
///
/// Vector of indicators ordered by timestamp, or a `MarketDataError`
///
/// # Errors
///
/// - `MarketDataError::InvalidSymbol` if the symbol is invalid
/// - `MarketDataError::InvalidTimeRange` if from >= to
///
/// - `MarketDataError::Database` if the query fails
async fn get_indicator_history(
&self,
symbol: &str,
indicator_type: IndicatorType,
from: DateTime<Utc>,
to: DateTime<Utc>,
) -> MarketDataResult<Vec<TechnicalIndicator>>;
/// Get all latest indicators for a symbol
///
/// Retrieves the most recent value for each indicator type available
/// for the specified symbol. Returns a map for easy lookup by indicator type.
///
/// # Arguments
///
/// * `symbol` - Trading symbol to query
///
/// # Returns
///
/// HashMap mapping indicator types to their latest values, or a `MarketDataError`
///
/// # Errors
///
/// - `MarketDataError::InvalidSymbol` if the symbol is invalid
/// - `MarketDataError::Database` if the query fails
async fn get_latest_indicators_for_symbol(
&self,
symbol: &str,
) -> MarketDataResult<HashMap<IndicatorType, TechnicalIndicator>>;
/// Get indicators for multiple symbols and a specific type
///
/// Efficiently retrieves the latest indicator values for multiple symbols
/// of the same indicator type. Useful for portfolio analysis and screening.
///
/// # Arguments
///
/// * `symbols` - List of trading symbols to query
/// * `indicator_type` - Type of technical indicator
///
/// # Returns
///
/// HashMap mapping symbols to their latest indicator values, or a `MarketDataError`
///
/// # Errors
///
/// - `MarketDataError::InvalidSymbol` if any symbol is invalid
/// - `MarketDataError::Database` if the query fails
async fn get_indicators_for_symbols(
&self,
symbols: &[String],
indicator_type: IndicatorType,
) -> MarketDataResult<HashMap<String, TechnicalIndicator>>;
/// Get all indicator types available for a symbol
///
/// Returns a list of all indicator types that have data stored
/// for the specified symbol. Useful for discovering available analysis.
///
/// # Arguments
///
/// * `symbol` - Trading symbol to query
///
/// # Returns
///
/// Vector of available indicator types, or a `MarketDataError`
///
/// # Errors
///
/// - `MarketDataError::InvalidSymbol` if the symbol is invalid
/// - `MarketDataError::Database` if the query fails
async fn get_available_indicator_types(
&self,
symbol: &str,
) -> MarketDataResult<Vec<IndicatorType>>;
/// Delete old indicator data before a given timestamp
///
/// Removes historical indicator data older than the specified timestamp.
///
/// This is useful for data retention management and storage optimization.
///
/// # Arguments
///
/// * `before` - Timestamp before which all data will be deleted
///
/// # Returns
///
/// Number of records deleted, or a `MarketDataError`
///
/// # Errors
///
/// - `MarketDataError::Database` if the deletion fails
async fn cleanup_old_indicators(&self, before: DateTime<Utc>) -> MarketDataResult<u64>;
/// Get indicator statistics (min, max, avg) over a time period
///
/// Calculates statistical summary of indicator values within the specified
/// time range. Useful for analysis and understanding indicator behavior.
///
/// # Arguments
///
/// * `symbol` - Trading symbol to analyze
/// * `indicator_type` - Type of technical indicator
///
/// * `from` - Start of analysis period
/// * `to` - End of analysis period
///
/// # Returns
///
/// `Some(statistics)` if data exists, `None` if no data, or a `MarketDataError`
///
/// # Errors
///
/// - `MarketDataError::InvalidSymbol` if the symbol is invalid
/// - `MarketDataError::InvalidTimeRange` if from >= to
///
/// - `MarketDataError::Database` if the query fails
async fn get_indicator_statistics(
&self,
symbol: &str,
indicator_type: IndicatorType,
from: DateTime<Utc>,
to: DateTime<Utc>,
) -> MarketDataResult<Option<IndicatorStatistics>>;
}
/// Statistical summary of indicator values
///
/// Provides comprehensive statistics for technical indicator values
/// over a specified time period, including distribution metrics
/// and temporal boundaries.
#[derive(Debug, Clone)]
pub struct IndicatorStatistics {
/// Trading symbol these statistics apply to
pub symbol: String,
/// Type of technical indicator analyzed
pub indicator_type: IndicatorType,
/// Number of data points in the analysis
pub count: i64,
/// Minimum indicator value in the period
pub min_value: Decimal,
/// Maximum indicator value in the period
pub max_value: Decimal,
/// Average indicator value in the period
pub avg_value: Decimal,
/// Timestamp of the first data point
pub first_timestamp: DateTime<Utc>,
/// Timestamp of the last data point
pub last_timestamp: DateTime<Utc>,
}
/// PostgreSQL implementation of IndicatorRepository
///
/// Provides a production-ready implementation of the `IndicatorRepository` trait
/// using PostgreSQL as the backend storage. This implementation is optimized
/// for time-series data with appropriate indexing and query patterns.
///
/// ## Features
///
/// - Transactional batch operations
/// - Optimized time-series queries
///
/// - Input validation and error handling
/// - Conflict resolution with upsert semantics
pub struct PostgresIndicatorRepository {
/// PostgreSQL connection pool for database operations
pool: PgPool,
}
impl PostgresIndicatorRepository {
/// Create a new PostgreSQL indicator repository
///
/// # Arguments
///
/// * `pool` - PostgreSQL connection pool
///
/// # Returns
///
/// A new `PostgresIndicatorRepository` instance
pub fn new(pool: PgPool) -> Self {
Self { pool }
}
/// Validate that a symbol meets format requirements
///
/// Ensures the symbol is non-empty and within length limits.
///
/// # Arguments
///
/// * `symbol` - Symbol to validate
///
/// # Returns
///
/// `Ok(())` if valid, `MarketDataError::InvalidSymbol` otherwise
async fn validate_symbol(&self, symbol: &str) -> MarketDataResult<()> {
if symbol.is_empty() || symbol.len() > 20 {
return Err(MarketDataError::InvalidSymbol {
symbol: symbol.to_string(),
});
}
Ok(())
}
/// Validate that a time range is logically correct
///
/// Ensures the start time is before the end time.
///
/// # Arguments
///
/// * `from` - Start time
/// * `to` - End time
///
/// # Returns
///
/// `Ok(())` if valid, `MarketDataError::InvalidTimeRange` otherwise
async fn validate_time_range(
&self,
from: DateTime<Utc>,
to: DateTime<Utc>,
) -> MarketDataResult<()> {
if from >= to {
return Err(MarketDataError::InvalidTimeRange { from, to });
}
Ok(())
}
}
#[async_trait]
impl IndicatorRepository for PostgresIndicatorRepository {
async fn store_indicator(&self, indicator: &TechnicalIndicator) -> MarketDataResult<()> {
self.validate_symbol(&indicator.symbol).await?;
sqlx::query(
r#"
INSERT INTO technical_indicators (id, symbol, indicator_type, timestamp, value, parameters, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (symbol, indicator_type, timestamp) DO UPDATE SET
value = EXCLUDED.value,
parameters = EXCLUDED.parameters
"#
)
.bind(indicator.id)
.bind(&indicator.symbol)
.bind(indicator.indicator_type as IndicatorType)
.bind(indicator.timestamp)
.bind(indicator.value)
.bind(&indicator.parameters)
.bind(indicator.created_at)
.execute(&self.pool)
.await?;
Ok(())
}
async fn store_indicators(&self, indicators: &[TechnicalIndicator]) -> MarketDataResult<()> {
if indicators.is_empty() {
return Ok(());
}
// Validate all symbols first
for indicator in indicators {
self.validate_symbol(&indicator.symbol).await?;
}
let mut tx = self.pool.begin().await?;
for indicator in indicators {
sqlx::query(
r#"
INSERT INTO technical_indicators (id, symbol, indicator_type, timestamp, value, parameters, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (symbol, indicator_type, timestamp) DO UPDATE SET
value = EXCLUDED.value,
parameters = EXCLUDED.parameters
"#
)
.bind(indicator.id)
.bind(&indicator.symbol)
.bind(indicator.indicator_type as IndicatorType)
.bind(indicator.timestamp)
.bind(indicator.value)
.bind(&indicator.parameters)
.bind(indicator.created_at)
.execute(&mut *tx)
.await?;
}
tx.commit().await?;
Ok(())
}
async fn get_latest_indicator(
&self,
symbol: &str,
indicator_type: IndicatorType,
) -> MarketDataResult<Option<TechnicalIndicator>> {
self.validate_symbol(symbol).await?;
let row = sqlx::query(
r#"
SELECT id, symbol, indicator_type, timestamp, value, parameters, created_at
FROM technical_indicators
WHERE symbol = $1 AND indicator_type = $2
ORDER BY timestamp DESC
LIMIT 1
"#,
)
.bind(symbol)
.bind(indicator_type as IndicatorType)
.fetch_optional(&self.pool)
.await?;
if let Some(row) = row {
Ok(Some(TechnicalIndicator {
id: row.get("id"),
symbol: row.get("symbol"),
indicator_type: row.get("indicator_type"),
timestamp: row.get("timestamp"),
value: row.get("value"),
parameters: row.get("parameters"),
created_at: row.get("created_at"),
}))
} else {
Ok(None)
}
}
async fn get_indicator_history(
&self,
symbol: &str,
indicator_type: IndicatorType,
from: DateTime<Utc>,
to: DateTime<Utc>,
) -> MarketDataResult<Vec<TechnicalIndicator>> {
self.validate_symbol(symbol).await?;
self.validate_time_range(from, to).await?;
let rows = sqlx::query(
r#"
SELECT id, symbol, indicator_type, timestamp, value, parameters, created_at
FROM technical_indicators
WHERE symbol = $1 AND indicator_type = $2 AND timestamp >= $3 AND timestamp <= $4
ORDER BY timestamp ASC
"#,
)
.bind(symbol)
.bind(indicator_type as IndicatorType)
.bind(from)
.bind(to)
.fetch_all(&self.pool)
.await?;
let indicators = rows
.into_iter()
.map(|row| TechnicalIndicator {
id: row.get("id"),
symbol: row.get("symbol"),
indicator_type: row.get("indicator_type"),
timestamp: row.get("timestamp"),
value: row.get("value"),
parameters: row.get("parameters"),
created_at: row.get("created_at"),
})
.collect();
Ok(indicators)
}
async fn get_latest_indicators_for_symbol(
&self,
symbol: &str,
) -> MarketDataResult<HashMap<IndicatorType, TechnicalIndicator>> {
self.validate_symbol(symbol).await?;
let rows = sqlx::query(
r#"
SELECT DISTINCT ON (indicator_type) id, symbol, indicator_type, timestamp, value, parameters, created_at
FROM technical_indicators
WHERE symbol = $1
ORDER BY indicator_type, timestamp DESC
"#
)
.bind(symbol)
.fetch_all(&self.pool)
.await?;
let mut indicators = HashMap::new();
for row in rows {
let indicator = TechnicalIndicator {
id: row.get("id"),
symbol: row.get("symbol"),
indicator_type: row.get("indicator_type"),
timestamp: row.get("timestamp"),
value: row.get("value"),
parameters: row.get("parameters"),
created_at: row.get("created_at"),
};
indicators.insert(indicator.indicator_type, indicator);
}
Ok(indicators)
}
async fn get_indicators_for_symbols(
&self,
symbols: &[String],
indicator_type: IndicatorType,
) -> MarketDataResult<HashMap<String, TechnicalIndicator>> {
if symbols.is_empty() {
return Ok(HashMap::new());
}
// Validate all symbols
for symbol in symbols {
self.validate_symbol(symbol).await?;
}
let rows = sqlx::query(
r#"
SELECT DISTINCT ON (symbol) id, symbol, indicator_type, timestamp, value, parameters, created_at
FROM technical_indicators
WHERE symbol = ANY($1) AND indicator_type = $2
ORDER BY symbol, timestamp DESC
"#
)
.bind(symbols)
.bind(indicator_type as IndicatorType)
.fetch_all(&self.pool)
.await?;
let mut indicators = HashMap::new();
for row in rows {
let symbol: String = row.get("symbol");
let indicator = TechnicalIndicator {
id: row.get("id"),
symbol: symbol.clone(),
indicator_type: row.get("indicator_type"),
timestamp: row.get("timestamp"),
value: row.get("value"),
parameters: row.get("parameters"),
created_at: row.get("created_at"),
};
indicators.insert(symbol, indicator);
}
Ok(indicators)
}
async fn get_available_indicator_types(
&self,
symbol: &str,
) -> MarketDataResult<Vec<IndicatorType>> {
self.validate_symbol(symbol).await?;
let rows = sqlx::query(
"SELECT DISTINCT indicator_type FROM technical_indicators WHERE symbol = $1",
)
.bind(symbol)
.fetch_all(&self.pool)
.await?;
let types = rows
.into_iter()
.map(|row| row.get("indicator_type"))
.collect();
Ok(types)
}
async fn cleanup_old_indicators(&self, before: DateTime<Utc>) -> MarketDataResult<u64> {
let result = sqlx::query("DELETE FROM technical_indicators WHERE timestamp < $1")
.bind(before)
.execute(&self.pool)
.await?;
Ok(result.rows_affected())
}
async fn get_indicator_statistics(
&self,
symbol: &str,
indicator_type: IndicatorType,
from: DateTime<Utc>,
to: DateTime<Utc>,
) -> MarketDataResult<Option<IndicatorStatistics>> {
self.validate_symbol(symbol).await?;
self.validate_time_range(from, to).await?;
let row = sqlx::query(
r#"
SELECT
COUNT(*) as count,
MIN(value) as min_value,
MAX(value) as max_value,
AVG(value) as avg_value,
MIN(timestamp) as first_timestamp,
MAX(timestamp) as last_timestamp
FROM technical_indicators
WHERE symbol = $1 AND indicator_type = $2 AND timestamp >= $3 AND timestamp <= $4
"#,
)
.bind(symbol)
.bind(indicator_type as IndicatorType)
.bind(from)
.bind(to)
.fetch_one(&self.pool)
.await?;
let count: i64 = row.get("count");
if count > 0 {
Ok(Some(IndicatorStatistics {
symbol: symbol.to_string(),
indicator_type,
count,
min_value: row
.get::<Option<Decimal>, _>("min_value")
.unwrap_or_default(),
max_value: row
.get::<Option<Decimal>, _>("max_value")
.unwrap_or_default(),
avg_value: row
.get::<Option<Decimal>, _>("avg_value")
.unwrap_or_default(),
first_timestamp: row
.get::<Option<DateTime<Utc>>, _>("first_timestamp")
.unwrap_or(from),
last_timestamp: row
.get::<Option<DateTime<Utc>>, _>("last_timestamp")
.unwrap_or(to),
}))
} else {
Ok(None)
}
}
}