SSD Nodes Learn Hosting plans →

VPSలో సొంత స్టాక్ రీసెర్చ్ ఏజెంట్‌ను నిర్మించడం ఎలా?

VPSలో Python, DuckDB మరియు systemd టైమర్ ఉపయోగించి స్టాక్ రీసెర్చ్ ఏజెంట్‌ను ఎలా తయారు చేయాలో తెలుసుకోండి. మార్కెట్ డేటా సేకరణ నుండి LLM విశ్లేషణ వరకు పూర్తి గైడ్ ఇక్కడ ఉంది.

స్వయంగా హోస్ట్ చేసిన స్టాక్ పరిశోధనా ఏజెంట్ అంటే ఏమిటి

స్వయంగా మీరు నిర్వహించే సర్వర్‌పై నడిచే చిన్న ప్రోగ్రామ్‌నే self-hosted stock research agent అంటారు. ఇది నిర్ణీత వ్యవధిలో market data ను సేకరించి, local database లో నిల్వ చేస్తుంది, దానిపై filtering చేస్తుంది, తరువాత ఏమి మారిందో large language model (LLM) తో వివరణగా రాయిస్తుంది. ఇది data ను చదివి filter చేస్తుంది. ఇది trading చేయదు. ఈ guide లోని సమాచారం financial advice కాదు.

దీన్ని మొదటి నుంచి నిర్మించే ఇద్దరు వ్యక్తులు వేర్వేరు libraries ఎంచుకున్నా, చివరకు ఒకే నాలుగు భాగాలతో కూడిన నిర్మాణానికి వస్తారు: prices మరియు fundamentals అందించే feed, మీరు ఇప్పటివరకు fetch చేసిన ప్రతి row ను నిల్వ చేసే local store, నిర్ణీత సమయానికి store ను refresh చేసే job, మిగిలిన rows ను వాక్యాలుగా మార్చే LLM layer. ఈ guide ఆ నిర్మాణాన్ని Python, DuckDB, systemd timer మరియు Claude API (application programming interface) ఉపయోగించి నిర్మిస్తుంది. ఇది fixed loop కలిగిన పరిమిత agent. మీరు ముందుగా సాధారణ రూపాన్ని అర్థం చేసుకోవాలనుకుంటే, AI agents నేర్చుకునే దశలవారీ మార్గం loop, tools మరియు memory ను క్రమంగా వివరిస్తుంది. ఆ తర్వాతే అవి domain problem గా మారతాయి. Execution అనేది ప్రత్యేక job, దానికి ప్రత్యేక failure modes ఉంటాయి. అందువల్ల దాన్ని trading bots కోసం సిద్ధం చేసిన VPS పై నిర్వహించాలి.

నాలుగు భాగాలు మరియు వాటి పనితీరు

