SSD Nodes Learn Hosting plans →
गाइड Matt Connorलेखक: Matt Connor · अपडेट किया गया: 2026-08-27

VPS पर अपना Stock Research Agent कैसे बनाएं

Python, DuckDB और Claude API का उपयोग करके अपना खुद का स्टॉक रिसर्च एजेंट बनाएं। इस गाइड में systemd timer और मार्केट डेटा फीड सेट करने की पूरी प्रक्रिया विस्तार से समझाई गई है।

Self-hosted stock research agent क्या है

Self-hosted stock research agent आपके अपने सर्वर पर चलने वाला एक छोटा प्रोग्राम है। यह एक निर्धारित समय पर मार्केट डेटा प्राप्त करता है, उसे लोकल डेटाबेस में सुरक्षित रखता है, उस पर एक फिल्टर चलाता है, और फिर एक Large Language Model (LLM) से यह पूछता है कि क्या बदलाव हुए हैं। यह डेटा को पढ़ता है और उसे फिल्टर करता है। यह ट्रेडिंग नहीं करता है, और इस गाइड में दी गई कोई भी जानकारी वित्तीय सलाह (financial advice) नहीं है।

इसे शुरू से बनाने वाले दो लोग अलग-अलग libraries चुन सकते हैं, फिर भी अंततः वही चार हिस्से बनाएँगे: prices और fundamentals देने वाला feed, आपके द्वारा कभी fetch की गई हर row को सुरक्षित रखने वाला local store, timer के आधार पर store को refresh करने वाला job, और surviving rows को वाक्यों में बदलने वाली LLM layer। यह guide Python, DuckDB, systemd timer और Claude API (application programming interface) से यह संरचना बनाती है। यह fixed loop वाला narrow agent है। इसलिए यदि आप पहले general version समझना चाहते हैं, तो AI agents सीखने का staged path किसी domain problem में बदलने से पहले loop, tools और memory को क्रम से समझाता है। Execution एक अलग job है और उसके failure modes भी अलग हैं। इसलिए इसे trading bots के लिए तैयार VPS पर चलाएँ।

चार भाग, और प्रत्येक का कार्य

The feed एकमात्र ऐसा भाग है जो बाहरी दुनिया से संपर्क करता है। इसे यह पता होता है कि ticker और date range के लिए अनुरोध कैसे करना है, और डेटा की पंक्तियों (rows) को वापस कैसे सौंपना है। downstream में मौजूद हर चीज़ feed के बजाय आपके database को पढ़ती है, इसलिए feed के बंद होने पर आपको केवल एक दिन के नए डेटा का नुकसान होता है, न कि पूरी स्क्रीन के खराब होने का।

The store इस पूरी प्रक्रिया का मुख्य उद्देश्य है। यदि आपने किसी दिन के close को रिकॉर्ड नहीं किया है, तो उसे आमतौर पर बाद में फिर से प्राप्त किया जा सकता है। लेकिन intraday quote, संशोधित होने से पहले का estimate, या restate होने से पहले का fundamentals figure दोबारा प्राप्त नहीं किया जा सकता। The store के माध्यम से आप उस डेटा का रिकॉर्ड बनाते हैं जो वास्तव में उस दिन उपलब्ध था।

The scheduler यह तय करता है कि refresh कब होगा। सर्वर पर यह एक systemd timer होता है, और यही कारण है कि यहाँ कोड की तुलना में VPS अधिक महत्वपूर्ण है।

The LLM layer आपके SQL द्वारा तैयार किए गए एक छोटे टेक्स्ट ब्लॉक को पढ़ता है और उसका सारांश लिखता है। यह कभी भी database से कनेक्ट नहीं होता और न ही कभी query बनाता है। यदि model खुद SQL लिखता है, तो एक गलत token एक धाराप्रवाह वाक्य के भीतर गलत संख्या बन सकता है, जिसकी तुलना करने के लिए कुछ भी नहीं होता। यदि SQL संख्याएँ उत्पन्न करता है, तो model केवल भाषा (prose) में गलती कर सकता है, और आप उस भाषा की तुलना उन पंक्तियों (rows) से कर सकते हैं जिन्हें आपने भेजा था।

लैपटॉप के बजाय VPS पर इसे क्यों चलाएं

