الوثائق1.0 (latest)
الصفحاتسلاسل المؤشرات · 15 / 29

سلاسل المؤشرات

عند انتهاء اختبار رجعي يعرض عرض نتائج الاختبار في بيئة التطوير رسومًا بيانية لتشغيلك. تتيح لك سلاسل المؤشرات إرفاق سلاسل زمنية إضافية حسبتها بنفسك — متوسطات متحركة وRSI ومدرّجات MACD وإشارات مخصّصة — بتلك الرسوم لتظهر بجانب شموع OHLCV، أو في لوحاتها الخاصة، أو لكل رمز في عمليات التشغيل متعددة الرموز.

لا يوفّر QuantCraft مؤشرات مدمجة. لا توجد واجهة SMA أو MACD أو RSI في المنصة. تنفّذ أنت الحسابات (أو تستخدم مكتبتك الخاصة)، وتبني قائمة صفوف {"time": ..., "value": ...}، ثم تسلّمها إلى مجمِّع وقت التشغيل. في الاختبار الرجعي ببيئة التطوير يملك المحرك كائن ChartIndicatorSeries على runtime.chart_indicators ويرفق chart.to_payload() بالنتيجة باسم chart_indicator_series. تقوم الوحدة بـتوحيد الأزمنة وتخزين السلاسل — لكنها لا تحسب قيم المؤشرات عنك.

تُسلَّم السلاسل دائمًا من on_finish. والأفضل جمع القيم أثناء مرور التشغيل على الأعمدة — راجع بداية سريعة.

تحوّل بيئة التطوير كل إدخال إلى تراكب خطي أو مدرّج على لوحة الرسم المقابلة.


كيف يعمل

  1. تحسب سلسلة مؤشرات واحدة أو أكثر (مثل SMA من الإغلاقات أو RSI بصيغتك) كقائمة صفوف {"time": ..., "value": ...}.
  2. تضيف كل سلسلة إلى مجمِّع وقت التشغيل مع خيارات العرض (أي لوحة، خط أم مدرّج، واسم/لون/رمز اختيارية).
  3. في نهاية الاختبار الرجعي يدمج المحرك تلك المجموعة في النتيجة تحت chart_indicator_series.
  4. يعرض رسم بيئة التطوير في Test Results كل سلسلة بالخيارات التي اخترتها.

اقرأ المجمِّع بصفته سمة وحدة، لا اسمًا مستوردًا:

import quantcraft.process.runtime as rt ci = rt.chart_indicators # assigned by the engine before on_init

chart_indicators كائن من ChartIndicatorSeries. وكل ما تضيفه عليه بـ .add(...) (بنقاط بنيتَها أنت) يُرفق بـ Test Results عبر to_payload() الخاص بالمحرك. لا تنشئ ChartIndicatorSeries() خاصًا لنتائج اختبار بيئة التطوير — استخدم مجمِّع وقت التشغيل.

لماذا rt.chart_indicators وليس from ... import chart_indicators؟ مقابض وقت التشغيل لكل جلسة، لذا فإن from ... import يربط لقطة. في ملف الخوارزمية الجذري لا بأس بذلك — فالمحرك يعيد ربط الاسم بعد تحميل نصّك. أما في وحدة مساعدة مقسّمة فلا: تبقى الوحدات المساعدة مخزّنة مؤقتًا في sys.modules، فيحتفظ الاسم بمجمِّع الجلسة التي كانت حالية عند أول استيراد، وهذا خاطئ للتشغيل التالي في العملية نفسها (أمامي / جماعي). راجع مقابض الوحدات مقابل الأسماء المستوردة.


بداية سريعة

النمط الموصى به هو تسجيل القيمة حيث تحسبها الاستراتيجية — في on_bar / on_tick — واستخدام on_finish فقط لتسليم السلاسل المجموعة. فتكون القيم المرسومة هي بالضبط القيم التي تداولت عليها الاستراتيجية، على ساعة تنفيذ المحرك نفسها، دون إعادة حساب ولا إعادة محاذاة.

