SSD Nodes Learn Hosting plans →
الأدلة Matt Connorبقلم Matt Connor · آخر تحديث في 2026-08-27

كيف تبني وكيل أبحاث أسهم على خادم VPS

أنشئ وكيلاً مستضافاً ذاتياً لأبحاث الأسهم على VPS باستخدام مصدر بيانات السوق وDuckDB ومؤقت systemd عند إغلاق السوق وواجهة Claude API للتصفية.

ما هو وكيل أبحاث الأسهم المستضاف ذاتياً

وكيل أبحاث الأسهم المستضاف ذاتياً هو برنامج صغير يعمل على خادم تملكه. يجلب بيانات السوق وفق جدول زمني، ويحفظها في قاعدة بيانات محلية، ويمررها عبر مرحلة تصفية، ثم يطلب من نموذج لغوي كبير (LLM) كتابة ملخص للتغييرات. يقرأ البيانات ويصفيها. ولا ينفذ عمليات تداول. كما أن أياً مما يرد في هذا الدليل لا يُعد نصيحة مالية.

قد يختار شخصان يبنيان هذا النظام من الصفر مكتبات مختلفة، لكنهما سينتهيان إلى الأجزاء الأربعة نفسها: مصدر بيانات يوفّر الأسعار والمؤشرات الأساسية، ومخزن محلي يحتفظ بكل صف جرى جلبه، ومهمة تحدّث المخزن وفق مؤقت، وطبقة LLM تحوّل الصفوف المتبقية إلى جمل. يبني هذا الدليل ذلك الهيكل باستخدام Python وDuckDB ومؤقت systemd وواجهة Claude API (واجهة برمجة التطبيقات). هذا وكيل محدود بحلقة ثابتة. لذلك، إذا كنت تفضّل التعرّف أولاً إلى النسخة العامة، فإن المسار التدريجي لتعلّم وكلاء الذكاء الاصطناعي يشرح الحلقة والأدوات والذاكرة بالترتيب قبل أن تتحول إلى مشكلة ضمن نطاق محدد. التنفيذ مهمة منفصلة لها أنماط فشل منفصلة، ولذلك يجب تشغيله على VPS مُعدّ لروبوتات التداول بدلاً من ذلك.

الأجزاء الأربعة ووظيفة كل جزء

مصدر البيانات هو الجزء الوحيد الذي يتصل بالعالم الخارجي. يعرف كيف يطلب رمزاً ونطاقاً زمنياً، وكيف يعيد الصفوف. تقرأ كل المكونات اللاحقة قاعدة بياناتك بدلاً من مصدر البيانات، لذلك لا يؤدي تعطل المصدر إلا إلى فقدان بيانات يوم واحد، بدلاً من تعطّل الشاشة.

مخزن البيانات هو جوهر العملية كلها. يمكن عادةً جلب سعر الإغلاق اليومي الذي لم تسجله مرة أخرى لاحقاً. لكن لا يمكن استعادة سعر لحظي، أو تقدير قبل تعديله، أو قيمة من البيانات الأساسية قبل إعادة بيانها. يتيح لك مخزن البيانات بناء سجل لما كانت البيانات تقوله فعلياً في اليوم الذي قالته فيه.

المجدول يحدد موعد التحديث. على الخادم، يكون ذلك عادةً مؤقت systemd، ولهذا يكون VPS أهم هنا من الشيفرة.

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

لماذا تشغّله على VPS بدلاً من جهاز محمول

الجدولة هي السبب. يكون إغلاق السوق الأمريكية عند الساعة 16:00 بتوقيت نيويورك، وعند الساعة 22:00 في برلين، وعند الساعة 04:00 من صباح اليوم التالي في جاكرتا. يكون جهازك المحمول في وضع السكون في كلتا الحالتين. وتكلفة التشغيل الفائت أعلى من تكلفة ملاحظة متأخرة: يمكن عادةً إعادة جلب البيانات اليومية لاحقاً، لكن لا يمكن استعادة ما خضع للمراجعة، لذلك تصبح الفجوة في سجلك دائمة. وينطبق المنطق نفسه على أي agent يجب أن يظل جدول تشغيله وحالته المخزّنة محفوظين بعد إعادة التشغيل، وهذا هو أساس تشغيل KiroCrew كـagent دائم التشغيل على VPS خاص بك.