इसका कारण शेड्यूलर है। न्यूयॉर्क समय के अनुसार 16:00 बजे अमेरिकी बाजार बंद होने का समय बर्लिन में 22:00 बजे और जकार्ता में अगली सुबह 04:00 बजे होता है। लैपटॉप इन दोनों समय पर स्लीप मोड में होता है। एक छूटा हुआ रन देर से मिले नोट से अधिक महंगा पड़ता है: दैनिक बार (daily bars) को आमतौर पर बाद में फिर से प्राप्त किया जा सकता है, लेकिन जो कुछ भी संशोधित (revised) हो जाता है उसे दोबारा प्राप्त नहीं किया जा सकता, इसलिए आपके रिकॉर्ड में आया अंतराल स्थायी होता है। यही तर्क किसी भी ऐसे एजेंट पर लागू होता है जिसका शेड्यूल और संग्रहीत स्थिति (stored state) को रीबूट के बाद भी जीवित रहना चाहिए, और यही वह आधार है जिसके इर्द-गिर्द KiroCrew को अपने VPS पर हमेशा चालू रहने वाले एजेंट के रूप में चलाना बनाया गया है।

दूसरा कारण छोटा है लेकिन फिर भी वास्तविक है। सर्वर में एक API key होती है, जो एक फाइल में होती है, जिसका स्वामित्व एक ऐसे सिस्टम यूजर के पास होता है जिसका कोई लॉगिन शेल नहीं है, और जिसका उपयोग केवल एक जॉब द्वारा किया जाता है। उस लैपटॉप पर इसे व्यवस्थित करना बहुत कठिन है जिसका उपयोग आप वेब ब्राउज़ करने के लिए भी करते हैं। key को कोड से और मॉडल के इनपुट से बाहर रखें, जो AI एजेंट से API keys को दूर रखने का विषय है।

आकार निर्धारित करना: disk, RAM और tokens

Disk का हिस्सा आसान है। एक दैनिक बार (daily bar) प्रति ticker प्रति trading day एक row है, और एक US trading year लगभग 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"
  }
]

बीस tickers के लिए एक वर्ष के बाद 5,040 rows होती हैं। 500 tickers लाइन दस वर्षों के बाद 1,260,000 rows तक पहुँच जाती है। प्रत्येक row में एक date और कुछ doubles होते हैं, और DuckDB columns को compress करके store करता है, इसलिए यह gigabytes के बजाय megabytes में होता है। इस अनुमान पर भरोसा न करें, मेरे अनुमान पर भी नहीं। अपने पहले backfill के बाद du -h /opt/research/data/market.duckdb चलाएँ और अपनी खुद की संख्या का उपयोग करें।

RAM वह जगह है जहाँ एक छोटा VPS समस्या पैदा करता है। DuckDB डिफ़ॉल्ट रूप से एक single query के लिए machine की 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 मरने के बजाय धीमी हो जाएगी।

Tokens को मापा जाना चाहिए, अनुमान नहीं लगाया जाना चाहिए। हर Messages API response में input_tokens और output_tokens के साथ एक usage object होता है। हर call पर दोनों को एक table में लिखें, और एक सप्ताह के बाद आपको अपनी वास्तविक volume पता चल जाएगी, जिसे आप उस दिन के model के मूल्य से गुणा करें जिस दिन आप जाँच करते हैं। दो चीजें योजना बनाने के लिए पर्याप्त स्थिर हैं। हर Claude model पर output tokens की कीमत input tokens से अधिक होती है, इसलिए note को 200 शब्दों तक सीमित करना आपके द्वारा भेजे जाने वाले डेटा को कम करने की तुलना में बिल पर अधिक प्रभाव डालता है। और prompt caching एक दिन में एक बार चलने वाले job के लिए मदद नहीं करती है, क्योंकि cache lifetime मिनटों में मापी जाती है: अगली बार चलने तक cached block expire हो चुका होता है और आप फिर से पूरी input price चुकाते हैं। 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

कोई भी कोड लिखने से पहले इंस्टॉलेशन की जाँच करें:

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

यह ok प्रिंट करता है। यदि आपने virtual environment को छोड़ दिया और सिस्टम Python पर pip install चलाया, तो Ubuntu 24.04 आपको error: externally-managed-environment के साथ रोक देगा, क्योंकि डिस्ट्रीब्यूशन का स्वामित्व /usr/lib/python3 पर है और वह pip को वहाँ लिखने की अनुमति नहीं देता है। venv केवल शिष्टाचार नहीं है। यह एकमात्र निर्देशिका है जिसे pip छू सकता है।

