diff --git a/.direnv/bin/nix-direnv-reload b/.direnv/bin/nix-direnv-reload index c3bfc7f..ac752dd 100755 --- a/.direnv/bin/nix-direnv-reload +++ b/.direnv/bin/nix-direnv-reload @@ -1,19 +1,19 @@ #!/usr/bin/env bash set -e -if [[ ! -d "/home/work/Documents/rust/invest-bot" ]]; then +if [[ ! -d "/home/mrfluffy/Documents/projects/rust/vibe-invest" ]]; then echo "Cannot find source directory; Did you move it?" - echo "(Looking for "/home/work/Documents/rust/invest-bot")" + echo "(Looking for "/home/mrfluffy/Documents/projects/rust/vibe-invest")" echo 'Cannot force reload with this script - use "direnv reload" manually and then try again' exit 1 fi # rebuild the cache forcefully -_nix_direnv_force_reload=1 direnv exec "/home/work/Documents/rust/invest-bot" true +_nix_direnv_force_reload=1 direnv exec "/home/mrfluffy/Documents/projects/rust/vibe-invest" true # Update the mtime for .envrc. # This will cause direnv to reload again - but without re-building. -touch "/home/work/Documents/rust/invest-bot/.envrc" +touch "/home/mrfluffy/Documents/projects/rust/vibe-invest/.envrc" # Also update the timestamp of whatever profile_rc we have. # This makes sure that we know we are up to date. -touch -r "/home/work/Documents/rust/invest-bot/.envrc" "/home/work/Documents/rust/invest-bot/.direnv"/*.rc +touch -r "/home/mrfluffy/Documents/projects/rust/vibe-invest/.envrc" "/home/mrfluffy/Documents/projects/rust/vibe-invest/.direnv"/*.rc diff --git a/analyze_regime.py b/analyze_regime.py deleted file mode 100644 index 6bdf3ae..0000000 --- a/analyze_regime.py +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env python3 -"""Analyze SPY regime detection during backtest periods.""" - -import pandas as pd -import yfinance as yf -from datetime import datetime, timedelta - -def calculate_ema(series, period): - """Calculate EMA using pandas.""" - return series.ewm(span=period, adjust=False).mean() - -def determine_regime(price, ema50, ema200): - """Replicate the Rust regime detection logic.""" - if pd.isna(price) or pd.isna(ema50) or pd.isna(ema200): - return "Caution" - - # Bear: price below 200 EMA AND 50 EMA below 200 EMA - if price < ema200 and ema50 < ema200: - return "Bear" - - # Caution: price below 50 EMA - if price < ema50: - return "Caution" - - # Bull: price above both, 50 above 200 - if ema50 > ema200: - return "Bull" - - # Edge case: price above both but 50 still below 200 - return "Caution" - -def analyze_period(start_date, end_date, period_name): - """Analyze SPY regime for a given period.""" - print(f"\n{'='*70}") - print(f"{period_name}: {start_date} to {end_date}") - print('='*70) - - # Fetch SPY data with extra warmup for EMA-200 - warmup_start = (datetime.strptime(start_date, '%Y-%m-%d') - timedelta(days=400)).strftime('%Y-%m-%d') - spy = yf.download('SPY', start=warmup_start, end=end_date, progress=False) - - if spy.empty: - print(f"ERROR: No SPY data available for {period_name}") - return - - # Calculate EMAs - spy['EMA50'] = calculate_ema(spy['Close'], 50) - spy['EMA200'] = calculate_ema(spy['Close'], 200) - - # Determine regime for each day - spy['Regime'] = spy.apply( - lambda row: determine_regime(row['Close'], row['EMA50'], row['EMA200']), - axis=1 - ) - - # Filter to actual trading period - trading_period = spy[start_date:end_date].copy() - - if trading_period.empty: - print(f"ERROR: No trading data for {period_name}") - return - - # Calculate SPY return - spy_start = trading_period['Close'].iloc[0] - spy_end = trading_period['Close'].iloc[-1] - spy_return = (spy_end - spy_start) / spy_start * 100 - - print(f"\nSPY Performance:") - print(f" Start: ${spy_start:.2f}") - print(f" End: ${spy_end:.2f}") - print(f" Return: {spy_return:+.2f}%") - - # Count regime days - regime_counts = trading_period['Regime'].value_counts() - total_days = len(trading_period) - - print(f"\nRegime Distribution ({total_days} trading days):") - for regime in ['Bull', 'Caution', 'Bear']: - count = regime_counts.get(regime, 0) - pct = count / total_days * 100 - print(f" {regime:8s}: {count:4d} days ({pct:5.1f}%)") - - # Show regime transitions - regime_changes = trading_period[trading_period['Regime'] != trading_period['Regime'].shift(1)] - if len(regime_changes) > 0: - print(f"\nRegime Transitions ({len(regime_changes)} total):") - for date, row in regime_changes.head(20).iterrows(): - print(f" {date.strftime('%Y-%m-%d')}: {row['Regime']:8s} (SPY: ${row['Close']:.2f}, " - f"EMA50: ${row['EMA50']:.2f}, EMA200: ${row['EMA200']:.2f})") - if len(regime_changes) > 20: - print(f" ... and {len(regime_changes) - 20} more transitions") - - # Identify problematic Bear periods during bull markets - bear_days = trading_period[trading_period['Regime'] == 'Bear'] - if len(bear_days) > 0: - print(f"\n⚠️ WARNING: {len(bear_days)} days classified as BEAR:") - for date, row in bear_days.head(10).iterrows(): - print(f" {date.strftime('%Y-%m-%d')}: SPY=${row['Close']:.2f}, " - f"EMA50=${row['EMA50']:.2f}, EMA200=${row['EMA200']:.2f}") - if len(bear_days) > 10: - print(f" ... and {len(bear_days) - 10} more Bear days") - - # Show first and last months in detail - print(f"\nFirst Month Detail:") - first_month = trading_period.head(22)[['Close', 'EMA50', 'EMA200', 'Regime']] - for date, row in first_month.iterrows(): - print(f" {date.strftime('%Y-%m-%d')}: {row['Regime']:8s} | " - f"SPY: ${row['Close']:7.2f} | EMA50: ${row['EMA50']:7.2f} | EMA200: ${row['EMA200']:7.2f}") - -if __name__ == '__main__': - # Analyze 2023 - analyze_period('2023-01-01', '2023-12-31', '2023 Backtest') - - # Analyze 2024 - analyze_period('2024-01-01', '2024-12-31', '2024 Backtest') - - print(f"\n{'='*70}") - print("Analysis complete.") - print('='*70) diff --git a/src/backtester.rs b/src/backtester.rs index 6bc92c7..8cb07ce 100644 --- a/src/backtester.rs +++ b/src/backtester.rs @@ -22,7 +22,7 @@ use crate::config::{ EQUITY_CURVE_SMA_PERIOD, REGIME_SPY_SYMBOL, REGIME_EMA_SHORT, REGIME_EMA_LONG, REGIME_CAUTION_SIZE_FACTOR, REGIME_CAUTION_THRESHOLD_BUMP, - HOURLY_REGIME_CAUTION_SIZE_FACTOR, HOURLY_REGIME_CAUTION_THRESHOLD_BUMP, + HOURLY_REGIME_CAUTION_SIZE_FACTOR, HOURLY_REGIME_CAUTION_THRESHOLD_BUMP, ALLOW_LONGS_IN_BEAR_MARKET, }; use crate::indicators::{calculate_all_indicators, calculate_ema, determine_market_regime, generate_signal}; use crate::strategy::Strategy; @@ -725,7 +725,7 @@ impl Backtester { REGIME_CAUTION_SIZE_FACTOR } }, - MarketRegime::Bear => 0.0, // No new longs + MarketRegime::Bear => if ALLOW_LONGS_IN_BEAR_MARKET { 1.0 } else { 0.0 }, }; // Log regime changes (only on transitions) @@ -801,7 +801,6 @@ impl Backtester { } // Phase 2: Process buys (only for top momentum stocks) - // In Bear regime, skip the entire buy phase (no new longs). if regime.allows_new_longs() { // In Caution regime, raise the buy threshold to require stronger signals // Use timeframe-specific parameters: hourly needs high bump, daily needs low bump @@ -1170,7 +1169,7 @@ impl Backtester { let regime_size_factor = match regime { MarketRegime::Bull => 1.0, MarketRegime::Caution => REGIME_CAUTION_SIZE_FACTOR, - MarketRegime::Bear => 0.0, + MarketRegime::Bear => if ALLOW_LONGS_IN_BEAR_MARKET { 1.0 } else { 0.0 }, }; if day_num % 100 == 0 { diff --git a/src/config.rs b/src/config.rs index d4baa12..0ccdc20 100644 --- a/src/config.rs +++ b/src/config.rs @@ -116,6 +116,10 @@ pub const HOURLY_REGIME_CAUTION_SIZE_FACTOR: f64 = 0.25; /// Hourly needs high bump (3.0) to avoid whipsaws. pub const HOURLY_REGIME_CAUTION_THRESHOLD_BUMP: f64 = 3.0; +/// If true, the bot is allowed to open new long positions during a Bear market regime. +/// This is a master switch for testing/debugging purposes. +pub const ALLOW_LONGS_IN_BEAR_MARKET: bool = false; + // ═══════════════════════════════════════════════════════════════════════ // Scaled Drawdown Circuit Breaker // ═══════════════════════════════════════════════════════════════════════ diff --git a/src/types.rs b/src/types.rs index edd1cb6..bfbf9ab 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,5 +1,6 @@ //! Data types and structures for the trading bot. +use crate::config::ALLOW_LONGS_IN_BEAR_MARKET; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -27,7 +28,10 @@ impl MarketRegime { /// Whether new long entries are permitted in this regime. pub fn allows_new_longs(&self) -> bool { - !matches!(self, MarketRegime::Bear) + match self { + MarketRegime::Bear => ALLOW_LONGS_IN_BEAR_MARKET, + _ => true, + } } }