SSD Nodes Learn 🎉 VPS dès $4.99/mois
Guides Matt ConnorPar Matt Connor

Agent de recherche boursière auto-hébergé sur VPS

Construisez un agent de recherche boursière sur VPS avec flux de marché, DuckDB, timer systemd à la clôture et filtrage par LLM via l’API Claude.

Ce qu’est un agent auto-hébergé de recherche boursière

Un agent auto-hébergé de recherche boursière est un petit programme exécuté sur un serveur que vous administrez. Il récupère périodiquement des données de marché, les conserve dans une base de données locale, les filtre, puis demande à un grand modèle de langage (LLM) de rédiger un compte rendu des changements. Il lit et filtre les données. Il n’exécute pas d’ordres, et rien dans ce guide ne constitue un conseil financier.

Deux personnes qui construisent ce système depuis zéro choisiront des bibliothèques différentes, mais obtiendront tout de même les mêmes quatre composants : un flux qui fournit les cours et les données fondamentales, un stockage local qui conserve chaque ligne récupérée, une tâche qui actualise le stockage à intervalles réguliers, et une couche LLM qui transforme les lignes conservées en phrases. Ce guide construit cette architecture avec Python, DuckDB, un timer systemd et l’API Claude (interface de programmation d’application). L’exécution des ordres est une tâche distincte, avec des modes de défaillance différents. Elle doit plutôt être déployée sur un VPS configuré pour les bots de trading.

Les quatre composants et leur rôle

Le flux de données est le seul composant qui communique avec l’extérieur. Il sait demander un ticker et une plage de dates, puis renvoyer des lignes. Tout ce qui suit lit votre base de données plutôt que le flux. Une panne du flux vous prive donc d’une journée de nouvelles données au lieu de rendre l’interface inutilisable.

Le stockage est le véritable objectif de l’ensemble. Une clôture journalière que vous n’avez pas enregistrée peut généralement être récupérée plus tard. En revanche, une cotation intraday, une estimation avant sa révision ou une donnée fondamentale avant sa rectification ne peuvent pas toujours l’être. Le stockage permet de conserver ce que les données indiquaient réellement le jour où elles l’indiquaient.

Le scheduler décide du moment de l’actualisation. Sur un serveur, il s’agit d’un timer systemd. C’est pourquoi le VPS est ici plus important que le code.

La couche LLM lit un court bloc de texte produit par votre SQL, puis en rédige un résumé. Elle ne se connecte jamais à la base de données et ne construit jamais la requête. Si le modèle écrit le SQL, un seul token incorrect transforme un nombre en valeur erronée dans une phrase fluide, sans référence pour effectuer une comparaison. Si le SQL produit les nombres, le modèle ne peut se tromper que dans le texte, que vous pouvez vérifier par rapport aux lignes que vous lui avez transmises.

Pourquoi l’exécuter sur un VPS plutôt que sur un ordinateur portable

Le scheduler est la raison principale. La clôture du marché américain à 16:00, heure de New York, a lieu à 22:00 à Berlin et à 04:00 le lendemain matin à Jakarta. Un ordinateur portable est en veille dans les deux cas. Une exécution manquée coûte plus cher qu’une note ajoutée en retard : les barres journalières peuvent généralement être récupérées plus tard, mais les données révisées ne peuvent pas l’être. La lacune dans votre historique est donc permanente.

La seconde raison est moins importante, mais bien réelle. Le serveur conserve une seule API key dans un seul fichier. Ce fichier appartient à un seul utilisateur système sans shell de connexion et n’est utilisé que par un seul job. Cette configuration est beaucoup plus difficile à garantir sur l’ordinateur portable que vous utilisez également pour naviguer sur le Web. Gardez la clé hors du code et hors des entrées du modèle. C’est le sujet de garder les API keys hors d’un agent IA.

Dimensionnement : disque, RAM et tokens

Le disque est la partie la plus simple. Une barre quotidienne correspond à une ligne par ticker et par jour de cotation, et une année de cotation aux États-Unis compte environ 252 jours.

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"
  }
]

Vingt tickers représentent 5,040 lignes après un an. La ligne 500 tickers atteint 1,260,000 lignes après dix ans. Chaque ligne contient une date et quelques valeurs double. DuckDB stocke les colonnes sous forme compressée : le volume se compte donc en dizaines de mégaoctets, pas en gigaoctets. Ne vous fiez pas à cette estimation, y compris à la mienne. Exécutez du -h /opt/research/data/market.duckdb après votre premier backfill et utilisez votre propre valeur.

