Cai
2026-08-20 f046e00dff100e7510089e4e568f2165ca8a199e
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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
import fs from "node:fs/promises";
import { SpreadsheetFile, Workbook } from "@oai/artifact-tool";
 
const outputDir = "E:/mb-ms-doc/project-info/outputs/019f8e66-e563-7c01-a454-d66ee126f5d6";
const outputPath = `${outputDir}/2026Q2_机器人股票公募基金持仓统计_主动指数拆分.xlsx`;
 
const universe = [{"code":"000837","name":"秦川机床","csi":true,"cni":false},{"code":"000967","name":"盈峰环境","csi":true,"cni":true},{"code":"001266","name":"宏英智能","csi":true,"cni":false},{"code":"001306","name":"夏厦精密","csi":true,"cni":false},{"code":"002008","name":"大族激光","csi":true,"cni":false},{"code":"002031","name":"巨轮智能","csi":true,"cni":false},{"code":"002050","name":"三花智控","csi":true,"cni":true},{"code":"002139","name":"拓邦股份","csi":true,"cni":true},{"code":"002230","name":"科大讯飞","csi":true,"cni":true},{"code":"002236","name":"大华股份","csi":true,"cni":false},{"code":"002248","name":"华东数控","csi":true,"cni":false},{"code":"002338","name":"奥普光电","csi":false,"cni":true},{"code":"002380","name":"科远智慧","csi":true,"cni":false},{"code":"002472","name":"双环传动","csi":true,"cni":true},{"code":"002527","name":"新时达","csi":true,"cni":false},{"code":"002600","name":"领益智造","csi":false,"cni":true},{"code":"002747","name":"埃斯顿","csi":true,"cni":true},{"code":"002892","name":"科力尔","csi":true,"cni":true},{"code":"002896","name":"中大力德","csi":true,"cni":true},{"code":"002957","name":"科瑞技术","csi":true,"cni":false},{"code":"002975","name":"博杰股份","csi":true,"cni":false},{"code":"002979","name":"雷赛智能","csi":true,"cni":true},{"code":"003021","name":"兆威机电","csi":false,"cni":true},{"code":"300007","name":"汉威科技","csi":false,"cni":true},{"code":"300024","name":"机器人","csi":true,"cni":true},{"code":"300124","name":"汇川技术","csi":true,"cni":true},{"code":"300161","name":"华中数控","csi":true,"cni":true},{"code":"300222","name":"科大智能","csi":true,"cni":true},{"code":"300276","name":"三丰智能","csi":true,"cni":true},{"code":"300278","name":"华昌达","csi":true,"cni":false},{"code":"300432","name":"富临精工","csi":false,"cni":true},{"code":"300455","name":"航天智装","csi":false,"cni":true},{"code":"300466","name":"赛摩智能","csi":false,"cni":true},{"code":"300503","name":"昊志机电","csi":true,"cni":true},{"code":"300580","name":"贝斯特","csi":false,"cni":true},{"code":"300607","name":"拓斯达","csi":true,"cni":true},{"code":"300660","name":"江苏雷利","csi":true,"cni":false},{"code":"300802","name":"矩子科技","csi":true,"cni":false},{"code":"300953","name":"震裕科技","csi":false,"cni":true},{"code":"301112","name":"信邦智能","csi":true,"cni":false},{"code":"301368","name":"丰立智能","csi":true,"cni":true},{"code":"301413","name":"安培龙","csi":false,"cni":true},{"code":"301510","name":"固高科技","csi":true,"cni":true},{"code":"601100","name":"恒立液压","csi":true,"cni":false},{"code":"601608","name":"中信重工","csi":true,"cni":true},{"code":"601689","name":"拓普集团","csi":true,"cni":true},{"code":"603015","name":"弘讯科技","csi":true,"cni":true},{"code":"603119","name":"浙江荣泰","csi":false,"cni":true},{"code":"603203","name":"快克智能","csi":true,"cni":false},{"code":"603416","name":"信捷电气","csi":true,"cni":true},{"code":"603486","name":"科沃斯","csi":true,"cni":true},{"code":"603662","name":"柯力传感","csi":false,"cni":true},{"code":"603666","name":"亿嘉和","csi":true,"cni":false},{"code":"603728","name":"鸣志电器","csi":true,"cni":true},{"code":"603915","name":"国茂股份","csi":true,"cni":true},{"code":"603960","name":"克来机电","csi":true,"cni":true},{"code":"688003","name":"天准科技","csi":true,"cni":false},{"code":"688017","name":"绿的谐波","csi":true,"cni":true},{"code":"688084","name":"晶品特装","csi":true,"cni":false},{"code":"688090","name":"瑞松科技","csi":true,"cni":false},{"code":"688160","name":"步科股份","csi":true,"cni":true},{"code":"688165","name":"埃夫特","csi":true,"cni":true},{"code":"688169","name":"石头科技","csi":true,"cni":true},{"code":"688188","name":"柏楚电子","csi":true,"cni":false},{"code":"688218","name":"江苏北人","csi":true,"cni":false},{"code":"688248","name":"南网科技","csi":true,"cni":true},{"code":"688277","name":"天智航","csi":true,"cni":true},{"code":"688279","name":"峰岹科技","csi":false,"cni":true},{"code":"688290","name":"景业智能","csi":true,"cni":false},{"code":"688306","name":"均普智能","csi":true,"cni":false},{"code":"688320","name":"禾川科技","csi":true,"cni":false},{"code":"688322","name":"奥比中光","csi":true,"cni":true},{"code":"688343","name":"云天励飞","csi":true,"cni":false},{"code":"688400","name":"凌云光","csi":false,"cni":true},{"code":"688686","name":"奥普特","csi":true,"cni":true},{"code":"688698","name":"伟创电气","csi":true,"cni":true},{"code":"688777","name":"中控技术","csi":true,"cni":false},{"code":"920593","name":"鼎智科技","csi":true,"cni":true}];
 
