Files
foxhunt/risk-data/src/models.rs
jgrusewski 34d8af5dec chore(clippy): add deny(unwrap_used) to 4 zero-violation crates
Add #![deny(clippy::unwrap_used, clippy::expect_used)] to:
- market-data/src/lib.rs
- model_loader/src/lib.rs
- risk-data/src/lib.rs
- services/data_acquisition_service/src/lib.rs

Add #[allow(clippy::unwrap_used, clippy::expect_used)] to all
cfg(test) modules in each crate and their sub-files to preserve
existing test patterns without introducing false positives.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 23:57:01 +01:00

1043 lines
35 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Risk Data Models
//!
//! Database schema models and data structures for risk management in
//! high-frequency trading systems. Provides comprehensive data models
//! for `VaR` calculations, compliance logging, and position limits.
// Allow pedantic lints for data models
#![allow(clippy::missing_docs_in_private_items)]
#![allow(missing_docs)]
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use std::collections::HashMap;
use uuid::Uuid;
/// Database connection pool - proper newtype wrapper
#[derive(Debug, Clone)]
pub struct DbPool(sqlx::PgPool);
impl DbPool {
/// Create a new database pool wrapper
pub const fn new(pool: sqlx::PgPool) -> Self {
Self(pool)
}
/// Get the underlying pool
pub const fn inner(&self) -> &sqlx::PgPool {
&self.0
}
/// Into the underlying pool
pub fn into_inner(self) -> sqlx::PgPool {
self.0
}
}
impl std::ops::Deref for DbPool {
type Target = sqlx::PgPool;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<sqlx::PgPool> for DbPool {
fn from(pool: sqlx::PgPool) -> Self {
Self::new(pool)
}
}
impl From<DbPool> for sqlx::PgPool {
fn from(pool: DbPool) -> Self {
pool.into_inner()
}
}
/// Redis connection - proper newtype wrapper
#[derive(Debug, Clone)]
pub struct RedisConnection(redis::aio::MultiplexedConnection);
impl RedisConnection {
/// Create a new Redis connection wrapper
pub const fn new(conn: redis::aio::MultiplexedConnection) -> Self {
Self(conn)
}
/// Get the underlying connection
pub const fn inner(&self) -> &redis::aio::MultiplexedConnection {
&self.0
}
/// Into the underlying connection
pub fn into_inner(self) -> redis::aio::MultiplexedConnection {
self.0
}
}
impl std::ops::Deref for RedisConnection {
type Target = redis::aio::MultiplexedConnection;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<redis::aio::MultiplexedConnection> for RedisConnection {
fn from(conn: redis::aio::MultiplexedConnection) -> Self {
Self::new(conn)
}
}
impl From<RedisConnection> for redis::aio::MultiplexedConnection {
fn from(conn: RedisConnection) -> Self {
conn.into_inner()
}
}
/// Financial instrument types
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
#[sqlx(type_name = "instrument_type", rename_all = "snake_case")]
pub enum InstrumentType {
/// Stock or equity security
Equity,
/// Fixed income bond
Bond,
/// Physical commodity or commodity future
Commodity,
/// Foreign exchange currency pair
Currency,
/// Generic derivative instrument
Derivative,
/// Futures contract
Future,
/// Options contract
Option,
/// Interest rate or currency swap
Swap,
/// Contract for difference
Cfd,
/// Cryptocurrency
Crypto,
}
/// Asset classes for risk categorization
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
#[sqlx(type_name = "asset_class", rename_all = "snake_case")]
pub enum AssetClass {
/// Equity securities and stocks
Equities,
/// Bonds and fixed income securities
FixedIncome,
/// Physical and financial commodities
Commodities,
/// Foreign exchange and currencies
Currencies,
/// Alternative investments
Alternatives,
/// Derivative instruments
Derivatives,
/// Cash and cash equivalents
Cash,
}
/// Market sectors for concentration risk
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
#[sqlx(type_name = "market_sector", rename_all = "snake_case")]
pub enum MarketSector {
/// Technology and software companies
Technology,
/// Healthcare and pharmaceutical companies
Healthcare,
/// Financial services and banking
Financials,
/// Energy and oil companies
Energy,
/// Consumer goods and services
Consumer,
/// Industrial and manufacturing companies
Industrials,
/// Basic materials and mining
Materials,
/// Utilities and infrastructure
Utilities,
/// Real estate and REITs
RealEstate,
/// Telecommunications and media
Telecommunications,
/// Government bonds and securities
Government,
/// Other or unclassified sectors
Other,
}
/// Trading venues
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
#[sqlx(type_name = "venue_type", rename_all = "snake_case")]
pub enum VenueType {
/// Regulated exchange
Exchange,
/// Electronic Communication Network
Ecn,
/// Dark pool venue
DarkPool,
/// Over-the-counter market
OverTheCounter,
/// Internal crossing network
InternalCross,
/// Systematic Internalizer
Systematic,
}
/// Risk metric types
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
#[sqlx(type_name = "risk_metric_type", rename_all = "snake_case")]
pub enum RiskMetricType {
/// Value at Risk calculation
Var,
/// Expected Shortfall (Conditional `VaR`)
ExpectedShortfall,
/// Maximum drawdown metric
MaxDrawdown,
/// Sharpe ratio calculation
SharpeRatio,
/// Beta coefficient
Beta,
/// Volatility measurement
Volatility,
/// Correlation analysis
Correlation,
/// Portfolio concentration risk
ConcentrationRisk,
/// Liquidity risk assessment
LiquidityRisk,
/// Counterparty risk evaluation
CounterpartyRisk,
}
/// Time periods for risk calculations
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
#[sqlx(type_name = "time_period", rename_all = "snake_case")]
pub enum TimePeriod {
/// Intraday time period (within a day)
Intraday,
/// Daily time period
Daily,
/// Weekly time period
Weekly,
/// Monthly time period
Monthly,
/// Quarterly time period
Quarterly,
/// Yearly time period
Yearly,
}
/// Financial instrument master data
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct Instrument {
/// Unique identifier for the instrument
pub id: Uuid,
/// Trading symbol (e.g., "AAPL", "MSFT")
pub symbol: String,
/// International Securities Identification Number
pub isin: Option<String>,
/// Committee on Uniform Securities Identification Procedures number
pub cusip: Option<String>,
/// Bloomberg identifier for the instrument
pub bloomberg_id: Option<String>,
/// Reuters identifier for the instrument
pub reuters_id: Option<String>,
/// Full name of the instrument (e.g., "Apple Inc.")
pub name: String,
/// Type of financial instrument
pub instrument_type: InstrumentType,
/// Asset class categorization
pub asset_class: AssetClass,
/// Market sector classification
pub sector: Option<MarketSector>,
/// Base currency of the instrument (ISO 3-letter code)
pub currency: String,
/// Primary exchange where the instrument is traded
pub exchange: Option<String>,
/// Minimum price movement (tick size)
pub tick_size: Option<Decimal>,
/// Standard trading lot size
pub lot_size: Option<Decimal>,
/// Contract multiplier for derivatives
pub multiplier: Option<Decimal>,
/// Maturity date for bonds, futures, and options
pub maturity_date: Option<DateTime<Utc>>,
/// Strike price for options and warrants
pub strike_price: Option<Decimal>,
/// Option type ("Call" or "Put" for options)
pub option_type: Option<String>,
/// Symbol of underlying asset for derivatives
pub underlying_symbol: Option<String>,
/// Whether the instrument is currently active for trading
pub is_active: bool,
/// Timestamp when the record was created
pub created_at: DateTime<Utc>,
/// Timestamp when the record was last updated
pub updated_at: DateTime<Utc>,
/// Additional instrument metadata as JSON
pub metadata: serde_json::Value,
}
/// Portfolio definition
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct Portfolio {
/// Unique portfolio identifier
pub id: String,
/// Human-readable portfolio name
pub name: String,
/// Optional portfolio description
pub description: Option<String>,
/// Base currency for portfolio calculations (ISO 3-letter code)
pub base_currency: String,
/// Portfolio type (Strategy, Client, Prop, etc.)
pub portfolio_type: String,
/// Date when the portfolio was created/incepted
pub inception_date: DateTime<Utc>,
/// Identifier of the portfolio manager
pub manager_id: String,
/// Benchmark symbol for performance comparison
pub benchmark: Option<String>,
/// Allocated risk budget (typically as volatility target)
pub risk_budget: Option<Decimal>,
/// Value at Risk limit for the portfolio
pub var_limit: Option<Decimal>,
/// Maximum allowed drawdown percentage
pub max_drawdown_limit: Option<Decimal>,
/// Whether the portfolio is currently active
pub is_active: bool,
/// Timestamp when the record was created
pub created_at: DateTime<Utc>,
/// Timestamp when the record was last updated
pub updated_at: DateTime<Utc>,
/// Additional portfolio metadata as JSON
pub metadata: serde_json::Value,
}
/// Position snapshot for risk calculations
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct Position {
/// Unique position identifier
pub id: Uuid,
/// Portfolio containing this position
pub portfolio_id: String,
/// Instrument symbol being held
pub symbol: String,
/// Number of shares/contracts held (positive for long, negative for short)
pub quantity: Decimal,
/// Average price at which the position was established
pub average_price: Decimal,
/// Current market price of the instrument
pub market_price: Decimal,
/// Total market value of the position (quantity × market price)
pub market_value: Decimal,
/// Unrealized profit and loss on the position
pub unrealized_pnl: Decimal,
/// Currency of the position (ISO 3-letter code)
pub currency: String,
/// Date when the position was first established
pub entry_date: DateTime<Utc>,
/// Timestamp of last update to position data
pub last_updated: DateTime<Utc>,
/// Position weight as percentage of total portfolio value
pub weight: Option<Decimal>,
/// Beta coefficient relative to market benchmark
pub beta: Option<Decimal>,
/// Duration for fixed income securities
pub duration: Option<Decimal>,
/// Delta sensitivity for derivatives (price sensitivity)
pub delta: Option<Decimal>,
/// Gamma sensitivity for derivatives (delta sensitivity)
pub gamma: Option<Decimal>,
/// Vega sensitivity for derivatives (volatility sensitivity)
pub vega: Option<Decimal>,
/// Theta sensitivity for derivatives (time decay)
pub theta: Option<Decimal>,
}
/// Daily portfolio performance metrics
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct PortfolioPerformance {
/// Unique performance record identifier
pub id: Uuid,
/// Portfolio this performance data belongs to
pub portfolio_id: String,
/// Date of the performance calculation
pub date: DateTime<Utc>,
/// Net Asset Value of the portfolio
pub nav: Decimal,
/// Daily return percentage
pub daily_return: Decimal,
/// Cumulative return since inception
pub cumulative_return: Decimal,
/// Annualized volatility
pub volatility: Decimal,
/// Sharpe ratio (risk-adjusted return)
pub sharpe_ratio: Option<Decimal>,
/// Maximum drawdown percentage
pub max_drawdown: Decimal,
/// Value at Risk at 95% confidence level
pub var_95: Option<Decimal>,
/// Value at Risk at 99% confidence level
pub var_99: Option<Decimal>,
/// Expected Shortfall at 95% confidence level
pub expected_shortfall_95: Option<Decimal>,
/// Beta coefficient relative to benchmark
pub beta: Option<Decimal>,
/// Alpha (excess return over benchmark)
pub alpha: Option<Decimal>,
/// Information ratio (alpha divided by tracking error)
pub information_ratio: Option<Decimal>,
/// Portfolio turnover rate
pub turnover: Option<Decimal>,
/// Weight of the largest position in the portfolio
pub largest_position: Option<Decimal>,
/// Total number of positions held
pub number_of_positions: i32,
/// Sector exposure breakdown as JSON
pub sector_concentration: serde_json::Value,
/// Currency exposure breakdown as JSON
pub currency_exposure: serde_json::Value,
}
/// Risk factor exposures
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct RiskFactorExposure {
/// Unique risk factor exposure identifier
pub id: Uuid,
/// Portfolio this exposure belongs to
pub portfolio_id: String,
/// Factor name (e.g., "Equity Market", "Interest Rates")
pub risk_factor: String,
/// Factor type ("Market", "Style", "Currency", "Country", etc.)
pub factor_type: String,
/// Factor loading/exposure amount
pub exposure: Decimal,
/// Contribution to portfolio variance
pub contribution_to_risk: Decimal,
/// Date of the exposure calculation
pub date: DateTime<Utc>,
/// Confidence interval for the exposure estimate
pub confidence_interval: Option<Decimal>,
/// R-squared goodness of fit measure
pub r_squared: Option<Decimal>,
}
/// Stress test scenarios
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct StressScenario {
/// Unique stress scenario identifier
pub id: Uuid,
/// Human-readable scenario name
pub name: String,
/// Detailed description of the stress scenario
pub description: String,
/// Scenario type (Historical, Hypothetical, Monte Carlo)
pub scenario_type: String,
/// Whether this scenario is currently active for testing
pub active: bool,
/// Factor shocks definition as JSON
pub shock_factors: serde_json::Value,
/// User who created this scenario
pub created_by: String,
/// Timestamp when the scenario was created
pub created_at: DateTime<Utc>,
/// Timestamp when the scenario was last updated
pub updated_at: DateTime<Utc>,
}
/// Stress test results
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct StressTestResult {
/// Unique stress test result identifier
pub id: Uuid,
/// Portfolio that was stress tested
pub portfolio_id: String,
/// Stress scenario that was applied
pub scenario_id: Uuid,
/// Date when the stress test was performed
pub test_date: DateTime<Utc>,
/// Portfolio value before applying stress
pub base_portfolio_value: Decimal,
/// Portfolio value after applying stress scenario
pub stressed_portfolio_value: Decimal,
/// Absolute loss amount from the stress test
pub absolute_loss: Decimal,
/// Percentage loss from the stress test
pub percentage_loss: Decimal,
/// Symbol of the worst performing position
pub worst_performing_position: Option<String>,
/// Loss amount of the worst performing position
pub worst_position_loss: Option<Decimal>,
/// Impact breakdown by sector as JSON
pub sector_impacts: serde_json::Value,
/// Detailed stress test results as JSON
pub detailed_results: serde_json::Value,
}
/// Counterparty information for counterparty risk
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct Counterparty {
/// Unique counterparty identifier
pub id: String,
/// Legal name of the counterparty
pub name: String,
/// Counterparty type (Bank, Broker, Exchange, etc.)
pub counterparty_type: String,
/// Country where the counterparty is domiciled
pub country: String,
/// Credit rating from rating agencies
pub credit_rating: Option<String>,
/// Legal Entity Identifier code
pub lei_code: Option<String>,
/// Parent company if applicable
pub parent_company: Option<String>,
/// Whether the counterparty is currently active
pub is_active: bool,
/// Maximum allowed exposure to this counterparty
pub exposure_limit: Option<Decimal>,
/// Margin requirement for this counterparty
pub margin_requirement: Option<Decimal>,
/// Whether a netting agreement is in place
pub netting_agreement: bool,
/// Timestamp when the record was created
pub created_at: DateTime<Utc>,
/// Timestamp when the record was last updated
pub updated_at: DateTime<Utc>,
/// Additional counterparty metadata as JSON
pub metadata: serde_json::Value,
}
/// Counterparty exposure tracking
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct CounterpartyExposure {
/// Unique counterparty exposure identifier
pub id: Uuid,
/// Counterparty this exposure relates to
pub counterparty_id: String,
/// Portfolio generating this exposure (if applicable)
pub portfolio_id: Option<String>,
/// Exposure type (Current, Potential, Settlement)
pub exposure_type: String,
/// Gross exposure amount before netting
pub gross_exposure: Decimal,
/// Net exposure amount after netting agreements
pub net_exposure: Decimal,
/// Collateral held from the counterparty
pub collateral_held: Decimal,
/// Collateral posted to the counterparty
pub collateral_posted: Decimal,
/// Current mark-to-market value
pub mark_to_market: Decimal,
/// Currency of the exposure (ISO 3-letter code)
pub currency: String,
/// Maturity bucket classification (0-1Y, 1-5Y, etc.)
pub maturity_bucket: Option<String>,
/// Risk weight for regulatory capital calculations
pub risk_weight: Option<Decimal>,
/// Date of the exposure calculation
pub date: DateTime<Utc>,
}
/// Liquidity metrics for positions
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct LiquidityMetrics {
/// Unique liquidity metrics identifier
pub id: Uuid,
/// Instrument symbol being analyzed
pub symbol: String,
/// Date of the liquidity analysis
pub date: DateTime<Utc>,
/// Average daily trading volume
pub average_daily_volume: Decimal,
/// Bid-ask spread in basis points
pub bid_ask_spread_bps: Decimal,
/// Market impact coefficient for large trades
pub market_impact_coefficient: Option<Decimal>,
/// Days to liquidate 10% of average daily volume
pub days_to_liquidate_10pct: Option<Decimal>,
/// Days to liquidate 50% of average daily volume
pub days_to_liquidate_50pct: Option<Decimal>,
/// Overall liquidity score (1-10 scale)
pub liquidity_score: Option<Decimal>,
/// High frequency trading volume ratio
pub high_frequency_ratio: Option<Decimal>,
/// Dark pool volume ratio
pub dark_pool_ratio: Option<Decimal>,
/// Price volatility measure
pub volatility: Decimal,
/// Amihud illiquidity measure
pub amihud_illiquidity: Option<Decimal>,
}
/// Economic scenarios for scenario analysis
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct EconomicScenario {
/// Unique economic scenario identifier
pub id: Uuid,
/// Human-readable scenario name
pub name: String,
/// Detailed description of the economic scenario
pub description: String,
/// Probability assignment for this scenario
pub probability: Option<Decimal>,
/// Time horizon for the scenario
pub time_horizon: TimePeriod,
/// GDP growth rate change in the scenario
pub gdp_growth_rate: Option<Decimal>,
/// Inflation rate change in the scenario
pub inflation_rate: Option<Decimal>,
/// Interest rate change in the scenario
pub interest_rate_change: Option<Decimal>,
/// Unemployment rate in the scenario
pub unemployment_rate: Option<Decimal>,
/// Currency pair shocks as JSON
pub currency_shock: serde_json::Value,
/// Commodity price shocks as JSON
pub commodity_shock: serde_json::Value,
/// Market index shocks as JSON
pub equity_market_shock: serde_json::Value,
/// Volatility regime changes as JSON
pub volatility_shock: serde_json::Value,
/// User who created this scenario
pub created_by: String,
/// Timestamp when the scenario was created
pub created_at: DateTime<Utc>,
/// Whether this scenario is currently active
pub is_active: bool,
}
/// Risk report templates
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct RiskReportTemplate {
/// Unique report template identifier
pub id: Uuid,
/// Human-readable template name
pub name: String,
/// Description of the report template
pub description: String,
/// Report type (Daily, Weekly, Monthly, Regulatory)
pub report_type: String,
/// Report structure and parameters as JSON
pub template_config: serde_json::Value,
/// Email distribution list as JSON
pub recipients: serde_json::Value,
/// Cron schedule for automated reports
pub schedule_cron: Option<String>,
/// Whether this template is currently active
pub is_active: bool,
/// User who created this template
pub created_by: String,
/// Timestamp when the template was created
pub created_at: DateTime<Utc>,
/// Timestamp when the template was last updated
pub updated_at: DateTime<Utc>,
}
/// Generated risk reports
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct RiskReport {
/// Unique risk report identifier
pub id: Uuid,
/// Template used to generate this report
pub template_id: Uuid,
/// Portfolio this report covers (if applicable)
pub portfolio_id: Option<String>,
/// Date the report covers
pub report_date: DateTime<Utc>,
/// Timestamp when the report was generated
pub generated_at: DateTime<Utc>,
/// User who generated the report
pub generated_by: String,
/// Full report content as JSON
pub report_data: serde_json::Value,
/// Path to generated PDF/Excel file
pub file_path: Option<String>,
/// Report status (Generated, Sent, Failed)
pub status: String,
/// Error message if report generation failed
pub error_message: Option<String>,
/// List of recipients who received the report as JSON
pub recipients_sent: serde_json::Value,
}
/// Market data feeds configuration
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct MarketDataFeed {
/// Unique market data feed identifier
pub id: Uuid,
/// Name of the data provider
pub provider_name: String,
/// Feed type (Real-time, End-of-day, Historical)
pub feed_type: String,
/// List of symbols covered by this feed as JSON
pub symbols_covered: serde_json::Value,
/// Connection parameters as JSON
pub connection_config: serde_json::Value,
/// Whether this is the primary feed (vs backup)
pub is_primary: bool,
/// Whether this feed is currently active
pub is_active: bool,
/// Latency SLA in milliseconds
pub latency_sla_ms: Option<i32>,
/// Uptime SLA as percentage
pub uptime_sla_pct: Option<Decimal>,
/// Timestamp of last heartbeat from the feed
pub last_heartbeat: Option<DateTime<Utc>>,
/// Timestamp when the record was created
pub created_at: DateTime<Utc>,
/// Timestamp when the record was last updated
pub updated_at: DateTime<Utc>,
}
/// Risk calculation jobs queue
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct RiskCalculationJob {
/// Unique job identifier
pub id: Uuid,
/// Job type (`VaR`, `StressTest`, Scenario, etc.)
pub job_type: String,
/// Portfolio to calculate risk for (if applicable)
pub portfolio_id: Option<String>,
/// Job-specific parameters as JSON
pub parameters: serde_json::Value,
/// Job priority (1-10, higher is more urgent)
pub priority: i32,
/// Job status (Queued, Running, Completed, Failed)
pub status: String,
/// Timestamp when job execution started
pub started_at: Option<DateTime<Utc>>,
/// Timestamp when job execution completed
pub completed_at: Option<DateTime<Utc>>,
/// Job completion percentage (0-100)
pub progress_pct: Option<Decimal>,
/// Job result data as JSON
pub result_data: Option<serde_json::Value>,
/// Error message if job failed
pub error_message: Option<String>,
/// Number of times this job has been retried
pub retry_count: i32,
/// Maximum number of retry attempts allowed
pub max_retries: i32,
/// User who created this job
pub created_by: String,
/// Timestamp when the job was created
pub created_at: DateTime<Utc>,
}
/// Custom risk metrics configuration
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct CustomRiskMetric {
/// Unique custom risk metric identifier
pub id: Uuid,
/// Human-readable metric name
pub name: String,
/// Description of what this metric measures
pub description: String,
/// Mathematical formula or SQL query for calculation
pub formula: String,
/// Configurable parameters as JSON
pub parameters: serde_json::Value,
/// Output type (Number, Percentage, Currency)
pub output_type: String,
/// Calculation frequency
pub frequency: TimePeriod,
/// Metric scope (Portfolio, Position, Global)
pub scope: String,
/// Whether this metric is currently active
pub is_active: bool,
/// User who created this metric
pub created_by: String,
/// Timestamp when the metric was created
pub created_at: DateTime<Utc>,
/// Timestamp when the metric was last updated
pub updated_at: DateTime<Utc>,
}
/// Calculated custom risk metrics
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct CustomRiskMetricResult {
/// Unique metric result identifier
pub id: Uuid,
/// Custom risk metric that was calculated
pub metric_id: Uuid,
/// Portfolio this result applies to (if applicable)
pub portfolio_id: Option<String>,
/// Symbol this result applies to (if applicable)
pub symbol: Option<String>,
/// Date when the calculation was performed
pub calculation_date: DateTime<Utc>,
/// Calculated metric value
pub value: Decimal,
/// Additional calculation details as JSON
pub metadata: serde_json::Value,
}
/// Common financial calculations and utilities
#[derive(Debug)]
pub struct FinancialCalculations;
impl FinancialCalculations {
/// Calculate annualized volatility from daily returns
#[allow(clippy::arithmetic_side_effects)]
pub fn annualized_volatility(daily_vol: Decimal) -> Decimal {
daily_vol * Decimal::from(16_i32) // sqrt(252) ≈ 15.87, using 16 as approximation
}
/// Calculate Sharpe ratio
#[allow(clippy::arithmetic_side_effects)]
pub fn sharpe_ratio(
returns: Decimal,
risk_free_rate: Decimal,
volatility: Decimal,
) -> Option<Decimal> {
if volatility == Decimal::ZERO {
None
} else {
Some((returns - risk_free_rate) / volatility)
}
}
/// Calculate maximum drawdown
///
/// # Returns
/// - `Ok(Decimal)` - Maximum drawdown as a percentage
///
/// - `Err(String)` - Error if peak is zero or calculation fails
///
///
/// # Errors
/// Returns error if the operation fails
/// # Errors
/// Returns error if peak is zero or negative, preventing valid drawdown calculation
#[allow(clippy::arithmetic_side_effects)]
pub fn max_drawdown(peak: Decimal, trough: Decimal) -> Result<Decimal, String> {
if peak == Decimal::ZERO {
// CRITICAL: Zero peak value prevents meaningful drawdown calculation
// This could indicate:
// 1. No historical high water mark (data corruption)
// 2. Portfolio started with zero value (configuration error)
// 3. Missing performance data
return Err(
"Cannot calculate drawdown with zero peak value - this may indicate \
missing performance data or data corruption"
.to_owned(),
);
}
if peak < Decimal::ZERO {
return Err(format!("Invalid negative peak value: {}", peak));
}
Ok(((trough - peak) / peak) * Decimal::from(100_i32))
}
}
/// Portfolio aggregation utilities
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PortfolioSummary {
/// Total market value of all positions
pub total_market_value: Decimal,
/// Breakdown of exposure by currency
pub currency_breakdown: HashMap<String, Decimal>,
/// Breakdown of exposure by market sector
pub sector_breakdown: HashMap<MarketSector, Decimal>,
/// Breakdown of exposure by asset class
pub asset_class_breakdown: HashMap<AssetClass, Decimal>,
/// Top positions by weight (Symbol, Weight)
pub top_positions: Vec<(String, Decimal)>,
/// Total number of positions in the portfolio
pub number_of_positions: usize,
/// Weight of the largest single position
pub largest_position_weight: Decimal,
/// Effective number of positions (diversification measure)
pub effective_number_of_positions: Decimal,
/// Gross exposure (sum of absolute position values)
pub gross_exposure: Decimal,
/// Net exposure (sum of signed position values)
pub net_exposure: Decimal,
/// Portfolio beta relative to benchmark
pub beta: Option<Decimal>,
/// Tracking error relative to benchmark
pub tracking_error: Option<Decimal>,
}
/// Risk factor model utilities
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FactorModel {
/// Name of the risk factor model
pub model_name: String,
/// List of risk factors in the model
pub factors: Vec<String>,
/// Factor loadings for each symbol (Symbol -> Factor -> Loading)
pub factor_loadings: HashMap<String, HashMap<String, Decimal>>,
/// Covariance matrix between factors
pub factor_covariance_matrix: HashMap<String, HashMap<String, Decimal>>,
/// Specific (idiosyncratic) risks for each symbol
pub specific_risks: HashMap<String, Decimal>,
/// R-squared values for each symbol's factor model fit
pub r_squared: HashMap<String, Decimal>,
/// Timestamp when the model was last updated
pub last_updated: DateTime<Utc>,
}
/// Validation utilities
impl Instrument {
/// Validates the instrument configuration
///
/// # Errors
/// Returns error if symbol or name is empty, or currency is not a 3-character ISO code
pub fn validate(&self) -> Result<(), String> {
if self.symbol.is_empty() {
return Err("Symbol cannot be empty".to_owned());
}
if self.name.is_empty() {
return Err("Instrument name cannot be empty".to_owned());
}
if self.currency.len() != 3 {
return Err("Currency must be 3-character ISO code".to_owned());
}
Ok(())
}
}
impl Portfolio {
/// Validates the portfolio configuration
///
/// # Errors
///
/// Returns error if:
/// - Portfolio name is empty
/// - Currency is not a 3-character ISO code
/// - No instruments are defined
///
/// # Errors
/// Returns error if portfolio ID or name is empty, currency is invalid, or VAR limit is not positive
pub fn validate(&self) -> Result<(), String> {
if self.id.is_empty() {
return Err("Portfolio ID cannot be empty".to_owned());
}
if self.name.is_empty() {
return Err("Portfolio name cannot be empty".to_owned());
}
if self.base_currency.len() != 3 {
return Err("Base currency must be 3-character ISO code".to_owned());
}
if let Some(var_limit) = self.var_limit {
if var_limit <= Decimal::ZERO {
return Err("VaR limit must be positive".to_owned());
}
}
Ok(())
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_decimal_calculations() {
let daily_vol = Decimal::from_str_exact("0.02").unwrap();
let annual_vol = FinancialCalculations::annualized_volatility(daily_vol);
assert!(annual_vol > daily_vol);
let returns = Decimal::from_str_exact("0.12").unwrap();
let risk_free = Decimal::from_str_exact("0.03").unwrap();
let volatility = Decimal::from_str_exact("0.15").unwrap();
let sharpe = FinancialCalculations::sharpe_ratio(returns, risk_free, volatility).unwrap();
assert!(sharpe > Decimal::ZERO);
let peak = Decimal::from(100_i32);
let trough = Decimal::from(85_i32);
let drawdown = FinancialCalculations::max_drawdown(peak, trough);
assert_eq!(drawdown, Ok(Decimal::from(-15_i32)));
}
#[test]
fn test_instrument_validation() {
let valid_instrument = Instrument {
id: Uuid::new_v4(),
symbol: "AAPL".to_string(),
isin: Some("US0378331005".to_string()),
cusip: None,
bloomberg_id: Some("AAPL UW Equity".to_string()),
reuters_id: None,
name: "Apple Inc.".to_string(),
instrument_type: InstrumentType::Equity,
asset_class: AssetClass::Equities,
sector: Some(MarketSector::Technology),
currency: "USD".to_string(),
exchange: Some("NASDAQ".to_string()),
tick_size: Some(Decimal::from_str_exact("0.01").unwrap()),
lot_size: Some(Decimal::from(1_i32)),
multiplier: Some(Decimal::from(1_i32)),
maturity_date: None,
strike_price: None,
option_type: None,
underlying_symbol: None,
is_active: true,
created_at: Utc::now(),
updated_at: Utc::now(),
metadata: serde_json::json!({}),
};
valid_instrument.validate().unwrap();
// Test invalid currency
let invalid_instrument = Instrument {
currency: "INVALID".to_string(),
..valid_instrument
};
assert!(invalid_instrument.validate().is_err());
}
#[test]
fn test_portfolio_validation() {
let valid_portfolio = Portfolio {
id: "TEST_PORTFOLIO".to_string(),
name: "Test Portfolio".to_string(),
description: Some("Test portfolio for validation".to_string()),
base_currency: "USD".to_string(),
portfolio_type: "Strategy".to_string(),
inception_date: Utc::now(),
manager_id: "test_manager".to_string(),
benchmark: Some("SPY".to_string()),
risk_budget: Some(Decimal::from_str_exact("0.15").unwrap()),
var_limit: Some(Decimal::from(100_000_i32)),
max_drawdown_limit: Some(Decimal::from_str_exact("0.20").unwrap()),
is_active: true,
created_at: Utc::now(),
updated_at: Utc::now(),
metadata: serde_json::json!({}),
};
valid_portfolio.validate().unwrap();
// Test invalid VaR limit
let invalid_portfolio = Portfolio {
var_limit: Some(Decimal::from(-1_000_i32)),
..valid_portfolio
};
assert!(invalid_portfolio.validate().is_err());
}
}