const { createApp } = Vue; const ALERT_CACHE_VERSION = 5; // 配置axios携带credentials(支持跨域session) axios.defaults.withCredentials = true; createApp({ data() { // 动态计算上一年1月1日 const lastYear = new Date().getFullYear() - 1; const defaultStartDate = `${lastYear}-01-01`; return { // 分析页面 stockCode: '', startDate: defaultStartDate, // 动态设置为上一年1月1日 endDate: '', loading: false, error: null, result: null, theoryExpanded: false, periods: ['当日', '未来1日', '未来2日', '未来3日', '未来4日', '未来5日'], superChart: null, mainChart: null, latestData: {}, expandedSections: { charts: false, table: false, summary: false }, searchHistory: [], showHistory: false, clockTick: Date.now(), // 全局提示模态框 toastMessage: '', toastType: 'info', // info, success, error, warning toastVisible: false, // 确认对话框 confirmVisible: false, confirmMessage: '', confirmCallback: null, // 用户登录 currentUser: null, checkingAuth: true, // 正在检查登录状态 showLoginModal: false, isRegister: false, loginForm: { email: '', password: '' }, loginError: '', loginLoading: false, // 修改密码 showPasswordModal: false, passwordForm: { oldPassword: '', newPassword: '', confirmPassword: '' }, passwordLoading: false, passwordError: '', passwordSuccess: '', // 导航 activeTab: 'alerts', // 提醒页面 alertsLoading: false, alertsProgress: { current: 0, total: 0 }, // 分析进度 stockAlerts: [], // 所有股票的分析结果 alertSubTab: 'holding', // 'holding' 或 'watching' showAddWatch: false, // 显示添加关注输入框 newWatchCode: '', // 新关注股票代码 // 今日信号 todaySignalStock: '', todaySignalData: null, // 技术信号 techSignalCode: '', techSignalResult: null, techBatchResults: [], techLoading: false, // 全量扫描 fullScanResults: [], fullScanStatus: null, fullScanLoading: false, fullScanPage: 1, fullScanTotalPages: 1, fullScanFilter: 'all', fullScanFilterTypes: [], fullScanFilterRecommend: '买入', // 扫描结果打开时缺省显示买入列表;all|买入|加仓|卖出|持有|关注|观察|观望 scanSummaryCollapsed: false, fullScanSignalDist: [], // 找牛股(保留兼容) bullStocksData: null, bullStocksLoading: false, bullActiveStage: 2, // 个股深析 deepCode: '', deepReport: null, deepLoading: false, // 模型页子tab modelSubTab: 'system', // 买入分析 buyAnalysisList: [], buyAnalysisLoading: false, buyAnalysisProgress: 0, buyAnalysisTotal: 0, // 交易记录页面 trades: [], tradeStats: null, availableCash: 0, // 可用资金 editingCash: false, // 是否在编辑可用资金 cashInputValue: '', // 编辑时的输入值 holdingPositions: {}, // 持仓信息:{ 股票代码: { quantity, cost, currentPrice, name } } priceRefreshing: false, // 刷新价格状态 stopLossAlerts: [], // 止损预警列表 stopLossAlertsExpanded: true, // 止损预警区域是否展开 expandedTradeGroups: {}, // 展开的交易组:{ 股票代码: true/false } tradeViewType: 'holding', // 交易视图类型:'holding' 或 'cleared' showFundamentalModal: false, // 基本面弹窗 fundamentalLoading: false, // 加载状态 fundamentalData: null, // 基本面数据 aiAnalyzing: false, // AI分析中 aiAnalysisResult: '', // AI分析结果(基本面弹窗用) aiReasoningResult: '', // AI思考过程(基本面弹窗用) aiReasoningExpanded: false, // AI思考过程是否展开 klineChart: null, // K线图实例 klinePeriod: 'monthly', // K线周期:weekly, monthly, quarterly, yearly klineLoading: false, // K线加载状态 showTradeForm: false, editingTradeId: null, selectedStockFromHistory: '', selectedStockSignal: { type: '', icon: '', title: '', description: '' }, analysisCache: {}, // 缓存每个股票的分析结果 // 分析页子导航 analysisSubTab: 'scan', // 'scan' 或 'sim' // 模拟交易相关 simStats: {}, // 模拟交易统计 simPositions: [], // 模拟持仓 simTrades: [], // 模拟交易记录 simAutoTrading: false, // 自动交易执行中 simPriceRefreshing: false, // 模拟持仓刷新现价中 simRuleExpanded: false, // 交易规则是否展开 // 智能交易引擎 smartAlgoConfig: null, // 当前算法配置 smartAlgoIsDefault: true, // 是否默认配置 smartTemplates: [], // 算法模板列表 smartEngineStatus: null, // 引擎状态 smartSignals: [], // 信号日志 smartPositionMeta: [], // 持仓元数据 smartConfigExpanded: false, // 算法配置展开 smartSignalExpanded: false, // 信号日志展开 smartConfigEditing: false, // 正在编辑配置 todayTradesExpanded: false, // 今日交易展开 simPositionsExpanded: false, // 当前持仓展开 simTradesExpanded: false, // 交易记录展开 // 智能执行结果模态框 smartResultVisible: false, smartResultData: null, // { algo, type, title, reasons, results, fees, summary } compareExpanded: false, // 模拟vs实盘对比展开 newTrade: { stock_code: '', stock_name: '', trade_type: 'buy', price: '', quantity: '', trade_date: '', reason: '', result: 'pending', profit_amount: '', stop_loss_price: '', notes: '' } }; }, computed: { // 今日日期 todayDate() { void this.clockTick; const now = new Date(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); return `${month}-${day}`; }, // 市场状态:开市/闭市(clockTick驱动自动刷新) marketStatus() { void this.clockTick; const now = new Date(); const day = now.getDay(); // 0=周日, 1-5=周一到周五, 6=周六 const hour = now.getHours(); const minute = now.getMinutes(); const time = hour * 100 + minute; // 如 1430 = 14:30 // 周末闭市 if (day === 0 || day === 6) { return { isOpen: false, text: '闭市' }; } // 交易时间:9:30-11:30, 13:00-15:00 if ((time >= 930 && time <= 1130) || (time >= 1300 && time <= 1500)) { return { isOpen: true, text: '开市' }; } return { isOpen: false, text: '闭市' }; }, // 数据最新日期(从API返回的stocks中获取) dataLatestDate() { if (this.stocks && this.stocks.length > 0) { const dates = this.stocks.map(s => s.lastUpdateTime || s.date).filter(d => d); if (dates.length > 0) { const latestDate = dates.sort().reverse()[0]; // 取前10个字符作为日期,格式化为 MM-DD const dateStr = latestDate.substring(0, 10); return dateStr.substring(5); } } return ''; }, // 买入信号列表(仅关注股票,按推荐率排序) buyAlerts() { const watchCodes = this.searchHistory.map(h => h.code); return this.stockAlerts .filter(a => a.signalType === 'buy' && !this.holdingStocks.includes(a.code) && watchCodes.includes(a.code)) .sort((a, b) => (b.recommendRate || 0) - (a.recommendRate || 0)); }, // 卖出信号列表(仅关注股票,按推荐率排序) sellAlerts() { const watchCodes = this.searchHistory.map(h => h.code); return this.stockAlerts .filter(a => a.signalType === 'sell' && !this.holdingStocks.includes(a.code) && watchCodes.includes(a.code)) .sort((a, b) => (b.recommendRate || 0) - (a.recommendRate || 0)); }, // 观望列表(仅关注股票) watchAlerts() { const watchCodes = this.searchHistory.map(h => h.code); return this.stockAlerts.filter(a => a.signalType === 'watch' && !this.holdingStocks.includes(a.code) && watchCodes.includes(a.code)); }, // 有效提醒数量(买入+卖出) alertCount() { return this.buyAlerts.length + this.sellAlerts.length; }, // 今日交易记录 todayTrades() { const today = new Date().toISOString().substring(0, 10); return (this.simTrades || []).filter(t => t.trade_date === today); }, // 全景扫描结果按「推荐」筛选(按具体内容:全部/买入/加仓/卖出/持有/关注/观察/观望,并显示数量) fullScanResultsFiltered() { const r = this.fullScanFilterRecommend; if (r === 'all') return this.fullScanResults; return this.fullScanResults.filter(item => { const text = (item.recommend_text != null) ? item.recommend_text : this.getScanRecommend(item).text; return text === r; }); }, // 推荐类型列表(含数量),顺序固定 fullScanRecommendOptions() { const counts = this.fullScanStatus?.recommend_counts || {}; const hints = { '持有': '仅持仓且触达真龙时显示,建议继续持有' }; const order = ['买入', '加仓', '卖出', '持有', '关注', '观察', '观望']; return order.map(text => ({ text, count: counts[text] || 0, hint: hints[text] })); }, // 持有的股票代码(从交易记录中获取买入但未卖出的) holdingStocks() { const buyStocks = {}; const sellStocks = {}; // 统计每个股票的买入和卖出数量 this.trades.forEach(trade => { if (trade.trade_type === 'buy') { buyStocks[trade.stock_code] = (buyStocks[trade.stock_code] || 0) + parseInt(trade.quantity || 0); } else if (trade.trade_type === 'sell') { sellStocks[trade.stock_code] = (sellStocks[trade.stock_code] || 0) + parseInt(trade.quantity || 0); } }); // 持有 = 买入数量 > 卖出数量 return Object.keys(buyStocks).filter(code => (buyStocks[code] || 0) > (sellStocks[code] || 0) ); }, // 关注的股票(在历史记录中但不在持有列表中) watchingStocks() { return this.searchHistory.filter(item => !this.holdingStocks.includes(item.code) ); }, // 所有可选股票(关注列表 + 持有股票,用于交易记录表单) allStocksForTrade() { const allStocks = [...this.searchHistory]; // 添加持有的股票(如果不在关注列表中) this.holdingStocks.forEach(code => { if (!allStocks.some(item => item.code === code)) { const trade = this.trades.find(t => t.stock_code === code); allStocks.push({ code: code, name: trade?.stock_name || `股票${code}`, isHolding: true // 标记为持有股票 }); } }); // 给关注列表中已持有的股票也加上标记 return allStocks.map(item => ({ ...item, isHolding: this.holdingStocks.includes(item.code) })); }, // 持有股票的提醒 holdingAlerts() { return this.stockAlerts .filter(a => this.holdingStocks.includes(a.code)) .sort((a, b) => (b.recommendRate || 0) - (a.recommendRate || 0)); }, // 按股票分组、按时间倒序排序的交易记录 groupedTrades() { if (!this.trades || this.trades.length === 0) return []; // 按股票代码分组 const groups = {}; this.trades.forEach(trade => { const code = trade.stock_code; if (!groups[code]) { groups[code] = { code: code, name: trade.stock_name || code, trades: [], latestDate: trade.trade_date, realizedProfit: 0 // 已实现盈亏 }; } groups[code].trades.push(trade); // 记录该组最新的交易日期 if (trade.trade_date > groups[code].latestDate) { groups[code].latestDate = trade.trade_date; } }); // 计算每个组的已实现盈亏(使用平均成本法) Object.values(groups).forEach(group => { // 按交易日期和创建时间排序(从早到晚) const sortedTrades = [...group.trades].sort((a, b) => { if (a.trade_date !== b.trade_date) { return a.trade_date.localeCompare(b.trade_date); } return (a.created_at || '').localeCompare(b.created_at || ''); }); let holdingQty = 0; let holdingCost = 0; let realizedProfit = 0; sortedTrades.forEach(trade => { const qty = parseInt(trade.quantity) || 0; const price = parseFloat(trade.price) || 0; if (trade.trade_type === 'buy') { holdingCost += qty * price; holdingQty += qty; } else if (trade.trade_type === 'sell' && holdingQty > 0) { // 计算平均成本 const avgCost = holdingCost / holdingQty; // 已实现盈亏 = 卖出价 - 平均成本) × 卖出数量 realizedProfit += (price - avgCost) * qty; // 更新持仓 holdingCost -= avgCost * qty; holdingQty -= qty; } }); group.realizedProfit = realizedProfit; }); // 每组内按时间倒序排序(显示用) Object.values(groups).forEach(group => { group.trades.sort((a, b) => { // 先按日期倒序 if (b.trade_date !== a.trade_date) { return b.trade_date.localeCompare(a.trade_date); } // 同日期按创建时间倒序 return (b.created_at || '').localeCompare(a.created_at || ''); }); }); // 组与组之间排序:先按是否持有(持有在前),再按最新交易日期倒序 const holdingPositions = this.holdingPositions || {}; return Object.values(groups).sort((a, b) => { const aHolding = (holdingPositions[a.code]?.quantity || 0) > 0; const bHolding = (holdingPositions[b.code]?.quantity || 0) > 0; // 持有的排在前面 if (aHolding !== bHolding) { return bHolding ? 1 : -1; } // 同类型按日期倒序 return b.latestDate.localeCompare(a.latestDate); }); }, // 根据视图类型过滤交易分组 filteredGroupedTrades() { if (!this.groupedTrades) return []; const holdingPositions = this.holdingPositions || {}; if (this.tradeViewType === 'holding') { // 持有中:显示当前持有股票数量 > 0 的 return this.groupedTrades.filter(group => (holdingPositions[group.code]?.quantity || 0) > 0 ); } else { // 已清仓:显示当前持有股票数量 = 0 的 return this.groupedTrades.filter(group => (holdingPositions[group.code]?.quantity || 0) === 0 ); } }, // 持有股票数量 holdingGroupCount() { if (!this.groupedTrades) return 0; const holdingPositions = this.holdingPositions || {}; return this.groupedTrades.filter(group => (holdingPositions[group.code]?.quantity || 0) > 0 ).length; }, // 清仓股票数量 clearedGroupCount() { if (!this.groupedTrades) return 0; const holdingPositions = this.holdingPositions || {}; return this.groupedTrades.filter(group => (holdingPositions[group.code]?.quantity || 0) === 0 ).length; }, tradingSignal() { if (!this.result) return { type: '', icon: '', title: '', description: '' }; const pricePos = this.latestData.pricePosition || 50; const superRatio = this.latestData.superRatio || 0; const mainRatio = this.latestData.mainRatio || 0; // 判断买卖信号 const isLow = pricePos <= 50; const isHigh = pricePos > 50; const isSuperInflow = superRatio >= 2; const isSuperOutflow = superRatio <= -2; const isMainInflow = mainRatio >= 2; const isMainOutflow = mainRatio <= -2; // 买入信号:低位 + 资金流入 if (isLow && (isSuperInflow || isMainInflow)) { return { type: 'buy', icon: '✅', title: '买入信号', description: `价格处于${pricePos <= 30 ? '低位' : '中低位'}(${pricePos.toFixed(1)}%),${isSuperInflow ? '超大单' : '主力'}大额流入,建议买入,仓位30-50%,止损-5%` }; } // 卖出信号:高位 + 资金流出 if (isHigh && (isSuperOutflow || isMainOutflow)) { return { type: 'sell', icon: '🔴', title: '卖出信号', description: `价格处于${pricePos >= 70 ? '高位' : '中高位'}(${pricePos.toFixed(1)}%),${isSuperOutflow ? '超大单' : '主力'}大额流出,建议卖出或减仓,及时锁定利润` }; } // 观望信号 let hint = ''; if (isLow) { hint = '价格在低位,可关注资金流入信号后买入'; } else if (isHigh) { hint = '价格在高位,可关注资金流出信号后卖出'; } else { hint = '当前无明确买卖信号,建议继续观察'; } return { type: '', icon: '⏸️', title: '观望信号', description: hint }; } }, async mounted() { setInterval(() => { this.clockTick = Date.now(); }, 30000); const today = new Date(); this.endDate = today.toISOString().split('T')[0]; // 检查当前用户登录状态 await this.checkCurrentUser(); // 未登录时不加载数据 if (!this.currentUser) { return; } // 已登录,加载数据 this.alertsLoading = true; this.alertsProgress = { current: 0, total: 0 }; // 加载关注列表(从服务器) await this.loadHistory(); // 先加载交易记录(用于计算持有股票) await this.loadTrades(); // 自动分析关注的股票 this.$nextTick(() => { this.refreshAlerts(); }); }, watch: {}, methods: { // ========== 用户认证 ========== async checkCurrentUser() { this.checkingAuth = true; try { const response = await axios.get('/api/me'); if (response.data.success && response.data.user) { this.currentUser = response.data.user; console.debug('已登录:', this.currentUser.username); } } catch (e) { console.debug('未登录'); } finally { this.checkingAuth = false; } }, async handleLogin() { if (this.loginLoading) return; const { email, password } = this.loginForm; if (!email || !password) { this.loginError = '请输入邮箱和密码'; return; } // 验证邮箱格式 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { this.loginError = '请输入有效的邮箱地址'; return; } if (this.isRegister && password.length < 6) { this.loginError = '密码至少6位'; return; } this.loginError = ''; this.loginLoading = true; try { const url = this.isRegister ? '/api/register' : '/api/login'; const response = await axios.post(url, { email, password }); if (response.data.success) { this.currentUser = response.data.user; this.showLoginModal = false; this.loginForm = { email: '', password: '' }; this.showToast(this.isRegister ? '注册成功' : '登录成功', 'success'); // 重新加载数据 await this.loadHistory(); await this.loadTrades(); this.refreshAlerts(); } else { this.loginError = response.data.error || '操作失败'; } } catch (e) { this.loginError = e.response?.data?.error || '网络错误'; } finally { this.loginLoading = false; } }, async handleChangePassword() { if (this.passwordLoading) return; const { oldPassword, newPassword, confirmPassword } = this.passwordForm; if (!oldPassword || !newPassword || !confirmPassword) { this.passwordError = '请填写所有字段'; return; } if (newPassword.length < 6) { this.passwordError = '新密码至少6位'; return; } if (newPassword !== confirmPassword) { this.passwordError = '两次输入的新密码不一致'; return; } this.passwordError = ''; this.passwordSuccess = ''; this.passwordLoading = true; try { const response = await axios.post('/api/change_password', { old_password: oldPassword, new_password: newPassword }); if (response.data.success) { this.passwordSuccess = '密码修改成功'; this.passwordForm = { oldPassword: '', newPassword: '', confirmPassword: '' }; setTimeout(() => { this.showPasswordModal = false; this.passwordSuccess = ''; }, 1500); } else { this.passwordError = response.data.error || '修改失败'; } } catch (e) { this.passwordError = e.response?.data?.error || '网络错误'; } finally { this.passwordLoading = false; } }, handleLogout() { this.showConfirm('确定要退出登录吗?', async () => { try { await axios.post('/api/logout'); this.currentUser = null; // 清空用户数据 this.searchHistory = []; this.trades = []; this.stockAlerts = []; this.showToast('已退出登录', 'info'); } catch (e) { console.error('退出失败', e); } }); }, // 显示提示信息(替代alert) showToast(message, type = 'info', duration = 3000) { this.toastMessage = message; this.toastType = type; this.toastVisible = true; // 自动关闭 setTimeout(() => { this.toastVisible = false; }, duration); }, // 显示确认对话框(替代confirm) showConfirm(message, callback) { this.confirmMessage = message; this.confirmCallback = callback; this.confirmVisible = true; }, // 确认对话框 - 确定 handleConfirmOk() { this.confirmVisible = false; if (this.confirmCallback) { this.confirmCallback(); } }, // 确认对话框 - 取消 handleConfirmCancel() { this.confirmVisible = false; this.confirmCallback = null; }, // 格式化金额,添加千分位 formatMoney(amount, decimals = 2) { if (amount === null || amount === undefined || isNaN(amount)) return '0.00'; return Number(amount).toLocaleString('zh-CN', { minimumFractionDigits: decimals, maximumFractionDigits: decimals }); }, // 格式化市值为亿元 formatMarketCap(value) { if (!value || value === '-') return '-'; const num = parseFloat(value); if (isNaN(num)) return value; // 转换为亿元 const yi = num / 100000000; return yi.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + '亿'; }, // 基本面弹窗中的AI分析(流式输出) async aiAnalyze(code, name) { if (!code) return; this.aiAnalyzing = true; this.aiAnalysisResult = ''; this.aiReasoningResult = ''; // 使用EventSource接收流式数据 const eventSource = new EventSource(`/api/ai_analyze_stream/${code}`); eventSource.onmessage = (event) => { if (event.data === '[DONE]') { eventSource.close(); this.aiAnalyzing = false; return; } try { const data = JSON.parse(event.data); if (data.type === 'reasoning') { this.aiReasoningResult += data.content; } else if (data.type === 'content') { this.aiAnalysisResult += data.content; } else if (data.type === 'error') { this.showToast(data.content, 'error'); eventSource.close(); this.aiAnalyzing = false; } } catch (e) { console.error('解析SSE数据失败', e); } }; eventSource.onerror = (e) => { console.error('SSE连接错误', e); eventSource.close(); this.aiAnalyzing = false; if (!this.aiAnalysisResult) { this.showToast('AI分析连接失败', 'error'); } }; }, // 格式化AI分析结果(支持Markdown格式) formatAiAnalysis(text) { if (!text) return ''; return text // 处理二级标题 .replace(/^## (.+)$/gm, '

