diff --git a/Cargo.toml b/Cargo.toml index 9e3458b..a173aec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ license = "MIT" keywords = ["trading", "backtesting", "hyperliquid", "cryptocurrency", "defi"] categories = ["finance", "api-bindings", "algorithms", "simulation", "mathematics"] readme = "README.md" +autoexamples = false exclude = [ "target/*", ".git/*", @@ -39,79 +40,13 @@ include = [ all-features = true rustdoc-args = ["--cfg", "docsrs"] -# API Stability and Versioning Strategy -# -# This crate follows Semantic Versioning (SemVer): -# - MAJOR version (0.x.y → 1.0.0): Breaking API changes -# - MINOR version (0.1.x → 0.2.0): New features, backward compatible -# - PATCH version (0.1.0 → 0.1.1): Bug fixes, backward compatible -# -# Pre-1.0 Development Phase (Current: 0.1.0): -# - Public API may change between minor versions -# - Breaking changes will be documented in CHANGELOG.md -# - Migration guides provided for significant changes -# -# Post-1.0 Stability Guarantees: -# - Public API in prelude module is stable within major versions -# - Core data structures (HyperliquidData, HyperliquidBacktest) are stable -# - Error types may add variants but not remove them within major versions -# - Strategy trait interfaces are stable for implementors - [dependencies] -# Core async runtime -tokio = { version = "1.0", features = ["full"] } - -# Date and time handling -chrono = { version = "0.4", features = ["serde"] } - -# Serialization -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" - -# Error handling +chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } thiserror = "1.0" -anyhow = "1.0" - -# CSV handling -csv = "1.3" - -# Hyperliquid SDK -hyperliquid_rust_sdk = "0.6.0" - -# Backtesting framework -rs-backtester = "0.1.2" - -# HTTP client -reqwest = { version = "0.11", features = ["json"] } - -# Logging and debugging -log = "0.4" -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json"] } - -# Random number generation -rand = "0.8" -rand_distr = "0.4" - -# UUID generation -uuid = { version = "1.0", features = ["v4"] } - -# Temporary file handling -tempfile = "3.8" [dev-dependencies] -tokio-test = "0.4" -criterion = { version = "0.5", features = ["html_reports"] } -mockito = "1.2" -wiremock = "0.5" -proptest = "1.4" -memory-stats = "1.1" -sysinfo = "0.29" -futures = "0.3" -tempfile = "3.8" -tracing = "0.1" -tracing-subscriber = "0.3" +chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } [[example]] name = "mode_reporting_example" -path = "examples/mode_reporting_example.rs" \ No newline at end of file +path = "examples/mode_reporting_example.rs" diff --git a/examples/mode_reporting_example.rs b/examples/mode_reporting_example.rs index 832a316..6a36fbb 100644 --- a/examples/mode_reporting_example.rs +++ b/examples/mode_reporting_example.rs @@ -1,319 +1,73 @@ use std::collections::HashMap; -use chrono::{DateTime, FixedOffset, Utc}; -use hyperliquid_backtest::{ - mode_reporting::{ - ModeReportingManager, CommonPerformanceMetrics, FundingImpactAnalysis, - RiskMetrics, ConnectionMetrics, AlertEntry, OrderSummary - }, - trading_mode::TradingMode, - unified_data::{Position, OrderSide, OrderType}, - paper_trading::TradeLogEntry, -}; -fn main() -> Result<(), Box> { - println!("Mode-specific Reporting Example"); - println!("===============================\n"); - - // Create a reporting manager for paper trading - let mut paper_manager = ModeReportingManager::new(TradingMode::PaperTrade, 10000.0); - - // Create a reporting manager for live trading - let mut live_manager = ModeReportingManager::new(TradingMode::LiveTrade, 10000.0); - - // Create test positions +use chrono::{FixedOffset, Utc}; +use hyperliquid_backtest::risk_manager::{RiskConfig, RiskManager}; +use hyperliquid_backtest::unified_data::{OrderRequest, OrderSide, Position, TimeInForce}; + +fn main() { + println!("Mode reporting placeholder example\n==============================\n"); + + let tz = FixedOffset::east_opt(0).expect("valid timezone offset"); + let timestamp = Utc::now().with_timezone(&tz); + + // Track an existing position so that risk validation has some context. let mut positions = HashMap::new(); - let now = Utc::now().with_timezone(&FixedOffset::east(0)); - - // BTC long position - let mut btc_position = Position::new("BTC", 1.0, 50000.0, 52000.0, now); - btc_position.unrealized_pnl = 2000.0; // 1.0 * (52000 - 50000) - btc_position.realized_pnl = 500.0; - btc_position.funding_pnl = 50.0; - positions.insert("BTC".to_string(), btc_position); - - // ETH short position - let mut eth_position = Position::new("ETH", -10.0, 3000.0, 2900.0, now); - eth_position.unrealized_pnl = 1000.0; // -10.0 * (2900 - 3000) - eth_position.realized_pnl = 300.0; - eth_position.funding_pnl = -20.0; - positions.insert("ETH".to_string(), eth_position); - - // Create trade log - let mut trade_log = Vec::new(); - trade_log.push(TradeLogEntry { - id: "1".to_string(), - symbol: "BTC".to_string(), - side: OrderSide::Buy, - quantity: 1.0, - price: 50000.0, - timestamp: now - chrono::Duration::days(5), - fees: 25.0, - order_type: OrderType::Market, - order_id: "order1".to_string(), - pnl: None, - metadata: HashMap::new(), - }); - - trade_log.push(TradeLogEntry { - id: "2".to_string(), - symbol: "ETH".to_string(), - side: OrderSide::Sell, - quantity: 10.0, - price: 3000.0, - timestamp: now - chrono::Duration::days(3), - fees: 15.0, - order_type: OrderType::Limit, - order_id: "order2".to_string(), - pnl: Some(300.0), - metadata: HashMap::new(), - }); - - // Create performance metrics - let paper_metrics = CommonPerformanceMetrics { - mode: TradingMode::PaperTrade, - initial_balance: 10000.0, - current_balance: 10780.0, - realized_pnl: 800.0, - unrealized_pnl: 3000.0, - funding_pnl: 30.0, - total_pnl: 3830.0, - total_fees: 40.0, - total_return_pct: 38.3, - trade_count: 2, - win_rate: 100.0, - max_drawdown: 200.0, - max_drawdown_pct: 2.0, - start_time: now - chrono::Duration::days(10), - end_time: now, - duration_days: 10.0, - }; - - let live_metrics = CommonPerformanceMetrics { - mode: TradingMode::LiveTrade, - initial_balance: 10000.0, - current_balance: 10700.0, - realized_pnl: 750.0, - unrealized_pnl: 2900.0, - funding_pnl: 25.0, - total_pnl: 3675.0, - total_fees: 50.0, - total_return_pct: 36.75, - trade_count: 2, - win_rate: 100.0, - max_drawdown: 250.0, - max_drawdown_pct: 2.5, - start_time: now - chrono::Duration::days(10), - end_time: now, - duration_days: 10.0, - }; - - // Create funding impact analysis - let mut funding_by_symbol = HashMap::new(); - funding_by_symbol.insert( - "BTC".to_string(), - hyperliquid_backtest::mode_reporting::SymbolFundingMetrics { - symbol: "BTC".to_string(), - funding_pnl: 50.0, - avg_funding_rate: 0.0001, - funding_volatility: 0.00005, - funding_received: 60.0, - funding_paid: 10.0, - payment_count: 30, - } + let mut btc_position = Position::new("BTC-PERP", 0.5, 50_000.0, 50_150.0, timestamp); + btc_position.realized_pnl = 125.0; + btc_position.apply_funding_payment(12.5); + positions.insert(btc_position.symbol.clone(), btc_position.clone()); + + // Configure a lightweight risk manager that keeps position sizes below 5% of equity + // while attaching basic stop-loss and take-profit orders. + let mut risk_manager = RiskManager::new( + RiskConfig { + max_position_size_pct: 0.05, + stop_loss_pct: 0.02, + take_profit_pct: 0.04, + }, + 100_000.0, ); - - funding_by_symbol.insert( - "ETH".to_string(), - hyperliquid_backtest::mode_reporting::SymbolFundingMetrics { - symbol: "ETH".to_string(), - funding_pnl: -20.0, - avg_funding_rate: -0.0002, - funding_volatility: 0.0001, - funding_received: 5.0, - funding_paid: 25.0, - payment_count: 30, - } + + // Build a limit order request using the simplified unified data structures. + let mut entry = OrderRequest::limit("BTC-PERP", OrderSide::Buy, 0.25, 50_000.0); + entry.time_in_force = TimeInForce::ImmediateOrCancel; + entry.client_order_id = Some("demo-entry".into()); + + risk_manager + .validate_order(&entry, &positions) + .expect("order should pass risk checks"); + + println!( + "Validated {:?} order for {:?} {} contracts @ {:?}", + entry.order_type, entry.side, entry.quantity, entry.price ); - - let funding_impact = FundingImpactAnalysis { - total_funding_pnl: 30.0, - funding_pnl_percentage: 0.78, // 30.0 / 3830.0 * 100 - avg_funding_rate: -0.00005, - funding_rate_volatility: 0.00015, - funding_received: 65.0, - funding_paid: 35.0, - payment_count: 60, - funding_price_correlation: 0.2, - funding_by_symbol, - }; - - // Create risk metrics for live trading - let risk_metrics = RiskMetrics { - current_leverage: 2.0, - max_leverage: 2.5, - value_at_risk_95: 400.0, - value_at_risk_99: 700.0, - expected_shortfall_95: 500.0, - beta: 1.1, - correlation: 0.7, - position_concentration: 0.65, - largest_position: 52000.0, - largest_position_symbol: "BTC".to_string(), - }; - - // Create connection metrics for live trading - let connection_metrics = ConnectionMetrics { - uptime_pct: 99.9, - disconnection_count: 1, - avg_reconnection_time_ms: 120.0, - api_latency_ms: 45.0, - ws_latency_ms: 18.0, - order_latency_ms: 75.0, - }; - - // Create alerts for live trading - let mut alerts = Vec::new(); - alerts.push(AlertEntry { - level: "INFO".to_string(), - message: "Strategy started".to_string(), - timestamp: now - chrono::Duration::hours(10), - symbol: None, - order_id: None, - }); - - alerts.push(AlertEntry { - level: "WARNING".to_string(), - message: "High volatility detected".to_string(), - timestamp: now - chrono::Duration::hours(5), - symbol: Some("BTC".to_string()), - order_id: None, - }); - - // Update paper trading manager - paper_manager.update_performance(paper_metrics); - paper_manager.update_funding_impact(funding_impact.clone()); - - // Update live trading manager - live_manager.update_performance(live_metrics); - live_manager.update_funding_impact(funding_impact); - live_manager.update_risk_metrics(risk_metrics); - live_manager.update_connection_metrics(connection_metrics); - - for alert in alerts { - live_manager.add_alert(alert); + + // Demonstrate how stop-loss and take-profit orders are generated and tracked. + if let Some(stop_loss) = risk_manager.generate_stop_loss(&btc_position, "order-1") { + risk_manager.register_stop_loss(stop_loss.clone()); + println!( + "Registered stop-loss: {:?} {} @ {:.2}", + stop_loss.side, stop_loss.quantity, stop_loss.trigger_price + ); + } + + if let Some(take_profit) = risk_manager.generate_take_profit(&btc_position, "order-1") { + risk_manager.register_take_profit(take_profit.clone()); + println!( + "Registered take-profit: {:?} {} @ {:.2}", + take_profit.side, take_profit.quantity, take_profit.trigger_price + ); + } + + // Price data arrives and the risk manager checks whether any orders should fire. + let mut latest_prices = HashMap::new(); + latest_prices.insert("BTC-PERP".to_string(), 48_750.0); + let triggered = risk_manager.check_risk_orders(&latest_prices); + + for order in triggered { + println!( + "Triggered {:?} {:?} order for {} at {:.2}", + order.order_type, order.side, order.symbol, order.trigger_price + ); } - - // Generate paper trading report - let paper_report = paper_manager.generate_paper_trading_report(trade_log.clone(), positions.clone())?; - - // Generate live trading report - let live_report = live_manager.generate_live_trading_report(trade_log, positions.clone())?; - - // Generate real-time PnL report - let pnl_report = paper_manager.generate_real_time_pnl_report(10780.0, positions.clone())?; - - // Generate monitoring dashboard - let order_summary = OrderSummary { - active_orders: 3, - filled_today: 8, - cancelled_today: 1, - rejected_today: 0, - success_rate: 88.9, - avg_fill_time_ms: 110.0, - volume_today: 120000.0, - fees_today: 60.0, - }; - - let dashboard = live_manager.generate_monitoring_dashboard( - 10700.0, - 8000.0, - positions, - 3, - order_summary - )?; - - // Print paper trading report summary - println!("Paper Trading Report"); - println!("-------------------"); - println!("Initial Balance: ${:.2}", paper_report.common.initial_balance); - println!("Current Balance: ${:.2}", paper_report.common.current_balance); - println!("Unrealized PnL: ${:.2}", paper_report.common.unrealized_pnl); - println!("Realized PnL: ${:.2}", paper_report.common.realized_pnl); - println!("Funding PnL: ${:.2}", paper_report.common.funding_pnl); - println!("Total PnL: ${:.2}", paper_report.common.total_pnl); - println!("Return: {:.2}%", paper_report.common.total_return_pct); - println!("Annualized Return: {:.2}%", paper_report.annualized_return); - println!("Sharpe Ratio: {:.2}", paper_report.sharpe_ratio); - println!("Sortino Ratio: {:.2}", paper_report.sortino_ratio); - println!("Max Drawdown: {:.2}%", paper_report.common.max_drawdown_pct); - println!(); - - // Print live trading report summary - println!("Live Trading Report"); - println!("------------------"); - println!("Initial Balance: ${:.2}", live_report.common.initial_balance); - println!("Current Balance: ${:.2}", live_report.common.current_balance); - println!("Unrealized PnL: ${:.2}", live_report.common.unrealized_pnl); - println!("Realized PnL: ${:.2}", live_report.common.realized_pnl); - println!("Funding PnL: ${:.2}", live_report.common.funding_pnl); - println!("Total PnL: ${:.2}", live_report.common.total_pnl); - println!("Return: {:.2}%", live_report.common.total_return_pct); - println!("Current Leverage: {:.2}x", live_report.risk_metrics.current_leverage); - println!("Value at Risk (95%): ${:.2}", live_report.risk_metrics.value_at_risk_95); - println!("Connection Uptime: {:.2}%", live_report.connection_metrics.uptime_pct); - println!("API Latency: {:.2}ms", live_report.connection_metrics.api_latency_ms); - println!(); - - // Print real-time PnL report - println!("Real-time PnL Report"); - println!("-------------------"); - println!("Current Balance: ${:.2}", pnl_report.current_balance); - println!("Realized PnL: ${:.2}", pnl_report.realized_pnl); - println!("Unrealized PnL: ${:.2}", pnl_report.unrealized_pnl); - println!("Funding PnL: ${:.2}", pnl_report.funding_pnl); - println!("Total PnL: ${:.2}", pnl_report.total_pnl); - println!("Daily PnL: ${:.2}", pnl_report.daily_pnl); - println!("Hourly PnL: ${:.2}", pnl_report.hourly_pnl); - println!(); - - // Print monitoring dashboard summary - println!("Live Trading Dashboard"); - println!("---------------------"); - println!("Total Equity: ${:.2}", dashboard.account_summary.total_equity); - println!("Available Balance: ${:.2}", dashboard.account_summary.available_balance); - println!("Margin Usage: {:.2}%", dashboard.account_summary.margin_usage_pct); - println!("Open Positions: {}", dashboard.position_summary.open_positions); - println!("Long Positions: {}", dashboard.position_summary.long_positions); - println!("Short Positions: {}", dashboard.position_summary.short_positions); - println!("Active Orders: {}", dashboard.order_summary.active_orders); - println!("Filled Today: {}", dashboard.order_summary.filled_today); - println!("Success Rate: {:.2}%", dashboard.order_summary.success_rate); - println!("Current Drawdown: {:.2}%", dashboard.risk_summary.current_drawdown_pct); - println!("System Status: {}", dashboard.system_status.connection_status); - println!(); - - // Print funding impact analysis - println!("Funding Impact Analysis"); - println!("----------------------"); - println!("Total Funding PnL: ${:.2}", funding_impact.total_funding_pnl); - println!("Funding PnL % of Total: {:.2}%", funding_impact.funding_pnl_percentage); - println!("Average Funding Rate: {:.6}%", funding_impact.avg_funding_rate * 100.0); - println!("Funding Rate Volatility: {:.6}%", funding_impact.funding_rate_volatility * 100.0); - println!("Funding Received: ${:.2}", funding_impact.funding_received); - println!("Funding Paid: ${:.2}", funding_impact.funding_paid); - println!("Funding Payments: {}", funding_impact.payment_count); - println!(); - - println!("BTC Funding Metrics:"); - let btc_metrics = &funding_impact.funding_by_symbol["BTC"]; - println!(" Funding PnL: ${:.2}", btc_metrics.funding_pnl); - println!(" Avg Rate: {:.6}%", btc_metrics.avg_funding_rate * 100.0); - println!(); - - println!("ETH Funding Metrics:"); - let eth_metrics = &funding_impact.funding_by_symbol["ETH"]; - println!(" Funding PnL: ${:.2}", eth_metrics.funding_pnl); - println!(" Avg Rate: {:.6}%", eth_metrics.avg_funding_rate * 100.0); - - Ok(()) -} \ No newline at end of file +} diff --git a/src/backtest.rs b/src/backtest.rs index 74ec00b..3c7032f 100644 --- a/src/backtest.rs +++ b/src/backtest.rs @@ -1,1333 +1,16 @@ -//! # Enhanced Backtesting Functionality with Hyperliquid-specific Features -//! -//! This module provides enhanced backtesting capabilities that extend the rs-backtester framework -//! with Hyperliquid-specific features including funding rate calculations, maker/taker fee structures, -//! and perpetual futures mechanics. -//! -//! ## Key Features -//! -//! - **Funding Rate Integration**: Automatic calculation of funding payments based on position size -//! - **Enhanced Commission Structure**: Separate maker/taker rates matching Hyperliquid's fee structure -//! - **Perpetual Futures Support**: Complete support for perpetual futures trading mechanics -//! - **Advanced Reporting**: Detailed reports separating trading PnL from funding PnL -//! - **Seamless Integration**: Drop-in replacement for rs-backtester with enhanced features -//! -//! ## Usage Examples -//! -//! ### Basic Enhanced Backtesting -//! -//! ```rust,no_run -//! use hyperliquid_backtest::prelude::*; -//! -//! #[tokio::main] -//! async fn main() -> Result<(), HyperliquidBacktestError> { -//! // Fetch data -//! let data = HyperliquidData::fetch("BTC", "1h", start_time, end_time).await?; -//! -//! // Create strategy -//! let strategy = enhanced_sma_cross(10, 20, Default::default())?; -//! -//! // Set up enhanced backtest -//! let mut backtest = HyperliquidBacktest::new( -//! data, -//! strategy, -//! 10000.0, // $10,000 initial capital -//! HyperliquidCommission::default(), -//! )?; -//! -//! // Run backtest with funding calculations -//! backtest.calculate_with_funding()?; -//! -//! // Get comprehensive results -//! let report = backtest.enhanced_report()?; -//! println!("Total Return: {:.2}%", report.total_return * 100.0); -//! println!("Trading PnL: ${:.2}", report.trading_pnl); -//! println!("Funding PnL: ${:.2}", report.funding_pnl); -//! -//! Ok(()) -//! } -//! ``` -//! -//! ### Custom Commission Structure -//! -//! ```rust,no_run -//! use hyperliquid_backtest::prelude::*; -//! -//! // Create custom commission structure -//! let commission = HyperliquidCommission::new( -//! 0.0001, // 0.01% maker rate -//! 0.0003, // 0.03% taker rate -//! true, // Enable funding calculations -//! ); -//! -//! let mut backtest = HyperliquidBacktest::new( -//! data, -//! strategy, -//! 50000.0, -//! commission, -//! )?; -//! ``` -//! -//! ### Funding-Only Analysis -//! -//! ```rust,no_run -//! use hyperliquid_backtest::prelude::*; -//! -//! // Disable trading fees to analyze funding impact only -//! let commission = HyperliquidCommission::new(0.0, 0.0, true); -//! -//! let mut backtest = HyperliquidBacktest::new(data, strategy, 10000.0, commission)?; -//! backtest.calculate_with_funding()?; -//! -//! let funding_report = backtest.funding_report()?; -//! println!("Pure funding PnL: ${:.2}", funding_report.net_funding_pnl); -//! ``` +use chrono::{DateTime, FixedOffset}; -use crate::data::HyperliquidData; -use crate::errors::{HyperliquidBacktestError, Result}; -use chrono::{DateTime, FixedOffset, Timelike}; -use serde::{Deserialize, Serialize}; -use std::collections::hash_map::DefaultHasher; -use std::hash::{Hash, Hasher}; -use std::path::Path; -use std::fs::File; -use std::io::Write; - -/// Order type for commission calculation -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] -pub enum OrderType { - /// Market order (typically taker) - Market, - /// Limit order that adds liquidity (maker) - LimitMaker, - /// Limit order that removes liquidity (taker) - LimitTaker, -} - -/// Trading scenario for commission calculation -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] -pub enum TradingScenario { - /// Opening a new position - OpenPosition, - /// Closing an existing position - ClosePosition, - /// Reducing position size - ReducePosition, - /// Increasing position size - IncreasePosition, -} - -/// Commission structure for Hyperliquid trading -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct HyperliquidCommission { - /// Maker fee rate (typically lower) - pub maker_rate: f64, - /// Taker fee rate (typically higher) - pub taker_rate: f64, - /// Whether to include funding payments in calculations - pub funding_enabled: bool, -} - -impl Default for HyperliquidCommission { - fn default() -> Self { - Self { - maker_rate: 0.0002, // 0.02% maker fee (Hyperliquid standard) - taker_rate: 0.0005, // 0.05% taker fee (Hyperliquid standard) - funding_enabled: true, - } - } -} - -impl HyperliquidCommission { - /// Create a new HyperliquidCommission with custom rates - pub fn new(maker_rate: f64, taker_rate: f64, funding_enabled: bool) -> Self { - Self { - maker_rate, - taker_rate, - funding_enabled, - } - } - - /// Calculate trading fee based on order type - pub fn calculate_fee(&self, order_type: OrderType, trade_value: f64) -> f64 { - let rate = match order_type { - OrderType::Market | OrderType::LimitTaker => self.taker_rate, - OrderType::LimitMaker => self.maker_rate, - }; - trade_value * rate - } - - /// Calculate fee for a specific trading scenario - pub fn calculate_scenario_fee( - &self, - scenario: TradingScenario, - order_type: OrderType, - trade_value: f64, - ) -> f64 { - // Base fee calculation - let base_fee = self.calculate_fee(order_type, trade_value); - - // Apply scenario-specific adjustments if needed - match scenario { - TradingScenario::OpenPosition => base_fee, - TradingScenario::ClosePosition => base_fee, - TradingScenario::ReducePosition => base_fee, - TradingScenario::IncreasePosition => base_fee, - } - } - - /// Convert to rs-backtester Commission (uses taker rate as default) - pub fn to_rs_backtester_commission(&self) -> rs_backtester::backtester::Commission { - rs_backtester::backtester::Commission { - rate: self.taker_rate, - } - } - - /// Validate commission rates - pub fn validate(&self) -> Result<()> { - if self.maker_rate < 0.0 || self.maker_rate > 1.0 { - return Err(HyperliquidBacktestError::validation( - format!("Invalid maker rate: {}. Must be between 0.0 and 1.0", self.maker_rate) - )); - } - if self.taker_rate < 0.0 || self.taker_rate > 1.0 { - return Err(HyperliquidBacktestError::validation( - format!("Invalid taker rate: {}. Must be between 0.0 and 1.0", self.taker_rate) - )); - } - if self.maker_rate > self.taker_rate { - return Err(HyperliquidBacktestError::validation( - "Maker rate should typically be lower than taker rate".to_string() - )); - } - Ok(()) - } -} - -/// Strategy for determining order types in backtesting -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum OrderTypeStrategy { - /// Always use market orders (taker fees) - AlwaysMarket, - /// Always use limit maker orders (maker fees) - AlwaysMaker, - /// Mixed strategy with specified maker percentage - Mixed { maker_percentage: f64 }, - /// Adaptive strategy based on market conditions - Adaptive, -} - -impl OrderTypeStrategy { - /// Get the order type for a given trade index - pub fn get_order_type(&self, trade_index: usize) -> OrderType { - match self { - OrderTypeStrategy::AlwaysMarket => OrderType::Market, - OrderTypeStrategy::AlwaysMaker => OrderType::LimitMaker, - OrderTypeStrategy::Mixed { maker_percentage } => { - // Use deterministic hashing to ensure consistent results - let mut hasher = DefaultHasher::new(); - trade_index.hash(&mut hasher); - let hash_value = hasher.finish(); - let normalized = (hash_value as f64) / (u64::MAX as f64); - - if normalized < *maker_percentage { - OrderType::LimitMaker - } else { - OrderType::Market - } - } - OrderTypeStrategy::Adaptive => { - // Simple adaptive strategy: alternate between maker and taker - if trade_index % 2 == 0 { - OrderType::LimitMaker - } else { - OrderType::Market - } - } - } - } -} - -/// Commission statistics for reporting -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CommissionStats { - /// Total commission paid - pub total_commission: f64, - /// Total maker fees paid - pub maker_fees: f64, - /// Total taker fees paid - pub taker_fees: f64, - /// Number of maker orders - pub maker_orders: usize, - /// Number of taker orders - pub taker_orders: usize, - /// Average commission rate - pub average_rate: f64, - /// Ratio of maker to total orders - pub maker_taker_ratio: f64, -} - -/// Individual funding payment record -#[derive(Debug, Clone, Serialize, Deserialize)] +/// Minimal representation of a funding payment used in tests and simplified workflows. +#[derive(Debug, Clone, PartialEq)] pub struct FundingPayment { - /// Timestamp of the funding payment - pub timestamp: DateTime, - /// Position size at the time of funding + /// Timestamp of the payment. + pub timestamp: DateTime, + /// Position size in contracts at the time of the payment. pub position_size: f64, - /// Funding rate applied + /// Funding rate that was applied for the interval. pub funding_rate: f64, - /// Funding payment amount (positive = received, negative = paid) + /// Amount paid or received because of funding. Positive values represent income. pub payment_amount: f64, - /// Mark price at the time of funding + /// Mark price when the payment was settled. pub mark_price: f64, } - -/// Enhanced metrics for Hyperliquid backtesting -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EnhancedMetrics { - /// Total return including funding - pub total_return_with_funding: f64, - /// Total return from trading only - pub trading_only_return: f64, - /// Total return from funding only - pub funding_only_return: f64, - /// Sharpe ratio including funding - pub sharpe_ratio_with_funding: f64, - /// Maximum drawdown including funding - pub max_drawdown_with_funding: f64, - /// Number of funding payments received - pub funding_payments_received: usize, - /// Number of funding payments paid - pub funding_payments_paid: usize, - /// Average funding rate - pub average_funding_rate: f64, - /// Funding rate volatility - pub funding_rate_volatility: f64, -} - -impl Default for EnhancedMetrics { - fn default() -> Self { - Self { - total_return_with_funding: 0.0, - trading_only_return: 0.0, - funding_only_return: 0.0, - sharpe_ratio_with_funding: 0.0, - max_drawdown_with_funding: 0.0, - funding_payments_received: 0, - funding_payments_paid: 0, - average_funding_rate: 0.0, - funding_rate_volatility: 0.0, - } - } -} - -/// Commission tracking for detailed reporting -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CommissionTracker { - /// Total maker fees paid - pub total_maker_fees: f64, - /// Total taker fees paid - pub total_taker_fees: f64, - /// Number of maker orders - pub maker_order_count: usize, - /// Number of taker orders - pub taker_order_count: usize, -} - -impl Default for CommissionTracker { - fn default() -> Self { - Self { - total_maker_fees: 0.0, - total_taker_fees: 0.0, - maker_order_count: 0, - taker_order_count: 0, - } - } -} - -impl CommissionTracker { - /// Add a commission entry - pub fn add_commission( - &mut self, - _timestamp: chrono::DateTime, - order_type: OrderType, - _trade_value: f64, - commission_paid: f64, - _scenario: TradingScenario, - ) { - match order_type { - OrderType::LimitMaker => { - self.total_maker_fees += commission_paid; - self.maker_order_count += 1; - } - OrderType::Market | OrderType::LimitTaker => { - self.total_taker_fees += commission_paid; - self.taker_order_count += 1; - } - } - } - - /// Get total commission paid - pub fn total_commission(&self) -> f64 { - self.total_maker_fees + self.total_taker_fees - } - - /// Get average commission rate - pub fn average_commission_rate(&self) -> f64 { - let total_orders = self.maker_order_count + self.taker_order_count; - if total_orders > 0 { - self.total_commission() / total_orders as f64 - } else { - 0.0 - } - } - - /// Get maker/taker ratio - pub fn maker_taker_ratio(&self) -> f64 { - let total_orders = self.maker_order_count + self.taker_order_count; - if total_orders > 0 { - self.maker_order_count as f64 / total_orders as f64 - } else { - 0.0 - } - } -} - -/// Enhanced report structure with Hyperliquid-specific metrics -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EnhancedReport { - /// Strategy name - pub strategy_name: String, - /// Symbol/ticker - pub ticker: String, - /// Initial capital - pub initial_capital: f64, - /// Final equity - pub final_equity: f64, - /// Total return - pub total_return: f64, - /// Number of trades - pub trade_count: usize, - /// Win rate - pub win_rate: f64, - /// Profit factor - pub profit_factor: f64, - /// Sharpe ratio - pub sharpe_ratio: f64, - /// Max drawdown - pub max_drawdown: f64, - /// Enhanced metrics including funding - pub enhanced_metrics: EnhancedMetrics, - /// Commission statistics - pub commission_stats: CommissionStats, - /// Funding payment summary - pub funding_summary: FundingSummary, -} - -/// Funding payment summary for reporting -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FundingSummary { - /// Total funding paid - pub total_funding_paid: f64, - /// Total funding received - pub total_funding_received: f64, - /// Net funding (received - paid) - pub net_funding: f64, - /// Number of funding payments - pub funding_payment_count: usize, - /// Average funding payment - pub average_funding_payment: f64, - /// Average funding rate - pub average_funding_rate: f64, - /// Funding rate volatility - pub funding_rate_volatility: f64, - /// Funding contribution to total return - pub funding_contribution_percentage: f64, -} - -/// Enhanced backtesting engine with Hyperliquid-specific features -#[derive(Clone)] -pub struct HyperliquidBacktest { - /// Underlying rs-backtester Backtest instance - pub base_backtest: Option, - /// Original Hyperliquid data with funding information - pub data: HyperliquidData, - /// Strategy name for identification - pub strategy_name: String, - /// Initial capital for the backtest - pub initial_capital: f64, - /// Commission configuration - pub commission_config: HyperliquidCommission, - /// Commission tracking - pub commission_tracker: CommissionTracker, - /// Order type strategy for commission calculation - pub order_type_strategy: OrderTypeStrategy, - /// Funding PnL tracking (separate from trading PnL) - pub funding_pnl: Vec, - /// Trading PnL tracking (without funding) - pub trading_pnl: Vec, - /// Total PnL tracking (trading + funding) - pub total_pnl: Vec, - /// Total funding paid (negative values) - pub total_funding_paid: f64, - /// Total funding received (positive values) - pub total_funding_received: f64, - /// Funding payment history - pub funding_payments: Vec, - /// Enhanced metrics - pub enhanced_metrics: EnhancedMetrics, -} - -impl HyperliquidBacktest { - /// Create a new HyperliquidBacktest instance - pub fn new( - data: HyperliquidData, - strategy_name: String, - initial_capital: f64, - commission: HyperliquidCommission, - ) -> Self { - Self { - base_backtest: None, - data, - strategy_name, - initial_capital, - commission_config: commission, - commission_tracker: CommissionTracker::default(), - order_type_strategy: OrderTypeStrategy::Mixed { maker_percentage: 0.5 }, - funding_pnl: Vec::new(), - trading_pnl: Vec::new(), - total_pnl: Vec::new(), - total_funding_paid: 0.0, - total_funding_received: 0.0, - funding_payments: Vec::new(), - enhanced_metrics: EnhancedMetrics::default(), - } - } - - /// Access the base backtest instance - pub fn base_backtest(&self) -> Option<&rs_backtester::backtester::Backtest> { - self.base_backtest.as_ref() - } - - /// Access the base backtest instance mutably - pub fn base_backtest_mut(&mut self) -> Option<&mut rs_backtester::backtester::Backtest> { - self.base_backtest.as_mut() - } - - /// Set the order type strategy for commission calculation - pub fn with_order_type_strategy(mut self, strategy: OrderTypeStrategy) -> Self { - self.order_type_strategy = strategy; - self - } - - /// Initialize the underlying rs-backtester with converted data - pub fn initialize_base_backtest(&mut self) -> Result<()> { - // Validate data before proceeding - self.data.validate_all_data()?; - - // Convert HyperliquidData to rs-backtester Data format - let rs_data = self.data.to_rs_backtester_data(); - - // Create rs-backtester Backtest instance - let rs_commission = self.commission_config.to_rs_backtester_commission(); - - // Create a simple do_nothing strategy as default - let strategy = rs_backtester::strategies::do_nothing(rs_data.clone()); - - let backtest = rs_backtester::backtester::Backtest::new( - rs_data, - strategy, - self.initial_capital, - rs_commission, - ); - - self.base_backtest = Some(backtest); - - // Initialize PnL tracking vectors - self.funding_pnl = vec![0.0; self.data.len()]; - self.trading_pnl = vec![0.0; self.data.len()]; - self.total_pnl = vec![0.0; self.data.len()]; - - Ok(()) - } - - /// Calculate backtest results including funding payments - /// This method applies funding payments to positions based on funding rates and timing - pub fn calculate_with_funding(&mut self) -> Result<()> { - // Ensure we have a base backtest to work with - if self.base_backtest.is_none() { - return Err(HyperliquidBacktestError::validation( - "Base backtest must be initialized before calculating funding" - )); - } - - // Get the data length - let data_len = self.data.len(); - - // Initialize funding and trading PnL vectors if not already done - if self.funding_pnl.len() != data_len { - self.funding_pnl = vec![0.0; data_len]; - } - if self.trading_pnl.len() != data_len { - self.trading_pnl = vec![0.0; data_len]; - } - - // Reset funding totals - self.total_funding_paid = 0.0; - self.total_funding_received = 0.0; - self.funding_payments.clear(); - - // Simulate position tracking (in a real implementation, this would come from strategy signals) - let current_position = 0.0; - let mut cumulative_funding_pnl = 0.0; - - // Process each data point - for i in 0..data_len { - let timestamp = self.data.datetime[i]; - let price = self.data.close[i]; - - // Check if this is a funding payment time (every 8 hours) - if self.is_funding_time(timestamp) { - // Get funding rate for this timestamp - if let Some(funding_rate) = self.get_funding_rate_for_timestamp(timestamp) { - // Calculate funding payment - let funding_payment = self.calculate_funding_payment( - current_position, - funding_rate, - price, - ); - - // Apply funding payment - cumulative_funding_pnl += funding_payment; - - // Track funding payment - if funding_payment > 0.0 { - self.total_funding_received += funding_payment; - } else { - self.total_funding_paid += funding_payment.abs(); - } - - // Record funding payment - self.funding_payments.push(FundingPayment { - timestamp, - position_size: current_position, - funding_rate, - payment_amount: funding_payment, - mark_price: price, - }); - } - } - - // Store cumulative funding PnL - self.funding_pnl[i] = cumulative_funding_pnl; - - // Calculate trading PnL (total PnL minus funding PnL) - // This would normally come from the base backtest results - self.trading_pnl[i] = 0.0; // Placeholder - would be calculated from actual trades - } - - // Update enhanced metrics - self.update_enhanced_metrics()?; - - Ok(()) - } - - /// Calculate funding payments with position tracking - /// This version allows external position tracking for more accurate funding calculations - pub fn calculate_with_funding_and_positions(&mut self, positions: &[f64]) -> Result<()> { - // Ensure we have a base backtest to work with - if self.base_backtest.is_none() { - return Err(HyperliquidBacktestError::validation( - "Base backtest must be initialized before calculating funding" - )); - } - - let data_len = self.data.len(); - - // Validate positions array length - if positions.len() != data_len { - return Err(HyperliquidBacktestError::validation( - "Positions array length must match data length" - )); - } - - // Initialize funding and trading PnL vectors if not already done - if self.funding_pnl.len() != data_len { - self.funding_pnl = vec![0.0; data_len]; - } - if self.trading_pnl.len() != data_len { - self.trading_pnl = vec![0.0; data_len]; - } - - // Reset funding totals - self.total_funding_paid = 0.0; - self.total_funding_received = 0.0; - self.funding_payments.clear(); - - let mut cumulative_funding_pnl = 0.0; - - // Process each data point - for i in 0..data_len { - let timestamp = self.data.datetime[i]; - let price = self.data.close[i]; - let position_size = positions[i]; - - // Check if this is a funding payment time (every 8 hours) - if self.is_funding_time(timestamp) { - // Get funding rate for this timestamp - if let Some(funding_rate) = self.get_funding_rate_for_timestamp(timestamp) { - // Calculate funding payment - let funding_payment = self.calculate_funding_payment( - position_size, - funding_rate, - price, - ); - - // Apply funding payment - cumulative_funding_pnl += funding_payment; - - // Track funding payment - if funding_payment > 0.0 { - self.total_funding_received += funding_payment; - } else { - self.total_funding_paid += funding_payment.abs(); - } - - // Record funding payment - self.funding_payments.push(FundingPayment { - timestamp, - position_size, - funding_rate, - payment_amount: funding_payment, - mark_price: price, - }); - } - } - - // Store cumulative funding PnL - self.funding_pnl[i] = cumulative_funding_pnl; - } - - // Update enhanced metrics - self.update_enhanced_metrics()?; - - Ok(()) - } - - /// Check if a given timestamp is a funding payment time (every 8 hours) - /// Hyperliquid funding payments occur at 00:00, 08:00, and 16:00 UTC - pub fn is_funding_time(&self, timestamp: DateTime) -> bool { - let hour = timestamp.hour(); - hour % 8 == 0 && timestamp.minute() == 0 && timestamp.second() == 0 - } - - /// Get funding rate for a specific timestamp from the data - pub fn get_funding_rate_for_timestamp(&self, timestamp: DateTime) -> Option { - self.data.get_funding_rate_at(timestamp) - } - - /// Calculate funding payment based on position size, funding rate, and mark price - /// Formula: funding_payment = position_size * funding_rate * mark_price - /// Positive payment means funding received, negative means funding paid - pub fn calculate_funding_payment(&self, position_size: f64, funding_rate: f64, mark_price: f64) -> f64 { - // If no position, no funding payment - if position_size == 0.0 { - return 0.0; - } - - // Calculate funding payment - // For long positions: pay funding when rate is positive, receive when negative - // For short positions: receive funding when rate is positive, pay when negative - let funding_payment = -position_size * funding_rate * mark_price; - - funding_payment - } - - /// Update enhanced metrics based on current funding and trading data - fn update_enhanced_metrics(&mut self) -> Result<()> { - if self.funding_pnl.is_empty() { - return Ok(()); - } - - // Calculate funding-only return - let final_funding_pnl = self.funding_pnl.last().unwrap_or(&0.0); - self.enhanced_metrics.funding_only_return = final_funding_pnl / self.initial_capital; - - // Calculate trading-only return - let final_trading_pnl = self.trading_pnl.last().unwrap_or(&0.0); - self.enhanced_metrics.trading_only_return = final_trading_pnl / self.initial_capital; - - // Calculate total return with funding - self.enhanced_metrics.total_return_with_funding = - self.enhanced_metrics.trading_only_return + self.enhanced_metrics.funding_only_return; - - // Count funding payments - self.enhanced_metrics.funding_payments_received = - self.funding_payments.iter().filter(|p| p.payment_amount > 0.0).count(); - self.enhanced_metrics.funding_payments_paid = - self.funding_payments.iter().filter(|p| p.payment_amount < 0.0).count(); - - // Calculate average funding rate - if !self.funding_payments.is_empty() { - let total_funding_rate: f64 = self.funding_payments.iter().map(|p| p.funding_rate).sum(); - self.enhanced_metrics.average_funding_rate = total_funding_rate / self.funding_payments.len() as f64; - - // Calculate funding rate volatility (standard deviation) - let mean_rate = self.enhanced_metrics.average_funding_rate; - let variance: f64 = self.funding_payments.iter() - .map(|p| (p.funding_rate - mean_rate).powi(2)) - .sum::() / self.funding_payments.len() as f64; - self.enhanced_metrics.funding_rate_volatility = variance.sqrt(); - } - - // Calculate maximum drawdown with funding - let mut peak = self.initial_capital; - let mut max_drawdown = 0.0; - - for i in 0..self.funding_pnl.len() { - let total_value = self.initial_capital + self.trading_pnl[i] + self.funding_pnl[i]; - if total_value > peak { - peak = total_value; - } - let drawdown = (peak - total_value) / peak; - if drawdown > max_drawdown { - max_drawdown = drawdown; - } - } - self.enhanced_metrics.max_drawdown_with_funding = -max_drawdown; - - // Calculate Sharpe ratio with funding (simplified version) - if self.funding_pnl.len() > 1 { - let returns: Vec = (1..self.funding_pnl.len()) - .map(|i| { - let prev_total = self.initial_capital + self.trading_pnl[i-1] + self.funding_pnl[i-1]; - let curr_total = self.initial_capital + self.trading_pnl[i] + self.funding_pnl[i]; - if prev_total > 0.0 { - (curr_total - prev_total) / prev_total - } else { - 0.0 - } - }) - .collect(); - - if !returns.is_empty() { - let mean_return = returns.iter().sum::() / returns.len() as f64; - let variance = returns.iter() - .map(|r| (r - mean_return).powi(2)) - .sum::() / returns.len() as f64; - let std_dev = variance.sqrt(); - - if std_dev > 0.0 { - self.enhanced_metrics.sharpe_ratio_with_funding = mean_return / std_dev; - } - } - } - - Ok(()) - } - - // Getter methods - pub fn data(&self) -> &HyperliquidData { &self.data } - pub fn strategy_name(&self) -> &str { &self.strategy_name } - pub fn initial_capital(&self) -> f64 { self.initial_capital } - pub fn commission_config(&self) -> &HyperliquidCommission { &self.commission_config } - pub fn funding_pnl(&self) -> &[f64] { &self.funding_pnl } - pub fn trading_pnl(&self) -> &[f64] { &self.trading_pnl } - pub fn total_funding_paid(&self) -> f64 { self.total_funding_paid } - pub fn total_funding_received(&self) -> f64 { self.total_funding_received } - pub fn funding_payments(&self) -> &[FundingPayment] { &self.funding_payments } - pub fn enhanced_metrics(&self) -> &EnhancedMetrics { &self.enhanced_metrics } - pub fn is_initialized(&self) -> bool { self.base_backtest.is_some() } - - pub fn validate(&self) -> Result<()> { - self.commission_config.validate()?; - self.data.validate_all_data()?; - if self.initial_capital <= 0.0 { - return Err(HyperliquidBacktestError::validation("Initial capital must be positive")); - } - if self.strategy_name.is_empty() { - return Err(HyperliquidBacktestError::validation("Strategy name cannot be empty")); - } - Ok(()) - } - - /// Get commission statistics - pub fn commission_stats(&self) -> CommissionStats { - CommissionStats { - total_commission: self.commission_tracker.total_commission(), - maker_fees: self.commission_tracker.total_maker_fees, - taker_fees: self.commission_tracker.total_taker_fees, - maker_orders: self.commission_tracker.maker_order_count, - taker_orders: self.commission_tracker.taker_order_count, - average_rate: self.commission_tracker.average_commission_rate(), - maker_taker_ratio: self.commission_tracker.maker_taker_ratio(), - } - } - - /// Calculate trade commission based on order type strategy - pub fn calculate_trade_commission( - &self, - trade_value: f64, - trade_index: usize, - scenario: TradingScenario, - ) -> (OrderType, f64) { - let order_type = self.order_type_strategy.get_order_type(trade_index); - let commission = self.commission_config.calculate_scenario_fee(scenario, order_type, trade_value); - (order_type, commission) - } - - /// Track commission for reporting - pub fn track_commission( - &mut self, - timestamp: DateTime, - order_type: OrderType, - trade_value: f64, - commission_paid: f64, - scenario: TradingScenario, - ) { - self.commission_tracker.add_commission( - timestamp, - order_type, - trade_value, - commission_paid, - scenario, - ); - } - - /// Generate a funding summary for reporting - pub fn funding_summary(&self) -> FundingSummary { - let net_funding = self.total_funding_received - self.total_funding_paid; - let funding_payment_count = self.funding_payments.len(); - - let average_funding_payment = if funding_payment_count > 0 { - let total_payments: f64 = self.funding_payments.iter() - .map(|p| p.payment_amount) - .sum(); - total_payments / funding_payment_count as f64 - } else { - 0.0 - }; - - let funding_contribution_percentage = if self.enhanced_metrics.total_return_with_funding != 0.0 { - (self.enhanced_metrics.funding_only_return / self.enhanced_metrics.total_return_with_funding) * 100.0 - } else { - 0.0 - }; - - FundingSummary { - total_funding_paid: self.total_funding_paid, - total_funding_received: self.total_funding_received, - net_funding, - funding_payment_count, - average_funding_payment, - average_funding_rate: self.enhanced_metrics.average_funding_rate, - funding_rate_volatility: self.enhanced_metrics.funding_rate_volatility, - funding_contribution_percentage, - } - } - - /// Generate an enhanced report with Hyperliquid-specific metrics - pub fn enhanced_report(&self) -> Result { - // Ensure we have a base backtest - let base_backtest = match &self.base_backtest { - Some(backtest) => backtest, - None => return Err(HyperliquidBacktestError::validation( - "Base backtest must be initialized before generating a report" - )), - }; - - // Calculate basic metrics from base backtest - let final_equity = if let Some(last_position) = base_backtest.position().last() { - if let Some(last_close) = self.data.close.last() { - if let Some(last_account) = base_backtest.account().last() { - last_position * last_close + last_account - } else { - self.initial_capital - } - } else { - self.initial_capital - } - } else { - self.initial_capital - }; - - let total_return = (final_equity - self.initial_capital) / self.initial_capital; - - // Calculate trade statistics - let mut trade_count = 0; - let mut win_count = 0; - let mut profit_sum = 0.0; - let mut loss_sum = 0.0; - - // Count trades and calculate win rate - let orders = base_backtest.orders(); - if orders.len() > 1 { - for i in 1..orders.len() { - if orders[i] != orders[i-1] && orders[i] != rs_backtester::orders::Order::NULL { - trade_count += 1; - - // Simple profit calculation (this is a simplification) - if i < self.data.close.len() - 1 { - let entry_price = self.data.close[i]; - let exit_price = self.data.close[i+1]; - let profit = match orders[i] { - rs_backtester::orders::Order::BUY => exit_price - entry_price, - rs_backtester::orders::Order::SHORTSELL => entry_price - exit_price, - _ => 0.0, - }; - - if profit > 0.0 { - win_count += 1; - profit_sum += profit; - } else { - loss_sum += profit.abs(); - } - } - } - } - } - - let win_rate = if trade_count > 0 { - win_count as f64 / trade_count as f64 - } else { - 0.0 - }; - - let profit_factor = if loss_sum > 0.0 { - profit_sum / loss_sum - } else if profit_sum > 0.0 { - f64::INFINITY - } else { - 0.0 - }; - - // Calculate Sharpe ratio (simplified) - let sharpe_ratio = self.enhanced_metrics.sharpe_ratio_with_funding; - - // Calculate max drawdown - let max_drawdown = self.enhanced_metrics.max_drawdown_with_funding; - - // Create enhanced report - let report = EnhancedReport { - strategy_name: self.strategy_name.clone(), - ticker: self.data.symbol.clone(), - initial_capital: self.initial_capital, - final_equity, - total_return, - trade_count, - win_rate, - profit_factor, - sharpe_ratio, - max_drawdown, - enhanced_metrics: self.enhanced_metrics.clone(), - commission_stats: self.commission_stats(), - funding_summary: self.funding_summary(), - }; - - Ok(report) - } - - /// Print enhanced report to console - pub fn print_enhanced_report(&self) -> Result<()> { - let report = self.enhanced_report()?; - - println!("\n=== HYPERLIQUID BACKTEST REPORT ==="); - println!("Strategy: {}", report.strategy_name); - println!("Symbol: {}", report.ticker); - println!("Period: {} to {}", - self.data.datetime.first().unwrap_or(&DateTime::parse_from_rfc3339("1970-01-01T00:00:00Z").unwrap()), - self.data.datetime.last().unwrap_or(&DateTime::parse_from_rfc3339("1970-01-01T00:00:00Z").unwrap()) - ); - println!("Initial Capital: ${:.2}", report.initial_capital); - println!("Final Equity: ${:.2}", report.final_equity); - - // Print base report metrics - println!("\n--- Base Performance Metrics ---"); - println!("Total Return: {:.2}%", report.total_return * 100.0); - println!("Sharpe Ratio: {:.2}", report.sharpe_ratio); - println!("Max Drawdown: {:.2}%", report.max_drawdown * 100.0); - println!("Win Rate: {:.2}%", report.win_rate * 100.0); - println!("Profit Factor: {:.2}", report.profit_factor); - println!("Trade Count: {}", report.trade_count); - - // Print enhanced metrics - println!("\n--- Enhanced Performance Metrics (with Funding) ---"); - println!("Total Return (with Funding): {:.2}%", report.enhanced_metrics.total_return_with_funding * 100.0); - println!("Trading-Only Return: {:.2}%", report.enhanced_metrics.trading_only_return * 100.0); - println!("Funding-Only Return: {:.2}%", report.enhanced_metrics.funding_only_return * 100.0); - println!("Sharpe Ratio (with Funding): {:.2}", report.enhanced_metrics.sharpe_ratio_with_funding); - println!("Max Drawdown (with Funding): {:.2}%", report.enhanced_metrics.max_drawdown_with_funding * 100.0); - - // Print commission statistics - println!("\n--- Commission Statistics ---"); - println!("Total Commission: ${:.2}", report.commission_stats.total_commission); - println!("Maker Fees: ${:.2} ({} orders)", - report.commission_stats.maker_fees, - report.commission_stats.maker_orders - ); - println!("Taker Fees: ${:.2} ({} orders)", - report.commission_stats.taker_fees, - report.commission_stats.taker_orders - ); - println!("Average Commission Rate: {:.4}%", report.commission_stats.average_rate * 100.0); - println!("Maker/Taker Ratio: {:.2}", report.commission_stats.maker_taker_ratio); - - // Print funding summary - println!("\n--- Funding Summary ---"); - println!("Total Funding Paid: ${:.2}", report.funding_summary.total_funding_paid); - println!("Total Funding Received: ${:.2}", report.funding_summary.total_funding_received); - println!("Net Funding: ${:.2}", report.funding_summary.net_funding); - println!("Funding Payments: {}", report.funding_summary.funding_payment_count); - println!("Average Funding Payment: ${:.2}", report.funding_summary.average_funding_payment); - println!("Average Funding Rate: {:.6}%", report.funding_summary.average_funding_rate * 100.0); - println!("Funding Rate Volatility: {:.6}%", report.funding_summary.funding_rate_volatility * 100.0); - println!("Funding Contribution: {:.2}% of total return", report.funding_summary.funding_contribution_percentage); - - println!("\n=== END OF REPORT ===\n"); - - Ok(()) - } - - /// Export backtest results to CSV - pub fn export_to_csv>(&self, path: P) -> Result<()> { - // Ensure we have a base backtest - if self.base_backtest.is_none() { - return Err(HyperliquidBacktestError::validation( - "Base backtest must be initialized before exporting to CSV" - )); - } - - // Create CSV writer - let file = File::create(path)?; - let mut wtr = csv::Writer::from_writer(file); - - // Write header - wtr.write_record(&[ - "Timestamp", - "Open", - "High", - "Low", - "Close", - "Volume", - "Funding Rate", - "Position", - "Trading PnL", - "Funding PnL", - "Total PnL", - "Equity", - ])?; - - // Write data rows - for i in 0..self.data.len() { - let timestamp = self.data.datetime[i].to_rfc3339(); - let open = self.data.open[i].to_string(); - let high = self.data.high[i].to_string(); - let low = self.data.low[i].to_string(); - let close = self.data.close[i].to_string(); - let volume = self.data.volume[i].to_string(); - - // Get funding rate (if available) - let funding_rate = match self.get_funding_rate_for_timestamp(self.data.datetime[i]) { - Some(rate) => rate.to_string(), - None => "".to_string(), - }; - - // Get position (placeholder - would come from base backtest) - let position = "0.0".to_string(); // Placeholder - - // Get PnL values - let trading_pnl = if i < self.trading_pnl.len() { - self.trading_pnl[i].to_string() - } else { - "0.0".to_string() - }; - - let funding_pnl = if i < self.funding_pnl.len() { - self.funding_pnl[i].to_string() - } else { - "0.0".to_string() - }; - - // Calculate total PnL and equity - let total_pnl = ( - self.trading_pnl.get(i).unwrap_or(&0.0) + - self.funding_pnl.get(i).unwrap_or(&0.0) - ).to_string(); - - let equity = ( - self.initial_capital + - self.trading_pnl.get(i).unwrap_or(&0.0) + - self.funding_pnl.get(i).unwrap_or(&0.0) - ).to_string(); - - // Write row - wtr.write_record(&[ - ×tamp, - &open, - &high, - &low, - &close, - &volume, - &funding_rate, - &position, - &trading_pnl, - &funding_pnl, - &total_pnl, - &equity, - ])?; - } - - // Flush writer - wtr.flush()?; - - Ok(()) - } - - /// Export funding payments to CSV - pub fn export_funding_to_csv>(&self, path: P) -> Result<()> { - // Create CSV writer - let file = File::create(path)?; - let mut wtr = csv::Writer::from_writer(file); - - // Write header - wtr.write_record(&[ - "Timestamp", - "Position Size", - "Funding Rate", - "Mark Price", - "Payment Amount", - ])?; - - // Write funding payment rows - for payment in &self.funding_payments { - wtr.write_record(&[ - &payment.timestamp.to_rfc3339(), - &payment.position_size.to_string(), - &payment.funding_rate.to_string(), - &payment.mark_price.to_string(), - &payment.payment_amount.to_string(), - ])?; - } - - // Flush writer - wtr.flush()?; - - Ok(()) - } - - /// Generate a detailed funding report - pub fn funding_report(&self) -> Result { - use crate::funding_report::FundingReport; - - // Ensure we have a base backtest - if self.base_backtest.is_none() { - return Err(HyperliquidBacktestError::validation( - "Base backtest must be initialized before generating a funding report" - )); - } - - // Get position sizes and values - let mut position_sizes = Vec::with_capacity(self.data.len()); - let mut position_values = Vec::with_capacity(self.data.len()); - - // Get positions from base backtest if available, otherwise use zeros - if let Some(base_backtest) = &self.base_backtest { - let positions = base_backtest.position(); - - for i in 0..self.data.len() { - let position_size = if i < positions.len() { - positions[i] - } else { - 0.0 - }; - - position_sizes.push(position_size); - position_values.push(position_size * self.data.close[i]); - } - } else { - // Fill with zeros if no base backtest - position_sizes = vec![0.0; self.data.len()]; - position_values = vec![0.0; self.data.len()]; - } - - // Calculate total trading PnL - let trading_pnl = if let Some(last) = self.trading_pnl.last() { - *last - } else { - 0.0 - }; - - // Calculate total funding PnL - let funding_pnl = if let Some(last) = self.funding_pnl.last() { - *last - } else { - 0.0 - }; - - // Create funding report - FundingReport::new( - &self.data.symbol, - &self.data, - &position_values, - self.funding_payments.clone(), - funding_pnl, - ) - } - - /// Export enhanced report to CSV - pub fn export_report_to_csv>(&self, path: P) -> Result<()> { - let report = self.enhanced_report()?; - - // Create CSV writer - let file = File::create(path)?; - let mut wtr = csv::Writer::from_writer(file); - - // Write header and data as key-value pairs - wtr.write_record(&["Metric", "Value"])?; - - // Strategy information - wtr.write_record(&["Strategy", &report.strategy_name])?; - wtr.write_record(&["Symbol", &report.ticker])?; - wtr.write_record(&["Initial Capital", &report.initial_capital.to_string()])?; - wtr.write_record(&["Final Equity", &report.final_equity.to_string()])?; - - // Base metrics - wtr.write_record(&["Total Return", &(report.total_return * 100.0).to_string()])?; - wtr.write_record(&["Sharpe Ratio", &report.sharpe_ratio.to_string()])?; - wtr.write_record(&["Max Drawdown", &(report.max_drawdown * 100.0).to_string()])?; - wtr.write_record(&["Win Rate", &(report.win_rate * 100.0).to_string()])?; - wtr.write_record(&["Profit Factor", &report.profit_factor.to_string()])?; - wtr.write_record(&["Trade Count", &report.trade_count.to_string()])?; - - // Enhanced metrics - wtr.write_record(&["Total Return (with Funding)", &(report.enhanced_metrics.total_return_with_funding * 100.0).to_string()])?; - wtr.write_record(&["Trading-Only Return", &(report.enhanced_metrics.trading_only_return * 100.0).to_string()])?; - wtr.write_record(&["Funding-Only Return", &(report.enhanced_metrics.funding_only_return * 100.0).to_string()])?; - wtr.write_record(&["Sharpe Ratio (with Funding)", &report.enhanced_metrics.sharpe_ratio_with_funding.to_string()])?; - wtr.write_record(&["Max Drawdown (with Funding)", &(report.enhanced_metrics.max_drawdown_with_funding * 100.0).to_string()])?; - - // Commission statistics - wtr.write_record(&["Total Commission", &report.commission_stats.total_commission.to_string()])?; - wtr.write_record(&["Maker Fees", &report.commission_stats.maker_fees.to_string()])?; - wtr.write_record(&["Taker Fees", &report.commission_stats.taker_fees.to_string()])?; - wtr.write_record(&["Maker Orders", &report.commission_stats.maker_orders.to_string()])?; - wtr.write_record(&["Taker Orders", &report.commission_stats.taker_orders.to_string()])?; - wtr.write_record(&["Average Commission Rate", &(report.commission_stats.average_rate * 100.0).to_string()])?; - wtr.write_record(&["Maker/Taker Ratio", &report.commission_stats.maker_taker_ratio.to_string()])?; - - // Funding summary - wtr.write_record(&["Total Funding Paid", &report.funding_summary.total_funding_paid.to_string()])?; - wtr.write_record(&["Total Funding Received", &report.funding_summary.total_funding_received.to_string()])?; - wtr.write_record(&["Net Funding", &report.funding_summary.net_funding.to_string()])?; - wtr.write_record(&["Funding Payments", &report.funding_summary.funding_payment_count.to_string()])?; - wtr.write_record(&["Average Funding Payment", &report.funding_summary.average_funding_payment.to_string()])?; - wtr.write_record(&["Average Funding Rate", &(report.funding_summary.average_funding_rate * 100.0).to_string()])?; - wtr.write_record(&["Funding Rate Volatility", &(report.funding_summary.funding_rate_volatility * 100.0).to_string()])?; - wtr.write_record(&["Funding Contribution", &report.funding_summary.funding_contribution_percentage.to_string()])?; - - // Flush writer - wtr.flush()?; - - Ok(()) - } -} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index d208507..6327518 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,527 +1,25 @@ -//! # Hyperliquid Backtester +//! Minimal Hyperliquid backtesting toolkit. //! -//! A comprehensive Rust library that integrates Hyperliquid trading data with the rs-backtester -//! framework to enable sophisticated backtesting of trading strategies using real Hyperliquid -//! market data, including perpetual futures mechanics and funding rate calculations. -//! -//! ## Features -//! -//! - 🚀 **Async Data Fetching**: Efficiently fetch historical OHLC data from Hyperliquid API -//! - 💰 **Funding Rate Support**: Complete funding rate data and perpetual futures mechanics -//! - 🔄 **Seamless Integration**: Drop-in replacement for rs-backtester with enhanced features -//! - 📊 **Enhanced Reporting**: Comprehensive metrics including funding PnL and arbitrage analysis -//! - ⚡ **High Performance**: Optimized for large datasets and complex multi-asset strategies -//! - 🛡️ **Type Safety**: Comprehensive error handling with detailed error messages -//! - 📈 **Advanced Strategies**: Built-in funding arbitrage and enhanced technical indicators -//! -//! ## API Stability -//! -//! This crate follows semantic versioning (SemVer): -//! - **Major version** (0.x.y → 1.0.0): Breaking API changes -//! - **Minor version** (0.1.x → 0.2.0): New features, backward compatible -//! - **Patch version** (0.1.0 → 0.1.1): Bug fixes, backward compatible -//! -//! Current version: **0.1.0** (Pre-1.0 development phase) -//! -//! ### Stability Guarantees -//! -//! - **Public API**: All items in the [`prelude`] module are considered stable within minor versions -//! - **Data Structures**: [`HyperliquidData`], [`HyperliquidBacktest`], and [`HyperliquidCommission`] are stable -//! - **Error Types**: [`HyperliquidBacktestError`] variants may be added but not removed in minor versions -//! - **Strategy Interface**: [`HyperliquidStrategy`] trait is stable for implementors -//! -//! ## Quick Start -//! -//! Add this to your `Cargo.toml`: -//! -//! ```toml -//! [dependencies] -//! hyperliquid-backtester = "0.1" -//! tokio = { version = "1.0", features = ["full"] } -//! ``` -//! -//! ### Basic Backtesting Example -//! -//! ```rust,no_run -//! use hyperliquid_backtest::prelude::*; -//! use chrono::{DateTime, FixedOffset, Utc}; -//! -//! #[tokio::main] -//! async fn main() -> Result<(), HyperliquidBacktestError> { -//! // Define time range (last 30 days) -//! let end_time = Utc::now().timestamp() as u64; -//! let start_time = end_time - (30 * 24 * 60 * 60); // 30 days ago -//! -//! // Fetch historical data for BTC with 1-hour intervals -//! let data = HyperliquidData::fetch("BTC", "1h", start_time, end_time).await?; -//! -//! // Create a simple moving average crossover strategy -//! let strategy = enhanced_sma_cross(10, 20, Default::default())?; -//! -//! // Set up backtest with $10,000 initial capital -//! let mut backtest = HyperliquidBacktest::new( -//! data, -//! strategy, -//! 10000.0, -//! HyperliquidCommission::default(), -//! )?; -//! -//! // Run backtest including funding calculations -//! backtest.calculate_with_funding()?; -//! -//! // Generate comprehensive report -//! let report = backtest.enhanced_report()?; -//! -//! println!("📊 Backtest Results:"); -//! println!("Total Return: {:.2}%", report.total_return * 100.0); -//! println!("Trading PnL: ${:.2}", report.trading_pnl); -//! println!("Funding PnL: ${:.2}", report.funding_pnl); -//! println!("Sharpe Ratio: {:.3}", report.sharpe_ratio); -//! -//! Ok(()) -//! } -//! ``` -//! -//! ### Funding Arbitrage Strategy Example -//! -//! ```rust,no_run -//! use hyperliquid_backtest::prelude::*; -//! -//! #[tokio::main] -//! async fn main() -> Result<(), HyperliquidBacktestError> { -//! let data = HyperliquidData::fetch("ETH", "1h", start_time, end_time).await?; -//! -//! // Create funding arbitrage strategy with 0.01% threshold -//! let strategy = funding_arbitrage_strategy(0.0001)?; -//! -//! let mut backtest = HyperliquidBacktest::new( -//! data, -//! strategy, -//! 50000.0, // Higher capital for arbitrage -//! HyperliquidCommission::default(), -//! )?; -//! -//! backtest.calculate_with_funding()?; -//! -//! // Get detailed funding analysis -//! let funding_report = backtest.funding_report()?; -//! -//! println!("💰 Funding Arbitrage Results:"); -//! println!("Total Funding Received: ${:.2}", funding_report.total_funding_received); -//! println!("Total Funding Paid: ${:.2}", funding_report.total_funding_paid); -//! println!("Net Funding PnL: ${:.2}", funding_report.net_funding_pnl); -//! println!("Average Funding Rate: {:.4}%", funding_report.avg_funding_rate * 100.0); -//! -//! Ok(()) -//! } -//! ``` -//! -//! ## Migration from rs-backtester -//! -//! This library is designed as a drop-in enhancement to rs-backtester. See the -//! [migration guide](https://docs.rs/hyperliquid-backtester/latest/hyperliquid_backtest/migration/index.html) -//! for detailed instructions on upgrading existing rs-backtester code. -//! -//! ## Error Handling -//! -//! All fallible operations return [`Result`](Result). The error type -//! provides detailed context and suggestions for resolution: -//! -//! ```rust,no_run -//! use hyperliquid_backtest::prelude::*; -//! -//! match HyperliquidData::fetch("INVALID", "1h", start, end).await { -//! Ok(data) => println!("Success!"), -//! Err(HyperliquidBacktestError::HyperliquidApi(msg)) => { -//! eprintln!("API Error: {}", msg); -//! // Handle API-specific errors -//! }, -//! Err(HyperliquidBacktestError::UnsupportedInterval(interval)) => { -//! eprintln!("Unsupported interval: {}", interval); -//! eprintln!("Supported intervals: {:?}", HyperliquidDataFetcher::supported_intervals()); -//! }, -//! Err(e) => eprintln!("Other error: {}", e), -//! } -//! ``` +//! This crate provides just enough building blocks to run lightweight experiments +//! in unit tests: a [`Position`] type, simple order requests, a [`FundingPayment`] +//! structure and a very small [`RiskManager`]. The implementation intentionally +//! avoids external dependencies or complex behaviours so the library can compile +//! quickly and remain easy to understand. -pub mod data; pub mod backtest; -pub mod strategies; -pub mod errors; -pub mod utils; -pub mod indicators; -pub mod funding_report; -pub mod csv_export; -pub mod migration; -pub mod api_docs; -pub mod trading_mode; -pub mod trading_mode_impl; -pub mod unified_data; -pub mod unified_data_impl; -pub mod paper_trading; -pub mod real_time_data_stream; -pub mod real_time_monitoring; pub mod risk_manager; -pub mod live_trading; -pub mod live_trading_safety; -pub mod mode_reporting; - -/// Logging and debugging utilities -pub mod logging { - //! Logging and debugging utilities for the hyperliquid-backtester crate. - //! - //! This module provides convenient functions for setting up structured logging - //! and debugging support throughout the library. - //! - //! ## Basic Usage - //! - //! ```rust - //! use hyperliquid_backtest::logging::init_logger; - //! - //! // Initialize with default settings (INFO level) - //! init_logger(); - //! - //! // Or with custom log level - //! init_logger_with_level("debug"); - //! ``` - //! - //! ## Environment Variables - //! - //! You can control logging behavior using environment variables: - //! - //! - `RUST_LOG`: Set log level (e.g., `debug`, `info`, `warn`, `error`) - //! - `HYPERLIQUID_LOG_FORMAT`: Set format (`json` or `pretty`) - //! - `HYPERLIQUID_LOG_FILE`: Write logs to file instead of stdout - //! - //! ## Examples - //! - //! ```bash - //! # Enable debug logging - //! RUST_LOG=debug cargo run --example basic_backtest - //! - //! # Use JSON format for structured logging - //! RUST_LOG=info HYPERLIQUID_LOG_FORMAT=json cargo run - //! - //! # Write logs to file - //! RUST_LOG=info HYPERLIQUID_LOG_FILE=backtest.log cargo run - //! ``` - - use std::env; - use tracing_subscriber::{ - fmt::{self, format::FmtSpan}, - layer::SubscriberExt, - util::SubscriberInitExt, - EnvFilter, - }; - - /// Initialize the default logger with INFO level logging. - /// - /// This sets up structured logging with reasonable defaults for most use cases. - /// The logger will respect the `RUST_LOG` environment variable if set. - /// - /// # Examples - /// - /// ```rust - /// use hyperliquid_backtest::logging::init_logger; - /// - /// init_logger(); - /// log::info!("Logger initialized successfully"); - /// ``` - pub fn init_logger() { - init_logger_with_level("info"); - } - - /// Initialize the logger with a specific log level. - /// - /// # Arguments - /// - /// * `level` - The log level to use (e.g., "debug", "info", "warn", "error") - /// - /// # Examples - /// - /// ```rust - /// use hyperliquid_backtest::logging::init_logger_with_level; - /// - /// init_logger_with_level("debug"); - /// log::debug!("Debug logging enabled"); - /// ``` - pub fn init_logger_with_level(level: &str) { - let env_filter = EnvFilter::try_from_default_env() - .unwrap_or_else(|_| EnvFilter::new(format!("hyperliquid_backtest={}", level))); - - let format = env::var("HYPERLIQUID_LOG_FORMAT").unwrap_or_else(|_| "pretty".to_string()); - let log_file = env::var("HYPERLIQUID_LOG_FILE").ok(); - - let subscriber = tracing_subscriber::registry().with(env_filter); - - match (format.as_str(), log_file) { - ("json", Some(file_path)) => { - let file = std::fs::OpenOptions::new() - .create(true) - .append(true) - .open(file_path) - .expect("Failed to open log file"); - - subscriber - .with( - fmt::layer() - .json() - .with_writer(file) - .with_span_events(FmtSpan::CLOSE) - ) - .init(); - } - ("json", None) => { - subscriber - .with( - fmt::layer() - .json() - .with_span_events(FmtSpan::CLOSE) - ) - .init(); - } - (_, Some(file_path)) => { - let file = std::fs::OpenOptions::new() - .create(true) - .append(true) - .open(file_path) - .expect("Failed to open log file"); - - subscriber - .with( - fmt::layer() - .pretty() - .with_writer(file) - .with_span_events(FmtSpan::CLOSE) - ) - .init(); - } - _ => { - subscriber - .with( - fmt::layer() - .pretty() - .with_span_events(FmtSpan::CLOSE) - ) - .init(); - } - } - } - - /// Initialize logger for testing with reduced verbosity. - /// - /// This is useful for tests where you want to capture logs but don't want - /// them to interfere with test output. - pub fn init_test_logger() { - let _ = tracing_subscriber::fmt() - .with_test_writer() - .with_env_filter("hyperliquid_backtest=warn") - .try_init(); - } - - /// Create a tracing span for performance monitoring. - /// - /// This is useful for tracking the performance of specific operations - /// like data fetching or backtest calculations. - /// - /// # Arguments - /// - /// * `name` - The name of the operation being tracked - /// * `details` - Additional details to include in the span - /// - /// # Examples - /// - /// ```rust - /// use hyperliquid_backtest::logging::performance_span; - /// use tracing::Instrument; - /// - /// async fn fetch_data() -> Result<(), Box> { - /// let span = performance_span("data_fetch", &[("symbol", "BTC"), ("interval", "1h")]); - /// - /// async { - /// // Your data fetching logic here - /// Ok(()) - /// } - /// .instrument(span) - /// .await - /// } - /// ``` - pub fn performance_span(name: &str, details: &[(&str, &str)]) -> tracing::Span { - let span = tracing::info_span!("performance", operation = name); - - // Record additional fields if needed - for (key, value) in details { - span.record(*key, *value); - } - - span - } -} +pub mod unified_data; #[cfg(test)] mod tests { - pub mod backtest_tests; - pub mod strategies_tests; - pub mod indicators_tests; - pub mod indicators_tests_extended; - pub mod funding_report_tests; - pub mod csv_export_tests; - pub mod csv_export_tests_enhanced; - pub mod data_tests; - pub mod data_price_tests; - pub mod data_conversion_tests; - pub mod errors_tests; - pub mod commission_tests; - pub mod funding_payment_tests; - pub mod mock_data; - pub mod integration_tests; - pub mod performance_tests; - pub mod regression_tests; - pub mod trading_mode_tests; - pub mod unified_data_tests; - pub mod unified_data_impl_tests; - pub mod standalone_unified_data_tests; - pub mod risk_manager_tests; - pub mod advanced_risk_controls_tests; - pub mod live_trading_tests; - pub mod live_trading_safety_tests; - pub mod real_time_monitoring_tests; - pub mod trading_strategy_tests; - // Mode-specific test suites - pub mod paper_trading_tests; - pub mod live_trading_integration_tests; - pub mod strategy_consistency_tests; - pub mod performance_stress_tests; - pub mod safety_validation_tests; - pub mod workflow_tests; - pub mod standalone_position_tests; + mod basic; } -// Re-export commonly used types -pub use data::{HyperliquidData, HyperliquidDataFetcher, FundingStatistics}; -pub use backtest::{ - HyperliquidBacktest, HyperliquidCommission, OrderType, TradingScenario, - CommissionTracker, CommissionStats, OrderTypeStrategy -}; -pub use strategies::{ - HyperliquidStrategy, TradingSignal, SignalStrength, FundingAwareConfig, - funding_arbitrage_strategy, enhanced_sma_cross -}; -pub use errors::{HyperliquidBacktestError, Result}; -pub use indicators::{ - FundingPredictionModel, FundingPredictionConfig, FundingPrediction, - FundingDirection, FundingVolatility, FundingMomentum, FundingCycle, - FundingAnomaly, FundingArbitrageOpportunity, FundingPriceCorrelation, - OpenInterestData, OpenInterestChange, LiquidationData, LiquidationImpact, - BasisIndicator, FundingRatePredictor -}; -pub use funding_report::{ - FundingReport, FundingDistribution, FundingRatePoint, - FundingDirectionStats, FundingMetricsByPeriod, FundingPeriodMetric -}; -pub use backtest::FundingPayment; -pub use csv_export::{ - EnhancedCsvExport, EnhancedCsvExportExt, StrategyComparisonData -}; -pub use trading_mode::{ - TradingMode, TradingConfig, RiskConfig, SlippageConfig, ApiConfig, - TradingModeError -}; -pub use trading_mode_impl::{ - TradingModeManager, TradingResult -}; -pub use unified_data::{ - Position, OrderRequest, OrderResult, MarketData, Signal, SignalDirection, - OrderSide, OrderType as TradingOrderType, TimeInForce, OrderStatus, - TradingStrategy, OrderBookLevel, OrderBookSnapshot, Trade -}; -pub use paper_trading::{ - PaperTradingEngine, PaperTradingError, SimulatedOrder, PaperTradingMetrics, - TradeLogEntry, PaperTradingReport -}; -pub use real_time_data_stream::{ - RealTimeDataStream, RealTimeDataError, SubscriptionType, DataSubscription -}; -pub use risk_manager::{ - RiskManager, RiskError, RiskOrder, Result as RiskResult -}; -pub use live_trading::{ - LiveTradingEngine, LiveTradingError, LiveOrder -}; -pub use mode_reporting::{ - ModeReportingManager, CommonPerformanceMetrics, PaperTradingReport as ModeSpecificPaperTradingReport, - LiveTradingReport, RealTimePnLReport, MonitoringDashboardData, FundingImpactAnalysis, - RiskMetrics, ConnectionMetrics, AlertEntry, OrderSummary -}; -pub use real_time_monitoring::{ - MonitoringServer, MonitoringClient, MonitoringManager, MonitoringError, - MonitoringMessage, TradeExecutionUpdate, ConnectionStatusUpdate, ConnectionStatus, - PerformanceMetricsUpdate -}; - -/// Prelude module for convenient imports +/// Convenient re-export of the most common items used when writing examples or tests. pub mod prelude { - pub use crate::data::{HyperliquidData, HyperliquidDataFetcher}; - pub use crate::backtest::{ - HyperliquidBacktest, HyperliquidCommission, OrderType, TradingScenario, - CommissionTracker, CommissionStats, OrderTypeStrategy - }; - pub use crate::strategies::{ - HyperliquidStrategy, funding_arbitrage_strategy, enhanced_sma_cross - }; - pub use crate::errors::{HyperliquidBacktestError, Result}; - pub use crate::indicators::{ - FundingDirection, FundingPrediction, FundingRatePredictor, - OpenInterestData, LiquidationData, BasisIndicator, - calculate_funding_volatility, calculate_funding_momentum, - calculate_funding_arbitrage, calculate_basis_indicator - }; - pub use crate::funding_report::{ - FundingReport, FundingDistribution, FundingRatePoint, - FundingDirectionStats, FundingMetricsByPeriod, FundingPeriodMetric - }; pub use crate::backtest::FundingPayment; - pub use crate::csv_export::{ - EnhancedCsvExport, EnhancedCsvExportExt, StrategyComparisonData - }; - pub use crate::logging::{init_logger, init_logger_with_level, performance_span}; - pub use crate::trading_mode::{ - TradingMode, TradingModeError - }; - pub use crate::trading_mode_impl::{ - TradingModeManager, TradingResult - }; + pub use crate::risk_manager::{RiskConfig, RiskError, RiskManager, RiskOrder}; pub use crate::unified_data::{ - Position, OrderRequest, OrderResult, MarketData, Signal, SignalDirection, - OrderSide, TimeInForce, OrderStatus, TradingStrategy, - OrderBookLevel, OrderBookSnapshot, Trade - }; - pub use crate::trading_mode::{ - TradingConfig, RiskConfig, SlippageConfig, ApiConfig - }; - pub use crate::paper_trading::{ - PaperTradingEngine, PaperTradingError, SimulatedOrder, PaperTradingMetrics, - TradeLogEntry, PaperTradingReport + OrderRequest, OrderResult, OrderSide, OrderType, Position, TimeInForce, }; - pub use crate::real_time_data_stream::{ - RealTimeDataStream, RealTimeDataError, SubscriptionType, DataSubscription - }; - pub use crate::risk_manager::{ - RiskManager, RiskError, RiskOrder, Result as RiskResult - }; - pub use crate::live_trading::{ - LiveTradingEngine, LiveTradingError, LiveOrder, - AlertLevel, AlertMessage, RetryPolicy, SafetyCircuitBreakerConfig - }; - pub use crate::mode_reporting::{ - ModeReportingManager, CommonPerformanceMetrics, PaperTradingReport as ModeSpecificPaperTradingReport, - LiveTradingReport, RealTimePnLReport, MonitoringDashboardData, FundingImpactAnalysis, - RiskMetrics, ConnectionMetrics, AlertEntry, OrderSummary - }; - pub use crate::real_time_monitoring::{ - MonitoringServer, MonitoringClient, MonitoringManager, MonitoringError, - MonitoringMessage, TradeExecutionUpdate, ConnectionStatusUpdate, ConnectionStatus, - PerformanceMetricsUpdate - }; - pub use chrono::{DateTime, FixedOffset}; -} \ No newline at end of file +} diff --git a/src/risk_manager.rs b/src/risk_manager.rs index 5320928..27a8bf9 100644 --- a/src/risk_manager.rs +++ b/src/risk_manager.rs @@ -1,1722 +1,279 @@ -//! # Risk Management System -//! -//! This module provides risk management functionality for trading strategies, -//! including position size limits, maximum daily loss protection, stop-loss and -//! take-profit mechanisms, and leverage limits. - use std::collections::HashMap; + use chrono::{DateTime, FixedOffset, Utc}; use thiserror::Error; -use tracing::{info, warn, error}; -use crate::trading_mode::{RiskConfig}; -use crate::unified_data::{Position, OrderRequest, OrderSide, OrderType}; +use crate::unified_data::{OrderRequest, OrderSide, OrderType, Position}; + +/// Configuration values used by the [`RiskManager`]. +#[derive(Debug, Clone)] +pub struct RiskConfig { + pub max_position_size_pct: f64, + pub stop_loss_pct: f64, + pub take_profit_pct: f64, +} + +impl Default for RiskConfig { + fn default() -> Self { + Self { + max_position_size_pct: 0.1, + stop_loss_pct: 0.05, + take_profit_pct: 0.1, + } + } +} -/// Error types specific to risk management operations -#[derive(Debug, Error)] +/// Errors that can be returned by [`RiskManager`]. +#[derive(Debug, Error, Clone)] pub enum RiskError { - /// Error when position size exceeds limits - #[error("Position size exceeds limit: {message}")] - PositionSizeExceeded { - message: String, - }, - - /// Error when daily loss limit is reached - #[error("Daily loss limit reached: {current_loss_pct}% exceeds {max_loss_pct}%")] - DailyLossLimitReached { - current_loss_pct: f64, - max_loss_pct: f64, - }, - - /// Error when leverage limit is exceeded - #[error("Leverage limit exceeded: {current_leverage}x exceeds {max_leverage}x")] - LeverageLimitExceeded { - current_leverage: f64, - max_leverage: f64, - }, - - /// Error when margin is insufficient - #[error("Insufficient margin: {required_margin} exceeds {available_margin}")] - InsufficientMargin { - required_margin: f64, - available_margin: f64, - }, - - /// Error when portfolio concentration limit is exceeded - #[error("Portfolio concentration limit exceeded: {asset_class} at {concentration_pct}% exceeds {max_concentration_pct}%")] - ConcentrationLimitExceeded { - asset_class: String, - concentration_pct: f64, - max_concentration_pct: f64, - }, - - /// Error when position correlation limit is exceeded - #[error("Position correlation limit exceeded: {symbol1} and {symbol2} correlation {correlation} exceeds {max_correlation}")] - CorrelationLimitExceeded { - symbol1: String, - symbol2: String, - correlation: f64, - max_correlation: f64, - }, - - /// Error when portfolio volatility limit is exceeded - #[error("Portfolio volatility limit exceeded: {current_volatility_pct}% exceeds {max_volatility_pct}%")] - VolatilityLimitExceeded { - current_volatility_pct: f64, - max_volatility_pct: f64, - }, - - /// Error when drawdown limit is exceeded - #[error("Drawdown limit exceeded: {current_drawdown_pct}% exceeds {max_drawdown_pct}%")] - DrawdownLimitExceeded { - current_drawdown_pct: f64, - max_drawdown_pct: f64, - }, - - /// General risk management error - #[error("Risk management error: {0}")] - General(String), + /// Returned when an order would exceed the configured position size. + #[error("position size exceeds configured limit: {message}")] + PositionSizeExceeded { message: String }, + /// Returned when trading is halted by the emergency stop flag. + #[error("trading is halted by the emergency stop toggle")] + TradingHalted, } -/// Result type for risk management operations +/// Convenience result type for risk management operations. pub type Result = std::result::Result; -/// Stop-loss or take-profit order +/// Representation of a stop-loss or take-profit order managed by [`RiskManager`]. #[derive(Debug, Clone)] pub struct RiskOrder { - /// Original order ID this risk order is associated with + /// Identifier of the originating order. pub parent_order_id: String, - - /// Symbol/ticker of the asset + /// Asset symbol. pub symbol: String, - - /// Order side (buy/sell) + /// Order side used to flatten the position when triggered. pub side: OrderSide, - - /// Order type + /// Order type used when submitting the risk order. pub order_type: OrderType, - - /// Order quantity + /// Quantity to trade when the order triggers. pub quantity: f64, - - /// Trigger price + /// Trigger price for the order. pub trigger_price: f64, - - /// Whether this is a stop-loss order + /// Whether the order acts as a stop-loss. pub is_stop_loss: bool, - - /// Whether this is a take-profit order + /// Whether the order acts as a take-profit. pub is_take_profit: bool, + /// Timestamp when the risk order was created. + pub created_at: DateTime, } -/// Daily risk tracking -#[derive(Debug, Clone)] -struct DailyRiskTracker { - /// Date of tracking - date: chrono::NaiveDate, - - /// Starting portfolio value - starting_value: f64, - - /// Current portfolio value - current_value: f64, - - /// Realized profit/loss for the day - realized_pnl: f64, - - /// Unrealized profit/loss for the day - unrealized_pnl: f64, - - /// Maximum drawdown for the day - max_drawdown: f64, - - /// Highest portfolio value for the day - highest_value: f64, -} - -impl DailyRiskTracker { - /// Create a new daily risk tracker - fn new(portfolio_value: f64) -> Self { +impl RiskOrder { + fn new( + parent_order_id: &str, + symbol: &str, + side: OrderSide, + quantity: f64, + trigger_price: f64, + is_stop_loss: bool, + is_take_profit: bool, + ) -> Self { Self { - date: Utc::now().date_naive(), - starting_value: portfolio_value, - current_value: portfolio_value, - realized_pnl: 0.0, - unrealized_pnl: 0.0, - max_drawdown: 0.0, - highest_value: portfolio_value, - } - } - - /// Update the tracker with new portfolio value - fn update(&mut self, portfolio_value: f64, realized_pnl_delta: f64) { - self.current_value = portfolio_value; - self.realized_pnl += realized_pnl_delta; - self.unrealized_pnl = portfolio_value - self.starting_value - self.realized_pnl; - - // Update highest value if needed - if portfolio_value > self.highest_value { - self.highest_value = portfolio_value; - } - - // Update max drawdown if needed - let current_drawdown = (self.highest_value - portfolio_value) / self.highest_value; - if current_drawdown > self.max_drawdown { - self.max_drawdown = current_drawdown; + parent_order_id: parent_order_id.to_string(), + symbol: symbol.to_string(), + side, + order_type: OrderType::Market, + quantity, + trigger_price, + is_stop_loss, + is_take_profit, + created_at: Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()), } } - - /// Check if the daily loss limit is reached - fn is_daily_loss_limit_reached(&self, max_daily_loss_pct: f64) -> bool { - let daily_loss_pct = (self.starting_value - self.current_value) / self.starting_value * 100.0; - daily_loss_pct >= max_daily_loss_pct - } - - /// Get the current daily loss percentage - fn daily_loss_pct(&self) -> f64 { - (self.starting_value - self.current_value) / self.starting_value * 100.0 - } - - /// Reset the tracker for a new day - fn reset(&mut self, portfolio_value: f64) { - self.date = Utc::now().date_naive(); - self.starting_value = portfolio_value; - self.current_value = portfolio_value; - self.realized_pnl = 0.0; - self.unrealized_pnl = 0.0; - self.max_drawdown = 0.0; - self.highest_value = portfolio_value; - } -} - -/// Asset class for correlation and concentration management -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum AssetClass { - /// Cryptocurrencies - Crypto, - - /// Stablecoins - Stablecoin, - - /// Defi tokens - Defi, - - /// Layer 1 blockchain tokens - Layer1, - - /// Layer 2 scaling solution tokens - Layer2, - - /// Meme coins - Meme, - - /// NFT related tokens - NFT, - - /// Gaming tokens - Gaming, - - /// Other tokens - Other, -} - -/// Historical volatility data for an asset -#[derive(Debug, Clone)] -pub struct VolatilityData { - /// Symbol of the asset - pub symbol: String, - - /// Daily volatility (percentage) - pub daily_volatility: f64, - - /// Weekly volatility (percentage) - pub weekly_volatility: f64, - - /// Monthly volatility (percentage) - pub monthly_volatility: f64, - - /// Historical price data for volatility calculation - pub price_history: Vec, - - /// Last update timestamp - pub last_update: DateTime, } -/// Correlation data between two assets +/// Minimal risk management component used by the higher level trading engines. #[derive(Debug, Clone)] -pub struct CorrelationData { - /// First symbol - pub symbol1: String, - - /// Second symbol - pub symbol2: String, - - /// Correlation coefficient (-1.0 to 1.0) - pub correlation: f64, - - /// Last update timestamp - pub last_update: DateTime, -} - -/// Portfolio metrics for risk management -#[derive(Debug, Clone)] -pub struct PortfolioMetrics { - /// Portfolio value - pub value: f64, - - /// Portfolio volatility (percentage) - pub volatility: f64, - - /// Maximum drawdown (percentage) - pub max_drawdown: f64, - - /// Value at Risk (VaR) at 95% confidence - pub var_95: f64, - - /// Value at Risk (VaR) at 99% confidence - pub var_99: f64, - - /// Concentration by asset class - pub concentration: HashMap, -} - -/// Risk manager for trading strategies -#[derive(Debug)] pub struct RiskManager { - /// Risk configuration config: RiskConfig, - - /// Current portfolio value portfolio_value: f64, - - /// Available margin - available_margin: f64, - - /// Daily risk tracker - daily_tracker: DailyRiskTracker, - - /// Stop-loss orders - stop_loss_orders: HashMap, - - /// Take-profit orders - take_profit_orders: HashMap, - - /// Emergency stop flag + stop_losses: Vec, + take_profits: Vec, emergency_stop: bool, - - /// Asset class mapping - asset_classes: HashMap, - - /// Volatility data by symbol - volatility_data: HashMap, - - /// Correlation data between symbols - correlation_data: HashMap<(String, String), CorrelationData>, - - /// Portfolio metrics - portfolio_metrics: PortfolioMetrics, - - /// Historical portfolio values for drawdown calculation - historical_portfolio_values: Vec<(DateTime, f64)>, } impl RiskManager { - /// Create a new risk manager with the specified configuration - pub fn new(config: RiskConfig, initial_portfolio_value: f64) -> Self { - let available_margin = initial_portfolio_value; - let now = Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()); - + /// Create a new [`RiskManager`] with the provided configuration. + pub fn new(config: RiskConfig, portfolio_value: f64) -> Self { Self { config, - portfolio_value: initial_portfolio_value, - available_margin, - daily_tracker: DailyRiskTracker::new(initial_portfolio_value), - stop_loss_orders: HashMap::new(), - take_profit_orders: HashMap::new(), + portfolio_value, + stop_losses: Vec::new(), + take_profits: Vec::new(), emergency_stop: false, - asset_classes: Self::default_asset_classes(), - volatility_data: HashMap::new(), - correlation_data: HashMap::new(), - portfolio_metrics: PortfolioMetrics { - value: initial_portfolio_value, - volatility: 0.0, - max_drawdown: 0.0, - var_95: 0.0, - var_99: 0.0, - concentration: HashMap::new(), - }, - historical_portfolio_values: vec![(now, initial_portfolio_value)], } } - - /// Create a new risk manager with default configuration - pub fn default(initial_portfolio_value: f64) -> Self { - Self::new(RiskConfig::default(), initial_portfolio_value) - } - - /// Create default asset class mappings - fn default_asset_classes() -> HashMap { - let mut map = HashMap::new(); - - // Major cryptocurrencies - map.insert("BTC".to_string(), AssetClass::Crypto); - map.insert("ETH".to_string(), AssetClass::Crypto); - map.insert("BNB".to_string(), AssetClass::Crypto); - map.insert("SOL".to_string(), AssetClass::Crypto); - map.insert("XRP".to_string(), AssetClass::Crypto); - map.insert("ADA".to_string(), AssetClass::Crypto); - map.insert("AVAX".to_string(), AssetClass::Crypto); - - // Stablecoins - map.insert("USDT".to_string(), AssetClass::Stablecoin); - map.insert("USDC".to_string(), AssetClass::Stablecoin); - map.insert("DAI".to_string(), AssetClass::Stablecoin); - map.insert("BUSD".to_string(), AssetClass::Stablecoin); - - // DeFi tokens - map.insert("UNI".to_string(), AssetClass::Defi); - map.insert("AAVE".to_string(), AssetClass::Defi); - map.insert("MKR".to_string(), AssetClass::Defi); - map.insert("COMP".to_string(), AssetClass::Defi); - map.insert("SNX".to_string(), AssetClass::Defi); - map.insert("SUSHI".to_string(), AssetClass::Defi); - - // Layer 1 blockchains - map.insert("DOT".to_string(), AssetClass::Layer1); - map.insert("ATOM".to_string(), AssetClass::Layer1); - map.insert("NEAR".to_string(), AssetClass::Layer1); - map.insert("ALGO".to_string(), AssetClass::Layer1); - - // Layer 2 solutions - map.insert("MATIC".to_string(), AssetClass::Layer2); - map.insert("LRC".to_string(), AssetClass::Layer2); - map.insert("OMG".to_string(), AssetClass::Layer2); - map.insert("IMX".to_string(), AssetClass::Layer2); - - // Meme coins - map.insert("DOGE".to_string(), AssetClass::Meme); - map.insert("SHIB".to_string(), AssetClass::Meme); - map.insert("PEPE".to_string(), AssetClass::Meme); - - // NFT related - map.insert("APE".to_string(), AssetClass::NFT); - map.insert("SAND".to_string(), AssetClass::NFT); - map.insert("MANA".to_string(), AssetClass::NFT); - - // Gaming - map.insert("AXS".to_string(), AssetClass::Gaming); - map.insert("ENJ".to_string(), AssetClass::Gaming); - map.insert("GALA".to_string(), AssetClass::Gaming); - - map - } - - /// Get the current risk configuration + + /// Access the underlying risk configuration. pub fn config(&self) -> &RiskConfig { &self.config } - - /// Update the risk configuration - pub fn update_config(&mut self, config: RiskConfig) { - info!("Updating risk configuration"); - self.config = config; - } - - /// Update portfolio value and check daily risk limits - pub fn update_portfolio_value(&mut self, new_value: f64, realized_pnl_delta: f64) -> Result<()> { - // Check if we need to reset the daily tracker (new day) - let current_date = Utc::now().date_naive(); - if current_date != self.daily_tracker.date { - info!("New trading day, resetting daily risk tracker"); - self.daily_tracker.reset(new_value); - } else { - // Update the daily tracker - self.daily_tracker.update(new_value, realized_pnl_delta); - - // Check if daily loss limit is reached - if self.daily_tracker.is_daily_loss_limit_reached(self.config.max_daily_loss_pct) { - let daily_loss_pct = self.daily_tracker.daily_loss_pct(); - warn!("Daily loss limit reached: {:.2}% exceeds {:.2}%", - daily_loss_pct, self.config.max_daily_loss_pct); - - // Set emergency stop - self.emergency_stop = true; - - return Err(RiskError::DailyLossLimitReached { - current_loss_pct: daily_loss_pct, - max_loss_pct: self.config.max_daily_loss_pct, - }); - } - } - - // Update portfolio value and available margin - self.portfolio_value = new_value; - self.available_margin = new_value; // Simplified, in reality would depend on existing positions - + + /// Update the tracked portfolio value. The current implementation simply records + /// the latest value so that position size checks have an up-to-date notion of the + /// account size. + pub fn update_portfolio_value( + &mut self, + new_value: f64, + _realized_pnl_delta: f64, + ) -> Result<()> { + self.portfolio_value = new_value.max(0.0); Ok(()) } - - /// Validate an order against risk limits - pub fn validate_order(&self, order: &OrderRequest, current_positions: &HashMap) -> Result<()> { - // Check if emergency stop is active + + /// Validate an order against simple position size limits and the emergency stop flag. + pub fn validate_order( + &self, + order: &OrderRequest, + _positions: &HashMap, + ) -> Result<()> { if self.emergency_stop { - return Err(RiskError::General("Emergency stop is active".to_string())); - } - - // Check daily loss limit - if self.daily_tracker.is_daily_loss_limit_reached(self.config.max_daily_loss_pct) { - let daily_loss_pct = self.daily_tracker.daily_loss_pct(); - return Err(RiskError::DailyLossLimitReached { - current_loss_pct: daily_loss_pct, - max_loss_pct: self.config.max_daily_loss_pct, - }); - } - - // Check drawdown limit - if self.portfolio_metrics.max_drawdown > self.config.max_drawdown_pct { - return Err(RiskError::DrawdownLimitExceeded { - current_drawdown_pct: self.portfolio_metrics.max_drawdown * 100.0, - max_drawdown_pct: self.config.max_drawdown_pct * 100.0, - }); - } - - // Calculate order value - let order_value = match order.price { - Some(price) => order.quantity * price, - None => { - // For market orders, we need to estimate the price - // In a real implementation, we would use the current market price - // For simplicity, we'll use the current position price if available - if let Some(position) = current_positions.get(&order.symbol) { - order.quantity * position.current_price - } else { - // If no position exists, we can't validate the order properly - return Err(RiskError::General( - "Cannot validate market order without price information".to_string() - )); - } - } - }; - - // Check position size limit - let max_position_value = self.portfolio_value * self.config.max_position_size_pct; - - // Calculate the total position value after this order - let mut new_position_value = order_value; - if let Some(position) = current_positions.get(&order.symbol) { - // Add existing position value - new_position_value = match order.side { - OrderSide::Buy => position.size.abs() * position.current_price + order_value, - OrderSide::Sell => { - if order.quantity <= position.size { - // Reducing position - (position.size - order.quantity).abs() * position.current_price - } else { - // Flipping position - (order.quantity - position.size).abs() * position.current_price - } - } - }; + return Err(RiskError::TradingHalted); } - - // Apply volatility-based position sizing if volatility data is available - if let Some(volatility_data) = self.volatility_data.get(&order.symbol) { - let volatility_adjusted_max_size = self.calculate_volatility_adjusted_position_size( - &order.symbol, - max_position_value - ); - - if new_position_value > volatility_adjusted_max_size { + + if let Some(price) = order.price { + let notional = price * order.quantity.abs(); + let max_notional = self.config.max_position_size_pct * self.portfolio_value; + if max_notional > 0.0 && notional > max_notional { return Err(RiskError::PositionSizeExceeded { message: format!( - "Position value ${:.2} exceeds volatility-adjusted limit ${:.2}", - new_position_value, volatility_adjusted_max_size + "order notional {:.2} exceeds {:.2} ({:.2}% of portfolio)", + notional, + max_notional, + self.config.max_position_size_pct * 100.0, ), }); } - } else if new_position_value > max_position_value { - return Err(RiskError::PositionSizeExceeded { - message: format!( - "Position value ${:.2} exceeds limit ${:.2} ({:.2}% of portfolio)", - new_position_value, max_position_value, self.config.max_position_size_pct * 100.0 - ), - }); - } - - // Check leverage limit - let total_position_value = current_positions.values() - .map(|p| p.size.abs() * p.current_price) - .sum::() + order_value; - - let current_leverage = total_position_value / self.portfolio_value; - if current_leverage > self.config.max_leverage { - return Err(RiskError::LeverageLimitExceeded { - current_leverage, - max_leverage: self.config.max_leverage, - }); - } - - // Check margin requirements - let required_margin = total_position_value / self.config.max_leverage; - if required_margin > self.available_margin { - return Err(RiskError::InsufficientMargin { - required_margin, - available_margin: self.available_margin, - }); - } - - // Check portfolio concentration limits - if let Some(asset_class) = self.get_asset_class(&order.symbol) { - let new_concentration = self.calculate_concentration_after_order( - current_positions, - order, - asset_class - ); - - if new_concentration > self.config.max_concentration_pct { - return Err(RiskError::ConcentrationLimitExceeded { - asset_class: format!("{:?}", asset_class), - concentration_pct: new_concentration * 100.0, - max_concentration_pct: self.config.max_concentration_pct * 100.0, - }); - } } - - // Check correlation limits - if let Err(e) = self.validate_correlation_limits(current_positions, order) { - return Err(e); - } - - // Check portfolio volatility limits - if let Err(e) = self.validate_portfolio_volatility(current_positions, order) { - return Err(e); - } - + Ok(()) } - - /// Generate stop-loss order for a position - pub fn generate_stop_loss(&self, position: &Position, parent_order_id: &str) -> Option { - if position.size == 0.0 { + + /// Produce a stop-loss order for the supplied position. + pub fn generate_stop_loss(&self, position: &Position, order_id: &str) -> Option { + if position.size == 0.0 || self.config.stop_loss_pct <= 0.0 { return None; } - - // Calculate stop loss price - let stop_loss_price = if position.size > 0.0 { - // Long position + + let trigger_price = if position.size > 0.0 { position.entry_price * (1.0 - self.config.stop_loss_pct) } else { - // Short position position.entry_price * (1.0 + self.config.stop_loss_pct) }; - - // Create stop loss order - Some(RiskOrder { - parent_order_id: parent_order_id.to_string(), - symbol: position.symbol.clone(), - side: if position.size > 0.0 { OrderSide::Sell } else { OrderSide::Buy }, - order_type: OrderType::StopMarket, - quantity: position.size.abs(), - trigger_price: stop_loss_price, - is_stop_loss: true, - is_take_profit: false, - }) + + let side = if position.size > 0.0 { + OrderSide::Sell + } else { + OrderSide::Buy + }; + + Some(RiskOrder::new( + order_id, + &position.symbol, + side, + position.size.abs(), + trigger_price, + true, + false, + )) } - - /// Generate take-profit order for a position - pub fn generate_take_profit(&self, position: &Position, parent_order_id: &str) -> Option { - if position.size == 0.0 { + + /// Produce a take-profit order for the supplied position. + pub fn generate_take_profit(&self, position: &Position, order_id: &str) -> Option { + if position.size == 0.0 || self.config.take_profit_pct <= 0.0 { return None; } - - // Calculate take profit price - let take_profit_price = if position.size > 0.0 { - // Long position + + let trigger_price = if position.size > 0.0 { position.entry_price * (1.0 + self.config.take_profit_pct) } else { - // Short position position.entry_price * (1.0 - self.config.take_profit_pct) }; - - // Create take profit order - Some(RiskOrder { - parent_order_id: parent_order_id.to_string(), - symbol: position.symbol.clone(), - side: if position.size > 0.0 { OrderSide::Sell } else { OrderSide::Buy }, - order_type: OrderType::TakeProfitMarket, - quantity: position.size.abs(), - trigger_price: take_profit_price, - is_stop_loss: false, - is_take_profit: true, - }) - } - - /// Register a stop-loss order - pub fn register_stop_loss(&mut self, order: RiskOrder) { - self.stop_loss_orders.insert(order.parent_order_id.clone(), order); - } - - /// Register a take-profit order - pub fn register_take_profit(&mut self, order: RiskOrder) { - self.take_profit_orders.insert(order.parent_order_id.clone(), order); - } - - /// Check if any stop-loss or take-profit orders should be triggered - pub fn check_risk_orders(&mut self, current_prices: &HashMap) -> Vec { - let mut triggered_orders = Vec::new(); - - // Check stop-loss orders - let mut triggered_stop_loss_ids = Vec::new(); - for (id, order) in &self.stop_loss_orders { - if let Some(¤t_price) = current_prices.get(&order.symbol) { - let should_trigger = match order.side { - OrderSide::Sell => current_price <= order.trigger_price, - OrderSide::Buy => current_price >= order.trigger_price, - }; - - if should_trigger { - triggered_orders.push(order.clone()); - triggered_stop_loss_ids.push(id.clone()); - } - } - } - - // Remove triggered stop-loss orders - for id in triggered_stop_loss_ids { - self.stop_loss_orders.remove(&id); - } - - // Check take-profit orders - let mut triggered_take_profit_ids = Vec::new(); - for (id, order) in &self.take_profit_orders { - if let Some(¤t_price) = current_prices.get(&order.symbol) { - let should_trigger = match order.side { - OrderSide::Sell => current_price >= order.trigger_price, - OrderSide::Buy => current_price <= order.trigger_price, - }; - - if should_trigger { - triggered_orders.push(order.clone()); - triggered_take_profit_ids.push(id.clone()); - } - } - } - - // Remove triggered take-profit orders - for id in triggered_take_profit_ids { - self.take_profit_orders.remove(&id); - } - - triggered_orders - } - - /// Check if trading should be stopped due to risk limits - pub fn should_stop_trading(&self) -> bool { - self.emergency_stop || - self.daily_tracker.is_daily_loss_limit_reached(self.config.max_daily_loss_pct) - } - - /// Activate emergency stop - pub fn activate_emergency_stop(&mut self) { - warn!("Emergency stop activated"); - self.emergency_stop = true; - } - - /// Deactivate emergency stop - pub fn deactivate_emergency_stop(&mut self) { - info!("Emergency stop deactivated"); - self.emergency_stop = false; - } - - /// Get current daily risk metrics - pub fn daily_risk_metrics(&self) -> (f64, f64, f64) { - ( - self.daily_tracker.daily_loss_pct(), - self.daily_tracker.max_drawdown * 100.0, - (self.daily_tracker.realized_pnl / self.daily_tracker.starting_value) * 100.0 - ) - } - - /// Calculate required margin for a position - pub fn calculate_required_margin(&self, position_value: f64) -> f64 { - position_value / self.config.max_leverage - } - - /// Get available margin - pub fn available_margin(&self) -> f64 { - self.available_margin - } - - /// Update available margin - pub fn update_available_margin(&mut self, margin: f64) { - self.available_margin = margin; - } - - /// Calculate volatility-adjusted position size - pub fn calculate_volatility_adjusted_position_size(&self, symbol: &str, base_max_size: f64) -> f64 { - if let Some(volatility_data) = self.volatility_data.get(symbol) { - // Adjust position size based on volatility - // Higher volatility = smaller position size - let volatility_factor = 1.0 / (1.0 + volatility_data.daily_volatility / 100.0); - base_max_size * volatility_factor + + let side = if position.size > 0.0 { + OrderSide::Sell } else { - // If no volatility data, use base max size - base_max_size - } - } - - /// Get asset class for a symbol - pub fn get_asset_class(&self, symbol: &str) -> Option<&AssetClass> { - self.asset_classes.get(symbol) - } - - /// Calculate concentration after order - pub fn calculate_concentration_after_order( - &self, - current_positions: &HashMap, - order: &OrderRequest, - asset_class: &AssetClass, - ) -> f64 { - let mut total_value = 0.0; - let mut asset_class_value = 0.0; - - // Calculate current portfolio value by asset class - for position in current_positions.values() { - let position_value = position.size.abs() * position.current_price; - total_value += position_value; - - if let Some(pos_asset_class) = self.asset_classes.get(&position.symbol) { - if pos_asset_class == asset_class { - asset_class_value += position_value; - } - } - } - - // Add the new order value if it's the same asset class - if let Some(order_asset_class) = self.asset_classes.get(&order.symbol) { - if order_asset_class == asset_class { - let order_value = match order.price { - Some(price) => order.quantity * price, - None => { - // Estimate using current position price if available - if let Some(position) = current_positions.get(&order.symbol) { - order.quantity * position.current_price - } else { - 0.0 // Can't calculate without price - } - } - }; - asset_class_value += order_value; - } - } - - // Add the order value to total - let order_value = match order.price { - Some(price) => order.quantity * price, - None => { - if let Some(position) = current_positions.get(&order.symbol) { - order.quantity * position.current_price - } else { - 0.0 - } - } + OrderSide::Buy }; - total_value += order_value; - - if total_value > 0.0 { - asset_class_value / total_value - } else { - 0.0 - } - } - - /// Validate correlation limits - pub fn validate_correlation_limits( - &self, - current_positions: &HashMap, - order: &OrderRequest, - ) -> Result<()> { - // Check correlation with existing positions - for position in current_positions.values() { - if position.symbol != order.symbol { - // Check if we have correlation data for this pair - let key1 = (position.symbol.clone(), order.symbol.clone()); - let key2 = (order.symbol.clone(), position.symbol.clone()); - - if let Some(correlation_data) = self.correlation_data.get(&key1) - .or_else(|| self.correlation_data.get(&key2)) { - - // If correlation is too high and positions are in same direction, reject - if correlation_data.correlation.abs() > self.config.max_correlation_pct { - let position_direction = if position.size > 0.0 { 1.0 } else { -1.0 }; - let order_direction = match order.side { - OrderSide::Buy => 1.0, - OrderSide::Sell => -1.0, - }; - - // If same direction and high correlation, it's risky - if position_direction * order_direction > 0.0 { - return Err(RiskError::CorrelationLimitExceeded { - symbol1: position.symbol.clone(), - symbol2: order.symbol.clone(), - correlation: correlation_data.correlation, - max_correlation: self.config.max_correlation_pct, - }); - } - } - } - } - } - - Ok(()) - } - - /// Validate portfolio volatility - pub fn validate_portfolio_volatility( - &self, - current_positions: &HashMap, - order: &OrderRequest, - ) -> Result<()> { - // Calculate portfolio volatility after adding the order - let mut portfolio_variance = 0.0; - let mut total_value = 0.0; - - // Current positions contribution to volatility - for position in current_positions.values() { - if let Some(volatility_data) = self.volatility_data.get(&position.symbol) { - let position_value = position.size.abs() * position.current_price; - let weight = position_value / self.portfolio_value; - portfolio_variance += (weight * volatility_data.daily_volatility / 100.0).powi(2); - total_value += position_value; - } - } - - // Add new order contribution - if let Some(volatility_data) = self.volatility_data.get(&order.symbol) { - let order_value = match order.price { - Some(price) => order.quantity * price, - None => { - if let Some(position) = current_positions.get(&order.symbol) { - order.quantity * position.current_price - } else { - return Ok(()); // Can't validate without price - } - } - }; - - let new_total_value = total_value + order_value; - let weight = order_value / new_total_value; - portfolio_variance += (weight * volatility_data.daily_volatility / 100.0).powi(2); - } - - let portfolio_volatility = portfolio_variance.sqrt() * 100.0; // Convert to percentage - - if portfolio_volatility > self.config.max_portfolio_volatility_pct { - return Err(RiskError::VolatilityLimitExceeded { - current_volatility_pct: portfolio_volatility, - max_volatility_pct: self.config.max_portfolio_volatility_pct, - }); - } - - Ok(()) - } -} -#[cfg(test)] -mod tests { - use super::*; - use chrono::TimeZone; - - fn create_test_position(symbol: &str, size: f64, entry_price: f64, current_price: f64) -> Position { - Position { - symbol: symbol.to_string(), - size, - entry_price, - current_price, - unrealized_pnl: (current_price - entry_price) * size, - realized_pnl: 0.0, - funding_pnl: 0.0, - timestamp: Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()), - } - } - - fn create_test_order(symbol: &str, side: OrderSide, quantity: f64, price: Option) -> OrderRequest { - OrderRequest { - symbol: symbol.to_string(), + Some(RiskOrder::new( + order_id, + &position.symbol, side, - order_type: OrderType::Limit, - quantity, - price, - reduce_only: false, - time_in_force: crate::trading_mode_impl::TimeInForce::GoodTillCancel, - } - } - - #[test] - fn test_position_size_validation() { - let config = RiskConfig { - max_position_size_pct: 0.1, // 10% of portfolio - max_daily_loss_pct: 0.02, // 2% max daily loss - stop_loss_pct: 0.05, // 5% stop loss - take_profit_pct: 0.1, // 10% take profit - max_leverage: 3.0, // 3x max leverage - }; - - let portfolio_value = 10000.0; - let mut risk_manager = RiskManager::new(config, portfolio_value); - - let mut positions = HashMap::new(); - - // Test valid order within position size limit - let order = create_test_order("BTC", OrderSide::Buy, 0.1, Some(9000.0)); - // Order value: 0.1 * 9000 = 900, which is < 10% of 10000 (1000) - assert!(risk_manager.validate_order(&order, &positions).is_ok()); - - // Test order exceeding position size limit - let order = create_test_order("BTC", OrderSide::Buy, 0.2, Some(9000.0)); - // Order value: 0.2 * 9000 = 1800, which is > 10% of 10000 (1000) - assert!(risk_manager.validate_order(&order, &positions).is_err()); - - // Test with existing position - positions.insert( - "BTC".to_string(), - create_test_position("BTC", 0.05, 8000.0, 9000.0) - ); - - // Test valid order with existing position - let order = create_test_order("BTC", OrderSide::Buy, 0.05, Some(9000.0)); - // Existing position value: 0.05 * 9000 = 450 - // Order value: 0.05 * 9000 = 450 - // Total: 900, which is < 10% of 10000 (1000) - assert!(risk_manager.validate_order(&order, &positions).is_ok()); - - // Test order exceeding position size limit with existing position - let order = create_test_order("BTC", OrderSide::Buy, 0.07, Some(9000.0)); - // Existing position value: 0.05 * 9000 = 450 - // Order value: 0.07 * 9000 = 630 - // Total: 1080, which is > 10% of 10000 (1000) - assert!(risk_manager.validate_order(&order, &positions).is_err()); - } - - #[test] - fn test_leverage_validation() { - let config = RiskConfig { - max_position_size_pct: 0.5, // 50% of portfolio (high to test leverage) - max_daily_loss_pct: 0.02, // 2% max daily loss - stop_loss_pct: 0.05, // 5% stop loss - take_profit_pct: 0.1, // 10% take profit - max_leverage: 2.0, // 2x max leverage - }; - - let portfolio_value = 10000.0; - let mut risk_manager = RiskManager::new(config, portfolio_value); - - let mut positions = HashMap::new(); - positions.insert( - "ETH".to_string(), - create_test_position("ETH", 2.0, 1500.0, 1600.0) - ); - // ETH position value: 2.0 * 1600 = 3200 - - // Test valid order within leverage limit - let order = create_test_order("BTC", OrderSide::Buy, 0.1, Some(9000.0)); - // Order value: 0.1 * 9000 = 900 - // Total position value: 3200 + 900 = 4100 - // Leverage: 4100 / 10000 = 0.41, which is < 2.0 - assert!(risk_manager.validate_order(&order, &positions).is_ok()); - - // Test order exceeding leverage limit - let order = create_test_order("BTC", OrderSide::Buy, 2.0, Some(9000.0)); - // Order value: 2.0 * 9000 = 18000 - // Total position value: 3200 + 18000 = 21200 - // Leverage: 21200 / 10000 = 2.12, which is > 2.0 - assert!(risk_manager.validate_order(&order, &positions).is_err()); - } - - #[test] - fn test_daily_loss_limit() { - let config = RiskConfig { - max_position_size_pct: 0.1, // 10% of portfolio - max_daily_loss_pct: 2.0, // 2% max daily loss - stop_loss_pct: 0.05, // 5% stop loss - take_profit_pct: 0.1, // 10% take profit - max_leverage: 3.0, // 3x max leverage - }; - - let portfolio_value = 10000.0; - let mut risk_manager = RiskManager::new(config, portfolio_value); - - // Update portfolio value with small loss (1%) - assert!(risk_manager.update_portfolio_value(9900.0, -100.0).is_ok()); - - // Verify daily loss is tracked correctly - let (daily_loss_pct, _, _) = risk_manager.daily_risk_metrics(); - assert_eq!(daily_loss_pct, 1.0); - - // Update portfolio value with loss exceeding daily limit (3% total) - assert!(risk_manager.update_portfolio_value(9700.0, -200.0).is_err()); - - // Verify trading should be stopped - assert!(risk_manager.should_stop_trading()); - } - - #[test] - fn test_stop_loss_generation() { - let config = RiskConfig { - max_position_size_pct: 0.1, // 10% of portfolio - max_daily_loss_pct: 2.0, // 2% max daily loss - stop_loss_pct: 0.05, // 5% stop loss - take_profit_pct: 0.1, // 10% take profit - max_leverage: 3.0, // 3x max leverage - }; - - let portfolio_value = 10000.0; - let risk_manager = RiskManager::new(config, portfolio_value); - - // Test stop loss for long position - let long_position = create_test_position("BTC", 0.1, 10000.0, 10000.0); - let stop_loss = risk_manager.generate_stop_loss(&long_position, "order1").unwrap(); - - assert_eq!(stop_loss.symbol, "BTC"); - assert!(matches!(stop_loss.side, OrderSide::Sell)); - assert!(matches!(stop_loss.order_type, OrderType::StopMarket)); - assert_eq!(stop_loss.quantity, 0.1); - assert_eq!(stop_loss.trigger_price, 9500.0); // 5% below entry price - - // Test stop loss for short position - let short_position = create_test_position("BTC", -0.1, 10000.0, 10000.0); - let stop_loss = risk_manager.generate_stop_loss(&short_position, "order2").unwrap(); - - assert_eq!(stop_loss.symbol, "BTC"); - assert!(matches!(stop_loss.side, OrderSide::Buy)); - assert!(matches!(stop_loss.order_type, OrderType::StopMarket)); - assert_eq!(stop_loss.quantity, 0.1); - assert_eq!(stop_loss.trigger_price, 10500.0); // 5% above entry price - } - - #[test] - fn test_take_profit_generation() { - let config = RiskConfig { - max_position_size_pct: 0.1, // 10% of portfolio - max_daily_loss_pct: 2.0, // 2% max daily loss - stop_loss_pct: 0.05, // 5% stop loss - take_profit_pct: 0.1, // 10% take profit - max_leverage: 3.0, // 3x max leverage - }; - - let portfolio_value = 10000.0; - let risk_manager = RiskManager::new(config, portfolio_value); - - // Test take profit for long position - let long_position = create_test_position("BTC", 0.1, 10000.0, 10000.0); - let take_profit = risk_manager.generate_take_profit(&long_position, "order1").unwrap(); - - assert_eq!(take_profit.symbol, "BTC"); - assert!(matches!(take_profit.side, OrderSide::Sell)); - assert!(matches!(take_profit.order_type, OrderType::TakeProfitMarket)); - assert_eq!(take_profit.quantity, 0.1); - assert_eq!(take_profit.trigger_price, 11000.0); // 10% above entry price - - // Test take profit for short position - let short_position = create_test_position("BTC", -0.1, 10000.0, 10000.0); - let take_profit = risk_manager.generate_take_profit(&short_position, "order2").unwrap(); - - assert_eq!(take_profit.symbol, "BTC"); - assert!(matches!(take_profit.side, OrderSide::Buy)); - assert!(matches!(take_profit.order_type, OrderType::TakeProfitMarket)); - assert_eq!(take_profit.quantity, 0.1); - assert_eq!(take_profit.trigger_price, 9000.0); // 10% below entry price - } - - #[test] - fn test_risk_orders_triggering() { - let config = RiskConfig { - max_position_size_pct: 0.1, - max_daily_loss_pct: 2.0, - stop_loss_pct: 0.05, - take_profit_pct: 0.1, - max_leverage: 3.0, - }; - - let portfolio_value = 10000.0; - let mut risk_manager = RiskManager::new(config, portfolio_value); - - // Create and register a stop loss order - let long_position = create_test_position("BTC", 0.1, 10000.0, 10000.0); - let stop_loss = risk_manager.generate_stop_loss(&long_position, "order1").unwrap(); - risk_manager.register_stop_loss(stop_loss); - - // Create and register a take profit order - let take_profit = risk_manager.generate_take_profit(&long_position, "order1").unwrap(); - risk_manager.register_take_profit(take_profit); - - // Test no orders triggered at current price - let mut current_prices = HashMap::new(); - current_prices.insert("BTC".to_string(), 10000.0); - let triggered = risk_manager.check_risk_orders(¤t_prices); - assert_eq!(triggered.len(), 0); - - // Test stop loss triggered - current_prices.insert("BTC".to_string(), 9400.0); // Below stop loss price - let triggered = risk_manager.check_risk_orders(¤t_prices); - assert_eq!(triggered.len(), 1); - assert!(triggered[0].is_stop_loss); - - // Register new orders - let long_position = create_test_position("BTC", 0.1, 10000.0, 10000.0); - let stop_loss = risk_manager.generate_stop_loss(&long_position, "order2").unwrap(); - risk_manager.register_stop_loss(stop_loss); - let take_profit = risk_manager.generate_take_profit(&long_position, "order2").unwrap(); - risk_manager.register_take_profit(take_profit); - - // Test take profit triggered - current_prices.insert("BTC".to_string(), 11100.0); // Above take profit price - let triggered = risk_manager.check_risk_orders(¤t_prices); - assert_eq!(triggered.len(), 1); - assert!(triggered[0].is_take_profit); + position.size.abs(), + trigger_price, + false, + true, + )) } - - #[test] - fn test_emergency_stop() { - let config = RiskConfig::default(); - let portfolio_value = 10000.0; - let mut risk_manager = RiskManager::new(config, portfolio_value); - - // Initially, emergency stop should be false - assert!(!risk_manager.should_stop_trading()); - - // Activate emergency stop - risk_manager.activate_emergency_stop(); - assert!(risk_manager.should_stop_trading()); - - // Orders should be rejected when emergency stop is active - let positions = HashMap::new(); - let order = create_test_order("BTC", OrderSide::Buy, 0.1, Some(10000.0)); - assert!(risk_manager.validate_order(&order, &positions).is_err()); - - // Deactivate emergency stop - risk_manager.deactivate_emergency_stop(); - assert!(!risk_manager.should_stop_trading()); - - // Orders should be accepted again - assert!(risk_manager.validate_order(&order, &positions).is_ok()); - } - /// Get the asset class for a symbol - pub fn get_asset_class(&self, symbol: &str) -> Option<&AssetClass> { - self.asset_classes.get(symbol) + + /// Store a generated stop-loss order. + pub fn register_stop_loss(&mut self, order: RiskOrder) { + self.stop_losses.push(order); } - - /// Set the asset class for a symbol - pub fn set_asset_class(&mut self, symbol: String, asset_class: AssetClass) { - self.asset_classes.insert(symbol, asset_class); + + /// Store a generated take-profit order. + pub fn register_take_profit(&mut self, order: RiskOrder) { + self.take_profits.push(order); } - - /// Calculate the concentration of an asset class after a potential order - fn calculate_concentration_after_order( - &self, - current_positions: &HashMap, - order: &OrderRequest, - asset_class: &AssetClass - ) -> f64 { - // Calculate current concentration by asset class - let mut asset_class_values = HashMap::new(); - let mut total_position_value = 0.0; - - // Add current positions - for (symbol, position) in current_positions { - let position_value = position.size.abs() * position.current_price; - total_position_value += position_value; - - if let Some(class) = self.get_asset_class(symbol) { - *asset_class_values.entry(class).or_insert(0.0) += position_value; - } - } - - // Calculate order value - let order_value = match order.price { - Some(price) => order.quantity * price, - None => { - if let Some(position) = current_positions.get(&order.symbol) { - order.quantity * position.current_price - } else { - // If we can't determine the price, assume zero (conservative) - 0.0 + + /// Inspect tracked risk orders against the latest market prices. + pub fn check_risk_orders(&mut self, current_prices: &HashMap) -> Vec { + fn should_trigger(order: &RiskOrder, price: f64) -> bool { + if order.is_stop_loss { + match order.side { + OrderSide::Sell => price <= order.trigger_price, + OrderSide::Buy => price >= order.trigger_price, } - } - }; - - // Update total position value - total_position_value += order_value; - - // Update asset class value - *asset_class_values.entry(asset_class).or_insert(0.0) += order_value; - - // Calculate concentration - if total_position_value > 0.0 { - asset_class_values.get(asset_class).unwrap_or(&0.0) / total_position_value - } else { - 0.0 - } - } - - /// Update volatility data for a symbol - pub fn update_volatility_data(&mut self, symbol: String, price_history: Vec) { - if price_history.len() < 30 { - warn!("Insufficient price history for volatility calculation for {}", symbol); - return; - } - - // Calculate daily returns - let mut daily_returns = Vec::with_capacity(price_history.len() - 1); - for i in 1..price_history.len() { - let daily_return = (price_history[i] - price_history[i-1]) / price_history[i-1]; - daily_returns.push(daily_return); - } - - // Calculate daily volatility (standard deviation of returns) - let mean_return = daily_returns.iter().sum::() / daily_returns.len() as f64; - let variance = daily_returns.iter() - .map(|r| (r - mean_return).powi(2)) - .sum::() / daily_returns.len() as f64; - let daily_volatility = variance.sqrt() * 100.0; // Convert to percentage - - // Calculate weekly volatility (approximate) - let weekly_volatility = daily_volatility * (5.0_f64).sqrt(); - - // Calculate monthly volatility (approximate) - let monthly_volatility = daily_volatility * (21.0_f64).sqrt(); - - // Update volatility data - self.volatility_data.insert(symbol.clone(), VolatilityData { - symbol, - daily_volatility, - weekly_volatility, - monthly_volatility, - price_history, - last_update: Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()), - }); - - // Update portfolio metrics - self.update_portfolio_metrics(); - } - - /// Calculate volatility-adjusted position size - fn calculate_volatility_adjusted_position_size(&self, symbol: &str, base_position_size: f64) -> f64 { - if let Some(volatility_data) = self.volatility_data.get(symbol) { - // Use daily volatility for adjustment - // Higher volatility -> smaller position size - let volatility_factor = 1.0 - (self.config.volatility_sizing_factor * - (volatility_data.daily_volatility / 100.0)); - - // Ensure factor is between 0.1 and 1.0 - let adjusted_factor = volatility_factor.max(0.1).min(1.0); - - base_position_size * adjusted_factor - } else { - // If no volatility data, return the base position size - base_position_size - } - } - - /// Update correlation data between two symbols - pub fn update_correlation_data(&mut self, symbol1: String, symbol2: String, price_history1: &[f64], price_history2: &[f64]) { - if price_history1.len() < 30 || price_history2.len() < 30 || price_history1.len() != price_history2.len() { - warn!("Insufficient or mismatched price history for correlation calculation"); - return; - } - - // Calculate returns - let mut returns1 = Vec::with_capacity(price_history1.len() - 1); - let mut returns2 = Vec::with_capacity(price_history2.len() - 1); - - for i in 1..price_history1.len() { - let return1 = (price_history1[i] - price_history1[i-1]) / price_history1[i-1]; - let return2 = (price_history2[i] - price_history2[i-1]) / price_history2[i-1]; - returns1.push(return1); - returns2.push(return2); - } - - // Calculate correlation coefficient - let mean1 = returns1.iter().sum::() / returns1.len() as f64; - let mean2 = returns2.iter().sum::() / returns2.len() as f64; - - let mut numerator = 0.0; - let mut denom1 = 0.0; - let mut denom2 = 0.0; - - for i in 0..returns1.len() { - let diff1 = returns1[i] - mean1; - let diff2 = returns2[i] - mean2; - numerator += diff1 * diff2; - denom1 += diff1 * diff1; - denom2 += diff2 * diff2; - } - - let correlation = if denom1 > 0.0 && denom2 > 0.0 { - numerator / (denom1.sqrt() * denom2.sqrt()) - } else { - 0.0 - }; - - // Store correlation data (both directions) - let correlation_data = CorrelationData { - symbol1: symbol1.clone(), - symbol2: symbol2.clone(), - correlation, - last_update: Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()), - }; - - self.correlation_data.insert((symbol1.clone(), symbol2.clone()), correlation_data.clone()); - self.correlation_data.insert((symbol2, symbol1), CorrelationData { - symbol1: correlation_data.symbol2, - symbol2: correlation_data.symbol1, - correlation: correlation_data.correlation, - last_update: correlation_data.last_update, - }); - } - - /// Get correlation between two symbols - pub fn get_correlation(&self, symbol1: &str, symbol2: &str) -> Option { - self.correlation_data.get(&(symbol1.to_string(), symbol2.to_string())) - .map(|data| data.correlation) - } - - /// Validate correlation limits for a new order - fn validate_correlation_limits(&self, current_positions: &HashMap, order: &OrderRequest) -> Result<()> { - // Skip validation if no correlation data or no existing positions - if self.correlation_data.is_empty() || current_positions.is_empty() { - return Ok(()); - } - - // Check correlation with existing positions - for (symbol, position) in current_positions { - // Skip the same symbol or positions with zero size - if symbol == &order.symbol || position.size == 0.0 { - continue; - } - - // Check if we have correlation data - if let Some(correlation) = self.get_correlation(&order.symbol, symbol) { - // Only check for high positive correlation if positions are in the same direction - let same_direction = (order.side == OrderSide::Buy && position.size > 0.0) || - (order.side == OrderSide::Sell && position.size < 0.0); - - if same_direction && correlation.abs() > self.config.max_position_correlation { - return Err(RiskError::CorrelationLimitExceeded { - symbol1: order.symbol.clone(), - symbol2: symbol.clone(), - correlation, - max_correlation: self.config.max_position_correlation, - }); + } else if order.is_take_profit { + match order.side { + OrderSide::Sell => price >= order.trigger_price, + OrderSide::Buy => price <= order.trigger_price, } - } - } - - Ok(()) - } - - /// Update portfolio metrics - pub fn update_portfolio_metrics(&mut self) { - // Calculate portfolio volatility if we have volatility data - if !self.volatility_data.is_empty() { - self.calculate_portfolio_volatility(); - } - - // Update drawdown - self.calculate_drawdown(); - - // Update Value at Risk (VaR) - self.calculate_value_at_risk(); - } - - /// Calculate portfolio volatility - fn calculate_portfolio_volatility(&mut self) { - // This is a simplified portfolio volatility calculation - // In a real implementation, we would use a covariance matrix - - // Get total portfolio value - let total_value = self.portfolio_value; - if total_value <= 0.0 { - self.portfolio_metrics.volatility = 0.0; - return; - } - - // Calculate weighted volatility - let mut weighted_volatility = 0.0; - let mut total_weighted_value = 0.0; - - for (symbol, volatility_data) in &self.volatility_data { - // Assume we have a position in this asset - // In a real implementation, we would check actual positions - let weight = 1.0 / self.volatility_data.len() as f64; - weighted_volatility += volatility_data.daily_volatility * weight; - total_weighted_value += weight; - } - - // Normalize - if total_weighted_value > 0.0 { - self.portfolio_metrics.volatility = weighted_volatility / total_weighted_value; - } else { - self.portfolio_metrics.volatility = 0.0; - } - } - - /// Calculate drawdown - fn calculate_drawdown(&mut self) { - if self.historical_portfolio_values.is_empty() { - self.portfolio_metrics.max_drawdown = 0.0; - return; - } - - // Find peak value - let mut peak_value = self.historical_portfolio_values[0].1; - let mut max_drawdown = 0.0; - - for &(_, value) in &self.historical_portfolio_values { - if value > peak_value { - peak_value = value; - } - - let drawdown = if peak_value > 0.0 { - (peak_value - value) / peak_value } else { - 0.0 - }; - - if drawdown > max_drawdown { - max_drawdown = drawdown; - } - } - - self.portfolio_metrics.max_drawdown = max_drawdown; - } - - /// Calculate Value at Risk (VaR) - fn calculate_value_at_risk(&mut self) { - // This is a simplified VaR calculation using historical simulation - // In a real implementation, we would use more sophisticated methods - - if self.historical_portfolio_values.len() < 30 { - self.portfolio_metrics.var_95 = 0.0; - self.portfolio_metrics.var_99 = 0.0; - return; - } - - // Calculate daily returns - let mut daily_returns = Vec::with_capacity(self.historical_portfolio_values.len() - 1); - for i in 1..self.historical_portfolio_values.len() { - let prev_value = self.historical_portfolio_values[i-1].1; - let curr_value = self.historical_portfolio_values[i].1; - - if prev_value > 0.0 { - let daily_return = (curr_value - prev_value) / prev_value; - daily_returns.push(daily_return); + false } } - - // Sort returns in ascending order - daily_returns.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); - - // Calculate VaR at 95% confidence - let index_95 = (daily_returns.len() as f64 * 0.05).floor() as usize; - if index_95 < daily_returns.len() { - self.portfolio_metrics.var_95 = -daily_returns[index_95] * self.portfolio_value; - } - - // Calculate VaR at 99% confidence - let index_99 = (daily_returns.len() as f64 * 0.01).floor() as usize; - if index_99 < daily_returns.len() { - self.portfolio_metrics.var_99 = -daily_returns[index_99] * self.portfolio_value; - } - } - - /// Validate portfolio volatility limits - fn validate_portfolio_volatility(&self, current_positions: &HashMap, order: &OrderRequest) -> Result<()> { - // Skip validation if we don't have enough volatility data - if self.volatility_data.is_empty() { - return Ok(()); - } - - // Check if adding this position would exceed portfolio volatility limits - // This is a simplified check - in a real implementation, we would recalculate portfolio volatility - - if let Some(volatility_data) = self.volatility_data.get(&order.symbol) { - // If the asset is more volatile than our limit and it's a significant position - let order_value = match order.price { - Some(price) => order.quantity * price, - None => { - if let Some(position) = current_positions.get(&order.symbol) { - order.quantity * position.current_price - } else { - 0.0 - } + + let mut triggered = Vec::new(); + + self.stop_losses.retain(|order| { + if let Some(price) = current_prices.get(&order.symbol) { + if should_trigger(order, *price) { + triggered.push(order.clone()); + return false; } - }; - - let position_weight = order_value / self.portfolio_value; - - // If this is a significant position in a highly volatile asset - if position_weight > 0.1 && volatility_data.daily_volatility > self.config.max_portfolio_volatility_pct { - return Err(RiskError::VolatilityLimitExceeded { - current_volatility_pct: volatility_data.daily_volatility, - max_volatility_pct: self.config.max_portfolio_volatility_pct, - }); } - - // If current portfolio volatility is already near the limit - if self.portfolio_metrics.volatility > self.config.max_portfolio_volatility_pct * 0.9 { - // And this asset is more volatile than the portfolio - if volatility_data.daily_volatility > self.portfolio_metrics.volatility { - return Err(RiskError::VolatilityLimitExceeded { - current_volatility_pct: self.portfolio_metrics.volatility, - max_volatility_pct: self.config.max_portfolio_volatility_pct, - }); + true + }); + + self.take_profits.retain(|order| { + if let Some(price) = current_prices.get(&order.symbol) { + if should_trigger(order, *price) { + triggered.push(order.clone()); + return false; } } - } - - Ok(()) - } - - /// Update portfolio value and track historical values - pub fn update_portfolio_value_with_history(&mut self, new_value: f64, realized_pnl_delta: f64) -> Result<()> { - // Update regular portfolio value - let result = self.update_portfolio_value(new_value, realized_pnl_delta); - - // Add to historical values - let now = Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()); - self.historical_portfolio_values.push((now, new_value)); - - // Limit history size to prevent memory issues - if self.historical_portfolio_values.len() > 1000 { - self.historical_portfolio_values.remove(0); - } - - // Update portfolio metrics - self.update_portfolio_metrics(); - - // Check drawdown limit - if self.portfolio_metrics.max_drawdown > self.config.max_drawdown_pct { - warn!( - "Maximum drawdown limit reached: {:.2}% exceeds {:.2}%", - self.portfolio_metrics.max_drawdown * 100.0, - self.config.max_drawdown_pct * 100.0 - ); - - // Activate emergency stop - self.activate_emergency_stop(); - - return Err(RiskError::DrawdownLimitExceeded { - current_drawdown_pct: self.portfolio_metrics.max_drawdown * 100.0, - max_drawdown_pct: self.config.max_drawdown_pct * 100.0, - }); - } - - result - } - - /// Get current portfolio metrics - pub fn get_portfolio_metrics(&self) -> &PortfolioMetrics { - &self.portfolio_metrics - } - - /// Get volatility data for a symbol - pub fn get_volatility_data(&self, symbol: &str) -> Option<&VolatilityData> { - self.volatility_data.get(symbol) - } - - /// Get all volatility data - pub fn get_all_volatility_data(&self) -> &HashMap { - &self.volatility_data - } - - /// Get all correlation data - pub fn get_all_correlation_data(&self) -> &HashMap<(String, String), CorrelationData> { - &self.correlation_data - } - - /// Calculate volatility-adjusted position size - pub fn calculate_volatility_adjusted_position_size(&self, symbol: &str, base_size: f64) -> f64 { - if let Some(volatility_data) = self.volatility_data.get(symbol) { - // Adjust position size based on volatility - let volatility_factor = 1.0 / (1.0 + volatility_data.daily_volatility); - base_size * volatility_factor - } else { - base_size - } - } + true + }); - /// Get asset class for a symbol (placeholder implementation) - pub fn get_asset_class(&self, _symbol: &str) -> Option { - // Placeholder - in real implementation, this would classify assets - Some("crypto".to_string()) + triggered } - /// Calculate concentration after order - pub fn calculate_concentration_after_order( - &self, - current_positions: &HashMap, - order: &OrderRequest, - asset_class: String - ) -> f64 { - // Calculate current concentration for this asset class - let current_class_value: f64 = current_positions.values() - .filter(|p| self.get_asset_class(&p.symbol).as_deref() == Some(&asset_class)) - .map(|p| p.size.abs() * p.current_price) - .sum(); - - // Add the new order value if it's the same asset class - let order_value = if self.get_asset_class(&order.symbol).as_deref() == Some(&asset_class) { - order.quantity.abs() * order.price.unwrap_or(0.0) - } else { - 0.0 - }; - - let total_class_value = current_class_value + order_value; - - // Return concentration as percentage of portfolio - if self.portfolio_value > 0.0 { - total_class_value / self.portfolio_value - } else { - 0.0 - } + /// Manually trigger the emergency stop. + pub fn activate_emergency_stop(&mut self) { + self.emergency_stop = true; } - /// Validate correlation limits - pub fn validate_correlation_limits( - &self, - current_positions: &HashMap, - order: &OrderRequest - ) -> Result<()> { - // Simplified implementation - check if adding this position would create high correlation - for position in current_positions.values() { - if let Some(correlation_data) = self.correlation_data.get(&(position.symbol.clone(), order.symbol.clone())) { - if correlation_data.correlation.abs() > 0.8 { - return Err(RiskError::CorrelationLimitExceeded { - symbol1: position.symbol.clone(), - symbol2: order.symbol.clone(), - correlation: correlation_data.correlation, - max_correlation: 0.8, - }); - } - } - } - Ok(()) + /// Clear the emergency stop condition. + pub fn deactivate_emergency_stop(&mut self) { + self.emergency_stop = false; } - /// Validate portfolio volatility - pub fn validate_portfolio_volatility( - &self, - _current_positions: &HashMap, - _order: &OrderRequest - ) -> Result<()> { - // Check if current portfolio volatility is within limits - if self.portfolio_metrics.volatility > self.config.max_portfolio_volatility_pct { - return Err(RiskError::VolatilityLimitExceeded { - current_volatility_pct: self.portfolio_metrics.volatility, - max_volatility_pct: self.config.max_portfolio_volatility_pct, - }); - } - Ok(()) + /// Check whether trading should be halted. + pub fn should_stop_trading(&self) -> bool { + self.emergency_stop } -} \ No newline at end of file +} diff --git a/src/tests/basic.rs b/src/tests/basic.rs new file mode 100644 index 0000000..d5004d3 --- /dev/null +++ b/src/tests/basic.rs @@ -0,0 +1,49 @@ +use chrono::{FixedOffset, Utc}; + +use crate::unified_data::FundingPayment; +use crate::unified_data::{OrderRequest, OrderSide, OrderType, Position, TimeInForce}; + +#[test] +fn position_updates_total_pnl_after_price_change_and_funding() { + let tz = FixedOffset::east_opt(0).expect("valid offset"); + let timestamp = Utc::now().with_timezone(&tz); + + let mut position = Position::new("BTC", 2.0, 100.0, 100.0, timestamp); + position.update_price(110.0); + position.apply_funding_payment(1.5); + + let expected_unrealized = 2.0 * (110.0 - 100.0); + let expected_total = expected_unrealized + 1.5; + + assert!((position.total_pnl() - expected_total).abs() < f64::EPSILON); +} + +#[test] +fn limit_order_builder_sets_expected_fields() { + let order = OrderRequest::limit("ETH", OrderSide::Sell, 1.25, 2000.0); + + assert_eq!(order.symbol, "ETH"); + assert!(matches!(order.side, OrderSide::Sell)); + assert!(matches!(order.order_type, OrderType::Limit)); + assert_eq!(order.quantity, 1.25); + assert_eq!(order.price, Some(2000.0)); + assert!(!order.reduce_only); + assert!(matches!(order.time_in_force, TimeInForce::GoodTillCancel)); +} + +#[test] +fn funding_payment_struct_is_constructible() { + let tz = FixedOffset::east_opt(0).expect("valid offset"); + let timestamp = Utc::now().with_timezone(&tz); + + let payment = FundingPayment { + timestamp, + position_size: 0.75, + funding_rate: 0.0001, + payment_amount: 1.2, + mark_price: 25000.0, + }; + + assert!(payment.payment_amount.is_finite()); + assert_eq!(payment.position_size, 0.75); +} diff --git a/src/unified_data.rs b/src/unified_data.rs index 858d25b..eb7ae79 100644 --- a/src/unified_data.rs +++ b/src/unified_data.rs @@ -1,64 +1,44 @@ -//! # Unified Data Structures -//! -//! This module provides unified data structures that work across all trading modes -//! (backtest, paper trading, live trading) to ensure consistent strategy execution -//! and seamless transitions between modes. -//! -//! ## Features -//! -//! - Position tracking across all trading modes -//! - Order request and result structures for unified order management -//! - Market data structure for real-time data handling -//! - Trading configuration and risk management structures -//! - Signal and strategy interfaces for consistent strategy execution - +use chrono::{DateTime, FixedOffset, Utc}; use std::collections::HashMap; -use chrono::{DateTime, FixedOffset}; -use serde::{Deserialize, Serialize}; -use crate::backtest::FundingPayment; -/// Position information across all trading modes -#[derive(Debug, Clone, Serialize, Deserialize)] +pub use crate::backtest::FundingPayment; + +/// Direction of an order. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OrderSide { + Buy, + Sell, +} + +/// Supported order execution types. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OrderType { + Market, + Limit, +} + +/// Time-in-force settings for orders. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TimeInForce { + GoodTillCancel, + ImmediateOrCancel, + FillOrKill, + GoodTillDate, +} + +/// Basic representation of a trading position. +#[derive(Debug, Clone, PartialEq)] pub struct Position { - /// Symbol/ticker of the asset pub symbol: String, - - /// Position size (positive for long, negative for short) pub size: f64, - - /// Entry price pub entry_price: f64, - - /// Current price pub current_price: f64, - - /// Unrealized profit and loss - pub unrealized_pnl: f64, - - /// Realized profit and loss pub realized_pnl: f64, - - /// Funding profit and loss (for perpetual futures) pub funding_pnl: f64, - - /// Position timestamp pub timestamp: DateTime, - - /// Leverage used for this position - pub leverage: f64, - - /// Liquidation price (if applicable) - pub liquidation_price: Option, - - /// Position margin (if applicable) - pub margin: Option, - - /// Additional position metadata - pub metadata: HashMap, } impl Position { - /// Create a new position pub fn new( symbol: &str, size: f64, @@ -66,184 +46,50 @@ impl Position { current_price: f64, timestamp: DateTime, ) -> Self { - let unrealized_pnl = if size != 0.0 { - size * (current_price - entry_price) - } else { - 0.0 - }; - Self { symbol: symbol.to_string(), size, entry_price, current_price, - unrealized_pnl, realized_pnl: 0.0, funding_pnl: 0.0, timestamp, - leverage: 1.0, - liquidation_price: None, - margin: None, - metadata: HashMap::new(), } } - - /// Update the position with a new price + pub fn update_price(&mut self, price: f64) { self.current_price = price; - if self.size != 0.0 { - self.unrealized_pnl = self.size * (price - self.entry_price); - } } - - /// Apply a funding payment to the position + pub fn apply_funding_payment(&mut self, payment: f64) { self.funding_pnl += payment; } - - /// Get the total PnL (realized + unrealized + funding) - pub fn total_pnl(&self) -> f64 { - self.realized_pnl + self.unrealized_pnl + self.funding_pnl - } - - /// Get the position notional value - pub fn notional_value(&self) -> f64 { - self.size.abs() * self.current_price - } - - /// Check if the position is long - pub fn is_long(&self) -> bool { - self.size > 0.0 - } - - /// Check if the position is short - pub fn is_short(&self) -> bool { - self.size < 0.0 - } - - /// Check if the position is flat (no position) - pub fn is_flat(&self) -> bool { - self.size == 0.0 - } -} - -/// Order side (buy/sell) -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum OrderSide { - /// Buy order - Buy, - - /// Sell order - Sell, -} - -impl std::fmt::Display for OrderSide { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - OrderSide::Buy => write!(f, "Buy"), - OrderSide::Sell => write!(f, "Sell"), - } - } -} - -/// Order type -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum OrderType { - /// Market order - Market, - - /// Limit order - Limit, - - /// Stop market order - StopMarket, - - /// Stop limit order - StopLimit, - - /// Take profit market order - TakeProfitMarket, - - /// Take profit limit order - TakeProfitLimit, -} -impl std::fmt::Display for OrderType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - OrderType::Market => write!(f, "Market"), - OrderType::Limit => write!(f, "Limit"), - OrderType::StopMarket => write!(f, "StopMarket"), - OrderType::StopLimit => write!(f, "StopLimit"), - OrderType::TakeProfitMarket => write!(f, "TakeProfitMarket"), - OrderType::TakeProfitLimit => write!(f, "TakeProfitLimit"), - } + pub fn total_pnl(&self) -> f64 { + self.realized_pnl + self.unrealized_pnl() + self.funding_pnl } -} - -/// Time in force policy -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum TimeInForce { - /// Good till cancelled - GoodTillCancel, - - /// Immediate or cancel - ImmediateOrCancel, - - /// Fill or kill - FillOrKill, - - /// Good till date - GoodTillDate, -} -impl std::fmt::Display for TimeInForce { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - TimeInForce::GoodTillCancel => write!(f, "GoodTillCancel"), - TimeInForce::ImmediateOrCancel => write!(f, "ImmediateOrCancel"), - TimeInForce::FillOrKill => write!(f, "FillOrKill"), - TimeInForce::GoodTillDate => write!(f, "GoodTillDate"), - } + pub fn unrealized_pnl(&self) -> f64 { + self.size * (self.current_price - self.entry_price) } } -/// Order request across all trading modes -#[derive(Debug, Clone)] +/// Request to place an order on the exchange. +#[derive(Debug, Clone, PartialEq)] pub struct OrderRequest { - /// Symbol/ticker of the asset pub symbol: String, - - /// Order side (buy/sell) pub side: OrderSide, - - /// Order type (market/limit/etc) pub order_type: OrderType, - - /// Order quantity pub quantity: f64, - - /// Order price (for limit orders) pub price: Option, - - /// Whether this order reduces position only pub reduce_only: bool, - - /// Time in force policy pub time_in_force: TimeInForce, - - /// Stop price (for stop orders) pub stop_price: Option, - - /// Client order ID (if any) pub client_order_id: Option, - - /// Additional order parameters pub parameters: HashMap, } impl OrderRequest { - /// Create a new market order pub fn market(symbol: &str, side: OrderSide, quantity: f64) -> Self { Self { symbol: symbol.to_string(), @@ -258,8 +104,7 @@ impl OrderRequest { parameters: HashMap::new(), } } - - /// Create a new limit order + pub fn limit(symbol: &str, side: OrderSide, quantity: f64, price: f64) -> Self { Self { symbol: symbol.to_string(), @@ -274,528 +119,28 @@ impl OrderRequest { parameters: HashMap::new(), } } - - /// Set the order as reduce-only - pub fn reduce_only(mut self) -> Self { - self.reduce_only = true; - self - } - - /// Set the time in force policy - pub fn with_time_in_force(mut self, time_in_force: TimeInForce) -> Self { - self.time_in_force = time_in_force; - self - } - - /// Set the client order ID - pub fn with_client_order_id(mut self, client_order_id: &str) -> Self { - self.client_order_id = Some(client_order_id.to_string()); - self - } - - /// Add a parameter to the order - pub fn with_parameter(mut self, key: &str, value: &str) -> Self { - self.parameters.insert(key.to_string(), value.to_string()); - self - } - - /// Validate the order request - pub fn validate(&self) -> Result<(), String> { - // Check for positive quantity - if self.quantity <= 0.0 { - return Err("Order quantity must be positive".to_string()); - } - - // Check for price on limit orders - if matches!(self.order_type, OrderType::Limit | OrderType::StopLimit | OrderType::TakeProfitLimit) - && self.price.is_none() { - return Err(format!("Price is required for {} orders", self.order_type)); - } - - // Check for stop price on stop orders - if matches!(self.order_type, OrderType::StopMarket | OrderType::StopLimit) - && self.stop_price.is_none() { - return Err(format!("Stop price is required for {} orders", self.order_type)); - } - - // Check for take profit price on take profit orders - if matches!(self.order_type, OrderType::TakeProfitMarket | OrderType::TakeProfitLimit) - && self.stop_price.is_none() { - return Err(format!("Take profit price is required for {} orders", self.order_type)); - } - - Ok(()) - } } -/// Order status -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum OrderStatus { - /// Order created but not yet submitted - Created, - - /// Order submitted to exchange - Submitted, - - /// Order partially filled - PartiallyFilled, - - /// Order fully filled - Filled, - - /// Order cancelled - Cancelled, - - /// Order rejected - Rejected, - - /// Order expired - Expired, -} - -impl std::fmt::Display for OrderStatus { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - OrderStatus::Created => write!(f, "Created"), - OrderStatus::Submitted => write!(f, "Submitted"), - OrderStatus::PartiallyFilled => write!(f, "PartiallyFilled"), - OrderStatus::Filled => write!(f, "Filled"), - OrderStatus::Cancelled => write!(f, "Cancelled"), - OrderStatus::Rejected => write!(f, "Rejected"), - OrderStatus::Expired => write!(f, "Expired"), - } - } -} - -/// Order result across all trading modes -#[derive(Debug, Clone)] +/// Outcome of an order execution. +#[derive(Debug, Clone, PartialEq)] pub struct OrderResult { - /// Order ID pub order_id: String, - - /// Symbol/ticker of the asset pub symbol: String, - - /// Order side (buy/sell) pub side: OrderSide, - - /// Order type - pub order_type: OrderType, - - /// Requested quantity - pub requested_quantity: f64, - - /// Filled quantity - pub filled_quantity: f64, - - /// Average fill price - pub average_price: Option, - - /// Order status - pub status: OrderStatus, - - /// Order timestamp + pub quantity: f64, + pub price: f64, pub timestamp: DateTime, - - /// Fees paid - pub fees: Option, - - /// Error message (if any) - pub error: Option, - - /// Client order ID (if any) - pub client_order_id: Option, - - /// Additional order result data - pub metadata: HashMap, } impl OrderResult { - /// Create a new order result - pub fn new( - order_id: &str, - symbol: &str, - side: OrderSide, - order_type: OrderType, - requested_quantity: f64, - timestamp: DateTime, - ) -> Self { + pub fn new(order_id: &str, symbol: &str, side: OrderSide, quantity: f64, price: f64) -> Self { Self { order_id: order_id.to_string(), symbol: symbol.to_string(), side, - order_type, - requested_quantity, - filled_quantity: 0.0, - average_price: None, - status: OrderStatus::Created, - timestamp, - fees: None, - error: None, - client_order_id: None, - metadata: HashMap::new(), - } - } - - /// Check if the order is active - pub fn is_active(&self) -> bool { - matches!(self.status, OrderStatus::Created | OrderStatus::Submitted | OrderStatus::PartiallyFilled) - } - - /// Check if the order is complete - pub fn is_complete(&self) -> bool { - matches!(self.status, OrderStatus::Filled | OrderStatus::Cancelled | OrderStatus::Rejected | OrderStatus::Expired) - } - - /// Check if the order is filled (partially or fully) - pub fn is_filled(&self) -> bool { - matches!(self.status, OrderStatus::PartiallyFilled | OrderStatus::Filled) - } - - /// Get the fill percentage - pub fn fill_percentage(&self) -> f64 { - if self.requested_quantity > 0.0 { - self.filled_quantity / self.requested_quantity * 100.0 - } else { - 0.0 - } - } - - /// Get the notional value of the filled quantity - pub fn filled_notional(&self) -> Option { - self.average_price.map(|price| self.filled_quantity * price) - } -} - -/// Market data structure for real-time data -#[derive(Debug, Clone)] -pub struct MarketData { - /// Symbol/ticker of the asset - pub symbol: String, - - /// Last price - pub price: f64, - - /// Best bid price - pub bid: f64, - - /// Best ask price - pub ask: f64, - - /// Trading volume - pub volume: f64, - - /// Timestamp - pub timestamp: DateTime, - - /// Current funding rate (if available) - pub funding_rate: Option, - - /// Next funding time (if available) - pub next_funding_time: Option>, - - /// Open interest (if available) - pub open_interest: Option, - - /// Market depth (order book) - pub depth: Option, - - /// Recent trades - pub recent_trades: Option>, - - /// 24-hour price change percentage - pub price_change_24h_pct: Option, - - /// 24-hour high price - pub high_24h: Option, - - /// 24-hour low price - pub low_24h: Option, - - /// Additional market data - pub metadata: HashMap, -} - -impl MarketData { - /// Create a new market data instance with basic price information - pub fn new( - symbol: &str, - price: f64, - bid: f64, - ask: f64, - volume: f64, - timestamp: DateTime, - ) -> Self { - Self { - symbol: symbol.to_string(), + quantity, price, - bid, - ask, - volume, - timestamp, - funding_rate: None, - next_funding_time: None, - open_interest: None, - depth: None, - recent_trades: None, - price_change_24h_pct: None, - high_24h: None, - low_24h: None, - metadata: HashMap::new(), + timestamp: Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()), } } - - /// Get the mid price (average of bid and ask) - pub fn mid_price(&self) -> f64 { - (self.bid + self.ask) / 2.0 - } - - /// Get the spread (ask - bid) - pub fn spread(&self) -> f64 { - self.ask - self.bid - } - - /// Get the spread as a percentage of the mid price - pub fn spread_percentage(&self) -> f64 { - let mid = self.mid_price(); - if mid > 0.0 { - self.spread() / mid * 100.0 - } else { - 0.0 - } - } - - /// Add funding rate information - pub fn with_funding_rate( - mut self, - funding_rate: f64, - next_funding_time: DateTime, - ) -> Self { - self.funding_rate = Some(funding_rate); - self.next_funding_time = Some(next_funding_time); - self - } - - /// Add open interest information - pub fn with_open_interest(mut self, open_interest: f64) -> Self { - self.open_interest = Some(open_interest); - self - } - - /// Add 24-hour statistics - pub fn with_24h_stats( - mut self, - price_change_pct: f64, - high: f64, - low: f64, - ) -> Self { - self.price_change_24h_pct = Some(price_change_pct); - self.high_24h = Some(high); - self.low_24h = Some(low); - self - } - - /// Add a metadata field - pub fn with_metadata(mut self, key: &str, value: &str) -> Self { - self.metadata.insert(key.to_string(), value.to_string()); - self - } -} - -/// Order book level (price and quantity) -#[derive(Debug, Clone)] -pub struct OrderBookLevel { - /// Price level - pub price: f64, - - /// Quantity at this price level - pub quantity: f64, -} - -/// Order book snapshot -#[derive(Debug, Clone)] -pub struct OrderBookSnapshot { - /// Bid levels (sorted by price descending) - pub bids: Vec, - - /// Ask levels (sorted by price ascending) - pub asks: Vec, - - /// Timestamp of the snapshot - pub timestamp: DateTime, -} - -/// Trade information -#[derive(Debug, Clone)] -pub struct Trade { - /// Trade ID - pub id: String, - - /// Trade price - pub price: f64, - - /// Trade quantity - pub quantity: f64, - - /// Trade timestamp - pub timestamp: DateTime, - - /// Trade side (buy/sell) - pub side: Option, -} - -/// Trading signal direction -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SignalDirection { - /// Buy signal - Buy, - - /// Sell signal - Sell, - - /// Hold/neutral signal - Neutral, - - /// Close position signal - Close, -} - -/// Trading signal -#[derive(Debug, Clone)] -pub struct Signal { - /// Symbol/ticker of the asset - pub symbol: String, - - /// Signal direction - pub direction: SignalDirection, - - /// Signal strength (0.0 to 1.0) - pub strength: f64, - - /// Signal timestamp - pub timestamp: DateTime, - - /// Signal metadata - pub metadata: HashMap, -} - -/// Trading strategy trait for unified strategy execution across all modes -pub trait TradingStrategy: Send + Sync { - /// Get the strategy name - fn name(&self) -> &str; - - /// Process market data and generate signals - fn on_market_data(&mut self, data: &MarketData) -> Result, String>; - - /// Process order fill events - fn on_order_fill(&mut self, fill: &OrderResult) -> Result<(), String>; - - /// Process funding payment events - fn on_funding_payment(&mut self, payment: &FundingPayment) -> Result<(), String>; - - /// Get current strategy signals - fn get_current_signals(&self) -> HashMap; } - -/// Trading configuration -#[derive(Debug, Clone)] -pub struct TradingConfig { - /// Initial balance for trading - pub initial_balance: f64, - - /// Risk management configuration - pub risk_config: Option, - - /// Slippage configuration for paper trading - pub slippage_config: Option, - - /// API configuration for live trading - pub api_config: Option, - - /// Additional mode-specific configuration parameters - pub parameters: HashMap, -} - -/// Risk management configuration -#[derive(Debug, Clone)] -pub struct RiskConfig { - /// Maximum position size as a percentage of portfolio value - pub max_position_size_pct: f64, - - /// Maximum daily loss as a percentage of portfolio value - pub max_daily_loss_pct: f64, - - /// Stop loss percentage for positions - pub stop_loss_pct: f64, - - /// Take profit percentage for positions - pub take_profit_pct: f64, - - /// Maximum leverage allowed - pub max_leverage: f64, - - /// Maximum number of concurrent positions - pub max_positions: usize, - - /// Maximum drawdown percentage before stopping trading - pub max_drawdown_pct: f64, - - /// Whether to use trailing stop loss - pub use_trailing_stop: bool, - - /// Trailing stop distance percentage - pub trailing_stop_distance_pct: Option, -} - -/// Slippage simulation configuration for paper trading -#[derive(Debug, Clone)] -pub struct SlippageConfig { - /// Base slippage as a percentage - pub base_slippage_pct: f64, - - /// Volume-based slippage factor - pub volume_impact_factor: f64, - - /// Volatility-based slippage factor - pub volatility_impact_factor: f64, - - /// Random slippage component maximum (percentage) - pub random_slippage_max_pct: f64, - - /// Simulated latency in milliseconds - pub simulated_latency_ms: u64, - - /// Whether to use order book for slippage calculation - pub use_order_book: bool, - - /// Maximum slippage percentage allowed - pub max_slippage_pct: f64, -} - -/// API configuration for live trading -#[derive(Debug, Clone)] -pub struct ApiConfig { - /// API key for authentication - pub api_key: String, - - /// API secret for authentication - pub api_secret: String, - - /// API endpoint URL - pub endpoint: String, - - /// Whether to use testnet - pub use_testnet: bool, - - /// Timeout for API requests in milliseconds - pub timeout_ms: u64, - - /// Rate limit (requests per second) - pub rate_limit: Option, - - /// Retry attempts for failed requests - pub retry_attempts: u32, - - /// Retry delay in milliseconds - pub retry_delay_ms: u64, -} \ No newline at end of file