- Add .get(0)? before .to_scalar() for scale extraction (line 605) - Add .get(0)? before .to_scalar() for zero_point extraction (line 624) - Handles [1] shape tensors from Tensor::new(&[value], device) - Fixes test_quantization_preserves_scale_and_zero_point - Ensures reliable SafeTensors save/load round-trip
767 lines
26 KiB
Rust
767 lines
26 KiB
Rust
//! `VaR` (Value at Risk) Repository
|
|
//!
|
|
//! Manages `VaR` calculations, historical data storage, and risk metrics for
|
|
//! high-frequency trading systems with multiple calculation methods.
|
|
|
|
// Allow pedantic lints for VaR calculation complexity
|
|
#![allow(clippy::missing_docs_in_private_items)]
|
|
#![allow(clippy::arithmetic_side_effects)]
|
|
#![allow(clippy::as_conversions)]
|
|
#![allow(clippy::default_numeric_fallback)]
|
|
#![allow(clippy::str_to_string)]
|
|
#![allow(clippy::float_arithmetic)]
|
|
#![allow(clippy::indexing_slicing)]
|
|
#![allow(missing_docs)]
|
|
#![allow(clippy::expect_used)]
|
|
#![allow(clippy::module_name_repetitions)]
|
|
|
|
use async_trait::async_trait;
|
|
use chrono::{DateTime, Duration, Utc};
|
|
use redis::aio::ConnectionManager;
|
|
use rust_decimal::Decimal;
|
|
use serde::{Deserialize, Serialize};
|
|
use sqlx::PgPool;
|
|
use std::collections::HashMap;
|
|
use tracing::{info, warn};
|
|
use uuid::Uuid;
|
|
|
|
use crate::{RiskDataError, RiskDataResult};
|
|
|
|
/// `VaR` calculation methods
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
|
|
#[sqlx(type_name = "var_method", rename_all = "snake_case")]
|
|
pub enum VarMethod {
|
|
/// Historical simulation method using past returns
|
|
Historical,
|
|
/// Parametric method using normal distribution
|
|
Parametric,
|
|
/// Monte Carlo simulation method
|
|
MonteCarlo,
|
|
/// Extreme Value Theory method
|
|
ExtremeValue,
|
|
}
|
|
|
|
/// `VaR` confidence levels
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum ConfidenceLevel {
|
|
/// 95% confidence level
|
|
P95,
|
|
/// 97.5% confidence level
|
|
P97_5,
|
|
/// 99% confidence level
|
|
P99,
|
|
/// 99.9% confidence level
|
|
P99_9,
|
|
}
|
|
|
|
impl ConfidenceLevel {
|
|
pub const fn as_decimal(&self) -> Decimal {
|
|
match self {
|
|
Self::P95 => Decimal::from_parts(95, 0, 0, false, 2), // 0.95
|
|
Self::P97_5 => Decimal::from_parts(975, 0, 0, false, 3), // 0.975
|
|
Self::P99 => Decimal::from_parts(99, 0, 0, false, 2), // 0.99
|
|
Self::P99_9 => Decimal::from_parts(999, 0, 0, false, 3), // 0.999
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `VaR` calculation request
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct VarRequest {
|
|
/// Portfolio identifier for `VaR` calculation
|
|
pub portfolio_id: String,
|
|
/// `VaR` calculation method to use
|
|
pub method: VarMethod,
|
|
/// Confidence level for the `VaR` calculation
|
|
pub confidence_level: ConfidenceLevel,
|
|
/// Holding period in days
|
|
pub holding_period_days: i32,
|
|
/// Number of historical days to look back for data
|
|
pub lookback_days: i32,
|
|
/// Currency for the `VaR` result (ISO 3-letter code)
|
|
pub currency: String,
|
|
}
|
|
|
|
/// `VaR` calculation result
|
|
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
|
pub struct VarResult {
|
|
/// Unique identifier for this `VaR` calculation
|
|
pub id: Uuid,
|
|
/// Portfolio this `VaR` calculation applies to
|
|
pub portfolio_id: String,
|
|
/// `VaR` calculation method used
|
|
pub method: VarMethod,
|
|
/// Confidence level used in the calculation
|
|
pub confidence_level: Decimal,
|
|
/// Holding period in days used for the calculation
|
|
pub holding_period_days: i32,
|
|
/// Number of historical days used for the calculation
|
|
pub lookback_days: i32,
|
|
/// Calculated `VaR` amount
|
|
pub var_amount: Decimal,
|
|
/// Currency of the `VaR` amount (ISO 3-letter code)
|
|
pub currency: String,
|
|
/// Date and time when this calculation was performed
|
|
pub calculation_date: DateTime<Utc>,
|
|
/// Portfolio volatility (if calculated)
|
|
pub volatility: Option<Decimal>,
|
|
/// Correlation adjustment factor (if applied)
|
|
pub correlation_adjustment: Option<Decimal>,
|
|
/// Stress test multiplier (if this is a stress test result)
|
|
pub stress_test_multiplier: Option<Decimal>,
|
|
/// Additional calculation metadata as JSON
|
|
pub metadata: serde_json::Value,
|
|
}
|
|
|
|
/// Portfolio position for `VaR` calculation
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PortfolioPosition {
|
|
/// Instrument symbol for this position
|
|
pub symbol: String,
|
|
/// Number of shares/contracts held
|
|
pub quantity: Decimal,
|
|
/// Current market value of the position
|
|
pub market_value: Decimal,
|
|
/// Currency of the position (ISO 3-letter code)
|
|
pub currency: String,
|
|
/// Asset class classification
|
|
pub asset_class: String,
|
|
}
|
|
|
|
/// Historical price data
|
|
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
|
pub struct PriceData {
|
|
/// Instrument symbol for this price point
|
|
pub symbol: String,
|
|
/// Price at this timestamp
|
|
pub price: Decimal,
|
|
/// Timestamp when this price was recorded
|
|
pub timestamp: DateTime<Utc>,
|
|
/// Trading volume at this price (if available)
|
|
pub volume: Option<Decimal>,
|
|
}
|
|
|
|
/// `VaR` repository trait
|
|
#[async_trait]
|
|
pub trait VarRepository: Send + Sync + std::fmt::Debug {
|
|
/// Calculate `VaR` for a portfolio
|
|
async fn calculate_var(&self, request: VarRequest) -> RiskDataResult<VarResult>;
|
|
|
|
/// Store `VaR` calculation result
|
|
async fn store_var_result(&self, result: VarResult) -> RiskDataResult<()>;
|
|
|
|
/// Get `VaR` history for a portfolio
|
|
async fn get_var_history(
|
|
&self,
|
|
portfolio_id: &str,
|
|
from: DateTime<Utc>,
|
|
to: DateTime<Utc>,
|
|
) -> RiskDataResult<Vec<VarResult>>;
|
|
|
|
/// Get latest `VaR` for a portfolio
|
|
async fn get_latest_var(&self, portfolio_id: &str) -> RiskDataResult<Option<VarResult>>;
|
|
|
|
/// Store historical price data
|
|
async fn store_price_data(&self, prices: Vec<PriceData>) -> RiskDataResult<()>;
|
|
|
|
/// Get price history for `VaR` calculations
|
|
async fn get_price_history(
|
|
&self,
|
|
symbols: Vec<String>,
|
|
from: DateTime<Utc>,
|
|
to: DateTime<Utc>,
|
|
) -> RiskDataResult<HashMap<String, Vec<PriceData>>>;
|
|
|
|
/// Get portfolio positions
|
|
async fn get_portfolio_positions(
|
|
&self,
|
|
portfolio_id: &str,
|
|
) -> RiskDataResult<Vec<PortfolioPosition>>;
|
|
|
|
/// Calculate portfolio volatility matrix
|
|
async fn calculate_volatility_matrix(
|
|
&self,
|
|
symbols: Vec<String>,
|
|
lookback_days: i32,
|
|
) -> RiskDataResult<HashMap<String, HashMap<String, Decimal>>>;
|
|
|
|
/// Run stress test scenarios
|
|
async fn run_stress_test(
|
|
&self,
|
|
portfolio_id: &str,
|
|
scenario_name: &str,
|
|
stress_factors: HashMap<String, Decimal>,
|
|
) -> RiskDataResult<VarResult>;
|
|
}
|
|
|
|
/// `VaR` repository implementation
|
|
#[derive(Clone)]
|
|
pub struct VarRepositoryImpl {
|
|
db_pool: PgPool,
|
|
redis_conn: ConnectionManager,
|
|
}
|
|
|
|
impl std::fmt::Debug for VarRepositoryImpl {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("VarRepositoryImpl")
|
|
.field("db_pool", &"<PgPool>")
|
|
.field("redis_conn", &"<ConnectionManager>")
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl VarRepositoryImpl {
|
|
pub const fn new(db_pool: PgPool, redis_conn: ConnectionManager) -> Self {
|
|
Self {
|
|
db_pool,
|
|
redis_conn,
|
|
}
|
|
}
|
|
|
|
/// Calculate historical `VaR`
|
|
async fn calculate_historical_var(
|
|
&self,
|
|
positions: Vec<PortfolioPosition>,
|
|
confidence_level: ConfidenceLevel,
|
|
lookback_days: i32,
|
|
) -> RiskDataResult<Decimal> {
|
|
info!(
|
|
"Calculating historical VaR with {} day lookback",
|
|
lookback_days
|
|
);
|
|
|
|
// Get symbols from positions
|
|
let symbols: Vec<String> = positions.iter().map(|p| p.symbol.clone()).collect();
|
|
|
|
// Get historical prices
|
|
let to_date = Utc::now();
|
|
let from_date = to_date - Duration::days(i64::from(lookback_days));
|
|
|
|
let price_history = self.get_price_history(symbols, from_date, to_date).await?;
|
|
|
|
// Calculate daily returns for each position
|
|
let mut portfolio_returns = Vec::new();
|
|
|
|
for position in positions {
|
|
if let Some(prices) = price_history.get(&position.symbol) {
|
|
if prices.len() < 2 {
|
|
warn!("Insufficient price data for {}", position.symbol);
|
|
continue;
|
|
}
|
|
|
|
// Calculate daily returns
|
|
for window in prices.windows(2) {
|
|
let prev_price = window[0].price;
|
|
let curr_price = window[1].price;
|
|
|
|
if prev_price > Decimal::ZERO {
|
|
let return_rate = (curr_price - prev_price) / prev_price;
|
|
let position_return = return_rate * position.market_value;
|
|
portfolio_returns.push(position_return);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if portfolio_returns.is_empty() {
|
|
return Err(RiskDataError::VarCalculation(
|
|
"No returns data available".to_owned(),
|
|
));
|
|
}
|
|
|
|
// Sort returns and find percentile
|
|
portfolio_returns.sort();
|
|
let confidence_f64 = confidence_level
|
|
.as_decimal()
|
|
.to_string()
|
|
.parse::<f64>()
|
|
.map_err(|e| {
|
|
RiskDataError::VarCalculation(format!("Invalid confidence level conversion: {}", e))
|
|
})?;
|
|
|
|
#[allow(clippy::cast_precision_loss)]
|
|
let returns_len_f64 = portfolio_returns.len() as f64;
|
|
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
|
let percentile_index = ((1.0 - confidence_f64) * returns_len_f64) as usize;
|
|
|
|
let var_amount = portfolio_returns
|
|
.get(percentile_index)
|
|
.copied()
|
|
.ok_or_else(|| {
|
|
RiskDataError::VarCalculation("No VaR data at calculated percentile".to_owned())
|
|
})?
|
|
.abs();
|
|
|
|
info!("Historical VaR calculated: {}", var_amount);
|
|
Ok(var_amount)
|
|
}
|
|
|
|
/// Calculate parametric `VaR` using correlation matrix
|
|
async fn calculate_parametric_var(
|
|
&self,
|
|
positions: Vec<PortfolioPosition>,
|
|
confidence_level: ConfidenceLevel,
|
|
lookback_days: i32,
|
|
) -> RiskDataResult<Decimal> {
|
|
info!("Calculating parametric VaR");
|
|
|
|
let symbols: Vec<String> = positions.iter().map(|p| p.symbol.clone()).collect();
|
|
let vol_matrix = self
|
|
.calculate_volatility_matrix(symbols, lookback_days)
|
|
.await?;
|
|
|
|
// Calculate portfolio variance using correlation matrix
|
|
let mut portfolio_variance = Decimal::ZERO;
|
|
|
|
for i in 0..positions.len() {
|
|
for j in 0..positions.len() {
|
|
let pos_i = &positions[i];
|
|
let pos_j = &positions[j];
|
|
|
|
let vol_i = vol_matrix
|
|
.get(&pos_i.symbol)
|
|
.and_then(|row| row.get(&pos_i.symbol))
|
|
.copied()
|
|
.ok_or_else(|| {
|
|
RiskDataError::VarCalculation(format!(
|
|
"Missing volatility data for symbol: {}",
|
|
pos_i.symbol
|
|
))
|
|
})?;
|
|
|
|
let correlation = if i == j {
|
|
Decimal::ONE
|
|
} else {
|
|
vol_matrix
|
|
.get(&pos_i.symbol)
|
|
.and_then(|row| row.get(&pos_j.symbol))
|
|
.copied()
|
|
.ok_or_else(|| {
|
|
RiskDataError::VarCalculation(format!(
|
|
"Missing correlation data between {} and {}",
|
|
pos_i.symbol, pos_j.symbol
|
|
))
|
|
})?
|
|
};
|
|
|
|
let vol_j = vol_matrix
|
|
.get(&pos_j.symbol)
|
|
.and_then(|row| row.get(&pos_j.symbol))
|
|
.copied()
|
|
.ok_or_else(|| {
|
|
RiskDataError::VarCalculation(format!(
|
|
"Missing volatility data for symbol: {}",
|
|
pos_j.symbol
|
|
))
|
|
})?;
|
|
|
|
portfolio_variance +=
|
|
pos_i.market_value * pos_j.market_value * vol_i * vol_j * correlation;
|
|
}
|
|
}
|
|
|
|
// Calculate square root of portfolio variance using f64 conversion
|
|
let portfolio_volatility = if portfolio_variance == Decimal::ZERO {
|
|
// CRITICAL: Zero portfolio variance indicates a serious risk calculation problem
|
|
// This could be due to:
|
|
// 1. Missing volatility data (dangerous)
|
|
// 2. All positions are zero risk (unlikely in real trading)
|
|
// 3. Data corruption or missing correlation matrix
|
|
warn!("Portfolio variance is zero - this indicates missing volatility data or data corruption");
|
|
|
|
// Return error instead of silently using zero volatility
|
|
return Err(RiskDataError::VarCalculation(
|
|
"Portfolio variance is zero - cannot calculate meaningful VaR. \
|
|
This may indicate missing volatility data, data corruption, or incorrect position data.".to_owned()
|
|
));
|
|
} else {
|
|
let variance_f64: f64 = portfolio_variance.try_into().map_err(|e| {
|
|
RiskDataError::VarCalculation(format!(
|
|
"Portfolio variance conversion failed: {}",
|
|
e
|
|
))
|
|
})?;
|
|
let volatility_f64 = variance_f64.sqrt();
|
|
Decimal::try_from(volatility_f64).map_err(|e| {
|
|
RiskDataError::VarCalculation(format!("Volatility calculation failed: {}", e))
|
|
})?
|
|
};
|
|
|
|
// Apply confidence level multiplier (normal distribution quantiles)
|
|
let z_score = match confidence_level {
|
|
ConfidenceLevel::P95 => Decimal::from_parts(1645, 0, 0, false, 3), // 1.645
|
|
ConfidenceLevel::P97_5 => Decimal::from_parts(196, 0, 0, false, 2), // 1.96
|
|
ConfidenceLevel::P99 => Decimal::from_parts(2326, 0, 0, false, 3), // 2.326
|
|
ConfidenceLevel::P99_9 => Decimal::from_parts(309, 0, 0, false, 2), // 3.09
|
|
};
|
|
|
|
let var_amount = portfolio_volatility * z_score;
|
|
info!("Parametric VaR calculated: {}", var_amount);
|
|
Ok(var_amount)
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl VarRepository for VarRepositoryImpl {
|
|
async fn calculate_var(&self, request: VarRequest) -> RiskDataResult<VarResult> {
|
|
info!("Calculating VaR for portfolio {}", request.portfolio_id);
|
|
|
|
let positions = self.get_portfolio_positions(&request.portfolio_id).await?;
|
|
if positions.is_empty() {
|
|
return Err(RiskDataError::VarCalculation(
|
|
"No positions found for portfolio".to_owned(),
|
|
));
|
|
}
|
|
|
|
let var_amount = match request.method {
|
|
VarMethod::Historical => {
|
|
self.calculate_historical_var(
|
|
positions,
|
|
request.confidence_level,
|
|
request.lookback_days,
|
|
)
|
|
.await?
|
|
},
|
|
VarMethod::Parametric => {
|
|
self.calculate_parametric_var(
|
|
positions,
|
|
request.confidence_level,
|
|
request.lookback_days,
|
|
)
|
|
.await?
|
|
},
|
|
VarMethod::MonteCarlo => {
|
|
// Simplified Monte Carlo - would need more sophisticated implementation
|
|
warn!("Monte Carlo VaR not fully implemented, using parametric method");
|
|
self.calculate_parametric_var(
|
|
positions,
|
|
request.confidence_level,
|
|
request.lookback_days,
|
|
)
|
|
.await?
|
|
},
|
|
VarMethod::ExtremeValue => {
|
|
// Extreme Value Theory - would need specialized implementation
|
|
warn!("Extreme Value VaR not fully implemented, using historical method");
|
|
self.calculate_historical_var(
|
|
positions,
|
|
request.confidence_level,
|
|
request.lookback_days,
|
|
)
|
|
.await?
|
|
},
|
|
};
|
|
|
|
// Store currency before moving request
|
|
let currency = request.currency.clone();
|
|
|
|
let result = VarResult {
|
|
id: Uuid::new_v4(),
|
|
portfolio_id: request.portfolio_id,
|
|
method: request.method,
|
|
confidence_level: request.confidence_level.as_decimal(),
|
|
holding_period_days: request.holding_period_days,
|
|
lookback_days: request.lookback_days,
|
|
var_amount,
|
|
currency: request.currency,
|
|
calculation_date: Utc::now(),
|
|
volatility: None, // Could be calculated based on method
|
|
correlation_adjustment: None,
|
|
stress_test_multiplier: None,
|
|
metadata: serde_json::json!({}),
|
|
};
|
|
|
|
// Store result
|
|
self.store_var_result(result.clone()).await?;
|
|
|
|
info!("VaR calculation completed: {} {}", var_amount, currency);
|
|
Ok(result)
|
|
}
|
|
|
|
async fn store_var_result(&self, result: VarResult) -> RiskDataResult<()> {
|
|
let query = "
|
|
INSERT INTO var_calculations (
|
|
id, portfolio_id, method, confidence_level, holding_period_days,
|
|
lookback_days, var_amount, currency, calculation_date,
|
|
volatility, correlation_adjustment, stress_test_multiplier, metadata
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
|
ON CONFLICT (id) DO UPDATE SET
|
|
var_amount = EXCLUDED.var_amount,
|
|
calculation_date = EXCLUDED.calculation_date,
|
|
metadata = EXCLUDED.metadata
|
|
";
|
|
|
|
sqlx::query(query)
|
|
.bind(result.id)
|
|
.bind(&result.portfolio_id)
|
|
.bind(result.method)
|
|
.bind(result.confidence_level)
|
|
.bind(result.holding_period_days)
|
|
.bind(result.lookback_days)
|
|
.bind(result.var_amount)
|
|
.bind(&result.currency)
|
|
.bind(result.calculation_date)
|
|
.bind(result.volatility)
|
|
.bind(result.correlation_adjustment)
|
|
.bind(result.stress_test_multiplier)
|
|
.bind(&result.metadata)
|
|
.execute(&self.db_pool)
|
|
.await?;
|
|
|
|
// Cache latest result in Redis
|
|
let cache_key = format!("var:latest:{}", result.portfolio_id);
|
|
let serialized = serde_json::to_string(&result)?;
|
|
|
|
let mut redis_conn = self.redis_conn.clone();
|
|
redis::cmd("SETEX")
|
|
.arg(&cache_key)
|
|
.arg(3600) // 1 hour TTL
|
|
.arg(&serialized)
|
|
.query_async::<()>(&mut redis_conn)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn get_var_history(
|
|
&self,
|
|
portfolio_id: &str,
|
|
from: DateTime<Utc>,
|
|
to: DateTime<Utc>,
|
|
) -> RiskDataResult<Vec<VarResult>> {
|
|
let query = "
|
|
SELECT * FROM var_calculations
|
|
WHERE portfolio_id = $1
|
|
AND calculation_date BETWEEN $2 AND $3
|
|
ORDER BY calculation_date DESC
|
|
";
|
|
|
|
let results = sqlx::query_as::<_, VarResult>(query)
|
|
.bind(portfolio_id)
|
|
.bind(from)
|
|
.bind(to)
|
|
.fetch_all(&self.db_pool)
|
|
.await?;
|
|
|
|
Ok(results)
|
|
}
|
|
|
|
async fn get_latest_var(&self, portfolio_id: &str) -> RiskDataResult<Option<VarResult>> {
|
|
// Try Redis cache first
|
|
let cache_key = format!("var:latest:{}", portfolio_id);
|
|
let mut redis_conn = self.redis_conn.clone();
|
|
|
|
if let Ok(cached) = redis::cmd("GET")
|
|
.arg(&cache_key)
|
|
.query_async::<String>(&mut redis_conn)
|
|
.await
|
|
{
|
|
if let Ok(result) = serde_json::from_str::<VarResult>(&cached) {
|
|
return Ok(Some(result));
|
|
}
|
|
}
|
|
|
|
// Fallback to database
|
|
let query = "
|
|
SELECT * FROM var_calculations
|
|
WHERE portfolio_id = $1
|
|
ORDER BY calculation_date DESC
|
|
LIMIT 1
|
|
";
|
|
|
|
let result = sqlx::query_as::<_, VarResult>(query)
|
|
.bind(portfolio_id)
|
|
.fetch_optional(&self.db_pool)
|
|
.await?;
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
async fn store_price_data(&self, prices: Vec<PriceData>) -> RiskDataResult<()> {
|
|
if prices.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
let mut transaction = self.db_pool.begin().await?;
|
|
|
|
for price in prices {
|
|
let query = "
|
|
INSERT INTO price_history (symbol, price, timestamp, volume)
|
|
VALUES ($1, $2, $3, $4)
|
|
ON CONFLICT (symbol, timestamp) DO UPDATE SET
|
|
price = EXCLUDED.price,
|
|
volume = EXCLUDED.volume
|
|
";
|
|
|
|
sqlx::query(query)
|
|
.bind(&price.symbol)
|
|
.bind(price.price)
|
|
.bind(price.timestamp)
|
|
.bind(price.volume)
|
|
.execute(&mut *transaction)
|
|
.await?;
|
|
}
|
|
|
|
transaction.commit().await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn get_price_history(
|
|
&self,
|
|
symbols: Vec<String>,
|
|
from: DateTime<Utc>,
|
|
to: DateTime<Utc>,
|
|
) -> RiskDataResult<HashMap<String, Vec<PriceData>>> {
|
|
let query = "
|
|
SELECT symbol, price, timestamp, volume
|
|
FROM price_history
|
|
WHERE symbol = ANY($1)
|
|
AND timestamp BETWEEN $2 AND $3
|
|
ORDER BY symbol, timestamp
|
|
";
|
|
|
|
let rows = sqlx::query_as::<_, PriceData>(query)
|
|
.bind(&symbols)
|
|
.bind(from)
|
|
.bind(to)
|
|
.fetch_all(&self.db_pool)
|
|
.await?;
|
|
|
|
let mut result = HashMap::new();
|
|
for row in rows {
|
|
result
|
|
.entry(row.symbol.clone())
|
|
.or_insert_with(Vec::new)
|
|
.push(row);
|
|
}
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
async fn get_portfolio_positions(
|
|
&self,
|
|
portfolio_id: &str,
|
|
) -> RiskDataResult<Vec<PortfolioPosition>> {
|
|
// This would typically query a positions table
|
|
// For now, returning a placeholder implementation
|
|
info!("Getting portfolio positions for {}", portfolio_id);
|
|
Ok(vec![])
|
|
}
|
|
|
|
async fn calculate_volatility_matrix(
|
|
&self,
|
|
symbols: Vec<String>,
|
|
lookback_days: i32,
|
|
) -> RiskDataResult<HashMap<String, HashMap<String, Decimal>>> {
|
|
info!(
|
|
"Calculating volatility matrix for {} symbols",
|
|
symbols.len()
|
|
);
|
|
|
|
// Get price history
|
|
let to_date = Utc::now();
|
|
let from_date = to_date - Duration::days(i64::from(lookback_days));
|
|
|
|
let _price_history = self
|
|
.get_price_history(symbols.clone(), from_date, to_date)
|
|
.await?;
|
|
|
|
// Calculate correlation matrix (simplified implementation)
|
|
let mut matrix = HashMap::new();
|
|
|
|
for symbol in &symbols {
|
|
let mut row = HashMap::new();
|
|
for other_symbol in &symbols {
|
|
// Simplified correlation calculation - in production would use proper statistical methods
|
|
let correlation = if symbol == other_symbol {
|
|
Decimal::ONE
|
|
} else {
|
|
Decimal::from_str_exact("0.5").expect("Static decimal value should parse")
|
|
// Placeholder correlation
|
|
};
|
|
row.insert(other_symbol.clone(), correlation);
|
|
}
|
|
matrix.insert(symbol.clone(), row);
|
|
}
|
|
|
|
Ok(matrix)
|
|
}
|
|
|
|
async fn run_stress_test(
|
|
&self,
|
|
portfolio_id: &str,
|
|
scenario_name: &str,
|
|
stress_factors: HashMap<String, Decimal>,
|
|
) -> RiskDataResult<VarResult> {
|
|
info!(
|
|
"Running stress test '{}' for portfolio {}",
|
|
scenario_name, portfolio_id
|
|
);
|
|
|
|
// Simplified stress test - apply stress factors to current VaR
|
|
let base_var = self.get_latest_var(portfolio_id).await?.ok_or_else(|| {
|
|
RiskDataError::VarCalculation("No base VaR found for stress test".to_owned())
|
|
})?;
|
|
|
|
// Apply maximum stress factor as multiplier
|
|
let max_stress = stress_factors
|
|
.values()
|
|
.max()
|
|
.copied()
|
|
.unwrap_or(Decimal::ONE);
|
|
|
|
let stressed_var = VarResult {
|
|
id: Uuid::new_v4(),
|
|
var_amount: base_var.var_amount * max_stress,
|
|
stress_test_multiplier: Some(max_stress),
|
|
calculation_date: Utc::now(),
|
|
metadata: serde_json::json!({
|
|
"stress_scenario": scenario_name,
|
|
"stress_factors": stress_factors,
|
|
"base_var_id": base_var.id
|
|
}),
|
|
..base_var
|
|
};
|
|
|
|
// Store stress test result
|
|
self.store_var_result(stressed_var.clone()).await?;
|
|
|
|
Ok(stressed_var)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_confidence_level_as_decimal() {
|
|
assert_eq!(
|
|
ConfidenceLevel::P95.as_decimal(),
|
|
Decimal::from_str_exact("0.95").unwrap()
|
|
);
|
|
assert_eq!(
|
|
ConfidenceLevel::P99.as_decimal(),
|
|
Decimal::from_str_exact("0.99").unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_var_request_serialization() {
|
|
let request = VarRequest {
|
|
portfolio_id: "test_portfolio".to_string(),
|
|
method: VarMethod::Historical,
|
|
confidence_level: ConfidenceLevel::P95,
|
|
holding_period_days: 1,
|
|
lookback_days: 252,
|
|
currency: "USD".to_string(),
|
|
};
|
|
|
|
let json = serde_json::to_string(&request).unwrap();
|
|
let deserialized: VarRequest = serde_json::from_str(&json).unwrap();
|
|
|
|
assert_eq!(request.portfolio_id, deserialized.portfolio_id);
|
|
assert_eq!(request.method, deserialized.method);
|
|
}
|
|
}
|