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,
)
.await
{
Ok(Some(result)) => {
info!(
"[{}] İşlem | Alış: {:.6} | Satış hedefi: {:.6} | Kar: %{:.2}",
config.symbol, result.buy_order.price, result.sell_order.price, config.profit_percent
);
{
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();
} }
let record = TradeRecord { // Yeterli mum var mı?
bot_id: config.id.clone(), if candle_buf.len() < min_candles {
bot_name: config.name.clone(), info!("[{}] Strateji için yeterli mum yok ({}/{}), bekleniyor.", config.symbol, candle_buf.len(), min_candles);
symbol: config.symbol.clone(), continue;
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 { // Sinyal üret
bot_id: config.id.clone(), let signal = strategy.generate_signal(&candle_buf);
bot_name: config.name.clone(),
symbol: config.symbol.clone(), match signal {
buy_price: result.buy_order.price, Signal::Buy => {
sell_target: result.sell_order.price, info!("[{}] BUY sinyali! Strateji: {}", config.symbol, config.strategy);
quantity: result.buy_order.quantity, if let Err(e) = execute_buy(&client, &config, &strategy, &kline, &db, &event_tx).await {
profit_percent: config.profit_percent, error!("[{}] Alım hatası: {:?}", config.symbol, e);
timestamp: result.timestamp,
}).ok();
}
Ok(None) => {
info!("[{}] İşlem yapılmadı (yeşil mum).", config.symbol);
{
let db_guard = db.lock().await; let db_guard = db.lock().await;
db_guard.insert_log(&config.id, "skip", "Yeşil mum, işlem yapılmadı.").ok(); db_guard.insert_log(&config.id, "error", &format!("Alım hatası: {}", e)).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, "error", &format!("Strateji hatası: {}", e)).ok(); db_guard.insert_log(&config.id, "skip", "Sell sinyali — bakkal modunda görmezden gelindi.").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

@ -29,6 +29,9 @@ impl Database {
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,
stop_loss_percent REAL NOT NULL DEFAULT 0,
strategy TEXT NOT NULL DEFAULT 'red_candle',
strategy_params TEXT NOT NULL DEFAULT '{}',
testnet INTEGER NOT NULL DEFAULT 0, testnet INTEGER NOT NULL DEFAULT 0,
active INTEGER NOT NULL DEFAULT 0 active INTEGER NOT NULL DEFAULT 0
); );
@ -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,12 +124,18 @@ 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)?,
@ -123,8 +143,11 @@ impl Database {
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-row">
<div class="form-group"> <div class="form-group">
<label>USDT Miktarı <span id="f-min-label" style="font-size:10px;color:var(--muted);font-weight:400;text-transform:none"></span></label> <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" /> <input id="f-usdt" type="number" step="1" placeholder="10" min="1" />
</div> </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 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();