MetalAPI
XAU3,997.89−1.54‎%‎XAG53.936−1.51‎%‎XPT1,554.47−0.83‎%‎XPD1,047.03−0.44‎%‎XCU6.5080+2.29‎%‎WTI103.27+3.82‎%‎BRENT111.70−1.30‎%‎EUR1.120400−0.01‎%‎GBP1.337500−0.01‎%‎JPY0.0063761+0.14‎%‎CHF1.213445−0.25‎%‎TRY0.020125−0.12‎%‎BTC80,329.75−0.55‎%‎ETH2,329.36−1.13‎%‎
جميع المقالات
خطافات الويبالتنبيهاتدرس تطبيقي

أتمتة تنبيهات أسعار الذهب والفضة باستخدام خطافات الويب

أتمت تنبيهات أسعار الذهب والفضة بخطافات ويب موقّعة: شروط التنبيه، وفترات التهدئة، والحمولة، والتحقق عبر HMAC-SHA256 في Node وPython، وإعادة المحاولة.

بقلم MetalAPI Team··وقت القراءة: 9 دقيقة

التنبيهات بدلًا من الاستطلاع

لا تهتم تكاملات كثيرة بالأسعار إلا في لحظات محددة: عندما يتجاوز الذهب مستوى سأل عنه أحد العملاء، أو عندما تهبط الفضة بحدة، أو عندما تتحرك عملة بما يكفي لإعادة تسعير كتالوج. واستطلاع /latest مرة كل دقيقة لالتقاط تلك اللحظات يكلّف نحو 45,000 طلب شهريًا، ومع ذلك يبقى عليك كتابة المنطق الذي يكتشف لحظة العبور.

تنقل تنبيهات الأسعار في MetalAPI هذا العمل إلى الخادم. إذ يتحقق محرك التنبيهات من كل تنبيه نشط مقابل الأسعار اللحظية فور وصولها، ويُشعرك عند تحقق الشرط. ولا تستهلك عمليات التحقق من التنبيهات حصة API لديك، ولا تعمل شيفرتك إلا عندما يحدث شيء ما.

الخطةالتنبيهاتالتسليم
Freeلا يوجد—
Lite3البريد الإلكتروني
Standard10البريد الإلكتروني
Advanced25البريد الإلكتروني أو خطاف الويب
Pro50البريد الإلكتروني أو خطاف الويب
Ultra100البريد الإلكتروني أو خطاف الويب
Business200البريد الإلكتروني أو خطاف الويب
Enterprise500البريد الإلكتروني أو خطاف الويب

تدير التنبيهات من Alerts في لوحة التحكم. يمثل دليل خطافات الويب المرجع المختصر؛ أما هذه المقالة فتغطي التفاصيل التي تحتاجها لنقطة نهاية تعمل في بيئة الإنتاج. وإذا كنت تحتاج إلى كل تحديث للأسعار بدلًا من أحداث العتبات، فإن مقالة البث اللحظي تتناول النهج الآخر.

شروط التنبيه ومتى تنطلق

يراقب كل تنبيه رمزًا واحدًا مسعّرًا بعملة واحدة، مثل XAU بعملة USD أو XAG بعملة EUR. ويصلح أي رمز نشط، بما في ذلك البلاتين والبلاديوم والمعادن الصناعية والطاقة والعملات والعملات المشفرة. وهناك أربعة شروط:

الشرطينطلق عندماالعتبة
aboveالسعر ≥ العتبةسعر بعملة التنبيه
belowالسعر ≤ العتبةسعر بعملة التنبيه
change_pct_upالتغيّر منذ الإغلاق السابق ≥ العتبةنسبة مئوية أقل من 100
change_pct_downالتغيّر منذ الإغلاق السابق ≤ −العتبةنسبة مئوية أقل من 100، تُدخل كرقم موجب

عتبات المعادن لكل أونصة تروي، وهي الوحدة المعيارية. ويشمل ذلك المعادن الصناعية مثل النحاس، التي كثيرًا ما تُسعَّر بالرطل في أماكن أخرى. أما الطاقة فتستخدم وحدة العقد الخاصة بها، أي البرميل للنفط الخام، وتذكر حمولة خطاف الويب الوحدة دائمًا.