const dataUrl = (page) => `https://data.eastmoney.com/dataapi/zlsj/list?date=2026-06-30&type=1&zjc=0&sortField=HOLD_VALUE&sortDirec=1&pageNum=${page}&pageSize=500`;
async function getJson(url) {
  let lastError;
  for (let attempt = 1; attempt <= 3; attempt++) {
    try {
      const response = await fetch(url, { headers: { "User-Agent": "Mozilla/5.0" } });
      if (!response.ok) throw new Error(`HTTP ${response.status}: ${url}`);
      return response.json();
    } catch (error) {
      lastError = error;
      if (attempt < 3) await new Promise((resolve) => setTimeout(resolve, 350 * attempt));
    }
  }
  throw lastError;
}
 
const first = await getJson(dataUrl(1));
const pageCount = Number(first.pages || 1);
const rest = pageCount > 1 ? await Promise.all(Array.from({ length: pageCount - 1 }, (_, i) => getJson(dataUrl(i + 2)))) : [];
const sourceRows = [first, ...rest].flatMap((x) => x.data || []);
const sourceMap = new Map(sourceRows.map((x) => [String(x.SECURITY_CODE), x]));
 
const fundCatalogResponse = await fetch("https://fund.eastmoney.com/js/fundcode_search.js", { headers: { "User-Agent": "Mozilla/5.0" } });
if (!fundCatalogResponse.ok) throw new Error(`HTTP ${fundCatalogResponse.status}: fund catalog`);
const fundCatalogText = await fundCatalogResponse.text();
const fundCatalogRows = JSON.parse(fundCatalogText.slice(fundCatalogText.indexOf("["), fundCatalogText.lastIndexOf("]") + 1));
const fundCatalog = new Map(fundCatalogRows.map((row) => [String(row[0]), { name: String(row[2] || ""), type: String(row[3] || "") }]));
 
const fundDetailUrl = (code, page) => `https://data.eastmoney.com/dataapi/zlsj/detail?SHType=1&SHCode=&SCode=${code}&ReportDate=2026-06-30&sortField=HOLDER_CODE&sortDirec=1&pageNum=${page}&pageSize=200`;
async function loadFundDetails(code) {
  const firstPage = await getJson(fundDetailUrl(code, 1));
  const pages = Number(firstPage.pages || 1);
  const morePages = pages > 1 ? await Promise.all(Array.from({ length: pages - 1 }, (_, i) => getJson(fundDetailUrl(code, i + 2)))) : [];
  return [firstPage, ...morePages].flatMap((x) => x.data || []);
}
async function pooledMap(items, limit, mapper) {
  const results = new Array(items.length);
  let cursor = 0;
  async function worker() {
    while (true) {
      const index = cursor++;
      if (index >= items.length) return;
      results[index] = await mapper(items[index], index);
    }
  }
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
  return results;
}
 
const rawRelationGroups = await pooledMap(universe, 6, async (stock) => {
  const rows = await loadFundDetails(stock.code);
  return rows.map((row) => ({ ...row, INDEX_LABEL: stock.csi && stock.cni ? "双指数" : stock.csi ? "中证H30590" : "国证980022" }));
});
for (let i = 0; i < universe.length; i++) {
  const stock = universe[i];
  const expectedCount = Number(sourceMap.get(stock.code)?.HOULD_NUM || 0);
  if (rawRelationGroups[i].length === expectedCount) continue;
  for (let retry = 1; retry <= 4; retry++) {
    await new Promise((resolve) => setTimeout(resolve, 700 * retry));
    const rows = await loadFundDetails(stock.code);
    rawRelationGroups[i] = rows.map((row) => ({ ...row, INDEX_LABEL: stock.csi && stock.cni ? "双指数" : stock.csi ? "中证H30590" : "国证980022" }));
    if (rawRelationGroups[i].length === expectedCount) break;
  }
  if (rawRelationGroups[i].length !== expectedCount) {
    throw new Error(`基金明细数量未对账:${stock.code} expected=${expectedCount} actual=${rawRelationGroups[i].length}`);
  }
}
const rawRelations = rawRelationGroups.flat();
 
const indexNamePattern = /ETF|指数|联接/iu;
const fundRelations = rawRelations.map((row) => {
  const fundCode = String(row.HOLDER_CODE || "");
  const fundName = String(row.HOLDER_NAME || "");
  const catalog = fundCatalog.get(fundCode);
  const fundType = catalog?.type || "未匹配";
  const managementType = fundType.startsWith("指数型") || indexNamePattern.test(fundName) ? "指数型" : "主动型";
  return {
    stockCode: String(row.SECURITY_CODE || ""),
    stockName: String(row.SECURITY_NAME_ABBR || ""),
    index: String(row.INDEX_LABEL || ""),
    fundCode,
    fundName,
    fundType,
    managementType,
    fundCompany: String(row.PARENT_ORG_NAME || ""),
    holdSharesWan: Number(row.TOTAL_SHARES || 0) / 1e4,
    holdValueYi: Number(row.HOLD_MARKET_CAP || 0) / 1e8,
    totalCapRatio: Number(row.TOTAL_SHARES_RATIO || 0) / 100,
    floatRatio: Number(row.FREE_SHARES_RATIO || 0) / 100,
    navRatio: Number(row.NETASSET_RATIO || 0) / 100,
  };
});
const relationsByStock = new Map(universe.map((stock) => [stock.code, []]));
for (const relation of fundRelations) relationsByStock.get(relation.stockCode)?.push(relation);
 