The feed అనేది బయటి ప్రపంచంతో సంభాషించే ఏకైక భాగం. ఇది ticker మరియు తేదీ పరిధిని ఎలా అడగాలో, అలాగే వరుసలను (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 అనేది మీ SQL ఉత్పత్తి చేసిన చిన్న టెక్స్ట్ బ్లాక్‌ను చదివి, దాని సారాంశాన్ని రాస్తుంది. ఇది ఎప్పుడూ database కు కనెక్ట్ అవ్వదు మరియు query ని నిర్మించదు. ఒకవేళ model నే SQL రాస్తే, ఒక తప్పు token ఒక సరళమైన వాక్యంలో తప్పు సంఖ్యగా మారుతుంది, దాన్ని సరిచూసుకోవడానికి ఏమీ ఉండదు. ఒకవేళ SQL సంఖ్యలను ఉత్పత్తి చేస్తే, model కేవలం వివరణలో మాత్రమే తప్పు చేయగలదు, మరియు మీరు పంపిన వరుసలతో ఆ వివరణను సరిచూసుకోవచ్చు.

ల్యాప్‌టాప్‌కు బదులుగా VPSలో ఎందుకు రన్ చేయాలి

దీనికి కారణం షెడ్యూలర్. న్యూయార్క్ సమయం ప్రకారం 16:00 గంటలకు US మార్కెట్ ముగిసినప్పుడు, బెర్లిన్‌లో సమయం 22:00, జకార్తాలో మరుసటి రోజు ఉదయం 04:00 అవుతుంది. ఈ రెండు సమయాల్లోనూ ల్యాప్‌టాప్ నిద్రాణస్థితిలో (asleep) ఉంటుంది. ఒక రన్ మిస్ అవ్వడం వల్ల వచ్చే నష్టం ఒక నోట్ ఆలస్యమవ్వడం కంటే ఎక్కువ: రోజువారీ బార్‌లను సాధారణంగా తర్వాత మళ్ళీ పొందవచ్చు, కానీ సవరించబడే ఏ డేటానైనా తిరిగి పొందలేము, కాబట్టి మీ రికార్డులో ఏర్పడే ఖాళీ శాశ్వతంగా ఉండిపోతుంది. షెడ్యూల్ మరియు నిల్వ చేసిన స్థితి రీబూట్ తర్వాత కూడా కొనసాగాల్సిన ఏ ఏజెంట్‌కైనా ఇదే వర్తిస్తుంది, దీని కోసమే KiroCrewని మీ స్వంత VPSలో ఎల్లప్పుడూ ఆన్‌లో ఉండే ఏజెంట్‌గా రన్ చేయడం రూపొందించబడింది.

రెండవ కారణం చిన్నదైనప్పటికీ వాస్తవమైనది. సర్వర్‌లో ఒకే API key ఉంటుంది, అది ఒకే ఫైల్‌లో ఉంటుంది, లాగిన్ షెల్ లేని ఒకే సిస్టమ్ యూజర్ ఆధీనంలో ఉంటుంది మరియు ఒకే జాబ్ కోసం ఉపయోగించబడుతుంది. మీరు వెబ్ బ్రౌజింగ్ కోసం కూడా ఉపయోగించే ల్యాప్‌టాప్‌లో ఇలాంటి అమరిక చేయడం చాలా కష్టం. కీని కోడ్‌లో మరియు మోడల్ ఇన్‌పుట్‌లో ఉంచకండి, దీని గురించి AI ఏజెంట్ నుండి API కీలను దూరంగా ఉంచడం అనే విభాగంలో వివరించబడింది.

పరిమాణాన్ని నిర్ణయించడం: డిస్క్, RAM మరియు tokens

డిస్క్ భాగం సులభం. రోజుకు ఒక 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 line పది సంవత్సరాల తర్వాత 1,260,000 rows కు చేరుతుంది. ప్రతి row లో ఒక date మరియు కొన్ని doubles ఉంటాయి. DuckDB columns ను compressed గా store చేస్తుంది. అందువల్ల పరిమాణం gigabytes కాకుండా tens of megabytes స్థాయిలో ఉంటుంది. ఈ estimate ను, నాదాన్నీ కూడా, పూర్తిగా నమ్మవద్దు. మొదటి backfill తర్వాత du -h /opt/research/data/market.duckdb అమలు చేసి, మీ స్వంత సంఖ్యను ఉపయోగించండి.

చిన్న VPS లో RAM ప్రధాన పరిమితిగా మారుతుంది. DuckDB default గా ఒక query కోసం machine memory లో పెద్ద భాగాన్ని, అలాగే అన్ని cores ను ఉపయోగిస్తుంది. ఇది analytics server కు సరైనదే. కానీ ఇతర సేవలు కూడా నడుస్తున్న 2 GB box లో ఇది సరైంది కాదు. మొత్తం prices table పై ఒక aggregation చేస్తే kernel process ను terminate చేయవచ్చు. అప్పుడు systemd Main process exited, code=killed, status=9/KILL ను report చేస్తుంది, అలాగే journalctl -k out of memory kill ను చూపిస్తుంది. memory_limit మరియు threads ను స్పష్టంగా set చేస్తే query నెమ్మదిగా నడుస్తుంది, కానీ terminate కాదు.

tokens ను అంచనా వేయకుండా కొలవాలి. ప్రతి Messages API response లో usage object ఉంటుంది. అందులో input_tokens మరియు output_tokens ఉంటాయి. ప్రతి call సమయంలో ఈ రెండింటినీ ఒక table లో రాయండి. ఒక వారం తర్వాత మీ వాస్తవ volume తెలుస్తుంది. మీరు తనిఖీ చేసే రోజున మీ model చూపించే ధరతో ఆ volume ను గుణించండి. ప్రణాళిక కోసం రెండు విషయాలు స్థిరంగా పరిగణించవచ్చు. ప్రతి Claude model లో output tokens ధర input tokens కంటే ఎక్కువగా ఉంటుంది. అందువల్ల note ను 200 words కు పరిమితం చేయడం, పంపే data ను తగ్గించడం కంటే bill ను ఎక్కువగా తగ్గిస్తుంది. రోజుకు ఒకసారి మాత్రమే నడిచే job కు prompt caching ఉపయోగపడదు. Cache default గా 5 minutes పాటు ఉంటుంది. Anthropic అందించే గరిష్ఠ lifetime 1 hour. 5 minute cache కోసం input ను రాయడానికి base input price కు 1.25x చెల్లించాలి. 1 hour cache కోసం 2x చెల్లించాలి. ఏ విధంగానైనా, మరుసటి రోజు run సమయానికి cached block గడువు ముగిసిపోతుంది. అందువల్ల మళ్లీ పూర్తి input price చెల్లించాలి. ఒకే పెద్ద text block పై ఒక run అనేక calls చేసినప్పుడు caching ప్రయోజనకరంగా ఉంటుంది.

అవసరమైన భాగాలను ఇన్‌స్టాల్ చేయండి

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

ఏదైనా కోడ్ రాసే ముందు ఇన్‌స్టాలేషన్‌ను తనిఖీ చేయండి:

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

ఇది ok ను ప్రింట్ చేస్తుంది. మీరు virtual environment ను దాటవేసి, సిస్టమ్ Python పై pip install ను రన్ చేస్తే, Ubuntu 24.04 మిమ్మల్ని error: externally-managed-environment తో ఆపుతుంది. ఎందుకంటే ఆ డిస్ట్రిబ్యూషన్ /usr/lib/python3 ను కలిగి ఉంటుంది మరియు అక్కడ pip ద్వారా రాయడానికి అనుమతించదు. venv అనేది కేవలం మర్యాద కోసం కాదు. pip రాయడానికి అనుమతి ఉన్న ఏకైక డైరెక్టరీ అదే.

API కీని సర్వీస్ యూజర్ మాత్రమే చదవగలిగేలా, ఇతరులెవరూ చూడలేని ఫైల్‌లో ఉంచండి:

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

అందులో ఒకే లైన్ రాయండి, కోట్స్ మరియు export లేకుండా. ఎందుకంటే systemd ఈ ఫైల్‌ను షెల్‌కు పంపకుండా, స్వయంగా పార్స్ చేస్తుంది:

ANTHROPIC_API_KEY=sk-ant-your-key-here

స్టోర్: రెండు పట్టికలు

# /opt/research/store.py
import duckdb

DB = '/opt/research/data/market.duckdb'

SCHEMA = [
    """
    CREATE TABLE IF NOT EXISTS prices (
      ticker VARCHAR,
      day    DATE,
      open   DOUBLE,
      high   DOUBLE,
      low    DOUBLE,
      close  DOUBLE,
      volume BIGINT,
      PRIMARY KEY (ticker, day)
    )
    """,
    """
    CREATE TABLE IF NOT EXISTS runs (
      started_at    TIMESTAMPTZ,
      model         VARCHAR,
      input_tokens  BIGINT,
      output_tokens BIGINT,
      hits          BIGINT
    )
    """,
]

def connect(read_only=False):
    con = duckdb.connect(DB, read_only=read_only)
    con.execute("SET memory_limit='512MB'")
    con.execute('SET threads=2')
    if not read_only:
        for statement in SCHEMA:
            con.execute(statement)
    return con

(ticker, day) పై ఉన్న primary key వల్ల refresh ను సురక్షితంగా మళ్లీ అమలు చేయవచ్చు. ఆ ticker మరియు రోజుకు ఇప్పటికే ఉన్న row ను INSERT OR REPLACE overwrite చేస్తుంది. అందువల్ల backfill ను రెండుసార్లు అమలు చేసినా పట్టికలో duplicate rows చేరవు. ఈ key లేకపోతే crash తర్వాత మరోసారి అమలు చేసినప్పుడు ప్రతి bar నిశ్శబ్దంగా duplicate అవుతుంది. తరువాత మీరు లెక్కించే ప్రతి average తప్పుగా ఉంటుంది. సమస్యను తెలియజేసే error message కూడా ఎక్కడా కనిపించదు.

DuckDB ఫైల్‌ను writing కోసం ఒకేసారి ఒక process మాత్రమే open గా ఉంచడానికి అనుమతిస్తుంది. రెండవ writer వెంటనే Could not set lock on file తో fail అవుతుంది. దాని తరువాత ఆ ఫైల్‌ను పట్టుకున్న PID కనిపిస్తుంది. సాధారణంగా అది మీరు మరో terminal లో open గా వదిలిన interactive duckdb shell అయి ఉంటుంది. Read-only connection ఉపయోగించినా ఈ పరిమితి తొలగదు. DuckDB concurrency documentation రెండు modes ను వివరిస్తుంది: ఒక process read మరియు write రెండూ చేయడం, లేదా అనేక processes read మాత్రమే చేయడం. ఈ రెండింటి మిశ్రమాన్ని అది అనుమతించదు. అనేక readers ఫైల్‌ను share చేయగలిగేలా connect కు read_only flag ఉంటుంది. అయితే ఫైల్‌ను writer open చేసి ఉంచని సమయంలో మాత్రమే ఇది పనిచేస్తుంది. అనేక processes నిజంగా ఒకే సమయంలో write చేయాల్సి ఉంటే, అది వేరే engine ఉపయోగించాల్సిన సందర్భం. SQLite లో WAL mode ఉపయోగిస్తే ఒక writer commit చేస్తున్న సమయంలో readers పనిచేయగలవు. busy timeout వల్ల ఇతర writers fail కాకుండా వేచి ఉంటాయి. server workload కోసం DuckDB మరియు SQLite పోలిక ఈ రెండింటిని పోలుస్తుంది. VPS లో production కోసం SQLite నడపడం ద్వారా WAL సరిగా పనిచేయడానికి అవసరమైన settings గురించి తెలుసుకోవచ్చు.

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() ఎటువంటి error ను చూపదు. ఇది ఆ symbol బహుశా delist అయి ఉండవచ్చు లేదా ధర డేటా దొరకలేదని ఒక warning ను ప్రింట్ చేస్తుంది (ఈ వాక్యం లైబ్రరీ వెర్షన్లను బట్టి మారుతుంటుంది) మరియు ఖాళీ DataFrame ను రిటర్న్ చేస్తుంది. ఆ చెక్ లేని job ఏమీ రాయకుండానే 0 తో exit అవుతుంది, అప్పుడు systemd ఆ run విజయవంతమైందని (green) చూపిస్తుంది, కానీ టేబుల్ మాత్రం పెరగడం ఆగిపోతుంది. ప్రతిరోజూ ఒకే రకమైన వరుసలను చూపే స్క్రీన్ ద్వారా, మీరు వారాల తర్వాత ఈ విషయాన్ని గుర్తిస్తారు.

timestamp నిర్వహణకు కూడా ఒక కారణం ఉంది. feed రిటర్న్ చేసే index లో exchange యొక్క timezone ఉండవచ్చు, మరియు offset ను తొలగించడం అంటే UTC కి మార్చడం కాదు. స్థానిక సమయం అర్ధరాత్రి ఉన్న టోక్యో సెషన్, UTC లోకి మారినప్పుడు మునుపటి క్యాలెండర్ రోజుకు మారుతుంది. కాబట్టి, UTC కి మార్చడం వల్ల ప్రతి జపనీస్ బార్ ఒక రోజు వెనక్కి జరిగి primary key దెబ్బతింటుంది. tz_localize(None) ఆ exchange యొక్క సొంత సెషన్ తేదీని అలాగే ఉంచుతుంది, డైలీ బార్ అంటే అదే అర్థం.

మార్కెట్ ముగిసే సమయానికి అమలయ్యే timer

US market New York సమయం ప్రకారం 16:00కు ముగుస్తుంది. చివరి trades స్థిరపడటానికి కొన్ని నిమిషాలు పడుతుంది. అందువల్ల job 16:20కు అమలవుతుంది. ఈ సమయాన్ని UTCలో కాకుండా New York సమయంగా రాయండి. శీతాకాలంలో New York, UTC కంటే 5 గంటలు వెనుక ఉంటుంది. వేసవిలో 4 గంటలు వెనుక ఉంటుంది. అందువల్ల fixed UTC hourతో రాసిన timer సంవత్సరానికి రెండుసార్లు ఒక గంట తేడాతో మారుతుంది. అప్పుడు అది market close కంటే ముందే అమలవుతుంది. systemd 235 మరియు తరువాతి versions OnCalendarలో timezoneను నేరుగా స్వీకరిస్తాయి. Ubuntu 24.04లో version 255 అందుబాటులో ఉంది. మీ versionను 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

ఒకటి కంటే ఎక్కువ ExecStartలను స్వీకరించే ఏకైక service type Type=oneshot. ఇది వాటిని క్రమంగా అమలు చేస్తుంది. వాటిలో ఏదైనా non zeroతో ముగిస్తే ఆపివేస్తుంది. మీకు కావాల్సిన ప్రవర్తన ఇదే. విఫలమైన refresh తర్వాత stale data చూపించే పని జరగకూడదు. Kernel updates కోసం reboot అయ్యే VPSలో Persistent=true ముఖ్యమైనది. దీన్ని లేకుండా 16:15కు reboot చేస్తే ఆ run పూర్తిగా కోల్పోతారు. దీన్ని ఉపయోగిస్తే machine తిరిగి అందుబాటులోకి వచ్చిన వెంటనే job అమలవుతుంది. Agent నిరంతరం నడుస్తూ, ఒక్కసారి అమలై ముగియకుండా ఉండాలి అంటే దీనికి విరుద్ధమైన unit నిర్మాణం అవసరం. అందులో Restart= నియమాలతో long lived process ఉంటుంది. Timer ఉండదు. VPSలో systemdతో headless dshను నడపడం ఈ విధానాన్ని వివరిస్తుంది.

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

list-timersలో తదుపరి weekday runను server యొక్క స్వంత local timeకు మార్చి చూపించే NEXT column ఉండాలి. జాబితా ఖాళీగా ఉంటే timer enabled కాలేదని లేదా unitలో [Install] section లేదని అర్థం. అందువల్ల enableకు timers.targetలోకి link చేయడానికి ఏదీ లేదు.

స్క్రీన్: ముందుగా 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 అందుబాటులో ఉన్న అన్ని అడ్డు వరుసల (rows) సగటును లెక్కిస్తుంది, కాబట్టి ఒక టిక్కర్ యొక్క మూడవ వరుస మూడు రోజుల సగటును తీసుకుని దానిని 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,
    thinking={'type': 'disabled'},
    system=SYSTEM,
    messages=[{'role': 'user', 'content': 'Screen hits, ticker / day / close / ma20 / ma50:\n' + block}],
)

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