API key को ऐसी फ़ाइल में रखें जिसे केवल 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

इसमें एक लाइन लिखें, बिना quotes और बिना 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) पर मौजूद primary key ही refresh को बार-बार सुरक्षित रूप से चलाने योग्य बनाती है। INSERT OR REPLACE उस ticker और उस दिन के लिए पहले से मौजूद row को overwrite कर देता है, इसलिए backfill को दो बार चलाने से टेबल में डेटा दोगुना नहीं होता। यदि key न हो, तो क्रैश के बाद दोबारा चलाने पर हर bar चुपचाप duplicate हो जाएगी, और उसके बाद आप जो भी average compute करेंगे वह गलत होगा, जबकि कहीं भी कोई error message नहीं मिलेगा।

DuckDB किसी फाइल को लिखने के लिए एक समय में केवल एक ही process को अनुमति देता है। दूसरा writer तुरंत Could not set lock on file के साथ fail हो जाता है, जिसके साथ उस process का PID भी दिखता है जिसने फाइल को होल्ड कर रखा है; व्यवहार में यह अक्सर वह interactive duckdb shell होता है जिसे आपने किसी दूसरे terminal में खुला छोड़ दिया है। Readers read_only=True पास करते हैं, इसीलिए connect इस flag का उपयोग करता है। यदि कई processes को वास्तव में एक ही समय पर लिखने की आवश्यकता हो, तो यह किसी अन्य engine का काम है: SQLite का WAL mode तब भी readers को काम करने देता है जब एक writer commit कर रहा हो, और एक busy timeout अन्य writers को fail होने के बजाय प्रतीक्षा करने के लिए मजबूर करता है। सर्वर वर्कलोड के लिए 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()

इसे एक बार मैन्युअल रूप से चलाएँ:

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

पहली बार चलाने पर यह वर्षों का डेटा बैकफिल करता है और प्रति टिकर हजारों की संख्या में एक लाइन प्रिंट करता है। एक मिनट बाद इसे फिर से चलाएँ, अब प्रत्येक लाइन 1 या 2 पंक्तियाँ दिखाएगी, क्योंकि जॉब उस अंतिम दिन से शुरू होती है जो पहले से स्टोर है। वह दूसरा रन ही असली टेस्ट है: यदि संख्या अभी भी हजारों में है, तो max(day) कुछ भी रिटर्न नहीं कर रहा है और इंसर्ट हर रात आपके पूरे इतिहास को फिर से लिख रहा है।

df.empty चेक फाइल की सबसे महत्वपूर्ण लाइन है। Ticker.history() गलत या डीलिस्ट किए गए सिंबल के लिए एरर नहीं देता है। यह इस बारे में एक चेतावनी प्रिंट करता है कि सिंबल संभवतः डीलिस्ट हो गया है और कोई प्राइस डेटा नहीं मिला (सटीक शब्दावली लाइब्रेरी वर्जन के अनुसार बदलती रहती है) और एक खाली DataFrame रिटर्न करता है। बिना उस चेक वाली जॉब कुछ भी नहीं लिखती, 0 के साथ एग्जिट हो जाती है, और systemd एक हेल्दी ग्रीन रन दिखाता है जबकि टेबल चुपचाप बढ़ना बंद कर देती है। आपको हफ्तों बाद पता चलता है, एक ऐसी स्क्रीन से जो हर दिन वही पंक्तियाँ दिखाती है।