La RAM est le point sensible sur un petit VPS. Par défaut, DuckDB utilise une grande partie de la mémoire et de tous les cœurs de la machine pour une seule requête. C’est adapté à un serveur d’analytics, mais pas à une machine de 2 GB qui exécute aussi d’autres services. Une agrégation sur toute la table des cours peut alors faire tuer le processus par le kernel. systemd signale Main process exited, code=killed, status=9/KILL, tandis que journalctl -k indique le kill provoqué par le manque de mémoire. Définissez explicitement memory_limit et threads : la requête sera plus lente au lieu d’échouer.

Les tokens doivent être mesurés, pas estimés. Chaque réponse de l’API Messages contient un objet usage avec input_tokens et output_tokens. Écrivez ces deux valeurs dans une table à chaque appel. Après une semaine, vous connaîtrez votre volume réel et pourrez le multiplier par les tarifs indiqués par votre modèle le jour où vous les vérifiez. Deux points sont suffisamment stables pour servir de base aux prévisions. Les output tokens coûtent plus cher que les input tokens sur tous les modèles Claude. Limiter la note à 200 mots réduit donc davantage la facture que de raccourcir les données envoyées. Le prompt caching n’aide pas une tâche exécutée une fois par jour, car sa durée de vie se mesure en minutes. À l’exécution suivante, le bloc mis en cache a expiré et vous payez de nouveau le tarif complet des input tokens. Le caching devient utile lorsqu’une même exécution effectue plusieurs appels avec un bloc de texte volumineux identique.

Installer les composants

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

Vérifiez l’installation avant d’écrire du code :

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

Cette commande affiche ok. Si vous avez ignoré l’environnement virtuel et exécuté pip install avec le Python système, Ubuntu 24.04 vous arrête avec error: externally-managed-environment, car la distribution gère /usr/lib/python3 et refuse que pip y écrive. Le venv n’est pas une simple précaution. C’est le seul répertoire dans lequel pip peut écrire.

La clé d’API doit être stockée dans un fichier que l’utilisateur du service peut lire, mais que personne d’autre ne peut lire :

sudo install -d -m 755 /etc/research
sudo install -m 640 -o root -g research /dev/null /etc/research/env
sudoedit /etc/research/env

Ajoutez-y une seule ligne, sans guillemets ni export, car systemd analyse directement ce fichier au lieu de le transmettre à un shell :

ANTHROPIC_API_KEY=sk-ant-your-key-here

Le stockage : deux tables

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

La clé primaire de (ticker, day) rend l’actualisation sûre à relancer. INSERT OR REPLACE remplace une ligne qui existe déjà pour ce ticker et cette journée. Vous pouvez donc exécuter le rattrapage deux fois sans dupliquer la table. Sans cette clé, une nouvelle exécution après un crash duplique silencieusement chaque barre. Toutes les moyennes calculées ensuite sont alors fausses, sans aucun message d’erreur pour vous en informer.

DuckDB permet à un seul processus à la fois d’ouvrir le fichier en écriture. Un deuxième writer échoue immédiatement avec Could not set lock on file, suivi du PID du processus qui détient le fichier. En pratique, il s’agit souvent du shell interactif duckdb que vous avez laissé ouvert dans un autre terminal. Les lecteurs passent par read_only=True, raison pour laquelle connect accepte le flag. Si plusieurs processus doivent réellement écrire au même moment, il faut utiliser un autre moteur. En mode WAL, SQLite permet aux lecteurs de travailler pendant qu’un writer valide une transaction. Un busy timeout fait attendre les autres writers au lieu de les faire échouer. DuckDB ou SQLite pour une charge serveur compare les deux solutions, et exécuter SQLite en production sur un VPS présente les réglages nécessaires au bon fonctionnement de WAL.

La tâche d’actualisation

# /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()

Exécutez-la manuellement une fois :

sudo -u research /opt/research/venv/bin/python /opt/research/refresh.py

La première exécution récupère plusieurs années d’historique et affiche une ligne par ticker, avec un nombre de lignes qui se compte en milliers. Exécutez-la de nouveau une minute plus tard. Chaque ligne doit alors afficher 1 ou 2 lignes, car la tâche reprend à partir du dernier jour déjà enregistré. Cette deuxième exécution constitue le véritable test : si les nombres restent de l’ordre de plusieurs milliers, max(day) ne renvoie rien et l’insertion réécrit tout votre historique chaque nuit.

