This commit is contained in:
2026-02-13 22:00:24 +00:00
parent 0e820852fa
commit 62847846d0
5 changed files with 17 additions and 129 deletions

View File

@@ -1,19 +1,19 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -e 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 "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' echo 'Cannot force reload with this script - use "direnv reload" manually and then try again'
exit 1 exit 1
fi fi
# rebuild the cache forcefully # 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. # Update the mtime for .envrc.
# This will cause direnv to reload again - but without re-building. # 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. # Also update the timestamp of whatever profile_rc we have.
# This makes sure that we know we are up to date. # 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

View File

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

View File

@@ -22,7 +22,7 @@ use crate::config::{
EQUITY_CURVE_SMA_PERIOD, EQUITY_CURVE_SMA_PERIOD,
REGIME_SPY_SYMBOL, REGIME_EMA_SHORT, REGIME_EMA_LONG, REGIME_SPY_SYMBOL, REGIME_EMA_SHORT, REGIME_EMA_LONG,
REGIME_CAUTION_SIZE_FACTOR, REGIME_CAUTION_THRESHOLD_BUMP, 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::indicators::{calculate_all_indicators, calculate_ema, determine_market_regime, generate_signal};
use crate::strategy::Strategy; use crate::strategy::Strategy;
@@ -725,7 +725,7 @@ impl Backtester {
REGIME_CAUTION_SIZE_FACTOR 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) // Log regime changes (only on transitions)
@@ -801,7 +801,6 @@ impl Backtester {
} }
// Phase 2: Process buys (only for top momentum stocks) // 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() { if regime.allows_new_longs() {
// In Caution regime, raise the buy threshold to require stronger signals // In Caution regime, raise the buy threshold to require stronger signals
// Use timeframe-specific parameters: hourly needs high bump, daily needs low bump // Use timeframe-specific parameters: hourly needs high bump, daily needs low bump
@@ -1170,7 +1169,7 @@ impl Backtester {
let regime_size_factor = match regime { let regime_size_factor = match regime {
MarketRegime::Bull => 1.0, MarketRegime::Bull => 1.0,
MarketRegime::Caution => REGIME_CAUTION_SIZE_FACTOR, 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 { if day_num % 100 == 0 {

View File

@@ -116,6 +116,10 @@ pub const HOURLY_REGIME_CAUTION_SIZE_FACTOR: f64 = 0.25;
/// Hourly needs high bump (3.0) to avoid whipsaws. /// Hourly needs high bump (3.0) to avoid whipsaws.
pub const HOURLY_REGIME_CAUTION_THRESHOLD_BUMP: f64 = 3.0; 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 // Scaled Drawdown Circuit Breaker
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════

View File

@@ -1,5 +1,6 @@
//! Data types and structures for the trading bot. //! Data types and structures for the trading bot.
use crate::config::ALLOW_LONGS_IN_BEAR_MARKET;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -27,7 +28,10 @@ impl MarketRegime {
/// Whether new long entries are permitted in this regime. /// Whether new long entries are permitted in this regime.
pub fn allows_new_longs(&self) -> bool { pub fn allows_new_longs(&self) -> bool {
!matches!(self, MarketRegime::Bear) match self {
MarketRegime::Bear => ALLOW_LONGS_IN_BEAR_MARKET,
_ => true,
}
} }
} }