SSD Nodes Learn 🎉 VPS da $4.99/mese
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-08-05

Creare un agente di ricerca azionaria su VPS

Guida pratica per costruire un sistema di analisi dati con Python, DuckDB e Claude. Automatizza il monitoraggio dei mercati usando un timer systemd sul tuo VPS dedicato.

Cos'è un agente di ricerca azionaria self-hosted

Un agente di ricerca azionaria self-hosted è un piccolo programma eseguito su un server di tua proprietà che preleva dati di mercato secondo una pianificazione, li archivia in un database locale, esegue uno screening e interroga un modello linguistico di grandi dimensioni (LLM) per riassumere le variazioni rilevate. Il sistema legge e filtra i dati. Non effettua operazioni di trading e nulla di quanto contenuto in questa guida costituisce consulenza finanziaria.

Due persone che sviluppano questo sistema da zero sceglieranno librerie diverse, ma arriveranno comunque a definire le stesse quattro componenti: un feed che fornisce prezzi e dati fondamentali, un archivio locale che conserva ogni riga scaricata, un job che aggiorna l'archivio a intervalli regolari e un livello LLM che trasforma le righe filtrate in testo leggibile. Questa guida costruisce tale struttura utilizzando Python, DuckDB, un timer systemd e la Claude API (application programming interface). L'esecuzione di operazioni di trading è un job distinto con modalità di errore differenti e va gestita su un VPS configurato per bot di trading.

Le quattro componenti e le relative funzioni

Il feed è l'unica parte che comunica con l'esterno. Sa come richiedere un ticker e un intervallo di date, e come restituire le righe di dati. Ogni componente a valle legge il tuo database anziché il feed; in questo modo, un'interruzione del feed comporta la perdita di un solo giorno di nuovi dati, evitando il blocco dell'interfaccia.

Lo store è il fulcro dell'intera operazione. Una chiusura giornaliera non registrata può solitamente essere recuperata in seguito. Una quotazione intraday, una stima prima della sua revisione o un dato fondamentale prima di essere ricalcolato non possono esserlo. Lo store serve a costruire un registro di ciò che i dati riportavano effettivamente nel giorno in cui sono stati pubblicati.

Lo scheduler decide quando eseguire l'aggiornamento. Su un server, questo compito è affidato a un systemd timer, motivo per cui la configurazione del VPS è più rilevante del codice stesso.

Il layer LLM legge un breve blocco di testo prodotto dalla tua query SQL e ne scrive un riassunto. Non si connette mai al database e non costruisce mai la query. Se fosse il modello a scrivere l'SQL, un singolo token errato si trasformerebbe in un numero sbagliato all'interno di una frase fluente, senza alcun termine di paragone. Se invece è l'SQL a produrre i numeri, il modello può sbagliare solo la prosa, che puoi verificare confrontandola con le righe che hai inviato.

Perché eseguirlo su una VPS invece che su un portatile

Il motivo è 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 seguente a Giacarta. In entrambi i casi, un portatile è in modalità sospensione. Un'esecuzione mancata costa più di una notifica in ritardo: le barre giornaliere possono solitamente essere recuperate in seguito, ma tutto ciò che viene revisionato non può esserlo, quindi il vuoto nel tuo registro diventa permanente.

La seconda ragione è meno rilevante ma comunque concreta. Il server ospita una sola API key, in un singolo file, di proprietà di un unico utente di sistema privo di login shell, utilizzata da un solo job. È molto più difficile organizzare tutto ciò sul portatile che usi anche per navigare sul web. Mantieni la chiave fuori dal codice e dall'input del modello, argomento trattato in mantenere le API key al sicuro da un agente AI.

Dimensionamento: disco, RAM e token

Il disco è l'aspetto più semplice. Una barra giornaliera corrisponde a una riga per ticker per ogni giorno di negoziazione; un anno di borsa statunitense conta circa 252 giorni.

ChartRows in the prices table by watchlist size, at 252 trading days a year
The data behind this chart
[
  {
    "label": "20 tickers",
    "rows_after_1y": "5,040",
    "rows_after_10y": "50,400"
  },
  {
    "label": "100 tickers",
    "rows_after_1y": "25,200",
    "rows_after_10y": "252,000"
  },
  {
    "label": "500 tickers",
    "rows_after_1y": "126,000",
    "rows_after_10y": "1,260,000"
  }
]

