教程电子表格

如何在 Google Sheets 中获取实时金价

无需保存 API 密钥,即可将实时和历史金价导入 Google Sheets,对比参考报价,并每 4 小时安全刷新。

2026年9月8日 · 阅读时间:8 分钟

添加 Apps Script 后,在任意单元格输入 =GOLDPRICE(),即可获取 goldprice.dev 的 XAU/USD 实时金价,单位为每金衡盎司。免费价格延迟 15 分钟。IMPORTDATA 无法读取此 JSON 端点,因此需要自定义函数。GOLDHISTORY() 返回 30 天的每日收盘价;GOLDPRICEDELTA() 返回相对于已统一币种和单位的参考单元格的价格差及百分比差,保留正负号。脚本不保存 API 密钥。运行一次 installGoldRefresh(),即可绑定一个刷新单元格,并创建符合额度限制的四小时触发器。

开始使用,无需在表格中保存 API 密钥

XAU 现货端点和 30 天的每日历史数据均支持匿名访问,因此本教程不会在 Apps Script 中保存凭据。匿名访问遵循 Free 限额:每个 IP 每分钟 10 次请求、每小时 100 次、每月 5,000 次。每四小时刷新的工作簿可控制在此额度内。

多个 Sheets 用户可能共用 Google 的出口 IP,因此可用的匿名额度会有变化。如果共享或生产工作簿需要稳定的账户额度,请通过自己的服务端代理调用,并将 goldprice.dev 密钥保存在代理端。不要将 bearer token 粘贴到其他编辑者可查看的绑定脚本中。免费账户密钥同样提供每月 5,000 次调用,额度归属账户,不与其他用户共用 IP 额度。

获取独立的请求额度

以下步骤无需密钥即可使用。免费 API 密钥可提供独立额度。更长的历史数据和更高的限额需要付费套餐。

将三个金价函数添加到 Apps Script

打开表格,点击扩展程序 → Apps Script。删除默认的 function myFunction() {},粘贴下方代码。代码定义了三个函数:GOLDPRICE(metal, currency, refreshToken) 获取当前现货价,GOLDHISTORY(fromDate, toDate, refreshToken) 获取可直接绘图的每日收盘价,GOLDPRICEDELTA(referencePrice, metal, currency, refreshToken) 计算保留正负号的价格差和百分比差。这些函数通过 CacheService 缓存相同请求,不发送 Authorization 请求头,并返回普通错误字符串,不抛出异常。

如需在同一工作簿中记录持有数量、买入成本和参考估值,请使用黄金投资组合跟踪模板。