Le contrôle df.empty est la ligne la plus importante du fichier. Ticker.history() ne lève pas d’exception pour un symbole incorrect ou retiré de la cote. Il affiche un avertissement indiquant que le symbole a peut-être été retiré de la cote et qu’aucune donnée de cours n’a été trouvée — le texte exact varie selon les versions de la bibliothèque — puis renvoie un DataFrame vide. Sans ce contrôle, la tâche n’écrit rien, se termine avec le code 0 et systemd affiche une exécution saine, tandis que la table cesse discrètement de croître. Vous ne le découvrirez que plusieurs semaines plus tard, en consultant un écran qui renvoie chaque jour les mêmes lignes.

La gestion des timestamps a également une raison précise. L’index renvoyé par le flux peut inclure le fuseau horaire de la place de marché. Supprimer le décalage n’est pas équivalent à convertir l’heure en UTC. Une session de Tokyo horodatée à minuit, heure locale, correspond au jour civil précédent en UTC. Une conversion en UTC décale donc silencieusement chaque barre japonaise d’un jour en arrière et casse la clé primaire. tz_localize(None) conserve la date de séance utilisée par la place de marché, ce qui correspond à la définition d’une barre journalière.

Le timer qui se déclenche à la clôture du marché

Le marché américain clôture à 16:00, heure de New York. Les dernières cotations mettent quelques minutes à se stabiliser. Le job s’exécute donc à 16:20. Indiquez cette heure selon le fuseau de New York, et non en UTC. New York est à UTC moins 5 en hiver et à UTC moins 4 en été. Un timer configuré avec une heure UTC fixe se décale donc d’une heure deux fois par an et commence à se déclencher avant la clôture. systemd 252 et les versions suivantes acceptent directement un fuseau horaire dans OnCalendar. Ubuntu 24.04 est livré avec la version 255. Vérifiez votre version avec 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 est le seul type de service qui accepte plusieurs ExecStart. Il les exécute dans l’ordre et s’arrête dès que l’un d’eux se termine avec un code différent de zéro. C’est exactement le comportement recherché : un échec de l’actualisation ne doit pas être suivi de l’affichage de données obsolètes. Persistent=true est important sur un VPS qui redémarre pour appliquer des mises à jour du kernel. Si la machine redémarre à 16:15 sans cette option, l’exécution est simplement perdue. Avec cette option, le job s’exécute dès que la machine est de nouveau disponible.

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

list-timers doit afficher une colonne NEXT contenant la prochaine exécution en semaine, convertie dans le fuseau horaire local du serveur. Une liste vide signifie que le timer n’est pas activé ou que l’unité ne contient pas de section [Install]. Dans ce cas, enable n’avait rien à relier dans timers.target.

L’écran : SQL d’abord, le modèle en dernier

SQL est exact et ne coûte rien à chaque exécution. Le modèle ne l’est pas. La requête réduit donc l’ensemble des données, et seuls les résultats restants sont envoyés au modèle. Enregistrez-la sous /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;

Le filtre n > 50 n’est pas décoratif. ROWS BETWEEN 49 PRECEDING AND CURRENT ROW calcule la moyenne des lignes disponibles, quelle que soit leur quantité. La troisième ligne d’un ticker renvoie donc la moyenne de trois jours et l’appelle quand même ma50. Si vous comparez cette valeur à ma20, vous créez un croisement au début de l’historique de chaque ticker, alors qu’il n’a jamais eu lieu. Le filtrage sur le numéro de ligne supprime les lignes pour lesquelles la fenêtre n’est pas complète.

Ce que le modèle voit et ce qu’il ne voit jamais

# /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()

La limite de mots définie dans le prompt système plafonne la moitié coûteuse de la facture. Vous devez vérifier l’instruction qui impose d’utiliser uniquement les lignes fournies, au lieu de lui faire confiance : supprimez une colonne du bloc, relancez l’exécution et lisez la sortie. Si une valeur pour cette colonne apparaît encore, le modèle a comblé la lacune et votre prompt n’est pas assez strict. Ce test prend deux minutes. C’est la seule manière fiable de le vérifier.

Le modèle ne voit jamais la clé API ni le chemin de la base de données. Il n’exécute aucune requête. Il reçoit des lignes et renvoie du texte. Cette séparation rend la sortie vérifiable : chaque nombre de la note doit également apparaître dans le bloc envoyé, ce qui vous permet de les comparer ligne par ligne. Pour approfondir l’aspect lié au prompting, utiliser Claude pour l’analyse financière explique plus précisément ce que le modèle sait bien lire.

Un run, de bout en bout

À 16:20, heure de New York, le timer démarre le service. refresh.py interroge le feed pour chaque ticker en partant du dernier jour enregistré, écrit une ou deux nouvelles bougies pour chacun et affiche une ligne par ticker. screen.py ouvre le même fichier, exécute la requête de moyenne mobile et récupère quelques lignes. Ces lignes deviennent un bloc de texte de quelques centaines de tokens. Un appel d’API les transforme en une courte note, la note est ajoutée au journal et une ligne est insérée dans runs avec le nombre de tokens et le nombre de résultats.

journalctl -u research-refresh.service -n 50 --no-pager

Un journal correct contient une ligne par ticker, puis la note, puis research-refresh.service: Deactivated successfully. Après une semaine, relisez vos coûts directement dans la base de données :

SELECT count(*) AS runs,
       sum(input_tokens)  AS in_tokens,
       sum(output_tokens) AS out_tokens
FROM runs;

Multipliez ces totaux par les tarifs par million de tokens publiés par votre modèle le jour où vous les consultez. Vous obtenez ainsi le montant réel, et non l’estimation de quelqu’un d’autre. Les tarifs publiés changent. Le calcul, lui, ne change pas.

Pourquoi les backtests surajustent les paramètres et comment l’observer

Réécrivez le screen en fonction des deux durées de fenêtre, testez une grille de couples et classez-les par rendement. Le meilleur couple semblera excellent. C’est le problème, pas le résultat. Une grille de 200 couples représente 200 expériences, et vous avez conservé celui qui a bénéficié du plus de chance.

Vous pouvez l’observer en dix minutes. Séparez le store en deux selon la date. Testez la grille uniquement sur la première moitié et notez le gagnant. Testez la même grille sur la seconde moitié. Si les deux gagnants sont très différents, les paramètres s’ajustent au bruit, et un couple qui ne gagne que sur la moitié ayant servi à le régler ne permet pas de tirer de conclusion sur demain.

Le survivorship bias est pire que le surajustement, car aucun réglage ne peut le corriger. Votre liste de tickers contient les membres actuels de l’indice, donc uniquement les entreprises qui ont survécu. Si vous demandez au feed un ticker radié en 2019, il renvoie une frame vide. Cette entreprise n’entre donc jamais dans votre store ni dans votre test. Tous les backtests que vous exécutez ont déjà exclu les échecs.

Les fondamentaux retraités rompent la chronologie. Le chiffre d’affaires que l’API renvoie aujourd’hui pour un trimestre de 2019 n’est pas toujours celui qui avait été publié en 2019. Un screen qui mélange les fondamentaux actuels avec les prix de 2019 utilise des informations qui n’existaient pas à l’époque. Les prix sont généralement fiables dans ce contexte. Les fondamentaux ne le sont généralement pas.

Les prix ajustés évoluent après coup. Avec auto_adjust=True, les cours de clôture sont ajustés rétroactivement pour tenir compte des dividendes et des splits. La même requête exécutée le mois prochain renvoie donc un historique légèrement différent. Enregistrer les lignes que vous avez réellement utilisées permet de reproduire le résultat. C’est aussi une raison supplémentaire de conserver un store local.

Le backtest ignore également les commissions et le slippage, et suppose que votre ordre ne fait pas varier le prix. Ces éléments relèvent de l’exécution, qui sort du périmètre de ce tutoriel et est traitée dans exécuter des trading bots sur un VPS.

Modes d’échec et messages affichés

error: externally-managed-environment lors de l’exécution de pip. Vous n’êtes pas dans l’environnement virtuel. Appelez /opt/research/venv/bin/pip avec son chemin complet.

Could not set lock on file, suivi d’un PID. Un autre processus a ouvert le fichier DuckDB en écriture, généralement un shell interactif que vous avez oublié de fermer. Fermez-le ou ouvrez la deuxième connexion avec read_only=True.

Main process exited, code=killed, status=9/KILL dans systemctl status. Le kernel a tué le job par manque de mémoire. Confirmez-le avec journalctl -k | grep -i oom, puis réduisez memory_limit dans store.py.

Une exécution réussie qui n’écrit rien. systemctl status lit active (exited) et la table n’a pas grandi. Le feed a renvoyé des frames vides. Cette erreur est la plus difficile à détecter. Faites donc échouer le job avec un code de sortie différent de zéro lorsque tous les tickers renvoient des résultats vides.

Le timer s’est déclenché un jour férié de marché. systemd ne connaît pas le calendrier de la place de marché. Mon-Fri inclut donc les jours fériés. L’exécution a lieu, le feed ne contient aucune nouvelle donnée et le job doit traiter cette situation comme normale, et non comme une erreur.

429 provenant de l’API. Vous avez dépassé une limite de débit. Le SDK Anthropic réessaie automatiquement avec un backoff, et Anthropic(max_retries=5) augmente le nombre de tentatives. Si l’échec se reproduit chaque jour, le job demande trop de données en une seule rafale.