ఈ మోడల్‌లో thinking లైన్ ముఖ్యమైనది. Claude Sonnet 5 డిఫాల్ట్‌గా ఆలోచిస్తుంది. దాని thinking tokens output గా billing అవుతాయి మరియు max_tokens లో లెక్కించబడతాయి. అవి text కు ముందు ఒక block గా తిరిగి వస్తాయి. ఆ లైన్ లేకపోతే, resp.content[0] thinking block కావచ్చు మరియు .text lookup విఫలమవుతుంది. Claude Sonnet 5 ఇప్పుడు legacy model. దాని successor, claude-sonnet-5-5, disabled ను తిరస్కరిస్తుంది. అక్కడ ఉపయోగించాల్సిన setting thinking={'type': 'between_tools'}. ఇది సమాధానానికి ముందు thinking ను ఆపివేస్తుంది.

system prompt లోని word limit ఖరీదైన bill భాగానికి పరిమితి విధిస్తుంది. ఇచ్చిన rows మాత్రమే ఉపయోగించాలనే instruction ను నమ్మకుండా మీరు verify చేయాలి. block నుంచి ఒక column తొలగించి, మళ్లీ run చేసి, output చదవండి. ఆ column కు సంబంధించిన figure ఇంకా కనిపిస్తే, model ఆ లోటును స్వయంగా పూరించింది. అంటే మీ prompt తగినంత స్పష్టంగా లేదు. ఈ test కు రెండు నిమిషాలు పడుతుంది. విషయాన్ని నిజాయితీగా తెలుసుకునే ఏకైక మార్గం ఇదే.

