Files
ai-exchange/src/web/app.js
T
2026-05-08 14:39:44 +08:00

810 lines
30 KiB
JavaScript

const healthStatus = document.getElementById('healthStatus');
const refreshBtn = document.getElementById('refreshBtn');
const instrumentIdInput = document.getElementById('instrumentId');
const timeframeInput = document.getElementById('timeframe');
const limitInput = document.getElementById('limit');
const autoRefreshSecondsInput = document.getElementById('autoRefreshSeconds');
const executionStatusFilterInput = document.getElementById('executionStatusFilter');
const searchTextInput = document.getElementById('searchText');
const lastUpdated = document.getElementById('lastUpdated');
const activeFilters = document.getElementById('activeFilters');
const overviewCards = document.getElementById('overviewCards');
const marketStatePanel = document.getElementById('marketStatePanel');
const runtimeStatusPanel = document.getElementById('runtimeStatusPanel');
const decisionPacketPanel = document.getElementById('decisionPacketPanel');
const preMarketPlanPanel = document.getElementById('preMarketPlanPanel');
const tradeCandidatesPanel = document.getElementById('tradeCandidatesPanel');
const executionTicketsPanel = document.getElementById('executionTicketsPanel');
const positionManagementPanel = document.getElementById('positionManagementPanel');
const postTradeReviewPanel = document.getElementById('postTradeReviewPanel');
const canvas = document.getElementById('candlesChart');
const ctx = canvas.getContext('2d');
let autoRefreshTimer = null;
async function fetchJson(url, options = {}) {
const fetchOptions = { ...options };
if (fetchOptions.body) {
fetchOptions.headers = {
'Content-Type': 'application/json',
...(fetchOptions.headers || {}),
};
}
const response = await fetch(url, fetchOptions);
if (!response.ok) {
throw new Error(`${response.status} ${url}`);
}
if (response.status === 204) {
return null;
}
return response.json();
}
async function postJson(url, body = {}) {
return fetchJson(url, {
method: 'POST',
body: JSON.stringify(body),
});
}
function setText(element, value) {
if (!element) return;
element.textContent = value == null ? '' : String(value);
}
function clearNode(node) {
if (!node) return;
while (node.firstChild) {
node.removeChild(node.firstChild);
}
}
function createElement(tag, className, text) {
const element = document.createElement(tag);
if (className) element.className = className;
if (text != null) setText(element, text);
return element;
}
function formatNumber(value, digits = 2) {
if (value == null || value === '') return 'n/a';
const number = Number(value);
if (!Number.isFinite(number)) return 'n/a';
return number.toFixed(digits);
}
function formatPercent(value) {
if (value == null) return 'n/a';
const number = Number(value);
if (!Number.isFinite(number)) return 'n/a';
return `${(number * 100).toFixed(1)}%`;
}
function formatTimestamp(value) {
if (!value) return 'n/a';
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return String(value);
}
return date.toLocaleString();
}
function formatList(values, emptyText = 'n/a') {
if (!Array.isArray(values) || values.length === 0) return emptyText;
return values.map((value) => String(value)).join(', ');
}
function formatJsonInline(value, emptyText = 'n/a') {
if (value == null) return emptyText;
if (Array.isArray(value) && value.length === 0) return emptyText;
if (!Array.isArray(value) && typeof value === 'object' && Object.keys(value).length === 0) return emptyText;
return JSON.stringify(value);
}
function statusClass(status) {
const raw = String(status || 'unknown').toLowerCase().replace(/[^a-z0-9]+/g, '_');
return `status-pill status-${raw || 'unknown'}`;
}
function buildStatusPill(status) {
return createElement('span', statusClass(status), status || 'unknown');
}
function buildMetricCard(label, value, detail = '') {
const card = createElement('div', 'card');
card.appendChild(createElement('div', 'metric', label));
card.appendChild(createElement('div', 'value-small', value));
if (detail) card.appendChild(createElement('div', 'metric', detail));
return card;
}
function buildInfoGrid(entries) {
const grid = createElement('div', 'info-grid');
entries.forEach(([label, value]) => {
const row = createElement('div', 'info-row');
row.appendChild(createElement('div', 'metric', label));
row.appendChild(createElement('div', 'info-value', value == null || value === '' ? 'n/a' : String(value)));
grid.appendChild(row);
});
return grid;
}
function buildActionBar(actions) {
const bar = createElement('div', 'actions');
actions
.filter((action) => action && typeof action.onClick === 'function')
.forEach((action) => {
const button = createElement('button', '', action.label);
button.type = 'button';
button.addEventListener('click', action.onClick);
bar.appendChild(button);
});
return bar;
}
function buildItemCard(title, status, entries, actions = [], detail = '') {
const card = createElement('div', 'card');
const header = createElement('div', 'card-header');
header.appendChild(createElement('strong', '', title));
header.appendChild(buildStatusPill(status));
card.appendChild(header);
card.appendChild(buildInfoGrid(entries));
if (detail) card.appendChild(createElement('div', 'metric card-detail', detail));
if (actions.length) card.appendChild(buildActionBar(actions));
return card;
}
function getSearchText() {
return String(searchTextInput.value || '').trim().toLowerCase();
}
function updateActiveFilters() {
const active = [];
const executionStatus = executionStatusFilterInput.value || 'all';
const search = getSearchText();
if (executionStatus !== 'all') active.push(`execution=${executionStatus}`);
if (search) active.push(`search=${search}`);
setText(activeFilters, `Filters: ${active.length ? active.join(', ') : 'none'}`);
}
function filterBySearch(items, projector) {
const search = getSearchText();
if (!search) return items;
return items.filter((item) => String(projector(item)).toLowerCase().includes(search));
}
function filterExecutions(items) {
const statusFilter = executionStatusFilterInput.value || 'all';
return filterBySearch(
items.filter((item) => statusFilter === 'all' || item.execution_status === statusFilter),
(item) => [
item.execution_id,
item.execution_status,
item.side,
item.session_code,
item.close_reason,
item.ai_explanation,
...(item.error_tags || []),
].join(' ')
);
}
function queryString(params) {
const search = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value == null || value === '') return;
search.set(key, String(value));
});
return search.toString();
}
async function runAction(url, body = {}) {
try {
await postJson(url, body);
await refreshDashboard();
} catch (error) {
showErrorPanel(String(error));
}
}
function renderHealth(health) {
setText(
healthStatus,
[
health?.app || 'ai-ict',
health?.database_configured ? 'db ready' : 'db missing',
health?.session_timezone || 'timezone unknown',
].join(' | ')
);
}
function renderOverview(weeklyReview, candidates, tickets, executions) {
clearNode(overviewCards);
const openExecutions = executions.filter((item) => item.execution_status === 'open');
const confirmedTickets = tickets.filter((item) => item.status === 'confirmed');
const executableCandidates = candidates.filter((item) => item.executable);
const cards = [
buildMetricCard('Executable Candidates', String(executableCandidates.length), 'ready to ticket'),
buildMetricCard('Confirmed Tickets', String(confirmedTickets.length), 'human confirmed'),
buildMetricCard('Open Positions', String(openExecutions.length), 'live management load'),
buildMetricCard('Framework Rate', formatPercent(weeklyReview?.framework_execution_rate), 'weekly execution discipline'),
buildMetricCard('Out-of-Model Rate', formatPercent(weeklyReview?.out_of_model_trade_rate), 'model drift'),
buildMetricCard('Risk Violations', String(weeklyReview?.risk_violation_count ?? 0), 'weekly count'),
];
cards.forEach((card) => overviewCards.appendChild(card));
}
function renderMarketState(payload, executions) {
clearNode(marketStatePanel);
const marketState = payload?.market_state || {};
const freshness = marketState.freshness || {};
const session = marketState.session || {};
const newsWindow = marketState.news_window || {};
const riskDecision = payload?.risk_decision || {};
const brokerState = payload?.broker_state || {};
const brokerAccount = brokerState.account || {};
const brokerAlerts = brokerState.alerts || [];
const panel = buildInfoGrid([
['symbol', String(instrumentIdInput.value || '1')],
['session', session.session_code || 'OFF_HOURS'],
['data freshness', freshness.code || 'unknown'],
['news state', newsWindow.code || 'unknown'],
['broker state', brokerState.status || 'unknown'],
['broker equity', formatNumber(brokerAccount.equity, 2)],
['broker available balance', formatNumber(brokerAccount.available_balance, 2)],
['broker open positions', String((brokerState.positions || []).length)],
['broker pending orders', String((brokerState.orders || []).length)],
['broker alerts', formatList(brokerAlerts)],
['account risk state', formatJsonInline(riskDecision.reject_codes || [])],
['open position', String(executions.filter((item) => item.execution_status === 'open').length)],
['kill switch', (riskDecision.reject_codes || []).includes('execution_kill_switch_enabled') ? 'active' : 'off'],
]);
marketStatePanel.appendChild(panel);
}
function renderRuntimeStatus(runtimeStatus) {
clearNode(runtimeStatusPanel);
const tasks = runtimeStatus?.tasks || {};
const blockers = runtimeStatus?.current_blockers || [];
const incidents = runtimeStatus?.incidents || [];
runtimeStatusPanel.appendChild(
buildInfoGrid([
['scheduler state', runtimeStatus?.scheduler_state || 'unknown'],
['supervisor state', runtimeStatus?.supervisor_state || 'unknown'],
['enabled', String(Boolean(runtimeStatus?.enabled))],
['degraded', String(Boolean(runtimeStatus?.degraded_mode_state?.is_degraded))],
['symbols', formatList(runtimeStatus?.symbols || [])],
['blockers', formatList(blockers)],
['last incident', incidents.length ? `${incidents[incidents.length - 1].code} @ ${formatTimestamp(incidents[incidents.length - 1].observed_ts)}` : 'none'],
])
);
const grid = createElement('div', 'cards');
Object.entries(tasks).forEach(([name, task]) => {
grid.appendChild(
buildItemCard(
name,
task.last_status || 'idle',
[
['last success', formatTimestamp(task.last_success_ts)],
['next run', formatTimestamp(task.next_run_ts)],
['run count', String(task.run_count ?? 0)],
['success / failure', `${task.success_count ?? 0} / ${task.failure_count ?? 0}`],
['last error', task.last_error || 'none'],
['details', formatJsonInline(task.details || {})],
]
)
);
});
runtimeStatusPanel.appendChild(grid);
}
function renderDecisionPacket(packet, alertRows) {
clearNode(decisionPacketPanel);
if (!packet || packet.status === 'unavailable') {
decisionPacketPanel.appendChild(createElement('div', 'empty-state', 'Decision packet unavailable.'));
return;
}
decisionPacketPanel.appendChild(
buildInfoGrid([
['status', packet.status || 'unknown'],
['recommendation', packet.action_recommendation || 'monitor'],
['bias', packet.bias || 'unknown'],
['setup', packet.setup_type || 'n/a'],
['candidate', packet.candidate_id || 'n/a'],
['ticket', packet.execution_ticket_id || 'n/a'],
['risk result', formatJsonInline(packet.risk_result?.reasons || [])],
['why allowed', formatList(packet.why_allowed || [])],
['why blocked', formatList(packet.why_blocked || [])],
['ai explanation', packet.ai_explanation || 'n/a'],
])
);
const grid = createElement('div', 'cards');
grid.appendChild(
buildItemCard(
'Plan Context',
packet.action_recommendation || 'monitor',
[
['narrative', packet.narrative || 'n/a'],
['primary draw', formatJsonInline(packet.primary_draw || {})],
['confirmation', formatJsonInline(packet.confirmation_chain || {})],
['entry zone', `${formatNumber(packet.entry_zone?.low, 3)} - ${formatNumber(packet.entry_zone?.high, 3)}`],
['stop', formatNumber(packet.stop_loss, 3)],
['targets', formatJsonInline(packet.targets || {})],
['invalidation', formatJsonInline(packet.invalidation || {})],
['expiry', formatTimestamp(packet.expiry_ts)],
]
)
);
const recentAlerts = alertRows?.items || packet.alerts || [];
recentAlerts.slice(0, 3).forEach((alert) => {
grid.appendChild(
buildItemCard(
alert.alert_type || 'alert',
alert.status || 'queued',
[
['channel', alert.channel || 'workbench'],
['message', alert.message || 'n/a'],
['payload', formatJsonInline(alert.payload || {})],
]
)
);
});
decisionPacketPanel.appendChild(grid);
}
function renderPreMarketPlan(plan) {
clearNode(preMarketPlanPanel);
preMarketPlanPanel.appendChild(
buildActionBar([
{
label: 'Generate Active Plan',
onClick: () => runAction('/trading/plans/generate', {
instrument_id: Number(instrumentIdInput.value || 1),
bias_tf: timeframeInput.value || '1m',
setup_tf: timeframeInput.value || '1m',
trigger_tf: timeframeInput.value || '1m',
activate: true,
}),
},
])
);
if (!plan) {
preMarketPlanPanel.appendChild(createElement('div', 'empty-state', 'No active plan for this instrument.'));
return;
}
preMarketPlanPanel.appendChild(
buildInfoGrid([
['narrative', plan.narrative || 'n/a'],
['primary draw', formatJsonInline(plan.primary_draw)],
['secondary draw', formatJsonInline(plan.secondary_draw)],
['bias', plan.bias || 'n/a'],
['allowed sessions', formatList(plan.allowed_sessions || [])],
['invalidations', formatJsonInline(plan.invalidations || [])],
['no-trade conditions', formatList(plan.no_trade_conditions || [])],
['status', plan.status || 'unknown'],
['activated', formatTimestamp(plan.activated_ts)],
])
);
}
function renderCandidates(items) {
clearNode(tradeCandidatesPanel);
tradeCandidatesPanel.appendChild(
buildActionBar([
{
label: 'Run Watch',
onClick: () => runAction('/trading/watch/run', {
instrument_id: Number(instrumentIdInput.value || 1),
setup_tf: timeframeInput.value || '1m',
trigger_tf: timeframeInput.value || '1m',
}),
},
])
);
const filtered = filterBySearch(items, (item) => [
item.id,
item.status,
item.side,
item.session_code,
item.ai_explanation,
...(item.reason_codes || []),
...(item.missing_conditions || []),
].join(' '));
if (!filtered.length) {
tradeCandidatesPanel.appendChild(createElement('div', 'empty-state', 'No candidate rows match the current filters.'));
return;
}
const grid = createElement('div', 'cards');
filtered.forEach((candidate) => {
const detail = `quality ${formatNumber(candidate.quality_score)} | ${candidate.ai_explanation || 'no explanation'}`;
grid.appendChild(
buildItemCard(
`Candidate #${candidate.id}`,
candidate.status,
[
['model', candidate.model_code],
['side', candidate.side || 'n/a'],
['session', candidate.session_code || 'n/a'],
['confirmation chain', formatList(candidate.present_conditions || [])],
['missing requirements', formatList(candidate.missing_conditions || [])],
['entry zone', `${formatNumber(candidate.entry_low, 3)} - ${formatNumber(candidate.entry_high, 3)}`],
['stop', formatNumber(candidate.stop_loss, 3)],
['TP1 / TP2', `${formatNumber(candidate.tp1, 3)} / ${formatNumber(candidate.tp2, 3)}`],
['RR TP1 / TP2', `${formatNumber(candidate.rr_tp1)} / ${formatNumber(candidate.rr_tp2)}`],
['expiry', formatTimestamp(candidate.expiry_ts)],
['management rules', formatJsonInline(candidate.management_rules)],
['evidence links', formatJsonInline(candidate.evidence)],
],
[
{
label: 'Build Paper Ticket',
onClick: () => runAction(`/trading/candidates/${candidate.id}/build-ticket`, {
execution_mode: 'paper',
account_equity: 10000,
risk_pct: 0.01,
}),
},
{
label: 'Build Semi-Auto Ticket',
onClick: () => runAction(`/trading/candidates/${candidate.id}/build-ticket`, {
execution_mode: 'semi_auto',
account_equity: 10000,
risk_pct: 0.01,
}),
},
],
detail
)
);
});
tradeCandidatesPanel.appendChild(grid);
}
function renderTickets(items) {
clearNode(executionTicketsPanel);
const filtered = filterBySearch(items, (item) => {
const snapshot = item.ticket_snapshot || {};
return [
item.id,
item.status,
item.execution_mode,
snapshot.signal?.side,
snapshot.signal?.session_code,
snapshot.decision?.meta?.ai_explanation,
...(snapshot.decision?.reasons || []),
].join(' ');
});
if (!filtered.length) {
executionTicketsPanel.appendChild(createElement('div', 'empty-state', 'No execution tickets match the current filters.'));
return;
}
const grid = createElement('div', 'cards');
filtered.forEach((ticket) => {
const snapshot = ticket.ticket_snapshot || {};
const signal = snapshot.signal || {};
const decision = snapshot.decision || {};
const riskGovernor = decision.meta?.risk_governor || {};
const riskAmount = Number(snapshot.account_equity || 0) * Number(decision.risk_pct || 0);
const actions = [];
actions.push({
label: 'Risk Check',
onClick: () => runAction(`/trading/tickets/${ticket.id}/risk-check`, {
account_equity: Number(snapshot.account_equity || 10000),
risk_pct: Number(decision.risk_pct || 0.01),
}),
});
if (ticket.status === 'awaiting_confirmation') {
actions.push({
label: 'Confirm',
onClick: () => runAction(`/trading/tickets/${ticket.id}/confirm`, { confirmed_by: 'local_operator' }),
});
}
if (ticket.status === 'confirmed' && ticket.execution_mode === 'paper') {
actions.push({
label: 'Paper Execute',
onClick: () => runAction(`/trading/tickets/${ticket.id}/paper-execute`, { confirmed_by: 'local_operator' }),
});
}
if (ticket.status === 'confirmed' && ticket.execution_mode === 'semi_auto') {
actions.push({
label: 'Semi-Auto Submit',
onClick: () => runAction(`/trading/tickets/${ticket.id}/semi-auto-submit`, { confirmed_by: 'local_operator' }),
});
}
grid.appendChild(
buildItemCard(
`Ticket #${ticket.id}`,
ticket.status,
[
['mode', ticket.execution_mode],
['side', signal.side || 'n/a'],
['entry zone', `${formatNumber(signal.entry_low, 3)} - ${formatNumber(signal.entry_high, 3)}`],
['stop', formatNumber(signal.stop_loss, 3)],
['TP1 / TP2', `${formatNumber(signal.tp1, 3)} / ${formatNumber(signal.tp2, 3)}`],
['RR', `${formatNumber(decision.meta?.tp1 ? Math.abs((Number(decision.meta.tp1) - Number(decision.entry_price || 0)) / Math.max(Math.abs(Number(decision.entry_price || 0) - Number(decision.meta.stop_loss || 0)), 1e-9)) : 0)} / ${formatNumber(decision.meta?.tp2 ? Math.abs((Number(decision.meta.tp2) - Number(decision.entry_price || 0)) / Math.max(Math.abs(Number(decision.entry_price || 0) - Number(decision.meta.stop_loss || 0)), 1e-9)) : 0)}`],
['risk amount', formatNumber(riskAmount, 2)],
['expiry', formatTimestamp(ticket.expires_ts)],
['management rules', formatJsonInline(signal.target_plan?.management_rules || [])],
['risk governor result', formatJsonInline(riskGovernor.reject_codes || [])],
['confirmed by', ticket.confirmed_by || 'n/a'],
['confirmed at', formatTimestamp(ticket.confirmed_at)],
],
actions,
decision.meta?.ai_explanation || ''
)
);
});
executionTicketsPanel.appendChild(grid);
}
function renderExecutions(items) {
clearNode(positionManagementPanel);
const filtered = filterExecutions(items);
if (!filtered.length) {
positionManagementPanel.appendChild(createElement('div', 'empty-state', 'No execution rows match the current filters.'));
return;
}
const grid = createElement('div', 'cards');
filtered.forEach((execution) => {
const tpProgress = [
execution.tp1_hit ? 'TP1 hit' : 'TP1 open',
execution.tp2_hit ? 'TP2 hit' : 'TP2 open',
].join(' | ');
grid.appendChild(
buildItemCard(
`Execution #${execution.execution_id}`,
execution.execution_status,
[
['mode', execution.execution_mode],
['side', execution.side || 'n/a'],
['fill state', execution.fill_state || 'n/a'],
['position state', execution.position_state || 'n/a'],
['TP progress', tpProgress],
['stop state', execution.close_reason || 'active'],
['invalidation alerts', formatList(execution.error_tags || [])],
['ticket', execution.execution_ticket_id || 'n/a'],
['opened', formatTimestamp(execution.opened_ts)],
['closed', formatTimestamp(execution.closed_ts)],
['quality', formatNumber(execution.quality_score)],
['bracket state', formatJsonInline(execution.bracket_state || {})],
['reconciliation', formatJsonInline(execution.reconciliation_state || {})],
],
[
{
label: 'Cancel',
onClick: () => runAction(`/trading/executions/${execution.execution_id}/cancel`, {}),
},
{
label: 'Close',
onClick: () => runAction(`/trading/executions/${execution.execution_id}/close`, {}),
},
{
label: 'Sync',
onClick: () => runAction(`/trading/executions/${execution.execution_id}/sync`, {}),
},
],
execution.ai_explanation || ''
)
);
});
positionManagementPanel.appendChild(grid);
}
function renderPostTradeReview(summary, journalItems, weeklyReview) {
clearNode(postTradeReviewPanel);
postTradeReviewPanel.appendChild(
buildActionBar([
{
label: 'Generate Journal Backfill',
onClick: () => runAction('/trading/journal/generate', {}),
},
])
);
const top = buildInfoGrid([
['entry count', String(summary?.entry_count ?? weeklyReview?.entry_count ?? 0)],
['framework execution rate', formatPercent(weeklyReview?.framework_execution_rate)],
['out-of-model rate', formatPercent(weeklyReview?.out_of_model_trade_rate)],
['risk violation count', String(weeklyReview?.risk_violation_count ?? 0)],
['high quality session', formatJsonInline(weeklyReview?.high_quality_session)],
['low quality session', formatJsonInline(weeklyReview?.low_quality_session)],
['next actions', formatList(weeklyReview?.next_week_actions || [])],
]);
postTradeReviewPanel.appendChild(top);
if (!journalItems.length) {
postTradeReviewPanel.appendChild(createElement('div', 'empty-state', 'No journal entries available for this instrument.'));
return;
}
const grid = createElement('div', 'cards');
journalItems.slice(0, 6).forEach((entry) => {
grid.appendChild(
buildItemCard(
`Journal #${entry.id}`,
entry.compliance_state || 'review',
[
['model compliance', String(Boolean(entry.model_compliance))],
['risk compliance', String(!entry.risk_violation)],
['execution violations', formatList(entry.execution_violations || [])],
['model failure', String(Boolean(entry.model_failure))],
['execution failure', String(Boolean(entry.execution_violation))],
['next actions', formatList(entry.next_actions || [])],
],
[],
formatJsonInline(entry.summary || {})
)
);
});
postTradeReviewPanel.appendChild(grid);
}
function drawCandles(items) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (!Array.isArray(items) || items.length === 0) {
ctx.fillStyle = '#9db0d3';
ctx.font = '14px sans-serif';
ctx.fillText('No candle data available.', 24, 36);
return;
}
const candles = [...items].reverse();
const highs = candles.map((item) => Number(item.high || 0));
const lows = candles.map((item) => Number(item.low || 0));
const closes = candles.map((item) => Number(item.close || 0));
const minPrice = Math.min(...lows);
const maxPrice = Math.max(...highs);
const range = Math.max(maxPrice - minPrice, 1e-9);
const chartLeft = 40;
const chartRight = canvas.width - 16;
const chartTop = 16;
const chartBottom = canvas.height - 24;
const chartHeight = chartBottom - chartTop;
const chartWidth = chartRight - chartLeft;
ctx.strokeStyle = '#24304f';
ctx.lineWidth = 1;
for (let i = 0; i <= 4; i += 1) {
const y = chartTop + (chartHeight / 4) * i;
ctx.beginPath();
ctx.moveTo(chartLeft, y);
ctx.lineTo(chartRight, y);
ctx.stroke();
}
ctx.strokeStyle = '#4f7cff';
ctx.lineWidth = 2;
ctx.beginPath();
closes.forEach((close, index) => {
const x = chartLeft + (chartWidth * index) / Math.max(closes.length - 1, 1);
const y = chartBottom - ((close - minPrice) / range) * chartHeight;
if (index === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
ctx.stroke();
ctx.fillStyle = '#9db0d3';
ctx.font = '12px sans-serif';
ctx.fillText(maxPrice.toFixed(2), 4, chartTop + 10);
ctx.fillText(minPrice.toFixed(2), 4, chartBottom);
}
function showErrorPanel(message) {
[
marketStatePanel,
runtimeStatusPanel,
decisionPacketPanel,
preMarketPlanPanel,
tradeCandidatesPanel,
executionTicketsPanel,
positionManagementPanel,
postTradeReviewPanel,
].forEach((panel) => {
clearNode(panel);
panel.appendChild(createElement('div', 'empty-state error-state', message));
});
}
function updateLastUpdated() {
setText(lastUpdated, `Last updated: ${new Date().toLocaleTimeString()}`);
}
function renderDashboardData({
health,
candles,
tradingPlan,
marketState,
runtimeStatus,
decisionPacket,
decisionAlerts,
tradeCandidates,
executionTickets,
tradingExecutions,
journalSummary,
journalItems,
weeklyReview,
}) {
renderHealth(health);
renderOverview(weeklyReview, tradeCandidates.items || [], executionTickets.items || [], tradingExecutions.items || []);
renderMarketState(marketState, tradingExecutions.items || []);
renderRuntimeStatus(runtimeStatus);
renderDecisionPacket(decisionPacket, decisionAlerts);
renderPreMarketPlan(tradingPlan);
renderCandidates(tradeCandidates.items || []);
renderTickets(executionTickets.items || []);
renderExecutions(tradingExecutions.items || []);
renderPostTradeReview(journalSummary, journalItems.items || [], weeklyReview);
drawCandles(candles.items || []);
updateActiveFilters();
updateLastUpdated();
}
async function refreshDashboard() {
const instrumentId = instrumentIdInput.value || '1';
const timeframe = timeframeInput.value || '1m';
const limit = limitInput.value || '20';
try {
const [health, candles, tradingPlan, marketState, runtimeStatus, decisionPacket, decisionAlerts, tradeCandidates, executionTickets, tradingExecutions, journalSummary, journalItems, weeklyReview] = await Promise.all([
fetchJson('/health'),
fetchJson(`/candles?${queryString({ instrument_id: instrumentId, timeframe, limit })}`),
fetchJson(`/trading/plans/latest?${queryString({ instrument_id: instrumentId })}`).catch(() => null),
fetchJson(`/trading/market-state?${queryString({ instrument_id: instrumentId, timeframe })}`).catch(() => ({ market_state: {} })),
fetchJson('/trading/runtime-status').catch(() => ({ scheduler_state: 'unavailable', tasks: {}, current_blockers: ['runtime_status_unavailable'] })),
fetchJson(`/trading/decision-packet?${queryString({ instrument_id: instrumentId })}`).catch(() => ({ status: 'unavailable', action_recommendation: 'monitor' })),
fetchJson(`/trading/alerts?${queryString({ instrument_id: instrumentId, limit: 10 })}`).catch(() => ({ items: [] })),
fetchJson(`/trading/candidates?${queryString({ instrument_id: instrumentId, limit })}`).catch(() => ({ items: [] })),
fetchJson(`/trading/tickets?${queryString({ instrument_id: instrumentId, limit })}`).catch(() => ({ items: [] })),
fetchJson(`/trading/executions?${queryString({ instrument_id: instrumentId, limit })}`).catch(() => ({ items: [] })),
fetchJson(`/trading/journal/summary?${queryString({ instrument_id: instrumentId })}`).catch(() => ({ entry_count: 0, next_week_actions: [] })),
fetchJson(`/trading/journal?${queryString({ instrument_id: instrumentId, limit })}`).catch(() => ({ items: [] })),
fetchJson(`/trading/research/weekly-review?${queryString({ instrument_id: instrumentId })}`).catch(() => ({ entry_count: 0, next_week_actions: [] })),
]);
renderDashboardData({
health,
candles,
tradingPlan,
marketState,
runtimeStatus,
decisionPacket,
decisionAlerts,
tradeCandidates,
executionTickets,
tradingExecutions,
journalSummary,
journalItems,
weeklyReview,
});
} catch (error) {
showErrorPanel(String(error));
}
}
function configureAutoRefresh() {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer);
autoRefreshTimer = null;
}
const seconds = Number(autoRefreshSecondsInput.value || 0);
if (seconds > 0) {
autoRefreshTimer = setInterval(refreshDashboard, seconds * 1000);
}
}
refreshBtn.addEventListener('click', refreshDashboard);
executionStatusFilterInput.addEventListener('change', refreshDashboard);
searchTextInput.addEventListener('input', refreshDashboard);
autoRefreshSecondsInput.addEventListener('change', configureAutoRefresh);
configureAutoRefresh();
refreshDashboard();