SSD Nodes Learn Hosting plans →
मार्गदर्शक Matt Connorद्वारे Matt Connor · अपडेटेड 2026-08-13

VPS वर स्वतःचा स्टॉक रिसर्च एजंट कसा तयार करावा?

Python, DuckDB आणि Claude API वापरून स्टॉक रिसर्च एजंट कसा बनवायचा ते शिका. मार्केट डेटा फीड, systemd टाइमर आणि LLM स्क्रीन वापरून स्वयंचलित सिस्टम तयार करण्याची संपूर्ण माहिती.

Self-hosted स्टॉक रिसर्च एजंट म्हणजे काय

Self-hosted स्टॉक रिसर्च एजंट हा तुमच्या मालकीच्या सर्व्हरवर चालणारा एक छोटा प्रोग्राम आहे. तो ठराविक वेळेत मार्केट डेटा मिळवतो, तो स्थानिक डेटाबेसमध्ये साठवतो, त्यावर फिल्टर लावतो आणि काय बदल झाले आहेत हे लिहिण्यासाठी एका large language model (LLM) ला विचारतो. तो डेटा वाचतो आणि फिल्टर करतो. हा प्रोग्राम ट्रेडिंग करत नाही आणि या मार्गदर्शिकेतील कोणतीही गोष्ट आर्थिक सल्ला नाही.

दोन व्यक्तींनी शून्यापासून हे तयार करायचे ठरवले, तर त्या वेगवेगळ्या लायब्ररीज निवडतील, तरीही शेवटी त्यांना चार समान भाग मिळतील: किमती आणि फंडामेंटल्स पुरवणारा फीड, तुम्ही आतापर्यंत मिळवलेली प्रत्येक ओळ साठवणारा स्थानिक स्टोअर, ठराविक वेळेवर स्टोअर रिफ्रेश करणारी जॉब, आणि उरलेल्या ओळींचे वाक्यांत रूपांतर करणारा LLM स्तर. ही मार्गदर्शिका Python, DuckDB, systemd timer आणि Claude API (application programming interface) वापरून ही रचना तयार करते. अंमलबजावणी (execution) हे एक वेगळे काम आहे आणि त्याचे अपयशाचे प्रकार वेगळे आहेत, त्यामुळे ते ट्रेडिंग बॉट्ससाठी सेट केलेल्या 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 आणि टोकन्स

डिस्कचा विचार करणे सोपे आहे. दररोजची एक बार म्हणजे प्रत्येक टिकरसाठी प्रत्येक ट्रेडिंग दिवसाची एक ओळ आणि अमेरिकन ट्रेडिंग वर्ष सुमारे 252 दिवसांचे असते.

ChartRows in the prices table by watchlist size, at 252 trading days a year
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"
  }
]

वीस टिकर्ससाठी एका वर्षानंतर 5,040 ओळी तयार होतात. 500 tickers ओळ दहा वर्षांनंतर 1,260,000 ओळींपर्यंत पोहोचते. प्रत्येक ओळीत एक तारीख आणि काही doubles असतात. DuckDB कॉलम्स कॉम्प्रेस करून साठवते, त्यामुळे हे प्रमाण गिगाबाइट्सऐवजी काही दहा मेगाबाइट्स इतकेच असते. माझ्या अंदाजासह कोणत्याही अंदाजावर आंधळेपणाने विश्वास ठेवू नका. तुमचा पहिला backfill पूर्ण झाल्यावर du -h /opt/research/data/market.duckdb चालवा आणि स्वतःचे आकडे वापरा.

