SSD Nodes Learn Hosting plans →
கல்வி வழிகாட்டிகள் Matt Connorஆல் Matt Connor · புதுப்பிக்கப்பட்டது 2026-08-13

VPS-ல் சொந்தமாக Stock Research Agent உருவாக்குவது எப்படி?

Python, DuckDB மற்றும் systemd timer பயன்படுத்தி உங்கள் சொந்த Stock Research Agent உருவாக்குங்கள். சந்தை தரவுகளை சேமித்து LLM மூலம் பகுப்பாய்வு செய்யும் முறையை அறியுங்கள்.

Self-hosted stock research agent என்றால் என்ன

Self-hosted stock research agent என்பது நீங்கள் சொந்தமாக வைத்திருக்கும் server-ல் இயங்கும் ஒரு சிறிய நிரலாகும். இது குறிப்பிட்ட கால இடைவெளியில் சந்தை தரவுகளை (market data) பெற்று, அவற்றை local database-ல் சேமித்து, அதன் மீது ஒரு screen-ஐ இயக்கி, என்ன மாற்றங்கள் நிகழ்ந்துள்ளன என்பதை விளக்குமாறு ஒரு large language model (LLM)-ஐக் கேட்கிறது. இது தரவுகளை வாசித்து வடிகட்டுகிறது. இது வர்த்தகம் (trade) செய்வதில்லை, மேலும் இந்த வழிகாட்டியில் உள்ள எதுவும் நிதி ஆலோசனையல்ல.

இதை புதிதாக உருவாக்கும் இருவர் வெவ்வேறு libraries-ஐத் தேர்ந்தெடுத்தாலும், இறுதியில் ஒரே மாதிரியான நான்கு பகுதிகளைக் கொண்டிருப்பார்கள்: விலைகள் மற்றும் அடிப்படைத் தரவுகளை வழங்கும் feed, நீங்கள் இதுவரை பெற்ற ஒவ்வொரு வரிசையையும் வைத்திருக்கும் local store, குறிப்பிட்ட கால இடைவெளியில் store-ஐப் புதுப்பிக்கும் job, மற்றும் எஞ்சியிருக்கும் வரிசைகளை வாக்கியங்களாக மாற்றும் LLM layer. இந்த வழிகாட்டி Python, DuckDB, systemd timer மற்றும் Claude API (application programming interface) ஆகியவற்றைப் பயன்படுத்தி இந்த அமைப்பை உருவாக்குகிறது. Execution என்பது தனித்துவமான தோல்வி முறைகளைக் கொண்ட ஒரு தனி job ஆகும், அது trading bots-க்காக அமைக்கப்பட்ட VPS-ல் மட்டுமே இருக்க வேண்டும்.

நான்கு பகுதிகள் மற்றும் அவற்றின் செயல்பாடுகள்

The feed என்பது வெளி உலகத்துடன் தொடர்பு கொள்ளும் ஒரே பகுதியாகும். இது ஒரு ticker மற்றும் தேதி வரம்பை (date range) எவ்வாறு கேட்பது என்பதையும், தரவு வரிசைகளை (rows) எவ்வாறு திருப்பித் தருவது என்பதையும் அறியும். இதற்குக் கீழே உள்ள அனைத்துப் பகுதிகளும் feed-ஐ விட உங்கள் database-ஐயே வாசிக்கின்றன. எனவே, feed செயலிழந்தால், ஒரு நாள் புதிய தரவு மட்டுமே கிடைக்காது; மாறாக, திரை (screen) செயலிழக்காது.

The store என்பது இந்த முழுச் செயல்பாட்டின் முக்கிய நோக்கமாகும். நீங்கள் பதிவு செய்யாத ஒரு daily close தரவை, பொதுவாகப் பிற்காலத்தில் மீண்டும் பெற்றுக்கொள்ள முடியும். ஆனால், ஒரு intraday quote, திருத்தப்படுவதற்கு முந்தைய மதிப்பீடு அல்லது மறுசீரமைக்கப்படுவதற்கு முந்தைய அடிப்படைத் தரவு (fundamentals figure) ஆகியவற்றை மீண்டும் பெற முடியாது. தரவு வழங்கப்பட்ட நாளில் அது என்ன கூறியது என்பதற்கான பதிவை உருவாக்குவதே the store-ன் பணியாகும்.

The scheduler என்பது தரவு புதுப்பித்தல் எப்போது நடக்க வேண்டும் என்பதைத் தீர்மானிக்கிறது. ஒரு server-ல் இது systemd timer மூலம் இயங்குகிறது; இதனால்தான் குறியீட்டை (code) விட VPS இங்கு முக்கியத்துவம் பெறுகிறது.

The LLM layer என்பது உங்கள் SQL உருவாக்கிய ஒரு சிறிய உரைத் தொகுதியை (text block) வாசித்து, அதன் சுருக்கத்தை எழுதுகிறது. இது ஒருபோதும் database-உடன் இணைக்கப்படுவதில்லை மற்றும் query-ஐ உருவாக்குவதில்லை. ஒருவேளை model-லே SQL-ஐ எழுதினால், ஒரு தவறான token, சரளமான வாக்கியத்திற்குள் தவறான எண்ணாக மாறிவிடும்; அதை ஒப்பிட்டுப் பார்க்க எதுவுமிருக்காது. SQL தரவுகளை உருவாக்கினால், model உரைநடையை (prose) மட்டுமே தவறாக எழுத முடியும்; நீங்கள் அனுப்பிய தரவு வரிசைகளை வைத்து அந்த உரைநடையைச் சரிபார்க்க முடியும்.

ஏன் ஒரு மடிக்கணினிக்கு பதிலாக VPS-ல் இயக்க வேண்டும்

