// ==UserScript==
// @name AGSV股票持仓收益分析
// @namespace http://tampermonkey.net/
// @version 0.1.3
// @license MIT License
// @description AGSV股市辅助收益计算
// @author PandaChan
// @match https://stock.agsvpt.cn
// @icon https://stock.agsvpt.cn/plugins/stock/favicon.svg
// @grant GM_xmlhttpRequest
// ==/UserScript==
(async function () {
'use strict';
let token = localStorage.getItem('auth_token');
// console.log('TOKEN:', token);
const getCurrentPrice = async function () {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET', // 或 'POST', 'PUT', 'DELETE' 等
url: 'https://stock.agsvpt.cn/api/stocks/info', // 请求的URL
headers: { // 可选:自定义请求头
'authorization': 'Bearer ' + token
},
responseType: 'json', // 可选:指定响应类型,如 'json', 'text', 'arraybuffer', 'blob'
timeout: 5000, // 可选:请求超时时间(毫秒)
onload: function (response) { // 请求成功时的回调函数
// console.log('请求成功:', response);
// console.log("当前价格:", JSON.stringify(response.response))
resolve(response.response)
},
onerror: function (response) { // 请求失败时的回调函数
console.error('请求失败:', response);
reject(response)
},
ontimeout: function (response) { // 请求超时时的回调函数
console.warn('请求超时:', response);
reject(response)
}
});
})
};
const getHistoryData = async function () {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET', // 或 'POST', 'PUT', 'DELETE' 等
url: 'https://stock.agsvpt.cn/api/user/history?&page=1&page_size=10000', // 请求的URL
headers: { // 可选:自定义请求头
'authorization': 'Bearer ' + token
},
responseType: 'json', // 可选:指定响应类型,如 'json', 'text', 'arraybuffer', 'blob'
timeout: 5000, // 可选:请求超时时间(毫秒)
onload: function (response) { // 请求成功时的回调函数
// console.log('请求成功:', response);
// console.log("历史数据:", JSON.stringify(response.response.data))
resolve(response.response.data)
},
onerror: function (response) { // 请求失败时的回调函数
console.error('请求失败:', response);
reject(response)
},
ontimeout: function (response) { // 请求超时时的回调函数
console.warn('请求超时:', response);
reject(response)
}
});
})
};
function calculatePortfolioPerformanceWithWeightedAverage(transactions, realTimePrices) {
const holdings = {};
transactions.reverse().forEach(transaction => {
const {stock_code, quantity, price, fee, type} = transaction;
if (!holdings[stock_code]) {
holdings[stock_code] = {
name: transaction.name,
current_quantity: 0, // 当前持有的数量
current_total_cost: 0.0 // 当前持有的股票的总成本
};
}
const stockHolding = holdings[stock_code];
switch (type) {
case 'BUY':
stockHolding.current_quantity += quantity;
stockHolding.current_total_cost += (price * quantity) + fee;
break;
case 'SELL':
if (stockHolding.current_quantity > 0) {
const averageCostPerShare = stockHolding.current_total_cost / stockHolding.current_quantity;
const costOfSoldShares = averageCostPerShare * quantity;
stockHolding.current_quantity -= quantity;
stockHolding.current_total_cost -= costOfSoldShares;
// 防止因浮点数运算或极端情况导致数量/成本为负数
if (stockHolding.current_quantity < 0) stockHolding.current_quantity = 0;
if (stockHolding.current_total_cost < 0) stockHolding.current_total_cost = 0.0;
} else {
// 如果在没有持仓的情况下卖出,只减少数量(不影响成本)
stockHolding.current_quantity -= quantity;
}
break;
// 'BORROW' 和 'REPAY' 类型在此函数中继续被忽略
default:
break;
}
});
const pricesMap = new Map();
realTimePrices.forEach(item => {
pricesMap.set(item.code, item.price);
});
const portfolioSummary = {};
for (const stock_code in holdings) {
const stockHolding = holdings[stock_code];
const {name, current_quantity, current_total_cost} = stockHolding;
let total_holding_cost = 0.0;
let estimated_profit_loss = 0.0;
let cost_per_share = 0.0; // 新增:持仓每股成本价格
const current_price = pricesMap.get(stock_code);
if (current_quantity > 0) {
total_holding_cost = current_total_cost;
cost_per_share = total_holding_cost / current_quantity; // 计算每股成本
if (current_price !== undefined) {
const current_market_value = current_price * current_quantity;
estimated_profit_loss = current_market_value - total_holding_cost;
} else {
console.warn(`股票 ${name} (${stock_code}) 没有实时价格数据,无法计算预计收益。`);
estimated_profit_loss = null;
}
} else {
// 如果没有正向持仓(数量为0或负数),所有相关值都为0
stockHolding.current_quantity = 0;
total_holding_cost = 0.0;
estimated_profit_loss = 0.0;
cost_per_share = 0.0; // 没有持仓,每股成本为0
}
portfolioSummary[stock_code] = {
name: name,
current_holding_quantity: stockHolding.current_quantity,
total_holding_cost: parseFloat(total_holding_cost.toFixed(2)),
cost_per_share: parseFloat(cost_per_share.toFixed(2)), // 新增:持仓每股成本价格
estimated_profit_loss: estimated_profit_loss !== null ? parseFloat(estimated_profit_loss.toFixed(2)) : 'N/A'
};
}
return portfolioSummary;
}
const currentProce = await getCurrentPrice();
// console.log("currentProce:", currentProce)
const historyData = await getHistoryData();
// console.log("historyData:", historyData)
const calculatedHoldings = calculatePortfolioPerformanceWithWeightedAverage(historyData, currentProce);
console.log("分析结果:", calculatedHoldings);
const appendExpandInfoToTable = function (calculatedHoldings) {
const positionTable = document.querySelector('div.positions-container').querySelector('table');
const stockNameToCodeMap = {};
for (const code in calculatedHoldings) {
if (calculatedHoldings.hasOwnProperty(code)) {
stockNameToCodeMap[calculatedHoldings[code].name] = code;
}
}
const headerRow = positionTable.querySelector('tbody tr');
if (!headerRow || headerRow.children.length === 0) {
console.warn('未找到表格的表头行。');
return;
}
// 检查是否已经添加过列,防止重复添加
if (headerRow.querySelector('.added-profit-loss-header')) { // 只需要检查一列即可
console.log('持仓成本、均价和预计收益列已存在,跳过添加。');
return;
}
// 增加新的表头列:持仓成本
const thHoldingCost = document.createElement('th');
thHoldingCost.textContent = '持仓成本';
thHoldingCost.classList.add('added-holding-cost-header');
headerRow.appendChild(thHoldingCost);
// 增加新的表头列:持仓均价
const thCostPerShare = document.createElement('th');
thCostPerShare.textContent = '持仓均价';
thCostPerShare.classList.add('added-cost-per-share-header');
headerRow.appendChild(thCostPerShare);
// 增加新的表头列:预计收益
const thEstimatedProfitLoss = document.createElement('th');
thEstimatedProfitLoss.textContent = '预计收益';
thEstimatedProfitLoss.classList.add('added-profit-loss-header');
headerRow.appendChild(thEstimatedProfitLoss);
// 获取数据行所在的 tbody (第二个 tbody)
const dataTbody = positionTable.querySelectorAll('tbody')[1];
if (!dataTbody) {
console.warn('未找到包含数据行的tbody元素。');
return;
}
const processTableFun = function () {
// 遍历数据行并添加相应的数据
dataTbody.querySelectorAll('tr').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length > 0) {
const stockName = cells[0].textContent.trim();
const stockCode = stockNameToCodeMap[stockName];
if (stockCode && calculatedHoldings[stockCode]) {
const stockData = calculatedHoldings[stockCode];
// 持仓成本TD
const tdHoldingCost = document.createElement('td');
tdHoldingCost.textContent = stockData.total_holding_cost.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
row.appendChild(tdHoldingCost);
// 持仓均价TD
const tdCostPerShare = document.createElement('td');
if (stockData.cost_per_share === 0) {
tdCostPerShare.textContent = '0.00';
} else {
tdCostPerShare.textContent = stockData.cost_per_share.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
}
row.appendChild(tdCostPerShare);
// 预计收益TD
const tdEstimatedProfitLoss = document.createElement('td');
if (stockData.estimated_profit_loss === 'N/A') {
tdEstimatedProfitLoss.textContent = 'N/A';
} else {
tdEstimatedProfitLoss.textContent = stockData.estimated_profit_loss.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
// 可以根据盈亏为单元格添加样式
if (stockData.estimated_profit_loss > 0) {
tdEstimatedProfitLoss.style.color = 'green';
} else if (stockData.estimated_profit_loss < 0) {
tdEstimatedProfitLoss.style.color = 'red';
}
}
row.appendChild(tdEstimatedProfitLoss);
} else {
console.warn(`未找到股票 ${stockName} 的计算数据,或该股票无持仓/交易记录。`);
// 如果没有找到数据或无持仓,添加空或 '--' TD
for (let i = 0; i < 3; i++) { // 3列: 持仓成本, 持仓均价, 预计收益
const tdEmpty = document.createElement('td');
tdEmpty.textContent = '--';
row.appendChild(tdEmpty);
}
}
}
});
};
const waitTableRawDataProcessFinish = function () {
const tableBody = dataTbody.innerHTML;
if (tableBody.length == 0) {
// console.log("表格原始数据尚未渲染, 延迟等待...");
setTimeout(waitTableRawDataProcessFinish, 200);
} else {
processTableFun();
}
};
waitTableRawDataProcessFinish();
}
appendExpandInfoToTable(calculatedHoldings);
console.log("表格处理完成");
// Your code here...
})();