Como criar um agente de ações em uma VPS
Monte um agente autoalojado com feed de mercado, DuckDB, timer systemd no fechamento do pregão e triagem por LLM, sem executar operações.
O que é um agente de pesquisa de ações autoalojado
Um agente de pesquisa de ações autoalojado é um pequeno programa num servidor que você controla. O programa obtém dados de mercado segundo um calendário, guarda-os numa base de dados local, aplica-lhes um filtro e pede a um modelo de linguagem de grande escala (LLM) que descreva o que mudou. O programa lê e filtra dados. Não executa operações, e nada neste guia constitui aconselhamento financeiro.
Duas pessoas que construam isto de raiz escolherão bibliotecas diferentes e, ainda assim, acabarão com as mesmas quatro partes: um feed que fornece preços e dados fundamentais, um armazenamento local que conserva cada linha obtida, um job 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 Claude API (application programming interface). É um agente limitado, com um ciclo fixo. Se preferir conhecer primeiro a versão geral, o percurso faseado para aprender agentes de IA explica o ciclo, as ferramentas e a memória por ordem, antes de qualquer um deles se transformar num problema de domínio. A execução é um job separado, com modos de falha próprios, e deve ficar em uma VPS preparada 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 a seguir 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 tenha registado pode normalmente ser obtido novamente mais tarde. Uma cotação intradiária, uma estimativa antes de ser revista ou um valor de fundamentos antes de ser reformulado não podem. O armazenamento permite criar um registo do que os dados realmente indicavam no dia em que o indicavam.
O scheduler decide quando ocorre a atualização. Num servidor, isso é um timer do systemd. É por isso que a 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 se liga à base de dados e nunca cria a query. 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 verificar essa prosa comparando-a com as linhas que lhe enviou.
Por que executá-lo num VPS em vez de num laptop
O agendador é o motivo. O fecho do mercado dos EUA às 16:00 no horário de Nova Iorque 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 nota atrasada: as barras diárias normalmente podem ser obtidas novamente mais tarde, mas os dados que forem revistos não podem, pelo que a lacuna no seu histórico é permanente. O mesmo raciocínio aplica-se a qualquer agente cujo agendamento e estado armazenado tenham de sobreviver a um reboot. É para isso que serve executar o KiroCrew como um agente sempre ativo no seu próprio VPS.
O segundo motivo é menor, mas continua a ser real. O servidor mantém uma única API key num único ficheiro, pertencente a um único utilizador do sistema sem shell de login e usada por um único job. É muito mais difícil organizar isto num laptop que também utiliza para navegar na Web. Mantenha a key fora do código e fora da entrada do modelo. Esse é o tema de manter API keys 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.
Os dados por trás deste gráfico
[
{
"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 ao fim de um ano. A linha 500 tickers chega a 1,260,000 linhas ao fim 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 na 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 parte significativa da memória da máquina e todos os seus cores numa única consulta. Isto é adequado num servidor de análise, mas não numa máquina 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 o processo foi terminado por falta de memória. Defina memory_limit e threads explicitamente. A consulta ficará mais lenta em vez de terminar.
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 seu volume real. Multiplique esse volume pelo preço indicado para o seu modelo no dia em que fizer a verificação. Há dois aspetos 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. Além disso, o prompt caching não ajuda numa tarefa executada uma vez por dia. A cache dura 5 minutos por predefinição. O período mais longo disponibilizado pela Anthropic é 1 hora. Esse período custa 2x o preço base dos tokens de entrada para escrever na cache, em vez de 1.25x no caso da cache de 5 minutos. Em qualquer dos casos, na execução do dia seguinte o bloco em cache já expirou e o preço total dos tokens de entrada é cobrado novamente. O caching compensa 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/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 a operação com error: externally-managed-environment, porque a distribuição gere /usr/lib/python3 e não permite que o pip escreva nesse local. O ambiente virtual não é apenas uma questão de organização. É o único diretório que o pip pode alterar.
A chave da API deve ficar num ficheiro que o utilizador do serviço consiga 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 uma 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 essa chave, uma nova execução após uma falha duplica silenciosamente cada barra, e todas as médias calculadas depois ficam erradas sem que apareça qualquer mensagem de erro a indicar o problema.
O DuckDB permite que exatamente 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. Uma ligação apenas de leitura não contorna esta limitação. A documentação de concorrência do DuckDB descreve dois modos: um processo que lê e escreve, ou vários processos que apenas leem, nunca uma combinação dos dois. connect aceita um sinalizador read_only para que vários leitores possam partilhar o ficheiro. Isto só funciona enquanto nenhum processo de escrita o tiver aberto. Se vários processos precisarem realmente de escrever ao mesmo tempo, deve usar outro motor: o SQLite em modo WAL permite que os leitores trabalhem enquanto um escritor faz commit, e um timeout de ocupação faz com que os outros escritores aguardem em vez de falharem. DuckDB e 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 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.pyA primeira execução 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 que tenha deixado de estar cotado. Emite um aviso de que o símbolo pode ter deixado de estar cotado 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. Um job sem essa verificação 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ó é descoberto semanas depois, quando um ecrã 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 processados. Por isso, o job é executado às 16:20. Defina 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 fica desfasado uma hora duas vezes por ano e começa a ser executado antes do fecho. O systemd 235 e versões posteriores aceitam diretamente um fuso horário em OnCalendar. O Ubuntu 24.04 inclui a versão 255. Confirme a versão instalada 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 pela apresentação de dados desatualizados. Persistent=true é importante num VPS que reinicia para instalar atualizações do kernel. Reinicie às 16:15 sem essa opção e a execução será simplesmente perdida. Com ela, o job é executado assim que a máquina voltar a estar disponível. Um agente que tenha de permanecer em execução, em vez de ser iniciado e terminar, precisa da estrutura de unidade oposta: um processo de longa duração com regras Restart= e sem qualquer temporizador. É isso que o guia executar dsh sem interface num VPS com systemd explica.
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 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 falta à unidade a secção [Install]. Nesse caso, 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 resultados restantes são enviados ao 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 de todas as linhas existentes, por isso a terceira linha de um ticker devolve a média de três dias e continua a chamar-se ma50. Comparar esse valor com ma20 cria um cruzamento no início do histórico de cada ticker que nunca aconteceu. 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,
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()A linha thinking é importante neste modelo. O Claude Sonnet 5 raciocina por predefinição. Os tokens de raciocínio são cobrados como saída e contam para max_tokens. Além disso, são devolvidos num bloco antes do texto. Sem essa linha, resp.content[0] pode ser o bloco de raciocínio e a consulta .text falha. O Claude Sonnet 5 é agora um modelo legado. O seu sucessor, claude-sonnet-5-5, rejeita disabled. A definição a usar nesse caso é thinking={'type': 'between_tools'}, que desativa o raciocínio antes da resposta.
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, em vez de aceitar sem confirmação: elimine uma coluna do bloco, execute novamente e leia a saída. Se continuar a aparecer um valor dessa coluna, o modelo preencheu a lacuna e o prompt não é suficientemente restritivo. Esse teste demora dois minutos e é a única forma honesta 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 texto. Essa separação torna a saída verificável, porque todos os números da nota também devem aparecer no bloco enviado e pode compará-los linha a linha. Assim que amplia essa entrada para incluir páginas obtidas na Web, a separação muda, porque um título ou uma publicação num fórum pode conter instruções dirigidas ao modelo, em vez de factos dirigidos a si. Essa é a superfície que disponibilizar um backend de pesquisa SearXNG a um agente expõe. Para mais informações sobre a criação de prompts, usar o Claude para análise financeira explica melhor o que o modelo consegue ler.
Uma execução completa, de ponta a ponta
À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 barras novas 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 transformam-se num 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. Se estiver a ler esse log numa sessão do Claude Code no servidor enquanto uma segunda sessão trabalha na consulta do ecrã, as duas podem trocar texto entre si em vez de copiar linhas entre terminais. 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 sofrem de 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 terá um aspeto 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 pelo acaso.
Pode observar isso em dez minutos. Divida o store ao meio por 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.
O viés de sobrevivência é pior do que o overfitting porque o ajuste não o consegue corrigir. A sua lista de tickers contém os membros atuais do índice e, por isso, apenas inclui as empresas que sobreviveram. Se pedir ao feed um ticker que foi retirado da bolsa em 2019, receberá um frame vazio. Isso significa que essa empresa nunca entra no seu store nem no teste. Todos os backtests que executar já excluíram as empresas que falharam.
Os fundamentos retificados 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 combine os fundamentos atuais com preços de 2019 está a usar informação que não existia nessa altura. Em geral, os preços são seguros neste caso. Os fundamentos, normalmente, não são.
Os preços ajustados alteram-se sem aviso. Com auto_adjust=True, os fechos são ajustados retroativamente para dividendos e splits, por isso a mesma consulta executada no próximo mês devolve um histórico ligeiramente diferente. Armazenar as linhas que efetivamente usou é o que torna um resultado reproduzível. É também outra razão para o store local existir.
O backtest também ignora comissões e slippage e assume que a sua ordem não move o preço. Esses aspetos pertencem à execução, que está fora do âmbito deste texto e é abordada em executar bots de trading numa VPS.
Modos de falha e mensagens apresentadas
error: externally-managed-environment quando o pip é executado. Está fora do ambiente virtual. Invoque /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. Abrir a segunda ligação com read_only=True não resolve enquanto esse processo de escrita continuar aberto, porque o DuckDB permite que vários processos leiam ou que um processo escreva, mas não as duas coisas ao mesmo tempo.
Main process exited, code=killed, status=9/KILL em systemctl status. O kernel terminou o processo por falta de memória. Confirme com journalctl -k | grep -i oom e 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 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 regressarem 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. Foi ultrapassado um limite de taxa. O Anthropic SDK repete automaticamente os pedidos com backoff, e Anthropic(max_retries=5) aumenta o número de tentativas. Se continuar a falhar todos os dias, o job está a solicitar dados a mais num único pico.
O que isto não é
Este é um assistente de pesquisa. Um modelo que resume um documento apresenta uma interpretação desse documento e pode estar seguramente errado sobre um número impresso no texto. Por isso, cada valor da nota tem de poder ser associado a uma linha que enviou. Trate o resultado como uma lista curta de itens para ler por si. Isto não é aconselhamento financeiro e nada aqui constitui um sinal.
Os backtests são úteis para rejeitar ideias, mas são 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. Isto é uma conclusão muito mais limitada do que parece à 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. Se quiser ver onde este padrão se enquadra em relação a 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 lhe deve nada. Normalmente falha devolvendo frames vazios em vez de uma exceção, por isso o seu job deve verificar o número 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 esquema e a interface permanecem iguais.
Devo manter os preços em SQLite ou DuckDB?
O DuckDB é colunar e foi concebido para percorrer muitas linhas ao calcular um agregado, que é exatamente o caso de uma média móvel sobre dez anos de barras. O SQLite é orientado a linhas e é melhor para muitas leituras e escritas pequenas de vários processos em simultâneo. Para um único job agendado que acrescenta algumas centenas de linhas e depois percorre milhões, o DuckDB é a opção mais adequada. Se vários processos tiverem de escrever ao mesmo tempo, o SQLite em modo WAL permite que os leitores continuem a trabalhar enquanto um escritor faz commit, e um timeout de busy faz os outros escritores esperar 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 consulta. 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.
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 feriados das bolsas. Ou o feed devolveu um frame vazio para cada ticker. As bibliotecas cliente muitas vezes comunicam isso 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 real de negociação. 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. É precisamente ao tentar fazê-lo que estes projetos costumam falhar. O modelo não tem acesso ao mercado, não conhece a sua posição nem a sua situação fiscal e não consegue validar os próprios valores com base em dados externos. É útil para ler grandes quantidades de texto e indicar quais são 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.