இதற்கான காரணம் scheduler ஆகும். நியூயார்க் நேரப்படி மாலை 16:00 மணிக்கு அமெரிக்க சந்தை முடிவடையும் நேரம், பெர்லினில் 22:00 மணி மற்றும் ஜகார்த்தாவில் அடுத்த நாள் காலை 04:00 மணி ஆகும். இந்த இரண்டு நேரங்களிலும் மடிக்கணினி உறக்க நிலையில் (asleep) இருக்கும். ஒருமுறை இயக்கம் தவறுவது, ஒரு குறிப்பு தாமதமாவதை விட அதிக இழப்பை ஏற்படுத்தும்: தினசரி தரவுகளை (daily bars) பின்னர் மீண்டும் பெற்றுக்கொள்ள முடியும், ஆனால் திருத்தப்படும் எந்தவொரு தரவையும் மீண்டும் பெற முடியாது, எனவே உங்கள் பதிவுகளில் ஏற்படும் இடைவெளி நிரந்தரமானதாகிவிடும். ஒரு முகவரின் (agent) கால அட்டவணையும் சேமிக்கப்பட்ட நிலையும் (stored state) கணினி மறுதொடக்கம் (reboot) செய்யப்பட்ட பிறகும் நீடிக்க வேண்டும் என்ற அதே தர்க்கம் தான், KiroCrew-ஐ உங்கள் சொந்த VPS-ல் எப்போதும் இயங்கும் முகவராக இயக்குதல் என்பதன் அடிப்படையாகும்.

இரண்டாவது காரணம் சிறியது, ஆனால் உண்மையானது. ஒரு server-ல் ஒரே ஒரு API key மட்டுமே இருக்கும், அது ஒரே ஒரு கோப்பில் இருக்கும், login shell இல்லாத ஒரே ஒரு system user-க்கு சொந்தமானதாக இருக்கும், மேலும் ஒரே ஒரு பணிக்கு மட்டுமே பயன்படுத்தப்படும். நீங்கள் இணையத்தில் உலாவப் பயன்படுத்தும் மடிக்கணினியில் இத்தகைய சூழலை அமைப்பது மிகவும் கடினம். குறியீட்டிற்குள்ளும் (code) மற்றும் மாதிரியின் உள்ளீட்டிற்குள்ளும் (model's input) key-ஐ வைக்காதீர்கள்; இதுவே AI முகவரிடமிருந்து API keys-ஐ பாதுகாத்தல் என்பதன் மையப்பொருள்.

வளங்களை அளவிடுதல்: Disk, RAM மற்றும் Tokens

Disk-ஐக் கணக்கிடுவது எளிது. ஒரு வர்த்தக நாளில் ஒரு ticker-க்கு ஒரு வரிசை (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"
  }
]

இருபது tickers-க்கு ஒரு வருடத்திற்குப் பிறகு 5,040 வரிசைகள் இருக்கும். 500 tickers வரியானது பத்து ஆண்டுகளுக்குப் பிறகு 1,260,000 வரிசைகளை எட்டும். ஒவ்வொரு வரிசையிலும் ஒரு தேதி மற்றும் சில doubles மதிப்புகள் இருக்கும். DuckDB நெடுவரிசைகளைச் சுருக்கி (compressed) சேமிப்பதால், இது gigabytes அளவில் இல்லாமல், tens of megabytes அளவிலேயே இருக்கும். இந்த மதிப்பீட்டை, என்னுடையது உட்பட, அப்படியே நம்ப வேண்டாம். உங்கள் முதல் backfill-க்கு பிறகு du -h /opt/research/data/market.duckdb-ஐ இயக்கி, உங்கள் சொந்த தரவுகளைக் கொண்டு கணக்கிடுங்கள்.

சிறிய VPS-களில் RAM ஒரு சிக்கலான காரணியாகும். DuckDB இயல்பாகவே ஒரு query-க்காக இயந்திரத்தின் நினைவகத்தில் பெரும் பகுதியையும், அனைத்து cores-களையும் எடுத்துக்கொள்ளும். இது ஒரு analytics server-க்குச் சரியாக இருக்கலாம், ஆனால் பிற சேவைகளும் இயங்கும் 2 GB அளவுள்ள ஒரு box-க்கு இது தவறு. முழு prices அட்டவணையிலும் ஒரு aggregation செய்யும்போது, kernel அந்த process-ஐக் கொன்றுவிடும் (kill). அப்போது systemd Main process exited, code=killed, status=9/KILL என்று காட்டும், மேலும் journalctl -k கட்டளையானது out of memory பிழையைக் காட்டும். memory_limit மற்றும் threads ஆகியவற்றைத் தெளிவாக அமைத்தால், query வேகம் குறையுமே தவிர, process பாதியிலேயே நின்றுவிடாது.

Tokens-ஐ மதிப்பீடு செய்யாமல், அளவிட வேண்டும். ஒவ்வொரு Messages API பதிலிலும் input_tokens மற்றும் output_tokens ஆகியவற்றை உள்ளடக்கிய ஒரு usage object இருக்கும். ஒவ்வொரு அழைப்பின் போதும் இவை இரண்டையும் ஒரு அட்டவணையில் எழுதுங்கள். ஒரு வாரத்திற்குப் பிறகு உங்கள் உண்மையான பயன்பாட்டு அளவு தெரிந்துவிடும். அதை நீங்கள் பயன்படுத்தும் model-ன் அன்றைய விலைப்பட்டியலுடன் பெருக்கிக்கொள்ளலாம். திட்டமிடுவதற்கு இரண்டு விஷயங்கள் நிலையானவை. அனைத்து Claude model-களிலும் output tokens-ன் விலை input tokens-ஐ விட அதிகம். எனவே, குறிப்புகளை 200 வார்த்தைகளுக்குள் கட்டுப்படுத்துவது, நீங்கள் அனுப்பும் தரவுகளைக் குறைப்பதை விட அதிகச் செலவைக் குறைக்கும். மேலும், ஒரு நாளைக்கு ஒருமுறை நடக்கும் பணிக்கு prompt caching உதவாது, ஏனெனில் cache-ன் ஆயுட்காலம் சில நிமிடங்கள் மட்டுமே. அடுத்த முறை அந்தப் பணி நடக்கும்போது, cached block காலாவதியாகிவிடும், நீங்கள் மீண்டும் முழு input விலையையும் செலுத்த வேண்டியிருக்கும். ஒரே பெரிய தரவுத் தொகுப்பின் மீது பலமுறை அழைப்புகளைச் செய்யும் பணிகளுக்கு மட்டுமே caching பயனுள்ளதாக இருக்கும்.

