Files
foxhunt/services/backtesting_service/src/storage.rs
jgrusewski 00ae84dd88 refactor: remove dead code and #[allow(dead_code)] annotations across workspace
Strip all 413 #[allow(dead_code)] annotations from 139 files and remove
the actual dead code they were suppressing: unused struct fields (and their
constructor sites), unused methods/functions, and entire dead structs.

Key removals:
- trading_engine compliance: ~50 dead structs/fields across audit, reporting, SOX modules
- trading_service: dead execution engine fields, broker routing, paper trading methods
- ml_training_service: dead TLS validation (~340 lines), GPU state, monitoring fields
- backtesting_service: dead model cache, TLS validation, TradeSignal fields
- risk: dead VaR engine fields, safety coordinator fields, position tracker fields
- adaptive-strategy: dead ensemble methods, regime detection, sizing functions

147 files changed, -4264 net lines. Workspace compiles with 0 errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 13:12:20 +01:00

359 lines
13 KiB
Rust

//! Storage layer for backtesting data persistence
use anyhow::{Context, Result};
use rust_decimal::{prelude::ToPrimitive, Decimal};
use sqlx::{PgPool, Row};
use tracing::info;
use crate::foxhunt::tli::BacktestStatus;
use crate::performance::PerformanceMetrics;
use crate::strategy_engine::BacktestTrade;
use common::database::DatabasePool;
use config::structures::BacktestingDatabaseConfig;
/// Backtest summary for listing
#[derive(Debug, Clone)]
pub struct BacktestSummary {
/// Backtest ID
pub backtest_id: String,
/// Strategy name
pub strategy_name: String,
/// Symbols tested
pub symbols: Vec<String>,
/// Current status
pub status: BacktestStatus,
/// Total return percentage
pub total_return: f64,
/// Sharpe ratio
pub sharpe_ratio: f64,
/// Maximum drawdown percentage
pub max_drawdown: f64,
/// Creation timestamp
pub created_at: chrono::DateTime<chrono::Utc>,
/// Start date of backtest
pub start_date: chrono::DateTime<chrono::Utc>,
/// End date of backtest
pub end_date: chrono::DateTime<chrono::Utc>,
/// Description
pub description: String,
}
/// Storage manager for backtesting data
#[derive(Debug)]
pub struct StorageManager {
/// Raw PgPool for compatibility with existing queries
pg_pool: PgPool,
}
impl StorageManager {
/// Create a new storage manager
pub async fn new(config: &BacktestingDatabaseConfig) -> Result<Self> {
info!("Initializing storage manager with HFT optimizations");
// Create backtesting-optimized database pool using config conversion
// The From implementation handles all field mapping automatically
let local_db_config: common::database::LocalDatabaseConfig = config.clone().into();
let db_pool = DatabasePool::new(local_db_config)
.await
.context("Failed to create HFT-optimized database pool")?;
let pg_pool = db_pool.pool().clone();
Ok(Self {
pg_pool,
})
}
/// Save backtest results to storage
pub async fn save_backtest_results(
&self,
backtest_id: &str,
trades: &[BacktestTrade],
metrics: &PerformanceMetrics,
) -> Result<()> {
info!("Saving backtest results for {}", backtest_id);
let mut tx = self.pg_pool.begin().await?;
// Save individual trades
for trade in trades {
sqlx::query(
r#"
INSERT INTO backtest_trades (
backtest_id, trade_id, symbol, side, quantity,
entry_price, exit_price, entry_time, exit_time,
pnl, return_percent, entry_signal, exit_signal
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
"#,
)
.bind(backtest_id)
.bind(&trade.trade_id)
.bind(&trade.symbol)
.bind(trade.side.to_string())
.bind(trade.quantity.to_f64())
.bind(trade.entry_price.to_f64())
.bind(trade.exit_price.to_f64())
.bind(trade.entry_time)
.bind(trade.exit_time)
.bind(trade.pnl.to_f64())
.bind(trade.return_percent.to_f64())
.bind(&trade.entry_signal)
.bind(&trade.exit_signal)
.execute(&mut *tx)
.await?;
}
// Save detailed performance metrics
sqlx::query(
r#"
INSERT INTO backtest_metrics (
backtest_id, total_return, annualized_return, sharpe_ratio,
sortino_ratio, max_drawdown, volatility, win_rate,
profit_factor, total_trades, winning_trades, losing_trades,
avg_win, avg_loss, largest_win, largest_loss, calmar_ratio,
var_95, expected_shortfall
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
"#,
)
.bind(backtest_id)
.bind(metrics.total_return)
.bind(metrics.annualized_return)
.bind(metrics.sharpe_ratio)
.bind(metrics.sortino_ratio)
.bind(metrics.max_drawdown)
.bind(metrics.volatility)
.bind(metrics.win_rate)
.bind(metrics.profit_factor)
.bind(metrics.total_trades as i64)
.bind(metrics.winning_trades as i64)
.bind(metrics.losing_trades as i64)
.bind(metrics.avg_win)
.bind(metrics.avg_loss)
.bind(metrics.largest_win)
.bind(metrics.largest_loss)
.bind(metrics.calmar_ratio)
.bind(metrics.var_95.unwrap_or(0.0))
.bind(metrics.expected_shortfall.unwrap_or(0.0))
.execute(&mut *tx)
.await?;
tx.commit().await?;
info!(
"Successfully saved {} trades and metrics for backtest {}",
trades.len(),
backtest_id
);
Ok(())
}
/// Load backtest results from storage
pub async fn load_backtest_results(
&self,
backtest_id: &str,
) -> Result<(Vec<BacktestTrade>, PerformanceMetrics)> {
info!("Loading backtest results for {}", backtest_id);
// Load trades
let trade_rows = sqlx::query(
r#"
SELECT trade_id, symbol, side, quantity, entry_price, exit_price,
entry_time, exit_time, pnl, return_percent, entry_signal, exit_signal
FROM backtest_trades
WHERE backtest_id = $1
ORDER BY entry_time
"#,
)
.bind(backtest_id)
.fetch_all(&self.pg_pool)
.await?;
let mut trades = Vec::new();
for row in trade_rows {
let side_str: String = row.try_get("side")?;
let side = match side_str.as_str() {
"Buy" => crate::strategy_engine::TradeSide::Buy,
"Sell" => crate::strategy_engine::TradeSide::Sell,
_ => continue, // Skip invalid trades
};
trades.push(BacktestTrade {
trade_id: row.try_get("trade_id")?,
symbol: row.try_get("symbol")?,
side,
quantity: Decimal::from_f64_retain(row.try_get::<f64, _>("quantity")?)
.unwrap_or(Decimal::ZERO),
entry_price: Decimal::from_f64_retain(row.try_get::<f64, _>("entry_price")?)
.unwrap_or(Decimal::ZERO),
exit_price: Decimal::from_f64_retain(row.try_get::<f64, _>("exit_price")?)
.unwrap_or(Decimal::ZERO),
entry_time: row.try_get("entry_time")?,
exit_time: row.try_get("exit_time")?,
pnl: Decimal::from_f64_retain(row.try_get::<f64, _>("pnl")?)
.unwrap_or(Decimal::ZERO),
return_percent: Decimal::from_f64_retain(row.try_get::<f64, _>("return_percent")?)
.unwrap_or(Decimal::ZERO),
entry_signal: row.try_get("entry_signal")?,
exit_signal: row.try_get("exit_signal")?,
});
}
// Load metrics
let metrics_row = sqlx::query(
r#"
SELECT total_return, annualized_return, sharpe_ratio, sortino_ratio,
max_drawdown, volatility, win_rate, profit_factor,
total_trades, winning_trades, losing_trades, avg_win, avg_loss,
largest_win, largest_loss, calmar_ratio, var_95, expected_shortfall
FROM backtest_metrics
WHERE backtest_id = $1
"#,
)
.bind(backtest_id)
.fetch_one(&self.pg_pool)
.await?;
// Calculate backtest duration from trades
let backtest_duration_nanos = if !trades.is_empty() {
let earliest = trades
.iter()
.map(|t| t.entry_time)
.min()
.ok_or_else(|| anyhow::anyhow!("No trades found for earliest time"))?;
let latest = trades
.iter()
.map(|t| t.exit_time)
.max()
.ok_or_else(|| anyhow::anyhow!("No trades found for latest time"))?;
(latest - earliest).num_nanoseconds().unwrap_or(0) as u64
} else {
0
};
let metrics = PerformanceMetrics {
total_return: metrics_row.try_get("total_return")?,
annualized_return: metrics_row.try_get("annualized_return")?,
sharpe_ratio: metrics_row.try_get("sharpe_ratio")?,
sortino_ratio: metrics_row.try_get("sortino_ratio")?,
max_drawdown: metrics_row.try_get("max_drawdown")?,
volatility: metrics_row.try_get("volatility")?,
win_rate: metrics_row.try_get("win_rate")?,
profit_factor: metrics_row.try_get("profit_factor")?,
total_trades: metrics_row.try_get::<i64, _>("total_trades")? as u64,
winning_trades: metrics_row.try_get::<i64, _>("winning_trades")? as u64,
losing_trades: metrics_row.try_get::<i64, _>("losing_trades")? as u64,
avg_win: metrics_row.try_get("avg_win")?,
avg_loss: metrics_row.try_get("avg_loss")?,
largest_win: metrics_row.try_get("largest_win")?,
largest_loss: metrics_row.try_get("largest_loss")?,
calmar_ratio: metrics_row.try_get("calmar_ratio")?,
backtest_duration_nanos: backtest_duration_nanos.try_into().unwrap_or(0),
beta: None,
alpha: None,
information_ratio: None,
var_95: Some(metrics_row.try_get("var_95")?),
expected_shortfall: Some(metrics_row.try_get("expected_shortfall")?),
};
info!(
"Loaded {} trades and metrics for backtest {}",
trades.len(),
backtest_id
);
Ok((trades, metrics))
}
/// List backtests with optional filtering
pub async fn list_backtests(
&self,
limit: u32,
offset: u32,
_strategy_name: Option<String>,
_status_filter: Option<BacktestStatus>,
) -> Result<Vec<BacktestSummary>> {
info!("Listing backtests with limit={}, offset={}", limit, offset);
// Simplified query without dynamic parameters for now
let rows = sqlx::query(
r#"
SELECT backtest_id, strategy_name, symbols, status, total_return,
sharpe_ratio, max_drawdown, created_at, start_date, end_date,
description
FROM backtests
ORDER BY created_at DESC
LIMIT $1 OFFSET $2
"#,
)
.bind(limit as i64)
.bind(offset as i64)
.fetch_all(&self.pg_pool)
.await?;
let mut summaries = Vec::new();
for row in rows {
let status_str: String = row.try_get("status")?;
let status = match status_str.as_str() {
"queued" => BacktestStatus::Queued,
"running" => BacktestStatus::Running,
"completed" => BacktestStatus::Completed,
"failed" => BacktestStatus::Failed,
"cancelled" => BacktestStatus::Cancelled,
"paused" => BacktestStatus::Paused,
_ => BacktestStatus::Unspecified,
};
// Parse symbols JSON array (simplified)
let symbols_str: String = row.try_get("symbols")?;
let symbols: Vec<String> =
serde_json::from_str(&symbols_str).unwrap_or_else(|_| vec![symbols_str.clone()]);
summaries.push(BacktestSummary {
backtest_id: row.try_get("backtest_id")?,
strategy_name: row.try_get("strategy_name")?,
symbols,
status,
total_return: row
.try_get::<Option<f64>, _>("total_return")?
.unwrap_or(0.0),
sharpe_ratio: row
.try_get::<Option<f64>, _>("sharpe_ratio")?
.unwrap_or(0.0),
max_drawdown: row
.try_get::<Option<f64>, _>("max_drawdown")?
.unwrap_or(0.0),
created_at: row.try_get("created_at")?,
start_date: row.try_get("start_date")?,
end_date: row.try_get("end_date")?,
description: row
.try_get::<Option<String>, _>("description")?
.unwrap_or_default(),
});
}
info!("Found {} backtest summaries", summaries.len());
Ok(summaries)
}
}
impl From<BacktestSummary> for crate::foxhunt::tli::BacktestSummary {
fn from(summary: BacktestSummary) -> Self {
Self {
backtest_id: summary.backtest_id,
strategy_name: summary.strategy_name,
symbols: summary.symbols,
status: summary.status as i32,
total_return: summary.total_return,
sharpe_ratio: summary.sharpe_ratio,
max_drawdown: summary.max_drawdown,
created_at_unix_nanos: summary.created_at.timestamp_nanos_opt().unwrap_or(0),
start_date_unix_nanos: summary.start_date.timestamp_nanos_opt().unwrap_or(0),
end_date_unix_nanos: summary.end_date.timestamp_nanos_opt().unwrap_or(0),
description: summary.description,
}
}
}