Experiment with hourly timeframe-specific stops
- Added HOURLY_ATR_STOP_MULTIPLIER (1.8x) vs daily (3.5x) - Added hourly-specific trail multipliers - Strategy now uses timeframe field to select appropriate stops - Tested multiple configurations on hourly: * 3.5x stops: -0.5% return, 45% max DD * 1.8x stops: -45% return, 53% max DD (worse) * Conservative regime (0.25x): -65% return, 67% max DD (terrible) - Conclusion: Hourly doesn't work with this strategy - Daily with relaxed regime remains best: +17.4% over 5yr, 24% max DD Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
50
src/main.rs
50
src/main.rs
@@ -50,7 +50,8 @@ use crate::config::{Timeframe, DEFAULT_INITIAL_CAPITAL};
|
||||
Backtest 6 months: invest-bot --backtest --months 6\n \
|
||||
Backtest 1y 6m: invest-bot --backtest --years 1 --months 6\n \
|
||||
Custom capital: invest-bot --backtest --years 5 --capital 50000\n \
|
||||
Hourly backtest: invest-bot --backtest --years 1 --timeframe hourly"
|
||||
Hourly backtest: invest-bot --backtest --years 1 --timeframe hourly\n \
|
||||
Custom date range: invest-bot --backtest --start-date 2007-01-01 --end-date 2008-12-31"
|
||||
)]
|
||||
struct Args {
|
||||
/// Run in backtest mode instead of live trading
|
||||
@@ -65,6 +66,14 @@ struct Args {
|
||||
#[arg(short, long, default_value_t = 0.0)]
|
||||
months: f64,
|
||||
|
||||
/// Start date for backtest (YYYY-MM-DD). Overrides --years/--months if provided.
|
||||
#[arg(long, value_name = "YYYY-MM-DD")]
|
||||
start_date: Option<String>,
|
||||
|
||||
/// End date for backtest (YYYY-MM-DD). Defaults to now if not provided.
|
||||
#[arg(long, value_name = "YYYY-MM-DD")]
|
||||
end_date: Option<String>,
|
||||
|
||||
/// Initial capital for backtesting
|
||||
#[arg(short, long, default_value_t = DEFAULT_INITIAL_CAPITAL)]
|
||||
capital: f64,
|
||||
@@ -171,14 +180,45 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
|
||||
async fn run_backtest(api_key: String, api_secret: String, args: Args) -> Result<()> {
|
||||
// Combine years and months (default to 1 year if neither specified)
|
||||
let total_years = args.years + (args.months / 12.0);
|
||||
let total_years = if total_years <= 0.0 { 1.0 } else { total_years };
|
||||
use chrono::NaiveDate;
|
||||
|
||||
let client = AlpacaClient::new(api_key, api_secret)?;
|
||||
let mut backtester = Backtester::new(args.capital, args.timeframe);
|
||||
|
||||
let result = backtester.run(&client, total_years).await?;
|
||||
let result = if args.start_date.is_some() || args.end_date.is_some() {
|
||||
// Custom date range mode
|
||||
let start_date = if let Some(ref s) = args.start_date {
|
||||
NaiveDate::parse_from_str(s, "%Y-%m-%d")
|
||||
.context("Invalid start date format. Use YYYY-MM-DD (e.g., 2007-01-01)")?
|
||||
} else {
|
||||
// If no start date provided, default to 1 year before end date
|
||||
let end = if let Some(ref e) = args.end_date {
|
||||
NaiveDate::parse_from_str(e, "%Y-%m-%d")?
|
||||
} else {
|
||||
chrono::Utc::now().date_naive()
|
||||
};
|
||||
end - chrono::Duration::days(365)
|
||||
};
|
||||
|
||||
let end_date = if let Some(ref e) = args.end_date {
|
||||
NaiveDate::parse_from_str(e, "%Y-%m-%d")
|
||||
.context("Invalid end date format. Use YYYY-MM-DD (e.g., 2008-12-31)")?
|
||||
} else {
|
||||
chrono::Utc::now().date_naive()
|
||||
};
|
||||
|
||||
// Validate date range
|
||||
if start_date >= end_date {
|
||||
anyhow::bail!("Start date must be before end date");
|
||||
}
|
||||
|
||||
backtester.run_with_dates(&client, start_date, end_date).await?
|
||||
} else {
|
||||
// Years/months mode (existing behavior)
|
||||
let total_years = args.years + (args.months / 12.0);
|
||||
let total_years = if total_years <= 0.0 { 1.0 } else { total_years };
|
||||
backtester.run(&client, total_years).await?
|
||||
};
|
||||
|
||||
// Save results to CSV
|
||||
save_backtest_results(&result)?;
|
||||
|
||||
Reference in New Issue
Block a user