تُحسب الأسعار والتغيرات بالنسبة المئوية بعملة التنبيه. بالنسبة إلى XAG بعملة EUR، يمكن لارتفاع اليورو وحده أن يدفع الفضة إلى ما دون عتبتك أو أن يُطلق change_pct_down، حتى لو لم يتغير سعر الفضة بالدولار الأمريكي.

تنطلق التنبيهات عند العبور، لا مع كل تحديث للسعر ما دام الشرط متحققًا. فعندما يرتفع الذهب متجاوزًا 4,000 دولار، تتلقى إشعارًا واحدًا. ويُعاد تفعيل التنبيه بعد أن يعود الشرط غير متحقق، لذا فإن هبوطًا لاحقًا دون المستوى يعقبه ارتفاع جديد فوقه يُشعرك مرة أخرى.

فترة التهدئة هي الحد الأدنى من الوقت بين إشعارين من التنبيه نفسه: من 5 دقائق إلى 7 أيام (10,080 دقيقة)، والقيمة الافتراضية 60 دقيقة. فالسعر الذي يتأرجح حول عتبة ما قد يعبرها مرات كثيرة في ساعة واحدة، وتحوّل فترة التهدئة ذلك إلى رسالة واحدة. وللمستوى الذي لا يهم إلا مرة واحدة، استخدم فترة تهدئة طويلة وأوقف التنبيه مؤقتًا بعد انطلاقه.

ثمة سلوك مفيد للاختبار. فالتنبيه الجديد لا حالة سابقة له، لذا إذا كان شرطه متحققًا بالفعل، فإنه ينطلق مع تحديث السعر التالي، عادةً خلال دقيقة ما دامت السوق مفتوحة للتداول. ويمنحك تنبيه لـ XAU فوق 1 USD مع فترة تهدئة مدتها 5 دقائق عملية تسليم حقيقية تختبر بها. احذفه بعد ذلك.

التسليم بالبريد الإلكتروني أو عبر خطاف الويب

تتوفر تنبيهات البريد الإلكتروني بدءًا من خطة Lite. وهي موجهة إلى الأشخاص: عنوان مثل «MetalAPI alert: Gold (XAU) rose above 4000 USD»، والسعر الحالي، والملاحظة التي أرفقتها بالتنبيه.

أما خطافات الويب، المتاحة بدءًا من خطة Advanced، فموجهة إلى الأنظمة: طلب HTTPS POST بجسم JSON إلى عنوان URL تختاره، موقَّع بحيث يمكنك إثبات أنه صادر من MetalAPI. ويجب أن يستوفي عنوان URL لخطاف الويب ما يلي:

  • أن يستخدم https://،
  • أن يُحل إلى عنوان IP عام (تُرفض النطاقات الخاصة وعناوين الاسترجاع loopback وعناوين link-local عند حفظ التنبيه، ويُعاد التحقق منها وقت التسليم)،
  • ألا يحتوي على اسم مستخدم أو كلمة مرور.

لا تُتبع عمليات إعادة التوجيه، لذا أدخل عنوان URL النهائي. وإذا انتقل حساب إلى خطة لا تتضمن خطافات الويب، فتُسلَّم تنبيهات خطافات الويب الخاصة به عبر البريد الإلكتروني بدلًا من إسقاطها.

تُوقَّع جميع خطافات الويب في الحساب بسر واحد يبدأ بـ whsec_. يمكنك إنشاؤه وإظهاره ونسخه وتدويره من لوحة Webhook signing secret في صفحة Alerts. وإذا لم تكن قد أنشأته، فإنه يُولَّد مع أول عملية تسليم عبر خطاف الويب ثم يُعرض في اللوحة. ويسري التدوير فورًا، وهذا أمر مهم لشيفرة التحقق أدناه.

طلب خطاف الويب

تبدو كل عملية تسليم على النحو التالي:

http
POST /webhooks/metalapi HTTP/1.1
Content-Type: application/json
User-Agent: MetalAPI-Webhooks/1.0
X-MetalAPI-Timestamp: 1792765800
X-MetalAPI-Signature: t=1792765800,v1=5f0c1e…9a2b

{
  "event": "alert.triggered",
  "id": 18342,
  "created": 1792765800,
  "alert": {
    "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "symbol": "XAU",
    "base": "USD",
    "condition": "above",
    "threshold": 4000,
    "note": "Gold above 4k"
  },
  "price": 4001.25,
  "unit": "troy_oz"
}
الحقلالمعنى
eventقيمته حاليًا دائمًا alert.triggered. تجاهل القيم غير المعروفة حتى لا تعطّل أنواع الأحداث الجديدة معالجك أبدًا.
idمعرّف رقمي للحدث، فريد لكل تنبيه منطلق ومتطابق في جميع محاولات إعادة الإرسال
createdوقت Unix الذي انطلق فيه التنبيه
alertالتنبيه كما ضُبط: id (معرّف UUID) وsymbol وbase وcondition وthreshold وnote
priceالسعر الذي أطلق التنبيه، بوحدة base لكل unit
unittroy_oz للمعادن، وbbl أو mmbtu أو gal للطاقة، وcurrency للعملات، وcoin للعملات المشفرة

الحقل note، الذي يصل إلى 200 حرف، مكان مناسب لمعلومات التوجيه التي تضبطها في لوحة التحكم، مثل اسم فريق أو قناة أو رمز منتج (SKU).

التحقق من التوقيع في Node.js

تأتي ترويسة X-MetalAPI-Signature على الشكل t=<timestamp>,v1=<signature>. والتوقيع هو قيمة HMAC-SHA256 للسلسلة <timestamp>.<raw body> مُرمّزة بالنظام الست عشري بأحرف صغيرة، باستخدام سر whsec_ الخاص بك كمفتاح. ويُرسل الطابع الزمني أيضًا منفردًا في X-MetalAPI-Timestamp. وثلاث تفاصيل تحدد ما إذا كان التحقق سيعمل في بيئة الإنتاج:

  1. احسب التجزئة على البايتات الخام. قد يؤدي تحليل JSON ثم إعادة تسلسله إلى تغيير المسافات البيضاء أو تنسيق الأرقام وإفساد التوقيع. اقرأ الجسم كمخزن مؤقت (buffer).
  2. تحقّق من الطابع الزمني في الترويسة. استخدم t من الترويسة، لا created من الجسم. فكل إعادة إرسال تُوقَّع من جديد وقت الإرسال بقيمة t جديدة، بينما تحتفظ created بالوقت الأصلي. ونافذة منع إعادة الإرسال المبنية على created سترفض عمليات إعادة إرسال صالحة.
  3. اقبل سرّين أثناء التدوير. يؤدي التدوير في لوحة التحكم إلى تحويل التوقيع إلى السر الجديد فورًا. أضف السر الجديد إلى جانب القديم وانشر التحديث؛ وستُعاد محاولة عمليات التسليم التي تفشل في هذه الأثناء لبضع دقائق. ثم أزل السر القديم بمجرد أن يعمل الجديد.

مع Express، يوفر express.raw() الجسم على شكل Buffer:

webhook-server.mjs
// webhook-server.mjs — Node.js 18+, npm install express
import crypto from "node:crypto";
import express from "express";

// Comma-separated: the current secret, plus the previous one during a rotation.
const SECRETS = (process.env.METALAPI_WEBHOOK_SECRETS ?? "").split(",").filter(Boolean);
const TOLERANCE_S = 300; // reject signatures older than 5 minutes

export function verifySignature(rawBody, header) {
  const parts = {};
  for (const item of (header ?? "").split(",")) {
    const i = item.indexOf("=");
    if (i > 0) parts[item.slice(0, i).trim()] = item.slice(i + 1).trim();
  }
  const ts = parts.t ?? "";
  if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > TOLERANCE_S) return false;
  if (!/^[0-9a-f]{64}$/.test(parts.v1 ?? "")) return false;
  const given = Buffer.from(parts.v1, "hex");
  return SECRETS.some((secret) => {
    const expected = crypto.createHmac("sha256", secret).update(`${ts}.`).update(rawBody).digest();
    return crypto.timingSafeEqual(expected, given);
  });
}

async function handleAlert(event) {
  const { alert, price, unit } = event;
  console.log(`${alert.symbol}/${alert.base} ${alert.condition} ${alert.threshold}: ${price} per ${unit}`);
}

const app = express();

app.post("/webhooks/metalapi", express.raw({ type: "application/json", limit: "64kb" }), (req, res) => {
  if (!Buffer.isBuffer(req.body) || !verifySignature(req.body, req.get("X-MetalAPI-Signature"))) {
    return res.status(401).send("invalid signature");
  }
  const event = JSON.parse(req.body.toString("utf8"));
  res.sendStatus(204); // answer within 8 seconds, then do the work
  if (event.event === "alert.triggered") {
    handleAlert(event).catch((err) => console.error("alert handler failed:", err));
  }
});

