feat(mse): multi-strategy support — EMA Cross, Bollinger+RSI, MACD, RSI, Supertrend

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mukan Erkin TÖRÜK 2026-04-25 21:42:43 +03:00
parent 78657f5ff4
commit dadf7b8394
14 changed files with 967 additions and 225 deletions

View file

@ -40,6 +40,16 @@ pub struct CreateBotRequest {
pub timeframe: crate::binance::models::Timeframe, pub timeframe: crate::binance::models::Timeframe,
pub usdt_amount: f64, pub usdt_amount: f64,
pub profit_percent: f64, pub profit_percent: f64,
#[serde(default)]
pub stop_loss_percent: f64,
#[serde(default = "default_strategy")]
pub strategy: crate::bot::strategy::StrategyKind,
#[serde(default = "serde_json::Value::default")]
pub strategy_params: serde_json::Value,
}
fn default_strategy() -> crate::bot::strategy::StrategyKind {
crate::bot::strategy::StrategyKind::RedCandle
} }
#[derive(Serialize)] #[derive(Serialize)]
@ -88,6 +98,9 @@ pub async fn create_bot(
timeframe: req.timeframe, timeframe: req.timeframe,
usdt_amount: req.usdt_amount, usdt_amount: req.usdt_amount,
profit_percent: req.profit_percent, profit_percent: req.profit_percent,
stop_loss_percent: req.stop_loss_percent,
strategy: req.strategy,
strategy_params: req.strategy_params,
testnet: mode == "testnet", testnet: mode == "testnet",
active: false, active: false,
}; };

View file

