- 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
684 lines
22 KiB
Rust
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)
|
|
}
|
|
}
|
|
}
|