टाइमस्टैम्प हैंडलिंग का भी एक कारण है। फीड जो इंडेक्स रिटर्न करती है उसमें एक्सचेंज का टाइमज़ोन हो सकता है, और ऑफसेट को हटाना 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 स्वीकार करता है, और यह उन्हें क्रम में चलाता है, यदि कोई नॉन-जीरो एग्जिट कोड देता है तो यह रुक जाता है। आपको बिल्कुल यही व्यवहार चाहिए: एक विफल रिफ्रेश के बाद पुराने डेटा पर स्क्रीन अपडेट नहीं होनी चाहिए। Persistent=true उन VPS पर महत्वपूर्ण है जो कर्नल अपडेट के लिए रीबूट होते हैं। इसके बिना 16:15 पर रीबूट होने पर रन छूट जाता है; इसके साथ, मशीन वापस आते ही जॉब चल जाती है। एक एजेंट जिसे फायर होकर बंद होने के बजाय चालू रहना है, उसे विपरीत यूनिट शेप की आवश्यकता होती है, जो Restart= नियमों के साथ एक लॉन्ग-लीव्ड प्रोसेस हो और जिसमें कोई टाइमर न हो, जैसा कि running dsh headless on a VPS with 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 मौजूद सभी पंक्तियों का औसत निकालता है, इसलिए किसी टिकर की तीसरी पंक्ति तीन दिनों का औसत लौटाती है और फिर भी उसे ma50 कहती है। यदि आप इसकी तुलना ma20 से करते हैं, तो आप प्रत्येक टिकर के इतिहास की शुरुआत में एक ऐसा क्रॉसओवर बना बैठेंगे जो कभी हुआ ही नहीं था। पंक्ति संख्या (row number) पर फ़िल्टर लगाने से वे पंक्तियाँ हट जाती हैं जहाँ विंडो पूरी नहीं थी।

मॉडल क्या देखता है और क्या कभी नहीं देखता

# /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()

सिस्टम प्रॉम्प्ट में शब्द सीमा बिल के महंगे हिस्से को सीमित करती है। केवल दी गई पंक्तियों (rows) का उपयोग करने के निर्देश को आपको विश्वास करने के बजाय सत्यापित करना होगा: ब्लॉक से एक कॉलम हटा दें, इसे फिर से चलाएं और आउटपुट पढ़ें। यदि उस कॉलम का कोई आंकड़ा अभी भी दिखाई देता है, तो मॉडल ने रिक्त स्थान को भर दिया है, और आपका प्रॉम्प्ट पर्याप्त रूप से सटीक नहीं है। वह परीक्षण दो मिनट लेता है और यह पता लगाने का एकमात्र ईमानदार तरीका है।

मॉडल कभी भी API key नहीं देखता, कभी भी database path नहीं देखता, और कभी भी कोई query नहीं चलाता। यह पंक्तियाँ प्राप्त करता है और गद्य (prose) लौटाता है। वह सीमा ही आउटपुट को जांचने योग्य बनाती है, क्योंकि नोट में प्रत्येक संख्या उस ब्लॉक में भी दिखाई देनी चाहिए जिसे आपने भेजा था, और आप उनकी पंक्ति-दर-पंक्ति तुलना कर सकते हैं। जिस क्षण आप उस इनपुट को वेब से ली गई पेजों को शामिल करने के लिए विस्तृत करते हैं, सीमा का आकार बदल जाता है, क्योंकि एक हेडलाइन या फ़ोरम पोस्ट आपके बजाय मॉडल के उद्देश्य से निर्देश ले जा सकती है, जो वह सतह है जिसे एजेंट को SearXNG सर्च बैकएंड देना खोलता है। प्रॉम्प्टिंग के इस पहलू पर अधिक जानकारी के लिए, वित्तीय विश्लेषण के लिए Claude का उपयोग करना इस बात पर गहराई से चर्चा करता है कि मॉडल क्या पढ़ने में अच्छा है।

एक बार का एंड-टू-एंड रन

न्यूयॉर्क समय के अनुसार 16:20 बजे टाइमर सर्विस को शुरू करता है। refresh.py अंतिम संग्रहीत दिन से शुरू होने वाले प्रत्येक टिकर के लिए फीड से डेटा मांगता है, प्रत्येक के लिए एक या दो नई बार (bars) लिखता है, और प्रति टिकर एक लाइन प्रिंट करता है। screen.py उसी फाइल को खोलता है, मूविंग एवरेज क्वेरी चलाता है, और कुछ पंक्तियाँ (rows) प्राप्त करता है। ये पंक्तियाँ कुछ सौ टोकन का एक टेक्स्ट ब्लॉक बन जाती हैं। एक API कॉल उन्हें एक संक्षिप्त नोट में बदल देती है, नोट जर्नल में चला जाता है, और runs में टोकन काउंट और हिट्स की संख्या के साथ एक पंक्ति दर्ज हो जाती है।

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