Install the pieces

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

Check the install before you write any code:

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

That prints ok. If you skipped the virtual environment and ran pip install against the system Python, Ubuntu 24.04 stops you with error: externally-managed-environment, because the distribution owns /usr/lib/python3 and refuses to let pip write there. The venv is not politeness. It is the only directory pip is allowed to touch.

The API key goes in a file the service user can read and nobody else can:

sudo install -d -m 755 /etc/research
sudo install -m 640 -o root -g research /dev/null /etc/research/env
sudoedit /etc/research/env

Put one line in it, with no quotes and no export, because systemd parses this file itself instead of passing it to a 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-ஐ இருமுறை இயக்கினாலும் அட்டவணையில் தரவுகள் இரட்டிப்பாகாது. இந்தச் சாவி இல்லையெனில், ஒரு செயலிழப்புக்குப் பிறகு மீண்டும் இயக்கும்போது ஒவ்வொரு bar-ம் தானாகவே நகலெடுக்கப்படும். அதன் பிறகு நீங்கள் கணக்கிடும் ஒவ்வொரு சராசரியும் தவறாக இருக்கும், ஆனால் இது குறித்து எந்தப் பிழைச் செய்தியும் வராது.

DuckDB-ல் ஒரு நேரத்தில் ஒரே ஒரு process மட்டுமே கோப்பை எழுதும் உரிமையுடன் (write) திறக்க முடியும். இரண்டாவது writer முயற்சிக்கும்போது Could not set lock on file பிழை ஏற்படும், அதைத் தொடர்ந்து அந்த கோப்பைத் தன் வசம் வைத்துள்ள PID-ன் எண் காட்டப்படும். நடைமுறையில், நீங்கள் மற்றொரு terminal-ல் திறந்து வைத்துள்ள duckdb shell-ஆகவே அது இருக்கும். வாசகர்கள் (readers) read_only=True-ஐப் பயன்படுத்தலாம், இதனால்தான் connect இந்த flag-ஐக் கோருகிறது. பல process-கள் ஒரே நேரத்தில் எழுத வேண்டிய அவசியம் இருந்தால், அது வேறு ஒரு engine-ன் வேலை: SQLite-ன் WAL mode, ஒரு writer commit செய்யும்போது வாசகர்களை வேலை செய்ய அனுமதிக்கிறது, மேலும் busy timeout வசதி மற்ற writer-களைப் பிழை காட்டாமல் காத்திருக்கச் செய்கிறது. சர்வர் பணிச்சுமைக்கு DuckDB மற்றும் SQLite ஒப்பீடு இவ்விரண்டையும் ஒப்பிடுகிறது, மேலும் VPS-ல் SQLite-ஐ production-ல் இயக்குதல் WAL சரியாகச் செயல்படுவதற்கான அமைப்புகளை விளக்குகிறது.

Refresh job

# /opt/research/refresh.py
import sys
import pandas as pd
import yfinance as yf
from store import connect

TICKERS = ['AAPL', 'MSFT', 'KO', 'SAP', 'TSM']
FIRST_DAY = '2016-01-01'
COLS = ['ticker', 'day', 'open', 'high', 'low', 'close', 'volume']

def fetch(ticker, start):
    df = yf.Ticker(ticker).history(start=start, auto_adjust=True)
    if df.empty:
        return None
    df = df.reset_index()
    stamps = pd.to_datetime(df['Date'])
    if stamps.dt.tz is not None:
        stamps = stamps.dt.tz_localize(None)
    df['day'] = stamps.dt.date
    df['ticker'] = ticker
    df = df.rename(columns={'Open': 'open', 'High': 'high', 'Low': 'low',
                            'Close': 'close', 'Volume': 'volume'})
    return df[COLS]

def main():
    con = connect()
    empty = 0
    for ticker in TICKERS:
        last = con.execute('SELECT max(day) FROM prices WHERE ticker = ?',
                           [ticker]).fetchone()[0]
        rows_df = fetch(ticker, str(last) if last else FIRST_DAY)
        if rows_df is None:
            print(ticker + ': feed returned no rows', file=sys.stderr)
            empty += 1
            continue
        con.register('rows_df', rows_df)
        con.execute('INSERT OR REPLACE INTO prices '
                    'SELECT ticker, day, open, high, low, close, volume FROM rows_df')
        print(ticker + ': ' + str(len(rows_df)) + ' rows')
    con.close()
    if empty == len(TICKERS):
        print('every ticker returned nothing: the feed is broken', file=sys.stderr)
        sys.exit(1)

main()

இதை ஒருமுறை கைமுறையாக இயக்கவும்:

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

