VPS پر خود میزبان stock research agent بنائیں
VPS پر stock research agent بنائیں: market data feed، DuckDB store، market close پر چلنے والا systemd timer، اور Claude API سے رپورٹ تیار کرنے والا LLM screen۔
خود میزبان stock research agent کیا ہے
خود میزبان stock research agent آپ کے زیرِ انتظام سرور پر چلنے والا ایک چھوٹا پروگرام ہے۔ یہ مقررہ وقفے سے market data حاصل کرتا ہے، اسے مقامی database میں محفوظ رکھتا ہے، اس پر screening چلاتا ہے، اور large language model (LLM) سے ہونے والی تبدیلیوں کی تحریری رپورٹ تیار کرواتا ہے۔ یہ data پڑھتا اور filter کرتا ہے۔ یہ trading نہیں کرتا، اور اس guide میں موجود کوئی بات financial advice نہیں ہے۔
دو افراد اگر یہ نظام شروع سے بنائیں تو وہ مختلف libraries منتخب کر سکتے ہیں، لیکن آخر میں ان کے نظام میں یہی چار حصے ہوں گے: ایک feed جو prices اور fundamentals فراہم کرے، ایک local store جو اب تک fetch کی گئی ہر row محفوظ رکھے، ایک job جو timer کے مطابق store کو refresh کرے، اور ایک LLM layer جو باقی رہ جانے والی rows کو جملوں میں تبدیل کرے۔ یہ guide Python، DuckDB، systemd timer اور Claude API (application programming interface) استعمال کرتے ہوئے یہی ساخت تیار کرتی ہے۔ Execution ایک الگ job ہے، اس کے failure modes بھی الگ ہیں، اور اسے اس کے بجائے trading bots کے لیے تیار کردہ VPS پر چلانا چاہیے۔
چار حصے، اور ہر حصے کا کام
feed وہ واحد حصہ ہے جو بیرونی دنیا سے رابطہ کرتا ہے۔ اسے معلوم ہوتا ہے کہ ticker اور date range کے لیے درخواست کیسے بھیجنی ہے اور rows واپس کیسے دینی ہیں۔ اس کے بعد کے تمام اجزا feed کے بجائے آپ کا database پڑھتے ہیں، اس لیے feed کی خرابی سے ایک دن کا نیا data ضائع ہوتا ہے، پوری screen خراب نہیں ہوتی۔
store اس پورے عمل کا بنیادی مقصد ہے۔ اگر آپ نے کسی دن کا daily close محفوظ نہیں کیا تو عموماً اسے بعد میں دوبارہ حاصل کیا جا سکتا ہے۔ لیکن intraday quote، نظرثانی سے پہلے کا estimate، یا restatement سے پہلے کا fundamentals figure دوبارہ حاصل نہیں کیا جا سکتا۔ store کے ذریعے آپ اس بات کا record بناتے ہیں کہ data نے اس دن حقیقتاً کیا بتایا تھا۔
scheduler یہ طے کرتا ہے کہ refresh کب ہوگا۔ server پر یہ systemd timer ہوتا ہے، اور اسی لیے یہاں VPS کی اہمیت code سے زیادہ ہے۔
LLM layer اس مختصر text block کو پڑھتی ہے جو آپ کی SQL تیار کرتی ہے، اور اس کا summary لکھتی ہے۔ یہ database سے کبھی connect نہیں ہوتی اور query بھی کبھی تیار نہیں کرتی۔ اگر model SQL لکھے تو ایک غلط token رواں جملے کے اندر غلط number بن جاتا ہے، اور اس کے مقابلے کے لیے کچھ موجود نہیں ہوتا۔ اگر SQL numbers تیار کرے تو model صرف prose میں غلطی کر سکتی ہے، اور آپ اس prose کو ان rows کے مقابل check کر سکتے ہیں جو آپ نے اسے بھیجی ہیں۔
VPS پر لیپ ٹاپ کے بجائے اسے کیوں چلائیں
Scheduler اس کی بنیادی وجہ ہے۔ US market میں 16:00 New York time پر اختتام Berlin میں 22:00 اور Jakarta میں اگلی صبح 04:00 ہوتا ہے۔ لیپ ٹاپ دونوں اوقات میں sleep mode میں ہوتا ہے۔ کوئی run رہ جائے تو اس کی قیمت صرف دیر سے ملنے والے نوٹ سے زیادہ ہوتی ہے: روزانہ کے bars عموماً بعد میں دوبارہ fetch کیے جا سکتے ہیں، لیکن جس چیز کے record میں بعد میں revision آ جائے اسے واپس نہیں لایا جا سکتا، اس لیے آپ کے record میں gap مستقل رہتا ہے۔ یہی منطق ایسے ہر agent پر لاگو ہوتی ہے جس کا schedule اور stored state reboot کے بعد بھی برقرار رہنا ضروری ہو۔ اسی مقصد کے لیے اپنے VPS پر KiroCrew کو ہمیشہ فعال agent کے طور پر چلانا بنایا گیا ہے۔
دوسری وجہ کم اہم ہے، لیکن حقیقی ہے۔ Server میں ایک ہی API key ایک ہی file میں رکھی جاتی ہے۔ اس file کا مالک ایک ایسا system user ہوتا ہے جس کا login shell نہیں ہوتا، اور اسے صرف ایک job استعمال کرتی ہے۔ جس laptop پر آپ web browsing بھی کرتے ہوں، اس پر یہی انتظام کرنا کہیں زیادہ مشکل ہے۔ Key کو code اور model کے input دونوں سے باہر رکھیں۔ یہی موضوع AI agent سے API keys کو باہر رکھنے میں بیان کیا گیا ہے۔
اس کا حجم مقرر کرنا: disk، RAM اور tokens
Disk آسان حصہ ہے۔ روزانہ کی ایک bar ہر ticker اور ہر trading day کے لیے ایک row ہوتی ہے، جبکہ US trading year میں تقریباً 252 دن ہوتے ہیں۔
The data behind this chart
[
{
"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"
}
]بیس tickers سے ایک سال بعد 5,040 rows بنتی ہیں۔ 500 tickers لائن دس سال بعد 1,260,000 rows تک پہنچتی ہے۔ ہر row میں ایک تاریخ اور چند doubles ہوتے ہیں، اور DuckDB columns کو compress کرکے store کرتا ہے، اس لیے حجم gigabytes کے بجائے دسیوں megabytes ہوتا ہے۔ اس تخمینے پر، میرے تخمینے سمیت، بھروسا نہ کریں۔ اپنی پہلی backfill کے بعد du -h /opt/research/data/market.duckdb چلائیں اور اپنا اصل حجم استعمال کریں۔
RAM وہ جگہ ہے جہاں چھوٹا VPS مسئلہ پیدا کرتا ہے۔ DuckDB بطور default ایک query کے لیے machine کی memory کا بڑا حصہ اور اس کے تمام cores استعمال کرتا ہے۔ analytics server پر یہ درست ہے، لیکن 2 GB box پر غلط ہے جہاں دوسری چیزیں بھی چل رہی ہوں۔ اس کے بعد پوری prices table پر ایک aggregation kernel کے ذریعے process کو kill کروا سکتی ہے، اور systemd Main process exited, code=killed, status=9/KILL report کرتا ہے، جبکہ journalctl -k out of memory kill دکھاتا ہے۔ memory_limit اور threads کو واضح طور پر set کریں، تو query ختم ہونے کے بجائے سست ہو جائے گی۔
Tokens کا اندازہ نہ لگائیں، انہیں measure کریں۔ ہر Messages API response میں usage object ہوتا ہے، جس میں input_tokens اور output_tokens شامل ہوتے ہیں۔ ہر call پر دونوں values ایک table میں لکھیں۔ ایک ہفتے بعد آپ کو اپنا اصل volume معلوم ہو جائے گا، جسے آپ اس دن model کی درج کردہ قیمت سے multiply کر سکتے ہیں۔ دو باتیں planning کے لیے کافی حد تک مستقل ہیں۔ ہر Claude model میں output tokens کی قیمت input tokens سے زیادہ ہے، اس لیے note کو 200 الفاظ تک محدود کرنے سے بھیجے جانے والے data کو کم کرنے کی نسبت bill زیادہ کم ہوتا ہے۔ Prompt caching روزانہ ایک بار چلنے والے job میں مدد نہیں کرتی، کیونکہ cache lifetime منٹوں میں ناپی جاتی ہے۔ اگلی run تک cached block expire ہو چکا ہوتا ہے اور آپ دوبارہ input کی پوری قیمت ادا کرتے ہیں۔ Caching اس وقت فائدہ دیتی ہے جب ایک run text کے اسی بڑے block پر بہت سی calls کرے۔
اجزا انسٹال کریں
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کوڈ لکھنے سے پہلے installation کی جانچ کریں:
sudo -u research /opt/research/venv/bin/python -c 'import duckdb, yfinance, anthropic; print("ok")'یہ ok دکھاتا ہے۔ اگر آپ نے virtual environment چھوڑ کر system Python کے خلاف pip install چلایا تو Ubuntu 24.04 آپ کو error: externally-managed-environment کے ساتھ روک دے گا، کیونکہ distribution /usr/lib/python3 کی مالک ہے اور pip کو وہاں لکھنے کی اجازت نہیں دیتی۔ venv محض احتیاط نہیں ہے۔ یہ واحد directory ہے جسے pip تبدیل کر سکتا ہے۔
API key ایسی file میں رکھیں جسے service user پڑھ سکے اور کوئی دوسرا نہ پڑھ سکے:
sudo install -d -m 755 /etc/research
sudo install -m 640 -o root -g research /dev/null /etc/research/env
sudoedit /etc/research/envاس file میں ایک line لکھیں، quotes اور export کے بغیر، کیونکہ systemd اس file کو خود parse کرتا ہے اور اسے 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) پر primary key ہی refresh کو بار بار محفوظ طریقے سے چلانے کے قابل بناتی ہے۔ INSERT OR REPLACE اس ticker اور اس دن کے لیے پہلے سے موجود row کو overwrite کر دیتی ہے، اس لیے backfill کو دو بار چلانے سے جدول میں duplicate rows شامل نہیں ہوتیں۔ اس key کے بغیر crash کے بعد دوبارہ چلانے سے ہر bar خاموشی سے duplicate ہو جاتی ہے، اور اس کے بعد نکالی جانے والی ہر average غلط ہوتی ہے، جبکہ کہیں کوئی error message موجود نہیں ہوتا جو آپ کو اس بارے میں بتائے۔
DuckDB صرف ایک process کو file کو writing کے لیے open رکھنے کی اجازت دیتا ہے۔ دوسرا writer فوراً Could not set lock on file کے ساتھ fail ہو جاتا ہے، جس کے بعد اس PID کا ذکر ہوتا ہے جو file کو hold کیے ہوئے ہے۔ عملی طور پر یہ عموماً وہ interactive duckdb shell ہوتی ہے جسے آپ نے کسی دوسرے terminal میں کھلا چھوڑ دیا ہوتا ہے۔ Readers read_only=True کے ذریعے گزر جاتے ہیں، اسی لیے connect یہ flag استعمال کرتا ہے۔ اگر متعدد processes کو واقعی ایک ہی وقت میں write کرنا ہو تو یہ کسی دوسرے engine کا کام ہے: WAL mode میں SQLite readers کو اس وقت کام کرنے دیتا ہے جب ایک writer commit کر رہا ہو، اور busy timeout دوسرے writers کو fail ہونے کے بجائے انتظار کرنے دیتا ہے۔ سرور workload کے لیے DuckDB اور SQLite کا تقابل دونوں کا موازنہ کرتا ہے، جبکہ VPS پر production میں SQLite چلانا ان settings کی وضاحت کرتا ہے جن سے WAL درست طور پر کام کرتا ہے۔
Refresh job
# /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پہلی بار چلنے پر یہ کئی سال کا سابقہ ڈیٹا شامل کرتا ہے اور ہر ticker کے لیے ہزاروں کی تعداد کے ساتھ ایک سطر دکھاتا ہے۔ ایک منٹ بعد اسے دوبارہ چلائیں۔ اب ہر سطر میں 1 یا 2 rows دکھائی دیں گی، کیونکہ job پہلے سے محفوظ آخری دن کے بعد سے شروع ہوتا ہے۔ یہی دوسری run اصل test ہے: اگر counts اب بھی ہزاروں میں ہوں تو max(day) کچھ واپس نہیں کر رہا، اور insert ہر رات آپ کی پوری history دوبارہ لکھ رہا ہے۔
فائل میں df.empty check سب سے اہم سطر ہے۔ Ticker.history() غلط یا delisted symbol پر exception نہیں اٹھاتا۔ یہ اس بارے میں warning دکھاتا ہے کہ symbol ممکنہ طور پر delisted ہے اور اس کے لیے price data نہیں ملا؛ درست wording library versions کے درمیان بدل سکتی ہے۔ پھر یہ خالی DataFrame واپس کرتا ہے۔ اس check کے بغیر job کچھ نہیں لکھتا، exit 0 کے ساتھ ختم ہو جاتا ہے، اور systemd ایک صحت مند green run دکھاتا رہتا ہے، جبکہ table خاموشی سے بڑھنا بند کر دیتی ہے۔ آپ کو کئی ہفتے بعد اس کا پتا چلتا ہے، جب کوئی screen ہر روز وہی rows واپس کرتی ہے۔
Timestamp handling کی بھی ایک وجہ ہے۔ Feed سے ملنے والے index میں exchange timezone شامل ہو سکتا ہے، اور offset حذف کرنا UTC میں تبدیل کرنے کے برابر نہیں ہے۔ Tokyo session کو مقامی وقت کے مطابق midnight پر درج کیا گیا ہو تو UTC میں تبدیل ہونے پر وہ پچھلے calendar day میں چلا جاتا ہے۔ اس طرح UTC conversion ہر Japanese bar کو خاموشی سے ایک دن پیچھے کر دیتی ہے اور primary key خراب ہو جاتی ہے۔ tz_localize(None) exchange کی اپنی session date برقرار رکھتا ہے، اور daily bar کا مطلب یہی date ہے۔
مارکیٹ بند ہونے کے وقت چلنے والا timer
US market New York کے وقت 16:00 پر بند ہوتی ہے، اور آخری trades کو settle ہونے میں چند منٹ لگتے ہیں، اس لیے job 16:20 پر چلتی ہے۔ یہ وقت New York کے وقت میں لکھیں، UTC میں نہیں۔ سردیوں میں New York کا وقت UTC سے 5 گھنٹے پیچھے اور گرمیوں میں 4 گھنٹے پیچھے ہوتا ہے۔ اس لیے fixed UTC hour کے طور پر لکھا گیا timer سال میں دو بار ایک گھنٹے کے فرق سے چلنے لگتا ہے اور market close سے پہلے fire ہوتا ہے۔ systemd 252 اور بعد کے ورژن OnCalendar میں timezone براہ راست قبول کرتے ہیں، جبکہ 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.targetType=oneshot واحد service type ہے جو ایک سے زیادہ ExecStart قبول کرتا ہے، اور انہیں ترتیب سے چلاتا ہے۔ اگر کوئی ایک non zero exit کرے تو یہ عمل رک جاتا ہے۔ یہی مطلوبہ رویہ ہے: failed refresh کے بعد stale data والی screen نہیں دکھانی چاہیے۔ Persistent=true اس VPS پر اہم ہے جو kernel updates کے لیے reboot ہوتا ہے۔ اس کے بغیر 16:15 پر reboot کرنے سے run ضائع ہو جاتی ہے؛ اس کے ساتھ machine واپس آتے ہی job چل جاتی ہے۔
sudo systemctl daemon-reload
sudo systemctl enable --now research-refresh.timer
systemctl list-timers research-refresh.timerlist-timers میں NEXT column دکھنا چاہیے، جس میں اگلی weekday run server کے اپنے local time میں converted ہو۔ خالی list کا مطلب ہے کہ timer enabled نہیں ہے، یا unit میں [Install] section موجود نہیں ہے، اس لیے enable کے پاس timers.target سے link کرنے کے لیے کچھ نہیں تھا۔
اسکرین: پہلے SQL، آخر میں model
SQL درست ہوتا ہے اور ہر run پر کوئی لاگت نہیں آتی۔ model کے ساتھ ایسا نہیں ہے۔ اس لیے query پہلے ممکنہ نتائج کا دائرہ محدود کرتی ہے، اور صرف باقی رہنے والے نتائج ہی model کو بھیجے جاتے ہیں۔ اسے /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 filter محض سجاوٹ نہیں ہے۔ ROWS BETWEEN 49 PRECEDING AND CURRENT ROW موجود rows کا اوسط نکالتا ہے، اس لیے ticker کی تیسری row تین دنوں کا اوسط واپس کرتی ہے اور پھر بھی اسے ma50 کہتی ہے۔ اس کا ma20 کے ساتھ موازنہ کریں تو آپ ہر ticker کی history کے آغاز میں ایک ایسا crossover بنا دیتے ہیں جو کبھی واقع ہی نہیں ہوا۔ row number پر filter لگانے سے وہ rows خارج ہو جاتی ہیں جن میں window مکمل نہیں تھی۔
ماڈل کیا دیکھتا ہے، اور کیا کبھی نہیں دیکھتا
# /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()system prompt میں word limit بل کے مہنگے حصے کو محدود کرتی ہے۔ صرف فراہم کردہ rows استعمال کرنے کی ہدایت وہ چیز ہے جس پر بھروسا کرنے کے بجائے آپ کو تصدیق کرنی چاہیے: block میں سے ایک column حذف کریں، اسے دوبارہ چلائیں، اور output پڑھیں۔ اگر اس column کا کوئی figure پھر بھی ظاہر ہو، تو ماڈل نے خالی جگہ خود پُر کی ہے اور آپ کا prompt کافی سخت نہیں ہے۔ اس test میں دو منٹ لگتے ہیں، اور حقیقت جاننے کا یہی واحد قابلِ اعتماد طریقہ ہے۔
ماڈل API key کبھی نہیں دیکھتا، database path کبھی نہیں دیکھتا، اور کوئی query بھی نہیں چلاتا۔ اسے rows موصول ہوتی ہیں اور یہ prose واپس کرتا ہے۔ یہی حد output کو قابلِ جانچ بناتی ہے، کیونکہ note میں موجود ہر number اس block میں بھی ہونا چاہیے جو آپ نے بھیجا ہے، اور آپ دونوں کا line by line موازنہ کر سکتے ہیں۔ prompting کے پہلو سے مزید معلومات کے لیے مالیاتی تجزیے کے لیے Claude کا استعمال دیکھیں؛ اس میں یہ بتایا گیا ہے کہ ماڈل کیا پڑھنے میں اچھا ہے۔
ایک run، ابتدا سے اختتام تک
New York وقت کے مطابق 16:20 پر timer سروس شروع کرتا ہے۔ refresh.py feed سے ہر ticker کے لیے آخری محفوظ شدہ دن سے آگے کا ڈیٹا طلب کرتا ہے، ہر ticker کے لیے ایک یا دو نئی bars لکھتا ہے، اور ہر ticker کے لیے ایک سطر print کرتا ہے۔ screen.py وہی file کھولتا ہے، moving average query چلاتا ہے، اور چند rows حاصل کرتا ہے۔ یہ rows چند سو tokens پر مشتمل text block بن جاتی ہیں۔ ایک API call انہیں مختصر note میں تبدیل کرتی ہے، note journal میں چلا جاتا ہے، اور runs میں ایک row درج ہوتی ہے جس میں token counts اور hits کی تعداد شامل ہوتی ہے۔
journalctl -u research-refresh.service -n 50 --no-pagerصحت مند log میں ہر ticker کے لیے ایک سطر، پھر note، اور اس کے بعد research-refresh.service: Deactivated successfully ہوتا ہے۔ ایک ہفتے بعد اپنے اصل اخراجات database سے حاصل کریں:
SELECT count(*) AS runs,
sum(input_tokens) AS in_tokens,
sum(output_tokens) AS out_tokens
FROM runs;ان totals کو ان per million token prices سے ضرب دیں جو آپ کا model اس دن درج کرتا ہے جس دن آپ انہیں پڑھتے ہیں۔ اس طرح کسی اور کے estimate کے بجائے اصل رقم معلوم ہو جاتی ہے۔ Published prices تبدیل ہوتی رہتی ہیں۔ حساب نہیں بدلتا۔
بیک ٹیسٹ overfit کیوں ہوتے ہیں، اور اسے ہوتے ہوئے کیسے دیکھیں
اس screen کو دونوں window lengths کے function کے طور پر دوبارہ لکھیں، pairs کی ایک grid پر sweep کریں، اور انہیں return کے لحاظ سے rank کریں۔ بہترین pair بہت عمدہ نظر آئے گا۔ مسئلہ یہی ہے، نتیجہ نہیں۔ 200 pairs کی grid دراصل 200 experiments ہیں، اور آپ نے ان میں سے سب سے خوش قسمت pair منتخب کیا ہے۔
آپ اسے دس منٹ میں ہوتے ہوئے دیکھ سکتے ہیں۔ تاریخ کے لحاظ سے store کو دو حصوں میں تقسیم کریں۔ صرف پہلے حصے پر grid sweep کریں اور winner لکھ لیں۔ پھر اسی grid کو دوسرے حصے پر sweep کریں۔ اگر دونوں winners میں نمایاں فرق ہو تو parameters noise پر fit ہو رہے ہیں، اور جو pair صرف اس نصف حصے پر جیتتا ہے جس پر آپ نے اسے tune کیا تھا، وہ کل کے بارے میں کچھ نہیں بتاتا۔
Survivorship overfitting سے بھی زیادہ خراب ہے، کیونکہ tuning اسے درست نہیں کر سکتی۔ آپ کی ticker list آج کے index members پر مشتمل ہے، اس لیے اس میں صرف وہ companies شامل ہیں جو باقی رہیں۔ Feed سے 2019 میں delist ہونے والے ticker کے بارے میں پوچھیں تو وہ ایک empty frame واپس کرتا ہے۔ اس کا مطلب ہے کہ وہ company کبھی آپ کے store میں شامل نہیں ہوتی اور نہ ہی آپ کے test میں آتی ہے۔ آپ کے چلائے گئے ہر backtest نے ناکام companies کو پہلے ہی خارج کر دیا ہے۔
Restated fundamentals timeline کو توڑ دیتے ہیں۔ API آج 2019 کی کسی quarter کے لیے جو revenue figure واپس کرتی ہے، وہ ہمیشہ وہ figure نہیں ہوتی جو 2019 میں شائع ہوئی تھی۔ آج کے fundamentals کو 2019 کی prices کے ساتھ ملانے والا screen ایسی معلومات استعمال کر رہا ہے جو اس وقت موجود نہیں تھیں۔ Prices عموماً اس معاملے میں محفوظ ہوتی ہیں۔ Fundamentals عموماً محفوظ نہیں ہوتیں۔
Adjusted prices آپ کے نیچے بدلتی رہتی ہیں۔ auto_adjust=True کے ساتھ closes کو dividends اور splits کے لیے ماضی کی طرف adjust کیا جاتا ہے، اس لیے اگلے ماہ یہی query چلانے پر قدرے مختلف history واپس آ سکتی ہے۔ استعمال کی گئی rows کو محفوظ کرنا ہی result کو reproducible بناتا ہے، اور یہی ایک اور وجہ ہے کہ local store موجود ہونا ضروری ہے۔
Backtest commissions اور slippage کو بھی نظرانداز کرتا ہے، اور یہ فرض کرتا ہے کہ آپ کا order price کو نہیں بدلے گا۔ یہ execution سے متعلق امور ہیں، جو یہاں scope سے باہر ہیں اور VPS پر trading bots چلانا میں بیان کیے گئے ہیں۔
ناکامی کی صورتیں اور نظر آنے والے پیغامات
error: externally-managed-environment جب pip چلتا ہے۔ آپ virtual environment سے باہر ہیں۔ /opt/research/venv/bin/pip کو مکمل path کے ساتھ چلائیں۔
Could not set lock on file، اور اس کے بعد PID موجود ہے۔ کوئی دوسرا process DuckDB file کو لکھنے کے لیے کھولے ہوئے ہے، عموماً وہ interactive shell جسے آپ بند کرنا بھول گئے ہیں۔ اسے بند کریں، یا دوسری connection read_only=True کے ساتھ کھولیں۔
Main process exited, code=killed, status=9/KILL systemctl status میں۔ kernel نے memory کی کمی کے باعث job ختم کر دی۔ journalctl -k | grep -i oom سے تصدیق کریں، پھر store.py میں memory_limit کم کریں۔
ایسی کامیاب run جس میں کچھ بھی نہ لکھا جائے۔ systemctl status، active (exited) پڑھتا ہے، لیکن table کا حجم نہیں بڑھا۔ feed نے خالی frames واپس کیے۔ یہ failure سب سے دیر سے ظاہر ہوتی ہے، اس لیے جب ہر ticker خالی واپس آئے تو job کو non zero exit status کے ساتھ ختم کریں۔
timer کا market holiday پر چل جانا۔ systemd کو exchange calendar کا علم نہیں ہوتا، اس لیے Mon-Fri میں holidays بھی شامل ہوتی ہیں۔ run ہو جاتی ہے، feed میں کوئی نیا data نہیں ہوتا، اور job کو اسے error کے بجائے معمول کی صورت حال سمجھنا چاہیے۔
API سے 429۔ آپ rate limit سے تجاوز کر رہے ہیں۔ Anthropic SDK خود backoff کے ساتھ دوبارہ کوشش کرتا ہے، اور Anthropic(max_retries=5) کوششوں کی تعداد بڑھاتا ہے۔ اگر اس کے باوجود یہ ہر روز ناکام ہو، تو job ایک ہی burst میں بہت زیادہ درخواستیں کر رہی ہے۔
یہ کیا نہیں ہے
یہ ایک تحقیقی معاون ہے۔ کسی filing کا خلاصہ تیار کرنے والا model اس filing کی اپنی تشریح پیش کرتا ہے، اور متن میں درج کسی عدد کے بارے میں پورے اعتماد سے غلط ہو سکتا ہے۔ اسی لیے note میں شامل ہر figure کا تعلق اس row سے ہونا چاہیے جو آپ نے بھیجی ہو۔ output کو ان چیزوں کی مختصر فہرست سمجھیں جنہیں آپ خود پڑھیں گے۔ یہاں کوئی financial advice شامل نہیں، اور یہ output کسی signal کی حیثیت نہیں رکھتا۔
Backtests خیالات کو مسترد کرنے کے لیے مفید، لیکن ان کی تصدیق کرنے میں کمزور ہوتے ہیں۔ جو strategy آپ کے اپنے data پر ناکام ہو جائے، وہ واقعی ختم ہے۔ جو strategy کامیاب ہو، اس نے صرف آپ کے data پر خود کو ثابت کیا ہے۔ یہ دعویٰ اس سے کہیں محدود ہے جتنا رات 1am بجے محسوس ہوتا ہے۔
Execution کو دانستہ طور پر دائرۂ کار سے باہر رکھا گیا ہے۔ Orders اور broker credentials کا risk profile read only research box سے مختلف ہوتا ہے، اور انہیں ملانے سے trading keys اسی machine پر آ جاتی ہیں جس پر LLM prompt چل رہا ہو۔ اگر آپ دیکھنا چاہتے ہیں کہ یہ pattern اپنے server پر چلانے کے قابل دیگر چیزوں کے ساتھ کہاں آتا ہے، تو اپنے server پر چلانے کے قابل self-hosted AI agents کا وسیع جائزہ دیکھیں۔
FAQ
کیا مجھے paid market data feed درکار ہے؟
prototype کے لیے نہیں۔ سیکھنے کے لیے ایک free unofficial feed کافی ہے، اور یہ ناکام ہوگا، کیونکہ یہ ایسی website پر منحصر ہے جس پر آپ کا کوئی حق نہیں۔ یہ عموماً exception کے بجائے empty frames کی صورت میں ناکام ہوتا ہے، اس لیے آپ کی job کو row counts ضرور چیک کرنے چاہییں۔ جب data کسی فیصلے میں استعمال ہونے لگے تو documented API اور support address والے paid feed پر منتقل ہو جائیں۔ data store اس تبدیلی کو کم خرچ بناتا ہے: صرف fetch function تبدیل ہوتا ہے، جبکہ schedule، schema اور screen جوں کے توں رہتے ہیں۔
کیا مجھے prices کو SQLite میں رکھنا چاہیے یا DuckDB میں؟
DuckDB columnar ہے اور aggregate نکالنے کے لیے بہت سی rows scan کرنے کے لیے بنایا گیا ہے، جو دس سال کے bars پر moving average نکالنے کے عین مطابق ہے۔ SQLite row oriented ہے اور ایک ہی وقت میں کئی processes کی بہت سی چھوٹی reads اور writes کے لیے بہتر ہے۔ ایک scheduled job کے لیے جو چند سو rows شامل کرے اور پھر millions rows scan کرے، DuckDB زیادہ موزوں ہے۔ اگر کئی processes کو بیک وقت write کرنا ہو تو WAL mode میں SQLite readers کو اس وقت بھی کام کرنے دیتا ہے جب ایک writer commit کر رہا ہو، اور busy timeout دوسرے writers کو فوراً fail ہونے کے بجائے انتظار کرنے دیتا ہے۔
LLM calls کی ماہانہ لاگت کتنی ہے؟
ہر response سے usage.input_tokens اور usage.output_tokens کو ایک table میں log کریں، پھر اپنے weekly totals کو اس per million token price سے ضرب دیں جو آپ کا model جانچنے کے دن درج کرتا ہے۔ اگلی سہ ماہی میں بھی درست رہنے والا واحد عدد یہی ہے۔ ایک مختصر screen پر روزانہ run میں calls کی تعداد کم ہوتی ہے، اور note کی length وہ حصہ ہے جسے آپ control کرتے ہیں: summary کو 200 words تک محدود کرنا rows کم بھیجنے سے زیادہ بچت کرتا ہے، کیونکہ ہر Claude model میں output tokens کی قیمت input tokens سے زیادہ ہے۔
میری job کامیاب ہوئی، لیکن نئی rows کیوں نہیں لکھیں؟
اس کی دو عام وجوہات ہیں۔ market بند تھی، کیونکہ systemd Mon-Fri schedule میں exchange holidays شامل ہوتی ہیں۔ یا feed نے ہر ticker کے لیے empty frame واپس کیا، جسے client libraries اکثر exception کے بجائے printed warning کے طور پر report کرتی ہیں؛ اس لیے process پھر بھی 0 پر exit ہوتا ہے اور systemd پھر بھی کامیاب run دکھاتا ہے۔ فرق معلوم کرنے کے لیے SELECT max(day) FROM prices کا آخری حقیقی trading day سے موازنہ کریں، اور جب ہر ticker empty واپس آئے تو job کو non zero پر exit کرائیں۔
کیا agent یہ فیصلہ کر سکتا ہے کہ کیا خریدنا ہے؟
نہیں، اور اسے ایسا کرنے کے لیے بنانا ہی وہ مقام ہے جہاں یہ projects غلط سمت میں جاتے ہیں۔ model کو market access حاصل نہیں، اسے آپ کی position یا tax situation کا علم نہیں، اور نہ ہی اس کے پاس اپنے numbers کو کسی چیز سے verify کرنے کا طریقہ ہے۔ یہ بڑی مقدار میں text پڑھ کر آپ کو بتانے میں مفید ہے کہ آج کن چند items پر آپ کی توجہ درکار ہے۔ اس کی لکھی ہوئی کوئی چیز financial advice نہیں ہے، اور فیصلہ، نیز اس کی ذمہ داری، آپ ہی کے پاس رہتی ہے۔