Własny agent do analizy giełdowej na VPS - poradnik
Dowiedz się, jak zbudować własnego agenta giełdowego na VPS. Przewodnik obejmuje konfigurację DuckDB, automatyzację zadań przez systemd oraz integrację z modelem LLM.
Czym jest samodzielnie hostowany agent do analizy giełdowej
Samodzielnie hostowany agent do analizy giełdowej to niewielki program uruchomiony na własnym serwerze, który zgodnie z harmonogramem pobiera dane rynkowe, przechowuje je w lokalnej bazie danych, przeprowadza ich selekcję i zleca modelowi językowemu (LLM) przygotowanie podsumowania zmian. Program ten odczytuje i filtruje dane. Nie dokonuje transakcji, a treść niniejszego przewodnika nie stanowi porady inwestycyjnej.
Dwie osoby budujące takie rozwiązanie od podstaw mogą wybrać różne biblioteki, ale ostatecznie uzyskają te same cztery elementy: kanał dostarczający ceny i dane fundamentalne, lokalny magazyn przechowujący wszystkie pobrane rekordy, zadanie odświeżające magazyn według harmonogramu oraz warstwę LLM, która przekształca wyselekcjonowane rekordy w tekst. Niniejszy przewodnik buduje taką strukturę w oparciu o Python, DuckDB, timer systemd oraz API (application programming interface) Claude. Realizacja transakcji to odrębne zadanie o innych punktach awarii, dlatego powinno ono zostać przeniesione na serwer VPS skonfigurowany pod boty transakcyjne.
Cztery elementy i ich funkcje
Feed to jedyny komponent komunikujący się ze światem zewnętrznym. Potrafi wysłać zapytanie o ticker oraz zakres dat, a następnie zwrócić wiersze danych. Wszystkie kolejne etapy przetwarzania odczytują dane z bazy, a nie bezpośrednio z feedu. Dzięki temu awaria źródła danych skutkuje jedynie brakiem danych z jednego dnia, zamiast awarią całego interfejsu.
Store stanowi główny cel całego procesu. Dzienną cenę zamknięcia, której nie udało się zapisać, zazwyczaj można pobrać ponownie w późniejszym terminie. Notowań śróddziennych, szacunków przed ich korektą czy danych fundamentalnych przed ich aktualizacją nie da się odzyskać. Store pozwala zbudować archiwum informacji w postaci, w jakiej były one dostępne w dniu publikacji.
Scheduler decyduje o momencie odświeżenia danych. Na serwerze rolę tę pełni systemd timer, dlatego w tym kontekście konfiguracja VPS jest ważniejsza niż sam kod.
Warstwa LLM odczytuje krótki blok tekstu wygenerowany przez SQL i tworzy jego podsumowanie. Model nigdy nie łączy się z bazą danych i nie tworzy zapytań. Jeśli model generuje SQL, jeden błędny token może skutkować błędną liczbą w poprawnie brzmiącym zdaniu, której nie ma do czego porównać. Jeśli to SQL dostarcza liczby, model może jedynie popełnić błąd w opisie tekstowym, który można zweryfikować, porównując go z przesłanymi wierszami danych.
Dlaczego warto uruchomić to na VPS zamiast na laptopie
Powodem jest harmonogram zadań. Zamknięcie sesji na rynku amerykańskim o 16:00 czasu nowojorskiego przypada na 22:00 w Berlinie i 04:00 rano następnego dnia w Dżakarcie. Laptop w obu tych przypadkach jest w trybie uśpienia. Pominięte uruchomienie kosztuje więcej niż tylko spóźniona notatka: dzienne słupki cenowe zazwyczaj można pobrać później, ale danych, które podlegają korekcie, nie da się odzyskać, więc luka w rejestrze jest trwała. To samo rozumowanie dotyczy każdego agenta, którego harmonogram i zapisany stan muszą przetrwać restart systemu, co stanowi podstawę uruchamiania KiroCrew jako zawsze aktywnego agenta na własnym VPS.
Drugi powód jest mniej istotny, ale wciąż realny. Serwer przechowuje jeden klucz API, w jednym pliku, należącym do jednego użytkownika systemowego bez powłoki logowania, używanego przez jedno zadanie. Znacznie trudniej jest to zorganizować na laptopie, którego używa się również do przeglądania sieci. Należy trzymać klucz z dala od kodu i danych wejściowych modelu, co jest przedmiotem przechowywania kluczy API poza zasięgiem agenta AI.
Wymiarowanie: dysk, RAM i tokeny
Dysk jest najprostszym elementem. Jeden dzienny słupek to jeden wiersz na ticker dla każdego dnia sesyjnego, a rok giełdowy w USA liczy około 252 dni.
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"
}
]Dwadzieścia tickerów to 5,040 wierszy po roku. Linia 500 tickers osiąga 1,260,000 wierszy po dziesięciu latach. Każdy wiersz zawiera datę i kilka wartości typu double, a DuckDB przechowuje kolumny w formie skompresowanej, więc mowa tu o dziesiątkach megabajtów, a nie gigabajtach. Nie należy polegać na tym szacunku, w tym również na moim. Po pierwszym uzupełnieniu danych (backfill) należy uruchomić du -h /opt/research/data/market.duckdb i wykorzystać własne wyniki.
RAM to miejsce, w którym mały VPS może sprawiać problemy. DuckDB domyślnie zajmuje znaczną część pamięci maszyny i wszystkie jej rdzenie na potrzeby pojedynczego zapytania, co jest poprawne na serwerze analitycznym, ale niewłaściwe na maszynie 2 GB, która obsługuje również inne zadania. Jedna agregacja na całej tabeli cen powoduje zabicie procesu przez jądro systemu, a systemd zgłasza Main process exited, code=killed, status=9/KILL, podczas gdy journalctl -k wskazuje na zakończenie procesu z powodu braku pamięci (OOM kill). Należy jawnie ustawić memory_limit oraz threads, dzięki czemu zapytanie będzie wolniejsze, zamiast kończyć się awarią.
Zużycie tokenów należy mierzyć, a nie szacować. Każda odpowiedź Messages API zawiera obiekt usage z polami input_tokens oraz output_tokens. Należy zapisywać obie te wartości do tabeli przy każdym wywołaniu, a po tygodniu znany będzie rzeczywisty wolumen, który należy pomnożyć przez cennik modelu obowiązujący w dniu sprawdzenia. Dwie kwestie są wystarczająco stabilne, aby planować w oparciu o nie budżet. Tokeny wyjściowe są wyżej wyceniane niż wejściowe w każdym modelu Claude, więc ograniczenie notatki do 200 słów wpłynie na rachunek bardziej niż przycinanie wysyłanych danych. Prompt caching nie pomaga w zadaniach uruchamianych raz dziennie, ponieważ czas życia pamięci podręcznej liczony jest w minutach: do czasu kolejnego uruchomienia zbuforowany blok wygasa i użytkownik ponownie płaci pełną cenę za dane wejściowe. Caching opłaca się wtedy, gdy jedno uruchomienie wykonuje wiele wywołań w obrębie tego samego, dużego bloku tekstu.
Instalacja komponentów
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/dataPrzed napisaniem jakiegokolwiek kodu należy sprawdzić instalację:
sudo -u research /opt/research/venv/bin/python -c 'import duckdb, yfinance, anthropic; print("ok")'Polecenie to wypisuje ok. Jeśli pominięto środowisko wirtualne i uruchomiono pip install w systemowym Python, Ubuntu 24.04 zablokuje operację komunikatem error: externally-managed-environment. Wynika to z faktu, że dystrybucja zarządza /usr/lib/python3 i nie zezwala narzędziu pip na zapis w tej lokalizacji. Środowisko venv nie jest kwestią dobrych praktyk, lecz jedynym katalogiem, w którym pip posiada uprawnienia do zapisu.
Klucz API należy umieścić w pliku, który może odczytać wyłącznie użytkownik usługi:
sudo install -d -m 755 /etc/research
sudo install -m 640 -o root -g research /dev/null /etc/research/env
sudoedit /etc/research/envW pliku należy umieścić jedną linię tekstu bez cudzysłowów i bez export, ponieważ systemd parsuje ten plik samodzielnie, zamiast przekazywać go do powłoki:
ANTHROPIC_API_KEY=sk-ant-your-key-hereMagazyn danych: dwie tabele
# /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 conKlucz główny w (ticker, day) sprawia, że operację odświeżania można bezpiecznie powtarzać. INSERT OR REPLACE nadpisuje wiersz, który już istnieje dla danego tickera i dnia, więc dwukrotne uruchomienie uzupełniania danych nie powoduje duplikacji w tabeli. Bez klucza, ponowne uruchomienie po awarii cicho powiela każdy słupek, a każda obliczona później średnia jest błędna, bez żadnego komunikatu o błędzie, który mógłby o tym poinformować.
DuckDB pozwala dokładnie jednemu procesowi na otwarcie pliku do zapisu. Drugi proces zapisujący kończy się natychmiast błędem Could not set lock on file, po którym następuje PID procesu blokującego plik, którym w praktyce jest interaktywna powłoka duckdb pozostawiona w innym terminalu. Procesy odczytujące przekazują read_only=True, dlatego connect wymaga tej flagi. Jeśli kilka procesów rzeczywiście musi zapisywać w tym samym momencie, jest to zadanie dla innego silnika: SQLite w trybie WAL pozwala na pracę czytelników podczas zatwierdzania transakcji przez jednego piszącego, a limit czasu oczekiwania (busy timeout) sprawia, że inni piszący czekają zamiast kończyć pracę błędem. DuckDB kontra SQLite w obciążeniu serwerowym porównuje oba rozwiązania, a uruchamianie SQLite na produkcji na VPS omawia ustawienia, które zapewniają poprawne działanie trybu WAL.
Zadanie odświeżania
# /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()Uruchom je jednorazowo ręcznie:
sudo -u research /opt/research/venv/bin/python /opt/research/refresh.pyPierwsze uruchomienie uzupełnia dane z lat ubiegłych i wyświetla po jednej linii dla każdego symbolu giełdowego z liczbą wierszy rzędu tysięcy. Uruchom zadanie ponownie minutę później; każda linia powinna wskazywać 1 lub 2 wiersze, ponieważ zadanie rozpoczyna się od ostatniego dnia zapisanego w bazie. To drugie uruchomienie jest właściwym testem: jeśli liczby wciąż są rzędu tysięcy, max(day) nie zwraca żadnych danych, a operacja insert nadpisuje całą historię każdej nocy.
Sprawdzenie df.empty jest najważniejszą linią w pliku. Ticker.history() nie zgłasza błędu w przypadku błędnego lub wycofanego z obrotu symbolu. Wyświetla jedynie ostrzeżenie o możliwym wycofaniu symbolu z obrotu i braku danych cenowych (dokładne brzmienie komunikatu zmienia się wraz z wersjami biblioteki) oraz zwraca pusty obiekt DataFrame. Zadanie pozbawione tego sprawdzenia nie zapisuje niczego, kończy się kodem 0, a systemd wskazuje poprawne wykonanie, podczas gdy tabela przestaje rosnąć. Problem wykrywa się dopiero po tygodniach, obserwując ekran, na którym codziennie wyświetlają się te same wiersze.
Obsługa znaczników czasu również ma swoje uzasadnienie. Indeks zwracany przez źródło danych może zawierać strefę czasową giełdy, a usunięcie przesunięcia nie jest równoważne z konwersją do UTC. Sesja tokijska o północy czasu lokalnego po konwersji do UTC przesuwa się na poprzedni dzień kalendarzowy, więc konwersja do UTC cicho przesuwa każdy japoński słupek o jeden dzień wstecz i narusza klucz główny. tz_localize(None) zachowuje datę sesji właściwą dla danej giełdy, co jest kluczowe dla definicji słupka dziennego.
Timer wyzwalany po zamknięciu sesji giełdowej
Rynek amerykański zamyka się o 16:00 czasu nowojorskiego, a ostatnie transakcje wymagają kilku minut na rozliczenie, dlatego zadanie uruchamiane jest o 16:20. Należy zapisać to w czasie nowojorskim, a nie w UTC. Nowy Jork stosuje czas UTC minus 5 zimą oraz UTC minus 4 latem, więc timer zapisany jako stała godzina UTC przesuwa się o godzinę dwa razy w roku i zaczyna uruchamiać się przed zamknięciem sesji. Wersja systemd 252 i nowsze akceptują strefę czasową bezpośrednio w OnCalendar, a Ubuntu 24.04 zawiera wersję 255. Wersję swojego systemu można sprawdzić za pomocą 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 to jedyny typ usługi, który akceptuje więcej niż jedną dyrektywę ExecStart; uruchamia je w kolejności, zatrzymując się, jeśli któraś z nich zakończy się kodem innym niż zero. Jest to dokładnie takie zachowanie, jakiego oczekujesz: nieudane odświeżenie nie może skutkować wyświetleniem nieaktualnych danych. Persistent=true ma znaczenie na serwerze VPS, który restartuje się w celu aktualizacji jądra. Restart o 16:15 bez tej opcji powoduje utratę uruchomienia; z nią zadanie wykona się natychmiast po powrocie maszyny do pracy.
sudo systemctl daemon-reload
sudo systemctl enable --now research-refresh.timer
systemctl list-timers research-refresh.timerlist-timers powinno wyświetlić kolumnę NEXT zawierającą czas następnego uruchomienia w dniu roboczym, przeliczony na czas lokalny serwera. Pusta lista oznacza, że timer nie jest włączony lub jednostce brakuje sekcji [Install], przez co enable nie miało czego powiązać z timers.target.
Ekran: najpierw SQL, na końcu model
SQL jest precyzyjny i nie generuje kosztów przy każdym uruchomieniu. Model nie posiada żadnej z tych cech. Zapytanie zawęża zatem zakres danych, a do modelu trafiają wyłącznie wyselekcjonowane rekordy. Zapisz to jako /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;Filtr n > 50 nie jest elementem dekoracyjnym. Funkcja ROWS BETWEEN 49 PRECEDING AND CURRENT ROW wylicza średnią dla wszystkich dostępnych wierszy, więc trzeci wiersz danych dla danego symbolu giełdowego zwraca średnią z trzech dni, wciąż nazywając ją ma50. W zestawieniu z ma20 prowadzi to do wygenerowania sygnału przecięcia na początku historii każdego symbolu, który w rzeczywistości nie wystąpił. Filtrowanie po numerze wiersza usuwa rekordy, dla których okno czasowe nie zostało w pełni wypełnione.
Co model widzi, a czego nigdy nie widzi
# /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()Limit słów w systemowym prompcie ogranicza kosztowną część rachunku. Instrukcję korzystania wyłącznie z dostarczonych wierszy należy zweryfikować, zamiast jej ufać: usuń jedną kolumnę z bloku, uruchom proces ponownie i odczytaj wynik. Jeśli wartość dla tej kolumny nadal się pojawia, model uzupełnił brak samodzielnie, co oznacza, że prompt nie jest wystarczająco restrykcyjny. Ten test zajmuje dwie minuty i jest jedynym rzetelnym sposobem weryfikacji.
Model nigdy nie widzi klucza API, nigdy nie widzi ścieżki do bazy danych i nigdy nie wykonuje zapytań. Otrzymuje wiersze i zwraca tekst. Ta granica sprawia, że wynik jest weryfikowalny, ponieważ każda liczba w notatce powinna również znajdować się w przesłanym bloku, co pozwala na porównanie ich wiersz po wierszu. Więcej informacji na temat tworzenia promptów w tym zakresie znajduje się w wykorzystaniu Claude do analizy finansowej, gdzie szerzej omówiono umiejętności analityczne modelu.
Jednorazowe uruchomienie, pełny cykl
O godzinie 16:20 czasu nowojorskiego timer uruchamia usługę. refresh.py pobiera z kanału dane dla każdego tickera, zaczynając od ostatniego zapisanego dnia, zapisuje jeden lub dwa nowe słupki dla każdego z nich i wypisuje jedną linię na ticker. screen.py otwiera ten sam plik, wykonuje zapytanie o średnią kroczącą i otrzymuje kilka wierszy. Wiersze te tworzą blok tekstowy o długości kilkuset tokenów. Jedno wywołanie API zamienia je w krótką notatkę, notatka trafia do dziennika, a jeden wiersz ląduje w runs wraz z liczbą tokenów i liczbą trafień.
journalctl -u research-refresh.service -n 50 --no-pagerPoprawny log zawiera jedną linię na ticker, następnie notatkę, a potem research-refresh.service: Deactivated successfully. Po tygodniu odczytaj własne koszty z bazy danych:
SELECT count(*) AS runs,
sum(input_tokens) AS in_tokens,
sum(output_tokens) AS out_tokens
FROM runs;Pomnóż te sumy przez ceny za milion tokenów, które podaje Twój model w dniu odczytu. Pozwoli to uzyskać rzeczywistą kwotę zamiast czyichś szacunków. Publikowane ceny ulegają zmianie. Arytmetyka pozostaje niezmienna.
Dlaczego backtesty ulegają overfittingowi i jak to monitorować
Przedstaw wynik jako funkcję dwóch długości okien, przetestuj siatkę par i uszereguj je według stopy zwrotu. Najlepsza para będzie wyglądać doskonale. To jest problem, a nie wynik. Siatka 200 par to 200 eksperymentów, z których zachowano ten, który miał najwięcej szczęścia.
Można zaobserwować ten proces w dziesięć minut. Podziel dane historyczne na pół według daty. Przeszukaj siatkę tylko na pierwszej połowie i zapisz zwycięzcę. Przeszukaj tę samą siatkę na drugiej połowie. Jeśli zwycięzcy znacznie się różnią, parametry dopasowują się do szumu, a para, która wygrywa tylko na połowie, na której została dostrojona, nie daje żadnych informacji o przyszłości.
Survivorship bias jest gorszy niż overfitting, ponieważ dostrajanie go nie naprawi. Lista tickerów to dzisiejsi członkowie indeksu, więc zawiera tylko firmy, które przetrwały. Jeśli zapytasz o ticker, który został wycofany z obrotu w 2019 roku, otrzymasz pustą ramkę danych, co oznacza, że firma ta nigdy nie trafi do Twojego magazynu danych i nigdy nie weźmie udziału w teście. Każdy przeprowadzony backtest już wykluczył porażki.
Restated fundamentals naruszają oś czasu. Wartość przychodów zwracana dzisiaj przez API dla kwartału z 2019 roku nie zawsze jest wartością, która została opublikowana w 2019 roku. Strategia łącząca dzisiejsze dane fundamentalne z cenami z 2019 roku wykorzystuje informacje, które wtedy nie istniały. Ceny są zazwyczaj bezpieczne, dane fundamentalne zazwyczaj nie.
Adjusted prices zmieniają się w czasie. W przypadku auto_adjust=True ceny zamknięcia są korygowane wstecznie o dywidendy i splity, więc to samo zapytanie uruchomione w przyszłym miesiącu zwróci nieco inną historię. Przechowywanie wierszy, które faktycznie zostały użyte, zapewnia powtarzalność wyników i jest kolejnym powodem istnienia lokalnego magazynu danych.
Backtest ignoruje również prowizje i poślizgi cenowe (slippage) oraz zakłada, że zlecenie nie wpływa na cenę. Kwestie te należą do egzekucji zleceń, która wykracza poza zakres tego opracowania i jest omówiona w uruchamianiu botów transakcyjnych na VPS.
Tryby awarii i komunikaty błędów
error: externally-managed-environment podczas uruchamiania pip. Znajdujesz się poza środowiskiem wirtualnym. Wywołaj /opt/research/venv/bin/pip, podając pełną ścieżkę.
Could not set lock on file, z numerem PID w treści. Inny proces blokuje plik DuckDB do zapisu, zazwyczaj jest to zapomniana sesja interaktywna. Zamknij ją lub otwórz drugie połączenie za pomocą read_only=True.
Main process exited, code=killed, status=9/KILL w systemctl status. Jądro systemu przerwało zadanie z powodu braku pamięci. Potwierdź to za pomocą journalctl -k | grep -i oom, a następnie zmniejsz memory_limit w store.py.
Poprawne wykonanie bez zapisu danych. systemctl status odczytuje active (exited), a tabela nie zwiększyła swojego rozmiaru. Strumień danych zwrócił puste ramki. Ten typ awarii jest najtrudniejszy do wykrycia, dlatego skonfiguruj zadanie tak, aby kończyło się kodem błędu, gdy każdy ticker zwraca puste dane.
Uruchomienie timera w dzień wolny od handlu. systemd nie posiada informacji o kalendarzu giełdowym, więc Mon-Fri obejmuje również dni świąteczne. Zadanie zostaje uruchomione, strumień danych nie zawiera nowych informacji, a proces powinien potraktować to jako stan normalny, a nie błąd.
429 z API. Przekroczono limit zapytań. SDK Anthropic automatycznie ponawia próby z mechanizmem backoff, a Anthropic(max_retries=5) zwiększa licznik prób. Jeśli błąd powtarza się codziennie, zadanie wysyła zbyt duże pakiety danych w jednym cyklu.
Czym to nie jest
To narzędzie jest asystentem badawczym. Model podsumowujący dokumentację tworzy jej interpretację i może błędnie zinterpretować liczbę zawartą w tekście, dlatego każda wartość w notatce musi być możliwa do zweryfikowania w przesłanym wierszu danych. Wynik należy traktować jako listę zagadnień do samodzielnego sprawdzenia. Treść nie stanowi porady finansowej ani sygnału inwestycyjnego.
Backtesty są przydatne do odrzucania nietrafionych pomysłów, ale słabo sprawdzają się w ich potwierdzaniu. Strategia, która nie sprawdza się na własnych danych, jest definitywnie nieskuteczna. Strategia, która przechodzi testy, jedynie przetrwała próbę na posiadanych danych, co jest znacznie słabszym wnioskiem, niż może się wydawać w nocy.
Realizacja zleceń celowo wykracza poza zakres tego narzędzia. Zlecenia i dane uwierzytelniające brokera wiążą się z innym profilem ryzyka niż środowisko badawcze typu read-only, a ich łączenie naraża klucze transakcyjne na przechowywanie na tej samej maszynie, na której przetwarzane są prompty LLM. Jeśli chcesz sprawdzić, jak ten wzorzec wpisuje się w inne rozwiązania warte uruchomienia na własnym serwerze, agenty AI do samodzielnego hostowania warte uwagi stanowią szerszy przegląd.
FAQ
Czy potrzebuję płatnego źródła danych rynkowych?
Nie w przypadku prototypu. Darmowe, nieoficjalne źródło wystarczy do poznania struktury systemu, jednak będzie ulegać awariom, ponieważ opiera się na stronie internetowej, która nie gwarantuje dostępności. Zazwyczaj awarie objawiają się pustymi ramkami danych, a nie wyjątkami, dlatego zadaniem użytkownika jest weryfikacja liczby wierszy. Przejdź na płatne źródło z udokumentowanym API i adresem wsparcia technicznego, gdy dane zaczną wpływać na podejmowanie decyzji. Magazyn danych sprawia, że taka zmiana jest tania: zmienia się tylko funkcja pobierania, podczas gdy harmonogram, schemat i interfejs pozostają bez zmian.
Czy przechowywać ceny w SQLite czy DuckDB?
DuckDB jest bazą kolumnową, stworzoną do skanowania wielu wierszy w celu obliczenia agregatów, co jest dokładnie tym, czym jest średnia krocząca z dziesięciu lat notowań. SQLite jest zorientowany wierszowo i lepiej radzi sobie z wieloma małymi operacjami odczytu i zapisu z kilku procesów jednocześnie. Dla pojedynczego zadania zaplanowanego, które dopisuje kilkaset wierszy, a następnie skanuje miliony, lepszym wyborem jest DuckDB. Jeśli kilka procesów musi zapisywać dane w tym samym czasie, SQLite w trybie WAL pozwala czytelnikom pracować, podczas gdy jeden proces zapisujący zatwierdza transakcję, a parametr busy timeout sprawia, że inni piszący czekają, zamiast kończyć działanie błędem.
Ile kosztują wywołania LLM miesięcznie?
Rejestruj usage.input_tokens oraz usage.output_tokens z każdej odpowiedzi w tabeli, a następnie pomnóż tygodniowe sumy przez cenę za milion tokenów, którą model podaje w dniu sprawdzenia. Jest to jedyna wartość, która pozostanie aktualna w następnym kwartale. Jedno dzienne uruchomienie dla krótkiego zestawienia to niewielka liczba wywołań, a długość notatki jest elementem, który można kontrolować: ograniczenie podsumowania do 200 słów daje większe oszczędności niż wysyłanie mniejszej liczby wierszy, ponieważ tokeny wyjściowe są wyżej wyceniane niż wejściowe w każdym modelu Claude.
Dlaczego zadanie zakończyło się sukcesem, ale nie zapisało nowych wierszy?
Istnieją dwie typowe przyczyny. Rynek był zamknięty, ponieważ harmonogram systemd Mon-Fri uwzględnia święta giełdowe. Alternatywnie, źródło danych zwróciło pustą ramkę dla każdego symbolu, co biblioteki klienckie często zgłaszają jako ostrzeżenie w konsoli, a nie jako wyjątek, więc proces kończy się kodem 0, a systemd pokazuje poprawny przebieg. Rozróżnij te sytuacje, porównując SELECT max(day) FROM prices z ostatnim rzeczywistym dniem handlowym i skonfiguruj zadanie tak, aby kończyło się niezerowym kodem wyjścia, gdy dane dla wszystkich symboli są puste.
Czy agent może decydować o zakupach?
Nie, a próba budowy takiego mechanizmu jest główną przyczyną niepowodzeń tych projektów. Model nie ma dostępu do rynku, nie zna Twojej pozycji ani sytuacji podatkowej i nie ma możliwości weryfikacji własnych obliczeń. Model sprawdza się w czytaniu dużej ilości tekstu i wskazywaniu kilku pozycji, które wymagają uwagi danego dnia. Nic, co napisze model, nie stanowi porady finansowej, a decyzja oraz odpowiedzialność za nią pozostają po stronie użytkownika.