SSD Nodes Learn Hosting plans →
Panduan Matt ConnorOleh Matt Connor · Dikemas kini 2026-08-13

Cara Bina Ejen Penyelidikan Saham Sendiri di VPS

Bina ejen penyelidikan saham automatik di VPS menggunakan Python, DuckDB dan API Claude. Panduan ini merangkumi suapan data, storan setempat serta penjadualan systemd.

Apakah ejen penyelidikan saham yang dihoskan sendiri

Ejen penyelidikan saham yang dihoskan sendiri ialah atur cara kecil pada pelayan milik anda yang menarik data pasaran mengikut jadual, menyimpannya dalam pangkalan data setempat, menjalankan saringan ke atasnya, dan meminta model bahasa besar (LLM) untuk menulis laporan tentang perubahan yang berlaku. Ia membaca dan menapis. Ia tidak melakukan dagangan, dan tiada apa-apa dalam panduan ini merupakan nasihat kewangan.

Dua orang yang membina sistem ini dari awal mungkin memilih pustaka yang berbeza namun tetap akan menghasilkan empat bahagian yang sama: suapan yang membekalkan harga dan data asas, stor setempat yang menyimpan setiap baris yang pernah anda ambil, tugasan yang menyegarkan stor mengikut pemasa, dan lapisan LLM yang menukarkan baris yang tersaring menjadi ayat. Panduan ini membina struktur tersebut menggunakan Python, DuckDB, pemasa systemd dan API (antara muka pengaturcaraan aplikasi) Claude. Pelaksanaan dagangan adalah tugasan berasingan dengan mod kegagalan yang berbeza, dan ia sepatutnya diletakkan pada VPS yang disediakan untuk bot dagangan sebaliknya.

Empat bahagian dan fungsi setiap satunya

Feed ialah satu-satunya bahagian yang berhubung dengan dunia luar. Ia tahu cara meminta ticker dan julat tarikh, serta cara menyerahkan baris data. Semua proses hiliran membaca pangkalan data anda dan bukannya feed tersebut, jadi gangguan pada feed hanya menyebabkan kehilangan data sehari dan bukannya kerosakan paparan.

Store adalah tujuan utama keseluruhan proses ini. Harga penutup harian yang tidak direkodkan biasanya boleh diambil semula kemudian. Sebut harga intraday, anggaran sebelum disemak semula, atau angka fundamental sebelum dinyatakan semula tidak boleh diambil semula. Store ialah cara anda membina rekod tentang perkara yang sebenarnya dinyatakan oleh data pada hari ia dikeluarkan.

Scheduler menentukan bila proses muat semula berlaku. Pada pelayan, ini merupakan systemd timer, itulah sebabnya VPS lebih penting di sini berbanding kod itu sendiri.

LLM layer membaca blok teks pendek yang dihasilkan oleh SQL anda, dan menulis ringkasannya. Ia tidak pernah bersambung dengan pangkalan data dan tidak pernah membina query. Jika model menulis SQL, satu token yang salah akan menjadi nombor yang salah di dalam ayat yang lancar, tanpa ada apa-apa untuk membandingkannya. Jika SQL menghasilkan nombor tersebut, model hanya boleh melakukan kesilapan pada prosa, dan anda boleh menyemak prosa tersebut dengan baris data yang anda hantar.

Mengapa menjalankannya pada VPS dan bukannya komputer riba

Penjadual adalah sebab utamanya. Pasaran AS ditutup pada pukul 16:00 waktu New York, iaitu pukul 22:00 di Berlin dan 04:00 pagi keesokan harinya di Jakarta. Komputer riba berada dalam mod tidur pada kedua-dua waktu tersebut. Kegagalan menjalankan tugasan lebih merugikan daripada sekadar nota lewat: bar harian biasanya boleh diambil semula kemudian, tetapi apa-apa yang telah disemak tidak boleh, jadi jurang dalam rekod anda adalah kekal. Penaakulan yang sama terpakai kepada mana-mana ejen yang jadual dan keadaan tersimpannya perlu bertahan selepas but semula, iaitu perkara yang menjadi asas kepada menjalankan KiroCrew sebagai ejen yang sentiasa aktif pada VPS anda sendiri.

Sebab kedua adalah lebih kecil namun tetap nyata. Pelayan menyimpan satu API key, dalam satu fail, yang dimiliki oleh satu pengguna sistem tanpa shell log masuk, dan digunakan oleh satu tugasan. Hal ini jauh lebih sukar untuk diatur pada komputer riba yang anda gunakan juga untuk melayari web. Pastikan key tersebut tidak berada dalam kod dan tidak berada dalam input model, yang merupakan subjek kepada menyimpan API keys di luar ejen AI.

