Агент для дослідження акцій на власному VPS
Зберіть self-hosted агента для акцій на VPS: ринкові дані, DuckDB, таймер systemd після закриття ринку та LLM-фільтр без виконання угод.
Що таке self-hosted агент для дослідження акцій
Self-hosted агент для дослідження акцій — це невелика програма на сервері, яким ви керуєте. Вона за розкладом отримує ринкові дані, зберігає їх у локальній базі даних, фільтрує їх і просить велику мовну модель (LLM) описати зміни. Агент лише читає та фільтрує дані. Він не виконує торгівлю, і цей посібник не є фінансовою консультацією.
Двоє людей, які створюють таку систему з нуля, можуть вибрати різні бібліотеки, але все одно отримають однакову структуру з чотирьох частин: джерело, яке надає ціни та фундаментальні показники; локальне сховище, що зберігає кожен отриманий рядок; завдання, яке оновлює сховище за таймером; і рівень LLM, який перетворює відібрані рядки на речення. У цьому посібнику цю структуру створено за допомогою Python, DuckDB, таймера systemd і Claude API (програмного інтерфейсу застосунку). Виконання угод — це окреме завдання з окремими режимами відмови. Його слід розміщувати на VPS, налаштованому для торгових ботів.
Чотири частини та призначення кожної
Канал даних — єдина частина, яка взаємодіє із зовнішнім світом. Він уміє запитувати тикер і діапазон дат, а потім повертати рядки. Усі наступні компоненти читають дані з вашої бази даних, а не з каналу. Тому збій каналу коштує вам одного дня нових даних, а не призводить до непрацездатності всього інтерфейсу.
Сховище — основна причина всієї цієї роботи. Денне значення закриття, яке ви не записали, зазвичай можна отримати пізніше. Внутрішньоденне котирування, оцінку до її перегляду або показник фундаментальних даних до його ретроспективного перерахунку відновити неможливо. Сховище дає змогу створити запис про те, які саме дані були доступні на певний день.
Планувальник визначає час оновлення. На сервері це systemd timer, тому VPS у цій схемі важливіший за код.
Рівень LLM читає короткий текстовий блок, сформований вашим SQL, і створює його резюме. Він ніколи не підключається до бази даних і не формує запит. Якщо модель пише SQL, один неправильний токен перетворюється на неправильне число всередині переконливого речення, і його немає з чим порівняти. Якщо SQL формує числа, модель може помилитися лише в тексті, а ви можете перевірити цей текст за рядками, які їй передали.
Чому запускати його на VPS, а не на ноутбуці
Причина — планувальник. Закриття ринку США о 16:00 за нью-йоркським часом — це 22:00 у Берліні та 04:00 наступного ранку в Джакарті. У цей час ноутбук не працює або перебуває в режимі сну. Пропущений запуск коштує дорожче, ніж затримка сповіщення: денні бари зазвичай можна повторно отримати пізніше, але дані, які було змінено під час перегляду, відновити неможливо, тому прогалина в історії залишається назавжди. Це саме стосується будь-якого агента, розклад і збережений стан якого мають переживати перезавантаження. Саме на цьому ґрунтується запуск KiroCrew як агента, що постійно працює на власному VPS.
Друга причина менш важлива, але також реальна. На сервері зберігається один API key в одному файлі. Власником файлу є один системний користувач без оболонки входу. Ключ використовує одне завдання. На ноутбуці, який ви також використовуєте для перегляду вебсторінок, організувати це набагато складніше. Зберігайте ключ окремо від коду та не передавайте його до вхідних даних моделі. Цьому присвячено матеріал як не передавати API keys AI-агенту.
Розрахунок ресурсів: диск, RAM і токени
Диск — найпростіша частина. Один денний бар — це один рядок для кожного тикера за кожен торговий день, а в торговому році у США приблизно 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 рядків. Кожен рядок містить дату та кілька значень double. DuckDB зберігає стовпці у стисненому вигляді, тому обсяг становитиме десятки мегабайтів, а не гігабайти. Не покладайтеся на цю оцінку, зокрема й на мою. Виконайте du -h /opt/research/data/market.duckdb після першого backfill і використовуйте власне значення.
RAM — це місце, де малий VPS швидко стикається з обмеженнями. За замовчуванням DuckDB використовує значну частину пам’яті машини та всі її ядра для одного запиту. Це правильно для аналітичного сервера, але неправильно для машини з 2 GB RAM, яка також запускає інші процеси. Тоді одна агрегація всієї таблиці цін може завершитися примусовим завершенням процесу ядром, а 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. Якщо пропустити віртуальне середовище та виконати pip install для системного Python, 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 самостійно розбирає цей файл, а не передає його shell:
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 перезаписує рядок, який уже існує для цього тикера та цього дня, тому повторне виконання backfill не дублює дані в таблиці. Без цього ключа повторний запуск після збою непомітно дублює кожен бар, а всі подальші середні значення будуть неправильними. Жодне повідомлення про помилку не вкаже на проблему.
DuckDB дає змогу лише одному процесу відкрити файл для запису. Другий процес запису одразу завершується з помилкою Could not set lock on file, після якої вказується PID процесу, що утримує файл. На практиці це інтерактивна оболонка duckdb, яку ви залишили відкритою в іншому терміналі. Процеси читання проходять через read_only=True, тому connect використовує цей прапорець. Якщо кільком процесам справді потрібно одночасно виконувати запис, слід використовувати інший рушій: SQLite у режимі WAL дає змогу читачам працювати, поки один процес фіксує запис, а тайм-аут очікування звільнення ресурсу змушує інші процеси запису чекати замість негайного завершення з помилкою. У матеріалі DuckDB і SQLite для серверного навантаження наведено порівняння цих двох рушіїв, а в матеріалі запуск SQLite у production на VPS описано параметри, за яких WAL працює належним чином.
Завдання оновлення
# /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Перший запуск завантажує дані за попередні роки та виводить по одному рядку для кожного тикера з кількістю записів у тисячах. Запустіть його знову через хвилину. Тоді в кожному рядку буде 1 або 2 записи, оскільки завдання починає з останнього вже збереженого дня. Саме другий запуск є фактичною перевіркою. Якщо кількість записів знову обчислюється тисячами, max(day) нічого не повертає, а операція вставки щоночі переписує всю історію.
Перевірка df.empty є найважливішим рядком у файлі. Ticker.history() не генерує виняток для неправильного символу або символу, вилученого з лістингу. Вона виводить попередження про те, що символ, можливо, більше не лістингується, а даних про ціни не знайдено (точне формулювання залежить від версії бібліотеки), і повертає порожній DataFrame. Завдання без цієї перевірки нічого не записує, завершується з кодом 0, а systemd показує успішний запуск, хоча таблиця непомітно перестає зростати. Ви дізнаєтеся про це лише через кілька тижнів, коли екран почне щодня повертати той самий набір рядків.
Обробка часових міток також має важливе обґрунтування. Індекс, який повертає джерело даних, може містити часовий пояс біржі. Видалення зміщення часу не те саме, що конвертація в UTC. Сесія в Токіо з часовою міткою опівночі за місцевим часом після конвертації в UTC припадає на попередню календарну дату. Тому конвертація в UTC непомітно зміщує кожну японську свічку на один день назад і порушує первинний ключ. tz_localize(None) зберігає власну дату біржової сесії, саме це й означає денна свічка.
Таймер, який запускається після закриття ринку
Ринок США закривається о 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. Він запускає їх послідовно й зупиняється, якщо один із них завершується з ненульовим кодом. Це саме та поведінка, яка потрібна: після невдалого оновлення не можна показувати екран зі застарілими даними. Persistent=true важливий на VPS, який перезавантажується через оновлення ядра. Якщо виконати перезавантаження о 16:15 без нього, запуск буде пропущено. З ним завдання запуститься, щойно машина знову стане доступною.
sudo systemctl daemon-reload
sudo systemctl enable --now research-refresh.timer
systemctl list-timers research-refresh.timerlist-timers має показати стовпце NEXT із часом наступного запуску в будній день, перетвореним на локальний час самого сервера. Порожній список означає, що таймер не ввімкнено або в unit відсутня секція [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 обчислює середнє для всіх наявних рядків, тому третій рядок тикера повертає середнє за три дні, але все одно називає його ma50. Якщо порівняти це значення з ma20, на початку історії кожного тикера буде створено перетин, якого насправді не було. Фільтрація за номером рядка вилучає рядки, для яких вікно ще не було заповнене повністю.
Що бачить модель, а чого вона ніколи не бачить
# /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()Обмеження кількості слів у системному промпті обмежує найдорожчу частину рахунка. Вимогу використовувати лише надані рядки потрібно перевіряти, а не приймати на віру: видаліть один стовпець із блока, запустіть обробку ще раз і прочитайте результат. Якщо значення для цього стовпця все одно з’явиться, модель заповнила відсутні дані, а ваш промпт недостатньо чіткий. Така перевірка займає дві хвилини. Це єдиний надійний спосіб з’ясувати, що відбувається.
Модель не бачить API key, не бачить шляху до бази даних і не виконує запитів. Вона отримує рядки та повертає текст. Саме ця межа робить результат придатним для перевірки: кожне число в нотатці також має бути в надісланому блоці, тому їх можна порівняти рядок за рядком. Докладніше про prompting див. у матеріалі використання Claude для фінансового аналізу, де детальніше розглянуто, які дані модель добре читає.
Один запуск від початку до кінця
О 16:20 за нью-йоркським часом таймер запускає сервіс. refresh.py запитує стрічку для кожного тикера, починаючи з останнього збереженого дня, записує для кожного один або два нові бари та виводить окремий рядок для кожного тикера. screen.py відкриває той самий файл, виконує запит для ковзного середнього та отримує кілька рядків. Ці рядки перетворюються на текстовий блок обсягом у кількасот токенів. Один API-виклик перетворює його на коротку нотатку. Нотатка потрапляє до журналу, а один рядок записується в runs із кількістю токенів і числом збігів.
journalctl -u research-refresh.service -n 50 --no-pagerУ справному журналі є один рядок для кожного тикера, потім нотатка, а потім research-refresh.service: Deactivated successfully. Через тиждень отримайте фактичні витрати з бази даних:
SELECT count(*) AS runs,
sum(input_tokens) AS in_tokens,
sum(output_tokens) AS out_tokens
FROM runs;Помножте ці підсумки на ціни за мільйон токенів, указані для вашої моделі на день перевірки. Так ви отримаєте фактичну суму, а не чиюсь оцінку. Опубліковані ціни змінюються. Арифметика — ні.
Чому backtest надмірно підганяється під дані та як це побачити
Перепишіть screen як функцію двох довжин вікна, переберіть сітку пар і ранжуйте їх за дохідністю. Найкраща пара виглядатиме чудово. У цьому й полягає проблема, а не результат. Сітка з 200 пар — це 200 експериментів, і ви залишили найвдаліший випадок.
Побачити це можна за десять хвилин. Розділіть store навпіл за датою. Переберіть сітку лише на першій половині та запишіть переможця. Переберіть ту саму сітку на другій половині. Якщо два переможці суттєво відрізняються, параметри підганяються під шум, а пара, яка перемагає лише на половині, на якій ви її налаштували, нічого не говорить про завтрашній день.
Survivorship гірший за надмірне підганяння, оскільки налаштування його не виправить. Ваш список тикерів містить учасників індексу на сьогодні, тому до нього входять лише компанії, які вижили. Якщо запитати feed про тикер, який було виключено з лістингу у 2019, він поверне порожній frame. Це означає, що така компанія не потрапляє до вашого store і не бере участі у вашому тесті. Кожен запущений вами backtest уже виключив невдалі випадки.
Restated fundamentals порушують часову послідовність. Показник виручки, який API повертає сьогодні для кварталу 2019 року, не завжди є показником, опублікованим у 2019 році. Screen, що поєднує сьогоднішні фундаментальні показники з цінами 2019 року, використовує інформацію, якої тоді ще не існувало. Ціни зазвичай безпечні в цьому аспекті. Фундаментальні показники — зазвичай ні.
Adjusted prices змінюються з часом. З auto_adjust=True ціни закриття коригуються назад з урахуванням дивідендів і дроблень акцій, тому той самий запит, виконаний наступного місяця, поверне дещо іншу історію. Збереження фактичних рядків, які ви використовували, забезпечує відтворюваність результату. Це ще одна причина, чому local store взагалі потрібен.
Backtest також ігнорує комісії та прослизання і припускає, що ваш ордер не впливає на ціну. Це належить до виконання ордерів, яке тут не розглядається. Докладніше про нього див. у запуск trading bots на VPS.
Режими відмови та повідомлення, які ви побачите
error: externally-managed-environment під час запуску pip. Ви перебуваєте за межами віртуального середовища. Викличте /opt/research/venv/bin/pip за повним шляхом.
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, а потім зменште memory_limit у store.py.
Успішний запуск, який нічого не записує. systemctl status читає active (exited), але таблиця не збільшилася. Джерело даних повернуло порожні фрейми. Таку помилку найважче помітити, тому завдання має завершуватися з ненульовим кодом, якщо для всіх тикерів повернуто порожні результати.
Таймер спрацював у біржове свято. systemd не знає календаря біржі, тому Mon-Fri включає святкові дні. Запуск відбувається, але джерело даних не повертає нічого нового. Завдання має вважати це нормальною ситуацією, а не помилкою.
429 від API. Ви перевищили ліміт запитів. Anthropic SDK самостійно повторює запити з поступовим збільшенням інтервалу, а Anthropic(max_retries=5) збільшує кількість спроб. Якщо помилка виникає щодня, завдання надсилає надто багато запитів одним пакетом.
Що це не таке
Це дослідницький асистент. Модель, яка підсумовує звітність, формує власну інтерпретацію цього документа. Вона може впевнено помилитися щодо числа, надрукованого в тексті. Тому кожне число в нотатці має вести до рядка, який ви надіслали. Розглядайте результат як список матеріалів для самостійного ознайомлення. Цей матеріал не є фінансовою порадою і не містить жодного сигналу.
Бектести корисні для відхилення ідей, але малопридатні для їх підтвердження. Стратегія, яка не працює на ваших даних, справді непридатна. Стратегія, яка пройшла перевірку, лише витримала тест на ваших даних. Це набагато слабше твердження, ніж може здаватися о 1:00 ночі.
Виконання операцій навмисно залишається поза межами цього матеріалу. Накази та облікові дані брокера мають інший профіль ризику, ніж read-only дослідницький сервер. Їхнє поєднання розміщує ключі для торгівлі на тому самому комп’ютері, що й prompt для LLM. Якщо ви хочете побачити, як цей підхід співвідноситься з іншими корисними інструментами для запуску на власному сервері, у ширшому огляді self-hosted AI-агентів, які варто запускати.
FAQ
Чи потрібен мені платний канал ринкових даних?
Не для прототипу. Безкоштовний неофіційний канал підходить для вивчення структури системи, але він перестане працювати, оскільки залежить від вебсайту, який вам нічого не винен. Зазвичай він повертає порожні фрейми, а не виняток, тому завдання має перевіряти кількість рядків. Коли дані почнуть впливати на рішення, перейдіть на платний канал із документованим API та адресою служби підтримки. Саме сховище робить таку заміну недорогою: змінюється лише функція отримання даних, а розклад, схема й інтерфейс залишаються без змін.
Зберігати ціни в SQLite чи DuckDB?
DuckDB використовує колонкове зберігання та призначений для сканування великої кількості рядків під час обчислення агрегатів. Це саме той випадок, що потрібен для ковзного середнього за десять років біржових свічок. SQLite використовує рядкове зберігання й краще підходить для численних невеликих операцій читання та запису з кількох процесів одночасно. Для одного запланованого завдання, яке додає кілька сотень рядків, а потім сканує мільйони, DuckDB підходить краще. Якщо кілька процесів мають записувати одночасно, SQLite у режимі WAL дає змогу читачам працювати, поки один записувач фіксує транзакцію, а тайм-аут очікування звільнення дає іншим записувачам змогу зачекати замість негайного завершення з помилкою.
Скільки коштують виклики LLM на місяць?
Записуйте usage.input_tokens і usage.output_tokens з кожної відповіді в таблицю, а потім помножте тижневі підсумки на ціну за мільйон токенів, яку ваша модель вказує на день перевірки. Це єдина сума, яка залишатиметься актуальною наступного кварталу. Один щоденний запуск для короткого списку перевірки створює небагато викликів. Довжина нотатки — параметр, який ви контролюєте: обмеження підсумку 200 словами заощаджує більше, ніж зменшення кількості рядків, оскільки в кожній моделі Claude вихідні токени коштують дорожче за вхідні.
Чому моє завдання завершилося успішно, але не записало нових рядків?
Є дві звичайні причини. Ринок був закритий, оскільки розклад systemd Mon-Fri враховує біржові свята. Або канал даних повернув порожній фрейм для кожного тикера. Клієнтські бібліотеки часто повідомляють про це попередженням у виводі, а не винятком, тому процес завершується з кодом 0, а systemd показує успішний запуск. Розрізнити ці випадки можна, порівнявши SELECT max(day) FROM prices з останнім фактичним торговим днем. Якщо дані для всіх тикерів порожні, завдання має завершуватися з ненульовим кодом.
Чи може агент вирішувати, що купувати?
Ні. Саме спроба створити його для цього призводить до помилок у таких проєктах. Модель не має доступу до ринку, не бачить ваших позицій або податкової ситуації та не може перевірити власні розрахунки за зовнішніми даними. Вона добре читає великі обсяги тексту й повідомляє, які кілька елементів сьогодні заслуговують на вашу увагу. Усе, що вона створює, не є фінансовою порадою. Рішення та відповідальність за нього залишаються за вами.