Ce que ceci n’est pas

Ceci est un assistant de recherche. Un modèle qui résume un document réglementaire produit une interprétation de ce document. Il peut se tromper avec assurance sur un chiffre imprimé dans le texte. C’est pourquoi chaque donnée de la note doit pouvoir être reliée à une ligne que vous avez envoyée. Considérez la sortie comme une liste restreinte d’éléments à lire vous-même. Il ne s’agit pas de conseils financiers, et rien de tout cela ne constitue un signal.

Les backtests sont utiles pour rejeter des idées, mais peu efficaces pour les confirmer. Une stratégie qui échoue sur vos propres données est réellement abandonnée. Une stratégie qui réussit n’a survécu qu’à vos données. C’est une conclusion beaucoup moins forte qu’elle ne le semble à 1am.

L’exécution reste volontairement hors périmètre. Les ordres et les identifiants du broker présentent un profil de risque différent de celui d’un serveur de recherche en lecture seule. Les mélanger revient à placer les clés de trading sur la même machine qu’un prompt destiné à un LLM. Pour voir où cette architecture se situe par rapport aux autres outils qu’il peut être pertinent d’exécuter sur votre propre serveur, consultez les agents IA auto-hébergés qu’il est pertinent d’exécuter.

FAQ

Ai-je besoin d’un flux de données de marché payant ?

Pas pour un prototype. Un flux gratuit et non officiel suffit pour comprendre la structure du système, mais il tombera en panne, car il dépend d’un site qui ne vous doit rien. Il tombe généralement en panne en renvoyant des DataFrames vides plutôt qu’une exception. Votre tâche doit donc vérifier le nombre de lignes. Passez à un flux payant avec une API documentée et une adresse de support dès que les données commencent à alimenter une décision. C’est le stockage qui rend ce remplacement simple : seule la fonction de récupération change, tandis que la planification, le schéma et l’interface restent inchangés.

Dois-je conserver les cours dans SQLite ou DuckDB ?

DuckDB est orienté colonnes et conçu pour parcourir de nombreuses lignes afin de calculer un agrégat. C’est exactement le cas d’une moyenne mobile calculée sur dix ans de barres. SQLite est orienté lignes et convient mieux à de nombreuses petites lectures et écritures simultanées depuis plusieurs processus. Pour une seule tâche planifiée qui ajoute quelques centaines de lignes, puis parcourt des millions de lignes, DuckDB est mieux adapté. Si plusieurs processus doivent écrire en même temps, SQLite en mode WAL permet aux lecteurs de continuer à travailler pendant qu’un writer valide sa transaction. Un délai d’attente en cas de verrouillage permet aux autres writers d’attendre au lieu d’échouer immédiatement.

Combien coûtent les appels au LLM par mois ?

Enregistrez usage.input_tokens et usage.output_tokens de chaque réponse dans une table, puis multipliez vos totaux hebdomadaires par le prix par million de tokens indiqué par votre modèle le jour où vous effectuez le calcul. C’est le seul chiffre qui restera exact au prochain trimestre. Une exécution quotidienne sur une liste courte ne génère qu’un petit nombre d’appels. Vous contrôlez surtout la longueur de la note : limiter le résumé à 200 mots permet d’économiser davantage que réduire le nombre de lignes envoyées, car les tokens de sortie sont plus chers que les tokens d’entrée avec tous les modèles Claude.

Pourquoi ma tâche a-t-elle réussi sans écrire de nouvelles lignes ?

Deux causes sont courantes. Le marché était fermé, car une planification systemd Mon-Fri tient compte des jours fériés des places de marché. Ou bien le flux a renvoyé un DataFrame vide pour chaque ticker. Les bibliothèques clientes signalent souvent ce cas par un avertissement affiché, plutôt que par une exception. Le processus se termine donc quand même avec le code 0 et systemd affiche une exécution réussie. Pour distinguer ces cas, comparez SELECT max(day) FROM prices au dernier jour réel de cotation et faites terminer la tâche avec un code différent de 0 lorsque tous les tickers renvoient un résultat vide.

L’agent peut-il décider quoi acheter ?

Non. C’est précisément en essayant de le faire que ces projets échouent. Le modèle n’a pas accès au marché, ne connaît pas votre position ni votre situation fiscale, et ne peut pas vérifier ses propres chiffres. Il est utile pour lire un grand volume de texte et vous indiquer les quelques éléments qui méritent votre attention aujourd’hui. Rien de ce qu’il produit ne constitue un conseil financier. La décision et la responsabilité associée restent les vôtres.