एक स्वस्थ लॉग में प्रति टिकर एक लाइन होती है, उसके बाद नोट, और फिर 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;

उन कुल योगों को उस प्रति मिलियन टोकन मूल्य से गुणा करें जो आपका मॉडल उस दिन सूचीबद्ध करता है जिस दिन आप उन्हें पढ़ते हैं। यह आपको किसी और के अनुमान के बजाय वास्तविक आंकड़ा देता है। प्रकाशित कीमतें बदलती रहती हैं। गणित नहीं बदलता।

बैकटेस्ट ओवरफिट क्यों होते हैं और इसे कैसे पहचानें

स्क्रीन को दो विंडो लेंथ के फंक्शन के रूप में लिखें, पेयर्स के ग्रिड को स्वीप करें और उन्हें रिटर्न के आधार पर रैंक करें। सबसे अच्छा पेयर बेहतरीन दिखेगा। यही समस्या है, परिणाम नहीं। 200 पेयर्स का ग्रिड 200 प्रयोगों के बराबर है, और आपने सबसे भाग्यशाली को चुन लिया है।

आप इसे दस मिनट में होते हुए देख सकते हैं। स्टोर को तारीख के आधार पर दो हिस्सों में बांटें। केवल पहले हिस्से पर ग्रिड स्वीप करें और विजेता को नोट करें। दूसरे हिस्से पर उसी ग्रिड को स्वीप करें। यदि दोनों विजेता एक-दूसरे से बहुत अलग हैं, तो पैरामीटर्स केवल नॉइज़ (noise) को फिट कर रहे हैं। जिस पेयर ने केवल उसी हिस्से पर जीत हासिल की जिस पर आपने उसे ट्यून किया था, वह कल के बारे में कुछ नहीं बताता।

सर्वाइवरशिप (Survivorship) ओवरफिटिंग से भी बदतर है क्योंकि ट्यूनिंग इसे ठीक नहीं कर सकती। आपकी टिकर लिस्ट आज के इंडेक्स मेंबर्स की है, इसलिए इसमें केवल वही कंपनियां शामिल हैं जो बची हुई हैं। फीड से किसी ऐसे टिकर के बारे में पूछें जिसे 2019 में डीलिस्ट कर दिया गया था, तो यह एक खाली फ्रेम लौटाएगा। इसका मतलब है कि वह कंपनी कभी आपके स्टोर में नहीं आई और कभी आपके टेस्ट में नहीं आई। आपके द्वारा चलाए गए हर बैकटेस्ट ने पहले ही विफलताओं को बाहर कर दिया है।

रीस्टेटेड फंडामेंटल्स (Restated fundamentals) टाइमलाइन को तोड़ देते हैं। आज API जो रेवेन्यू फिगर 2019 की तिमाही के लिए लौटाता है, वह हमेशा वही आंकड़ा नहीं होता जो 2019 में प्रकाशित हुआ था। आज के फंडामेंटल्स को 2019 की कीमतों के साथ मिलाने वाली स्क्रीन ऐसी जानकारी का उपयोग कर रही है जो उस समय मौजूद ही नहीं थी। कीमतें आमतौर पर यहाँ सुरक्षित होती हैं, लेकिन फंडामेंटल्स अक्सर नहीं।

एडजस्टेड कीमतें आपके नीचे बदलती रहती हैं। auto_adjust=True के साथ, क्लोजिंग प्राइसेज को डिविडेंड और स्प्लिट्स के लिए पीछे की ओर एडजस्ट किया जाता है, इसलिए अगले महीने चलाई गई वही क्वेरी थोड़ा अलग इतिहास लौटाती है। जिन पंक्तियों (rows) का आपने वास्तव में उपयोग किया है, उन्हें स्टोर करना ही परिणाम को पुनरुत्पादनीय (reproducible) बनाता है, और यही एक कारण है कि लोकल स्टोर का अस्तित्व है।

बैकटेस्ट कमीशन और स्लिपेज (slippage) को भी नजरअंदाज करता है, और यह मान लेता है कि आपका ऑर्डर कीमत को प्रभावित नहीं करता है। ये निष्पादन (execution) से संबंधित हैं, जो यहाँ दायरे से बाहर है और running trading bots on a VPS में कवर किया गया है।

विफलता के प्रकार और दिखाई देने वाले संदेश