@ -247,6 +247,26 @@ impl BinanceClient {
Ok(SymbolFilters { qty_decimals, price_decimals, step_size, min_qty, min_notional }) Ok(SymbolFilters { qty_decimals, price_decimals, step_size, min_qty, min_notional })
} }
/// Kline geçmişi çeker — strateji indikatörü için
pub async fn get_klines(&self, symbol: &str, interval: &str, limit: usize) -> Result<Vec<Kline>> {
let url = format!(
"{}/api/v3/klines?symbol={}&interval={}&limit={}",
self.base_url, symbol, interval, limit
);
let data: Value = self.http.get(&url).send().await?.json().await?;
let arr = data.as_array().ok_or_else(|| anyhow!("klines parse hatası"))?;
let candles = arr.iter().map(|k| Kline {
open_time: k[0].as_i64().unwrap_or(0),
open: k[1].as_str().unwrap_or("0").parse().unwrap_or(0.0),
high: k[2].as_str().unwrap_or("0").parse().unwrap_or(0.0),
low: k[3].as_str().unwrap_or("0").parse().unwrap_or(0.0),
close: k[4].as_str().unwrap_or("0").parse().unwrap_or(0.0),
volume: k[5].as_str().unwrap_or("0").parse().unwrap_or(0.0),
close_time: k[6].as_i64().unwrap_or(0),
}).collect();
Ok(candles)
}
/// Tüm USDT spot işlem çiftlerini döner — public endpoint, API key gerektirmez /// Tüm USDT spot işlem çiftlerini döner — public endpoint, API key gerektirmez
pub async fn get_usdt_symbols(&self) -> Result<Vec<String>> { pub async fn get_usdt_symbols(&self) -> Result<Vec<String>> {
let url = format!("{}/api/v3/ticker/price", self.base_url); let url = format!("{}/api/v3/ticker/price", self.base_url);

View file

@ -8,7 +8,16 @@ use tokio::time::{sleep, Duration};
use tokio_tungstenite::connect_async; use tokio_tungstenite::connect_async;
use crate::binance::{client::BinanceClient, models::Kline}; use crate::binance::{client::BinanceClient, models::Kline};
use crate::bot::strategy::RedCandleStrategy; use crate::bot::strategy::{
Signal, StrategyKind,
red_candle::RedCandleStrategy,
ema_cross::EmaCrossStrategy,
bollinger_rsi::BollingerRsiStrategy,
macd::MacdStrategy,
rsi::RsiStrategy,
supertrend::SupertrendStrategy,
Strategy,
};
use crate::storage::closed::ClosedPosition; use crate::storage::closed::ClosedPosition;
use crate::storage::config::BotConfig; use crate::storage::config::BotConfig;
use crate::storage::db::Database; use crate::storage::db::Database;
@ -16,7 +25,7 @@ use crate::storage::history::TradeRecord;
use crate::storage::positions::OpenPosition; use crate::storage::positions::OpenPosition;
const BINANCE_WS_URL: &str = "wss://stream.binance.com/ws"; const BINANCE_WS_URL: &str = "wss://stream.binance.com/ws";
const BINANCE_TESTNET_WS_URL: &str = "wss://testnet.binance.vision/ws"; const KLINE_HISTORY_LIMIT: usize = 300;
#[derive(Debug, Clone, serde::Serialize)] #[derive(Debug, Clone, serde::Serialize)]
pub struct TradeEvent { pub struct TradeEvent {
@ -32,6 +41,19 @@ pub struct TradeEvent {
pub struct BotRunner; pub struct BotRunner;
/// Config'ten strateji nesnesi oluşturur
fn build_strategy(config: &BotConfig) -> Box<dyn Strategy> {
let params = &config.strategy_params;
match config.strategy {
StrategyKind::RedCandle => Box::new(RedCandleStrategy::new(params, config.profit_percent, config.stop_loss_percent)),
StrategyKind::EmaCross => Box::new(EmaCrossStrategy::new(params)),
StrategyKind::BollingerRsi => Box::new(BollingerRsiStrategy::new(params)),
StrategyKind::Macd => Box::new(MacdStrategy::new(params)),
StrategyKind::Rsi => Box::new(RsiStrategy::new(params)),
StrategyKind::Supertrend => Box::new(SupertrendStrategy::new(params)),
}
}
impl BotRunner { impl BotRunner {
pub async fn run( pub async fn run(
config: BotConfig, config: BotConfig,
@ -42,13 +64,31 @@ impl BotRunner {
event_tx: broadcast::Sender<TradeEvent>, event_tx: broadcast::Sender<TradeEvent>,
) { ) {
let client = BinanceClient::new(api_key.clone(), api_secret.clone(), config.testnet); let client = BinanceClient::new(api_key.clone(), api_secret.clone(), config.testnet);
// Fiyat verisi her zaman canlı Binance stream'inden gelir
let ws_base = BINANCE_WS_URL; let ws_base = BINANCE_WS_URL;
let stream = format!("{}/{}@kline_{}", ws_base, config.symbol.to_lowercase(), config.timeframe.as_str()); let stream = format!("{}/{}@kline_{}", ws_base, config.symbol.to_lowercase(), config.timeframe.as_str());
let strategy = build_strategy(&config);
let min_candles = strategy.min_candles();
info!("[{}] Bot başlatıldı. WS: {}", config.symbol, stream); info!("[{}] Bot başlatıldı. Strateji: {} | WS: {}", config.symbol, config.strategy, stream);
// Açık pozisyonların order durumunu periyodik sorgula // Kline geçmişini tut — strateji indikatörü için
let mut candle_buf: Vec<Kline> = Vec::with_capacity(KLINE_HISTORY_LIMIT + 10);
// Başlangıçta geçmiş kline'ları çek
let history_limit = (min_candles + 50).max(KLINE_HISTORY_LIMIT);
match client.get_klines(&config.symbol, config.timeframe.as_str(), history_limit).await {
Ok(mut hist) => {
// Son mum henüz kapanmamış olabilir, onu çıkar
if hist.len() > 1 { hist.pop(); }
info!("[{}] {} adet geçmiş mum yüklendi.", config.symbol, hist.len());
candle_buf.extend(hist);
}
Err(e) => {
warn!("[{}] Geçmiş mum çekilemedi: {}", config.symbol, e);
}
}
// Açık pozisyon kontrolü için arka plan task
{ {
let db2 = Arc::clone(&db); let db2 = Arc::clone(&db);
let ev2 = event_tx.clone(); let ev2 = event_tx.clone();
@ -94,100 +134,54 @@ impl BotRunner {
Err(_) => continue, Err(_) => continue,
}; };
let kline = match parse_kline_message(&text) { let kline_ws = match parse_kline_message(&text) {
Some(k) => k, Some(k) => k,
None => continue, None => continue,
}; };
// Sadece kapanmış mumları işle // Sadece kapanmış mumları işle
if !kline.is_closed { if !kline_ws.is_closed { continue; }
continue;
}
let kline = Kline::from(&kline_ws);
info!("[{}] Mum kapandı. Açılış: {:.6} | Kapanış: {:.6}", config.symbol, kline.open, kline.close); info!("[{}] Mum kapandı. Açılış: {:.6} | Kapanış: {:.6}", config.symbol, kline.open, kline.close);
{ {
let db_guard = db.lock().await; let db_guard = db.lock().await;
db_guard.insert_log(&config.id, "info", &format!("Mum kapandı. Açılış: {:.6} | Kapanış: {:.6}", kline.open, kline.close)).ok(); db_guard.insert_log(&config.id, "info", &format!("Mum kapandı. Açılış: {:.6} | Kapanış: {:.6}", kline.open, kline.close)).ok();
} }
let kline_data = Kline::from(&kline); // Buffer güncelle
match RedCandleStrategy::execute( candle_buf.push(kline.clone());
&client, if candle_buf.len() > KLINE_HISTORY_LIMIT + 50 {
&config.symbol, candle_buf.drain(0..50);
&kline_data, }
config.usdt_amount,
config.profit_percent, // Yeterli mum var mı?
) if candle_buf.len() < min_candles {
.await info!("[{}] Strateji için yeterli mum yok ({}/{}), bekleniyor.", config.symbol, candle_buf.len(), min_candles);
{ continue;
Ok(Some(result)) => { }
info!(
"[{}] İşlem | Alış: {:.6} | Satış hedefi: {:.6} | Kar: %{:.2}", // Sinyal üret
config.symbol, result.buy_order.price, result.sell_order.price, config.profit_percent let signal = strategy.generate_signal(&candle_buf);
);
{ match signal {
Signal::Buy => {
info!("[{}] BUY sinyali! Strateji: {}", config.symbol, config.strategy);
if let Err(e) = execute_buy(&client, &config, &strategy, &kline, &db, &event_tx).await {
error!("[{}] Alım hatası: {:?}", config.symbol, e);
let db_guard = db.lock().await; let db_guard = db.lock().await;
db_guard.insert_log(&config.id, "buy", &format!("İşlem gerçekleşti | Alış: {:.6} | Satış hedefi: {:.6} | Miktar: {:.6} | Kar: %{:.2}", result.buy_order.price, result.sell_order.price, result.buy_order.quantity, config.profit_percent)).ok(); db_guard.insert_log(&config.id, "error", &format!("Alım hatası: {}", e)).ok();
}
let record = TradeRecord {
bot_id: config.id.clone(),
bot_name: config.name.clone(),
symbol: config.symbol.clone(),
buy_price: result.buy_order.price,
sell_target: result.sell_order.price,
quantity: result.buy_order.quantity,
profit_percent: config.profit_percent,
timestamp: result.timestamp,
};
let position = OpenPosition {
bot_id: config.id.clone(),
bot_name: config.name.clone(),
symbol: config.symbol.clone(),
order_id: result.sell_order.order_id,
buy_price: result.buy_order.price,
sell_target: result.sell_order.price,
quantity: result.buy_order.quantity,
profit_percent: config.profit_percent,
opened_at: result.timestamp,
buy_commission_usdt: result.buy_order.commission_usdt,
};
{
let db_guard = db.lock().await;
if let Err(e) = db_guard.insert_trade(&record) {
error!("[{}] DB trade kayıt hatası: {}", config.symbol, e);
}
if let Err(e) = db_guard.insert_position(&position) {
error!("[{}] DB pozisyon kayıt hatası: {}", config.symbol, e);
}
}
event_tx.send(TradeEvent {
bot_id: config.id.clone(),
bot_name: config.name.clone(),
symbol: config.symbol.clone(),
buy_price: result.buy_order.price,
sell_target: result.sell_order.price,
quantity: result.buy_order.quantity,
profit_percent: config.profit_percent,
timestamp: result.timestamp,
}).ok();
}
Ok(None) => {
info!("[{}] İşlem yapılmadı (yeşil mum).", config.symbol);
{
let db_guard = db.lock().await;
db_guard.insert_log(&config.id, "skip", "Yeşil mum, işlem yapılmadı.").ok();
} }
} }
Err(e) => { Signal::Sell => {
error!("[{}] Strateji hatası: {:?}", config.symbol, e); info!("[{}] SELL sinyali (strateji) — açık pozisyon kontrolü yapılmıyor, bot bakkal modunda çalışıyor.", config.symbol);
{ let db_guard = db.lock().await;
let db_guard = db.lock().await; db_guard.insert_log(&config.id, "skip", "Sell sinyali — bakkal modunda görmezden gelindi.").ok();
db_guard.insert_log(&config.id, "error", &format!("Strateji hatası: {}", e)).ok(); }
} Signal::Hold => {
info!("[{}] HOLD — işlem yok.", config.symbol);
let db_guard = db.lock().await;
db_guard.insert_log(&config.id, "skip", "Hold sinyali, işlem yapılmadı.").ok();
} }
} }
} }
@ -211,15 +205,118 @@ impl BotRunner {
} }
} }
if *shutdown.lock().await { if *shutdown.lock().await { break; }
break;
}
sleep(Duration::from_secs(5)).await; sleep(Duration::from_secs(5)).await;
} }
} }
} }
async fn execute_buy(
client: &BinanceClient,
config: &BotConfig,
strategy: &Box<dyn Strategy>,
kline: &Kline,
db: &Arc<Mutex<Database>>,
event_tx: &broadcast::Sender<TradeEvent>,
) -> anyhow::Result<()> {
// Bakiye kontrolü
let usdt_balance = client.get_usdt_spot_balance().await?;
if usdt_balance < config.usdt_amount {
info!("[{}] Yetersiz USDT bakiyesi ({:.2} < {:.2}), bekleniyor", config.symbol, usdt_balance, config.usdt_amount);
let db_guard = db.lock().await;
db_guard.insert_log(&config.id, "skip", &format!("Yetersiz bakiye: {:.2} USDT", usdt_balance)).ok();
return Ok(());
}
let filters = client.get_symbol_filters(&config.symbol).await?;
let raw_qty = config.usdt_amount / kline.close;
let quantity = filters.format_qty(raw_qty);
let qty_f: f64 = quantity.parse().unwrap_or(0.0);
if !filters.qty_is_valid(qty_f) {
return Err(anyhow::anyhow!(
"Miktar ({}) min lot size ({}) altında — USDT miktarını artır",
quantity, filters.min_qty
));
}
if !filters.notional_is_valid(qty_f, kline.close) {
return Err(anyhow::anyhow!(
"İşlem tutarı ({:.2} USDT) min notional ({} USDT) altında — USDT miktarını artır",
qty_f * kline.close, filters.min_notional
));
}
info!("[{}] Market alım | Miktar: {} | Fiyat tahmini: {:.6}", config.symbol, quantity, kline.close);
let buy_order = client.market_buy(&config.symbol, &quantity).await?;
info!(
"[{}] Alım gerçekleşti | Fiyat: {:.6} | Miktar: {} | Emir ID: {}",
config.symbol, buy_order.price, filters.format_qty(buy_order.quantity), buy_order.order_id
);
let sell_price = strategy.sell_price(buy_order.price);
let sell_price_str = filters.format_price(sell_price);
let sell_quantity = filters.format_qty(buy_order.quantity);
let sell_order = client.limit_sell(&config.symbol, &sell_quantity, &sell_price_str).await?;
info!(
"[{}] Limit satış emri | Fiyat: {:.6} | Emir ID: {}",
config.symbol, sell_price, sell_order.order_id
);
let timestamp = chrono::Utc::now().timestamp_millis();
let record = TradeRecord {
bot_id: config.id.clone(),
bot_name: config.name.clone(),
symbol: config.symbol.clone(),
buy_price: buy_order.price,
sell_target: sell_price,
quantity: buy_order.quantity,
profit_percent: config.profit_percent,
timestamp,
};
let position = OpenPosition {
bot_id: config.id.clone(),
bot_name: config.name.clone(),
symbol: config.symbol.clone(),
order_id: sell_order.order_id,
buy_price: buy_order.price,
sell_target: sell_price,
quantity: buy_order.quantity,
profit_percent: config.profit_percent,
opened_at: timestamp,
buy_commission_usdt: buy_order.commission_usdt,
};
{
let db_guard = db.lock().await;
db_guard.insert_log(&config.id, "buy", &format!(
"İşlem gerçekleşti | Alış: {:.6} | Satış hedefi: {:.6} | Miktar: {:.6} | Kar: %{:.2}",
buy_order.price, sell_price, buy_order.quantity, config.profit_percent
)).ok();
if let Err(e) = db_guard.insert_trade(&record) {
error!("[{}] DB trade kayıt hatası: {}", config.symbol, e);
}
if let Err(e) = db_guard.insert_position(&position) {
error!("[{}] DB pozisyon kayıt hatası: {}", config.symbol, e);
}
}
event_tx.send(TradeEvent {
bot_id: config.id.clone(),
bot_name: config.name.clone(),
symbol: config.symbol.clone(),
buy_price: buy_order.price,
sell_target: sell_price,
quantity: buy_order.quantity,
profit_percent: config.profit_percent,
timestamp,
}).ok();
Ok(())
}
struct KlineWs { struct KlineWs {
open: f64, open: f64,
close: f64, close: f64,
@ -246,9 +343,17 @@ fn parse_kline_message(text: &str) -> Option<KlineWs> {
}) })
} }
impl KlineWs { impl From<&KlineWs> for Kline {
fn is_red(&self) -> bool { fn from(k: &KlineWs) -> Self {
self.close < self.open Kline {
open_time: k.open_time,
close_time: k.close_time,
open: k.open,
close: k.close,
high: k.high,
low: k.low,
volume: k.volume,
}
} }
} }
@ -313,18 +418,3 @@ async fn check_open_positions(
} }
} }
} }
// Kline dönüşümü strategy için
impl From<&KlineWs> for Kline {
fn from(k: &KlineWs) -> Self {
Kline {
open_time: k.open_time,
close_time: k.close_time,
open: k.open,
close: k.close,
high: k.high,
low: k.low,
volume: k.volume,
}
}
}