السبب الثاني أقل أهمية، لكنه حقيقي أيضاً. يحتفظ الخادم بمفتاح API واحد في ملف واحد، يملكه مستخدم نظام واحد بلا shell لتسجيل الدخول، وتستخدمه مهمة واحدة. ويصعب ترتيب ذلك كثيراً على الجهاز المحمول الذي تستخدمه أيضاً لتصفح الويب. أبقِ المفتاح خارج التعليمات البرمجية وخارج إدخال النموذج، فهذا هو موضوع إبقاء مفاتيح API خارج agent للذكاء الاصطناعي.

تحديد الحجم: القرص وRAM والرموز

القرص هو الجزء الأسهل. يمثّل كل شريط يومي صفاً واحداً لكل ticker ولكل يوم تداول، وتضم سنة التداول في الولايات المتحدة نحو 252 يوماً.

رسم بيانيRows in the prices table by watchlist size, at 252 trading days a year
البيانات وراء هذا الرسم البياني
[
  {
    "label": "20 tickers",
    "rows_after_1y": "5,040",
    "rows_after_10y": "50,400"
  },
  {
    "label": "100 tickers",
    "rows_after_1y": "25,200",
    "rows_after_10y": "252,000"
  },
  {
    "label": "500 tickers",
    "rows_after_1y": "126,000",
    "rows_after_10y": "1,260,000"
  }
]

تنتج عشرون ticker ما مجموعه 5,040 صفاً بعد سنة. ويصل السطر 500 tickers إلى 1,260,000 صفاً بعد عشر سنوات. يحتوي كل صف على تاريخ وعدد قليل من قيم double، ويخزّن DuckDB الأعمدة بعد ضغطها، لذلك يكون الحجم عشرات الميغابايت، لا غيغابايت. لا تعتمد على هذا التقدير، بما في ذلك تقديري. شغّل du -h /opt/research/data/market.duckdb بعد أول عملية backfill، واستخدم الرقم الذي تحصل عليه بنفسك.

تظهر قيود RAM على VPS صغير. يستخدم DuckDB، افتراضياً، حصة كبيرة من ذاكرة الجهاز وجميع أنويته لتنفيذ استعلام واحد. هذا مناسب لخادم تحليلات، لكنه غير مناسب لجهاز بسعة 2 GB يشغّل خدمات أخرى أيضاً. قد تؤدي عملية aggregation واحدة على جدول الأسعار بأكمله إلى إنهاء العملية بواسطة kernel، وعندها يعرض systemd الرسالة Main process exited, code=killed, status=9/KILL، بينما يوضح journalctl -k حدوث عملية القتل بسبب نفاد الذاكرة. اضبط memory_limit وthreads صراحةً، وعندها يصبح الاستعلام أبطأ بدلاً من أن يتوقف.

يجب قياس الرموز، لا تقديرها. يحتوي كل رد من Messages API على كائن usage يتضمن input_tokens وoutput_tokens. اكتب القيمتين في جدول عند كل استدعاء. بعد أسبوع ستعرف حجم الاستخدام الفعلي، ويمكنك ضربه في السعر الذي يحدده النموذج في يوم التحقق. هناك نقطتان ثابتتان بما يكفي للتخطيط. تُسعَّر رموز الإخراج بأكثر من رموز الإدخال في كل نموذج Claude، لذلك فإن تحديد الملاحظة بـ200 كلمة يخفض التكلفة أكثر من تقليل البيانات التي ترسلها. كما أن prompt caching لا يفيد مهمة تُنفَّذ مرة واحدة يومياً، لأن مدة التخزين المؤقت تُقاس بالدقائق. عند التشغيل التالي تكون الكتلة المخزنة مؤقتاً قد انتهت صلاحيتها، فتدفع تكلفة الإدخال كاملة مرة أخرى. يكون caching مفيداً عندما ينفذ تشغيل واحد استدعاءات متعددة باستخدام الكتلة النصية الكبيرة نفسها.

ثبّت المكوّنات

sudo apt update
sudo apt install -y python3-venv
sudo useradd --system --create-home --home-dir /opt/research --shell /usr/sbin/nologin research
sudo -u research python3 -m venv /opt/research/venv
sudo -u research /opt/research/venv/bin/pip install duckdb pandas yfinance anthropic
sudo install -d -o research -g research -m 750 /opt/research/data

