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');