const indexLabel = (x) => x.csi && x.cni ? "双指数" : x.csi ? "中证H30590" : "国证980022";
const listedCode = (code) => `${code}.${code.startsWith("6") ? "SH" : code.startsWith("9") ? "BJ" : "SZ"}`;
const data = universe.map((stock) => {
  const d = sourceMap.get(stock.code);
  const relations = relationsByStock.get(stock.code) || [];
  const activeRelations = relations.filter((x) => x.managementType === "主动型");
  const indexRelations = relations.filter((x) => x.managementType === "指数型");
  return {
    ...stock,
    index: indexLabel(stock),
    fundCount: d ? Number(d.HOULD_NUM || 0) : 0,
    activeFundCount: activeRelations.length,
    indexFundCount: indexRelations.length,
    holdSharesWan: d ? Number(d.TOTAL_SHARES || 0) / 1e4 : 0,
    holdValueYi: d ? Number(d.HOLD_VALUE || 0) / 1e8 : 0,
    activeHoldValueYi: activeRelations.reduce((sum, x) => sum + x.holdValueYi, 0),
    indexHoldValueYi: indexRelations.reduce((sum, x) => sum + x.holdValueYi, 0),
    totalCapRatio: d ? Number(d.TOTALSHARES_RATIO || 0) / 100 : 0,
    floatRatio: d ? Number(d.FREESHARES_RATIO || 0) / 100 : 0,
    q2ShareChangeWan: d ? Number(d.HOLDCHA_NUM || 0) / 1e4 : 0,
    direction: d ? String(d.HOLDCHA || "-") : "未进入重仓统计",
    status: d ? "已进入基金重仓统计" : "未进入基金重仓统计",
  };
}).sort((a, b) => b.holdValueYi - a.holdValueYi || b.fundCount - a.fundCount || a.code.localeCompare(b.code));
 
const uniqueActiveFunds = new Set(fundRelations.filter((x) => x.managementType === "主动型").map((x) => x.fundCode));
const uniqueIndexFunds = new Set(fundRelations.filter((x) => x.managementType === "指数型").map((x) => x.fundCode));
 
const workbook = Workbook.create();
const summary = workbook.worksheets.add("摘要");
const detail = workbook.worksheets.add("明细");
const fundDetail = workbook.worksheets.add("基金级明细");
const sources = workbook.worksheets.add("来源与口径");
const checks = workbook.worksheets.add("检查");
for (const sheet of [summary, detail, fundDetail, sources, checks]) sheet.showGridLines = false;
 
const navy = "#17365D";
const teal = "#0F6B78";
const lightBlue = "#DCE6F1";
const pale = "#F4F7FA";
const green = "#E2F0D9";
const red = "#FCE4D6";
const yellow = "#FFF2CC";
const gray = "#666666";
 
// 明细表
detail.mergeCells("A1:V1");
detail.getRange("A1").values = [["2026年二季度机器人相关股票公募基金持仓统计"]];
detail.getRange("A1:V1").format = { fill: navy, font: { bold: true, color: "#FFFFFF", size: 16 }, verticalAlignment: "center" };
detail.getRange("A1:V1").format.rowHeight = 30;
detail.mergeCells("A2:V2");
detail.getRange("A2").values = [["股票范围:中证机器人H30590与国证机器人产业980022成分股并集(78只);总持仓已包含主动型和指数型基金,本版新增主动/指数拆分。"]];
detail.getRange("A2:V2").format = { fill: pale, font: { color: gray, size: 10 }, wrapText: true, verticalAlignment: "center" };
detail.getRange("A2:V2").format.rowHeight = 30;
const headers = ["排名","股票代码","股票简称","指数归属","持有基金数","期末持股数(万股)","期末持仓市值(亿元)","占总股本","占流通股本","Q2增减持股数(万股)","6/30收盘价(元)","Q2净增减持估算(亿元)","增减方向","披露状态","来源ID","主动型基金数","指数型基金数","主动型持仓市值(亿元)","指数型持仓市值(亿元)","主动型占基金持仓","主动型持仓占总股本","指数型持仓占总股本"];
detail.getRange("A4:V4").values = [headers];
detail.getRange("A4:V4").format = { fill: teal, font: { bold: true, color: "#FFFFFF" }, wrapText: true, horizontalAlignment: "center", verticalAlignment: "center" };
detail.getRange("A4:V4").format.rowHeight = 38;
const detailValues = data.map((d, i) => [i + 1, null, d.name, d.index, d.fundCount, d.holdSharesWan, d.holdValueYi, d.totalCapRatio, d.floatRatio, d.q2ShareChangeWan, null, null, d.direction, d.status, "EM-Q2-2026", d.activeFundCount, d.indexFundCount, d.activeHoldValueYi, d.indexHoldValueYi, null, null, null]);
detail.getRange(`A5:V${data.length + 4}`).values = detailValues;
for (let row = 5; row <= data.length + 4; row++) {
  detail.getRange(`B${row}`).formulas = [[`="${listedCode(data[row - 5].code)}"`]];
  detail.getRange(`K${row}`).formulas = [[`=IFERROR(G${row}/F${row}*10000,0)`]];
  detail.getRange(`L${row}`).formulas = [[`=IFERROR(J${row}*K${row}/10000,0)`]];
  detail.getRange(`T${row}`).formulas = [[`=IFERROR(R${row}/G${row},0)`]];
  detail.getRange(`U${row}`).formulas = [[`=IFERROR(R${row}/G${row}*H${row},0)`]];
  detail.getRange(`V${row}`).formulas = [[`=IFERROR(S${row}/G${row}*H${row},0)`]];
}
detail.getRange(`A5:V${data.length + 4}`).format.font = { name: "Microsoft YaHei", size: 10 };
detail.getRange(`A5:D${data.length + 4}`).format.horizontalAlignment = "left";
detail.getRange(`E5:L${data.length + 4}`).format.horizontalAlignment = "right";
detail.getRange(`A5:A${data.length + 4}`).format.numberFormat = "#,##0";
detail.getRange(`B5:B${data.length + 4}`).format.numberFormat = "@";
detail.getRange(`E5:E${data.length + 4}`).format.numberFormat = "#,##0;[Red](#,##0);-";
detail.getRange(`F5:G${data.length + 4}`).format.numberFormat = "#,##0.00;[Red](#,##0.00);-";
detail.getRange(`H5:I${data.length + 4}`).format.numberFormat = "0.00%;[Red](0.00%);-";
detail.getRange(`J5:J${data.length + 4}`).format.numberFormat = "#,##0.00;[Red](#,##0.00);-";
detail.getRange(`K5:K${data.length + 4}`).format.numberFormat = "0.00";
detail.getRange(`L5:L${data.length + 4}`).format.numberFormat = "#,##0.00;[Red](#,##0.00);-";
detail.getRange(`P5:Q${data.length + 4}`).format.numberFormat = "#,##0;[Red](#,##0);-";
detail.getRange(`R5:S${data.length + 4}`).format.numberFormat = "#,##0.00;[Red](#,##0.00);-";
detail.getRange(`T5:V${data.length + 4}`).format.numberFormat = "0.00%;[Red](0.00%);-";
detail.getRange(`E5:E${data.length + 4}`).conditionalFormats.add("dataBar", { color: "#5B9BD5", gradient: true });
detail.getRange(`G5:G${data.length + 4}`).conditionalFormats.add("dataBar", { color: "#70AD47", gradient: true });
detail.getRange(`P5:Q${data.length + 4}`).conditionalFormats.add("dataBar", { color: "#5B9BD5", gradient: true });
detail.getRange(`R5:S${data.length + 4}`).conditionalFormats.add("dataBar", { color: "#70AD47", gradient: true });
detail.getRange(`H5:I${data.length + 4}`).conditionalFormats.add("colorScale", { colors: ["#FFFFFF", "#FFE699", "#F8696B"], thresholds: ["min", "50%", "max"] });
detail.getRange(`L5:L${data.length + 4}`).conditionalFormats.add("cellIs", { operator: "greaterThan", formula: 0, format: { fill: green, font: { color: "#006100" } } });
detail.getRange(`L5:L${data.length + 4}`).conditionalFormats.add("cellIs", { operator: "lessThan", formula: 0, format: { fill: red, font: { color: "#9C0006" } } });
detail.getRange(`N5:N${data.length + 4}`).conditionalFormats.add("containsText", { text: "未进入", format: { fill: yellow, font: { color: "#9C6500" } } });
const detailTable = detail.tables.add(`A4:V${data.length + 4}`, true, "RobotFundHoldingsTable");
detailTable.style = "TableStyleMedium2";
detail.freezePanes.freezeRows(4);
detail.freezePanes.freezeColumns(3);
const widths = { A: 7, B: 11, C: 12, D: 13, E: 11, F: 16, G: 18, H: 12, I: 13, J: 18, K: 14, L: 20, M: 11, N: 20, O: 14, P: 13, Q: 13, R: 19, S: 19, T: 17, U: 18, V: 18 };
for (const [col, width] of Object.entries(widths)) detail.getRange(`${col}:${col}`).format.columnWidth = width;
 
