Move 17 library crates into crates/, CLI binary into bin/fxt, consolidate 10 test crates into testing/, split config crate from deployment config files. Root directory reduced from 38+ to ~17 directories. All Cargo.toml paths and build.rs proto refs updated. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1043 lines
35 KiB
Rust
1043 lines
35 KiB
Rust
//! 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());
|
||
}
|
||
}
|