import sys import quantcraft.process.runtime as rt PERIOD = 45 # symbol -> parallel lists, appended in lockstep so indices can never drift _bar_times: dict[str, list] = {} _sma: dict[str, list[float]] = {} def on_bar(bar_index, bar, fundamentals=None, symbol=None): sym = (symbol or "").upper() # `bar.close[i]` returns None past the start of the loaded series. closes = [bar.close[i] for i in range(PERIOD)] # One append per list, per bar — NaN while warming up, never a skip. value = float("nan") if None in closes else sum(closes) / PERIOD _bar_times.setdefault(sym, []).append(bar["t"]) # UTC bar time, verbatim _sma.setdefault(sym, []).append(value) ... # trade on `value` def on_finish(bars, symbol=None, bars_by_symbol=None): ci = rt.chart_indicators if ci is None: return for sym in sorted(_bar_times): times = _bar_times[sym] vals = _sma.get(sym, []) if len(times) != len(vals): # cheap guard against drift print(f"{sym}: {len(times)} times vs {len(vals)} values", file=sys.stderr) continue # NaN warmup rows and unparseable times are dropped by `add`. points = [{"time": t, "value": v} for t, v in zip(times, vals)] ci.add( points, pane="main", render="line", line_style="solid", id=f"sma_{PERIOD}_{sym}", name=f"SMA({PERIOD}) {sym}", color="#ff9800", symbol=sym, )

يعرض الرسم الناتج SMA(45) لديك كتراكب متصل على رسم الشموع.

اشتقّ id وname من الثابت نفسه الذي تتداول عليه الاستراتيجية (PERIOD أعلاه)، لا من رقم مكتوب يدويًا — فوسيلة إيضاح قديمة لا تختلف عن رسم قديم.

لقواعد مسارات الاستيراد (quantcraft.* في كل مكان؛ وما زالت مسارات ide.* الأقدم تعمل للنصوص الحالية) راجع المرجع.


إبقاء الأزمنة والقيم متحاذية

يقرن add بين time وvalue بحسب الموضع، لذا فإن أي عدم تطابق يزيح الخط كله بصمت — إذ يُرسم، لكن مقابل الشموع الخطأ.

  • أضف الزمن والقيمة في العبارة نفسها، في كل عمود. قائمتان متوازيتان تُملآن بمعدلين مختلفين ثم تُدمجان لاحقًا بـ zip هي العلّة الكلاسيكية: يقتطع zip إلى القائمة الأقصر، فيقرن فجوة تهيئة من N عمودًا أول N زمنًا بقيم تعود للأعمدة N… — فيقع الخط N عمودًا إلى اليسار دون أي خطأ في أي مكان.
  • أضف float("nan") أثناء تهيئة المؤشر بدل التخطي. يُسقط add القيم غير المحدودة عنك (راجع بناء السلاسل)، فتختفي صفوف التهيئة من الرسم بينما تبقى القائمتان بالطول نفسه.
  • مرّر t العمود كما هو. يقبل coerce_chart_time سلاسل ISO وثواني/ملّي ثواني epoch وdatetime. تُقرأ السلاسل بلا منطقة زمنية — بما فيها تواريخ QuantCraft B2 اليومية مثل "2024-01-02" — بتوقيت UTC. أما كائن datetime الساذج فيُقرأ بالتوقيت المحلي، مما يزيح كل نقطة. تحمل صفوف الأعمدة t بتوقيت UTC؛ فاحتفظ به.
  • يوحّد add بالفعل كل time ويُسقط القيم غير المحدودة، لذا فالتصفية المسبقة بـ coerce_chart_time / math.isnan في حلقتك اختيارية.