// 基金级明细
fundDetail.mergeCells("A1:N1");
fundDetail.getRange("A1").values = [["2026Q2机器人股票基金级持仓明细|主动型与指数型拆分"]];
fundDetail.getRange("A1:N1").format = { fill: navy, font: { bold: true, color: "#FFFFFF", size: 16 }, verticalAlignment: "center" };
fundDetail.getRange("A1:N1").format.rowHeight = 30;
fundDetail.mergeCells("A2:N2");
fundDetail.getRange("A2").values = [["主动型定义:持仓基金明细中的非指数型产品(含公募化资管产品);指数型包含ETF、ETF联接、普通指数及增强指数。每行是一条“股票-基金”持仓关系。"]];
fundDetail.getRange("A2:N2").format = { fill: yellow, font: { color: "#7F6000", size: 10 }, wrapText: true, verticalAlignment: "center" };
fundDetail.getRange("A2:N2").format.rowHeight = 32;
const fundDetailHeaders = ["股票代码","股票简称","指数归属","基金代码","基金名称","基金公司","基金分类","管理类型","持股数(万股)","持仓市值(亿元)","占股票总股本","占股票流通股本","占基金净值","来源URL"];
fundDetail.getRange("A4:N4").values = [fundDetailHeaders];
fundDetail.getRange("A4:N4").format = { fill: teal, font: { bold: true, color: "#FFFFFF" }, wrapText: true, horizontalAlignment: "center", verticalAlignment: "center" };
fundDetail.getRange("A4:N4").format.rowHeight = 34;
const fundDetailSorted = [...fundRelations].sort((a, b) => b.holdValueYi - a.holdValueYi || a.stockCode.localeCompare(b.stockCode) || a.fundCode.localeCompare(b.fundCode));
const fundDetailValues = fundDetailSorted.map((x) => [listedCode(x.stockCode), x.stockName, x.index, x.fundCode, x.fundName, x.fundCompany, x.fundType, x.managementType, x.holdSharesWan, x.holdValueYi, x.totalCapRatio, x.floatRatio, x.navRatio, fundDetailUrl(x.stockCode, 1)]);
const fundDetailLastRow = fundDetailValues.length + 4;
fundDetail.getRange(`A5:N${fundDetailLastRow}`).values = fundDetailValues;
fundDetail.getRange(`A5:N${fundDetailLastRow}`).format.font = { name: "Microsoft YaHei", size: 9 };
fundDetail.getRange(`A5:H${fundDetailLastRow}`).format.horizontalAlignment = "left";
fundDetail.getRange(`I5:M${fundDetailLastRow}`).format.horizontalAlignment = "right";
fundDetail.getRange(`A5:A${fundDetailLastRow}`).format.numberFormat = "@";
fundDetail.getRange(`D5:D${fundDetailLastRow}`).format.numberFormat = "@";
fundDetail.getRange(`I5:J${fundDetailLastRow}`).format.numberFormat = "#,##0.00;[Red](#,##0.00);-";
fundDetail.getRange(`K5:M${fundDetailLastRow}`).format.numberFormat = "0.00%;[Red](0.00%);-";
fundDetail.getRange(`H5:H${fundDetailLastRow}`).conditionalFormats.add("containsText", { text: "主动型", format: { fill: green, font: { color: "#006100" } } });
fundDetail.getRange(`H5:H${fundDetailLastRow}`).conditionalFormats.add("containsText", { text: "指数型", format: { fill: lightBlue, font: { color: navy } } });
fundDetail.getRange(`J5:J${fundDetailLastRow}`).conditionalFormats.add("dataBar", { color: "#70AD47", gradient: true });
fundDetail.getRange(`N5:N${fundDetailLastRow}`).format.font = { color: "#FF0000", size: 8 };
const fundRelationsTable = fundDetail.tables.add(`A4:N${fundDetailLastRow}`, true, "RobotFundRelationsTable");
fundRelationsTable.style = "TableStyleMedium2";
fundDetail.freezePanes.freezeRows(4);
fundDetail.freezePanes.freezeColumns(5);
const fundDetailWidths = { A: 12, B: 12, C: 13, D: 11, E: 32, F: 28, G: 18, H: 12, I: 14, J: 18, K: 16, L: 16, M: 14, N: 72 };
for (const [col, width] of Object.entries(fundDetailWidths)) fundDetail.getRange(`${col}:${col}`).format.columnWidth = width;
 