Penentuan saiz: cakera, RAM dan token

Bahagian cakera adalah mudah. Satu bar harian ialah satu baris bagi setiap ticker untuk setiap hari dagangan, dan setahun dagangan di AS adalah kira-kira 252 hari.

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

Dua puluh ticker bersamaan dengan 5,040 baris selepas setahun. Baris 500 tickers mencapai 1,260,000 baris selepas sepuluh tahun. Setiap baris mengandungi tarikh dan beberapa nilai double, dan DuckDB menyimpan lajur dalam bentuk termampat, jadi saiznya adalah dalam puluhan megabait dan bukannya gigabait. Jangan percaya anggaran tersebut, termasuk anggaran saya. Jalankan du -h /opt/research/data/market.duckdb selepas backfill pertama anda dan gunakan angka anda sendiri.

RAM adalah bahagian yang mencabar bagi VPS kecil. Secara lalai, DuckDB mengambil sebahagian besar memori mesin dan semua terasnya untuk satu pertanyaan, yang sesuai untuk pelayan analitik tetapi tidak sesuai untuk kotak 2 GB yang turut menjalankan proses lain. Satu pengagregatan ke atas keseluruhan jadual harga kemudiannya akan menyebabkan proses tersebut dimatikan oleh kernel, dan systemd melaporkan Main process exited, code=killed, status=9/KILL manakala journalctl -k menunjukkan proses tersebut dimatikan kerana kehabisan memori (OOM kill). Tetapkan memory_limit dan threads secara eksplisit supaya pertanyaan menjadi lebih perlahan dan bukannya terhenti.

Token harus diukur, bukan dianggar. Setiap respons Messages API membawa objek usage dengan input_tokens dan output_tokens. Tulis kedua-duanya ke dalam jadual pada setiap panggilan, dan selepas seminggu anda akan mengetahui volum sebenar anda, yang kemudiannya didarabkan dengan harga yang disenaraikan oleh model anda pada hari anda menyemak. Dua perkara cukup stabil untuk dijadikan perancangan. Token output diletakkan harga lebih tinggi daripada token input pada setiap model Claude, jadi mengehadkan nota kepada 200 perkataan lebih berkesan dalam mengurangkan bil berbanding memotong data yang anda hantar. Selain itu, prompt caching tidak membantu tugasan yang dijalankan sekali sehari, kerana jangka hayat cache diukur dalam minit: menjelang larian seterusnya, blok yang dicache telah tamat tempoh dan anda membayar harga input penuh sekali lagi. Caching berbaloi apabila satu larian membuat banyak panggilan ke atas blok teks besar yang sama.

Memasang komponen

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

Semak pemasangan sebelum anda menulis sebarang kod:

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

Perintah tersebut mencetak ok. Jika anda melangkau persekitaran maya dan menjalankan pip install pada Python sistem, Ubuntu 24.04 akan menghalang anda dengan error: externally-managed-environment, kerana pengedaran tersebut memiliki /usr/lib/python3 dan tidak membenarkan pip menulis di situ. Venv bukanlah sekadar adab. Ia adalah satu-satunya direktori yang dibenarkan untuk disentuh oleh pip.

Kunci API diletakkan dalam fail yang boleh dibaca oleh pengguna servis dan tiada orang lain:

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

Masukkan satu baris di dalamnya, tanpa tanda petikan dan tanpa export, kerana systemd menghuraikan fail ini sendiri dan bukannya menghantarnya kepada shell:

ANTHROPIC_API_KEY=sk-ant-your-key-here

Storan: dua jadual

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

Primary key pada (ticker, day) menjadikan proses muat semula selamat untuk diulang. INSERT OR REPLACE akan menulis ganti baris yang sudah wujud untuk ticker dan hari tersebut, jadi menjalankan backfill dua kali tidak akan menggandakan jadual. Tanpa kunci tersebut, satu ulangan selepas kegagalan sistem akan menduplikasi setiap bar secara senyap, dan setiap purata yang anda kira selepas itu akan menjadi salah tanpa sebarang mesej ralat untuk memberitahu anda.

