API للمطورين

إنشاء بث مباشر لتداول عبر SAHMK WebSocket ‏(JavaScript + Python)

اتصل بواجهة SAHMK WebSocket باستخدام مفتاح API، واشترك في الرموز وألغِ الاشتراك، وعالج ping وpong، وابنِ منطقاً آمناً للإنتاج لإعادة الاتصال ومعالجة أخطاء تحديثات أسعار تداول الفورية.

WebSocketPro+متوسطقراءة 13 دقيقة

1. متى تستخدم WebSocket بدلاً من REST

استخدم REST للقطات عند الطلب وعمليات البحث السريعة. استخدم WebSocket عندما تحتاج إلى تحديثات أسعار مباشرة تُدفع عند تغيرها بزمن استجابة منخفض خلال ساعات التداول.

  • REST: استقصاء بسيط وأسهل للتحديثات منخفضة التكرار.
  • WebSocket: بث فوري بلا حلقة استقصاء، وهو أنسب للوحات والتنبيهات المباشرة.

2. المتطلبات (مفتاح API والباقة)

  • مفتاح SAHMK API: `shmk_live_*` أو `shmk_test_*`.
  • باقة Pro أو Business أو Enterprise للوصول إلى WebSocket.
  • بيئة تشغيل JavaScript (متصفح) أو Python 3.9 فأحدث.

نقطة نهاية WebSocket:

text
wss://api.sahmk.sa/ws/v1/stocks/?api_key=YOUR_API_KEY

سلوك الباقات

  • Pro/Business: اشترك باستخدام رموز صريحة (من دون `*`)
  • Enterprise: يُسمح بالبدل العام `*`
  • اعتمد `connected.limits` مرجعاً فعلياً لحدود حسابك الحالية، بما فيها عدد الرموز لكل اتصال ولكل استدعاء.

3. اتصل وافحص حمولة `connected`

أول رسالة ينبغي معالجتها هي `connected`. اقرأ `connected.limits` واعتمدها مرجعاً فعلياً أثناء التشغيل.

json
{
  "type": "connected",
  "plan": "pro",
  "limits": {
    "max_symbols_per_connection": 60,
    "max_symbols_per_call": 20,
    "stream_modes": ["standard"]
  },
  "message": "Connected to SAHMK real-time stock stream",
  "timestamp": "2026-02-10T10:00:00.000Z"
}

4. الاشتراك في رموز محددة

صيغ رسائل العميل:

json
{"action":"subscribe","symbols":["2222","1120"]}
{"action":"unsubscribe","symbols":["2222"]}
{"action":"ping"}
{"action":"subscribe","symbols":["*"]}

استخدم `*` مع Enterprise فقط. أرسل دائماً قوائم رموز صريحة مع Pro وBusiness.

أنواع رسائل الخادم التي ينبغي معالجتها: `connected`, `subscribed`, `quote`, `error`, و`pong`.

5. معالجة بث الأسعار وعرض الواجهة (JavaScript browser)

يتضمن مثال المتصفح الاتصال والاشتراك وتحليل الأسعار وping كل 30 ثانية وإعادة الاتصال بتراجع أُسّي مع عشوائية طفيفة وإلغاء الاشتراك بسلاسة عند مغادرة الصفحة.

websocket_browser.js
const API_KEY = "YOUR_API_KEY";
const URL = `wss://api.sahmk.sa/ws/v1/stocks/?api_key=${API_KEY}`;
const SYMBOLS = ["2222", "1120"];

let ws = null;
let pingTimer = null;
let reconnectAttempt = 0;
let manualClose = false;
let lastErrorSignal = null;

function nextDelayMs(attempt) {
  const base = 1000;
  const cap = 30000;
  const exp = Math.min(cap, base * 2 ** attempt);
  const jitter = Math.floor(Math.random() * 500);
  return exp + jitter;
}

function startPing() {
  clearInterval(pingTimer);
  pingTimer = setInterval(() => {
    if (ws && ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify({ action: "ping" }));
    }
  }, 30000);
}

function stopPing() {
  clearInterval(pingTimer);
  pingTimer = null;
}

function parseRetryAfterSeconds(details) {
  if (!details || typeof details !== "object") return null;
  const raw = details.retry_after_seconds;
  const parsed = Number(raw);
  if (!Number.isFinite(parsed) || parsed <= 0) return null;
  return parsed;
}

