feat(mse): add market sell button for open positions
- BinanceClient::market_sell() — SELL MARKET order - sell_position handler: cancel limit order then fire market sell - bot.html: "Sat" button (yellow) alongside existing ✕ cancel button - cancel = iptal, coin elde kalır; sat = iptal + anında market satış - Sell kaydı log'a "sell" level ile düşülür; closed status = SOLD Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2ff8d57c08
commit
d08ab100c4
4 changed files with 121 additions and 5 deletions
|
|
@ -305,3 +305,79 @@ pub async fn cancel_position(
|
|||
|
||||
StatusCode::OK.into_response()
|
||||
}
|
||||
|
||||
pub async fn sell_position(
|
||||
State(state): State<AppState>,
|
||||
Path((bot_id, order_id)): Path<(String, u64)>,
|
||||
) -> impl IntoResponse {
|
||||
let db = state.db.lock().await;
|
||||
let positions = match db.get_positions_by_bot(&bot_id) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
let pos = match positions.into_iter().find(|p| p.order_id == order_id) {
|
||||
Some(p) => p,
|
||||
None => return StatusCode::NOT_FOUND.into_response(),
|
||||
};
|
||||
let bots = match db.get_bots() {
|
||||
Ok(b) => b,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
let config = match bots.into_iter().find(|b| b.id == bot_id) {
|
||||
Some(c) => c,
|
||||
None => return StatusCode::NOT_FOUND.into_response(),
|
||||
};
|
||||
drop(db);
|
||||
|
||||
let mode = state.current_mode.lock().await.clone();
|
||||
let (api_key, api_secret) = if mode == "live" {
|
||||
(state.live_api_key.clone(), state.live_api_secret.clone())
|
||||
} else {
|
||||
(state.testnet_api_key.clone(), state.testnet_api_secret.clone())
|
||||
};
|
||||
let client = BinanceClient::new(api_key, api_secret, config.testnet);
|
||||
|
||||
// 1. Limit satış emrini iptal et
|
||||
if let Err(e) = client.cancel_order(&pos.symbol, order_id).await {
|
||||
return (StatusCode::BAD_GATEWAY, format!("Emir iptal hatası: {}", e)).into_response();
|
||||
}
|
||||
|
||||
// 2. Lot size'a göre miktarı yuvarla ve market satışı gönder
|
||||
let qty_str = match client.get_symbol_filters(&pos.symbol).await {
|
||||
Ok(filters) => filters.format_qty(pos.quantity),
|
||||
Err(_) => format!("{:.8}", pos.quantity),
|
||||
};
|
||||
let sell_result = client.market_sell(&pos.symbol, &qty_str).await;
|
||||
let actual_price = sell_result.as_ref().map(|o| o.price).unwrap_or(0.0);
|
||||
let sell_ok = sell_result.is_ok();
|
||||
|
||||
let db = state.db.lock().await;
|
||||
let closed = crate::storage::closed::ClosedPosition {
|
||||
bot_id: pos.bot_id.clone(),
|
||||
bot_name: pos.bot_name.clone(),
|
||||
symbol: pos.symbol.clone(),
|
||||
order_id: pos.order_id,
|
||||
buy_price: pos.buy_price,
|
||||
sell_target: if sell_ok && actual_price > 0.0 { actual_price } else { pos.sell_target },
|
||||
quantity: pos.quantity,
|
||||
profit_percent: pos.profit_percent,
|
||||
opened_at: pos.opened_at,
|
||||
closed_at: chrono::Utc::now().timestamp_millis(),
|
||||
status: if sell_ok { "SOLD".to_string() } else { "CANCELED".to_string() },
|
||||
};
|
||||
db.insert_closed(&closed).ok();
|
||||
db.remove_position(order_id).ok();
|
||||
|
||||
if sell_ok {
|
||||
db.insert_log(&bot_id, "sell", &format!(
|
||||
"Manuel market satış | {} @ {:.4} | Order #{}",
|
||||
pos.symbol, actual_price, order_id
|
||||
)).ok();
|
||||
} else {
|
||||
db.insert_log(&bot_id, "warn", &format!(
|
||||
"Emir iptal edildi ama market satış başarısız | Order #{}", order_id
|
||||
)).ok();
|
||||
}
|
||||
|
||||
StatusCode::OK.into_response()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ pub fn build(state: AppState) -> Router {
|
|||
.route("/bots/:id/positions/closed", get(bots::get_bot_closed))
|
||||
.route("/bots/:id/log-stream", get(events::bot_log_sse))
|
||||
.route("/bots/:id/positions/:order_id/cancel", post(bots::cancel_position))
|
||||
.route("/bots/:id/positions/:order_id/sell", post(bots::sell_position))
|
||||
.route("/positions", get(positions::open_positions))
|
||||
.route("/positions/closed", get(positions::closed_positions))
|
||||
.route("/mode", get(mode::get_mode).post(mode::set_mode))
|
||||
|
|
|
|||
|
|
@ -172,6 +172,35 @@ impl BinanceClient {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn market_sell(&self, symbol: &str, quantity: &str) -> Result<FilledOrder> {
|
||||
let timestamp = chrono::Utc::now().timestamp_millis().to_string();
|
||||
let params = format!(
|
||||
"symbol={}&side=SELL&type=MARKET&quantity={}×tamp={}",
|
||||
symbol, quantity, timestamp
|
||||
);
|
||||
let signature = self.sign(¶ms);
|
||||
let url = format!("{}/api/v3/order", self.base_url);
|
||||
|
||||
let response = self
|
||||
.http
|
||||
.post(&url)
|
||||
.header("X-MBX-APIKEY", &self.api_key)
|
||||
.query(&[
|
||||
("symbol", symbol),
|
||||
("side", "SELL"),
|
||||
("type", "MARKET"),
|
||||
("quantity", quantity),
|
||||
("timestamp", ×tamp),
|
||||
("signature", &signature),
|
||||
])
|
||||
.send()
|
||||
.await?
|
||||
.json::<Value>()
|
||||
.await?;
|
||||
|
||||
parse_filled_order(&response, OrderSide::Sell)
|
||||
}
|
||||
|
||||
/// Sembolün lot size ve tick size filtrelerini getirir
|
||||
pub async fn get_symbol_filters(&self, symbol: &str) -> Result<SymbolFilters> {
|
||||
let url = format!("{}/api/v3/exchangeInfo", self.base_url);
|
||||
|
|
|
|||
|
|
@ -165,9 +165,11 @@
|
|||
.pg-btn.active { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||||
.pg-info { font-size: 10px; color: var(--muted); }
|
||||
|
||||
/* cancel btn in table */
|
||||
/* cancel / sell btns in table */
|
||||
.btn-cancel { background: none; border: none; color: var(--muted); cursor: pointer; font-size: 12px; padding: 0 2px; line-height: 1; }
|
||||
.btn-cancel:hover { color: var(--red); }
|
||||
.btn-sell { background: none; border: 1px solid var(--yellow); color: var(--yellow); border-radius: 4px; cursor: pointer; font-size: 11px; padding: 2px 8px; line-height: 1; }
|
||||
.btn-sell:hover { background: rgba(243,156,18,.15); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -211,7 +213,7 @@
|
|||
</div>
|
||||
<div class="pos-scroll">
|
||||
<table class="pos-table">
|
||||
<thead><tr><th>Zaman</th><th>Alış</th><th>Hedef</th><th>PnL</th><th></th></tr></thead>
|
||||
<thead><tr><th>Zaman</th><th>Alış</th><th>Hedef</th><th>PnL</th><th></th><th></th></tr></thead>
|
||||
<tbody id="open-body"><tr><td colspan="5"><div class="empty-row">—</div></td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
@ -604,7 +606,7 @@ async function loadPositions() {
|
|||
function renderOpenTable() {
|
||||
const tbody = document.getElementById('open-body');
|
||||
if (!openPositionsCache.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="5"><div class="empty-row">Açık pozisyon yok</div></td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="6"><div class="empty-row">Açık pozisyon yok</div></td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = openPositionsCache.map(p => {
|
||||
|
|
@ -619,7 +621,8 @@ function renderOpenTable() {
|
|||
<td>${fmt(p.buy_price, 4)}</td>
|
||||
<td class="c-green">${fmt(p.sell_target, 4)}</td>
|
||||
<td>${pnlHtml}</td>
|
||||
<td><button class="btn-cancel" title="İptal" onclick="cancelPosition(${p.order_id})">✕</button></td>
|
||||
<td><button class="btn-cancel" title="Emri iptal et (coin elde kalır)" onclick="cancelPosition(${p.order_id})">✕</button></td>
|
||||
<td><button class="btn-sell" title="Emri iptal et + market satış yap" onclick="sellPosition(${p.order_id})">Sat</button></td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
|
@ -630,12 +633,19 @@ function updateOpenPnl() {
|
|||
}
|
||||
|
||||
async function cancelPosition(orderId) {
|
||||
if (!confirm('Bu pozisyonu Binance\'te iptal et?')) return;
|
||||
if (!confirm('Limit emri iptal edilecek, coin elde kalacak. Devam?')) return;
|
||||
const res = await api('POST', `/bots/${BOT_ID}/positions/${orderId}/cancel`);
|
||||
if (res.ok) { loadPositions(); loadClosed(); }
|
||||
else { alert('İptal başarısız: ' + (await res.text())); }
|
||||
}
|
||||
|
||||
async function sellPosition(orderId) {
|
||||
if (!confirm('Limit emri iptal edilip market fiyatından satılacak. Devam?')) return;
|
||||
const res = await api('POST', `/bots/${BOT_ID}/positions/${orderId}/sell`);
|
||||
if (res.ok) { loadPositions(); loadClosed(); }
|
||||
else { alert('Satış başarısız: ' + (await res.text())); }
|
||||
}
|
||||
|
||||
async function loadClosed() {
|
||||
const res = await api('GET', `/bots/${BOT_ID}/positions/closed`);
|
||||
closedPositionsCache = await res.json();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue