Backtest local-currency gold without FX look-ahead bias
Build a reproducible local-currency gold backtest from settled XAU/USD bars and dated FX observations, without inserting future rates into historical rows.
← اقرأبناء حاسبة أسعار مجوهرات الذهب من أسعار القيراط الحية لكل غرام، بحساب عشري دقيق، وتخزين مؤقت، وهوامش ربح، وحدود تقييم واضحة.
يحتاج الصاغة، ومحلات الرهن، وبائعو التجارة الإلكترونية جميعاً الأداة الصغيرة نفسها: أدخل وزناً وقيراطاً، واحصل على قيمة المادة الخام. تبني هذه المقالة تلك الأداة من البداية إلى النهاية، من السعر الحي لكل غرام إلى واجهة مستخدم في المتصفح، باستخدام JavaScript خالص.
الناتج هو قيمة المادة: قيمة محتوى الذهب في القطعة عند السعر الفوري الحالي، قبل إضافة أي شيء آخر. إنها ليست تثميناً (appraisal)، وليست قيمة إعادة بيع مضمونة، وليست ما سيدفعه المشتري فعلياً.
مستبعد تماماً:
كل رقم تنتجه هذه الحاسبة يجب أن يرافقه سطر يخبر المستخدم بذلك. ابنِ إخلاء المسؤولية داخل الواجهة، لا في التوثيق فقط.
تعيد نقطة نهاية /v1/carat في الـ API أسعار الغرام الواحد لثمانية أنواع قيراط شائعة في طلب واحد، دون الحاجة إلى مفتاح API:
GET https://api.goldprice.dev/v1/carat?currency=USD
استجابة حقيقية تبدو هكذا:
{
"currency": "USD",
"timestamp": "2026-07-16T18:12:06.394451Z",
"price_gram_24k": "128.24",
"price_gram_22k": "117.55",
"price_gram_21k": "112.21",
"price_gram_20k": "106.86",
"price_gram_18k": "96.18",
"price_gram_16k": "85.49",
"price_gram_14k": "74.80",
"price_gram_10k": "53.43"
}
يهم الكود أدناه تفصيلان. كل حقل سعر هو سلسلة نصية (string)، وليس رقماً، لتبقى الأرقام العشرية الدقيقة سليمة عبر رحلة JSON. وحقل currency يقبل أياً من 31 عملة يدعمها API، فتغطي نقطة النهاية نفسها USD أو EUR أو INR أو IDR بتغيير معامل استعلام واحد فقط.
تتحدث البيانات المصدرية نفسها بوتيرة مشابهة، فطلبها في كل تحميل صفحة لا يضيف شيئاً سوى زمن انتقال إضافي. خادم Node صغير بذاكرة تخزين مؤقت داخلية يقف أمامها:
import { createServer } from "node:http";
const UPSTREAM = "https://api.goldprice.dev/v1/carat";
const TTL_MS = 60_000;
const cache = new Map(); // currency -> { data, fetchedAt }
async function fetchCarat(currency) {
const res = await fetch(`${UPSTREAM}?currency=${encodeURIComponent(currency)}`);
if (!res.ok) {
throw new Error(`upstream ${res.status}`);
}
const data = await res.json();
if (typeof data.price_gram_24k !== "string") {
throw new Error("malformed upstream response");
}
return data;
}
async function getCarat(currency) {
const cached = cache.get(currency);
const now = Date.now();
if (cached && now - cached.fetchedAt < TTL_MS) {
return cached.data;
}
try {
const data = await fetchCarat(currency);
cache.set(currency, { data, fetchedAt: now });
return data;
} catch (err) {
if (cached) {
return cached.data; // stale-if-error: serve last known value
}
throw err;
}
}
const server = createServer(async (req, res) => {
const url = new URL(req.url, "http://localhost");
if (url.pathname !== "/api/carat") {
res.writeHead(404).end("not found");
return;
}
const currency = (url.searchParams.get("currency") || "USD").toUpperCase();
try {
const data = await getCarat(currency);
res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(data));
} catch (err) {
res.writeHead(502, { "content-type": "application/json" }).end(
JSON.stringify({ error: "carat_unavailable" })
);
}
});
server.listen(3001);
هذا node:http خالص، دون حاجة إلى إطار عمل لنقطة نهاية واحدة. معالجة الفشل مهمة بقدر أهمية المسار السعيد: استجابة مصدرية غير 200 أو جسم مشوّه تُطلق خطأً، وتعود getCarat إلى آخر قيمة مخزنة مؤقتاً إن وُجدت وكانت لا تزال ضمن نافذة TTL. فقط عندما لا توجد ذاكرة مؤقتة على الإطلاق يصل الخطأ إلى العميل، كاستجابة 502.
لا تستدعِ api.goldprice.dev مباشرةً من JavaScript في المتصفح. الـ API عمداً بلا واجهة CORS للمتصفح، بحسب التصميم، لذا سيفشل أي fetch من كود جهة العميل. مرّر كل طلب عبر نقطة نهاية خادمك الخاص، وهو ما يبقي أيضاً الذاكرة المؤقتة مشتركة بين كل الزوار بدلاً من واحدة لكل تبويب متصفح.
الصيغة الأساسية عملية ضرب واحدة: weight_grams × per_gram_karat_price. المشكلة أن كلا الطرفين يجب أن يبقيا في حساب عشري دقيق، لأن ضرب أرقام النقطة العائمة (floating-point) لقيم مالية ينحرف عن الدقة:
console.log(0.1 + 0.2); // 0.30000000000000004
يمنحك الـ API بالفعل سلاسل عشرية دقيقة. الحل هو إبقاؤها أعداداً صحيحة بالوحدات الصغرى (السنتات) بدلاً من تحويلها إلى Number ثم إعادتها:
function toMinorUnits(decimalString, decimals = 2) {
const [whole, frac = ""] = decimalString.split(".");
const paddedFrac = (frac + "0".repeat(decimals)).slice(0, decimals);
const sign = whole.startsWith("-") ? -1n : 1n;
const wholeAbs = whole.replace("-", "");
return sign * (BigInt(wholeAbs) * 10n ** BigInt(decimals) + BigInt(paddedFrac || "0"));
}
function fromMinorUnits(minorUnits, decimals = 2) {
const negative = minorUnits < 0n;
const abs = negative ? -minorUnits : minorUnits;
const divisor = 10n ** BigInt(decimals);
const whole = abs / divisor;
const frac = (abs % divisor).toString().padStart(decimals, "0");
return `${negative ? "-" : ""}${whole}.${frac}`;
}
function materialValue(weightGrams, perGramPriceString) {
const priceMinor = toMinorUnits(perGramPriceString, 2); // cents
const weightMilligrams = BigInt(Math.round(weightGrams * 1000)); // 3dp precision
const totalMinor = (priceMinor * weightMilligrams) / 1000n;
return fromMinorUnits(totalMinor, 2);
}
لا حاجة إلى مكتبة أعداد كبيرة هنا، فقط BigInt، الذي تشحنه كل بيئة تشغيل JS حالية بشكل أصلي. يصل الوزن كرقم عشري عادي (ميزان الصائغ يقرأ حتى 0.01g أو 0.001g)، لذا يُحوَّل إلى مليغرامات صحيحة قبل الضرب، وتحدث القسمة للعودة لاحقاً، بعد الضرب، كي تبقى القيم الوسيطة دقيقة. لاحظ أن قسمة الأعداد الصحيحة تُقرِّب بالبتر (truncate) لا بالتقريب؛ اختر قاعدة تقريب صريحة للوحدة الصغرى لعملتك قبل نشر هذا في الإنتاج، على غرار الطريقة التي يقرّب بها الـ API نفسه كل سعر قيراط إلى خطوة العملة العشرية الخاصة بها.
قيمة المادة من الخطوة 3 رقم مشتق من السعر الفوري. إنها ليست ما ينبغي أن يعرضه المحل على الزبون. مدخلان إضافيان ينضمان فوقها، ويجب أن يكونا بندين منفصلين، لا أن يُدمَجا في رقم السعر الفوري نفسه أبداً:
function quoteBreakdown(materialValueMinor, marginPercent, makingChargeMinor) {
const marginMinor = (materialValueMinor * BigInt(Math.round(marginPercent * 100))) / 10000n;
return {
material: materialValueMinor,
margin: marginMinor,
makingCharge: makingChargeMinor,
total: materialValueMinor + marginMinor + makingChargeMinor,
};
}
إبقاء هذه ثلاثة حقول بدلاً من إجمالي واحد مدمج هو ما يجعل الحاسبة قابلة للتدقيق. ينبغي أن يتمكن الزبون، أو حتى جهة رقابية، من رؤية قيمة المادة المشتقة من السعر الفوري في سطرها الخاص، منفصلة عما يختار المحل إضافته.
جانب المتصفح يستدعي خادمك، لا الـ API المصدري أبداً:
async function loadCarat(currency) {
const res = await fetch(`/api/carat?currency=${encodeURIComponent(currency)}`);
if (!res.ok) {
throw new Error(`carat request failed: ${res.status}`);
}
return res.json();
}
function estimateValue(weightGrams, karat) {
const karatKey = `price_gram_${karat}k`;
return loadCarat("USD").then((data) => {
const perGram = data[karatKey];
if (!perGram) {
throw new Error(`unsupported karat: ${karat}`);
}
return {
material: materialValue(weightGrams, perGram),
timestamp: data.timestamp,
currency: data.currency,
};
});
}
document.getElementById("calc-btn").addEventListener("click", () => {
const grams = Number(document.getElementById("weight").value);
const karat = Number(document.getElementById("karat").value);
estimateValue(grams, karat)
.then((result) => {
document.getElementById("result").textContent =
`${result.material} ${result.currency} as of ${result.timestamp}`;
})
.catch((err) => {
document.getElementById("result").textContent =
"Price unavailable right now. Try again shortly.";
console.error(err);
});
});
كل نتيجة تعرضها الواجهة تحتاج الطابع الزمني والعملة إلى جانبها، وسطر إخلاء مسؤولية ثابت أسفلها: هذا تقدير لقيمة المادة، وليس تثميناً، ويستبعد الأحجار والصنعة والضرائب والحالة. ضع تلك الجملة في القالب مرة واحدة، لا في تلميح (tooltip) يمكن لأحدهم أن يفوّته.
للفشل ثلاثة أشكال عند حدود الخادم، والكود أعلاه يعالجها جميعاً بالفعل:
fetchCarat، ويعود إلى الذاكرة المؤقتةtypeof data.price_gram_24k !== "string" يلتقط جسماً مشوّهاً قبل تخزينه مؤقتاً أو تقديمهعندما يوجد مدخل ذاكرة مؤقتة حديث، لا يصل شيء من هذا إلى المستخدم؛ يحصل على آخر قيمة جيدة بطابعها الزمني الحقيقي. وعندما لا توجد ذاكرة مؤقتة قابلة للاستخدام، يحصل العميل على حالة خطأ واضحة بدلاً من حساب معطل أو صفر صامت.
نقطة النهاية المجهولة المستخدمة في هذه المقالة مناسبة للتطوير والاختبار. الاستخدام التجاري لبيانات الـ API الخام (في منتج تفرض رسوماً عليه، أو يدرّ إيرادات) يتطلب ترخيص خطة Pro أو أعلى. إذا كان كل ما تحتاجه سعراً مباشراً معروضاً على صفحة، لا بيانات خام للحساب عليها، فإن الأداة الرسمية القابلة للتضمين مجانية للاستخدام التجاري في العرض ولا تحتاج مفتاح API.
مقالات ذات صلة
Build a reproducible local-currency gold backtest from settled XAU/USD bars and dated FX observations, without inserting future rates into historical rows.
← اقرأThe real-time gold streaming questions that come up on Reddit, answered: whether a gold WebSocket exists, how the goldprice.dev XAU and XAG tick stream works end to end, and which other APIs actually offer one (most metals APIs just poll).
← اقرأThe gold-price-API errors developers hit on Stack Overflow, fixed: 401 auth, 400 invalid_symbol, 429 rate limits, CORS from the browser, reading the response, and pulling historical data.
← اقرأgoldprice.dev
أسعار الذهب اللحظية، بيانات OHLC التاريخية، وتجميع متعدد المصادر — متاحة عبر REST و SSE.