كل return في on_finish صامت. يظل التشغيل ناجحًا وتظل Test Results تُعرض — لكن دون سلاسلك. وعندما لا تظهر سلسلة، استخدم print(..., file=sys.stderr) عند كل حارس؛ تظهر الرسالة في لوحة المخرجات.


بديل — إعادة الحساب في النهاية

لمؤشر لم تتتبّعه عمودًا بعمود، اشتقّه في on_finish من bars — النافذة المنفَّذة لرمز الساعة، الأقدم أولًا، وهي الصفوف نفسها التي يرسمها مخطط Test Results — أو من bars_by_symbol. خذ الطوابع الزمنية من الصفوف نفسها التي غذّيت بها الحساب، لكيلا يؤدي صف محذوف أو مصفّى إلى فقدان التزامن في التعيين:

import quantcraft.process.runtime as rt PERIOD = 20 def on_finish(bars, symbol=None, bars_by_symbol=None): ci = rt.chart_indicators if ci is None or not bars: return closes = [float(b["close"]) for b in bars] points = [ {"time": bars[i]["t"], "value": sum(closes[i - PERIOD + 1 : i + 1]) / PERIOD} for i in range(PERIOD - 1, len(bars)) ] if not points: return ci.add( points, pane="main", render="line", line_style="dotted", id=f"sma_{PERIOD}", name=f"SMA({PERIOD})", symbol=symbol, )

لا تبنِ السلاسل من runtime.qc_ohlcv: فهو None في تشغيل بهدف ماسح، وحيث يكون مضبوطًا يمتد على النطاق المحمَّل كاملًا بما فيه التهيئة لا النافذة المنفَّذة التي يرسمها الرسم البياني.


بناء السلاسل — قائمة points

كل سلسلة قائمة قواميس بنيتَها بعد تشغيل منطق مؤشرك الخاص. يحتاج كل صف إلى:

المفتاحالنوعملاحظات
timeرقم / datetime / سلسلة ISOيُوحَّد إلى ثواني Unix (int). مرّر الطابع الزمني للعمود — bar["t"] في دالة استدعاء، وbars[i]["t"] في on_finish — كما هو.
valueرقميجب أن يكون محدودًا — تُسقط صفوف NaN / inf، وهكذا تختفي صفوف التهيئة من الرسم.

تُتجاهل المفاتيح الأخرى في القاموس، وتُسقط الصفوف ذات الزمن الخاطئ أو القيمة غير المحدودة بصمت — بما فيها الأزمنة الرقمية الأصغر من أن تكون ثواني epoch (أقل من 100,000,000). أما الصف الذي ليس قاموسًا أصلًا فهو خطأ: يرفع add الاستثناء TypeError: points[i] must be a dict with 'time' and 'value' keys.


خيارات السلسلة (لكل استدعاء add)

تضبطها مرة لكل سلسلة عند استدعاء ci.add(...):

الوسيطمطلوبالقيم
paneنعم"main" — تراكب على رسم السعر/الشموع. "separate" — لوحة خاصة تحت الرسم (جيدة لـ RSI وMACD وغيرهما).
renderنعم"line" أو "histogram".
line_styleعند render="line""solid" أو "dotted". مطلوب للخطوط — حذفه يرفع ValueError: When render is "line", line_style must be "solid" or "dotted". احذفه للمدرّجات (يُتجاهل).
idلامعرّف ثابت للسلسلة؛ يُولَّد تلقائيًا إن لم تقدّمه. مفيد للحفاظ على اللون/الترتيب نفسه بين عمليات التشغيل.
nameلاالاسم المعروض في وسيلة إيضاح الرسم. الافتراضي id.
colorلاتلميح لون (مثل "#22c55e").
symbolلارمز بأحرف كبيرة (مثل "MSFT"). في الاختبارات الرجعية متعددة الرموز يثبّت السلسلة على رسم ذلك الرمز. احذفه لرمز الساعة / الأساسي. ولا رمز ساعة في تشغيل Screener، لذا مرّر symbol دائمًا هناك.
pane_idلاعند pane="separate" تتشارك السلاسل ذات pane_id نفسه لوحة فرعية واحدة (مثل RSI مع خطي مرجع منقطين 30/70). احذفه لسلسلة واحدة في كل لوحة. يُتجاهل عند pane="main".

