SSD Nodes Learn 🎉 VPS desde $4.99/mês
Guias Matt ConnorPor Matt Connor

Agente de pesquisa de ações 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 via API do Claude.

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 controla. Obtém dados de mercado segundo um agendamento, guarda-os numa base de dados local, aplica-lhes um filtro e pede a um modelo de linguagem de grande escala (LLM) para descrever o que mudou. Faz leitura e filtragem. Não executa transações, e nada neste guia constitui aconselhamento financeiro.

Duas pessoas que criem este sistema de raiz podem escolher bibliotecas diferentes e acabar com as mesmas quatro partes: um feed que fornece preços e fundamentais, um armazenamento local que conserva todas as linhas obtidas, um job que atualiza o armazenamento com uma periodicidade definida e uma camada LLM que transforma as linhas restantes em frases. Este guia cria essa estrutura com Python, DuckDB, um temporizador systemd e a API do Claude (interface de programação de aplicações). A execução é um job separado, com modos de falha próprios, e deve ficar 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 está a jusante lê a sua base de dados em vez do feed. Assim, uma indisponibilidade do feed custa-lhe um dia de dados novos, e não um ecrã avariado.

O armazenamento é o objetivo principal de todo o processo. Um fecho diário que não registou normalmente pode ser obtido novamente mais tarde. Uma cotação intradiária, uma estimativa anterior à sua revisão ou um valor fundamental anterior a uma republicação não podem. O armazenamento permite criar um registo do que os dados efetivamente 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 neste caso 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 se liga à base de dados e nunca cria 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 no texto, e pode comparar o texto com as linhas que lhe enviou.

Por que executá-lo em um VPS em vez de um laptop

O agendador é o motivo principal. O fechamento do mercado dos EUA às 16:00 no horário de Nova York ocorre às 22:00 em Berlim e às 04:00 da manhã seguinte em Jacarta. Um laptop está suspenso nos dois horários. Uma execução perdida custa mais do que uma anotação atrasada: as barras diárias geralmente podem ser obtidas novamente depois, mas isso não é possível com dados que foram revisados. Portanto, a lacuna no seu histórico é permanente.

O segundo motivo é menor, mas continua sendo real. O servidor mantém uma chave de API em um único arquivo, pertencente a um único usuário do sistema sem shell de login e usada por um único job. É muito mais difícil obter esse isolamento no laptop que você também usa para navegar na web. Mantenha a chave fora do código e da entrada do modelo. Esse é o tema de manter chaves de API fora de um agente de IA.

Dimensionamento: disco, RAM e tokens

O disco é a parte mais 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.

ChartRows in the prices table by watchlist size, at 252 trading days a year
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 após um ano. A linha 500 tickers chega a 1,260,000 linhas após dez anos. Cada linha contém uma data e alguns valores double. O DuckDB armazena as colunas comprimidas, portanto o volume é de dezenas de megabytes, não de gigabytes. Não confie nesta estimativa, incluindo na minha. Execute du -h /opt/research/data/market.duckdb após o 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. Isto é adequado num servidor de análise, mas não num sistema 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 apresenta Main process exited, code=killed, status=9/KILL, enquanto journalctl -k mostra que ocorreu 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 inclui um objeto usage com input_tokens e output_tokens. Grave ambos numa tabela em cada chamada. Após uma semana, conhecerá o volume real e poderá multiplicá-lo pelo preço indicado para o modelo no dia da consulta. Há dois aspetos suficientemente estáveis para o 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 mais o custo do que reduzir os dados enviados. O prompt caching também não ajuda num job executado 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. O caching é vantajoso quando uma execução faz várias chamadas com 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/data

Verifique a instalação antes de escrever código:

sudo -u research /opt/research/venv/bin/python -c 'import duckdb, yfinance, anthropic; print("ok")'

Esse comando imprime ok. Se ignorou o ambiente virtual e executou pip install no Python do sistema, o Ubuntu 24.04 interrompe a operação e apresenta error: externally-managed-environment, porque a distribuição controla /usr/lib/python3 e não permite que o pip escreva nesse local. O venv não é opcional. É o único diretório que o pip pode modificar.

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/env

