SSD Nodes Learn Hosting plans →
Mga Gabay Matt ConnorNi Matt Connor · Na-update 2026-08-13

Paano Gumawa ng Stock Research Agent sa VPS

Alamin ang setup ng self-hosted stock research agent gamit ang Python, DuckDB, systemd timer, market data feed, at Claude API para sa scheduled screening.

Ano ang self-hosted stock research agent

Ang self-hosted stock research agent ay isang maliit na program na tumatakbo sa server na pagmamay-ari mo. Kumukuha ito ng market data ayon sa iskedyul, iniimbak ito sa local database, nagsasagawa ng screening dito, at gumagamit ng large language model (LLM) upang isulat kung ano ang nagbago. Nagbabasa at nagsasala lamang ito. Hindi ito nagta-trade, at walang bahagi ng gabay na ito ang maituturing na financial advice.

Dalawang taong bumuo nito mula sa simula ay maaaring pumili ng magkaibang library ngunit magtatapos pa rin sa parehong apat na bahagi: isang feed na nagbibigay ng prices at fundamentals, isang local store na nag-iingat ng bawat row na nakuhanan mo, isang job na nagre-refresh sa store ayon sa timer, at isang LLM layer na ginagawang mga pangungusap ang natitirang rows. Binubuo ng gabay na ito ang ganitong setup gamit ang Python, DuckDB, systemd timer, at Claude API (application programming interface). Hiwalay na job ang execution at may sarili itong mga failure mode. Mas angkop itong ilagay sa isang VPS na naka-setup para sa trading bots.

Ang apat na bahagi at ang ginagawa ng bawat isa

Ang feed ang tanging bahaging nakikipag-ugnayan sa labas na mundo. Alam nito kung paano humingi ng ticker at date range, at kung paano magbalik ng mga row. Lahat ng downstream component ay nagbabasa mula sa database mo sa halip na sa feed, kaya ang pagkawala ng feed ay magdudulot lamang ng isang araw na walang bagong data, hindi ng sirang screen.

Ang store ang pangunahing layunin ng buong proseso. Karaniwang maaaring kunin muli sa ibang pagkakataon ang daily close na hindi mo naitala. Ngunit hindi na karaniwang maibabalik ang intraday quote, estimate bago ito baguhin, o fundamentals figure bago ito i-restatement. Sa pamamagitan ng store, nakakabuo ka ng tala ng aktuwal na sinabi ng data noong araw na iyon.

Ang scheduler ang nagpapasya kung kailan magaganap ang refresh. Sa isang server, systemd timer ito. Ito ang dahilan kung bakit mas mahalaga rito ang VPS kaysa sa code.

Ang LLM layer ay nagbabasa ng maikling text block na ginawa ng iyong SQL at sumusulat ng buod nito. Hindi ito kailanman kumokonekta sa database at hindi rin ito bumubuo ng query. Kung ang model ang sumusulat ng SQL, ang isang maling token ay maaaring maging maling numero sa loob ng maayos na pangungusap, nang walang maihahambing na reference. Kung SQL ang gumagawa ng mga numero, ang mali lamang na magagawa ng model ay ang prose, at maaari mong suriin ang prose laban sa mga row na ipinadala mo.

Bakit patakbuhin ito sa VPS sa halip na sa laptop

Ang scheduler ang dahilan. Ang pagsasara ng US market sa 16:00 New York time ay 22:00 sa Berlin at 04:00 kinabukasan sa Jakarta. Naka-sleep ang laptop sa dalawang oras na iyon. Mas magastos ang isang missed run kaysa sa late na abiso: karaniwang maaaring i-fetch muli sa ibang pagkakataon ang daily bars, pero hindi na maibabalik ang anumang na-revise, kaya permanente ang puwang sa iyong record. Pareho ang batayan para sa anumang agent na kailangang manatili ang schedule at stored state nito kahit mag-reboot, na siyang layunin ng pagpapatakbo ng KiroCrew bilang always-on agent sa sarili mong VPS.

Mas maliit ang ikalawang dahilan, pero mahalaga pa rin. Naglalaman ang server ng isang API key sa isang file, na pagmamay-ari ng isang system user na walang login shell at ginagamit ng isang job. Mas mahirap ayusin ang ganitong setup sa laptop na ginagamit mo rin sa pag-browse sa web. Panatilihing wala ang key sa code at sa input ng model. Ito ang paksa ng pag-iwas na mailagay ang API keys sa AI agent.