DuckDB membenarkan hanya satu proses untuk membuka fail bagi tujuan penulisan pada satu-satu masa. Penulis kedua akan gagal serta-merta dengan Could not set lock on file, diikuti oleh PID yang memegang fail tersebut, yang dalam praktiknya adalah shell duckdb interaktif yang anda biarkan terbuka di terminal lain. Pembaca melepasi read_only=True, itulah sebabnya connect menggunakan flag tersebut. Jika beberapa proses benar-benar perlu menulis pada saat yang sama, itu adalah tugas enjin yang berbeza: SQLite dalam mod WAL membenarkan pembaca bekerja sementara seorang penulis melakukan komit, dan busy timeout membuatkan penulis lain menunggu dan bukannya gagal. DuckDB berbanding SQLite untuk beban kerja pelayan membandingkan kedua-duanya, dan menjalankan SQLite dalam pengeluaran pada VPS merangkumi tetapan yang menjadikan WAL berfungsi dengan stabil.

Tugas muat semula

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

Jalankan tugas ini secara manual sekali:

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

Jalankan kali pertama akan mengisi data selama bertahun-tahun dan mencetak satu baris bagi setiap ticker dengan kiraan dalam ribuan. Jalankan sekali lagi seminit kemudian dan setiap baris akan menunjukkan 1 atau 2 baris data, kerana tugas tersebut bermula dari hari terakhir yang sudah disimpan. Jalankan kali kedua itu adalah ujian sebenar: jika kiraan masih dalam ribuan, max(day) tidak mengembalikan apa-apa dan operasi insert menulis semula keseluruhan sejarah anda setiap malam.

Semakan df.empty adalah baris paling penting dalam fail tersebut. Ticker.history() tidak mengeluarkan ralat untuk simbol yang salah atau telah dinyahsenarai. Ia mencetak amaran tentang simbol yang mungkin telah dinyahsenarai tanpa data harga ditemui (perkataan tepat berubah mengikut versi pustaka) dan mengembalikan DataFrame kosong. Tugas tanpa semakan itu tidak menulis apa-apa, keluar dengan kod 0, dan systemd menunjukkan status hijau yang sihat sedangkan jadual tersebut berhenti berkembang secara senyap. Anda hanya akan mengetahuinya beberapa minggu kemudian, daripada skrin yang memaparkan baris yang sama setiap hari.

Pengendalian cap masa (timestamp) juga mempunyai sebabnya. Indeks yang dikembalikan oleh suapan boleh membawa zon waktu bursa, dan membuang offset tidak sama dengan menukar kepada UTC. Sesi Tokyo yang dicap pada tengah malam waktu tempatan akan bertukar kepada hari kalendar sebelumnya dalam UTC, jadi penukaran UTC akan mengalihkan setiap bar Jepun ke belakang satu hari secara senyap dan merosakkan kunci utama (primary key). tz_localize(None) mengekalkan tarikh sesi bursa itu sendiri, yang merupakan maksud sebenar bar harian.

Pemasa yang dicetuskan semasa penutupan pasaran

Pasaran AS ditutup pada jam 16:00 waktu New York, dan cetakan terakhir mengambil masa beberapa minit untuk diselesaikan, jadi tugasan ini dijalankan pada jam 16:20. Tuliskan masa tersebut dalam waktu New York, bukan dalam UTC. New York ialah UTC tolak 5 pada musim sejuk dan UTC tolak 4 pada musim panas, jadi pemasa yang ditulis sebagai jam UTC tetap akan tersasar sebanyak satu jam dua kali setahun dan mula dicetuskan sebelum pasaran ditutup. systemd 252 dan versi terkemudian menerima zon masa secara terus dalam OnCalendar, dan Ubuntu 24.04 mengeluarkan versi 255. Sahkan versi anda dengan 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 ialah satu-satunya jenis servis yang menerima lebih daripada satu ExecStart, dan ia menjalankannya mengikut urutan, serta berhenti jika salah satu daripadanya keluar dengan kod bukan sifar. Itulah tingkah laku yang anda inginkan: penyegaran yang gagal tidak boleh diikuti dengan paparan data yang lapuk. Persistent=true penting pada VPS yang but semula untuk kemas kini kernel. But semula pada jam 16:15 tanpa tetapan ini akan menyebabkan tugasan tersebut hilang begitu sahaja; dengan tetapan ini, tugasan akan dijalankan sebaik sahaja mesin kembali aktif.

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

list-timers sepatutnya menunjukkan lajur NEXT yang mengandungi waktu jalankan hari kerja seterusnya, yang ditukar kepada waktu tempatan pelayan itu sendiri. Senarai kosong bermakna pemasa tidak didayakan, atau unit tersebut tiada bahagian [Install], jadi enable tidak mempunyai apa-apa untuk dipautkan ke dalam timers.target.