Venti ticker equivalgono a 5,040 righe dopo un anno. La linea 500 tickers raggiunge 1,260,000 righe dopo dieci anni. Ogni riga contiene una data e alcuni valori in virgola mobile; DuckDB archivia le colonne in modo compresso, quindi si parla di decine di megabyte anziché di gigabyte. Non fare affidamento su questa stima, inclusa la mia. Esegui du -h /opt/research/data/market.duckdb dopo il primo backfill e utilizza i tuoi dati reali.

La RAM è il punto critico per i VPS di piccole dimensioni. Per impostazione predefinita, DuckDB occupa una quota elevata della memoria della macchina e utilizza tutti i core disponibili per una singola query. Questo comportamento è corretto su un server di analisi, ma non su una macchina da 2 GB che esegue anche altri processi. Un'aggregazione sull'intera tabella dei prezzi causa l'interruzione del processo da parte del kernel; systemd segnala Main process exited, code=killed, status=9/KILL, mentre journalctl -k mostra l'evento di terminazione per mancanza di memoria (OOM kill). Imposta esplicitamente memory_limit e threads: la query risulterà più lenta, ma non verrà terminata.

I token vanno misurati, non stimati. Ogni risposta della Messages API contiene un oggetto usage con input_tokens e output_tokens. Registra entrambi in una tabella a ogni chiamata; dopo una settimana conoscerai il tuo volume reale, che moltiplicherai per il costo indicato dal tuo modello nel giorno della verifica. Due fattori sono sufficientemente stabili per la pianificazione. I token di output costano più di quelli di input in ogni modello Claude, quindi limitare la nota a 200 parole incide sul costo più che ridurre i dati inviati. Inoltre, il prompt caching non è utile per un'attività eseguita una volta al giorno, poiché la durata della cache si misura in minuti: al momento dell'esecuzione successiva, il blocco memorizzato è scaduto e pagherai nuovamente l'intero prezzo di input. Il caching è vantaggioso quando una singola esecuzione effettua molte chiamate sullo stesso ampio blocco di testo.

Installazione dei 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/data

Verifica l'installazione prima di scrivere qualsiasi riga di codice:

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

Il comando stampa ok. Se hai saltato la creazione dell'ambiente virtuale ed eseguito pip install sul Python di sistema, Ubuntu 24.04 blocca l'operazione con error: externally-managed-environment. Questo accade perché la distribuzione gestisce /usr/lib/python3 e impedisce a pip di scrivervi. L'uso di un venv non è una questione di cortesia. È l'unica directory in cui a pip è consentito operare.

La API key va inserita in un file leggibile esclusivamente dall'utente di servizio:

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

Inserisci una sola riga, senza virgolette e senza export, poiché systemd analizza direttamente il file invece di passarlo a una shell:

ANTHROPIC_API_KEY=sk-ant-your-key-here

Lo 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 con

La chiave primaria su (ticker, day) rende sicuro ripetere l'aggiornamento. INSERT OR REPLACE sovrascrive una riga già esistente per quel ticker e quel giorno, quindi eseguire il backfill due volte non duplica la tabella. Senza la chiave, una riesecuzione dopo un crash duplica silenziosamente ogni barra e ogni media calcolata successivamente risulterà errata, senza alcun messaggio di errore a segnalarlo.

DuckDB consente a un solo processo di mantenere il file aperto in scrittura. Un secondo writer fallisce immediatamente con Could not set lock on file, seguito dal PID che lo detiene, che nella pratica è la shell interattiva duckdb lasciata aperta in un altro terminale. I reader passano read_only=True, motivo per cui connect richiede il flag. Se più processi devono scrivere effettivamente nello stesso momento, si tratta di un compito per un altro motore: SQLite in modalità WAL consente ai reader di lavorare mentre un writer esegue il commit, e un busy timeout fa attendere gli altri writer invece di fallire. DuckDB contro SQLite per un carico di lavoro server confronta i due, e eseguire SQLite in produzione su una VPS copre le impostazioni che rendono il comportamento di WAL affidabile.

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.py