लहान VPS वर RAM ही समस्या ठरू शकते. DuckDB डीफॉल्टनुसार मशीनच्या मेमरीचा मोठा हिस्सा आणि सर्व कोअर्स एकाच क्वेरीसाठी वापरते. हे ॲनालिटिक्स सर्व्हरसाठी योग्य असले तरी, इतर सेवा चालवणाऱ्या 2 GB च्या बॉक्ससाठी ते चुकीचे आहे. संपूर्ण prices टेबलवर एक aggregation केल्यास kernel द्वारे प्रक्रिया बंद केली जाते आणि systemd Main process exited, code=killed, status=9/KILL रिपोर्ट करते, तर journalctl -k मध्ये out of memory मुळे झालेला kill दिसून येतो. memory_limit आणि threads स्पष्टपणे सेट करा, ज्यामुळे क्वेरी बंद होण्याऐवजी संथ होईल.

टोकन्सचा अंदाज घेण्याऐवजी ते मोजले पाहिजेत. प्रत्येक Messages API रिस्पॉन्समध्ये usage ऑब्जेक्ट असतो, ज्यामध्ये input_tokens आणि output_tokens असतात. प्रत्येक कॉलवर हे दोन्ही एका टेबलमध्ये लिहा. एका आठवड्यानंतर तुम्हाला तुमच्या प्रत्यक्ष वापराचे प्रमाण समजेल, ज्याला तुम्ही तपासणीच्या दिवशी तुमच्या मॉडेलच्या किमतीने गुणू शकता. नियोजनासाठी दोन गोष्टी स्थिर आहेत. प्रत्येक Claude मॉडेलवर आउटपुट टोकन्सची किंमत इनपुट टोकन्सपेक्षा जास्त असते, त्यामुळे नोट 200 शब्दांवर मर्यादित ठेवल्यास डेटा कमी करण्यापेक्षा बिलावर जास्त परिणाम होतो. तसेच, दिवसातून एकदा चालणाऱ्या कामासाठी prompt 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) वरील प्रायमरी की मुळे रिफ्रेश प्रक्रिया पुन्हा पुन्हा करणे सुरक्षित ठरते. INSERT OR REPLACE मुळे त्या टिकर आणि त्या दिवसासाठी आधीच अस्तित्वात असलेली ओळ (row) ओव्हरराईट होते, त्यामुळे बॅकफिल दोनदा चालवल्यास टेबलमध्ये डेटा दुप्पट होत नाही. जर की नसेल, तर क्रॅश झाल्यानंतर पुन्हा रन केल्यास प्रत्येक बार नकळतपणे दुप्पट होतो आणि त्यानंतर तुम्ही काढलेली प्रत्येक सरासरी चुकीची ठरते, ज्याबद्दल कोणतीही त्रुटी (error message) मिळत नाही.

DuckDB मध्ये एका वेळी फक्त एकच प्रोसेस फाईल रायटिंगसाठी उघडी ठेवू शकते. दुसरा रायटर लगेच Could not set lock on file मुळे अपयशी ठरतो, त्यानंतर तो PID दिसतो ज्याने फाईल उघडी ठेवली आहे; प्रॅक्टिसमध्ये ही सहसा दुसरी टर्मिनल विंडो असते जिथे तुम्ही duckdb शेल उघडे ठेवलेले असते. रीडर्स read_only=True पास करतात, म्हणूनच connect ला हा फ्लॅग लागतो. जर अनेक प्रोसेसना एकाच वेळी लिहिण्याची गरज असेल, तर ते काम दुसऱ्या इंजिनचे आहे: SQLite चे WAL मोड रीडर्सना काम करू देते तर एक रायटर कमिट करतो, आणि 'busy timeout' मुळे इतर रायटर्स अपयशी होण्याऐवजी प्रतीक्षा करतात. सर्व्हर वर्कलोडसाठी DuckDB विरुद्ध SQLite या दोघांची तुलना करते, आणि VPS वर SQLite प्रोडक्शनमध्ये चालवणे या विषयावर 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()

तो एकदा हाताने (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) एक्सचेंजची स्वतःची सत्र तारीख कायम ठेवते, ज्याचा अर्थ डेली बार असा होतो.