Code.gs
// google-sheets: Apps Script Code.gsconst SPOT_CACHE_TTL = 55; // Cache successful spot responses for 55 seconds.const BARS_CACHE_TTL = 300; // Cache successful daily bars for 5 minutes.const REFRESH_RANGE_NAME = 'GOLDPRICE_REFRESH_TOKEN';const SPREADSHEET_ID_KEY = 'GOLDPRICE_SPREADSHEET_ID';const INSTALLER_EMAIL_KEY = 'GOLDPRICE_INSTALLER_EMAIL';const LAST_REFRESH_AT_KEY = 'GOLDPRICE_LAST_REFRESH_AT';const MIN_REFRESH_INTERVAL_MS = 4 * 60 * 60 * 1000;/** * Returns the current spot price for a metal/currency pair. * @customfunction */function GOLDPRICE(metal, currency, refreshToken) {  const symbol = metal + '-' + currency + '-SPOT';  const cache = CacheService.getScriptCache();  const cacheKey = 'spot_' + symbol;  const cached = cache.get(cacheKey);  if (cached) return cached;  const url = `https://api.goldprice.dev/v1/spot/${symbol}`;  const resp = UrlFetchApp.fetch(url, {    muteHttpExceptions: true,  });  const code = resp.getResponseCode();  if (code === 401) return '#ACCESS_ERROR: use a server-side proxy';  if (code === 403) return '#PLAN_GATED: symbol not on your tier';  if (code === 429) return '#RATE_LIMIT: wait a minute and retry';  if (code !== 200) return '#ERROR_' + code;  const data = JSON.parse(resp.getContentText());  if (!data || !data.price) return '#NO_DATA';  // Price stays a string, same as the API wire format: Sheets displays and  // sorts it fine. Wrap in VALUE() if you need it in arithmetic.  cache.put(cacheKey, data.price, SPOT_CACHE_TTL);  return data.price;}/** * Compares live spot with a reference cell in the same currency and unit. * Spills [signed price delta, signed percentage delta] into two cells. * @customfunction */function GOLDPRICEDELTA(referencePrice, metal, currency, refreshToken) {  const reference = Number(referencePrice);  if (!Number.isFinite(reference) || reference <= 0) {    return [['#BAD_REFERENCE: enter a positive price', '']];  }  const liveResult = GOLDPRICE(metal, currency, refreshToken);  const live = Number(liveResult);  if (!Number.isFinite(live)) return [[liveResult, '']];  const signedDelta = live - reference;  return [[Number(signedDelta.toFixed(2)), signedDelta / reference]];}/** * Returns chart-ready daily closes for a date range. * @customfunction */function GOLDHISTORY(fromDate, toDate, refreshToken) {  const now = new Date();  const rollingFrom = new Date(now.getTime() - 29 * 24 * 60 * 60 * 1000);  const from = toIsoDate_(    fromDate === '' || fromDate == null ? rollingFrom : fromDate  );  const to = toIsoDate_(toDate === '' || toDate == null ? now : toDate);  if (!from || !to) return [['#BAD_DATE: put real dates in B2/B3']];  const cache = CacheService.getScriptCache();  const cacheKey = 'bars_' + from + '_' + to;  const cached = cache.get(cacheKey);  if (cached) return JSON.parse(cached);  const url = 'https://api.goldprice.dev/v1/bars?symbol=XAU-USD-SPOT&interval=1d'    + '&from=' + from + '&to=' + to;  const resp = UrlFetchApp.fetch(url, {    muteHttpExceptions: true,  });  const code = resp.getResponseCode();  if (code === 401) return [['#ACCESS_ERROR: use a server-side proxy']];  if (code === 403) return [['#PLAN_GATED: range not on your tier']];  if (code === 429) return [['#RATE_LIMIT: wait a minute and retry']];  if (code !== 200) return [['#ERROR_' + code]];  const data = JSON.parse(resp.getContentText());  const bars = Array.isArray(data.bars) ? data.bars : [];  if (bars.length === 0) return [['#NO_DATA: empty range']];  // /v1/bars returns bars newest-first; reverse to chronological order so  // a line chart built from this range reads left-to-right in time.  const rows = bars.slice().reverse().map(function (bar) {    return [bar.bar_start.slice(0, 10), Number(bar.close)];  });  cache.put(cacheKey, JSON.stringify(rows), BARS_CACHE_TTL);  return rows;}function installGoldRefresh() {  const lock = LockService.getScriptLock();  lock.waitLock(30000);  try {    const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();    if (!spreadsheet) throw new Error('Open the bound Google Sheet and run again');    const installerEmail = Session.getEffectiveUser().getEmail();    if (!installerEmail) throw new Error('Run the installer from your Google account');    const properties = PropertiesService.getScriptProperties();    const existingInstaller = properties.getProperty(INSTALLER_EMAIL_KEY);    if (existingInstaller && existingInstaller !== installerEmail) {      throw new Error('Gold refresh is already owned by another editor');    }    const refreshRange = spreadsheet.getActiveSheet().getRange('B1');    spreadsheet.setNamedRange(REFRESH_RANGE_NAME, refreshRange);    // getProjectTriggers() only returns this user's triggers. The shared    // installer-email claim above prevents a second editor from creating a    // second schedule, while this replacement keeps owner reruns idempotent.    ScriptApp.getProjectTriggers()      .filter(function (trigger) {        return trigger.getHandlerFunction() === 'refreshGoldData';      })      .forEach(function (trigger) {        ScriptApp.deleteTrigger(trigger);      });    ScriptApp.newTrigger('refreshGoldData')      .timeBased()      .everyHours(4)      .create();    properties.setProperties({      [SPREADSHEET_ID_KEY]: spreadsheet.getId(),      [INSTALLER_EMAIL_KEY]: installerEmail,    });  } finally {    lock.releaseLock();  }  refreshGoldData();}function uninstallGoldRefresh() {  const lock = LockService.getScriptLock();  lock.waitLock(30000);  try {    const properties = PropertiesService.getScriptProperties();    const installerEmail = Session.getEffectiveUser().getEmail();    const existingInstaller = properties.getProperty(INSTALLER_EMAIL_KEY);    if (existingInstaller && existingInstaller !== installerEmail) {      throw new Error('Only the trigger owner can uninstall gold refresh');    }    ScriptApp.getProjectTriggers()      .filter(function (trigger) {        return trigger.getHandlerFunction() === 'refreshGoldData';      })      .forEach(function (trigger) {        ScriptApp.deleteTrigger(trigger);      });    properties.deleteProperty(SPREADSHEET_ID_KEY);    properties.deleteProperty(INSTALLER_EMAIL_KEY);    properties.deleteProperty(LAST_REFRESH_AT_KEY);  } finally {    lock.releaseLock();  }}function refreshGoldData(event) {  const lock = LockService.getScriptLock();  lock.waitLock(30000);  try {    // Clock triggers pass an event object, but they cannot supply the metal,    // currency, date-range, or reference arguments required by sheet formulas.    // This wrapper only changes the named token cell those formulas reference.    const properties = PropertiesService.getScriptProperties();    const spreadsheetId = properties.getProperty(SPREADSHEET_ID_KEY);    if (!spreadsheetId) throw new Error('Run installGoldRefresh first');    // Script properties and the script lock are shared across editors. This    // gate coalesces legacy triggers owned by different users into one refresh    // per four hours even though Apps Script cannot enumerate their triggers.    const now = Date.now();    const lastRefreshAt = Number(properties.getProperty(LAST_REFRESH_AT_KEY));    if (lastRefreshAt && now - lastRefreshAt < MIN_REFRESH_INTERVAL_MS) return;    const spreadsheet = SpreadsheetApp.openById(spreadsheetId);    const refreshRange = spreadsheet.getRangeByName(REFRESH_RANGE_NAME);    if (!refreshRange) throw new Error('Run installGoldRefresh to restore B1');    refreshRange.setValue(new Date(now).toISOString());    properties.setProperty(LAST_REFRESH_AT_KEY, String(now));  } finally {    lock.releaseLock();  }}function toIsoDate_(value) {  const d = value instanceof Date ? value : new Date(value);  if (isNaN(d.getTime())) return null;  return d.toISOString().slice(0, 10);}

