we ball again
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
//! Technical indicator calculations.
|
||||
|
||||
use crate::config::{
|
||||
IndicatorParams, ADX_STRONG, ADX_THRESHOLD, BB_STD, RSI_OVERBOUGHT, RSI_OVERSOLD,
|
||||
RSI_PULLBACK_HIGH, RSI_PULLBACK_LOW, VOLUME_THRESHOLD,
|
||||
IndicatorParams, ADX_RANGE_THRESHOLD, ADX_STRONG, ADX_TREND_THRESHOLD, BB_STD,
|
||||
RSI2_OVERBOUGHT, RSI2_OVERSOLD, RSI_OVERBOUGHT, RSI_OVERSOLD, VOLUME_THRESHOLD,
|
||||
};
|
||||
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
|
||||
let rsi = calculate_rsi(&closes, params.rsi_period);
|
||||
let rsi_short = calculate_rsi(&closes, params.rsi_short_period);
|
||||
let (macd, macd_signal, macd_histogram) =
|
||||
calculate_macd(&closes, params.macd_fast, params.macd_slow, params.macd_signal);
|
||||
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,
|
||||
volume: bar.volume,
|
||||
rsi: rsi[i],
|
||||
rsi_short: rsi_short[i],
|
||||
macd: macd[i],
|
||||
macd_signal: macd_signal[i],
|
||||
macd_histogram: macd_histogram[i],
|
||||
@@ -421,165 +423,180 @@ pub fn calculate_all_indicators(bars: &[Bar], params: &IndicatorParams) -> Vec<I
|
||||
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 {
|
||||
let rsi = current.rsi;
|
||||
let macd = current.macd;
|
||||
let macd_signal_val = current.macd_signal;
|
||||
let rsi2 = current.rsi_short;
|
||||
let macd_hist = current.macd_histogram;
|
||||
let momentum = current.momentum;
|
||||
let ema_short = current.ema_short;
|
||||
let ema_long = current.ema_long;
|
||||
let current_price = current.close;
|
||||
|
||||
// Advanced indicators
|
||||
// Safe NaN handling
|
||||
let trend_bullish = current.trend_bullish;
|
||||
let volume_ratio = if current.volume_ratio.is_nan() {
|
||||
1.0
|
||||
} else {
|
||||
current.volume_ratio
|
||||
};
|
||||
let adx = if current.adx.is_nan() { 25.0 } else { current.adx };
|
||||
let di_plus = if current.di_plus.is_nan() {
|
||||
25.0
|
||||
} else {
|
||||
current.di_plus
|
||||
};
|
||||
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 ema_distance = if current.ema_distance.is_nan() {
|
||||
0.0
|
||||
} else {
|
||||
current.ema_distance
|
||||
};
|
||||
let volume_ratio = if current.volume_ratio.is_nan() { 1.0 } else { current.volume_ratio };
|
||||
let adx = if current.adx.is_nan() { 22.0 } else { current.adx };
|
||||
let di_plus = if current.di_plus.is_nan() { 25.0 } else { current.di_plus };
|
||||
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 ema_distance = if current.ema_distance.is_nan() { 0.0 } else { current.ema_distance };
|
||||
|
||||
// REGIME DETECTION
|
||||
let is_ranging = adx < ADX_RANGE_THRESHOLD;
|
||||
let is_trending = adx > ADX_TREND_THRESHOLD;
|
||||
let strong_trend = adx > ADX_STRONG;
|
||||
let trend_up = di_plus > di_minus;
|
||||
|
||||
// EMA state
|
||||
let ema_bullish = !ema_short.is_nan() && !ema_long.is_nan() && ema_short > ema_long;
|
||||
|
||||
// MACD crossover detection
|
||||
let macd_crossed_up = !previous.macd.is_nan()
|
||||
&& !previous.macd_signal.is_nan()
|
||||
&& !macd.is_nan()
|
||||
&& !macd_signal_val.is_nan()
|
||||
&& !current.macd.is_nan()
|
||||
&& !current.macd_signal.is_nan()
|
||||
&& previous.macd < previous.macd_signal
|
||||
&& macd > macd_signal_val;
|
||||
&& current.macd > current.macd_signal;
|
||||
|
||||
let macd_crossed_down = !previous.macd.is_nan()
|
||||
&& !previous.macd_signal.is_nan()
|
||||
&& !macd.is_nan()
|
||||
&& !macd_signal_val.is_nan()
|
||||
&& !current.macd.is_nan()
|
||||
&& !current.macd_signal.is_nan()
|
||||
&& 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 sell_score: f64 = 0.0;
|
||||
|
||||
// TREND STRENGTH FILTER
|
||||
if is_trending {
|
||||
if trend_up && trend_bullish {
|
||||
buy_score += 3.0;
|
||||
} else if !trend_up && !trend_bullish {
|
||||
sell_score += 3.0;
|
||||
}
|
||||
} else {
|
||||
// Ranging market - use mean reversion
|
||||
if bb_pct < 0.1 {
|
||||
buy_score += 2.0;
|
||||
} else if bb_pct > 0.9 {
|
||||
sell_score += 2.0;
|
||||
}
|
||||
}
|
||||
|
||||
// PULLBACK ENTRY (buy-side)
|
||||
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;
|
||||
}
|
||||
if bb_pct < 0.3 {
|
||||
buy_score += 2.0;
|
||||
}
|
||||
}
|
||||
|
||||
// PULLBACK EXIT (sell-side symmetry — bearish trend with RSI bounce)
|
||||
if !trend_bullish && !ema_bullish {
|
||||
if !rsi.is_nan() && rsi > (100.0 - RSI_PULLBACK_HIGH) && rsi < (100.0 - RSI_PULLBACK_LOW) {
|
||||
sell_score += 3.0;
|
||||
}
|
||||
if ema_distance < 0.0 && ema_distance > -0.03 {
|
||||
sell_score += 1.5;
|
||||
}
|
||||
if bb_pct > 0.7 {
|
||||
sell_score += 2.0;
|
||||
}
|
||||
}
|
||||
|
||||
// OVERSOLD/OVERBOUGHT (symmetrized)
|
||||
if !rsi.is_nan() {
|
||||
if rsi < RSI_OVERSOLD {
|
||||
if trend_bullish {
|
||||
buy_score += 4.0;
|
||||
} else {
|
||||
buy_score += 2.0;
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// REGIME 1: MEAN REVERSION (ranging market, ADX < 20)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
if is_ranging {
|
||||
// Connors RSI-2 mean reversion: buy extreme oversold in uptrend context
|
||||
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
|
||||
}
|
||||
if bb_pct < 0.05 {
|
||||
buy_score += 2.0; // Price at/below lower BB
|
||||
}
|
||||
} else if rsi2 < 20.0 {
|
||||
buy_score += 2.5;
|
||||
if trend_bullish {
|
||||
buy_score += 1.5;
|
||||
}
|
||||
}
|
||||
} else if rsi > RSI_OVERBOUGHT {
|
||||
if !trend_bullish {
|
||||
|
||||
// Sell: RSI-2 overbought = take profit on mean reversion
|
||||
if rsi2 > RSI2_OVERBOUGHT {
|
||||
sell_score += 4.0;
|
||||
} else {
|
||||
if !trend_bullish {
|
||||
sell_score += 2.0;
|
||||
}
|
||||
} else if rsi2 > 80.0 && !trend_bullish {
|
||||
sell_score += 2.0;
|
||||
}
|
||||
}
|
||||
|
||||
// Bollinger Band extremes in range
|
||||
if bb_pct < 0.0 {
|
||||
buy_score += 2.0; // Below lower band
|
||||
} else if bb_pct > 1.0 {
|
||||
sell_score += 2.0; // Above upper band
|
||||
}
|
||||
}
|
||||
|
||||
// MACD MOMENTUM (symmetrized)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// UNIVERSAL SIGNALS (both regimes)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
// RSI-14 extremes (strong conviction regardless of regime)
|
||||
if !rsi.is_nan() {
|
||||
if rsi < RSI_OVERSOLD && trend_bullish {
|
||||
buy_score += 3.0; // Oversold in uptrend = strong buy
|
||||
} else if rsi > RSI_OVERBOUGHT && !trend_bullish {
|
||||
sell_score += 3.0; // Overbought in downtrend = strong sell
|
||||
}
|
||||
}
|
||||
|
||||
// MACD crossover
|
||||
if macd_crossed_up {
|
||||
buy_score += 2.5;
|
||||
if strong_trend && trend_up {
|
||||
buy_score += 1.0;
|
||||
buy_score += 2.0;
|
||||
if is_trending && trend_up {
|
||||
buy_score += 1.0; // Trend-confirming crossover
|
||||
}
|
||||
} else if macd_crossed_down {
|
||||
sell_score += 2.5;
|
||||
if strong_trend && !trend_up {
|
||||
sell_score += 2.0;
|
||||
if is_trending && !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
|
||||
// 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 += 2.0;
|
||||
} else if momentum > 2.0 {
|
||||
buy_score += 1.0;
|
||||
} else if momentum < -5.0 {
|
||||
sell_score += 2.0;
|
||||
} else if momentum < -2.0 {
|
||||
sell_score += 1.0;
|
||||
}
|
||||
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
|
||||
// EMA crossover events
|
||||
let prev_ema_bullish = !previous.ema_short.is_nan()
|
||||
&& !previous.ema_long.is_nan()
|
||||
&& previous.ema_short > previous.ema_long;
|
||||
@@ -588,47 +605,39 @@ pub fn generate_signal(symbol: &str, current: &IndicatorRow, previous: &Indicato
|
||||
buy_score += 2.0;
|
||||
} else if !ema_bullish && prev_ema_bullish {
|
||||
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
|
||||
let has_volume = volume_ratio >= VOLUME_THRESHOLD;
|
||||
if !has_volume {
|
||||
// Dampen scores when volume is too low
|
||||
// Volume gate
|
||||
if volume_ratio < VOLUME_THRESHOLD {
|
||||
buy_score *= 0.5;
|
||||
sell_score *= 0.5;
|
||||
}
|
||||
|
||||
// DETERMINE SIGNAL
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// SIGNAL DETERMINATION
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
let total_score = buy_score - sell_score;
|
||||
|
||||
let signal = if total_score >= 6.0 {
|
||||
let signal = if total_score >= 7.0 {
|
||||
Signal::StrongBuy
|
||||
} else if total_score >= 4.5 {
|
||||
Signal::Buy
|
||||
} else if total_score <= -6.0 {
|
||||
} else if total_score <= -7.0 {
|
||||
Signal::StrongSell
|
||||
} else if total_score <= -3.5 {
|
||||
} else if total_score <= -4.0 {
|
||||
Signal::Sell
|
||||
} else {
|
||||
Signal::Hold
|
||||
};
|
||||
|
||||
let confidence = (total_score.abs() / 10.0).min(1.0);
|
||||
let confidence = (total_score.abs() / 12.0).min(1.0);
|
||||
|
||||
TradeSignal {
|
||||
symbol: symbol.to_string(),
|
||||
signal,
|
||||
rsi: if rsi.is_nan() { 0.0 } else { rsi },
|
||||
macd: if macd.is_nan() { 0.0 } else { macd },
|
||||
macd_signal: if macd_signal_val.is_nan() {
|
||||
0.0
|
||||
} else {
|
||||
macd_signal_val
|
||||
},
|
||||
macd: if current.macd.is_nan() { 0.0 } else { current.macd },
|
||||
macd_signal: if current.macd_signal.is_nan() { 0.0 } else { current.macd_signal },
|
||||
macd_histogram: if macd_hist.is_nan() { 0.0 } else { macd_hist },
|
||||
momentum: if momentum.is_nan() { 0.0 } else { momentum },
|
||||
ema_short: if ema_short.is_nan() { 0.0 } else { ema_short },
|
||||
|
||||
Reference in New Issue
Block a user