முதல்முறை இயக்கும்போது, பல ஆண்டுகளுக்கான தரவுகள் நிரப்பப்பட்டு, ஒவ்வொரு ticker-க்கும் ஆயிரக்கணக்கான வரிசைகள் கொண்ட ஒரு வரி அச்சிடப்படும். ஒரு நிமிடம் கழித்து மீண்டும் இயக்கினால், ஒவ்வொரு வரியிலும் 1 அல்லது 2 வரிசைகள் மட்டுமே இருக்கும், ஏனெனில் ஏற்கனவே சேமிக்கப்பட்ட கடைசி நாளிலிருந்து இந்த job தொடங்குகிறது. அந்த இரண்டாவது இயக்கமே உண்மையான சோதனை: எண்ணிக்கை இன்னும் ஆயிரக்கணக்கில் இருந்தால், max(day) எதையும் திரும்பத் தரவில்லை என்று அர்த்தம், மேலும் ஒவ்வொரு இரவும் உங்கள் முழு வரலாற்றையும் insert மீண்டும் எழுதுகிறது.

df.empty சரிபார்ப்புதான் கோப்பில் உள்ள மிக முக்கியமான வரியாகும். தவறான அல்லது delist செய்யப்பட்ட குறியீட்டிற்கு (symbol) Ticker.history() பிழையை (raise) ஏற்படுத்தாது. இது அந்த குறியீடு delist செய்யப்பட்டிருக்கலாம் அல்லது விலை தரவு கிடைக்கவில்லை என்ற எச்சரிக்கையை மட்டும் அச்சிடும் (இந்த வாசகம் library பதிப்புகளுக்கு ஏற்ப மாறும்) மற்றும் காலியான DataFrame-ஐத் திரும்பத் தரும். அந்தச் சரிபார்ப்பு இல்லாத ஒரு job எதையும் எழுதாது, 0 என்ற exit code-ஐத் தரும், மேலும் systemd அந்த run-ஐ வெற்றிகரமாக (green) காட்டும், ஆனால் அட்டவணை மெதுவாக வளர்வதை நிறுத்திவிடும். பல வாரங்களுக்குப் பிறகு, ஒவ்வொரு நாளும் ஒரே வரிசைகளைத் தரும் திரையைப் பார்க்கும்போதுதான் நீங்கள் இதைக் கண்டறிவீர்கள்.

Timestamp கையாளுதலுக்கும் ஒரு காரணம் உள்ளது. feed திரும்பத் தரும் index, exchange-ன் timezone-ஐக் கொண்டிருக்கலாம், மேலும் offset-ஐ நீக்குவது UTC-க்கு மாற்றுவதற்குச் சமமல்ல. நள்ளிரவு உள்ளூர் நேரத்தைக் கொண்ட ஒரு Tokyo session, UTC-யில் முந்தைய நாட்காட்டி நாளாக மாறும், எனவே UTC மாற்றம் ஒவ்வொரு Japanese bar-ஐயும் அமைதியாக ஒரு நாள் பின்னோக்கி நகர்த்தி primary key-ஐச் சிதைத்துவிடும். tz_localize(None) அந்தந்த exchange-ன் session தேதியையே வைத்திருக்கிறது, இதுதான் ஒரு daily bar-ன் உண்மையான அர்த்தமாகும்.

சந்தை முடிவடையும் நேரத்தில் இயங்கும் timer

அமெரிக்கச் சந்தை நியூயார்க் நேரப்படி 16:00 மணிக்கு முடிவடைகிறது. கடைசி வர்த்தகப் பதிவுகள் (prints) முடிவடையச் சில நிமிடங்கள் ஆவதால், இந்த job 16:20 மணிக்கு இயங்குகிறது. இதை UTC-ல் குறிப்பிடாமல் நியூயார்க் நேரத்திலேயே எழுதவும். குளிர்காலத்தில் நியூயார்க் நேரம் UTC-5 ஆகவும், கோடையில் UTC-4 ஆகவும் இருக்கும். எனவே, நிலையான UTC நேரத்தைக் குறிப்பிட்டால், ஆண்டுக்கு இருமுறை நேரம் ஒரு மணிநேரம் மாறி, சந்தை முடிவதற்கு முன்பே job இயங்கத் தொடங்கிவிடும். systemd 252 மற்றும் அதற்குப் பிந்தைய பதிப்புகள் OnCalendar-ல் நேரடியாக timezone-ஐ ஏற்றுக்கொள்கின்றன. Ubuntu 24.04-ல் 255 பதிப்பு உள்ளது. உங்கள் பதிப்பை systemctl --version மூலம் உறுதிப்படுத்தவும்.

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

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

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

[Install]
WantedBy=timers.target

Type=oneshot என்பது ஒன்றுக்கும் மேற்பட்ட ExecStart-களை ஏற்கும் ஒரே service வகை ஆகும். இது அவற்றை வரிசையாக இயக்கும்; ஏதேனும் ஒன்று non-zero status-ல் முடிந்தால், அடுத்ததை இயக்காமல் நிறுத்திவிடும். இதுவே நமக்குத் தேவையான செயல்பாடு: தரவு புதுப்பித்தல் தோல்வியடைந்தால், பழைய தரவைக் கொண்டு திரையை மாற்றக்கூடாது. kernel updates-க்காக VPS reboot செய்யப்படும்போது Persistent=true முக்கியமானது. இது இல்லையென்றால், 16:15 மணிக்கு reboot நடக்கும்போது அந்த run விடுபட்டுவிடும்; இது இருந்தால், machine மீண்டும் இயங்கியவுடன் job உடனடியாகத் தொடங்கும்.

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

list-timers கட்டளையில் NEXT என்ற பத்தி (column) இருக்க வேண்டும். அது அடுத்த வேலைநாளில் இயங்கும் நேரத்தை server-ன் உள்ளூர் நேரத்திற்கேற்ப மாற்றிக் காட்டும். பட்டியல் காலியாக இருந்தால், timer enabled நிலையில் இல்லை அல்லது unit-ல் [Install] பகுதி விடுபட்டுள்ளது என்று பொருள். இதனால் enable-ஆல் timers.target-ல் எதையும் இணைக்க முடியவில்லை.

திரை: முதலில் SQL, இறுதியில் model

SQL துல்லியமானது, அதை இயக்குவதற்கு செலவு ஏதுமில்லை. model அவ்வாறு இல்லை. எனவே, query தரவுகளின் அளவைக் குறைக்கிறது; எஞ்சிய தரவுகள் மட்டுமே model-க்கு அனுப்பப்படுகின்றன. இதை /opt/research/screen.sql ஆக சேமிக்கவும்.

WITH ma AS (
  SELECT ticker, day, close,
         avg(close) OVER w20 AS ma20,
         avg(close) OVER w50 AS ma50,
         row_number() OVER (PARTITION BY ticker ORDER BY day) AS n
  FROM prices
  WINDOW
    w20 AS (PARTITION BY ticker ORDER BY day ROWS BETWEEN 19 PRECEDING AND CURRENT ROW),
    w50 AS (PARTITION BY ticker ORDER BY day ROWS BETWEEN 49 PRECEDING AND CURRENT ROW)
)
SELECT ticker, day, close, round(ma20, 2) AS ma20, round(ma50, 2) AS ma50
FROM ma
WHERE n > 50 AND ma20 > ma50
ORDER BY day DESC, ticker
LIMIT 20;

n > 50 filter என்பது அலங்காரத்திற்காக அல்ல. ROWS BETWEEN 49 PRECEDING AND CURRENT ROW என்பது இருக்கும் அனைத்து வரிசைகளின் சராசரியைக் கணக்கிடுகிறது. எனவே, ஒரு ticker-ன் மூன்றாவது வரிசை மூன்று நாட்களின் சராசரியைக் காட்டி, அதை ma50 என்று அழைக்கும். இதை ma20 உடன் ஒப்பிடும்போது, ஒவ்வொரு ticker-ன் வரலாற்றின் தொடக்கத்திலும் நடக்காத ஒரு crossover-ஐ நீங்கள் உருவாக்குகிறீர்கள். வரிசை எண்ணை (row number) வைத்து filter செய்வதன் மூலம், window முழுமையடையாத வரிசைகள் நீக்கப்படுகின்றன.

மாதிரி எதைப் பார்க்கிறது, எதைப் பார்க்காது

# /opt/research/screen.py
from anthropic import Anthropic
from store import connect

SYSTEM = (
    'You are a research assistant. Use only the rows in the message. '
    'If a number is not in the rows, say that it is not available. '
    'Do not give investment advice, price targets or buy and sell calls. '
    'Write at most 200 words.'
)

con = connect()
sql = open('/opt/research/screen.sql').read()
rows = con.execute(sql).fetchall()
block = '\n'.join(' / '.join(str(v) for v in row) for row in rows)

client = Anthropic(max_retries=5)   # reads ANTHROPIC_API_KEY from the environment
resp = client.messages.create(
    model='claude-sonnet-5',
    max_tokens=600,
    system=SYSTEM,
    messages=[{'role': 'user', 'content': 'Screen hits, ticker / day / close / ma20 / ma50:\n' + block}],
)

print(resp.content[0].text)
con.execute('INSERT INTO runs VALUES (now(), ?, ?, ?, ?)',
            ['claude-sonnet-5', resp.usage.input_tokens,
             resp.usage.output_tokens, len(rows)])
con.close()

System prompt-ல் உள்ள வார்த்தை வரம்பு, செலவு மிகுந்த பகுதியைக் கட்டுப்படுத்துகிறது. கொடுக்கப்பட்ட வரிசைகளை (rows) மட்டுமே பயன்படுத்த வேண்டும் என்ற அறிவுறுத்தலை, நீங்கள் நம்புவதை விட சரிபார்ப்பதே சிறந்தது: தொகுப்பிலிருந்து ஒரு column-ஐ நீக்கிவிட்டு, மீண்டும் இயக்கி, வெளியீட்டைப் பார்க்கவும். அந்த column-க்கான எண் இன்னும் தோன்றினால், மாதிரி அந்த இடைவெளியை நிரப்பியுள்ளது என்று அர்த்தம்; உங்கள் prompt போதுமான அளவு இறுக்கமாக இல்லை. இந்தச் சோதனையைச் செய்ய இரண்டு நிமிடங்கள் மட்டுமே ஆகும், இதுவே உண்மையை அறிய உதவும் ஒரே நேர்மையான வழி.

மாதிரி ஒருபோதும் API key-ஐப் பார்ப்பதில்லை, database path-ஐப் பார்ப்பதில்லை, எந்த query-யையும் இயக்குவதில்லை. அது வரிசைகளைப் பெற்று உரைநடையைத் தருகிறது. இந்த எல்லையே வெளியீட்டைச் சரிபார்க்கக்கூடியதாக மாற்றுகிறது, ஏனெனில் குறிப்பில் உள்ள ஒவ்வொரு எண்ணும் நீங்கள் அனுப்பிய தொகுப்பிலும் இருக்க வேண்டும், அவற்றை நீங்கள் வரிசையாக ஒப்பிடலாம். இதற்கான prompting முறைகள் குறித்து மேலும் அறிய, நிதி பகுப்பாய்விற்கு Claude-ஐப் பயன்படுத்துதல் என்ற பகுதி, மாதிரி எதைப் படிப்பதில் சிறந்தது என்பதை விரிவாக விளக்குகிறது.

முழுமையான செயல்பாட்டு சுழற்சி

