今年开始理财,但因为购买渠道众多导致持仓比较分散需要打开各个APP查看盈亏情况,所以给博客加了一个自动更新持仓净值的页面(参考:示例页面)。主要使用的是天天基金的API接口。
代码分3个文件,分别是用以自动更新基金净值的update_funds.php
,用以存储基金信息的funds.json
,以及用以展示基金信息的模板文件。
使用方法
首先需要把update_funds.php文件上传到支持php的web服务器中,然后在/data目录创建名为funds.json
的数据文件,其中name、code、cost_price、shares
四个字段需要手动填写,分别对应基金名称、基金代码、持仓成本、持仓份额,每次update_funds.php
文件时,脚本会把最新的净值写入到latest_net_value
字段中并将更新时间写入到last_updated
中,可以考虑设置计划任务来定期访问该脚本。
另外可以向json中加入update_enabled
字段,当值为false时,会跳过更新基金净值,可以在基金清仓后添加,用以跳过基金净值更新。
前端展现的代码仅供参考,原理无非就是调取funds.json
文件中的内容,并计算出收益((最新净值-持仓成本)*持仓份额)、收益率在前端页面展现。
update_funds.php
<?php
/*
* 基金净值自动更新脚本
* 数据源:https://fund.eastmoney.com/
* 配置文件:funds.json
*/
// 配置参数 ================================================
define('USER_AGENTS', [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15',
'Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15'
]);
define('REQUEST_DELAY', 1); // 请求间隔(秒)
define('MAX_RETRY', 2); // 单基金重试次数
// 主程序 ==================================================
try {
// 加载基金数据
$funds = loadFundsData();
// 遍历更新净值
foreach ($funds as &$fund) {
// 新增逻辑:检查是否允许更新
if (isset($fund['update_enabled']) && !$fund['update_enabled']) {
echo "跳过更新:{$fund['code']} (已禁用更新)\n";
continue;
}
try {
$result = fetchFundValue($fund['code']);
// 只更新净值相关字段
$fund['latest_net_value'] = $result['value'];
$fund['last_updated'] = date('Y-m-d H:i:s');
echo "成功更新:{$fund['code']} => {$result['value']}\n";
} catch (Exception $e) {
echo "更新失败:{$fund['code']} - {$e->getMessage()}\n";
logError($fund['code'], $e->getMessage());
continue;
}
// 遵守请求间隔
sleep(REQUEST_DELAY);
}
// 保存更新后的数据
saveFundsData($funds);
echo "全部更新完成!\n";
} catch (Exception $e) {
die("致命错误:" . $e->getMessage());
}
// 核心函数 ================================================
/**
* 加载基金数据文件
*/
function loadFundsData() {
$filename = '/data/funds.json';
if (!file_exists($filename)) {
throw new Exception("基金数据文件不存在");
}
$data = json_decode(file_get_contents($filename), true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception("JSON解析错误:" . json_last_error_msg());
}
return $data;
}
/**
* 保存基金数据文件
*/
function saveFundsData($data) {
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
if (file_put_contents('/data/funds.json', $json) === false) {
throw new Exception("文件保存失败");
}
}
/**
* 获取基金净值(带重试机制)
*/
function fetchFundValue($code) {
for ($i = 0; $i <= MAX_RETRY; $i++) {
try {
return [
'value' => getLatestNetValue($code),
'timestamp' => time()
];
} catch (Exception $e) {
if ($i == MAX_RETRY) {
throw $e;
}
usleep(500000 * ($i + 1)); // 递增延时
}
}
}
/**
* 核心抓取逻辑
*/
function getLatestNetValue($code) {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => "https://fund.eastmoney.com/pingzhongdata/{$code}.js",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 8,
CURLOPT_HTTPHEADER => [
'Referer: https://fund.eastmoney.com/',
'User-Agent: USER_AGENTS[array_rand(USER_AGENTS)]'
]
]);
$content = curl_exec($ch);
// 错误处理
if (curl_errno($ch)) {
throw new Exception("网络请求失败:" . curl_error($ch));
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpCode != 200) {
throw new Exception("HTTP错误代码:{$httpCode}");
}
curl_close($ch);
// 解析数据
if (preg_match('/Data_netWorthTrend\s*=\s*(\[.*?\])/s', $content, $matches)) {
$data = json_decode($matches[1], true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception("JSON解析失败");
}
$latest = end($data);
return $latest['y'];
}
throw new Exception("未找到净值数据");
}
funds.json示例
[
{
"name": "南方红利",
"code": "008163",
"cost_price": 1.1424,
"shares": 43766.09,
"latest_net_value": 1.1679,
"last_updated": "2025-07-16 03:12:56"
},
{
"name": "南方中债",
"code": "006961",
"cost_price": 1.3677,
"shares": 36535.8,
"latest_net_value": 1.3675,
"last_updated": "2025-07-16 03:12:58"
},
{
"name": "鹏华中债",
"code": "008040",
"cost_price": 1.0818,
"shares": 46196.16,
"latest_net_value": 1.0817,
"last_updated": "2025-07-16 03:12:59"
},
{
"name": "华泰红利",
"code": "007467",
"cost_price": 1.6458,
"shares": 30380.4,
"latest_net_value": 1.7166,
"last_updated": "2025-07-15 14:01:51",
"update_enabled": false
},
{
"name": "摩根标普",
"code": "019305",
"cost_price": 1.4634,
"shares": 823.61,
"latest_net_value": 1.4636,
"last_updated": "2025-07-16 03:13:00"
},
{
"name": "纳斯达克",
"code": "006479",
"cost_price": 6.6801,
"shares": 180.87,
"latest_net_value": 6.6769,
"last_updated": "2025-07-16 03:13:01"
}
]
前端模板文件
<style>
table.fund h1 {
color: #2d2d2d;
font-weight: 600;
margin-bottom: 1.5rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid #c62541;
}
table.fund {
border-collapse: collapse;
width: 100%;
background: white;
text-align: center;
border-radius: 8px;
margin-bottom: 5rem;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
transition: box-shadow 0.3s ease;
}
table.fund:hover {
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
table.fund th {
background: #c62541;
color: white;
padding: 14px 16px;
font-weight: 600;
text-transform: uppercase;
font-size: 0.9em;
}
table.fund td {
padding: 12px 16px;
color: #444;
border: 1px solid #f0f0f0;
}
table.fund tr:last-child td {
border-bottom: none;
}
table.fund tr:hover td {
background: #fff5f7;
}
table.fund .positive {
color: #c62541;
font-weight: 500;
}
table.fund .negative {
color: #27ae60;
font-weight: 500;
}
/* 响应式处理 */
@media (max-width: 768px) {
table.fund td, th {
padding: 10px 12px;
font-size: 0.9em;
}
table.fund h1 {
font-size: 1.4rem;
}
}
/* 时间显示样式 */
table.fund #current-time {
color: #c62541;
font-weight: 500;
}
</style>
<table>
<thead>
<tr>
<th>基金代码</th>
<th>持有份额</th>
<th>成本价</th>
<th>当前净值</th>
<th>持仓收益</th>
<th>更新时间</th>
</tr>
</thead>
<tbody id="funds-body"></tbody>
<tfoot class="highlight" id="summary-footer"></tfoot>
</table>
<p style="margin-top: 1rem; color: #666;">
数据更新频率:每日凌晨3点自动更新
<br>当前时间:<span id="current-time"></span>
</p>
<script>
// 配置参数
const JSON_URL = 'https://static.goldrun.click/json/fund.json';
// 初始化
document.addEventListener('DOMContentLoaded', async () => {
try {
const funds = await loadFundsData();
renderTable(funds);
startClock();
} catch (error) {
showError(error.message);
}
});
// 加载远程数据
async function loadFundsData() {
try {
const response = await fetch(JSON_URL);
if (!response.ok) throw new Error('网络响应异常');
return await response.json();
} catch (error) {
throw new Error('数据加载失败,请稍后刷新');
}
}
// 渲染表格
function renderTable(funds) {
let total = { cost: 0, current: 0, profit: 0 };
const tbody = document.getElementById('funds-body');
// 生成数据行
tbody.innerHTML = funds.map(fund => {
const fundCost = fund.cost_price * fund.shares;
const fundCurrent = fund.latest_net_value * fund.shares;
const profit = fundCurrent - fundCost;
// 累计总数
total.cost += fundCost;
total.current += fundCurrent;
total.profit += profit;
return `
<tr>
<td>${fund.code}</td>
<td>${formatNumber(fund.shares, 2)}</td>
<td>${formatNumber(fund.cost_price, 4)}</td>
<td>${fund.latest_net_value ? formatNumber(fund.latest_net_value, 4) : '--'}</td>
<td class="${profit >= 0 ? 'positive' : 'negative'}">
${formatNumber(profit, 2)}
</td>
<td>${fund.last_updated || '--'}</td>
</tr>`;
}).join('');
// 生成汇总行
const footer = document.getElementById('summary-footer');
footer.innerHTML = `
<tr>
<td colspan="4">总收益</td>
<td class="${total.profit >= 0 ? 'positive' : 'negative'}">
${formatNumber(total.profit, 2)}
</td>
<td></td>
</tr>
<tr>
<td colspan="4">总收益率</td>
<td class="${total.profit >= 0 ? 'positive' : 'negative'}">
${total.cost ? formatNumber((total.profit / total.cost) * 100, 2) + '%' : '--'}
</td>
<td></td>
</tr>`;
}
// 数字格式化
function formatNumber(num, digits) {
return num.toLocaleString('zh-CN', {
minimumFractionDigits: digits,
maximumFractionDigits: digits
});
}
// 实时时钟
function startClock() {
function updateTime() {
document.getElementById('current-time').textContent =
new Date().toLocaleString('zh-CN');
}
updateTime();
setInterval(updateTime, 1000);
}
// 错误显示
function showError(message) {
const tbody = document.getElementById('funds-body');
tbody.innerHTML = `<tr><td colspan="6" style="color:red">${message}</td></tr>`;
}
</script>
评论(0)