మోడల్ API key ను చూడదు. database path ను చూడదు. query ను run చేయదు. దానికి rows అందుతాయి, అది prose ను తిరిగి ఇస్తుంది. ఈ పరిమితి output ను తనిఖీ చేయగలిగేలా చేస్తుంది. ఎందుకంటే note లోని ప్రతి number మీరు పంపిన block లో కూడా ఉండాలి. వాటిని line by line గా పోల్చవచ్చు. వెబ్ నుంచి తీసుకున్న pages ను ఈ input లో చేర్చిన క్షణం, ఈ పరిమితి స్వరూపం మారుతుంది. headline లేదా forum post మీకు అందించాల్సిన facts కు బదులుగా మోడల్‌ను లక్ష్యంగా చేసుకున్న instructions ను కలిగి ఉండవచ్చు. agent కు SearXNG search backend అందించడం తెరిచే attack surface ఇదే. Prompting గురించి మరింత సమాచారం కోసం, finance analysis కోసం Claude ను ఉపయోగించడం మోడల్ ఏ విషయాలను బాగా చదవగలదో మరింత వివరిస్తుంది.

ఒకసారి నడిపితే, ప్రారంభం నుంచి ముగింపు వరకు

New York సమయం ప్రకారం 16:20కి timer service ను ప్రారంభిస్తుంది. refresh.py చివరిగా నిల్వ చేసిన రోజు నుంచి ప్రారంభించి ప్రతి ticker కోసం feed ను అడుగుతుంది, ప్రతి ticker కు ఒకటి లేదా రెండు కొత్త bars ను రాస్తుంది, అలాగే ప్రతి ticker కు ఒక line ను ముద్రిస్తుంది. screen.py అదే file ను తెరిచి, moving average query ను అమలు చేసి, కొన్ని rows ను తిరిగి పొందుతుంది. ఆ rows కొన్ని వందల tokens ఉన్న text block గా మారతాయి. ఒక API call వాటిని చిన్న note గా మారుస్తుంది. ఆ note journal కు వెళుతుంది. token counts మరియు hits సంఖ్యతో ఒక row runs లో చేరుతుంది.

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

సరిగ్గా పనిచేసే log లో ప్రతి ticker కు ఒక line, తరువాత note, ఆ తరువాత research-refresh.service: Deactivated successfully ఉంటాయి. Server పై Claude Code session నుంచి మీరు ఆ log ను చదువుతుండగా, మరో session screen query పై పనిచేస్తే, terminals మధ్య lines ను మీరు copy చేయాల్సిన అవసరం లేకుండా రెండూ పరస్పరం text ను పంపుకోవచ్చు. ఒక వారం తరువాత, database నుంచి మీ ఖర్చును తిరిగి పొందండి:

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

మీరు వాటిని చదివే రోజున మీ model చూపించే ప్రతి million tokens ధరలతో ఆ totals ను గుణించండి. అప్పుడు ఇతరుల అంచనా కాకుండా మీ వాస్తవ మొత్తం ఖర్చు తెలుస్తుంది. ప్రచురించిన ధరలు మారుతాయి. లెక్కింపు మారదు.

Backtests ఎందుకు overfit అవుతాయి మరియు దానిని ఎలా గమనించాలి

రెండు window lengths ఆధారంగా screen ను ఒక function గా మార్చండి, జతల గ్రిడ్ (grid of pairs) ను sweep చేయండి, మరియు వాటిని return ఆధారంగా rank చేయండి. అత్యుత్తమ జత అద్భుతంగా కనిపిస్తుంది. అదే సమస్య, ఫలితం కాదు. 200 జతల గ్రిడ్ అంటే 200 ప్రయోగాలు, మీరు అందులో అత్యంత అదృష్టవంతమైన దానిని ఎంచుకున్నారు.