View file

@ -1,95 +0,0 @@
use anyhow::Result;
use chrono::Utc;
use log::info;
use crate::binance::{client::BinanceClient, models::Kline};
pub struct RedCandleStrategy;
impl RedCandleStrategy {
/// Kapanmış kline verilir. Kırmızıysa market alım + limit satış emri girer.
pub async fn execute(
client: &BinanceClient,
symbol: &str,
kline: &Kline,
usdt_amount: f64,
profit_percent: f64,
) -> Result<Option<TradeResult>> {
if !kline.is_red() {
info!(
"[{}] Son mum kırmızı değil. Açılış: {:.6} | Kapanış: {:.6}",
symbol, kline.open, kline.close
);
return Ok(None);
}
info!(
"[{}] KIRMIZI MUM! Açılış: {:.6} | Kapanış: {:.6} | Alım yapılıyor...",
symbol, kline.open, kline.close
);
let usdt_balance = client.get_usdt_spot_balance().await?;
if usdt_balance < usdt_amount {
info!(
"[{}] Yetersiz USDT bakiyesi ({:.2} < {:.2}), bekleniyor",
symbol, usdt_balance, usdt_amount
);
return Ok(None);
}
let filters = client.get_symbol_filters(symbol).await?;
let raw_qty = usdt_amount / kline.close;
let quantity = filters.format_qty(raw_qty);
let qty_f: f64 = quantity.parse().unwrap_or(0.0);
if !filters.qty_is_valid(qty_f) {
return Err(anyhow::anyhow!(
"Miktar ({}) min lot size ({}) altında — USDT miktarını artır",
quantity, filters.min_qty
));
}
if !filters.notional_is_valid(qty_f, kline.close) {
return Err(anyhow::anyhow!(
"İşlem tutarı ({:.2} USDT) min notional ({} USDT) altında — USDT miktarını artır",
qty_f * kline.close, filters.min_notional
));
}
info!("[{}] Market alım | Miktar: {} | Fiyat tahmini: {:.6}", symbol, quantity, kline.close);
let buy_order = client.market_buy(symbol, &quantity).await?;
info!(
"[{}] Alım gerçekleşti | Fiyat: {:.6} | Miktar: {} | Emir ID: {}",
symbol, buy_order.price, filters.format_qty(buy_order.quantity), buy_order.order_id
);
// +0.2: alış (%0.1) + satış (%0.1) komisyonları dahil, net kar = profit_percent
let sell_price = buy_order.price * (1.0 + (profit_percent + 0.2) / 100.0);
let sell_price_str = filters.format_price(sell_price);
let sell_quantity = filters.format_qty(buy_order.quantity);
let sell_order = client.limit_sell(symbol, &sell_quantity, &sell_price_str).await?;
info!(
"[{}] Limit satış emri | Fiyat: {:.6} | Emir ID: {}",
symbol, sell_price, sell_order.order_id
);
Ok(Some(TradeResult {
symbol: symbol.to_string(),
kline: kline.clone(),
buy_order,
sell_order,
profit_percent,
timestamp: Utc::now().timestamp_millis(),
}))
}
}
#[derive(Debug, Clone)]
pub struct TradeResult {
pub symbol: String,
pub kline: Kline,
pub buy_order: crate::binance::models::FilledOrder,
pub sell_order: crate::binance::models::FilledOrder,
pub profit_percent: f64,
pub timestamp: i64,
}