function connect() {
  ws = new WebSocket(URL);

  ws.onopen = () => {
    reconnectAttempt = 0;
    lastErrorSignal = null;
    ws.send(JSON.stringify({ action: "subscribe", symbols: SYMBOLS }));
    startPing();
  };

  ws.onmessage = (event) => {
    let msg;
    try {
      msg = JSON.parse(event.data);
    } catch {
      console.error("Invalid JSON payload from stream:", event.data);
      return;
    }

    if (msg.type === "connected") {
      console.log("Connected limits:", msg.limits);
      return;
    }

    if (msg.type === "subscribed") {
      console.log("Subscribed symbols:", msg.symbols);
      return;
    }

    if (msg.type === "quote") {
      const { symbol, data } = msg;
      console.log(`${symbol}: ${data.price} (${data.change_percent}%)`);
      // Example UI hook:
      // document.querySelector(__TOKEN_14__).textContent = data.price;
      return;
    }

    if (msg.type === "error") {
      // Keep latest message-level signal so close handling can choose the right action.
      lastErrorSignal = {
        code: msg.code || null,
        details: msg.details || null,
      };
      console.error("Server error:", msg);
      return;
    }

    if (msg.type === "pong") {
      console.log("pong");
    }
  };

  ws.onerror = (event) => {
    console.error("WebSocket error:", event);
  };

  ws.onclose = (event) => {
    stopPing();
    console.warn(`Closed: code=${event.code} reason=${event.reason}`);

    // Auth path. Do not loop forever without intervention.
    if (event.code === 4401) {
      console.error("Authentication failure (4401). Check API key.");
      return;
    }

    // 4403 means access/entitlement class. Stop and fix account state.
    if (event.code === 4403) {
      console.error("Access denied (4403). Fix account/plan status before retrying.");
      return;
    }

    // 4429 means temporary throttle class. Retry with backoff + jitter.
    if (event.code === 4429) {
      const retryAfterSeconds = parseRetryAfterSeconds(lastErrorSignal?.details);
      const jitterMs = Math.floor(Math.random() * 500);
      const requestedDelayMs = (retryAfterSeconds || 1) * 1000 + jitterMs;
      const delayMs = Math.max(nextDelayMs(reconnectAttempt++), requestedDelayMs);
      console.warn(
        `Throttled (4429). Reconnecting in ${delayMs}ms.`
      );
      if (!manualClose) setTimeout(connect, delayMs);
      return;
    }

    // Deploy/restart can drop active sockets; reconnect and resubscribe on open.
    if (!manualClose) {
      const delay = nextDelayMs(reconnectAttempt++);
      setTimeout(connect, delay);
    }
  };
}

window.addEventListener("beforeunload", () => {
  manualClose = true;
  if (ws && ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ action: "unsubscribe", symbols: SYMBOLS }));
  }
  stopPing();
  ws?.close(1000, "Page unload");
});

connect();

6. منطق إبقاء الاتصال وإعادة الاتصال (Python async)

يستخدم مثال Python مكتبة `websockets` ويتضمن إعادة اتصال آمنة بتراجع عشوائي وفاصل ping وتحليل الأسعار ومعالجة أخطاء الخادم. في الإنتاج، افترض أن النشر أو إعادة التشغيل قد يقطع العملاء مؤقتاً، ثم أعد الاتصال والاشتراك.

stream_quotes.py
import asyncio
import contextlib
import json
import random
import websockets

API_KEY = "YOUR_API_KEY"
URL = f"wss://api.sahmk.sa/ws/v1/stocks/?api_key={API_KEY}"
SYMBOLS = ["2222", "1120"]

def next_delay_seconds(attempt: int) -> float:
  base = 1.0
  cap = 30.0
  exp = min(cap, base * (2 ** attempt))
  jitter = random.uniform(0.0, 0.5)
  return exp + jitter

def parse_retry_after_seconds(details):
  if not isinstance(details, dict):
    return None
  raw = details.get("retry_after_seconds")
  try:
    value = float(raw)
  except (TypeError, ValueError):
    return None
  if value <= 0:
    return None
  return value

async def ping_loop(ws):
  while True:
    await asyncio.sleep(30)
    await ws.send(json.dumps({"action": "ping"}))

