=GOLDPRICE() trả giá XAU/USD trực tiếp mỗi ounce troy từ goldprice.dev vào ô bất kỳ sau khi thêm Apps Script. Giá Free trễ 15 phút. IMPORTDATA không đọc được JSON này, nên cần hàm tùy chỉnh.
GOLDHISTORY() trả giá đóng cửa hằng ngày trong 30 ngày; GOLDPRICEDELTA() trả chênh lệch giá và tỷ lệ có dấu so với ô tham chiếu đã chuẩn hóa. Không lưu khóa API. Chạy installGoldRefresh() một lần để gắn ô làm mới và đặt lịch bốn giờ trong hạn mức.
Bắt đầu mà không lưu khóa API trong bảng tính
Endpoint giá giao ngay XAU và lịch sử hằng ngày trong 30 ngày đều dùng được khi không đăng nhập. Hướng dẫn này không lưu thông tin xác thực trong Apps Script. Truy cập ẩn danh theo hạn mức Free: 10 yêu cầu/phút, 100/giờ và 5.000/tháng mỗi IP. Làm mới mỗi bốn giờ nằm trong hạn mức.
Google có thể dùng chung IP, khiến hạn mức ẩn danh thay đổi. Bảng tính dùng chung hoặc vận hành cần hạn mức riêng: gọi proxy máy chủ, giữ khóa goldprice.dev tại đó. Đừng lưu bearer token trong script mà người chỉnh sửa đọc được. Khóa Free cũng có 5.000 lượt/tháng, tính theo tài khoản thay vì IP chung.
Các bước dưới đây không cần khóa. Khóa API miễn phí cấp hạn mức riêng cho bạn. Lịch sử dài hơn và hạn mức cao hơn cần gói trả phí.
Thêm ba hàm giá vàng vào Apps Script
Mở bảng tính, chọn Tiện ích mở rộng → Apps Script. Xóa function myFunction() {}, dán mã dưới đây. GOLDPRICE(metal, currency, refreshToken) lấy giá giao ngay; GOLDHISTORY(fromDate, toDate, refreshToken) lấy giá đóng cửa để vẽ biểu đồ; GOLDPRICEDELTA(referencePrice, metal, currency, refreshToken) trả chênh lệch giá và tỷ lệ có dấu. Các hàm dùng CacheService lưu đệm yêu cầu trùng, không gửi header Authorization, trả chuỗi lỗi thay vì ném ngoại lệ.
Để theo dõi số lượng, chi phí mua và định giá tham khảo trong cùng bảng tính, dùng mẫu theo dõi danh mục vàng.
// 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);}Thiết lập các ô giá, lịch sử và tham chiếu
Đặt giá trị kích hoạt làm mới ở B1, giá tham chiếu USD mỗi ounce troy ở C1 nếu cần. Để trống B2 và B3 để lấy 30 ngày gần nhất, hoặc nhập ngày cố định. GOLDPRICEDELTA() trả chênh lệch giá trực tiếp trừ tham chiếu và tỷ lệ có dấu. Hai ô phải cùng tiền tệ, đơn vị.
Tài liệu nêu báo giá GOOGLEFINANCE có thể trễ tới 20 phút; không hỗ trợ mọi hợp đồng tương lai, Apps Script không đọc được lịch sử. Chuẩn hóa giá tham chiếu về USD/ounce troy tại C1; goldprice.dev cung cấp giá và lịch sử qua API. Không truyền NOW(), TODAY() vào hàm: GOLDHISTORY() tự tính ngày khi B2/B3 trống.
// B1: mã làm mới do refreshGoldData() ghi// B2/B3: để trống để lấy 30 ngày lịch gần nhất, hoặc nhập ngày cố định// C1: optional reference quote normalized to USD per troy ounce// Giá giao ngay bằng USD hiện tại: tính lại khi B1 thay đổi=GOLDPRICE("XAU", "USD", $B$1)// Giá giao ngay trừ C1: trả về [chênh lệch USD có dấu, chênh lệch phần trăm có dấu]// Định dạng ô kết quả thứ hai thành phần trăm.=GOLDPRICEDELTA($C$1, "XAU", "USD", $B$1)// Giá giao ngay bằng EUR hiện tại: cả hai dùng chung ô mã làm mới=GOLDPRICE("XAU", "EUR", $B$1)// Giá đóng cửa 30 ngày, sẵn để vẽ biểu đồ (ngày, giá đóng cửa)=GOLDHISTORY($B$2, $B$3, $B$1)Tạo biểu đồ đường từ dữ liệu lịch sử
Chọn các hàng do GOLDHISTORY() trả về, chọn Chèn → Biểu đồ, rồi chọn Biểu đồ đường. Số hàng tùy khoảng ngày và dữ liệu hằng ngày hiện có. Hàm đã đảo dữ liệu API từ mới nhất trước sang thứ tự thời gian, nên trục ngang chạy từ cũ đến mới mà không cần sắp xếp thêm.
Đặt lịch làm mới mỗi 4 giờ
Chọn một tài khoản Google quản lý lịch. Ai từng theo hướng dẫn cũ phải xóa mục refreshGoldData của mình trong Apps Script → Trình kích hoạt. Chạy installGoldRefresh một lần, cấp quyền. Hàm gắn B1, tạo lịch bốn giờ. Khóa chung và dấu thời gian gộp các lịch cũ không truy cập được thành một lần làm mới.
Khi bàn giao, chủ lịch chạy uninstallGoldRefresh, rồi chủ mới cài lại. Nếu chủ cũ vắng mặt, xóa ba mục GOLDPRICE_* trong Cài đặt dự án → Thuộc tính script rồi cài lại; cơ chế chung vẫn ngăn gọi trùng. Bốn công thức × sáu lần làm mới: tối đa 24 lượt/ngày, khoảng 720/tháng, dưới mức Free 100 yêu cầu/giờ, 5.000/tháng/IP.
Kết quả mong đợi
{ "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 hiển thị trường price ở trên dưới dạng văn bản, ví dụ "4012.75". GOLDPRICEDELTA trả chênh lệch giá và tỷ lệ thập phân có dấu vào hai ô; định dạng ô thứ hai thành Phần trăm. GOLDHISTORY điền các hàng [date, close] hiện có trong khoảng ngày yêu cầu.
Lỗi thường gặp
| Mã | Biểu hiện | Cách sửa |
|---|---|---|
| 401 | Ô hiện #ACCESS_ERROR: use a server-side proxy | Yêu cầu ẩn danh bị từ chối. Kiểm tra Apps Script → Lượt thực thi trước. Nếu bảng tính cần hạn mức có xác thực, gửi yêu cầu qua proxy phía máy chủ; đừng dán khóa vào script gắn với bảng tính. |
| 403 | Ô hiện #PLAN_GATED: symbol not on your tier | Free hỗ trợ giá giao ngay vàng (XAU), bạc (XAG) và đồng (HG). Kiểm tra mã; không hỗ trợ bạch kim và palladium. Xem /pricing. |
| 429 | Ô hiện #RATE_LIMIT: wait a minute and retry | IP dùng chung chạm hạn mức Free: 10 yêu cầu/phút, 100/giờ hoặc 5.000/tháng. Bộ nhớ đệm thường ngăn lỗi này; tăng các hằng số TTL, giảm công thức khác nhau hoặc chuyển yêu cầu có xác thực qua proxy phía máy chủ. |
| N/A | GOLDHISTORY hiện #BAD_DATE: put real dates in B2/B3 | B2 hoặc B3 chứa văn bản mà Apps Script không đọc được thành ngày, như nhãn thay vì giá trị. Nhập ngày hợp lệ, hoặc tham chiếu ô có định dạng Ngày qua Định dạng → Số → Ngày. |
| N/A | GOLDPRICEDELTA hiện #BAD_REFERENCE: enter a positive price | C1 đang trống, chứa văn bản, bằng không hoặc âm. Nhập giá tham chiếu dương, cùng tiền tệ và đơn vị mỗi ounce troy với công thức giá giao ngay. |
| N/A | Ô cứ hiện Loading… | Apps Script cần quyền gửi yêu cầu ra ngoài. Chọn tên hàm trên thanh công cụ của trình chỉnh sửa, chạy thủ công một lần để hiện yêu cầu cấp quyền, rồi chấp nhận. |
Làm thêm
- Tạo công thức cảnh báo giá:
=IF(GOLDPRICE("XAU","USD",$B$1) > 4800, "ALERT", "OK") - Để lấy lịch sử hằng ngày xa hơn, dùng proxy phía máy chủ có khóa: Basic hỗ trợ 1 năm; Plus và Pro hỗ trợ toàn bộ lịch sử
- Theo dõi bạc cùng vàng trên Free, dùng chung ô kích hoạt làm mới
- Dùng các hàng GOLDHISTORY để kiểm thử chiến lược trên dữ liệu quá khứ, xem kiểm thử chiến lược vàng với dữ liệu OHLC
- Tham chiếu
=GOLDPRICE(...)trong mô hình định giá hoặc chi phí để cập nhật từ giá trực tiếp thay cho số nhập cố định