Créer un agent d’analyse boursière sur un VPS
Construisez un agent boursier auto-hébergé avec un flux de marché, DuckDB, un timer systemd à la clôture et un filtrage par LLM, sans exécuter de transactions.
Un agent d’analyse boursière auto-hébergé
Un agent d’analyse boursière auto-hébergé est un petit programme exécuté sur un serveur dont vous avez la maîtrise. 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 de transactions, et ce guide ne constitue pas un conseil financier.
Deux personnes qui construisent ce système à partir de zéro choisiront des bibliothèques différentes, mais obtiendront 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 ce stockage selon un calendrier, 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 applicative). Il s’agit d’un agent limité, doté d’une boucle fixe. Si vous préférez commencer par la version générale, le parcours progressif pour apprendre les agents IA présente successivement la boucle, les outils et la mémoire, avant de les appliquer à un problème métier. L’exécution constitue une tâche distincte, avec ses propres modes de défaillance. Elle doit plutôt être hébergée sur un VPS configuré pour les bots de trading.
Les quatre parties et leur rôle
Le feed est la seule partie qui communique avec l’extérieur. Il sait demander un ticker et une plage de dates, puis renvoyer des lignes. Tout ce qui se trouve en aval lit votre base de données plutôt que le feed. Une indisponibilité du feed vous fait donc perdre une journée de nouvelles données, au lieu de casser l’interface.
Le store est l’objectif principal de l’ensemble. Un cours de clôture quotidien que vous n’avez pas enregistré peut généralement être récupéré plus tard. En revanche, une cotation intraday, une estimation avant sa révision ou une donnée fondamentale avant sa mise à jour ne peuvent pas toujours l’être. Le store permet de conserver ce que les données indiquaient réellement le jour où elles l’indiquaient.
Le scheduler détermine le 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 et 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 produit un nombre erroné dans une phrase fluide, sans élément de 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 transmises.
Pourquoi l’exécuter sur un VPS plutôt que sur un ordinateur portable
Le planificateur 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 ultérieurement, mais les données qui sont révisées ne peuvent pas l’être. La lacune dans votre historique est donc définitive. Le même raisonnement s’applique à tout agent dont la planification et l’état enregistré doivent survivre à un redémarrage. C’est précisément ce que permet l’exécution de KiroCrew comme agent toujours actif sur votre propre VPS.
La deuxième raison est moins importante, mais bien réelle. Le serveur conserve une seule clé API, dans un seul fichier, appartenant à un seul utilisateur système sans shell de connexion, et utilisée par une seule tâche. Il est beaucoup plus difficile d’obtenir cette configuration sur l’ordinateur portable qui vous sert aussi à naviguer sur le Web. Gardez la clé en dehors du code et des entrées du modèle. C’est le sujet de la protection des clés API 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.
Les données de ce graphique
[
{
"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 au bout d’un an. La ligne 500 tickers atteint 1,260,000 lignes au bout de dix ans. Chaque ligne contient une date et quelques valeurs doubles. DuckDB stocke les colonnes sous forme compressée. Il faut donc compter des dizaines de mégaoctets, et non des gigaoctets. Ne faites pas confiance à 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 critique sur un petit VPS. Par défaut, DuckDB utilise une grande partie de la mémoire de la machine et tous ses cœurs 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 prix fait alors tuer le processus par le kernel. systemd signale Main process exited, code=killed, status=9/KILL, tandis que journalctl -k affiche le kill pour manque de mémoire. Définissez explicitement memory_limit et threads. La requête sera plus lente au lieu d’échouer.
La consommation de tokens doit être mesurée, et non estimée. 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. Au bout d’une semaine, vous connaîtrez votre volume réel. Multipliez-le ensuite par le tarif indiqué par votre modèle le jour où vous vérifiez. Deux points sont suffisamment stables pour servir de base à la planification. Les output tokens sont plus chers 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’est pas utile pour une tâche exécutée une fois par jour, car la durée de vie du cache se mesure en minutes. Lors de l’exécution suivante, le bloc mis en cache a expiré et vous payez de nouveau le prix complet des input tokens. Le caching est rentable lorsqu’une exécution effectue plusieurs appels sur le même bloc de texte volumineux.
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/dataVé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 renvoie 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 bonne pratique. C’est le seul répertoire dans lequel pip est autorisé à écrire.
La clé d’API doit être stockée dans un fichier que l’utilisateur du service peut lire et 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/envAjoutez-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-hereLe 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 conLa clé primaire sur (ticker, day) permet de relancer l’actualisation sans risque. INSERT OR REPLACE remplace une ligne qui existe déjà pour ce ticker et cette journée. Une deuxième exécution du backfill ne double donc pas le contenu de la table. Sans cette clé, une relance 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 avertir.
DuckDB autorise un seul processus à ouvrir le fichier en écriture. Un deuxième processus d’écriture échoue immédiatement avec Could not set lock on file, suivi du PID qui détient le verrou. En pratique, il s’agit généralement du shell interactif duckdb que vous avez laissé ouvert dans un autre terminal. Les lecteurs passent par read_only=True, ce qui explique pourquoi connect accepte ce flag. Si plusieurs processus doivent réellement écrire au même moment, il faut utiliser un autre moteur. SQLite en mode WAL permet aux lecteurs de travailler pendant qu’un processus valide une écriture. Un délai d’attente évite alors que les autres processus d’écriture échouent immédiatement. DuckDB et SQLite pour une charge serveur compare les deux moteurs, et exécuter SQLite en production sur un VPS présente les réglages nécessaires au bon fonctionnement de WAL.
Le job de rafraîchissement
# /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-le une fois manuellement :
sudo -u research /opt/research/venv/bin/python /opt/research/refresh.pyLa première exécution récupère plusieurs années d’historique et affiche une ligne par ticker, avec un nombre de lignes de l’ordre de plusieurs milliers. Exécutez-le de nouveau une minute plus tard. Chaque ligne affiche alors 1 ou 2 lignes, car le job reprend à partir du dernier jour déjà enregistré. Cette deuxième exécution constitue le vrai 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 (la formulation exacte varie selon les versions de la bibliothèque), puis renvoie un DataFrame vide. Sans ce contrôle, le job n’insère rien, se termine avec le code 0, et systemd affiche une exécution saine alors que la table cesse discrètement de croître. Vous ne le découvrez que plusieurs semaines plus tard, lorsqu’un écran renvoie les mêmes lignes chaque jour.
La gestion des timestamps répond également à une raison précise. L’index renvoyé par le flux peut inclure le fuseau horaire de la place boursière. Supprimer le décalage ne revient pas à convertir l’heure en UTC. Une séance de Tokyo horodatée à minuit en heure locale est convertie au jour calendaire précédent en UTC. Une conversion en UTC décale donc silencieusement chaque barre japonaise d’un jour et casse la clé primaire. tz_localize(None) conserve la date de séance propre à la place boursière, 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, et les dernières transactions prennent quelques minutes avant d’être finalisées. Le job s’exécute donc à 16:20. Indiquez cette heure dans le fuseau de New York, pas en UTC. New York est en UTC moins 5 en hiver et en 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 ultérieures acceptent directement un fuseau horaire dans OnCalendar, et Ubuntu 24.04 fournit 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.targetType=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é : une actualisation échouée ne doit pas être suivie de l’affichage de données obsolètes. Persistent=true est important sur un VPS qui redémarre pour appliquer des mises à jour du noyau. Redémarrez à 16:15 sans cette directive et l’exécution est simplement perdue. Avec elle, le job s’exécute dès que la machine est de nouveau disponible. Un agent qui doit rester actif au lieu de se lancer puis de se terminer nécessite une structure d’unité opposée : un processus long avec des règles Restart=, sans timer. C’est ce que présente exécuter dsh sans interface sur un VPS avec systemd.
sudo systemctl daemon-reload
sudo systemctl enable --now research-refresh.timer
systemctl list-timers research-refresh.timerlist-timers doit afficher une colonne NEXT contenant la prochaine exécution en semaine, convertie dans l’heure locale du serveur. Une liste vide signifie que le timer n’est pas activé ou que l’unité ne contient pas sa section [Install]. Dans ce cas, enable n’avait rien à relier dans timers.target.
L’écran : SQL d’abord, modèle en dernier
SQL est exact et ne coûte rien par exécution. Ce n’est pas le cas du modèle. La requête réduit donc l’univers, et seuls les résultats restants sont envoyés au modèle. Enregistrez cette requête 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, si bien que la troisième ligne d’un ticker renvoie la moyenne de trois jours tout en l’appelant encore 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 élimine les lignes pour lesquelles la fenêtre n’était 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 longueur définie dans le prompt système plafonne la moitié la plus coûteuse de la facture. C’est l’instruction d’utiliser uniquement les lignes fournies qu’il faut vérifier, plutôt que prendre pour acquise : supprimez une colonne du bloc, relancez l’analyse, puis lisez la sortie. Si une valeur de cette colonne apparaît encore, le modèle a comblé le manque et votre prompt n’est pas assez strict. Ce test prend deux minutes et c’est le seul moyen fiable de le vérifier.
Le modèle ne voit jamais la clé d’API, ne voit jamais le chemin de la base de données et n’exécute aucune requête. Il reçoit des lignes et renvoie du texte. Cette séparation rend la sortie vérifiable, car chaque nombre de la note doit également apparaître dans le bloc envoyé et vous pouvez les comparer ligne par ligne. Dès que vous élargissez cette entrée pour y inclure des pages récupérées sur le Web, la séparation change, car un titre ou une publication de forum peut contenir des instructions destinées au modèle plutôt que des faits qui vous sont destinés. C’est la surface qu’ouvre l’ajout d’un backend de recherche SearXNG à un agent. Pour approfondir l’aspect lié au prompting, l’utilisation de Claude pour l’analyse financière explique plus en détail ce que le modèle sait bien lire.
Une exécution complète, de bout en bout
À 16:20, heure de New York, le timer démarre le service. refresh.py interroge le feed pour chaque ticker à partir du dernier jour enregistré, écrit une ou deux nouvelles barres 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 note courte, la note est envoyée dans le journal et une ligne est ajoutée dans runs avec le nombre de tokens et le nombre de correspondances.
journalctl -u research-refresh.service -n 50 --no-pagerUn journal en bon état contient une ligne par ticker, puis la note, puis research-refresh.service: Deactivated successfully. Si vous lisez ce journal depuis une session Claude Code sur le serveur pendant qu’une deuxième session travaille sur la requête affichée à l’écran, les deux sessions peuvent s’échanger du texte au lieu de vous obliger à copier des lignes entre les terminaux. Après une semaine, récupérez vos propres coûts 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 indiqués par votre modèle le jour où vous les consultez. Vous obtenez ainsi le montant réel, et non une estimation. Les tarifs publiés changent. Le calcul, lui, ne change pas.
Pourquoi les backtests surajustent les données et comment l’observer
Réécrivez le résultat en fonction des deux durées de fenêtre, parcourez une grille de paires et classez-les par rendement. La meilleure paire semblera excellente. C’est le problème, pas le résultat. Une grille de 200 paires représente 200 expériences, et vous avez conservé celle qui a bénéficié du plus de chance.
Vous pouvez l’observer en dix minutes. Divisez le store en deux selon la date. Parcourez la grille uniquement sur la première moitié et notez la paire gagnante. Parcourez la même grille sur la seconde moitié. Si les deux paires gagnantes sont très différentes, les paramètres s’ajustent au bruit. Une paire qui ne gagne que sur la moitié ayant servi à son réglage ne dit rien sur demain.
Le biais de survie 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. Chaque backtest que vous exécutez a 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 cours de 2019 utilise des informations qui n’existaient pas à l’époque. Les cours sont généralement fiables dans ce contexte. Les fondamentaux ne le sont généralement pas.
Les cours ajustés changent après coup. Avec auto_adjust=True, les cours de clôture sont ajustés rétrospectivement 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. Stocker les lignes réellement utilisées permet de reproduire un résultat. C’est aussi une raison supplémentaire de conserver un store local.
Le backtest ignore également les commissions et le slippage. Il suppose aussi que votre ordre ne fait pas varier le cours. Ces éléments relèvent de l’exécution, qui est hors sujet ici et traitée dans lancer des bots de trading 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 faute 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), mais la table n’a pas grandi. Le flux a renvoyé des frames vides. Cette erreur est difficile à détecter, ajoutez donc une condition qui fait quitter le job avec un code différent de zéro lorsque tous les tickers reviennent vides.
Le timer s’est déclenché un jour férié boursier. systemd ne connaît pas le calendrier de la place boursière, donc Mon-Fri inclut les jours fériés. L’exécution a lieu, le flux ne contient aucune nouvelle donnée et le job doit traiter cette situation comme normale, pas comme une erreur.
429 renvoyé par 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 envoie trop de requêtes en une seule rafale.
Ce que ceci n’est pas
Il s’agit d’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 chiffrée de la note doit renvoyer à une ligne que vous lui avez transmise. Considérez la sortie comme une liste restreinte d’éléments à lire vous-même. Rien de tout cela ne constitue un conseil financier, et aucun élément ne constitue un signal.
Les backtests servent à écarter des idées et sont peu fiables pour les confirmer. Une stratégie qui échoue sur vos propres données est réellement invalidée. Une stratégie qui réussit n’a résisté qu’à vos données. C’est une conclusion bien plus limitée qu’elle n’en a l’air à 1 h du matin.
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. Si vous voulez voir où ce modèle se situe par rapport aux autres services qu’il peut être utile d’exécuter sur votre propre serveur, les agents IA auto-hébergés qu’il vaut la peine d’exécuter proposent une vue d’ensemble plus large.
FAQ
Ai-je besoin d’un flux de données de marché payant ?
Pas pour un prototype. Un flux gratuit non officiel convient pour comprendre le fonctionnement du système, mais il finira par tomber en panne, car il dépend d’un site qui ne vous doit rien. Il échoue généralement en renvoyant des frames vides plutôt qu’une exception. Votre job doit donc vérifier le nombre de lignes. Passez à un flux payant doté d’une API documentée et d’une adresse de support dès que les données commencent à guider une décision. C’est le store qui rend ce changement peu coûteux : seule la fonction de récupération change, tandis que la planification, le schéma et l’écran restent identiques.
Dois-je conserver les cours dans SQLite ou dans DuckDB ?
DuckDB utilise un stockage en colonnes et est 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 effectuées simultanément par plusieurs processus. Pour un job planifié unique 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 une 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 aux 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 entraîne peu d’appels. La longueur de la note est le paramètre que vous contrôlez : 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 pour chaque modèle Claude.
Pourquoi mon job a-t-il 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 boursières. Ou bien le flux a renvoyé une frame vide pour chaque ticker. Les bibliothèques clientes signalent souvent ce cas par un avertissement affiché, et non par une exception. Le processus se termine donc avec le code 0, et systemd indique toujours une exécution réussie. Distinguez ces deux cas en comparant SELECT max(day) FROM prices au dernier jour réel de cotation. Faites également échouer le job avec un code différent de zéro lorsque tous les tickers sont vides.
L’agent peut-il décider quoi acheter ?
Non. C’est précisément en essayant de le faire que ces projets rencontrent des problèmes. Le modèle n’a pas accès au marché, ne connaît ni vos positions ni votre situation fiscale et ne peut pas vérifier ses propres chiffres. Il est surtout utile pour lire une grande quantité de texte et vous indiquer les quelques éléments qui méritent votre attention aujourd’hui. Ce qu’il rédige ne constitue pas un conseil financier. La décision et la responsabilité qui en découle restent les vôtres.