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:
Mazonia
2026-09-21 10:48:28 +00:00
parent 312b78201b
commit 1725f4c829
4 changed files with 448 additions and 74 deletions
+76 -22
View File
@@ -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}")
+145
View File
@@ -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
View File
@@ -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
View File
@@ -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;