View file

@ -0,0 +1,71 @@
use serde_json::Value;
use crate::binance::models::Kline;
use super::{Signal, Strategy, param_f64, param_i64, rsi};
pub struct BollingerRsiStrategy {
bb_period: usize,
bb_std: f64,
rsi_period: usize,
rsi_oversold: f64,
rsi_overbought: f64,
profit_pct: f64,
stop_loss_pct: f64,
}
impl BollingerRsiStrategy {
pub fn new(params: &Value) -> Self {
Self {
bb_period: param_i64(params, "bb_period", 20) as usize,
bb_std: param_f64(params, "bb_std", 2.0),
rsi_period: param_i64(params, "rsi_period", 14) as usize,
rsi_oversold: param_f64(params, "rsi_oversold", 35.0),
rsi_overbought: param_f64(params, "rsi_overbought", 65.0),
profit_pct: param_f64(params, "profit_target_pct", 1.5),
stop_loss_pct: param_f64(params, "stop_loss_pct", 2.0),
}
}
}
impl Strategy for BollingerRsiStrategy {
fn generate_signal(&self, candles: &[Kline]) -> Signal {
let n = candles.len();
if n < self.min_candles() { return Signal::Hold; }
let closes: Vec<f64> = candles.iter().map(|c| c.close).collect();
// Bollinger Bands (son kapanmış mum = n-2)
let idx = n - 2;
let start = if idx + 1 >= self.bb_period { idx + 1 - self.bb_period } else { 0 };
let window = &closes[start..=idx];
let sma: f64 = window.iter().sum::<f64>() / window.len() as f64;
let variance: f64 = window.iter().map(|v| (v - sma).powi(2)).sum::<f64>() / window.len() as f64;
let std = variance.sqrt();
let upper = sma + self.bb_std * std;
let lower = sma - self.bb_std * std;
// RSI
let rsi_vals = rsi(&closes, self.rsi_period);
let curr_rsi = rsi_vals[idx];
let curr_close = closes[idx];
if curr_close <= lower && curr_rsi <= self.rsi_oversold {
return Signal::Buy;
}
if curr_close >= upper && curr_rsi >= self.rsi_overbought {
return Signal::Sell;
}
Signal::Hold
}
fn sell_price(&self, buy_price: f64) -> f64 {
buy_price * (1.0 + (self.profit_pct + 0.2) / 100.0)
}
fn stop_price(&self, buy_price: f64) -> f64 {
if self.stop_loss_pct > 0.0 { buy_price * (1.0 - self.stop_loss_pct / 100.0) } else { 0.0 }
}
fn min_candles(&self) -> usize {
self.bb_period + self.rsi_period + 5
}
}

View file

