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:
zastian-dev
2026-02-13 19:20:01 +00:00
parent edc655ca2c
commit 79816b9e2e
7 changed files with 757 additions and 13 deletions

View File

@@ -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)?;