ఇది ఎలా జరుగుతుందో మీరు పది నిమిషాల్లో చూడవచ్చు. డేటా స్టోర్‌ను తేదీ ఆధారంగా రెండుగా విభజించండి. మొదటి సగంలో మాత్రమే గ్రిడ్‌ను sweep చేసి విజేతను నమోదు చేయండి. అదే గ్రిడ్‌ను రెండో సగంలో sweep చేయండి. రెండు విజేతలు చాలా భిన్నంగా ఉంటే, ఆ parameters కేవలం noise ను fit చేస్తున్నాయని అర్థం. మీరు tune చేసిన సగంలో మాత్రమే గెలిచిన జత, భవిష్యత్తు గురించి ఏమీ చెప్పదు.

Survivorship అనేది overfitting కంటే ప్రమాదకరమైనది, ఎందుకంటే tuning తో దీనిని సరిచేయలేము. మీ ticker జాబితా నేటి ఇండెక్స్ సభ్యులది, కాబట్టి ఇందులో మనుగడ సాగించిన కంపెనీలు మాత్రమే ఉంటాయి. 2019లో delist అయిన ticker కోసం feed ను అడిగితే, అది ఖాళీ frame ను ఇస్తుంది. అంటే ఆ కంపెనీ మీ స్టోర్‌లోకి లేదా మీ test లోకి ఎప్పటికీ రాదు. మీరు చేసే ప్రతి backtest ఇప్పటికే విఫలమైన వాటిని మినహాయించింది.

Restated fundamentals కాలక్రమాన్ని దెబ్బతీస్తాయి. 2019 త్రైమాసికం కోసం API నేడు ఇచ్చే revenue సంఖ్య, 2019లో ప్రచురించబడిన సంఖ్య కాకపోవచ్చు. నేటి fundamentals ను 2019 ధరలతో కలిపే screen, అప్పట్లో లేని సమాచారాన్ని వాడుతోంది. ధరలు సాధారణంగా ఇక్కడ సురక్షితం, కానీ fundamentals అలా ఉండవు.

Adjusted prices మీ అంచనాలను మారుస్తాయి. auto_adjust=True తో, closes అన్నీ dividends మరియు splits కోసం వెనుకకు సర్దుబాటు చేయబడతాయి. కాబట్టి వచ్చే నెలలో అదే query రన్ చేస్తే కొద్దిగా భిన్నమైన చరిత్ర వస్తుంది. మీరు వాస్తవానికి ఉపయోగించిన rows ను భద్రపరచడం వల్లే ఫలితం పునరుత్పత్తి (reproducible) అవుతుంది, అందుకే local store అవసరం.

Backtest కమీషన్లను మరియు slippage ను విస్మరిస్తుంది, అలాగే మీ order ధరను మార్చదని భావిస్తుంది. ఇవి execution కు సంబంధించినవి, ఇవి ఇక్కడ పరిధిలో లేవు మరియు running trading bots on a VPS లో వివరించబడ్డాయి.

విఫలమయ్యే పరిస్థితులు మరియు మీరు చూసే సందేశాలు

error: externally-managed-environment కనిపించినప్పుడు pip నడుస్తుంది. మీరు virtual environment వెలుపల ఉన్నారు. పూర్తి path ఉపయోగించి /opt/research/venv/bin/pip ను అమలు చేయండి.

