الصفحاتسلاسل المؤشرات · 15 / 29
سلاسل المؤشرات
عند انتهاء اختبار رجعي يعرض عرض نتائج الاختبار في بيئة التطوير رسومًا بيانية لتشغيلك. تتيح لك سلاسل المؤشرات إرفاق سلاسل زمنية إضافية حسبتها بنفسك — متوسطات متحركة وRSI ومدرّجات MACD وإشارات مخصّصة — بتلك الرسوم لتظهر بجانب شموع OHLCV، أو في لوحاتها الخاصة، أو لكل رمز في عمليات التشغيل متعددة الرموز.
لا يوفّر QuantCraft مؤشرات مدمجة. لا توجد واجهة SMA أو MACD أو RSI في المنصة. تنفّذ أنت الحسابات (أو تستخدم مكتبتك الخاصة)، وتبني قائمة صفوف {"time": ..., "value": ...}، ثم تسلّمها إلى مجمِّع وقت التشغيل. في الاختبار الرجعي ببيئة التطوير يملك المحرك كائن ChartIndicatorSeries على runtime.chart_indicators ويرفق chart.to_payload() بالنتيجة باسم chart_indicator_series. تقوم الوحدة بـتوحيد الأزمنة وتخزين السلاسل — لكنها لا تحسب قيم المؤشرات عنك.
تُسلَّم السلاسل دائمًا من on_finish. والأفضل جمع القيم أثناء مرور التشغيل على الأعمدة — راجع بداية سريعة.
تحوّل بيئة التطوير كل إدخال إلى تراكب خطي أو مدرّج على لوحة الرسم المقابلة.
كيف يعمل
- تحسب سلسلة مؤشرات واحدة أو أكثر (مثل SMA من الإغلاقات أو RSI بصيغتك) كقائمة صفوف
{"time": ..., "value": ...}. - تضيف كل سلسلة إلى مجمِّع وقت التشغيل مع خيارات العرض (أي لوحة، خط أم مدرّج، واسم/لون/رمز اختيارية).
- في نهاية الاختبار الرجعي يدمج المحرك تلك المجموعة في النتيجة تحت
chart_indicator_series. - يعرض رسم بيئة التطوير في Test Results كل سلسلة بالخيارات التي اخترتها.
اقرأ المجمِّع بصفته سمة وحدة، لا اسمًا مستوردًا:
import quantcraft.process.runtime as rt
ci = rt.chart_indicators # assigned by the engine before on_initchart_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دائمًا. لا يوجد رمز ساعة يُرجَع إليه.