@ -0,0 +1,101 @@
use serde_json::Value;
use crate::binance::models::Kline;
use super::{Signal, Strategy, param_f64, param_i64, ema, atr};
pub struct EmaCrossStrategy {
fast_period: usize,
slow_period: usize,
adx_period: usize,
adx_threshold: f64,
profit_pct: f64,
stop_loss_pct: f64,
}
impl EmaCrossStrategy {
pub fn new(params: &Value) -> Self {
Self {
fast_period: param_i64(params, "fast_period", 9) as usize,
slow_period: param_i64(params, "slow_period", 21) as usize,
adx_period: param_i64(params, "adx_period", 14) as usize,
adx_threshold: param_f64(params, "adx_threshold", 20.0),
profit_pct: param_f64(params, "profit_target_pct", 2.0),
stop_loss_pct: param_f64(params, "stop_loss_pct", 1.5),
}
}
fn calc_adx(&self, candles: &[Kline]) -> f64 {
let n = candles.len();
if n < self.adx_period + 2 { return 0.0; }
let p = self.adx_period;
let atr_vals = atr(candles, p);
let mut plus_dm = vec![0.0f64; n];
let mut minus_dm = vec![0.0f64; n];
for i in 1..n {
let up = candles[i].high - candles[i-1].high;
let down = candles[i-1].low - candles[i].low;
if up > down && up > 0.0 { plus_dm[i] = up; }
if down > up && down > 0.0 { minus_dm[i] = down; }
}
let plus_dm_ema = ema(&plus_dm, p);
let minus_dm_ema = ema(&minus_dm, p);
let mut dx_vals = vec![0.0f64; n];
for i in 0..n {
let a = atr_vals[i];
if a == 0.0 { continue; }
let plus_di = 100.0 * plus_dm_ema[i] / a;
let minus_di = 100.0 * minus_dm_ema[i] / a;
let denom = plus_di + minus_di;
if denom > 0.0 {
dx_vals[i] = 100.0 * (plus_di - minus_di).abs() / denom;
}
}
let adx = ema(&dx_vals, p);
// son kapanmış mum = index n-2
if n >= 2 { adx[n - 2] } else { 0.0 }
}
}
impl Strategy for EmaCrossStrategy {
fn generate_signal(&self, candles: &[Kline]) -> Signal {
let n = candles.len();
if n < self.min_candles() { return Signal::Hold; }
let closes: Vec<f64> = candles.iter().map(|c| c.close).collect();
let fast = ema(&closes, self.fast_period);
let slow = ema(&closes, self.slow_period);
// son kapanmış = n-2, bir önceki = n-3
let (pf, cf) = (fast[n-3], fast[n-2]);
let (ps, cs) = (slow[n-3], slow[n-2]);
let cross_up = pf <= ps && cf > cs;
let cross_down = pf >= ps && cf < cs;
if !cross_up && !cross_down { return Signal::Hold; }
if self.adx_threshold > 0.0 {
let adx = self.calc_adx(candles);
if adx < self.adx_threshold { return Signal::Hold; }
}
if cross_up { Signal::Buy }
else { Signal::Sell }
}
fn sell_price(&self, buy_price: f64) -> f64 {
buy_price * (1.0 + (self.profit_pct + 0.2) / 100.0)
}
fn stop_price(&self, buy_price: f64) -> f64 {
if self.stop_loss_pct > 0.0 { buy_price * (1.0 - self.stop_loss_pct / 100.0) } else { 0.0 }
}
fn min_candles(&self) -> usize {
self.slow_period + self.adx_period + 10
}
}

68
src/bot/strategy/macd.rs Normal file
View file

@ -0,0 +1,68 @@
use serde_json::Value;
use crate::binance::models::Kline;
use super::{Signal, Strategy, param_f64, param_i64, ema};
pub struct MacdStrategy {
fast_period: usize,
slow_period: usize,
signal_period: usize,
require_zero: bool,
profit_pct: f64,
stop_loss_pct: f64,
}
impl MacdStrategy {
pub fn new(params: &Value) -> Self {
Self {
fast_period: param_i64(params, "fast_period", 12) as usize,
slow_period: param_i64(params, "slow_period", 26) as usize,
signal_period: param_i64(params, "signal_period", 9) as usize,
require_zero: params.get("require_zero_cross").and_then(|v| v.as_bool()).unwrap_or(false),
profit_pct: param_f64(params, "profit_target_pct", 2.0),
stop_loss_pct: param_f64(params, "stop_loss_pct", 2.0),
}
}
}
impl Strategy for MacdStrategy {
fn generate_signal(&self, candles: &[Kline]) -> Signal {
let n = candles.len();
if n < self.min_candles() { return Signal::Hold; }
let closes: Vec<f64> = candles.iter().map(|c| c.close).collect();
let fast_ema = ema(&closes, self.fast_period);
let slow_ema = ema(&closes, self.slow_period);
let macd_line: Vec<f64> = fast_ema.iter().zip(slow_ema.iter()).map(|(f, s)| f - s).collect();
let signal_line = ema(&macd_line, self.signal_period);
let idx = n - 2; // son kapanmış mum
let (pm, cm) = (macd_line[idx - 1], macd_line[idx]);
let (ps, cs) = (signal_line[idx - 1], signal_line[idx]);
let cross_up = pm <= ps && cm > cs;
let cross_down = pm >= ps && cm < cs;
if !cross_up && !cross_down { return Signal::Hold; }
if self.require_zero {
if cross_up && cm < 0.0 { return Signal::Hold; }
if cross_down && cm > 0.0 { return Signal::Hold; }
}
if cross_up { Signal::Buy }
else { Signal::Sell }
}
fn sell_price(&self, buy_price: f64) -> f64 {
buy_price * (1.0 + (self.profit_pct + 0.2) / 100.0)
}
fn stop_price(&self, buy_price: f64) -> f64 {
if self.stop_loss_pct > 0.0 { buy_price * (1.0 - self.stop_loss_pct / 100.0) } else { 0.0 }
}
fn min_candles(&self) -> usize {
self.slow_period + self.signal_period + 10
}
}

165
src/bot/strategy/mod.rs Normal file
View file

@ -0,0 +1,165 @@
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)
}

View file

@ -0,0 +1,42 @@
use serde_json::Value;
use crate::binance::models::Kline;
use super::{Signal, Strategy, param_f64};
pub struct RedCandleStrategy {
profit_pct: f64,
stop_loss_pct: f64,
}
impl RedCandleStrategy {
pub fn new(params: &Value, profit_pct: f64, stop_loss_pct: f64) -> Self {
Self {
profit_pct: param_f64(params, "profit_target_pct", profit_pct),
stop_loss_pct: param_f64(params, "stop_loss_pct", stop_loss_pct),
}
}
}
impl Strategy for RedCandleStrategy {
fn generate_signal(&self, candles: &[Kline]) -> Signal {
let last = match candles.last() {
Some(c) => c,
None => return Signal::Hold,
};
if last.close < last.open { Signal::Buy } else { Signal::Hold }
}
fn sell_price(&self, buy_price: f64) -> f64 {
// +0.2 komisyon dahil, net kar = profit_pct
buy_price * (1.0 + (self.profit_pct + 0.2) / 100.0)
}
fn stop_price(&self, buy_price: f64) -> f64 {
if self.stop_loss_pct > 0.0 {
buy_price * (1.0 - self.stop_loss_pct / 100.0)
} else {
0.0
}
}
fn min_candles(&self) -> usize { 1 }
}

65
src/bot/strategy/rsi.rs Normal file
View file

