/** * Stock Buddy 前端逻辑 */ const API_BASE = 'http://localhost:8000/api'; // 页面状态 let currentPage = 'dashboard'; let positions = []; // ═════════════════════════════════════════════════════════════════════ // 初始化 // ═════════════════════════════════════════════════════════════════════ document.addEventListener('DOMContentLoaded', () => { initNavigation(); initEventListeners(); loadDashboard(); }); function initNavigation() { document.querySelectorAll('.nav-item').forEach(item => { item.addEventListener('click', (e) => { e.preventDefault(); const page = item.dataset.page; switchPage(page); }); }); } function initEventListeners() { // 刷新按钮 document.getElementById('refresh-btn').addEventListener('click', () => { loadCurrentPage(); }); // 运行分析 document.getElementById('run-analysis-btn').addEventListener('click', async () => { await runDailyAnalysis(); }); // 添加持仓 document.getElementById('add-position-btn').addEventListener('click', () => { showModal('add-position-modal'); }); document.getElementById('cancel-add').addEventListener('click', () => { hideModal('add-position-modal'); }); document.querySelector('.modal-close').addEventListener('click', () => { hideModal('add-position-modal'); }); document.getElementById('add-position-form').addEventListener('submit', async (e) => { e.preventDefault(); await addPosition(); }); // 股票分析 document.getElementById('analyze-btn').addEventListener('click', async () => { const input = document.getElementById('stock-search').value.trim(); if (input) { await analyzeStock(input); } }); document.getElementById('stock-search').addEventListener('keypress', (e) => { if (e.key === 'Enter') { document.getElementById('analyze-btn').click(); } }); } // ═════════════════════════════════════════════════════════════════════ // 页面切换 // ═════════════════════════════════════════════════════════════════════ function switchPage(page) { currentPage = page; // 更新导航状态 document.querySelectorAll('.nav-item').forEach(item => { item.classList.remove('active'); if (item.dataset.page === page) { item.classList.add('active'); } }); // 切换页面内容 document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); document.getElementById(`page-${page}`).classList.add('active'); // 更新标题 const titles = { dashboard: '总览', positions: '持仓管理', analysis: '股票分析', sentiment: '舆情监控', settings: '设置' }; document.getElementById('page-title').textContent = titles[page]; // 加载页面数据 loadCurrentPage(); } function loadCurrentPage() { switch (currentPage) { case 'dashboard': loadDashboard(); break; case 'positions': loadPositions(); break; case 'sentiment': loadSentiment(); break; } } // ═════════════════════════════════════════════════════════════════════ // 数据加载 // ═════════════════════════════════════════════════════════════════════ async function loadDashboard() { try { const response = await fetch(`${API_BASE}/positions`); positions = await response.json(); updateDashboardStats(); renderPositionsTable(); } catch (error) { console.error('加载失败:', error); showError('数据加载失败'); } } function updateDashboardStats() { const totalValue = positions.reduce((sum, p) => sum + (p.market_value || 0), 0); const totalCost = positions.reduce((sum, p) => sum + (p.shares * p.cost_price), 0); const totalPnl = totalValue - totalCost; const totalPnlPercent = totalCost > 0 ? (totalPnl / totalCost) * 100 : 0; document.getElementById('total-value').textContent = formatMoney(totalValue); document.getElementById('total-pnl').textContent = `${totalPnl >= 0 ? '+' : ''}${formatMoney(totalPnl)} (${totalPnlPercent.toFixed(2)}%)`; document.getElementById('total-pnl').className = `stat-change ${totalPnl >= 0 ? 'positive' : 'negative'}`; document.getElementById('position-count').textContent = positions.length; // 统计买入信号 const buySignals = positions.filter(p => p.pnl_percent < -5).length; // 简化逻辑 document.getElementById('buy-signals').textContent = buySignals; } function renderPositionsTable() { const tbody = document.getElementById('positions-tbody'); if (positions.length === 0) { tbody.innerHTML = '暂无持仓'; return; } tbody.innerHTML = positions.map(p => { const pnlClass = p.pnl >= 0 ? 'positive' : 'negative'; const signal = p.pnl_percent < -10 ? 'BUY' : p.pnl_percent > 20 ? 'SELL' : 'HOLD'; const signalClass = signal.toLowerCase(); return ` ${p.stock_name} ${p.ticker} ${p.shares} ${p.cost_price.toFixed(3)} ${(p.current_price || 0).toFixed(3)} ${formatMoney(p.market_value || 0)} ${p.pnl >= 0 ? '+' : ''}${formatMoney(p.pnl)} (${p.pnl_percent.toFixed(2)}%) ${signal} `; }).join(''); } async function loadPositions() { try { const response = await fetch(`${API_BASE}/positions`); positions = await response.json(); const tbody = document.getElementById('manage-positions-tbody'); if (positions.length === 0) { tbody.innerHTML = '暂无持仓'; return; } tbody.innerHTML = positions.map(p => { const pnlClass = p.pnl >= 0 ? 'positive' : 'negative'; return ` ${p.stock_name} ${p.ticker} ${p.shares} ${p.cost_price.toFixed(3)} ${(p.current_price || 0).toFixed(3)} ${p.pnl >= 0 ? '+' : ''}${formatMoney(p.pnl)} ${p.strategy} `; }).join(''); } catch (error) { console.error('加载失败:', error); } } async function loadSentiment() { const list = document.getElementById('sentiment-list'); if (positions.length === 0) { list.innerHTML = '