设置价格、历史数据和参考报价单元格

在 B1 中填入刷新标记,可在 C1 中填入以 USD 计价的每金衡盎司参考报价。将 B2 和 B3 留空,可获取滚动 30 个自然日的历史数据,也可输入固定日期。GOLDPRICEDELTA() 返回实时价减去参考价的价格差和百分比差,保留正负号。只有两个单元格的币种和单位一致时,对比才有效。

Google 表示,GOOGLEFINANCE 报价最多可能延迟 20 分钟,部分期货不受支持,且无法通过 Apps Script 访问历史结果。如需使用单独的参考报价,请先将其换算为每金衡盎司 USD,再填入 C1;程序调用的现货价和历史数据由 goldprice.dev 提供。不要将 NOW() 或 TODAY() 作为自定义函数的参数:当 B2/B3 留空时,GOLDHISTORY() 会在内部计算滚动日期。

Sheets 公式 · B1/B2/B3/C1 单元格关联
// B1:刷新标记,由 refreshGoldData() 写入// B2/B3:留空可使用滚动的 30 个日历日范围,也可输入固定日期// C1:可选的参考报价,统一换算为每金衡盎司的 USD 价格// 当前 USD 现货价,每次 B1 变化时重新计算=GOLDPRICE("XAU", "USD", $B$1)// 实时现货价减去 C1:溢出结果为 [带正负号的 USD 差值, 带正负号的百分比差值]// 将第二个结果单元格的格式设为百分比。=GOLDPRICEDELTA($C$1, "XAU", "USD", $B$1)// 当前 EUR 现货价,同一刷新标记单元格触发两项计算=GOLDPRICE("XAU", "EUR", $B$1)// 30 天的每日收盘价,可直接用于图表(日期,收盘价)=GOLDHISTORY($B$2, $B$3, $B$1)