تحقّق من التثبيت قبل كتابة أي تعليمات برمجية:

sudo -u research /opt/research/venv/bin/python -c 'import duckdb, yfinance, anthropic; print("ok")'

يطبع ذلك ok. إذا تجاوزت virtual environment وشغّلت pip install باستخدام Python الخاص بالنظام، فسيوقفك Ubuntu 24.04 برسالة error: externally-managed-environment، لأن التوزيعة تملك /usr/lib/python3 وترفض السماح لـpip بالكتابة فيه. لا تُعدّ venv إجراءً شكلياً. فهي الدليل الوحيد الذي يُسمح لـpip بالكتابة فيه.

ضع مفتاح API في ملف يستطيع مستخدم الخدمة قراءته ولا يستطيع أي مستخدم آخر قراءته:

sudo install -d -m 755 /etc/research
sudo install -m 640 -o root -g research /dev/null /etc/research/env
sudoedit /etc/research/env

ضع فيه سطراً واحداً، من دون علامات اقتباس ومن دون export، لأن systemd يحلّل هذا الملف بنفسه بدلاً من تمريره إلى shell:

ANTHROPIC_API_KEY=sk-ant-your-key-here

المخزن: جدولان

# /opt/research/store.py
import duckdb

DB = '/opt/research/data/market.duckdb'

SCHEMA = [
    """
    CREATE TABLE IF NOT EXISTS prices (
      ticker VARCHAR,
      day    DATE,
      open   DOUBLE,
      high   DOUBLE,
      low    DOUBLE,
      close  DOUBLE,
      volume BIGINT,
      PRIMARY KEY (ticker, day)
    )
    """,
    """
    CREATE TABLE IF NOT EXISTS runs (
      started_at    TIMESTAMPTZ,
      model         VARCHAR,
      input_tokens  BIGINT,
      output_tokens BIGINT,
      hits          BIGINT
    )
    """,
]

def connect(read_only=False):
    con = duckdb.connect(DB, read_only=read_only)
    con.execute("SET memory_limit='512MB'")
    con.execute('SET threads=2')
    if not read_only:
        for statement in SCHEMA:
            con.execute(statement)
    return con

المفتاح الأساسي في (ticker, day) هو ما يجعل تحديث البيانات آمناً عند تكراره. يستبدل INSERT OR REPLACE الصف الموجود مسبقاً لهذا الرمز وفي ذلك اليوم، لذلك لا يؤدي تشغيل عملية ملء البيانات الناقصة مرتين إلى مضاعفة الصفوف في الجدول. من دون هذا المفتاح، تؤدي إعادة التشغيل بعد تعطل إلى تكرار كل شمعة سعريّة بصمت، وتصبح كل قيمة متوسطة تحسبها بعد ذلك خاطئة، من دون ظهور أي رسالة خطأ تنبّهك إلى المشكلة.

يسمح DuckDB لعملية واحدة فقط بفتح الملف للكتابة. تفشل أي عملية كتابة ثانية فوراً مع Could not set lock on file، ويتبعها PID الذي يحتفظ بالملف مفتوحاً؛ ويكون ذلك عملياً غالباً صدفة duckdb التفاعلية التي تركتها مفتوحة في نافذة طرفية أخرى. يمكن لعمليات القراءة أن تمر عبر read_only=True، ولهذا تستخدم connect هذا الخيار. إذا احتاجت عدة عمليات فعلاً إلى الكتابة في الوقت نفسه، فاستخدم محركاً آخر لهذا الغرض: يتيح SQLite في وضع WAL للقرّاء العمل أثناء تنفيذ عملية كتابة واحدة، وتؤدي مهلة الانتظار عند الانشغال إلى انتظار عمليات الكتابة الأخرى بدلاً من فشلها. تقارن DuckDB وSQLite لأحمال الخوادم بين المحركين، بينما يشرح تشغيل SQLite في بيئة الإنتاج على VPS الإعدادات التي تجعل WAL يعمل كما ينبغي.

مهمة التحديث

# /opt/research/refresh.py
import sys
import pandas as pd
import yfinance as yf
from store import connect

TICKERS = ['AAPL', 'MSFT', 'KO', 'SAP', 'TSM']
FIRST_DAY = '2016-01-01'
COLS = ['ticker', 'day', 'open', 'high', 'low', 'close', 'volume']

