// ==UserScript==
// @name 英为财情定投计算器
// @namespace http://tampermonkey.net/
// @version 2025-07-10
// @description 英为财情定投计算器,用于写定投报告用,输入起止时间和定投金额,计算回报率等数据
// @author meteor
// @match https://cn.investing.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=investing.com
// @grant none
// @license GPLv3
// ==/UserScript==
(function () {
'use strict';
// 延迟执行以确保 DOM 加载完成
function initScript() {
if (!document.body) {
console.warn('document.body 未加载,稍后重试');
setTimeout(initScript, 100);
return;
}
const styleContent = `
#calculateButton {
position: fixed;
top: 50px;
right: 10px;
z-index: 10000;
background-color: #28a745;
color: white;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
#calculatorIframeOverlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: none;
}
#calculatorIframe {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 98%;
height: 95%;
border: none;
}
`;
const styleElement = document.createElement('style');
styleElement.textContent = styleContent;
document.head.appendChild(styleElement);
const calculateButton = document.createElement('div');
calculateButton.id = 'calculateButton';
calculateButton.textContent = '计算';
try {
document.body.appendChild(calculateButton);
} catch (e) {
console.error('无法添加 calculateButton:', e);
return;
}
const overlay = document.createElement('div');
overlay.id = 'calculatorIframeOverlay';
const iframe = document.createElement('iframe');
iframe.id = 'calculatorIframe';
overlay.appendChild(iframe);
try {
document.body.appendChild(overlay);
} catch (e) {
console.error('无法添加 overlay:', e);
return;
}
calculateButton.addEventListener('click', function () {
iframe.srcdoc = `
<!DOCTYPE html>
<html style="background: aliceblue;">
<head>
<meta charset="UTF-8">
<title>价格均值计算</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bignumber.js/9.0.1/bignumber.min.js"></script>
<script src="https://registry.npmmirror.com/echarts/5.5.1/files/dist/echarts.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<style>
#chartContainer {
width: 1400px;
height: 1000px;
}
.input-group {
margin-bottom: 10px;
}
.input-group label {
display: inline-block;
width: 120px;
}
</style>
<script>
function getInstrumentId() {
// 优先尝试从第一个路径获取 instrument_id
try {
const instrumentIdFromForecast = top.__NEXT_DATA__.props.pageProps.state.forecastStore.forecast.instrument_id;
if (instrumentIdFromForecast) {
return instrumentIdFromForecast;
}
} catch (e) {
console.error('从 forecastStore 获取 instrument_id 失败:', e);
// 不返回,继续尝试第二个路径
}
// 如果第一个路径没有找到或者出错,尝试从第二个路径获取
try {
const instrumentIdFromRouter = window.next.router.components['/equities/[...equity]'].props.pageProps.state.pageInfoStore.identifiers.instrument_id;
if (instrumentIdFromRouter) {
return instrumentIdFromRouter;
}
} catch (e) {
console.error('从 router components 获取 instrument_id 失败:', e);
}
// 如果两个路径都无法获取,则返回 null
return null;
}
function getWeekNumber(date) {
const onejan = new Date(date.getFullYear(), 0, 1);
const dayOffset = (date.getDay() + 6) % 7; // ISO day of the week
const startDayOffset = (onejan.getDay() + 6) % 7;
return Math.floor(((date - onejan) / (24 * 60 * 60 * 1000) + startDayOffset) / 7) + 1;
}
document.addEventListener('DOMContentLoaded', function() {
const iframeWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
const chartContainer = document.getElementById('chartContainer');
if (chartContainer) {
chartContainer.style.width = (iframeWidth < 1400 ? 1400 : iframeWidth) + 'px';
}
function fetchData() {
const instrumentId = getInstrumentId();
if (!instrumentId) {
alert('无法获取 instrument_id');
return;
}
// 强制获取160周的数据
const url = \`https://api.investing.com/api/financialdata/\${instrumentId}/historical/chart/?interval=P1W&pointscount=160\`;
fetch(url, {
method: 'GET',
headers: {
'accept': '*/*',
'accept-encoding': 'gzip, deflate, br, zstd',
'accept-language': 'zh-CN,zh;q=0.9,zh-TW;q=0.8,en-US;q=0.7,en;q=0.6,ru;q=0.5',
'cache-control': 'no-cache',
'content-type': 'application/json',
'domain-id': 'cn',
'origin': 'https://cn.investing.com',
'pragma': 'no-cache',
'priority': 'u=1, i',
'referer': 'https://cn.investing.com/',
'sec-ch-ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-site',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
}
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
console.log('Success:', data);
const klines = data.data.map(entry => ({
date: entry[0], // 时间戳(毫秒)
close: new BigNumber(entry[4]) // 收盘价
}));
window.allKlines = klines; // 存储所有获取到的数据
calculateAndDraw();
})
.catch(error => {
console.error('Fetch error:', error);
alert('获取数据失败: ' + error.message);
});
}
document.getElementById("fetchDataBtn").addEventListener("click", fetchData);
document.getElementById("calculateBtn").addEventListener("click", calculateAndDraw); // 为新按钮添加事件监听
function calculateAndDraw() {
const BigNumber = window.BigNumber;
const investmentAmount = new BigNumber(document.getElementById("investmentAmount").value);
const startWeekOffset = parseInt(document.getElementById("startWeekOffset").value);
const durationWeeks = parseInt(document.getElementById("durationWeeks").value);
if (isNaN(investmentAmount) || investmentAmount.lte(0)) {
alert("请输入有效的定投金额。");
return;
}
if (isNaN(startWeekOffset) || startWeekOffset < 0 || startWeekOffset > 159) {
alert("请输入有效的定投开始周数 (0-159)。");
return;
}
if (isNaN(durationWeeks) || durationWeeks <= 0 || (startWeekOffset + durationWeeks) > 160) {
alert("请输入有效的定投持续周数,且开始周数加持续周数不能超过160周。");
return;
}
if (!window.allKlines || window.allKlines.length === 0) {
alert("请先点击 '获取数据' 按钮。");
return;
}
let klines = Array.from(window.allKlines);
klines.sort((a, b) => a.date - b.date); // 按时间戳排序
// 截取指定范围的数据
const startIndex = klines.length - 1 - startWeekOffset - (durationWeeks - 1);
const endIndex = klines.length - 1 - startWeekOffset;
if (startIndex < 0 || endIndex >= klines.length || startIndex > endIndex) {
alert("设定的定投开始周数和持续周数超出了可获取的历史数据范围。请调整。");
return;
}
const filteredKlines = klines.slice(startIndex, endIndex + 1);
let tableBody = document.getElementById("resultTable").getElementsByTagName("tbody")[0];
tableBody.innerHTML = "";
let weekData = {};
filteredKlines.forEach(entry => {
let date = new Date(entry.date);
let year = date.getFullYear();
let weekNumber = getWeekNumber(date);
let closePrice = entry.close;
const key = \`\${year}-\${weekNumber}\`;
if (!weekData[key]) {
weekData[key] = { year, week: weekNumber, prices: [] };
}
weekData[key].prices.push(closePrice);
});
let accumulatedShares = new BigNumber(0);
let totalPrincipal = new BigNumber(0);
let chartData = [];
Object.entries(weekData).forEach(([key, data], index) => {
let sum = data.prices.reduce((acc, val) => acc.plus(val), new BigNumber(0));
let average = sum.dividedBy(data.prices.length);
let sharesPurchased = investmentAmount.dividedBy(average);
accumulatedShares = accumulatedShares.plus(sharesPurchased);
totalPrincipal = totalPrincipal.plus(investmentAmount);
let npv = accumulatedShares.times(average);
let returnRate = totalPrincipal.isZero() ? new BigNumber(0) : npv.minus(totalPrincipal).dividedBy(totalPrincipal).times(100);
let row = document.createElement("tr");
let serialCell = document.createElement("td");
let yearCell = document.createElement("td");
let weekCell = document.createElement("td");
let averageCell = document.createElement("td");
let sharesCell = document.createElement("td");
let totalSharesCell = document.createElement("td");
let npvCell = document.createElement("td");
let principalCell = document.createElement("td");
let returnRateCell = document.createElement("td");
serialCell.textContent = index + 1;
yearCell.textContent = data.year;
weekCell.textContent = \`第\${data.week}周\`;
averageCell.textContent = average.toFixed(8);
sharesCell.textContent = sharesPurchased.toFixed(8);
totalSharesCell.textContent = accumulatedShares.toFixed(8);
npvCell.textContent = npv.toFixed(8);
principalCell.textContent = totalPrincipal.toFixed(8);
returnRateCell.textContent = returnRate.toFixed(2) + '%';
row.appendChild(serialCell);
row.appendChild(yearCell);
row.appendChild(weekCell);
row.appendChild(averageCell);
row.appendChild(sharesCell);
row.appendChild(totalSharesCell);
row.appendChild(npvCell);
row.appendChild(principalCell);
row.appendChild(returnRateCell);
tableBody.appendChild(row);
chartData.push({
week: \`\${data.year}年第\${data.week}周\`,
average: average.toNumber(),
sharesPurchased: sharesPurchased.toNumber(),
accumulatedShares: accumulatedShares.toNumber(),
npv: npv.toNumber(),
principal: totalPrincipal.toNumber(),
returnRate: returnRate.toNumber()
});
});
function calculateMovingAverageCost(data, weeks) {
if (data.length < weeks) {
return Array(data.length).fill(null);
}
return data.map((_, index, array) => {
if (index < weeks - 1) { // 修正:前几周不计算
return null;
}
const slice = array.slice(index - weeks + 1, index + 1);
const totalShares = slice.reduce((sum, item) => {
return sum.plus(new BigNumber(item.sharesPurchased));
}, new BigNumber(0));
if (totalShares.isZero()) {
return null;
}
const totalCost = new BigNumber(weeks).times(investmentAmount);
return totalCost.dividedBy(totalShares).toNumber();
});
}
const ma28 = calculateMovingAverageCost(chartData, 28);
const ma48 = calculateMovingAverageCost(chartData, 48);
const ma72 = calculateMovingAverageCost(chartData, 72);
drawChart(chartData, ma28, ma48, ma72);
document.getElementById("exportBtn").onclick = function() {
exportToExcel(chartData);
};
}
function drawChart(chartData, ma28, ma48, ma72) {
let chartDom = document.getElementById('chartContainer');
let myChart = echarts.init(chartDom);
let option = {
title: {
text: '每周价格与投资'
},
tooltip: {
trigger: 'axis'
},
legend: {
data: ['均价', '净现值', '总本金', '回报率', '28周平均成本', '48周平均成本', '72周平均成本'],
selected: {
'均价': true,
'净现值': true,
'总本金': true,
'回报率': true,
'28周平均成本': false,
'48周平均成本': false,
'72周平均成本': false
}
},
xAxis: {
type: 'category',
data: chartData.map(data => data.week)
},
yAxis: [
{
type: 'value',
name: '价格',
position: 'left'
},
{
type: 'value',
name: '金额',
position: 'right'
},
{
type: 'value',
name: '回报率',
position: 'right',
offset: 80
}
],
series: [
{
name: '均价',
data: chartData.map(data => data.average),
type: 'line',
yAxisIndex: 0,
itemStyle: {color: 'blue'}
},
{
name: '净现值',
data: chartData.map(data => data.npv),
type: 'line',
yAxisIndex: 1,
itemStyle: {color: 'green'}
},
{
name: '总本金',
data: chartData.map(data => data.principal),
type: 'line',
yAxisIndex: 1,
itemStyle: {color: 'red'}
},
{
name: '回报率',
data: chartData.map(data => data.returnRate),
type: 'line',
yAxisIndex: 2,
itemStyle: {color: 'purple'}
},
{
name: '28周平均成本',
data: ma28,
type: 'line',
yAxisIndex: 0,
itemStyle: {color: '#FF8C00'},
lineStyle: {type: 'dashed'} //虚线
},
{
name: '48周平均成本',
data: ma48,
type: 'line',
yAxisIndex: 0,
itemStyle: {color: '#9370DB'},
lineStyle: {type: 'dashed'} //虚线
},
{
name: '72周平均成本',
data: ma72,
type: 'line',
yAxisIndex: 0,
itemStyle: {color: '#20B2AA'},
lineStyle: {type: 'dashed'} //虚线
}
]
};
myChart.setOption(option);
}
function exportToExcel(chartData) {
let workbook = XLSX.utils.book_new();
let weeklySheetData = [['投资周数', '年份', '周数', '均价', '投资股份', '累计股份', '净现值', '总本金', '投资回报率']];
chartData.forEach((item, index) => weeklySheetData.push([index + 1, item.week.split('年')[0], item.week.split('第')[1].replace('周', ''), item.average.toFixed(8), item.sharesPurchased.toFixed(8), item.accumulatedShares.toFixed(8), item.npv.toFixed(8), item.principal.toFixed(8), item.returnRate.toFixed(2) + '%']));
let weeklySheet = XLSX.utils.aoa_to_sheet(weeklySheetData);
XLSX.utils.book_append_sheet(workbook, weeklySheet, '每周投资数据');
XLSX.writeFile(workbook, '价格与投资数据.xlsx');
}
});
</script>
</head>
<body>
<h1>价格均值与投资计算</h1>
<div class="input-group">
<label for="investmentAmount">定投金额:</label>
<input type="number" id="investmentAmount" value="100">
</div>
<div class="input-group">
<label for="startWeekOffset">倒推开始周数 (0-159):</label>
<input type="number" id="startWeekOffset" value="0" min="0" max="159">
</div>
<div class="input-group">
<label for="durationWeeks">定投持续周数 (1-160):</label>
<input type="number" id="durationWeeks" value="10" min="1" max="160">
</div>
<button id="fetchDataBtn">获取数据 (强制160周)</button>
<button id="calculateBtn">计算并绘图</button>
<button id="exportBtn">导出 Excel</button>
<h2>结果</h2>
<table id="resultTable" border="1" style="display: inline-block;">
<thead>
<tr>
<th>投资周数</th>
<th>年份</th>
<th>周数</th>
<th>均价 (美元)</th>
<th>投资股份</th>
<th>累计股份</th>
<th>净现值 (美元)</th>
<th>总本金 (美元)</th>
<th>投资回报率 (%)</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<div id="chartContainer" style="display: inline-block;"></div>
</body>
</html>
`;
overlay.style.display = 'block';
});
overlay.addEventListener('click', function (event) {
if (event.target === overlay) {
overlay.style.display = 'none';
}
});
}
// 监听 DOMContentLoaded 或延迟执行
if (document.readyState === 'complete' || document.readyState === 'interactive') {
initScript();
} else {
document.addEventListener('DOMContentLoaded', initScript);
}
})();