根据历史数据创建折线图

选中 GOLDHISTORY() 展开的数据行,点击插入 → 图表,然后选择折线图。行数取决于请求的日期范围和可用的日线数据。函数已将 API 返回的从新到旧的数据反转为时间顺序,因此横轴会从旧到新排列,无需额外排序。

添加每 4 小时刷新的触发器

选择一个 Google 账户负责定时刷新。如果编辑者曾按旧版教程操作,每个人都需先在 Apps Script → 触发器中删除自己的 refreshGoldData 条目。然后运行一次 installGoldRefresh 并授权。安装函数会绑定 B1,创建一个四小时触发器。共享锁和时间戳会合并无法访问的旧触发器调用,确保每四小时只刷新一次。

计划交接时,由原负责人运行 uninstallGoldRefresh,再由新负责人安装。如果原负责人无法操作,请先在项目设置 → 脚本属性中删除三个 GOLDPRICE_* 条目,再重新安装;共享间隔检查仍会防止重复调用。四个公式 × 每天六次刷新,最多为每天 24 次调用、每月约 720 次,低于匿名 Free 每个 IP 每小时 100 次、每月 5,000 次的限额。

预期输出

JSON
{  "symbol": "XAU",  "quote_currency": "USD",  "unit": "troy_ounce",  "contract_type": "spot",  "price": "4012.75",  "is_stale": false,  "computed_at": "2026-10-23T07:00:18Z"}

GOLDPRICE 将上方的 price 字段显示为文本,例如 "4012.75"。GOLDPRICEDELTA 将保留正负号的价格差和以小数表示的百分比差展开到两个单元格,请将第二个设为百分比格式。GOLDHISTORY 填充请求范围内可用的 [date, close] 数据行。

常见错误

代码现象解决方法
401单元格显示 #ACCESS_ERROR: use a server-side proxy匿名请求未被接受。先查看 Apps Script → 执行记录。如果工作簿需要通过身份验证获取额度,请通过服务端代理发送请求,不要将密钥粘贴到绑定脚本中。
403单元格显示 #PLAN_GATED: symbol not on your tierFree 支持黄金(XAU)、白银(XAG)和铜(HG)现货。请检查品种代码;目前不提供铂金和钯金。详见 /pricing。
429单元格显示 #RATE_LIMIT: wait a minute and retry共享 IP 已达到 Free 限额:每分钟 10 次请求、每小时 100 次或每月 5,000 次。缓存通常可避免此问题;请调大 TTL 常量、减少不同的公式,或通过服务端代理发送带身份验证的请求。
N/AGOLDHISTORY 显示 #BAD_DATE: put real dates in B2/B3B2 或 B3 中的文本无法被 Apps Script 解析为日期,例如填入了标签而非日期值。请输入实际日期,或引用通过格式 → 数字 → 日期设置为日期格式的单元格。
N/AGOLDPRICEDELTA 显示 #BAD_REFERENCE: enter a positive priceC1 为空、文本、零或负数。请输入正的参考价格,并使用与实时现货公式相同的币种和每金衡盎司单位。
N/A单元格一直显示 Loading…Apps Script 需要访问外部数据的授权。在编辑器工具栏中选择一个函数,手动运行一次以触发授权提示,然后同意授权。