error: externally-managed-environment जब pip चलता है। आप virtual environment के बाहर हैं। /opt/research/venv/bin/pip को full path के साथ कॉल करें।

Could not set lock on file, जिसके बाद PID आता है। कोई अन्य process DuckDB फ़ाइल को लिखने के लिए open रखे हुए है, आमतौर पर यह कोई interactive shell होता है जिसे आप बंद करना भूल गए हैं। इसे बंद करें, या read_only=True के साथ दूसरा connection खोलें।

Main process exited, code=killed, status=9/KILL जो systemctl status में दिखाई देता है। kernel ने memory की कमी के कारण job को kill कर दिया है। journalctl -k | grep -i oom के साथ इसकी पुष्टि करें, फिर store.py में memory_limit को कम करें।

एक सफल रन जो कुछ भी नहीं लिखता है। systemctl status, active (exited) को पढ़ता है और table का आकार नहीं बढ़ा है। feed ने empty frames लौटाए हैं। यह विफलता सबसे लंबे समय तक छिपी रहती है, इसलिए जब हर ticker खाली वापस आए तो job को non-zero exit code के साथ समाप्त करें।

timer का market holiday पर चलना। systemd को exchange calendar की जानकारी नहीं होती है, इसलिए Mon-Fri में छुट्टियां भी शामिल होती हैं। रन होता है, feed में कुछ भी नया नहीं होता है, और job को इसे त्रुटि के बजाय सामान्य स्थिति मानना चाहिए।

API से 429। आप rate limit से अधिक उपयोग कर रहे हैं। Anthropic SDK अपने आप backoff के साथ retry करता है, और Anthropic(max_retries=5) प्रयास की संख्या (attempt count) को बढ़ाता है। यदि यह हर दिन विफल रहता है, तो job एक ही बार में बहुत अधिक डेटा मांग रही है।

यह क्या नहीं है

यह एक रिसर्च असिस्टेंट है। किसी फाइलिंग का सारांश तैयार करने वाला मॉडल उस फाइलिंग का एक विश्लेषण प्रस्तुत करता है, और यह टेक्स्ट में छपे किसी नंबर के बारे में पूरी तरह गलत हो सकता है। यही कारण है कि नोट में मौजूद हर आंकड़े का मिलान आपके द्वारा भेजी गई पंक्ति से होना चाहिए। इस आउटपुट को उन चीजों की एक संक्षिप्त सूची मानें जिन्हें आपको स्वयं पढ़ना चाहिए। यहाँ दी गई कोई भी जानकारी वित्तीय सलाह नहीं है, और इनमें से कोई भी ट्रेडिंग सिग्नल नहीं है।

Backtests विचारों को खारिज करने के लिए उपयोगी हैं, लेकिन उन्हें सही साबित करने में कमजोर हैं। जो रणनीति आपके अपने डेटा पर विफल हो जाती है, वह वास्तव में मृत है। जो रणनीति सफल होती है, उसने केवल आपके डेटा का परीक्षण पास किया है, जो कि रात के 1 बजे महसूस होने वाली बात की तुलना में बहुत छोटा दावा है।

Execution को जानबूझकर दायरे से बाहर रखा गया है। Orders और broker credentials का जोखिम प्रोफाइल एक read-only रिसर्च बॉक्स से अलग होता है, और उन्हें मिलाने का मतलब है कि ट्रेडिंग keys उसी मशीन पर रखना जहाँ LLM prompt चल रहा है। यदि आप यह देखना चाहते हैं कि यह पैटर्न आपके अपने सर्वर पर चलने वाली अन्य उपयोगी चीजों के साथ कहाँ स्थित है, तो the self-hosted AI agents worth running एक विस्तृत जानकारी प्रदान करता है।

FAQ

क्या मुझे पेड मार्केट डेटा फीड की आवश्यकता है?

प्रोटोटाइप के लिए इसकी आवश्यकता नहीं है। सिस्टम की संरचना को समझने के लिए एक मुफ्त अनौपचारिक फीड पर्याप्त है, लेकिन यह कभी भी बंद हो सकता है, क्योंकि यह ऐसी वेबसाइट पर निर्भर है जिसकी आपके प्रति कोई जिम्मेदारी नहीं है। यह आमतौर पर एक्सेप्शन के बजाय खाली फ्रेम के रूप में विफल होता है, इसलिए आपके जॉब को रो (row) काउंट की जांच करनी चाहिए। जब डेटा निर्णय लेने की प्रक्रिया में उपयोग होने लगे, तब एक डॉक्यूमेंटेड API और सपोर्ट एड्रेस वाली पेड फीड पर स्विच करें। स्टोर के कारण यह बदलाव सस्ता हो जाता है: केवल फेच फंक्शन बदलता है, जबकि शेड्यूल, स्कीमा और स्क्रीन वैसे ही रहते हैं।

