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>
359 lines
13 KiB
Rust
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,
|
|
}
|
|
}
|
|
}
|