=GOLDPRICE() devuelve en cualquier celda el precio actual XAU/USD por onza troy de goldprice.dev tras añadir Apps Script. Los precios gratuitos llevan 15 minutos de retraso. IMPORTDATA no lee este endpoint JSON: necesitas una función personalizada. GOLDHISTORY() devuelve cierres diarios de 30 días; GOLDPRICEDELTA(), diferencias de precio y porcentaje con signo frente a una referencia normalizada. No se guarda ninguna clave. Ejecuta installGoldRefresh() una vez para vincular la celda de actualización y crear un activador cada cuatro horas que respete la cuota.
Empieza sin guardar una clave de API en la hoja
El endpoint de precio al contado XAU y el historial diario de 30 días funcionan sin identificarte. Este tutorial no guarda credenciales en Apps Script. El acceso anónimo usa los límites de Free: 10 solicitudes por minuto, 100 por hora y 5.000 al mes por IP. Actualizar cada cuatro horas cabe en esa cuota.
Google puede compartir la IP de salida entre usuarios de Sheets, así que la capacidad varía. Para una cuota por cuenta, usa un proxy en tu servidor y guarda allí la clave. No pegues tokens bearer en scripts que otros editores puedan ver. Una clave gratuita asigna las mismas 5.000 llamadas mensuales a tu cuenta.
Los pasos siguientes funcionan sin clave. Una clave de API gratuita te da tu propia cuota. Para acceder a más historial y límites mayores, necesitas un plan de pago.
Añade las tres funciones de precios del oro a Apps Script
Abre tu hoja y selecciona Extensiones → Apps Script. Sustituye function myFunction() {} por el código siguiente. Incluye GOLDPRICE(metal, currency, refreshToken) para precios actuales, GOLDHISTORY(fromDate, toDate, refreshToken) para cierres diarios y GOLDPRICEDELTA(referencePrice, metal, currency, refreshToken) para diferencias de precio y porcentaje con signo. Usa CacheService para solicitudes idénticas, omite la cabecera Authorization y devuelve errores como texto sin lanzar excepciones.
Para reunir cantidades, costes de compra y valoraciones orientativas, usa la plantilla de seguimiento de una cartera de oro.
// 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);}Configura las celdas de precio, historial y referencia
Pon un valor de actualización en B1 y, opcionalmente, una referencia en USD por onza troy en C1. Deja B2 y B3 vacías para consultar los últimos 30 días naturales o introduce fechas fijas. GOLDPRICEDELTA() devuelve diferencias de precio actual menos referencia y porcentajes con signo. Ambas cotizaciones deben compartir moneda y unidad.
GOOGLEFINANCE puede llevar hasta 20 minutos de retraso, no admite todos los futuros ni permite consultar históricos desde Apps Script. Normaliza tu referencia a USD por onza troy en C1; goldprice.dev aporta precios actuales e históricos. No uses NOW() ni TODAY() como argumentos: GOLDHISTORY() calcula las fechas cuando B2/B3 están vacías.
// B1: token de actualización que escribe refreshGoldData()// B2/B3: deja en blanco para los últimos 30 días naturales o introduce fechas fijas// C1: cotización de referencia opcional normalizada a USD por onza troy// Precio spot actual en USD: se recalcula cuando cambia B1=GOLDPRICE("XAU", "USD", $B$1)// Spot en vivo menos C1: devuelve [diferencia en USD con signo, diferencia porcentual con signo]// Aplica el formato Porcentaje a la segunda celda del resultado.=GOLDPRICEDELTA($C$1, "XAU", "USD", $B$1)// Precio spot actual en EUR: ambos usan la misma celda del token=GOLDPRICE("XAU", "EUR", $B$1)// 30 días de cierres diarios para gráficos (fecha, cierre)=GOLDHISTORY($B$2, $B$3, $B$1)Crea un gráfico de líneas con el historial
Selecciona las filas que devuelve GOLDHISTORY(), abre Insertar → Gráfico y elige Gráfico de líneas. Las filas dependen del periodo solicitado y los datos diarios disponibles. La función ordena los registros de la API cronológicamente, así que el eje horizontal va del más antiguo al más reciente sin ordenar nada más.
Añade un activador cada 4 horas
Elige una cuenta de Google para gestionar la programación. Quienes siguieran el tutorial anterior deben borrar su activador refreshGoldData en Apps Script → Activadores. Ejecuta installGoldRefresh y autorízalo. Vincula B1 y crea un activador cada cuatro horas. Un bloqueo y una marca temporal compartidos limitan los activadores antiguos inaccesibles a una actualización cada cuatro horas.
Para cambiar responsable, ejecuta uninstallGoldRefresh y deja que el nuevo instale. Si el anterior no está disponible, borra las tres entradas GOLDPRICE_* en Configuración del proyecto → Propiedades del script. El control compartido evita duplicados. Cuatro fórmulas × seis actualizaciones: máximo 24 llamadas/día, unas 720/mes, dentro de Free: 100 solicitudes/hora y 5.000/mes por IP.
Resultado esperado
{ "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 muestra el campo price anterior como texto, por ejemplo, "4012.75". GOLDPRICEDELTA distribuye las diferencias de precio y porcentaje decimal con signo en dos celdas; aplica formato de porcentaje a la segunda. GOLDHISTORY devuelve filas [date, close] disponibles para el periodo solicitado.
Errores comunes
| Código | Qué ves | Solución |
|---|---|---|
| 401 | La celda muestra #ACCESS_ERROR: use a server-side proxy | La solicitud anónima no se ha aceptado. Revisa primero Apps Script → Ejecuciones. Si necesitas capacidad autenticada, envía las solicitudes mediante un proxy en tu servidor; no pegues una clave en el script vinculado. |
| 403 | La celda muestra #PLAN_GATED: symbol not on your tier | Free incluye precios al contado de oro (XAU), plata (XAG) y cobre (HG). Comprueba el símbolo; no se ofrecen platino ni paladio. Consulta /pricing. |
| 429 | La celda muestra #RATE_LIMIT: wait a minute and retry | La IP compartida alcanzó un límite de Free: 10 solicitudes/minuto, 100/hora o 5.000/mes. La caché suele evitarlo; aumenta las constantes TTL, reduce las fórmulas distintas o envía solicitudes autenticadas mediante un proxy en tu servidor. |
| N/A | GOLDHISTORY muestra #BAD_DATE: put real dates in B2/B3 | B2 o B3 contiene texto que Apps Script no reconoce como fecha, por ejemplo, una etiqueta. Introduce fechas válidas o referencia celdas con formato de fecha mediante Formato → Número → Fecha. |
| N/A | GOLDPRICEDELTA muestra #BAD_REFERENCE: enter a positive price | C1 está vacía o contiene texto, cero o un valor negativo. Introduce un precio de referencia positivo con la misma moneda y unidad por onza troy que la fórmula del precio actual. |
| N/A | La celda muestra Loading… indefinidamente | Apps Script necesita autorización para las solicitudes externas. Selecciona una función en la barra del editor y ejecútala manualmente para abrir la solicitud de autorización. Después, acepta. |
Más opciones
- Crea una fórmula de alerta de precio:
=IF(GOLDPRICE("XAU","USD",$B$1) > 4800, "ALERT", "OK") - Para consultar históricos diarios más antiguos, usa un proxy en tu servidor con clave: Basic incluye 1 año; Plus y Pro, todo el historial
- Sigue la plata junto al oro con Free y usa la misma celda de actualización para ambos
- Usa las filas de GOLDHISTORY para probar una estrategia con datos históricos: consulta cómo probar una estrategia de oro con datos OHLC
- Referencia
=GOLDPRICE(...)en un modelo de valoración o costes para actualizarlo con datos actuales