$1

') // 处理三级标题 .replace(/^### (.+)$/gm, '
$1
') // 处理加粗 .replace(/\*\*([^*]+)\*\*/g, '$1') // 处理列表项 .replace(/^- (.+)$/gm, '
  • $1
  • ') // 处理换行 .replace(/\n/g, '
    ') // 清理多余的br .replace(/

    /g, '') .replace(/<\/h5>
    /g, ''); }, // 根据股票代码获取名称 async getStockNameByCode(code) { try { const response = await axios.get(`/api/fundamental/${code}`); if (response.data.success && response.data.data) { return response.data.data.stock_name; } } catch (e) { console.debug('获取股票名称失败'); } return null; }, async analyze() { if (!this.stockCode) { this.error = '请输入股票代码'; return; } this.loading = true; this.error = null; this.result = null; try { const response = await axios.post('/api/analyze', { stock_code: this.stockCode, start_date: this.startDate, end_date: this.endDate }); if (response.data.success) { this.result = response.data; // 缓存分析结果(用于交易记录页面) this.analysisCache[this.stockCode] = response.data; // 保存到历史记录 this.saveHistory(response.data.stock_code, response.data.stock_name); // 获取最新数据 this.calculateLatestData(); // 如果图表区域已展开,渲染图表 if (this.expandedSections.charts) { this.$nextTick(() => { this.renderCharts(); }); } // 跳转到提醒页面并刷新 this.activeTab = 'alerts'; this.$nextTick(() => { this.refreshAlerts(); }); } else { this.error = response.data.error || '分析失败'; } } catch (err) { console.error('请求错误:', err); this.error = err.response?.data?.error || err.message || '请求失败'; } finally { this.loading = false; } }, // 从基本面弹窗添加到关注 async addToWatchFromFundamental() { if (!this.fundamentalData) return; const code = this.fundamentalData.stock_code; const name = this.fundamentalData.stock_name; if (this.searchHistory.some(h => h.code === code)) { this.showToast('该股票已在关注列表中', 'warning'); return; } // 添加到关注列表(通过API) try { const response = await axios.post('/api/watchlist', { code, name }); if (response.data.success) { this.searchHistory = [...response.data.watchlist]; // 强制更新stockAlerts触发关注列表刷新 this.stockAlerts = [...this.stockAlerts]; // 强制更新fundamentalData触发按钮状态刷新 this.fundamentalData = { ...this.fundamentalData }; this.showToast('已添加到关注列表', 'success'); } } catch (e) { console.error('添加关注失败', e); this.showToast('添加关注失败', 'error'); } }, // 从基本面弹窗取消关注 async removeFromWatchFromFundamental() { if (!this.fundamentalData) return; const code = this.fundamentalData.stock_code; try { const response = await axios.delete(`/api/watchlist/${code}`); if (response.data.success) { this.searchHistory = [...response.data.watchlist]; // 同时从stockAlerts中移除 this.stockAlerts = this.stockAlerts.filter(a => a.code !== code); // 强制更新fundamentalData触发按钮状态刷新 this.fundamentalData = { ...this.fundamentalData }; this.showToast('已取消关注', 'success'); } } catch (e) { console.error('取消关注失败', e); this.showToast('取消关注失败', 'error'); } }, async addToWatchFromSignal() { if (!this.techSignalResult) return; const code = this.techSignalResult.stock_code; const name = this.techSignalResult.stock_name; if (this.searchHistory.some(h => h.code === code)) { this.showToast('该股票已在关注列表中', 'warning'); return; } try { const response = await axios.post('/api/watchlist', { code, name }); if (response.data.success) { this.searchHistory = [...response.data.watchlist]; if (!this.stockAlerts.some(a => a.code === code)) { try { const alertResp = await axios.post('/api/signal_alerts', { stocks: [{ code, name }], holding_codes: this.holdingStocks }); if (alertResp.data.success && alertResp.data.results?.length) { this.stockAlerts = [...this.stockAlerts, ...alertResp.data.results]; this.saveAlertsCache(this.stockAlerts); } } catch (e) { /* fallback: alerts will refresh on tab switch */ } } this.showToast('已添加到关注列表', 'success'); } } catch (e) { console.error('添加关注失败', e); this.showToast('添加关注失败', 'error'); } }, async removeFromWatchFromSignal() { if (!this.techSignalResult) return; const code = this.techSignalResult.stock_code; try { const response = await axios.delete(`/api/watchlist/${code}`); if (response.data.success) { this.searchHistory = [...response.data.watchlist]; this.stockAlerts = this.stockAlerts.filter(a => a.code !== code); this.saveAlertsCache(this.stockAlerts); this.showToast('已取消关注', 'success'); } } catch (e) { console.error('取消关注失败', e); this.showToast('取消关注失败', 'error'); } }, async toggleWatchFromScan(code, name) { const isWatched = this.searchHistory.some(h => h.code === code); try { if (isWatched) { const response = await axios.delete(`/api/watchlist/${code}`); if (response.data.success) { this.searchHistory = response.data.watchlist || []; this.stockAlerts = this.stockAlerts.filter(a => a.code !== code); this.saveAlertsCache(this.stockAlerts); this.refreshAlerts(true); this.showToast('已取消关注', 'success'); } } else { const response = await axios.post('/api/watchlist', { code, name: name || code }); if (response.data.success) { this.searchHistory = response.data.watchlist || []; this.refreshAlerts(true); this.showToast('已添加关注', 'success'); } } } catch (e) { console.error(isWatched ? '取消关注失败' : '添加关注失败', e); this.showToast(isWatched ? '取消关注失败' : '添加关注失败', 'error'); } }, calculateLatestData() { // 从API返回的最新数据中获取 const latest = this.result.data['最新数据']; if (latest) { this.latestData = { date: latest['日期'], price: latest['收盘价'], change: latest['涨跌幅'], pricePosition: latest['价格位置'], superRatio: latest['超大单净流入占比'], mainRatio: latest['主力净流入占比'], superDirection: latest['超大单流向'], mainDirection: latest['主力流向'] }; } else { this.latestData = { price: 0, pricePosition: 50, superRatio: 0, mainRatio: 0 }; } }, renderCharts() { // 准备数据 const labels = this.periods; // 超大单数据 const superInflowData = labels.map(period => { const val = this.getComparisonValue('超大单', period, '大额流入表现'); return val !== null ? val : 0; }); const superOutflowData = labels.map(period => { const val = this.getComparisonValue('超大单', period, '大额流出表现'); return val !== null ? val : 0; }); // 主力数据 const mainInflowData = labels.map(period => { const val = this.getComparisonValue('主力', period, '大额流入表现'); return val !== null ? val : 0; }); const mainOutflowData = labels.map(period => { const val = this.getComparisonValue('主力', period, '大额流出表现'); return val !== null ? val : 0; }); // 销毁旧图表 if (this.superChart) this.superChart.destroy(); if (this.mainChart) this.mainChart.destroy(); // 超大单图表 const superCtx = this.$refs.superChart.getContext('2d'); this.superChart = new Chart(superCtx, { type: 'bar', data: { labels: labels, datasets: [ { label: '大额流入日表现', data: superInflowData, backgroundColor: 'rgba(231, 76, 60, 0.7)', borderColor: 'rgba(231, 76, 60, 1)', borderWidth: 1 }, { label: '大额流出日表现', data: superOutflowData, backgroundColor: 'rgba(39, 174, 96, 0.7)', borderColor: 'rgba(39, 174, 96, 1)', borderWidth: 1 } ] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, ticks: { callback: function(value) { return value + '%'; } } } }, plugins: { legend: { position: 'top' } } } }); // 主力图表 const mainCtx = this.$refs.mainChart.getContext('2d'); this.mainChart = new Chart(mainCtx, { type: 'bar', data: { labels: labels, datasets: [ { label: '大额流入日表现', data: mainInflowData, backgroundColor: 'rgba(231, 76, 60, 0.7)', borderColor: 'rgba(231, 76, 60, 1)', borderWidth: 1 }, { label: '大额流出日表现', data: mainOutflowData, backgroundColor: 'rgba(39, 174, 96, 0.7)', borderColor: 'rgba(39, 174, 96, 1)', borderWidth: 1 } ] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, ticks: { callback: function(value) { return value + '%'; } } } }, plugins: { legend: { position: 'top' } } } }); }, getComparisonValue(flowType, period, key) { try { const comparison = this.result.data['对比分析'][flowType]; if (comparison && comparison[period]) { return comparison[period][key]; } } catch (e) {} return null; }, formatPercent(value) { if (value === null || value === undefined) return '-'; return value.toFixed(2) + '%'; }, getClass(value) { if (value === null || value === undefined) return ''; return value >= 0 ? 'positive' : 'negative'; }, toggleSection(section) { this.expandedSections[section] = !this.expandedSections[section]; // 如果展开图表区域,需要重新渲染图表 if (section === 'charts' && this.expandedSections.charts && this.result) { this.$nextTick(() => { this.renderCharts(); }); } }, // 关注列表相关方法(使用服务器存储) async loadHistory() { try { const response = await axios.get('/api/watchlist'); if (response.data.success) { this.searchHistory = response.data.watchlist; } } catch (e) { console.error('加载关注列表失败', e); } }, async saveHistory(code, name) { try { const response = await axios.post('/api/watchlist', { code, name }); if (response.data.success) { this.searchHistory = response.data.watchlist; } } catch (e) { console.error('保存关注列表失败', e); } }, selectHistory(item) { this.stockCode = item.code; this.showHistory = false; // 自动开始分析 this.analyze(); }, async removeHistory(code) { try { const response = await axios.delete(`/api/watchlist/${code}`); if (response.data.success) { this.searchHistory = response.data.watchlist; } } catch (e) { console.error('移除关注失败', e); } }, hideHistory() { // 延迟隐藏,让点击事件能够触发 setTimeout(() => { this.showHistory = false; }, 200); }, // ========== 提醒页面相关方法 ========== async refreshAlerts(forceRefresh = false) { // 合并关注列表和持有股票(去重) const allStocks = [...this.searchHistory]; // 添加持有的股票(如果不在关注列表中) this.holdingStocks.forEach(code => { if (!allStocks.some(item => item.code === code)) { const trade = this.trades.find(t => t.stock_code === code); allStocks.push({ code: code, name: trade?.stock_name || `股票${code}` }); } }); if (allStocks.length === 0) { this.stockAlerts = []; this.alertsLoading = false; return; } const today = new Date().toISOString().split('T')[0]; // 1. 先尝试加载缓存 if (!forceRefresh) { try { const cacheResponse = await axios.get('/api/alerts_cache'); if (cacheResponse.data.success && cacheResponse.data.alerts?.length > 0) { const cachedAlerts = cacheResponse.data.alerts; const lastUpdate = cacheResponse.data.lastUpdate; const cacheDate = lastUpdate ? lastUpdate.split(' ')[0] : null; const cacheVer = cacheResponse.data.version || 0; // 版本不匹配则强制刷新 if (cacheVer < ALERT_CACHE_VERSION) { console.debug(`缓存版本过旧(v${cacheVer} < v${ALERT_CACHE_VERSION}),强制刷新`); } else { this.stockAlerts = cachedAlerts; console.debug(`加载缓存数据: ${cachedAlerts.length}条, 更新时间: ${lastUpdate}, v${cacheVer}`); if (cacheDate === today) { console.debug('缓存是今天的,无需更新'); this.alertsLoading = false; this.updateHoldingPricesFromAlerts(); const cachedCodes = new Set(cachedAlerts.map(a => a.code)); const newStocks = allStocks.filter(s => !cachedCodes.has(s.code)); if (newStocks.length > 0) { console.debug(`发现${newStocks.length}只新股票,增量更新`); await this.analyzeNewStocks(newStocks, today); } return; } } } } catch (err) { console.debug('加载缓存失败,将重新分析:', err); } } // 2. 使用信号扫描接口 this.alertsLoading = true; this.alertsProgress = { current: 0, total: allStocks.length }; try { console.debug(`开始信号分析 ${allStocks.length} 只股票...`); const startTime = Date.now(); const response = await axios.post('/api/signal_alerts', { stocks: allStocks, holding_codes: this.holdingStocks }); const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); console.debug(`信号分析完成,耗时 ${elapsed}s`); if (response.data.success) { const results = response.data.results || []; this.alertsProgress.current = allStocks.length; this.stockAlerts = [...results]; console.debug(`成功: ${response.data.success_count}`); // 3. 保存到缓存 this.saveAlertsCache(this.stockAlerts); // 4. 更新持仓现价 this.updateHoldingPricesFromAlerts(); // 5. 后台用实时接口更新提醒中的现价(避免显示表里的旧价) this.refreshAlertPricesFromRealtime(); } else { console.error('信号分析失败:', response.data.error); } } catch (err) { console.error('批量分析请求失败:', err); // 回退到逐个分析 console.debug('回退到逐个分析模式...'); await this.analyzeStocksOneByOne(allStocks, today, forceRefresh); } this.alertsLoading = false; }, // 逐个分析(批量失败时的回退方案) async analyzeStocksOneByOne(allStocks, today, forceRefresh) { const results = []; const BATCH_SIZE = 5; for (let i = 0; i < allStocks.length; i += BATCH_SIZE) { const batch = allStocks.slice(i, i + BATCH_SIZE); const batchPromises = batch.map(item => this.analyzeStock(item, today).catch(err => { console.error(`分析${item.code}失败:`, err); return null; }) ); const batchResults = await Promise.all(batchPromises); batchResults.forEach(alertData => { if (alertData) results.push(alertData); }); this.alertsProgress.current = Math.min(i + BATCH_SIZE, allStocks.length); this.stockAlerts = [...results]; } this.stockAlerts = results; if (forceRefresh && results.length > 0) { for (const alert of results) { try { const resp = await axios.get(`/api/realtime_price/${alert.code}`); const priceData = resp.data.price || resp.data.data?.price; if (resp.data.success && priceData) { const newPrice = priceData; const oldPrice = alert.price || 0; if (oldPrice > 0 && newPrice > oldPrice) { alert.priceDirection = 'up'; } else if (oldPrice > 0 && newPrice < oldPrice) { alert.priceDirection = 'down'; } alert.price = newPrice; } } catch (err) {} } this.stockAlerts = [...results]; } this.saveAlertsCache(this.stockAlerts); this.updateHoldingPricesFromAlerts(); }, // 分析单只股票 async analyzeStock(item, today) { const response = await axios.post('/api/analyze', { stock_code: item.code, start_date: this.startDate, end_date: today }); if (response.data.success) { const latest = response.data.data['最新数据']; if (latest) { const pricePos = latest['价格位置'] || 50; const superRatio = latest['超大单净流入占比'] || 0; const mainRatio = latest['主力净流入占比'] || 0; const isLow = pricePos <= 50; const isHigh = pricePos > 50; const isSuperInflow = superRatio >= 2; const isSuperOutflow = superRatio <= -2; const isMainInflow = mainRatio >= 2; const isMainOutflow = mainRatio <= -2; let signalType = 'watch'; let reason = '无明确信号'; let recommendRate = 0; if (isLow && (isSuperInflow || isMainInflow)) { signalType = 'buy'; reason = `低位${pricePos.toFixed(0)}% + ${isSuperInflow ? '超大单' : '主力'}流入`; const posScore = Math.max(0, (50 - pricePos) / 50) * 50; const flowScore = Math.min(Math.max(superRatio, mainRatio), 10) * 5; recommendRate = Math.round(posScore + flowScore); } else if (isHigh && (isSuperOutflow || isMainOutflow)) { signalType = 'sell'; reason = `高位${pricePos.toFixed(0)}% + ${isSuperOutflow ? '超大单' : '主力'}流出`; const posScore = Math.max(0, (pricePos - 50) / 50) * 50; const flowScore = Math.min(Math.abs(Math.min(superRatio, mainRatio)), 10) * 5; recommendRate = Math.round(posScore + flowScore); } const closePrice = latest['收盘价']; return { code: item.code, name: response.data.stock_name || item.name, price: closePrice, changePct: 0, scanPrice: closePrice, scanChangePct: 0, pricePosition: pricePos, superRatio: superRatio, mainRatio: mainRatio, signalType: signalType, reason: reason, recommendRate: recommendRate, updateDate: today }; } } return null; }, // 分析新增股票(增量更新,并行) async analyzeNewStocks(newStocks, today) { this.alertsLoading = true; try { const response = await axios.post('/api/signal_alerts', { stocks: newStocks, holding_codes: this.holdingStocks }); if (response.data.success) { const newResults = response.data.results || []; newResults.forEach(r => { if (!this.stockAlerts.some(a => a.code === r.code)) { this.stockAlerts.push(r); } }); this.stockAlerts = [...this.stockAlerts]; } } catch (err) { console.error('增量信号分析失败:', err); } this.alertsLoading = false; this.saveAlertsCache(this.stockAlerts); }, // 保存分析结果到缓存 async saveAlertsCache(alerts) { try { await axios.post('/api/alerts_cache', { alerts: alerts, lastUpdate: new Date().toISOString().replace('T', ' ').split('.')[0], version: ALERT_CACHE_VERSION }); console.debug('分析结果已保存到缓存'); } catch (err) { console.error('保存缓存失败:', err); } }, // 添加关注 async addToWatch() { if (!this.newWatchCode || this.newWatchCode.length < 6) return; const code = this.newWatchCode.trim(); // 检查是否已存在 if (this.searchHistory.some(item => item.code === code)) { this.showToast('该股票已在关注列表中', 'warning'); return; } // 获取股票信息 try { const today = new Date().toISOString().split('T')[0]; const response = await axios.post('/api/analyze', { stock_code: code, start_date: this.startDate, end_date: today }); if (response.data.success) { const stockName = response.data.stock_name || code; // 添加到关注列表(通过API) await axios.post('/api/watchlist', { code: code, name: stockName }); await this.loadHistory(); // 刷新提醒 await this.refreshAlerts(); // 清空输入并隐藏 this.newWatchCode = ''; this.showAddWatch = false; // 切换到关注标签 this.alertSubTab = 'watching'; } else { this.showToast('添加失败:' + (response.data.error || '未知错误'), 'error'); } } catch (err) { this.showToast('添加失败:' + (err.response?.data?.error || err.message), 'error'); } }, // 移除关注 removeFromWatch(code) { this.showConfirm('确定移除该股票的关注吗?', async () => { // 从关注列表中移除(通过API) try { const response = await axios.delete(`/api/watchlist/${code}`); if (response.data.success) { this.searchHistory = response.data.watchlist; } } catch (e) { console.error('移除关注失败', e); } // 从提醒列表中移除 this.stockAlerts = this.stockAlerts.filter(a => a.code !== code); // 清除缓存 delete this.analysisCache[code]; }); }, // 从提醒页面快速交易 quickTradeFromAlert(alert, tradeType) { // 如果股票不在关注列表中,临时添加 if (!this.searchHistory.some(item => item.code === alert.code)) { this.searchHistory.push({ code: alert.code, name: alert.name || `股票${alert.code}` }); } // 预填充交易表单 this.newTrade.stock_code = alert.code; this.newTrade.stock_name = alert.name; this.newTrade.trade_type = tradeType; this.newTrade.price = alert.price; this.newTrade.trade_date = new Date().toISOString().split('T')[0]; this.newTrade.reason = alert.reason; this.selectedStockFromHistory = alert.code; this.selectedStockSignal = { type: tradeType, icon: tradeType === 'buy' ? '✓' : '!', title: tradeType === 'buy' ? '买入信号' : '卖出信号', description: alert.reason }; // 切换到交易页面并打开表单 this.activeTab = 'trades'; this.showTradeForm = true; }, // ========== 交易记录相关方法 ========== async loadTrades() { try { const response = await axios.get('/api/trades'); if (response.data.success) { this.trades = response.data.trades || []; await this.calculateHoldingPositions(); this.calculateTradeStats(); // 检查止损线 await this.checkStopLoss(); } // 加载可用资金 await this.loadAvailableCash(); } catch (err) { console.error('加载交易记录失败:', err); this.showToast('加载交易记录失败', 'error'); } }, async loadAvailableCash() { try { const response = await axios.get('/api/available_cash'); if (response.data.success) { this.availableCash = response.data.available_cash || 0; } } catch (err) { console.error('加载可用资金失败:', err); } }, startEditCash() { this.editingCash = true; this.cashInputValue = this.availableCash.toString(); this.$nextTick(() => { const input = this.$refs.cashInput; if (input) input.focus(); }); }, async saveCash() { try { const amount = parseFloat(this.cashInputValue); if (isNaN(amount)) { this.showToast('请输入有效金额', 'warning'); return; } const response = await axios.put('/api/available_cash', { amount }); if (response.data.success) { this.availableCash = response.data.available_cash; this.editingCash = false; } else { this.showToast('保存失败: ' + (response.data.error || '未知错误'), 'error'); } } catch (err) { console.error('保存可用资金失败:', err); this.showToast('保存失败', 'error'); } }, cancelEditCash() { this.editingCash = false; this.cashInputValue = ''; }, // 计算持仓(不获取现价,等refreshAlerts后再更新) async calculateHoldingPositions() { const positions = {}; // 计算每只股票的持仓数量和成本 this.trades.forEach(trade => { const code = trade.stock_code; if (!positions[code]) { positions[code] = { quantity: 0, totalCost: 0, name: trade.stock_name, currentPrice: 0 }; } const qty = parseInt(trade.quantity) || 0; const price = parseFloat(trade.price) || 0; if (trade.trade_type === 'buy') { positions[code].totalCost += qty * price; positions[code].quantity += qty; } else if (trade.trade_type === 'sell') { // 卖出时按比例减少成本 const avgCost = positions[code].quantity > 0 ? positions[code].totalCost / positions[code].quantity : 0; positions[code].totalCost -= qty * avgCost; positions[code].quantity -= qty; } }); // 先从 stockAlerts 缓存中更新现价(不发请求) const holdingCodes = Object.keys(positions).filter(code => positions[code].quantity > 0); for (const code of holdingCodes) { const alert = this.stockAlerts.find(a => a.code === code); if (alert && alert.price) { positions[code].currentPrice = alert.price; } } this.holdingPositions = positions; }, // 从分析结果更新持仓现价 updateHoldingPricesFromAlerts() { const holdingCodes = Object.keys(this.holdingPositions).filter( code => this.holdingPositions[code].quantity > 0 ); for (const code of holdingCodes) { const alert = this.stockAlerts.find(a => a.code === code); if (alert && alert.price) { this.holdingPositions[code].currentPrice = alert.price; } } this.calculateTradeStats(); }, /** * 用实时接口更新现价(提醒与交易共用)。 * @param {string[]} [codesOnly] - 若传则只刷新这些 code(交易 tab 传持仓 code);不传则刷新全部 stockAlerts(提醒 tab) * @param {number} [timeoutMs] - 单次请求超时,默认 12s */ async refreshAlertPricesFromRealtime(codesOnly, timeoutMs = 12000) { const list = codesOnly && codesOnly.length > 0 ? (this.stockAlerts || []).filter(a => codesOnly.includes(a.code)) : (this.stockAlerts || []); if (list.length === 0) { if (codesOnly && codesOnly.length > 0) { // 持仓在 stockAlerts 里可能没有,仍要拉价并写回 holdingPositions for (const code of codesOnly) { try { const resp = await axios.get(`/api/realtime_price/${code}`, { timeout: timeoutMs }); const priceData = resp.data.price ?? resp.data.data?.price; const changeData = resp.data.data?.change; if (resp.data.success && priceData != null && this.holdingPositions[code]) { this.holdingPositions[code].currentPrice = Number(priceData); this.holdingPositions = { ...this.holdingPositions }; this.calculateTradeStats(); } } catch (e) { console.error(`获取${code}实时价格失败:`, e); } } } return; } const BATCH = 5; for (let i = 0; i < list.length; i += BATCH) { const batch = list.slice(i, i + BATCH); await Promise.all(batch.map(async (alert) => { try { const resp = await axios.get(`/api/realtime_price/${alert.code}`, { timeout: timeoutMs }); const priceData = resp.data.price ?? resp.data.data?.price; const changeData = resp.data.data?.change; if (resp.data.success && priceData != null) { const newPrice = Number(priceData); const oldPrice = alert.price || 0; alert.price = newPrice; // 同步更新实时涨跌幅 if (changeData != null) alert.changePct = Number(changeData); if (oldPrice > 0 && newPrice > oldPrice) alert.priceDirection = 'up'; else if (oldPrice > 0 && newPrice < oldPrice) alert.priceDirection = 'down'; if (alert.latest_data) alert.latest_data['收盘价'] = newPrice; } } catch (e) { /* 单只失败忽略 */ } })); this.stockAlerts = [...this.stockAlerts]; if (codesOnly) { this.updateHoldingPricesFromAlerts(); this.holdingPositions = { ...this.holdingPositions }; this.calculateTradeStats(); } } this.saveAlertsCache(this.stockAlerts); this.updateHoldingPricesFromAlerts(); }, /** 交易 tab:刷新持仓现价,与提醒 tab 共用 refreshAlertPricesFromRealtime,仅传持仓 code */ async refreshHoldingPrices() { const holdingCodes = Object.keys(this.holdingPositions).filter( code => this.holdingPositions[code].quantity > 0 ); if (holdingCodes.length === 0) return; this.priceRefreshing = true; try { await this.refreshAlertPricesFromRealtime(holdingCodes, 12000); await this.checkStopLoss(); } catch (e) { console.error('刷新持仓价格异常:', e); } finally { this.priceRefreshing = false; } }, // 检查止损线 async checkStopLoss() { try { const response = await axios.get('/api/stoploss_check'); if (response.data.success) { this.stopLossAlerts = response.data.alerts || []; } } catch (error) { console.error('检查止损失败:', error); this.showToast('检查止损失败,请稍后重试', 'error'); } }, // 切换交易组展开/折叠 toggleTradeGroup(code) { if (this.expandedTradeGroups[code]) { delete this.expandedTradeGroups[code]; } else { this.expandedTradeGroups[code] = true; } // 触发响应式更新 this.expandedTradeGroups = { ...this.expandedTradeGroups }; }, // 加载基本面数据 async loadFundamental(code, name) { this.aiAnalysisResult = ''; this.aiReasoningResult = ''; this.aiAnalyzing = false; this.showFundamentalModal = true; this.fundamentalLoading = true; this.fundamentalData = { stock_code: code, stock_name: name, fundFlow: [], klineData: [] }; try { // 使用 Promise.allSettled 确保单个请求失败不影响其他数据 const [fundResult, flowResult, klineResult] = await Promise.allSettled([ axios.get(`/api/fundamental/${code}`), axios.get(`/api/fundflow/${code}?days=3`), axios.get(`/api/kline/${code}?period=${this.klinePeriod}`) ]); // 基本面数据 if (fundResult.status === 'fulfilled' && fundResult.value.data.success) { this.fundamentalData = { ...fundResult.value.data.data, fundFlow: [], klineData: [] }; } // 近3天资金流向 if (flowResult.status === 'fulfilled' && flowResult.value.data.success) { this.fundamentalData.fundFlow = flowResult.value.data.data || []; } // K线数据(可能失败,不影响其他数据显示) if (klineResult.status === 'fulfilled' && klineResult.value.data.success) { this.fundamentalData.klineData = klineResult.value.data.data || []; } } catch (error) { console.error('获取数据失败:', error); } finally { this.fundamentalLoading = false; // 绘制K线图 this.$nextTick(() => { this.renderKlineChart(); }); } }, // 切换K线周期 async changeKlinePeriod(period) { if (!this.fundamentalData?.stock_code) return; this.klinePeriod = period; this.klineLoading = true; try { const response = await axios.get(`/api/kline/${this.fundamentalData.stock_code}?period=${period}`); if (response.data.success) { this.fundamentalData.klineData = response.data.data || []; this.renderKlineChart(); } } catch (error) { console.error('获取K线数据失败:', error); } finally { this.klineLoading = false; } }, // 渲染K线图(使用收盘价折线图) renderKlineChart() { if (!this.fundamentalData?.klineData || this.fundamentalData.klineData.length === 0) return; const canvas = this.$refs.klineCanvas; if (!canvas) return; // 销毁旧图表 if (this.klineChart) { this.klineChart.destroy(); } const data = this.fundamentalData.klineData; const labels = data.map(d => d.date.slice(5)); // 只显示 MM-DD const prices = data.map(d => d.close); // 计算涨跌颜色 const firstPrice = prices[0]; const lastPrice = prices[prices.length - 1]; const isUp = lastPrice >= firstPrice; const lineColor = isUp ? '#00ff88' : '#ff4444'; const bgColor = isUp ? 'rgba(0, 255, 136, 0.1)' : 'rgba(255, 68, 68, 0.1)'; this.klineChart = new Chart(canvas, { type: 'line', data: { labels: labels, datasets: [{ label: '收盘价', data: prices, borderColor: lineColor, backgroundColor: bgColor, fill: true, tension: 0.3, pointRadius: 0, pointHoverRadius: 4, borderWidth: 2 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { callbacks: { label: (ctx) => `¥${ctx.raw.toFixed(2)}` } } }, scales: { x: { display: true, grid: { display: false }, ticks: { color: 'rgba(255,255,255,0.5)', font: { size: 10 }, maxTicksLimit: 6 } }, y: { display: true, grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: 'rgba(255,255,255,0.5)', font: { size: 10 }, callback: (v) => '¥' + v.toFixed(2) } } } } }); }, // 从止损预警快速卖出 quickSellFromStopLoss(alert) { // 添加到关注列表(如果不存在) if (!this.searchHistory.some(h => h.code === alert.code)) { this.searchHistory.push({ code: alert.code, name: alert.name }); } this.editingTradeId = null; this.selectedStockFromHistory = alert.code; this.selectedStockSignal = { type: 'sell', icon: '🔴', title: '止损信号', description: alert.message }; this.newTrade = { stock_code: alert.code, stock_name: alert.name, trade_type: 'sell', price: alert.current_price, quantity: alert.quantity, trade_date: new Date().toISOString().split('T')[0], reason: alert.message, result: 'pending', profit_amount: '', stop_loss_price: '', notes: `止损卖出,亏损 ${alert.profit_percent.toFixed(1)}%` }; this.showTradeForm = true; }, async submitTrade() { // 设置默认日期 if (!this.newTrade.trade_date) { this.newTrade.trade_date = new Date().toISOString().split('T')[0]; } // 生成唯一ID const tradeData = { ...this.newTrade, id: this.editingTradeId || Date.now().toString(), created_at: new Date().toISOString() }; try { let response; if (this.editingTradeId) { // 更新已有记录 response = await axios.put(`/api/trades/${this.editingTradeId}`, tradeData); } else { // 添加新记录 response = await axios.post('/api/trades', tradeData); } if (response.data.success) { this.showTradeForm = false; this.resetTradeForm(); if (response.data.available_cash != null) { this.availableCash = response.data.available_cash; } await this.loadTrades(); } } catch (err) { console.error('保存交易记录失败:', err); this.showToast('保存失败: ' + (err.response?.data?.error || err.message), 'error'); } }, editTrade(trade) { this.editingTradeId = trade.id; this.newTrade = { ...trade }; // 如果股票不在关注列表中,临时添加 if (!this.searchHistory.some(item => item.code === trade.stock_code)) { this.searchHistory.push({ code: trade.stock_code, name: trade.stock_name || `股票${trade.stock_code}` }); } // 设置选中的股票,用于显示在下拉框中 this.selectedStockFromHistory = trade.stock_code; // 设置信号显示 this.selectedStockSignal = { type: trade.trade_type, icon: trade.trade_type === 'buy' ? '✓' : '!', title: trade.trade_type === 'buy' ? '买入' : '卖出', description: trade.reason || '' }; this.showTradeForm = true; }, deleteTrade(tradeId) { this.showConfirm('确定删除这条交易记录吗?', async () => { try { const response = await axios.delete(`/api/trades/${tradeId}`); if (response.data.success) { if (response.data.available_cash != null) { this.availableCash = response.data.available_cash; } await this.loadTrades(); } } catch (err) { console.error('删除交易记录失败:', err); this.showToast('删除失败: ' + (err.response?.data?.error || err.message), 'error'); } }); }, resetTradeForm() { this.editingTradeId = null; this.selectedStockFromHistory = ''; this.selectedStockSignal = { type: '', icon: '', title: '', description: '' }; this.newTrade = { stock_code: '', stock_name: '', trade_type: 'buy', price: '', quantity: '', trade_date: new Date().toISOString().split('T')[0], // 默认当日 reason: '', result: 'pending', profit_amount: '', stop_loss_price: '', notes: '' }; }, openTradeForm() { if (this.allStocksForTrade.length === 0) { this.showToast('请先在分析页面查询至少一支股票或有持有股票后再记录交易', 'warning'); return; } this.resetTradeForm(); // 设置默认日期为当日 this.newTrade.trade_date = new Date().toISOString().split('T')[0]; this.showTradeForm = true; }, async onStockSelected() { if (!this.selectedStockFromHistory) { this.selectedStockSignal = { type: '', icon: '', title: '', description: '' }; return; } const stockCode = this.selectedStockFromHistory; const stockItem = this.searchHistory.find(item => item.code === stockCode); // 更新交易表单的股票信息 this.newTrade.stock_code = stockCode; this.newTrade.stock_name = stockItem ? stockItem.name : ''; // 检查是否有缓存的分析结果 if (this.analysisCache[stockCode]) { this.applyAnalysisToTrade(this.analysisCache[stockCode]); return; } // 重新获取最新分析 try { const response = await axios.post('/api/analyze', { stock_code: stockCode, start_date: this.startDate, end_date: this.endDate || new Date().toISOString().split('T')[0] }); if (response.data.success) { // 缓存分析结果 this.analysisCache[stockCode] = response.data; this.applyAnalysisToTrade(response.data); } else { this.selectedStockSignal = { type: '', icon: '⚠️', title: '无法获取分析', description: response.data.error || '分析失败' }; } } catch (err) { this.selectedStockSignal = { type: '', icon: '⚠️', title: '获取分析失败', description: err.message }; } }, async fetchTechSignals(scanItem) { if (!this.techSignalCode) return; this.techLoading = true; const code = scanItem?.code || this.techSignalCode; const name = scanItem?.name || ''; if (scanItem && scanItem.code) { // 方案C:先展示扫描结果,后台请求实时检测 const scanCount = (scanItem.signal_status || []).filter(s => s.triggered).length; this.techSignalResult = { stock_code: code, stock_name: name, signals: [], latest_signals: scanItem.latest_signals || [], signal_summary: {}, indicators: scanItem.indicators || {}, signal_status: scanItem.signal_status || [], recommend_type: scanItem.recommend_type, recommend_text: scanItem.recommend_text, recommend_reason: scanItem.recommend_reason || '', recommend_rate: scanItem.recommend_rate, has_scan_data: true, scan_triggered_count: scanCount, realtime_loading: true, realtime_signal_status: null, realtime_triggered_count: null, realtime_recommend_text: null, realtime_recommend_type: null, realtime_recommend_reason: null, realtime_recommend_rate: null, realtime_indicators: null, }; this.techLoading = false; // 后台请求实时检测 try { const holding = (this.holdingStocks || []).join(','); const url = holding ? `/api/technical_signals/${code}?lookback=5&days=120&holding_codes=${encodeURIComponent(holding)}` : `/api/technical_signals/${code}?lookback=5&days=120`; const resp = await axios.get(url); if (resp.data.success && this.techSignalResult && this.techSignalResult.stock_code === code) { const rt = resp.data; const rtCount = (rt.signal_status || []).filter(s => s.triggered).length; this.techSignalResult.realtime_loading = false; this.techSignalResult.realtime_signal_status = rt.signal_status || []; this.techSignalResult.realtime_triggered_count = rtCount; this.techSignalResult.realtime_recommend_text = rt.recommend_text; this.techSignalResult.realtime_recommend_type = rt.recommend_type; this.techSignalResult.realtime_recommend_reason = rt.recommend_reason; this.techSignalResult.realtime_recommend_rate = rt.recommend_rate; this.techSignalResult.realtime_indicators = rt.indicators; this.techSignalResult.realtime_holding_note = rt.holding_note; this.techSignalResult.signals = rt.signals || []; } } catch (err) { if (this.techSignalResult && this.techSignalResult.stock_code === code) { this.techSignalResult.realtime_loading = false; this.techSignalResult.realtime_error = err.message || '获取失败'; } } return; } try { const holding = (this.holdingStocks || []).join(','); const url = holding ? `/api/technical_signals/${this.techSignalCode}?lookback=5&days=120&holding_codes=${encodeURIComponent(holding)}` : `/api/technical_signals/${this.techSignalCode}?lookback=5&days=120`; const resp = await axios.get(url); if (resp.data.success) { this.techSignalResult = resp.data; this.techSignalResult.has_scan_data = false; } } catch (err) { console.error('技术信号检测失败:', err); this.showToast('技术信号检测失败,请稍后重试', 'error'); } finally { this.techLoading = false; } }, async batchTechSignals() { if (!this.searchHistory || this.searchHistory.length === 0) { this.showToast('请先添加关注股票', 'warning'); return; } this.techLoading = true; this.techBatchResults = []; try { const codes = this.searchHistory.map(h => h.code); const resp = await axios.post('/api/batch_technical_signals', { codes: codes, lookback: 5, days: 120, holding_codes: this.holdingStocks }); if (resp.data.success) { this.techBatchResults = (resp.data.results || []) .sort((a, b) => (b.triggered_count || 0) - (a.triggered_count || 0)); } } catch (err) { console.error('批量技术信号检测失败:', err); this.showToast('批量检测失败,请稍后重试', 'error'); } finally { this.techLoading = false; } }, async fetchFullScanStatus() { try { const resp = await axios.get('/api/scan_status'); if (resp.data.success) { this.fullScanStatus = resp.data; } } catch (err) { console.error('获取扫描状态失败:', err); } }, async fetchFullScanResults(page) { this.fullScanLoading = true; if (page) this.fullScanPage = page; try { const minTriggered = (this.fullScanFilter === 'triggered' || this.fullScanFilter === 'multi') ? 1 : 0; let signalTypes = ''; if (this.fullScanFilterTypes.length > 0) { signalTypes = this.fullScanFilterTypes.join(','); } const resp = await axios.get('/api/scan_results', { params: { page: this.fullScanPage, per_page: 50, min_triggered: minTriggered, signal_type: signalTypes, holding_codes: (this.holdingStocks || []).join(','), recommend_text: this.fullScanFilterRecommend === 'all' ? '' : this.fullScanFilterRecommend, with_scores: true, } }); if (resp.data.success) { this.fullScanResults = resp.data.results || []; this.fullScanTotalPages = resp.data.total_pages || 1; this.fullScanSignalDist = resp.data.signal_distribution || []; this.fullScanStatus = { total: resp.data.total_stocks || resp.data.total_scanned, scanned: resp.data.total_scanned, triggered: resp.data.triggered_stocks, progress: 100, is_complete: true, scan_date: resp.data.scan_date, scan_start: resp.data.scan_start, scan_end: resp.data.scan_end, recommend_counts: resp.data.recommend_counts || {}, }; } } catch (err) { console.error('获取扫描结果失败:', err); this.showToast('获取扫描结果失败,请稍后重试', 'error'); } finally { this.fullScanLoading = false; } }, async fetchBullStocks() { this.bullStocksLoading = true; try { const holding = (this.holdingStocks || []).join(','); const resp = await axios.get('/api/bull_stocks', { params: holding ? { holdingStocks: holding } : {} }); if (resp.data.success) { this.bullStocksData = resp.data; } else { this.showToast(resp.data.error || '获取牛股数据失败', 'error'); } } catch (err) { console.error('获取牛股数据失败:', err); this.showToast('获取牛股数据失败', 'error'); } finally { this.bullStocksLoading = false; } }, async fetchDeepAnalysis() { const code = (this.deepCode || '').trim(); if (!code || code.length < 6) { this.showToast('请输入6位股票代码', 'error'); return; } this.deepLoading = true; this.deepReport = null; try { const resp = await axios.post('/api/deep_analyze', { stock_code: code }); if (resp.data.success) { this.deepReport = resp.data.report; } else { this.showToast(resp.data.error || '分析失败', 'error'); } } catch (err) { console.error('深度分析失败:', err); this.showToast('深度分析失败: ' + (err.response?.data?.error || err.message), 'error'); } finally { this.deepLoading = false; } }, async fetchBuyAnalysis() { if (this.buyAnalysisLoading) return; this.buyAnalysisLoading = true; this.buyAnalysisList = []; this.buyAnalysisProgress = 0; try { const scanResp = await axios.get('/api/scan_results', { params: { per_page: 200, recommend_text: '买入' } }); if (!scanResp.data.success) { this.showToast('获取买入推荐失败', 'error'); return; } const buyStocks = scanResp.data.results || []; this.buyAnalysisTotal = buyStocks.length; if (buyStocks.length === 0) { this.showToast('当前无买入推荐股票', 'info'); return; } const results = []; for (let i = 0; i < buyStocks.length; i++) { this.buyAnalysisProgress = i + 1; try { const resp = await axios.post('/api/deep_analyze', { stock_code: buyStocks[i].code, skip_llm: true }); if (resp.data.success) { const report = resp.data.report; report._expanded = false; results.push(report); } } catch (e) { console.warn('分析失败:', buyStocks[i].code, e.message); } } results.sort((a, b) => b.deep_score - a.deep_score); this.buyAnalysisList = results; } catch (err) { console.error('买入分析失败:', err); this.showToast('买入分析失败: ' + (err.message || '未知错误'), 'error'); } finally { this.buyAnalysisLoading = false; } }, getBullStageStocks(stageNum) { if (!this.bullStocksData || !this.bullStocksData.stages) return []; return this.bullStocksData.stages[String(stageNum)] || []; }, getBullStageCount(stageNum) { if (!this.bullStocksData || !this.bullStocksData.summary) return 0; return this.bullStocksData.summary[String(stageNum)] || this.bullStocksData.summary[stageNum] || 0; }, getBullStageInfo(stageNum) { if (!this.bullStocksData || !this.bullStocksData.stage_info) return null; return this.bullStocksData.stage_info.find(s => s.stage === stageNum); }, getScanRecommend(item) { if (item.recommend_type != null && item.recommend_text != null) return { text: item.recommend_text, cls: item.recommend_type }; if (!item.signal_status || item.signal_status.length === 0) return { text: '观望', cls: 'watch' }; const sigMap = {}; item.signal_status.forEach(s => { sigMap[s.type] = s.triggered; }); const hasDivergence = sigMap['daily_bottom_divergence']; const hasDragon = sigMap['dragon_head']; const hasMainWave = sigMap['main_rising_wave']; const hasRealDragon = sigMap['true_dragon']; const macd = (item.indicators || {}).macd || {}; const dif = macd.dif, dea = macd.dea; if (hasDivergence && hasDragon) return { text: '买入', cls: 'buy' }; if (hasMainWave) return { text: '加仓', cls: 'buy' }; if (hasRealDragon) return { text: '持有', cls: 'hold' }; if (!hasMainWave && dif != null && dea != null && dif < dea) return { text: '卖出', cls: 'sell' }; if (hasDivergence) return { text: '关注', cls: 'watch-active' }; if (hasDragon) return { text: '关注', cls: 'watch-active' }; if (item.triggered_count > 0) return { text: '观察', cls: 'watch' }; return { text: '观望', cls: 'watch' }; }, changeFullScanFilter(filter) { this.fullScanFilter = filter; this.fullScanFilterTypes = []; this.fullScanPage = 1; this.fetchFullScanResults(1); }, changeFullScanFilterRecommend(recommend) { this.fullScanFilterRecommend = recommend; this.fullScanPage = 1; this.fetchFullScanResults(1); }, getRecommendFilterClass(text) { const map = { '买入': 'rec-buy', '加仓': 'rec-buy', '卖出': 'rec-sell', '持有': 'rec-hold', '关注': 'rec-watch', '观察': 'rec-watch', '观望': 'rec-watch' }; return map[text] || 'rec-watch'; }, toggleSignalFilter(type) { const idx = this.fullScanFilterTypes.indexOf(type); if (idx >= 0) { this.fullScanFilterTypes.splice(idx, 1); } else { this.fullScanFilterTypes.push(type); } if (this.fullScanFilterTypes.length > 0) { this.fullScanFilter = 'multi'; } else { this.fullScanFilter = 'triggered'; } this.fullScanPage = 1; this.fetchFullScanResults(1); }, async fetchTodaySignal() { if (!this.todaySignalStock) { this.todaySignalData = null; return; } const stockCode = this.todaySignalStock; const stockItem = this.searchHistory.find(item => item.code === stockCode); const today = new Date().toISOString().split('T')[0]; // 检查缓存 if (this.analysisCache[stockCode]) { this.processSignalData(stockCode, stockItem?.name, this.analysisCache[stockCode]); return; } // 获取最新分析 try { const response = await axios.post('/api/analyze', { stock_code: stockCode, start_date: this.startDate, end_date: today }); if (response.data.success) { this.analysisCache[stockCode] = response.data; this.processSignalData(stockCode, response.data.stock_name, response.data); } else { this.showToast('获取信号失败: ' + (response.data.error || '未知错误'), 'error'); } } catch (err) { this.showToast('获取信号失败: ' + err.message, 'error'); } }, processSignalData(stockCode, stockName, analysisData) { const latest = analysisData.data['最新数据']; if (!latest) { this.todaySignalData = null; return; } const pricePos = latest['价格位置'] || 50; const superRatio = latest['超大单净流入占比'] || 0; const mainRatio = latest['主力净流入占比'] || 0; // 生成信号 const isLow = pricePos <= 50; const isHigh = pricePos > 50; const isSuperInflow = superRatio >= 2; const isSuperOutflow = superRatio <= -2; const isMainInflow = mainRatio >= 2; const isMainOutflow = mainRatio <= -2; let signal = { type: '', icon: '⏸️', title: '观望信号', description: '当前无明确买卖信号,建议继续观察' }; let reason = '无明确信号'; if (isLow && (isSuperInflow || isMainInflow)) { signal = { type: 'buy', icon: '✅', title: '买入信号', description: `价格处于${pricePos <= 30 ? '低位' : '中低位'}(${pricePos.toFixed(1)}%),${isSuperInflow ? '超大单' : '主力'}大额流入,建议买入,仓位30-50%,止损-5%` }; reason = isSuperInflow ? '低位+超大单流入' : '低位+主力流入'; } else if (isHigh && (isSuperOutflow || isMainOutflow)) { signal = { type: 'sell', icon: '🔴', title: '卖出信号', description: `价格处于${pricePos >= 70 ? '高位' : '中高位'}(${pricePos.toFixed(1)}%),${isSuperOutflow ? '超大单' : '主力'}大额流出,建议卖出或减仓` }; reason = isSuperOutflow ? '高位+超大单流出' : '高位+主力流出'; } this.todaySignalData = { stock_code: stockCode, stock_name: stockName, date: latest['日期'], price: latest['收盘价'], pricePosition: pricePos, superRatio: superRatio, mainRatio: mainRatio, signal: signal, reason: reason }; }, quickTrade(tradeType) { if (!this.todaySignalData) return; // 预填充交易表单 this.newTrade.stock_code = this.todaySignalData.stock_code; this.newTrade.stock_name = this.todaySignalData.stock_name; this.newTrade.trade_type = tradeType; this.newTrade.price = this.todaySignalData.price; this.newTrade.trade_date = new Date().toISOString().split('T')[0]; this.newTrade.reason = this.todaySignalData.reason; this.selectedStockFromHistory = this.todaySignalData.stock_code; this.selectedStockSignal = this.todaySignalData.signal; this.showTradeForm = true; }, openTradeFormWithStock() { if (!this.todaySignalData) return; this.newTrade.stock_code = this.todaySignalData.stock_code; this.newTrade.stock_name = this.todaySignalData.stock_name; this.newTrade.trade_date = new Date().toISOString().split('T')[0]; this.newTrade.reason = this.todaySignalData.reason; this.selectedStockFromHistory = this.todaySignalData.stock_code; this.selectedStockSignal = this.todaySignalData.signal; this.showTradeForm = true; }, applyAnalysisToTrade(analysisData) { const latest = analysisData.data['最新数据']; if (!latest) { this.selectedStockSignal = { type: '', icon: '⚠️', title: '无最新数据', description: '无法获取最新分析数据' }; return; } const pricePos = latest['价格位置'] || 50; const superRatio = latest['超大单净流入占比'] || 0; const mainRatio = latest['主力净流入占比'] || 0; // 判断信号 const isLow = pricePos <= 50; const isHigh = pricePos > 50; const isSuperInflow = superRatio >= 2; const isSuperOutflow = superRatio <= -2; const isMainInflow = mainRatio >= 2; const isMainOutflow = mainRatio <= -2; // 生成信号描述 let signal = { type: '', icon: '⏸️', title: '观望信号', description: '当前无明确买卖信号' }; if (isLow && (isSuperInflow || isMainInflow)) { signal = { type: 'buy', icon: '✅', title: '买入信号', description: `价格处于${pricePos <= 30 ? '低位' : '中低位'}(${pricePos.toFixed(1)}%),${isSuperInflow ? '超大单' : '主力'}大额流入` }; this.newTrade.reason = isLow && isSuperInflow ? '低位+超大单流入' : '低位+主力流入'; } else if (isHigh && (isSuperOutflow || isMainOutflow)) { signal = { type: 'sell', icon: '🔴', title: '卖出信号', description: `价格处于${pricePos >= 70 ? '高位' : '中高位'}(${pricePos.toFixed(1)}%),${isSuperOutflow ? '超大单' : '主力'}大额流出` }; this.newTrade.reason = isHigh && isSuperOutflow ? '高位+超大单流出' : '高位+主力流出'; } else { this.newTrade.reason = '无明确信号-谨慎操作'; } this.selectedStockSignal = signal; }, calculateTradeStats() { const completedTrades = this.trades.filter(t => t.result !== 'pending'); const profitTrades = completedTrades.filter(t => t.result === 'profit').length; const lossTrades = completedTrades.filter(t => t.result === 'loss').length; // 用平均成本法计算所有已实现盈亏(从卖出交易中计算) let realizedProfit = 0; const stockGroups = {}; // 按股票分组并按时间排序 this.trades.forEach(trade => { const code = trade.stock_code; if (!stockGroups[code]) { stockGroups[code] = []; } stockGroups[code].push(trade); }); // 计算每只股票的已实现盈亏 Object.values(stockGroups).forEach(trades => { // 按时间排序(从早到晚) trades.sort((a, b) => { if (a.trade_date !== b.trade_date) { return a.trade_date.localeCompare(b.trade_date); } return (a.created_at || '').localeCompare(b.created_at || ''); }); let holdingQty = 0; let holdingCost = 0; trades.forEach(trade => { const qty = parseInt(trade.quantity) || 0; const price = parseFloat(trade.price) || 0; if (trade.trade_type === 'buy') { holdingCost += qty * price; holdingQty += qty; } else if (trade.trade_type === 'sell' && holdingQty > 0) { // 计算平均成本 const avgCost = holdingCost / holdingQty; // 已实现盈亏 = (卖出价 - 平均成本) × 卖出数量 realizedProfit += (price - avgCost) * qty; // 更新持仓 holdingCost -= avgCost * qty; holdingQty -= qty; } }); }); // 计算总市值和总成本(基于现价,仅计算持仓股票) let totalMarketValue = 0; let totalCost = 0; Object.keys(this.holdingPositions).forEach(code => { const pos = this.holdingPositions[code]; if (pos.quantity > 0) { totalMarketValue += pos.quantity * (pos.currentPrice || 0); totalCost += pos.totalCost; } }); // 浮动盈亏 = 总市值 - 总成本 const unrealizedProfit = totalMarketValue - totalCost; // 总盈亏 = 已实现盈亏 + 浮动盈亏 const totalProfit = realizedProfit + unrealizedProfit; this.tradeStats = { total_trades: this.trades.length, completed_trades: completedTrades.length, profit_trades: profitTrades, loss_trades: lossTrades, win_rate: completedTrades.length > 0 ? (profitTrades / completedTrades.length * 100) : 0, total_profit: totalProfit, total_market_value: totalMarketValue, total_cost: totalCost, unrealized_profit: unrealizedProfit, realized_profit: realizedProfit // 新增:已实现盈亏 }; }, // ========== 模拟交易相关方法 ========== async loadSimData() { // 加载模拟交易数据(含智能引擎数据) await Promise.all([ this.loadSimStats(), this.loadSimPositions(), this.loadSimTrades(), this.loadSmartAlgoConfig(), this.loadSmartTemplates(), this.loadSmartSignals(), this.loadSmartPositionMeta(), ]); }, async loadSimStats() { try { const response = await axios.get('/api/sim/stats'); if (response.data.success) { this.simStats = response.data.stats; } } catch (e) { console.error('加载模拟交易统计失败', e); } }, async loadSimPositions() { try { const response = await axios.get('/api/sim/positions'); if (response.data.success) { this.simPositions = response.data.positions; } } catch (e) { console.error('加载模拟持仓失败', e); } }, async loadSimTrades() { try { const response = await axios.get('/api/sim/trades'); if (response.data.success) { this.simTrades = response.data.trades; } } catch (e) { console.error('加载模拟交易记录失败', e); } }, // ========== 智能交易引擎方法 ========== async loadSmartAlgoConfig() { try { const resp = await axios.get('/api/smart/config'); if (resp.data.success) { this.smartAlgoConfig = resp.data.config; this.smartAlgoIsDefault = resp.data.is_default; } } catch (e) { console.error('加载算法配置失败', e); } }, async loadSmartTemplates() { try { const resp = await axios.get('/api/smart/templates'); if (resp.data.success) { this.smartTemplates = resp.data.templates; } } catch (e) { console.error('加载算法模板失败', e); } }, async loadSmartSignals() { try { const resp = await axios.get('/api/smart/signals?limit=30&days=7'); if (resp.data.success) { this.smartSignals = resp.data.signals; } } catch (e) { console.error('加载信号日志失败', e); } }, async loadSmartPositionMeta() { try { const resp = await axios.get('/api/smart/position_meta'); if (resp.data.success) { this.smartPositionMeta = resp.data.positions; } } catch (e) { console.error('加载持仓元数据失败', e); } }, async applyAlgoTemplate(templateName) { try { const resp = await axios.post('/api/smart/apply_template', { template_name: templateName }); if (resp.data.success) { this.showToast(resp.data.message, 'success'); await this.loadSmartAlgoConfig(); this.smartConfigEditing = false; } else { this.showToast(resp.data.error, 'error'); } } catch (e) { this.showToast('应用模板失败: ' + (e.response?.data?.error || e.message), 'error'); } }, async saveSmartConfig() { if (!this.smartAlgoConfig) return; try { const resp = await axios.post('/api/smart/config', this.smartAlgoConfig); if (resp.data.success) { this.showToast('算法配置已保存', 'success'); this.smartConfigEditing = false; this.smartAlgoIsDefault = false; } else { this.showToast(resp.data.error, 'error'); } } catch (e) { this.showToast('保存失败: ' + (e.response?.data?.error || e.message), 'error'); } }, getPositionMeta(stockCode) { return this.smartPositionMeta.find(m => m.stock_code === stockCode) || null; }, getActiveRules(stockCode) { const meta = this.getPositionMeta(stockCode); if (!meta) return []; const rules = []; if (meta.breakeven_active) rules.push('🛡️保本'); if (meta.momentum_trailing_active) rules.push('📈跟踪'); if (meta.partial_exit_done) rules.push('✂️已减仓'); return rules; }, async executeAutoTrade() { // 使用智能引擎执行交易 this.simAutoTrading = true; try { this.showToast('正在执行智能交易引擎...', 'info'); // 优先使用智能引擎(30秒超时) const response = await axios.post('/api/smart/trigger', {}, { timeout: 30000 }); if (response.data.success) { const results = response.data.results || []; const buyCount = results.filter(r => r.type === 'buy').length; const sellCount = results.filter(r => ['sell', 'partial_sell'].includes(r.type)).length; const algo = response.data.algo || '?'; const fees = response.data.total_fees || 0; const detailReasons = response.data.detail_reasons || []; const availableCash = response.data.available_cash || 0; if (buyCount === 0 && sellCount === 0) { // 无交易 — 用模态框显示详细原因 this.smartResultData = { algo: algo, type: 'no_trade', title: '智能引擎执行完成', reasons: detailReasons.length > 0 ? detailReasons : ['当前没有符合条件的交易信号'], results: [], fees: 0, availableCash: availableCash, summary: '无交易操作' }; this.smartResultVisible = true; } else { // 有交易 — 用模态框显示交易结果 + 原因 this.smartResultData = { algo: algo, type: 'traded', title: '智能引擎执行完成', reasons: detailReasons, results: results, fees: fees, availableCash: availableCash, summary: `买入${buyCount}笔, 卖出${sellCount}笔` }; this.smartResultVisible = true; } // 刷新数据 await this.loadSimData(); } else { this.showToast(response.data.error || '智能交易失败', 'error'); } } catch (e) { console.error('智能交易失败', e); this.showToast('智能交易失败: ' + (e.response?.data?.error || e.message), 'error'); } finally { this.simAutoTrading = false; } }, // 智能执行结果模态框 — 原因分类 getReasonClass(reason) { if (reason.includes('T+1') || reason.includes('限制')) return 'reason-limit'; if (reason.includes('涨停') || reason.includes('跌停')) return 'reason-limit'; if (reason.includes('可用现金') || reason.includes('资金')) return 'reason-cash'; if (reason.includes('卖出') || reason.includes('止盈') || reason.includes('止损')) return 'reason-sell'; if (reason.includes('买入')) return 'reason-buy'; return 'reason-info'; }, getReasonIcon(reason) { if (reason.includes('T+1')) return '🔒'; if (reason.includes('涨停') || reason.includes('跌停')) return '🚫'; if (reason.includes('可用现金') || reason.includes('资金')) return '💰'; if (reason.includes('卖出') || reason.includes('止盈')) return '📤'; if (reason.includes('止损')) return '🛡️'; if (reason.includes('买入')) return '📥'; return 'ℹ️'; }, confirmResetSim() { this.showConfirm('确定要重置模拟交易吗?所有交易记录和持仓将被清空。', async () => { try { const response = await axios.post('/api/sim/reset'); if (response.data.success) { this.showToast('模拟交易已重置', 'success'); this.simStats = {}; this.simPositions = []; this.simTrades = []; } else { this.showToast(response.data.error || '重置失败', 'error'); } } catch (e) { this.showToast('重置失败', 'error'); } }); }, async updateSimPrices() { if (this.simPositions.length === 0) return; this.simPriceRefreshing = true; const prices = {}; try { await Promise.all(this.simPositions.map(async (pos) => { try { const resp = await axios.get(`/api/realtime_price/${pos.stock_code}`, { timeout: 12000 }); if (resp.data.success && (resp.data.price ?? resp.data.data?.price) != null) { const p = Number(resp.data.price ?? resp.data.data?.price); prices[pos.stock_code] = p; pos.current_price = p; } } catch (e) { console.error(`获取${pos.stock_code}价格失败`, e); } })); if (Object.keys(prices).length > 0) { await axios.post('/api/sim/update_prices', { prices }); await this.loadSimStats(); } } finally { this.simPriceRefreshing = false; } } } }).mount('#app');