Как создать self-hosted AI-агента для анализа акций
Руководство по созданию системы для анализа рынка на VPS. Вы настроите сбор данных, хранилище DuckDB, автоматизацию через systemd timer и интеграцию с LLM для отчетов.
Что такое self-hosted агент для анализа акций
Self-hosted агент для анализа акций — это небольшая программа на вашем собственном сервере, которая по расписанию получает рыночные данные, сохраняет их в локальную базу данных, выполняет скрининг и запрашивает у большой языковой модели (LLM) отчет об изменениях. Агент считывает и фильтрует информацию. Он не совершает сделок, и ничто в этом руководстве не является финансовой рекомендацией.
Два человека, создающие такую систему с нуля, выберут разные библиотеки, но всё равно получат одни и те же четыре компонента: источник данных, который предоставляет цены и фундаментальные показатели; локальное хранилище, в котором сохраняется каждая когда-либо загруженная строка; задачу, которая обновляет хранилище по расписанию; и слой LLM, который превращает оставшиеся строки в предложения. В этом руководстве такая структура создаётся на основе Python, DuckDB, таймера systemd и Claude API (программного интерфейса приложения). Это узкоспециализированный агент с фиксированным циклом. Если вы сначала хотите ознакомиться с общей версией, поэтапный путь изучения AI-агентов последовательно разбирает цикл, инструменты и память, прежде чем всё это превращается в предметную задачу. Выполнение — отдельная задача с отдельными типами сбоев. Для неё лучше использовать VPS, настроенный для торговых ботов.
Четыре компонента и их назначение
Лента (feed) — это единственный компонент, взаимодействующий с внешним миром. Она умеет запрашивать тикер и диапазон дат, а также возвращать строки данных. Все последующие компоненты считывают данные из вашей базы, а не из ленты, поэтому сбой в работе ленты приведет лишь к потере данных за один день, а не к неработоспособности всего интерфейса.
Хранилище (store) — это главная цель всей системы. Дневную цену закрытия, которую вы не записали, обычно можно получить позже. Внутридневную котировку, оценку до её пересмотра или фундаментальный показатель до внесения правок — нельзя. Хранилище позволяет создать архив того, что данные сообщали в день их публикации.
Планировщик (scheduler) определяет время обновления. На сервере это systemd timer, поэтому VPS здесь важнее, чем сам программный код.
Уровень LLM (LLM layer) считывает короткий текстовый блок, сформированный вашим SQL-запросом, и пишет его резюме. Он никогда не подключается к базе данных и не составляет запросы. Если модель сама пишет SQL, один неверный токен превращается в неверное число внутри связного предложения, которое не с чем сравнить. Если числа предоставляет SQL, модель может ошибиться только в тексте, а вы сможете проверить этот текст по строкам, которые отправили на обработку.
Почему стоит запускать это на VPS, а не на ноутбуке
Причина — планировщик задач. Закрытие торгов на рынке США в 16:00 по нью-йоркскому времени соответствует 22:00 в Берлине и 04:00 следующего дня в Джакарте. В это время ноутбук обычно находится в спящем режиме. Пропуск запуска обходится дороже, чем просто несвоевременное уведомление: дневные свечи обычно можно догрузить позже, но данные, которые подвергаются пересмотру, восстановить нельзя, поэтому пробел в истории останется навсегда. Это же касается любого агента, чье расписание и сохраненное состояние должны пережить перезагрузку системы — именно на этом строится запуск KiroCrew в качестве постоянно работающего агента на собственном VPS.
Вторая причина менее значима, но все еще актуальна. На сервере хранится один API key, в одном файле, владельцем которого является системный пользователь без доступа к командной оболочке, и используется он только одной задачей. Такую конфигурацию гораздо сложнее реализовать на ноутбуке, который вы также используете для веб-серфинга. Храните ключ отдельно от кода и входных данных модели, что является темой безопасного хранения API keys в AI-агентах.
Расчет ресурсов: диск, оперативная память и токены
С диском все просто. Одна дневная запись — это одна строка на каждый тикер в торговый день, а в торговом году в США примерно 252 дня.
Данные, на которых построен график
[
{
"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 строк через десять лет. Каждая строка состоит из даты и нескольких чисел с плавающей запятой, а DuckDB хранит столбцы в сжатом виде, поэтому речь идет о десятках мегабайт, а не гигабайтах. Не доверяйте этой оценке, включая мою. Запустите du -h /opt/research/data/market.duckdb после первого заполнения базы и используйте собственные данные.
Оперативная память — это то, на чем спотыкается небольшой VPS. По умолчанию DuckDB занимает значительную часть памяти машины и все ее ядра для одного запроса. Это оправдано на аналитическом сервере, но недопустимо на машине с 2 GB RAM, где работают и другие процессы. Одна агрегация по всей таблице цен приведет к тому, что ядро принудительно завершит процесс, systemd сообщит Main process exited, code=killed, status=9/KILL, а journalctl -k покажет, что процесс был убит из-за нехватки памяти (OOM kill). Установите memory_limit и threads явно, тогда запрос будет выполняться медленнее, но не будет прерван.
Токены нужно измерять, а не оценивать. Каждый ответ Messages API содержит объект usage с полями input_tokens и output_tokens. Записывайте оба значения в таблицу при каждом вызове, и через неделю вы будете знать свой реальный объем. Умножьте его на стоимость, указанную для вашей модели на день проверки. Две вещи достаточно стабильны, чтобы планировать бюджет. Выходные токены стоят дороже входных в любой модели Claude, поэтому ограничение заметки 200 словами сильнее влияет на счет, чем сокращение объема отправляемых данных. Кэширование промптов (prompt caching) не помогает при запуске задачи раз в день. Кэш живет 5 минут по умолчанию, а максимальный срок жизни, предлагаемый Anthropic, составляет 1 час. Это стоит 2x от базовой цены ввода, вместо 1.25x за 5-минутный кэш. В любом случае, к моменту запуска на следующий день блок в кэше истечет, и вы снова заплатите полную цену за ввод. Кэширование выгодно, когда один запуск делает множество вызовов с одним и тем же большим блоком текста.
Установка компонентов
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 считывает этот файл самостоятельно, не передавая его оболочке:
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 перезаписывает существующую строку для данного тикера и дня, поэтому повторный запуск заполнения данных не приводит к дублированию записей в таблице. Без ключа повторный запуск после сбоя незаметно продублирует каждый бар, и все последующие вычисления средних значений будут неверными, при этом система не выдаст никаких сообщений об ошибках.
DuckDB позволяет только одному процессу открывать файл для записи. Второй процесс записи немедленно завершается с ошибкой Could not set lock on file, за которой следует PID процесса, удерживающего файл; на практике это часто оказывается интерактивная оболочка duckdb, оставленная открытой в другом терминале. Соединение в режиме только для чтения не решает эту проблему. Документация по параллелизму DuckDB описывает два режима: один процесс, который читает и пишет, или несколько процессов, которые только читают, но никогда не смешивают эти операции. connect принимает флаг read_only, позволяющий нескольким читателям совместно использовать файл, что работает только до тех пор, пока ни один процесс записи не открыл его. Если нескольким процессам действительно необходимо записывать данные одновременно, это задача для другого движка: SQLite в режиме WAL позволяет читателям работать, пока один процесс записи фиксирует транзакцию, а параметр busy timeout заставляет другие процессы записи ждать, вместо того чтобы завершаться с ошибкой. 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) не возвращает данные, и операция insert каждую ночь перезаписывает всю вашу историю.
Проверка df.empty — самая важная строка в файле. Ticker.history() не вызывает исключение при неверном или исключённом из листинга символе. Библиотека выводит предупреждение о том, что символ, возможно, исключён из листинга и данные о цене не найдены (точная формулировка зависит от версии библиотеки), и возвращает пустой DataFrame. Задача без этой проверки ничего не записывает, завершается с кодом 0, а systemd показывает успешный статус, в то время как таблица перестаёт пополняться. Вы узнаете об этом только через несколько недель, когда увидите, что на экране каждый день отображаются одни и те же строки.
Обработка временных меток также имеет смысл. Индекс, который возвращает поток данных, может содержать часовой пояс биржи, и отбрасывание смещения — это не то же самое, что преобразование в UTC. Токийская сессия, отмеченная полночью по местному времени, при конвертации в UTC переносится на предыдущий календарный день. Таким образом, преобразование в UTC незаметно сдвигает каждый японский бар на один день назад и нарушает работу первичного ключа. tz_localize(None) сохраняет дату сессии самой биржи, что и является определением дневного бара.
Таймер, срабатывающий при закрытии рынка
Американский рынок закрывается в 16:00 по времени Нью-Йорка, а последние сделки обрабатываются в течение нескольких минут, поэтому задача запускается в 16:20. Указывайте время по Нью-Йорку, а не по UTC. Нью-Йорк находится в часовом поясе UTC-5 зимой и UTC-4 летом, поэтому таймер, привязанный к фиксированному часу UTC, будет смещаться на час дважды в год и начнет срабатывать до закрытия торгов. systemd версии 235 и выше поддерживает указание часового пояса непосредственно в 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 приведет к пропуску запуска; с ним задача выполнится сразу после того, как машина вернется в сеть. Агенту, который должен работать постоянно, а не запускаться и завершаться, требуется иная структура юнита: долгоживущий процесс с правилами Restart= и без таймера. Подробности описаны в запуске dsh в фоновом режиме на VPS с помощью systemd.
sudo systemctl daemon-reload
sudo systemctl enable --now research-refresh.timer
systemctl list-timers research-refresh.timerlist-timers должен отображать столбец NEXT, содержащий время следующего запуска в будний день, преобразованное в локальное время сервера. Пустой список означает, что таймер не включен или в юните отсутствует секция [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,
thinking={'type': 'disabled'},
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()Строка thinking имеет значение для этой модели. Claude Sonnet 5 по умолчанию использует процесс мышления, его токены мышления тарифицируются как выходные данные и учитываются в max_tokens, а также возвращаются в виде блока перед основным текстом. Без этой строки resp.content[0] может оказаться блоком мышления, и поиск .text завершится ошибкой. Claude Sonnet 5 теперь является устаревшей моделью. Её преемник, claude-sonnet-5-5, отклоняет disabled, и для него следует использовать настройку thinking={'type': 'between_tools'}, которая отключает мышление перед формированием ответа.
Ограничение количества слов в системном промпте позволяет контролировать наиболее затратную часть счета. Инструкцию использовать только предоставленные строки необходимо проверять, а не принимать на веру: удалите один столбец из блока, запустите процесс снова и изучите результат. Если значение для этого столбца всё равно отображается, значит, модель заполнила пробел самостоятельно, и ваш промпт недостаточно строг. Этот тест занимает две минуты и является единственным честным способом проверки.
Модель никогда не видит API key, не видит путь к базе данных и никогда не выполняет запросы. Она получает строки и возвращает текст. Эта граница делает результат проверяемым, так как каждое число в заметке должно присутствовать в отправленном вами блоке, и вы можете сравнить их строка за строкой. Как только вы расширяете входные данные, добавляя страницы из Интернета, форма границы меняется, поскольку заголовок или пост на форуме могут содержать инструкции для модели, а не факты для вас. Именно такую поверхность атаки открывает предоставление агенту поискового бэкенда SearXNG. Подробнее о составлении промптов для таких задач можно узнать в использовании Claude для финансового анализа, где глубже рассматриваются возможности модели по чтению данных.
Один цикл, от начала до конца
В 16:20 по нью-йоркскому времени таймер запускает сервис. refresh.py запрашивает у ленты данные по каждому тикеру, начиная с последнего сохранённого дня, записывает по одному или два новых бара для каждого и выводит по одной строке на тикер. screen.py открывает тот же файл, выполняет запрос скользящего среднего и получает несколько строк. Эти строки превращаются в текстовый блок из нескольких сотен токенов. Один вызов API превращает их в краткую заметку, заметка отправляется в журнал, а одна строка с количеством токенов и числом совпадений попадает в runs.
journalctl -u research-refresh.service -n 50 --no-pagerКорректный лог содержит по одной строке на тикер, затем заметку, а после — research-refresh.service: Deactivated successfully. Если вы читаете этот лог из сессии Claude Code на сервере, пока вторая сессия работает с экранным запросом, они могут передавать текст друг другу, вместо того чтобы вы копировали строки между терминалами. Через неделю считайте свои расходы из базы данных:
SELECT count(*) AS runs,
sum(input_tokens) AS in_tokens,
sum(output_tokens) AS out_tokens
FROM runs;Умножьте эти итоговые значения на стоимость миллиона токенов, которую ваша модель указывает в день проверки. Это даст вам реальную цифру вместо чьих-то оценок. Опубликованные цены меняются. Арифметика — нет.
Почему бэктесты переобучаются и как это заметить
Представьте экран как функцию от двух длин окон, просканируйте сетку пар и отранжируйте их по доходности. Лучшая пара будет выглядеть превосходно. Это проблема, а не результат. Сетка из 200 пар — это 200 экспериментов, и вы просто выбрали самый удачный.
Вы можете увидеть это за десять минут. Разделите хранилище данных пополам по дате. Просканируйте сетку только на первой половине и запишите победителя. Просканируйте ту же сетку на второй половине. Если победители сильно различаются, параметры подстраиваются под шум, а пара, которая выигрывает только на той половине, где вы её настраивали, ничего не говорит о завтрашнем дне.
Ошибка выжившего хуже переобучения, потому что настройкой её не исправить. Ваш список тикеров состоит из текущих участников индекса, поэтому он содержит только те компании, которые выжили. Если запросить у источника данных тикер, который был исключен из листинга в 2019 году, вы получите пустой фрейм. Это значит, что компания никогда не попадает в ваше хранилище и никогда не участвует в тесте. Каждый ваш бэктест уже исключил неудачников.
Пересмотренные фундаментальные показатели нарушают хронологию. Показатель выручки, который API возвращает сегодня за квартал 2019 года, не всегда совпадает с цифрой, опубликованной в 2019 году. Скрининг, смешивающий сегодняшние фундаментальные показатели с ценами 2019 года, использует информацию, которой тогда не существовало. С ценами здесь обычно всё в порядке. С фундаментальными показателями — как правило, нет.
Скорректированные цены меняются под вами. При использовании auto_adjust=True цены закрытия корректируются назад с учетом дивидендов и сплитов, поэтому тот же запрос, выполненный в следующем месяце, вернет немного другую историю. Сохранение строк, которые вы фактически использовали, делает результат воспроизводимым, и это еще одна причина, по которой существует локальное хранилище.
Бэктест также игнорирует комиссии и проскальзывание, а также предполагает, что ваш ордер не двигает цену. Это относится к исполнению, которое выходит за рамки данной темы и рассматривается в запуске торговых ботов на VPS.
Типичные сбои и сообщения об ошибках
error: externally-managed-environment при запуске pip. Вы находитесь вне виртуального окружения. Вызывайте /opt/research/venv/bin/pip по полному пути.
Could not set lock on file с указанием PID. Другой процесс удерживает файл DuckDB открытым для записи, обычно это интерактивная оболочка, которую вы забыли закрыть. Закройте её. Открытие второго соединения с помощью read_only=True не поможет, пока активен процесс записи, так как DuckDB разрешает либо одновременное чтение несколькими процессами, либо запись одним процессом, но не то и другое одновременно.
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. Вы превысили лимит запросов. SDK Anthropic самостоятельно выполняет повторные попытки с экспоненциальной задержкой, а Anthropic(max_retries=5) увеличивает счетчик попыток. Если ошибка повторяется ежедневно, значит, задание запрашивает слишком много данных за один раз.
Чем это не является
Это исследовательский помощник. Модель, составляющая резюме по документу, лишь интерпретирует его содержание и может ошибаться в цифрах, приведенных в тексте. Поэтому каждое число в заметке должно быть проверено по исходным данным, которые вы предоставили. Рассматривайте результат как список материалов для самостоятельного изучения. Это не является финансовой консультацией или торговым сигналом.
Бэктесты полезны для отсеивания нерабочих идей, но слабы для подтверждения их эффективности. Стратегия, которая не проходит проверку на ваших данных, однозначно нежизнеспособна. Стратегия, которая прошла проверку, лишь доказала свою состоятельность на конкретном наборе данных, что гораздо менее значимо, чем кажется в час ночи.
Исполнение торговых операций намеренно вынесено за рамки данного руководства. Ордера и учетные данные брокера несут иные риски, чем среда для чтения данных, и их совмещение означает хранение торговых ключей на той же машине, где обрабатываются запросы к LLM. Если вы хотите узнать, как этот шаблон соотносится с другими решениями для вашего сервера, ознакомьтесь с обзором полезных self-hosted AI-агентов.
FAQ
Нужна ли мне платная лента рыночных данных?
Для прототипа — нет. Бесплатная неофициальная лента подходит для изучения структуры системы, но она будет ломаться, так как зависит от сайта, который вам ничем не обязан. Обычно она выдает пустые кадры вместо исключений, поэтому ваша задача — проверять количество строк. Переходите на платную ленту с документированным API и адресом техподдержки, как только данные начнут влиять на принятие решений. Хранилище делает такую замену дешевой: меняется только функция получения данных, а расписание, схема и экран остаются прежними.
Где хранить цены: в SQLite или DuckDB?
DuckDB — колоночная база, созданная для сканирования множества строк при вычислении агрегатов, что идеально подходит для скользящего среднего по десятилетним барам. SQLite ориентирована на строки и лучше справляется с множеством мелких операций чтения и записи из нескольких процессов одновременно. Для одиночного запланированного задания, которое добавляет несколько сотен строк и сканирует миллионы, DuckDB подходит лучше. Если несколько процессов должны писать данные одновременно, SQLite в режиме WAL позволяет читателям работать, пока один писатель фиксирует транзакцию, а busy timeout заставляет других писателей ждать, вместо того чтобы сразу выдавать ошибку.
Сколько стоят вызовы LLM в месяц?
Записывайте usage.input_tokens и usage.output_tokens из каждого ответа в таблицу, затем умножайте еженедельные итоги на цену за миллион токенов, указанную для вашей модели на день проверки. Это единственная цифра, которая останется актуальной в следующем квартале. Один ежедневный запуск по короткому списку — это небольшое количество вызовов, и объем заметки — это то, что вы контролируете: ограничение резюме 200 словами экономит больше, чем отправка меньшего количества строк, так как выходные токены во всех моделях Claude стоят дороже входных.
Почему задание выполнилось успешно, но не записало новые строки?
Есть две обычные причины. Рынок был закрыт, так как расписание Mon-Fri в systemd учитывает праздничные дни биржи. Либо лента вернула пустой кадр для каждого тикера, что клиентские библиотеки часто сообщают как предупреждение в консоли, а не как исключение, поэтому процесс завершается с кодом 0, а systemd показывает успешный запуск. Различайте эти ситуации, сравнивая SELECT max(day) FROM prices с последним реальным торговым днем, и настройте задание так, чтобы оно завершалось с ненулевым кодом, если данные по всем тикерам приходят пустыми.
Может ли агент решать, что покупать?
Нет, и попытки реализовать это — главная ошибка в подобных проектах. У модели нет доступа к рынку, нет данных о вашей позиции или налоговой ситуации, и нет способа проверить свои расчеты. В чем она хороша — так это в чтении больших объемов текста и указании на то, какие несколько позиций заслуживают вашего внимания сегодня. Все, что она пишет, не является финансовым советом, а решение и ответственность за него остаются на вас.