@ -0,0 +1,65 @@
use serde_json::Value;
use crate::binance::models::Kline;
use super::{Signal, Strategy, param_f64, param_i64, rsi};
pub struct RsiStrategy {
period: usize,
oversold: f64,
overbought: f64,
exit_level: f64,
profit_pct: f64,
stop_loss_pct: f64,
}
impl RsiStrategy {
pub fn new(params: &Value) -> Self {
Self {
period: param_i64(params, "rsi_period", 14) as usize,
oversold: param_f64(params, "rsi_oversold", 30.0),
overbought: param_f64(params, "rsi_overbought", 70.0),
exit_level: param_f64(params, "rsi_exit", 50.0),
profit_pct: param_f64(params, "profit_target_pct", 2.0),
stop_loss_pct: param_f64(params, "stop_loss_pct", 3.0),
}
}
}
impl Strategy for RsiStrategy {
fn generate_signal(&self, candles: &[Kline]) -> Signal {
let n = candles.len();
if n < self.min_candles() { return Signal::Hold; }
let closes: Vec<f64> = candles.iter().map(|c| c.close).collect();
let rsi_vals = rsi(&closes, self.period);
let idx = n - 2; // son kapanmış mum
let curr = rsi_vals[idx];
let prev = rsi_vals[idx - 1];
// BUY: oversold bölgesine girdi (geçiş anı)
if prev >= self.oversold && curr < self.oversold {
return Signal::Buy;
}
// SELL: aşırı alım
if curr >= self.overbought {
return Signal::Sell;
}
// SELL: orta seviyeye döndü
if self.exit_level > 0.0 && prev < self.exit_level && curr >= self.exit_level {
return Signal::Sell;
}
Signal::Hold
}
fn sell_price(&self, buy_price: f64) -> f64 {
buy_price * (1.0 + (self.profit_pct + 0.2) / 100.0)
}
fn stop_price(&self, buy_price: f64) -> f64 {
if self.stop_loss_pct > 0.0 { buy_price * (1.0 - self.stop_loss_pct / 100.0) } else { 0.0 }
}
fn min_candles(&self) -> usize {
self.period + 10
}
}

View file

@ -0,0 +1,73 @@
use serde_json::Value;
use crate::binance::models::Kline;
use super::{Signal, Strategy, param_f64, param_i64, atr};
pub struct SupertrendStrategy {
atr_period: usize,
multiplier: f64,
profit_pct: f64,
stop_loss_pct: f64,
}
impl SupertrendStrategy {
pub fn new(params: &Value) -> Self {
Self {
atr_period: param_i64(params, "atr_period", 10) as usize,
multiplier: param_f64(params, "multiplier", 3.0),
profit_pct: param_f64(params, "profit_target_pct", 3.0),
stop_loss_pct: param_f64(params, "stop_loss_pct", 2.0),
}
}
fn calc_direction(&self, candles: &[Kline]) -> Vec<i8> {
let n = candles.len();
let atr_vals = atr(candles, self.atr_period);
let mut upper = vec![0.0f64; n];
let mut lower = vec![0.0f64; n];
let mut dir = vec![1i8; n];
for i in 1..n {
let hl2 = (candles[i].high + candles[i].low) / 2.0;
let a = atr_vals[i];
let ub = hl2 + self.multiplier * a;
let lb = hl2 - self.multiplier * a;
upper[i] = if ub < upper[i-1] || candles[i-1].close > upper[i-1] { ub } else { upper[i-1] };
lower[i] = if lb > lower[i-1] || candles[i-1].close < lower[i-1] { lb } else { lower[i-1] };
let prev_dir = dir[i-1];
dir[i] = if candles[i].close > upper[i] { 1 }
else if candles[i].close < lower[i] { -1 }
else { prev_dir };
}
dir
}
}
impl Strategy for SupertrendStrategy {
fn generate_signal(&self, candles: &[Kline]) -> Signal {
let n = candles.len();
if n < self.min_candles() { return Signal::Hold; }
let dir = self.calc_direction(candles);
let curr = dir[n - 2]; // son kapanmış mum
let prev = dir[n - 3];
if prev == -1 && curr == 1 { Signal::Buy }
else if prev == 1 && curr == -1 { Signal::Sell }
else { Signal::Hold }
}
fn sell_price(&self, buy_price: f64) -> f64 {
buy_price * (1.0 + (self.profit_pct + 0.2) / 100.0)
}
fn stop_price(&self, buy_price: f64) -> f64 {
if self.stop_loss_pct > 0.0 { buy_price * (1.0 - self.stop_loss_pct / 100.0) } else { 0.0 }
}
fn min_candles(&self) -> usize {
self.atr_period * 3 + 5
}
}

View file

@ -1,6 +1,7 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::binance::models::Timeframe; use crate::binance::models::Timeframe;
use crate::bot::strategy::StrategyKind;
/// Tek bir botun konfigürasyonu /// Tek bir botun konfigürasyonu
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -11,6 +12,9 @@ pub struct BotConfig {
pub timeframe: Timeframe, pub timeframe: Timeframe,
pub usdt_amount: f64, pub usdt_amount: f64,
pub profit_percent: f64, pub profit_percent: f64,
pub stop_loss_percent: f64,
pub strategy: StrategyKind,
pub strategy_params: serde_json::Value,
pub testnet: bool, pub testnet: bool,
pub active: bool, pub active: bool,
} }

View file

