we ball again
This commit is contained in:
@@ -1,56 +1,189 @@
|
|||||||
# Consistency Auditor Memory
|
# Consistency Auditor Memory
|
||||||
|
|
||||||
## Last Audit: 2026-02-11 (Hourly Trading Update)
|
## Last Audit: 2026-02-12 (Regime-Adaptive Dual Strategy Update)
|
||||||
|
|
||||||
### CRITICAL FINDINGS
|
### AUDIT RESULT: ✅ NO CRITICAL BUGS FOUND
|
||||||
|
|
||||||
#### 1. Cooldown Timer Missing in Live Bot ❌
|
The refactor to extract shared logic into `strategy.rs` has **eliminated all previous consistency issues**. Bot and backtester now share identical implementations for all critical trading logic.
|
||||||
**Location**: backtester.rs has it (lines 40, 63, 174-179, 275-281), bot.rs missing
|
|
||||||
**Issue**: Backtester prevents whipsaw re-entry for REENTRY_COOLDOWN_BARS (7 bars) after stop-loss. Live bot can immediately re-buy on same cycle.
|
|
||||||
**Impact**: Live bot will churn more, potentially re-entering failed positions immediately. Backtest vs live divergence.
|
|
||||||
**Fix Required**: Add cooldown_timers HashMap to TradingBot, track in execute_sell, check in execute_buy.
|
|
||||||
|
|
||||||
#### 2. Gradual Ramp-Up Missing in Live Bot ⚠️
|
---
|
||||||
**Location**: backtester.rs has it (lines 42, 64, 196-198, 226, 508), bot.rs missing
|
|
||||||
**Issue**: Backtester limits new positions to 1 per bar during first RAMPUP_PERIOD_BARS (30 bars). Live bot could deploy all capital on first cycle.
|
|
||||||
**Impact**: Live initial deployment faster/riskier than backtest simulates.
|
|
||||||
**Fix Required**: Add new_positions_this_cycle counter to TradingBot, reset each cycle, check in execute_buy.
|
|
||||||
|
|
||||||
### Confirmed Consistent (2026-02-11)
|
## VERIFIED CONSISTENT (2026-02-12)
|
||||||
|
|
||||||
#### Core Trading Logic ✅
|
### Core Trading Logic ✅
|
||||||
- **Drawdown halt**: Time-based (35 bars), bot uses trading_cycle_count vs backtester current_bar (equivalent)
|
- **Signal generation**: Both use shared `indicators::generate_signal()` (indicators.rs:442-650)
|
||||||
- **bars_held increment**: Both at START of trading cycle/bar (bot:660-663, bt:531-534) — previous bug FIXED
|
- **Position sizing**: Both use shared `Strategy::calculate_position_size()` (strategy.rs:29-55)
|
||||||
- **Position sizing**: Identical ATR volatility adjustment, confidence scaling (0.7+0.3*conf), caps
|
- Volatility-adjusted via ATR
|
||||||
- **Stop-loss**: Identical 2.5x ATR + 4% hard cap + fixed fallback
|
- Confidence scaling: 0.7 + 0.3 * confidence
|
||||||
- **Trailing stop**: Identical 1.5x ATR activation/distance + fixed fallback
|
- Max position size cap: 25%
|
||||||
- **Time exit**: Identical 30-bar threshold
|
- Cash reserve: 5%
|
||||||
- **Sector limits**: Both max 2 per sector (was 3 in daily)
|
- **Stop-loss/trailing/time exit**: Both use shared `Strategy::check_stop_loss_take_profit()` (strategy.rs:57-128)
|
||||||
- **Max positions**: Both 5 concurrent (was 8 in daily)
|
- Hard max loss cap: 5%
|
||||||
- **Config constants**: All parameters identical (verified config.rs)
|
- ATR-based stop: 3.0x ATR below entry
|
||||||
|
- Fixed fallback stop: 2.5%
|
||||||
|
- Trailing stop: 2.0x ATR after 2.0x ATR gain
|
||||||
|
- Time exit: 40 bars if below trailing activation threshold
|
||||||
|
|
||||||
#### Warmup Requirements ✅
|
### Portfolio Controls ✅
|
||||||
**Hourly min_bars()**: max(35 MACD, 15 RSI, 100 EMA, 28 ADX, 20 BB, 63 momentum) + 5 = 105 bars
|
- **Cooldown timers**: Both implement 5-bar cooldown after stop-loss (bot:395-406,521-533; bt:133-138,242-247)
|
||||||
Both fetch ~158 calendar days for hourly. MACD needs slow+signal (26+9=35), ADX needs 2x (14*2=28), all accounted for.
|
- **Ramp-up period**: Both limit to 1 new position per bar for first 15 bars (bot:433-441; bt:158-161)
|
||||||
|
- **Drawdown circuit breaker**: Both halt for 20 bars at 12% drawdown (bot:244-268; bt:83-118)
|
||||||
|
- **Sector limits**: Both enforce max 2 per sector (bot:423-430; bt:149-156)
|
||||||
|
- **Max concurrent positions**: Both enforce max 7 (bot:414-421; bt:145-147)
|
||||||
|
- **Momentum ranking**: Both filter to top 10 momentum stocks (bot:669-690; bt:438-449)
|
||||||
|
- **bars_held increment**: Both increment at START of trading cycle/bar (bot:614-617; bt:433-436)
|
||||||
|
|
||||||
#### Expected Differences ✅
|
### Warmup Requirements ✅
|
||||||
- **Slippage**: Backtester 10 bps, live actual fills (correct)
|
**Daily mode**: `max(35 MACD, 15 RSI, 50 EMA, 28 ADX, 20 BB, 63 momentum) + 5 = 68 bars`
|
||||||
- **Already-holding**: Different APIs, same logic
|
**Hourly mode**: `max(35 MACD, 15 RSI, 200 EMA, 28 ADX, 20 BB, 63 momentum) + 5 = 205 bars`
|
||||||
|
|
||||||
### Config (2026-02-11 Hourly)
|
Calculation in `config.rs:169-183` (`IndicatorParams::min_bars()`)
|
||||||
- RISK_PER_TRADE: 0.75% (was 1% daily)
|
- RSI-2/3 warmup covered by RSI-14 requirement (15 > 3)
|
||||||
- ATR_STOP_MULTIPLIER: 2.5x (was 2.0x daily)
|
- MACD needs slow + signal periods (26 + 9 = 35)
|
||||||
- ATR_TRAIL_MULTIPLIER: 1.5x
|
- ADX needs 2x period for smoothing (14 * 2 = 28)
|
||||||
- ATR_TRAIL_ACTIVATION_MULTIPLIER: 1.5x
|
- Hourly EMA-200 dominates warmup requirement
|
||||||
- MAX_CONCURRENT_POSITIONS: 5 (was 8 daily)
|
|
||||||
- MAX_SECTOR_POSITIONS: 2 (was 3 daily)
|
|
||||||
- TIME_EXIT_BARS: 30 (~4.3 days)
|
|
||||||
- REENTRY_COOLDOWN_BARS: 7 (~1 day)
|
|
||||||
- RAMPUP_PERIOD_BARS: 30 (~4.3 days)
|
|
||||||
- DRAWDOWN_HALT_BARS: 35 (~5 days)
|
|
||||||
- Partial exits REMOVED (was destroying avg win/loss ratio)
|
|
||||||
- Take profit REMOVED (was capping winners)
|
|
||||||
|
|
||||||
### Hourly Indicator Periods
|
Both bot.rs and backtester.rs fetch sufficient historical data and validate bar count before trading.
|
||||||
RSI/MACD/ADX/BB/ATR: Standard periods (14, 12/26/9, etc) — NOT 7x scaled
|
|
||||||
Momentum: 63 (~9 days), EMA: 20/50/100. Uses textbook periods appropriate for hourly bars.
|
---
|
||||||
|
|
||||||
|
## INTENTIONAL DIFFERENCES (Not Bugs) ✅
|
||||||
|
|
||||||
|
### 1. Slippage Modeling
|
||||||
|
- **Backtester**: Applies 10 bps on both entry and exit (backtester.rs:63-71)
|
||||||
|
- **Live bot**: Uses actual fill prices from Alpaca API (bot.rs:456-460)
|
||||||
|
- **Verdict**: Expected difference. Backtester simulates realistic costs; live bot gets market fills.
|
||||||
|
|
||||||
|
### 2. RSI Short Period Scaling
|
||||||
|
- **Daily mode**: `rsi_short_period: 2` (Connors RSI-2 for mean reversion)
|
||||||
|
- **Hourly mode**: `rsi_short_period: 3` (adjusted for intraday noise)
|
||||||
|
- **Verdict**: Intentional design choice per comment "Slightly longer for hourly noise"
|
||||||
|
|
||||||
|
### 3. EMA Trend Period Scaling
|
||||||
|
- **Daily mode**: `ema_trend: 50` (50-day trend filter)
|
||||||
|
- **Hourly mode**: `ema_trend: 200` (200-hour ≈ 28.5-day trend filter)
|
||||||
|
- **Verdict**: Hourly uses 4x scaling (not 7x like other indicators) for longer-term trend context. Appears intentional.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## STRATEGY ARCHITECTURE (2026-02-12)
|
||||||
|
|
||||||
|
### Regime-Adaptive Dual Signal
|
||||||
|
The new strategy uses **ADX for regime detection** and switches between two modes:
|
||||||
|
|
||||||
|
#### RANGE-BOUND (ADX < 20): Mean Reversion
|
||||||
|
- **Entry**: Connors RSI-2 extreme oversold (RSI-2 < 10) + price above 200 EMA
|
||||||
|
- **Exit**: RSI-2 extreme overbought (RSI-2 > 90) or standard exits
|
||||||
|
- **Conviction boosters**: Bollinger Band extremes, volume confirmation
|
||||||
|
- **Logic**: indicators.rs:490-526
|
||||||
|
|
||||||
|
#### TRENDING (ADX > 25): Momentum Pullback
|
||||||
|
- **Entry**: Pullbacks in strong trends (RSI-14 dips 25-40, price near EMA support, MACD confirming)
|
||||||
|
- **Exit**: Trend break (EMA crossover down) or standard exits
|
||||||
|
- **Conviction boosters**: Strong trend (ADX > 40), DI+/DI- alignment
|
||||||
|
- **Logic**: indicators.rs:531-557
|
||||||
|
|
||||||
|
#### UNIVERSAL SIGNALS (Both Regimes)
|
||||||
|
- RSI-14 extremes in trending context (indicators.rs:564-570)
|
||||||
|
- MACD crossovers (indicators.rs:573-583)
|
||||||
|
- EMA crossovers (indicators.rs:599-608)
|
||||||
|
- Volume gate (reduces scores 50% if volume < 80% of 20-period MA) (indicators.rs:611-614)
|
||||||
|
|
||||||
|
### Signal Thresholds
|
||||||
|
- **StrongBuy**: total_score >= 7.0
|
||||||
|
- **Buy**: total_score >= 4.5
|
||||||
|
- **StrongSell**: total_score <= -7.0
|
||||||
|
- **Sell**: total_score <= -4.0
|
||||||
|
- **Hold**: everything else
|
||||||
|
|
||||||
|
Confidence: `(total_score.abs() / 12.0).min(1.0)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CONFIG PARAMETERS (2026-02-12)
|
||||||
|
|
||||||
|
### Indicator Periods
|
||||||
|
- RSI: 14 (standard), RSI-2 (daily) / RSI-3 (hourly) for mean reversion
|
||||||
|
- MACD: 12/26/9 (standard)
|
||||||
|
- Momentum: 63 bars
|
||||||
|
- EMA: 9/21/50 (daily), 9/21/200 (hourly)
|
||||||
|
- ADX: 14, thresholds: 20 (range), 25 (trend), 40 (strong)
|
||||||
|
- Bollinger Bands: 20-period, 2 std dev
|
||||||
|
- ATR: 14-period
|
||||||
|
- Volume MA: 20-period, threshold: 0.8x
|
||||||
|
|
||||||
|
### Risk Management
|
||||||
|
- **Position sizing**: 1.2% risk per trade (RISK_PER_TRADE)
|
||||||
|
- **ATR stop**: 3.0x ATR below entry (was 2.5x)
|
||||||
|
- **ATR trailing stop**: 2.0x ATR distance, activates after 2.0x ATR gain (was 1.5x/1.5x)
|
||||||
|
- **Max position size**: 25% (was 22%)
|
||||||
|
- **Max loss cap**: 5% (was 4%)
|
||||||
|
- **Stop loss fallback**: 2.5% (when ATR unavailable)
|
||||||
|
- **Time exit**: 40 bars (was 30)
|
||||||
|
- **Cash reserve**: 5%
|
||||||
|
|
||||||
|
### Portfolio Limits
|
||||||
|
- **Max concurrent positions**: 7 (was 5)
|
||||||
|
- **Max per sector**: 2 (unchanged)
|
||||||
|
- **Momentum ranking**: Top 10 stocks (was 4)
|
||||||
|
- **Drawdown halt**: 12% triggers 20-bar cooldown (was 35 bars)
|
||||||
|
- **Reentry cooldown**: 5 bars after stop-loss (was 7)
|
||||||
|
- **Ramp-up period**: 15 bars, 1 new position per bar (was 30 bars)
|
||||||
|
|
||||||
|
### Backtester
|
||||||
|
- **Slippage**: 10 bps per trade
|
||||||
|
- **Risk-free rate**: 5% annually for Sharpe/Sortino
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## KEY LESSONS
|
||||||
|
|
||||||
|
### 1. Shared Logic Eliminates Drift
|
||||||
|
Extracting common logic into `strategy.rs` ensures bot and backtester CANNOT diverge. Previously, duplicate implementations led to subtle differences (partial exits, bars_held increment timing, cooldown logic).
|
||||||
|
|
||||||
|
### 2. Warmup Must Account for Longest Chain
|
||||||
|
For hourly mode, EMA-200 dominates warmup (205 bars). ADX also needs 2x period (28 bars) for proper smoothing. The `+ 5` safety margin is critical.
|
||||||
|
|
||||||
|
### 3. NaN Handling is Critical
|
||||||
|
Indicators can produce NaN during warmup or with insufficient data. The signal generator uses safe defaults (e.g., `if adx.is_nan() { 22.0 }`) to prevent scoring errors.
|
||||||
|
|
||||||
|
### 4. ATR Fallbacks Prevent Edge Cases
|
||||||
|
When ATR is zero/unavailable (e.g., low volatility or warmup), code falls back to fixed percentage stops. Without this, position sizing could explode or stops could fail.
|
||||||
|
|
||||||
|
### 5. Slippage Modeling is Non-Negotiable
|
||||||
|
The backtester applies 10 bps slippage on both sides (20 bps round-trip) to simulate realistic fills. This prevents overfitting to unrealistic backtest performance.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AUDIT CHECKLIST (For Future Audits)
|
||||||
|
|
||||||
|
When new changes are made, verify:
|
||||||
|
|
||||||
|
1. **Signal generation**: Still using shared `indicators::generate_signal()`?
|
||||||
|
2. **Position sizing**: Still using shared `Strategy::calculate_position_size()`?
|
||||||
|
3. **Risk management**: Still using shared `Strategy::check_stop_loss_take_profit()`?
|
||||||
|
4. **Cooldown timers**: Identical logic in both files?
|
||||||
|
5. **Ramp-up period**: Identical logic in both files?
|
||||||
|
6. **Drawdown halt**: Identical trigger and resume logic?
|
||||||
|
7. **Sector limits**: Same `MAX_SECTOR_POSITIONS` constant?
|
||||||
|
8. **Max positions**: Same `MAX_CONCURRENT_POSITIONS` constant?
|
||||||
|
9. **Momentum ranking**: Same `TOP_MOMENTUM_COUNT` constant?
|
||||||
|
10. **bars_held increment**: Both increment at START of cycle/bar?
|
||||||
|
11. **Warmup calculation**: Does `min_bars()` cover all indicators?
|
||||||
|
12. **Config propagation**: Are new constants used consistently?
|
||||||
|
13. **NaN handling**: Safe defaults for all indicator checks?
|
||||||
|
14. **ATR guards**: Checks for `> 0.0` before division?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FILES AUDITED (2026-02-12)
|
||||||
|
- `/home/work/Documents/rust/invest-bot/src/bot.rs` (785 lines)
|
||||||
|
- `/home/work/Documents/rust/invest-bot/src/backtester.rs` (880 lines)
|
||||||
|
- `/home/work/Documents/rust/invest-bot/src/config.rs` (199 lines)
|
||||||
|
- `/home/work/Documents/rust/invest-bot/src/indicators.rs` (651 lines)
|
||||||
|
- `/home/work/Documents/rust/invest-bot/src/strategy.rs` (141 lines)
|
||||||
|
- `/home/work/Documents/rust/invest-bot/src/types.rs` (234 lines)
|
||||||
|
|
||||||
|
**Total**: 2,890 lines audited
|
||||||
|
**Issues found**: 0 critical, 0 medium, 0 low
|
||||||
|
**Status**: ✅ PRODUCTION READY
|
||||||
|
|||||||
@@ -23,55 +23,61 @@ pub fn get_all_symbols() -> Vec<&'static str> {
|
|||||||
symbols.extend_from_slice(SP500_ENERGY);
|
symbols.extend_from_slice(SP500_ENERGY);
|
||||||
symbols
|
symbols
|
||||||
}
|
}
|
||||||
// Strategy Parameters - Further tweaked for better performance
|
// Strategy Parameters — Regime-Adaptive Dual Signal
|
||||||
pub const RSI_PERIOD: usize = 14; // Standard reliable period
|
// RSI-14 for trend assessment, RSI-2 for mean-reversion entries (Connors)
|
||||||
pub const RSI_OVERSOLD: f64 = 30.0; // Standard to reduce false entries
|
pub const RSI_PERIOD: usize = 14;
|
||||||
pub const RSI_OVERBOUGHT: f64 = 70.0; // Standard to reduce false signals
|
pub const RSI_SHORT_PERIOD: usize = 2; // Connors RSI-2 for mean reversion
|
||||||
pub const RSI_PULLBACK_LOW: f64 = 35.0; // Slight adjustment
|
pub const RSI_OVERSOLD: f64 = 30.0;
|
||||||
pub const RSI_PULLBACK_HIGH: f64 = 60.0; // Slight adjustment
|
pub const RSI_OVERBOUGHT: f64 = 70.0;
|
||||||
|
pub const RSI2_OVERSOLD: f64 = 10.0; // Extreme oversold for mean reversion entries
|
||||||
|
pub const RSI2_OVERBOUGHT: f64 = 90.0; // Extreme overbought for mean reversion exits
|
||||||
pub const MACD_FAST: usize = 12;
|
pub const MACD_FAST: usize = 12;
|
||||||
pub const MACD_SLOW: usize = 26;
|
pub const MACD_SLOW: usize = 26;
|
||||||
pub const MACD_SIGNAL: usize = 9;
|
pub const MACD_SIGNAL: usize = 9;
|
||||||
pub const MOMENTUM_PERIOD: usize = 63;
|
pub const MOMENTUM_PERIOD: usize = 63;
|
||||||
pub const EMA_SHORT: usize = 9; // Standard short EMA
|
pub const EMA_SHORT: usize = 9;
|
||||||
pub const EMA_LONG: usize = 21;
|
pub const EMA_LONG: usize = 21;
|
||||||
pub const EMA_TREND: usize = 50;
|
pub const EMA_TREND: usize = 50;
|
||||||
// ADX - Trend Strength
|
// ADX — Regime Detection
|
||||||
|
// ADX < RANGE_THRESHOLD = ranging (use mean reversion)
|
||||||
|
// ADX > TREND_THRESHOLD = trending (use momentum/pullback)
|
||||||
|
// Between = transition zone (reduce size, be cautious)
|
||||||
pub const ADX_PERIOD: usize = 14;
|
pub const ADX_PERIOD: usize = 14;
|
||||||
pub const ADX_THRESHOLD: f64 = 20.0;
|
pub const ADX_RANGE_THRESHOLD: f64 = 20.0; // Below this = range-bound
|
||||||
pub const ADX_STRONG: f64 = 35.0;
|
pub const ADX_TREND_THRESHOLD: f64 = 25.0; // Above this = trending
|
||||||
|
pub const ADX_STRONG: f64 = 40.0; // Strong trend for bonus conviction
|
||||||
// Bollinger Bands
|
// Bollinger Bands
|
||||||
pub const BB_PERIOD: usize = 20;
|
pub const BB_PERIOD: usize = 20;
|
||||||
pub const BB_STD: f64 = 2.0;
|
pub const BB_STD: f64 = 2.0;
|
||||||
// ATR for volatility-based stops
|
// ATR
|
||||||
pub const ATR_PERIOD: usize = 14;
|
pub const ATR_PERIOD: usize = 14;
|
||||||
pub const MIN_ATR_PCT: f64 = 0.005;
|
pub const MIN_ATR_PCT: f64 = 0.005;
|
||||||
// Volume filter
|
// Volume filter
|
||||||
pub const VOLUME_MA_PERIOD: usize = 20;
|
pub const VOLUME_MA_PERIOD: usize = 20;
|
||||||
pub const VOLUME_THRESHOLD: f64 = 0.8;
|
pub const VOLUME_THRESHOLD: f64 = 0.8;
|
||||||
// Momentum Ranking
|
// Momentum Ranking
|
||||||
pub const TOP_MOMENTUM_COUNT: usize = 8;
|
pub const TOP_MOMENTUM_COUNT: usize = 10; // Wider pool for more opportunities
|
||||||
// Risk Management
|
// Risk Management
|
||||||
pub const MAX_POSITION_SIZE: f64 = 0.22;
|
pub const MAX_POSITION_SIZE: f64 = 0.25; // Slightly larger for concentrated bets
|
||||||
pub const MIN_CASH_RESERVE: f64 = 0.05;
|
pub const MIN_CASH_RESERVE: f64 = 0.05;
|
||||||
pub const STOP_LOSS_PCT: f64 = 0.025;
|
pub const STOP_LOSS_PCT: f64 = 0.025;
|
||||||
pub const MAX_LOSS_PCT: f64 = 0.04;
|
pub const MAX_LOSS_PCT: f64 = 0.05; // Wider max loss — let mean reversion work
|
||||||
pub const TRAILING_STOP_ACTIVATION: f64 = 0.06;
|
pub const TRAILING_STOP_ACTIVATION: f64 = 0.06;
|
||||||
pub const TRAILING_STOP_DISTANCE: f64 = 0.04;
|
pub const TRAILING_STOP_DISTANCE: f64 = 0.04;
|
||||||
// ATR-based risk management
|
// ATR-based risk management
|
||||||
pub const RISK_PER_TRADE: f64 = 0.008;
|
pub const RISK_PER_TRADE: f64 = 0.012; // More aggressive sizing for higher conviction
|
||||||
pub const ATR_STOP_MULTIPLIER: f64 = 2.5;
|
pub const ATR_STOP_MULTIPLIER: f64 = 3.0; // Wider stops — research shows tighter stops hurt
|
||||||
pub const ATR_TRAIL_MULTIPLIER: f64 = 1.5;
|
pub const ATR_TRAIL_MULTIPLIER: f64 = 2.0; // Wider trail to let winners run
|
||||||
pub const ATR_TRAIL_ACTIVATION_MULTIPLIER: f64 = 1.5;
|
pub const ATR_TRAIL_ACTIVATION_MULTIPLIER: f64 = 2.0; // Activate after 2x ATR gain
|
||||||
// Portfolio-level controls
|
// Portfolio-level controls
|
||||||
pub const MAX_CONCURRENT_POSITIONS: usize = 5;
|
pub const MAX_CONCURRENT_POSITIONS: usize = 7; // More positions for diversification
|
||||||
pub const MAX_SECTOR_POSITIONS: usize = 2;
|
pub const MAX_SECTOR_POSITIONS: usize = 2;
|
||||||
pub const MAX_DRAWDOWN_HALT: f64 = 0.10;
|
pub const MAX_DRAWDOWN_HALT: f64 = 0.12; // Wider drawdown tolerance
|
||||||
pub const DRAWDOWN_HALT_BARS: usize = 35;
|
pub const DRAWDOWN_HALT_BARS: usize = 20; // Shorter cooldown to get back in
|
||||||
// Time-based exit
|
// Time-based exit
|
||||||
pub const TIME_EXIT_BARS: usize = 30;
|
pub const TIME_EXIT_BARS: usize = 40; // Longer patience for mean reversion
|
||||||
pub const REENTRY_COOLDOWN_BARS: usize = 7;
|
pub const REENTRY_COOLDOWN_BARS: usize = 5; // Shorter cooldown
|
||||||
pub const RAMPUP_PERIOD_BARS: usize = 30;
|
pub const RAMPUP_PERIOD_BARS: usize = 15; // Faster ramp-up
|
||||||
// Backtester slippage
|
// Backtester slippage
|
||||||
pub const SLIPPAGE_BPS: f64 = 10.0;
|
pub const SLIPPAGE_BPS: f64 = 10.0;
|
||||||
// Trading intervals
|
// Trading intervals
|
||||||
@@ -110,6 +116,7 @@ pub fn get_sector(symbol: &str) -> &'static str {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct IndicatorParams {
|
pub struct IndicatorParams {
|
||||||
pub rsi_period: usize,
|
pub rsi_period: usize,
|
||||||
|
pub rsi_short_period: usize, // RSI-2 for mean reversion
|
||||||
pub macd_fast: usize,
|
pub macd_fast: usize,
|
||||||
pub macd_slow: usize,
|
pub macd_slow: usize,
|
||||||
pub macd_signal: usize,
|
pub macd_signal: usize,
|
||||||
@@ -126,7 +133,8 @@ impl IndicatorParams {
|
|||||||
/// Create parameters for daily timeframe.
|
/// Create parameters for daily timeframe.
|
||||||
pub fn daily() -> Self {
|
pub fn daily() -> Self {
|
||||||
Self {
|
Self {
|
||||||
rsi_period: 14, // Standard
|
rsi_period: 14,
|
||||||
|
rsi_short_period: 2, // Connors RSI-2
|
||||||
macd_fast: 12,
|
macd_fast: 12,
|
||||||
macd_slow: 26,
|
macd_slow: 26,
|
||||||
macd_signal: 9,
|
macd_signal: 9,
|
||||||
@@ -143,8 +151,9 @@ impl IndicatorParams {
|
|||||||
/// Create parameters for hourly timeframe.
|
/// Create parameters for hourly timeframe.
|
||||||
pub fn hourly() -> Self {
|
pub fn hourly() -> Self {
|
||||||
Self {
|
Self {
|
||||||
rsi_period: 14, // Standard even for intraday to reduce noise
|
rsi_period: 14,
|
||||||
macd_fast: 12, // Standard for balance
|
rsi_short_period: 3, // Slightly longer for hourly noise
|
||||||
|
macd_fast: 12,
|
||||||
macd_slow: 26,
|
macd_slow: 26,
|
||||||
macd_signal: 9,
|
macd_signal: 9,
|
||||||
momentum_period: 63,
|
momentum_period: 63,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
//! Technical indicator calculations.
|
//! Technical indicator calculations.
|
||||||
|
|
||||||
use crate::config::{
|
use crate::config::{
|
||||||
IndicatorParams, ADX_STRONG, ADX_THRESHOLD, BB_STD, RSI_OVERBOUGHT, RSI_OVERSOLD,
|
IndicatorParams, ADX_RANGE_THRESHOLD, ADX_STRONG, ADX_TREND_THRESHOLD, BB_STD,
|
||||||
RSI_PULLBACK_HIGH, RSI_PULLBACK_LOW, VOLUME_THRESHOLD,
|
RSI2_OVERBOUGHT, RSI2_OVERSOLD, RSI_OVERBOUGHT, RSI_OVERSOLD, VOLUME_THRESHOLD,
|
||||||
};
|
};
|
||||||
use crate::types::{Bar, IndicatorRow, Signal, TradeSignal};
|
use crate::types::{Bar, IndicatorRow, Signal, TradeSignal};
|
||||||
|
|
||||||
@@ -348,6 +348,7 @@ pub fn calculate_all_indicators(bars: &[Bar], params: &IndicatorParams) -> Vec<I
|
|||||||
|
|
||||||
// Calculate all indicators
|
// Calculate all indicators
|
||||||
let rsi = calculate_rsi(&closes, params.rsi_period);
|
let rsi = calculate_rsi(&closes, params.rsi_period);
|
||||||
|
let rsi_short = calculate_rsi(&closes, params.rsi_short_period);
|
||||||
let (macd, macd_signal, macd_histogram) =
|
let (macd, macd_signal, macd_histogram) =
|
||||||
calculate_macd(&closes, params.macd_fast, params.macd_slow, params.macd_signal);
|
calculate_macd(&closes, params.macd_fast, params.macd_slow, params.macd_signal);
|
||||||
let momentum = calculate_roc(&closes, params.momentum_period);
|
let momentum = calculate_roc(&closes, params.momentum_period);
|
||||||
@@ -392,6 +393,7 @@ pub fn calculate_all_indicators(bars: &[Bar], params: &IndicatorParams) -> Vec<I
|
|||||||
close: bar.close,
|
close: bar.close,
|
||||||
volume: bar.volume,
|
volume: bar.volume,
|
||||||
rsi: rsi[i],
|
rsi: rsi[i],
|
||||||
|
rsi_short: rsi_short[i],
|
||||||
macd: macd[i],
|
macd: macd[i],
|
||||||
macd_signal: macd_signal[i],
|
macd_signal: macd_signal[i],
|
||||||
macd_histogram: macd_histogram[i],
|
macd_histogram: macd_histogram[i],
|
||||||
@@ -421,165 +423,180 @@ pub fn calculate_all_indicators(bars: &[Bar], params: &IndicatorParams) -> Vec<I
|
|||||||
rows
|
rows
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate trading signal from current and previous indicator rows.
|
/// Generate trading signal using regime-adaptive dual strategy.
|
||||||
|
///
|
||||||
|
/// REGIME DETECTION (via ADX):
|
||||||
|
/// - ADX < 20: Range-bound → use Connors RSI-2 mean reversion
|
||||||
|
/// - ADX > 25: Trending → use momentum pullback entries
|
||||||
|
/// - 20-25: Transition → require extra confirmation
|
||||||
|
///
|
||||||
|
/// MEAN REVERSION (ranging markets):
|
||||||
|
/// - Buy when RSI-2 < 10 AND price above 200 EMA (long-term uptrend filter)
|
||||||
|
/// - Sell when RSI-2 > 90 (take profit at mean)
|
||||||
|
/// - Bollinger Band extremes add conviction
|
||||||
|
///
|
||||||
|
/// TREND FOLLOWING (trending markets):
|
||||||
|
/// - Buy pullbacks in uptrends: RSI-14 dips + EMA support + MACD confirming
|
||||||
|
/// - Sell when trend breaks: EMA crossover down + momentum loss
|
||||||
|
/// - Strong trend bonus for high ADX
|
||||||
pub fn generate_signal(symbol: &str, current: &IndicatorRow, previous: &IndicatorRow) -> TradeSignal {
|
pub fn generate_signal(symbol: &str, current: &IndicatorRow, previous: &IndicatorRow) -> TradeSignal {
|
||||||
let rsi = current.rsi;
|
let rsi = current.rsi;
|
||||||
let macd = current.macd;
|
let rsi2 = current.rsi_short;
|
||||||
let macd_signal_val = current.macd_signal;
|
|
||||||
let macd_hist = current.macd_histogram;
|
let macd_hist = current.macd_histogram;
|
||||||
let momentum = current.momentum;
|
let momentum = current.momentum;
|
||||||
let ema_short = current.ema_short;
|
let ema_short = current.ema_short;
|
||||||
let ema_long = current.ema_long;
|
let ema_long = current.ema_long;
|
||||||
let current_price = current.close;
|
let current_price = current.close;
|
||||||
|
|
||||||
// Advanced indicators
|
// Safe NaN handling
|
||||||
let trend_bullish = current.trend_bullish;
|
let trend_bullish = current.trend_bullish;
|
||||||
let volume_ratio = if current.volume_ratio.is_nan() {
|
let volume_ratio = if current.volume_ratio.is_nan() { 1.0 } else { current.volume_ratio };
|
||||||
1.0
|
let adx = if current.adx.is_nan() { 22.0 } else { current.adx };
|
||||||
} else {
|
let di_plus = if current.di_plus.is_nan() { 25.0 } else { current.di_plus };
|
||||||
current.volume_ratio
|
let di_minus = if current.di_minus.is_nan() { 25.0 } else { current.di_minus };
|
||||||
};
|
let bb_pct = if current.bb_pct.is_nan() { 0.5 } else { current.bb_pct };
|
||||||
let adx = if current.adx.is_nan() { 25.0 } else { current.adx };
|
let ema_distance = if current.ema_distance.is_nan() { 0.0 } else { current.ema_distance };
|
||||||
let di_plus = if current.di_plus.is_nan() {
|
|
||||||
25.0
|
// REGIME DETECTION
|
||||||
} else {
|
let is_ranging = adx < ADX_RANGE_THRESHOLD;
|
||||||
current.di_plus
|
let is_trending = adx > ADX_TREND_THRESHOLD;
|
||||||
};
|
let strong_trend = adx > ADX_STRONG;
|
||||||
let di_minus = if current.di_minus.is_nan() {
|
let trend_up = di_plus > di_minus;
|
||||||
25.0
|
|
||||||
} else {
|
// EMA state
|
||||||
current.di_minus
|
let ema_bullish = !ema_short.is_nan() && !ema_long.is_nan() && ema_short > ema_long;
|
||||||
};
|
|
||||||
let bb_pct = if current.bb_pct.is_nan() {
|
|
||||||
0.5
|
|
||||||
} else {
|
|
||||||
current.bb_pct
|
|
||||||
};
|
|
||||||
let ema_distance = if current.ema_distance.is_nan() {
|
|
||||||
0.0
|
|
||||||
} else {
|
|
||||||
current.ema_distance
|
|
||||||
};
|
|
||||||
|
|
||||||
// MACD crossover detection
|
// MACD crossover detection
|
||||||
let macd_crossed_up = !previous.macd.is_nan()
|
let macd_crossed_up = !previous.macd.is_nan()
|
||||||
&& !previous.macd_signal.is_nan()
|
&& !previous.macd_signal.is_nan()
|
||||||
&& !macd.is_nan()
|
&& !current.macd.is_nan()
|
||||||
&& !macd_signal_val.is_nan()
|
&& !current.macd_signal.is_nan()
|
||||||
&& previous.macd < previous.macd_signal
|
&& previous.macd < previous.macd_signal
|
||||||
&& macd > macd_signal_val;
|
&& current.macd > current.macd_signal;
|
||||||
|
|
||||||
let macd_crossed_down = !previous.macd.is_nan()
|
let macd_crossed_down = !previous.macd.is_nan()
|
||||||
&& !previous.macd_signal.is_nan()
|
&& !previous.macd_signal.is_nan()
|
||||||
&& !macd.is_nan()
|
&& !current.macd.is_nan()
|
||||||
&& !macd_signal_val.is_nan()
|
&& !current.macd_signal.is_nan()
|
||||||
&& previous.macd > previous.macd_signal
|
&& previous.macd > previous.macd_signal
|
||||||
&& macd < macd_signal_val;
|
&& current.macd < current.macd_signal;
|
||||||
|
|
||||||
// EMA trend
|
|
||||||
let ema_bullish = !ema_short.is_nan() && !ema_long.is_nan() && ema_short > ema_long;
|
|
||||||
|
|
||||||
// ADX trend strength
|
|
||||||
let is_trending = adx > ADX_THRESHOLD;
|
|
||||||
let strong_trend = adx > ADX_STRONG;
|
|
||||||
let trend_up = di_plus > di_minus;
|
|
||||||
|
|
||||||
// Calculate scores
|
|
||||||
let mut buy_score: f64 = 0.0;
|
let mut buy_score: f64 = 0.0;
|
||||||
let mut sell_score: f64 = 0.0;
|
let mut sell_score: f64 = 0.0;
|
||||||
|
|
||||||
// TREND STRENGTH FILTER
|
// ═══════════════════════════════════════════════════════════════
|
||||||
if is_trending {
|
// REGIME 1: MEAN REVERSION (ranging market, ADX < 20)
|
||||||
if trend_up && trend_bullish {
|
// ═══════════════════════════════════════════════════════════════
|
||||||
buy_score += 3.0;
|
if is_ranging {
|
||||||
} else if !trend_up && !trend_bullish {
|
// Connors RSI-2 mean reversion: buy extreme oversold in uptrend context
|
||||||
sell_score += 3.0;
|
if !rsi2.is_nan() {
|
||||||
|
// Buy: RSI-2 extremely oversold + long-term trend intact
|
||||||
|
if rsi2 < RSI2_OVERSOLD {
|
||||||
|
buy_score += 5.0; // Strong mean reversion signal
|
||||||
|
if trend_bullish {
|
||||||
|
buy_score += 3.0; // With-trend mean reversion = highest conviction
|
||||||
}
|
}
|
||||||
} else {
|
if bb_pct < 0.05 {
|
||||||
// Ranging market - use mean reversion
|
buy_score += 2.0; // Price at/below lower BB
|
||||||
if bb_pct < 0.1 {
|
|
||||||
buy_score += 2.0;
|
|
||||||
} else if bb_pct > 0.9 {
|
|
||||||
sell_score += 2.0;
|
|
||||||
}
|
}
|
||||||
}
|
} else if rsi2 < 20.0 {
|
||||||
|
buy_score += 2.5;
|
||||||
// PULLBACK ENTRY (buy-side)
|
if trend_bullish {
|
||||||
if trend_bullish && ema_bullish {
|
|
||||||
if !rsi.is_nan() && rsi > RSI_PULLBACK_LOW && rsi < RSI_PULLBACK_HIGH {
|
|
||||||
buy_score += 3.0;
|
|
||||||
}
|
|
||||||
if ema_distance > 0.0 && ema_distance < 0.03 {
|
|
||||||
buy_score += 1.5;
|
buy_score += 1.5;
|
||||||
}
|
}
|
||||||
if bb_pct < 0.3 {
|
}
|
||||||
buy_score += 2.0;
|
|
||||||
|
// Sell: RSI-2 overbought = take profit on mean reversion
|
||||||
|
if rsi2 > RSI2_OVERBOUGHT {
|
||||||
|
sell_score += 4.0;
|
||||||
|
if !trend_bullish {
|
||||||
|
sell_score += 2.0;
|
||||||
|
}
|
||||||
|
} else if rsi2 > 80.0 && !trend_bullish {
|
||||||
|
sell_score += 2.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PULLBACK EXIT (sell-side symmetry — bearish trend with RSI bounce)
|
// Bollinger Band extremes in range
|
||||||
if !trend_bullish && !ema_bullish {
|
if bb_pct < 0.0 {
|
||||||
if !rsi.is_nan() && rsi > (100.0 - RSI_PULLBACK_HIGH) && rsi < (100.0 - RSI_PULLBACK_LOW) {
|
buy_score += 2.0; // Below lower band
|
||||||
sell_score += 3.0;
|
} else if bb_pct > 1.0 {
|
||||||
|
sell_score += 2.0; // Above upper band
|
||||||
}
|
}
|
||||||
if ema_distance < 0.0 && ema_distance > -0.03 {
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// REGIME 2: TREND FOLLOWING (trending market, ADX > 25)
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
if is_trending {
|
||||||
|
// Trend direction confirmation
|
||||||
|
if trend_up && trend_bullish {
|
||||||
|
buy_score += 3.0;
|
||||||
|
// Pullback entry: price dipped but trend intact
|
||||||
|
if !rsi.is_nan() && rsi < 40.0 && rsi > 25.0 {
|
||||||
|
buy_score += 3.0; // Pullback in uptrend
|
||||||
|
}
|
||||||
|
if ema_distance > 0.0 && ema_distance < 0.02 {
|
||||||
|
buy_score += 2.0; // Near EMA support
|
||||||
|
}
|
||||||
|
if strong_trend {
|
||||||
|
buy_score += 1.5; // Strong trend bonus
|
||||||
|
}
|
||||||
|
} else if !trend_up && !trend_bullish {
|
||||||
|
sell_score += 3.0;
|
||||||
|
if !rsi.is_nan() && rsi > 60.0 && rsi < 75.0 {
|
||||||
|
sell_score += 3.0; // Bounce in downtrend
|
||||||
|
}
|
||||||
|
if ema_distance < 0.0 && ema_distance > -0.02 {
|
||||||
|
sell_score += 2.0; // Near EMA resistance
|
||||||
|
}
|
||||||
|
if strong_trend {
|
||||||
sell_score += 1.5;
|
sell_score += 1.5;
|
||||||
}
|
}
|
||||||
if bb_pct > 0.7 {
|
|
||||||
sell_score += 2.0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// OVERSOLD/OVERBOUGHT (symmetrized)
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// UNIVERSAL SIGNALS (both regimes)
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// RSI-14 extremes (strong conviction regardless of regime)
|
||||||
if !rsi.is_nan() {
|
if !rsi.is_nan() {
|
||||||
if rsi < RSI_OVERSOLD {
|
if rsi < RSI_OVERSOLD && trend_bullish {
|
||||||
if trend_bullish {
|
buy_score += 3.0; // Oversold in uptrend = strong buy
|
||||||
buy_score += 4.0;
|
} else if rsi > RSI_OVERBOUGHT && !trend_bullish {
|
||||||
} else {
|
sell_score += 3.0; // Overbought in downtrend = strong sell
|
||||||
buy_score += 2.0;
|
|
||||||
}
|
|
||||||
} else if rsi > RSI_OVERBOUGHT {
|
|
||||||
if !trend_bullish {
|
|
||||||
sell_score += 4.0;
|
|
||||||
} else {
|
|
||||||
sell_score += 2.0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MACD MOMENTUM (symmetrized)
|
// MACD crossover
|
||||||
if macd_crossed_up {
|
if macd_crossed_up {
|
||||||
buy_score += 2.5;
|
buy_score += 2.0;
|
||||||
if strong_trend && trend_up {
|
if is_trending && trend_up {
|
||||||
buy_score += 1.0;
|
buy_score += 1.0; // Trend-confirming crossover
|
||||||
}
|
}
|
||||||
} else if macd_crossed_down {
|
} else if macd_crossed_down {
|
||||||
sell_score += 2.5;
|
|
||||||
if strong_trend && !trend_up {
|
|
||||||
sell_score += 1.0;
|
|
||||||
}
|
|
||||||
} else if !macd_hist.is_nan() {
|
|
||||||
if macd_hist > 0.0 {
|
|
||||||
buy_score += 0.5;
|
|
||||||
} else if macd_hist < 0.0 {
|
|
||||||
sell_score += 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MOMENTUM
|
|
||||||
if !momentum.is_nan() {
|
|
||||||
if momentum > 5.0 {
|
|
||||||
buy_score += 2.0;
|
|
||||||
} else if momentum > 2.0 {
|
|
||||||
buy_score += 1.0;
|
|
||||||
} else if momentum < -5.0 {
|
|
||||||
sell_score += 2.0;
|
sell_score += 2.0;
|
||||||
} else if momentum < -2.0 {
|
if is_trending && !trend_up {
|
||||||
sell_score += 1.0;
|
sell_score += 1.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// EMA CROSSOVER
|
// MACD histogram direction
|
||||||
|
if !macd_hist.is_nan() {
|
||||||
|
if macd_hist > 0.0 { buy_score += 0.5; }
|
||||||
|
else if macd_hist < 0.0 { sell_score += 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Momentum
|
||||||
|
if !momentum.is_nan() {
|
||||||
|
if momentum > 5.0 { buy_score += 1.5; }
|
||||||
|
else if momentum > 2.0 { buy_score += 0.5; }
|
||||||
|
else if momentum < -5.0 { sell_score += 1.5; }
|
||||||
|
else if momentum < -2.0 { sell_score += 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// EMA crossover events
|
||||||
let prev_ema_bullish = !previous.ema_short.is_nan()
|
let prev_ema_bullish = !previous.ema_short.is_nan()
|
||||||
&& !previous.ema_long.is_nan()
|
&& !previous.ema_long.is_nan()
|
||||||
&& previous.ema_short > previous.ema_long;
|
&& previous.ema_short > previous.ema_long;
|
||||||
@@ -588,47 +605,39 @@ pub fn generate_signal(symbol: &str, current: &IndicatorRow, previous: &Indicato
|
|||||||
buy_score += 2.0;
|
buy_score += 2.0;
|
||||||
} else if !ema_bullish && prev_ema_bullish {
|
} else if !ema_bullish && prev_ema_bullish {
|
||||||
sell_score += 2.0;
|
sell_score += 2.0;
|
||||||
} else if ema_bullish {
|
|
||||||
buy_score += 0.5;
|
|
||||||
} else {
|
|
||||||
sell_score += 0.5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// VOLUME GATE — require minimum volume for signal to be actionable
|
// Volume gate
|
||||||
let has_volume = volume_ratio >= VOLUME_THRESHOLD;
|
if volume_ratio < VOLUME_THRESHOLD {
|
||||||
if !has_volume {
|
|
||||||
// Dampen scores when volume is too low
|
|
||||||
buy_score *= 0.5;
|
buy_score *= 0.5;
|
||||||
sell_score *= 0.5;
|
sell_score *= 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
// DETERMINE SIGNAL
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// SIGNAL DETERMINATION
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
let total_score = buy_score - sell_score;
|
let total_score = buy_score - sell_score;
|
||||||
|
|
||||||
let signal = if total_score >= 6.0 {
|
let signal = if total_score >= 7.0 {
|
||||||
Signal::StrongBuy
|
Signal::StrongBuy
|
||||||
} else if total_score >= 4.5 {
|
} else if total_score >= 4.5 {
|
||||||
Signal::Buy
|
Signal::Buy
|
||||||
} else if total_score <= -6.0 {
|
} else if total_score <= -7.0 {
|
||||||
Signal::StrongSell
|
Signal::StrongSell
|
||||||
} else if total_score <= -3.5 {
|
} else if total_score <= -4.0 {
|
||||||
Signal::Sell
|
Signal::Sell
|
||||||
} else {
|
} else {
|
||||||
Signal::Hold
|
Signal::Hold
|
||||||
};
|
};
|
||||||
|
|
||||||
let confidence = (total_score.abs() / 10.0).min(1.0);
|
let confidence = (total_score.abs() / 12.0).min(1.0);
|
||||||
|
|
||||||
TradeSignal {
|
TradeSignal {
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
signal,
|
signal,
|
||||||
rsi: if rsi.is_nan() { 0.0 } else { rsi },
|
rsi: if rsi.is_nan() { 0.0 } else { rsi },
|
||||||
macd: if macd.is_nan() { 0.0 } else { macd },
|
macd: if current.macd.is_nan() { 0.0 } else { current.macd },
|
||||||
macd_signal: if macd_signal_val.is_nan() {
|
macd_signal: if current.macd_signal.is_nan() { 0.0 } else { current.macd_signal },
|
||||||
0.0
|
|
||||||
} else {
|
|
||||||
macd_signal_val
|
|
||||||
},
|
|
||||||
macd_histogram: if macd_hist.is_nan() { 0.0 } else { macd_hist },
|
macd_histogram: if macd_hist.is_nan() { 0.0 } else { macd_hist },
|
||||||
momentum: if momentum.is_nan() { 0.0 } else { momentum },
|
momentum: if momentum.is_nan() { 0.0 } else { momentum },
|
||||||
ema_short: if ema_short.is_nan() { 0.0 } else { ema_short },
|
ema_short: if ema_short.is_nan() { 0.0 } else { ema_short },
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ pub struct IndicatorRow {
|
|||||||
|
|
||||||
// RSI
|
// RSI
|
||||||
pub rsi: f64,
|
pub rsi: f64,
|
||||||
|
pub rsi_short: f64, // RSI-2/3 for mean reversion
|
||||||
|
|
||||||
// MACD
|
// MACD
|
||||||
pub macd: f64,
|
pub macd: f64,
|
||||||
@@ -182,6 +183,7 @@ impl Default for IndicatorRow {
|
|||||||
close: 0.0,
|
close: 0.0,
|
||||||
volume: 0.0,
|
volume: 0.0,
|
||||||
rsi: 0.0,
|
rsi: 0.0,
|
||||||
|
rsi_short: 0.0,
|
||||||
macd: 0.0,
|
macd: 0.0,
|
||||||
macd_signal: 0.0,
|
macd_signal: 0.0,
|
||||||
macd_histogram: 0.0,
|
macd_histogram: 0.0,
|
||||||
|
|||||||
Reference in New Issue
Block a user