Pagsusukat: disk, RAM, at tokens

Madali ang disk. Ang isang daily bar ay isang row para sa bawat ticker sa bawat trading day, at ang isang trading year sa US ay humigit-kumulang 252 araw.

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"
  }
]

Ang dalawampung ticker ay magiging 5,040 rows pagkalipas ng isang taon. Aabot ang line na 500 tickers sa 1,260,000 rows pagkalipas ng sampung taon. Ang bawat row ay may date at ilang double, at compressed ang pag-store ng DuckDB sa mga column, kaya sampu-sampung megabytes ito sa halip na gigabytes. Huwag pagtiwalaan ang estimate na ito, pati ang sa akin. Patakbuhin ang du -h /opt/research/data/market.duckdb pagkatapos ng unang backfill at gamitin ang sarili mong resulta.

Dito nagiging problema ang RAM sa isang maliit na VPS. Bilang default, kumukuha ang DuckDB ng malaking bahagi ng memory ng machine at ng lahat ng core nito para sa isang query. Tama ito sa isang analytics server, pero hindi sa isang 2 GB box na nagpapatakbo rin ng ibang workload. Kapag nagpatakbo ka ng isang aggregation sa buong prices table, maaaring patayin ng kernel ang process. Magrereport ang systemd ng Main process exited, code=killed, status=9/KILL, habang ipapakita ng journalctl -k ang out of memory kill. Itakda nang malinaw ang memory_limit at threads upang bumagal ang query sa halip na mamatay.

Dapat sukatin ang tokens, hindi tantiyahin. May usage object ang bawat Messages API response na naglalaman ng input_tokens at output_tokens. Isulat ang dalawang value sa isang table sa bawat call. Pagkalipas ng isang linggo, alam mo na ang aktuwal mong volume. I-multiply ito sa presyong inililista ng model sa araw na magsusuri ka. May dalawang bagay na sapat ang stability para gawing batayan ng pagpaplano. Mas mataas ang presyo ng output tokens kaysa input tokens sa bawat Claude model. Kaya mas malaki ang epekto sa bill ng paglilimita sa note sa 200 words kaysa sa pagbabawas ng data na ipinapadala mo. Hindi rin nakatutulong ang prompt caching sa job na tumatakbo isang beses bawat araw, dahil sinusukat sa minuto ang cache lifetime. Pagdating ng susunod na run, expired na ang cached block at muli mong babayaran ang buong input price. May pakinabang ang caching kapag maraming call ang ginagawa ng isang run gamit ang parehong malaking block ng text.

I-install ang mga bahagi

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

Suriin ang installation bago magsulat ng anumang code:

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

Ini-print nito ang ok. Kung nilaktawan mo ang virtual environment at pinatakbo ang pip install laban sa system Python, pipigilan ka ng Ubuntu 24.04 gamit ang error: externally-managed-environment, dahil pagmamay-ari ng distribution ang /usr/lib/python3 at hindi nito pinapayagang doon magsulat ang pip. Hindi lang kagandahang-asal ang venv. Ito ang tanging directory na maaaring galawin ng pip.

Ilagay ang API key sa isang file na mababasa ng service user at walang ibang makakabasa:

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

Maglagay ng isang linya rito, na walang quotes at walang export, dahil systemd mismo ang nagpa-parse sa file na ito sa halip na ipasa ito sa isang shell:

ANTHROPIC_API_KEY=sk-ant-your-key-here

Ang store: dalawang table

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

Ang primary key sa (ticker, day) ang dahilan kung bakit ligtas ulitin ang refresh. Ino-overwrite ng INSERT OR REPLACE ang row na mayroon na para sa ticker at araw na iyon, kaya hindi dumodoble ang laman ng table kapag dalawang beses pinatakbo ang backfill. Kung wala ang key, tahimik na madodoble ang bawat bar kapag muling pinatakbo matapos ang crash, at magiging mali ang bawat average na kukuwentahin pagkatapos nito nang walang error message na magsasabi sa iyo.