app.listen(3000);

يستجيب المعالج قبل القيام بأي عمل. وإذا كان العمل مهمًا، فخزّن الحدث بشكل دائم أولًا (توضح الأقسام التالية كيفية ذلك)، حتى لا يؤدي تعطّل بعد الاستجابة إلى فقدانه.

التحقق من التوقيع في Python

يستخدم التحقق نفسه في Python المكتبة القياسية فقط. وتوفر hmac.compare_digest مقارنة في زمن ثابت:

webhook_app.py
# webhook_app.py — pip install flask, then: flask --app webhook_app run --port 3000
import hashlib
import hmac
import json
import os
import time

from flask import Flask, abort, request

# Comma-separated: the current secret, plus the previous one during a rotation.
SECRETS = [s for s in os.environ.get("METALAPI_WEBHOOK_SECRETS", "").split(",") if s]
TOLERANCE_S = 300  # reject signatures older than 5 minutes

app = Flask(__name__)


def verify_signature(raw: bytes, header: str) -> bool:
    parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
    ts, given = parts.get("t", ""), parts.get("v1", "")
    if not ts.isdigit() or abs(time.time() - int(ts)) > TOLERANCE_S:
        return False
    signed = ts.encode() + b"." + raw
    return any(
        hmac.compare_digest(hmac.new(s.encode(), signed, hashlib.sha256).hexdigest(), given)
        for s in SECRETS
    )


@app.post("/webhooks/metalapi")
def metalapi_webhook():
    raw = request.get_data()  # raw bytes, before any JSON parsing
    if not verify_signature(raw, request.headers.get("X-MetalAPI-Signature", "")):
        abort(401)
    event = json.loads(raw)
    if event.get("event") == "alert.triggered":
        alert = event["alert"]
        # Hand the event to a job queue here and return quickly.
        app.logger.warning("%s/%s %s %s: %s", alert["symbol"], alert["base"],
                           alert["condition"], alert["threshold"], event["price"])
    return "", 204

لا تختلف أطر العمل الأخرى إلا في طريقة قراءة الجسم الخام: await request.body() في FastAPI وrequest.body في Django. وأيًّا كان إطار العمل، تأكد من أن أي برمجية وسيطة لم تحلّل الجسم وتستهلكه قبل تشغيل معالجك.

إعادة المحاولة والتكرار وأزمنة الاستجابة

تنجح عملية التسليم عندما تستجيب نقطة النهاية لديك بأي حالة 2xx خلال 8 ثوانٍ. وتُعد انتهاءات المهلة وأخطاء الاتصال وعمليات إعادة التوجيه وأي حالة أخرى إخفاقات، فيُعاد إرسالها:

المحاولةالتوقيت
1فور انطلاق التنبيه
2بعد نحو دقيقة واحدة من انطلاق التنبيه
3بعد نحو 4 دقائق من انطلاق التنبيه

بعد ثلاث محاولات يُوسَم الحدث بأنه فاشل. ويسرد جدول Recent deliveries في صفحة Alerts كل حدث مع نتيجته، بما في ذلك حالة HTTP أو رسالة الخطأ في المحاولة الأخيرة. وهو أول مكان ينبغي أن تنظر فيه عندما تتصرف نقطة نهاية بشكل غير متوقع.

تستخدم عمليات إعادة الإرسال id الحدث نفسه. فالتسليم الذي عالجه خادمك لكنه استجاب له بعد أكثر من 8 ثوانٍ يُحتسب فاشلًا من جهة MetalAPI ويصل مرة أخرى، لذا يجب أن يكون المعالج آمنًا عند التكرار (idempotent). وأبسط طريقة لذلك جدول مفهرس بمعرّف الحدث:

PostgreSQL
create table metalapi_events (
  event_id     bigint primary key,      -- payload "id", identical across retries
  alert_id     uuid not null,
  payload      jsonb not null,
  received_at  timestamptz not null default now(),
  processed_at timestamptz
);

-- In the webhook handler, after verifying the signature and before responding:
insert into metalapi_events (event_id, alert_id, payload)
values ($1, $2, $3)
on conflict (event_id) do nothing;