请先添加持仓股票

'; return; } list.innerHTML = '

舆情分析功能开发中...

'; } // ═════════════════════════════════════════════════════════════════════ // 持仓操作 // ═════════════════════════════════════════════════════════════════════ async function addPosition() { const data = { stock_name: document.getElementById('pos-name').value, ticker: document.getElementById('pos-ticker').value, shares: parseInt(document.getElementById('pos-shares').value), cost_price: parseFloat(document.getElementById('pos-cost').value), strategy: document.getElementById('pos-strategy').value, notes: document.getElementById('pos-notes').value }; try { const response = await fetch(`${API_BASE}/positions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (response.ok) { hideModal('add-position-modal'); document.getElementById('add-position-form').reset(); loadCurrentPage(); showSuccess('持仓添加成功'); } else { throw new Error('添加失败'); } } catch (error) { showError('添加失败: ' + error.message); } } async function deletePosition(id) { if (!confirm('确定删除此持仓吗?')) return; try { const response = await fetch(`${API_BASE}/positions/${id}`, { method: 'DELETE' }); if (response.ok) { loadPositions(); showSuccess('删除成功'); } } catch (error) { showError('删除失败'); } } // ═════════════════════════════════════════════════════════════════════ // 股票分析 // ═════════════════════════════════════════════════════════════════════ async function analyzeStock(input) { const resultDiv = document.getElementById('analysis-result'); resultDiv.classList.remove('hidden'); resultDiv.innerHTML = '
分析中,请稍候...
'; try { // 判断是名称还是代码 const isTicker = input.endsWith('.HK') || /^\d{4,5}$/.test(input); const requestData = isTicker ? { stock_name: input, ticker: input } : { stock_name: input }; const response = await fetch(`${API_BASE}/analyze`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestData) }); const result = await response.json(); if (response.ok) { renderAnalysisResult(result); } else { throw new Error(result.detail || '分析失败'); } } catch (error) { resultDiv.innerHTML = `

分析失败: ${error.message}

`; } } function renderAnalysisResult(result) { const resultDiv = document.getElementById('analysis-result'); const signalClass = result.signal.action.toLowerCase(); const sentimentClass = result.sentiment.score >= 0 ? 'positive' : 'negative'; resultDiv.innerHTML = `
${result.stock_name} (${result.ticker})
分析时间: ${new Date(result.timestamp).toLocaleString()}
${result.signal.action}
综合评分
${result.signal.score >= 0 ? '+' : ''}${result.signal.score.toFixed(2)}
当前价格
${result.technical.current_price.toFixed(3)}
建议仓位
${(result.signal.position_ratio * 100).toFixed(0)}%

技术分析

RSI
${result.technical.rsi.toFixed(1)}
趋势
${result.technical.trend === 'UP' ? '上涨📈' : result.technical.trend === 'DOWN' ? '下跌📉' : '震荡➡️'}
止损位
${(result.signal.stop_loss * 100).toFixed(1)}%

舆情分析

${result.sentiment.score > 0 ? '+' : ''}${result.sentiment.score} ${result.sentiment.label}
影响因素: ${result.sentiment.factors.join('、')}
展望: ${result.sentiment.outlook}

分析理由

`; } // ═════════════════════════════════════════════════════════════════════ // 任务操作 // ═════════════════════════════════════════════════════════════════════ async function runDailyAnalysis() { const btn = document.getElementById('run-analysis-btn'); btn.disabled = true; btn.innerHTML = ' 分析中...'; try { const response = await fetch(`${API_BASE}/tasks/daily-analysis`, { method: 'POST' }); if (response.ok) { showSuccess('每日分析任务已启动'); } else { throw new Error('启动失败'); } } catch (error) { showError('启动失败: ' + error.message); } finally { btn.disabled = false; btn.innerHTML = '▶️ 运行分析'; } } // ═════════════════════════════════════════════════════════════════════ // 工具函数 // ═════════════════════════════════════════════════════════════════════ function formatMoney(amount) { if (amount === undefined || amount === null) return '--'; if (amount >= 100000000) { return (amount / 100000000).toFixed(2) + '亿'; } else if (amount >= 10000) { return (amount / 10000).toFixed(2) + '万'; } return amount.toFixed(2); } function showModal(id) { document.getElementById(id).classList.remove('hidden'); } function hideModal(id) { document.getElementById(id).classList.add('hidden'); } function showSuccess(message) { // 简化实现,实际可用 toast console.log('✅', message); } function showError(message) { console.error('❌', message); alert(message); } // ═════════════════════════════════════════════════════════════════════ // 其他功能 // ═════════════════════════════════════════════════════════════════════ function viewPosition(id) { const pos = positions.find(p => p.id === id); if (pos) { alert(`股票: ${pos.stock_name}\n代码: ${pos.ticker}\n持仓: ${pos.shares}股\n成本: ${pos.cost_price}\n策略: ${pos.strategy}\n\n${pos.notes || ''}`); } }