async def stream_forever():
  attempt = 0
  last_error_signal = None
  while True:
    try:
      async with websockets.connect(URL, ping_interval=None) as ws:
        attempt = 0
        last_error_signal = None
        await ws.send(json.dumps({"action": "subscribe", "symbols": SYMBOLS}))
        pinger = asyncio.create_task(ping_loop(ws))

        try:
          async for raw in ws:
            try:
              msg = json.loads(raw)
            except json.JSONDecodeError:
              print("Invalid JSON payload from stream:", raw)
              continue
            msg_type = msg.get("type")

            if msg_type == "connected":
              print("Connected limits:", msg.get("limits"))
            elif msg_type == "subscribed":
              print("Subscribed:", msg.get("symbols"))
            elif msg_type == "quote":
              symbol = msg.get("symbol")
              data = msg.get("data", {})
              print(f"{symbol}: {data.get('price')} ({data.get('change_percent')}%)")
            elif msg_type == "error":
              last_error_signal = {
                "code": msg.get("code"),
                "details": msg.get("details"),
              }
              print("Server error:", msg)
            elif msg_type == "pong":
              print("pong")
        finally:
          pinger.cancel()
          with contextlib.suppress(asyncio.CancelledError):
            await pinger
    except websockets.exceptions.ConnectionClosed as exc:
      print(f"Connection closed: code={exc.code}, reason={exc.reason}")
      if exc.code == 4401:
        print("Authentication failure (4401). Check API key.")
        return
      if exc.code == 4403:
        print("Access denied (4403). Fix account/plan status before retrying.")
        return
      if exc.code == 4429:
        signal = last_error_signal or {}
        retry_after = parse_retry_after_seconds(signal.get("details"))
        jitter = random.uniform(0.0, 0.5)
        requested_delay = (retry_after if retry_after else 1.0) + jitter
        delay = max(next_delay_seconds(attempt), requested_delay)
        print(f"Throttled (4429). Reconnecting in {delay:.2f}s")
        await asyncio.sleep(delay)
        attempt += 1
        continue
    except Exception as exc:
      print(f"Unexpected error: {exc}")

    delay = next_delay_seconds(attempt)
    print(f"Reconnecting in {delay:.2f}s")
    await asyncio.sleep(delay)
    attempt += 1

if __name__ == "__main__":
  asyncio.run(stream_forever())
bash
pip install websockets

7. استخدام قناة الصفقات

تستخدم قناة الصفقات أنماط المصادقة والاشتراك وإلغائه وping وإعادة الاتصال والاشتراك نفسها الموضحة هنا. غيّر نقطة النهاية وعالج أنواع رسائل الصفقات.

text
wss://api.sahmk.sa/ws/v1/market/trades/?api_key=YOUR_API_KEY
  • بعد تأكيد الاشتراك بالرموز الصريحة، يرسل الخادم `trades_snapshot` لكل رمز اشتركت فيه حديثاً.
  • تصل الصفقات المنفذة مباشرة كرسائل `trade`.
  • حافظ على ping كل 30 ثانية، وعلى منطق إعادة الاتصال ومعالجة الأخطاء المستخدم للأسعار.
اعرض مرجع حمولة الصفقات ←

8. قائمة التحقق للإنتاج

  • أعد الاتصال بتراجع أُسّي مع عشوائية طفيفة.
  • أعد الاشتراك بعد كل اتصال جديد (حالة الاشتراك خاصة بكل اتصال).
  • اعتبر رمز الإغلاق فئة الخطأ و`error.code/details` إشارة الإجراء.
  • `4401`: توقف وأصلح المصادقة قبل إعادة الاتصال.
  • `4403`: توقف وأصلح صلاحية الحساب أو حالته قبل إعادة الاتصال.
  • `4429`: تقييد مؤقت؛ أعد المحاولة بتراجع مع عشوائية طفيفة واحترم `retry_after_seconds` عند توفره.
  • عامل استجابات JSON غير الصالحة أو الإجراءات المجهولة كأحداث `error` على مستوى الرسالة، لا كفشل إغلاق للمقبس.
  • سجّل رموز وأسباب قطع الاتصال للمراقبة والتنبيه.
  • لا تفترض استمرار اشتراكات الخادم بعد إعادة الاتصال.
  • إذا استخدمت غلاف Python SDK، فلا تعتمد إعادة الاتصال والاشتراك التلقائية إلا بعد تأكيد السلوك في توثيق إصدارك واختباراته.

ضوابط الاتصالات المتعددة

  • افتح عدة مقابس بفاصل `200-400ms` بدلاً من فتحها دفعة واحدة.
  • ضع محاولات إعادة الاتصال في طابور عام لكل مفتاح API.
  • تجنب موجات إعادة الاتصال المتزامنة عند افتتاح السوق.

الاتصال خارج ساعات التداول

