feat: coin seçici dropdown + otomatik minNotional
This commit is contained in:
parent
a68dc599a9
commit
0ba182834b
6 changed files with 121 additions and 2 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue