pandas: بيانات الأسعار في جدول
ولّد سنة اصطناعية من شموع OHLCV اليومية، واحفظها في DataFrame من pandas مفهرس بالتاريخ، ثم اختر الأعمدة والصفوف والشروط، وحوّل الشموع اليومية إلى أسبوعية.
في هذا الدرس ستتعلّم أن
- ابنِ DataFrame من pandas لشموع OHLCV اليومية بحيث تكون التواريخ هي الفهرس.
- اختر عموداً ونطاقاً زمنياً والصفوف التي تحقق شرطاً.
- حوّل الشموع اليومية إلى أسبوعية (Resample) بالقاعدة الصحيحة لكل عمود.
- اشرح لماذا تُستخدم البيانات الاصطناعية للتعلّم، وما الذي لا تستطيع إظهاره.
بيانات الأسعار جدول: صف واحد لكل شمعة، وأعمدة للافتتاح (Open) والأعلى (High) والأدنى (Low) والإغلاق (Close) وحجم التداول (Volume). والقوائم، من الدرس السابق، غير مريحة لذلك. ستحتاج إلى خمس قوائم متوازية وسادسة للتواريخ، وسيكون إبقاؤها متوافقة مشكلتك أنت. وتحلّ مكتبة pandas ذلك. فهي تخزّن البيانات في DataFrame: جدول بأعمدة مسمّاة وفهرس معنون، ويكون الفهرس في بيانات الأسعار هو التاريخ. وتسمّى البيانات المرتبة بالزمن هكذا سلسلة زمنية، ولدى pandas أدوات مصمَّمة خصيصاً لها.
يبني هذا الدرس سنة من الشموع اليومية الاصطناعية، ثم يعرض العمليات التي ستستخدمها باستمرار: اختيار الأعمدة، واختيار التواريخ، والترشيح بالشرط، وتحويل الشموع اليومية إلى أسبوعية.
توليد بيانات أسعار اصطناعية
جميع البيانات في هذه الدورة مولَّدة لا منزَّلة. وهذا يُبقي كل مثال قابلاً لإعادة الإنتاج، وخالياً من أخطاء البيانات التي تغطيها الوحدة 02، ومنفصلاً بوضوح عن أي سوق حقيقي. تنشئ الدالة أدناه شموع OHLCV من مسار عشوائي (Random Walk).
import numpy as np
import pandas as pd
def make_prices(n_days=260, seed=42):
"""Synthetic daily OHLCV bars from a random walk. Not real market data."""
rng = np.random.default_rng(seed)
dates = pd.bdate_range("2024-01-01", periods=n_days, name="date")
close = 100 * np.exp(np.cumsum(rng.normal(0.0003, 0.012, n_days)))
prev_close = np.concatenate([[100.0], close[:-1]])
open_ = prev_close * (1 + rng.normal(0, 0.002, n_days))
high = np.maximum(open_, close) * (1 + np.abs(rng.normal(0, 0.004, n_days)))
low = np.minimum(open_, close) * (1 - np.abs(rng.normal(0, 0.004, n_days)))
volume = rng.integers(800_000, 1_500_000, n_days)
return pd.DataFrame(
{"open": open_, "high": high, "low": low, "close": close, "volume": volume},
index=dates,
)
prices = make_prices()
print(prices.head().round(2))
print(prices.shape)
print(prices.index.min().date(), "to", prices.index.max().date()) open high low close volume
date
2024-01-01 99.95 100.79 99.93 100.40 1029686
2024-01-02 100.34 100.70 99.15 99.18 1025550
2024-01-03 99.23 100.64 98.51 100.11 1399629
2024-01-04 100.00 101.35 99.64 101.27 1181183
2024-01-05 101.37 101.94 98.96 98.96 1293259
(260, 5)
2024-01-01 to 2024-12-27لا تحتاج إلى حفظ المولّد، لكنه يستحق الفهم مرة واحدة، لأنه يبيّن ما تحويه البيانات وما لا تحويه.
- ينشئ
rng = np.random.default_rng(seed)مولّد أرقام عشوائية. وتثبّت البذرة تسلسله، فينتج كل تشغيل، على كل حاسوب، الأرقام نفسها. - ينشئ
pd.bdate_range(...)260 يوم عمل، من الاثنين إلى الجمعة، بدءاً من 1 يناير 2024. وهذا يقارب سنة تداول. - يسحب
rng.normal(0.0003, 0.012, n_days)260 عائداً يومياً من توزيع طبيعي بمتوسط صغير وتشتت نحو 1.2% في اليوم. وتجمعهاnp.cumsumيوماً بعد يوم، وتحوّل100 * np.exp(...)هذا المجموع المتراكم إلى مسار سعري يبدأ قرب 100. وسترى لماذا ينجح ذلك في الدرس التالي. - يقع كل افتتاح قرب الإغلاق السابق؛ و
close[:-1]تعني «كل إغلاق عدا الأخير»، مُزاحاً يوماً لاحقاً بوضع 100 في المقدمة. - يكون أعلى سعر فوق الأكبر من الافتتاح والإغلاق بقليل، وأدنى سعر تحت الأصغر بقليل، فتكون كل شمعة متسقة داخلياً.
- حجم التداول عدد صحيح عشوائي ضمن نطاق ثابت.
وأخيراً، يجمّع pd.DataFrame({...}, index=dates) الجدول. وتربط الأقواس المعقوفة اسم كل عمود بقيمه، ويعنون index=dates كل صف بتاريخه.
في المخرجات، يعرض .head() أول خمسة صفوف، ويقرّب .round(2) للعرض دون تغيير البيانات. ويعطي .shape الحجم على هيئة (الصفوف، الأعمدة). وللفهرس طرقه الخاصة: .min() و.max() تعطيان أول تاريخ وآخره.
اختيار البيانات
ثلاثة أنواع من الاختيار تغطي معظم الحاجات. يعيد كل مثال بناء البيانات أولاً ليعمل بمفرده؛ وفي دفتر الملاحظات ستشغّل make_prices مرة واحدة في خلية سابقة.
import numpy as np
import pandas as pd
def make_prices(n_days=260, seed=42):
"""Synthetic daily OHLCV bars from a random walk. Not real market data."""
rng = np.random.default_rng(seed)
dates = pd.bdate_range("2024-01-01", periods=n_days, name="date")
close = 100 * np.exp(np.cumsum(rng.normal(0.0003, 0.012, n_days)))
prev_close = np.concatenate([[100.0], close[:-1]])
open_ = prev_close * (1 + rng.normal(0, 0.002, n_days))
high = np.maximum(open_, close) * (1 + np.abs(rng.normal(0, 0.004, n_days)))
low = np.minimum(open_, close) * (1 - np.abs(rng.normal(0, 0.004, n_days)))
volume = rng.integers(800_000, 1_500_000, n_days)
return pd.DataFrame(
{"open": open_, "high": high, "low": low, "close": close, "volume": volume},
index=dates,
)
prices = make_prices()
close = prices["close"] # one column: a Series
print(close.tail(3).round(2))
week = prices.loc["2024-03-04":"2024-03-08", ["open", "close"]] # rows by date label
print(week.round(2))
up_days = prices[prices["close"] > prices["open"]] # rows where a condition is True
print(f"Up days: {len(up_days)} of {len(prices)}")date
2024-12-25 94.85
2024-12-26 94.73
2024-12-27 93.20
Freq: B, Name: close, dtype: float64
open close
date
2024-03-04 104.23 104.75
2024-03-05 104.68 105.89
2024-03-06 106.16 106.20
2024-03-07 106.33 107.10
2024-03-08 107.47 107.22
Up days: 121 of 260عمود. يُرجع prices["close"] عموداً واحداً على هيئة Series: نسخة أحادية البعد من DataFrame، بفهرس التواريخ نفسه. ويعرض .tail(3) آخر ثلاث قيم. ويصفها التذييل: Freq: B تعني تكرار أيام العمل، وdtype: float64 تعني أن القيم أعداد عشرية.
صفوف بحسب التاريخ. يختار .loc[...] الصفوف بتسميات فهرسها. وحين تكون التواريخ هي الفهرس، يمكنك كتابتها نصوصاً. والشريحة "2024-03-04":"2024-03-08" تشمل الطرفين، وهذا يختلف عن تقطيع القوائم. أما الجزء الثاني، وهو قائمة أسماء أعمدة، فيُبقي تلك الأعمدة فقط.
صفوف بحسب شرط. يقارن prices["close"] > prices["open"] العمودين صفاً بصف وينتج Series من True وFalse. ووضعه داخل prices[...] يُبقي الصفوف التي قيمتها True فقط. وهنا أُغلقت 121 شمعة من أصل 260 فوق افتتاحها.
إعادة التجميع إلى إطار زمني أعلى
تدمج إعادة التجميع الشموع في إطار زمني أطول. والنقطة الأساسية أن لكل عمود قاعدته الخاصة. فالشمعة الأسبوعية تفتح عند أول افتتاح يومي، وتبلغ أعلى سعر يومي وأدنى سعر يومي، وتغلق عند آخر إغلاق يومي، ويكون حجمها مجموع أحجام التداول.
import numpy as np
import pandas as pd
def make_prices(n_days=260, seed=42):
"""Synthetic daily OHLCV bars from a random walk. Not real market data."""
rng = np.random.default_rng(seed)
dates = pd.bdate_range("2024-01-01", periods=n_days, name="date")
close = 100 * np.exp(np.cumsum(rng.normal(0.0003, 0.012, n_days)))
prev_close = np.concatenate([[100.0], close[:-1]])
open_ = prev_close * (1 + rng.normal(0, 0.002, n_days))
high = np.maximum(open_, close) * (1 + np.abs(rng.normal(0, 0.004, n_days)))
low = np.minimum(open_, close) * (1 - np.abs(rng.normal(0, 0.004, n_days)))
volume = rng.integers(800_000, 1_500_000, n_days)
return pd.DataFrame(
{"open": open_, "high": high, "low": low, "close": close, "volume": volume},
index=dates,
)
prices = make_prices()
weekly = prices.resample("W-FRI").agg(
{"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}
)
print(weekly.head(3).round(2))
print(f"{len(prices)} daily bars -> {len(weekly)} weekly bars") open high low close volume
date
2024-01-05 99.95 101.94 98.51 98.96 5929307
2024-01-12 99.16 99.34 96.28 96.34 5898319
2024-01-19 96.20 101.03 95.92 100.41 6326310
260 daily bars -> 52 weekly barsيجمّع .resample("W-FRI") الصفوف في أسابيع تنتهي يوم الجمعة، ويعنون كل أسبوع بتاريخ ذلك الجمعة. ويطبّق .agg({...}) قاعدة مختلفة على كل عمود. قارن أول شمعة أسبوعية بالجدول اليومي الوارد سابقاً في هذا الدرس: افتتاحها، 99.95، هو افتتاح الاثنين؛ وأعلى سعر فيها، 101.94، هو أعلى سعر الجمعة، وهو الأكبر في الأسبوع؛ وأدنى سعر فيها، 98.51، هو أدنى سعر الأربعاء؛ وإغلاقها، 98.96، هو إغلاق الجمعة.
وتعمل القواعد الأخرى بالطريقة نفسها. يجمّع "ME" بحسب الشهر التقويمي، ويعنون بآخر يوم في الشهر، وستستخدمه في التمرين.
ما تعلّمته
يمكنك الآن أن تحتفظ بسنة من بيانات الأسعار في جدول واحد، وأن تختار أي جزء منها بحسب العمود أو التاريخ أو الشرط، وأن تحوّله إلى إطار زمني أعلى بالطريقة الصحيحة. يجري الدرس التالي عمليات حسابية على هذه الأعمدة: العوائد والمتوسطات المتحركة وأول إشارة.
أهم الخلاصات
- الـ DataFrame جدول بأعمدة معنونة وفهرس معنون؛ وفي بيانات الأسعار يكون الفهرس هو التاريخ.
- تُختار الأعمدة بالأقواس المربعة، والصفوف بالتاريخ بـ.loc، والصفوف بالشرط بمرشّح منطقي.
- إعادة التجميع (Resample) تدمج الشموع، ولكل عمود قاعدته الخاصة: أول Open، وأعلى High، وأدنى Low، وآخر Close، ومجموع Volume.
- البيانات الاصطناعية لا تحوي أخطاء خفية وتعطي الجميع الأرقام نفسها، لكنها لا تحوي أي سلوك حقيقي للسوق.
استكشف جدول أسعار اصطناعياً
باستخدام make_prices، ولّد بيانات سنتين ببذرة مختلفة. اعثر على أعلى إغلاق والتاريخ الذي حدث فيه (ابحث عن idxmax)، وعُدّ الأيام الصاعدة في كل شهر بإعادة تجميع عمود منطقي، وابنِ جدول OHLCV شهرياً بالقاعدة "ME". تحقّق يدوياً من أن أعلى سعر في شهر ما هو أكبر أعلى سعر يومي في ذلك الشهر.
اختبار ذاتي
أجب بكلماتك أولاً، ثم اكشف الإجابة.
ما الفرق بين
prices["close"]وprices.loc["2024-03-04":"2024-03-08"]؟اعرض الإجابةأخفِ الإجابة
الأول يختار عموداً واحداً، لكل التواريخ، على هيئة Series. والثاني يختار الصفوف بحسب تسميات تواريخها، لكل الأعمدة، على هيئة DataFrame. الأقواس المربعة مع اسم عمود تختار الأعمدة؛ و.loc تختار الصفوف بتسمية الفهرس.
عند إعادة تجميع الشموع اليومية إلى أسبوعية، لماذا لا يمكن أن تستخدم كل الأعمدة القاعدة نفسها، كالمتوسط؟
اعرض الإجابةأخفِ الإجابة
كل عمود يعني شيئاً مختلفاً. فافتتاح الأسبوع هو أول افتتاح يومي، وأعلى سعر هو الحد الأقصى لأعلى الأسعار اليومية، وأدنى سعر هو الحد الأدنى لأدنى الأسعار اليومية، والإغلاق هو آخر إغلاق يومي، وحجم التداول هو المجموع. وأخذ المتوسط لأي منها سينتج شمعة لم يجرِ عليها أي تداول.
تتضمن البيانات الاصطناعية يوم 25 ديسمبر 2024. لماذا، ولماذا يهمّ ذلك في البيانات الحقيقية؟
اعرض الإجابةأخفِ الإجابة
ينتج pd.bdate_range أيام الأسبوع ويتجاوز عطلات نهاية الأسبوع، لكنه لا يتجاوز عطلات البورصة. أما البيانات الحقيقية فلن تحوي شمعة في يوم عطلة. وعند العمل بالبيانات الحقيقية، خذ التواريخ من البيانات نفسها بدلاً من توليد تقويم، وافحص الأيام الناقصة أو الزائدة.
