165 lines
5 KiB
Rust
165 lines
5 KiB
Rust
use crate::binance::models::Kline;
|
||
|
||
pub mod red_candle;
|
||
pub mod ema_cross;
|
||
pub mod bollinger_rsi;
|
||
pub mod macd;
|
||
pub mod rsi;
|
||
pub mod supertrend;
|
||
|
||
/// Strateji sinyali
|
||
#[derive(Debug, Clone, PartialEq)]
|
||
pub enum Signal {
|
||
Buy,
|
||
Sell,
|
||
Hold,
|
||
}
|
||
|
||
/// Tüm stratejilerin uygulaması gereken trait
|
||
pub trait Strategy: Send + Sync {
|
||
/// Son kapanmış mumu içeren kline dizisini alır.
|
||
/// candles.last() = son kapanmış mum (açık mum değil)
|
||
fn generate_signal(&self, candles: &[Kline]) -> Signal;
|
||
|
||
/// Alış fiyatından limit satış fiyatı hesaplar
|
||
fn sell_price(&self, buy_price: f64) -> f64;
|
||
|
||
/// Stop loss fiyatı (0.0 = stop loss yok)
|
||
fn stop_price(&self, buy_price: f64) -> f64;
|
||
|
||
/// Sinyal üretmek için gereken minimum mum sayısı
|
||
fn min_candles(&self) -> usize;
|
||
}
|
||
|
||
/// Strateji tipi — DB'de saklanır, config'ten okunur
|
||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||
#[serde(rename_all = "snake_case")]
|
||
pub enum StrategyKind {
|
||
RedCandle,
|
||
EmaCross,
|
||
BollingerRsi,
|
||
Macd,
|
||
Rsi,
|
||
Supertrend,
|
||
}
|
||
|
||
impl StrategyKind {
|
||
pub fn as_str(&self) -> &'static str {
|
||
match self {
|
||
StrategyKind::RedCandle => "red_candle",
|
||
StrategyKind::EmaCross => "ema_cross",
|
||
StrategyKind::BollingerRsi => "bollinger_rsi",
|
||
StrategyKind::Macd => "macd",
|
||
StrategyKind::Rsi => "rsi",
|
||
StrategyKind::Supertrend => "supertrend",
|
||
}
|
||
}
|
||
|
||
pub fn display_name(&self) -> &'static str {
|
||
match self {
|
||
StrategyKind::RedCandle => "Kırmızı Mum",
|
||
StrategyKind::EmaCross => "EMA Crossover",
|
||
StrategyKind::BollingerRsi => "Bollinger + RSI",
|
||
StrategyKind::Macd => "MACD",
|
||
StrategyKind::Rsi => "RSI Aşırı Satım",
|
||
StrategyKind::Supertrend => "Supertrend",
|
||
}
|
||
}
|
||
}
|
||
|
||
impl std::fmt::Display for StrategyKind {
|
||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
write!(f, "{}", self.as_str())
|
||
}
|
||
}
|
||
|
||
impl std::str::FromStr for StrategyKind {
|
||
type Err = ();
|
||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||
match s {
|
||
"red_candle" => Ok(StrategyKind::RedCandle),
|
||
"ema_cross" => Ok(StrategyKind::EmaCross),
|
||
"bollinger_rsi" => Ok(StrategyKind::BollingerRsi),
|
||
"macd" => Ok(StrategyKind::Macd),
|
||
"rsi" => Ok(StrategyKind::Rsi),
|
||
"supertrend" => Ok(StrategyKind::Supertrend),
|
||
_ => Err(()),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// JSON params'tan f64 çeker, yoksa default döner
|
||
pub fn param_f64(params: &serde_json::Value, key: &str, default: f64) -> f64 {
|
||
params.get(key).and_then(|v| v.as_f64()).unwrap_or(default)
|
||
}
|
||
|
||
pub fn param_i64(params: &serde_json::Value, key: &str, default: i64) -> i64 {
|
||
params.get(key).and_then(|v| v.as_i64()).unwrap_or(default)
|
||
}
|
||
|
||
/// EMA hesabı (exponential moving average, span parametreli)
|
||
pub fn ema(values: &[f64], span: usize) -> Vec<f64> {
|
||
if values.is_empty() || span == 0 {
|
||
return vec![];
|
||
}
|
||
let k = 2.0 / (span as f64 + 1.0);
|
||
let mut result = vec![0.0f64; values.len()];
|
||
result[0] = values[0];
|
||
for i in 1..values.len() {
|
||
result[i] = values[i] * k + result[i - 1] * (1.0 - k);
|
||
}
|
||
result
|
||
}
|
||
|
||
/// RSI hesabı (Wilder's smoothing = ewm com=period-1)
|
||
pub fn rsi(closes: &[f64], period: usize) -> Vec<f64> {
|
||
if closes.len() < period + 1 {
|
||
return vec![50.0; closes.len()];
|
||
}
|
||
let k = 1.0 / period as f64; // com = period-1 → alpha = 1/period
|
||
let mut avg_gain = 0.0f64;
|
||
let mut avg_loss = 0.0f64;
|
||
|
||
for i in 1..=period {
|
||
let diff = closes[i] - closes[i - 1];
|
||
if diff > 0.0 { avg_gain += diff; } else { avg_loss += diff.abs(); }
|
||
}
|
||
avg_gain /= period as f64;
|
||
avg_loss /= period as f64;
|
||
|
||
let mut result = vec![50.0f64; closes.len()];
|
||
if avg_loss == 0.0 {
|
||
result[period] = 100.0;
|
||
} else {
|
||
result[period] = 100.0 - 100.0 / (1.0 + avg_gain / avg_loss);
|
||
}
|
||
|
||
for i in (period + 1)..closes.len() {
|
||
let diff = closes[i] - closes[i - 1];
|
||
let gain = if diff > 0.0 { diff } else { 0.0 };
|
||
let loss = if diff < 0.0 { diff.abs() } else { 0.0 };
|
||
avg_gain = avg_gain * (1.0 - k) + gain * k;
|
||
avg_loss = avg_loss * (1.0 - k) + loss * k;
|
||
result[i] = if avg_loss == 0.0 {
|
||
100.0
|
||
} else {
|
||
100.0 - 100.0 / (1.0 + avg_gain / avg_loss)
|
||
};
|
||
}
|
||
result
|
||
}
|
||
|
||
/// ATR hesabı
|
||
pub fn atr(candles: &[Kline], period: usize) -> Vec<f64> {
|
||
if candles.len() < 2 {
|
||
return vec![0.0; candles.len()];
|
||
}
|
||
let mut tr_vals = vec![0.0f64; candles.len()];
|
||
for i in 1..candles.len() {
|
||
let h = candles[i].high;
|
||
let l = candles[i].low;
|
||
let pc = candles[i - 1].close;
|
||
tr_vals[i] = (h - l).max((h - pc).abs()).max((l - pc).abs());
|
||
}
|
||
ema(&tr_vals, period)
|
||
}
|