VPSలో సొంత స్టాక్ రీసెర్చ్ ఏజెంట్ను నిర్మించడం ఎలా?
VPSలో Python, DuckDB మరియు systemd టైమర్ ఉపయోగించి స్టాక్ రీసెర్చ్ ఏజెంట్ను ఎలా తయారు చేయాలో తెలుసుకోండి. మార్కెట్ డేటా సేకరణ నుండి LLM విశ్లేషణ వరకు పూర్తి గైడ్ ఇక్కడ ఉంది.
Self-hosted స్టాక్ రీసెర్చ్ ఏజెంట్ అంటే ఏమిటి
Self-hosted స్టాక్ రీసెర్చ్ ఏజెంట్ అనేది మీరు సొంతంగా నిర్వహించే సర్వర్లో ఉండే ఒక చిన్న ప్రోగ్రామ్. ఇది నిర్ణీత సమయాల్లో మార్కెట్ డేటాను సేకరించి, స్థానిక డేటాబేస్లో భద్రపరుస్తుంది, దానిపై స్క్రీనింగ్ నిర్వహిస్తుంది మరియు మార్పులను విశ్లేషించి నివేదిక రాయమని ఒక Large Language Model (LLM)ని కోరుతుంది. ఇది కేవలం సమాచారాన్ని చదివి, వడపోస్తుంది. ఇది ట్రేడింగ్ చేయదు, మరియు ఈ గైడ్లో ఉన్న ఏదీ ఆర్థిక సలహా కాదు.
దీనిని మొదటి నుండి నిర్మించే ఇద్దరు వ్యక్తులు వేర్వేరు లైబ్రరీలను ఎంచుకున్నప్పటికీ, చివరికి ఒకే విధమైన నాలుగు భాగాలను కలిగి ఉంటారు: ధరలు మరియు ప్రాథమిక అంశాలను అందించే ఫీడ్, మీరు సేకరించిన ప్రతి అంకెను భద్రపరిచే లోకల్ స్టోర్, టైమర్ ఆధారంగా స్టోర్ను రిఫ్రెష్ చేసే జాబ్, మరియు మిగిలిన డేటాను వాక్యాలుగా మార్చే LLM లేయర్. ఈ గైడ్ Python, DuckDB, systemd టైమర్ మరియు Claude API (application programming interface) ఉపయోగించి ఈ నిర్మాణాన్ని రూపొందిస్తుంది. ఎగ్జిక్యూషన్ అనేది వేరే జాబ్ మరియు దానికి వేరే రకమైన వైఫల్యాలు ఉంటాయి, కాబట్టి అది ట్రేడింగ్ బాట్ల కోసం సెటప్ చేసిన 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 మరియు టోకెన్లు
డిస్క్ విషయానికి వస్తే ఇది చాలా సులభం. ఒక రోజువారీ బార్ అంటే ప్రతి టిక్కర్కు, ప్రతి ట్రేడింగ్ రోజుకు ఒక వరుస (row). ఒక అమెరికన్ ట్రేడింగ్ సంవత్సరంలో సుమారు 252 రోజులు ఉంటాయి.
The data behind this chart
[
{
"label": "20 tickers",
"rows_after_1y": "5,040",
"rows_after_10y": "50,400"
},
{
"label": "100 tickers",
"rows_after_1y": "25,200",
"rows_after_10y": "252,000"
},
{
"label": "500 tickers",
"rows_after_1y": "126,000",
"rows_after_10y": "1,260,000"
}
]ఇరవై టిక్కర్లు ఉంటే, ఒక సంవత్సరం తర్వాత 5,040 వరుసలు ఉంటాయి. 500 tickers లైన్ పది సంవత్సరాల తర్వాత 1,260,000 వరుసలకు చేరుకుంటుంది. ప్రతి వరుసలో ఒక తేదీ మరియు కొన్ని డబుల్స్ (doubles) ఉంటాయి. DuckDB కాలమ్స్ను కంప్రెస్ చేసి నిల్వ చేస్తుంది కాబట్టి, ఇది గిగాబైట్లలో కాకుండా పదుల మెగాబైట్లలోనే ఉంటుంది. ఈ అంచనాను, నా అంచనాతో సహా, పూర్తిగా నమ్మకండి. మీ మొదటి బ్యాక్ఫిల్ తర్వాత du -h /opt/research/data/market.duckdb రన్ చేసి, మీ స్వంత గణాంకాలను ఉపయోగించండి.
చిన్న VPS లలో RAM సమస్యగా మారుతుంది. DuckDB డిఫాల్ట్గా ఒకే క్వెరీ కోసం మెషీన్ మెమరీలో ఎక్కువ భాగాన్ని మరియు అన్ని కోర్లను తీసుకుంటుంది. ఇది అనలిటిక్స్ సర్వర్కు సరైనదే కానీ, ఇతర పనులు కూడా నడిచే 2 GB బాక్స్కు ఇది సరికాదు. మొత్తం ప్రైసెస్ టేబుల్పై ఒక అగ్రిగేషన్ చేసినప్పుడు, ప్రాసెస్ కెర్నల్ ద్వారా కిల్ చేయబడుతుంది. అప్పుడు systemd Main process exited, code=killed, status=9/KILL అని రిపోర్ట్ చేస్తుంది, మరియు journalctl -k లో మెమరీ సరిపోక కిల్ అయినట్లు కనిపిస్తుంది. memory_limit మరియు threads లను స్పష్టంగా సెట్ చేయండి; అప్పుడు క్వెరీ కిల్ అవ్వడానికి బదులుగా నెమ్మదిగా నడుస్తుంది.
టోకెన్లను అంచనా వేయకూడదు, కొలవాలి. ప్రతి Messages API రెస్పాన్స్లో usage ఆబ్జెక్ట్ ఉంటుంది, అందులో input_tokens మరియు output_tokens ఉంటాయి. ప్రతి కాల్ తర్వాత వీటిని ఒక టేబుల్లో రాయండి. ఒక వారం తర్వాత మీకు మీ అసలు వాల్యూమ్ తెలుస్తుంది. మీరు చెక్ చేసే రోజున మీ మోడల్ ధరలతో దీన్ని గుణించండి. ప్రణాళిక వేసుకోవడానికి రెండు విషయాలు స్థిరంగా ఉంటాయి. ప్రతి Claude మోడల్లో అవుట్పుట్ టోకెన్ల ధర ఇన్పుట్ టోకెన్ల కంటే ఎక్కువగా ఉంటుంది. కాబట్టి, మీరు పంపే డేటాను తగ్గించడం కంటే, నోట్ను 200 పదాలకు పరిమితం చేయడం వల్ల బిల్లుపై ఎక్కువ ప్రభావం ఉంటుంది. ప్రాంప్ట్ క్యాషింగ్ (prompt caching) రోజుకు ఒకసారి చేసే పనులకు ఉపయోగపడదు, ఎందుకంటే క్యాష్ లైఫ్టైమ్ నిమిషాల్లోనే ఉంటుంది: తదుపరి రన్ సమయానికి క్యాష్ చేయబడిన బ్లాక్ ఎక్స్పైర్ అవుతుంది, కాబట్టి మీరు మళ్ళీ పూర్తి ఇన్పుట్ ధరను చెల్లించాల్సి ఉంటుంది. ఒకే పెద్ద టెక్స్ట్ బ్లాక్పై ఒకే రన్లో అనేక కాల్స్ చేసినప్పుడు మాత్రమే క్యాషింగ్ వల్ల ప్రయోజనం ఉంటుంది.
అవసరమైన భాగాలను ఇన్స్టాల్ చేయండి
sudo apt update
sudo apt install -y python3-venv
sudo useradd --system --create-home --home-dir /opt/research --shell /usr/sbin/nologin research
sudo -u research python3 -m venv /opt/research/venv
sudo -u research /opt/research/venv/bin/pip install duckdb pandas yfinance anthropic
sudo install -d -o research -g research -m 750 /opt/research/dataఏదైనా కోడ్ రాసే ముందు ఇన్స్టాలేషన్ను తనిఖీ చేయండి:
sudo -u research /opt/research/venv/bin/python -c 'import duckdb, yfinance, anthropic; print("ok")'ఇది ok ను ప్రింట్ చేస్తుంది. మీరు 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) పై ఉన్న ప్రైమరీ కీ, రిఫ్రెష్ ప్రక్రియను సురక్షితంగా మళ్ళీ మళ్ళీ నిర్వహించేలా చేస్తుంది. INSERT OR REPLACE ఆ టిక్కర్ మరియు ఆ రోజుకు ఇప్పటికే ఉన్న అడ్డు వరుసను (row) ఓవర్రైట్ చేస్తుంది, కాబట్టి బ్యాక్ఫిల్ ప్రక్రియను రెండుసార్లు రన్ చేసినా టేబుల్లో డేటా డూప్లికేట్ అవ్వదు. ఈ కీ లేకపోతే, క్రాష్ తర్వాత ఒకసారి మళ్ళీ రన్ చేసినప్పుడు ప్రతి బార్ డూప్లికేట్ అవుతుంది, ఆ తర్వాత మీరు లెక్కించే ప్రతి సగటు విలువ తప్పుగా వస్తుంది, దీని గురించి ఎటువంటి ఎర్రర్ మెసేజ్ కూడా రాదు.
DuckDB ఫైల్ను రైటింగ్ కోసం ఒకే సమయంలో ఒక ప్రాసెస్ మాత్రమే తెరిచి ఉంచడానికి అనుమతిస్తుంది. రెండవ రైటర్ ప్రయత్నించినప్పుడు Could not set lock on file తో విఫలమవుతుంది, దానితో పాటు ఆ ఫైల్ను పట్టుకున్న PID కూడా కనిపిస్తుంది; ఆచరణలో ఇది మీరు మరొక టెర్మినల్లో తెరిచి ఉంచిన ఇంటరాక్టివ్ duckdb షెల్ అయి ఉంటుంది. రీడర్లు read_only=True ని పాస్ చేస్తాయి, అందుకే connect ఆ ఫ్లాగ్ను తీసుకుంటుంది. ఒకే సమయంలో అనేక ప్రాసెస్లు నిజంగా రాయాల్సి వస్తే, అది వేరే ఇంజిన్ పని: SQLite లోని WAL మోడ్, ఒక రైటర్ కమిట్ చేస్తున్నప్పుడు రీడర్లు పనిచేయడానికి అనుమతిస్తుంది, మరియు బిజీ టైమ్అవుట్ కారణంగా ఇతర రైటర్లు విఫలం కాకుండా వేచి ఉంటాయి. సర్వర్ వర్క్లోడ్ కోసం DuckDB మరియు SQLite ల పోలిక ఈ రెండింటిని పోలుస్తుంది, మరియు VPS లో SQLite ను ప్రొడక్షన్లో రన్ చేయడం 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() ఎటువంటి 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 యొక్క సొంత సెషన్ తేదీని అలాగే ఉంచుతుంది, డైలీ బార్ అంటే అదే అర్థం.
మార్కెట్ ముగింపు సమయంలో పనిచేసే టైమర్
అమెరికా మార్కెట్ న్యూయార్క్ సమయం ప్రకారం 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.targetType=oneshot అనేది ఒకటి కంటే ఎక్కువ ExecStartలను అనుమతించే ఏకైక సర్వీస్ రకం. ఇది వాటిని వరుస క్రమంలో రన్ చేస్తుంది, ఏదైనా ఒకటి సున్నా కాని ఎగ్జిట్ కోడ్తో ముగిస్తే ఆగిపోతుంది. మీకు కావలసిన ప్రవర్తన ఇదే: రిఫ్రెష్ విఫలమైతే, పాత డేటాతో స్క్రీన్ అప్డేట్ కాకూడదు. కెర్నల్ అప్డేట్ల కోసం రీబూట్ అయ్యే VPSలో Persistent=true చాలా ముఖ్యం. ఇది లేకపోతే 16:15 గంటలకు రీబూట్ అయినప్పుడు ఆ రన్ కోల్పోతారు; ఇది ఉంటే, మెషీన్ తిరిగి ఆన్ అవ్వగానే జాబ్ రన్ అవుతుంది.
sudo systemctl daemon-reload
sudo systemctl enable --now research-refresh.timer
systemctl list-timers research-refresh.timerlist-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 అందుబాటులో ఉన్న అన్ని అడ్డు వరుసల (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,
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) మాత్రమే ఉపయోగించాలనే సూచనను మీరు నమ్మడం కంటే సరిచూసుకోవాలి: బ్లాక్ నుండి ఒక నిలువు వరుసను (column) తొలగించి, మళ్ళీ రన్ చేసి, అవుట్పుట్ను చదవండి. ఒకవేళ ఆ నిలువు వరుసకు సంబంధించిన సంఖ్య ఇంకా కనిపిస్తే, మోడల్ ఆ ఖాళీని తనంతట తానుగా పూరించినట్లు అర్థం, అంటే మీ ప్రాంప్ట్ సరిగ్గా లేదు. ఆ పరీక్షకు రెండు నిమిషాలు పడుతుంది మరియు ఇది నిజానిజాలను తెలుసుకోవడానికి ఉన్న ఏకైక నిజాయితీ గల మార్గం.
మోడల్ ఎప్పుడూ API key ని చూడదు, database path ని చూడదు మరియు ఏ క్వెరీని రన్ చేయదు. ఇది కేవలం అడ్డు వరుసలను స్వీకరించి, సమాచారాన్ని వచన రూపంలో ఇస్తుంది. ఆ సరిహద్దు వల్లే అవుట్పుట్ను తనిఖీ చేయడం సాధ్యమవుతుంది, ఎందుకంటే నోట్లోని ప్రతి సంఖ్య మీరు పంపిన బ్లాక్లో కూడా ఉండాలి, మీరు వాటిని వరుసల వారీగా సరిపోల్చుకోవచ్చు. దీనికి సంబంధించిన ప్రాంప్టింగ్ అంశాల గురించి మరింత తెలుసుకోవడానికి, finance analysis కోసం Claude ని ఉపయోగించడం అనే అంశం మోడల్ దేనిని బాగా చదవగలదో వివరిస్తుంది.
ఒక రన్, ఎండ్ టు ఎండ్
న్యూయార్క్ సమయం 16:20 గంటలకు టైమర్ సేవను ప్రారంభిస్తుంది. refresh.py చివరిగా నిల్వ చేసిన రోజు నుండి ప్రతి ticker కోసం ఫీడ్ను అడుగుతుంది, ఒక్కో దానికి ఒకటి లేదా రెండు కొత్త bars రాస్తుంది మరియు ప్రతి ticker కు ఒక లైన్ను ప్రింట్ చేస్తుంది. screen.py అదే ఫైల్ను తెరిచి, moving average క్వెరీని రన్ చేస్తుంది, మరియు కొన్ని వరుసలను తిరిగి పొందుతుంది. ఆ వరుసలు కొన్ని వందల tokens ఉన్న టెక్స్ట్ బ్లాక్గా మారుతాయి. ఒక API కాల్ వాటిని చిన్న నోట్గా మారుస్తుంది, ఆ నోట్ జర్నల్లోకి వెళ్తుంది, మరియు ఒక వరుస token కౌంట్లు మరియు hits సంఖ్యతో runs లో చేరుతుంది.
journalctl -u research-refresh.service -n 50 --no-pagerఆరోగ్యకరమైన లాగ్ ప్రతి ticker కు ఒక లైన్ను, ఆపై నోట్ను, ఆ తర్వాత research-refresh.service: Deactivated successfully ను కలిగి ఉంటుంది. ఒక వారం తర్వాత, డేటాబేస్ నుండి మీ స్వంత ఖర్చును తిరిగి చదవండి:
SELECT count(*) AS runs,
sum(input_tokens) AS in_tokens,
sum(output_tokens) AS out_tokens
FROM runs;ఆ మొత్తాలను మీరు చదివిన రోజున మీ మోడల్ పేర్కొన్న మిలియన్ టోకెన్ ధరలతో గుణించండి. ఇది ఇతరుల అంచనాలకు బదులుగా మీకు అసలైన గణాంకాన్ని ఇస్తుంది. ప్రచురించబడిన ధరలు మారుతుంటాయి. కానీ గణితం మారదు.
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 వెలుపల ఉన్నారు. /opt/research/venv/bin/pip ను పూర్తి పాత్ (full path) ద్వారా పిలవండి.
Could not set lock on file, దీని పక్కన PID ఉంటుంది. మరొక ప్రాసెస్ DuckDB ఫైల్ను రైటింగ్ కోసం ఓపెన్ చేసి ఉంచింది, సాధారణంగా మీరు మర్చిపోయిన ఇంటరాక్టివ్ షెల్ ఇది. దాన్ని క్లోజ్ చేయండి, లేదా read_only=True తో రెండో కనెక్షన్ను ఓపెన్ చేయండి.
Main process exited, code=killed, status=9/KILL ఇది systemctl status లో కనిపిస్తుంది. మెమరీ సరిపోక కెర్నల్ ఈ జాబ్ను నిలిపివేసింది. journalctl -k | grep -i oom తో దీన్ని నిర్ధారించుకోండి, ఆపై store.py లో memory_limit విలువను తగ్గించండి.
ఏమీ రాయకుండానే గ్రీన్ రన్ (Green run) పూర్తి కావడం. systemctl status అనేది active (exited) ను చదువుతుంది, కానీ టేబుల్ పెరగలేదు. ఫీడ్ ఖాళీ ఫ్రేమ్లను ఇచ్చింది. ఈ వైఫల్యం చాలా కాలం పాటు బయటపడదు, కాబట్టి ప్రతి టిక్కర్ ఖాళీగా వచ్చినప్పుడు జాబ్ నాన్-జీరో (non-zero) ఎగ్జిట్ కోడ్ను ఇచ్చేలా చేయండి.
మార్కెట్ సెలవు దినాన టైమర్ ట్రిగ్గర్ అవ్వడం. systemd కి ఎక్స్ఛేంజ్ క్యాలెండర్ తెలియదు, కాబట్టి Mon-Fri లో సెలవు దినాలు కూడా ఉంటాయి. రన్ జరుగుతుంది, ఫీడ్లో కొత్త సమాచారం ఉండదు, కాబట్టి జాబ్ దీన్ని ఎర్రర్గా కాకుండా సాధారణ విషయంగా పరిగణించాలి.
API నుండి 429. మీరు రేట్ లిమిట్ను దాటారు. Anthropic SDK స్వయంగా బ్యాక్-ఆఫ్ (backoff) పద్ధతిలో మళ్లీ ప్రయత్నిస్తుంది, మరియు Anthropic(max_retries=5) ప్రయత్నాల సంఖ్యను పెంచుతుంది. ఒకవేళ ప్రతిరోజూ ఇదే వైఫల్యం ఎదురైతే, జాబ్ ఒకేసారి ఎక్కువ అభ్యర్థనలను పంపుతోందని అర్థం.
ఇది ఏమి కాదు
ఇది ఒక పరిశోధనా సహాయక సాధనం. ఒక ఫైలింగ్ను సంగ్రహించే మోడల్ ఆ ఫైలింగ్ యొక్క విశ్లేషణను మాత్రమే అందిస్తుంది. అందులో ఉన్న అంకెలను తప్పుగా అర్థం చేసుకునే అవకాశం ఉంది, కాబట్టి నోట్లో పేర్కొన్న ప్రతి సంఖ్యను మీరు పంపిన డేటాతో సరిచూసుకోవాలి. ఈ అవుట్పుట్ను మీరు స్వయంగా చదవాల్సిన అంశాల జాబితాగా మాత్రమే పరిగణించండి. ఇది ఆర్థిక సలహా కాదు, అలాగే ఇది ఎటువంటి ట్రేడింగ్ సిగ్నల్ కూడా కాదు.
Backtests అనేవి ఆలోచనలను తిరస్కరించడానికి ఉపయోగపడతాయి, కానీ వాటిని ధృవీకరించడంలో అంతగా పనికిరావు. మీ సొంత డేటాపై విఫలమైన వ్యూహం ఖచ్చితంగా పనికిరానిదే. ఒక వ్యూహం మీ డేటాలో పాస్ అయ్యిందంటే, అది కేవలం మీ డేటా పరీక్షను మాత్రమే దాటిందని అర్థం; రాత్రి 1 గంటకు అనిపించినట్లుగా అది గొప్ప విజయం ఏమీ కాదు.
Execution (ఆర్డర్ల అమలు) ఉద్దేశపూర్వకంగానే ఈ పరిధిలోకి రాదు. ఆర్డర్లు మరియు బ్రోకర్ క్రెడెన్షియల్స్ అనేవి కేవలం రీడ్-ఓన్లీ (read only) పరిశోధనా సాధనాల కంటే భిన్నమైన రిస్క్ ప్రొఫైల్ను కలిగి ఉంటాయి. వీటిని కలపడం వల్ల LLM ప్రాంప్ట్ ఉన్న మెషీన్పైనే ట్రేడింగ్ కీలను ఉంచినట్లవుతుంది. మీ సర్వర్లో రన్ చేయదగిన ఇతర అంశాలతో పాటు ఈ పద్ధతి ఎక్కడ సరిపోతుందో తెలుసుకోవాలంటే, రన్ చేయదగిన self-hosted AI ఏజెంట్లు అనే వ్యాసం మీకు పూర్తి అవగాహన కల్పిస్తుంది.
FAQ
నాకు పెయిడ్ మార్కెట్ డేటా ఫీడ్ అవసరమా?
ప్రోటోటైప్ కోసం అవసరం లేదు. సిస్టమ్ ఎలా పనిచేస్తుందో నేర్చుకోవడానికి ఉచిత అనధికారిక ఫీడ్ సరిపోతుంది. అయితే, ఇది ఏ బాధ్యత లేని వెబ్సైట్పై ఆధారపడి ఉంటుంది కాబట్టి, ఎప్పుడైనా ఆగిపోవచ్చు. ఇది సాధారణంగా exception లా కాకుండా ఖాళీ ఫ్రేమ్లుగా వస్తుంది, కాబట్టి మీ జాబ్ రో కౌంట్లను (row counts) తనిఖీ చేయాలి. డేటా ఆధారంగా నిర్ణయాలు తీసుకోవడం ప్రారంభించినప్పుడు, డాక్యుమెంట్ చేయబడిన API మరియు సపోర్ట్ అడ్రస్ ఉన్న పెయిడ్ ఫీడ్కు మారండి. స్టోర్ ఉండటం వల్ల ఈ మార్పు సులభం అవుతుంది: కేవలం fetch ఫంక్షన్ మాత్రమే మారుతుంది, షెడ్యూల్, స్కీమా మరియు స్క్రీన్ అలాగే ఉంటాయి.
ధరలను SQLite లో ఉంచాలా లేక DuckDB లోనా?
DuckDB కాలమ్-ఆధారితమైనది (columnar) మరియు ఎక్కువ రోలను స్కాన్ చేసి అగ్రిగేట్ లెక్కించడానికి రూపొందించబడింది, పదేళ్ల బార్ డేటాపై మూవింగ్ యావరేజ్ లెక్కించడానికి ఇది సరైనది. SQLite రో-ఆధారితమైనది మరియు అనేక ప్రాసెస్ల నుండి ఒకేసారి చిన్న చిన్న రీడ్/రైట్ ఆపరేషన్లు చేయడానికి మెరుగైనది. కొన్ని వందల రోలను యాడ్ చేసి, మిలియన్ల కొద్దీ రోలను స్కాన్ చేసే ఒకే షెడ్యూల్డ్ జాబ్ కోసం DuckDB ఉత్తమం. ఒకే సమయంలో అనేక ప్రాసెస్లు రైట్ చేయాల్సి వస్తే, SQLite లోని WAL మోడ్ ఒక రైటర్ కమిట్ చేస్తున్నప్పుడు రీడర్లను పనిచేయనిస్తుంది, మరియు busy timeout వల్ల ఇతర రైటర్లు ఫెయిల్ అవ్వకుండా వేచి ఉంటాయి.
నెలకు LLM కాల్లకు ఎంత ఖర్చవుతుంది?
ప్రతి రెస్పాన్స్ నుండి usage.input_tokens మరియు usage.output_tokens లను ఒక టేబుల్లోకి లాగ్ చేయండి, ఆపై మీ వారపు మొత్తాలను మీరు తనిఖీ చేసిన రోజున మీ మోడల్ పేర్కొన్న పర్-మిలియన్ టోకెన్ ధరతో గుణించండి. వచ్చే త్రైమాసికంలో కూడా ఖచ్చితంగా ఉండే లెక్క ఇదే. ఒక చిన్న స్క్రీన్పై రోజువారీ రన్ చేసే జాబ్ కోసం తక్కువ కాల్స్ సరిపోతాయి, మరియు నోట్ పొడవును మీరు నియంత్రించవచ్చు: సమ్మరీని 200 పదాలకు పరిమితం చేయడం వల్ల తక్కువ రోలను పంపడం కంటే ఎక్కువ ఆదా అవుతుంది, ఎందుకంటే ప్రతి Claude మోడల్లో అవుట్పుట్ టోకెన్ల ధర ఇన్పుట్ టోకెన్ల కంటే ఎక్కువగా ఉంటుంది.
నా జాబ్ సక్సెస్ అయింది కానీ కొత్త రోలు ఎందుకు రాయలేదు?
దీనికి రెండు సాధారణ కారణాలు ఉన్నాయి. మార్కెట్ మూసి ఉండటం ఒక కారణం, ఎందుకంటే systemd Mon-Fri షెడ్యూల్లో ఎక్స్ఛేంజ్ సెలవులు కూడా ఉంటాయి. లేదా ఫీడ్ ప్రతి టిక్కర్కు ఖాళీ ఫ్రేమ్ను పంపి ఉండవచ్చు, క్లయింట్ లైబ్రరీలు దీనిని exception గా కాకుండా కేవలం ప్రింటెడ్ వార్నింగ్గా చూపిస్తాయి, కాబట్టి ప్రాసెస్ 0 తో ఎగ్జిట్ అవుతుంది మరియు systemd గ్రీన్ స్టేటస్ను చూపిస్తుంది. చివరి ట్రేడింగ్ రోజుతో SELECT max(day) FROM prices ను పోల్చి చూసి వీటి మధ్య తేడాను గుర్తించండి, మరియు ప్రతి టిక్కర్ ఖాళీగా వచ్చినప్పుడు జాబ్ నాన్-జీరో (non-zero) ఎగ్జిట్ కోడ్తో ఆగిపోయేలా చేయండి.
ఏజెంట్ ఏమి కొనాలో నిర్ణయించగలదా?
లేదు, అలా ప్రయత్నించేలా ఏజెంట్ను రూపొందించడమే ఈ ప్రాజెక్టులు విఫలం కావడానికి ప్రధాన కారణం. మోడల్కు మార్కెట్ యాక్సెస్ లేదు, మీ పొజిషన్ లేదా పన్ను పరిస్థితి గురించి అవగాహన లేదు, మరియు దాని లెక్కలను సరిచూసుకోవడానికి మార్గం లేదు. ఇది చేయగలిగినది ఏమిటంటే, ఎక్కువ మొత్తంలో ఉన్న టెక్స్ట్ను చదివి, ఈరోజు ఏ అంశాలు మీ దృష్టిని ఆకర్షించాలో చెప్పడం మాత్రమే. ఇది రాసేది ఏదీ ఆర్థిక సలహా కాదు, నిర్ణయం మరియు దానికి సంబంధించిన బాధ్యత మీదే.