बाजार बंद होण्याच्या वेळी कार्यान्वित होणारा टायमर

अमेरिकन बाजार न्यूयॉर्क वेळेनुसार 16:00 वाजता बंद होतो आणि शेवटचे व्यवहार पूर्ण होण्यासाठी काही मिनिटे लागतात, म्हणून हे काम 16:20 वाजता चालवले जाते. हे न्यूयॉर्क वेळेनुसार लिहा, UTC नुसार नको. हिवाळ्यात न्यूयॉर्क UTC उणे 5 आणि उन्हाळ्यात UTC उणे 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 स्वीकारतो आणि तो त्या क्रमाने चालवतो; जर एखादी प्रक्रिया शून्य नसलेला (non-zero) एक्झिट कोड देऊन थांबली, तर पुढील प्रक्रिया थांबवल्या जातात. तुम्हाला नेमके हेच अपेक्षित आहे: जर रिफ्रेश अयशस्वी झाला, तर जुन्या डेटावर आधारित स्क्रीन अपडेट होऊ नये. Persistent=true हे अशा VPS वर महत्त्वाचे ठरते जे कर्नल अपडेट्ससाठी रीबूट होतात. याशिवाय 16:15 वाजता रीबूट झाल्यास ती वेळ निघून जाते; मात्र हे वापरल्यास, मशीन सुरू होताच काम लगेच पूर्ण केले जाते.

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