Pinapayagan ng DuckDB na isang process lang ang magbukas ng file para sa pagsusulat. Agad na magfa-fail ang ikalawang writer gamit ang Could not set lock on file, na sinusundan ng PID ng process na may hawak nito. Sa aktuwal, karaniwan itong ang interactive na duckdb shell na naiwan mong bukas sa ibang terminal. Nakakadaan ang mga reader gamit ang read_only=True, kaya ginagamit ng connect ang flag na iyon. Kung talagang kailangang magsulat ng maraming process sa parehong oras, ibang engine ang dapat gamitin: sa WAL mode ng SQLite, makakabasa ang mga reader habang nagco-commit ang isang writer, at maghihintay ang ibang writer dahil sa busy timeout sa halip na mag-fail. Inihahambing ng DuckDB kumpara sa SQLite para sa server workload ang dalawang ito, at tinatalakay ng pagpapatakbo ng SQLite sa production sa isang VPS ang mga setting para gumana nang maayos ang WAL.

Ang 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()

Manually itong patakbuhin nang isang beses:

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

Sa unang run, isi-store nito ang data mula sa mga nakaraang taon at magpi-print ng isang linya bawat ticker, na may bilang na nasa libo. Patakbuhin itong muli makalipas ang isang minuto. Dapat magpakita ang bawat linya ng 1 o 2 row dahil nagsisimula ang job sa huling araw na naka-store na. Ang ikalawang run ang aktuwal na test: kung nasa libo pa rin ang mga bilang, walang ibinabalik ang max(day) at nire-rewrite ng insert ang buong history mo gabi-gabi.

Ang check na df.empty ang pinakamahalagang linya sa file. Hindi nagra-raise ang Ticker.history() kapag mali o na-delist ang symbol. Nagpi-print ito ng warning na posibleng na-delist ang symbol at walang nakitang price data; nagbabago ang eksaktong wording depende sa library version. Nagbabalik din ito ng walang laman na DataFrame. Kapag wala ang check na ito, walang naisusulat ang job, nag-e-exit ito gamit ang 0, at ipinapakita ng systemd na healthy at green ang run habang tahimik na tumitigil ang paglaki ng table. Malalaman mo lang ito makalipas ang ilang linggo, kapag pare-pareho na ang mga row na ibinabalik ng screen araw-araw.

May dahilan din ang pag-handle sa timestamp. Maaaring may exchange timezone ang index na ibinabalik ng feed. Hindi katumbas ng pag-convert sa UTC ang pagtanggal ng offset. Kapag ang Tokyo session ay may timestamp na midnight sa lokal na oras, mapupunta ito sa nakaraang calendar day kapag kino-convert sa UTC. Dahil dito, tahimik na naiuusog ng UTC conversion nang isang araw pabalik ang bawat Japanese bar at nasisira ang primary key. Pinananatili ng tz_localize(None) ang sariling session date ng exchange, na siyang kahulugan ng isang daily bar.

Ang timer na tumatakbo kapag nagsasara ang market

Nagsasara ang US market sa 16:00 New York time, at inaabot ng ilang minuto bago ma-settle ang mga huling print, kaya tumatakbo ang job sa 16:20. Isulat ito sa New York time, hindi sa UTC. Ang New York ay UTC minus 5 kapag winter at UTC minus 4 kapag summer. Kaya ang timer na gumagamit ng fixed UTC hour ay nauurong o nauuna nang isang oras dalawang beses bawat taon at nagsisimulang tumakbo bago magsara ang market. Direktang tumatanggap ang systemd 252 at mas bago ng timezone sa OnCalendar, at may kasamang version 255 ang Ubuntu 24.04. Kumpirmahin ang version gamit ang 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

Ang Type=oneshot lamang ang service type na tumatanggap ng higit sa isang ExecStart. Pinapatakbo nito ang mga ito nang sunod-sunod at humihinto kapag may nag-exit na non zero. Ito mismo ang kinakailangang behavior: hindi dapat sundan ng pag-display ng stale data ang isang nabigong refresh. Mahalaga ang Persistent=true sa VPS na nagre-reboot para sa kernel updates. Kung magre-reboot ito sa 16:15 nang wala nito, tuluyang mawawala ang run. Kapag mayroon nito, tatakbo ang job sa sandaling bumalik online ang machine.

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

Dapat magpakita ang list-timers ng column na NEXT na naglalaman ng susunod na weekday run, na na-convert sa sariling local time ng server. Kapag walang laman ang listahan, hindi naka-enable ang timer o nawawala sa unit ang section na [Install]. Dahil dito, walang mai-link ang enable sa timers.target.

