feat: coin seçici dropdown + otomatik minNotional

This commit is contained in:
Mukan Erkin TÖRÜK 2026-04-19 06:41:58 +03:00
parent a68dc599a9
commit 0ba182834b
6 changed files with 121 additions and 2 deletions

View file

@ -7,9 +7,31 @@ use axum::{
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::binance::client::BinanceClient;
use crate::storage::config::BotConfig;
use crate::AppState;
#[derive(Serialize)]
pub struct SymbolInfo {
pub symbol: String,
pub min_notional: f64,
}
pub async fn list_symbols(State(state): State<AppState>) -> impl IntoResponse {
let client = BinanceClient::new(state.api_key, state.api_secret, state.testnet);
match client.get_usdt_symbols_with_min().await {
Ok(mut symbols) => {
symbols.sort_by(|a, b| a.0.cmp(&b.0));
let result: Vec<SymbolInfo> = symbols
.into_iter()
.map(|(symbol, min_notional)| SymbolInfo { symbol, min_notional })
.collect();
Json(result).into_response()
}
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
}
}
#[derive(Deserialize)]
pub struct CreateBotRequest {
pub name: String,

View file

@ -9,6 +9,7 @@ use crate::AppState;
pub fn build(state: AppState) -> Router {
let api = Router::new()
.route("/symbols", get(bots::list_symbols))
.route("/bots", get(bots::list_bots).post(bots::create_bot))
.route("/bots/{id}", delete(bots::delete_bot))
.route("/bots/{id}/start", post(bots::start_bot))

View file

@ -231,6 +231,40 @@ impl BinanceClient {
Ok(symbols)
}
/// USDT çiftlerini ve her birinin minNotional değerini döner
pub async fn get_usdt_symbols_with_min(&self) -> Result<Vec<(String, f64)>> {
let url = format!("{}/api/v3/exchangeInfo", self.base_url);
let response = self.http.get(&url).send().await?.json::<Value>().await?;
let symbols = response["symbols"]
.as_array()
.ok_or_else(|| anyhow!("exchangeInfo parse hatası"))?
.iter()
.filter_map(|s| {
let symbol = s["symbol"].as_str()?;
let quote = s["quoteAsset"].as_str()?;
let status = s["status"].as_str()?;
if quote != "USDT" || status != "TRADING" || symbol.starts_with("USDT") {
return None;
}
let base = s["baseAsset"].as_str()?;
let filters: std::collections::HashMap<&str, &Value> = s["filters"]
.as_array()?
.iter()
.filter_map(|f| Some((f["filterType"].as_str()?, f)))
.collect();
let min_notional = filters
.get("NOTIONAL")
.and_then(|f| f["minNotional"].as_str())
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(5.0);
Some((base.to_string(), min_notional))
})
.collect();
Ok(symbols)
}
/// Binance hesabındaki sıfır olmayan tüm bakiyeleri döner: Vec<(asset, free, locked)>
pub async fn get_account_balances(&self) -> Result<Vec<(String, f64, f64)>> {
let timestamp = chrono::Utc::now().timestamp_millis().to_string();

View file

@ -7,6 +7,7 @@ pub struct AppConfig {
pub auth_token: String,
pub db_path: String,
pub listen_addr: String,
pub testnet: bool,
}
impl AppConfig {
@ -17,6 +18,7 @@ impl AppConfig {
auth_token: env::var("AUTH_TOKEN").expect("AUTH_TOKEN gerekli"),
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()),
testnet: env::var("BINANCE_TESTNET").map(|v| v == "true").unwrap_or(false),
}
}
}

View file

@ -22,6 +22,9 @@ pub struct AppState {
pub manager: Arc<Mutex<BotManager>>,
pub event_tx: broadcast::Sender<TradeEvent>,
pub auth_token: String,
pub api_key: String,
pub api_secret: String,
pub testnet: bool,
}
#[tokio::main]
@ -63,6 +66,9 @@ async fn main() {
manager,
event_tx,
auth_token: cfg.auth_token,
api_key: cfg.api_key,
api_secret: cfg.api_secret,
testnet: cfg.testnet,
};
let router = api::routes::build(state);

View file

@ -89,7 +89,12 @@
<div class="section-header">Yeni Bot Ekle</div>
<div class="form-grid">
<div class="form-group"><label>İsim</label><input id="f-name" placeholder="DOGE Bot" /></div>
<div class="form-group"><label>Sembol</label><input id="f-symbol" placeholder="DOGEUSDT" /></div>
<div class="form-group" style="position:relative">
<label>Coin (USDT çifti)</label>
<input id="f-symbol-search" placeholder="Ara: DOGE, BTC..." autocomplete="off" oninput="filterSymbols(this.value)" onfocus="showDropdown()" onblur="setTimeout(hideDropdown,150)" />
<div id="symbol-dropdown" style="display:none;position:absolute;top:100%;left:0;right:0;background:var(--surface);border:1px solid var(--border);border-radius:5px;max-height:200px;overflow-y:auto;z-index:50;"></div>
<input type="hidden" id="f-symbol" />
</div>
<div class="form-group"><label>Timeframe</label>
<select id="f-timeframe">
<option value="1m">1m</option>
@ -101,7 +106,10 @@
<option value="1d">1d</option>
</select>
</div>
<div class="form-group"><label>USDT Miktarı</label><input id="f-usdt" type="number" step="0.1" placeholder="10" /></div>
<div class="form-group">
<label>USDT Miktarı <span id="f-min-label" style="color:var(--muted);font-size:10px"></span></label>
<input id="f-usdt" type="number" step="0.1" placeholder="Min. seçilecek" />
</div>
<div class="form-group"><label>Kar %</label><input id="f-profit" type="number" step="0.1" placeholder="2" /></div>
<div class="form-group"><label>Testnet</label>
<select id="f-testnet">
@ -133,6 +141,7 @@
<script>
let AUTH_TOKEN = localStorage.getItem('mse_token') || '';
let sseSource = null;
let allSymbols = []; // [{symbol, min_notional}]
function login() {
const t = document.getElementById('token-input').value.trim();
@ -171,6 +180,51 @@ async function loadAll() {
loadBots();
loadPositions();
loadClosed();
loadSymbols();
}
async function loadSymbols() {
const res = await api('GET', '/symbols');
allSymbols = await res.json();
}
function filterSymbols(query) {
const q = query.toUpperCase();
const matches = q
? allSymbols.filter(s => s.symbol.startsWith(q) || s.symbol.includes(q)).slice(0, 50)
: allSymbols.slice(0, 50);
renderDropdown(matches);
showDropdown();
}
function renderDropdown(items) {
const dd = document.getElementById('symbol-dropdown');
if (!items.length) { dd.innerHTML = '<div style="padding:8px 12px;color:var(--muted)">Bulunamadı</div>'; return; }
dd.innerHTML = items.map(s =>
`<div style="padding:7px 12px;cursor:pointer;display:flex;justify-content:space-between;align-items:center"
onmousedown="selectSymbol('${s.symbol}',${s.min_notional})">
<strong>${s.symbol}</strong><span style="color:var(--muted);font-size:11px">min ${s.min_notional} USDT</span>
</div>`
).join('');
}
function selectSymbol(symbol, minNotional) {
document.getElementById('f-symbol').value = symbol;
document.getElementById('f-symbol-search').value = symbol;
document.getElementById('f-usdt').value = minNotional;
document.getElementById('f-min-label').textContent = `(min ${minNotional} USDT)`;
hideDropdown();
}
function showDropdown() {
const dd = document.getElementById('symbol-dropdown');
if (!allSymbols.length) return;
if (!dd.innerHTML) filterSymbols(document.getElementById('f-symbol-search').value);
dd.style.display = 'block';
}
function hideDropdown() {
document.getElementById('symbol-dropdown').style.display = 'none';
}
async function loadBots() {