list-timers मध्ये NEXT कॉलम दिसला पाहिजे, ज्यामध्ये पुढील कामकाजाच्या दिवशी चालणारी वेळ सर्व्हरच्या स्थानिक वेळेत रूपांतरित केलेली असते. जर यादी रिकामी असेल, तर याचा अर्थ टायमर सुरू (enabled) नाही किंवा युनिटमध्ये [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 अस्तित्वात असलेल्या सर्व ओळींची सरासरी काढते, त्यामुळे टिकरच्या तिसऱ्या ओळीवर तीन दिवसांची सरासरी मिळते आणि तरीही त्याला 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,
    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 key पाहत नाही, database path पाहत नाही आणि कोणतीही query रन करत नाही. ते फक्त ओळी (rows) स्वीकारते आणि मजकूर (prose) परत करते. ही सीमा आउटपुटची पडताळणी करणे शक्य करते, कारण नोटमधील प्रत्येक आकडा तुम्ही पाठवलेल्या ब्लॉक मध्येही दिसला पाहिजे आणि तुम्ही त्यांची ओळीनुसार तुलना करू शकता. यातील प्रॉम्प्टिंगच्या बाजूबद्दल अधिक माहितीसाठी, using Claude for finance analysis मध्ये मॉडेल कशा प्रकारे वाचन करण्यात सक्षम आहे, यावर अधिक सविस्तर चर्चा केली आहे.

एक रन, एंड-टू-एंड

न्यूयॉर्क वेळेनुसार 16:20 वाजता टायमर सेवा सुरू करतो. refresh.py शेवटच्या साठवलेल्या दिवसापासून प्रत्येक टिकरसाठी फीड विचारते, प्रत्येकासाठी एक किंवा दोन नवीन बार लिहिते आणि प्रत्येक टिकरसाठी एक ओळ प्रिंट करते. screen.py तीच फाईल उघडते, मूव्हिंग ॲव्हरेज क्वेरी चालवते आणि काही ओळी परत मिळवते. त्या ओळी काहीशे टोकन्सचा एक मजकूर ब्लॉक बनतात. एक API कॉल त्यांचे रूपांतर एका लहान नोटमध्ये करतो, ती नोट जर्नलमध्ये जाते आणि टोकन काउंट व हिट्सच्या संख्येसह एक ओळ runs मध्ये जमा होते.

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

एका निरोगी लॉगमध्ये प्रत्येक टिकरसाठी एक ओळ, त्यानंतर नोट आणि मग research-refresh.service: Deactivated successfully असते. एका आठवड्यानंतर, डेटाबेसमधून तुमचा स्वतःचा खर्च वाचून काढा:

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

त्या एकूण बेरजेला तुम्ही ज्या दिवशी वाचता त्या दिवशी तुमच्या मॉडेलने दिलेल्या प्रति दशलक्ष टोकन किमतीने गुणा. यामुळे तुम्हाला कोणाच्या तरी अंदाजाऐवजी प्रत्यक्ष आकडा मिळेल. प्रकाशित किमती बदलतात. गणित बदलत नाही.

बॅकटस्ट ओव्हरफिट का होतात आणि ते कसे ओळखावे

स्क्रीनला दोन विंडो लांबीचे फंक्शन म्हणून लिहा, जोड्यांच्या ग्रिडवर स्वीप करा आणि त्यांना परताव्याच्या (return) आधारावर रँक करा. सर्वोत्तम जोडी दिसायला उत्कृष्ट असेल. हीच समस्या आहे, निकाल नाही. 200 जोड्यांचा ग्रिड म्हणजे 200 प्रयोग आहेत आणि तुम्ही त्यातील सर्वात नशीबवान जोडी निवडली आहे.

तुम्ही हे दहा मिनिटांत घडताना पाहू शकता. डेटाच्या आधारे स्टोअरचे दोन भाग करा. फक्त पहिल्या भागावर ग्रिड स्वीप करा आणि विजेत्याची नोंद करा. दुसऱ्या भागावर तोच ग्रिड स्वीप करा. जर दोन्ही विजेते एकमेकांपासून खूप वेगळे असतील, तर पॅरामीटर्स केवळ नॉइज (noise) फिट करत आहेत. ज्या भागावर तुम्ही ट्यूनिंग केले आहे, तिथेच जिंकणारी जोडी उद्याच्या दिवसासाठी काहीही भाकीत करत नाही.

Survivorship ही समस्या ओव्हरफिटिंगपेक्षा वाईट आहे कारण ट्यूनिंगने ती दुरुस्त करता येत नाही. तुमची टिकर लिस्ट ही आजच्या इंडेक्समधील सदस्यांची आहे, त्यामुळे त्यात फक्त टिकून राहिलेल्या कंपन्यांचा समावेश आहे. 2019 मध्ये डीलिस्ट झालेल्या टिकरबद्दल फीडला विचारले, तर ते रिकामे फ्रेम देते. याचा अर्थ ती कंपनी तुमच्या स्टोअरमध्ये कधीच येत नाही आणि तुमच्या टेस्टमध्येही कधीच येत नाही. तुम्ही चालवलेल्या प्रत्येक बॅकटस्टने आधीच अपयशी कंपन्यांना वगळलेले असते.

Restated fundamentals टाइमलाइन खंडित करतात. आज API द्वारे मिळणारा 2019 च्या तिमाहीचा महसूल आकडा हा नेहमी तोच नसतो जो 2019 मध्ये प्रकाशित झाला होता. आजचे फंडामेंटल्स आणि 2019 च्या किमती एकत्र करणारा स्क्रीन अशी माहिती वापरत आहे जी तेव्हा अस्तित्वातच नव्हती. किमतींच्या बाबतीत हे सहसा सुरक्षित असते, पण फंडामेंटल्सच्या बाबतीत नाही.

Adjusted prices तुमच्या नकळत बदलतात. auto_adjust=True सह, क्लोजिंग किमती डिव्हिडंड आणि स्प्लिट्ससाठी मागे ॲडजस्ट केल्या जातात. त्यामुळे पुढच्या महिन्यात केलेली तीच क्वेरी थोडा वेगळा इतिहास देते. तुम्ही प्रत्यक्षात वापरलेल्या ओळी (rows) स्टोअर करून ठेवल्यानेच निकाल पुन्हा मिळवता (reproducible) येतात आणि स्थानिक स्टोअर असण्याचे हे एक मुख्य कारण आहे.

बॅकटस्ट कमिशन आणि स्लिपेजकडेही दुर्लक्ष करतो आणि असे गृहीत धरतो की तुमच्या ऑर्डरमुळे किमतीवर कोणताही परिणाम होत नाही. हे घटक एक्झिक्युशनशी संबंधित आहेत, जे या विषयाच्या कक्षेबाहेर आहेत आणि running trading bots on a VPS मध्ये कव्हर केले आहेत.

अपयशाचे प्रकार आणि दिसणारे संदेश

error: externally-managed-environment जेव्हा pip चालते. तुम्ही virtual environment च्या बाहेर आहात. /opt/research/venv/bin/pip ला पूर्ण path वापरून कॉल करा.

Could not set lock on file, त्यानंतर PID येतो. दुसरी प्रक्रिया DuckDB फाईल रायटिंगसाठी उघडी ठेवत आहे, सहसा हे एखादे interactive shell असते जे तुम्ही बंद करायला विसरला असाल. ते बंद करा, किंवा read_only=True वापरून दुसरे कनेक्शन उघडा.

Main process exited, code=killed, status=9/KILL हे systemctl status मध्ये दिसते. मेमरी संपल्यामुळे kernel ने ही job बंद केली आहे. journalctl -k | grep -i oom ने याची खात्री करा, आणि त्यानंतर store.py मधील memory_limit कमी करा.

यशस्वी रन पण काहीही लिहिले जात नाही. systemctl status हे active (exited) वाचते आणि टेबलमध्ये कोणतीही वाढ झालेली नाही. फीडने रिकामे frames परत केले आहेत. हे अपयश शोधणे सर्वात कठीण असते, म्हणून जेव्हा प्रत्येक ticker रिकामे येते तेव्हा job ने non-zero exit code द्यावा अशी रचना करा.

बाजाराच्या सुट्टीच्या दिवशी टायमर सुरू होणे. systemd ला शेअर बाजाराचे कॅलेंडर माहित नसते, त्यामुळे Mon-Fri मध्ये सुट्ट्यांचाही समावेश होतो. रन होते, फीडमध्ये नवीन काहीही नसते, आणि job ने याला त्रुटी न मानता सामान्य स्थिती मानले पाहिजे.

API कडून 429. तुम्ही rate limit ओलांडली आहे. Anthropic SDK स्वतःहून backoff सह पुन्हा प्रयत्न करते, आणि Anthropic(max_retries=5) प्रयत्नांची संख्या वाढवते. जर हे दररोज अपयशी ठरत असेल, तर तुमची job एकाच वेळी खूप जास्त विनंत्या करत आहे.

हे काय नाही

हे एक संशोधन सहाय्यक (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 ची तुलना शेवटच्या प्रत्यक्ष ट्रेडिंग दिवसाशी करून यातील फरक ओळखा आणि जेव्हा सर्व टिकर्स रिकामे येतात तेव्हा जॉबला नॉन-झिरो एक्झिट कोड द्यायला सांगा.

एजंट काय खरेदी करायचे याचा निर्णय घेऊ शकतो का?

नाही, आणि तसा प्रयत्न करणे हीच या प्रकल्पांमधील मोठी चूक ठरते. मॉडेलला मार्केटचा ॲक्सेस नसतो, तुमच्या पोझिशनची किंवा टॅक्स परिस्थितीची माहिती नसते आणि स्वतःच्या आकड्यांची पडताळणी करण्याची सोय नसते. मॉडेल मोठ्या प्रमाणात मजकूर वाचून आज कोणत्या गोष्टींकडे लक्ष देणे गरजेचे आहे, हे सांगण्यात उत्तम आहे. ते जे काही लिहिते तो आर्थिक सल्ला नसून, निर्णय आणि त्याची जबाबदारी पूर्णपणे तुमचीच असते.