أين تظهر كل سلسلة

يحدد إعدادان معًا أين تُرسم السلسلة:

  • يتحكم pane في الموضع الرأسي:
    • "main" — تجلس فوق الشموع في رسمها.
    • "separate" — تحصل على لوحة خاصة تحت الشموع.
  • يتحكم symbol في أي رسم في تشغيل متعدد الرموز:
    • محذوف — تُرفق برسم الرمز الأساسي / الساعة (وهو الافتراضي في التشغيل برمز واحد).
    • "AAPL" (وغيره) — تُرفق فقط برسم ذلك الرمز.

في الاختبارات الرجعية بالرمز الواحد يمكنك تجاهل symbol تمامًا؛ إذ يذهب كل شيء إلى الرسم الوحيد.


الخطوط مقابل المدرّجات

هذه خيارات عرض لسلاسل حسبتها بالفعل. أمثلة على ما قد ترسمه:

تراكبات الخطوط (render="line")

استخدمها للسلاسل المتصلة المحاذية للإغلاقات:

  • المتوسطات المتحركة (SMA وEMA) — عادةً pane="main".
  • نطاقات بولينجر / كيلتنر — pane="main"، عدة سلاسل.
  • RSI / ستوكاستك / ADX — pane="separate" حتى لا يُضغط مقياس السعر.

اختر line_style:

  • "solid" للمؤشرات الأساسية.
  • "dotted" للسلاسل الثانوية / المرجعية (متوسط بطيء بجانب سريع، نطاق علوي).
# rsi_points must come from your own RSI calculation rt.chart_indicators.add(rsi_points, pane="separate", render="line", line_style="solid", id="rsi_14", name="RSI(14)")

المدرّجات (render="histogram")

استخدمها لسلاسل نمط الأعمدة/الأشرطة (تُعرض القيم الموجبة والسالبة أعمدةً):

  • مدرّج MACD ودرجات شبيهة بالحجم وقوة الإشارة — بعد حسابها.
rt.chart_indicators.add( [{"time": t, "value": v} for t, v in zip(hist_times, hist_vals)], pane="separate", render="histogram", id="macd_hist", name="MACD Hist", )

لا يُستخدم line_style للمدرّجات — احذفه.


الاختبارات الرجعية متعددة الرموز

في عمليات التشغيل متعددة الرموز يكون لكل رمز محمَّل رسمه الخاص في Test Results. استخدم symbol="..." لإرسال سلسلة إلى رسم بعينه، واستدعِ add مرة لكل رمز.

التجميع في كل عمود يحل هذا مجانًا. يعمل on_bar أصلًا مرة لكل رمز، فتجميع القيم المجمّعة بحسب symbol — كما في بداية سريعة — يعطي سلسلة لكل علامة تبويب دون عمل إضافي. أما on_finish فيعمل مرة واحدة فقط في نهاية التشغيل كله، ووسيطه bars لرمز الساعة وحده: فإن بنيت مؤشرات من bars وحدها ومرّرت symbol=symbol فستقع كل سلسلة على رسم رمز الساعة، وتبقى علامات التبويب الأخرى بلا سلاسل مؤشرات.

الإصلاح لمسار إعادة الحساب: كرّر على bars_by_symbol واحسب النقاط من أعمدة كل رمز ومرّر ذلك الرمز في كل add:

def on_finish(bars, symbol=None, bars_by_symbol=None): ci = rt.chart_indicators if ci is None: return sources = bars_by_symbol if bars_by_symbol else {(symbol or "").upper(): bars} for ticker, ticker_bars in sources.items(): sma_points = compute_sma(ticker_bars, window=20) # your function ci.add( sma_points, pane="main", render="line", line_style="solid", id=f"sma_20_{ticker.lower()}", name="SMA(20)", symbol=ticker, )

تذهب السلاسل بدون symbol إلى رسم الساعة / الأساسي. وتذهب السلاسل ذات symbol إلى رسم ذلك الرمز فقط.

نصيحة: أبقِ name نفسه (مثل "SMA(20)") عبر الرموز — أما id و**pane_id** (عند استخدامه) فيجب أن يكونا فريدين لكل رمز.


تجميع السلاسل في لوحة فرعية واحدة (pane_id)

عند pane="separate" يحصل كل استدعاء add(...) عادةً على لوحة فرعية خاصة. مرّر pane_id نفسه لتكديس سلاسل مترابطة — مثل RSI مع خطي ذروة الشراء/البيع المنقطين:

import quantcraft.process.runtime as rt RSI_PANE = "rsi" def on_finish(bars, symbol=None, bars_by_symbol=None): ci = rt.chart_indicators if ci is None or not bars: return bar_times = [b["t"] for b in bars] rsi_points = compute_rsi(bars) # your function ci.add( rsi_points, pane="separate", render="line", line_style="solid", id="rsi_14", name="RSI(14)", pane_id=RSI_PANE, ) ci.add( [{"time": t, "value": 70.0} for t in bar_times], pane="separate", render="line", line_style="dotted", id="rsi_ob", name="Overbought", pane_id=RSI_PANE, color="#94a3b8", ) ci.add( [{"time": t, "value": 30.0} for t in bar_times], pane="separate", render="line", line_style="dotted", id="rsi_os", name="Oversold", pane_id=RSI_PANE, color="#94a3b8", )

في عمليات التشغيل متعددة الرموز استخدم pane_id لكل رمز (مثل f"rsi_{ticker}") و**id فريدًا** لكل رمز (مثل f"rsi_{ticker.lower()}").


عدة سلاسل دفعة واحدة

يمكنك إضافة ما تشاء من السلاسل — استدعِ add(...) مرة لكل سلسلة، ولكلٍّ منها نقاط حسبتها:

ci.add(sma20_points, pane="main", render="line", line_style="solid", id="sma_20", name="SMA(20)") ci.add(sma50_points, pane="main", render="line", line_style="dotted", id="sma_50", name="SMA(50)") ci.add(rsi_points, pane="separate", render="line", line_style="solid", id="rsi_14", name="RSI(14)") ci.add(macd_hist, pane="separate", render="histogram", id="macd", name="MACD Hist")

بناء السلاسل خارج الاختبار الرجعي

to_result_dict(key=DEFAULT_RESULT_KEY) وmerge_into(result, key=...) مساعدان لـالدفاتر / المشغّلات المخصّصة التي تملك قاموس النتيجة — يُرجعان {key: to_payload()} أو يدمجان ذلك في result. وDEFAULT_RESULT_KEY هو "chart_indicator_series". لا يستخدمهما مسار الاختبار الرجعي في بيئة التطوير (إذ يقرأ المحرك to_payload() من runtime.chart_indicators).

أعضاء أخرى: يحتفظ ChartIndicatorSeries(max_entries=None) بأحدث N سلسلة فقط عند ضبط max_entries (وتضبط عمليات التشغيل الجماعية ذلك على المجمِّع المشترك)؛ وadd(...) تُرجع SeriesEntry المخزَّن؛ وentries() تسرد كائنات SeriesEntry المضافة حتى الآن.

from quantcraft.indicator_series import ChartIndicatorSeries # Points come from your own indicator logic, not from this module points = [ {"time": 1_700_000_000, "value": 150.0}, {"time": 1_700_008_640, "value": 151.2}, ] series = ChartIndicatorSeries() series.add( points, pane="main", render="line", line_style="dotted", id="sma_20", name="SMA(20)", ) test_result = {"success": True, "stdout": "..."} test_result = series.merge_into(test_result) # test_result["chart_indicator_series"] -> ready for the chart client