Skrin: SQL dahulu, model kemudian

SQL adalah tepat dan tidak menelan kos bagi setiap pelaksanaan. Model pula tidak mempunyai kedua-dua sifat tersebut. Oleh itu, pertanyaan (query) mengecilkan skop data, dan hanya data yang melepasi tapisan sahaja yang dihantar ke model. Simpan ini sebagai /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;

Penapis n > 50 bukanlah hiasan. ROWS BETWEEN 49 PRECEDING AND CURRENT ROW mengira purata bagi mana-mana baris yang wujud, jadi baris ketiga bagi sesuatu ticker akan mengembalikan purata tiga hari dan masih melabelkannya sebagai ma50. Bandingkan perkara itu dengan ma20 dan anda akan mencipta persilangan (crossover) pada permulaan sejarah setiap ticker yang sebenarnya tidak pernah berlaku. Menapis berdasarkan nombor baris akan membuang baris yang mana tetingkap (window) datanya belum lengkap.

Apa yang dilihat oleh model, dan apa yang tidak pernah dilihatnya

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

Had perkataan dalam system prompt mengehadkan bahagian kos yang tinggi. Arahan untuk menggunakan hanya baris yang diberikan adalah perkara yang perlu anda sahkan dan bukannya sekadar mempercayainya: padamkan satu lajur daripada blok tersebut, jalankannya semula, dan baca outputnya. Jika angka bagi lajur tersebut masih muncul, model itu telah mengisi ruang kosong tersebut, dan prompt anda tidak cukup ketat. Ujian itu mengambil masa dua minit dan ia merupakan satu-satunya cara yang jujur untuk mengetahuinya.

Model tidak pernah melihat API key, tidak pernah melihat database path, dan tidak pernah menjalankan sebarang query. Ia menerima baris data dan mengembalikan prosa. Sempadan itulah yang menjadikan output tersebut boleh disemak, kerana setiap nombor dalam nota itu sepatutnya muncul juga dalam blok yang anda hantar, dan anda boleh membandingkannya baris demi baris. Untuk maklumat lanjut mengenai aspek prompting ini, menggunakan Claude untuk analisis kewangan membincangkan dengan lebih mendalam tentang perkara yang mahir dibaca oleh model.

Satu larian, hujung ke hujung

Pada pukul 16:20 waktu New York, pemasa memulakan servis tersebut. refresh.py meminta suapan bagi setiap ticker bermula dari hari terakhir yang disimpan, menulis satu atau dua bar baharu bagi setiap satu, dan mencetak satu baris bagi setiap ticker. screen.py membuka fail yang sama, menjalankan kuiri purata bergerak, dan mendapatkan semula beberapa baris data. Baris-baris tersebut menjadi blok teks yang mengandungi beberapa ratus token. Satu panggilan API menukarkannya menjadi nota ringkas, nota tersebut dihantar ke jurnal, dan satu baris data disimpan ke dalam runs bersama kiraan token dan bilangan padanan.

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

Log yang sihat mengandungi satu baris bagi setiap ticker, diikuti dengan nota, kemudian research-refresh.service: Deactivated successfully. Selepas seminggu, baca semula kos anda daripada pangkalan data:

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

Darabkan jumlah tersebut dengan harga per juta token yang disenaraikan oleh model anda pada hari anda membacanya. Ini memberikan angka sebenar dan bukannya anggaran pihak lain. Harga yang diterbitkan berubah-ubah. Pengiraan aritmetik tidak berubah.

Mengapa backtest mengalami overfitting, dan cara memantaunya

Tulis semula saringan sebagai fungsi bagi dua panjang tetingkap, imbas grid pasangan, dan susun mengikut pulangan. Pasangan terbaik akan kelihatan sangat cemerlang. Itu adalah masalahnya, bukan hasilnya. Grid sebanyak 200 pasangan bermakna 200 eksperimen, dan anda hanya menyimpan yang paling bernasib baik.

Anda boleh melihat perkara ini berlaku dalam masa sepuluh minit. Bahagikan stor kepada dua bahagian mengikut tarikh. Imbas grid pada separuh pertama sahaja dan catatkan pemenangnya. Imbas grid yang sama pada separuh kedua. Jika kedua-dua pemenang jauh berbeza, parameter tersebut hanya menyesuaikan diri dengan hingar (noise), dan pasangan yang hanya menang pada bahagian yang anda tala tidak memberikan sebarang petunjuk tentang hari esok.

