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