تُرجع =GOLDPRICE() سعر XAU/USD المباشر للأونصة التروي من goldprice.dev في أي خلية بعد إضافة Apps Script. تتأخر الأسعار المجانية 15 دقيقة. لا تقرأ IMPORTDATA نقطة النهاية هذه بصيغة JSON، لذا تحتاج إلى دالة مخصصة. تُرجع GOLDHISTORY() الإغلاقات اليومية لمدة 30 يومًا، وتُرجع GOLDPRICEDELTA() فرق السعر ونسبته بإشارتيهما مقابل خلية مرجعية موحّدة العملة والوحدة. لا يُخزّن مفتاح API. شغّل installGoldRefresh() مرة لربط خلية تحديث وإنشاء مشغّل كل أربع ساعات ضمن الحصة.
ابدأ دون وضع مفتاح API في جدول البيانات
تعمل نقطة نهاية السعر الفوري لـXAU وسجل الإغلاقات اليومية لمدة 30 يومًا دون تسجيل، لذا لا يخزّن هذا الشرح بيانات اعتماد في Apps Script. يخضع الوصول المجهول لحدود Free: 10 طلبات في الدقيقة، و100 في الساعة، و5,000 شهريًا لكل IP. يبقى التحديث كل أربع ساعات ضمن هذه الحدود.
قد يشترك مستخدمو Sheets في عنوان خروج لدى Google، فتتفاوت السعة المجهولة. لحصة ثابتة على مستوى الحساب في مصنف مشترك أو إنتاجي، استخدم وسيطًا على خادمك واحتفظ بمفتاح goldprice.dev هناك. لا تلصق رمز bearer في سكربت مرتبط يستطيع المحررون فحصه. يمنح المفتاح المجاني الحصة الشهرية نفسها، 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، وتُرجع الأخطاء كنصوص بدل إطلاق استثناءات.
لجمع الكميات وتكاليف الشراء والتقييمات التقريبية في مصنف واحد، استخدم قالب تتبّع محفظة الذهب.
// 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 وسعرًا مرجعيًا اختياريًا بوحدة USD للأونصة التروي في C1. اترك B2 وB3 فارغتين لسجل متحرك مدته 30 يومًا تقويميًا، أو أدخل تاريخين ثابتين. تُرجع GOLDPRICEDELTA() فرق السعر المباشر ناقص المرجعي ونسبته، مع الإشارة. تصح المقارنة فقط عند تطابق العملة والوحدة في الخليتين.
توضح Google أن أسعار GOOGLEFINANCE قد تتأخر حتى 20 دقيقة، وبعض العقود الآجلة غير مدعومة، ولا يمكن الوصول للسجل عبر Apps Script. وحّد السعر المرجعي إلى USD للأونصة التروي في C1؛ يوفّر goldprice.dev السعر الفوري والسجل برمجيًا. لا تمرّر NOW() أو TODAY() للدوال المخصصة؛ تحسب GOLDHISTORY() التواريخ المتحركة داخليًا عند ترك B2/B3 فارغتين.
// 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 واحدًا لامتلاك الجدول الزمني. إذا اتبع محررون الشرح القديم، فعلى كل منهم أولًا حذف مشغّل refreshGoldData الخاص به من Apps Script → المشغّلات. شغّل installGoldRefresh مرة وامنحه الإذن. يربط المثبّت B1 وينشئ مشغّلًا كل أربع ساعات. يدمج قفل وطابع زمني مشتركان المشغّلات القديمة التي يتعذر الوصول إليها في تحديث واحد كل أربع ساعات.
لنقل المسؤولية، يشغّل المالك uninstallGoldRefresh ثم يثبّت المالك الجديد المشغّل. عند تعذر الوصول للمالك، احذف إدخالات GOLDPRICE_* الثلاثة من إعدادات المشروع → خصائص السكربت ثم أعد التثبيت؛ يمنع الفاصل الزمني المشترك بين الصيغ تكرار الاستدعاءات. أربع صيغ × ستة تحديثات تعني بحد أقصى 24 استدعاءً يوميًا، ونحو 720 شهريًا، ضمن حدود Free المجهولة: 100 طلب/ساعة و5,000 شهريًا لكل IP.
النتيجة المتوقعة
{ "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). تحقّق من الرمز؛ البلاتين والبلاديوم غير متاحين. راجع الأسعار. |
| 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 سنة واحدة، وتشمل Plus وPro السجل الكامل
- تتبّع الفضة إلى جانب الذهب في Free، باستخدام خلية رمز التحديث نفسها لكليهما
- استخدم صفوف GOLDHISTORY الجاهزة للرسم في اختبار تاريخي، راجع اختبار استراتيجية ذهب على بيانات OHLC
- اربط
=GOLDPRICE(...)بنموذج تقييم أو تكلفة ليتحدّث بالبيانات المباشرة بدل قيمة ثابتة