@ -23,14 +23,17 @@ impl Database {
conn.execute_batch( conn.execute_batch(
"PRAGMA journal_mode=WAL; "PRAGMA journal_mode=WAL;
CREATE TABLE IF NOT EXISTS bots ( CREATE TABLE IF NOT EXISTS bots (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
name TEXT NOT NULL, name TEXT NOT NULL,
symbol TEXT NOT NULL, symbol TEXT NOT NULL,
timeframe TEXT NOT NULL, timeframe TEXT NOT NULL,
usdt_amount REAL NOT NULL, usdt_amount REAL NOT NULL,
profit_percent REAL NOT NULL, profit_percent REAL NOT NULL,
testnet INTEGER NOT NULL DEFAULT 0, stop_loss_percent REAL NOT NULL DEFAULT 0,
active INTEGER NOT NULL DEFAULT 0 strategy TEXT NOT NULL DEFAULT 'red_candle',
strategy_params TEXT NOT NULL DEFAULT '{}',
testnet INTEGER NOT NULL DEFAULT 0,
active INTEGER NOT NULL DEFAULT 0
); );
CREATE TABLE IF NOT EXISTS trade_records ( CREATE TABLE IF NOT EXISTS trade_records (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -85,10 +88,19 @@ impl Database {
); );
CREATE INDEX IF NOT EXISTS idx_bot_logs_bot_id ON bot_logs(bot_id, created_at DESC);", CREATE INDEX IF NOT EXISTS idx_bot_logs_bot_id ON bot_logs(bot_id, created_at DESC);",
)?; )?;
// migration: mevcut DB'ye buy_commission_usdt sütunu ekle // migrations — idempotent (.ok() ile hata yutulur)
conn.execute_batch( conn.execute_batch(
"ALTER TABLE open_positions ADD COLUMN buy_commission_usdt REAL NOT NULL DEFAULT 0;", "ALTER TABLE open_positions ADD COLUMN buy_commission_usdt REAL NOT NULL DEFAULT 0;",
).ok(); ).ok();
conn.execute_batch(
"ALTER TABLE bots ADD COLUMN stop_loss_percent REAL NOT NULL DEFAULT 0;",
).ok();
conn.execute_batch(
"ALTER TABLE bots ADD COLUMN strategy TEXT NOT NULL DEFAULT 'red_candle';",
).ok();
conn.execute_batch(
"ALTER TABLE bots ADD COLUMN strategy_params TEXT NOT NULL DEFAULT '{}';",
).ok();
Ok(Self { conn }) Ok(Self { conn })
} }
@ -96,12 +108,14 @@ impl Database {
pub fn insert_bot(&self, b: &BotConfig) -> SqlResult<()> { pub fn insert_bot(&self, b: &BotConfig) -> SqlResult<()> {
self.conn.execute( self.conn.execute(
"INSERT INTO bots (id, name, symbol, timeframe, usdt_amount, profit_percent, testnet, active) "INSERT INTO bots (id, name, symbol, timeframe, usdt_amount, profit_percent, stop_loss_percent, strategy, strategy_params, testnet, active)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
params![ params![
b.id, b.name, b.symbol, b.id, b.name, b.symbol,
serde_json::to_string(&b.timeframe).unwrap_or_default(), serde_json::to_string(&b.timeframe).unwrap_or_default(),
b.usdt_amount, b.profit_percent, b.usdt_amount, b.profit_percent, b.stop_loss_percent,
b.strategy.as_str(),
b.strategy_params.to_string(),
b.testnet as i32, b.active as i32 b.testnet as i32, b.active as i32
], ],
)?; )?;
@ -110,21 +124,30 @@ impl Database {
pub fn get_bots(&self) -> SqlResult<Vec<BotConfig>> { pub fn get_bots(&self) -> SqlResult<Vec<BotConfig>> {
let mut stmt = self.conn.prepare( let mut stmt = self.conn.prepare(
"SELECT id, name, symbol, timeframe, usdt_amount, profit_percent, testnet, active FROM bots ORDER BY rowid", "SELECT id, name, symbol, timeframe, usdt_amount, profit_percent, stop_loss_percent, strategy, strategy_params, testnet, active FROM bots ORDER BY rowid",
)?; )?;
let rows = stmt.query_map([], |row| { let rows = stmt.query_map([], |row| {
let timeframe_str: String = row.get(3)?; let timeframe_str: String = row.get(3)?;
let timeframe = serde_json::from_str(&timeframe_str) let timeframe = serde_json::from_str(&timeframe_str)
.unwrap_or(crate::binance::models::Timeframe::FiveMinutes); .unwrap_or(crate::binance::models::Timeframe::FiveMinutes);
let strategy_str: String = row.get(7)?;
let strategy = strategy_str.parse()
.unwrap_or(crate::bot::strategy::StrategyKind::RedCandle);
let params_str: String = row.get(8)?;
let strategy_params: serde_json::Value = serde_json::from_str(&params_str)
.unwrap_or(serde_json::Value::Object(Default::default()));
Ok(BotConfig { Ok(BotConfig {
id: row.get(0)?, id: row.get(0)?,
name: row.get(1)?, name: row.get(1)?,
symbol: row.get(2)?, symbol: row.get(2)?,
timeframe, timeframe,
usdt_amount: row.get(4)?, usdt_amount: row.get(4)?,
profit_percent: row.get(5)?, profit_percent: row.get(5)?,
testnet: row.get::<_, i32>(6)? != 0, stop_loss_percent: row.get(6)?,
active: row.get::<_, i32>(7)? != 0, strategy,
strategy_params,
testnet: row.get::<_, i32>(9)? != 0,
active: row.get::<_, i32>(10)? != 0,
}) })
})?; })?;
rows.collect() rows.collect()

View file

@ -32,10 +32,28 @@
<input id="f-profit" type="number" step="0.1" placeholder="2" min="0.1" /> <input id="f-profit" type="number" step="0.1" placeholder="2" min="0.1" />
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-row">
<label>USDT Miktarı <span id="f-min-label" style="font-size:10px;color:var(--muted);font-weight:400;text-transform:none"></span></label> <div class="form-group">
<input id="f-usdt" type="number" step="1" placeholder="10" min="1" /> <label>USDT Miktarı <span id="f-min-label" style="font-size:10px;color:var(--muted);font-weight:400;text-transform:none"></span></label>
<input id="f-usdt" type="number" step="1" placeholder="10" min="1" />
</div>
<div class="form-group">
<label>Stop Loss % <span style="font-size:10px;color:var(--muted);font-weight:400">(0 = yok)</span></label>
<input id="f-stoploss" type="number" step="0.1" placeholder="0" min="0" value="0" />
</div>
</div> </div>
<div class="form-group">
<label>Strateji</label>
<select id="f-strategy" onchange="onStrategyChange()">
<option value="red_candle">Kırmızı Mum — kırmızı mum kapanınca al</option>
<option value="ema_cross">EMA Crossover — hızlı/yavaş EMA kesişimi</option>
<option value="bollinger_rsi">Bollinger + RSI — alt band + aşırı satım</option>
<option value="macd">MACD — sinyal kesişimi</option>
<option value="rsi">RSI Aşırı Satım — oversold geçişinde al</option>
<option value="supertrend">Supertrend — ATR tabanlı trend bandı</option>
</select>
</div>
<div id="strategy-params-container"></div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn" onclick="closeModal()">İptal</button> <button class="btn" onclick="closeModal()">İptal</button>
@ -50,6 +68,71 @@
let _symbolMinMap = {}; let _symbolMinMap = {};
let _symbolsLoaded = false; let _symbolsLoaded = false;
const STRATEGY_PARAMS = {
red_candle: [],
ema_cross: [
{ key: 'fast_period', label: 'Hızlı EMA', type: 'int', default: 9, min: 2, max: 50 },
{ key: 'slow_period', label: 'Yavaş EMA', type: 'int', default: 21, min: 5, max: 200 },
{ key: 'adx_period', label: 'ADX Periyodu', type: 'int', default: 14, min: 5, max: 50 },
{ key: 'adx_threshold', label: 'ADX Eşiği', type: 'float', default: 20, min: 0, max: 50 },
],
bollinger_rsi: [
{ key: 'bb_period', label: 'BB Periyodu', type: 'int', default: 20, min: 5, max: 100 },
{ key: 'bb_std', label: 'BB Std Sapması', type: 'float', default: 2.0, min: 0.5, max: 4 },
{ key: 'rsi_period', label: 'RSI Periyodu', type: 'int', default: 14, min: 2, max: 50 },
{ key: 'rsi_oversold', label: 'RSI Aşırı Satım',type: 'int', default: 35, min: 10, max: 49 },
{ key: 'rsi_overbought', label: 'RSI Aşırı Alım', type: 'int', default: 65, min: 51, max: 90 },
],
macd: [
{ key: 'fast_period', label: 'Hızlı EMA', type: 'int', default: 12, min: 3, max: 50 },
{ key: 'slow_period', label: 'Yavaş EMA', type: 'int', default: 26, min: 5, max: 200 },
{ key: 'signal_period', label: 'Sinyal EMA', type: 'int', default: 9, min: 2, max: 50 },
{ key: 'require_zero_cross', label: 'Sıfır Filtresi', type: 'bool', default: false },
],
rsi: [
{ key: 'rsi_period', label: 'RSI Periyodu', type: 'int', default: 14, min: 2, max: 50 },
{ key: 'rsi_oversold', label: 'Alım Seviyesi', type: 'int', default: 30, min: 5, max: 49 },
{ key: 'rsi_overbought',label: 'Satım Seviyesi', type: 'int', default: 70, min: 51, max: 95 },
{ key: 'rsi_exit', label: 'Orta Çıkış', type: 'int', default: 50, min: 0, max: 70 },
],
supertrend: [
{ key: 'atr_period', label: 'ATR Periyodu', type: 'int', default: 10, min: 3, max: 50 },
{ key: 'multiplier', label: 'Çarpan', type: 'float', default: 3.0, min: 0.5, max: 10 },
],
};
window.onStrategyChange = function() {
const strategy = document.getElementById('f-strategy').value;
const params = STRATEGY_PARAMS[strategy] || [];
const container = document.getElementById('strategy-params-container');
if (!params.length) { container.innerHTML = ''; return; }
const rows = [];
for (let i = 0; i < params.length; i += 2) {
const p1 = params[i];
const p2 = params[i + 1];
if (p1.type === 'bool') {
rows.push(`<div class="form-group" style="flex-direction:row;align-items:center;gap:8px">
<input id="fp-${p1.key}" type="checkbox" ${p1.default ? 'checked' : ''} style="width:auto"/>
<label style="text-transform:none;font-size:12px">${p1.label}</label>
</div>`);
} else {
const f1 = `<div class="form-group"><label>${p1.label}</label>
<input id="fp-${p1.key}" type="number" step="${p1.type==='float'?'0.1':'1'}" value="${p1.default}" min="${p1.min??''}" max="${p1.max??''}"/></div>`;
const f2 = p2 && p2.type !== 'bool' ? `<div class="form-group"><label>${p2.label}</label>
<input id="fp-${p2.key}" type="number" step="${p2.type==='float'?'0.1':'1'}" value="${p2.default}" min="${p2.min??''}" max="${p2.max??''}"/></div>` : '';
rows.push(`<div class="form-row">${f1}${f2}</div>`);
if (p2 && p2.type === 'bool') {
rows.push(`<div class="form-group" style="flex-direction:row;align-items:center;gap:8px">
<input id="fp-${p2.key}" type="checkbox" ${p2.default ? 'checked' : ''} style="width:auto"/>
<label style="text-transform:none;font-size:12px">${p2.label}</label>
</div>`);
}
}
}
container.innerHTML = rows.join('');
};
async function _api(method, path, body) { async function _api(method, path, body) {
const res = await fetch('/api' + path, { const res = await fetch('/api' + path, {
method, credentials: 'include', method, credentials: 'include',
@ -96,6 +179,7 @@
document.getElementById('modal-overlay').classList.add('open'); document.getElementById('modal-overlay').classList.add('open');
await _loadSymbols(); await _loadSymbols();
if (_symbolSelect) _symbolSelect.focus(); if (_symbolSelect) _symbolSelect.focus();
onStrategyChange();
}; };
window.closeModal = function() { window.closeModal = function() {
@ -103,7 +187,10 @@
document.getElementById('f-name').value = ''; document.getElementById('f-name').value = '';
document.getElementById('f-usdt').value = ''; document.getElementById('f-usdt').value = '';
document.getElementById('f-profit').value = ''; document.getElementById('f-profit').value = '';
document.getElementById('f-stoploss').value = '0';
document.getElementById('f-min-label').textContent = ''; document.getElementById('f-min-label').textContent = '';
document.getElementById('f-strategy').value = 'red_candle';
document.getElementById('strategy-params-container').innerHTML = '';
if (_symbolSelect) _symbolSelect.clear(); if (_symbolSelect) _symbolSelect.clear();
}; };
@ -113,10 +200,25 @@
const timeframe = document.getElementById('f-tf').value; const timeframe = document.getElementById('f-tf').value;
const usdt_amount = parseFloat(document.getElementById('f-usdt').value); const usdt_amount = parseFloat(document.getElementById('f-usdt').value);
const profit_percent = parseFloat(document.getElementById('f-profit').value); const profit_percent = parseFloat(document.getElementById('f-profit').value);
const stop_loss_percent = parseFloat(document.getElementById('f-stoploss').value) || 0;
const strategy = document.getElementById('f-strategy').value;
if (!name || !symbol || !usdt_amount || !profit_percent) { alert('Tüm alanları doldurun'); return; } if (!name || !symbol || !usdt_amount || !profit_percent) { alert('Tüm alanları doldurun'); return; }
const min = _symbolMinMap[symbol] || 0; const min = _symbolMinMap[symbol] || 0;
if (usdt_amount < min) { alert(`Minimum işlem miktarı ${min} USDT`); document.getElementById('f-usdt').focus(); return; } if (usdt_amount < min) { alert(`Minimum işlem miktarı ${min} USDT`); document.getElementById('f-usdt').focus(); return; }
const res = await _api('POST', '/bots', { name, symbol, timeframe, usdt_amount, profit_percent });
// Strateji parametrelerini topla
const params = STRATEGY_PARAMS[strategy] || [];
const strategy_params = {};
for (const p of params) {
const el = document.getElementById('fp-' + p.key);
if (!el) continue;
if (p.type === 'bool') strategy_params[p.key] = el.checked;
else if (p.type === 'int') strategy_params[p.key] = parseInt(el.value);
else strategy_params[p.key] = parseFloat(el.value);
}
const res = await _api('POST', '/bots', { name, symbol, timeframe, usdt_amount, profit_percent, stop_loss_percent, strategy, strategy_params });
if (!res.ok) { alert('Bot oluşturulamadı: ' + await res.text()); return; } if (!res.ok) { alert('Bot oluşturulamadı: ' + await res.text()); return; }
closeModal(); closeModal();
if (typeof onBotCreated === 'function') onBotCreated(); if (typeof onBotCreated === 'function') onBotCreated();