Coloque uma linha no 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-here

O 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 con

A chave primária em (ticker, day) é o que torna seguro repetir a atualização. INSERT OR REPLACE substitui uma linha que já exista 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 pelo PID que o mantém aberto. Na prática, esse processo é normalmente a shell interativa duckdb que deixou aberta noutro terminal. Os leitores passam por read_only=True, razão pela qual connect recebe a flag. Se vários processos precisarem realmente de escrever ao mesmo tempo, essa é uma tarefa para outro motor: o SQLite em modo WAL permite que os leitores trabalhem enquanto um escritor faz o commit, e um timeout de espera faz com que os outros escritores aguardem em vez de falharem. DuckDB contra SQLite para uma carga de trabalho de servidor compara os dois, e executar SQLite em produção numa VPS aborda as definições necessárias para que o WAL funcione corretamente.

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.py

Na primeira execução, o job preenche retroativamente vários anos e mostra 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 o insert 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. Mostra 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. O problema só é detetado semanas depois, quando uma consulta devolve as mesmas linhas todos os dias.

O tratamento dos timestamps também tem uma razão. O índice devolvido pelo feed pode conter o fuso horário da bolsa, e remover o offset não equivale a converter para UTC. Uma sessão de Tóquio registada à meia-noite no horário local passa para o dia civil 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 trabalho é executado às 16:20. Especifique esse horário na hora de Nova Iorque, não em UTC. Nova Iorque está em UTC menos 5 no inverno e em 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.target

Type=oneshot é o único tipo de serviço que aceita mais do que 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 apresentação de 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 trabalho é executado assim que a máquina voltar a estar disponível.

sudo systemctl daemon-reload
sudo systemctl enable --now research-refresh.timer
systemctl list-timers research-refresh.timer

list-timers deve apresentar uma coluna NEXT com a próxima execução num dia útil, convertida para a hora local do próprio servidor. Uma lista vazia significa que o temporizador não está ativado ou que a unidade não tem a secção [Install], pelo que enable não tinha nada para associar a timers.target.

A tela: 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 que passam pelo filtro 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 os registos existentes, por isso a terceira linha de um ticker devolve a média de três dias e continua a chamá-la de ma50. Comparar esse valor com ma20 cria um cruzamento no início do histórico de cada ticker, embora ele nunca tenha ocorrido. Filtrar pelo número da linha elimina 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 limita a parte mais dispendiosa da fatura. A instrução para usar apenas as linhas fornecidas é a que deve verificar, não aceitar sem confirmação: elimine uma coluna do bloco, execute novamente e leia o resultado. Se ainda aparecer um valor dessa coluna, o modelo preencheu a lacuna e o seu prompt não é suficientemente restritivo. Esse teste demora dois minutos e é a única forma fiável de saber.

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 o resultado verificável, porque todos os números da nota também devem aparecer no bloco enviado e pode compará-los linha a linha. Para obter mais informações sobre a elaboração do prompt, usar o Claude para análise financeira explica com mais detalhe o que o modelo consegue interpretar bem.

Uma execução, do início ao fim

Às 16:20, no fuso horário de Nova Iorque, o temporizador 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 formam um bloco de texto com algumas centenas de tokens. Uma chamada à API transforma o bloco numa nota curta. A nota é enviada para o diário. 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-pager

Um log saudável contém uma linha por ticker, depois a nota e, em seguida, research-refresh.service: Deactivated successfully. Ao fim de uma semana, leia os custos diretamente 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 apresentados pelo seu modelo no dia em que os consultar. Assim obtém o valor real, em vez de uma estimativa de terceiros. 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 tamanhos de janela, percorra uma grelha de pares e ordene-os pelo retorno. O melhor par terá um resultado 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.

É possível 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ó vence na metade usada para o ajustar não diz nada sobre o dia seguinte.

Survivorship é pior do que o overfitting porque o tuning não o consegue corrigir. 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 removido da cotação 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.

