gg
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user