أدرج الحدث، واستجب بالحالة 204، ودع عاملًا في الخلفية يعالج الصفوف التي تكون فيها processed_at فارغة. أما التسليم المكرر فيصطدم بالمفتاح الأساسي ولا يغيّر شيئًا. ويستغرق الإدراج أجزاءً من الثانية، ما يُبقيك ضمن نافذة الثواني الثماني بأمان حتى عندما يكون العمل اللاحق بطيئًا.

تمرير التنبيهات إلى تطبيقات المحادثة والأنظمة الأخرى

من الاستخدامات الشائعة نشر التنبيهات في محادثة الفريق. يقبل كل من Slack وDiscord خطافات الويب الواردة، لكن لا توجّه تنبيه MetalAPI إليهما مباشرةً: فهما يتوقعان حمولة مختلفة، وستفقد التحقق من التوقيع. تحقّق في نقطة النهاية الخاصة بك، ثم نسّق الرسالة ومرّرها. تحل هذه النسخة من handleAlert محل دالة التسجيل المبدئية في خادم Node.js أعلاه:

التمرير إلى خطاف ويب للمحادثة
const VERBS = { above: "rose above", below: "fell below", change_pct_up: "is up", change_pct_down: "is down" };

async function handleAlert({ alert, price, unit }) {
  const level = alert.condition.startsWith("change_pct")
    ? `${alert.threshold}% since the previous close`
    : `${alert.threshold} ${alert.base}`;
  const per = unit && unit !== "currency" ? ` per ${unit}` : "";
  const text = `${alert.symbol} ${VERBS[alert.condition]} ${level}: now ${price} ${alert.base}${per}` +
    (alert.note ? ` (${alert.note})` : "");

  // Slack incoming webhooks expect {"text": ...}; Discord webhooks expect {"content": ...}.
  const body = process.env.CHAT_KIND === "discord" ? { content: text } : { text };
  const res = await fetch(process.env.CHAT_WEBHOOK_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
    signal: AbortSignal.timeout(10_000),
  });
  if (!res.ok) throw new Error(`chat webhook returned ${res.status}`);
}

بالنسبة إلى الحمولة النموذجية، تكون الرسالة «XAU rose above 4000 USD: now 4001.25 USD per troy_oz (Gold above 4k)». ويصلح النمط نفسه لأي نظام يوفر واجهة HTTP API: فتح تذكرة دعم، أو تحديث أسعار المنتجات المخزّنة مؤقتًا في متجرك عندما يتحرك الذهب بأكثر من 2% في يوم واحد، أو إشعار المسؤول عن تغذية الأسعار. شغّل عملية التمرير في عامل خلفي بدلًا من معالج الطلب، حتى لا تتسبب خدمة محادثة بطيئة أبدًا في انتهاء مهلة التسليم.

لاختبار نقطة النهاية دون انتظار السوق، وقّع حمولة نموذجية بنفسك باستخدام سرك:

إرسال حدث اختبار موقّع
SECRET="whsec_your_secret"
BODY='{"event":"alert.triggered","id":1,"created":1792765800,"alert":{"id":"00000000-0000-0000-0000-000000000000","symbol":"XAU","base":"USD","condition":"above","threshold":4000,"note":"test"},"price":4001.25,"unit":"troy_oz"}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')

curl -i -X POST http://localhost:3000/webhooks/metalapi \
  -H "Content-Type: application/json" \
  -H "X-MetalAPI-Timestamp: $TS" \
  -H "X-MetalAPI-Signature: t=$TS,v1=$SIG" \
  --data-raw "$BODY"

تعني الحالة 204 أن التحقق من التوقيع قد نجح. ولإجراء اختبار شامل من البداية إلى النهاية، أنشئ تنبيهًا يكون شرطه متحققًا بالفعل، كما هو موضح أعلاه، وراقب ظهور الحدث في Recent deliveries. تبدأ التنبيهات التي تُسلَّم عبر خطافات الويب من خطة Advanced؛ راجع الأسعار للاطلاع على الخطط، أو أنشئ حسابًا لتجربة تنبيهات البريد الإلكتروني في خطة Lite أولًا.

جرّبه مع بياناتك الخاصة

أنشئ حسابًا مجانيًا وأرسل طلبك الأول في أقل من دقيقتين. 100 طلب شهريًا، دون بطاقة ائتمان.

تابع القراءة