Files
stock/stock-html/static/js/app.js
T

2968 lines
148 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
fullScanPerPage: 5, // 每页显示5只股票
fullScanFilter: 'all',
fullScanFilterTypes: [],
fullScanFilterRecommend: '买入', // 扫描结果打开时缺省显示买入列表;all|买入|加仓|卖出|持有|关注|观察|观望
scanSummaryCollapsed: false,
scanResultsCollapsed: 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, '<h4 class="ai-section-title">$1</h4>')
// 处理三级标题
.replace(/^### (.+)$/gm, '<h5 class="ai-subsection-title">$1</h5>')
// 处理加粗
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
// 处理列表项
.replace(/^- (.+)$/gm, '<li>$1</li>')
// 处理换行
.replace(/\n/g, '<br>')
// 清理多余的br
.replace(/<br><h/g, '<h')
.replace(/<\/h4><br>/g, '</h4>')
.replace(/<\/h5><br>/g, '</h5>');
},
// 根据股票代码获取名称
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 || '请求失败';
this.showToast(this.error, 'error');
} 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);
this.showToast('加载关注列表失败', 'error');
}
},
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);
this.showToast('移除关注失败', 'error');
}
},
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.showToast('信号分析失败,请稍后重试', 'error');
}
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.showToast('移除关注失败', 'error');
}
// 从提醒列表中移除
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);
this.showToast('加载可用资金失败', 'error');
}
},
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);
this.showToast('刷新持仓价格失败,请稍后重试', 'error');
} 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);
this.showToast('获取基本面数据失败', '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);
this.showToast('获取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,
final_score: scanItem.final_score,
technical_score: scanItem.technical_score,
external_score: scanItem.external_score,
verdict: scanItem.verdict,
score_factors: null,
score_summary: '',
score_detail_loading: true,
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 || '获取失败';
}
}
// 异步请求评分详情(外部因素),传入列表技术得分确保一致
try {
const techScore = scanItem.final_score ? Math.round(scanItem.final_score - (scanItem.external_score || 0)) : 50;
const scoreResp = await axios.get(`/api/stock_score_detail/${code}?tech_score=${techScore}`);
if (scoreResp.data.success && this.techSignalResult && this.techSignalResult.stock_code === code) {
this.techSignalResult.score_factors = scoreResp.data.factors || null;
this.techSignalResult.score_summary = scoreResp.data.summary || '';
this.techSignalResult.final_score = scoreResp.data.final_score;
this.techSignalResult.technical_score = scoreResp.data.technical_score;
this.techSignalResult.external_score = scoreResp.data.external_score;
this.techSignalResult.verdict = scoreResp.data.verdict;
}
} catch (e) {
console.debug('评分详情获取失败:', e);
}
if (this.techSignalResult && this.techSignalResult.stock_code === code) {
this.techSignalResult.score_detail_loading = false;
}
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: this.fullScanPerPage,
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);
this.showToast('加载模拟交易统计失败', 'error');
}
},
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);
this.showToast('加载模拟持仓失败', 'error');
}
},
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);
this.showToast('加载交易记录失败', 'error');
}
},
// ========== 智能交易引擎方法 ==========
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);
this.showToast('加载算法配置失败', 'error');
}
},
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);
this.showToast('加载算法模板失败', 'error');
}
},
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);
this.showToast('加载信号日志失败', 'error');
}
},
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);
this.showToast('加载持仓元数据失败', 'error');
}
},
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');