Greasy Fork

Greasy Fork is available in English.

AGSV股票持仓收益分析

AGSV股市辅助收益计算

当前为 2025-07-17 提交的版本,查看 最新版本

// ==UserScript==
// @name         AGSV股票持仓收益分析
// @namespace    http://tampermonkey.net/
// @version      0.1.1
// @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...
})();