PID తర్వాత కనిపించే Could not set lock on file సందేశం. మరొక process DuckDB file ను writing కోసం open చేసి ఉంచింది. సాధారణంగా మీరు మూసివేయడం మర్చిపోయిన interactive shell కారణం. దాన్ని మూసివేయండి. ఆ writer ఇంకా openగా ఉన్నప్పుడు రెండవ connection ను read_only=True తో open చేయడం సహాయపడదు. DuckDB ఒకేసారి అనేక processes ను చదవడానికి లేదా ఒక process ను రాయడానికి మాత్రమే అనుమతిస్తుంది. రెండింటినీ ఒకేసారి అనుమతించదు.

systemctl status లో కనిపించే Main process exited, code=killed, status=9/KILL సందేశం. Kernel memory కొరత కారణంగా job ను terminate చేసింది. journalctl -k | grep -i oom తో నిర్ధారించండి. తరువాత store.py లోని memory_limit విలువను తగ్గించండి.

ఏమీ రాయకుండానే విజయవంతంగా పూర్తయ్యే run. systemctl status, active (exited) ను చదువుతుంది. కానీ table పరిమాణం పెరగదు. Feed ఖాళీ frames ను తిరిగి ఇచ్చింది. ఈ సమస్య ఎక్కువకాలం గుర్తించబడకుండా ఉండవచ్చు. అందువల్ల ప్రతి ticker కు ఖాళీ ఫలితం వచ్చినప్పుడు job non-zero status తో ముగిసేలా చేయండి.

Market holiday రోజున timer అమలైంది. systemd కు exchange calendar తెలియదు. అందువల్ల Mon-Fri లో holidays కూడా ఉంటాయి. Run జరుగుతుంది, feed లో కొత్త data ఉండదు. Job దీన్ని error గా కాకుండా సాధారణ పరిస్థితిగా పరిగణించాలి.

API నుంచి 429. మీరు rate limit ను మించిపోయారు. Anthropic SDK స్వయంగా backoff తో retries చేస్తుంది. Anthropic(max_retries=5) ప్రయత్నాల సంఖ్యను పెంచుతుంది. అయినా ప్రతి రోజు విఫలమైతే, job ఒకే burst లో అధిక data ను అభ్యర్థిస్తోంది.

ఇది ఏమి కాదు

ఇది ఒక పరిశోధనా సహాయక సాధనం. ఒక ఫైలింగ్‌ను సంగ్రహించే మోడల్ ఆ ఫైలింగ్ యొక్క విశ్లేషణను మాత్రమే అందిస్తుంది. అందులో ఉన్న అంకెలను తప్పుగా అర్థం చేసుకునే అవకాశం ఉంది, కాబట్టి నోట్‌లో పేర్కొన్న ప్రతి సంఖ్యను మీరు పంపిన డేటాతో సరిచూసుకోవాలి. ఈ అవుట్‌పుట్‌ను మీరు స్వయంగా చదవాల్సిన అంశాల జాబితాగా మాత్రమే పరిగణించండి. ఇది ఆర్థిక సలహా కాదు, అలాగే ఇది ఎటువంటి ట్రేడింగ్ సిగ్నల్ కూడా కాదు.

Backtests అనేవి ఆలోచనలను తిరస్కరించడానికి ఉపయోగపడతాయి, కానీ వాటిని ధృవీకరించడంలో అంతగా పనికిరావు. మీ సొంత డేటాపై విఫలమైన వ్యూహం ఖచ్చితంగా పనికిరానిదే. ఒక వ్యూహం మీ డేటాలో పాస్ అయ్యిందంటే, అది కేవలం మీ డేటా పరీక్షను మాత్రమే దాటిందని అర్థం; రాత్రి 1 గంటకు అనిపించినట్లుగా అది గొప్ప విజయం ఏమీ కాదు.

Execution (ఆర్డర్ల అమలు) ఉద్దేశపూర్వకంగానే ఈ పరిధిలోకి రాదు. ఆర్డర్లు మరియు బ్రోకర్ క్రెడెన్షియల్స్ అనేవి కేవలం రీడ్-ఓన్లీ (read only) పరిశోధనా సాధనాల కంటే భిన్నమైన రిస్క్ ప్రొఫైల్‌ను కలిగి ఉంటాయి. వీటిని కలపడం వల్ల LLM ప్రాంప్ట్ ఉన్న మెషీన్‌పైనే ట్రేడింగ్ కీలను ఉంచినట్లవుతుంది. మీ సర్వర్‌లో రన్ చేయదగిన ఇతర అంశాలతో పాటు ఈ పద్ధతి ఎక్కడ సరిపోతుందో తెలుసుకోవాలంటే, రన్ చేయదగిన self-hosted AI ఏజెంట్లు అనే వ్యాసం మీకు పూర్తి అవగాహన కల్పిస్తుంది.

FAQ