داخل اختبار رجعي عادي في بيئة التطوير استخدم rt.chart_indicators و.add(...) فقط — ولا تبنِ مجمِّعًا منفصلًا ولا تستخدم merge_into على قاموس محلي.


شكل حمولة النتيجة (للمرجع)

ينتهي كل إدخال في chart_indicator_series هكذا:

الحقلالوصف
id, name, pane, render, pointsبالضبط ما مرّرته إلى add(...).
line_style"solid" / "dotted" للخطوط، وnull للمدرّجات.
colorموجود فقط إذا مرّرت color=.
symbolموجود فقط إذا مرّرت symbol= — رمز بأحرف كبيرة ليضعه المحرر على الرسم الصحيح.
pane_idموجود فقط عندما pane="separate" وتمرّر pane_id= غير فارغ.

يُوحَّد كل صف في points إلى {"time": int, "value": float} فقط.


نصائح ومزالق

  • QuantCraft يرسم؛ وأنت تحسب. استخدم pandas أو numpy أو TA-Lib أو حلقاتك الخاصة — ثم مرّر points الناتجة إلى ci.add(...).
  • احسب حيث تتداول، وأصدِر في on_finish. تسجيل (زمن العمود، القيمة) لكل رمز أثناء مرور التشغيل على الأعمدة يرسم بالضبط ما تصرّفت الاستراتيجية بناءً عليه. أعد الحساب من bars في on_finish فقط للمؤشرات التي لم تتتبّعها لكل عمود.
  • اقرأ المجمِّع باسم rt.chart_indicators. يربط from quantcraft.process.runtime import chart_indicators لقطة — آمنة في ملف الخوارزمية الجذري، وخاطئة في وحدة مساعدة مقسّمة.
  • أضف NaN أثناء التهيئة ولا تتخطَّ. تخطّي الصفوف في قائمة دون الأخرى يزيح الخط كله؛ ويُسقط add الصفوف غير المحدودة عنك.
  • استخدم الطوابع الزمنية للأعمدة في time كما هي. غالبًا ما يكون t لعمود الاختبار الرجعي سلسلة ISO (مثل "2024-06-10T13:30:00+00:00" أو "2024-01-02" لأعمدة B2 اليومية) وبتوقيت UTC. لا تفترض أن int(b["t"]) يعمل، ولا تعد بناءه كـ datetime ساذج — إذ يُقرأ كتوقيت محلي ويزيح كل نقطة.
  • عمليات return في on_finish صامتة. عدم وجود سلسلة ولا خطأ يعني غالبًا return مبكرًا؛ اطبع إلى sys.stderr عند كل حارس.
  • ليس من qc_ohlcv. فهو None في تشغيل بهدف ماسح، وإلا فيتضمن أعمدة تهيئة لا يرسمها الرسم أبدًا.
  • المعرّفات id الثابتة تساعد في التنسيق. إعادة استخدام id نفسه بين عمليات التشغيل تتيح للرسم تثبيت الألوان والترتيب، واشتقاق id / name من الثابت نفسه الذي تستخدمه الاستراتيجية يُبقي وسيلة الإيضاح صادقة.
  • اختر اللوحة المناسبة. المؤشرات المحدودة (RSI من 0 إلى 100) مكانها "separate" — وضعها في "main" سيشوّه مقياس السعر.
  • المدرّج = بلا line_style؛ الخط = line_style مطلوب. الخط بدونه يرفع ValueError.
  • عمليات تشغيل Screener: مرّر symbol دائمًا. لا يوجد رمز ساعة يُرجَع إليه.
12 دقائق للقراءةاقترح تعديلًا ↗
سلاسل المؤشرات — الوثائق (1.0 (latest)) | QuantCraft