Ang screen: SQL muna, model sa huli

Eksakto ang SQL at walang gastos sa bawat run. Hindi ganoon ang model. Kaya nililimitahan muna ng query ang mga posibleng resulta, at ang mga natitirang row lamang ang ipinapadala sa model. I-save ito bilang /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;

Hindi dekorasyon ang n > 50 filter. Kinukuwenta ng ROWS BETWEEN 49 PRECEDING AND CURRENT ROW ang average ng anumang row na mayroon, kaya sa ikatlong row ng isang ticker, average ng tatlong araw ang ibinabalik nito at tinatawag pa rin itong ma50. Kapag ikinumpara ito sa ma20, makagagawa ka ng crossover sa simula ng history ng bawat ticker kahit hindi naman ito nangyari. Kapag nag-filter batay sa row number, inaalis ang mga row kung saan hindi pa kumpleto ang window.

Ang Nakikita ng Model, at ang Hindi Nito Kailanman Nakikita

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

Nililimitahan ng word limit sa system prompt ang mas magastos na bahagi ng bill. Ang instruction na gamitin lamang ang ibinigay na rows ang kailangan mong i-verify sa halip na basta pagkatiwalaan: mag-delete ng isang column mula sa block, patakbuhin itong muli, at basahin ang output. Kung lumitaw pa rin ang figure para sa column na iyon, pinunan ng model ang puwang, at hindi sapat na mahigpit ang prompt mo. Dalawang minuto lamang ang kailangan para sa test na ito, at ito ang tanging tapat na paraan para malaman ang resulta.

Hindi kailanman nakikita ng model ang API key o ang database path, at hindi rin ito nagpapatakbo ng query. Tumatanggap ito ng rows at nagbabalik ng prose. Ang hangganang ito ang dahilan kung bakit nasusuri ang output, dahil dapat lumitaw din sa block na ipinadala mo ang bawat numero sa note, at maaari mong paghambingin ang mga ito line by line. Para sa higit pang impormasyon tungkol sa prompting, mas malalim na tinatalakay sa paggamit ng Claude para sa pagsusuri ng finance kung ano ang mahusay basahin ng model.

Isang run, mula simula hanggang matapos

Sa 16:20 New York time, sinisimulan ng timer ang service. Kinukuha ng refresh.py ang data ng bawat ticker mula sa feed, simula sa huling naka-store na araw. Sumusulat ito ng isa o dalawang bagong bar para sa bawat ticker at nagpi-print ng isang linya bawat ticker. Binubuksan ng screen.py ang parehong file, pinapatakbo ang moving average query, at kumukuha ng ilang row. Nagiging text block na may ilang daang token ang mga row na iyon. Sa isang API call, ginagawa ang mga ito bilang maikling note. Itinatala ang note sa journal, at may isang row na napupunta sa runs kasama ang token counts at bilang ng mga hit.

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

Ang maayos na log ay may isang linya bawat ticker, kasunod ang note, at pagkatapos ay research-refresh.service: Deactivated successfully. Pagkalipas ng isang linggo, kunin sa database ang aktuwal mong gastos:

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

I-multiply ang mga total na iyon sa presyong bawat million token na inililista ng iyong model sa araw na basahin mo ang mga ito. Makukuha mo ang aktuwal na halaga sa halip na estimate ng iba. Nagbabago ang mga inilalathalang presyo. Hindi nagbabago ang arithmetic.

Bakit nag-o-overfit ang mga backtest, at paano ito mapapanood habang nangyayari

Isulat muli ang screen bilang function ng dalawang window length, i-sweep ang grid ng mga pair, at i-rank ang mga ito ayon sa return. Magmumukhang napakahusay ng pinakamagandang pair. Iyan ang problema, hindi ang resulta. Ang grid na may 200 pair ay 200 eksperimento, at pinili mo ang pinakasuwerte.

Mapapanood mo itong mangyari sa loob ng sampung minuto. Hatiin sa dalawa ang store ayon sa petsa. I-sweep ang grid sa unang bahagi lamang at itala ang nanalo. I-sweep ang parehong grid sa ikalawang bahagi. Kung malaki ang agwat ng dalawang nanalo, nagfi-fit sa noise ang mga parameter, at walang sinasabi tungkol sa bukas ang isang pair na nananalo lamang sa bahaging pinag-tune-an mo rito.