Prototype కోసం అవసరం లేదు. వ్యవస్థ నిర్మాణాన్ని అర్థం చేసుకోవడానికి ఉచిత unofficial feed సరిపోతుంది. అయితే అది పనిచేయడం ఆపుతుంది, ఎందుకంటే మీకు ఎలాంటి హామీ ఇవ్వని websiteపై అది ఆధారపడి ఉంటుంది. సాధారణంగా అది exception రూపంలో కాకుండా empty frames రూపంలో విఫలమవుతుంది. అందువల్ల మీ job తప్పనిసరిగా row counts తనిఖీ చేయాలి. Data ఆధారంగా నిర్ణయం తీసుకోవడం ప్రారంభమైన తర్వాత documented API మరియు support address ఉన్న paid feed కు మారండి. ఈ మార్పును చవకగా చేసేది store: fetch function మాత్రమే మారుతుంది; schedule, schema, screen మాత్రం అలాగే ఉంటాయి.

Prices ను SQLite లేదా DuckDB లో ఉంచాలా?

DuckDB columnar విధానాన్ని ఉపయోగిస్తుంది. Aggregate లెక్కించడానికి అనేక rows ను scan చేయడానికి ఇది రూపొందించబడింది. పది సంవత్సరాల bars పై moving average లెక్కించడం దీనికి సరైన ఉదాహరణ. SQLite row oriented గా ఉంటుంది. ఒకేసారి అనేక processes నుంచి జరిగే అనేక చిన్న reads మరియు writes కు ఇది మెరుగ్గా సరిపోతుంది. ఒక scheduled job కొన్ని వందల rows ను append చేసి, తరువాత millions of rows ను scan చేస్తే, DuckDB మెరుగైన ఎంపిక. ఒకేసారి అనేక processes write చేయాల్సి వస్తే, WAL mode లోని SQLite ఒక writer commit చేస్తున్న సమయంలో readers పనిచేయడానికి అనుమతిస్తుంది. busy timeout వల్ల ఇతర writers వెంటనే విఫలమవకుండా వేచి ఉంటాయి.

LLM calls కు నెలకు ఎంత ఖర్చవుతుంది?

ప్రతి response నుంచి usage.input_tokens మరియు usage.output_tokens ను table లోకి log చేయండి. తరువాత మీరు పరిశీలించే రోజున మీ model చూపించే per million token ధరతో weekly totals ను గుణించండి. వచ్చే quarter లో కూడా ఖచ్చితంగా ఉండే ఏకైక సంఖ్య అదే. చిన్న screen పై ప్రతిరోజు ఒక run చేస్తే calls సంఖ్య తక్కువగా ఉంటుంది. Note పొడవు మాత్రం మీ నియంత్రణలో ఉంటుంది. Summary ను 200 words కు పరిమితం చేయడం, తక్కువ rows పంపడం కంటే ఎక్కువ ఆదా చేస్తుంది. కారణం, ప్రతి Claude model లో output tokens ధర input tokens కంటే ఎక్కువగా ఉంటుంది.

నా job విజయవంతమైనా కొత్త rows ఎందుకు రాయలేదు?

సాధారణంగా రెండు కారణాలు ఉంటాయి. Exchange holidays ను systemd Mon-Fri schedule లో చేర్చినందున market మూసి ఉండవచ్చు. లేదా feed ప్రతి ticker కు empty frame ను తిరిగి ఇచ్చి ఉండవచ్చు. Client libraries దీన్ని తరచుగా exception గా కాకుండా printed warning గా చూపిస్తాయి. అందువల్ల process ఇప్పటికీ 0 తో exit అవుతుంది, systemd కూడా run ను green గా చూపిస్తుంది. SELECT max(day) FROM prices ను చివరి నిజమైన trading day తో పోల్చి రెండింటిని వేరు చేయండి. ప్రతి ticker ఖాళీగా తిరిగి వచ్చినప్పుడు job non zero తో exit అయ్యేలా చేయండి.

Agent ఏమి కొనాలో నిర్ణయించగలదా?

లేదు. Agent అలా ప్రయత్నించేలా నిర్మించడమే ఈ projects విఫలమయ్యే ప్రధాన కారణం. Model కు market access ఉండదు. మీ position లేదా tax situation గురించి దానికి తెలియదు. తన numbers ను ఏదైనా వాస్తవ ఆధారంతో తనిఖీ చేసుకునే మార్గం కూడా దానికి ఉండదు. పెద్ద మొత్తంలో text చదివి, ఈరోజు మీ దృష్టికి అర్హమైన కొన్ని అంశాలను చెప్పడంలో అది ఉపయోగకరం. అది రాసేది ఏదీ financial advice కాదు. నిర్ణయం, దానికి సంబంధించిన బాధ్యత కూడా మీ వద్దే ఉంటాయి.

ఈ వ్యాసం ఉపయోగకరంగా ఉందా?