def fetch(ticker, start):
    df = yf.Ticker(ticker).history(start=start, auto_adjust=True)
    if df.empty:
        return None
    df = df.reset_index()
    stamps = pd.to_datetime(df['Date'])
    if stamps.dt.tz is not None:
        stamps = stamps.dt.tz_localize(None)
    df['day'] = stamps.dt.date
    df['ticker'] = ticker
    df = df.rename(columns={'Open': 'open', 'High': 'high', 'Low': 'low',
                            'Close': 'close', 'Volume': 'volume'})
    return df[COLS]

def main():
    con = connect()
    empty = 0
    for ticker in TICKERS:
        last = con.execute('SELECT max(day) FROM prices WHERE ticker = ?',
                           [ticker]).fetchone()[0]
        rows_df = fetch(ticker, str(last) if last else FIRST_DAY)
        if rows_df is None:
            print(ticker + ': feed returned no rows', file=sys.stderr)
            empty += 1
            continue
        con.register('rows_df', rows_df)
        con.execute('INSERT OR REPLACE INTO prices '
                    'SELECT ticker, day, open, high, low, close, volume FROM rows_df')
        print(ticker + ': ' + str(len(rows_df)) + ' rows')
    con.close()
    if empty == len(TICKERS):
        print('every ticker returned nothing: the feed is broken', file=sys.stderr)
        sys.exit(1)

main()

شغّلها يدوياً مرة واحدة:

sudo -u research /opt/research/venv/bin/python /opt/research/refresh.py

تعيد العملية الأولى ملء بيانات السنوات السابقة، وتطبع سطراً واحداً لكل رمز تداول، مع عدد بالآلاف. شغّلها مرة أخرى بعد دقيقة. سيعرض كل سطر صفاً أو صفين، لأن العملية تبدأ من آخر يوم مخزّن مسبقاً. هذا التشغيل الثاني هو الاختبار الفعلي: إذا بقيت الأعداد بالآلاف، فإن max(day) لا يعيد أي بيانات، وتعيد عملية الإدراج كتابة السجل الكامل كل ليلة.

يُعد فحص df.empty أهم سطر في الملف. لا يرفع Ticker.history() استثناءً عند استخدام رمز خاطئ أو رمز أُزيل من التداول. بل يطبع تحذيراً يفيد بإمكان إزالة الرمز من التداول مع عدم العثور على بيانات أسعار، وقد تختلف الصياغة الدقيقة بين إصدارات المكتبة، ثم يعيد DataFrame فارغاً. إذا لم تتضمن المهمة هذا الفحص، فلن تكتب شيئاً، وستخرج بالرمز 0، وسيعرض systemd عملية ناجحة باللون الأخضر، بينما يتوقف الجدول عن النمو دون ملاحظة. ستكتشف ذلك بعد أسابيع، عندما تعيد الشاشة الصفوف نفسها كل يوم.

كما أن لمعالجة الطوابع الزمنية سبباً. قد يحمل الفهرس الذي يعيده مصدر البيانات المنطقة الزمنية للبورصة، وإسقاط الإزاحة الزمنية ليس مماثلاً للتحويل إلى UTC. تتحول جلسة في Tokyo تحمل طابعاً عند منتصف الليل بالتوقيت المحلي إلى اليوم التقويمي السابق في UTC. لذلك يؤدي التحويل إلى UTC بصمت إلى إرجاع كل شمعة يابانية يوماً واحداً، ويكسر المفتاح الأساسي. يحافظ tz_localize(None) على تاريخ جلسة البورصة نفسه، وهذا هو المقصود بالشمعة اليومية.

المؤقت الذي يعمل عند إغلاق السوق

يغلق السوق الأمريكي عند الساعة 16:00 بتوقيت نيويورك، وتحتاج آخر الصفقات إلى بضع دقائق حتى تستقر، لذلك تعمل المهمة عند الساعة 16:20. اكتب ذلك بتوقيت نيويورك، وليس بتوقيت UTC. يكون توقيت نيويورك متأخراً عن UTC بمقدار 5 ساعات في الشتاء و4 ساعات في الصيف، لذلك ينحرف المؤقت المكتوب كساعة UTC ثابتة بمقدار ساعة مرتين سنوياً، ويبدأ بالعمل قبل الإغلاق. تقبل systemd 252 والإصدارات الأحدث المنطقة الزمنية مباشرة في OnCalendar، وتأتي Ubuntu 24.04 مع الإصدار 255. تحقّق من إصدارك باستخدام systemctl --version.

