feat(logs): color-coded log levels for buy/sell/skip events
Replace emoji prefixes with dedicated log level types: buy (orange), sell (green), skip (purple), info (gray). CSS updated in bot.html. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
64806dd32e
commit
050cc18051
2 changed files with 319 additions and 313 deletions
|
|
@ -122,12 +122,12 @@ impl BotRunner {
|
||||||
{
|
{
|
||||||
Ok(Some(result)) => {
|
Ok(Some(result)) => {
|
||||||
info!(
|
info!(
|
||||||
"[{}] ✅ İşlem | Alış: {:.6} | Satış hedefi: {:.6} | Kar: %{:.2}",
|
"[{}] İşlem | Alış: {:.6} | Satış hedefi: {:.6} | Kar: %{:.2}",
|
||||||
config.symbol, result.buy_order.price, result.sell_order.price, config.profit_percent
|
config.symbol, result.buy_order.price, result.sell_order.price, config.profit_percent
|
||||||
);
|
);
|
||||||
{
|
{
|
||||||
let db_guard = db.lock().await;
|
let db_guard = db.lock().await;
|
||||||
db_guard.insert_log(&config.id, "trade", &format!("✅ İşlem gerçekleşti | Alış: {:.6} | Satış hedefi: {:.6} | Miktar: {:.6} | Kar: %{:.2}", result.buy_order.price, result.sell_order.price, result.buy_order.quantity, config.profit_percent)).ok();
|
db_guard.insert_log(&config.id, "buy", &format!("İşlem gerçekleşti | Alış: {:.6} | Satış hedefi: {:.6} | Miktar: {:.6} | Kar: %{:.2}", result.buy_order.price, result.sell_order.price, result.buy_order.quantity, config.profit_percent)).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
let record = TradeRecord {
|
let record = TradeRecord {
|
||||||
|
|
@ -175,17 +175,17 @@ impl BotRunner {
|
||||||
}).ok();
|
}).ok();
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
info!("[{}] ⏭ İşlem yapılmadı (yeşil mum).", config.symbol);
|
info!("[{}] İşlem yapılmadı (yeşil mum).", config.symbol);
|
||||||
{
|
{
|
||||||
let db_guard = db.lock().await;
|
let db_guard = db.lock().await;
|
||||||
db_guard.insert_log(&config.id, "info", "⏭ Yeşil mum, işlem yapılmadı.").ok();
|
db_guard.insert_log(&config.id, "skip", "Yeşil mum, işlem yapılmadı.").ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("[{}] ❌ Strateji hatası: {:?}", config.symbol, e);
|
error!("[{}] Strateji hatası: {:?}", config.symbol, e);
|
||||||
{
|
{
|
||||||
let db_guard = db.lock().await;
|
let db_guard = db.lock().await;
|
||||||
db_guard.insert_log(&config.id, "error", &format!("❌ Strateji hatası: {}", e)).ok();
|
db_guard.insert_log(&config.id, "error", &format!("Strateji hatası: {}", e)).ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -295,7 +295,7 @@ async fn check_open_positions(
|
||||||
} else {
|
} else {
|
||||||
db_guard.remove_position(pos.order_id).ok();
|
db_guard.remove_position(pos.order_id).ok();
|
||||||
info!("[{}] Pozisyon kapatıldı #{} ({})", config.symbol, pos.order_id, status);
|
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();
|
db_guard.insert_log(&config.id, "sell", &format!("Pozisyon kapatıldı | Order #{} | Durum: {}", pos.order_id, status)).ok();
|
||||||
}
|
}
|
||||||
event_tx.send(TradeEvent {
|
event_tx.send(TradeEvent {
|
||||||
bot_id: config.id.clone(),
|
bot_id: config.id.clone(),
|
||||||
|
|
|
||||||
618
src/web/bot.html
618
src/web/bot.html
|
|
@ -16,233 +16,275 @@
|
||||||
html, body { height: 100%; overflow: hidden; }
|
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; }
|
body { background: var(--bg); color: var(--text); font-family: 'Segoe UI', system-ui, sans-serif; font-size: 13px; display: flex; flex-direction: column; }
|
||||||
|
|
||||||
/* ── HEADER / TOP BAR ───────────────────────── */
|
/* ── SHARED HEADER STYLES ────────────────────── */
|
||||||
#topbar {
|
header { background: var(--surface); border-bottom: 1px solid var(--border); padding: 0 24px; height: 52px; display: flex; align-items: center; gap: 16px; flex-shrink: 0; }
|
||||||
background: var(--surface); border-bottom: 1px solid var(--border);
|
.logo { font-size: 16px; font-weight: 700; }
|
||||||
padding: 0 16px; height: 52px; display: flex; align-items: center; gap: 0; flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.tb-logo { font-size: 15px; font-weight: 700; margin-right: 12px; }
|
|
||||||
.logo-crypto { color: #d0d0d0; } .logo-fox { color: #00d4ff; }
|
.logo-crypto { color: #d0d0d0; } .logo-fox { color: #00d4ff; }
|
||||||
nav { display: flex; gap: 4px; margin-right: 12px; }
|
.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 { 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:hover { color: var(--text); background: rgba(255,255,255,.05); }
|
||||||
.nav-link.active { color: var(--text); background: rgba(108,99,255,.15); }
|
.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); }
|
|
||||||
.spacer { flex: 1; }
|
.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 .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); }
|
||||||
|
.btn-logout { padding: 5px 12px; border-radius: 5px; border: 1px solid var(--red); background: transparent; color: var(--red); font-size: 12px; cursor: pointer; transition: all .15s; }
|
||||||
|
.btn-logout:hover { background: rgba(231,76,60,.1); }
|
||||||
|
|
||||||
|
/* ── BUTTONS ─────────────────────────────────── */
|
||||||
|
.btn { padding: 5px 12px; border-radius: 5px; border: 1px solid var(--border); background: transparent; color: var(--text); font-size: 12px; cursor: pointer; transition: all .15s; white-space: nowrap; }
|
||||||
|
.btn:hover { background: rgba(255,255,255,.05); }
|
||||||
|
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||||||
|
.btn-primary:hover { background: #5a52d5; }
|
||||||
|
.btn-danger { border-color: var(--red); color: var(--red); }
|
||||||
|
.btn-danger:hover { background: rgba(231,76,60,.1); }
|
||||||
|
|
||||||
|
/* ── BADGES ──────────────────────────────────── */
|
||||||
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
|
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
|
||||||
.badge-running { background: rgba(46,204,113,.15); color: var(--green); }
|
.badge-running { background: rgba(46,204,113,.15); color: var(--green); }
|
||||||
.badge-stopped { background: rgba(136,136,136,.1); color: var(--muted); }
|
.badge-stopped { background: rgba(136,136,136,.1); color: var(--muted); }
|
||||||
.btn { padding: 5px 12px; border-radius: 5px; border: 1px solid var(--border); background: transparent; color: var(--text); font-size: 12px; cursor: pointer; transition: all .15s; white-space: nowrap; }
|
|
||||||
.btn:hover { background: rgba(255,255,255,.05); }
|
|
||||||
.btn-start { border-color: var(--green); color: var(--green); }
|
|
||||||
.btn-start:hover { background: rgba(46,204,113,.1); }
|
|
||||||
.btn-stop { border-color: var(--yellow); color: var(--yellow); }
|
|
||||||
.btn-stop:hover { background: rgba(243,156,18,.1); }
|
|
||||||
.btn-delete { border-color: var(--red); color: var(--red); }
|
|
||||||
.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; }
|
|
||||||
.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 ─────────────────────────────── */
|
/* ── PRICE BAR ───────────────────────────────── */
|
||||||
|
#price-bar {
|
||||||
|
background: var(--surface2); border-bottom: 1px solid var(--border);
|
||||||
|
padding: 0 16px; height: 44px; display: flex; align-items: center; gap: 0; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.pb-sep { width: 1px; height: 20px; background: var(--border); margin: 0 14px; }
|
||||||
|
#pb-name { font-size: 14px; font-weight: 600; }
|
||||||
|
#pb-symbol { font-size: 12px; color: var(--muted); margin-left: 6px; }
|
||||||
|
#pb-price { font-size: 18px; font-weight: 700; margin-left: 0; min-width: 90px; }
|
||||||
|
#pb-change { font-size: 12px; font-weight: 600; min-width: 52px; }
|
||||||
|
.pb-stat { display: flex; flex-direction: column; gap: 1px; }
|
||||||
|
.pb-stat .lbl { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
|
||||||
|
.pb-stat .val { font-size: 12px; color: var(--text); }
|
||||||
|
#pb-tf { background: rgba(108,99,255,.15); color: var(--accent); padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
|
||||||
|
#chart-ohlc { font-size: 11px; color: var(--muted); display: flex; gap: 10px; }
|
||||||
|
#chart-ohlc span b { color: var(--text); }
|
||||||
|
|
||||||
|
/* ── MAIN 3-COLUMN LAYOUT ────────────────────── */
|
||||||
#layout {
|
#layout {
|
||||||
flex: 1; display: grid; min-height: 0;
|
flex: 1; display: grid; min-height: 0;
|
||||||
grid-template-columns: 360px 1fr;
|
grid-template-columns: 260px 1fr 260px;
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── LEFT PANE ───────────────────────────────── */
|
/* ── PANE COMMONS ────────────────────────────── */
|
||||||
#left-pane {
|
.pane { border-right: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden; }
|
||||||
border-right: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden;
|
.pane:last-child { border-right: none; border-left: 1px solid var(--border); }
|
||||||
|
.pane-hdr {
|
||||||
|
padding: 7px 12px; border-bottom: 1px solid var(--border);
|
||||||
|
font-size: 10px; font-weight: 600; text-transform: uppercase;
|
||||||
|
letter-spacing: .06em; color: var(--muted); flex-shrink: 0;
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
background: var(--surface2);
|
||||||
}
|
}
|
||||||
.pane-header {
|
|
||||||
padding: 8px 14px; border-bottom: 1px solid var(--border);
|
|
||||||
font-size: 11px; font-weight: 600; text-transform: uppercase;
|
|
||||||
letter-spacing: .05em; color: var(--muted); display: flex; align-items: center; justify-content: space-between;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.pane-tabs { display: flex; border-bottom: 1px solid var(--border); flex-shrink: 0; }
|
|
||||||
.pane-tab {
|
|
||||||
flex: 1; padding: 9px 8px; text-align: center; font-size: 11px; font-weight: 600;
|
|
||||||
text-transform: uppercase; letter-spacing: .04em; color: var(--muted);
|
|
||||||
cursor: pointer; border-bottom: 2px solid transparent; transition: all .15s;
|
|
||||||
}
|
|
||||||
.pane-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
||||||
.pane-tab:hover:not(.active) { color: var(--text); }
|
|
||||||
.pane-content { overflow-y: auto; }
|
|
||||||
.pane-content::-webkit-scrollbar { width: 4px; }
|
|
||||||
.pane-content::-webkit-scrollbar-track { background: transparent; }
|
|
||||||
.pane-content::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
|
||||||
|
|
||||||
/* settings section */
|
/* ── LEFT PANE — POSITIONS ───────────────────── */
|
||||||
#settings-section { border-bottom: 1px solid var(--border); flex-shrink: 0; }
|
#left-pane { }
|
||||||
.form-group { display: flex; flex-direction: column; gap: 4px; }
|
.pos-half { flex: 1; display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
|
||||||
.form-group label { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; }
|
.pos-half + .pos-half { border-top: 1px solid var(--border); }
|
||||||
.form-group input {
|
.pos-scroll { flex: 1; overflow-y: auto; }
|
||||||
background: var(--bg); border: 1px solid var(--border); border-radius: 4px;
|
.pos-scroll::-webkit-scrollbar { width: 3px; }
|
||||||
padding: 6px 9px; color: var(--text); font-size: 12px; width: 100%;
|
.pos-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
||||||
}
|
|
||||||
.form-group input:focus { outline: none; border-color: var(--accent); }
|
|
||||||
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
||||||
#save-status { font-size: 10px; color: var(--green); display: none; }
|
|
||||||
|
|
||||||
/* positions */
|
|
||||||
#pos-section { flex: 1; display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
|
|
||||||
.pos-table { width: 100%; border-collapse: collapse; }
|
.pos-table { width: 100%; border-collapse: collapse; }
|
||||||
.pos-table th, .pos-table td { padding: 7px 14px; text-align: right; border-bottom: 1px solid rgba(255,255,255,.04); font-size: 12px; }
|
.pos-table th { font-size: 9px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); font-weight: 500; padding: 5px 10px; text-align: right; position: sticky; top: 0; background: var(--surface2); z-index: 1; border-bottom: 1px solid var(--border); }
|
||||||
.pos-table th { font-size: 10px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); font-weight: 500; text-align: right; position: sticky; top: 0; background: var(--surface2); z-index: 1; }
|
.pos-table th:first-child { text-align: left; }
|
||||||
.pos-table td:first-child, .pos-table th:first-child { text-align: left; }
|
.pos-table td { padding: 6px 10px; text-align: right; font-size: 11px; border-bottom: 1px solid rgba(255,255,255,.03); }
|
||||||
|
.pos-table td:first-child { text-align: left; font-size: 10px; color: var(--muted); }
|
||||||
.pos-table tr:hover td { background: rgba(255,255,255,.02); }
|
.pos-table tr:hover td { background: rgba(255,255,255,.02); }
|
||||||
.empty-row { padding: 24px 14px; text-align: center; color: var(--muted); font-size: 12px; }
|
.empty-row { padding: 16px 10px; text-align: center; color: var(--muted); font-size: 11px; }
|
||||||
.profit-up { color: var(--green); }
|
.c-green { color: var(--green); }
|
||||||
.profit-down { color: var(--red); }
|
.c-red { color: var(--red); }
|
||||||
|
.c-muted { color: var(--muted); }
|
||||||
|
|
||||||
/* ── CENTER PANE ─────────────────────────────── */
|
/* ── CENTER PANE ─────────────────────────────── */
|
||||||
#center-pane { display: flex; flex-direction: column; overflow: hidden; }
|
#center-pane { display: flex; flex-direction: column; overflow: hidden; border-right: 1px solid var(--border); }
|
||||||
|
|
||||||
/* chart */
|
|
||||||
#chart-header { padding: 8px 14px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 12px; flex-shrink: 0; }
|
|
||||||
#chart-tf-label { background: rgba(108,99,255,.15); color: var(--accent); padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
|
|
||||||
#chart-ohlc { font-size: 11px; color: var(--muted); display: flex; gap: 10px; }
|
|
||||||
#chart-ohlc span b { color: var(--text); }
|
|
||||||
#chart-container { flex: 6; min-height: 0; position: relative; }
|
#chart-container { flex: 6; min-height: 0; position: relative; }
|
||||||
#chart { width: 100%; height: 100%; }
|
#chart { width: 100%; height: 100%; }
|
||||||
|
#log-section { flex: 3; display: flex; flex-direction: column; min-height: 0; border-top: 1px solid var(--border); }
|
||||||
/* log terminal */
|
|
||||||
#log-section { flex: 4; display: flex; flex-direction: column; min-height: 0; border-top: 1px solid var(--border); }
|
|
||||||
#log-terminal {
|
#log-terminal {
|
||||||
flex: 1; background: #0a0a0e; padding: 10px 12px; overflow-y: auto;
|
flex: 1; background: #0a0a0e; padding: 8px 10px; overflow-y: auto;
|
||||||
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace; font-size: 11px; line-height: 1.7;
|
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace; font-size: 11px; line-height: 1.6;
|
||||||
}
|
}
|
||||||
#log-terminal::-webkit-scrollbar { width: 4px; }
|
#log-terminal::-webkit-scrollbar { width: 3px; }
|
||||||
#log-terminal::-webkit-scrollbar-thumb { background: #2a2a38; border-radius: 2px; }
|
#log-terminal::-webkit-scrollbar-thumb { background: #2a2a38; border-radius: 2px; }
|
||||||
.log-line { display: flex; gap: 8px; }
|
.log-line { display: flex; gap: 8px; }
|
||||||
.log-time { color: #444; min-width: 70px; flex-shrink: 0; font-size: 10px; padding-top: 1px; }
|
.log-time { color: #444; min-width: 62px; flex-shrink: 0; font-size: 10px; padding-top: 1px; }
|
||||||
.log-level { min-width: 40px; flex-shrink: 0; font-weight: 700; font-size: 10px; padding-top: 1px; }
|
.log-level { min-width: 38px; flex-shrink: 0; font-weight: 700; font-size: 10px; padding-top: 1px; }
|
||||||
.log-level.info { color: #6c63ff; }
|
.log-level.info { color: #888; }
|
||||||
.log-level.trade { color: var(--green); }
|
.log-level.buy { color: #f0a500; }
|
||||||
|
.log-level.sell { color: var(--green); }
|
||||||
|
.log-level.skip { color: #6c63ff; }
|
||||||
.log-level.error { color: var(--red); }
|
.log-level.error { color: var(--red); }
|
||||||
|
.log-level.trade { color: var(--green); }
|
||||||
.log-msg { color: #a0a0b8; word-break: break-word; }
|
.log-msg { color: #a0a0b8; word-break: break-word; }
|
||||||
#log-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--muted); display: inline-block; }
|
#log-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--muted); display: inline-block; }
|
||||||
|
|
||||||
|
/* ── RIGHT PANE ──────────────────────────────── */
|
||||||
|
#right-pane { overflow-y: auto; }
|
||||||
|
#right-pane::-webkit-scrollbar { width: 3px; }
|
||||||
|
#right-pane::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
||||||
|
|
||||||
|
/* status card */
|
||||||
|
.status-card { padding: 16px 14px; border-bottom: 1px solid var(--border); display: flex; flex-direction: column; align-items: center; gap: 12px; }
|
||||||
|
.status-badge-lg { padding: 4px 16px; border-radius: 20px; font-size: 12px; font-weight: 700; letter-spacing: .04em; }
|
||||||
|
.play-btn {
|
||||||
|
width: 52px; height: 52px; border-radius: 50%; border: 2px solid var(--border);
|
||||||
|
background: transparent; cursor: pointer; display: flex; align-items: center; justify-content: center;
|
||||||
|
font-size: 20px; color: var(--text); transition: all .2s;
|
||||||
|
}
|
||||||
|
.play-btn.state-running { border-color: var(--yellow); color: var(--yellow); }
|
||||||
|
.play-btn.state-running:hover { background: rgba(243,156,18,.1); }
|
||||||
|
.play-btn.state-stopped { border-color: var(--green); color: var(--green); }
|
||||||
|
.play-btn.state-stopped:hover { background: rgba(46,204,113,.1); }
|
||||||
|
.del-btn-small { font-size: 11px; color: var(--muted); background: none; border: none; cursor: pointer; text-decoration: underline; }
|
||||||
|
.del-btn-small:hover { color: var(--red); }
|
||||||
|
|
||||||
|
/* settings card */
|
||||||
|
.settings-card { padding: 12px 14px; border-bottom: 1px solid var(--border); display: flex; flex-direction: column; gap: 10px; }
|
||||||
|
.form-group { display: flex; flex-direction: column; gap: 4px; }
|
||||||
|
.form-group label { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; }
|
||||||
|
.form-group input { background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 6px 9px; color: var(--text); font-size: 12px; width: 100%; }
|
||||||
|
.form-group input:focus { outline: none; border-color: var(--accent); }
|
||||||
|
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||||
|
#save-status { font-size: 10px; color: var(--green); }
|
||||||
|
|
||||||
|
/* stats card */
|
||||||
|
.stats-card { padding: 12px 14px; display: flex; flex-direction: column; gap: 10px; }
|
||||||
|
.stat-row { display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.stat-row .lbl { font-size: 11px; color: var(--muted); }
|
||||||
|
.stat-row .val { font-size: 12px; font-weight: 600; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<!-- TOP BAR -->
|
{% include "_header.html" %}
|
||||||
<div id="topbar">
|
|
||||||
<div class="tb-logo"><span class="logo-crypto">Crypto</span><span class="logo-fox">Fox</span></div>
|
<!-- FIYAT ÇUBUĞU -->
|
||||||
<nav>
|
<div id="price-bar">
|
||||||
<a href="/dashboard" class="nav-link">Dashboard</a>
|
<span id="pb-name">—</span>
|
||||||
<a href="/bots" class="nav-link active">Botlar</a>
|
<span id="pb-symbol"></span>
|
||||||
<a href="/positions" class="nav-link">Pozisyonlar</a>
|
<div class="pb-sep"></div>
|
||||||
</nav>
|
<span id="pb-price">—</span>
|
||||||
<div class="tb-sep"></div>
|
<span id="pb-change" style="margin-left:8px">—</span>
|
||||||
<div id="tb-name">—</div>
|
<div class="pb-sep"></div>
|
||||||
<div id="tb-symbol">—</div>
|
<div class="pb-stat"><span class="lbl">24s Yüksek</span><span class="val" id="pb-high">—</span></div>
|
||||||
<div class="tb-sep"></div>
|
<div class="pb-sep" style="margin:0 10px"></div>
|
||||||
<div id="tb-price">—</div>
|
<div class="pb-stat"><span class="lbl">24s Düşük</span><span class="val" id="pb-low">—</span></div>
|
||||||
<div id="tb-change">—</div>
|
<div class="pb-sep" style="margin:0 10px"></div>
|
||||||
<div class="tb-sep"></div>
|
<div class="pb-stat"><span class="lbl">24s Hacim</span><span class="val" id="pb-vol">—</span></div>
|
||||||
<div class="tb-stat"><span class="lbl">24s Yüksek</span><span class="val" id="tb-high">—</span></div>
|
<div class="pb-sep"></div>
|
||||||
<div class="tb-stat"><span class="lbl">24s Düşük</span><span class="val" id="tb-low">—</span></div>
|
<span id="pb-tf">—</span>
|
||||||
<div class="tb-stat"><span class="lbl">24s Hacim</span><span class="val" id="tb-vol">—</span></div>
|
<div class="pb-sep" style="margin:0 10px"></div>
|
||||||
<div class="spacer"></div>
|
<div id="chart-ohlc">
|
||||||
<span id="tb-badge"></span>
|
<span>A: <b id="ohlc-o">—</b></span>
|
||||||
<div id="tb-actions" style="display:flex;gap:8px"></div>
|
<span>Y: <b id="ohlc-h">—</b></span>
|
||||||
<div class="tb-sep"></div>
|
<span>D: <b id="ohlc-l">—</b></span>
|
||||||
<div class="mode-toggle">
|
<span>K: <b id="ohlc-c">—</b></span>
|
||||||
<button class="mode-btn" id="btn-testnet" onclick="">Testnet</button>
|
|
||||||
<button class="mode-btn" id="btn-live" onclick="">Canlı</button>
|
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-logout" onclick="logout()">Çıkış</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- MAIN LAYOUT -->
|
<!-- 3 SÜTUN LAYOUT -->
|
||||||
<div id="layout">
|
<div id="layout">
|
||||||
|
|
||||||
<!-- LEFT: Ayarlar + Pozisyonlar -->
|
<!-- SOL: Açık + Kapalı Pozisyonlar -->
|
||||||
<div id="left-pane">
|
<div id="left-pane" class="pane">
|
||||||
|
|
||||||
<!-- Settings -->
|
<div class="pos-half">
|
||||||
<div id="settings-section">
|
<div class="pane-hdr">
|
||||||
<div class="pane-header">
|
Açık Pozisyonlar
|
||||||
Bot Ayarları
|
<span id="open-count" class="c-muted">0</span>
|
||||||
<span id="save-status">✓ Kaydedildi</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div style="padding:12px;display:flex;flex-direction:column;gap:10px">
|
<div class="pos-scroll">
|
||||||
<div class="form-group">
|
<table class="pos-table">
|
||||||
<label>İsim</label>
|
<thead><tr><th>Zaman</th><th>Alış</th><th>Hedef</th><th>Adet</th></tr></thead>
|
||||||
<input id="e-name" type="text" />
|
<tbody id="open-body"><tr><td colspan="4"><div class="empty-row">—</div></td></tr></tbody>
|
||||||
</div>
|
</table>
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>USDT Miktarı</label>
|
|
||||||
<input id="e-usdt" type="number" step="1" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Kar %</label>
|
|
||||||
<input id="e-profit" type="number" step="0.1" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-primary" onclick="saveBot()" style="width:100%">Kaydet</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pozisyonlar -->
|
<div class="pos-half">
|
||||||
<div id="pos-section">
|
<div class="pane-hdr">
|
||||||
<div class="pane-header">Pozisyonlar</div>
|
Kapalı İşlemler
|
||||||
<div class="pane-tabs">
|
<span id="closed-count" class="c-muted">0</span>
|
||||||
<div class="pane-tab active" id="tab-open" onclick="switchTab('open')">Açık</div>
|
|
||||||
<div class="pane-tab" id="tab-closed" onclick="switchTab('closed')">Kapalı</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="pane-content" id="left-content">
|
<div class="pos-scroll">
|
||||||
<div class="empty-row">Yükleniyor...</div>
|
<table class="pos-table">
|
||||||
|
<thead><tr><th>Zaman</th><th>Alış</th><th>Satış</th><th>Sonuç</th></tr></thead>
|
||||||
|
<tbody id="closed-body"><tr><td colspan="4"><div class="empty-row">—</div></td></tr></tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CENTER: Chart + Log -->
|
<!-- ORTA: Grafik + Log -->
|
||||||
<div id="center-pane">
|
<div id="center-pane">
|
||||||
<div id="chart-header">
|
|
||||||
<span id="chart-tf-label">—</span>
|
|
||||||
<div id="chart-ohlc">
|
|
||||||
<span>A: <b id="ohlc-o">—</b></span>
|
|
||||||
<span>Y: <b id="ohlc-h">—</b></span>
|
|
||||||
<span>D: <b id="ohlc-l">—</b></span>
|
|
||||||
<span>K: <b id="ohlc-c">—</b></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="chart-container">
|
<div id="chart-container">
|
||||||
<div id="chart"></div>
|
<div id="chart"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Log -->
|
|
||||||
<div id="log-section">
|
<div id="log-section">
|
||||||
<div class="pane-header">
|
<div class="pane-hdr">
|
||||||
Canlı Log
|
Canlı Log
|
||||||
<div style="display:flex;align-items:center;gap:6px">
|
<div style="display:flex;align-items:center;gap:6px">
|
||||||
<span id="log-dot"></span>
|
<span id="log-dot"></span>
|
||||||
<span id="log-status" style="font-size:10px;color:var(--muted)">—</span>
|
<span id="log-status" style="font-size:10px;color:var(--muted)">—</span>
|
||||||
<button class="btn" style="font-size:10px;padding:2px 7px" onclick="clearLog()">Temizle</button>
|
<button class="btn" style="font-size:10px;padding:2px 6px" onclick="clearLog()">Temizle</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="log-terminal"></div>
|
<div id="log-terminal"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- SAĞ: Durum + Ayarlar + İstatistikler -->
|
||||||
|
<div id="right-pane" class="pane">
|
||||||
|
|
||||||
|
<!-- Durum Kartı -->
|
||||||
|
<div class="status-card">
|
||||||
|
<span id="status-badge" class="badge badge-stopped status-badge-lg">Durduruldu</span>
|
||||||
|
<button id="play-btn" class="play-btn state-stopped" onclick="toggleBot()">▶</button>
|
||||||
|
<button class="del-btn-small" onclick="deleteBot()">botu sil</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ayarlar Kartı -->
|
||||||
|
<div class="pane-hdr">
|
||||||
|
Bot Ayarları
|
||||||
|
<span id="save-status" style="display:none">✓ kaydedildi</span>
|
||||||
|
</div>
|
||||||
|
<div class="settings-card">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>İsim</label>
|
||||||
|
<input id="e-name" type="text" />
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>USDT</label>
|
||||||
|
<input id="e-usdt" type="number" step="1" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Kar %</label>
|
||||||
|
<input id="e-profit" type="number" step="0.1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" onclick="saveBot()" style="width:100%;font-size:12px">Kaydet</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- İstatistikler (mock) -->
|
||||||
|
<div class="pane-hdr">İstatistikler</div>
|
||||||
|
<div class="stats-card">
|
||||||
|
<div class="stat-row"><span class="lbl">Toplam İşlem</span><span class="val" id="stat-total">—</span></div>
|
||||||
|
<div class="stat-row"><span class="lbl">Kazanılan</span><span class="val c-green" id="stat-won">—</span></div>
|
||||||
|
<div class="stat-row"><span class="lbl">Kaybedilen</span><span class="val c-red" id="stat-lost">—</span></div>
|
||||||
|
<div class="stat-row"><span class="lbl">Açık Pozisyon</span><span class="val" id="stat-open">—</span></div>
|
||||||
|
<div class="stat-row"><span class="lbl">Sembol</span><span class="val" id="stat-symbol">—</span></div>
|
||||||
|
<div class="stat-row"><span class="lbl">Timeframe</span><span class="val" id="stat-tf">—</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -252,13 +294,15 @@ let logSource = null;
|
||||||
let lastLogId = 0;
|
let lastLogId = 0;
|
||||||
let chart = null;
|
let chart = null;
|
||||||
let candleSeries = null;
|
let candleSeries = null;
|
||||||
let currentTab = 'open';
|
|
||||||
let priceWs = null;
|
let priceWs = null;
|
||||||
let priceLines = [];
|
let priceLines = [];
|
||||||
let openPositionsCache = [];
|
let openPositionsCache = [];
|
||||||
let closedPositionsCache = [];
|
let closedPositionsCache = [];
|
||||||
|
|
||||||
// ── API helper ────────────────────────────────────
|
const TF_MAP = { '1m':'1m','5m':'5m','15m':'15m','30m':'30m','1h':'1h','4h':'4h','1d':'1d','1w':'1w' };
|
||||||
|
const TF_SECONDS = { '1m':60,'5m':300,'15m':900,'30m':1800,'1h':3600,'4h':14400,'1d':86400,'1w':604800 };
|
||||||
|
|
||||||
|
// ── helpers ───────────────────────────────────────
|
||||||
async function api(method, path, body) {
|
async function api(method, path, body) {
|
||||||
const res = await fetch('/api' + path, {
|
const res = await fetch('/api' + path, {
|
||||||
method, credentials: 'include',
|
method, credentials: 'include',
|
||||||
|
|
@ -270,19 +314,15 @@ async function api(method, path, body) {
|
||||||
}
|
}
|
||||||
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||||||
function fmt(n, d=2) { return parseFloat(n).toLocaleString('tr-TR', { minimumFractionDigits: d, maximumFractionDigits: d }); }
|
function fmt(n, d=2) { return parseFloat(n).toLocaleString('tr-TR', { minimumFractionDigits: d, maximumFractionDigits: d }); }
|
||||||
|
function shortDate(ms) { return new Date(ms).toLocaleString('tr-TR',{day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}); }
|
||||||
// ── Timeframe → Binance interval ─────────────────
|
|
||||||
const TF_MAP = { '1m':'1m','5m':'5m','15m':'15m','30m':'30m','1h':'1h','4h':'4h','1d':'1d','1w':'1w' };
|
|
||||||
const TF_SECONDS = { '1m':60,'5m':300,'15m':900,'30m':1800,'1h':3600,'4h':14400,'1d':86400,'1w':604800 };
|
|
||||||
function buyTime(ms) {
|
function buyTime(ms) {
|
||||||
const tf = TF_SECONDS[bot.timeframe] || 300;
|
const tf = TF_SECONDS[bot.timeframe] || 300;
|
||||||
const sec = Math.floor(ms / 1000);
|
const sec = Math.floor(ms / 1000);
|
||||||
// Hangi mum aralığına düştüğünü bul, bir önceki mumun açılışını döndür
|
const cur = Math.floor(sec / tf) * tf;
|
||||||
const currentCandleOpen = Math.floor(sec / tf) * tf;
|
return cur - tf;
|
||||||
return currentCandleOpen - tf;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Load bot ──────────────────────────────────────
|
// ── Load ──────────────────────────────────────────
|
||||||
async function loadBot() {
|
async function loadBot() {
|
||||||
const [botRes, modeRes] = await Promise.all([
|
const [botRes, modeRes] = await Promise.all([
|
||||||
api('GET', `/bots/${BOT_ID}`),
|
api('GET', `/bots/${BOT_ID}`),
|
||||||
|
|
@ -290,31 +330,45 @@ async function loadBot() {
|
||||||
]);
|
]);
|
||||||
if (!botRes.ok) { window.location.href = '/dashboard'; return; }
|
if (!botRes.ok) { window.location.href = '/dashboard'; return; }
|
||||||
bot = await botRes.json();
|
bot = await botRes.json();
|
||||||
|
|
||||||
if (modeRes.ok) {
|
if (modeRes.ok) {
|
||||||
const { mode } = await modeRes.json();
|
const { mode } = await modeRes.json();
|
||||||
const btnLive = document.getElementById('btn-live');
|
document.getElementById('btn-live').className = 'mode-btn' + (mode === 'live' ? ' active-live' : '');
|
||||||
const btnTest = document.getElementById('btn-testnet');
|
document.getElementById('btn-testnet').className = 'mode-btn' + (mode === 'testnet' ? ' active-testnet' : '');
|
||||||
btnLive.className = 'mode-btn' + (mode === 'live' ? ' active-live' : '');
|
|
||||||
btnTest.className = 'mode-btn' + (mode === 'testnet' ? ' active-testnet' : '');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.title = `${bot.name} — CryptoFox Mukan Edition`;
|
document.title = `${bot.name} — CryptoFox Mukan Edition`;
|
||||||
renderTopBar();
|
renderPriceBar();
|
||||||
|
renderStatusCard();
|
||||||
renderEditForm();
|
renderEditForm();
|
||||||
document.getElementById('chart-tf-label').textContent = bot.timeframe;
|
renderStats();
|
||||||
|
|
||||||
initChart();
|
initChart();
|
||||||
loadTicker();
|
loadTicker();
|
||||||
startPriceWs();
|
startPriceWs();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTopBar() {
|
function renderPriceBar() {
|
||||||
document.getElementById('tb-name').textContent = bot.name;
|
document.getElementById('pb-name').textContent = bot.name;
|
||||||
document.getElementById('tb-symbol').textContent = bot.symbol;
|
document.getElementById('pb-symbol').textContent = bot.symbol;
|
||||||
document.getElementById('tb-badge').innerHTML = `<span class="badge ${bot.running ? 'badge-running' : 'badge-stopped'}">${bot.running ? 'Çalışıyor' : 'Durdu'}</span>`;
|
document.getElementById('pb-tf').textContent = bot.timeframe;
|
||||||
const actions = document.getElementById('tb-actions');
|
}
|
||||||
if (bot.running) {
|
|
||||||
actions.innerHTML = `<button class="btn btn-stop" onclick="stopBot()">Durdur</button><button class="btn btn-delete" onclick="deleteBot()">Sil</button>`;
|
function renderStatusCard() {
|
||||||
|
const running = bot.running;
|
||||||
|
const badge = document.getElementById('status-badge');
|
||||||
|
badge.textContent = running ? 'Çalışıyor' : 'Durduruldu';
|
||||||
|
badge.className = 'badge status-badge-lg ' + (running ? 'badge-running' : 'badge-stopped');
|
||||||
|
|
||||||
|
const btn = document.getElementById('play-btn');
|
||||||
|
if (running) {
|
||||||
|
btn.textContent = '⏸';
|
||||||
|
btn.className = 'play-btn state-running';
|
||||||
|
btn.title = 'Durdur';
|
||||||
} else {
|
} else {
|
||||||
actions.innerHTML = `<button class="btn btn-start" onclick="startBot()">Başlat</button><button class="btn btn-delete" onclick="deleteBot()">Sil</button>`;
|
btn.textContent = '▶';
|
||||||
|
btn.className = 'play-btn state-stopped';
|
||||||
|
btn.title = 'Başlat';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -324,20 +378,34 @@ function renderEditForm() {
|
||||||
document.getElementById('e-profit').value = bot.profit_percent;
|
document.getElementById('e-profit').value = bot.profit_percent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderStats() {
|
||||||
|
document.getElementById('stat-symbol').textContent = bot.symbol;
|
||||||
|
document.getElementById('stat-tf').textContent = bot.timeframe;
|
||||||
|
const total = closedPositionsCache.length;
|
||||||
|
const won = closedPositionsCache.filter(c => c.status === 'FILLED').length;
|
||||||
|
const lost = total - won;
|
||||||
|
document.getElementById('stat-total').textContent = total || '—';
|
||||||
|
document.getElementById('stat-won').textContent = won || '—';
|
||||||
|
document.getElementById('stat-lost').textContent = lost || '—';
|
||||||
|
document.getElementById('stat-open').textContent = openPositionsCache.length || '—';
|
||||||
|
}
|
||||||
|
|
||||||
// ── Bot actions ───────────────────────────────────
|
// ── Bot actions ───────────────────────────────────
|
||||||
async function startBot() {
|
async function toggleBot() {
|
||||||
await api('POST', `/bots/${BOT_ID}/start`);
|
if (bot.running) {
|
||||||
await loadBot();
|
await api('POST', `/bots/${BOT_ID}/stop`);
|
||||||
}
|
} else {
|
||||||
async function stopBot() {
|
await api('POST', `/bots/${BOT_ID}/start`);
|
||||||
await api('POST', `/bots/${BOT_ID}/stop`);
|
}
|
||||||
await loadBot();
|
await loadBot();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteBot() {
|
async function deleteBot() {
|
||||||
if (!confirm(`"${bot.name}" botunu silmek istiyor musun?`)) return;
|
if (!confirm(`"${bot.name}" botunu silmek istiyor musun?`)) return;
|
||||||
await api('DELETE', `/bots/${BOT_ID}`);
|
await api('DELETE', `/bots/${BOT_ID}`);
|
||||||
window.location.href = '/dashboard';
|
window.location.href = '/bots';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveBot() {
|
async function saveBot() {
|
||||||
const name = document.getElementById('e-name').value.trim();
|
const name = document.getElementById('e-name').value.trim();
|
||||||
const usdt_amount = parseFloat(document.getElementById('e-usdt').value);
|
const usdt_amount = parseFloat(document.getElementById('e-usdt').value);
|
||||||
|
|
@ -345,30 +413,31 @@ async function saveBot() {
|
||||||
if (!name || !usdt_amount || !profit_percent) { alert('Tüm alanları doldurun'); return; }
|
if (!name || !usdt_amount || !profit_percent) { alert('Tüm alanları doldurun'); return; }
|
||||||
const res = await api('PUT', `/bots/${BOT_ID}`, { name, usdt_amount, profit_percent });
|
const res = await api('PUT', `/bots/${BOT_ID}`, { name, usdt_amount, profit_percent });
|
||||||
bot = await res.json();
|
bot = await res.json();
|
||||||
renderTopBar();
|
renderPriceBar();
|
||||||
|
renderStatusCard();
|
||||||
|
renderStats();
|
||||||
const s = document.getElementById('save-status');
|
const s = document.getElementById('save-status');
|
||||||
s.style.display = 'inline';
|
s.style.display = 'inline';
|
||||||
setTimeout(() => s.style.display = 'none', 2000);
|
setTimeout(() => s.style.display = 'none', 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Ticker (24h stats) ────────────────────────────
|
// ── Ticker ────────────────────────────────────────
|
||||||
async function loadTicker() {
|
async function loadTicker() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`https://api.binance.com/api/v3/ticker/24hr?symbol=${bot.symbol}`);
|
const res = await fetch(`https://api.binance.com/api/v3/ticker/24hr?symbol=${bot.symbol}`);
|
||||||
const t = await res.json();
|
const t = await res.json();
|
||||||
const change = parseFloat(t.priceChangePercent);
|
const change = parseFloat(t.priceChangePercent);
|
||||||
document.getElementById('tb-price').textContent = fmt(t.lastPrice, 2);
|
document.getElementById('pb-price').textContent = fmt(t.lastPrice, 2);
|
||||||
const chEl = document.getElementById('tb-change');
|
const chEl = document.getElementById('pb-change');
|
||||||
chEl.textContent = (change >= 0 ? '+' : '') + change.toFixed(2) + '%';
|
chEl.textContent = (change >= 0 ? '+' : '') + change.toFixed(2) + '%';
|
||||||
chEl.style.color = change >= 0 ? 'var(--green)' : 'var(--red)';
|
chEl.style.color = change >= 0 ? 'var(--green)' : 'var(--red)';
|
||||||
document.getElementById('tb-high').textContent = fmt(t.highPrice, 2);
|
document.getElementById('pb-high').textContent = fmt(t.highPrice, 2);
|
||||||
document.getElementById('tb-low').textContent = fmt(t.lowPrice, 2);
|
document.getElementById('pb-low').textContent = fmt(t.lowPrice, 2);
|
||||||
const vol = parseFloat(t.quoteVolume);
|
const vol = parseFloat(t.quoteVolume);
|
||||||
document.getElementById('tb-vol').textContent = vol >= 1e9 ? (vol/1e9).toFixed(2)+'B' : vol >= 1e6 ? (vol/1e6).toFixed(2)+'M' : fmt(vol, 0);
|
document.getElementById('pb-vol').textContent = vol >= 1e9 ? (vol/1e9).toFixed(2)+'B' : vol >= 1e6 ? (vol/1e6).toFixed(2)+'M' : fmt(vol, 0);
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Live price via Binance WS ─────────────────────
|
|
||||||
function startPriceWs() {
|
function startPriceWs() {
|
||||||
if (priceWs) priceWs.close();
|
if (priceWs) priceWs.close();
|
||||||
const sym = bot.symbol.toLowerCase();
|
const sym = bot.symbol.toLowerCase();
|
||||||
|
|
@ -377,8 +446,8 @@ function startPriceWs() {
|
||||||
const t = JSON.parse(e.data);
|
const t = JSON.parse(e.data);
|
||||||
const price = parseFloat(t.c);
|
const price = parseFloat(t.c);
|
||||||
const change = parseFloat(t.P);
|
const change = parseFloat(t.P);
|
||||||
document.getElementById('tb-price').textContent = fmt(price, 2);
|
document.getElementById('pb-price').textContent = fmt(price, 2);
|
||||||
const chEl = document.getElementById('tb-change');
|
const chEl = document.getElementById('pb-change');
|
||||||
chEl.textContent = (change >= 0 ? '+' : '') + change.toFixed(2) + '%';
|
chEl.textContent = (change >= 0 ? '+' : '') + change.toFixed(2) + '%';
|
||||||
chEl.style.color = change >= 0 ? 'var(--green)' : 'var(--red)';
|
chEl.style.color = change >= 0 ? 'var(--green)' : 'var(--red)';
|
||||||
};
|
};
|
||||||
|
|
@ -404,8 +473,6 @@ function initChart() {
|
||||||
borderUpColor: '#2ecc71', borderDownColor: '#e74c3c',
|
borderUpColor: '#2ecc71', borderDownColor: '#e74c3c',
|
||||||
wickUpColor: '#2ecc71', wickDownColor: '#e74c3c',
|
wickUpColor: '#2ecc71', wickDownColor: '#e74c3c',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Crosshair OHLC güncelle
|
|
||||||
chart.subscribeCrosshairMove(p => {
|
chart.subscribeCrosshairMove(p => {
|
||||||
if (!p.seriesData || !p.seriesData.size) return;
|
if (!p.seriesData || !p.seriesData.size) return;
|
||||||
const d = p.seriesData.get(candleSeries);
|
const d = p.seriesData.get(candleSeries);
|
||||||
|
|
@ -415,33 +482,22 @@ function initChart() {
|
||||||
document.getElementById('ohlc-l').textContent = fmt(d.low, 2);
|
document.getElementById('ohlc-l').textContent = fmt(d.low, 2);
|
||||||
document.getElementById('ohlc-c').textContent = fmt(d.close, 2);
|
document.getElementById('ohlc-c').textContent = fmt(d.close, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Resize observer
|
|
||||||
new ResizeObserver(() => {
|
new ResizeObserver(() => {
|
||||||
chart.applyOptions({ width: container.clientWidth, height: container.clientHeight });
|
chart.applyOptions({ width: container.clientWidth, height: container.clientHeight });
|
||||||
}).observe(container);
|
}).observe(container);
|
||||||
|
|
||||||
loadKlines();
|
loadKlines();
|
||||||
startKlineWs();
|
startKlineWs();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadKlines() {
|
async function loadKlines() {
|
||||||
const interval = TF_MAP[bot.timeframe] || '5m';
|
const interval = TF_MAP[bot.timeframe] || '5m';
|
||||||
const baseUrl = 'https://api.binance.com/api/v3/klines';
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${baseUrl}?symbol=${bot.symbol}&interval=${interval}&limit=200`);
|
const res = await fetch(`https://api.binance.com/api/v3/klines?symbol=${bot.symbol}&interval=${interval}&limit=200`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const candles = data.map(k => ({
|
const candles = data.map(k => ({ time: Math.floor(k[0]/1000), open: parseFloat(k[1]), high: parseFloat(k[2]), low: parseFloat(k[3]), close: parseFloat(k[4]) }));
|
||||||
time: Math.floor(k[0] / 1000),
|
|
||||||
open: parseFloat(k[1]),
|
|
||||||
high: parseFloat(k[2]),
|
|
||||||
low: parseFloat(k[3]),
|
|
||||||
close: parseFloat(k[4]),
|
|
||||||
}));
|
|
||||||
candleSeries.setData(candles);
|
candleSeries.setData(candles);
|
||||||
chart.timeScale().scrollToRealTime();
|
chart.timeScale().scrollToRealTime();
|
||||||
chart.timeScale().setVisibleLogicalRange({ from: candles.length - 80, to: candles.length + 5 });
|
chart.timeScale().setVisibleLogicalRange({ from: candles.length - 80, to: candles.length + 5 });
|
||||||
|
|
||||||
if (candles.length) {
|
if (candles.length) {
|
||||||
const last = candles[candles.length - 1];
|
const last = candles[candles.length - 1];
|
||||||
document.getElementById('ohlc-o').textContent = fmt(last.open, 2);
|
document.getElementById('ohlc-o').textContent = fmt(last.open, 2);
|
||||||
|
|
@ -450,7 +506,7 @@ async function loadKlines() {
|
||||||
document.getElementById('ohlc-c').textContent = fmt(last.close, 2);
|
document.getElementById('ohlc-c').textContent = fmt(last.close, 2);
|
||||||
}
|
}
|
||||||
updateChartOverlays();
|
updateChartOverlays();
|
||||||
} catch(e) { console.error('loadKlines error:', e); }
|
} catch(e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let klineWs = null;
|
let klineWs = null;
|
||||||
|
|
@ -458,139 +514,92 @@ function startKlineWs() {
|
||||||
if (klineWs) klineWs.close();
|
if (klineWs) klineWs.close();
|
||||||
const interval = TF_MAP[bot.timeframe] || '5m';
|
const interval = TF_MAP[bot.timeframe] || '5m';
|
||||||
const sym = bot.symbol.toLowerCase();
|
const sym = bot.symbol.toLowerCase();
|
||||||
const wsUrl = `wss://stream.binance.com:9443/ws/${sym}@kline_${interval}`;
|
klineWs = new WebSocket(`wss://stream.binance.com:9443/ws/${sym}@kline_${interval}`);
|
||||||
klineWs = new WebSocket(wsUrl);
|
|
||||||
klineWs.onmessage = e => {
|
klineWs.onmessage = e => {
|
||||||
const msg = JSON.parse(e.data);
|
const k = JSON.parse(e.data).k;
|
||||||
const k = msg.k;
|
const candle = { time: Math.floor(k.t/1000), open: parseFloat(k.o), high: parseFloat(k.h), low: parseFloat(k.l), close: parseFloat(k.c) };
|
||||||
const candle = {
|
|
||||||
time: Math.floor(k.t / 1000),
|
|
||||||
open: parseFloat(k.o), high: parseFloat(k.h),
|
|
||||||
low: parseFloat(k.l), close: parseFloat(k.c),
|
|
||||||
};
|
|
||||||
candleSeries.update(candle);
|
candleSeries.update(candle);
|
||||||
document.getElementById('ohlc-o').textContent = fmt(candle.open, 2);
|
document.getElementById('ohlc-o').textContent = fmt(candle.open, 2);
|
||||||
document.getElementById('ohlc-h').textContent = fmt(candle.high, 2);
|
document.getElementById('ohlc-h').textContent = fmt(candle.high, 2);
|
||||||
document.getElementById('ohlc-l').textContent = fmt(candle.low, 2);
|
document.getElementById('ohlc-l').textContent = fmt(candle.low, 2);
|
||||||
document.getElementById('ohlc-c').textContent = fmt(candle.close, 2);
|
document.getElementById('ohlc-c').textContent = fmt(candle.close, 2);
|
||||||
// Fiyatı da güncelle
|
document.getElementById('pb-price').textContent = fmt(candle.close, 2);
|
||||||
document.getElementById('tb-price').textContent = fmt(candle.close, 2);
|
|
||||||
};
|
};
|
||||||
klineWs.onerror = () => {};
|
klineWs.onerror = () => {};
|
||||||
klineWs.onclose = () => setTimeout(startKlineWs, 5000);
|
klineWs.onclose = () => setTimeout(startKlineWs, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Chart overlays ────────────────────────────────
|
|
||||||
function updateChartOverlays() {
|
function updateChartOverlays() {
|
||||||
if (!candleSeries) return;
|
if (!candleSeries) return;
|
||||||
|
|
||||||
// Price lines temizle
|
|
||||||
priceLines.forEach(pl => candleSeries.removePriceLine(pl));
|
priceLines.forEach(pl => candleSeries.removePriceLine(pl));
|
||||||
priceLines = [];
|
priceLines = [];
|
||||||
|
|
||||||
// Açık pozisyonlar → hedef price line (yeşil) + alış marker
|
|
||||||
openPositionsCache.forEach(p => {
|
openPositionsCache.forEach(p => {
|
||||||
priceLines.push(candleSeries.createPriceLine({
|
priceLines.push(candleSeries.createPriceLine({
|
||||||
price: p.sell_target,
|
price: p.sell_target, color: '#2ecc71', lineWidth: 1,
|
||||||
color: '#2ecc71',
|
|
||||||
lineWidth: 1,
|
|
||||||
lineStyle: LightweightCharts.LineStyle.Dashed,
|
lineStyle: LightweightCharts.LineStyle.Dashed,
|
||||||
axisLabelVisible: true,
|
axisLabelVisible: true, title: fmt(p.sell_target, 2),
|
||||||
title: fmt(p.sell_target, 2),
|
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
const buyMap = new Map(), sellMap = new Map();
|
||||||
// Markers: açık + kapalı pozisyonlar
|
|
||||||
const buyMap = new Map();
|
|
||||||
const sellMap = new Map();
|
|
||||||
|
|
||||||
openPositionsCache.forEach(p => {
|
openPositionsCache.forEach(p => {
|
||||||
const t = buyTime(p.opened_at);
|
const t = buyTime(p.opened_at);
|
||||||
if (!buyMap.has(t)) buyMap.set(t, []);
|
if (!buyMap.has(t)) buyMap.set(t, []);
|
||||||
buyMap.get(t).push(fmt(p.buy_price, 2));
|
buyMap.get(t).push(fmt(p.buy_price, 2));
|
||||||
});
|
});
|
||||||
|
|
||||||
closedPositionsCache.forEach(c => {
|
closedPositionsCache.forEach(c => {
|
||||||
const tb = buyTime(c.opened_at);
|
const tb = buyTime(c.opened_at);
|
||||||
if (!buyMap.has(tb)) buyMap.set(tb, []);
|
if (!buyMap.has(tb)) buyMap.set(tb, []);
|
||||||
buyMap.get(tb).push(fmt(c.buy_price, 2));
|
buyMap.get(tb).push(fmt(c.buy_price, 2));
|
||||||
|
|
||||||
if (c.status === 'FILLED') {
|
if (c.status === 'FILLED') {
|
||||||
const ts = Math.floor(c.closed_at / 1000);
|
const ts = Math.floor(c.closed_at / 1000);
|
||||||
if (!sellMap.has(ts)) sellMap.set(ts, []);
|
if (!sellMap.has(ts)) sellMap.set(ts, []);
|
||||||
sellMap.get(ts).push(fmt(c.sell_target, 2));
|
sellMap.get(ts).push(fmt(c.sell_target, 2));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const markers = [];
|
const markers = [];
|
||||||
buyMap.forEach((prices, time) => {
|
buyMap.forEach((_, time) => markers.push({ time, position: 'belowBar', color: '#6c63ff', shape: 'arrowUp', size: 1 }));
|
||||||
markers.push({
|
sellMap.forEach((_, time) => markers.push({ time, position: 'aboveBar', color: '#2ecc71', shape: 'arrowDown', size: 1 }));
|
||||||
time,
|
|
||||||
position: 'belowBar',
|
|
||||||
color: '#6c63ff',
|
|
||||||
shape: 'arrowUp',
|
|
||||||
text: prices.length > 1 ? `×${prices.length}` : '',
|
|
||||||
size: 1,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
sellMap.forEach((prices, time) => {
|
|
||||||
markers.push({
|
|
||||||
time,
|
|
||||||
position: 'aboveBar',
|
|
||||||
color: '#2ecc71',
|
|
||||||
shape: 'arrowDown',
|
|
||||||
text: prices.length > 1 ? `×${prices.length}` : '',
|
|
||||||
size: 1,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
markers.sort((a, b) => a.time - b.time);
|
markers.sort((a, b) => a.time - b.time);
|
||||||
candleSeries.setMarkers(markers);
|
candleSeries.setMarkers(markers);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Pozisyon sekmeleri ────────────────────────────
|
// ── Positions ─────────────────────────────────────
|
||||||
function switchTab(tab) {
|
|
||||||
currentTab = tab;
|
|
||||||
document.getElementById('tab-open').classList.toggle('active', tab === 'open');
|
|
||||||
document.getElementById('tab-closed').classList.toggle('active', tab === 'closed');
|
|
||||||
if (tab === 'open') loadPositions();
|
|
||||||
else loadClosed();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadPositions() {
|
async function loadPositions() {
|
||||||
const res = await api('GET', `/bots/${BOT_ID}/positions`);
|
const res = await api('GET', `/bots/${BOT_ID}/positions`);
|
||||||
const data = await res.json();
|
openPositionsCache = await res.json();
|
||||||
openPositionsCache = data;
|
|
||||||
if (candleSeries) updateChartOverlays();
|
if (candleSeries) updateChartOverlays();
|
||||||
const el = document.getElementById('left-content');
|
document.getElementById('open-count').textContent = openPositionsCache.length;
|
||||||
if (!data.length) { el.innerHTML = '<div class="empty-row">Açık pozisyon yok</div>'; return; }
|
const tbody = document.getElementById('open-body');
|
||||||
el.innerHTML = `<table class="pos-table">
|
if (!openPositionsCache.length) {
|
||||||
<thead><tr><th>Alış</th><th>Hedef</th><th>Miktar</th><th>Zaman</th></tr></thead>
|
tbody.innerHTML = '<tr><td colspan="4"><div class="empty-row">Açık pozisyon yok</div></td></tr>';
|
||||||
<tbody>${data.map(p => `<tr>
|
} else {
|
||||||
|
tbody.innerHTML = openPositionsCache.map(p => `<tr>
|
||||||
|
<td>${shortDate(p.opened_at)}</td>
|
||||||
<td>${fmt(p.buy_price, 4)}</td>
|
<td>${fmt(p.buy_price, 4)}</td>
|
||||||
<td class="profit-up">${fmt(p.sell_target, 4)}</td>
|
<td class="c-green">${fmt(p.sell_target, 4)}</td>
|
||||||
<td>${p.quantity}</td>
|
<td class="c-muted">${parseFloat(p.quantity).toFixed(4)}</td>
|
||||||
<td style="font-size:10px;color:var(--muted)">${new Date(p.opened_at).toLocaleString('tr-TR',{hour:'2-digit',minute:'2-digit',day:'2-digit',month:'2-digit'})}</td>
|
</tr>`).join('');
|
||||||
</tr>`).join('')}</tbody>
|
}
|
||||||
</table>`;
|
renderStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadClosed() {
|
async function loadClosed() {
|
||||||
const res = await api('GET', `/bots/${BOT_ID}/positions/closed`);
|
const res = await api('GET', `/bots/${BOT_ID}/positions/closed`);
|
||||||
const data = await res.json();
|
closedPositionsCache = await res.json();
|
||||||
closedPositionsCache = data;
|
|
||||||
if (candleSeries) updateChartOverlays();
|
if (candleSeries) updateChartOverlays();
|
||||||
const el = document.getElementById('left-content');
|
document.getElementById('closed-count').textContent = closedPositionsCache.length;
|
||||||
if (!data.length) { el.innerHTML = '<div class="empty-row">Henüz kapalı işlem yok</div>'; return; }
|
const tbody = document.getElementById('closed-body');
|
||||||
el.innerHTML = `<table class="pos-table">
|
if (!closedPositionsCache.length) {
|
||||||
<thead><tr><th>Alış</th><th>Satış</th><th>Durum</th><th>Kapanış</th></tr></thead>
|
tbody.innerHTML = '<tr><td colspan="4"><div class="empty-row">Henüz işlem yok</div></td></tr>';
|
||||||
<tbody>${data.map(c => `<tr>
|
} else {
|
||||||
|
tbody.innerHTML = closedPositionsCache.map(c => `<tr>
|
||||||
|
<td>${shortDate(c.opened_at)}</td>
|
||||||
<td>${fmt(c.buy_price, 4)}</td>
|
<td>${fmt(c.buy_price, 4)}</td>
|
||||||
<td class="${c.status==='FILLED'?'profit-up':'profit-down'}">${fmt(c.sell_target, 4)}</td>
|
<td>${fmt(c.sell_target, 4)}</td>
|
||||||
<td><span class="badge ${c.status==='FILLED'?'badge-running':'badge-stopped'}" style="font-size:10px">${c.status}</span></td>
|
<td class="${c.status === 'FILLED' ? 'c-green' : 'c-muted'}" style="font-size:10px;font-weight:600">${c.status}</td>
|
||||||
<td style="font-size:10px;color:var(--muted)">${new Date(c.closed_at).toLocaleString('tr-TR',{hour:'2-digit',minute:'2-digit',day:'2-digit',month:'2-digit'})}</td>
|
</tr>`).join('');
|
||||||
</tr>`).join('')}</tbody>
|
}
|
||||||
</table>`;
|
renderStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Log ───────────────────────────────────────────
|
// ── Log ───────────────────────────────────────────
|
||||||
|
|
@ -621,10 +630,7 @@ function connectLogSSE() {
|
||||||
if (log.id <= lastLogId) return;
|
if (log.id <= lastLogId) return;
|
||||||
lastLogId = log.id;
|
lastLogId = log.id;
|
||||||
appendLog(log);
|
appendLog(log);
|
||||||
if (log.level === 'trade') {
|
if (log.level === 'trade') { loadPositions(); loadClosed(); }
|
||||||
loadPositions();
|
|
||||||
loadClosed();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
logSource.onopen = () => {
|
logSource.onopen = () => {
|
||||||
document.getElementById('log-dot').style.background = 'var(--green)';
|
document.getElementById('log-dot').style.background = 'var(--green)';
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue