feat(dashboard): comprehensive UI/UX overhaul, order mode switcher (market vs scheduled limit), pending orders table with cancel action, and fix /api/learning 404
This commit is contained in:
+76
-22
@@ -176,6 +176,7 @@ async def get_recent_news():
|
||||
class OrderRequest(BaseModel):
|
||||
action: str
|
||||
volume: float
|
||||
target_price: Optional[float] = None
|
||||
sl_pips: Optional[float] = None
|
||||
tp_pips: Optional[float] = None
|
||||
|
||||
@@ -281,39 +282,92 @@ async def close_all_positions():
|
||||
return JSONResponse({"success": False, "error": str(e)}, status_code=500)
|
||||
|
||||
|
||||
@app.get("/api/pending")
|
||||
async def get_pending_orders():
|
||||
"""Get active scheduled pending orders from MT5."""
|
||||
try:
|
||||
from core.mt5_connector import MT5Connector
|
||||
mt5 = MT5Connector()
|
||||
if mt5.connect(max_retries=1, retry_delay=0.1):
|
||||
pending = mt5.get_pending_orders(auto_reconnect=False)
|
||||
for p in pending:
|
||||
if "time" in p and hasattr(p["time"], "isoformat"):
|
||||
p["time"] = p["time"].isoformat()
|
||||
return JSONResponse(pending)
|
||||
return JSONResponse([])
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching pending orders: {e}")
|
||||
return JSONResponse([])
|
||||
|
||||
|
||||
@app.post("/api/cancel-order/{ticket}")
|
||||
async def cancel_pending_order(ticket: int):
|
||||
"""Cancel a scheduled pending order by ticket."""
|
||||
try:
|
||||
from core.mt5_connector import MT5Connector
|
||||
mt5 = MT5Connector()
|
||||
if mt5.connect(max_retries=1, retry_delay=0.1):
|
||||
success = mt5.cancel_order(ticket)
|
||||
return JSONResponse({"success": success, "ticket": ticket})
|
||||
return JSONResponse({"success": False, "error": "MT5 offline"}, status_code=500)
|
||||
except Exception as e:
|
||||
logger.error(f"Error canceling order {ticket}: {e}")
|
||||
return JSONResponse({"success": False, "error": str(e)}, status_code=500)
|
||||
|
||||
|
||||
@app.post("/api/order")
|
||||
async def place_manual_order(req: OrderRequest):
|
||||
"""Place a manual market order from the dashboard."""
|
||||
"""Place a manual market or scheduled pending order from the dashboard."""
|
||||
try:
|
||||
from core.mt5_connector import MT5Connector
|
||||
mt5 = MT5Connector()
|
||||
if mt5.connect(max_retries=1, retry_delay=0.1):
|
||||
symbol = "XAUUSD"
|
||||
tick = mt5.get_current_price(symbol)
|
||||
tick = mt5.get_current_tick(symbol)
|
||||
if not tick:
|
||||
return JSONResponse({"success": False, "error": "Could not fetch XAUUSD price"}, status_code=400)
|
||||
return JSONResponse({"success": False, "error": "Could not fetch live XAUUSD tick"}, status_code=400)
|
||||
|
||||
price = tick["ask"] if req.action.upper() == "BUY" else tick["bid"]
|
||||
act = req.action.upper()
|
||||
vol = max(0.01, round(req.volume, 2))
|
||||
pip = 0.10
|
||||
sl = None
|
||||
tp = None
|
||||
if req.sl_pips:
|
||||
sl = price - (req.sl_pips * pip) if req.action.upper() == "BUY" else price + (req.sl_pips * pip)
|
||||
if req.tp_pips:
|
||||
tp = price + (req.tp_pips * pip) if req.action.upper() == "BUY" else price - (req.tp_pips * pip)
|
||||
|
||||
order_type = "BUY" if req.action.upper() == "BUY" else "SELL"
|
||||
res = mt5.open_position(
|
||||
symbol=symbol,
|
||||
order_type=order_type,
|
||||
volume=max(0.01, round(req.volume, 2)),
|
||||
sl=sl,
|
||||
tp=tp,
|
||||
comment="Dashboard Order"
|
||||
)
|
||||
if res:
|
||||
return JSONResponse({"success": True, "ticket": res.get("ticket"), "price": price})
|
||||
return JSONResponse({"success": False, "error": "MT5 order execution failed"}, status_code=400)
|
||||
if act in ["BUY", "SELL"]:
|
||||
price = tick["ask"] if act == "BUY" else tick["bid"]
|
||||
sl = (price - req.sl_pips * pip) if (req.sl_pips and act == "BUY") else ((price + req.sl_pips * pip) if req.sl_pips else 0.0)
|
||||
tp = (price + req.tp_pips * pip) if (req.tp_pips and act == "BUY") else ((price - req.tp_pips * pip) if req.tp_pips else 0.0)
|
||||
res = mt5.send_market_order(
|
||||
order_type=act,
|
||||
symbol=symbol,
|
||||
volume=vol,
|
||||
sl=sl,
|
||||
tp=tp,
|
||||
comment="DASHBOARD_MANUAL"
|
||||
)
|
||||
if res and (res.get("deal", 0) > 0 or res.get("order", 0) > 0):
|
||||
return JSONResponse({"success": True, "ticket": res.get("order"), "deal": res.get("deal"), "price": res.get("price", price)})
|
||||
err = res.get("comment", "Order rejected by broker") if res else "Order dispatch failed"
|
||||
return JSONResponse({"success": False, "error": err}, status_code=400)
|
||||
|
||||
elif act in ["BUY_LIMIT", "SELL_LIMIT", "BUY_STOP", "SELL_STOP"]:
|
||||
target_p = req.target_price if (req.target_price and req.target_price > 0) else (tick["bid"] if "BUY" in act else tick["ask"])
|
||||
sl = (target_p - req.sl_pips * pip) if (req.sl_pips and "BUY" in act) else ((target_p + req.sl_pips * pip) if req.sl_pips else 0.0)
|
||||
tp = (target_p + req.tp_pips * pip) if (req.tp_pips and "BUY" in act) else ((target_p - req.tp_pips * pip) if req.tp_pips else 0.0)
|
||||
res = mt5.send_pending_order(
|
||||
order_type=act,
|
||||
price=target_p,
|
||||
symbol=symbol,
|
||||
volume=vol,
|
||||
sl=sl,
|
||||
tp=tp,
|
||||
comment="DASHBOARD_PENDING"
|
||||
)
|
||||
if res and res.get("order", 0) > 0:
|
||||
return JSONResponse({"success": True, "ticket": res.get("order"), "price": target_p})
|
||||
err = res.get("comment", "Pending order rejected by broker") if res else "Pending order failed"
|
||||
return JSONResponse({"success": False, "error": err}, status_code=400)
|
||||
|
||||
return JSONResponse({"success": False, "error": f"Unsupported action {act}"}, status_code=400)
|
||||
|
||||
return JSONResponse({"success": False, "error": "MT5 offline"}, status_code=500)
|
||||
except Exception as e:
|
||||
logger.error(f"Manual order placement error: {e}")
|
||||
|
||||
@@ -1315,3 +1315,148 @@ body.dark-theme {
|
||||
line-height: 1.4;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ── Modern Order Mode Toggle & Scheduling UI ───────────────────────────── */
|
||||
.order-mode-toggle {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
padding: 3px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.mode-pill {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #94a3b8;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 180ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mode-pill:hover {
|
||||
color: #f8fafc;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.mode-pill.active {
|
||||
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.35);
|
||||
}
|
||||
|
||||
.sub-table-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
background: rgba(15, 23, 42, 0.5);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 6px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.sub-table-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.amber-badge {
|
||||
background: rgba(245, 158, 11, 0.15) !important;
|
||||
color: #fbbf24 !important;
|
||||
border: 1px solid rgba(245, 158, 11, 0.3) !important;
|
||||
}
|
||||
|
||||
.btn-cancel-sm {
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
color: #fca5a5;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.btn-cancel-sm:hover {
|
||||
background: rgba(239, 68, 68, 0.25);
|
||||
color: #ffffff;
|
||||
transform: scale(1.04) translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.btn-cancel-sm:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.btn-close-sm {
|
||||
background: rgba(244, 63, 94, 0.15);
|
||||
border: 1px solid rgba(244, 63, 94, 0.35);
|
||||
color: #fda4af;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.btn-close-sm:hover {
|
||||
background: #e11d48;
|
||||
color: #ffffff;
|
||||
transform: scale(1.04) translateY(-1px);
|
||||
box-shadow: 0 2px 10px rgba(225, 29, 72, 0.4);
|
||||
}
|
||||
|
||||
.btn-close-sm:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
/* ── Emil Kowalski Spring Motion Enhancements ───────────────────────────── */
|
||||
.kpi-card {
|
||||
transition: transform 220ms cubic-bezier(0.16, 1, 0.3, 1), box-shadow 220ms cubic-bezier(0.16, 1, 0.3, 1), border-color 220ms ease !important;
|
||||
}
|
||||
|
||||
.kpi-card:hover {
|
||||
transform: translateY(-3px) scale(1.01) !important;
|
||||
border-color: rgba(255, 255, 255, 0.18) !important;
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5), 0 0 15px rgba(59, 130, 246, 0.15) !important;
|
||||
}
|
||||
|
||||
.btn-trade {
|
||||
transition: transform 150ms cubic-bezier(0.16, 1, 0.3, 1), box-shadow 150ms cubic-bezier(0.16, 1, 0.3, 1), filter 150ms ease !important;
|
||||
}
|
||||
|
||||
.btn-trade:hover {
|
||||
transform: translateY(-2px) scale(1.02) !important;
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.btn-trade:active {
|
||||
transform: scale(0.97) !important;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
transition: transform 150ms cubic-bezier(0.16, 1, 0.3, 1), box-shadow 150ms cubic-bezier(0.16, 1, 0.3, 1) !important;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
transform: scale(1.03) translateY(-1px) !important;
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
transform: scale(0.96) !important;
|
||||
}
|
||||
|
||||
+52
-14
@@ -264,22 +264,31 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Order Execution Panel -->
|
||||
<!-- Quick Order Execution & Scheduling Panel -->
|
||||
<div class="glass-card execution-card">
|
||||
<div class="card-header borderless">
|
||||
<div class="header-left">
|
||||
<span class="section-badge amber">MT5 DIRECT</span>
|
||||
<h3 class="card-title-sm">Manual Quick Execution</h3>
|
||||
<h3 class="card-title-sm">Execution & Scheduling</h3>
|
||||
</div>
|
||||
<div class="order-mode-toggle">
|
||||
<button class="mode-pill active" id="mode-market-btn" onclick="setOrderMode('market')">⚡ Market</button>
|
||||
<button class="mode-pill" id="mode-pending-btn" onclick="setOrderMode('pending')">📌 Limit Order</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="order-inputs">
|
||||
<div class="input-field" id="target-price-group" style="display: none;">
|
||||
<label for="order-target-price">Target Trigger Price ($)</label>
|
||||
<input type="number" id="order-target-price" placeholder="e.g. 4325.00" step="0.50">
|
||||
</div>
|
||||
<div class="input-field">
|
||||
<label for="order-volume">Volume (Lots)</label>
|
||||
<div class="volume-control">
|
||||
<input type="number" id="order-volume" value="0.01" step="0.01" min="0.01" max="10.0">
|
||||
<div class="lot-presets">
|
||||
<button class="lot-pill" onclick="setLot(0.01)">0.01</button>
|
||||
<button class="lot-pill" onclick="setLot(0.02)">0.02</button>
|
||||
<button class="lot-pill" onclick="setLot(0.05)">0.05</button>
|
||||
<button class="lot-pill" onclick="setLot(0.10)">0.10</button>
|
||||
</div>
|
||||
@@ -288,22 +297,22 @@
|
||||
<div class="inputs-row">
|
||||
<div class="input-field">
|
||||
<label for="order-sl">Stop Loss (Pips)</label>
|
||||
<input type="number" id="order-sl" placeholder="Optional" min="0" step="5">
|
||||
<input type="number" id="order-sl" placeholder="e.g. 100" min="0" step="5">
|
||||
</div>
|
||||
<div class="input-field">
|
||||
<label for="order-tp">Take Profit (Pips)</label>
|
||||
<input type="number" id="order-tp" placeholder="Optional" min="0" step="5">
|
||||
<input type="number" id="order-tp" placeholder="e.g. 150" min="0" step="5">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="order-actions-row">
|
||||
<button class="btn-trade btn-buy" id="btn-exec-buy" onclick="executeTrade('BUY')">
|
||||
<span class="trade-dir">BUY</span>
|
||||
<button class="btn-trade btn-buy" id="btn-exec-buy" onclick="handleOrderSubmit('BUY')">
|
||||
<span class="trade-dir" id="buy-label">BUY</span>
|
||||
<span class="trade-hint" id="buy-ask-hint">ASK: —</span>
|
||||
</button>
|
||||
<button class="btn-trade btn-sell" id="btn-exec-sell" onclick="executeTrade('SELL')">
|
||||
<span class="trade-dir">SELL</span>
|
||||
<button class="btn-trade btn-sell" id="btn-exec-sell" onclick="handleOrderSubmit('SELL')">
|
||||
<span class="trade-dir" id="sell-label">SELL</span>
|
||||
<span class="trade-hint" id="sell-bid-hint">BID: —</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -316,25 +325,32 @@
|
||||
<!-- ── Lower Stage: Positions, Trades & News Feed ───────── -->
|
||||
<section class="bottom-stage-grid">
|
||||
|
||||
<!-- Open Positions Panel -->
|
||||
<!-- Open Positions & Scheduled Orders Panel -->
|
||||
<div class="glass-card positions-station">
|
||||
<div class="card-header">
|
||||
<div class="header-left">
|
||||
<span class="section-badge green">LIVE EXPOSURE</span>
|
||||
<h2 class="card-title-sm">Active Positions</h2>
|
||||
<h2 class="card-title-sm">Active Positions & Orders</h2>
|
||||
<span class="counter-badge" id="open-positions-count">0</span>
|
||||
<span class="counter-badge amber-badge" id="pending-orders-count">0 Scheduled</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button class="btn-outline-sm btn-danger" onclick="openCloseAllModal()">Emergency Close All</button>
|
||||
</div>
|
||||
<button class="btn-outline-sm btn-danger" onclick="openCloseAllModal()">Close All</button>
|
||||
</div>
|
||||
<div class="card-body scrollable-table">
|
||||
<!-- Live Market Positions Table -->
|
||||
<div class="sub-table-header">
|
||||
<span class="sub-table-title">🟢 Open Market Positions</span>
|
||||
</div>
|
||||
<table class="station-table" id="positions-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Ticket</th>
|
||||
<th>Type</th>
|
||||
<th>Lots</th>
|
||||
<th>Open Price</th>
|
||||
<th>Current</th>
|
||||
<th>Entry Price</th>
|
||||
<th>Current Price</th>
|
||||
<th>SL</th>
|
||||
<th>TP</th>
|
||||
<th>P&L ($)</th>
|
||||
@@ -342,7 +358,29 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="positions-body">
|
||||
<tr><td colspan="9" class="empty-state">No open positions. Ready for signals.</td></tr>
|
||||
<tr><td colspan="9" class="empty-state">No open positions. Scanning market regimes...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Scheduled Pending Orders Table -->
|
||||
<div class="sub-table-header mt-4">
|
||||
<span class="sub-table-title">⏳ Scheduled Pending Orders</span>
|
||||
</div>
|
||||
<table class="station-table" id="pending-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Order #</th>
|
||||
<th>Type</th>
|
||||
<th>Lots</th>
|
||||
<th>Trigger Price</th>
|
||||
<th>SL</th>
|
||||
<th>TP</th>
|
||||
<th>Comment</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="pending-body">
|
||||
<tr><td colspan="8" class="empty-state">No scheduled pending orders in book.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
+175
-38
@@ -252,53 +252,190 @@ async function refreshAccount() {
|
||||
}
|
||||
|
||||
// ── Positions Station ───────────────────────────────────────────────────
|
||||
async function refreshPositions() {
|
||||
const data = await fetchJSON('/api/positions');
|
||||
if (!data) return;
|
||||
let currentOrderMode = 'market';
|
||||
|
||||
const countBadge = document.getElementById('open-positions-count');
|
||||
const statusSub = document.getElementById('floating-status');
|
||||
const tbody = document.getElementById('positions-body');
|
||||
function setOrderMode(mode) {
|
||||
currentOrderMode = mode;
|
||||
const marketBtn = document.getElementById('mode-market-btn');
|
||||
const pendingBtn = document.getElementById('mode-pending-btn');
|
||||
const targetGroup = document.getElementById('target-price-group');
|
||||
const buyLabel = document.getElementById('buy-label');
|
||||
const sellLabel = document.getElementById('sell-label');
|
||||
|
||||
const count = data.length || 0;
|
||||
if (countBadge) countBadge.textContent = count;
|
||||
if (statusSub) statusSub.textContent = `${count} Open Position${count === 1 ? '' : 's'}`;
|
||||
if (mode === 'market') {
|
||||
if (marketBtn) marketBtn.classList.add('active');
|
||||
if (pendingBtn) pendingBtn.classList.remove('active');
|
||||
if (targetGroup) targetGroup.style.display = 'none';
|
||||
if (buyLabel) buyLabel.textContent = 'BUY';
|
||||
if (sellLabel) sellLabel.textContent = 'SELL';
|
||||
} else {
|
||||
if (marketBtn) marketBtn.classList.remove('active');
|
||||
if (pendingBtn) pendingBtn.classList.add('active');
|
||||
if (targetGroup) targetGroup.style.display = 'block';
|
||||
if (buyLabel) buyLabel.textContent = 'BUY LIMIT';
|
||||
if (sellLabel) sellLabel.textContent = 'SELL LIMIT';
|
||||
|
||||
if (!tbody) return;
|
||||
// Suggest target price if empty
|
||||
const targetInput = document.getElementById('order-target-price');
|
||||
if (targetInput && !targetInput.value) {
|
||||
const curP = parseFloat(document.getElementById('current-price')?.textContent?.replace(/[^0-9.]/g, '') || '0');
|
||||
if (curP > 0) targetInput.value = (curP - 15.0).toFixed(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="9" class="empty-state">No open positions. Ready for signals.</td></tr>';
|
||||
async function handleOrderSubmit(direction) {
|
||||
if (currentOrderMode === 'pending') {
|
||||
const action = direction === 'BUY' ? 'BUY_LIMIT' : 'SELL_LIMIT';
|
||||
await executePendingOrder(action);
|
||||
} else {
|
||||
await executeTrade(direction);
|
||||
}
|
||||
}
|
||||
|
||||
async function executePendingOrder(action) {
|
||||
const volumeInput = document.getElementById('order-volume');
|
||||
const targetInput = document.getElementById('order-target-price');
|
||||
const slInput = document.getElementById('order-sl');
|
||||
const tpInput = document.getElementById('order-tp');
|
||||
|
||||
const volume = parseFloat(volumeInput?.value || '0.01');
|
||||
const target_price = parseFloat(targetInput?.value || '0');
|
||||
const sl_pips = slInput?.value ? parseFloat(slInput.value) : null;
|
||||
const tp_pips = tpInput?.value ? parseFloat(tpInput.value) : null;
|
||||
|
||||
if (isNaN(volume) || volume <= 0) {
|
||||
showToast('Invalid volume lot size', 'error');
|
||||
return;
|
||||
}
|
||||
if (isNaN(target_price) || target_price <= 0) {
|
||||
showToast('Please specify a valid Target Trigger Price ($)', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = data.map(pos => {
|
||||
const typeClass = (pos.type || '').toLowerCase();
|
||||
const profit = pos.profit || 0;
|
||||
const profitClass = profit >= 0 ? 'profit' : 'loss';
|
||||
const priceOpen = pos.price_open ? pos.price_open.toFixed(2) : '—';
|
||||
const priceCurrent = pos.price_current ? pos.price_current.toFixed(2) : '—';
|
||||
const sl = pos.sl ? pos.sl.toFixed(2) : '—';
|
||||
const tp = pos.tp ? pos.tp.toFixed(2) : '—';
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>#${pos.ticket}</td>
|
||||
<td><span class="type-pill ${typeClass}">${pos.type}</span></td>
|
||||
<td>${pos.volume?.toFixed(2)}</td>
|
||||
<td>$${priceOpen}</td>
|
||||
<td>$${priceCurrent}</td>
|
||||
<td>${sl}</td>
|
||||
<td>${tp}</td>
|
||||
<td class="pnl-cell ${profitClass}">${formatCurrency(profit, true)}</td>
|
||||
<td>
|
||||
<button class="btn-close-single" onclick="closePosition(${pos.ticket})">✕ Close</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
showToast(`Scheduling ${action} order (${volume} lots @ $${target_price.toFixed(2)})...`);
|
||||
try {
|
||||
const res = await fetch('/api/order', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action, volume, target_price, sl_pips, tp_pips })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
showToast(`Scheduled Order #${data.ticket} successfully placed at $${data.price.toFixed(2)}!`, 'success');
|
||||
refreshPositions();
|
||||
} else {
|
||||
showToast(`Order failed: ${data.error}`, 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(`Scheduling error: ${e}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelOrder(ticket) {
|
||||
showToast(`Canceling pending order #${ticket}...`);
|
||||
try {
|
||||
const res = await fetch(`/api/cancel-order/${ticket}`, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
showToast(`Order #${ticket} canceled successfully!`, 'success');
|
||||
refreshPositions();
|
||||
} else {
|
||||
showToast(`Failed to cancel: ${data.error}`, 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(`Cancel error: ${e}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshPositions() {
|
||||
const [positionsData, pendingData] = await Promise.all([
|
||||
fetchJSON('/api/positions'),
|
||||
fetchJSON('/api/pending')
|
||||
]);
|
||||
|
||||
const countBadge = document.getElementById('open-positions-count');
|
||||
const pendingBadge = document.getElementById('pending-orders-count');
|
||||
const statusSub = document.getElementById('floating-status');
|
||||
const posTbody = document.getElementById('positions-body');
|
||||
const pendingTbody = document.getElementById('pending-body');
|
||||
|
||||
// 1. Render Active Positions
|
||||
const posList = Array.isArray(positionsData) ? positionsData : (positionsData?.positions || []);
|
||||
const posCount = posList.length;
|
||||
if (countBadge) countBadge.textContent = posCount;
|
||||
if (statusSub) statusSub.textContent = `${posCount} Open Position${posCount === 1 ? '' : 's'}`;
|
||||
|
||||
if (posTbody) {
|
||||
if (posCount === 0) {
|
||||
posTbody.innerHTML = '<tr><td colspan="9" class="empty-state">No open positions. Scanning market regimes...</td></tr>';
|
||||
} else {
|
||||
posTbody.innerHTML = posList.map(pos => {
|
||||
const typeClass = (pos.type || '').toLowerCase();
|
||||
const profit = pos.profit || 0;
|
||||
const profitClass = profit >= 0 ? 'profit' : 'loss';
|
||||
const priceOpen = pos.price_open ? pos.price_open.toFixed(2) : '—';
|
||||
const priceCurrent = pos.price_current ? pos.price_current.toFixed(2) : '—';
|
||||
const sl = pos.sl ? pos.sl.toFixed(2) : '—';
|
||||
const tp = pos.tp ? pos.tp.toFixed(2) : '—';
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td><strong>#${pos.ticket}</strong></td>
|
||||
<td><span class="type-pill ${typeClass}">${pos.type}</span></td>
|
||||
<td>${pos.volume?.toFixed(2)}</td>
|
||||
<td>$${priceOpen}</td>
|
||||
<td>$${priceCurrent}</td>
|
||||
<td>${sl}</td>
|
||||
<td>${tp}</td>
|
||||
<td class="pnl-cell ${profitClass}"><strong>${formatCurrency(profit, true)}</strong></td>
|
||||
<td>
|
||||
<button class="btn-close-sm" onclick="closePosition(${pos.ticket})" title="Market Close Position">Close</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Render Scheduled Pending Orders
|
||||
const pendList = Array.isArray(pendingData) ? pendingData : [];
|
||||
const pendCount = pendList.length;
|
||||
if (pendingBadge) {
|
||||
pendingBadge.textContent = `${pendCount} Scheduled`;
|
||||
pendingBadge.style.display = pendCount > 0 ? 'inline-block' : 'none';
|
||||
}
|
||||
|
||||
if (pendingTbody) {
|
||||
if (pendCount === 0) {
|
||||
pendingTbody.innerHTML = '<tr><td colspan="8" class="empty-state">No scheduled pending orders in book.</td></tr>';
|
||||
} else {
|
||||
pendingTbody.innerHTML = pendList.map(o => {
|
||||
const typeClass = (o.type || '').toLowerCase();
|
||||
const targetP = o.price ? o.price.toFixed(2) : '—';
|
||||
const sl = o.sl ? o.sl.toFixed(2) : '—';
|
||||
const tp = o.tp ? o.tp.toFixed(2) : '—';
|
||||
const cmt = o.comment || 'Scheduled';
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td><strong>#${o.ticket}</strong></td>
|
||||
<td><span class="type-pill ${typeClass}">${o.type}</span></td>
|
||||
<td>${o.volume?.toFixed(2)}</td>
|
||||
<td><strong>$${targetP}</strong></td>
|
||||
<td>${sl}</td>
|
||||
<td>${tp}</td>
|
||||
<td><span class="comment-text">${cmt}</span></td>
|
||||
<td>
|
||||
<button class="btn-cancel-sm" onclick="cancelOrder(${o.ticket})" title="Cancel Scheduled Order">Cancel</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Trade History ───────────────────────────────────────────────────────
|
||||
async function refreshTrades() {
|
||||
const data = await fetchJSON('/api/trades');
|
||||
if (!data) return;
|
||||
|
||||
Reference in New Issue
Block a user