// 摘要页
summary.mergeCells("A1:W2");
summary.getRange("A1").values = [["机器人股票的公募基金持仓全景|2026Q2主动/指数拆分"]];
summary.getRange("A1:W2").format = { fill: navy, font: { bold: true, color: "#FFFFFF", size: 18 }, verticalAlignment: "center" };
const cards = [
  { label: "机器人股票并集", labelRange: "A4:D4", valueRange: "A5:D6", formula: "=COUNTA('明细'!$B$5:$B$82)", format: "#,##0\"只\"" },
  { label: "主动型基金-股票关系", labelRange: "F4:I4", valueRange: "F5:I6", formula: "=SUM('明细'!$P$5:$P$82)", format: "#,##0\"条\"" },
  { label: "指数型基金-股票关系", labelRange: "K4:N4", valueRange: "K5:N6", formula: "=SUM('明细'!$Q$5:$Q$82)", format: "#,##0\"条\"" },
  { label: "主动型重仓持仓市值", labelRange: "P4:S4", valueRange: "P5:S6", formula: "=SUM('明细'!$R$5:$R$82)", format: "#,##0.00\"亿元\"" },
  { label: "指数型重仓持仓市值", labelRange: "U4:W4", valueRange: "U5:W6", formula: "=SUM('明细'!$S$5:$S$82)", format: "#,##0.00\"亿元\"" },
];
for (const card of cards) {
  summary.mergeCells(card.labelRange);
  summary.mergeCells(card.valueRange);
  summary.getRange(card.labelRange.split(":")[0]).values = [[card.label]];
  summary.getRange(card.valueRange.split(":")[0]).formulas = [[card.formula]];
  summary.getRange(card.labelRange).format = { fill: lightBlue, font: { bold: true, color: navy }, horizontalAlignment: "center", verticalAlignment: "center", borders: { preset: "outside", style: "thin", color: "#A6A6A6" } };
  summary.getRange(card.valueRange).format = { fill: "#FFFFFF", font: { bold: true, color: navy, size: 16 }, horizontalAlignment: "center", verticalAlignment: "center", numberFormat: card.format, borders: { preset: "outside", style: "thin", color: "#A6A6A6" } };
}
summary.mergeCells("A8:W9");
summary.getRange("A8").values = [["重要口径:原总数已包含主动型与指数型基金;本版按基金分类拆分。主动型=非指数型产品(含公募化资管),指数型含ETF、联接、普通指数及增强指数。当前仍是二季报前十大重仓股口径,并非8月底半年报完整持仓。"]];
summary.getRange("A8:W9").format = { fill: yellow, font: { color: "#7F6000", size: 10 }, wrapText: true, verticalAlignment: "center", borders: { preset: "outside", style: "thin", color: "#D6B656" } };
 
summary.getRange("A11:J11").values = [["排名","股票代码","股票简称","指数归属","总基金数","主动型基金数","指数型基金数","总持仓市值(亿元)","主动型持仓市值(亿元)","主动型占基金持仓"]];
summary.getRange("A11:J11").format = { fill: teal, font: { bold: true, color: "#FFFFFF" }, wrapText: true, horizontalAlignment: "center", verticalAlignment: "center" };
for (let i = 0; i < 50; i++) {
  const r = 12 + i;
  const d = 5 + i;
  summary.getRange(`A${r}:J${r}`).formulas = [[`='明细'!A${d}`, `='明细'!B${d}`, `='明细'!C${d}`, `='明细'!D${d}`, `='明细'!E${d}`, `='明细'!P${d}`, `='明细'!Q${d}`, `='明细'!G${d}`, `='明细'!R${d}`, `='明细'!T${d}`]];
}
summary.getRange("A12:A61").format.numberFormat = "#,##0";
summary.getRange("B12:B61").format.numberFormat = "@";
summary.getRange("E12:G61").format.numberFormat = "#,##0";
summary.getRange("H12:I61").format.numberFormat = "#,##0.00;[Red](#,##0.00);-";
summary.getRange("J12:J61").format.numberFormat = "0.00%";
summary.getRange("A11:J61").format.borders = { preset: "inside", style: "thin", color: "#D9E2F3" };
summary.getRange("I12:I61").conditionalFormats.add("dataBar", { color: "#70AD47", gradient: true });
summary.getRange("J12:J61").conditionalFormats.add("colorScale", { colors: ["#FFFFFF", "#FFE699", "#70AD47"], thresholds: ["min", "50%", "max"] });
 