# /etc/systemd/system/research-refresh.service
[Unit]
Description=Refresh market data and run the daily screen
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
User=research
Group=research
WorkingDirectory=/opt/research
EnvironmentFile=/etc/research/env
ExecStart=/opt/research/venv/bin/python /opt/research/refresh.py
ExecStart=/opt/research/venv/bin/python /opt/research/screen.py
# /etc/systemd/system/research-refresh.timer
[Unit]
Description=Run the refresh after the US market close

[Timer]
OnCalendar=Mon-Fri 16:20 America/New_York
Persistent=true
RandomizedDelaySec=180

[Install]
WantedBy=timers.target

Type=oneshot هو نوع الخدمة الوحيد الذي يقبل أكثر من ExecStart، ويشغّلها بالترتيب، مع التوقف إذا انتهت إحداها بقيمة غير صفرية. هذا هو السلوك المطلوب تماماً: يجب ألا يتبع فشل تحديث البيانات عرض شاشة تحتوي على بيانات قديمة. يهم Persistent=true على VPS يُعاد تشغيله بسبب تحديثات kernel. إذا أعدت التشغيل عند الساعة 16:15 من دونه، فستُفقد المهمة ببساطة؛ أما معه، فتعمل المهمة فور عودة الجهاز. يحتاج الوكيل الذي يجب أن يبقى قيد التشغيل، بدلاً من أن يعمل ثم ينتهي، إلى شكل الوحدة المعاكس: عملية طويلة التشغيل مع قواعد Restart= ومن دون مؤقت على الإطلاق. يشرح تشغيل dsh دون واجهة على VPS باستخدام systemd هذه الطريقة.

sudo systemctl daemon-reload
sudo systemctl enable --now research-refresh.timer
systemctl list-timers research-refresh.timer

يجب أن يعرض list-timers عموداً باسم NEXT يحتوي على موعد التشغيل التالي في يوم من أيام الأسبوع، بعد تحويله إلى التوقيت المحلي الخاص بالخادم. تعني القائمة الفارغة أن المؤقت غير مفعّل، أو أن الوحدة تفتقد القسم [Install]، ولذلك لم يكن لدى enable ما يربطه بـtimers.target.

الشاشة: SQL أولاً، والنموذج أخيراً

SQL دقيق ولا يكلّف شيئاً في كل عملية تشغيل. أما النموذج فليس كذلك. لذلك يضيّق الاستعلام نطاق البيانات، ولا تُرسل إلى النموذج إلا النتائج التي اجتازت التصفية. احفظ هذا باسم /opt/research/screen.sql.

WITH ma AS (
  SELECT ticker, day, close,
         avg(close) OVER w20 AS ma20,
         avg(close) OVER w50 AS ma50,
         row_number() OVER (PARTITION BY ticker ORDER BY day) AS n
  FROM prices
  WINDOW
    w20 AS (PARTITION BY ticker ORDER BY day ROWS BETWEEN 19 PRECEDING AND CURRENT ROW),
    w50 AS (PARTITION BY ticker ORDER BY day ROWS BETWEEN 49 PRECEDING AND CURRENT ROW)
)
SELECT ticker, day, close, round(ma20, 2) AS ma20, round(ma50, 2) AS ma50
FROM ma
WHERE n > 50 AND ma20 > ma50
ORDER BY day DESC, ticker
LIMIT 20;

عامل التصفية n > 50 ليس للزينة. يحسب ROWS BETWEEN 49 PRECEDING AND CURRENT ROW متوسط أي صفوف موجودة، ولذلك يعيد الصف الثالث من ticker متوسط ثلاثة أيام، ومع ذلك يسمّيه ma50. إذا قارنت ذلك مع ma20، فستنشئ تقاطعاً عند بداية سجل كل ticker، رغم أنه لم يحدث فعلياً. تؤدي التصفية حسب رقم الصف إلى إسقاط الصفوف التي لم تكن النافذة فيها مكتملة.

ما يراه النموذج، وما لا يراه أبداً

# /opt/research/screen.py
from anthropic import Anthropic
from store import connect