La prima esecuzione effettua il backfill di anni di dati 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, poiché il job riparte dall'ultimo giorno già archiviato. Questa seconda esecuzione è il vero test: se i conteggi sono ancora nell'ordine delle migliaia, max(day) non sta restituendo nulla e l'operazione di insert sta sovrascrivendo l'intera cronologia ogni notte.

Il controllo df.empty è la riga più importante nel file. Ticker.history() non solleva eccezioni per un simbolo errato o rimosso dal listino. Stampa un avviso indicando che il simbolo potrebbe essere stato rimosso o che non sono stati trovati dati di prezzo (la dicitura esatta varia tra le versioni della libreria) e restituisce un DataFrame vuoto. Un job privo di 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, osservando una schermata che restituisce sempre le stesse righe ogni giorno.

Anche la gestione del timestamp ha una sua motivazione. L'indice restituito dal feed può contenere il fuso orario della borsa; rimuovere l'offset non equivale a convertire in UTC. Una sessione di Tokyo marcata a mezzanotte ora locale viene convertita nel giorno di calendario precedente in UTC; pertanto, una conversione UTC sposta silenziosamente ogni barra giapponese indietro di un giorno, invalidando la chiave primaria. tz_localize(None) mantiene la data di sessione propria della borsa, che è il riferimento corretto per 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 consolidate, pertanto il job viene eseguito alle 16:20. Imposta l'orario basandoti sul fuso orario di New York, non su UTC. New York è UTC meno 5 in inverno e UTC meno 4 in estate; di conseguenza, un timer impostato su un'ora UTC fissa subirebbe uno scostamento di un'ora due volte l'anno, attivandosi prima della chiusura. systemd 252 e versioni successive accettano un fuso orario direttamente in OnCalendar, e Ubuntu 24.04 rilascia la versione 255. Verifica la tua versione 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.target

Type=oneshot è l'unico tipo di servizio che accetta più di un ExecStart; li esegue in ordine e si arresta se uno di essi termina con un codice di uscita diverso da zero. Questo è esattamente il comportamento desiderato: un aggiornamento fallito non deve essere seguito da una visualizzazione di dati obsoleti. Persistent=true è importante su un VPS che si riavvia per aggiornamenti del kernel. Senza questa opzione, un riavvio alle 16:15 comporterebbe la perdita dell'esecuzione; con essa, il job viene avviato non appena la macchina torna operativa.

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

list-timers dovrebbe mostrare una colonna NEXT contenente la prossima esecuzione nei giorni feriali, convertita nell'ora locale del server. Un elenco vuoto indica che il timer non è abilitato o che all'unità manca la sezione [Install], impedendo a enable di creare il collegamento in timers.target.

Lo schermo: prima SQL, poi il modello

SQL è preciso e non ha costi per esecuzione. Il modello non è né l'uno né l'altro. Pertanto, la query restringe l'universo e solo i risultati superstiti vengono inviati al modello. Salva questo 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 è un elemento decorativo. ROWS BETWEEN 49 PRECEDING AND CURRENT ROW calcola la media di tutte le righe esistenti; di conseguenza, la terza riga di un ticker restituisce la media di tre giorni e la definisce comunque ma50. Confronta questo valore con ma20 e creerai un crossover all'inizio della cronologia di ogni ticker che non è mai avvenuto. Filtrare in base al numero di riga elimina le righe in cui 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,
    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()

Il limite di parole nel prompt di sistema limita la parte costosa del conteggio. L'istruzione di utilizzare solo le righe fornite è un vincolo che devi verificare invece di dare per scontato: elimina una colonna dal blocco, esegui nuovamente e leggi l'output. Se appare ancora un valore per quella colonna, il modello ha colmato la lacuna e il tuo prompt non è abbastanza restrittivo. Questo test richiede due minuti ed è l'unico modo onesto per verificarlo.

Il modello non vede mai la API key, non vede mai il percorso del database e non esegue mai una query. Riceve righe e restituisce testo. Questo confine è ciò che rende l'output verificabile, poiché ogni numero nella nota dovrebbe apparire anche nel blocco che hai inviato, permettendoti di confrontarli riga per riga. Per ulteriori approfondimenti sul prompting, utilizzare Claude per l'analisi finanziaria spiega meglio in cosa il modello è efficace durante la lettura.

Esecuzione singola, end-to-end

