Agente azionario self-hosted su VPS: guida pratica
Crea un agente self-hosted su VPS con feed di mercato, archivio DuckDB, timer systemd a chiusura dei mercati e filtro LLM tramite API Claude.
Che cos'è un agente self-hosted per la ricerca azionaria
Un agente self-hosted per la ricerca azionaria è un piccolo programma eseguito su un server di tua proprietà. Recupera i dati di mercato secondo una pianificazione, li conserva in un database locale, li sottopone a un filtro e chiede a un large language model (LLM) di descrivere le variazioni rilevate. Legge e filtra i dati. Non esegue operazioni di trading e nulla di ciò che è riportato in questa guida costituisce consulenza finanziaria.
Due persone che realizzano questo sistema da zero possono scegliere librerie diverse e ottenere comunque gli stessi quattro componenti: un feed che fornisce prezzi e dati fondamentali, un archivio locale che conserva ogni riga recuperata, un job che aggiorna l'archivio a intervalli regolari e un livello LLM che trasforma le righe rimaste in frasi. Questa guida costruisce questa struttura usando Python, DuckDB, un timer systemd e l'API Claude (application programming interface). Si tratta di un agente circoscritto, con un ciclo fisso. Se preferisci conoscere prima la versione generale, il percorso graduale per imparare gli agenti AI illustra in ordine il ciclo, gli strumenti e la memoria, prima di applicarli a un problema di dominio. L'esecuzione è un job separato, con modalità di errore distinte, e deve essere eseguita su un VPS configurato per i bot di trading.
Le quattro parti e la funzione di ciascuna
Il feed è l’unica parte che comunica con il mondo esterno. Sa come richiedere un ticker e un intervallo di date e come restituire le righe. Tutto ciò che viene dopo legge il database invece del feed. Se il feed non è disponibile, perdi un giorno di nuovi dati anziché ritrovarti con un’interfaccia non funzionante.
Lo store è il motivo principale dell’intero sistema. Una chiusura giornaliera che non hai registrato di solito può essere recuperata in seguito. Un’informazione intraday, una stima precedente a una revisione o un dato fondamentale precedente a una rettifica, invece, potrebbero non essere recuperabili. Lo store consente di conservare una registrazione di ciò che i dati riportavano nel giorno in cui lo riportavano.
Lo scheduler decide quando eseguire l’aggiornamento. Su un server si tratta di un timer systemd. Per questo, in questo caso, il VPS è più importante del codice.
Il livello LLM legge un breve blocco di testo prodotto dal tuo SQL e ne scrive un riepilogo. Non si connette mai al database e non genera mai la query. Se il modello scrive l’SQL, un token errato diventa un numero errato all’interno di una frase scorrevole, senza alcun valore di confronto. Se è l’SQL a produrre i numeri, il modello può sbagliare soltanto la parte testuale, che puoi verificare confrontandola con le righe che gli hai inviato.
Perché eseguirlo su un VPS invece che su un laptop
Il motivo principale è lo scheduler. La chiusura del mercato statunitense alle 16:00 ora di New York corrisponde alle 22:00 a Berlino e alle 04:00 del mattino successivo a Jakarta. Un laptop è in sospensione in entrambi gli orari. Un'esecuzione mancata costa più di una nota inserita in ritardo: le barre giornaliere possono generalmente essere recuperate in seguito, ma non è possibile recuperare ciò che viene revisionato, quindi il vuoto nello storico resta permanente. Lo stesso ragionamento vale per qualsiasi agente il cui programma di esecuzione e stato persistente debbano sopravvivere a un riavvio. È questo l'obiettivo di eseguire KiroCrew come agente sempre attivo sul proprio VPS.
Il secondo motivo è meno importante, ma comunque concreto. Il server contiene una sola API key, in un solo file, di proprietà di un solo utente di sistema senza login shell, utilizzata da un solo job. È molto più difficile ottenere questa configurazione sul laptop che usi anche per navigare sul web. Mantieni la key fuori dal codice e dall'input del modello: questo è l'argomento di mantenere le API key fuori da un agente AI.
Dimensionamento: disco, RAM e token
Il disco è la parte più semplice. Una barra giornaliera corrisponde a una riga per ticker e per giorno di contrattazione, e un anno di contrattazioni negli Stati Uniti conta circa 252 giorni.
I dati alla base di questo grafico
[
{
"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"
}
]Venti ticker corrispondono a 5,040 righe dopo un anno. La riga 500 tickers raggiunge 1,260,000 righe dopo dieci anni. Ogni riga contiene una data e alcuni valori double. DuckDB memorizza le colonne in forma compressa, quindi si parla di decine di megabyte, non di gigabyte. Non considerare questa stima, inclusa la mia, come un dato affidabile. Esegui du -h /opt/research/data/market.duckdb dopo il primo backfill e usa il valore effettivo del tuo sistema.
La RAM è il punto critico su un VPS di piccole dimensioni. Per impostazione predefinita, DuckDB utilizza una quota elevata della memoria della macchina e tutti i relativi core per una singola query. Questo comportamento è adatto a un server di analisi, ma non a una macchina da 2 GB che esegue anche altri servizi. Una singola aggregazione sull'intera tabella dei prezzi può quindi causare la terminazione del processo da parte del kernel. systemd segnala Main process exited, code=killed, status=9/KILL, mentre journalctl -k mostra la terminazione per esaurimento della memoria. Imposta esplicitamente memory_limit e threads: la query sarà più lenta invece di causare l'arresto del processo.
I token devono essere misurati, non stimati. Ogni risposta della Messages API contiene un oggetto usage con input_tokens e output_tokens. Scrivi entrambi in una tabella a ogni chiamata. Dopo una settimana conoscerai il volume effettivo e potrai moltiplicarlo per il prezzo pubblicato dal modello nel giorno della verifica. Ci sono due aspetti sufficientemente stabili per la pianificazione. Su tutti i modelli Claude, i token di output hanno un prezzo superiore a quello dei token di input. Limitare la nota a 200 parole incide quindi sul costo più che ridurre i dati inviati. Inoltre, il prompt caching non è utile per un processo eseguito una volta al giorno. La cache resta disponibile per impostazione predefinita per 5 minuti. La durata massima offerta da Anthropic è 1 ora e la scrittura del contenuto costa 2x il prezzo base dell'input, invece di 1.25x per la cache di 5 minuti. In entrambi i casi, al momento dell'esecuzione del giorno successivo il blocco memorizzato nella cache è scaduto e il prezzo dell'input viene applicato nuovamente per intero. Il caching conviene quando una singola esecuzione effettua molte chiamate sullo stesso blocco di testo di grandi dimensioni.
Installare i componenti
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/dataVerificare l'installazione prima di scrivere il codice:
sudo -u research /opt/research/venv/bin/python -c 'import duckdb, yfinance, anthropic; print("ok")'Questo comando stampa ok. Se si salta l'ambiente virtuale e si esegue pip install usando il Python di sistema, Ubuntu 24.04 restituisce error: externally-managed-environment, perché la distribuzione gestisce /usr/lib/python3 e impedisce a pip di scrivervi. Il venv non è un accorgimento opzionale. È l'unica directory che pip può modificare.
La chiave API va inserita in un file leggibile dall'utente del servizio e da nessun altro:
sudo install -d -m 755 /etc/research
sudo install -m 640 -o root -g research /dev/null /etc/research/env
sudoedit /etc/research/envInserire una sola riga, senza virgolette e senza export, perché systemd analizza direttamente questo file invece di passarlo a una shell:
ANTHROPIC_API_KEY=sk-ant-your-key-hereIl data store: due tabelle
# /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 chiave primaria su (ticker, day) rende sicuro ripetere l'aggiornamento. INSERT OR REPLACE sovrascrive una riga già esistente per quel ticker e per quel giorno, quindi eseguire il backfill due volte non raddoppia le righe della tabella. Senza la chiave, una nuova esecuzione dopo un arresto anomalo duplica silenziosamente ogni barra e tutte le medie calcolate in seguito risultano errate, senza alcun messaggio che lo segnali.
DuckDB consente a un solo processo alla volta di aprire il file in scrittura. Un secondo processo in scrittura fallisce immediatamente con Could not set lock on file, seguito dal PID che lo tiene aperto; nella pratica, di solito è la shell interattiva duckdb lasciata aperta in un altro terminale. Una connessione in sola lettura non risolve il problema. La documentazione sulla concorrenza di DuckDB descrive due modalità: un processo che legge e scrive oppure più processi che eseguono soltanto letture, mai una combinazione delle due. connect accetta un flag read_only che consente a più lettori di condividere il file. Questa modalità funziona solo quando nessun processo lo ha aperto in scrittura. Se più processi devono realmente scrivere nello stesso momento, è necessario usare un altro motore: SQLite in modalità WAL consente ai lettori di lavorare mentre un solo processo esegue il commit in scrittura, mentre un timeout di attesa fa attendere gli altri processi in scrittura invece di farli fallire. DuckDB e SQLite per un carico di lavoro server confronta le due soluzioni, mentre eseguire SQLite in produzione su un VPS descrive le impostazioni necessarie per far funzionare correttamente WAL.
Il job di aggiornamento
# /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()Eseguilo manualmente una volta:
sudo -u research /opt/research/venv/bin/python /opt/research/refresh.pyLa prima esecuzione recupera gli anni arretrati e stampa una riga per ogni ticker, con un conteggio nell'ordine delle migliaia. Eseguilo di nuovo un minuto dopo: ogni riga mostrerà 1 o 2 record, perché il job riparte dall'ultimo giorno già memorizzato. Questa seconda esecuzione è il test effettivo: se i conteggi sono ancora nell'ordine delle migliaia, max(day) non restituisce dati e l'inserimento riscrive ogni notte tutta la cronologia.
Il controllo df.empty è la riga più importante del file. Ticker.history() non genera un errore per un simbolo errato o non più quotato. Stampa un avviso che segnala la possibile rimozione del simbolo dal listino e l'assenza di dati sui prezzi (la formulazione esatta cambia tra le versioni della libreria), quindi restituisce un DataFrame vuoto. Un job senza questo controllo non scrive nulla, termina con codice 0 e systemd mostra un'esecuzione corretta, mentre la tabella smette silenziosamente di crescere. Te ne accorgi settimane dopo, quando una schermata restituisce ogni giorno le stesse righe.
Anche la gestione dei timestamp ha una motivazione precisa. L'indice restituito dal feed può contenere il fuso orario della borsa, e rimuovere l'offset non equivale a convertire il valore in UTC. Una sessione di Tokyo con timestamp a mezzanotte nell'ora locale viene convertita al giorno di calendario precedente in UTC. La conversione in UTC sposta quindi silenziosamente indietro di un giorno ogni barra giapponese e viola la chiave primaria. tz_localize(None) conserva la data della sessione nella borsa di riferimento, che è il significato di una barra giornaliera.
Il timer che si attiva alla chiusura del mercato
Il mercato statunitense chiude alle 16:00, ora di New York, e le ultime transazioni richiedono alcuni minuti per essere contabilizzate. Per questo il job viene eseguito alle 16:20. Indica questo orario in base all'ora di New York, non in UTC. New York è a UTC meno 5 in inverno e a UTC meno 4 in estate. Un timer configurato con un'ora UTC fissa cambia quindi di un'ora due volte all'anno e inizia ad attivarsi prima della chiusura. systemd 235 e versioni successive accettano direttamente un fuso orario in OnCalendar, e Ubuntu 24.04 include la versione 255. Verifica la versione installata con systemctl --version.
# /etc/systemd/system/research-refresh.service
[Unit]
Description=Refresh market data and run the daily screen
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
User=research
Group=research
WorkingDirectory=/opt/research
EnvironmentFile=/etc/research/env
ExecStart=/opt/research/venv/bin/python /opt/research/refresh.py
ExecStart=/opt/research/venv/bin/python /opt/research/screen.py# /etc/systemd/system/research-refresh.timer
[Unit]
Description=Run the refresh after the US market close
[Timer]
OnCalendar=Mon-Fri 16:20 America/New_York
Persistent=true
RandomizedDelaySec=180
[Install]
WantedBy=timers.targetType=oneshot è l'unico tipo di servizio che accetta più ExecStart e li esegue in ordine, interrompendosi se uno termina con un codice diverso da zero. È esattamente il comportamento necessario: un aggiornamento non riuscito non deve essere seguito dalla visualizzazione di dati obsoleti. Persistent=true è importante su un VPS che viene riavviato per gli aggiornamenti del kernel. Se il riavvio avviene alle 16:15 senza questa impostazione, l'esecuzione viene semplicemente persa; con questa impostazione, il job viene eseguito appena la macchina torna disponibile. Un agent che deve rimanere attivo invece di avviarsi e terminare richiede la forma opposta dell'unità: un processo di lunga durata con regole Restart= e senza alcun timer. Questo scenario è illustrato in eseguire dsh senza interfaccia su un VPS con systemd.
sudo systemctl daemon-reload
sudo systemctl enable --now research-refresh.timer
systemctl list-timers research-refresh.timerlist-timers dovrebbe mostrare una colonna NEXT contenente la prossima esecuzione nel giorno feriale, convertita nell'ora locale del server. Un elenco vuoto indica che il timer non è abilitato oppure che nell'unità manca la sezione [Install]. In tal caso enable non aveva nulla da collegare a timers.target.
La schermata: prima SQL, poi il modello
SQL è deterministico e non ha costi di esecuzione. Il modello non lo è. La query restringe quindi l’insieme dei dati e solo i record rimasti vengono inviati al modello. Salva questo risultato come /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;Il filtro n > 50 non è ornamentale. ROWS BETWEEN 49 PRECEDING AND CURRENT ROW calcola la media sulle righe disponibili, quindi la terza riga di un ticker restituisce la media di tre giorni e la confronta comunque con ma50. Il confronto con ma20 crea un crossover all’inizio della cronologia di ogni ticker, anche se non si è mai verificato. Il filtro sul numero di riga elimina le righe per le quali la finestra non era completa.
Cosa vede il modello e cosa non vede mai
# /opt/research/screen.py
from anthropic import Anthropic
from store import connect
SYSTEM = (
'You are a research assistant. Use only the rows in the message. '
'If a number is not in the rows, say that it is not available. '
'Do not give investment advice, price targets or buy and sell calls. '
'Write at most 200 words.'
)
con = connect()
sql = open('/opt/research/screen.sql').read()
rows = con.execute(sql).fetchall()
block = '\n'.join(' / '.join(str(v) for v in row) for row in rows)
client = Anthropic(max_retries=5) # reads ANTHROPIC_API_KEY from the environment
resp = client.messages.create(
model='claude-sonnet-5',
max_tokens=600,
thinking={'type': 'disabled'},
system=SYSTEM,
messages=[{'role': 'user', 'content': 'Screen hits, ticker / day / close / ma20 / ma50:\n' + block}],
)
print(resp.content[0].text)
con.execute('INSERT INTO runs VALUES (now(), ?, ?, ?, ?)',
['claude-sonnet-5', resp.usage.input_tokens,
resp.usage.output_tokens, len(rows)])
con.close()La riga thinking è importante per questo modello. Claude Sonnet 5 ragiona per impostazione predefinita; i token usati per il ragionamento vengono fatturati come output e concorrono a max_tokens, quindi vengono restituiti in un blocco prima del testo. Senza quella riga, resp.content[0] può corrispondere al blocco di ragionamento e la ricerca .text non va a buon fine. Claude Sonnet 5 è ormai un modello legacy. Il suo successore, claude-sonnet-5-5, rifiuta disabled; l'impostazione da usare in quel caso è thinking={'type': 'between_tools'}, che disattiva il ragionamento prima della risposta.
Il limite di parole nel prompt di sistema pone un tetto alla parte più costosa della fattura. L'istruzione che impone di usare solo le righe fornite è quella che devi verificare, non accettare senza controlli: elimina una colonna dal blocco, esegui di nuovo l'operazione e leggi l'output. Se compare ancora un dato per quella colonna, il modello ha colmato la lacuna e il prompt non è abbastanza vincolante. Il test richiede due minuti ed è l'unico modo corretto per accertarlo.
Il modello non vede mai la chiave API, non vede mai il percorso del database e non esegue mai una query. Riceve righe e restituisce testo. Questo confine rende l'output verificabile, perché ogni numero nella nota dovrebbe comparire anche nel blocco inviato e puoi confrontarli riga per riga. Nel momento in cui ampli l'input includendo pagine recuperate dal Web, il confine cambia, perché un titolo o un post su un forum può contenere istruzioni rivolte al modello invece di informazioni rivolte a te: è la superficie che fornire a un agente un backend di ricerca SearXNG espone. Per ulteriori informazioni sull'uso dei prompt, usare Claude per l'analisi finanziaria approfondisce ciò che il modello è in grado di leggere.
Un'esecuzione completa, dall'inizio alla fine
Alle 16:20, ora di New York, il timer avvia il servizio. refresh.py interroga il feed per ogni ticker a partire dall'ultimo giorno memorizzato, scrive una o due nuove barre per ciascuno e stampa una riga per ticker. screen.py apre lo stesso file, esegue la query della media mobile e restituisce alcune righe. Queste righe diventano un blocco di testo di alcune centinaia di token. Una chiamata API le trasforma in una breve nota, la nota viene scritta nel journal e una riga viene inserita in runs con il conteggio dei token e il numero di hit.
journalctl -u research-refresh.service -n 50 --no-pagerUn log integro contiene una riga per ticker, poi la nota e infine research-refresh.service: Deactivated successfully. Se leggi quel log da una sessione di Claude Code sul server mentre una seconda sessione lavora sulla query a schermo, le due sessioni possono passarsi il testo senza che tu debba copiare le righe tra i terminali. Dopo una settimana, ricava dal database il costo effettivo:
SELECT count(*) AS runs,
sum(input_tokens) AS in_tokens,
sum(output_tokens) AS out_tokens
FROM runs;Moltiplica questi totali per i prezzi per milione di token indicati dal tuo modello nel giorno in cui li leggi. Otterrai il valore reale, invece della stima di qualcun altro. I prezzi pubblicati cambiano. Il calcolo no.
Perché i backtest vanno in overfitting e come osservarlo
Riscrivi lo screen come funzione delle due lunghezze delle finestre, esegui una scansione su una griglia di coppie e ordinala in base al rendimento. La coppia migliore sembrerà eccellente. Questo è il problema, non il risultato. Una griglia di 200 coppie corrisponde a 200 esperimenti e hai conservato quella più fortunata.
Puoi osservare il fenomeno in dieci minuti. Dividi lo store a metà in base alla data. Esegui la scansione della griglia soltanto sulla prima metà e annota la coppia vincente. Esegui la stessa scansione sulla seconda metà. Se le due coppie vincenti sono molto diverse, i parametri stanno adattandosi al rumore. Una coppia che vince soltanto sulla metà usata per il tuning non fornisce alcuna informazione sul giorno successivo.
Il survivorship bias è peggiore dell'overfitting perché il tuning non può correggerlo. L'elenco dei ticker contiene i componenti attuali dell'indice, quindi include soltanto le società sopravvissute. Se chiedi al feed un ticker delistato nel 2019, restituisce un frame vuoto. Di conseguenza, quella società non entra nello store e non entra nel test. Ogni backtest che esegui ha già escluso i casi falliti.
I fondamentali riformulati alterano la sequenza temporale. Il valore dei ricavi che l'API restituisce oggi per un trimestre del 2019 non è sempre quello pubblicato nel 2019. Uno screen che combina i fondamentali odierni con i prezzi del 2019 usa informazioni che allora non esistevano. In questo caso, i prezzi sono generalmente affidabili. I fondamentali, invece, di norma non lo sono.
I prezzi rettificati cambiano nel tempo. Con auto_adjust=True, i prezzi di chiusura vengono rettificati a ritroso per dividendi e frazionamenti. Di conseguenza, la stessa query eseguita il mese prossimo restituisce una serie storica leggermente diversa. Memorizzare le righe effettivamente utilizzate rende il risultato riproducibile. È un altro motivo per cui esiste lo store locale.
Il backtest ignora inoltre commissioni e slippage e presuppone che l'ordine non sposti il prezzo. Questi aspetti appartengono all'esecuzione, che non rientra nell'ambito di questa guida ed è trattata in eseguire trading bot su un VPS.
Modalità di errore e stringhe visualizzate
error: externally-managed-environment durante l'esecuzione di pip. Ti trovi al di fuori dell'ambiente virtuale. Richiama /opt/research/venv/bin/pip tramite il percorso completo.
Could not set lock on file, seguito da un PID. Un altro processo tiene aperto il file DuckDB in scrittura, di solito una shell interattiva che hai lasciato aperta. Chiudila. Aprire la seconda connessione con read_only=True non risolve il problema finché il processo di scrittura resta attivo, perché DuckDB consente la lettura da parte di più processi oppure la scrittura da parte di un solo processo, ma non entrambe contemporaneamente.
Main process exited, code=killed, status=9/KILL in systemctl status. Il kernel ha terminato il job per mancanza di memoria. Conferma con journalctl -k | grep -i oom, quindi riduci memory_limit in store.py.
Un'esecuzione completata correttamente che non scrive nulla. systemctl status legge active (exited) e la tabella non è aumentata. Il feed ha restituito frame vuoti. Questo errore è il più difficile da individuare, quindi fai terminare il job con un codice diverso da zero quando tutti i ticker restituiscono dati vuoti.
Il timer è scattato durante una festività di mercato. systemd non conosce il calendario della borsa, quindi Mon-Fri include le festività. L'esecuzione avviene, il feed non contiene dati nuovi e il job deve considerare questa situazione normale, non un errore.
429 dall'API. Hai superato un limite di frequenza. Anthropic SDK esegue automaticamente nuovi tentativi con backoff e Anthropic(max_retries=5) aumenta il numero di tentativi. Se il problema si verifica ogni giorno, il job sta richiedendo troppi dati in un'unica raffica.
Cosa non è
Questo è un assistente per la ricerca. Un modello che riassume un filing produce una propria interpretazione di quel filing e può sbagliare con sicurezza su un numero riportato nel testo. Per questo, ogni dato numerico nella nota deve poter essere ricondotto a una riga che hai fornito. Considera l'output come un elenco ristretto di elementi da leggere personalmente. Questo contenuto non costituisce consulenza finanziaria e non rappresenta alcun segnale.
I backtest sono utili per scartare le idee e poco efficaci per confermarle. Una strategia che fallisce sui tuoi dati è realmente da abbandonare. Una strategia che supera il test ha soltanto superato i tuoi dati: è un'affermazione molto più limitata di quanto sembri all'1am.
L'esecuzione degli ordini resta deliberatamente fuori ambito. Gli ordini e le credenziali del broker hanno un profilo di rischio diverso rispetto a un sistema di ricerca in sola lettura, e combinarli significa mettere le chiavi di trading sulla stessa macchina del prompt di un LLM. Se vuoi vedere dove si colloca questo modello rispetto alle altre soluzioni che vale la pena eseguire sul tuo server, gli agenti AI self-hosted che vale la pena eseguire offrono una panoramica più ampia.
FAQ
Devo acquistare un feed di dati di mercato a pagamento?
Non per un prototipo. Un feed gratuito e non ufficiale è sufficiente per comprendere la struttura del sistema, ma si interromperà, perché dipende da un sito web che non ti deve nulla. In genere si interrompe restituendo frame vuoti invece di generare un'eccezione, quindi il job deve controllare il numero di righe. Passa a un feed a pagamento con un'API documentata e un indirizzo di supporto non appena i dati iniziano a influenzare una decisione. È lo store a rendere economico il passaggio: cambia solo la funzione di recupero, mentre pianificazione, schema e interfaccia restano invariati.
Devo archiviare i prezzi in SQLite o DuckDB?
DuckDB è colonnare ed è progettato per analizzare molte righe e calcolare un aggregato, che è esattamente ciò che serve per una media mobile su dieci anni di barre. SQLite è orientato alle righe ed è più adatto a molte letture e scritture di piccole dimensioni eseguite contemporaneamente da più processi. Per un singolo job pianificato che aggiunge alcune centinaia di righe e poi analizza milioni di righe, DuckDB è la scelta più adatta. Se più processi devono scrivere contemporaneamente, SQLite in modalità WAL consente ai lettori di operare mentre un writer esegue il commit, e un timeout di attesa fa attendere gli altri writer invece di farli fallire immediatamente.
Quanto costano al mese le chiamate LLM?
Registra usage.input_tokens e usage.output_tokens di ogni risposta in una tabella, quindi moltiplica i totali settimanali per il prezzo per milione di token indicato dal modello nel giorno in cui esegui il controllo. È l'unico valore che resterà valido anche nel prossimo trimestre. Un'esecuzione giornaliera su uno screen breve comporta un numero ridotto di chiamate, e la lunghezza della nota è l'elemento sotto il tuo controllo: limitare il riepilogo a 200 parole fa risparmiare più che inviare meno righe, perché i token di output hanno un prezzo superiore a quello dei token di input in ogni modello Claude.
Perché il mio job è terminato correttamente ma non ha scritto nuove righe?
Le cause comuni sono due. Il mercato era chiuso, perché una pianificazione systemd Mon-Fri include le festività dei mercati. Oppure il feed ha restituito un frame vuoto per ogni ticker; le librerie client spesso segnalano questo caso con un warning stampato invece di generare un'eccezione, quindi il processo termina comunque con codice 0 e systemd mostra comunque un'esecuzione riuscita. Distingui i due casi confrontando SELECT max(day) FROM prices con l'ultimo giorno effettivo di negoziazione e fai terminare il job con codice diverso da zero quando ogni ticker restituisce un risultato vuoto.
L'agent può decidere cosa acquistare?
No. È proprio cercando di farlo che questi progetti diventano problematici. Il modello non ha accesso al mercato, non conosce la tua posizione né la tua situazione fiscale e non può verificare i propri calcoli rispetto a una fonte esterna. È invece efficace nel leggere grandi quantità di testo e indicarti i pochi elementi che meritano la tua attenzione quel giorno. Nulla di ciò che scrive costituisce consulenza finanziaria; la decisione e la relativa responsabilità restano a te.