summary.getRange("A66:B66").values = [["股票", "主动型持仓市值(亿元)"]];
summary.getRange("D66:E66").values = [["股票", "主动型基金数"]];
const detailRowByCode = new Map(data.map((item, index) => [item.code, index + 5]));
const activeValueTop = [...data].sort((a, b) => b.activeHoldValueYi - a.activeHoldValueYi || b.activeFundCount - a.activeFundCount || a.code.localeCompare(b.code));
const fundCountTop = [...data].sort((a, b) => b.activeFundCount - a.activeFundCount || b.activeHoldValueYi - a.activeHoldValueYi || a.code.localeCompare(b.code));
for (let i = 0; i < 15; i++) {
  const s = 67 + i;
  const activeValueRow = detailRowByCode.get(activeValueTop[i].code);
  const fundRow = detailRowByCode.get(fundCountTop[i].code);
  summary.getRange(`A${s}:B${s}`).formulas = [[`='明细'!C${activeValueRow}`, `='明细'!R${activeValueRow}`]];
  summary.getRange(`D${s}:E${s}`).formulas = [[`='明细'!C${fundRow}`, `='明细'!P${fundRow}`]];
}
const valueChart = summary.charts.add("bar", summary.getRange("A66:B81"));
valueChart.title = "主动型重仓持仓市值TOP15(亿元)";
valueChart.hasLegend = false;
valueChart.xAxis = { axisType: "textAxis", textStyle: { fontSize: 9 } };
valueChart.yAxis = { numberFormatCode: "#,##0", textStyle: { fontSize: 9 } };
valueChart.setPosition("L11", "Q27");
const countChart = summary.charts.add("bar", summary.getRange("D66:E81"));
countChart.title = "主动型基金数TOP15(只)";
countChart.hasLegend = false;
countChart.xAxis = { axisType: "textAxis", textStyle: { fontSize: 9 } };
countChart.yAxis = { numberFormatCode: "#,##0", textStyle: { fontSize: 9 } };
countChart.setPosition("R11", "W27");
 
summary.getRange("A63:W63").merge();
summary.getRange("A63").values = [["摘要展示前50;完整78只股票拆分见“明细”,逐基金持仓见“基金级明细”,数据解释与限制见“来源与口径”。"]];
summary.getRange("A63:W63").format = { fill: pale, font: { italic: true, color: gray }, wrapText: true };
summary.freezePanes.freezeRows(2);
const summaryWidths = { A: 7, B: 11, C: 12, D: 13, E: 10, F: 12, G: 12, H: 16, I: 18, J: 16, K: 2, L: 10, M: 10, N: 10, O: 10, P: 10, Q: 10, R: 10, S: 10, T: 10, U: 10, V: 10, W: 10 };
for (const [col, width] of Object.entries(summaryWidths)) summary.getRange(`${col}:${col}`).format.columnWidth = width;
 
// 来源与口径
sources.mergeCells("A1:F1");
sources.getRange("A1").values = [["来源、定义与限制"]];
sources.getRange("A1:F1").format = { fill: navy, font: { bold: true, color: "#FFFFFF", size: 16 } };
sources.getRange("A3:F3").values = [["来源ID","用途","日期/期间","数据提供方","URL","说明"]];
sources.getRange("A3:F3").format = { fill: teal, font: { bold: true, color: "#FFFFFF" }, wrapText: true };
const sourceData = [
  ["CSI-H30590","中证机器人指数成分股与权重","2026-06-30","中证指数有限公司","https://oss-ch.csindex.com.cn/static/html/csindex/public/uploads/file/autofile/closeweight/H30590closeweight.xls","官方样本权重表;64只"],
  ["CNI-980022","国证机器人产业指数成分股","2026-07-23(6月调样后)","深圳证券信息有限公司","https://www.cnindex.com.cn/sample-detail/detail?indexcode=980022&dateStr=2026-07&pageNum=1&rows=100&isFirstCall=1","官方当前成分表;50只"],
  ["EM-Q2-2026","公募基金重仓持股汇总","2026-06-30","东方财富Choice数据","https://data.eastmoney.com/zlsj/bx.html","字段包括持有基金数、持股数、持仓市值、占总股本/流通股比例、环比持股变化"],
  ["EM-API","逐股汇总接口","2026-06-30","东方财富数据中心","https://data.eastmoney.com/dataapi/zlsj/list?date=2026-06-30&type=1&zjc=0&sortField=HOLD_VALUE&sortDirec=1&pageNum=1&pageSize=500","本工作簿抓取全部分页后与78只指数成分股并集匹配"],
  ["EM-FUND-DETAIL","逐股票的持仓基金明细","2026-06-30","东方财富数据中心","https://data.eastmoney.com/dataapi/zlsj/detail?SHType=1&SCode={股票代码}&ReportDate=2026-06-30&pageNum=1&pageSize=200","用于拆分每只股票的主动型/指数型基金数量及持仓市值;逐行来源链接见基金级明细"],
  ["EM-FUND-CATALOG","基金名称与基金分类","抓取日2026-07-24","天天基金网","https://fund.eastmoney.com/js/fundcode_search.js","指数型按基金分类及名称识别;增强指数归入指数型,其余非指数产品归入主动型"],
];
sources.getRange("A4:F9").values = sourceData;
sources.getRange("A4:F9").format.wrapText = true;
sources.getRange("4:9").format.rowHeight = 46;
sources.getRange("E4:E9").format.font = { color: "#FF0000" };
sources.getRange("A11:F11").merge();
sources.getRange("A11").values = [["统计定义"]];
sources.getRange("A11:F11").format = { fill: lightBlue, font: { bold: true, color: navy } };
const definitions = [
  ["机器人股票范围","H30590与980022成分股去重后的并集,共78只。","同一股票若同时属于两个指数,指数归属标为“双指数”。"],
  ["持有基金数","东方财富Choice在该报告期统计的基金数量。","是逐股票的基金数;不同股票之间不可简单去重,汇总后称“股票-基金持仓关系”。"],
  ["主动型基金","持仓基金明细中的非指数型产品,包括股票型、混合型、灵活配置及公募化资管产品。","采用“非指数型”操作口径;未进一步按权益仓位、投资风格或是否量化细分。"],
  ["指数型基金","基金分类为指数型,或名称含ETF、指数、联接的产品。","包含普通指数、ETF、ETF联接及增强指数;增强指数未归入主动型。"],
  ["期末持仓市值","进入公开重仓统计的基金持股数量×2026-06-30股价。","是公开重仓口径下限,不代表全市场公募实际完整持仓。"],
  ["占股票总市值","基金披露持股数÷公司总股本,等价于持仓市值÷总市值。","表中直接采用数据源TOTALSHARES_RATIO字段。"],
  ["Q2净增减持估算","二季度披露持股数量变化×2026-06-30收盘价。","不是实际买卖成交额,忽略成交时点、成交价格及季度内往返交易。"],
  ["0只基金的解释","未进入当前基金重仓统计。","不等于所有基金都完全没有持有;基金二季报通常只披露前十大重仓股。"],
];
sources.getRange("A12:C20").values = [["项目","定义","限制/解释"], ...definitions];
sources.getRange("A12:C12").format = { fill: teal, font: { bold: true, color: "#FFFFFF" } };
sources.getRange("A12:C20").format.wrapText = true;
sources.getRange("13:20").format.rowHeight = 58;
sources.getRange("A21:F21").merge();
sources.getRange("A21").values = [["更新提示:截至2026-07-24,当前统计仍以基金二季报前十大重仓股为主。完整半年报通常要到8月底披露,届时可用全部持仓重新统计,结果通常会高于当前口径。"]];
sources.getRange("A21:F21").format = { fill: yellow, font: { color: "#7F6000" }, wrapText: true };
sources.getRange("21:21").format.rowHeight = 34;
const sourceWidths = { A: 20, B: 42, C: 44, D: 24, E: 75, F: 48 };
for (const [col, width] of Object.entries(sourceWidths)) sources.getRange(`${col}:${col}`).format.columnWidth = width;
sources.getRange("A1:F21").format.font = { name: "Microsoft YaHei", size: 10 };
sources.freezePanes.freezeRows(3);
 
