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:
Mukan Erkin TÖRÜK 2026-04-19 20:38:09 +03:00
parent 64806dd32e
commit 050cc18051
2 changed files with 319 additions and 313 deletions

View file

@ -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(),

View file

@ -16,179 +16,252 @@
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"> ı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">
<table class="pos-table">
<thead><tr><th>Zaman</th><th>Alış</th><th>Hedef</th><th>Adet</th></tr></thead>
<tbody id="open-body"><tr><td colspan="4"><div class="empty-row"></div></td></tr></tbody>
</table>
</div>
</div>
<div class="pos-half">
<div class="pane-hdr">
Kapalı İşlemler
<span id="closed-count" class="c-muted">0</span>
</div>
<div class="pos-scroll">
<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>
<!-- ORTA: Grafik + Log -->
<div id="center-pane">
<div id="chart-container">
<div id="chart"></div>
</div>
<div id="log-section">
<div class="pane-hdr">
Canlı Log
<div style="display:flex;align-items:center;gap:6px">
<span id="log-dot"></span>
<span id="log-status" style="font-size:10px;color:var(--muted)"></span>
<button class="btn" style="font-size:10px;padding:2px 6px" onclick="clearLog()">Temizle</button>
</div>
</div>
<div id="log-terminal"></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"> <div class="form-group">
<label>İsim</label> <label>İsim</label>
<input id="e-name" type="text" /> <input id="e-name" type="text" />
</div> </div>
<div class="form-grid"> <div class="form-grid">
<div class="form-group"> <div class="form-group">
<label>USDT Miktarı</label> <label>USDT</label>
<input id="e-usdt" type="number" step="1" /> <input id="e-usdt" type="number" step="1" />
</div> </div>
<div class="form-group"> <div class="form-group">
@ -196,53 +269,22 @@
<input id="e-profit" type="number" step="0.1" /> <input id="e-profit" type="number" step="0.1" />
</div> </div>
</div> </div>
<button class="btn btn-primary" onclick="saveBot()" style="width:100%">Kaydet</button> <button class="btn btn-primary" onclick="saveBot()" style="width:100%;font-size:12px">Kaydet</button>
</div>
</div> </div>
<!-- Pozisyonlar --> <!-- İstatistikler (mock) -->
<div id="pos-section"> <div class="pane-hdr">İstatistikler</div>
<div class="pane-header">Pozisyonlar</div> <div class="stats-card">
<div class="pane-tabs"> <div class="stat-row"><span class="lbl">Toplam İşlem</span><span class="val" id="stat-total"></span></div>
<div class="pane-tab active" id="tab-open" onclick="switchTab('open')">ık</div> <div class="stat-row"><span class="lbl">Kazanılan</span><span class="val c-green" id="stat-won"></span></div>
<div class="pane-tab" id="tab-closed" onclick="switchTab('closed')">Kapalı</div> <div class="stat-row"><span class="lbl">Kaybedilen</span><span class="val c-red" id="stat-lost"></span></div>
</div> <div class="stat-row"><span class="lbl">ık Pozisyon</span><span class="val" id="stat-open"></span></div>
<div class="pane-content" id="left-content"> <div class="stat-row"><span class="lbl">Sembol</span><span class="val" id="stat-symbol"></span></div>
<div class="empty-row">Yükleniyor...</div> <div class="stat-row"><span class="lbl">Timeframe</span><span class="val" id="stat-tf"></span></div>
</div>
</div> </div>
</div> </div>
<!-- CENTER: Chart + Log -->
<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"></div>
</div>
<!-- Log -->
<div id="log-section">
<div class="pane-header">
Canlı Log
<div style="display:flex;align-items:center;gap:6px">
<span id="log-dot"></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>
</div>
</div>
<div id="log-terminal"></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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); } function esc(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
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();
}
async function stopBot() {
await api('POST', `/bots/${BOT_ID}/stop`); await api('POST', `/bots/${BOT_ID}/stop`);
} else {
await api('POST', `/bots/${BOT_ID}/start`);
}
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">ı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">ı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)';