நியூயார்க் நேரப்படி 16:20 மணிக்கு டைமர் சேவையைத் தொடங்குகிறது. refresh.py கடைசியாகச் சேமிக்கப்பட்ட நாளிலிருந்து ஒவ்வொரு ticker-க்கும் தரவுகளைக் கோருகிறது, ஒவ்வொன்றிற்கும் ஒன்று அல்லது இரண்டு புதிய bars-களை எழுதுகிறது, மேலும் ஒவ்வொரு ticker-க்கும் ஒரு வரியை அச்சிடுகிறது. screen.py அதே கோப்பைத் திறந்து, moving average வினவலை (query) இயக்குகிறது, மேலும் சில வரிசைகளைத் திரும்பப் பெறுகிறது. அந்த வரிசைகள் சில நூறு tokens கொண்ட உரைத் தொகுதியாக மாறுகின்றன. ஒரு API அழைப்பு அவற்றை ஒரு சிறிய குறிப்பாக மாற்றுகிறது, அந்தக் குறிப்பு journal-க்குச் செல்கிறது, மேலும் token எண்ணிக்கை மற்றும் hits எண்ணிக்கையுடன் ஒரு வரி runs-ல் சேமிக்கப்படுகிறது.

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

சரியான log கோப்பில் ஒவ்வொரு ticker-க்கும் ஒரு வரி, அதைத் தொடர்ந்து குறிப்பு, பின்னர் research-refresh.service: Deactivated successfully ஆகியவை இருக்கும். ஒரு வாரத்திற்குப் பிறகு, தரவுத்தளத்திலிருந்து உங்கள் செலவை நீங்களே கணக்கிடுங்கள்:

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

அந்த மொத்தத் தொகையை, நீங்கள் கணக்கிடும் நாளில் உங்கள் model குறிப்பிடும் ஒரு மில்லியன் token-க்கான விலையுடன் பெருக்கவும். இது மற்றவர்களின் மதிப்பீட்டிற்குப் பதிலாக உண்மையான தொகையை உங்களுக்கு வழங்கும். வெளியிடப்பட்ட விலைகள் மாறலாம். ஆனால் கணக்கீடு மாறாது.

Backtests ஏன் overfit ஆகின்றன மற்றும் அதை எவ்வாறு கண்காணிப்பது

இரண்டு window நீளங்களின் செயல்பாடாக screen-ஐ மாற்றி, ஜோடிகளின் grid-ஐ sweep செய்து, அவற்றை வருவாயின் அடிப்படையில் வரிசைப்படுத்தவும். சிறந்த ஜோடி மிகச்சிறப்பாகத் தோன்றும். இதுவே சிக்கல், முடிவு அல்ல. 200 ஜோடிகளைக் கொண்ட ஒரு grid என்பது 200 சோதனைகள் ஆகும், அதில் நீங்கள் அதிர்ஷ்டமான ஒன்றைத் தேர்ந்தெடுத்துள்ளீர்கள்.

இதை பத்து நிமிடங்களில் நீங்கள் கண்காணிக்கலாம். தரவு சேமிப்பகத்தை (store) தேதியின் அடிப்படையில் பாதியாகப் பிரிக்கவும். முதல் பாதியில் மட்டும் grid-ஐ sweep செய்து வெற்றியாளரைக் குறித்துக்கொள்ளவும். அதே grid-ஐ இரண்டாம் பாதியில் sweep செய்யவும். இரண்டு வெற்றியாளர்களும் வெகு தொலைவில் இருந்தால், parameters இரைச்சலை (noise) பொருத்துகின்றன (fitting) என்று அர்த்தம். நீங்கள் tune செய்த பாதியில் மட்டும் வெற்றி பெறும் ஜோடி, நாளை என்ன நடக்கும் என்பதைப் பற்றி எதையும் கூறாது.

Survivorship என்பது overfitting-ஐ விட மோசமானது, ஏனெனில் tuning மூலம் இதைச் சரிசெய்ய முடியாது. உங்கள் ticker பட்டியல் இன்றைய index உறுப்பினர்களைக் கொண்டது, எனவே இது தப்பிப்பிழைத்த நிறுவனங்களை மட்டுமே கொண்டுள்ளது. 2019-ல் நீக்கப்பட்ட (delisted) ஒரு ticker-ஐ feed-இடம் கேட்டால், அது காலியான frame-ஐத் தரும். அதாவது அந்த நிறுவனம் உங்கள் store-க்குள் நுழையாது, உங்கள் சோதனையிலும் நுழையாது. நீங்கள் இயக்கும் ஒவ்வொரு backtest-ம் தோல்வியுற்ற நிறுவனங்களை ஏற்கனவே தவிர்த்துவிடுகிறது.

Restated fundamentals காலவரிசையைச் சிதைக்கின்றன. 2019-ம் ஆண்டின் காலாண்டுக்கு API இன்று வழங்கும் வருவாய் புள்ளிவிவரம், 2019-ல் வெளியிடப்பட்ட புள்ளிவிவரம் அல்ல. இன்றைய fundamentals-ஐ 2019-ன் விலைகளுடன் கலக்கும் ஒரு screen, அப்போது இல்லாத தகவலைப் பயன்படுத்துகிறது. இதில் விலைகள் பொதுவாகப் பாதுகாப்பானவை. Fundamentals பொதுவாகப் பாதுகாப்பானவை அல்ல.

Adjusted prices உங்களுக்குத் தெரியாமலேயே மாறுகின்றன. auto_adjust=True மூலம், dividend மற்றும் split-களுக்காக closes பின்னோக்கி சரிசெய்யப்படுகின்றன. எனவே அடுத்த மாதம் அதே query-ஐ இயக்கினால் சற்று மாறுபட்ட வரலாறு கிடைக்கும். நீங்கள் உண்மையில் பயன்படுத்திய rows-ஐச் சேமித்து வைப்பதுதான் ஒரு முடிவை மீண்டும் உருவாக்கக்கூடியதாக (reproducible) மாற்றுகிறது. உள்ளூர் store இருப்பதற்கான மற்றொரு காரணமும் இதுவே.