क्या मुझे कीमतें SQLite में रखनी चाहिए या DuckDB में?

DuckDB कॉलम-आधारित है और बड़ी संख्या में रो को स्कैन करके एग्रीगेट निकालने के लिए बनाया गया है, जो कि दस वर्षों के बार्स (bars) पर मूविंग एवरेज निकालने जैसा ही है। SQLite रो-ओरिएंटेड है और एक साथ कई प्रोसेस से छोटे रीड और राइट ऑपरेशंस के लिए बेहतर है। एक ऐसे शेड्यूल जॉब के लिए जो कुछ सौ रो जोड़ता है और फिर लाखों को स्कैन करता है, DuckDB बेहतर विकल्प है। यदि कई प्रोसेस को एक साथ राइट करना है, तो SQLite का WAL मोड रीडर्स को काम करने देता है जबकि एक राइटर कमिट करता है, और बिजी टाइमआउट अन्य राइटर्स को विफल होने के बजाय प्रतीक्षा करने के लिए मजबूर करता है।

LLM कॉल्स का मासिक खर्च कितना होता है?

प्रत्येक रिस्पॉन्स से usage.input_tokens और usage.output_tokens को एक टेबल में लॉग करें, फिर अपने साप्ताहिक योग को उस प्रति मिलियन टोकन कीमत से गुणा करें जो आपके मॉडल की वेबसाइट पर उस दिन दी गई है। केवल यही आंकड़ा अगले क्वार्टर के लिए सटीक रहता है। एक छोटी स्क्रीन पर दैनिक रन में बहुत कम कॉल्स होती हैं, और नोट की लंबाई वह हिस्सा है जिसे आप नियंत्रित कर सकते हैं: समरी को 200 शब्दों तक सीमित रखना कम रो भेजने से ज्यादा बचत करता है, क्योंकि हर Claude मॉडल पर आउटपुट टोकन की कीमत इनपुट टोकन से अधिक होती है।

मेरा जॉब सफल क्यों रहा लेकिन कोई नई रो क्यों नहीं लिखी गई?

इसके दो सामान्य कारण हैं। बाजार बंद था, क्योंकि systemd Mon-Fri शेड्यूल में एक्सचेंज की छुट्टियां शामिल होती हैं। या फिर फीड ने हर टिकर के लिए एक खाली फ्रेम लौटाया, जिसे क्लाइंट लाइब्रेरी अक्सर एक्सेप्शन के बजाय एक प्रिंटेड चेतावनी के रूप में रिपोर्ट करती हैं, इसलिए प्रोसेस 0 के साथ एग्जिट होती है और systemd इसे सफल दिखाता है। SELECT max(day) FROM prices की तुलना पिछले वास्तविक ट्रेडिंग दिन से करके इनके बीच अंतर पता करें, और जब हर टिकर खाली आए तो जॉब को नॉन-जीरो एग्जिट कोड के साथ समाप्त करें।

क्या एजेंट यह तय कर सकता है कि क्या खरीदना है?

नहीं, और इसे ऐसा करने के लिए बनाना ही वह बिंदु है जहाँ ये प्रोजेक्ट गलत दिशा में चले जाते हैं। मॉडल के पास मार्केट का एक्सेस नहीं है, आपकी पोजीशन या टैक्स स्थिति की जानकारी नहीं है, और अपने आंकड़ों को किसी भी चीज से क्रॉस-चेक करने का कोई तरीका नहीं है। यह बड़ी मात्रा में टेक्स्ट को पढ़ने और आपको यह बताने में सक्षम है कि आज किन कुछ चीजों पर आपको ध्यान देना चाहिए। इसके द्वारा लिखा गया कुछ भी वित्तीय सलाह नहीं है, और निर्णय लेने की जिम्मेदारी पूरी तरह से आपकी है।