Apps Scriptを追加すると、任意のセルで=GOLDPRICE()を使い、goldprice.devのXAU/USD価格をトロイオンス単位で取得できます。無料価格は15分遅れです。IMPORTDATAはこのJSONエンドポイントを読み込めないため、カスタム関数が必要です。GOLDHISTORY()は30日間の日次終値を、GOLDPRICEDELTA()は単位をそろえた基準セルとの差額と騰落率を符号付きで返します。APIキーは保存しません。installGoldRefresh()を一度実行すると、更新用セルを指定し、利用枠に収まる4時間間隔のトリガーを作成できます。
シートにAPIキーを保存せずに始める
XAUのスポット価格と30日間の日次履歴は、認証なしで取得できます。この手順ではApps Scriptに認証情報を保存しません。匿名アクセスには、IPごとに毎分10件、毎時100件、毎月5,000件のFree制限が適用されます。4時間間隔で更新するブックなら、この枠に収まります。
Google Sheetsの複数ユーザーが同じ送信元IPを共有する場合があるため、匿名アクセスで使える枠は変動します。共有用や本番用のブックでアカウント専用の枠が必要なら、自分のサーバー側プロキシ経由で呼び出し、goldprice.devのキーをそこで管理してください。他の編集者が閲覧できるスクリプトにBearerトークンを貼り付けないでください。無料アカウントのキーも月5,000回ですが、枠は共有IPではなくアカウントに割り当てられます。
Apps Scriptに金価格用の関数を追加する
シートを開き、拡張機能 → Apps Scriptを選びます。初期コードのfunction myFunction() {}を削除し、以下のコードを貼り付けてください。現在のスポット価格を取得するGOLDPRICE(metal, currency, refreshToken)、グラフ用の日次終値を取得するGOLDHISTORY(fromDate, toDate, refreshToken)、符号付きの差額と騰落率を取得するGOLDPRICEDELTA(referencePrice, metal, currency, refreshToken)を定義します。同じリクエストはCacheServiceでキャッシュし、Authorizationヘッダーは送信せず、例外を投げずにエラー文字列を返します。
保有量、購入費用、参考評価額を同じブックで管理するには、金ポートフォリオ管理テンプレートを使ってください。
// 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基準価格を入力します。直近30暦日分の履歴にはB2とB3を空欄にし、期間を固定する場合は日付を入力してください。GOLDPRICEDELTA()は現在価格から基準価格を引いた差額と騰落率を符号付きで返します。比較するセルの通貨と単位をそろえてください。
Googleによると、GOOGLEFINANCEの価格は最大20分遅れる場合があります。すべての先物に対応しているわけではなく、過去データはApps Scriptから取得できません。別の基準価格を使う場合は、トロイオンス当たりのUSD価格に換算してC1に入れてください。スポット価格と履歴はgoldprice.devからプログラムで取得します。カスタム関数の引数にNOW()やTODAY()を渡さないでください。B2/B3が空欄なら、GOLDHISTORY()内で日付範囲を計算します。
// B1: 更新用トークンです。refreshGoldData()が書き込みます。// B2/B3: 空欄にすると直近30暦日が対象になります。期間を固定する場合は日付を入力します。// C1: トロイオンス当たりのUSD建てに換算した基準価格です。入力は任意です。// 現在のUSD建てスポット価格です。B1が変わるたびに再計算します。=GOLDPRICE("XAU", "USD", $B$1)// 現在のスポット価格からC1を引き、[符号付きのUSD差額, 符号付きの変化率]を複数セルに展開します。// 結果の2番目のセルの表示形式をパーセントに設定します。=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が更新用セルに設定され、4時間間隔のトリガーが作成されます。削除できない旧トリガーが残っていても、共有ロックとタイムスタンプが更新を4時間に一度にまとめます。
管理者を引き継ぐ場合は、現在の管理者がuninstallGoldRefreshを実行してから、新しい管理者がインストールします。現在の管理者に連絡できない場合は、プロジェクトの設定 → スクリプトプロパティでGOLDPRICE_*の3項目を削除して再インストールしてください。共有の間隔チェックが重複呼び出しを防ぎます。数式4個×更新6回で最大24回/日、約720回/月となり、匿名Free枠のIPごとに毎時100件、毎月5,000件に収まります。
出力例
{ "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 tierと表示されます | Freeでは金(XAU)、銀(XAG)、銅(HG)のスポット価格を利用できます。シンボルを確認してください。プラチナとパラジウムは提供していません。詳しくは/pricingをご覧ください。 |
| 429 | セルに#RATE_LIMIT: wait a minute and retryと表示されます | 共有IPがFreeの制限である毎分10件、毎時100件、毎月5,000件のいずれかに達しました。通常はキャッシュで防げます。TTL定数を大きくする、異なる数式を減らす、認証付きリクエストをサーバー側プロキシ経由にする方法があります。 |
| N/A | GOLDHISTORYに#BAD_DATE: put real dates in B2/B3と表示されます | B2またはB3に、Apps Scriptが日付として解釈できない文字列が入っています。値ではなくラベルを入力していないか確認してください。日付を入力するか、表示形式 → 数字 → 日付で設定したセルを参照してください。 |
| N/A | GOLDPRICEDELTAに#BAD_REFERENCE: enter a positive priceと表示されます | C1が空欄、文字列、ゼロ、負の値のいずれかです。現在のスポット価格の数式と同じ通貨を使い、トロイオンス当たりの正の基準価格を入力してください。 |
| N/A | セルのLoading…表示が消えません | Apps Scriptで外部取得の権限を許可する必要があります。エディタのツールバーで関数を選び、手動で一度実行してください。認証画面が表示されたら許可します。 |
活用方法
- 価格アラートの数式を作る:
=IF(GOLDPRICE("XAU","USD",$B$1) > 4800, "ALERT", "OK") - より古い日次履歴には、キーを管理するサーバー側プロキシを使います。Basicは1年分、PlusとProは全期間の履歴に対応します
- Freeで金と銀を並べて確認し、同じ更新トークンのセルを使います
- グラフ用のGOLDHISTORYの行をバックテストに使います。OHLCデータで金の戦略をバックテストする方法をご覧ください
- 評価額や費用の計算モデルから
=GOLDPRICE(...)を参照し、固定値を入力せずに現在価格で更新します