Backtest என்பது commissions மற்றும் slippage-ஐப் புறக்கணிக்கிறது, மேலும் உங்கள் order விலையை மாற்றாது என்றும் கருதுகிறது. இவை execution சார்ந்தவை, அவை இங்கு விவாதத்திற்கு அப்பாற்பட்டவை மற்றும் VPS-ல் trading bots-ஐ இயக்குதல் என்பதில் விளக்கப்பட்டுள்ளன.

தோல்வி நிலைகளும் நீங்கள் காணும் செய்திகளும்

error: externally-managed-environment என்பது pip இயங்கும்போது தோன்றும். நீங்கள் virtual environment-க்கு வெளியே இருக்கிறீர்கள். /opt/research/venv/bin/pip-ஐ அதன் முழுப் பாதையைக் கொண்டு அழைக்கவும்.

Could not set lock on file, அதைத் தொடர்ந்து ஒரு PID எண் வரும். வேறொரு process DuckDB கோப்பை எழுதும் அனுமதியுடன் திறந்து வைத்துள்ளது, பெரும்பாலும் நீங்கள் கவனிக்காமல் விட்ட interactive shell-ஆக இருக்கலாம். அதை மூடவும், அல்லது read_only=True மூலம் இரண்டாவது இணைப்பைத் திறக்கவும்.

Main process exited, code=killed, status=9/KILL என்பது systemctl status-ல் தோன்றும். நினைவகப் பற்றாக்குறையால் kernel அந்த வேலையை நிறுத்திவிட்டது. journalctl -k | grep -i oom மூலம் இதை உறுதிப்படுத்தவும், பின் store.py-ல் memory_limit மதிப்பை குறைக்கவும்.

எந்தத் தரவையும் எழுதாத வெற்றிகரமான இயக்கம். systemctl status என்பது active (exited)-ஐ வாசிக்கிறது, ஆனால் அட்டவணை வளரவில்லை. feed காலியான frames-களைத் திருப்பியளித்துள்ளது. இந்தத் தோல்வி நீண்ட காலம் கண்டறியப்படாமல் இருக்கும், எனவே ஒவ்வொரு ticker-ம் காலியாக வரும்போது அந்த job-ஐ non-zero exit code-டன் முடிவடையச் செய்யவும்.

சந்தை விடுமுறை நாளில் timer இயங்குதல். systemd-க்கு பங்குச்சந்தை நாட்காட்டி தெரியாது, எனவே Mon-Fri விடுமுறை நாட்களையும் உள்ளடக்கும். இயக்கம் நடக்கும், feed-ல் புதிய தரவு இருக்காது, இதை ஒரு பிழையாகக் கருதாமல் இயல்பான ஒன்றாக job கையாள வேண்டும்.

API-லிருந்து 429. நீங்கள் rate limit-ஐத் தாண்டிவிட்டீர்கள். Anthropic SDK தானாகவே backoff செய்து மீண்டும் முயலும், மேலும் Anthropic(max_retries=5) முயற்சியின் எண்ணிக்கையை உயர்த்தும். இது தினமும் தோல்வியடைந்தால், அந்த job ஒரே நேரத்தில் அதிகப்படியான கோரிக்கைகளை அனுப்புகிறது என்று அர்த்தம்.

இது எதைக் குறிக்கவில்லை

இது ஒரு ஆராய்ச்சி உதவியாளர். ஒரு கோப்பைச் சுருக்கித் தரும் மாதிரி, அந்த ஆவணத்தின் வாசிப்பை மட்டுமே வழங்குகிறது. அதில் உள்ள எண்களைத் தவறாகக் குறிப்பிட வாய்ப்புள்ளது, எனவே குறிப்பில் உள்ள ஒவ்வொரு எண்ணையும் நீங்கள் அனுப்பிய தரவுகளுடன் சரிபார்க்க வேண்டும். இந்த வெளியீட்டை நீங்கள் நீங்களே வாசிக்க வேண்டிய விஷயங்களின் பட்டியலாகக் கருதவும். இது நிதி ஆலோசனையோ அல்லது வர்த்தக சிக்னலோ அல்ல.

Backtests என்பது ஒரு யோசனையை நிராகரிக்கப் பயன்படுமே தவிர, அதை உறுதிப்படுத்தப் போதுமானதல்ல. உங்கள் சொந்தத் தரவில் தோல்வியடையும் ஒரு உத்தி முற்றிலும் பயனற்றது. உங்கள் தரவில் வெற்றிபெறும் ஒரு உத்தி, அந்தத் தரவை மட்டுமே கடந்து வந்துள்ளது; இது நள்ளிரவில் தோன்றுவது போலப் பெரிய சாதனை அல்ல.

Execution என்பது திட்டமிட்டே இந்த வரம்பிற்குள் சேர்க்கப்படவில்லை. Orders மற்றும் broker credentials ஆகியவை read-only ஆராய்ச்சி சூழலை விட அதிக ஆபத்து கொண்டவை. இவற்றை ஒன்றாக இணைப்பது, LLM prompt இயங்கும் அதே கணினியில் வர்த்தகக் குறியீடுகளை (trading keys) வைப்பதற்குச் சமம். உங்கள் server-ல் இயக்க வேண்டிய பிற பயனுள்ள விஷயங்களுடன் இந்த அமைப்பு எவ்வாறு பொருந்துகிறது என்பதை அறிய, இயக்க வேண்டிய self-hosted AI agents குறித்த விரிவான வழிகாட்டியைப் பார்க்கவும்.

FAQ

எனக்கு கட்டண சந்தா கொண்ட market data feed தேவையா?