进阶用法

  • 创建价格提醒公式:=IF(GOLDPRICE("XAU","USD",$B$1) > 4800, "ALERT", "OK")
  • 如需更早的每日历史数据,请使用带密钥的服务端代理:Basic 提供 1 年数据,Plus 和 Pro 提供完整历史数据
  • 使用 Free 同时跟踪黄金和白银,两者共用同一个刷新标记单元格
  • 将可直接绘图的 GOLDHISTORY 数据行用于回测,参阅使用 OHLC 数据回测黄金策略
  • 在估值或成本模型中引用 =GOLDPRICE(...),让模型随实时数据更新,无需写死价格

常见问题

价格多久更新一次?

现货端点每 60 秒刷新一次,提供获授权的实时现货数据。Free 和无密钥访问的价格延迟 15 分钟,付费套餐提供实时价格。GOLDPRICE 的缓存遵循此更新频率,在 TTL 内重复调用会返回相同值,不会额外消耗 API 调用次数。

每 4 小时刷新会超出免费额度吗?

不会。四个公式绑定到 $B$1 刷新单元格:两个 GOLDPRICE、一个 GOLDPRICEDELTA 和一个 GOLDHISTORY。最坏情况下,每 4 小时触发一次,每天 6 次 × 4 次调用 = 每天 24 次,每月约 720 次。GOLDPRICEDELTA 会复用 USD 现货请求,因此缓存通常还能减少调用。此用量低于匿名访问每个 IP 每分钟 10 次、每小时 100 次、每月 5,000 次的限额。

为什么不用 GOOGLEFINANCE 获取金价?

GOOGLEFINANCE 用于受支持的证券和货币数据,并非有文档说明的 XAU/USD 现货 API。Google 表示,报价最多可能延迟 20 分钟,部分期货不受支持,且无法通过 Apps Script 访问历史数据。请用 GOLDPRICE() 获取现货价、GOLDHISTORY() 获取脚本可读取的历史数据,并用 GOLDPRICEDELTA() 对比已统一币种和单位的参考报价,不应假定两种金融工具等同。

为什么触发器调用 refreshGoldData,而非 GOLDPRICE?

Apps Script 会向所选处理函数传入时钟事件对象,但无法提供表格公式所需的金属、币种、日期范围或参考价格参数。因此,installGoldRefresh() 会为 refreshGoldData(event) 创建触发器。该函数只向命名的 B1 区域写入新时间戳,让四个公式重新计算。

可以与其他编辑者共享此表格吗?

可以。此示例使用匿名访问,不保存 API 密钥,因此编辑者查看或复制绑定脚本时不会获得凭据。如果工作簿以后需要带身份验证的额度,请将 bearer token 保存在服务端代理中,再由表格调用该服务。

可以商用吗?

所有套餐均允许商用。付费套餐无需添加署名链接;使用 Free 时,必须在价格附近放置清晰可见的 goldprice.dev 链接。

支持哪些金属?

所有套餐均支持黄金(XAU)、白银(XAG)和铜(HG)现货,包括 Free。任何套餐均不提供铂金和钯金。完整说明见 API 参考文档。

免费套餐真的免费吗?

是的。匿名 XAU 现货和 30 天的每日历史数据无需密钥,遵循 Free 限额:每个 IP 每分钟 10 次请求、每小时 100 次、每月 5,000 次。如果服务端集成需要账户额度,可申请免费账户密钥,每月额度相同,无需信用卡,也不会过期。

Claude 等 AI 助手可以帮我写吗?

可以。让 Claude、Cursor 或 ChatGPT 读取 goldprice.dev MCP 服务器或其 llms.txt,然后提出要求:编写调用 api.goldprice.dev 的 Google Sheets 自定义函数,并提供可重复运行的安装函数,绑定命名刷新单元格,创建一个四小时的时钟触发器。助手读取实时接口结构后,生成的代码可直接使用正确的字段名和错误代码。

Apps Script 能在 Excel 中使用吗?

不能。Excel 使用 Power Query M,不使用 Apps Script。具体设置方法请参阅 Excel 教程。

继续阅读

从免费 API 密钥开始

免费套餐每月提供 5,000 次请求。无需信用卡。

获取免费密钥