Eliminate ~4,260 clippy deny-level errors that blocked workspace-wide clippy runs. Errors cascaded: upstream crate failures (ctrader-openapi, risk-data) hid thousands of downstream errors in ml, tli, backtesting. Key changes: - ctrader-openapi: fix shadow_unrelated/shadow_reuse (renamed vars) - risk-data/risk: replace non-ASCII em dashes with ASCII equivalents - tli: allow deny lints on prost-generated proto code, fix shadows - trading_engine: fix let_underscore_must_use, wildcard matches, shadows - broker_gateway_service: allow dead_code on unused redis_client field - ml (4030 errors): remove local deny overrides for unwrap/expect/indexing (workspace warn level sufficient), add crate-level allows for non-safety mass-violation lints (non_ascii_literal, shadow_*, str_to_string, etc.), batch-fix em dashes, unseparated literal suffixes, format_push_string, wildcard matches, impl_trait_in_params, mutex_atomic, and more - backtesting: replace unwrap() on first()/last() with match destructure - tests: simplify loop-that-never-loops, fix mutex unwrap Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
357 lines
12 KiB
Rust
357 lines
12 KiB
Rust
//! Type System Bridge for ML-Financial Integration
|
|
//!
|
|
//! This module provides seamless conversion between ML computational types (f64/f32)
|
|
//! and canonical financial types (common::Price, common::Decimal). It ensures type
|
|
//! consistency across the ML-financial system boundary while maintaining computational
|
|
//! efficiency for pure ML operations.
|
|
|
|
use crate::{MLError, MLResult};
|
|
use common::Price;
|
|
use rust_decimal::prelude::FromPrimitive;
|
|
use rust_decimal::Decimal;
|
|
|
|
// Note: Using common::Price directly now, no alias needed
|
|
|
|
/// Conversion utilities for ML numeric types to financial types
|
|
#[derive(Debug)]
|
|
pub struct MLFinancialBridge;
|
|
|
|
impl MLFinancialBridge {
|
|
/// Convert f64 ML value to common::Price with validation
|
|
pub fn f64_to_price(value: f64) -> MLResult<Price> {
|
|
Price::from_f64(value).map_err(|e| {
|
|
MLError::InvalidInput(format!(
|
|
"Price conversion failed for value {}: {}",
|
|
value, e
|
|
))
|
|
})
|
|
}
|
|
|
|
/// Convert f64 ML value to common::Price (fixed-point) with validation
|
|
pub fn f64_to_common_price(value: f64) -> MLResult<Price> {
|
|
Price::from_f64(value).map_err(|e| {
|
|
MLError::InvalidInput(format!(
|
|
"Common price conversion failed for value {}: {}",
|
|
value, e
|
|
))
|
|
})
|
|
}
|
|
|
|
/// Convert f32 ML value to common::Price with validation
|
|
pub fn f32_to_price(value: f32) -> MLResult<Price> {
|
|
Self::f64_to_price(value as f64)
|
|
}
|
|
|
|
/// Convert f32 ML value to common::Price (fixed-point) with validation
|
|
pub fn f32_to_common_price(value: f32) -> MLResult<Price> {
|
|
Self::f64_to_common_price(value as f64)
|
|
}
|
|
|
|
/// Convert f64 ML value to common::Decimal with validation
|
|
pub fn f64_to_decimal(value: f64) -> MLResult<Decimal> {
|
|
Decimal::from_f64(value).ok_or_else(|| {
|
|
MLError::InvalidInput(format!(
|
|
"Decimal conversion failed for f64 value: {}",
|
|
value
|
|
))
|
|
})
|
|
}
|
|
|
|
/// Convert f32 ML value to common::Decimal with validation
|
|
pub fn f32_to_decimal(value: f32) -> MLResult<Decimal> {
|
|
Self::f64_to_decimal(value as f64)
|
|
}
|
|
|
|
/// Convert common::Price to f64 for ML computations
|
|
pub fn price_to_f64(price: &Price) -> f64 {
|
|
price.to_f64()
|
|
}
|
|
|
|
/// Convert common::Price (fixed-point) to f64 for ML computations
|
|
pub fn common_price_to_f64(price: &Price) -> f64 {
|
|
price.to_f64()
|
|
}
|
|
|
|
/// Convert common::Price to f32 for ML computations
|
|
pub fn price_to_f32(price: &Price) -> f32 {
|
|
price.to_f64() as f32
|
|
}
|
|
|
|
/// Convert common::Price (fixed-point) to f32 for ML computations
|
|
pub fn common_price_to_f32(price: &Price) -> f32 {
|
|
price.to_f64() as f32
|
|
}
|
|
|
|
/// Convert common::Decimal to f64 for ML computations
|
|
pub fn decimal_to_f64(decimal: &Decimal) -> MLResult<f64> {
|
|
use rust_decimal::prelude::ToPrimitive;
|
|
decimal.to_f64().ok_or_else(|| {
|
|
MLError::InvalidInput(format!("Failed to convert Decimal {} to f64", decimal))
|
|
})
|
|
}
|
|
|
|
/// Convert common::Decimal to f32 for ML computations
|
|
pub fn decimal_to_f32(decimal: &Decimal) -> MLResult<f32> {
|
|
use rust_decimal::prelude::ToPrimitive;
|
|
decimal.to_f32().ok_or_else(|| {
|
|
MLError::InvalidInput(format!("Failed to convert Decimal {} to f32", decimal))
|
|
})
|
|
}
|
|
|
|
/// Batch convert f64 vector to Price vector
|
|
pub fn f64_vec_to_prices(values: &[f64]) -> MLResult<Vec<Price>> {
|
|
values.iter().map(|&v| Self::f64_to_price(v)).collect()
|
|
}
|
|
|
|
/// Batch convert Price vector to f64 vector
|
|
pub fn prices_to_f64_vec(prices: &[Price]) -> Vec<f64> {
|
|
prices.iter().map(Self::price_to_f64).collect()
|
|
}
|
|
|
|
/// Batch convert f64 vector to Decimal vector
|
|
pub fn f64_vec_to_decimals(values: &[f64]) -> MLResult<Vec<Decimal>> {
|
|
values.iter().map(|&v| Self::f64_to_decimal(v)).collect()
|
|
}
|
|
|
|
/// Batch convert Decimal vector to f64 vector
|
|
pub fn decimals_to_f64_vec(decimals: &[Decimal]) -> MLResult<Vec<f64>> {
|
|
decimals.iter().map(Self::decimal_to_f64).collect()
|
|
}
|
|
}
|
|
|
|
/// Trait for types that can be converted to financial types
|
|
pub trait ToFinancial {
|
|
/// Convert to common::Price
|
|
fn to_price(&self) -> MLResult<Price>;
|
|
|
|
/// Convert to common::Decimal
|
|
fn to_decimal(&self) -> MLResult<Decimal>;
|
|
}
|
|
|
|
/// Trait for types that can be converted from financial types
|
|
pub trait FromFinancial<T> {
|
|
/// Convert from common::Price
|
|
fn from_price(price: &Price) -> Self;
|
|
|
|
/// Convert from common::Decimal
|
|
fn from_decimal(decimal: &Decimal) -> MLResult<Self>
|
|
where
|
|
Self: Sized;
|
|
}
|
|
|
|
impl ToFinancial for f64 {
|
|
fn to_price(&self) -> MLResult<Price> {
|
|
MLFinancialBridge::f64_to_price(*self)
|
|
}
|
|
|
|
fn to_decimal(&self) -> MLResult<Decimal> {
|
|
MLFinancialBridge::f64_to_decimal(*self)
|
|
}
|
|
}
|
|
|
|
impl ToFinancial for f32 {
|
|
fn to_price(&self) -> MLResult<Price> {
|
|
MLFinancialBridge::f32_to_price(*self)
|
|
}
|
|
|
|
fn to_decimal(&self) -> MLResult<Decimal> {
|
|
MLFinancialBridge::f32_to_decimal(*self)
|
|
}
|
|
}
|
|
|
|
impl FromFinancial<Price> for f64 {
|
|
fn from_price(price: &Price) -> Self {
|
|
MLFinancialBridge::price_to_f64(price)
|
|
}
|
|
|
|
fn from_decimal(decimal: &Decimal) -> MLResult<Self> {
|
|
MLFinancialBridge::decimal_to_f64(decimal)
|
|
}
|
|
}
|
|
|
|
impl FromFinancial<Price> for f32 {
|
|
fn from_price(price: &Price) -> Self {
|
|
MLFinancialBridge::price_to_f32(price)
|
|
}
|
|
|
|
fn from_decimal(decimal: &Decimal) -> MLResult<Self> {
|
|
MLFinancialBridge::decimal_to_f32(decimal)
|
|
}
|
|
}
|
|
|
|
/// Specialized converters for common ML use cases
|
|
pub mod converters {
|
|
use super::*;
|
|
|
|
/// Convert ML prediction results to financial format
|
|
#[derive(Debug)]
|
|
pub struct PredictionConverter;
|
|
|
|
impl PredictionConverter {
|
|
/// Convert ML model output (f64) to trading signal with Price
|
|
pub fn prediction_to_signal(
|
|
prediction: f64,
|
|
confidence: f64,
|
|
current_price: &Price,
|
|
) -> MLResult<(Price, Decimal)> {
|
|
// Convert prediction to price change
|
|
let price_change = prediction * MLFinancialBridge::price_to_f64(current_price);
|
|
let new_price = MLFinancialBridge::f64_to_price(
|
|
MLFinancialBridge::price_to_f64(current_price) + price_change,
|
|
)?;
|
|
|
|
let confidence_decimal = MLFinancialBridge::f64_to_decimal(confidence)?;
|
|
|
|
Ok((new_price, confidence_decimal))
|
|
}
|
|
|
|
/// Convert order book features (f32 array) to Price/Decimal format
|
|
pub fn features_to_financial(
|
|
features: &[f32],
|
|
feature_names: &[&str],
|
|
) -> MLResult<Vec<(String, Decimal)>> {
|
|
features
|
|
.iter()
|
|
.zip(feature_names.iter())
|
|
.map(|(&value, &name)| {
|
|
let decimal = MLFinancialBridge::f32_to_decimal(value)?;
|
|
Ok((name.to_string(), decimal))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Convert portfolio weights (f64 array) to Decimal format
|
|
pub fn weights_to_decimals(weights: &[f64]) -> MLResult<Vec<Decimal>> {
|
|
MLFinancialBridge::f64_vec_to_decimals(weights)
|
|
}
|
|
}
|
|
|
|
/// Convert financial data to ML format
|
|
#[derive(Debug)]
|
|
pub struct FinancialConverter;
|
|
|
|
impl FinancialConverter {
|
|
/// Convert price history to ML features (f64 array)
|
|
pub fn prices_to_features(prices: &[Price]) -> Vec<f64> {
|
|
MLFinancialBridge::prices_to_f64_vec(prices)
|
|
}
|
|
|
|
/// Convert price and volume data to ML input matrix
|
|
pub fn market_data_to_matrix(
|
|
prices: &[Price],
|
|
volumes: &[Decimal],
|
|
) -> MLResult<Vec<Vec<f64>>> {
|
|
let price_features = Self::prices_to_features(prices);
|
|
let volume_features = MLFinancialBridge::decimals_to_f64_vec(volumes)?;
|
|
|
|
Ok(price_features
|
|
.into_iter()
|
|
.zip(volume_features.into_iter())
|
|
.map(|(p, v)| vec![p, v])
|
|
.collect())
|
|
}
|
|
|
|
/// Normalize prices for ML input (log returns)
|
|
pub fn prices_to_log_returns(prices: &[Price]) -> Vec<f64> {
|
|
let price_values = Self::prices_to_features(prices);
|
|
price_values
|
|
.windows(2)
|
|
.filter_map(|window| {
|
|
let p0 = window.get(0)?;
|
|
let p1 = window.get(1)?;
|
|
(*p0 > 0.0).then(|| (p1 / p0).ln())
|
|
})
|
|
.collect()
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_f64_to_price_conversion() {
|
|
let value = 123.45;
|
|
let price = MLFinancialBridge::f64_to_price(value).unwrap();
|
|
assert!((MLFinancialBridge::price_to_f64(&price) - value).abs() < 1e-8);
|
|
}
|
|
|
|
#[test]
|
|
fn test_f64_to_decimal_conversion() {
|
|
let value = 123.45;
|
|
let decimal = MLFinancialBridge::f64_to_decimal(value).unwrap();
|
|
let back_to_f64 = MLFinancialBridge::decimal_to_f64(&decimal).unwrap();
|
|
assert!((back_to_f64 - value).abs() < 1e-10);
|
|
}
|
|
|
|
#[test]
|
|
fn test_batch_conversions() {
|
|
let values = vec![10.0, 20.0, 30.0];
|
|
let prices = MLFinancialBridge::f64_vec_to_prices(&values).unwrap();
|
|
let back_to_f64 = MLFinancialBridge::prices_to_f64_vec(&prices);
|
|
|
|
for (original, converted) in values.into_iter().zip(back_to_f64.into_iter()) {
|
|
assert!((original - converted).abs() < 1e-8);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_trait_implementations() {
|
|
let value = 42.0_f64;
|
|
let price = value.to_price().unwrap();
|
|
let back_to_f64 = f64::from_price(&price);
|
|
assert!((value - back_to_f64).abs() < 1e-8);
|
|
}
|
|
|
|
#[test]
|
|
fn test_prediction_converter() {
|
|
use converters::PredictionConverter;
|
|
|
|
let current_price_decimal = Decimal::from_f64(100.0).unwrap();
|
|
let current_price = Price::from(current_price_decimal);
|
|
let prediction = 0.05; // 5% increase
|
|
let confidence = 0.85;
|
|
|
|
let (new_price, conf_decimal) =
|
|
PredictionConverter::prediction_to_signal(prediction, confidence, ¤t_price)
|
|
.unwrap();
|
|
|
|
assert!((MLFinancialBridge::price_to_f64(&new_price) - 105.0).abs() < 1e-6);
|
|
assert!((MLFinancialBridge::decimal_to_f64(&conf_decimal).unwrap() - 0.85).abs() < 1e-10);
|
|
}
|
|
|
|
#[test]
|
|
fn test_financial_converter() {
|
|
use converters::FinancialConverter;
|
|
|
|
let prices: Vec<Price> = vec![
|
|
Decimal::from_f64(100.0).unwrap(),
|
|
Decimal::from_f64(105.0).unwrap(),
|
|
Decimal::from_f64(110.0).unwrap(),
|
|
]
|
|
.into_iter()
|
|
.map(Price::from)
|
|
.collect();
|
|
|
|
let log_returns = FinancialConverter::prices_to_log_returns(&prices);
|
|
assert_eq!(log_returns.len(), 2);
|
|
let r0 = log_returns.get(0).copied().unwrap();
|
|
let r1 = log_returns.get(1).copied().unwrap();
|
|
assert!((r0 - (105.0_f64 / 100.0_f64).ln()).abs() < 1e-10);
|
|
assert!((r1 - (110.0_f64 / 105.0_f64).ln()).abs() < 1e-10);
|
|
}
|
|
|
|
#[test]
|
|
fn test_invalid_conversions() {
|
|
// Test negative price conversion
|
|
assert!(MLFinancialBridge::f64_to_price(-10.0).is_err());
|
|
|
|
// Test NaN conversion
|
|
assert!(MLFinancialBridge::f64_to_price(f64::NAN).is_err());
|
|
|
|
// Test infinity conversion
|
|
assert!(MLFinancialBridge::f64_to_price(f64::INFINITY).is_err());
|
|
}
|
|
}
|