Как создать self-hosted агент для анализа акций на VPS
Соберите self-hosted агент на VPS: источник рыночных данных, хранилище DuckDB, таймер systemd после закрытия рынка и LLM-фильтр без исполнения сделок.
Что такое self-hosted агент для анализа акций
Self-hosted агент для анализа акций — это небольшая программа на принадлежащем вам сервере. Она по расписанию получает рыночные данные, сохраняет их в локальной базе данных, фильтрует их и просит большую языковую модель (LLM) описать изменения. Агент только читает и фильтрует данные. Он не совершает сделки, и это руководство не является финансовой рекомендацией.
Два человека, создающие такую систему с нуля, могут выбрать разные библиотеки и всё равно получить одинаковые четыре компонента: источник, который предоставляет цены и фундаментальные показатели; локальное хранилище, в котором сохраняется каждая полученная строка; задание, обновляющее хранилище по таймеру; и слой LLM, преобразующий оставшиеся строки в текст. В этом руководстве такая схема создаётся с помощью Python, DuckDB, таймера systemd и Claude API (application programming interface). Исполнение сделок — отдельное задание с отдельными типами сбоев. Для него лучше использовать VPS, настроенный для торговых ботов.
Четыре компонента и функции каждого
Источник данных — единственный компонент, который взаимодействует с внешним миром. Он умеет запрашивать тикер и диапазон дат, а затем возвращать строки. Все последующие компоненты читают данные из базы, а не из источника. Поэтому отказ источника приводит к потере данных только за один день, а не к неработающему интерфейсу.
Хранилище — основа всей системы. Дневное значение закрытия, которое не было сохранено, обычно можно получить повторно. Внутридневную котировку, оценку до её пересмотра или показатель фундаментального анализа до его повторной публикации получить уже нельзя. Хранилище позволяет сохранять сведения о том, какие именно данные были опубликованы в конкретный день.
Планировщик определяет время обновления. На сервере это systemd timer. Поэтому в данном случае VPS важнее самого кода.
Слой LLM читает короткий текстовый блок, сформированный вашим SQL-запросом, и составляет его сводку. Он не подключается к базе данных и не формирует запрос. Если модель пишет SQL, одна ошибочная лексема превращает число в связный, но неверный текст, который не с чем сравнить. Если SQL формирует числа, модель может ошибиться только в тексте, а вы можете проверить этот текст по переданным ей строкам.
Зачем запускать его на VPS, а не на ноутбуке
Причина — планировщик. Закрытие рынка США в 16:00 по времени Нью-Йорка — это 22:00 в Берлине и 04:00 следующего утра в Джакарте. В это время ноутбук находится в спящем режиме. Пропущенный запуск обходится дороже, чем задержка в записи: дневные бары обычно можно повторно получить позже, но пересмотренные данные восстановить нельзя, поэтому пробел в истории навсегда останется.
Вторая причина менее значительна, но также важна. Сервер хранит один API key в одном файле. Файл принадлежит одному системному пользователю без login shell и используется одной задачей. На ноутбуке, с которого вы также просматриваете веб-сайты, обеспечить такую изоляцию намного сложнее. Храните ключ отдельно от кода и не передавайте его в input модели. Этой теме посвящён материал как не передавать 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 и используйте полученное значение.
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 словами сильнее влияет на стоимость, чем сокращение передаваемых данных. Кэширование промпта не помогает для задачи, выполняемой раз в день, поскольку срок жизни кэша измеряется минутами. К следующему запуску кэшированный блок уже истечёт, и за входные данные снова придётся платить по полной цене. Кэширование окупается, когда один запуск выполняет много вызовов с одним и тем же большим блоком текста.
Установите необходимые компоненты
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 записывать туда данные. Виртуальное окружение — не формальность. Это единственный каталог, в который 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 усредняет все доступные строки, поэтому для третьей строки тикера возвращается среднее за 3 дня, но результат всё равно называется 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, не видит путь к базе данных и не выполняет запросы. Она получает строки и возвращает текст. Благодаря этой границе вывод можно проверить: каждое число в заметке должно также присутствовать в отправленном блоке, и их можно построчно сравнить. Подробнее о составлении промптов рассказывается в материале использование Claude для анализа финансовых данных.
Один запуск от начала до конца
В 16:20 по времени Нью-Йорка timer запускает сервис. refresh.py запрашивает в feed данные по каждому тикеру, начиная с последнего сохранённого дня, записывает для каждого один или два новых бара и выводит отдельную строку для каждого тикера. 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;Умножьте эти итоги на цены за миллион токенов, указанные для вашей модели на день проверки. Так вы получите фактическую сумму, а не чью-либо оценку. Опубликованные цены меняются. Арифметика — нет.
Почему бэктесты переобучаются и как это увидеть
Представьте screen как функцию двух длин окон, переберите сетку пар и ранжируйте их по доходности. Лучшая пара будет выглядеть превосходно. В этом и заключается проблема, а не результат. Сетка из 200 пар — это 200 экспериментов, и вы выбрали самый удачный случай.
Увидеть это можно за десять минут. Разделите store пополам по датам. Переберите сетку только на первой половине и запишите победителя. Затем переберите ту же сетку на второй половине. Если два победителя сильно различаются, параметры подгоняются под шум. Пара, которая выигрывает только на половине, использованной для настройки, ничего не говорит о завтрашнем результате.
Выжившие компании создают более серьёзную проблему, чем переобучение, потому что настройкой её не исправить. В вашем списке тикеров находятся сегодняшние компоненты индекса, поэтому в нём есть только компании, которые сохранились. Если запросить в feed тикер компании, исключённой из листинга в 2019 году, он вернёт пустой frame. В результате эта компания не попадает в store и не участвует в тесте. Каждый запущенный вами бэктест уже исключил неудачные компании.
Пересчитанные фундаментальные показатели нарушают временную последовательность. Значение выручки, которое API возвращает сегодня для квартала 2019 года, не всегда совпадает со значением, опубликованным в 2019 году. screen, объединяющий сегодняшние фундаментальные показатели с ценами за 2019 год, использует информацию, которой тогда ещё не существовало. Цены обычно не создают здесь такой проблемы. Фундаментальные показатели — обычно создают.
Скорректированные цены меняются со временем. При использовании auto_adjust=True цены закрытия пересчитываются назад с учётом дивидендов и сплитов, поэтому тот же запрос, выполненный в следующем месяце, вернёт немного другую историю. Сохранение строк, которые вы фактически использовали, обеспечивает воспроизводимость результата. Это ещё одна причина, по которой вообще нужен локальный store.
Бэктест также не учитывает комиссии и проскальзывание и предполагает, что ваш ордер не влияет на цену. Это относится к исполнению ордеров, которое здесь не рассматривается. Подробнее об этом говорится в разделе запуск торговых ботов на 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) увеличивает число попыток. Если ошибка повторяется каждый день, задание отправляет слишком много запросов за один интервал.
Что это не такое
Это исследовательский помощник. Модель, которая суммирует отчётность, формирует собственное толкование документа и может уверенно ошибиться в числе, напечатанном в тексте. Поэтому каждое значение в заметке должно быть связано с исходной строкой, которую вы передали. Рассматривайте результат как список материалов для самостоятельного изучения. Здесь нет финансовых рекомендаций, и результат не является сигналом.
Бэктесты полезны для отклонения идей, но плохо подходят для их подтверждения. Стратегия, которая не работает на ваших данных, действительно не подходит. Стратегия, которая прошла проверку, выжила только на ваших данных. Это гораздо более ограниченное утверждение, чем может показаться в 1am.
Исполнение ордеров намеренно не рассматривается. Ордера и учётные данные брокера связаны с другим уровнем риска по сравнению с исследовательским сервером только для чтения. Если объединить эти функции, торговые ключи окажутся на той же машине, что и запрос к LLM. Если вы хотите понять, какое место этот подход занимает рядом с другими решениями, которые стоит запускать на собственном сервере, ознакомьтесь с самостоятельно размещаемыми 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 с последним фактическим торговым днём. Если данные по всем тикерам пусты, задание должно завершаться с ненулевым кодом.
Может ли агент решить, что покупать?
Нет. Попытка наделить его такой функцией часто приводит к проблемам в подобных проектах. У модели нет доступа к рынку, информации о вашей позиции и налоговой ситуации. Она также не может проверить собственные расчёты по внешним данным. Модель хорошо читает большие объёмы текста и сообщает, какие несколько объектов заслуживают вашего внимания сегодня. Всё, что она пишет, не является финансовой рекомендацией. Решение и ответственность за него остаются за вами.