// ==UserScript== // @name AGSV股票持仓收益及借入收益分析 // @namespace http://tampermonkey.net/ // @version 0.3.1 // @license MIT License // @description AGSV股市辅助收益计算,结构优化,逻辑更健壮,包含借入收益分析。 // @author PandaChan & AGSV骄阳 // @match https://stock.agsvpt.cn/ // @icon https://stock.agsvpt.cn/plugins/stock/favicon.svg // @grant GM_xmlhttpRequest // @downloadURL https://update.greasyfork.icu/scripts/542780/AGSV%E8%82%A1%E7%A5%A8%E6%8C%81%E4%BB%93%E6%94%B6%E7%9B%8A%E5%8F%8A%E5%80%9F%E5%85%A5%E6%94%B6%E7%9B%8A%E5%88%86%E6%9E%90.user.js // @updateURL https://update.greasyfork.icu/scripts/542780/AGSV%E8%82%A1%E7%A5%A8%E6%8C%81%E4%BB%93%E6%94%B6%E7%9B%8A%E5%8F%8A%E5%80%9F%E5%85%A5%E6%94%B6%E7%9B%8A%E5%88%86%E6%9E%90.meta.js // ==/UserScript== (async function () { 'use strict'; // --- 配置常量 --- const API_BASE_URL = 'https://stock.agsvpt.cn/api'; const CONFIG = { API_INFO_URL: `${API_BASE_URL}/stocks/info`, API_HISTORY_URL: `${API_BASE_URL}/user/history?&page=1&page_size=10000`, TARGET_TABLE_DIV: 'div.positions-container', TARGET_TABLE_SELECTOR: 'div.positions-container table', TOKEN_KEY: 'auth_token', HEADERS: { 'Content-Type': 'application/json', }, }; // 获取身份验证的 token const token = localStorage.getItem(CONFIG.TOKEN_KEY); if (!token) { console.warn('未找到认证Token,脚本无法运行。'); return; } // --- 1. API 请求模块 --- function fetchApiData(url, options) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url, responseType: 'json', timeout: 8000, ...options, onload: response => { if (response.status >= 200 && response.status < 300) { resolve(response.response); } else { reject(new Error(`HTTP Error: ${response.status} ${response.statusText}`)); } }, onerror: response => reject(new Error('请求失败: ' + response.statusText)), ontimeout: () => reject(new Error('请求超时')), }); }); } // 获取当前价格的函数 const getCurrentPrice = async function () { return fetchApiData(CONFIG.API_INFO_URL, { headers: { 'authorization': 'Bearer ' + token } }); }; // --- 2. 数据计算模块 --- function calculatePortfolioPerformance(transactions, realTimePrices) { const holdings = {}; transactions.slice().reverse().forEach(transaction => { const { stock_code, quantity, price, fee, type, name } = transaction; if (!holdings[stock_code]) { holdings[stock_code] = { name, quantity: 0, totalCost: 0.0 }; } const stock = holdings[stock_code]; if (type === 'BUY') { stock.quantity += quantity; stock.totalCost += (price * quantity) + fee; } else if (type === 'SELL') { if (stock.quantity > 0) { const avgCost = stock.totalCost / stock.quantity; stock.totalCost -= avgCost * quantity; stock.quantity -= quantity; } else { stock.quantity -= quantity; } if (stock.quantity <= 0) { stock.quantity = 0; stock.totalCost = 0; } } }); const pricesMap = new Map(realTimePrices.map(item => [item.code, item.price])); const portfolioSummary = {}; for (const code in holdings) { const stock = holdings[code]; const { name, quantity, totalCost } = stock; const currentPrice = pricesMap.get(code); if (quantity <= 0) continue; const costPerShare = totalCost / quantity; let profitLoss = 'N/A', returnRate = 'N/A'; if (currentPrice !== undefined) { const marketValue = currentPrice * quantity; const calculatedProfitLoss = marketValue - totalCost; profitLoss = parseFloat(calculatedProfitLoss.toFixed(2)); returnRate = totalCost !== 0 ? parseFloat(((calculatedProfitLoss / totalCost) * 100).toFixed(2)) : 0; } portfolioSummary[code] = { name, quantity, totalHoldingCost: parseFloat(totalCost.toFixed(2)), costPerShare: parseFloat(costPerShare.toFixed(2)), estimatedProfitLoss: profitLoss, estimatedReturnRate: returnRate, }; } return portfolioSummary; } // --- 3. DOM 操作模块 --- function createCell(content, color = null) { const cell = document.createElement('td'); cell.textContent = content; if (color) { cell.style.color = color; } return cell; } function injectDataIntoTable(calculatedHoldings) { const table = document.querySelector(CONFIG.TARGET_TABLE_SELECTOR); if (!table || table.dataset.enhanced) return; table.dataset.enhanced = 'true'; const headerRow = table.querySelector('thead tr, tbody tr'); const dataBody = table.querySelectorAll('tbody')[1] || table.querySelector('tbody'); if (!headerRow || !dataBody) { console.warn('未找到表格的表头或数据体。'); return; } const headers = ['持仓均价', '持仓成本', '预计收益', '预计收益率']; headers.forEach(text => { const th = document.createElement('th'); th.textContent = text; headerRow.appendChild(th); }); const stockNameToCodeMap = Object.fromEntries( Object.entries(calculatedHoldings).map(([code, data]) => [data.name, code]) ); dataBody.querySelectorAll('tr').forEach(row => { const stockName = row.cells[0]?.textContent.trim(); if (!stockName) return; const stockCode = stockNameToCodeMap[stockName]; const stockData = calculatedHoldings[stockCode]; if (stockData) { const { costPerShare, totalHoldingCost, estimatedProfitLoss, estimatedReturnRate } = stockData; const profitColor = estimatedProfitLoss > 0 ? 'green' : (estimatedProfitLoss < 0 ? 'red' : null); row.appendChild(createCell(costPerShare.toFixed(2))); row.appendChild(createCell(totalHoldingCost.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }))); row.appendChild(createCell(estimatedProfitLoss === 'N/A' ? 'N/A' : estimatedProfitLoss.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }), profitColor)); row.appendChild(createCell(estimatedReturnRate === 'N/A' ? '--' : `${estimatedReturnRate.toFixed(2)}%`, profitColor)); } else { for (let i = 0; i < 4; i++) { row.appendChild(createCell('--')); } } }); console.log('持仓收益分析数据已成功注入表格。'); } // --- 4. 借入收益分析模块 --- const insertEstimatedProfitColumns = function (table) { const headerRow = table.querySelector('thead tr'); const thCostBasis = document.createElement('th'); thCostBasis.textContent = '持仓成本'; headerRow.appendChild(thCostBasis); const thCurrentMarketValue = document.createElement('th'); thCurrentMarketValue.textContent = '当前市值'; headerRow.appendChild(thCurrentMarketValue); const thEstimatedProfit = document.createElement('th'); thEstimatedProfit.textContent = '预计收益'; headerRow.appendChild(thEstimatedProfit); const thEstimatedProfitRate = document.createElement('th'); thEstimatedProfitRate.textContent = '预计收益率 (%)'; headerRow.appendChild(thEstimatedProfitRate); }; const appendEstimatedProfitColumnsToTable = async function () { const currentPrices = await getCurrentPrice(); const pricesMap = new Map(); currentPrices.forEach(item => { pricesMap.set(item.name, item.price); }); const dataTbody = document.querySelector("#root > div > div.positions-container > div > div:nth-child(2) > table > tbody"); if (!dataTbody) { console.warn('未找到包含数据行的tbody元素。'); return; } const table = dataTbody.closest('table'); insertEstimatedProfitColumns(table); dataTbody.querySelectorAll('tr').forEach(row => { const cells = row.querySelectorAll('td'); if (cells.length > 0) { const stockName = cells[1].textContent.trim(); const borrowedQuantity = parseFloat(cells[2].textContent.trim()); const unitPrice = parseFloat(cells[3].textContent.trim()); const unpaidInterest = parseFloat(cells[5].textContent.trim()); const currentPrice = pricesMap.get(stockName); console.log(`股票名称: ${stockName}, 借入数量: ${borrowedQuantity}, 单位价值: ${unitPrice}, 当前价格: ${currentPrice}`); const tdCostBasis = document.createElement('td'); const tdCurrentMarketValue = document.createElement('td'); const tdEstimatedProfit = document.createElement('td'); const tdEstimatedProfitRate = document.createElement('td'); const costBasis = (unitPrice * borrowedQuantity) + unpaidInterest; tdCostBasis.textContent = costBasis.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); if (!isNaN(borrowedQuantity) && !isNaN(unitPrice) && currentPrice !== undefined) { const currentMarketValue = borrowedQuantity * currentPrice; tdCurrentMarketValue.textContent = currentMarketValue.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const estimatedProfit = costBasis - currentMarketValue; tdEstimatedProfit.textContent = estimatedProfit.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const estimatedProfitRate = costBasis > 0 ? (estimatedProfit / costBasis) * 100 : 0; tdEstimatedProfitRate.textContent = estimatedProfitRate.toFixed(2) + '%'; if (estimatedProfit < 0) { tdEstimatedProfit.style.color = 'red'; tdEstimatedProfitRate.style.color = 'red'; } else { tdEstimatedProfit.style.color = 'green'; tdEstimatedProfitRate.style.color = 'green'; } } else { tdCurrentMarketValue.textContent = 'N/A'; tdEstimatedProfit.textContent = 'N/A'; tdEstimatedProfitRate.textContent = 'N/A'; } row.appendChild(tdCostBasis); row.appendChild(tdCurrentMarketValue); row.appendChild(tdEstimatedProfit); row.appendChild(tdEstimatedProfitRate); } }); }; // --- 5. 主执行函数 --- async function main() { let prices; let history; const authHeader = { 'authorization': `Bearer ${token}` }; let dataLoadFinish = false; let tableDomReady = false; const processExpandInfo = function () { if (!dataLoadFinish || !tableDomReady) { return; } const calculatedHoldings = calculatePortfolioPerformance(history.data, prices); console.log("分析结果", calculatedHoldings); injectDataIntoTable(calculatedHoldings); appendEstimatedProfitColumnsToTable(); }; Promise.all([ fetchApiData(CONFIG.API_INFO_URL, { headers: authHeader }), fetchApiData(CONFIG.API_HISTORY_URL, { headers: authHeader }) ]).then(results => { prices = results[0]; history = results[1]; dataLoadFinish = true; processExpandInfo(); }).catch(error => { console.error('数据请求错误:', error); }); const observer = new MutationObserver(async (mutations, obs) => { const tableDom = document.querySelector(CONFIG.TARGET_TABLE_SELECTOR); const td = tableDom.querySelector('td'); if (td) { obs.disconnect(); tableDomReady = true; processExpandInfo(); } }); observer.observe(document, { childList: true, subtree: true }); } // --- 启动脚本 --- main().catch(error => { console.error('启动时发生严重错误:', error); }); })();