Zelfgehoste aandelenonderzoek agent bouwen op een VPS
Bouw uw eigen aandelenonderzoek agent op een VPS met Python, DuckDB en een systemd timer. Leer hoe u marktdata automatiseert en via een LLM laat samenvatten na beursluiting.
Wat een zelfgehoste agent voor aandelenonderzoek is
Een zelfgehoste agent voor aandelenonderzoek is een klein programma op een server in uw eigen beheer. Het haalt volgens een vast schema marktgegevens op, slaat deze op in een lokale database, voert een selectie uit en vraagt een large language model (LLM) om een samenvatting van de wijzigingen te schrijven. Het leest en filtert gegevens. Het voert geen transacties uit en niets in deze handleiding vormt financieel advies.
Twee personen die dit vanaf nul opbouwen, zullen verschillende bibliotheken kiezen, maar komen uiteindelijk uit bij dezelfde vier onderdelen: een feed die koersen en fundamentele gegevens levert, een lokale opslag die elke opgehaalde rij bewaart, een taak die de opslag periodiek ververst, en een LLM-laag die de overgebleven rijen omzet in zinnen. Deze handleiding bouwt die structuur op met Python, DuckDB, een systemd timer en de Claude API (application programming interface). Uitvoering van transacties is een afzonderlijke taak met eigen faalmodi en hoort thuis op een VPS die is ingericht voor trading bots.
De vier onderdelen en hun respectievelijke functies
De feed is het enige onderdeel dat communiceert met de buitenwereld. Het is in staat om een ticker en een datumbereik op te vragen en rijen met gegevens terug te leveren. Alles wat zich stroomafwaarts bevindt, leest uw database in plaats van de feed; een storing in de feed kost u daarom slechts één dag aan nieuwe gegevens in plaats van een defect scherm.
De store is het hoofddoel van deze hele opzet. Een dagelijkse slotkoers die u niet heeft vastgelegd, kan meestal later opnieuw worden opgehaald. Een intraday-koers, een schatting voordat deze werd herzien, of een fundamenteel cijfer voordat dit werd gecorrigeerd, kan dat niet. De store is de manier waarop u een archief opbouwt van wat de data daadwerkelijk aangaf op de dag dat deze werd gepubliceerd.
De scheduler bepaalt wanneer de verversing plaatsvindt. Op een server is dit een systemd timer, wat de reden is dat de VPS hier belangrijker is dan de code zelf.
De LLM-laag leest een kort tekstblok dat door uw SQL is gegenereerd en schrijft daar een samenvatting van. Deze laag maakt nooit verbinding met de database en bouwt nooit de query op. Als het model de SQL schrijft, kan één onjuist token leiden tot een foutief getal in een vloeiende zin, zonder dat er iets is om dit mee te vergelijken. Als SQL de getallen genereert, kan het model alleen de tekst foutief formuleren, en u kunt de tekst controleren aan de hand van de rijen die u heeft verzonden.
Waarom op een VPS draaien in plaats van op een laptop
De scheduler is de reden. Een beurssluiting in de VS om 16:00 uur New Yorkse tijd is 22:00 uur in Berlijn en 04:00 uur de volgende ochtend in Jakarta. Een laptop is op beide tijdstippen in slaapstand. Een gemiste uitvoering kost meer dan een late notitie: dagelijkse koersgrafieken kunnen meestal later opnieuw worden opgehaald, maar alles wat wordt herzien kan dat niet, waardoor het gat in uw gegevens permanent is. Dezelfde redenering geldt voor elke agent waarvan de planning en de opgeslagen status een herstart moeten overleven, wat de kern vormt van KiroCrew als een altijd actieve agent op uw eigen VPS draaien.
De tweede reden is kleiner, maar nog steeds reëel. De server bevat één API key, in één bestand, eigendom van één systeemgebruiker zonder login-shell, gebruikt door één taak. Dat is veel lastiger te regelen op de laptop waarmee u ook op internet surft. Houd de key buiten de code en buiten de input van het model, wat het onderwerp is van API keys buiten een AI-agent houden.
Dimensionering: schijfruimte, RAM en tokens
Schijfruimte is het eenvoudige gedeelte. Eén dagelijkse bar is één rij per ticker per handelsdag, en een Amerikaans handelsjaar telt ongeveer 252 dagen.
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"
}
]Twintig tickers resulteren in 5,040 rijen na een jaar. De 500 tickers lijn bereikt 1,260,000 rijen na tien jaar. Elke rij bevat een datum en een handvol doubles, en DuckDB slaat kolommen gecomprimeerd op, dus dit betreft tientallen megabytes in plaats van gigabytes. Vertrouw niet op deze schatting, ook niet op de mijne. Voer du -h /opt/research/data/market.duckdb uit na uw eerste backfill en gebruik uw eigen cijfers.
RAM is waar een kleine VPS tegen beperkingen aanloopt. DuckDB neemt standaard een groot deel van het geheugen van de machine en alle cores in beslag voor een enkele query. Dit is correct voor een analytics-server, maar onjuist voor een 2 GB-machine die ook andere taken uitvoert. Eén aggregatie over de volledige prijzentabel zorgt er dan voor dat het proces door de kernel wordt beëindigd, waarbij systemd Main process exited, code=killed, status=9/KILL rapporteert en journalctl -k de out of memory kill toont. Stel memory_limit en threads expliciet in; de query wordt dan trager in plaats van dat deze crasht.
Tokens moeten worden gemeten, niet geschat. Elk antwoord van de Messages API bevat een usage-object met input_tokens en output_tokens. Schrijf beide bij elke aanroep naar een tabel; na een week kent u uw werkelijke volume. Dit vermenigvuldigt u met de tarieven die uw model hanteert op de dag dat u controleert. Twee zaken zijn stabiel genoeg om op te plannen. Output-tokens zijn bij elk Claude-model duurder geprijsd dan input-tokens, dus het beperken van de notitie tot 200 woorden heeft meer invloed op de kosten dan het inkorten van de data die u verstuurt. Prompt caching helpt bovendien niet bij een taak die één keer per dag wordt uitgevoerd, omdat de cache-levensduur in minuten wordt gemeten: bij de volgende run is het gecachte blok verlopen en betaalt u opnieuw de volledige input-prijs. Caching loont wanneer één run vele aanroepen doet over hetzelfde grote tekstblok.
De onderdelen installeren
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/dataControleer de installatie voordat u code schrijft:
sudo -u research /opt/research/venv/bin/python -c 'import duckdb, yfinance, anthropic; print("ok")'Dit print ok. Als u de virtuele omgeving heeft overgeslagen en pip install heeft uitgevoerd op de systeem-Python, blokkeert Ubuntu 24.04 u met error: externally-managed-environment. Dit komt doordat de distributie eigenaar is van /usr/lib/python3 en pip daar niet laat schrijven. De venv is geen kwestie van beleefdheid. Het is de enige map waar pip wijzigingen mag aanbrengen.
De API-sleutel wordt opgeslagen in een bestand dat alleen de servicegebruiker kan lezen:
sudo install -d -m 755 /etc/research
sudo install -m 640 -o root -g research /dev/null /etc/research/env
sudoedit /etc/research/envPlaats hierin één regel, zonder aanhalingstekens en zonder export, omdat systemd dit bestand zelf parseert in plaats van het door te geven aan een shell:
ANTHROPIC_API_KEY=sk-ant-your-key-hereDe opslag: twee tabellen
# /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 conDe primaire sleutel op (ticker, day) zorgt ervoor dat de verversing veilig herhaald kan worden. INSERT OR REPLACE overschrijft een rij die al bestaat voor die ticker en die dag, waardoor het tweemaal uitvoeren van de backfill de tabel niet verdubbelt. Zonder deze sleutel zou een herstart na een crash stilletjes elke bar dupliceren, en zou elk gemiddelde dat u daarna berekent onjuist zijn, zonder dat er ergens een foutmelding verschijnt.
DuckDB staat toe dat precies één proces het bestand geopend houdt om te schrijven. Een tweede schrijver faalt direct met Could not set lock on file, gevolgd door de PID die het bestand in gebruik heeft; in de praktijk is dit de interactieve duckdb-shell die u in een andere terminal open hebt laten staan. Lezers passen read_only=True toe, wat de reden is dat connect de flag gebruikt. Als meerdere processen daadwerkelijk op hetzelfde moment moeten schrijven, is dat een taak voor een andere engine: SQLite in WAL-modus laat lezers hun werk doen terwijl één schrijver commits uitvoert, en een busy timeout zorgt ervoor dat andere schrijvers wachten in plaats van te falen. DuckDB versus SQLite voor een server-workload vergelijkt de twee, en SQLite in productie draaien op een VPS behandelt de instellingen die WAL correct laten functioneren.
De verversingstaak
# /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()Voer deze eenmalig handmatig uit:
sudo -u research /opt/research/venv/bin/python /opt/research/refresh.pyDe eerste uitvoering vult jaren aan data aan en print één regel per ticker met een aantal in de duizendtallen. Voer het een minuut later opnieuw uit; elke regel toont nu 1 of 2 rijen, omdat de taak start vanaf de laatste dag die al is opgeslagen. Die tweede uitvoering is de echte test: als de aantallen nog steeds in de duizendtallen lopen, geeft max(day) niets terug en overschrijft de insert elke nacht uw volledige geschiedenis.
De df.empty-controle is de belangrijkste regel in het bestand. Ticker.history() genereert geen foutmelding bij een onjuist of uit de handel genomen symbool. Het print een waarschuwing dat het symbool mogelijk niet meer verhandeld wordt en er geen prijsdata is gevonden (de exacte bewoording verschilt per bibliotheekversie) en geeft een leeg DataFrame terug. Een taak zonder die controle schrijft niets weg, sluit af met exitcode 0, en systemd toont een gezonde, groene status terwijl de tabel stilletjes stopt met groeien. U komt er pas weken later achter, wanneer een scherm elke dag dezelfde rijen toont.
De afhandeling van de tijdstempels heeft ook een reden. De index die de feed teruggeeft kan de tijdzone van de beurs bevatten, en het verwijderen van de offset is niet hetzelfde als converteren naar UTC. Een sessie in Tokio met een tijdstempel om middernacht lokale tijd wordt in UTC geconverteerd naar de voorgaande kalenderdag. Een UTC-conversie verschuift dus stilletjes elke Japanse bar één dag terug en maakt de primaire sleutel ongeldig. tz_localize(None) behoudt de sessiedatum van de beurs zelf, wat de definitie is van een dagelijkse bar.
De timer die wordt geactiveerd bij het sluiten van de markt
De Amerikaanse markt sluit om 16:00 uur New York-tijd en de laatste transacties hebben enkele minuten nodig om te verwerken, dus de taak wordt uitgevoerd om 16:20 uur. Noteer dit in New York-tijd, niet in UTC. New York is in de winter UTC minus 5 en in de zomer UTC minus 4. Een timer die als een vast UTC-uur is geschreven, verschuift daarom twee keer per jaar een uur en wordt dan vóór sluitingstijd geactiveerd. systemd 252 en later accepteren een tijdzone direct in OnCalendar, en Ubuntu 24.04 levert versie 255. Controleer uw versie met 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 is het enige servicetype dat meer dan één ExecStart accepteert. Het voert deze in volgorde uit en stopt als er een met een foutcode afsluit. Dat is precies het gewenste gedrag: een mislukte verversing mag niet worden gevolgd door een scherm met verouderde gegevens. Persistent=true is van belang op een VPS die herstart voor kernel-updates. Zonder deze optie gaat de taak verloren bij een herstart om 16:15 uur; met deze optie wordt de taak uitgevoerd zodra de machine weer online is.
sudo systemctl daemon-reload
sudo systemctl enable --now research-refresh.timer
systemctl list-timers research-refresh.timerlist-timers hoort een NEXT-kolom te tonen met de volgende uitvoering op een doordeweekse dag, omgerekend naar de lokale tijd van de server. Een lege lijst betekent dat de timer niet is ingeschakeld, of dat de unit de sectie [Install] mist, waardoor enable niets had om aan timers.target te koppelen.
Het scherm: eerst SQL, dan het model
SQL is exact en kost niets per uitvoering. Het model is geen van beide. De query beperkt daarom het universum, en alleen de overblijvers worden naar het model gestuurd. Sla dit op als /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;Het n > 50-filter is geen decoratie. ROWS BETWEEN 49 PRECEDING AND CURRENT ROW berekent het gemiddelde van alle bestaande rijen; de derde rij van een ticker geeft dus het gemiddelde van drie dagen weer en noemt dit nog steeds ma50. Vergelijk dit met ma20 en u creëert aan het begin van de historie van elke ticker een crossover die nooit heeft plaatsgevonden. Filteren op het rijnummer verwijdert de rijen waar het venster niet volledig was.
Wat het model ziet en wat het nooit ziet
# /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()De woordlimiet in de systeemprompt beperkt het kostbare deel van de factuur. De instructie om uitsluitend de verstrekte rijen te gebruiken, is iets wat u moet verifiëren in plaats van blindelings te vertrouwen: verwijder één kolom uit het blok, voer het opnieuw uit en lees de uitvoer. Als er nog steeds een cijfer voor die kolom verschijnt, heeft het model het gat opgevuld en is uw prompt niet strikt genoeg. Die test duurt twee minuten en is de enige eerlijke manier om daarachter te komen.
Het model ziet nooit de API key, ziet nooit het databasepad en voert nooit een query uit. Het ontvangt rijen en retourneert proza. Die grens maakt de uitvoer controleerbaar, omdat elk cijfer in de notitie ook in het verzonden blok moet voorkomen, waardoor u ze regel voor regel kunt vergelijken. Voor meer informatie over de prompt-kant hiervan, gaat Claude gebruiken voor financiële analyse dieper in op waar het model goed in is bij het lezen van gegevens.
Eén run, end-to-end
Om 16:20 New York-tijd start de timer de service. refresh.py vraagt de feed om elke ticker vanaf de laatst opgeslagen dag, schrijft per ticker één of twee nieuwe bars en print één regel per ticker. screen.py opent hetzelfde bestand, voert de query voor het voortschrijdend gemiddelde uit en ontvangt een handvol rijen terug. Die rijen vormen een tekstblok van enkele honderden tokens. Eén API-aanroep zet deze om in een korte notitie, de notitie gaat naar het logboek en één rij belandt in runs met de tokentellingen en het aantal hits.
journalctl -u research-refresh.service -n 50 --no-pagerEen gezond logboek bevat één regel per ticker, gevolgd door de notitie en daarna research-refresh.service: Deactivated successfully. Lees na een week uw eigen kosten uit de database:
SELECT count(*) AS runs,
sum(input_tokens) AS in_tokens,
sum(output_tokens) AS out_tokens
FROM runs;Vermenigvuldig deze totalen met de prijzen per miljoen tokens die uw model vermeldt op de dag dat u ze uitleest. Dat geeft u het werkelijke cijfer in plaats van iemands schatting. Gepubliceerde prijzen veranderen. De rekenkunde niet.
Waarom backtests overfitten en hoe u dit kunt observeren
Schrijf de screen als een functie van de twee vensterlengtes, doorloop een grid van paren en rangschik deze op basis van rendement. Het beste paar zal er uitstekend uitzien. Dat is het probleem, niet het resultaat. Een grid van 200 paren betekent 200 experimenten, en u heeft de gelukkigste gekozen.
U kunt dit binnen tien minuten zien gebeuren. Splits de opslag in tweeën op basis van datum. Doorloop het grid alleen op de eerste helft en noteer de winnaar. Doorloop hetzelfde grid op de tweede helft. Als de twee winnaars ver uit elkaar liggen, passen de parameters zich aan op ruis. Een paar dat alleen wint op de helft waarop u het heeft afgesteld, zegt niets over morgen.
Survivorship bias is erger dan overfitting omdat afstellen dit niet kan verhelpen. Uw lijst met tickers bestaat uit de huidige indexleden; deze bevat dus alleen de bedrijven die hebben overleefd. Vraag de feed om een ticker die in 2019 van de beurs is gehaald en u krijgt een leeg frame terug. Dit betekent dat dat bedrijf nooit in uw opslag en nooit in uw test terechtkomt. Elke backtest die u uitvoert, heeft de mislukkingen al uitgesloten.
Aangepaste fundamentals doorbreken de tijdlijn. Het omzetcijfer dat de API vandaag teruggeeft voor een kwartaal in 2019, is niet altijd het cijfer dat in 2019 werd gepubliceerd. Een screen die de huidige fundamentals combineert met prijzen uit 2019, gebruikt informatie die toen nog niet bestond. Prijzen zijn hier meestal veilig. Fundamentals meestal niet.
Aangepaste prijzen veranderen onder u. Bij auto_adjust=True worden de slotkoersen achterwaarts aangepast voor dividenden en aandelensplitsingen, waardoor dezelfde query die u volgende maand uitvoert een iets andere historie teruggeeft. Het opslaan van de rijen die u daadwerkelijk heeft gebruikt, maakt een resultaat reproduceerbaar; dit is tevens een reden waarom de lokale opslag überhaupt bestaat.
De backtest negeert ook commissies en slippage, en gaat ervan uit dat uw order de prijs niet beïnvloedt. Deze zaken vallen onder executie, wat buiten de scope van dit onderwerp valt en wordt behandeld in trading bots draaien op een VPS.
Foutmodi en de meldingen die u zult zien
error: externally-managed-environment wanneer pip wordt uitgevoerd. U bevindt zich buiten de virtuele omgeving. Roep /opt/research/venv/bin/pip aan via het volledige pad.
Could not set lock on file, gevolgd door een PID. Een ander proces houdt het DuckDB-bestand open voor schrijven, meestal een interactieve shell die u bent vergeten. Sluit deze, of open de tweede verbinding met read_only=True.
Main process exited, code=killed, status=9/KILL in systemctl status. De kernel heeft de taak beëindigd vanwege een tekort aan geheugen. Bevestig dit met journalctl -k | grep -i oom en verlaag vervolgens memory_limit in store.py.
Een succesvolle uitvoering die niets schrijft. systemctl status leest active (exited) en de tabel is niet gegroeid. De feed gaf lege frames terug. Deze fout is het lastigst te detecteren; zorg er daarom voor dat de taak een non-zero exitcode geeft wanneer elke ticker leeg terugkomt.
De timer werd geactiveerd op een beursvakantiedag. systemd is niet op de hoogte van de beurskalender, dus Mon-Fri bevat ook vakantiedagen. De uitvoering vindt plaats, de feed bevat niets nieuws, en de taak moet dit als normaal beschouwen in plaats van als een fout.
429 vanuit de API. U overschrijdt een rate limit. De Anthropic SDK voert automatisch retries uit met backoff, en Anthropic(max_retries=5) verhoogt het aantal pogingen. Als het dagelijks blijft mislukken, vraagt de taak om te veel in één keer.
Wat dit niet is
Dit is een onderzoeksassistent. Een model dat een document samenvat, produceert een interpretatie van dat document. Het kan met grote stelligheid onjuiste cijfers uit de tekst presenteren; daarom moet elk cijfer in de notitie herleidbaar zijn naar een rij die u heeft aangeleverd. Beschouw de output als een selectie van zaken die u zelf moet nalezen. Niets hiervan is financieel advies en niets is een handelssignaal.
Backtests zijn nuttig om ideeën te verwerpen, maar zwak in het bevestigen ervan. Een strategie die faalt op uw eigen data is definitief onbruikbaar. Een strategie die slaagt, heeft enkel uw data overleefd; dat is een veel kleinere claim dan het om 01:00 uur 's nachts lijkt.
De uitvoering valt bewust buiten de scope. Orders en broker-inloggegevens brengen een ander risicoprofiel met zich mee dan een read-only onderzoeksomgeving. Het combineren hiervan plaatst trading-keys op dezelfde machine als een LLM-prompt. Als u wilt zien waar dit patroon zich verhoudt tot andere zaken die de moeite waard zijn om op uw eigen server te draaien, dan biedt de zelf-gehoste AI-agents die het draaien waard zijn het bredere overzicht.
FAQ
Heb ik een betaalde marktgegevensfeed nodig?
Niet voor een prototype. Een gratis, officieuze feed is prima om de structuur van het systeem te leren kennen. Deze zal echter falen, omdat de feed afhankelijk is van een website die u geen enkele garantie biedt. Dit uit zich meestal in lege frames in plaats van een uitzondering, dus uw taak moet het aantal rijen controleren. Stap over op een betaalde feed met een gedocumenteerde API en een ondersteuningsadres zodra de data wordt gebruikt voor besluitvorming. De opslaglaag maakt deze overstap eenvoudig: alleen de fetch-functie verandert, terwijl het schema, de planning en de weergave ongewijzigd blijven.
Moet ik prijzen opslaan in SQLite of DuckDB?
DuckDB is kolomgeoriënteerd en gebouwd voor het scannen van grote hoeveelheden rijen om aggregaten te berekenen; dit is precies wat een voortschrijdend gemiddelde over tien jaar aan koersdata is. SQLite is rijgeoriënteerd en beter in het verwerken van veel kleine lees- en schrijfacties door meerdere processen tegelijk. Voor een enkele geplande taak die enkele honderden rijen toevoegt en vervolgens miljoenen rijen scant, is DuckDB de betere keuze. Als meerdere processen tegelijkertijd moeten schrijven, staat SQLite in WAL-modus toe dat lezers doorwerken terwijl één schrijver een commit uitvoert. Een busy timeout zorgt er dan voor dat andere schrijvers wachten in plaats van direct te falen.
Wat zijn de maandelijkse kosten voor de LLM-aanroepen?
Log usage.input_tokens en usage.output_tokens van elk antwoord in een tabel en vermenigvuldig uw wekelijkse totalen met de prijs per miljoen tokens die uw model hanteert op de dag dat u dit controleert. Dat is het enige cijfer dat het volgende kwartaal nog relevant is. Eén dagelijkse run over een beperkte selectie resulteert in een klein aantal aanroepen. De lengte van de notitie is de factor die u zelf in de hand heeft: het beperken van de samenvatting tot 200 woorden bespaart meer dan het versturen van minder rijen, omdat output-tokens bij elk Claude-model duurder zijn dan input-tokens.
Waarom is mijn taak geslaagd maar zijn er geen nieuwe rijen geschreven?
Er zijn twee gebruikelijke oorzaken. De markt was gesloten, omdat een systemd Mon-Fri-schema geen rekening houdt met beursvakanties. Of de feed gaf voor elk ticker-symbool een leeg frame terug. Client-bibliotheken rapporteren dit vaak als een waarschuwing in de console in plaats van als een uitzondering, waardoor het proces met exitcode 0 afsluit en systemd een succesvolle run toont. Maak onderscheid door SELECT max(day) FROM prices te vergelijken met de laatste echte handelsdag en laat de taak met een foutcode afsluiten als elk ticker-symbool leeg terugkomt.
Kan de agent beslissen wat ik moet kopen?
Nee, en het bouwen van een systeem dat dit probeert, is waar deze projecten vaak misgaan. Het model heeft geen toegang tot de markt, geen inzicht in uw positie of fiscale situatie, en geen manier om de eigen berekeningen te verifiëren. Waar het model wel goed in is, is het lezen van grote hoeveelheden tekst en het aangeven welke items vandaag uw aandacht verdienen. Niets van wat het model schrijft is financieel advies; de beslissing, en de verantwoordelijkheid daarvoor, blijft bij u.