Survivorship bias adalah lebih buruk daripada overfitting kerana penalaan tidak dapat membaikinya. Senarai ticker anda adalah ahli indeks hari ini, jadi ia hanya mengandungi syarikat yang terselamat. Jika anda meminta ticker yang telah dinyahsenarai pada tahun 2019 daripada suapan data, ia akan memberikan bingkai kosong, yang bermaksud syarikat itu tidak pernah masuk ke dalam stor anda dan tidak pernah masuk ke dalam ujian anda. Setiap backtest yang anda jalankan telah pun mengecualikan kegagalan.

Fundamental yang dinyatakan semula merosakkan garis masa. Angka hasil yang dikembalikan oleh API hari ini untuk suku tahun 2019 tidak selalunya angka yang diterbitkan pada tahun 2019. Saringan yang mencampurkan fundamental hari ini dengan harga tahun 2019 menggunakan maklumat yang tidak wujud pada masa itu. Harga biasanya selamat di sini. Fundamental biasanya tidak.

Harga terlaras berubah di bawah anda. Dengan auto_adjust=True, harga tutup dilaraskan ke belakang untuk dividen dan pecahan saham (splits), jadi pertanyaan yang sama yang dijalankan bulan depan akan mengembalikan sejarah yang sedikit berbeza. Menyimpan baris yang anda benar-benar gunakan adalah perkara yang menjadikan sesuatu hasil boleh dihasilkan semula, dan ia merupakan satu lagi sebab mengapa stor tempatan itu wujud.

Backtest juga mengabaikan komisen dan slippage, serta mengandaikan pesanan anda tidak menggerakkan harga. Perkara tersebut tergolong dalam pelaksanaan, yang berada di luar skop di sini dan diliputi dalam menjalankan bot dagangan pada VPS.

Mod kegagalan dan rentetan yang akan anda lihat

error: externally-managed-environment apabila pip dijalankan. Anda berada di luar persekitaran maya. Panggil /opt/research/venv/bin/pip menggunakan laluan penuh.

Could not set lock on file, diikuti dengan PID. Proses lain memegang fail DuckDB terbuka untuk penulisan, biasanya shell interaktif yang anda terlupa. Tutup shell tersebut, atau buka sambungan kedua dengan read_only=True.

Main process exited, code=killed, status=9/KILL dalam systemctl status. Kernel menamatkan tugas tersebut kerana masalah memori. Sahkan dengan journalctl -k | grep -i oom, kemudian rendahkan memory_limit dalam store.py.

Jalankan yang berjaya tetapi tidak menulis apa-apa. systemctl status membaca active (exited) dan jadual tersebut tidak bertambah. Suapan (feed) mengembalikan bingkai kosong. Kegagalan ini paling sukar dikesan, jadi tetapkan tugas untuk keluar dengan kod bukan sifar apabila setiap ticker kembali kosong.

Pemasa tercetus pada hari cuti pasaran. systemd tidak mengetahui kalendar bursa, jadi Mon-Fri merangkumi hari cuti. Proses berjalan, suapan tidak mempunyai data baharu, dan tugas tersebut harus menganggapnya sebagai perkara biasa dan bukannya ralat.

429 daripada API. Anda telah melebihi had kadar (rate limit). SDK Anthropic membuat percubaan semula dengan backoff secara automatik, dan Anthropic(max_retries=5) meningkatkan kiraan percubaan. Jika ia masih gagal setiap hari, tugas tersebut meminta terlalu banyak data dalam satu masa.

Apa yang bukan ini

Ini ialah pembantu penyelidikan. Model yang meringkaskan pemfailan menghasilkan bacaan bagi pemfailan tersebut, dan ia boleh tersilap dengan yakin tentang nombor yang dicetak dalam teks, itulah sebabnya setiap angka dalam nota ini mesti dapat dikesan kembali kepada baris yang anda hantar. Anggap output ini sebagai senarai pendek perkara untuk anda baca sendiri. Tiada apa-apa di sini merupakan nasihat kewangan, dan tiada satu pun daripadanya merupakan isyarat.

Ujian ke belakang (backtest) berguna untuk menolak idea dan lemah dalam mengesahkannya. Strategi yang gagal pada data anda sendiri adalah benar-benar tidak berdaya maju. Strategi yang lulus hanya terselamat daripada data anda, yang merupakan tuntutan yang jauh lebih kecil daripada yang dirasakan pada pukul 1 pagi.

