Cara Membuat Agent Riset Saham di VPS Sendiri
Pelajari cara membangun agent riset saham di VPS dengan feed data pasar, penyimpanan DuckDB, systemd timer saat pasar tutup, dan penyaring LLM.
Apa yang dimaksud dengan agent riset saham yang di-hosting sendiri
Agent riset saham yang di-hosting sendiri adalah program kecil pada server milik Anda yang mengambil data pasar sesuai jadwal, menyimpannya dalam database lokal, memfilter data tersebut, lalu meminta large language model (LLM) untuk menuliskan perubahan yang terjadi. Agent ini membaca dan memfilter data. Agent ini tidak melakukan transaksi, dan tidak ada bagian dalam panduan ini yang merupakan nasihat keuangan.
Dua orang yang membangun sistem ini dari awal dapat memilih library yang berbeda dan tetap menghasilkan empat komponen yang sama: feed yang menyediakan harga dan data fundamental, penyimpanan lokal yang menyimpan setiap baris data yang pernah diambil, job yang memperbarui penyimpanan tersebut berdasarkan timer, serta lapisan LLM yang mengubah baris data yang tersisa menjadi kalimat. Panduan ini membangun struktur tersebut menggunakan Python, DuckDB, systemd timer, dan Claude API (application programming interface). Eksekusi adalah job terpisah dengan mode kegagalan yang berbeda. Karena itu, eksekusi sebaiknya ditempatkan di VPS yang disiapkan untuk bot trading.
Empat bagian dan fungsi masing-masing
Feed adalah satu-satunya bagian yang berkomunikasi dengan dunia luar. Feed mengetahui cara meminta ticker dan rentang tanggal, serta cara mengembalikan baris data. Semua komponen setelahnya membaca database, bukan feed. Jadi, jika feed mengalami gangguan, dampaknya hanya kehilangan data baru selama satu hari, bukan halaman yang rusak.
Store adalah inti dari seluruh proses ini. Data penutupan harian yang tidak Anda catat biasanya masih dapat diambil kembali nanti. Namun, kuotasi intraday, estimasi sebelum direvisi, atau angka fundamentals sebelum dinyatakan ulang tidak selalu dapat dipulihkan. Store memungkinkan Anda membangun catatan tentang data yang benar-benar disampaikan pada hari data tersebut disampaikan.
Scheduler menentukan waktu refresh dijalankan. Pada server, scheduler tersebut berupa systemd timer. Inilah alasan VPS lebih penting dalam konteks ini daripada kode.
Lapisan LLM membaca blok teks singkat yang dihasilkan SQL, lalu menulis ringkasannya. Lapisan ini tidak pernah terhubung ke database dan tidak pernah membuat query. Jika model yang menulis SQL, satu token yang salah dapat menjadi angka yang salah di dalam kalimat yang terdengar lancar, tanpa pembanding. Jika SQL yang menghasilkan angka, model hanya dapat membuat kesalahan pada teks, dan Anda dapat memeriksa teks tersebut berdasarkan baris data yang dikirim.
Mengapa menjalankannya pada VPS, bukan laptop
Scheduler adalah alasannya. Penutupan pasar AS pada pukul 16:00 waktu New York berlangsung pada pukul 22:00 di Berlin dan pukul 04:00 keesokan harinya di Jakarta. Laptop sedang tidur pada kedua waktu tersebut. Eksekusi yang terlewat menimbulkan masalah yang lebih besar daripada catatan yang terlambat: data harian biasanya masih dapat diambil ulang nanti, tetapi data yang sudah direvisi tidak dapat dipulihkan, sehingga celah dalam catatan Anda bersifat permanen. Alasan yang sama berlaku untuk agent apa pun yang jadwal dan state tersimpannya harus tetap ada setelah reboot. Inilah dasar menjalankan KiroCrew sebagai agent yang selalu aktif pada VPS Anda sendiri.
Alasan kedua lebih kecil, tetapi tetap penting. Server menyimpan satu API key dalam satu file. File tersebut dimiliki oleh satu system user tanpa login shell dan digunakan oleh satu job. Pengaturan seperti ini jauh lebih sulit diterapkan pada laptop yang juga Anda gunakan untuk menjelajah web. Simpan key di luar kode dan di luar input model. Hal ini dibahas dalam menjauhkan API key dari agent AI.
Ukuran: disk, RAM, dan token
Disk adalah bagian yang mudah. Satu batang harian adalah satu baris per ticker per hari perdagangan, dan satu tahun perdagangan di AS sekitar 252 hari.
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 menjadi 5,040 baris setelah satu tahun. Baris 500 tickers mencapai 1,260,000 baris setelah sepuluh tahun. Setiap baris berisi tanggal dan beberapa nilai double. DuckDB menyimpan kolom dalam bentuk terkompresi, sehingga ukurannya puluhan megabita, bukan gigabita. Jangan langsung mempercayai perkiraan ini, termasuk perkiraan saya. Jalankan du -h /opt/research/data/market.duckdb setelah pengisian data awal selesai dan gunakan angka Anda sendiri.
RAM adalah bagian yang menjadi masalah pada VPS kecil. Secara default, DuckDB mengambil porsi besar memori mesin dan seluruh core-nya untuk satu query. Hal ini sesuai untuk server analitik, tetapi tidak sesuai untuk mesin 2 GB yang juga menjalankan layanan lain. Satu agregasi atas seluruh tabel prices kemudian membuat kernel menghentikan proses. systemd melaporkan Main process exited, code=killed, status=9/KILL, sedangkan journalctl -k menunjukkan penghentian karena kehabisan memori. Tetapkan memory_limit dan threads secara eksplisit agar query berjalan lebih lambat, bukan berhenti.
Token harus diukur, bukan diperkirakan. Setiap respons Messages API menyertakan objek usage yang berisi input_tokens dan output_tokens. Tulis keduanya ke dalam tabel pada setiap pemanggilan. Setelah satu minggu, Anda akan mengetahui volume aktual dan dapat mengalikannya dengan harga yang tercantum untuk model Anda pada hari pemeriksaan. Ada dua hal yang cukup stabil untuk dijadikan dasar perencanaan. Pada setiap model Claude, token output memiliki harga lebih tinggi daripada token input. Karena itu, membatasi catatan hingga 200 kata lebih berpengaruh terhadap tagihan daripada mengurangi data yang dikirim. Prompt caching juga tidak membantu pekerjaan yang berjalan satu kali sehari karena masa berlaku cache dihitung dalam menit. Pada proses berikutnya, blok yang disimpan dalam cache sudah kedaluwarsa sehingga Anda kembali membayar harga input penuh. Caching bermanfaat jika satu proses melakukan banyak pemanggilan dengan blok teks besar yang sama.
Pasang 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/dataPeriksa hasil instalasi sebelum menulis kode apa pun:
sudo -u research /opt/research/venv/bin/python -c 'import duckdb, yfinance, anthropic; print("ok")'Perintah tersebut menampilkan ok. Jika Anda melewati virtual environment dan menjalankan pip install pada system Python, Ubuntu 24.04 akan menghentikannya dengan error: externally-managed-environment karena distribusi mengelola /usr/lib/python3 dan tidak mengizinkan pip menulis ke sana. venv bukan sekadar pilihan yang sopan. venv adalah satu-satunya direktori yang boleh diakses untuk menulis oleh pip.
API key disimpan dalam file yang dapat dibaca oleh service user dan tidak dapat dibaca oleh pengguna lain:
sudo install -d -m 755 /etc/research
sudo install -m 640 -o root -g research /dev/null /etc/research/env
sudoedit /etc/research/envMasukkan satu baris ke dalamnya, tanpa tanda kutip dan tanpa export, karena systemd mengurai file ini sendiri, bukan meneruskannya ke shell:
ANTHROPIC_API_KEY=sk-ant-your-key-herePenyimpanan: dua tabel
# /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 conKunci utama pada (ticker, day) membuat proses refresh aman untuk diulang. INSERT OR REPLACE menimpa baris yang sudah ada untuk ticker dan hari tersebut, sehingga menjalankan backfill dua kali tidak menggandakan isi tabel. Tanpa kunci ini, pengulangan setelah crash akan menggandakan setiap bar secara diam-diam, dan semua rata-rata yang Anda hitung setelahnya menjadi salah tanpa pesan error apa pun yang memberi tahu Anda.
DuckDB mengizinkan tepat satu proses membuka file untuk penulisan. Penulis kedua langsung gagal dengan Could not set lock on file, diikuti PID yang memegang file tersebut. Dalam praktiknya, PID itu biasanya adalah shell interaktif duckdb yang masih terbuka di terminal lain. Pembaca dapat mengakses file secara bersamaan melalui read_only=True. Karena itu, connect menggunakan flag tersebut. Jika beberapa proses benar-benar perlu menulis pada saat yang sama, gunakan engine lain: SQLite dalam mode WAL memungkinkan pembaca bekerja saat satu penulis melakukan commit, sedangkan busy timeout membuat penulis lain menunggu alih-alih gagal. DuckDB dibandingkan dengan SQLite untuk beban kerja server membandingkan keduanya, sedangkan menjalankan SQLite di production pada VPS membahas pengaturan agar WAL berfungsi dengan baik.
Tugas penyegaran
# /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 secara manual sekali:
sudo -u research /opt/research/venv/bin/python /opt/research/refresh.pyProses pertama mengisi data untuk beberapa tahun sekaligus dan mencetak satu baris per ticker dengan jumlah data dalam ribuan. Jalankan lagi satu menit kemudian. Setiap baris akan menunjukkan 1 atau 2 baris data karena tugas ini dimulai dari hari terakhir yang sudah tersimpan. Proses kedua adalah pengujian sebenarnya. Jika jumlahnya masih dalam ribuan, max(day) tidak mengembalikan data apa pun dan operasi insert menulis ulang seluruh histori setiap malam.
Pemeriksaan df.empty adalah baris terpenting dalam file ini. Ticker.history() tidak menghasilkan error untuk simbol yang salah atau sudah delisted. Fungsi tersebut mencetak peringatan bahwa simbol mungkin sudah delisted dan tidak ada data harga yang ditemukan. Teks peringatannya dapat berbeda antarversi library. Fungsi tersebut juga mengembalikan DataFrame kosong. Tugas tanpa pemeriksaan ini tidak menulis data apa pun, keluar dengan kode 0, dan systemd menampilkan eksekusi yang berhasil. Sementara itu, tabel berhenti bertambah tanpa diketahui. Masalahnya baru terlihat beberapa minggu kemudian, ketika sebuah layar menampilkan baris yang sama setiap hari.
Penanganan timestamp juga memiliki alasan. Index yang dikembalikan feed dapat menyertakan timezone bursa. Menghapus offset tidak sama dengan mengonversi waktu ke UTC. Sesi Tokyo yang memiliki timestamp tengah malam waktu setempat akan dikonversi menjadi hari kalender sebelumnya dalam UTC. Karena itu, konversi ke UTC secara diam-diam menggeser setiap bar Jepang satu hari ke belakang dan merusak primary key. tz_localize(None) mempertahankan tanggal sesi bursa tersebut. Itulah makna bar harian.
Timer yang berjalan saat pasar ditutup
Pasar AS ditutup pada pukul 16:00 waktu New York, dan transaksi terakhir memerlukan beberapa menit untuk diselesaikan. Karena itu, job dijalankan pada pukul 16:20. Tuliskan waktu tersebut dalam waktu New York, bukan UTC. New York berada pada UTC minus 5 saat musim dingin dan UTC minus 4 saat musim panas. Karena itu, timer yang ditulis sebagai jam UTC tetap akan bergeser satu jam dua kali setahun dan mulai berjalan sebelum pasar ditutup. systemd 252 dan versi setelahnya dapat menerima zona waktu secara langsung dalam OnCalendar, dan Ubuntu 24.04 menyertakan versi 255. Konfirmasikan versi yang digunakan 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.targetType=oneshot adalah satu-satunya jenis service yang menerima lebih dari satu ExecStart dan menjalankannya secara berurutan. Proses berhenti jika salah satunya keluar dengan status non-zero. Perilaku ini sesuai dengan kebutuhan Anda: refresh yang gagal tidak boleh diikuti dengan tampilan data yang sudah usang. Persistent=true penting pada VPS yang melakukan reboot untuk pembaruan kernel. Jika reboot dilakukan pada pukul 16:15 tanpa opsi tersebut, eksekusi akan hilang begitu saja. Dengan opsi tersebut, job dijalankan segera setelah mesin kembali aktif.
sudo systemctl daemon-reload
sudo systemctl enable --now research-refresh.timer
systemctl list-timers research-refresh.timerlist-timers seharusnya menampilkan kolom NEXT yang berisi jadwal eksekusi berikutnya pada hari kerja, setelah dikonversi ke waktu lokal server. Daftar yang kosong berarti timer belum diaktifkan, atau unit tidak memiliki bagian [Install]. Akibatnya, enable tidak memiliki unit untuk ditautkan ke timers.target.
Layar: SQL terlebih dahulu, model terakhir
SQL bersifat presisi dan tidak memerlukan biaya pada setiap eksekusi. Model tidak demikian. Karena itu, kueri mempersempit cakupan data, dan hanya data yang tersisa yang dikirim 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;Filter n > 50 bukan sekadar hiasan. ROWS BETWEEN 49 PRECEDING AND CURRENT ROW menghitung rata-rata dari baris apa pun yang tersedia. Akibatnya, baris ketiga suatu ticker menghasilkan rata-rata tiga hari dan tetap menyebutnya ma50. Jika hasil itu dibandingkan dengan ma20, Anda membuat crossover pada awal riwayat setiap ticker, padahal crossover tersebut tidak pernah terjadi. Pemfilteran berdasarkan nomor baris menghapus baris saat window belum berisi data lengkap.
Apa yang dilihat 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()Batas jumlah kata dalam system prompt membatasi bagian biaya yang paling mahal. Instruksi untuk hanya menggunakan baris yang diberikan harus Anda verifikasi, bukan langsung dipercaya: hapus satu kolom dari blok tersebut, jalankan kembali, lalu baca outputnya. Jika angka untuk kolom itu masih muncul, model telah mengisi data yang hilang, dan prompt Anda belum cukup ketat. Pengujian ini hanya memerlukan waktu dua menit dan merupakan satu-satunya cara yang benar untuk memastikannya.
Model tidak pernah melihat API key, tidak pernah melihat database path, dan tidak pernah menjalankan query. Model menerima baris data lalu mengembalikan prosa. Batasan ini membuat output dapat diperiksa, karena setiap angka dalam catatan juga seharusnya muncul dalam blok yang Anda kirimkan. Anda dapat membandingkannya baris demi baris. Untuk informasi lebih lanjut tentang penggunaan prompt, menggunakan Claude untuk analisis keuangan membahas lebih lanjut hal-hal yang dapat dibaca model dengan baik.
Satu eksekusi dari awal hingga akhir
Pada pukul 16:20 waktu New York, timer memulai service. refresh.py meminta data dari feed untuk setiap ticker, dimulai dari hari terakhir yang tersimpan, menulis satu atau dua bar baru untuk masing-masing ticker, lalu mencetak satu baris per ticker. screen.py membuka file yang sama, menjalankan kueri moving average, dan menerima beberapa baris hasil. Baris-baris tersebut menjadi blok teks yang terdiri dari beberapa ratus token. Satu panggilan API mengubahnya menjadi catatan singkat, catatan tersebut ditulis ke journal, dan satu baris masuk ke runs bersama jumlah token dan jumlah hit.
journalctl -u research-refresh.service -n 50 --no-pagerLog yang sehat berisi satu baris untuk setiap ticker, kemudian catatan, lalu research-refresh.service: Deactivated successfully. Setelah satu minggu, baca sendiri biaya Anda dari database:
SELECT count(*) AS runs,
sum(input_tokens) AS in_tokens,
sum(output_tokens) AS out_tokens
FROM runs;Kalikan total tersebut dengan harga per satu juta token yang tercantum untuk model Anda pada hari saat Anda membacanya. Dengan demikian, Anda memperoleh angka sebenarnya, bukan perkiraan orang lain. Harga yang dipublikasikan dapat berubah. Perhitungannya tidak berubah.
Mengapa backtest mengalami overfitting dan cara mengamatinya
Tulis ulang screen sebagai fungsi dari dua panjang window, lakukan sweep pada grid pasangan nilai, lalu urutkan berdasarkan return. Pasangan terbaik akan terlihat sangat bagus. Itulah masalahnya, bukan hasilnya. Grid yang berisi 200 pasangan berarti 200 eksperimen, dan Anda memilih pasangan yang paling beruntung.
Anda dapat mengamati prosesnya dalam sepuluh menit. Bagi store menjadi dua berdasarkan tanggal. Lakukan sweep pada grid hanya untuk paruh pertama dan catat pemenangnya. Lakukan sweep pada grid yang sama untuk paruh kedua. Jika kedua pemenang berbeda jauh, berarti parameter menyesuaikan noise. Pasangan yang hanya menang pada bagian data yang digunakan untuk tuning tidak memberi informasi tentang hari esok.
Survivorship lebih buruk daripada overfitting karena tuning tidak dapat memperbaikinya. Daftar ticker Anda berisi anggota indeks saat ini, sehingga hanya mencakup perusahaan yang bertahan. Jika Anda meminta feed mengembalikan ticker yang delisted pada 2019, feed tersebut mengembalikan frame kosong. Akibatnya, perusahaan itu tidak pernah masuk ke store dan tidak pernah masuk ke pengujian Anda. Setiap backtest yang Anda jalankan sudah mengecualikan perusahaan yang gagal.
Fundamental yang direvisi merusak timeline. Angka pendapatan yang dikembalikan API saat ini untuk kuartal 2019 tidak selalu merupakan angka yang dipublikasikan pada 2019. Screen yang mencampurkan fundamental saat ini dengan harga 2019 menggunakan informasi yang belum tersedia pada waktu itu. Harga biasanya aman dalam hal ini. Fundamental biasanya tidak.
Harga yang disesuaikan berubah setelah digunakan. Dengan auto_adjust=True, harga penutupan disesuaikan mundur untuk dividen dan stock split, sehingga query yang sama jika dijalankan bulan depan dapat mengembalikan histori yang sedikit berbeda. Menyimpan baris data yang benar-benar Anda gunakan membuat hasil dapat direproduksi. Hal ini juga menjadi alasan lain mengapa local store diperlukan.
Backtest juga mengabaikan komisi dan slippage, serta mengasumsikan order Anda tidak menggerakkan harga. Hal tersebut termasuk dalam execution, yang berada di luar cakupan pembahasan ini dan dijelaskan dalam menjalankan trading bot pada VPS.
Mode kegagalan dan string yang akan Anda lihat
error: externally-managed-environment saat pip berjalan. Anda berada di luar virtual environment. Panggil /opt/research/venv/bin/pip dengan path lengkap.
Could not set lock on file, dengan PID setelahnya. Proses lain sedang membuka file DuckDB untuk ditulis, biasanya shell interaktif yang terlupa masih terbuka. Tutup proses tersebut, atau buka koneksi kedua dengan read_only=True.
Main process exited, code=killed, status=9/KILL di systemctl status. Kernel menghentikan job karena kehabisan memori. Konfirmasikan dengan journalctl -k | grep -i oom, lalu turunkan memory_limit di store.py.
Proses berjalan tanpa error, tetapi tidak menulis apa pun. systemctl status membaca active (exited) dan ukuran tabel tidak bertambah. Feed mengembalikan frame kosong. Kegagalan ini paling sulit terdeteksi, jadi buat job keluar dengan status non-zero ketika semua ticker mengembalikan data kosong.
Timer berjalan pada hari libur bursa. systemd tidak mengetahui kalender bursa, sehingga Mon-Fri mencakup hari libur. Proses tetap berjalan, feed tidak memiliki data baru, dan job harus memperlakukannya sebagai kondisi normal, bukan sebagai error.
429 dari API. Anda telah melampaui batas rate. Anthropic SDK secara otomatis mencoba ulang dengan backoff, dan Anthropic(max_retries=5) meningkatkan jumlah percobaan. Jika tetap gagal setiap hari, job meminta terlalu banyak data dalam satu burst.
Hal yang bukan bagian dari ini
Ini adalah asisten riset. Model yang merangkum dokumen pengajuan menghasilkan interpretasi atas dokumen tersebut dan dapat keliru dengan sangat meyakinkan mengenai angka yang tercetak di dalamnya. Karena itu, setiap angka dalam catatan harus dapat ditelusuri kembali ke baris yang Anda kirimkan. Perlakukan output ini sebagai daftar singkat hal-hal yang perlu Anda baca sendiri. Ini bukan nasihat keuangan, dan tidak satu pun output ini merupakan sinyal.
Backtest berguna untuk menolak suatu ide, tetapi kurang efektif untuk mengonfirmasinya. Strategi yang gagal pada data Anda sendiri memang tidak layak digunakan. Strategi yang lolos hanya berhasil pada data Anda, yang merupakan klaim jauh lebih terbatas daripada kesan yang muncul saat Anda mengujinya pada pukul 1am.
Eksekusi sengaja tidak dibahas. Order dan kredensial broker memiliki profil risiko yang berbeda dari server riset read-only. Mencampurkannya berarti menempatkan kunci trading pada mesin yang sama dengan prompt LLM. Jika Anda ingin melihat posisi pola ini dibandingkan dengan hal-hal lain yang layak dijalankan pada server Anda sendiri, agen AI self-hosted yang layak dijalankan memberikan gambaran yang lebih luas.
FAQ
Apakah saya memerlukan feed data pasar berbayar?
Tidak untuk prototipe. Feed tidak resmi gratis cukup untuk mempelajari bentuk sistem, dan feed tersebut akan mengalami kerusakan karena bergantung pada situs web yang tidak berkewajiban apa pun kepada Anda. Biasanya kerusakan muncul sebagai frame kosong, bukan exception, sehingga tugas Anda harus memeriksa jumlah baris. Beralihlah ke feed berbayar dengan API terdokumentasi dan alamat dukungan setelah data mulai digunakan untuk mengambil keputusan. Store-lah yang membuat penggantian tersebut mudah: hanya fungsi fetch yang berubah, sedangkan schedule, schema, dan screen tetap sama.
Sebaiknya saya menyimpan harga di SQLite atau DuckDB?
DuckDB bersifat columnar dan dirancang untuk memindai banyak baris guna menghitung agregat. Hal ini sesuai dengan perhitungan moving average atas bar selama sepuluh tahun. SQLite berorientasi baris dan lebih baik untuk banyak operasi baca-tulis kecil dari beberapa proses secara bersamaan. Untuk satu scheduled job yang menambahkan beberapa ratus baris lalu memindai jutaan baris, DuckDB lebih sesuai. Jika beberapa proses harus menulis secara bersamaan, SQLite dalam mode WAL memungkinkan pembaca bekerja saat satu penulis melakukan commit, sedangkan busy timeout membuat penulis lain menunggu alih-alih langsung gagal.
Berapa biaya panggilan LLM per bulan?
Catat usage.input_tokens dan usage.output_tokens dari setiap response ke dalam tabel, lalu kalikan total mingguan Anda dengan harga per juta token yang dicantumkan model Anda pada hari pemeriksaan. Itulah satu-satunya angka yang tetap akurat pada kuartal berikutnya. Satu eksekusi harian atas screen pendek hanya memerlukan sedikit panggilan. Panjang note adalah bagian yang dapat Anda kendalikan: membatasi summary hingga 200 kata menghemat lebih banyak daripada mengirim lebih sedikit baris karena token output dihargai lebih tinggi daripada token input pada setiap model Claude.
Mengapa job saya berhasil tetapi tidak menulis baris baru?
Ada dua penyebab umum. Pasar sedang tutup karena schedule systemd Mon-Fri mencakup hari libur bursa. Atau feed mengembalikan frame kosong untuk setiap ticker. Client library sering melaporkan kondisi ini sebagai warning yang dicetak, bukan exception, sehingga proses tetap keluar dengan status 0 dan systemd tetap menampilkan eksekusi berstatus hijau. Bedakan kedua kondisi tersebut dengan membandingkan SELECT max(day) FROM prices dengan hari perdagangan terakhir yang benar-benar berlangsung, lalu buat job keluar dengan status non-zero jika semua ticker kembali kosong.
Dapatkah agent memutuskan apa yang harus dibeli?
Tidak. Membangun agent agar mencoba melakukannya adalah penyebab proyek semacam ini sering gagal. Model tidak memiliki akses pasar, tidak mengetahui posisi atau situasi pajak Anda, dan tidak dapat memeriksa angkanya sendiri terhadap sumber apa pun. Kemampuan model yang berguna adalah membaca banyak teks dan memberi tahu Anda beberapa item yang perlu mendapat perhatian hari ini. Tidak ada yang ditulisnya merupakan nasihat keuangan. Keputusan dan tanggung jawab atas keputusan tersebut tetap berada pada Anda.