Agente de ações autoalojado numa VPS com DuckDB
Monte um agente de pesquisa de ações numa VPS com feed de mercado, DuckDB, timer systemd no fecho do mercado e triagem por LLM, sem executar ordens.
O que é um agente de pesquisa de ações autoalojado
Um agente de pesquisa de ações autoalojado é um pequeno programa executado num servidor que você controla. Obtém dados de mercado segundo um agendamento, guarda-os numa base de dados local, aplica-lhes filtros e pede a um modelo de linguagem de grande dimensão (LLM) que descreva o que mudou. Lê e filtra dados. Não faz transações, e nada neste guia constitui aconselhamento financeiro.
Duas pessoas que criem isto de raiz podem escolher bibliotecas diferentes e, ainda assim, chegar às mesmas quatro partes: uma fonte que fornece preços e dados fundamentais, um armazenamento local que conserva todas as linhas obtidas, uma tarefa que atualiza o armazenamento segundo um temporizador e uma camada LLM que transforma as linhas restantes em frases. Este guia constrói essa estrutura com Python, DuckDB, um temporizador systemd e a API Claude (interface de programação de aplicações). A execução é uma tarefa separada, com modos de falha próprios, e deve ser colocada antes em uma VPS configurada para bots de trading.
As quatro partes e a função de cada uma
O feed é a única parte que comunica com o exterior. Sabe como pedir um ticker e um intervalo de datas, e como devolver linhas. Tudo o que vem depois lê a sua base de dados em vez do feed, por isso uma indisponibilidade do feed custa-lhe um dia de dados novos, e não um ecrã avariado.
O armazenamento é o objetivo central de todo o processo. Um fecho diário que não tenha registado pode normalmente ser obtido mais tarde. Uma cotação intradiária, uma estimativa anterior à sua revisão ou um valor fundamental anterior à sua reformulação não podem. O armazenamento permite construir um registo do que os dados realmente indicavam no dia em que o indicavam.
O agendador decide quando ocorre a atualização. Num servidor, é um temporizador do systemd. É por isso que o VPS é mais importante aqui do que o código.
A camada LLM lê um bloco curto de texto produzido pelo seu SQL e escreve um resumo desse conteúdo. Nunca estabelece ligação à base de dados e nunca constrói a consulta. Se o modelo escrever o SQL, um token incorreto transforma-se num número incorreto dentro de uma frase fluida, sem nada com que o possa comparar. Se o SQL produzir os números, o modelo só pode errar na prosa, e pode comparar a prosa com as linhas que lhe enviou.
Por que executá-lo em um VPS em vez de num laptop
O agendador é o motivo. O fecho do mercado dos EUA às 16:00, hora de Nova Iorque, corresponde a 22:00 em Berlim e a 04:00 da manhã seguinte em Jacarta. Um laptop está suspenso nos dois horários. Uma execução perdida custa mais do que uma nota atrasada: as barras diárias normalmente podem ser obtidas novamente mais tarde, mas não é possível recuperar o que tiver sido revisto, por isso a lacuna no seu registo é permanente. O mesmo raciocínio aplica-se a qualquer agente cujo agendamento e estado armazenado tenham de sobreviver a um reboot, que é precisamente o objetivo de executar o KiroCrew como um agente sempre ativo no seu próprio VPS.
O segundo motivo é menor, mas continua a ser real. O servidor guarda uma chave API, num único ficheiro, pertencente a um único utilizador do sistema sem shell de login e utilizada por um único job. É muito mais difícil organizar isto no laptop que também utiliza para navegar na Web. Mantenha a chave fora do código e fora da entrada do modelo. Esse é o tema de manter as chaves API fora de um agente de IA.
Dimensionamento: disco, RAM e tokens
O disco é a parte simples. Uma barra diária corresponde a uma linha por ticker e por dia de negociação, e um ano de negociação nos EUA tem cerca de 252 dias.
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"
}
]Vinte tickers correspondem a 5,040 linhas depois de um ano. A linha 500 tickers chega a 1,260,000 linhas depois de dez anos. Cada linha contém uma data e alguns valores double. O DuckDB armazena as colunas comprimidas, por isso o resultado ocupa dezenas de megabytes, não gigabytes. Não confie nessa estimativa, incluindo a minha. Execute du -h /opt/research/data/market.duckdb depois do primeiro backfill e use o seu próprio valor.
A RAM é o ponto crítico num VPS pequeno. Por predefinição, o DuckDB utiliza uma grande parte da memória da máquina e todos os seus cores numa única consulta. Isso é adequado num servidor de analytics, mas não num servidor com 2 GB que também executa outros serviços. Uma agregação sobre toda a tabela de preços pode fazer com que o kernel termine o processo. Nesse caso, o systemd comunica Main process exited, code=killed, status=9/KILL, enquanto journalctl -k mostra a terminação por falta de memória. Defina memory_limit e threads explicitamente. A consulta ficará mais lenta em vez de terminar inesperadamente.
Os tokens devem ser medidos, não estimados. Cada resposta da Messages API contém um objeto usage com input_tokens e output_tokens. Grave ambos numa tabela em cada chamada. Ao fim de uma semana, conhecerá o volume real e poderá multiplicá-lo pelo preço indicado para o seu modelo no dia da consulta. Há dois pontos suficientemente estáveis para servirem de base ao planeamento. Os tokens de saída têm um preço superior ao dos tokens de entrada em todos os modelos Claude. Por isso, limitar a nota a 200 palavras reduz a fatura mais do que reduzir os dados enviados. O prompt caching também não ajuda numa tarefa executada uma vez por dia, porque a validade da cache é medida em minutos. Na execução seguinte, o bloco em cache já expirou e o preço total dos tokens de entrada é cobrado novamente. A cache é vantajosa quando uma execução faz várias chamadas utilizando o mesmo bloco grande de texto.
Instale os componentes
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/dataVerifique a instalação antes de escrever código:
sudo -u research /opt/research/venv/bin/python -c 'import duckdb, yfinance, anthropic; print("ok")'Isto imprime ok. Se ignorou o ambiente virtual e executou pip install no Python do sistema, o Ubuntu 24.04 interrompe o processo com error: externally-managed-environment, porque a distribuição gere /usr/lib/python3 e não permite que o pip escreva nesse local. O venv não é opcional por conveniência. É o único diretório onde o pip tem permissão para escrever.
A chave da API deve ficar num ficheiro que o utilizador do serviço possa ler e ao qual mais ninguém tenha acesso:
sudo install -d -m 755 /etc/research
sudo install -m 640 -o root -g research /dev/null /etc/research/env
sudoedit /etc/research/envColoque uma linha nesse ficheiro, sem aspas e sem export, porque o systemd analisa este ficheiro diretamente em vez de o passar a um shell:
ANTHROPIC_API_KEY=sk-ant-your-key-hereO armazenamento: duas tabelas
# /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 conA chave primária em (ticker, day) é o que torna seguro repetir a atualização. INSERT OR REPLACE substitui uma linha que já existe para esse ticker e esse dia, por isso executar o preenchimento retroativo duas vezes não duplica a tabela. Sem a chave, uma nova execução depois de uma falha duplica silenciosamente cada barra, e todas as médias calculadas depois ficam erradas, sem qualquer mensagem de erro que o indique.
O DuckDB permite que apenas um processo mantenha o ficheiro aberto para escrita. Um segundo processo de escrita falha imediatamente com Could not set lock on file, seguido do PID que mantém o ficheiro aberto. Na prática, esse processo é normalmente a shell interativa duckdb que ficou aberta noutro terminal. Os leitores passam por read_only=True, razão pela qual connect recebe essa opção. Se vários processos precisarem realmente de escrever ao mesmo tempo, deverá usar outro motor: o SQLite em modo WAL permite que os leitores trabalhem enquanto um processo de escrita faz o commit, e um tempo limite de espera evita que os outros processos de escrita falhem. DuckDB versus SQLite para uma carga de trabalho de servidor compara os dois, e executar SQLite em produção numa VPS descreve as definições necessárias para o WAL funcionar.
O job de atualização
# /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()Execute-o manualmente uma vez:
sudo -u research /opt/research/venv/bin/python /opt/research/refresh.pyNa primeira execução, o job preenche os anos em falta e imprime uma linha por ticker, com uma contagem na ordem dos milhares. Execute-o novamente um minuto depois. Cada linha deverá mostrar 1 ou 2 linhas, porque o job começa no último dia já armazenado. Essa segunda execução é o teste real: se as contagens continuarem na ordem dos milhares, max(day) não está a devolver dados e a inserção está a reescrever todo o histórico todas as noites.
A verificação df.empty é a linha mais importante do ficheiro. Ticker.history() não gera uma exceção para um símbolo incorreto ou retirado da listagem. Emite um aviso de que o símbolo pode ter sido retirado da listagem e de que não foram encontrados dados de preços (o texto exato varia entre versões da biblioteca) e devolve um DataFrame vazio. Sem essa verificação, o job não escreve nada, termina com o código 0 e o systemd mostra uma execução saudável, enquanto a tabela deixa silenciosamente de crescer. Só descobrirá o problema semanas depois, ao consultar um ecrã que devolve as mesmas linhas todos os dias.
O tratamento dos timestamps também tem uma razão. O índice devolvido pelo feed pode incluir o fuso horário da bolsa, e remover o offset não é o mesmo que converter para UTC. Uma sessão de Tóquio registada à meia-noite no horário local é convertida para o dia de calendário anterior em UTC. Assim, uma conversão para UTC desloca silenciosamente cada barra japonesa um dia para trás e quebra a chave primária. tz_localize(None) mantém a data da sessão da própria bolsa, que é o significado de uma barra diária.
O temporizador que é executado no fecho do mercado
O mercado dos EUA fecha às 16:00, hora de Nova Iorque, e os últimos negócios demoram alguns minutos a ser liquidados, por isso o job é executado às 16:20. Especifique esse horário na hora de Nova Iorque, não em UTC. Nova Iorque corresponde a UTC menos 5 no inverno e a UTC menos 4 no verão. Por isso, um temporizador definido com uma hora UTC fixa varia uma hora duas vezes por ano e começa a ser executado antes do fecho. O systemd 252 e posteriores aceitam diretamente um fuso horário em OnCalendar, e o Ubuntu 24.04 inclui a versão 255. Confirme a sua versão com 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 é o único tipo de serviço que aceita mais de um ExecStart e executa-os por ordem, parando se algum terminar com um código diferente de zero. Esse é exatamente o comportamento necessário: uma atualização que falhe não deve ser seguida por uma captura de ecrã com dados desatualizados. Persistent=true é importante num VPS que reinicia para instalar atualizações do kernel. Se reiniciar às 16:15 sem essa opção, a execução é simplesmente perdida; com ela, o job é executado assim que a máquina volta a estar disponível.
sudo systemctl daemon-reload
sudo systemctl enable --now research-refresh.timer
systemctl list-timers research-refresh.timerlist-timers deve mostrar uma coluna NEXT com a próxima execução em dia útil, convertida para a hora local do próprio servidor. Uma lista vazia significa que o temporizador não está ativado ou que falta à unidade a secção [Install]. Nesse caso, enable não tinha nada para associar a timers.target.
O ecrã: SQL primeiro, modelo por último
SQL é exato e não tem custo por execução. O modelo não é. Por isso, a consulta reduz o universo e apenas os registos restantes são enviados para o modelo. Guarde isto como /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;O filtro n > 50 não é decorativo. ROWS BETWEEN 49 PRECEDING AND CURRENT ROW calcula a média com as linhas existentes, por isso a terceira linha de um ticker devolve a média de três dias e continua a designá-la como ma50. Se comparar esse valor com ma20, irá criar um crossover no início do histórico de cada ticker, embora nunca tenha acontecido. Filtrar pelo número da linha remove as linhas em que a janela ainda não estava completa.
O que o modelo vê e o que nunca vê
# /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()O limite de palavras no prompt do sistema controla a parte mais dispendiosa da fatura. A instrução para usar apenas as linhas fornecidas é a que deve verificar, em vez de aceitar sem confirmação: elimine uma coluna do bloco, execute novamente e leia a saída. Se ainda aparecer um valor dessa coluna, o modelo preencheu a lacuna e o prompt não é suficientemente restritivo. Esse teste demora 2 minutos e é a única forma objetiva de descobrir.
O modelo nunca vê a chave da API, nunca vê o caminho da base de dados e nunca executa uma consulta. Recebe linhas e devolve prosa. Esse limite torna a saída verificável, porque todos os números da nota também devem aparecer no bloco enviado, permitindo compará-los linha a linha. Para saber mais sobre a criação de prompts neste contexto, usar o Claude para análise financeira explica melhor o que o modelo consegue ler.
Uma execução, de ponta a ponta
Às 16:20, no horário de Nova Iorque, o timer inicia o serviço. refresh.py consulta o feed para cada ticker, começando pelo último dia armazenado, grava uma ou duas novas barras para cada um e imprime uma linha por ticker. screen.py abre o mesmo ficheiro, executa a consulta da média móvel e recebe algumas linhas. Essas linhas tornam-se um bloco de texto com algumas centenas de tokens. Uma chamada à API transforma-o numa nota curta, a nota é enviada para o journal e uma linha é gravada em runs com as contagens de tokens e o número de ocorrências.
journalctl -u research-refresh.service -n 50 --no-pagerUm log saudável contém uma linha por ticker, depois a nota e, em seguida, research-refresh.service: Deactivated successfully. Depois de uma semana, consulte os seus próprios custos na base de dados:
SELECT count(*) AS runs,
sum(input_tokens) AS in_tokens,
sum(output_tokens) AS out_tokens
FROM runs;Multiplique esses totais pelos preços por milhão de tokens indicados pelo seu modelo no dia em que os consultar. Assim obtém o valor real, em vez da estimativa de outra pessoa. Os preços publicados mudam. A aritmética não.
Por que os backtests fazem overfitting e como observar isso
Reescreva o screen como uma função dos dois comprimentos das janelas, percorra uma grelha de pares e ordene-os pelo retorno. O melhor par parecerá excelente. Esse é o problema, não o resultado. Uma grelha com 200 pares corresponde a 200 experiências, e ficou com o par mais favorecido pela sorte.
Pode observar isso em dez minutos. Divida o store ao meio pela data. Percorra a grelha apenas na primeira metade e registe o vencedor. Percorra a mesma grelha na segunda metade. Se os dois vencedores forem muito diferentes, os parâmetros estão a ajustar-se ao ruído. Um par que só ganha na metade usada para o ajustar não diz nada sobre o dia seguinte.
Survivorship é pior do que o overfitting porque o ajuste não o corrige. A sua lista de tickers contém os membros atuais do índice, portanto inclui apenas as empresas que sobreviveram. Se pedir ao feed um ticker que foi retirado da bolsa em 2019, ele devolve um frame vazio. Isso significa que essa empresa nunca entra no seu store nem no seu teste. Todos os backtests que executa já excluíram as empresas que falharam.
Fundamentals reformulados quebram a linha temporal. O valor da receita que a API devolve hoje para um trimestre de 2019 nem sempre é o valor publicado em 2019. Um screen que mistura os fundamentals atuais com preços de 2019 usa informação que não existia nessa altura. Normalmente, os preços são seguros neste aspeto. Os fundamentals, normalmente, não são.
Os preços ajustados mudam com o tempo. Com auto_adjust=True, os preços de fecho são ajustados retroativamente para dividendos e splits, portanto a mesma consulta executada no próximo mês devolve um histórico ligeiramente diferente. Armazenar as linhas que realmente utilizou torna o resultado reproduzível e é outra razão para o local store existir.
O backtest também ignora comissões e slippage e pressupõe que a sua ordem não altera o preço. Esses aspetos pertencem à execução, que está fora do âmbito deste tutorial e é abordada em executar bots de trading num VPS.
Modos de falha e mensagens que verá
error: externally-managed-environment quando o pip é executado. Está fora do ambiente virtual. Chame /opt/research/venv/bin/pip pelo caminho completo.
Could not set lock on file, com um PID a seguir. Outro processo mantém o ficheiro DuckDB aberto para escrita, normalmente uma shell interativa que ficou aberta. Feche-a ou abra a segunda ligação com read_only=True.
Main process exited, code=killed, status=9/KILL em systemctl status. O kernel terminou o job por falta de memória. Confirme com journalctl -k | grep -i oom e depois reduza memory_limit em store.py.
Uma execução concluída com sucesso que não escreve nada. systemctl status lê active (exited) e a tabela não cresceu. O feed devolveu frames vazios. Esta falha pode passar despercebida durante mais tempo, por isso faça o job terminar com código diferente de zero quando todos os tickers devolverem resultados vazios.
O timer foi acionado num feriado do mercado. O systemd não conhece o calendário da bolsa, por isso Mon-Fri inclui feriados. A execução ocorre, o feed não tem dados novos e o job deve tratar essa situação como normal, não como um erro.
429 da API. Excedeu um limite de pedidos. O Anthropic SDK repete as tentativas com backoff automaticamente, e Anthropic(max_retries=5) aumenta o número de tentativas. Se continuar a falhar todos os dias, o job está a fazer demasiados pedidos num único pico.
O que isto não é
Isto é um assistente de pesquisa. Um modelo que resume um filing produz uma interpretação desse filing e pode estar confiantemente errado sobre um número apresentado no texto. Por isso, todos os valores da nota têm de permitir rastrear a linha que enviou. Trate o resultado como uma lista preliminar de itens que deve ler por si. Isto não é aconselhamento financeiro e nenhum destes elementos é um sinal.
Os backtests são úteis para rejeitar ideias e pouco eficazes para as confirmar. Uma estratégia que falha nos seus próprios dados está efetivamente descartada. Uma estratégia que passa apenas sobreviveu aos seus dados. Esta é uma afirmação muito mais limitada do que parece às 1am.
A execução fica deliberadamente fora do âmbito. As ordens e as credenciais do broker têm um perfil de risco diferente do de uma máquina de pesquisa apenas de leitura. Misturá-las coloca as chaves de trading na mesma máquina que um prompt de LLM. Para ver onde este padrão se enquadra em relação às outras coisas que vale a pena executar no seu próprio servidor, os agentes de IA self-hosted que vale a pena executar apresentam uma visão mais ampla.
FAQ
Preciso de um feed pago de dados de mercado?
Não para um protótipo. Um feed gratuito não oficial é suficiente para aprender a estrutura do sistema, mas vai falhar porque depende de um site que não tem qualquer obrigação para consigo. Normalmente falha devolvendo frames vazios, em vez de uma exceção, por isso o job deve verificar a contagem de linhas. Passe para um feed pago com uma API documentada e um endereço de suporte assim que os dados começarem a influenciar uma decisão. É o armazenamento que torna essa troca simples: apenas a função de obtenção dos dados muda, enquanto o agendamento, o esquema e a interface permanecem iguais.
Devo guardar os preços em SQLite ou DuckDB?
DuckDB é colunar e foi concebido para percorrer muitas linhas e calcular um agregado, que é exatamente o caso de uma média móvel sobre dez anos de barras. SQLite é orientado a linhas e é melhor para muitas leituras e escritas pequenas feitas simultaneamente por vários processos. Para um único job agendado que acrescenta algumas centenas de linhas e depois percorre milhões, DuckDB é a opção mais adequada. Se vários processos tiverem de escrever ao mesmo tempo, SQLite em modo WAL permite que os leitores trabalhem enquanto um escritor faz commit, e um timeout de busy faz os outros escritores esperar em vez de falharem imediatamente.
Quanto custam por mês as chamadas ao LLM?
Registe usage.input_tokens e usage.output_tokens de todas as respostas numa tabela. Depois, multiplique os totais semanais pelo preço por milhão de tokens indicado pelo seu modelo no dia em que fizer a verificação. Esse é o único valor que continuará correto no próximo trimestre. Uma execução diária sobre uma seleção curta gera poucas chamadas. O tamanho da nota é a parte que pode controlar: limitar o resumo a 200 palavras poupa mais do que enviar menos linhas, porque os tokens de saída têm um preço superior ao dos tokens de entrada em todos os modelos Claude.
Porque é que o meu job terminou com sucesso, mas não escreveu linhas novas?
Há duas causas comuns. O mercado estava fechado porque um agendamento Mon-Fri do systemd inclui feriados das bolsas. Ou o feed devolveu um frame vazio para todos os tickers. As bibliotecas cliente muitas vezes comunicam esta situação através de um aviso impresso, em vez de uma exceção. Assim, o processo termina com o código 0 e o systemd continua a mostrar uma execução bem-sucedida. Distinga os casos comparando SELECT max(day) FROM prices com o último dia de negociação real. Faça também o job terminar com um código diferente de zero quando todos os tickers forem devolvidos vazios.
O agente pode decidir o que comprar?
Não. Tentar fazê-lo é uma das causas destes projetos falharem. O modelo não tem acesso ao mercado, não conhece as suas posições nem a sua situação fiscal e não consegue validar os próprios números comparando-os com uma fonte. É bom a ler grandes volumes de texto e a indicar quais os poucos itens que merecem a sua atenção hoje. Nada do que escrever constitui aconselhamento financeiro. A decisão e a responsabilidade por ela continuam a ser suas.