Pelaksanaan sengaja dikekalkan di luar skop. Pesanan dan kelayakan broker membawa profil risiko yang berbeza daripada kotak penyelidikan baca sahaja, dan mencampurkannya meletakkan kunci dagangan pada mesin yang sama dengan gesaan LLM. Jika anda ingin melihat di mana corak ini terletak di sebelah perkara lain yang berbaloi untuk dijalankan pada pelayan anda sendiri, ejen AI layan diri yang berbaloi untuk dijalankan ialah tinjauan yang lebih luas.

FAQ

Adakah saya memerlukan suapan data pasaran berbayar?

Tidak perlu untuk prototaip. Suapan percuma tidak rasmi memadai untuk mempelajari bentuk sistem, namun ia akan tergendala kerana ia bergantung pada laman web yang tidak mempunyai obligasi kepada anda. Ia biasanya tergendala dalam bentuk bingkai kosong dan bukannya sebagai pengecualian (exception), jadi tugas anda mestilah menyemak kiraan baris. Beralihlah kepada suapan berbayar yang mempunyai API berdokumen dan alamat sokongan sebaik sahaja data tersebut mula digunakan untuk membuat keputusan. Storan adalah perkara yang menjadikan pertukaran tersebut murah: hanya fungsi ambil (fetch) yang berubah, manakala jadual, skema dan skrin kekal seperti sedia ada.

Patutkah saya menyimpan harga dalam SQLite atau DuckDB?

DuckDB adalah berasaskan lajur dan dibina untuk mengimbas banyak baris bagi mengira agregat, yang merupakan fungsi tepat bagi purata bergerak (moving average) untuk data bar selama sepuluh tahun. SQLite adalah berorientasikan baris dan lebih baik untuk banyak bacaan serta tulisan kecil daripada beberapa proses serentak. Bagi satu tugas berjadual yang menambah beberapa ratus baris dan kemudian mengimbas berjuta-juta baris, DuckDB adalah pilihan yang lebih sesuai. Jika beberapa proses perlu menulis pada masa yang sama, SQLite dalam mod WAL membolehkan pembaca bekerja sementara satu penulis melakukan komit, dan tamat masa sibuk (busy timeout) akan menyebabkan penulis lain menunggu dan bukannya terus gagal.

Berapakah kos panggilan LLM sebulan?

Log usage.input_tokens dan usage.output_tokens daripada setiap respons ke dalam jadual, kemudian darabkan jumlah mingguan anda dengan harga per juta token yang disenaraikan oleh model anda pada hari anda menyemak. Itu adalah satu-satunya angka yang kekal tepat pada suku tahun hadapan. Satu larian harian ke atas skrin yang singkat melibatkan bilangan panggilan yang kecil, dan panjang nota adalah bahagian yang anda kawal: mengehadkan ringkasan kepada 200 perkataan menjimatkan lebih banyak kos berbanding menghantar lebih sedikit baris, kerana token output diletakkan harga lebih tinggi daripada token input pada setiap model Claude.

Mengapakah tugas saya berjaya tetapi tidak menulis baris baharu?

Terdapat dua punca biasa. Pasaran ditutup kerana jadual systemd Mon-Fri merangkumi cuti bursa. Atau, suapan mengembalikan bingkai kosong untuk setiap ticker, yang sering dilaporkan oleh pustaka klien sebagai amaran bercetak dan bukannya pengecualian, jadi proses tersebut masih keluar dengan kod 0 dan systemd masih menunjukkan larian hijau. Bezakan kedua-duanya dengan membandingkan SELECT max(day) FROM prices dengan hari dagangan sebenar yang terakhir, dan tetapkan tugas untuk keluar dengan kod bukan sifar apabila setiap ticker kembali kosong.

Bolehkah ejen memutuskan apa yang perlu dibeli?

Tidak, dan membina ejen untuk mencuba perkara tersebut adalah punca projek sebegini sering gagal. Model tersebut tidak mempunyai akses pasaran, tiada pandangan tentang kedudukan atau situasi cukai anda, dan tiada cara untuk menyemak nombornya sendiri dengan mana-mana sumber. Apa yang ia mahir lakukan ialah membaca sejumlah besar teks dan memberitahu anda beberapa item yang memerlukan perhatian anda hari ini. Tiada apa yang ditulisnya merupakan nasihat kewangan, dan keputusan tersebut, berserta tanggungjawab untuknya, kekal di tangan anda.