// 检查页
checks.mergeCells("A1:G1");
checks.getRange("A1").values = [["数据与公式检查"]];
checks.getRange("A1:G1").format = { fill: navy, font: { bold: true, color: "#FFFFFF", size: 16 } };
checks.getRange("A3:G3").values = [["检查项","实际值","预期值","差异","容差","状态","说明"]];
checks.getRange("A3:G3").format = { fill: teal, font: { bold: true, color: "#FFFFFF" } };
const checkRows = [
  ["股票并集行数", "=COUNTA('明细'!$B$5:$B$82)", 78, "=B4-C4", 0, "=IF(ABS(D4)<=E4,\"OK\",\"FAIL\")", "H30590与980022去重并集"],
  ["中证成分股数量", "=COUNTIF('明细'!$D$5:$D$82,\"中证H30590\")+COUNTIF('明细'!$D$5:$D$82,\"双指数\")", 64, "=B5-C5", 0, "=IF(ABS(D5)<=E5,\"OK\",\"FAIL\")", "官方H30590样本数"],
  ["国证成分股数量", "=COUNTIF('明细'!$D$5:$D$82,\"国证980022\")+COUNTIF('明细'!$D$5:$D$82,\"双指数\")", 50, "=B6-C6", 0, "=IF(ABS(D6)<=E6,\"OK\",\"FAIL\")", "官方980022样本数"],
  ["进入重仓统计股票数", "=COUNTIF('明细'!$E$5:$E$82,\">0\")", data.filter((x) => x.fundCount > 0).length, "=B7-C7", 0, "=IF(ABS(D7)<=E7,\"OK\",\"FAIL\")", "来源匹配检查"],
  ["期末持仓市值合计(亿元)", "=SUM('明细'!$G$5:$G$82)", data.reduce((s, x) => s + x.holdValueYi, 0), "=B8-C8", 0.01, "=IF(ABS(D8)<=E8,\"OK\",\"FAIL\")", "公式汇总与源数据汇总比对"],
  ["主动+指数关系加总", "=SUM('明细'!$P$5:$Q$82)", "=SUM('明细'!$E$5:$E$82)", "=B9-C9", 0, "=IF(ABS(D9)<=E9,\"OK\",\"FAIL\")", "分类关系数必须等于原总基金数"],
  ["基金级明细行数", `=COUNTA('基金级明细'!$D$5:$D$${fundDetailLastRow})`, "=SUM('明细'!$E$5:$E$82)", "=B10-C10", 0, "=IF(ABS(D10)<=E10,\"OK\",\"FAIL\")", "每条股票-基金关系对应一行"],
  ["主动+指数市值加总(亿元)", "=SUM('明细'!$R$5:$S$82)", "=SUM('明细'!$G$5:$G$82)", "=B11-C11", 0.02, "=IF(ABS(D11)<=E11,\"OK\",\"FAIL\")", "分类市值与原汇总市值比对"],
  ["基金级明细市值加总(亿元)", `=SUM('基金级明细'!$J$5:$J$${fundDetailLastRow})`, "=SUM('明细'!$G$5:$G$82)", "=B12-C12", 0.02, "=IF(ABS(D12)<=E12,\"OK\",\"FAIL\")", "逐基金市值与原汇总市值比对"],
  ["持有基金数非负", "=MIN('明细'!$E$5:$E$82)", 0, "=B13-C13", 0, "=IF(B13>=C13,\"OK\",\"FAIL\")", "基础合理性"],
  ["占总股本比例上限", "=MAX('明细'!$H$5:$H$82)", 1, "=MAX(B14-C14,0)", 0, "=IF(B14<=C14,\"OK\",\"FAIL\")", "比例应不超过100%"],
];
for (let i = 0; i < checkRows.length; i++) {
  const row = 4 + i;
  const [label, actualFormula, expected, diffFormula, tolerance, statusFormula, note] = checkRows[i];
  checks.getRange(`A${row}:G${row}`).values = [[label, null, typeof expected === "string" && expected.startsWith("=") ? null : expected, null, tolerance, null, note]];
  checks.getRange(`B${row}`).formulas = [[actualFormula]];
  if (typeof expected === "string" && expected.startsWith("=")) checks.getRange(`C${row}`).formulas = [[expected]];
  checks.getRange(`D${row}`).formulas = [[diffFormula]];
  checks.getRange(`F${row}`).formulas = [[statusFormula]];
}
const checkLastRow = checkRows.length + 3;
const overallCheckRow = checkLastRow + 2;
checks.getRange(`B4:E${checkLastRow}`).format.numberFormat = "#,##0.00;[Red](#,##0.00);-";
checks.getRange(`F4:F${checkLastRow}`).conditionalFormats.add("containsText", { text: "OK", format: { fill: green, font: { bold: true, color: "#006100" } } });
checks.getRange(`F4:F${checkLastRow}`).conditionalFormats.add("containsText", { text: "FAIL", format: { fill: red, font: { bold: true, color: "#9C0006" } } });
checks.mergeCells(`A${overallCheckRow}:E${overallCheckRow}`);
checks.getRange(`A${overallCheckRow}`).values = [["总体状态"]];
checks.mergeCells(`F${overallCheckRow}:G${overallCheckRow}`);
checks.getRange(`F${overallCheckRow}`).formulas = [[`=IF(COUNTIF(F4:F${checkLastRow},\"FAIL\")=0,\"OK\",\"FAIL\")`]];
checks.getRange(`A${overallCheckRow}:E${overallCheckRow}`).format = { fill: lightBlue, font: { bold: true, color: navy } };
checks.getRange(`F${overallCheckRow}:G${overallCheckRow}`).format = { font: { bold: true, size: 14 }, horizontalAlignment: "center" };
checks.getRange(`F${overallCheckRow}:G${overallCheckRow}`).conditionalFormats.add("containsText", { text: "OK", format: { fill: green, font: { bold: true, color: "#006100" } } });
checks.getRange(`F${overallCheckRow}:G${overallCheckRow}`).conditionalFormats.add("containsText", { text: "FAIL", format: { fill: red, font: { bold: true, color: "#9C0006" } } });
const checkWidths = { A: 28, B: 18, C: 18, D: 14, E: 12, F: 12, G: 45 };
for (const [col, width] of Object.entries(checkWidths)) checks.getRange(`${col}:${col}`).format.columnWidth = width;
checks.getRange(`A1:G${overallCheckRow}`).format.font = { name: "Microsoft YaHei", size: 10 };
checks.freezePanes.freezeRows(3);
 