Prototype-க்கு தேவையில்லை. கணினியின் கட்டமைப்பைக் கற்றுக்கொள்ள இலவசமான, அதிகாரப்பூர்வமற்ற feed போதுமானது. ஆனால், இது எந்தப் பொறுப்பும் இல்லாத ஒரு இணையதளத்தைச் சார்ந்திருப்பதால், எப்போது வேண்டுமானாலும் செயலிழக்கலாம். இது பெரும்பாலும் பிழையை (exception) காட்டாமல், வெறும் காலி தரவுகளை மட்டுமே அனுப்பும். எனவே, உங்கள் நிரல் வரிசைகளின் எண்ணிக்கையை (row counts) சரிபார்க்க வேண்டும். தரவுகள் முடிவெடுக்கும் நிலைக்கு வரும்போது, ஆவணப்படுத்தப்பட்ட API மற்றும் ஆதரவு முகவரி கொண்ட கட்டண feed-க்கு மாறிவிடுங்கள். Store-ஐப் பயன்படுத்துவதால் இந்த மாற்றம் எளிதாகிறது: fetch function-ஐ மட்டும் மாற்றினால் போதும், schedule, schema மற்றும் screen ஆகியவை அப்படியே இருக்கும்.

விலைகளை SQLite-ல் சேமிக்க வேண்டுமா அல்லது DuckDB-ல் சேமிக்க வேண்டுமா?

DuckDB என்பது columnar முறையில் இயங்கக்கூடியது; பல வரிசைகளை ஸ்கேன் செய்து கூட்டுத்தொகையைக் கணக்கிட இது உருவாக்கப்பட்டது. பத்து ஆண்டுகால தரவுகளின் moving average-ஐக் கணக்கிட இதுவே சிறந்தது. SQLite என்பது row-oriented முறையில் இயங்கக்கூடியது; ஒரே நேரத்தில் பல process-கள் சிறிய அளவில் தரவுகளைப் படிக்கவும் எழுதவும் இது சிறந்தது. ஒரு குறிப்பிட்ட நேரத்தில் சில நூறு வரிசைகளைச் சேர்த்து, பின் மில்லியன் கணக்கான வரிசைகளை ஸ்கேன் செய்யும் வேலைக்கு DuckDB சிறந்தது. பல process-கள் ஒரே நேரத்தில் எழுத வேண்டியிருந்தால், SQLite-ன் WAL mode-ஐப் பயன்படுத்தலாம்; இது ஒரு writer தரவைச் சேர்க்கும்போது மற்றவர்கள் படிக்க அனுமதிக்கிறது, மேலும் busy timeout வசதி மற்ற writer-களை பிழை ஏற்படாமல் காத்திருக்கச் செய்கிறது.

ஒரு மாதத்திற்கு LLM அழைப்புகளுக்கு எவ்வளவு செலவாகும்?

ஒவ்வொரு பதிலிலிருந்தும் usage.input_tokens மற்றும் usage.output_tokens ஆகியவற்றை ஒரு அட்டவணையில் பதிவு செய்யுங்கள். பின், உங்கள் வாராந்திர மொத்த எண்ணிக்கையை, உங்கள் model-ன் ஒரு மில்லியன் token-க்கான விலையுடன் பெருக்கிப் பாருங்கள். இதுவே அடுத்த காலாண்டிலும் துல்லியமாக இருக்கும். ஒரு நாளைக்கு ஒருமுறை சிறிய screen-ல் இயக்கும்போது அழைப்புகளின் எண்ணிக்கை குறைவாகவே இருக்கும். குறிப்பின் நீளத்தை நீங்கள் கட்டுப்படுத்தலாம்: சுருக்கத்தை 200 வார்த்தைகளுக்குள் வைப்பது, குறைவான வரிசைகளை அனுப்புவதை விட அதிக சேமிப்பைத் தரும். ஏனெனில், அனைத்து Claude model-களிலும் output token-களின் விலை input token-களை விட அதிகம்.

எனது வேலை வெற்றிகரமாக முடிந்தது, ஆனால் ஏன் புதிய வரிசைகள் எழுதப்படவில்லை?

இதற்கு இரண்டு பொதுவான காரணங்கள் உள்ளன. சந்தை விடுமுறையில் இருந்திருக்கலாம், ஏனெனில் systemd Mon-Fri schedule-ல் பங்குச்சந்தை விடுமுறைகளும் அடங்கும். அல்லது, feed அனைத்து ticker-களுக்கும் காலி தரவுகளை அனுப்பியிருக்கலாம். Client libraries இதை பிழையாகக் கருதாமல் வெறும் எச்சரிக்கையாகவே அச்சிடும், எனவே process 0 என்ற நிலையிலேயே முடிந்து systemd-ல் பச்சை நிறத்தில் காட்டும். SELECT max(day) FROM prices-ஐ கடைசி வர்த்தக நாளுடன் ஒப்பிட்டுப் பார்த்து, அனைத்து ticker-களும் காலியாக இருந்தால், process-ஐ non-zero நிலையில் முடிக்கச் செய்யுங்கள்.

எதை வாங்க வேண்டும் என்று agent முடிவெடுக்க முடியுமா?

முடியாது. அதைச் செய்ய முயற்சிப்பதுதான் இத்தகைய திட்டங்களில் ஏற்படும் தவறு. இந்த model-க்கு சந்தை அணுகல் கிடையாது, உங்கள் முதலீடு அல்லது வரி நிலை குறித்த பார்வை கிடையாது, மேலும் தனது சொந்த எண்களைச் சரிபார்க்கும் வசதியும் கிடையாது. அதிகப்படியான உரைகளைப் படித்து, இன்று எவற்றில் கவனம் செலுத்த வேண்டும் என்று சொல்வதில் மட்டுமே இது சிறந்தது. இது எழுதும் எதுவும் நிதி ஆலோசனை அல்ல; முடிவெடுப்பதும், அதற்கான பொறுப்பும் உங்களிடமே உள்ளது.