Mas malala ang survivorship kaysa overfitting dahil hindi ito maaayos ng tuning. Ang ticker list mo ay mga kasalukuyang miyembro ng index, kaya naglalaman lamang ito ng mga kumpanyang nakaligtas. Kapag hiningi mo sa feed ang ticker na na-delist noong 2019, magbabalik ito ng empty frame. Ibig sabihin, hindi kailanman nakapasok ang kumpanyang iyon sa store at sa test mo. Naibukod na ang mga natalo sa bawat backtest na pinapatakbo mo.

Sinisira ng restated fundamentals ang timeline. Ang revenue figure na ibinabalik ng API ngayon para sa isang quarter noong 2019 ay hindi laging kapareho ng figure na inilathala noong 2019. Gumagamit ang screen na naghahalo ng fundamentals ngayon at mga presyo noong 2019 ng impormasyong hindi pa umiiral noon. Karaniwang ligtas ang mga presyo rito. Karaniwang hindi ligtas ang fundamentals.

Nagbabago ang adjusted prices habang ginagamit mo ang mga ito. Sa auto_adjust=True, ina-adjust paatras ang mga closing price para sa dividends at splits, kaya bahagyang naiibang history ang ibinabalik ng parehong query kapag pinatakbo sa susunod na buwan. Ang pag-store sa mga row na aktuwal mong ginamit ang dahilan kung bakit reproducible ang resulta. Isa rin ito sa mga dahilan kung bakit umiiral ang local store.

Hindi rin isinasaalang-alang ng backtest ang commissions at slippage, at ipinapalagay nitong hindi gagalaw ang presyo dahil sa order mo. Bahagi ang mga iyon ng execution, na wala sa scope dito at saklaw sa pagpapatakbo ng trading bots sa isang VPS.

Mga failure mode at mga string na makikita mo

error: externally-managed-environment kapag tumatakbo ang pip. Nasa labas ka ng virtual environment. Tawagin ang /opt/research/venv/bin/pip gamit ang buong path.

Could not set lock on file, na may PID pagkatapos nito. May ibang process na nakabukas para sa pagsusulat sa DuckDB file, karaniwang isang interactive shell na nakalimutan mong isara. Isara ito, o buksan ang second connection gamit ang read_only=True.

Main process exited, code=killed, status=9/KILL sa systemctl status. Pinatay ng kernel ang job dahil sa kakulangan ng memory. Kumpirmahin gamit ang journalctl -k | grep -i oom, pagkatapos ay babaan ang memory_limit sa store.py.

Isang green run na walang naisusulat. Binabasa ng systemctl status ang active (exited) ngunit hindi lumalaki ang table. Walang laman ang mga frame na ibinalik ng feed. Pinakamatagal bago mapansin ang failure na ito, kaya dapat mag-exit ang job nang non-zero kapag walang laman ang ibinalik ng bawat ticker.

Tumakbo ang timer sa market holiday. Hindi alam ng systemd ang exchange calendar, kaya kasama sa Mon-Fri ang mga holiday. Tumatakbo ang job, walang bagong data ang feed, at dapat ituring ito ng job bilang normal sa halip na error.

429 mula sa API. Lumampas ka sa rate limit. Awtomatikong nagre-retry ang Anthropic SDK na may backoff, at pinapataas ng Anthropic(max_retries=5) ang bilang ng pagtatangka. Kung nabibigo pa rin ito araw-araw, masyadong maraming request ang ginagawa ng job sa isang burst.

Hindi ito

Isa itong research assistant. Ang model na nagbubuod ng isang filing ay nagbibigay lamang ng interpretasyon sa filing na iyon, at maaari itong maging lubos na mali tungkol sa numerong nakalimbag sa teksto. Kaya dapat mai-trace ang bawat figure sa note pabalik sa row na ipinadala mo. Ituring ang output bilang shortlist ng mga bagay na ikaw mismo ang dapat magbasa. Hindi ito financial advice, at wala sa mga ito ang isang signal.