// 全局字体与导出前验证
summary.getRange("A1:W81").format.font = { name: "Microsoft YaHei" };
await fs.mkdir(outputDir, { recursive: true });
console.log("CLASSIFICATION_SUMMARY");
console.log(JSON.stringify({
  relations: fundRelations.length,
  activeRelations: fundRelations.filter((x) => x.managementType === "主动型").length,
  indexRelations: fundRelations.filter((x) => x.managementType === "指数型").length,
  uniqueActiveFunds: uniqueActiveFunds.size,
  uniqueIndexFunds: uniqueIndexFunds.size,
  activeValueYi: data.reduce((sum, x) => sum + x.activeHoldValueYi, 0),
  indexValueYi: data.reduce((sum, x) => sum + x.indexHoldValueYi, 0),
  totalValueYi: data.reduce((sum, x) => sum + x.holdValueYi, 0),
  unmatchedFundCatalogRows: fundRelations.filter((x) => x.fundType === "未匹配").length,
}));
const summaryInspect = await workbook.inspect({ kind: "table", range: "摘要!A1:J16", include: "values,formulas", tableMaxRows: 16, tableMaxCols: 10, maxChars: 7000 });
console.log("SUMMARY_INSPECT");
console.log(summaryInspect.ndjson);
const summaryTailInspect = await workbook.inspect({ kind: "table", range: "摘要!A55:J63", include: "values,formulas", tableMaxRows: 9, tableMaxCols: 10, maxChars: 6000 });
console.log("SUMMARY_TAIL_INSPECT");
console.log(summaryTailInspect.ndjson);
const errorScan = await workbook.inspect({ kind: "match", searchTerm: "#REF!|#DIV/0!|#VALUE!|#NAME\\?|#N/A", options: { useRegex: true, maxResults: 300 }, summary: "final formula error scan" });
console.log("ERROR_SCAN");
console.log(errorScan.ndjson);
const fundDetailInspect = await workbook.inspect({ kind: "table", range: "基金级明细!A1:N10", include: "values,formulas", tableMaxRows: 10, tableMaxCols: 14, maxChars: 7000 });
console.log("FUND_DETAIL_INSPECT");
console.log(fundDetailInspect.ndjson);
const checksInspect = await workbook.inspect({ kind: "table", range: `检查!A3:G${overallCheckRow}`, include: "values,formulas", tableMaxRows: overallCheckRow, tableMaxCols: 7, maxChars: 7000 });
console.log("CHECKS_INSPECT");
console.log(checksInspect.ndjson);
const summaryPreview = await workbook.render({ sheetName: "摘要", range: "A1:W33", scale: 1.2, format: "png" });
await fs.writeFile(`${outputDir}/summary_preview.png`, new Uint8Array(await summaryPreview.arrayBuffer()));
const summaryTop50Preview = await workbook.render({ sheetName: "摘要", range: "A28:J63", scale: 1.2, format: "png" });
await fs.writeFile(`${outputDir}/summary_top50_preview.png`, new Uint8Array(await summaryTop50Preview.arrayBuffer()));
const detailPreview = await workbook.render({ sheetName: "明细", range: "A1:V25", scale: 1, format: "png" });
await fs.writeFile(`${outputDir}/detail_preview.png`, new Uint8Array(await detailPreview.arrayBuffer()));
const fundDetailPreview = await workbook.render({ sheetName: "基金级明细", range: "A1:N20", scale: 1, format: "png" });
await fs.writeFile(`${outputDir}/fund_detail_preview.png`, new Uint8Array(await fundDetailPreview.arrayBuffer()));
const sourcesPreview = await workbook.render({ sheetName: "来源与口径", range: "A1:F21", scale: 1, format: "png" });
await fs.writeFile(`${outputDir}/sources_preview.png`, new Uint8Array(await sourcesPreview.arrayBuffer()));
const checksPreview = await workbook.render({ sheetName: "检查", range: `A1:G${overallCheckRow}`, scale: 1.2, format: "png" });
await fs.writeFile(`${outputDir}/checks_preview.png`, new Uint8Array(await checksPreview.arrayBuffer()));
const xlsx = await SpreadsheetFile.exportXlsx(workbook);
await xlsx.save(outputPath);
console.log(`OUTPUT=${outputPath}`);