SYSTEM = (
    'You are a research assistant. Use only the rows in the message. '
    'If a number is not in the rows, say that it is not available. '
    'Do not give investment advice, price targets or buy and sell calls. '
    'Write at most 200 words.'
)

con = connect()
sql = open('/opt/research/screen.sql').read()
rows = con.execute(sql).fetchall()
block = '\n'.join(' / '.join(str(v) for v in row) for row in rows)

client = Anthropic(max_retries=5)   # reads ANTHROPIC_API_KEY from the environment
resp = client.messages.create(
    model='claude-sonnet-5',
    max_tokens=600,
    system=SYSTEM,
    messages=[{'role': 'user', 'content': 'Screen hits, ticker / day / close / ma20 / ma50:\n' + block}],
)

print(resp.content[0].text)
con.execute('INSERT INTO runs VALUES (now(), ?, ?, ?, ?)',
            ['claude-sonnet-5', resp.usage.input_tokens,
             resp.usage.output_tokens, len(rows)])
con.close()

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

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

تشغيل واحد من البداية إلى النهاية

عند الساعة 16:20 بتوقيت نيويورك، يشغّل المؤقت الخدمة. يطلب refresh.py بيانات كل ticker بدءاً من آخر يوم مخزّن، ويكتب bar واحداً أو barين جديدين لكل منها، ثم يطبع سطراً لكل ticker. يفتح screen.py الملف نفسه، ويشغّل استعلام المتوسط المتحرك، ويحصل على عدة صفوف. تتحول هذه الصفوف إلى كتلة نصية تتكوّن من بضع مئات من الرموز. يحوّلها استدعاء API واحد إلى ملاحظة قصيرة، وتُكتب الملاحظة في journal، ويُضاف صف واحد إلى runs يتضمن أعداد الرموز وعدد مرات الاستدعاء.

journalctl -u research-refresh.service -n 50 --no-pager

يحتوي السجل السليم على سطر واحد لكل ticker، ثم الملاحظة، ثم research-refresh.service: Deactivated successfully. إذا كنت تقرأ هذا السجل من جلسة Claude Code على الخادم، بينما تعمل جلسة ثانية على استعلام الشاشة، فيمكن للجلسـتين تمرير النص إحداهما إلى الأخرى بدلاً من نسخ الأسطر يدوياً بين الطرفيات. بعد أسبوع، استخرج تكلفتك الفعلية من قاعدة البيانات:

SELECT count(*) AS runs,
       sum(input_tokens)  AS in_tokens,
       sum(output_tokens) AS out_tokens
FROM runs;

اضرب هذه الإجماليات في أسعار كل مليون token التي يوردها نموذجك في اليوم الذي تقرأها فيه. تحصل بذلك على الرقم الفعلي بدلاً من تقدير شخص آخر. تتغير الأسعار المنشورة. أما الحساب فلا يتغير.

لماذا تُفرِط اختبارات الأداء التاريخية في الملاءمة، وكيف تراقب حدوث ذلك

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

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

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

البيانات الأساسية المعاد بيانها تكسر التسلسل الزمني. فقيمة الإيرادات التي تعيدها واجهة API اليوم لربع من عام 2019 ليست دائماً القيمة التي نُشرت في 2019. ويستخدم الفرز الذي يخلط البيانات الأساسية الحالية مع أسعار 2019 معلومات لم تكن متاحة آنذاك. تكون الأسعار آمنة عادةً في هذه الحالة، أما البيانات الأساسية فعادةً لا تكون كذلك.

تتغير الأسعار المعدّلة من دون ثبات تحت الاختبار. مع auto_adjust=True، تُعدَّل أسعار الإغلاق بأثر رجعي بسبب توزيعات الأرباح وتقسيمات الأسهم، ولذلك يعيد الاستعلام نفسه عند تشغيله الشهر المقبل سجلاً تاريخياً مختلفاً قليلاً. إن تخزين الصفوف التي استخدمتها فعلياً هو ما يجعل النتيجة قابلة لإعادة الإنتاج، وهذا سبب آخر لوجود مخزن البيانات المحلي أصلاً.

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

أوضاع الفشل والرسائل التي ستظهر لك

error: externally-managed-environment عند تشغيل pip. أنت خارج البيئة الافتراضية. شغّل /opt/research/venv/bin/pip باستخدام المسار الكامل.

