VPS वर स्वतःचा स्टॉक रिसर्च एजंट कसा तयार करावा?
Python, DuckDB आणि Claude API वापरून स्टॉक रिसर्च एजंट कसा बनवायचा ते शिका. मार्केट डेटा फीड, systemd टाइमर आणि LLM स्क्रीन वापरून स्वयंचलित सिस्टम तयार करण्याची संपूर्ण माहिती.
स्वतः होस्ट केलेला stock research agent म्हणजे काय
स्वतःच्या नियंत्रणाखालील server वर चालणारा self-hosted stock research agent हा एक छोटा program असतो. तो ठरावीक वेळापत्रकानुसार market data आणतो, तो local database मध्ये साठवतो, त्या data वर screening करतो आणि काय बदलले याचा अहवाल large language model (LLM) कडून लिहून घेतो. तो data वाचतो आणि filter करतो. तो trading करत नाही. या मार्गदर्शिकेतील कोणतीही माहिती financial advice नाही.
हे काम सुरुवातीपासून करणारे दोन लोक वेगवेगळ्या libraries निवडू शकतात आणि तरीही त्यांची रचना समान चार भागांची असेल: prices आणि fundamentals देणारा feed, आतापर्यंत fetch केलेली प्रत्येक row साठवणारा local store, timer नुसार store refresh करणारा job आणि उरलेल्या rows चे वाक्यांमध्ये रूपांतर करणारा LLM layer. या मार्गदर्शिकेत ही रचना Python, DuckDB, systemd timer आणि Claude API (application programming interface) वापरून तयार केली आहे. हा निश्चित loop असलेला मर्यादित agent आहे. त्यामुळे आधी सामान्य स्वरूप समजून घ्यायचे असल्यास, AI agents शिकण्यासाठीचा टप्प्याटप्प्याचा मार्ग loop, tools आणि memory यांचा क्रमाने परिचय करून देतो आणि त्यांना एखाद्या domain problem मध्ये वापरण्यापूर्वीची रचना स्पष्ट करतो. Execution हा स्वतंत्र job आहे आणि त्याचे failure modes वेगळे आहेत. त्यामुळे तो trading bots साठी तयार केलेल्या VPS वर ठेवणे योग्य आहे.
चार भाग आणि प्रत्येकाचे कार्य
The feed हा बाहेरील जगाशी संवाद साधणारा एकमेव भाग आहे. त्याला टिकर (ticker) आणि तारखेची श्रेणी (date range) कशी मागायची आणि डेटाच्या ओळी (rows) कशा परत करायच्या हे माहित असते. फीडच्या पुढील सर्व प्रक्रिया फीडऐवजी थेट तुमच्या डेटाबेसवरून वाचतात, त्यामुळे फीड बंद पडल्यास केवळ एका दिवसाचा नवीन डेटा गहाळ होतो, संपूर्ण सिस्टिम बंद पडत नाही.
The store हा संपूर्ण प्रक्रियेचा मुख्य उद्देश आहे. तुम्ही नोंदवलेली नसलेली दैनंदिन क्लोजिंग किंमत सहसा नंतर पुन्हा मिळवता येते. परंतु, इंट्राडे कोट (intraday quote), सुधारित होण्यापूर्वीचा अंदाज किंवा पुन्हा मांडणी करण्यापूर्वीचा फंडामेंटल आकडा पुन्हा मिळवता येत नाही. डेटाने ज्या दिवशी जे सांगितले, त्या दिवसाची नोंद कशी ठेवायची हे The store ठरवते.
The scheduler रिफ्रेश कधी करायचा हे ठरवतो. सर्व्हरवर हे काम systemd timer द्वारे केले जाते, म्हणूनच येथे कोडपेक्षा VPS चे महत्त्व अधिक आहे.
The LLM layer तुमच्या SQL ने तयार केलेला एक छोटा मजकूर वाचतो आणि त्याचा सारांश लिहितो. तो कधीही डेटाबेसशी जोडला जात नाही आणि तो कधीही क्वेरी तयार करत नाही. जर मॉडेलनेच SQL लिहिले, तर एक चुकीचा टोकन प्रवाही वाक्यात चुकीचा आकडा बनू शकतो, ज्याची तुलना करण्यासाठी काहीही उरत नाही. जर SQL ने आकडे तयार केले, तर मॉडेल फक्त मजकुरात चूक करू शकते, आणि तुम्ही त्या मजकुराची पडताळणी तुम्ही पाठवलेल्या डेटाच्या ओळींशी (rows) करू शकता.
लॅपटॉपऐवजी VPS वर चालवणे का फायदेशीर आहे
याचे मुख्य कारण म्हणजे शेड्युलर. न्यूयॉर्क वेळेनुसार दुपारी 16:00 वाजता अमेरिकन बाजार बंद होतो, तेव्हा बर्लिनमध्ये रात्रीचे 22:00 वाजलेले असतात आणि जकार्तामध्ये दुसऱ्या दिवशी पहाटेचे 04:00 वाजलेले असतात. या दोन्ही वेळी लॅपटॉप स्लीप मोडमध्ये असतो. एखादी रन चुकल्यास उशिरा मिळालेल्या माहितीपेक्षा जास्त नुकसान होते: डेली बार्स सहसा नंतर पुन्हा मिळवता येतात, परंतु ज्या गोष्टींमध्ये सुधारणा (revision) केली जाते, त्या पुन्हा मिळवता येत नाहीत. त्यामुळे तुमच्या रेकॉर्डमधील ती त्रुटी कायमस्वरूपी राहते. हेच तर्क अशा प्रत्येक एजंटला लागू होते ज्याचे वेळापत्रक आणि साठवलेली स्थिती रीबूटनंतरही टिकून राहणे आवश्यक असते. KiroCrew ला तुमच्या स्वतःच्या VPS वर नेहमी चालू असलेला एजंट म्हणून चालवणे याच संकल्पनेवर आधारित आहे.
दुसरे कारण लहान असले तरी महत्त्वाचे आहे. सर्व्हरवर एक API key एका फाईलमध्ये साठवलेली असते, ज्याची मालकी अशा सिस्टिम युजरकडे असते ज्याला लॉगिन शेल नसते आणि ती फक्त एकाच कामासाठी वापरली जाते. तुम्ही ज्या लॅपटॉपवर इंटरनेट ब्राउझ करता, त्यावर अशी रचना करणे कठीण असते. की (key) कोडपासून आणि मॉडेलच्या इनपुटपासून दूर ठेवा, ज्याबद्दल API keys ला AI एजंटपासून सुरक्षित ठेवणे या विभागात सविस्तर माहिती दिली आहे.
आकारमान ठरवणे: डिस्क, RAM आणि tokens
डिस्क हा सोपा भाग आहे. दररोजची एक bar म्हणजे प्रत्येक ticker आणि trading day साठी एक row. US trading year मध्ये सुमारे 252 दिवस असतात.
या तक्त्यामागील डेटा
[
{
"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 rows असतील. 500 tickers line दहा वर्षांनंतर 1,260,000 rows पर्यंत पोहोचते. प्रत्येक row मध्ये एक date आणि काही doubles असतात. DuckDB columns compress करून साठवते. त्यामुळे आकार gigabytes नसून tens of megabytes इतका असेल. या अंदाजावर, माझ्या अंदाजावरही, पूर्णपणे विश्वास ठेवू नका. पहिला backfill झाल्यानंतर du -h /opt/research/data/market.duckdb चालवा आणि स्वतःचा आकडा वापरा.
लहान VPS साठी RAM हा महत्त्वाचा अडथळा ठरतो. DuckDB default ने एका query साठी मशीनच्या memory चा मोठा भाग आणि सर्व cores वापरते. Analytics server साठी हे योग्य आहे. पण इतर सेवा चालू असलेल्या 2 GB box साठी ते योग्य नाही. संपूर्ण prices table वर केलेली एक aggregation process ला kernel कडून kill केली जाऊ शकते. त्यानंतर systemd Main process exited, code=killed, status=9/KILL दाखवते आणि journalctl -k out of memory kill दाखवते. memory_limit आणि threads स्पष्टपणे सेट केल्यास query fail न होता धीमी होते.
Tokens चा अंदाज न लावता त्यांचे मोजमाप करा. प्रत्येक Messages API response मध्ये usage object असतो. त्यात input_tokens आणि output_tokens असतात. प्रत्येक call वेळी दोन्ही values table मध्ये लिहा. एका आठवड्यानंतर तुमचा प्रत्यक्ष volume कळेल. त्याला तुम्ही तपासणीच्या दिवशी model ने दिलेल्या किंमतीने गुणू शकता. नियोजनासाठी दोन गोष्टी पुरेशा स्थिर आहेत. प्रत्येक Claude model मध्ये output tokens ची किंमत input tokens पेक्षा जास्त असते. त्यामुळे note 200 words पर्यंत मर्यादित केल्यास तुम्ही पाठवणारा data कमी करण्यापेक्षा bill वर अधिक परिणाम होतो. तसेच, दिवसातून एकदा चालणाऱ्या job साठी prompt caching उपयोगी ठरत नाही. Cache default ने 5 minutes राहतो. Anthropic देत असलेला सर्वाधिक lifetime 1 hour आहे. त्यात cache लिहिण्याची किंमत base input price च्या 2x असते; 5 minute cache साठी ती 1.25x असते. कोणताही पर्याय निवडला तरी पुढील दिवसाच्या run पर्यंत cached block expire झालेला असतो. त्यामुळे पुन्हा पूर्ण input price द्यावी लागते. एका run मध्ये त्याच मोठ्या text block वर अनेक calls केल्या जात असतील, तेव्हा 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 प्रिंट करते. जर तुम्ही व्हर्च्युअल एन्व्हायरनमेंट वगळले आणि सिस्टम Python वर pip install चालवले, तर 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 ही फाईल स्वतः पार्स करते, ती शेलकडे पाठवत नाही:
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 नोंदी तयार होत नाहीत. ही key नसल्यास crash नंतर पुन्हा चालवल्यावर प्रत्येक bar शांतपणे duplicate होतो. त्यानंतर काढलेली प्रत्येक average चुकीची असते. हे सांगणारा कोणताही error message दिसत नाही.
DuckDB फाइल writing साठी उघडी ठेवण्याची परवानगी एका वेळी फक्त एका process ला देते. दुसरा writer त्वरित Could not set lock on file सह fail होतो. त्यानंतर ती फाइल धरून ठेवलेल्या PID ची माहिती दिसते. प्रत्यक्षात हा PID दुसऱ्या terminal मध्ये उघडा ठेवलेला interactive duckdb shell असतो. read-only connection वापरल्याने ही अडचण दूर होत नाही. DuckDB च्या concurrency दस्तऐवजात दोन modes दिले आहेत: एक process read आणि write करतो, किंवा अनेक processes फक्त read करतात. या दोन्हींचे मिश्रण करता येत नाही. connect मध्ये read_only flag देता येतो. त्यामुळे अनेक readers फाइल share करू शकतात. मात्र त्या वेळी कोणताही writer फाइल उघडून ठेवलेला नसावा. अनेक processes ना एकाच वेळी प्रत्यक्षात write करायचे असल्यास, त्यासाठी दुसरा database engine वापरावा: SQLite च्या WAL mode मध्ये एक writer commit करत असताना readers काम करू शकतात. busy timeout मुळे इतर writers fail होण्याऐवजी प्रतीक्षा करतात. server workload साठी DuckDB आणि SQLite ची तुलना या दोघांची तुलना करते. VPS वर production मध्ये SQLite चालवणे WAL योग्य प्रकारे कार्य करण्यासाठी आवश्यक settings स्पष्ट करते.
रिफ्रेश जॉब
# /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()तो एकदा हाताने (manually) चालवून पहा:
sudo -u research /opt/research/venv/bin/python /opt/research/refresh.pyपहिली रन अनेक वर्षांचा डेटा बॅकफिल करते आणि प्रत्येक टिकरसाठी हजारो ओळींची संख्या दर्शवते. एक मिनिटानंतर पुन्हा रन केल्यास प्रत्येक ओळीत 1 किंवा 2 ओळी दिसतात, कारण जॉब आधीच साठवलेल्या शेवटच्या दिवसापासून सुरू होतो. ती दुसरी रन हीच खरी चाचणी आहे: जर संख्या अजूनही हजारांमध्ये असेल, तर max(day) काहीही परत करत नाहीये आणि तुमची इन्सर्ट प्रक्रिया दररोज तुमचा संपूर्ण इतिहास पुन्हा लिहित आहे.
df.empty तपासणी ही फाईलमधील सर्वात महत्त्वाची ओळ आहे. Ticker.history() चुकीच्या किंवा डीलिस्ट केलेल्या सिम्बॉलसाठी एरर देत नाही. ते सिम्बॉल कदाचित डीलिस्ट झाले असावे आणि त्याची किंमत सापडली नाही अशी चेतावणी प्रिंट करते (याचे नेमके शब्द लायब्ररीच्या आवृत्त्यांनुसार बदलतात) आणि एक रिकामे DataFrame परत करते. ज्या जॉबमध्ये ही तपासणी नसते, तो काहीही न लिहिता 0 एक्झिट कोडसह थांबतो आणि systemd मध्ये तो यशस्वी (green) दिसतो, तर दुसरीकडे टेबलमध्ये डेटा वाढणे शांतपणे थांबलेले असते. हे तुम्हाला आठवड्याभराने समजते, जेव्हा स्क्रीनवर दररोज सारख्याच ओळी दिसतात.
टाइमस्टॅम्प हाताळण्यामागेही एक कारण आहे. फीडद्वारे मिळणाऱ्या इंडेक्समध्ये एक्सचेंजचे टाइमझोन असू शकते आणि ऑफसेट काढून टाकणे म्हणजे UTC मध्ये रूपांतर करणे नव्हे. टोकियो सत्राचा स्थानिक मध्यरात्रीचा टाइमस्टॅम्प UTC मध्ये रूपांतरित केल्यास तो मागील कॅलेंडर दिवसात जातो. त्यामुळे UTC रूपांतरण जपानी बारला नकळत एक दिवस मागे ढकलते आणि प्रायमरी की खराब करते. tz_localize(None) एक्सचेंजची स्वतःची सत्र तारीख कायम ठेवते, ज्याचा अर्थ डेली बार असा होतो.
बाजार बंद होताना चालणारा timer
US बाजार New York वेळेनुसार 16:00 वाजता बंद होतो. शेवटचे व्यवहार स्थिर होण्यासाठी काही मिनिटे लागतात. त्यामुळे job 16:20 वाजता चालवा. ही वेळ UTC मध्ये न देता New York वेळेनुसार लिहा. हिवाळ्यात New York वेळ UTC पेक्षा 5 तास मागे असते आणि उन्हाळ्यात 4 तास मागे असते. त्यामुळे fixed UTC hour वापरून लिहिलेला timer वर्षातून दोनदा एक तासाने बदलतो आणि बाजार बंद होण्यापूर्वी चालू लागतो. systemd 235 आणि त्यानंतरच्या आवृत्त्या 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 status ने बंद झाल्यास पुढील प्रक्रिया थांबवतो. तुम्हाला नेमके हेच वर्तन हवे आहे: refresh अयशस्वी झाल्यानंतर जुन्या डेटावर आधारित screen दाखवली जाऊ नये. Kernel updates साठी VPS reboot होत असल्यास Persistent=true महत्त्वाचे ठरते. Persistent=true शिवाय 16:15 वाजता reboot केल्यास run पूर्णपणे गमावला जातो. Persistent=true सह मशीन पुन्हा उपलब्ध होताच job चालते. एखादा agent सुरू करून लगेच बंद होण्याऐवजी सतत चालू ठेवायचा असल्यास उलट unit रचना आवश्यक असते. त्यात Restart= नियमांसह दीर्घकाळ चालणारी process असते आणि timer नसतो. systemd वापरून VPS वर dsh headless चालवणे याची सविस्तर पद्धत दाखवते.
sudo systemctl daemon-reload
sudo systemctl enable --now research-refresh.timer
systemctl list-timers research-refresh.timerlist-timers मध्ये NEXT column दिसला पाहिजे. त्यात server च्या स्थानिक वेळेत रूपांतरित केलेली पुढील weekday run वेळ असते. यादी रिकामी असल्यास timer enabled नाही किंवा unit मध्ये [Install] section नाही. त्यामुळे 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 अस्तित्वात असलेल्या सर्व ओळींची सरासरी काढते, त्यामुळे टिकरच्या तिसऱ्या ओळीवर तीन दिवसांची सरासरी मिळते आणि तरीही त्याला ma50 म्हटले जाते. याची तुलना ma20 शी केल्यास, प्रत्येक टिकरच्या इतिहासाच्या सुरुवातीला तुम्ही एक असा क्रॉसओव्हर तयार करता जो प्रत्यक्षात कधीच घडलेला नसतो. रो नंबरवर फिल्टर लावल्यामुळे ज्या ठिकाणी विंडो पूर्ण नव्हती, अशा ओळी वगळल्या जातात.
मॉडेलला काय दिसते आणि काय कधीच दिसत नाही
# /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,
thinking={'type': 'disabled'},
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()या मॉडेलमध्ये thinking ही ओळ महत्त्वाची आहे. Claude Sonnet 5 default पद्धतीने विचार करतो. त्याचे thinking tokens output म्हणून बिल केले जातात आणि max_tokens मध्ये मोजले जातात. तसेच ते मजकुराच्या आधी एका block स्वरूपात परत येतात. ही ओळ नसल्यास, resp.content[0] हा thinking block असू शकतो आणि .text lookup अयशस्वी होतो. Claude Sonnet 5 आता legacy model आहे. त्याचा उत्तराधिकारी claude-sonnet-5-5 हा disabled नाकारतो. त्यात वापरायची setting thinking={'type': 'between_tools'} आहे. ती उत्तर देण्यापूर्वी thinking बंद करते.
system prompt मधील word limit बिलाच्या महागड्या भागावर मर्यादा घालते. दिलेल्या rowsच वापरण्याची सूचना मात्र पडताळून पाहणे आवश्यक आहे; तिच्यावर फक्त विश्वास ठेवू नका. त्या block मधील एक column काढून पुन्हा चालवा आणि output वाचा. त्या column साठीची आकडेवारी अजूनही दिसत असेल, तर मॉडेलने रिकामी जागा स्वतः भरली आहे आणि तुमचा prompt पुरेसा काटेकोर नाही. या चाचणीला दोन मिनिटे लागतात. हे सत्यापित करण्याचा हा एकमेव प्रामाणिक मार्ग आहे.
मॉडेलला API key दिसत नाही. त्याला database path दिसत नाही. ते कोणतीही query चालवत नाही. त्याला rows मिळतात आणि ते prose परत करते. या सीमेमुळे output तपासता येतो. कारण note मधील प्रत्येक संख्या तुम्ही पाठवलेल्या block मध्येही असली पाहिजे आणि तुम्ही त्यांची line by line तुलना करू शकता. या input मध्ये web वरून मिळवलेली pages समाविष्ट केल्यावर ही सीमा बदलते. कारण headline किंवा forum post मध्ये तुमच्यासाठी असलेल्या facts ऐवजी मॉडेलला उद्देशून instructions असू शकतात. agent ला SearXNG search backend देणे याच प्रकारची surface उघडते. Prompting संदर्भातील अधिक माहितीसाठी finance analysis साठी Claude वापरणे मॉडेल काय वाचण्यात सक्षम आहे हे अधिक सविस्तर सांगते.
एक रन, सुरुवातीपासून शेवटपर्यंत
New York वेळेनुसार 16:20 वाजता timer सेवा सुरू करतो. refresh.py प्रत्येक ticker साठी मागील संग्रहित दिवसापासून सुरुवात करून feed कडून माहिती मागवतो, प्रत्येकासाठी एक किंवा दोन नवीन bars लिहितो आणि प्रत्येक ticker साठी एक ओळ छापतो. screen.py तीच file उघडतो, moving average query चालवतो आणि काही rows मिळवतो. त्या rows चे रूपांतर काहीशे tokens असलेल्या text block मध्ये होते. एका API call मधून short note तयार होते. तो note journal मध्ये लिहिला जातो आणि token counts तसेच hits ची संख्या असलेली एक row runs मध्ये नोंदवली जाते.
journalctl -u research-refresh.service -n 50 --no-pagerनिरोगी log मध्ये प्रत्येक ticker साठी एक ओळ, त्यानंतर note आणि मग research-refresh.service: Deactivated successfully असते. सर्व्हरवरील Claude Code session मधून तुम्ही हा log वाचत असताना दुसरे session screen query वर काम करत असेल, तर तुम्ही terminals दरम्यान ओळी copy करण्याऐवजी दोन्ही session मध्ये text पाठवू शकता. एका आठवड्यानंतर database मधून तुमचा प्रत्यक्ष खर्च काढा:
SELECT count(*) AS runs,
sum(input_tokens) AS in_tokens,
sum(output_tokens) AS out_tokens
FROM runs;हे totals तुम्ही ते वाचत असलेल्या दिवशी model ने दिलेल्या प्रति million token prices ने गुणा. त्यामुळे कोणाच्यातरी अंदाजाऐवजी तुम्हाला प्रत्यक्ष आकडा मिळतो. Published prices बदलतात. Arithmetic बदलत नाही.
Backtest मध्ये overfitting का होते आणि ते कसे घडताना पाहावे
दोन window lengths च्या function म्हणून screen पुन्हा लिहा, pairs चा grid तपासा आणि return नुसार त्यांची क्रमवारी लावा. सर्वोत्तम pair उत्कृष्ट दिसेल. हीच समस्या आहे; हा निकाल नाही. 200 pairs चा grid म्हणजे 200 experiments आहेत आणि तुम्ही त्यापैकी सर्वाधिक नशीबवान pair ठेवला आहे.
हे दहा मिनिटांत घडताना पाहता येते. Date नुसार store चे दोन समान भाग करा. फक्त पहिल्या भागावर grid तपासा आणि winner लिहून ठेवा. त्याच grid ची दुसऱ्या भागावर तपासणी करा. दोन्ही winners मध्ये मोठा फरक असल्यास parameters हे noise शी fit झाले आहेत. तसेच, ज्या अर्ध्या भागावर pair tune केला त्यावरच तो जिंकत असेल, तर उद्याबद्दल त्यातून काहीही निष्कर्ष निघत नाही.
Survivorship ही overfitting पेक्षा अधिक गंभीर समस्या आहे, कारण tuning ती दूर करू शकत नाही. तुमची ticker list आजच्या index members ची आहे. त्यामुळे त्यात फक्त टिकून राहिलेल्या कंपन्यांचा समावेश होतो. 2019 मध्ये delist झालेल्या ticker साठी feed कडे विचारल्यास तो empty frame परत करतो. त्यामुळे ती कंपनी तुमच्या store मध्ये कधीच येत नाही आणि तुमच्या test मध्येही येत नाही. तुम्ही चालवलेल्या प्रत्येक backtest मधून failures आधीच वगळले गेले आहेत.
Restated fundamentals मुळे timeline विस्कळीत होते. आज API द्वारे मिळणारा 2019 च्या तिमाहीचा महसूल आकडा हा 2019 मध्ये प्रकाशित झालेला आकडा असेलच असे नाही. आजचे fundamentals आणि 2019 चे prices एकत्र वापरणारा screen त्या वेळी उपलब्ध नसलेली माहिती वापरतो. येथे prices सहसा सुरक्षित असतात. Fundamentals सहसा सुरक्षित नसतात.
Adjusted prices तुमच्या नियंत्रणाखाली स्थिर राहत नाहीत. auto_adjust=True वापरल्यास dividends आणि splits साठी closes मागील तारखांकडे 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 writing साठी उघडी ठेवते. सहसा ती तुम्ही बंद करायला विसरलेली interactive shell असते. ती बंद करा. त्या writer ची process अद्याप सुरू असताना दुसरे connection read_only=True सह उघडल्याने उपयोग होत नाही, कारण DuckDB एकाच वेळी अनेक processes ना read किंवा एका process ला write करू देते; दोन्ही एकाच वेळी करू देत नाही.
Main process exited, code=killed, status=9/KILL in systemctl status. Kernel ने memory अभावी job बंद केली. journalctl -k | grep -i oom सह याची खात्री करा आणि नंतर store.py मधील memory_limit कमी करा.
हिरवा run, पण कोणतेही लेखन नाही. systemctl status active (exited) वाचते, पण table वाढलेली नाही. Feed ने रिकामे frames परत केले. हे अपयश सर्वाधिक काळ लपून राहते. त्यामुळे प्रत्येक ticker साठी रिकामा result आल्यास job ने non zero status सह बाहेर पडावे.
Timer market holiday दिवशी चालला. systemd ला exchange calendar माहीत नसतो. त्यामुळे Mon-Fri मध्ये holidays समाविष्ट असतात. Run होतो, feed मध्ये नवीन माहिती नसते आणि job ने याला error न मानता सामान्य स्थिती मानावी.
API कडून 429. तुम्ही rate limit ओलांडली आहे. Anthropic SDK स्वतःहून backoff सह retry करते आणि Anthropic(max_retries=5) प्रयत्नांची संख्या वाढवते. तरीही ते दररोज अपयशी ठरत असल्यास, job एका burst मध्ये खूप मोठी विनंती करत आहे.
हे काय नाही
हे एक संशोधन सहाय्यक (research assistant) आहे. फाइलिंगचा सारांश देणारे मॉडेल त्या फाइलिंगचे वाचन सादर करते. मजकुरात छापलेल्या आकड्यांबाबत ते आत्मविश्वासाने चुकीची माहिती देऊ शकते, म्हणूनच नोटमधील प्रत्येक आकृती तुम्ही पाठवलेल्या ओळीशी जुळवून तपासणे आवश्यक आहे. या आउटपुटला तुम्ही स्वतः वाचायच्या गोष्टींची एक निवडक यादी समजा. यात कोणताही आर्थिक सल्ला नाही आणि हा कोणताही सिग्नल नाही.
बॅकटेस्ट (backtests) कल्पना नाकारण्यासाठी उपयुक्त असतात, परंतु त्या सिद्ध करण्यासाठी त्या कमकुवत असतात. तुमच्या स्वतःच्या डेटावर जी रणनीती अपयशी ठरते, ती खरोखरच मृत असते. जी रणनीती यशस्वी होते, ती फक्त तुमच्या डेटावर टिकली आहे, हा दावा रात्री 1 वाजता वाटतो त्यापेक्षा खूपच मर्यादित आहे.
एक्झिक्युशन (execution) मुद्दाम व्याप्तीच्या बाहेर ठेवले आहे. ऑर्डर्स आणि ब्रोकर क्रेडेंशियल्सचा जोखीम प्रोफाइल हा केवळ वाचण्यासाठी असलेल्या (read only) रिसर्च बॉक्सपेक्षा वेगळा असतो. त्यांना एकत्र केल्यास ट्रेडिंग कीज (trading keys) त्याच मशीनवर येतात जिथे LLM प्रॉम्प्ट चालतो. तुमच्या स्वतःच्या सर्व्हरवर चालवण्यायोग्य इतर गोष्टींच्या तुलनेत ही रचना कुठे बसते हे तुम्हाला पाहायचे असेल, तर चालवण्यायोग्य self-hosted AI agents हा अधिक व्यापक आढावा आहे.
FAQ
मला सशुल्क मार्केट डेटा फीडची गरज आहे का?
प्रोटोटाइपसाठी गरज नाही. शिकण्याच्या प्रक्रियेत सिस्टिमचे स्वरूप समजून घेण्यासाठी मोफत अनधिकृत फीड वापरणे ठीक आहे. हे फीड कधीही बंद पडू शकते, कारण ते अशा वेबसाइटवर अवलंबून असते ज्याची तुमच्याप्रती कोणतीही जबाबदारी नाही. हे सहसा अपवादाऐवजी रिकाम्या फ्रेम्सच्या स्वरूपात बंद पडते, त्यामुळे तुमच्या जॉबने रो (row) काउंट तपासणे आवश्यक आहे. जेव्हा डेटाचा वापर निर्णयासाठी केला जाऊ लागेल, तेव्हा डॉक्युमेंटेड API आणि सपोर्ट ॲड्रेस असलेल्या सशुल्क फीडवर स्थलांतर करा. स्टोअरमुळे हा बदल स्वस्त होतो: फक्त फेच फंक्शन बदलते, तर शेड्युल, स्कीमा आणि स्क्रीन जसेच्या तसे राहतात.
किमतींचा डेटा SQLite मध्ये ठेवावा की DuckDB मध्ये?
DuckDB हे कॉलम-आधारित (columnar) असून ते अनेक रो स्कॅन करून ॲग्रीगेट काढण्यासाठी बनवलेले आहे, जे दहा वर्षांच्या बार्सचा मूव्हिंग ॲव्हरेज काढण्यासाठी अगदी योग्य आहे. SQLite हे रो-ओरिएंटेड आहे आणि एकाच वेळी अनेक प्रोसेसकडून होणाऱ्या लहान रीड आणि राइट ऑपरेशन्ससाठी अधिक चांगले आहे. जर एखादा शेड्युल्ड जॉब काही शे रो ॲपेंड करत असेल आणि नंतर लाखो रो स्कॅन करत असेल, तर DuckDB हा उत्तम पर्याय आहे. जर अनेक प्रोसेसना एकाच वेळी राइट करायचे असेल, तर SQLite मधील WAL मोड वापरल्यास एक रायटर कमिट करत असताना रीडर्स काम करू शकतात आणि बिझी टाइमआउटमुळे इतर रायटर्स फेल होण्याऐवजी प्रतीक्षा करतात.
LLM कॉल्सचा दरमहा खर्च किती येतो?
प्रत्येक रिस्पॉन्स मधील usage.input_tokens आणि usage.output_tokens एका टेबलमध्ये लॉग करा, त्यानंतर तुमच्या साप्ताहिक बेरजेला तुमच्या मॉडेलच्या प्रति दशलक्ष टोकन किमतीने गुणा (जी किंमत तुम्ही तपासता त्या दिवशीची असावी). हाच आकडा पुढच्या तिमाहीतही खरा राहील. एका लहान स्क्रीनवर दररोज चालवलेला जॉब कमी कॉल्स घेतो आणि नोटची लांबी तुमच्या नियंत्रणात असते: समरी 200 शब्दांवर मर्यादित ठेवल्यास कमी रो पाठवण्यापेक्षा जास्त बचत होते, कारण प्रत्येक Claude मॉडेलवर आउटपुट टोकन्सची किंमत इनपुट टोकन्सपेक्षा जास्त असते.
माझा जॉब यशस्वी झाला पण नवीन रो का लिहिल्या गेल्या नाहीत?
याची दोन सामान्य कारणे आहेत. मार्केट बंद असू शकते, कारण systemd Mon-Fri शेड्युलमध्ये एक्सचेंजच्या सुट्ट्यांचा समावेश नसतो. किंवा फीडने प्रत्येक टिकरसाठी रिकामी फ्रेम दिली असू शकते, ज्याची माहिती क्लायंट लायब्ररी सहसा अपवादाऐवजी प्रिंटेड वॉर्निंग म्हणून देतात, त्यामुळे प्रोसेस 0 एक्झिट कोड देते आणि systemd मध्ये रन यशस्वी (हिरवी) दिसते. SELECT max(day) FROM prices ची तुलना शेवटच्या प्रत्यक्ष ट्रेडिंग दिवसाशी करून यातील फरक ओळखा आणि जेव्हा सर्व टिकर्स रिकामे येतात तेव्हा जॉबला नॉन-झिरो एक्झिट कोड द्यायला सांगा.
एजंट काय खरेदी करायचे याचा निर्णय घेऊ शकतो का?
नाही, आणि तसा प्रयत्न करणे हीच या प्रकल्पांमधील मोठी चूक ठरते. मॉडेलला मार्केटचा ॲक्सेस नसतो, तुमच्या पोझिशनची किंवा टॅक्स परिस्थितीची माहिती नसते आणि स्वतःच्या आकड्यांची पडताळणी करण्याची सोय नसते. मॉडेल मोठ्या प्रमाणात मजकूर वाचून आज कोणत्या गोष्टींकडे लक्ष देणे गरजेचे आहे, हे सांगण्यात उत्तम आहे. ते जे काही लिहिते तो आर्थिक सल्ला नसून, निर्णय आणि त्याची जबाबदारी पूर्णपणे तुमचीच असते.