Kapaki-pakinabang ang backtests para tanggihan ang mga ideya, pero mahina ang mga ito sa pagkumpirma sa mga ideya. Ang strategy na bumagsak sa sarili mong data ay talagang hindi na magagamit. Ang strategy na pumasa ay nakalampas lamang sa pagsubok gamit ang sarili mong data. Mas maliit na claim ito kaysa sa pakiramdam nito kapag 1am na.

Sadyang hindi saklaw ang execution. Iba ang risk profile ng orders at broker credentials kumpara sa isang read-only research box. Kapag pinagsama ang mga ito, napupunta ang trading keys sa parehong machine na may LLM prompt. Kung gusto mong makita kung saan pumapailalim ang pattern na ito kumpara sa iba pang kapaki-pakinabang na patakbuhin sa sarili mong server, ang mga self-hosted AI agent na kapaki-pakinabang patakbuhin ang mas malawak na tour.

FAQ

Kailangan ko ba ng bayad na market data feed?

Hindi para sa prototype. Ayos ang libreng unofficial feed para matutuhan ang istruktura ng system, at masisira ito dahil nakadepende ito sa website na walang obligasyong magbigay sa iyo ng anuman. Karaniwan itong nasisira bilang mga empty frame, hindi bilang exception, kaya dapat mag-check ang job mo ng row counts. Lumipat sa bayad na feed na may documented API at support address kapag ginagamit na ang data sa isang desisyon. Ang store ang nagpapamura sa paglipat na ito: fetch function lang ang kailangang baguhin, habang nananatiling pareho ang schedule, schema, at screen.

Dapat ko bang itago ang mga presyo sa SQLite o DuckDB?

Columnar ang DuckDB at ginawa ito para mag-scan ng maraming row upang mag-compute ng aggregate. Ito mismo ang kailangan para sa moving average ng bars sa loob ng sampung taon. Row-oriented ang SQLite at mas angkop sa maraming maliliit na read at write mula sa ilang process nang sabay-sabay. Para sa isang scheduled job na nag-a-append ng ilang daang row at pagkatapos ay nagso-scan ng milyun-milyong row, mas angkop ang DuckDB. Kung kailangang magsulat nang sabay-sabay ang ilang process, hinahayaan ng SQLite sa WAL mode na magpatuloy ang mga reader habang nagco-commit ang isang writer. Dahil dito, maaaring maghintay ang ibang writer sa busy timeout sa halip na tuluyang mag-fail.

Magkano ang magiging gastos ng LLM calls bawat buwan?

I-log ang usage.input_tokens at usage.output_tokens mula sa bawat response sa isang table. Pagkatapos, i-multiply ang weekly totals sa presyo bawat milyong token na inililista ng model mo sa araw ng pag-check. Iyon lang ang halagang mananatiling tama sa susunod na quarter. Ang isang daily run sa maikling screen ay maliit na bilang ng calls. Ang haba ng note ang bahaging makokontrol mo: mas malaki ang matitipid sa pag-cap ng summary sa 200 words kaysa sa pagbawas ng bilang ng rows na ipinapadala, dahil mas mataas ang presyo ng output tokens kaysa input tokens sa bawat Claude model.

Bakit naging successful ang job ko pero walang bagong row na naisulat?

May dalawang karaniwang sanhi. Sarado ang market dahil kasama sa systemd Mon-Fri schedule ang mga exchange holiday. O nagbalik ang feed ng empty frame para sa bawat ticker. Madalas itong iniuulat ng client libraries bilang printed warning sa halip na exception, kaya nag-e-exit pa rin ang process sa 0 at ipinapakita pa rin ng systemd na successful ang run. Tukuyin ang sanhi sa pamamagitan ng paghahambing ng SELECT max(day) FROM prices sa huling totoong trading day. Gawing non-zero ang exit status ng job kapag empty ang resulta para sa lahat ng ticker.

Maaari bang magpasya ang agent kung ano ang bibilhin?

Hindi. Dito nagkakamali ang mga project kapag ginawa itong subukang magpasya. Walang market access ang model, wala itong impormasyon tungkol sa position o tax situation mo, at wala itong paraan upang i-check ang sarili nitong mga numero laban sa anumang source. Ang mahusay nitong gawin ay magbasa ng maraming text at tukuyin kung aling ilang item ang dapat mong bigyang-pansin ngayong araw. Walang financial advice sa anumang isinusulat nito. Ang desisyon, pati ang pananagutan dito, ay nananatili sa iyo.