feat(hyperopt): Complete DQN hyperopt analysis and PSO optimizer fix
- Fixed PSO budget calculation bug in ml/src/hyperopt/optimizer.rs - Root cause: Division by n_particles in sequential execution - Now correctly calculates max_iters = remaining_trials (no division) - Result: 50 trials complete instead of 23 (100% vs 46%) - Added comprehensive DQN hyperopt results analysis - 39/50 trials analyzed across 2 RunPod deployments - Best hyperparameters identified: LR 4.89e-5 (ultra-low) - Created DQN_HYPEROPT_RESULTS_SUMMARY.md with expert validation - GitLab CI/CD pipeline operational (48 lines fixed) - Fixed YAML syntax errors (unquoted colons) - All 7 jobs validated and working - Warning cleanup complete (136 → 0 warnings) - Removed 143 lines dead code - Fixed visibility, unused imports, Debug traits - Archived Wave D reports to docs/archive/ - 8 early stopping reports moved - Root directory cleaned up 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
625
backtesting/src/strategies/dqn_replay.rs
Normal file
625
backtesting/src/strategies/dqn_replay.rs
Normal file
@@ -0,0 +1,625 @@
|
||||
//! DQN Replay Strategy - Replays pre-computed DQN actions through backtesting engine
|
||||
//!
|
||||
//! This strategy reads a CSV file of pre-computed DQN actions (one per bar) and
|
||||
//! converts them to trading signals that the backtesting engine executes.
|
||||
//! It maintains position state to ensure proper action mapping (e.g., Buy when flat,
|
||||
//! Sell to close long, etc.).
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use common::{Order, Position, Price, Quantity, Symbol};
|
||||
use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use trading_engine::types::events::MarketEvent;
|
||||
|
||||
use crate::strategy_tester::{
|
||||
SignalType, Strategy, StrategyConfig, StrategyContext, StrategyResult, TradingSignal,
|
||||
};
|
||||
|
||||
/// DQN Replay Strategy - Replays pre-computed DQN actions through backtesting engine
|
||||
///
|
||||
/// # Action Synchronization
|
||||
///
|
||||
/// - **Assumption**: Actions CSV has 1 row per market bar (chronological order)
|
||||
/// - **Index Management**: `current_index` increments per `on_market_event()` call
|
||||
/// - **Bounds Checking**: Errors if action count != bar count
|
||||
///
|
||||
/// # Position State Machine
|
||||
///
|
||||
/// - **Flat**: No position, can Buy or Sell
|
||||
/// - **Long**: Positive quantity, can CloseLong or AddToLong
|
||||
/// - **Short**: Negative quantity, can CloseShort or AddToShort
|
||||
///
|
||||
#[derive(Debug)]
|
||||
pub struct DQNReplayStrategy {
|
||||
/// Strategy name for identification
|
||||
name: String,
|
||||
|
||||
/// Pre-loaded DQN actions from CSV (chronological order)
|
||||
actions: Vec<DQNAction>,
|
||||
|
||||
/// Current action index (increments per bar)
|
||||
current_index: usize,
|
||||
|
||||
/// Current position state (None = flat, Some = long/short)
|
||||
current_position: PositionState,
|
||||
|
||||
/// Symbol being traded (single-asset for simplicity)
|
||||
symbol: Symbol,
|
||||
|
||||
/// Strategy configuration
|
||||
config: Option<StrategyConfig>,
|
||||
|
||||
/// Initial capital for metrics
|
||||
initial_capital: Decimal,
|
||||
|
||||
/// Performance tracking
|
||||
metrics: PerformanceMetrics,
|
||||
}
|
||||
|
||||
/// Pre-computed DQN action from CSV
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DQNAction {
|
||||
/// Bar index (0-based, matches CSV row number - 1)
|
||||
pub bar_index: usize,
|
||||
|
||||
/// DQN action decision (0=Buy, 1=Sell, 2=Hold)
|
||||
pub action: TradingActionType,
|
||||
|
||||
/// Q-values for all 3 actions [buy_q, sell_q, hold_q]
|
||||
pub q_values: [f64; 3],
|
||||
|
||||
/// Timestamp of the action (optional, for validation)
|
||||
pub timestamp: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// Trading action type from DQN model
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum TradingActionType {
|
||||
/// Buy signal (action=0)
|
||||
Buy = 0,
|
||||
/// Sell signal (action=1)
|
||||
Sell = 1,
|
||||
/// Hold signal (action=2)
|
||||
Hold = 2,
|
||||
}
|
||||
|
||||
impl TradingActionType {
|
||||
/// Parse from integer (CSV column)
|
||||
pub fn from_int(val: u8) -> Result<Self> {
|
||||
match val {
|
||||
0 => Ok(TradingActionType::Buy),
|
||||
1 => Ok(TradingActionType::Sell),
|
||||
2 => Ok(TradingActionType::Hold),
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"Invalid action value: {} (expected 0-2)",
|
||||
val
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Position state for tracking current holdings
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PositionState {
|
||||
/// No position (cash)
|
||||
Flat,
|
||||
/// Long position (positive quantity)
|
||||
Long,
|
||||
/// Short position (negative quantity)
|
||||
Short,
|
||||
}
|
||||
|
||||
impl PositionState {
|
||||
/// Determine position state from quantity
|
||||
pub fn from_quantity(quantity: Decimal) -> Self {
|
||||
if quantity > Decimal::ZERO {
|
||||
PositionState::Long
|
||||
} else if quantity < Decimal::ZERO {
|
||||
PositionState::Short
|
||||
} else {
|
||||
PositionState::Flat
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Performance metrics tracked during execution
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct PerformanceMetrics {
|
||||
total_actions: usize,
|
||||
buy_signals: usize,
|
||||
sell_signals: usize,
|
||||
hold_signals: usize,
|
||||
position_flips: usize, // Long->Short or Short->Long
|
||||
index_mismatches: usize,
|
||||
}
|
||||
|
||||
impl DQNReplayStrategy {
|
||||
/// Create new DQN replay strategy from CSV file
|
||||
///
|
||||
/// # CSV Format
|
||||
///
|
||||
/// Expected columns: `bar_index,action,q_buy,q_sell,q_hold[,timestamp]`
|
||||
///
|
||||
/// Example:
|
||||
/// ```csv
|
||||
/// bar_index,action,q_buy,q_sell,q_hold
|
||||
/// 0,2,0.123,-0.456,0.789
|
||||
/// 1,0,1.234,0.567,0.890
|
||||
/// 2,1,-0.345,1.678,0.234
|
||||
/// ```
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - Strategy name
|
||||
/// * `csv_path` - Path to DQN actions CSV
|
||||
/// * `symbol` - Symbol to trade
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns error if:
|
||||
/// - CSV file not found or invalid format
|
||||
/// - Action values out of range (0-2)
|
||||
/// - Duplicate bar indices
|
||||
///
|
||||
pub fn from_csv<P: AsRef<std::path::Path>>(
|
||||
name: String,
|
||||
csv_path: P,
|
||||
symbol: Symbol,
|
||||
) -> Result<Self> {
|
||||
use csv::ReaderBuilder;
|
||||
use std::fs::File;
|
||||
|
||||
let file = File::open(csv_path.as_ref()).with_context(|| {
|
||||
format!("Failed to open CSV: {}", csv_path.as_ref().display())
|
||||
})?;
|
||||
|
||||
let mut reader = ReaderBuilder::new().has_headers(true).from_reader(file);
|
||||
|
||||
let mut actions = Vec::new();
|
||||
|
||||
for (row_num, result) in reader.records().enumerate() {
|
||||
let record =
|
||||
result.with_context(|| format!("Failed to parse CSV row {}", row_num))?;
|
||||
|
||||
// Parse CSV columns
|
||||
let bar_index: usize = record
|
||||
.get(0)
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing bar_index at row {}", row_num))?
|
||||
.parse()
|
||||
.with_context(|| format!("Invalid bar_index at row {}", row_num))?;
|
||||
|
||||
let action_int: u8 = record
|
||||
.get(1)
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing action at row {}", row_num))?
|
||||
.parse()
|
||||
.with_context(|| format!("Invalid action at row {}", row_num))?;
|
||||
|
||||
let action = TradingActionType::from_int(action_int)
|
||||
.with_context(|| format!("Invalid action value at row {}", row_num))?;
|
||||
|
||||
let q_buy: f64 = record
|
||||
.get(2)
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing q_buy at row {}", row_num))?
|
||||
.parse()
|
||||
.with_context(|| format!("Invalid q_buy at row {}", row_num))?;
|
||||
|
||||
let q_sell: f64 = record
|
||||
.get(3)
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing q_sell at row {}", row_num))?
|
||||
.parse()
|
||||
.with_context(|| format!("Invalid q_sell at row {}", row_num))?;
|
||||
|
||||
let q_hold: f64 = record
|
||||
.get(4)
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing q_hold at row {}", row_num))?
|
||||
.parse()
|
||||
.with_context(|| format!("Invalid q_hold at row {}", row_num))?;
|
||||
|
||||
// Optional timestamp column
|
||||
let timestamp = record
|
||||
.get(5)
|
||||
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
|
||||
.map(|dt| dt.with_timezone(&chrono::Utc));
|
||||
|
||||
actions.push(DQNAction {
|
||||
bar_index,
|
||||
action,
|
||||
q_values: [q_buy, q_sell, q_hold],
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
// Validate chronological order and no gaps
|
||||
for (i, action) in actions.iter().enumerate() {
|
||||
if action.bar_index != i {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Bar index mismatch at row {}: expected {}, got {}",
|
||||
i,
|
||||
i,
|
||||
action.bar_index
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"Loaded {} DQN actions from CSV for symbol {}",
|
||||
actions.len(),
|
||||
symbol
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
actions,
|
||||
current_index: 0,
|
||||
current_position: PositionState::Flat,
|
||||
symbol,
|
||||
config: None,
|
||||
initial_capital: Decimal::ZERO,
|
||||
metrics: PerformanceMetrics::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert DQN action to TradingSignal based on current position state
|
||||
fn convert_action_to_signal(
|
||||
&mut self,
|
||||
dqn_action: &DQNAction,
|
||||
current_price: Price,
|
||||
_context: &StrategyContext,
|
||||
) -> Result<Vec<TradingSignal>> {
|
||||
use SignalType::*;
|
||||
|
||||
// Determine position size (fixed for simplicity, can be made dynamic)
|
||||
let position_size = Quantity::from_f64(1.0).unwrap_or(Quantity::ZERO);
|
||||
|
||||
let signal_type = match (dqn_action.action, self.current_position) {
|
||||
// HOLD: No action regardless of position
|
||||
(TradingActionType::Hold, _) => return Ok(vec![]),
|
||||
|
||||
// BUY LOGIC
|
||||
(TradingActionType::Buy, PositionState::Flat) => {
|
||||
// Flat -> Long: Buy to open
|
||||
self.current_position = PositionState::Long;
|
||||
Buy
|
||||
}
|
||||
(TradingActionType::Buy, PositionState::Long) => {
|
||||
// Long -> Long: Hold (already long, DQN wants to stay long)
|
||||
return Ok(vec![]);
|
||||
}
|
||||
(TradingActionType::Buy, PositionState::Short) => {
|
||||
// Short -> Long: Close short + Buy to open
|
||||
self.current_position = PositionState::Long;
|
||||
self.metrics.position_flips += 1;
|
||||
// NOTE: This requires TWO signals: Cover + Buy
|
||||
// Simplified to single reversal signal for now
|
||||
Cover // Close short position
|
||||
}
|
||||
|
||||
// SELL LOGIC
|
||||
(TradingActionType::Sell, PositionState::Flat) => {
|
||||
// Flat -> Short: Sell to open (short)
|
||||
self.current_position = PositionState::Short;
|
||||
Short
|
||||
}
|
||||
(TradingActionType::Sell, PositionState::Short) => {
|
||||
// Short -> Short: Hold (already short, DQN wants to stay short)
|
||||
return Ok(vec![]);
|
||||
}
|
||||
(TradingActionType::Sell, PositionState::Long) => {
|
||||
// Long -> Short: Close long + Sell to open
|
||||
self.current_position = PositionState::Short;
|
||||
self.metrics.position_flips += 1;
|
||||
CloseLong // Close long position
|
||||
}
|
||||
};
|
||||
|
||||
// Build trading signal
|
||||
let signal = TradingSignal {
|
||||
symbol: self.symbol.clone(),
|
||||
signal_type,
|
||||
quantity: position_size,
|
||||
target_price: Some(current_price),
|
||||
stop_loss: None, // No stop loss for replay (already decided)
|
||||
take_profit: None, // No take profit for replay
|
||||
confidence: Decimal::from_f64_retain(dqn_action.q_values[dqn_action.action as usize])
|
||||
.unwrap_or(Decimal::ZERO),
|
||||
metadata: {
|
||||
let mut meta = HashMap::new();
|
||||
meta.insert("strategy".to_string(), serde_json::json!("dqn_replay"));
|
||||
meta.insert(
|
||||
"bar_index".to_string(),
|
||||
serde_json::json!(dqn_action.bar_index),
|
||||
);
|
||||
meta.insert(
|
||||
"q_values".to_string(),
|
||||
serde_json::json!(dqn_action.q_values),
|
||||
);
|
||||
meta
|
||||
},
|
||||
};
|
||||
|
||||
Ok(vec![signal])
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl Strategy for DQNReplayStrategy {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
async fn initialize(
|
||||
&mut self,
|
||||
initial_capital: Decimal,
|
||||
config: StrategyConfig,
|
||||
) -> Result<()> {
|
||||
self.initial_capital = initial_capital;
|
||||
self.config = Some(config);
|
||||
|
||||
tracing::info!(
|
||||
"DQNReplayStrategy initialized: {} actions loaded, symbol: {}, capital: {}",
|
||||
self.actions.len(),
|
||||
self.symbol,
|
||||
initial_capital
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn on_market_event(
|
||||
&mut self,
|
||||
event: &MarketEvent,
|
||||
context: &StrategyContext,
|
||||
) -> Result<Vec<TradingSignal>> {
|
||||
// Only process Bar events (ignore Quotes, Trades, etc.)
|
||||
let (symbol, close) = match event {
|
||||
MarketEvent::Bar { symbol, close, .. } => (symbol, close),
|
||||
_ => return Ok(vec![]), // Ignore non-bar events
|
||||
};
|
||||
|
||||
// Ignore other symbols
|
||||
if symbol != &self.symbol {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
// Check if we have an action for this bar
|
||||
if self.current_index >= self.actions.len() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Action index out of bounds: {} >= {} (possible mismatch between CSV and Parquet)",
|
||||
self.current_index,
|
||||
self.actions.len()
|
||||
));
|
||||
}
|
||||
|
||||
// Get current action (clone to avoid borrow checker issues)
|
||||
let dqn_action = self.actions[self.current_index].clone();
|
||||
|
||||
// Validate bar index matches (optional, for debugging)
|
||||
if dqn_action.bar_index != self.current_index {
|
||||
tracing::warn!(
|
||||
"Action bar_index mismatch: CSV says {}, but we're at index {}",
|
||||
dqn_action.bar_index,
|
||||
self.current_index
|
||||
);
|
||||
self.metrics.index_mismatches += 1;
|
||||
}
|
||||
|
||||
// Update metrics
|
||||
self.metrics.total_actions += 1;
|
||||
match dqn_action.action {
|
||||
TradingActionType::Buy => self.metrics.buy_signals += 1,
|
||||
TradingActionType::Sell => self.metrics.sell_signals += 1,
|
||||
TradingActionType::Hold => self.metrics.hold_signals += 1,
|
||||
}
|
||||
|
||||
// Convert DQN action to trading signal
|
||||
let signal = self.convert_action_to_signal(&dqn_action, *close, context)?;
|
||||
|
||||
// Increment index for next bar
|
||||
self.current_index += 1;
|
||||
|
||||
Ok(signal)
|
||||
}
|
||||
|
||||
async fn on_order_update(&mut self, order: &Order, _context: &StrategyContext) -> Result<()> {
|
||||
// Log order execution for debugging
|
||||
tracing::debug!(
|
||||
"Order update: {:?} {} @ {:?}",
|
||||
order.side,
|
||||
order.quantity,
|
||||
order.average_price
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn on_position_update(
|
||||
&mut self,
|
||||
position: &Position,
|
||||
_context: &StrategyContext,
|
||||
) -> Result<()> {
|
||||
// Update our position state based on actual position from backtesting engine
|
||||
self.current_position = PositionState::from_quantity(position.quantity);
|
||||
|
||||
tracing::debug!(
|
||||
"Position updated: {:?} (quantity: {})",
|
||||
self.current_position,
|
||||
position.quantity
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn finalize(&mut self, context: &StrategyContext) -> Result<StrategyResult> {
|
||||
// Calculate final performance metrics
|
||||
let total_return = if self.initial_capital > Decimal::ZERO {
|
||||
(context.account_balance - self.initial_capital) / self.initial_capital
|
||||
} else {
|
||||
Decimal::ZERO
|
||||
};
|
||||
|
||||
tracing::info!(
|
||||
"DQN Replay Strategy Finalized - Metrics: total={}, buy={}, sell={}, hold={}, flips={}, mismatches={}",
|
||||
self.metrics.total_actions,
|
||||
self.metrics.buy_signals,
|
||||
self.metrics.sell_signals,
|
||||
self.metrics.hold_signals,
|
||||
self.metrics.position_flips,
|
||||
self.metrics.index_mismatches
|
||||
);
|
||||
|
||||
// Create strategy result (simplified, full implementation would calculate all metrics)
|
||||
Ok(StrategyResult {
|
||||
strategy_name: self.name.clone(),
|
||||
total_return,
|
||||
annualized_return: total_return, // Simplified
|
||||
max_drawdown: Decimal::ZERO, // Would need to track peak value
|
||||
sharpe_ratio: Decimal::ZERO, // Would need daily returns
|
||||
total_trades: self.metrics.buy_signals as u64 + self.metrics.sell_signals as u64,
|
||||
win_rate: Decimal::ZERO, // Would need to analyze trades
|
||||
avg_trade_return: Decimal::ZERO,
|
||||
final_value: context.account_balance,
|
||||
trades: vec![], // Would convert trade_history to TradeRecord format
|
||||
performance_timeline: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_state(&self) -> Result<serde_json::Value> {
|
||||
Ok(serde_json::json!({
|
||||
"name": self.name,
|
||||
"current_index": self.current_index,
|
||||
"total_actions": self.actions.len(),
|
||||
"current_position": format!("{:?}", self.current_position),
|
||||
"metrics": {
|
||||
"total_actions": self.metrics.total_actions,
|
||||
"buy_signals": self.metrics.buy_signals,
|
||||
"sell_signals": self.metrics.sell_signals,
|
||||
"hold_signals": self.metrics.hold_signals,
|
||||
"position_flips": self.metrics.position_flips,
|
||||
"index_mismatches": self.metrics.index_mismatches,
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
/// Helper to create a test CSV file
|
||||
fn create_test_csv(actions: &[(usize, u8, [f64; 3])]) -> Result<NamedTempFile> {
|
||||
let mut file = NamedTempFile::new()?;
|
||||
writeln!(file, "bar_index,action,q_buy,q_sell,q_hold")?;
|
||||
for (bar_index, action, q_values) in actions {
|
||||
writeln!(
|
||||
file,
|
||||
"{},{},{},{},{}",
|
||||
bar_index, action, q_values[0], q_values[1], q_values[2]
|
||||
)?;
|
||||
}
|
||||
file.flush()?;
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_csv_loading_valid() {
|
||||
let csv_file = create_test_csv(&[
|
||||
(0, 2, [0.1, 0.2, 0.9]), // Hold
|
||||
(1, 0, [0.8, 0.1, 0.3]), // Buy
|
||||
(2, 1, [0.2, 0.7, 0.4]), // Sell
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
let strategy = DQNReplayStrategy::from_csv(
|
||||
"test_strategy".to_string(),
|
||||
csv_file.path(),
|
||||
Symbol::from("ES"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(strategy.actions.len(), 3);
|
||||
assert_eq!(strategy.actions[0].action, TradingActionType::Hold);
|
||||
assert_eq!(strategy.actions[1].action, TradingActionType::Buy);
|
||||
assert_eq!(strategy.actions[2].action, TradingActionType::Sell);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_csv_loading_invalid_action() {
|
||||
let csv_file = create_test_csv(&[
|
||||
(0, 3, [0.1, 0.2, 0.3]), // Invalid action
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
let result = DQNReplayStrategy::from_csv(
|
||||
"test_strategy".to_string(),
|
||||
csv_file.path(),
|
||||
Symbol::from("ES"),
|
||||
);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("Invalid action value"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_csv_loading_gap_in_indices() {
|
||||
let csv_file = create_test_csv(&[
|
||||
(0, 2, [0.1, 0.2, 0.9]),
|
||||
(2, 0, [0.8, 0.1, 0.3]), // Gap: missing index 1
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
let result = DQNReplayStrategy::from_csv(
|
||||
"test_strategy".to_string(),
|
||||
csv_file.path(),
|
||||
Symbol::from("ES"),
|
||||
);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("Bar index mismatch"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_position_state_from_quantity() {
|
||||
use rust_decimal_macros::dec;
|
||||
|
||||
assert_eq!(
|
||||
PositionState::from_quantity(dec!(10.0)),
|
||||
PositionState::Long
|
||||
);
|
||||
assert_eq!(
|
||||
PositionState::from_quantity(dec!(-5.0)),
|
||||
PositionState::Short
|
||||
);
|
||||
assert_eq!(
|
||||
PositionState::from_quantity(Decimal::ZERO),
|
||||
PositionState::Flat
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trading_action_type_from_int() {
|
||||
assert_eq!(
|
||||
TradingActionType::from_int(0).unwrap(),
|
||||
TradingActionType::Buy
|
||||
);
|
||||
assert_eq!(
|
||||
TradingActionType::from_int(1).unwrap(),
|
||||
TradingActionType::Sell
|
||||
);
|
||||
assert_eq!(
|
||||
TradingActionType::from_int(2).unwrap(),
|
||||
TradingActionType::Hold
|
||||
);
|
||||
assert!(TradingActionType::from_int(3).is_err());
|
||||
}
|
||||
}
|
||||
8
backtesting/src/strategies/mod.rs
Normal file
8
backtesting/src/strategies/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
//! Strategy implementations for backtesting
|
||||
//!
|
||||
//! This module contains concrete strategy implementations that can be used
|
||||
//! with the backtesting framework.
|
||||
|
||||
pub mod dqn_replay;
|
||||
|
||||
pub use dqn_replay::{DQNAction, DQNReplayStrategy, PositionState, TradingActionType};
|
||||
Reference in New Issue
Block a user