Alle 16:20 ora di New York il timer avvia il servizio. refresh.py richiede al feed ogni ticker a partire dall'ultimo giorno archiviato, scrive una o due nuove barre per ciascuno e stampa una riga per ticker. screen.py apre lo stesso file, esegue la query sulla media mobile e riceve alcune righe. Tali righe diventano un blocco di testo di alcune centinaia di token. Una chiamata API le trasforma in una breve nota, la nota viene inviata al journal e una riga viene salvata in runs con il conteggio dei token e il numero di risultati.

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

Un log integro contiene una riga per ticker, seguita dalla nota e infine da research-refresh.service: Deactivated successfully. Dopo una settimana, è possibile estrarre dal database il costo sostenuto:

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 effettui la lettura. Questo fornisce la cifra reale invece di una stima generica. I prezzi pubblicati cambiano. Il calcolo no.

Perché i backtest soffrono di overfitting e come monitorare il fenomeno

Riscrivi lo screen come funzione delle due lunghezze di finestra, esegui uno sweep su una griglia di coppie e classificale in base al rendimento. La coppia migliore apparirà eccellente. Questo è il problema, non il risultato. Una griglia di 200 coppie equivale a 200 esperimenti e tu hai conservato quello più fortunato.

Puoi osservare il fenomeno in dieci minuti. Dividi lo storico a metà in base alla data. Esegui lo sweep della griglia solo sulla prima metà e annota il vincitore. Esegui la stessa griglia sulla seconda metà. Se i due vincitori sono molto distanti, i parametri stanno adattandosi al rumore; una coppia che vince solo sulla metà su cui è stata ottimizzata non fornisce alcuna indicazione sul domani.

La survivorship bias è peggiore dell'overfitting perché l'ottimizzazione non può correggerla. La tua lista di ticker è composta dai membri dell'indice odierno, quindi contiene solo le aziende che sono sopravvissute. Se richiedi al feed un ticker che è stato delistato nel 2019, questo restituisce un frame vuoto, il che significa che quell'azienda non entra mai nel tuo archivio e mai nel tuo test. Ogni backtest che esegui ha già escluso i fallimenti.

I fondamentali ricalcolati rompono la linea temporale. Il dato sui ricavi che l'API restituisce oggi per un trimestre del 2019 non è sempre il dato che fu pubblicato nel 2019. Uno screen che mescola i fondamentali odierni con i prezzi del 2019 utilizza informazioni che all'epoca non esistevano. I prezzi solitamente sono sicuri in questo contesto. I fondamentali solitamente non lo sono.

I prezzi rettificati cambiano sotto i tuoi piedi. Con auto_adjust=True le chiusure vengono rettificate retroattivamente per dividendi e split, quindi la stessa query eseguita il mese prossimo restituisce uno storico leggermente diverso. Archiviare le righe che hai effettivamente utilizzato è ciò che rende un risultato riproducibile, ed è un altro motivo per cui esiste l'archivio locale.

Il backtest ignora inoltre commissioni e slippage, e assume che il tuo ordine non sposti il prezzo. Questi aspetti appartengono all'esecuzione, che esula dallo scopo di questa guida ed è trattata in esecuzione di bot di trading su un VPS.

Modalità di errore e stringhe visualizzate

error: externally-managed-environment durante l'esecuzione di pip. Ti trovi all'esterno 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 mantiene aperto il file DuckDB in scrittura, solitamente una shell interattiva che hai dimenticato aperta. Chiudila, oppure apri la seconda connessione con read_only=True.

Main process exited, code=killed, status=9/KILL in systemctl status. Il kernel ha terminato il processo per esaurimento della 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 configura il job affinché termini con un codice diverso da zero quando ogni ticker restituisce un risultato vuoto.

Il timer è scattato durante un giorno festivo di mercato. systemd non conosce il calendario delle borse, quindi Mon-Fri include anche i giorni festivi. L'esecuzione avviene, il feed non contiene dati nuovi e il job dovrebbe gestire questa condizione come normale anziché come un errore.

429 dall'API. Hai superato un limite di frequenza (rate limit). L'SDK di Anthropic esegue automaticamente dei tentativi con backoff e Anthropic(max_retries=5) incrementa il conteggio dei tentativi. Se l'errore persiste quotidianamente, il job sta richiedendo una quantità eccessiva di dati in un singolo burst.

