feat: testnet/live mod toggle, çift API key desteği
This commit is contained in:
parent
2a6c9620f7
commit
b5763d0e81
10 changed files with 199 additions and 54 deletions
|
|
@ -1,5 +1,7 @@
|
||||||
BINANCE_API_KEY=your-binance-api-key
|
BINANCE_API_KEY=your-live-api-key
|
||||||
BINANCE_API_SECRET=your-binance-api-secret
|
BINANCE_API_SECRET=your-live-api-secret
|
||||||
|
BINANCE_TESTNET_API_KEY=your-testnet-api-key
|
||||||
|
BINANCE_TESTNET_API_SECRET=your-testnet-api-secret
|
||||||
AUTH_TOKEN=your-dashboard-access-token
|
AUTH_TOKEN=your-dashboard-access-token
|
||||||
DB_PATH=data/bots.db
|
DB_PATH=data/bots.db
|
||||||
LISTEN_ADDR=127.0.0.1:4646
|
LISTEN_ADDR=127.0.0.1:4646
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,14 @@ pub struct SymbolInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_symbols(State(state): State<AppState>) -> impl IntoResponse {
|
pub async fn list_symbols(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
let client = BinanceClient::new(state.api_key, state.api_secret, state.testnet);
|
let mode = state.current_mode.lock().await.clone();
|
||||||
|
let is_testnet = mode == "testnet";
|
||||||
|
let (key, secret) = if is_testnet {
|
||||||
|
(state.testnet_api_key, state.testnet_api_secret)
|
||||||
|
} else {
|
||||||
|
(state.live_api_key, state.live_api_secret)
|
||||||
|
};
|
||||||
|
let client = BinanceClient::new(key, secret, is_testnet);
|
||||||
match client.get_usdt_symbols_with_min().await {
|
match client.get_usdt_symbols_with_min().await {
|
||||||
Ok(mut symbols) => {
|
Ok(mut symbols) => {
|
||||||
symbols.sort_by(|a, b| a.0.cmp(&b.0));
|
symbols.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
|
@ -39,7 +46,6 @@ 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,
|
||||||
pub testnet: Option<bool>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
|
@ -50,6 +56,9 @@ pub struct BotResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_bots(State(state): State<AppState>) -> impl IntoResponse {
|
pub async fn list_bots(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
|
let mode = state.current_mode.lock().await.clone();
|
||||||
|
let is_testnet = mode == "testnet";
|
||||||
|
|
||||||
let db = state.db.lock().await;
|
let db = state.db.lock().await;
|
||||||
let bots = match db.get_bots() {
|
let bots = match db.get_bots() {
|
||||||
Ok(b) => b,
|
Ok(b) => b,
|
||||||
|
|
@ -60,6 +69,7 @@ pub async fn list_bots(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
let manager = state.manager.lock().await;
|
let manager = state.manager.lock().await;
|
||||||
let response: Vec<BotResponse> = bots
|
let response: Vec<BotResponse> = bots
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
.filter(|b| b.testnet == is_testnet)
|
||||||
.map(|b| {
|
.map(|b| {
|
||||||
let running = manager.is_running(&b.id);
|
let running = manager.is_running(&b.id);
|
||||||
BotResponse { config: b, running }
|
BotResponse { config: b, running }
|
||||||
|
|
@ -73,6 +83,7 @@ pub async fn create_bot(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(req): Json<CreateBotRequest>,
|
Json(req): Json<CreateBotRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
|
let mode = state.current_mode.lock().await.clone();
|
||||||
let config = BotConfig {
|
let config = BotConfig {
|
||||||
id: Uuid::new_v4().to_string(),
|
id: Uuid::new_v4().to_string(),
|
||||||
name: req.name,
|
name: req.name,
|
||||||
|
|
@ -80,7 +91,7 @@ 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,
|
||||||
testnet: req.testnet.unwrap_or(false),
|
testnet: mode == "testnet",
|
||||||
active: false,
|
active: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod bots;
|
pub mod bots;
|
||||||
pub mod events;
|
pub mod events;
|
||||||
|
pub mod mode;
|
||||||
pub mod positions;
|
pub mod positions;
|
||||||
pub mod routes;
|
pub mod routes;
|
||||||
|
|
|
||||||
54
src/api/mode.rs
Normal file
54
src/api/mode.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::AppState;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct ModeResponse {
|
||||||
|
pub mode: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct SetModeRequest {
|
||||||
|
pub mode: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_mode(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
|
let mode = state.current_mode.lock().await.clone();
|
||||||
|
Json(ModeResponse { mode }).into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_mode(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(req): Json<SetModeRequest>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
if req.mode != "live" && req.mode != "testnet" {
|
||||||
|
return (StatusCode::BAD_REQUEST, "Geçersiz mod").into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut current = state.current_mode.lock().await;
|
||||||
|
if *current == req.mode {
|
||||||
|
return Json(ModeResponse { mode: current.clone() }).into_response();
|
||||||
|
}
|
||||||
|
*current = req.mode.clone();
|
||||||
|
drop(current);
|
||||||
|
|
||||||
|
// DB'ye kaydet
|
||||||
|
let db = state.db.lock().await;
|
||||||
|
if let Err(e) = db.set_config("current_mode", &req.mode) {
|
||||||
|
return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response();
|
||||||
|
}
|
||||||
|
drop(db);
|
||||||
|
|
||||||
|
// Tüm çalışan botları durdur
|
||||||
|
let mut manager = state.manager.lock().await;
|
||||||
|
let running = manager.running_ids();
|
||||||
|
for id in running {
|
||||||
|
manager.stop(&id).await;
|
||||||
|
let db = state.db.lock().await;
|
||||||
|
let _ = db.set_bot_active(&id, false);
|
||||||
|
}
|
||||||
|
drop(manager);
|
||||||
|
|
||||||
|
Json(ModeResponse { mode: req.mode }).into_response()
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,7 @@ use axum::{
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::api::{auth::require_auth, bots, events, positions};
|
use crate::api::{auth::require_auth, bots, events, mode, positions};
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
|
||||||
pub fn build(state: AppState) -> Router {
|
pub fn build(state: AppState) -> Router {
|
||||||
|
|
@ -16,6 +16,7 @@ pub fn build(state: AppState) -> Router {
|
||||||
.route("/bots/{id}/stop", post(bots::stop_bot))
|
.route("/bots/{id}/stop", post(bots::stop_bot))
|
||||||
.route("/positions", get(positions::open_positions))
|
.route("/positions", get(positions::open_positions))
|
||||||
.route("/positions/closed", get(positions::closed_positions))
|
.route("/positions/closed", get(positions::closed_positions))
|
||||||
|
.route("/mode", get(mode::get_mode).post(mode::set_mode))
|
||||||
.route("/events", get(events::sse_handler))
|
.route("/events", get(events::sse_handler))
|
||||||
.layer(middleware::from_fn_with_state(state.clone(), require_auth));
|
.layer(middleware::from_fn_with_state(state.clone(), require_auth));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,18 +19,22 @@ pub struct BotManager {
|
||||||
bots: HashMap<String, RunningBot>,
|
bots: HashMap<String, RunningBot>,
|
||||||
db: Arc<Mutex<Database>>,
|
db: Arc<Mutex<Database>>,
|
||||||
event_tx: broadcast::Sender<TradeEvent>,
|
event_tx: broadcast::Sender<TradeEvent>,
|
||||||
api_key: String,
|
live_api_key: String,
|
||||||
api_secret: String,
|
live_api_secret: String,
|
||||||
|
testnet_api_key: String,
|
||||||
|
testnet_api_secret: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BotManager {
|
impl BotManager {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
db: Arc<Mutex<Database>>,
|
db: Arc<Mutex<Database>>,
|
||||||
event_tx: broadcast::Sender<TradeEvent>,
|
event_tx: broadcast::Sender<TradeEvent>,
|
||||||
api_key: String,
|
live_api_key: String,
|
||||||
api_secret: String,
|
live_api_secret: String,
|
||||||
|
testnet_api_key: String,
|
||||||
|
testnet_api_secret: String,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self { bots: HashMap::new(), db, event_tx, api_key, api_secret }
|
Self { bots: HashMap::new(), db, event_tx, live_api_key, live_api_secret, testnet_api_key, testnet_api_secret }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start(&mut self, config: BotConfig) {
|
pub fn start(&mut self, config: BotConfig) {
|
||||||
|
|
@ -44,8 +48,11 @@ impl BotManager {
|
||||||
let shutdown_clone = Arc::clone(&shutdown);
|
let shutdown_clone = Arc::clone(&shutdown);
|
||||||
let db = Arc::clone(&self.db);
|
let db = Arc::clone(&self.db);
|
||||||
let tx = self.event_tx.clone();
|
let tx = self.event_tx.clone();
|
||||||
let key = self.api_key.clone();
|
let (key, secret) = if config.testnet {
|
||||||
let secret = self.api_secret.clone();
|
(self.testnet_api_key.clone(), self.testnet_api_secret.clone())
|
||||||
|
} else {
|
||||||
|
(self.live_api_key.clone(), self.live_api_secret.clone())
|
||||||
|
};
|
||||||
let cfg = config.clone();
|
let cfg = config.clone();
|
||||||
|
|
||||||
let handle = tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,25 @@ use std::env;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
pub api_key: String,
|
pub live_api_key: String,
|
||||||
pub api_secret: String,
|
pub live_api_secret: String,
|
||||||
|
pub testnet_api_key: String,
|
||||||
|
pub testnet_api_secret: String,
|
||||||
pub auth_token: String,
|
pub auth_token: String,
|
||||||
pub db_path: String,
|
pub db_path: String,
|
||||||
pub listen_addr: String,
|
pub listen_addr: String,
|
||||||
pub testnet: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppConfig {
|
impl AppConfig {
|
||||||
pub fn from_env() -> Self {
|
pub fn from_env() -> Self {
|
||||||
Self {
|
Self {
|
||||||
api_key: env::var("BINANCE_API_KEY").expect("BINANCE_API_KEY gerekli"),
|
live_api_key: env::var("BINANCE_API_KEY").expect("BINANCE_API_KEY gerekli"),
|
||||||
api_secret: env::var("BINANCE_API_SECRET").expect("BINANCE_API_SECRET gerekli"),
|
live_api_secret: env::var("BINANCE_API_SECRET").expect("BINANCE_API_SECRET gerekli"),
|
||||||
|
testnet_api_key: env::var("BINANCE_TESTNET_API_KEY").expect("BINANCE_TESTNET_API_KEY gerekli"),
|
||||||
|
testnet_api_secret: env::var("BINANCE_TESTNET_API_SECRET").expect("BINANCE_TESTNET_API_SECRET gerekli"),
|
||||||
auth_token: env::var("AUTH_TOKEN").expect("AUTH_TOKEN gerekli"),
|
auth_token: env::var("AUTH_TOKEN").expect("AUTH_TOKEN gerekli"),
|
||||||
db_path: env::var("DB_PATH").unwrap_or_else(|_| "data/bots.db".to_string()),
|
db_path: env::var("DB_PATH").unwrap_or_else(|_| "data/bots.db".to_string()),
|
||||||
listen_addr: env::var("LISTEN_ADDR").unwrap_or_else(|_| "127.0.0.1:4646".to_string()),
|
listen_addr: env::var("LISTEN_ADDR").unwrap_or_else(|_| "127.0.0.1:4646".to_string()),
|
||||||
testnet: env::var("BINANCE_TESTNET").map(|v| v == "true").unwrap_or(false),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
36
src/main.rs
36
src/main.rs
|
|
@ -22,9 +22,11 @@ pub struct AppState {
|
||||||
pub manager: Arc<Mutex<BotManager>>,
|
pub manager: Arc<Mutex<BotManager>>,
|
||||||
pub event_tx: broadcast::Sender<TradeEvent>,
|
pub event_tx: broadcast::Sender<TradeEvent>,
|
||||||
pub auth_token: String,
|
pub auth_token: String,
|
||||||
pub api_key: String,
|
pub live_api_key: String,
|
||||||
pub api_secret: String,
|
pub live_api_secret: String,
|
||||||
pub testnet: bool,
|
pub testnet_api_key: String,
|
||||||
|
pub testnet_api_secret: String,
|
||||||
|
pub current_mode: Arc<Mutex<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
|
@ -40,22 +42,34 @@ async fn main() {
|
||||||
|
|
||||||
let (event_tx, _) = broadcast::channel::<TradeEvent>(64);
|
let (event_tx, _) = broadcast::channel::<TradeEvent>(64);
|
||||||
|
|
||||||
|
// Modu DB'den oku, yoksa "testnet" default
|
||||||
|
let mode = {
|
||||||
|
let db_guard = db.lock().await;
|
||||||
|
let m = db_guard.get_config("current_mode").unwrap_or_default().unwrap_or_else(|| "testnet".to_string());
|
||||||
|
drop(db_guard);
|
||||||
|
m
|
||||||
|
};
|
||||||
|
let current_mode = Arc::new(Mutex::new(mode.clone()));
|
||||||
|
|
||||||
let manager = BotManager::new(
|
let manager = BotManager::new(
|
||||||
Arc::clone(&db),
|
Arc::clone(&db),
|
||||||
event_tx.clone(),
|
event_tx.clone(),
|
||||||
cfg.api_key.clone(),
|
cfg.live_api_key.clone(),
|
||||||
cfg.api_secret.clone(),
|
cfg.live_api_secret.clone(),
|
||||||
|
cfg.testnet_api_key.clone(),
|
||||||
|
cfg.testnet_api_secret.clone(),
|
||||||
);
|
);
|
||||||
let manager = Arc::new(Mutex::new(manager));
|
let manager = Arc::new(Mutex::new(manager));
|
||||||
|
|
||||||
// Aktif botları otomatik başlat
|
// Aktif botları otomatik başlat (mevcut modla eşleşenler)
|
||||||
{
|
{
|
||||||
let db_guard = db.lock().await;
|
let db_guard = db.lock().await;
|
||||||
let bots = db_guard.get_bots().unwrap_or_default();
|
let bots = db_guard.get_bots().unwrap_or_default();
|
||||||
drop(db_guard);
|
drop(db_guard);
|
||||||
|
|
||||||
|
let is_testnet = mode == "testnet";
|
||||||
let mut mgr = manager.lock().await;
|
let mut mgr = manager.lock().await;
|
||||||
for bot in bots.into_iter().filter(|b| b.active) {
|
for bot in bots.into_iter().filter(|b| b.active && b.testnet == is_testnet) {
|
||||||
info!("Otomatik başlatılıyor: {} ({})", bot.symbol, bot.id);
|
info!("Otomatik başlatılıyor: {} ({})", bot.symbol, bot.id);
|
||||||
mgr.start(bot);
|
mgr.start(bot);
|
||||||
}
|
}
|
||||||
|
|
@ -66,9 +80,11 @@ async fn main() {
|
||||||
manager,
|
manager,
|
||||||
event_tx,
|
event_tx,
|
||||||
auth_token: cfg.auth_token,
|
auth_token: cfg.auth_token,
|
||||||
api_key: cfg.api_key,
|
live_api_key: cfg.live_api_key,
|
||||||
api_secret: cfg.api_secret,
|
live_api_secret: cfg.live_api_secret,
|
||||||
testnet: cfg.testnet,
|
testnet_api_key: cfg.testnet_api_key,
|
||||||
|
testnet_api_secret: cfg.testnet_api_secret,
|
||||||
|
current_mode,
|
||||||
};
|
};
|
||||||
|
|
||||||
let router = api::routes::build(state);
|
let router = api::routes::build(state);
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,10 @@ impl Database {
|
||||||
opened_at INTEGER NOT NULL,
|
opened_at INTEGER NOT NULL,
|
||||||
closed_at INTEGER NOT NULL,
|
closed_at INTEGER NOT NULL,
|
||||||
status TEXT NOT NULL
|
status TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS config (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL
|
||||||
);",
|
);",
|
||||||
)?;
|
)?;
|
||||||
Ok(Self { conn })
|
Ok(Self { conn })
|
||||||
|
|
@ -220,6 +224,24 @@ impl Database {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Config ──────────────────────────────────────
|
||||||
|
|
||||||
|
pub fn get_config(&self, key: &str) -> SqlResult<Option<String>> {
|
||||||
|
let mut stmt = self.conn.prepare("SELECT value FROM config WHERE key = ?1")?;
|
||||||
|
let mut rows = stmt.query_map(params![key], |row| row.get(0))?;
|
||||||
|
Ok(rows.next().transpose()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_config(&self, key: &str, value: &str) -> SqlResult<()> {
|
||||||
|
self.conn.execute(
|
||||||
|
"INSERT INTO config (key, value) VALUES (?1, ?2) ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
||||||
|
params![key, value],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Closed positions ────────────────────────────
|
||||||
|
|
||||||
pub fn get_closed_positions(&self) -> SqlResult<Vec<ClosedPosition>> {
|
pub fn get_closed_positions(&self) -> SqlResult<Vec<ClosedPosition>> {
|
||||||
let mut stmt = self.conn.prepare(
|
let mut stmt = self.conn.prepare(
|
||||||
"SELECT order_id, bot_id, bot_name, symbol, buy_price, sell_target, quantity, profit_percent, opened_at, closed_at, status
|
"SELECT order_id, bot_id, bot_name, symbol, buy_price, sell_target, quantity, profit_percent, opened_at, closed_at, status
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,11 @@
|
||||||
header h1 { font-size: 18px; font-weight: 600; color: var(--accent); }
|
header h1 { font-size: 18px; font-weight: 600; color: var(--accent); }
|
||||||
header .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); }
|
header .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); }
|
||||||
header .status-dot.connected { background: var(--green); }
|
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; }
|
||||||
|
.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); }
|
||||||
main { padding: 24px; max-width: 1400px; margin: 0 auto; display: flex; flex-direction: column; gap: 24px; }
|
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 { 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; }
|
.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; }
|
||||||
|
|
@ -117,7 +122,6 @@
|
||||||
<label>Coin (USDT çifti)</label>
|
<label>Coin (USDT çifti)</label>
|
||||||
<select id="f-symbol"><option value="">Yükleniyor...</option></select>
|
<select id="f-symbol"><option value="">Yükleniyor...</option></select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Timeframe</label>
|
<label>Timeframe</label>
|
||||||
<select id="f-timeframe">
|
<select id="f-timeframe">
|
||||||
|
|
@ -130,14 +134,6 @@
|
||||||
<option value="1d">1d</option>
|
<option value="1d">1d</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<label>Testnet</label>
|
|
||||||
<select id="f-testnet">
|
|
||||||
<option value="false">Hayır (Gerçek)</option>
|
|
||||||
<option value="true">Evet (Testnet)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>USDT Miktarı <span id="f-min-label" style="color:var(--muted);font-size:10px"></span></label>
|
<label>USDT Miktarı <span id="f-min-label" style="color:var(--muted);font-size:10px"></span></label>
|
||||||
|
|
@ -160,6 +156,11 @@
|
||||||
<h1>MSE Dashboard</h1>
|
<h1>MSE Dashboard</h1>
|
||||||
<span class="status-dot" id="sse-dot"></span>
|
<span class="status-dot" id="sse-dot"></span>
|
||||||
<span id="sse-label" style="font-size:12px; color:var(--muted)">bağlanıyor...</span>
|
<span id="sse-label" style="font-size:12px; color:var(--muted)">bağlanıyor...</span>
|
||||||
|
<span class="spacer"></span>
|
||||||
|
<div class="mode-toggle">
|
||||||
|
<button class="mode-btn" id="btn-testnet" onclick="switchMode('testnet')">Testnet</button>
|
||||||
|
<button class="mode-btn" id="btn-live" onclick="switchMode('live')">Canlı</button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
|
|
@ -196,6 +197,7 @@ let AUTH_TOKEN = localStorage.getItem('mse_token') || '';
|
||||||
let sseSource = null;
|
let sseSource = null;
|
||||||
let symbolSelect = null;
|
let symbolSelect = null;
|
||||||
let allSymbols = [];
|
let allSymbols = [];
|
||||||
|
let currentMode = 'testnet';
|
||||||
|
|
||||||
function login() {
|
function login() {
|
||||||
const t = document.getElementById('token-input').value.trim();
|
const t = document.getElementById('token-input').value.trim();
|
||||||
|
|
@ -231,12 +233,38 @@ async function tryConnect() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadAll() {
|
async function loadAll() {
|
||||||
|
await loadMode();
|
||||||
loadBots();
|
loadBots();
|
||||||
loadPositions();
|
loadPositions();
|
||||||
loadClosed();
|
loadClosed();
|
||||||
loadSymbols();
|
loadSymbols();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadMode() {
|
||||||
|
const res = await api('GET', '/mode');
|
||||||
|
const data = await res.json();
|
||||||
|
currentMode = data.mode;
|
||||||
|
updateModeUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateModeUI() {
|
||||||
|
const btnTestnet = document.getElementById('btn-testnet');
|
||||||
|
const btnLive = document.getElementById('btn-live');
|
||||||
|
btnTestnet.className = 'mode-btn' + (currentMode === 'testnet' ? ' active-testnet' : '');
|
||||||
|
btnLive.className = 'mode-btn' + (currentMode === 'live' ? ' active-live' : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function switchMode(mode) {
|
||||||
|
if (mode === currentMode) return;
|
||||||
|
if (mode === 'live') {
|
||||||
|
if (!confirm('Canlı moda geçilecek. Çalışan tüm botlar durdurulacak. Devam edilsin mi?')) return;
|
||||||
|
}
|
||||||
|
await api('POST', '/mode', { mode });
|
||||||
|
currentMode = mode;
|
||||||
|
updateModeUI();
|
||||||
|
loadAll();
|
||||||
|
}
|
||||||
|
|
||||||
async function loadSymbols() {
|
async function loadSymbols() {
|
||||||
const res = await api('GET', '/symbols');
|
const res = await api('GET', '/symbols');
|
||||||
allSymbols = await res.json();
|
allSymbols = await res.json();
|
||||||
|
|
@ -270,6 +298,8 @@ async function loadSymbols() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function openModal() {
|
function openModal() {
|
||||||
|
const modeLabel = currentMode === 'testnet' ? '🟡 Testnet' : '🟢 Canlı';
|
||||||
|
document.querySelector('.modal-header h2').textContent = `Yeni Bot Ekle — ${modeLabel}`;
|
||||||
document.getElementById('modal-overlay').classList.add('open');
|
document.getElementById('modal-overlay').classList.add('open');
|
||||||
if (symbolSelect) symbolSelect.focus();
|
if (symbolSelect) symbolSelect.focus();
|
||||||
}
|
}
|
||||||
|
|
@ -344,12 +374,11 @@ async function createBot() {
|
||||||
const timeframe = document.getElementById('f-timeframe').value;
|
const timeframe = document.getElementById('f-timeframe').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 testnet = document.getElementById('f-testnet').value === 'true';
|
|
||||||
const usdtInput = document.getElementById('f-usdt');
|
const usdtInput = document.getElementById('f-usdt');
|
||||||
const minNotional = parseFloat(usdtInput.min) || 0;
|
const minNotional = parseFloat(usdtInput.min) || 0;
|
||||||
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; }
|
||||||
if (usdt_amount < minNotional) { alert(`Minimum işlem miktarı ${minNotional} USDT`); usdtInput.focus(); return; }
|
if (usdt_amount < minNotional) { alert(`Minimum işlem miktarı ${minNotional} USDT`); usdtInput.focus(); return; }
|
||||||
await api('POST', '/bots', { name, symbol, timeframe, usdt_amount, profit_percent, testnet });
|
await api('POST', '/bots', { name, symbol, timeframe, usdt_amount, profit_percent });
|
||||||
closeModal();
|
closeModal();
|
||||||
document.getElementById('f-name').value = '';
|
document.getElementById('f-name').value = '';
|
||||||
if (symbolSelect) symbolSelect.clear();
|
if (symbolSelect) symbolSelect.clear();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue