SSD Nodes Learn 🎉 VPS $4.99/माह से
गाइड Matt Connorलेखक: Matt Connor

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

Python, DuckDB और systemd का उपयोग करके अपना स्टॉक रिसर्च एजेंट बनाएं। यह गाइड मार्केट डेटा फीड, ऑटोमेटेड डेटा स्टोर और LLM आधारित एनालिसिस सेटअप करने की पूरी प्रक्रिया समझाती है।

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

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

दो लोग यदि इसे शून्य से बनाना शुरू करें, तो वे अलग-अलग libraries चुन सकते हैं, लेकिन अंत में वे चार समान हिस्से ही बनाएंगे: एक feed जो कीमतें और fundamentals प्रदान करती है, एक local store जो आपके द्वारा प्राप्त की गई हर row को सुरक्षित रखता है, एक job जो timer पर store को refresh करती है, और एक LLM layer जो बची हुई rows को वाक्यों में बदलती है। यह गाइड Python, DuckDB, एक systemd timer और Claude API (application programming interface) का उपयोग करके इस ढांचे को तैयार करती है। Execution एक अलग job है जिसके failure modes अलग हैं, और इसे trading bots के लिए सेटअप किए गए VPS पर ही रखा जाना चाहिए।

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

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

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

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

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

लैपटॉप के बजाय VPS पर चलाने का कारण

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

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

Sizing it: disk, RAM and tokens

Disk का प्रबंधन आसान है। एक दैनिक बार का अर्थ है प्रति टिकर, प्रति ट्रेडिंग दिन एक row, और एक अमेरिकी ट्रेडिंग वर्ष में लगभग 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 rows है। 500 tickers लाइन दस वर्षों के बाद 1,260,000 rows तक पहुँच जाती है। प्रत्येक row में एक तारीख और कुछ doubles होते हैं, और DuckDB columns को compress करके स्टोर करता है, इसलिए यह gigabytes के बजाय tens of megabytes का डेटा होता है। इस अनुमान पर भरोसा न करें, मेरे अनुमान पर भी नहीं। अपने पहले backfill के बाद du -h /opt/research/data/market.duckdb चलाएँ और अपनी स्वयं की संख्या का उपयोग करें।

RAM वह जगह है जहाँ एक छोटा VPS समस्या पैदा करता है। DuckDB डिफ़ॉल्ट रूप से एक ही query के लिए मशीन की मेमोरी का एक बड़ा हिस्सा और उसके सभी cores का उपयोग करता है, जो एक analytics सर्वर के लिए तो सही है, लेकिन 2 GB वाले बॉक्स के लिए गलत है जिस पर अन्य चीजें भी चल रही हों। पूरे prices table पर एक aggregation चलाने से kernel द्वारा process को kill कर दिया जाता है, और systemd Main process exited, code=killed, status=9/KILL रिपोर्ट करता है जबकि journalctl -k आउट ऑफ मेमोरी (OOM) किल दिखाता है। memory_limit और threads को स्पष्ट रूप से सेट करें, इससे query मर जाने के बजाय धीमी हो जाएगी।

Tokens को मापा जाना चाहिए, अनुमान नहीं लगाया जाना चाहिए। प्रत्येक Messages API रिस्पॉन्स में usage ऑब्जेक्ट होता है जिसमें input_tokens और output_tokens शामिल होते हैं। हर कॉल पर दोनों को एक table में लिखें, और एक सप्ताह के बाद आपको अपना वास्तविक वॉल्यूम पता चल जाएगा, जिसे आप उस दिन के मॉडल के रेट से गुणा कर सकते हैं जिस दिन आप चेक करते हैं। दो चीजें योजना बनाने के लिए पर्याप्त स्थिर हैं। हर Claude मॉडल पर output tokens की कीमत input tokens से अधिक होती है, इसलिए नोट को 200 शब्दों तक सीमित करना, आपके द्वारा भेजे जाने वाले डेटा को कम करने की तुलना में बिल पर अधिक प्रभाव डालता है। और prompt caching एक बार प्रतिदिन चलने वाले जॉब में मदद नहीं करता है, क्योंकि cache की अवधि मिनटों में मापी जाती है: अगली बार चलने तक cached ब्लॉक समाप्त हो जाता है और आप फिर से पूरा input price चुकाते हैं। Caching तब फायदेमंद होती है जब एक ही रन में एक ही बड़े टेक्स्ट ब्लॉक पर कई कॉल की जाती हैं।

घटकों को इंस्टॉल करें

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

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

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

यह ok प्रिंट करता है। यदि आपने virtual environment को छोड़ दिया और सिस्टम 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) निकालेंगे वह गलत होगा, जबकि कहीं भी कोई error message नहीं मिलेगा जो आपको इसकी सूचना दे।

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

The 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 उस अंतिम दिन से शुरू होती है जो पहले से स्टोर है। यह दूसरा रन ही असली टेस्ट है: यदि संख्या अभी भी हजारों में है, तो max(day) कुछ भी वापस नहीं कर रहा है और insert हर रात आपके पूरे इतिहास को फिर से लिख रहा है।

df.empty चेक फाइल की सबसे महत्वपूर्ण लाइन है। Ticker.history() गलत या delist हो चुके symbol के लिए error नहीं देता है। यह इस बारे में एक warning प्रिंट करता है कि symbol शायद delist हो गया है और कोई price data नहीं मिला (सटीक शब्दों का चयन library versions के अनुसार बदलता रहता है) और यह एक खाली DataFrame लौटाता है। बिना उस चेक वाली job कुछ भी नहीं लिखती, 0 के साथ exit हो जाती है, और systemd एक healthy green run दिखाता है जबकि table चुपचाप बढ़ना बंद कर देती है। आपको हफ्तों बाद पता चलता है, जब स्क्रीन पर हर दिन वही पुरानी rows दिखाई देती हैं।

timestamp handling का भी एक कारण है। feed द्वारा लौटाया गया index exchange का timezone ले जा सकता है, और offset को हटाना UTC में बदलने के समान नहीं है। टोक्यो सत्र का midnight local time UTC में पिछले कैलेंडर दिन में बदल जाता है, इसलिए UTC conversion चुपचाप हर जापानी bar को एक दिन पीछे खिसका देता है और primary key को खराब कर देता है। tz_localize(None) exchange के अपने सत्र की तारीख को बनाए रखता है, जिसका अर्थ ही daily bar होता है।

बाज़ार बंद होने पर चलने वाला टाइमर

अमेरिकी बाज़ार न्यूयॉर्क समय के अनुसार 16:00 बजे बंद होता है, और अंतिम प्रिंट्स को सेटल होने में कुछ मिनट लगते हैं, इसलिए यह जॉब 16:20 पर चलती है। इसे UTC में नहीं, बल्कि न्यूयॉर्क समय में लिखें। न्यूयॉर्क में सर्दियों में समय UTC से 5 घंटे पीछे और गर्मियों में 4 घंटे पीछे होता है, इसलिए एक निश्चित UTC घंटे पर लिखा गया टाइमर साल में दो बार एक घंटे आगे-पीछे हो जाता है और बाज़ार बंद होने से पहले ही चलने लगता है। systemd 252 और उसके बाद के वर्ज़न सीधे OnCalendar में टाइमज़ोन स्वीकार करते हैं, और Ubuntu 24.04 में 255 वर्ज़न आता है। अपने वर्ज़न की पुष्टि systemctl --version से करें।

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

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

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

[Install]
WantedBy=timers.target

Type=oneshot एकमात्र सर्विस प्रकार है जो एक से अधिक ExecStart स्वीकार करता है, और यह उन्हें क्रम में चलाता है, यदि कोई नॉन-ज़ीरो (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 कॉलम दिखना चाहिए जिसमें अगले कार्यदिवस (weekday) का रन समय हो, जिसे सर्वर के स्थानीय समय में बदल दिया गया हो। यदि सूची खाली है, तो इसका मतलब है कि टाइमर इनेबल नहीं है, या यूनिट में [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 नहीं चलाता। यह पंक्तियाँ प्राप्त करता है और गद्य (prose) लौटाता है। वह सीमा ही आउटपुट को जाँचने योग्य बनाती है, क्योंकि नोट में प्रत्येक संख्या आपके द्वारा भेजे गए ब्लॉक में भी दिखाई देनी चाहिए, और आप उनकी पंक्ति-दर-पंक्ति तुलना कर सकते हैं। इस विषय पर प्रॉम्प्टिंग के बारे में अधिक जानकारी के लिए, वित्तीय विश्लेषण के लिए Claude का उपयोग इस बात पर विस्तार से चर्चा करता है कि मॉडल पढ़ने में किस हद तक सक्षम है।

एक रन, शुरू से अंत तक

न्यूयॉर्क समय के अनुसार 16:20 पर टाइमर सर्विस को शुरू करता है। refresh.py अंतिम संग्रहीत दिन से शुरू होने वाले प्रत्येक टिकर के लिए फीड से डेटा मांगता है, प्रत्येक के लिए एक या दो नई बार (bars) लिखता है, और प्रति टिकर एक लाइन प्रिंट करता है। 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;

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

Backtests में overfitting क्यों होती है, और इसे कैसे पहचानें

Screen को दो window lengths के फलन (function) के रूप में लिखें, pairs के ग्रिड को sweep करें, और उन्हें return के आधार पर rank करें। सबसे अच्छा pair बेहतरीन दिखेगा। यही समस्या है, परिणाम नहीं। 200 pairs का ग्रिड 200 प्रयोगों के बराबर है, और आपने केवल सबसे भाग्यशाली को चुना है।

आप इसे दस मिनट में होते हुए देख सकते हैं। डेटा को तारीख के आधार पर दो हिस्सों में बाँटें। केवल पहले हिस्से पर ग्रिड को sweep करें और विजेता को नोट करें। दूसरे हिस्से पर उसी ग्रिड को sweep करें। यदि दोनों विजेता एक-दूसरे से बहुत अलग हैं, तो parameters केवल noise को fit कर रहे हैं, और जिस pair ने केवल उसी हिस्से पर जीत हासिल की जिस पर आपने उसे tune किया था, वह भविष्य के बारे में कुछ नहीं बताता।

Survivorship overfitting से भी बदतर है क्योंकि tuning इसे ठीक नहीं कर सकती। आपकी ticker list आज के index members की है, इसलिए इसमें केवल वही कंपनियाँ शामिल हैं जो बची रहीं। feed से किसी ऐसे ticker के बारे में पूछें जिसे 2019 में delist कर दिया गया था, तो यह एक खाली frame लौटाएगा, जिसका अर्थ है कि वह कंपनी कभी आपके store में नहीं आई और कभी आपके test में शामिल नहीं हुई। आपके द्वारा चलाए गए हर backtest ने पहले ही विफलताओं को बाहर कर दिया है।

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

Adjusted prices आपके नीचे बदलती रहती हैं। auto_adjust=True के साथ closes को dividends और splits के लिए पीछे की ओर adjust किया जाता है, इसलिए अगले महीने चलाई गई वही query थोड़ा अलग इतिहास लौटाती है। जिन rows का आपने वास्तव में उपयोग किया है उन्हें store करना ही परिणाम को reproducible बनाता है, और यही एक और कारण है कि local store का अस्तित्व है।

Backtest commissions और slippage को भी अनदेखा करता है, और यह मान लेता है कि आपका order कीमत को प्रभावित नहीं करता। ये execution का हिस्सा हैं, जो यहाँ scope से बाहर है और 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 फ़ाइल को लिखने के लिए खुला रखे हुए है, आमतौर पर यह कोई interactive shell होता है जिसे आप बंद करना भूल गए हैं। इसे बंद करें, या read_only=True के साथ दूसरा connection खोलें।

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) को पढ़ता है और table का आकार नहीं बढ़ा है। feed ने खाली frames लौटाए हैं। यह विफलता सबसे लंबे समय तक छिपी रहती है, इसलिए जब हर ticker खाली वापस आए तो job को non-zero exit code के साथ समाप्त करें।

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

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

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

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

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

एक्जीक्यूशन को जानबूझकर दायरे से बाहर रखा गया है। ऑर्डर और ब्रोकर क्रेडेंशियल्स का जोखिम प्रोफाइल रीड-ओनली रिसर्च बॉक्स से अलग होता है, और उन्हें मिलाने का मतलब है ट्रेडिंग कीज को उसी मशीन पर रखना जहाँ LLM प्रॉम्प्ट चल रहा है। यदि आप यह देखना चाहते हैं कि यह पैटर्न आपके अपने सर्वर पर चलने वाली अन्य उपयोगी चीजों के साथ कहाँ स्थित है, तो चलाने योग्य सेल्फ-होस्टेड AI एजेंट्स एक विस्तृत टूर है।

FAQ

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

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

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

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

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

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

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

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

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

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