给博客添加一个自动更新基金持仓盈亏的页面

今年开始理财,但因为购买渠道众多导致持仓比较分散需要打开各个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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
<?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示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
[
{
"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"
}
]

前端模板文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
<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>

给博客添加一个自动更新基金持仓盈亏的页面

https://careful.fyi/post/549988.html

作者

John Doe

发布于

2026-02-25

更新于

2026-02-25

许可协议

You need to set install_url to use ShareThis. Please set it in _config.yml.
You forgot to set the business or currency_code for Paypal. Please set it in _config.yml.

评论

You forgot to set the shortname for Disqus. Please set it in _config.yml.
You need to set client_id and slot_id to show this AD unit. Please set it in _config.yml.