Fundamentos 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 fundamentos atuais com preços de 2019 usa informação que não existia nessa altura. Os preços são normalmente seguros neste caso. Os fundamentos, normalmente, não são.

Os preços ajustados mudam sem aviso. Com auto_adjust=True, os preços de fecho são ajustados retroativamente para dividendos e splits. Por isso, a mesma consulta executada no próximo mês pode devolver um histórico ligeiramente diferente. Armazenar as linhas que efetivamente usou torna o resultado reproduzível. Esse é também outro motivo para o store local existir.

O backtest também ignora comissões e slippage e assume que a sua ordem não altera o preço. Esses aspetos pertencem à execução, que está fora do âmbito deste texto e é abordada em executar bots de trading num VPS.

Modos de falha e as mensagens que verá

error: externally-managed-environment quando o pip é executado. Está fora do ambiente virtual. Execute /opt/research/venv/bin/pip pelo caminho completo.

Could not set lock on file, seguido de um PID. 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 reduza memory_limit em store.py.

Uma execução verde que não escreve nada. systemctl statusactive (exited) e a tabela não aumentou. O feed devolveu frames vazios. Esta falha pode permanecer oculta durante mais tempo, por isso faça o job terminar com código diferente de zero quando todos os tickers voltarem 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 isso como normal, não como um erro.

429 da API. Foi excedido um limite de taxa. 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 enviar demasiados pedidos num único pico.

O que isto não é

Isto é um assistente de investigação. Um modelo que resume um documento produz uma interpretação desse documento e pode estar totalmente errado sobre um número apresentado no texto. Por isso, todos os valores da nota devem poder ser rastreados até uma linha que enviou. Trate o resultado como uma lista curta de itens para ler por si. Isto não é aconselhamento financeiro e não constitui um sinal.

Os backtests são úteis para rejeitar ideias, mas são fracos 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 conclusã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 investigação apenas para leitura. Misturá-las coloca as chaves de trading na mesma máquina que um prompt de LLM. Se quiser ver onde este padrão se enquadra em relação às outras opções que vale a pena executar no seu próprio servidor, o percurso mais amplo pelos agentes de IA self-hosted que vale a pena executar apresenta essa visão geral.

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, e vai falhar porque depende de um site que não tem qualquer obrigação para consigo. Normalmente falha com frames vazios em vez de gerar uma exceção, por isso o seu job deve verificar a contagem de linhas. Mude 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 muda, enquanto o agendamento, o schema e o ecrã permanecem iguais.

Devo manter os preços em SQLite ou DuckDB?

DuckDB é orientado por colunas e foi concebido para consultar muitas linhas ao calcular um agregado, que é exatamente o caso de uma média móvel sobre dez anos de barras. SQLite é orientado por 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 consulta 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 continuem a trabalhar enquanto um escritor faz commit, e um timeout de ocupação faz com que os outros escritores aguardem em vez de falharem imediatamente.

Quanto custam as chamadas ao LLM por mês?

Registe usage.input_tokens e usage.output_tokens de cada resposta 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 um conjunto curto de ativos 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.

Por que razão o meu job terminou com sucesso, mas não escreveu linhas novas?

Há duas causas comuns. O mercado estava fechado porque um agendamento systemd Mon-Fri inclui os feriados da bolsa. Ou o feed devolveu um frame vazio para cada ticker. As bibliotecas cliente muitas vezes comunicam isso através de um aviso apresentado no terminal, em vez de gerar uma exceção. Assim, o processo termina com o código 0 e systemd continua a mostrar uma execução bem-sucedida. Distinga os casos comparando SELECT max(day) FROM prices com o último dia real de negociação. Faça o job terminar com um código diferente de zero quando todos os tickers forem devolvidos vazios.

O agente pode decidir o que devo comprar?

Não. Tentar fazê-lo é uma das causas que levam estes projetos a falhar. 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 com base em dados externos. É útil para ler grandes quantidades de texto e indicar quais os poucos itens que merecem a sua atenção hoje. Nada do que escreve constitui aconselhamento financeiro. A decisão e a responsabilidade por ela continuam a ser suas.