Cosa non è questo strumento

Questo è un assistente alla ricerca. Un modello che riassume un documento produce una lettura di tale documento e può sbagliare con sicurezza un numero riportato nel testo; per questo motivo, ogni cifra nella nota deve essere riconducibile a una riga che hai inviato. Considera l'output come una lista di controllo di elementi da leggere personalmente. Nulla di quanto qui riportato costituisce consulenza finanziaria e nessuno dei contenuti rappresenta un segnale di trading.

I backtest sono utili per scartare idee, ma sono poco efficaci per confermarle. Una strategia che fallisce sui tuoi dati è realmente inefficace. Una strategia che li supera è semplicemente sopravvissuta ai tuoi dati, il che è un'affermazione molto meno solida di quanto possa sembrare all'una di notte.

L'esecuzione rimane deliberatamente fuori dallo scopo. Gli ordini e le credenziali del broker comportano un profilo di rischio diverso rispetto a un ambiente di ricerca in sola lettura; mescolarli significa conservare le chiavi di trading sulla stessa macchina che elabora i prompt di un LLM. Se vuoi vedere dove si colloca questo modello rispetto ad altri strumenti utili da eseguire sul tuo server, gli agenti AI self-hosted che vale la pena eseguire è la panoramica completa.

FAQ

È necessario un feed di dati di mercato a pagamento?

Non per un prototipo. Un feed gratuito non ufficiale è sufficiente per comprendere la struttura del sistema, ma è soggetto a interruzioni poiché dipende da un sito web che non offre garanzie. Solitamente i guasti si manifestano come frame vuoti anziché come eccezioni, quindi il tuo processo deve verificare il conteggio delle righe. Passa a un feed a pagamento con API documentata e un indirizzo di supporto non appena i dati iniziano a influenzare decisioni reali. Lo storage rende questa transizione economica: cambiano solo la funzione di fetch, mentre lo schedule, lo schema e la schermata rimangono invariati.

Devo conservare i prezzi in SQLite o DuckDB?

DuckDB è orientato alle colonne ed è progettato per scansionare molte righe al fine di calcolare aggregati, operazione ideale per una media mobile su dieci anni di barre. SQLite è orientato alle righe ed è più efficiente per molte piccole operazioni di lettura e scrittura simultanee da parte di più processi. Per un job pianificato che aggiunge poche centinaia di righe e ne scansiona milioni, DuckDB è la scelta migliore. Se più processi devono scrivere contemporaneamente, SQLite in modalità WAL consente ai lettori di lavorare mentre un writer esegue il commit, e un busy timeout fa attendere gli altri writer invece di fallire immediatamente.

Quanto costano le chiamate LLM al mese?

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 tuo modello nel giorno in cui effettui il controllo. È l'unico dato che rimane valido nel trimestre successivo. Un'esecuzione giornaliera su uno screen limitato comporta un numero ridotto di chiamate, e la lunghezza della nota è la variabile che puoi controllare: limitare il riassunto a 200 parole permette di risparmiare più che inviare meno righe, poiché i token di output hanno un costo superiore a quelli di input su ogni modello Claude.

Perché il mio job è terminato con successo ma non ha scritto nuove righe?

Ci sono due cause comuni. Il mercato era chiuso, poiché uno schedule systemd Mon-Fri include le festività di borsa. Oppure il feed ha restituito un frame vuoto per ogni ticker, situazione che le librerie client spesso riportano come avviso stampato anziché come eccezione, facendo sì che il processo termini con codice 0 e systemd mostri un esito positivo. Distingui i due casi confrontando SELECT max(day) FROM prices con l'ultimo giorno di trading effettivo e configura il job affinché termini con un codice diverso da zero quando tutti i ticker risultano vuoti.

L'agente può decidere cosa acquistare?

No, e tentare di implementare questa funzione è l'errore tipico di questi progetti. Il modello non ha accesso al mercato, non ha visibilità sulla tua posizione o sulla tua situazione fiscale e non ha modo di verificare i propri calcoli. Il suo punto di forza è leggere grandi quantità di testo e indicarti quali pochi elementi meritano la tua attenzione oggi. Nulla di ciò che scrive costituisce consulenza finanziaria; la decisione, insieme alla relativa responsabilità, rimane tua.