first comit

This commit is contained in:
zastian-dev
2026-02-09 19:20:47 +00:00
commit 79625743bd
24 changed files with 8726 additions and 0 deletions

621
src/indicators.rs Normal file
View File

@@ -0,0 +1,621 @@
//! 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,
};
use crate::types::{Bar, IndicatorRow, Signal, TradeSignal};
/// Calculate Exponential Moving Average (EMA).
pub fn calculate_ema(data: &[f64], period: usize) -> Vec<f64> {
if data.is_empty() || period == 0 {
return vec![];
}
let mut ema = vec![f64::NAN; data.len()];
let multiplier = 2.0 / (period as f64 + 1.0);
// Start with SMA for the first period values
if data.len() >= period {
let sma: f64 = data[..period].iter().sum::<f64>() / period as f64;
ema[period - 1] = sma;
for i in period..data.len() {
ema[i] = (data[i] - ema[i - 1]) * multiplier + ema[i - 1];
}
}
ema
}
/// Calculate Simple Moving Average (SMA).
pub fn calculate_sma(data: &[f64], period: usize) -> Vec<f64> {
if data.is_empty() || period == 0 {
return vec![];
}
let mut sma = vec![f64::NAN; data.len()];
for i in (period - 1)..data.len() {
let sum: f64 = data[(i + 1 - period)..=i].iter().sum();
sma[i] = sum / period as f64;
}
sma
}
/// Calculate standard deviation over a rolling window.
pub fn calculate_rolling_std(data: &[f64], period: usize) -> Vec<f64> {
if data.is_empty() || period == 0 {
return vec![];
}
let mut std = vec![f64::NAN; data.len()];
for i in (period - 1)..data.len() {
let window = &data[(i + 1 - period)..=i];
let mean: f64 = window.iter().sum::<f64>() / period as f64;
let variance: f64 = window.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / period as f64;
std[i] = variance.sqrt();
}
std
}
/// Calculate Relative Strength Index (RSI).
pub fn calculate_rsi(closes: &[f64], period: usize) -> Vec<f64> {
if closes.len() < 2 || period == 0 {
return vec![f64::NAN; closes.len()];
}
let mut rsi = vec![f64::NAN; closes.len()];
// Calculate price changes
let mut gains = vec![0.0; closes.len()];
let mut losses = vec![0.0; closes.len()];
for i in 1..closes.len() {
let change = closes[i] - closes[i - 1];
if change > 0.0 {
gains[i] = change;
} else {
losses[i] = -change;
}
}
// Calculate initial average gain/loss
if closes.len() > period {
let mut avg_gain: f64 = gains[1..=period].iter().sum::<f64>() / period as f64;
let mut avg_loss: f64 = losses[1..=period].iter().sum::<f64>() / period as f64;
if avg_loss == 0.0 {
rsi[period] = 100.0;
} else {
let rs = avg_gain / avg_loss;
rsi[period] = 100.0 - (100.0 / (1.0 + rs));
}
// Smoothed RSI calculation
for i in (period + 1)..closes.len() {
avg_gain = (avg_gain * (period - 1) as f64 + gains[i]) / period as f64;
avg_loss = (avg_loss * (period - 1) as f64 + losses[i]) / period as f64;
if avg_loss == 0.0 {
rsi[i] = 100.0;
} else {
let rs = avg_gain / avg_loss;
rsi[i] = 100.0 - (100.0 / (1.0 + rs));
}
}
}
rsi
}
/// Calculate MACD (Moving Average Convergence Divergence).
pub fn calculate_macd(
closes: &[f64],
fast_period: usize,
slow_period: usize,
signal_period: usize,
) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
let fast_ema = calculate_ema(closes, fast_period);
let slow_ema = calculate_ema(closes, slow_period);
let mut macd_line = vec![f64::NAN; closes.len()];
for i in 0..closes.len() {
if !fast_ema[i].is_nan() && !slow_ema[i].is_nan() {
macd_line[i] = fast_ema[i] - slow_ema[i];
}
}
// Calculate signal line from MACD line (excluding NaN values)
let valid_macd: Vec<f64> = macd_line.iter().copied().filter(|x| !x.is_nan()).collect();
let signal_ema = calculate_ema(&valid_macd, signal_period);
// Map signal EMA back to original indices
let mut signal_line = vec![f64::NAN; closes.len()];
let mut valid_idx = 0;
for i in 0..closes.len() {
if !macd_line[i].is_nan() {
if valid_idx < signal_ema.len() {
signal_line[i] = signal_ema[valid_idx];
}
valid_idx += 1;
}
}
// Calculate histogram
let mut histogram = vec![f64::NAN; closes.len()];
for i in 0..closes.len() {
if !macd_line[i].is_nan() && !signal_line[i].is_nan() {
histogram[i] = macd_line[i] - signal_line[i];
}
}
(macd_line, signal_line, histogram)
}
/// Calculate Rate of Change (Momentum).
pub fn calculate_roc(closes: &[f64], period: usize) -> Vec<f64> {
if closes.is_empty() || period == 0 {
return vec![];
}
let mut roc = vec![f64::NAN; closes.len()];
for i in period..closes.len() {
if closes[i - period] != 0.0 {
roc[i] = ((closes[i] - closes[i - period]) / closes[i - period]) * 100.0;
}
}
roc
}
/// Calculate True Range.
fn calculate_true_range(highs: &[f64], lows: &[f64], closes: &[f64]) -> Vec<f64> {
let mut tr = vec![f64::NAN; highs.len()];
if !highs.is_empty() {
tr[0] = highs[0] - lows[0];
}
for i in 1..highs.len() {
let hl = highs[i] - lows[i];
let hc = (highs[i] - closes[i - 1]).abs();
let lc = (lows[i] - closes[i - 1]).abs();
tr[i] = hl.max(hc).max(lc);
}
tr
}
/// Calculate Average True Range (ATR).
pub fn calculate_atr(highs: &[f64], lows: &[f64], closes: &[f64], period: usize) -> Vec<f64> {
let tr = calculate_true_range(highs, lows, closes);
// Use Wilder's smoothing (similar to EMA but with different multiplier)
let mut atr = vec![f64::NAN; tr.len()];
if tr.len() >= period {
// First ATR is simple average
let first_atr: f64 = tr[..period].iter().filter(|x| !x.is_nan()).sum::<f64>() / period as f64;
atr[period - 1] = first_atr;
// Subsequent ATR values use smoothing
for i in period..tr.len() {
atr[i] = (atr[i - 1] * (period - 1) as f64 + tr[i]) / period as f64;
}
}
atr
}
/// Calculate ADX (Average Directional Index) along with DI+ and DI-.
pub fn calculate_adx(
highs: &[f64],
lows: &[f64],
closes: &[f64],
period: usize,
) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
let len = highs.len();
if len < 2 {
return (vec![f64::NAN; len], vec![f64::NAN; len], vec![f64::NAN; len]);
}
let mut plus_dm = vec![0.0; len];
let mut minus_dm = vec![0.0; len];
// Calculate +DM and -DM
for i in 1..len {
let up_move = highs[i] - highs[i - 1];
let down_move = lows[i - 1] - lows[i];
if up_move > down_move && up_move > 0.0 {
plus_dm[i] = up_move;
}
if down_move > up_move && down_move > 0.0 {
minus_dm[i] = down_move;
}
}
let tr = calculate_true_range(highs, lows, closes);
// Smooth the values using Wilder's smoothing
let mut smoothed_plus_dm = vec![f64::NAN; len];
let mut smoothed_minus_dm = vec![f64::NAN; len];
let mut smoothed_tr = vec![f64::NAN; len];
if len >= period {
smoothed_plus_dm[period - 1] = plus_dm[..period].iter().sum();
smoothed_minus_dm[period - 1] = minus_dm[..period].iter().sum();
smoothed_tr[period - 1] = tr[..period].iter().filter(|x| !x.is_nan()).sum();
for i in period..len {
smoothed_plus_dm[i] =
smoothed_plus_dm[i - 1] - (smoothed_plus_dm[i - 1] / period as f64) + plus_dm[i];
smoothed_minus_dm[i] =
smoothed_minus_dm[i - 1] - (smoothed_minus_dm[i - 1] / period as f64) + minus_dm[i];
smoothed_tr[i] =
smoothed_tr[i - 1] - (smoothed_tr[i - 1] / period as f64) + tr[i];
}
}
// Calculate DI+ and DI-
let mut di_plus = vec![f64::NAN; len];
let mut di_minus = vec![f64::NAN; len];
for i in 0..len {
if !smoothed_tr[i].is_nan() && smoothed_tr[i] != 0.0 {
di_plus[i] = (smoothed_plus_dm[i] / smoothed_tr[i]) * 100.0;
di_minus[i] = (smoothed_minus_dm[i] / smoothed_tr[i]) * 100.0;
}
}
// Calculate DX
let mut dx = vec![f64::NAN; len];
for i in 0..len {
if !di_plus[i].is_nan() && !di_minus[i].is_nan() {
let di_sum = di_plus[i] + di_minus[i];
if di_sum != 0.0 {
dx[i] = ((di_plus[i] - di_minus[i]).abs() / di_sum) * 100.0;
}
}
}
// Calculate ADX (smoothed DX)
let mut adx = vec![f64::NAN; len];
let adx_start = period * 2 - 1;
if len > adx_start {
// First ADX is simple average of DX
let first_adx: f64 = dx[(period - 1)..adx_start]
.iter()
.filter(|x| !x.is_nan())
.sum::<f64>()
/ period as f64;
adx[adx_start - 1] = first_adx;
for i in adx_start..len {
if !dx[i].is_nan() {
adx[i] = (adx[i - 1] * (period - 1) as f64 + dx[i]) / period as f64;
}
}
}
(adx, di_plus, di_minus)
}
/// Calculate Bollinger Bands.
pub fn calculate_bollinger_bands(
closes: &[f64],
period: usize,
std_dev: f64,
) -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>) {
let middle = calculate_sma(closes, period);
let std = calculate_rolling_std(closes, period);
let mut upper = vec![f64::NAN; closes.len()];
let mut lower = vec![f64::NAN; closes.len()];
let mut pct_b = vec![f64::NAN; closes.len()];
for i in 0..closes.len() {
if !middle[i].is_nan() && !std[i].is_nan() {
upper[i] = middle[i] + std_dev * std[i];
lower[i] = middle[i] - std_dev * std[i];
let band_width = upper[i] - lower[i];
if band_width != 0.0 {
pct_b[i] = (closes[i] - lower[i]) / band_width;
}
}
}
(upper, middle, lower, pct_b)
}
/// Calculate all technical indicators for a series of bars.
pub fn calculate_all_indicators(bars: &[Bar], params: &IndicatorParams) -> Vec<IndicatorRow> {
if bars.is_empty() {
return vec![];
}
let closes: Vec<f64> = bars.iter().map(|b| b.close).collect();
let highs: Vec<f64> = bars.iter().map(|b| b.high).collect();
let lows: Vec<f64> = bars.iter().map(|b| b.low).collect();
let volumes: Vec<f64> = bars.iter().map(|b| b.volume).collect();
// Calculate all indicators
let rsi = calculate_rsi(&closes, params.rsi_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);
let ema_short = calculate_ema(&closes, params.ema_short);
let ema_long = calculate_ema(&closes, params.ema_long);
let ema_trend = calculate_ema(&closes, params.ema_trend);
let atr = calculate_atr(&highs, &lows, &closes, params.atr_period);
let (adx, di_plus, di_minus) = calculate_adx(&highs, &lows, &closes, params.adx_period);
let (bb_upper, bb_middle, bb_lower, bb_pct) =
calculate_bollinger_bands(&closes, params.bb_period, BB_STD);
let volume_ma = calculate_sma(&volumes, params.volume_ma_period);
// Build indicator rows
let mut rows = Vec::with_capacity(bars.len());
for i in 0..bars.len() {
let bar = &bars[i];
let vol_ratio = if !volume_ma[i].is_nan() && volume_ma[i] != 0.0 {
bar.volume / volume_ma[i]
} else {
f64::NAN
};
let ema_dist = if !ema_trend[i].is_nan() && ema_trend[i] != 0.0 {
(bar.close - ema_trend[i]) / ema_trend[i]
} else {
f64::NAN
};
let atr_pct_val = if !atr[i].is_nan() && bar.close != 0.0 {
atr[i] / bar.close
} else {
f64::NAN
};
rows.push(IndicatorRow {
timestamp: bar.timestamp,
open: bar.open,
high: bar.high,
low: bar.low,
close: bar.close,
volume: bar.volume,
rsi: rsi[i],
macd: macd[i],
macd_signal: macd_signal[i],
macd_histogram: macd_histogram[i],
momentum: momentum[i],
ema_short: ema_short[i],
ema_long: ema_long[i],
ema_trend: ema_trend[i],
ema_bullish: !ema_short[i].is_nan()
&& !ema_long[i].is_nan()
&& ema_short[i] > ema_long[i],
trend_bullish: !ema_trend[i].is_nan() && bar.close > ema_trend[i],
atr: atr[i],
atr_pct: atr_pct_val,
adx: adx[i],
di_plus: di_plus[i],
di_minus: di_minus[i],
bb_upper: bb_upper[i],
bb_middle: bb_middle[i],
bb_lower: bb_lower[i],
bb_pct: bb_pct[i],
volume_ma: volume_ma[i],
volume_ratio: vol_ratio,
ema_distance: ema_dist,
});
}
rows
}
/// Generate trading signal from current and previous indicator rows.
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 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
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
};
// MACD crossover detection
let macd_crossed_up = !previous.macd.is_nan()
&& !previous.macd_signal.is_nan()
&& !macd.is_nan()
&& !macd_signal_val.is_nan()
&& previous.macd < previous.macd_signal
&& macd > macd_signal_val;
let macd_crossed_down = !previous.macd.is_nan()
&& !previous.macd_signal.is_nan()
&& !macd.is_nan()
&& !macd_signal_val.is_nan()
&& previous.macd > previous.macd_signal
&& macd < macd_signal_val;
// 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
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;
}
}
// OVERSOLD/OVERBOUGHT
if !rsi.is_nan() {
if rsi < RSI_OVERSOLD {
if trend_bullish {
buy_score += 4.0;
} else {
buy_score += 2.0;
}
} else if rsi > RSI_OVERBOUGHT {
sell_score += 3.0;
}
}
// MACD MOMENTUM
if macd_crossed_up {
buy_score += 2.5;
if strong_trend && trend_up {
buy_score += 1.0;
}
} else if macd_crossed_down {
sell_score += 2.5;
} 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;
} else if momentum < -2.0 {
sell_score += 1.0;
}
}
// EMA CROSSOVER
let prev_ema_bullish = !previous.ema_short.is_nan()
&& !previous.ema_long.is_nan()
&& previous.ema_short > previous.ema_long;
if ema_bullish && !prev_ema_bullish {
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 CONFIRMATION
let has_volume = volume_ratio >= VOLUME_THRESHOLD;
if has_volume && volume_ratio > 1.5 {
if buy_score > sell_score {
buy_score += 1.0;
} else if sell_score > buy_score {
sell_score += 1.0;
}
}
// DETERMINE SIGNAL
let total_score = buy_score - sell_score;
let signal = if total_score >= 6.0 {
Signal::StrongBuy
} else if total_score >= 3.5 {
Signal::Buy
} else if total_score <= -6.0 {
Signal::StrongSell
} else if total_score <= -3.5 {
Signal::Sell
} else {
Signal::Hold
};
let confidence = (total_score.abs() / 10.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_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 },
ema_long: if ema_long.is_nan() { 0.0 } else { ema_long },
current_price,
confidence,
}
}