يمكنك إبقاء اتصالات WebSocket مفتوحة خارج ساعات التداول؛ لا تقطع SAHMK العملاء عمداً عند إغلاق السوق. واصل إرسال ping كل 30 ثانية، وأعد الاتصال بتراجع أُسّي محدود مع عشوائية طفيفة عند الحاجة، واستعد الاشتراكات بعد كل اتصال. قد تغيب أحداث السوق أثناء توقفه.

9. صلاحيات الباقات ونطاق البدل العام

استخدم قوائم رموز صريحة مع Pro وBusiness. اشتراك البدل العام `*` متاح لـEnterprise فقط.

enterprise_subscribe_all.js
// Enterprise-only example:
ws.send(JSON.stringify({
  action: "subscribe",
  symbols: ["*"]
}));

تحذير بشأن حجم الرسائل

قد ينتج استخدام `*` حجماً كبيراً من الرسائل. عالج الرسائل بشكل غير متزامن، وتجنب الحسابات الثقيلة لكل رسالة في المسار الرئيسي، واستخدم التخزين المؤقت أو العرض المحدود في الواجهة.

10. الأخطاء الشائعة واستكشاف المشكلات

ملاحظات مهمة

  • بعد إرسال `ping` قد تتلقى `quote` قبل `pong` في البث المزدحم، وهذا طبيعي.
  • اعتمد قيم `connected.limits` مرجعاً فعلياً أثناء التشغيل.
  • تُنشر التحديثات عند تغير الرموز خلال جلسات السوق النشطة.
  • لا تضف يدوياً ترويسات `Origin` مكررة في العملاء المخصصين أو الوكلاء.

قائمة التحقق لمعالجة الأخطاء

  • أظهر رسائل `error` من الخادم في السجلات والمراقبة، واحتفظ بأحدث `error.code/details`.
  • عامل رمز الإغلاق `4401` كفشل مصادقة وأوقف المحاولات العمياء.
  • عند الإغلاق `4403` استخدم أحدث `error.code/details`: عامله كرفض لصلاحية الحساب وأوقف المحاولات حتى إصلاح حالة الحساب أو الباقة.
  • عند الإغلاق `4429` عامله كتقييد مؤقت: أعد المحاولة بتراجع مع عشوائية طفيفة واحترم `retry_after_seconds` في أحدث `error.details`.
  • إذا أرسل عميل Pro القيمة `[*]`، فعالج خطأ الباقة وارجع إلى قوائم رموز صريحة.
  • يعيد JSON غير الصالح أو الإجراء المجهول `type: "error"` مع بقاء المقبس مفتوحاً؛ عامله كفشل على مستوى الرسالة.
  • قد يقطع نشر الخادم أو إعادة تشغيله الاتصالات النشطة؛ أعد الاتصال والاشتراك تلقائياً.
  • استخدم تراجعاً أُسّياً مع عشوائية طفيفة وحداً أقصى للتأخير.
  • ألغِ الاشتراك وأغلق الاتصال بسلاسة عند مغادرة الصفحة أو إيقاف التطبيق.

الأسئلة الشائعة لاستكشاف المشكلات

اتصلت لكنني لا أرى تحديثات. لماذا؟

تأكد من إرسال رسالة `subscribe` واختبر خلال ساعات التداول. تُدفع التحديثات عند تغير الأسعار.

ينقطع اتصالي سريعاً بالرمز 4401 أو 4403.

عند `4401` تحقق من صيغة مفتاح API وصلاحيته. عند `4403` أصلح صلاحية الحساب قبل المحاولة. عند `4429` استخدم إعادة اتصال مؤجلة مع عشوائية طفيفة واحترم `retry_after_seconds` من أحدث تفاصيل خطأ.

لست على باقة Enterprise ويفشل اشتراك `*`.

البدل `*` متاح لـEnterprise فقط. مع Pro وBusiness اشترك برموز صريحة ضمن حدود `connected.limits`.

11. الخطوات التالية

  • أضف منطق قواعد الأحداث باستخدام webhooks وقواعد التنبيهات في SAHMK.
  • نفّذ لقطات REST احتياطية أثناء إعادة اتصال البث.
  • احفظ أحدث حالة للأسعار واعرضها في لوحة البيانات.

المصادر

أنشئ بث تداول المباشر

ابدأ باشتراكات الرموز على Pro أو Business، ثم توسع إلى أنماط Enterprise عند حاجتك إلى تغطية أوسع.

نشر بواسطة @sahmk_sa · مرخّص من تداول السعودية