diff --git a/src/api/bots.rs b/src/api/bots.rs index 90df7e2..55fb4af 100644 --- a/src/api/bots.rs +++ b/src/api/bots.rs @@ -207,7 +207,12 @@ pub async fn update_bot( } drop(db); - let manager = state.manager.lock().await; + let mut manager = state.manager.lock().await; + let was_running = manager.is_running(&id); + if was_running { + manager.stop(&id).await; + manager.start(config.clone()); + } let running = manager.is_running(&id); Json(BotResponse { config, running }).into_response() } diff --git a/src/api/routes.rs b/src/api/routes.rs index 5ed21f5..02d943f 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -2,7 +2,7 @@ use axum::{ extract::State, middleware, response::{IntoResponse, Redirect}, - routing::{delete, get, post, put}, + routing::{get, post}, Router, }; use axum_extra::extract::CookieJar; @@ -36,7 +36,9 @@ pub fn build(state: AppState) -> Router { .nest("/api", api) .route("/", get(login_handler)) .route("/dashboard", get(dashboard_handler)) + .route("/bots", get(bots_handler)) .route("/bots/:id", get(bot_detail_handler)) + .route("/positions", get(positions_handler)) .with_state(state) } @@ -59,6 +61,32 @@ async fn login_handler( } } +async fn bots_handler( + State(state): State, + jar: CookieJar, +) -> impl IntoResponse { + let token = jar.get("mse_session").map(|c| c.value().to_string()); + let authed = match token { + Some(t) => { let db = state.db.lock().await; db.session_exists(&t).unwrap_or(false) } + None => false, + }; + if authed { axum::response::Html(include_str!("../web/bots.html")).into_response() } + else { Redirect::to("/").into_response() } +} + +async fn positions_handler( + State(state): State, + jar: CookieJar, +) -> impl IntoResponse { + let token = jar.get("mse_session").map(|c| c.value().to_string()); + let authed = match token { + Some(t) => { let db = state.db.lock().await; db.session_exists(&t).unwrap_or(false) } + None => false, + }; + if authed { axum::response::Html(include_str!("../web/positions.html")).into_response() } + else { Redirect::to("/").into_response() } +} + async fn bot_detail_handler( State(state): State, jar: CookieJar, diff --git a/src/bot/runner.rs b/src/bot/runner.rs index 6fdfa6c..5782b59 100644 --- a/src/bot/runner.rs +++ b/src/bot/runner.rs @@ -7,8 +7,9 @@ use tokio::sync::{broadcast, Mutex}; use tokio::time::{sleep, Duration}; use tokio_tungstenite::connect_async; -use crate::binance::{client::BinanceClient, models::{Kline, Timeframe}}; +use crate::binance::{client::BinanceClient, models::Kline}; use crate::bot::strategy::RedCandleStrategy; +use crate::storage::closed::ClosedPosition; use crate::storage::config::BotConfig; use crate::storage::db::Database; use crate::storage::history::TradeRecord; @@ -40,13 +41,31 @@ impl BotRunner { db: Arc>, event_tx: broadcast::Sender, ) { - let client = BinanceClient::new(api_key, api_secret, 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 stream = format!("{}/{}@kline_{}", ws_base, config.symbol.to_lowercase(), config.timeframe.as_str()); info!("[{}] Bot başlatıldı. WS: {}", config.symbol, stream); + // Açık pozisyonların order durumunu periyodik sorgula + { + let db2 = Arc::clone(&db); + let ev2 = event_tx.clone(); + let shutdown2 = Arc::clone(&shutdown); + let cfg2 = config.clone(); + let api_key2 = api_key.clone(); + let api_secret2 = api_secret.clone(); + tokio::spawn(async move { + let check_client = BinanceClient::new(api_key2, api_secret2, cfg2.testnet); + loop { + sleep(Duration::from_secs(30)).await; + if *shutdown2.lock().await { break; } + check_open_positions(&check_client, &db2, &ev2, &cfg2).await; + } + }); + } + loop { if *shutdown.lock().await { info!("[{}] Bot durduruldu.", config.symbol); @@ -232,6 +251,68 @@ impl KlineWs { } } +async fn check_open_positions( + client: &BinanceClient, + db: &Arc>, + event_tx: &broadcast::Sender, + config: &BotConfig, +) { + let positions = { + let db_guard = db.lock().await; + match db_guard.get_positions_by_bot(&config.id) { + Ok(p) => p, + Err(_) => return, + } + }; + + for pos in positions { + let status = match client.get_order_status(&pos.symbol, pos.order_id).await { + Ok(s) => s, + Err(e) => { + warn!("[{}] Order status sorgu hatası #{}: {}", config.symbol, pos.order_id, e); + continue; + } + }; + + match status.as_str() { + "FILLED" | "CANCELED" | "EXPIRED" | "REJECTED" => { + let closed = ClosedPosition { + order_id: pos.order_id, + bot_id: pos.bot_id.clone(), + bot_name: pos.bot_name.clone(), + symbol: pos.symbol.clone(), + buy_price: pos.buy_price, + sell_target: pos.sell_target, + quantity: pos.quantity, + profit_percent: pos.profit_percent, + opened_at: pos.opened_at, + closed_at: chrono::Utc::now().timestamp_millis(), + status: status.clone(), + }; + let db_guard = db.lock().await; + if let Err(e) = db_guard.insert_closed(&closed) { + error!("[{}] Kapalı pozisyon kayıt hatası: {}", config.symbol, e); + } else { + db_guard.remove_position(pos.order_id).ok(); + info!("[{}] Pozisyon kapatıldı #{} ({})", config.symbol, pos.order_id, status); + db_guard.insert_log(&config.id, "trade", &format!("✅ Pozisyon kapatıldı | Order #{} | Durum: {}", pos.order_id, status)).ok(); + } + event_tx.send(TradeEvent { + bot_id: config.id.clone(), + bot_name: config.name.clone(), + symbol: config.symbol.clone(), + buy_price: pos.buy_price, + sell_target: pos.sell_target, + quantity: pos.quantity, + profit_percent: pos.profit_percent, + timestamp: chrono::Utc::now().timestamp_millis(), + }).ok(); + } + _ => {} + } + } +} + // Kline dönüşümü strategy için impl From<&KlineWs> for Kline { fn from(k: &KlineWs) -> Self { diff --git a/src/web/bot.html b/src/web/bot.html index dcbecaa..15127e5 100644 --- a/src/web/bot.html +++ b/src/web/bot.html @@ -16,19 +16,22 @@ html, body { height: 100%; overflow: hidden; } body { background: var(--bg); color: var(--text); font-family: 'Segoe UI', system-ui, sans-serif; font-size: 13px; display: flex; flex-direction: column; } - /* ── TOP BAR ─────────────────────────────────── */ + /* ── HEADER / TOP BAR ───────────────────────── */ #topbar { background: var(--surface); border-bottom: 1px solid var(--border); - padding: 0 16px; height: 52px; display: flex; align-items: center; gap: 20px; flex-shrink: 0; + padding: 0 16px; height: 52px; display: flex; align-items: center; gap: 0; flex-shrink: 0; } - .tb-logo { display: flex; align-items: center; gap: 8px; } - .tb-logo span { font-size: 15px; font-weight: 700; } + .tb-logo { font-size: 15px; font-weight: 700; margin-right: 12px; } .logo-crypto { color: #d0d0d0; } .logo-fox { color: #00d4ff; } - .tb-sep { width: 1px; height: 24px; background: var(--border); } - #tb-name { font-size: 16px; font-weight: 600; color: var(--text); } - #tb-symbol { font-size: 13px; color: var(--muted); } - #tb-price { font-size: 20px; font-weight: 700; color: var(--text); min-width: 100px; } - #tb-change { font-size: 13px; font-weight: 600; min-width: 60px; } + nav { display: flex; gap: 4px; margin-right: 12px; } + .nav-link { padding: 5px 12px; border-radius: 5px; font-size: 13px; color: var(--muted); text-decoration: none; transition: all .15s; } + .nav-link:hover { color: var(--text); background: rgba(255,255,255,.05); } + .nav-link.active { color: var(--text); background: rgba(108,99,255,.15); } + .tb-sep { width: 1px; height: 24px; background: var(--border); margin: 0 12px; } + #tb-name { font-size: 15px; font-weight: 600; color: var(--text); } + #tb-symbol { font-size: 13px; color: var(--muted); margin-left: 4px; } + #tb-price { font-size: 18px; font-weight: 700; color: var(--text); min-width: 90px; } + #tb-change { font-size: 13px; font-weight: 600; min-width: 55px; } .tb-stat { display: flex; flex-direction: column; gap: 1px; } .tb-stat .lbl { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; } .tb-stat .val { font-size: 12px; color: var(--text); } @@ -46,8 +49,12 @@ .btn-delete:hover { background: rgba(231,76,60,.1); } .btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; } .btn-primary:hover { background: #5a52d5; } - .back-link { color: var(--muted); font-size: 12px; text-decoration: none; display: flex; align-items: center; gap: 4px; } - .back-link:hover { color: var(--text); } + .btn-logout { border-color: var(--red); color: var(--red); font-size: 12px; } + .btn-logout:hover { background: rgba(231,76,60,.1); } + .mode-toggle { display: flex; align-items: center; gap: 4px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 3px; margin-right: 8px; } + .mode-btn { padding: 3px 10px; border-radius: 4px; border: none; background: transparent; color: var(--muted); font-size: 11px; font-weight: 600; cursor: pointer; transition: all .15s; } + .mode-btn.active-live { background: rgba(46,204,113,.15); color: var(--green); } + .mode-btn.active-testnet { background: rgba(243,156,18,.15); color: var(--yellow); } /* ── MAIN LAYOUT ─────────────────────────────── */ #layout { @@ -135,12 +142,15 @@
- + +
-
+
@@ -152,8 +162,11 @@
- ← Dashboard - +
+ + +
+
@@ -263,14 +276,27 @@ const TF_MAP = { '1m':'1m','5m':'5m','15m':'15m','30m':'30m','1h':'1h','4h':'4h' const TF_SECONDS = { '1m':60,'5m':300,'15m':900,'30m':1800,'1h':3600,'4h':14400,'1d':86400,'1w':604800 }; function buyTime(ms) { const tf = TF_SECONDS[bot.timeframe] || 300; - return Math.floor(ms / 1000) - tf; + const sec = Math.floor(ms / 1000); + // Hangi mum aralığına düştüğünü bul, bir önceki mumun açılışını döndür + const currentCandleOpen = Math.floor(sec / tf) * tf; + return currentCandleOpen - tf; } // ── Load bot ────────────────────────────────────── async function loadBot() { - const res = await api('GET', `/bots/${BOT_ID}`); - if (!res.ok) { window.location.href = '/dashboard'; return; } - bot = await res.json(); + const [botRes, modeRes] = await Promise.all([ + api('GET', `/bots/${BOT_ID}`), + api('GET', '/mode'), + ]); + if (!botRes.ok) { window.location.href = '/dashboard'; return; } + bot = await botRes.json(); + if (modeRes.ok) { + const { mode } = await modeRes.json(); + const btnLive = document.getElementById('btn-live'); + const btnTest = document.getElementById('btn-testnet'); + btnLive.className = 'mode-btn' + (mode === 'live' ? ' active-live' : ''); + btnTest.className = 'mode-btn' + (mode === 'testnet' ? ' active-testnet' : ''); + } document.title = `${bot.name} — CryptoFox Mukan Edition`; renderTopBar(); renderEditForm(); @@ -362,6 +388,7 @@ function startPriceWs() { // ── Chart ───────────────────────────────────────── function initChart() { + if (chart) return; const container = document.getElementById('chart'); chart = LightweightCharts.createChart(container, { layout: { background: { color: '#0f0f13' }, textColor: '#888' }, diff --git a/src/web/bots.html b/src/web/bots.html new file mode 100644 index 0000000..ef939f6 --- /dev/null +++ b/src/web/bots.html @@ -0,0 +1,351 @@ + + + + + + Botlar — CryptoFox Mukan Edition + + + + + + +
+ +
+ +
+
+ + +
+ + +
+ +
+
+
Çalışıyor 0
+
Çalışan bot yok
+
+
+
Durdurulmuş 0
+
Durdurulan bot yok
+
+
+ + + + + + + diff --git a/src/web/index.html b/src/web/index.html index 2bda1d5..c43d2ef 100644 --- a/src/web/index.html +++ b/src/web/index.html @@ -21,18 +21,22 @@ --yellow: #f39c12; } body { background: var(--bg); color: var(--text); font-family: 'Segoe UI', system-ui, sans-serif; font-size: 14px; min-height: 100vh; } - header { background: var(--surface); border-bottom: 1px solid var(--border); padding: 16px 24px; display: flex; align-items: center; gap: 12px; } - header h1 { font-size: 18px; font-weight: 600; color: var(--accent); margin: 0; } + header { background: var(--surface); border-bottom: 1px solid var(--border); padding: 0 24px; height: 52px; display: flex; align-items: center; gap: 16px; } + .logo { font-size: 16px; font-weight: 700; } .logo-crypto { color: #d0d0d0; } .logo-fox { color: #00d4ff; } - .logo-edition { font-size: 11px; font-weight: 400; color: var(--muted); } - header .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); } - header .status-dot.connected { background: var(--green); } - header .spacer { flex: 1; } - .mode-toggle { display: flex; align-items: center; gap: 8px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 4px; } - .mode-btn { padding: 4px 14px; border-radius: 4px; border: none; background: transparent; color: var(--muted); font-size: 12px; font-weight: 600; cursor: pointer; transition: all 0.15s; } + .h-sep { width: 1px; height: 20px; background: var(--border); } + nav { display: flex; gap: 4px; } + .nav-link { padding: 5px 12px; border-radius: 5px; font-size: 13px; color: var(--muted); text-decoration: none; transition: all .15s; } + .nav-link:hover { color: var(--text); background: rgba(255,255,255,.05); } + .nav-link.active { color: var(--text); background: rgba(108,99,255,.15); } + .spacer { flex: 1; } + .mode-toggle { display: flex; align-items: center; gap: 4px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 3px; } + .mode-btn { padding: 3px 12px; border-radius: 4px; border: none; background: transparent; color: var(--muted); font-size: 11px; font-weight: 600; cursor: pointer; transition: all 0.15s; } .mode-btn.active-live { background: rgba(46,204,113,0.15); color: var(--green); } .mode-btn.active-testnet { background: rgba(243,156,18,0.15); color: var(--yellow); } + .btn-logout { border-color: var(--red); color: var(--red); font-size: 12px; } + .btn-logout:hover { background: rgba(231,76,60,.1); } main { padding: 24px; max-width: 1400px; margin: 0 auto; display: flex; flex-direction: column; gap: 24px; } section { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; } .section-header { padding: 14px 18px; border-bottom: 1px solid var(--border); font-weight: 600; font-size: 13px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); display: flex; align-items: center; justify-content: space-between; } @@ -141,19 +145,19 @@
- logo -
-

CryptoFox

- [ MUKAN EDITION ] -
- - bağlanıyor... - + +
+ +
- +
diff --git a/src/web/positions.html b/src/web/positions.html new file mode 100644 index 0000000..8cd555a --- /dev/null +++ b/src/web/positions.html @@ -0,0 +1,141 @@ + + + + + + Pozisyonlar — CryptoFox Mukan Edition + + + + +
+ +
+ +
+
+ + +
+ +
+ +
+
+
Açık Pozisyonlar
+ + + +
BotSembolAlışHedefMiktarZaman
Yükleniyor...
+
+
+
Kapalı İşlemler
+ + + +
BotSembolAlışHedefMiktarDurumKapanış
Yükleniyor...
+
+
+ + + +