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 dat volgens een schema marktgegevens ophaalt, deze in een lokale database opslaat, er een screening op uitvoert en een large language model (LLM) vraagt om een verslag te schrijven over de wijzigingen. Het leest en filtert. Het voert geen transacties uit en niets in deze handleiding is 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 rij bijhoudt die u ooit heeft opgehaald, een taak die de opslag op een timer 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). Het is een specifieke agent met een vaste lus. Als u liever eerst de algemene versie leert kennen, werkt het stapsgewijze pad voor het leren van AI-agents de lus, tools en het geheugen door voordat dit alles een domeinspecifiek probleem wordt. Uitvoering is een aparte taak met aparte faalmodi en hoort thuis op een VPS die is ingericht voor handelsbots.
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 balk is één rij per ticker per handelsdag, en een Amerikaans handelsjaar telt ongeveer 252 dagen.
De gegevens achter deze grafiek
[
{
"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-regel bereikt 1,260,000 rijen na tien jaar. Elke rij bevat een datum en een handvol doubles, en DuckDB slaat kolommen gecomprimeerd op; dit resulteert dus in tientallen megabytes in plaats van gigabytes. Vertrouw niet blindelings 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 machinegeheugen en alle cores in beslag voor een enkele query. Dit is correct voor een analytics-server, maar onjuist voor een 2 GB-box 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, waarna 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. Vermenigvuldig dit met de prijzen 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; het beperken van de notitie tot 200 woorden heeft dus meer invloed op de rekening dan het inkorten van de data die u verstuurt. Prompt caching helpt bovendien niet bij een taak die één keer per dag wordt uitgevoerd. De cache blijft standaard 5 minuten actief en de langste levensduur die Anthropic aanbiedt is 1 uur, wat 2x de basis-inputprijs kost om te schrijven in plaats van 1,25x voor de cache van 5 minuten. Hoe dan ook, tegen de tijd dat de taak de volgende dag draait, is het gecachte blok verlopen en betaalt u opnieuw de volledige inputprijs. Caching is rendabel wanneer één run meerdere 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 herhaling na een crash stilletjes elke bar dupliceren, en zou elk gemiddelde dat u daarna berekent onjuist zijn, zonder dat er een foutmelding is die u daarop wijst.
DuckDB staat toe dat precies één proces het bestand geopend houdt voor 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. Een verbinding met alleen-lezenrechten omzeilt dit niet. De documentatie over concurrency van DuckDB beschrijft twee modi: één proces dat leest en schrijft, of meerdere processen die alleen lezen, nooit een combinatie van beide. connect accepteert een read_only-vlag zodat meerdere lezers het bestand kunnen delen, wat alleen werkt zolang geen enkele schrijver het bestand geopend heeft. 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 sluiting van de markt
De Amerikaanse markt sluit om 16:00 uur New Yorkse 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 Yorkse tijd, niet in UTC. New York is in de winter UTC minus 5 en in de zomer UTC minus 4, dus een timer die als een vaste UTC-tijd is geschreven, verschuift twee keer per jaar een uur en wordt dan vóór sluitingstijd geactiveerd. systemd 235 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, en het voert deze in volgorde uit, waarbij het stopt als er een met een niet-nul status afsluit. Dat is precies het gedrag dat u wilt: 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. Herstart om 16:15 uur zonder deze optie en de uitvoering gaat simpelweg verloren; met deze optie wordt de taak uitgevoerd zodra de machine weer online is. Een agent die actief moet blijven in plaats van eenmalig uit te voeren en af te sluiten, vereist de tegenovergestelde unit-vorm: een langlopend proces met Restart=-regels en helemaal geen timer. Dit wordt behandeld in dsh headless draaien op een VPS met systemd.
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 [Install]-sectie mist, waardoor enable niets had om te koppelen aan timers.target.
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,
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()De regel thinking is van belang bij dit model. Claude Sonnet 5 denkt standaard na; de denktokens worden gefactureerd als output en tellen mee voor max_tokens. Ze worden geretourneerd als een blok voorafgaand aan de tekst. Zonder die regel kan resp.content[0] het denkblok zijn en mislukt de .text-opzoekactie. Claude Sonnet 5 is inmiddels een legacy-model. De opvolger, claude-sonnet-5-5, wijst disabled af. De instelling die u daar moet gebruiken is thinking={'type': 'between_tools'}, waarmee het nadenken vóór het antwoord wordt uitgeschakeld.
De woordlimiet in de systeemprompt begrenst het kostbare deel van de factuur. De instructie om uitsluitend de verstrekte rijen te gebruiken, moet u verifiëren in plaats van erop te vertrouwen: verwijder één kolom uit het blok, voer het opnieuw uit en lees de output. 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 erachter 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 output controleerbaar, omdat elk getal in de notitie ook moet voorkomen in het blok dat u heeft verzonden; u kunt ze regel voor regel vergelijken. Zodra u die input verbreedt met pagina's van het web, verandert de vorm van de grens. Een koptekst of een forumpost kan immers instructies bevatten die op het model zijn gericht in plaats van feiten voor u. Dat is het aanvalsoppervlak dat een agent voorzien van een SearXNG-zoekbackend creëert. Voor meer informatie over de prompt-kant hiervan, gaat Claude gebruiken voor financiële analyse dieper in op wat het model goed kan lezen.
Eén run, van begin tot eind
Om 16:20 uur New Yorkse tijd start de timer de service. refresh.py vraagt de feed om elke ticker vanaf de laatst opgeslagen dag, schrijft één of twee nieuwe bars per ticker 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 treffers.
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. Als u dat logboek leest vanuit een Claude Code-sessie op de server terwijl een tweede sessie aan de schermquery werkt, kunnen de twee tekst aan elkaar doorgeven in plaats van dat u regels kopieert tussen terminals. 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 die totalen met de prijzen per miljoen tokens die uw model vermeldt op de dag dat u ze leest. 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. Het openen van de tweede verbinding met read_only=True helpt niet zolang die schrijver actief is, omdat DuckDB meerdere processen laat lezen of één proces laat schrijven, maar niet beide tegelijk.
Main process exited, code=killed, status=9/KILL in systemctl status. De kernel heeft de taak beëindigd vanwege een gebrek 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 met een non-zero status afsluit 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 feestdagen. De uitvoering vindt plaats, de feed bevat niets nieuws, en de taak moet dit als normaal beschouwen in plaats van als een fout.
429 van 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.