Steam 市场价格均线

在Steam市场的历史成交价格上显示任意日期内的均线。

// ==UserScript==
// @name         Steam 市场价格均线
// @namespace    http://tampermonkey.net/
// @version      2024-03-29
// @description  在Steam市场的历史成交价格上显示任意日期内的均线。
// @author       Cliencer Goge
// @match        https://steamcommunity.com/market/listings/*/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=steamcommunity.com
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @license      GPLv3
// ==/UserScript==
(function() {
    'use strict';
    Hook_pricehistory_zoomLifetime()
    Hook_pricehistory_zoomDays()
    var settingdialog=initsettingdialog()
    var saves = readstorage()
    const url = window.location.href;
    const parts = new URL(url).pathname.split('/');
    const appId = parts[3];
    const itemName = parts[4];
    var lineoption={lines:[],series:[],seriesColors:[]}
    var strFormatPrefix = "¥"
    var strFormatSuffix = ""
    var mainbutton = createButton()
    var prices = g_plotPriceHistory.data[0]
    myCreatePriceHistoryGraph( 5 )
    function generatelineoption(){
        lineoption ={lines:[prices],series:[{lineWidth:3, markerOptions:{show: false, style:'circle'}}],seriesColors:["#688F3E"]}
        for(var line of saves.linelist){
            var lineav = calculateAverage(prices, line.days)
            lineoption.lines.push(lineav)
            lineoption.series.push({lineWidth:1, markerOptions:{show: false, style:'circle'},highlighter:{formatString: '<h5>'+line.days +'日线</h5><strong>%s</strong><br>'+strFormatPrefix +'%0.2f'+strFormatSuffix+'<br>日均售出 %d 件'}})
            lineoption.seriesColors.push(line.color)
        }
    }
    function Hook_pricehistory_zoomDays(){
        if (typeof pricehistory_zoomDays === 'function') {
            console.log("Hook pricehistory_zoomDays成功");
            const originalPricehistoryZoomDays = pricehistory_zoomDays;
            pricehistory_zoomDays = function(arg1, arg2, arg3, arg4) {
                console.log('pricehistory_zoomDays:', arg1, arg2, arg3, arg4);
                return originalPricehistoryZoomDays.apply(this, [arg1, arg2, arg3, arg4]);
            };
        } else {
            console.log("Hook失败,重试");
            setTimeout(Hook_pricehistory_zoomDays,1000)
        }
    }
    function Hook_pricehistory_zoomLifetime(){
        if (typeof pricehistory_zoomLifetime === 'function') {
            console.log("Hook pricehistory_zoomLifetime成功");
            const originalPricehistoryZoomLifetime = pricehistory_zoomLifetime;
            pricehistory_zoomLifetime = function(arg1, arg2, arg3) {
                console.log('pricehistory_zoomLifetime:', arg1, arg2, arg3);
                return originalPricehistoryZoomLifetime.apply(this, [arg1, arg2, arg3]);
            };
        } else {
            console.log("Hook失败,重试");
            setTimeout(Hook_pricehistory_zoomLifetime,1000)
        }
    }
    function myCreatePriceHistoryGraph(numYAxisTicks){
        generatelineoption()
        g_plotPriceHistory.destroy()
        g_plotPriceHistory = null
        var plot = $J.jqplot('pricehistory', lineoption.lines, {
            title:{text: '售价中位数', textAlign: 'left' },
            gridPadding:{left: 45, right:45, top:25},
            axesDefaults:{ showTickMarks:true },
            axes:{
                xaxis:{
                    renderer:$J.jqplot.DateAxisRenderer,
                    tickOptions:{formatString:'%b %#d<span class="priceHistoryTime"> %#I%p<span>'},
                    pad: 1
                },
                yaxis: {
                    pad: 1.1,
                    tickOptions:{formatString:strFormatPrefix + '%0.2f' + strFormatSuffix, labelPosition:'start', showMark: false},
                    numberTicks: numYAxisTicks
                }
            },
            grid: {
                gridLineColor: '#1b2939',
                borderColor: '#1b2939',
                background: '#101822'
            },
            cursor: {
                show: true,
                zoom: true,
                showTooltip: false
            },
            highlighter: {
                show: true,
                lineWidthAdjust: 2.5,
                sizeAdjust: 5,
                showTooltip: true,
                tooltipLocation: 'n',
                tooltipOffset: 20,
                fadeTooltip: true,
                yvalues: 2,
                formatString: '<strong>%s</strong><br>%s<br>已售出 %d 件'
            },
                series:lineoption.series,
                seriesColors: lineoption.seriesColors
            });
            plot.defaultNumberTicks = numYAxisTicks;
            g_plotPriceHistory = plot
            pricehistory_zoomDays( g_plotPriceHistory, g_timePriceHistoryEarliest, g_timePriceHistoryLatest, 7 )
        return plot;
    }
    function calculateAverage(prices, days) {
        const result = [];
        const oneHour = 60 * 60 * 1000;
        const hoursInDay = 24;
        const totalHours = days * hoursInDay;
        prices.forEach((item, index) => {
            const [dateStr, price, quantityStr] = item;
            const date = new Date(dateStr.substring(0, 11) + " " + dateStr.substring(12, 14) + ":00:00");
            const priceNumber = parseFloat(price);
            const quantity = parseInt(quantityStr, 10);
            let totalWeightedPrice = priceNumber * quantity;
            let totalQuantity = quantity;
            let earliestDate = new Date(date.getTime() - totalHours * oneHour);
            let actualHours = 1;
            for (let j = index - 1; j >= 0; j--) {
                const [prevDateStr, prevPrice, prevQuantityStr] = prices[j];
                const prevDate = new Date(prevDateStr.substring(0, 11) + " " + prevDateStr.substring(12, 14) + ":00:00");
                if (prevDate >= earliestDate) {
                    const prevPriceNumber = parseFloat(prevPrice);
                    const prevQuantity = parseInt(prevQuantityStr, 10);
                    totalWeightedPrice += prevPriceNumber * prevQuantity;
                    totalQuantity += prevQuantity;
                    const diffHours = Math.abs((date - prevDate) / oneHour);
                    actualHours += diffHours;
                } else {
                    break;
                }
            }
            const averagePrice = totalWeightedPrice / totalQuantity;
            const averageDailyQuantity = totalQuantity / (actualHours / hoursInDay);
            result.push([dateStr, averagePrice.toFixed(3), averageDailyQuantity.toFixed(3)]);
        });
        return result;
    }
    function createButton(){
        const button = document.createElement('button');
        button.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 256 256" preserveAspectRatio="xMidYMid"><path d="M127.779 0C60.42 0 5.24 52.412 0 119.014l68.724 28.674a35.812 35.812 0 0 1 20.426-6.366c.682 0 1.356.019 2.02.056l30.566-44.71v-.626c0-26.903 21.69-48.796 48.353-48.796 26.662 0 48.352 21.893 48.352 48.796 0 26.902-21.69 48.804-48.352 48.804-.37 0-.73-.009-1.098-.018l-43.593 31.377c.028.582.046 1.163.046 1.735 0 20.204-16.283 36.636-36.294 36.636-17.566 0-32.263-12.658-35.584-29.412L4.41 164.654c15.223 54.313 64.673 94.132 123.369 94.132 70.818 0 128.221-57.938 128.221-129.393C256 57.93 198.597 0 127.779 0zM80.352 196.332l-15.749-6.568c2.787 5.867 7.621 10.775 14.033 13.47 13.857 5.83 29.836-.803 35.612-14.799a27.555 27.555 0 0 0 .046-21.035c-2.768-6.79-7.999-12.086-14.706-14.909-6.67-2.795-13.811-2.694-20.085-.304l16.275 6.79c10.222 4.3 15.056 16.145 10.794 26.46-4.253 10.314-15.998 15.195-26.22 10.895zm121.957-100.29c0-17.925-14.457-32.52-32.217-32.52-17.769 0-32.226 14.595-32.226 32.52 0 17.926 14.457 32.512 32.226 32.512 17.76 0 32.217-14.586 32.217-32.512zm-56.37-.055c0-13.488 10.84-24.42 24.2-24.42 13.368 0 24.208 10.932 24.208 24.42 0 13.488-10.84 24.421-24.209 24.421-13.359 0-24.2-10.933-24.2-24.42z" fill="#1A1918"/></svg>`;
        button.style.position = 'fixed';
        button.style.top = saves.buttonposition;
        button.style.zIndex = '9999'
        button.style.transition = 'transform 0.3s ease';
        button.style.opacity = '0.5';
        button.style.backgroundColor = 'transparent';
        button.style.border = 'none';
        button.style.cursor = 'pointer';
        button.style.left = '0'
        button.style.clipPath = 'polygon(100% 50%, 85% 100%, 0 100%,0 0, 85% 0)';
        button.style.transform = 'translate(0%, -50%)';
        button.style.background = 'linear-gradient(to bottom right, pink, lightblue)';
        var isDragging = false;
        button.onmousedown = function(e) {
            isDragging = true;
            function onMouseMove(e) {
                if (!isDragging) return;
                button.style.top = `${e.clientY}px`;
            }
            function onMouseUp() {
                isDragging = false;
                saves.buttonposition = button.style.top
                window.removeEventListener('mousemove', onMouseMove);
                window.removeEventListener('mouseup', onMouseUp);
                savestorage()
            }
            window.addEventListener('mousemove', onMouseMove);
            window.addEventListener('mouseup', onMouseUp);
        };
        button.onmouseover = function() {
            this.style.opacity = '1';
            this.style.transform = 'translate(0%, -50%)';
        };
        button.onmouseleave = function() {
            if(isDragging) return;
            this.style.opacity = '0.5';
            this.style.transform = 'translate(-50%, -50%)';
        };
        button.addEventListener('click', showsettingdialog);
        button.ondragstart = function() {
            return false;
        };
        document.body.appendChild(button);
        return button
    }
    function showsettingdialog(){
        const colorPickers = document.getElementById('colorPickers');
        colorPickers.innerHTML = '';
        saves.linelist.forEach(item => {
            const colorPickerContainer = document.createElement('div');
            colorPickerContainer.innerHTML = `
            <input type="number" min="1" max="120" class="numberInput" value="${item.days}">
            <input type="color" class="colorInput" value="${item.color}">
        `;
            colorPickers.appendChild(colorPickerContainer);
        });
        settingdialog.style.display = 'block'
    }
    function initsettingdialog(){
        const modal = document.createElement('div');
        modal.style.position = 'fixed';
        modal.style.top = '20%';
        modal.style.left = '5%';
        modal.style.transform = 'translate(0, 0%)';
        modal.style.backgroundColor = '#fff';
        modal.style.padding = '20px';
        modal.style.zIndex = '9999';
        modal.style.display = 'none';
        modal.style.border = '1px solid #ccc';
        modal.style.boxShadow = '0 4px 6px rgba(0,0,0,.1)';
        document.body.appendChild(modal);
        modal.innerHTML = `
       <div> 均线设置</div>
  <div id="colorPickers"></div>
  <button id="confirmBtn">确定</button>
  <button id="cancelBtn">取消</button>
`;
        document.getElementById('confirmBtn').addEventListener('click', function() {
            const numberInputs = document.querySelectorAll('.numberInput');
            const colorInputs = document.querySelectorAll('.colorInput');
            const lineList = [];
            numberInputs.forEach((input, index) => {
                const days = parseInt(input.value);
                const color = colorInputs[index].value;
                if(days < 1) days=1
                if(days > 120) days =120
                lineList.push({ days, color });
            });
            saves.linelist=lineList
            savestorage()
            modal.style.display = 'none';
            try{document.getElementById('modalBG').style.display = 'none'}catch(e){}
            myCreatePriceHistoryGraph( 5 )
            g_plotPriceHistory.redraw()
        });
        document.getElementById('cancelBtn').addEventListener('click', function() {
            modal.style.display = 'none';
            try{document.getElementById('modalBG').style.display = 'none'}catch(e){}
        });
        return modal
    }
    function readstorage(){
        var saves = GM_getValue('saves')
        if(saves) return saves
        saves = {
            buttonposition:'50%',
            linelist:[{
                days:5,
                color:"#FFFFFF",
            },{
                days:10,
                color:"#FFFF0B",
            },{
                days:20,
                color:"#FF80FF",
            },{
                days:30,
                color:"#00E600",
            }]
        }
        return saves
    }
    function savestorage(){
        GM_setValue('saves',saves)
    }
})();