Agente bursátil autoalojado en un VPS
Crea un agente de investigación bursátil con feed de mercado, DuckDB, temporizador de systemd al cierre y un filtro LLM que resume los cambios.
Qué es un agente de investigación bursátil autoalojado
Un agente de investigación bursátil autoalojado es un programa pequeño que se ejecuta en un servidor propio, obtiene datos de mercado según un calendario, los guarda en una base de datos local, los filtra y pide a un modelo de lenguaje grande (LLM) que redacte un resumen de los cambios. Lee y filtra datos. No ejecuta operaciones, y nada de lo que se explica en esta guía constituye asesoramiento financiero.
Dos personas que construyan esto desde cero elegirán bibliotecas distintas y aun así terminarán con las mismas cuatro partes: un feed que proporciona precios y datos fundamentales, un almacén local que conserva cada fila que se haya obtenido, un trabajo que actualiza el almacén según un temporizador y una capa LLM que convierte las filas restantes en frases. Esta guía crea esa estructura con Python, DuckDB, un temporizador de systemd y la API de Claude (interfaz de programación de aplicaciones). Es un agente limitado con un bucle fijo. Si prefiere conocer primero la versión general, la ruta por etapas para aprender sobre agentes de IA recorre el bucle, las herramientas y la memoria en ese orden antes de convertirlos en un problema de dominio. La ejecución es un trabajo independiente con modos de fallo independientes, por lo que debe ubicarse en un VPS configurado para bots de trading.
Las cuatro partes y qué hace cada una
El feed es la única parte que se comunica con el exterior. Sabe cómo solicitar un ticker y un intervalo de fechas, y cómo devolver filas. Todo lo que viene después lee de la base de datos en lugar del feed, por lo que una interrupción del feed sólo implica perder un día de datos nuevos, no dejar una pantalla sin funcionar.
El almacén es el objetivo principal de todo el proceso. Un cierre diario que no haya registrado normalmente se puede recuperar más adelante. Una cotización intradía, una estimación anterior a su revisión o un dato de fundamentales anterior a su reformulación no se pueden recuperar. El almacén permite crear un registro de lo que los datos indicaban el día en que lo indicaban.
El planificador decide cuándo se ejecuta la actualización. En un servidor es un temporizador de systemd, por lo que el VPS es más importante aquí que el código.
La capa LLM lee un bloque de texto breve generado por SQL y redacta un resumen. Nunca se conecta a la base de datos ni construye la consulta. Si el modelo escribe el SQL, un token incorrecto se convierte en un número incorrecto dentro de una frase fluida, sin nada con lo que compararlo. Si SQL genera los números, el modelo sólo puede equivocarse en la redacción, y usted puede comprobarla comparándola con las filas que le envió.
Por qué ejecutarlo en un VPS en lugar de un portátil
El programador es el motivo principal. El cierre del mercado estadounidense a las 16:00, hora de Nueva York, es a las 22:00 en Berlín y a las 04:00 de la mañana siguiente en Yakarta. Un portátil está suspendido en ambos momentos. Omitir una ejecución cuesta más que recibir una nota tarde: normalmente se pueden volver a obtener las barras diarias más adelante, pero no se puede recuperar la información que haya sido revisada. Por tanto, la falta en el registro es permanente. El mismo razonamiento se aplica a cualquier agente cuyo calendario y estado almacenado deban sobrevivir a un reinicio. Ese es el objetivo de ejecutar KiroCrew como un agente siempre activo en tu propio VPS.
El segundo motivo es menos importante, pero también es real. El servidor almacena una clave de API en un solo archivo, propiedad de un único usuario del sistema sin shell de inicio de sesión, y la utiliza un solo trabajo. Es mucho más difícil conseguir ese aislamiento en el portátil que también usas para navegar por Internet. Mantén la clave fuera del código y fuera de la entrada del modelo. Ese es el tema de mantener las claves de API fuera de un agente de IA.
Dimensionamiento: disco, RAM y tokens
El disco es la parte sencilla. Una barra diaria ocupa una fila por ticker y día de negociación, y un año bursátil de EE. UU. tiene unos 252 días.
Los datos de este 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"
}
]Veinte tickers son 5,040 filas después de un año. La línea 500 tickers alcanza 1,260,000 filas después de diez años. Cada fila contiene una fecha y unos pocos valores double. DuckDB almacena las columnas comprimidas, por lo que el tamaño es de decenas de megabytes, no de gigabytes. No confíe en esa estimación, incluida la mía. Ejecute du -h /opt/research/data/market.duckdb después de la primera carga histórica y use su propio valor.
La RAM es el punto problemático en una VPS pequeña. De forma predeterminada, DuckDB usa una parte importante de la memoria de la máquina y todos sus núcleos para una sola consulta. Esto es adecuado en un servidor de análisis, pero no en una máquina de 2 GB que también ejecuta otros servicios. Una agregación sobre toda la tabla de precios puede hacer que el kernel termine el proceso. En ese caso, systemd informa Main process exited, code=killed, status=9/KILL y journalctl -k muestra que se terminó por falta de memoria. Defina explícitamente memory_limit y threads. La consulta será más lenta en lugar de terminarse.
Los tokens se deben medir, no estimar. Cada respuesta de Messages API incluye un objeto usage con input_tokens y output_tokens. Escriba ambos valores en una tabla en cada llamada. Después de una semana conocerá su volumen real. Multiplique ese volumen por el precio que indique el modelo el día que lo consulte. Hay dos aspectos suficientemente estables para planificar. Los tokens de salida tienen un precio superior al de los tokens de entrada en todos los modelos de Claude. Por eso, limitar la nota a 200 palabras reduce el coste más que recortar los datos enviados. Además, el almacenamiento en caché de prompts no ayuda en una tarea que se ejecuta una vez al día. La caché dura 5 minutos de forma predeterminada. La duración máxima que ofrece Anthropic es de 1 hora. Escribir en ella cuesta 2x el precio base de entrada, frente a 1.25x para la caché de 5 minutos. En cualquier caso, la siguiente ejecución diaria se produce cuando el bloque almacenado en caché ya ha caducado. Por tanto, vuelve a pagar el precio completo de entrada. La caché resulta útil cuando una ejecución realiza muchas llamadas sobre el mismo bloque grande de texto.
Instale los 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/dataCompruebe la instalación antes de escribir código:
sudo -u research /opt/research/venv/bin/python -c 'import duckdb, yfinance, anthropic; print("ok")'Esto muestra ok. Si omitió el entorno virtual y ejecutó pip install con el Python del sistema, Ubuntu 24.04 detiene la operación con error: externally-managed-environment porque la distribución administra /usr/lib/python3 y no permite que pip escriba allí. El entorno virtual no es una cuestión de cortesía. Es el único directorio que pip puede modificar.
La clave de API debe estar en un archivo que pueda leer el usuario del servicio y nadie más:
sudo install -d -m 755 /etc/research
sudo install -m 640 -o root -g research /dev/null /etc/research/env
sudoedit /etc/research/envEscriba una línea en el archivo, sin comillas ni export, porque systemd analiza este archivo directamente en lugar de pasarlo a un shell:
ANTHROPIC_API_KEY=sk-ant-your-key-hereEl almacén: dos tablas
# /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 conLa clave principal de (ticker, day) es lo que hace que la actualización sea segura al repetirla. INSERT OR REPLACE sobrescribe una fila que ya existe para ese ticker y ese día, por lo que ejecutar el proceso de carga histórica dos veces no duplica la tabla. Sin la clave, una segunda ejecución después de un bloqueo duplica silenciosamente cada barra, y todos los promedios que calcule después son incorrectos, sin ningún mensaje de error que lo indique.
DuckDB permite que un solo proceso mantenga el archivo abierto para escritura. Un segundo escritor falla de inmediato con Could not set lock on file, seguido del PID que lo mantiene abierto. En la práctica, suele ser el shell interactivo duckdb que quedó abierto en otra terminal. Una conexión de solo lectura no evita esta limitación. La documentación de concurrencia de DuckDB describe dos modos: un proceso que lee y escribe, o varios procesos que solo leen, pero nunca una combinación de ambos. connect acepta una opción read_only para que varios lectores compartan el archivo. Esto solo funciona mientras ningún escritor lo tenga abierto. Si varios procesos necesitan escribir realmente al mismo tiempo, debe usar otro motor: SQLite en modo WAL permite que los lectores trabajen mientras un escritor confirma los cambios, y un tiempo de espera para operaciones ocupadas hace que los demás escritores esperen en lugar de fallar. DuckDB frente a SQLite para una carga de servidor compara ambos motores, y ejecutar SQLite en producción en un VPS explica los ajustes necesarios para que WAL funcione correctamente.
El trabajo de actualización
# /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()Ejecútelo manualmente una vez:
sudo -u research /opt/research/venv/bin/python /opt/research/refresh.pyLa primera ejecución completa los años pendientes e imprime una línea por cada ticker con un recuento de miles. Ejecútelo de nuevo un minuto después. Cada línea mostrará 1 o 2 filas, porque el trabajo empieza en el último día almacenado. Esta segunda ejecución es la prueba real: si los recuentos siguen siendo de miles, max(day) no devuelve nada y la inserción vuelve a escribir todo el histórico cada noche.
La comprobación df.empty es la línea más importante del archivo. Ticker.history() no genera un error si el símbolo es incorrecto o se ha retirado de cotización. Imprime una advertencia que indica que el símbolo podría haber sido retirado de cotización y que no se encontraron datos de precios (el texto exacto cambia entre versiones de la biblioteca), y devuelve un DataFrame vacío. Un trabajo sin esa comprobación no escribe nada, termina con el código 0 y systemd muestra una ejecución correcta, mientras la tabla deja de crecer sin que nadie lo advierta. Se descubre semanas después, cuando una pantalla devuelve las mismas filas todos los días.
El tratamiento de las marcas de tiempo también tiene un motivo. El índice que devuelve el feed puede incluir la zona horaria de la bolsa, y eliminar el desplazamiento no equivale a convertirlo a UTC. Una sesión de Tokio marcada a medianoche en hora local se convierte en el día natural anterior en UTC. Por tanto, una conversión a UTC desplaza silenciosamente cada barra japonesa un día hacia atrás y rompe la clave primaria. tz_localize(None) conserva la fecha de sesión propia de la bolsa, que es lo que representa una barra diaria.
El temporizador que se ejecuta al cierre del mercado
El mercado estadounidense cierra a las 16:00, hora de Nueva York, y las últimas operaciones tardan unos minutos en liquidarse. Por eso, el trabajo se ejecuta a las 16:20. Especifique esa hora en la zona horaria de Nueva York, no en UTC. Nueva York está en UTC menos 5 durante el invierno y en UTC menos 4 durante el verano. Por tanto, un temporizador configurado con una hora UTC fija se desplaza una hora dos veces al año y empieza a ejecutarse antes del cierre. systemd 235 y posteriores aceptan directamente una zona horaria en OnCalendar, y Ubuntu 24.04 incluye la versión 255. Confirme la suya con 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 es el único tipo de servicio que acepta más de un ExecStart y los ejecuta en orden. Se detiene si uno termina con un código distinto de cero. Ese es exactamente el comportamiento que necesita: una actualización fallida no debe ir seguida de una pantalla con datos obsoletos. Persistent=true es importante en un VPS que se reinicia para instalar actualizaciones del kernel. Si se reinicia a las 16:15 sin esta opción, la ejecución se pierde. Con ella, el trabajo se ejecuta en cuanto la máquina vuelve a estar disponible. Un agente que debe permanecer activo en lugar de ejecutarse y terminar necesita la estructura de unidad opuesta: un proceso de larga duración con reglas Restart= y sin ningún temporizador. Esto se explica en ejecutar dsh sin interfaz en un VPS con systemd.
sudo systemctl daemon-reload
sudo systemctl enable --now research-refresh.timer
systemctl list-timers research-refresh.timerlist-timers debería mostrar una columna NEXT con la próxima ejecución entre semana, convertida a la hora local del propio servidor. Una lista vacía significa que el temporizador no está habilitado o que a la unidad le falta la sección [Install]. En ese caso, enable no tenía nada que enlazar en timers.target.
La pantalla: SQL primero, modelo al final
SQL es exacto y no tiene coste por ejecución. El modelo no cumple ninguna de las dos condiciones. Por eso, la consulta reduce el universo y sólo los registros restantes se envían al modelo. Guarde esta consulta 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;El filtro n > 50 no es decorativo. ROWS BETWEEN 49 PRECEDING AND CURRENT ROW calcula el promedio de las filas disponibles, por lo que la tercera fila de un ticker devuelve el promedio de tres días y aun así la clasifica como ma50. Si se compara con ma20, se crea un cruce al principio del historial de cada ticker que nunca ocurrió. El filtrado por número de fila elimina las filas en las que la ventana no estaba completa.
Qué ve el modelo y qué nunca ve
# /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()La línea thinking es importante en este modelo. Claude Sonnet 5 razona de forma predeterminada; sus tokens de razonamiento se facturan como salida y cuentan para max_tokens, y se devuelven en un bloque delante del texto. Sin esa línea, resp.content[0] puede ser el bloque de razonamiento y la consulta .text falla. Claude Sonnet 5 es ahora un modelo heredado. Su sucesor, claude-sonnet-5-5, rechaza disabled, y la opción que se debe usar ahí es thinking={'type': 'between_tools'}, que desactiva el razonamiento antes de generar la respuesta.
El límite de palabras del prompt del sistema limita la parte más costosa de la factura. La instrucción de usar sólo las filas proporcionadas es la que debe verificar, no aceptar sin más: elimine una columna del bloque, ejecútelo de nuevo y lea la salida. Si todavía aparece una cifra de esa columna, el modelo rellenó el dato que faltaba y el prompt no es suficientemente preciso. Esta prueba tarda dos minutos y es la única forma fiable de comprobarlo.
El modelo nunca ve la API key, nunca ve la ruta de la base de datos y nunca ejecuta una consulta. Recibe filas y devuelve texto. Este límite permite comprobar la salida, porque todos los números de la nota también deberían aparecer en el bloque enviado y se pueden comparar línea por línea. En cuanto amplía esa entrada para incluir páginas obtenidas de la web, el límite cambia, porque un titular o una publicación de un foro puede contener instrucciones dirigidas al modelo en lugar de datos dirigidos a usted. Esa es la superficie que proporcionar a un agente un backend de búsqueda SearXNG expone. Para obtener más información sobre el uso de prompts, usar Claude para el análisis financiero explica con más detalle qué información puede interpretar bien el modelo.
Una ejecución completa, de principio a fin
A las 16:20, hora de Nueva York, el temporizador inicia el servicio. refresh.py consulta el feed para cada ticker desde el último día almacenado, escribe una o dos barras nuevas para cada uno e imprime una línea por ticker. screen.py abre el mismo archivo, ejecuta la consulta de la media móvil y recibe unas pocas filas. Esas filas se convierten en un bloque de texto de unos cientos de tokens. Una llamada a la API las convierte en una nota breve, la nota se escribe en el diario y una fila termina en runs con los recuentos de tokens y el número de coincidencias.
journalctl -u research-refresh.service -n 50 --no-pagerUn registro correcto contiene una línea por ticker, después la nota y, por último, research-refresh.service: Deactivated successfully. Si lee ese registro desde una sesión de Claude Code en el servidor mientras una segunda sesión trabaja en la consulta en pantalla, ambas pueden pasarse texto en lugar de que usted copie líneas entre terminales. Después de una semana, consulte sus propios costes en la base de datos:
SELECT count(*) AS runs,
sum(input_tokens) AS in_tokens,
sum(output_tokens) AS out_tokens
FROM runs;Multiplique esos totales por los precios por millón de tokens que indique su modelo el día que los consulte. Así obtiene la cifra real en lugar de una estimación ajena. Los precios publicados cambian. La aritmética no.
Por qué los backtests se sobreajustan y cómo observarlo
Reescriba el filtro como una función de las dos longitudes de ventana, recorra una cuadrícula de pares y ordénelos por rentabilidad. El mejor par parecerá excelente. Ese es el problema, no el resultado. Una cuadrícula de 200 pares equivale a 200 experimentos, y usted se quedó con el que tuvo más suerte.
Puede observarlo en diez minutos. Divida el almacén en dos partes por fecha. Recorra la cuadrícula sólo en la primera mitad y anote el ganador. Recorra la misma cuadrícula en la segunda mitad. Si los dos ganadores están muy alejados, los parámetros se están ajustando al ruido, y un par que sólo gana en la mitad con la que se ajustó no dice nada sobre mañana.
La supervivencia es peor que el sobreajuste porque el ajuste no puede corregirla. Su lista de tickers contiene los miembros actuales del índice, por lo que sólo incluye las empresas que sobrevivieron. Si pide al feed un ticker que dejó de cotizar en 2019, devuelve un frame vacío. Eso significa que la empresa nunca entra en el almacén ni en la prueba. Todos los backtests que ejecuta ya han excluido los casos fallidos.
Los datos fundamentales reformulados rompen la línea temporal. La cifra de ingresos que la API devuelve hoy para un trimestre de 2019 no siempre es la cifra publicada en 2019. Un filtro que mezcla los datos fundamentales actuales con precios de 2019 utiliza información que no existía entonces. Los precios suelen ser seguros en este aspecto. Los datos fundamentales normalmente no lo son.
Los precios ajustados cambian con el tiempo. Con auto_adjust=True, los cierres se ajustan hacia atrás por dividendos y splits, por lo que la misma consulta ejecutada el mes que viene devuelve un historial ligeramente diferente. Guardar las filas que realmente utilizó permite reproducir el resultado y es otra razón para que exista el almacén local.
El backtest también ignora las comisiones y el deslizamiento, y supone que su orden no mueve el precio. Estos aspectos pertenecen a la ejecución, que queda fuera del alcance de este documento y se trata en ejecutar bots de trading en un VPS.
Modos de fallo y cadenas que verá
error: externally-managed-environment al ejecutar pip. Está fuera del entorno virtual. Ejecute /opt/research/venv/bin/pip mediante su ruta completa.
Could not set lock on file, seguido de un PID. Otro proceso mantiene abierto el archivo de DuckDB para escritura, normalmente un shell interactivo que olvidó cerrar. Ciérrelo. Abrir la segunda conexión con read_only=True no sirve mientras ese proceso siga escribiendo, porque DuckDB permite que varios procesos lean o que un proceso escriba, pero no ambas cosas a la vez.
Main process exited, code=killed, status=9/KILL en systemctl status. El kernel terminó el trabajo por falta de memoria. Confírmelo con journalctl -k | grep -i oom y, después, reduzca memory_limit en store.py.
Una ejecución correcta que no escribe nada. systemctl status lee active (exited) y la tabla no ha crecido. El feed devolvió frames vacíos. Este fallo es el más difícil de detectar, así que haga que el trabajo termine con un código distinto de cero cuando todos los tickers devuelvan resultados vacíos.
El temporizador se activó en un día festivo del mercado. systemd no conoce el calendario de la bolsa, por lo que Mon-Fri incluye los días festivos. La ejecución se realiza, el feed no contiene datos nuevos y el trabajo debe tratarlo como una situación normal, no como un error.
429 de la API. Ha superado un límite de tasa. El SDK de Anthropic reintenta automáticamente con espera progresiva y Anthropic(max_retries=5) aumenta el número de intentos. Si sigue fallando todos los días, el trabajo está solicitando demasiados datos en una sola ráfaga.
Lo que esto no es
Esto es un asistente de investigación. Un modelo que resume una presentación genera una interpretación de esa presentación y puede equivocarse con seguridad sobre una cifra impresa en el texto. Por eso, cada cifra de la nota debe poder rastrearse hasta una fila que usted haya enviado. Trate el resultado como una lista breve de elementos que debe leer por su cuenta. Esto no constituye asesoramiento financiero y ninguno de sus elementos es una señal.
Las pruebas retrospectivas son útiles para descartar ideas, pero no son fiables para confirmarlas. Una estrategia que falla con sus propios datos está realmente descartada. Una estrategia que supera la prueba sólo ha sobrevivido a sus datos. Esta afirmación es mucho menos importante de lo que parece a la 1 a. m.
La ejecución queda deliberadamente fuera del alcance. Las órdenes y las credenciales del broker tienen un perfil de riesgo distinto al de un sistema de investigación de sólo lectura. Mezclarlos coloca las claves de trading en la misma máquina que una instrucción para un LLM. Si quiere ver dónde encaja este patrón junto a otras opciones que merece la pena ejecutar en su propio servidor, los agentes de IA autoalojados que merece la pena ejecutar ofrecen una visión más amplia.
FAQ
¿Necesito un feed de datos de mercado de pago?
No para un prototipo. Un feed gratuito no oficial sirve para aprender cómo funciona el sistema, pero fallará porque depende de un sitio web que no tiene ninguna obligación contigo. Normalmente falla devolviendo frames vacíos en lugar de una excepción, por lo que el trabajo debe comprobar el número de filas. Cambia a un feed de pago con una API documentada y una dirección de soporte cuando los datos empiecen a influir en una decisión. El almacén de datos es lo que abarata ese cambio: sólo cambia la función de obtención, mientras que la programación, el esquema y la pantalla permanecen igual.
¿Debo guardar los precios en SQLite o DuckDB?
DuckDB usa un formato columnar y está diseñado para recorrer muchas filas y calcular un agregado, que es exactamente lo que hace una media móvil sobre diez años de barras. SQLite está orientado a filas y funciona mejor con muchas lecturas y escrituras pequeñas de varios procesos a la vez. Para un único trabajo programado que añade unos cientos de filas y después recorre millones, DuckDB encaja mejor. Si varios procesos deben escribir al mismo tiempo, SQLite en modo WAL permite que los lectores trabajen mientras un escritor confirma la transacción, y un tiempo de espera por ocupación hace que los demás escritores esperen en lugar de fallar directamente.
¿Cuánto cuestan las llamadas al LLM al mes?
Registra usage.input_tokens y usage.output_tokens de cada respuesta en una tabla. Después multiplica los totales semanales por el precio por millón de tokens que indique tu modelo el día que hagas la comprobación. Esa es la única cifra que seguirá siendo válida el próximo trimestre. Una ejecución diaria sobre una selección corta genera pocas llamadas, y la longitud de la nota es la parte que puedes controlar: limitar el resumen a 200 palabras ahorra más que enviar menos filas, porque los tokens de salida tienen un precio superior al de los tokens de entrada en todos los modelos de Claude.
¿Por qué mi trabajo terminó correctamente, pero no escribió filas nuevas?
Hay dos causas habituales. El mercado estaba cerrado porque una programación de systemd Mon-Fri incluye los festivos bursátiles. O el feed devolvió un frame vacío para todos los valores, algo que las bibliotecas cliente suelen comunicar mediante una advertencia impresa en lugar de una excepción. Por eso el proceso termina con el código 0 y systemd muestra una ejecución correcta. Distingue ambos casos comparando SELECT max(day) FROM prices con el último día de negociación real, y haz que el trabajo termine con un código distinto de cero cuando todos los valores devuelvan un resultado vacío.
¿Puede el agente decidir qué comprar?
No. Intentar construirlo para que lo haga es precisamente donde estos proyectos fallan. El modelo no tiene acceso al mercado, no conoce tus posiciones ni tu situación fiscal y no puede comprobar sus propios cálculos con ninguna fuente. Lo que hace bien es leer una gran cantidad de texto e indicarte qué pocos elementos merecen tu atención hoy. Nada de lo que escriba constituye asesoramiento financiero. La decisión y la responsabilidad correspondiente siguen siendo tuyas.