Could not set lock on file، ويتبعه PID. توجد عملية أخرى تفتح ملف DuckDB للكتابة، وعادةً تكون shell تفاعلية نسيتها مفتوحة. أغلقها، أو افتح الاتصال الثاني باستخدام read_only=True.

Main process exited, code=killed, status=9/KILL في systemctl status. أوقف kernel المهمة بسبب استهلاك الذاكرة. تأكد باستخدام journalctl -k | grep -i oom، ثم خفّض memory_limit في store.py.

تشغيل ناجح لا يكتب شيئاً. يقرأ systemctl status من active (exited)، ولم يزد حجم الجدول. أعاد مصدر البيانات إطارات فارغة. يصعب اكتشاف هذا الفشل أكثر من غيره، لذلك اجعل المهمة تنتهي برمز خطأ عندما تعود جميع مؤشرات التداول فارغة.

عمل المؤقت في عطلة من عطلات السوق. لا يعرف systemd تقويم البورصة، لذلك يتضمن Mon-Fri العطلات. تعمل المهمة، لكن مصدر البيانات لا يعيد بيانات جديدة، ويجب أن تتعامل المهمة مع ذلك بوصفه حالة طبيعية لا خطأ.

429 من API. لقد تجاوزت حد معدل الطلبات. يعيد Anthropic SDK المحاولة تلقائياً مع تأخير تدريجي، ويزيد Anthropic(max_retries=5) عدد المحاولات. إذا استمر الفشل يومياً، فالمهمة تطلب بيانات كثيرة جداً في دفعة واحدة.

ما هذا ليس عليه

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

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

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

FAQ

هل أحتاج إلى موجّه بيانات سوق مدفوع؟

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

هل أحتفظ بالأسعار في SQLite أم DuckDB؟

DuckDB قاعدة بيانات عمودية ومصممة لفحص عدد كبير من الصفوف لحساب قيمة تجميعية، وهذا يطابق تماماً حساب متوسط متحرك على بيانات عشر سنوات من الشموع. أما SQLite فهي موجهة إلى الصفوف وتناسب عمليات القراءة والكتابة الصغيرة الكثيرة من عدة عمليات في الوقت نفسه. بالنسبة إلى مهمة مجدولة واحدة تضيف بضع مئات من الصفوف ثم تفحص ملايين الصفوف، فإن DuckDB أنسب. وإذا كان يجب على عدة عمليات الكتابة في الوقت نفسه، فإن SQLite في وضع WAL تتيح للقرّاء العمل بينما ينفّذ كاتب واحد عملية commit، كما يجعل مهلة الانتظار للانشغال الكتّاب الآخرين ينتظرون بدلاً من فشلهم مباشرةً.

ما تكلفة استدعاءات LLM شهرياً؟

سجّل usage.input_tokens وusage.output_tokens من كل استجابة في جدول، ثم اضرب الإجماليات الأسبوعية في السعر لكل مليون token الذي يورده النموذج في يوم التحقق. هذا هو الرقم الوحيد الذي يبقى صحيحاً في الربع القادم. إن التشغيل اليومي على قائمة قصيرة يتطلب عدداً قليلاً من الاستدعاءات، وطول الملاحظة هو الجزء الذي يمكنك التحكم فيه: يؤدي تحديد الملخص بـ200 كلمة إلى توفير أكبر من إرسال عدد أقل من الصفوف، لأن سعر output tokens أعلى من سعر input tokens في كل نماذج Claude.

لماذا نجحت مهمتي لكنها لم تكتب صفوفاً جديدة؟

هناك سببان شائعان. كان السوق مغلقاً، لأن جدولة systemd Mon-Fri تتضمن عطلات البورصة. أو أعاد الموجّه إطاراً فارغاً لكل رمز، وهو ما تعرضه مكتبات العملاء غالباً كتحذير مطبوع بدلاً من استثناء، لذلك تنتهي العملية بالحالة 0 ويظل systemd يعرض تشغيلًا ناجحاً. ميّز بين السببين بمقارنة SELECT max(day) FROM prices بآخر يوم تداول فعلي، واجعل المهمة تنتهي بحالة غير صفرية عندما تكون نتيجة كل الرموز فارغة.

هل يستطيع الوكيل أن يقرر ما الذي أشتريه؟

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