SSD Nodes Learn 🎉 VPS $4.99/月〜
ガイド Matt Connor著者 Matt Connor ・更新日 2026-08-07

VPSで自作する株式リサーチエージェントの構成

VPS上で株式リサーチエージェントを構築します。市場データをDuckDBに保存し、systemd timerで市場終了後に実行、LLMで銘柄をスクリーニングする構成を解説します。

自己ホスト型の株式リサーチエージェントとは

自己ホスト型の株式リサーチエージェントは、自分で管理するサーバー上で動作する小さなプログラムです。スケジュールに従って市場データを取得し、ローカルデータベースに保存し、データをスクリーニングして、変更点を大規模言語モデル(LLM)に要約させます。データの読み取りと絞り込みを行います。取引は実行しません。このガイドの内容は、金融アドバイスではありません。

これをゼロから構築する 2 人の担当者が異なるライブラリを選んでも、最終的には同じ 4 つの部分に分かれます。価格とファンダメンタルズを提供するデータフィード、取得したすべての行を保持するローカルストア、タイマーでストアを更新するジョブ、残った行を文章に変換する LLM 層です。このガイドでは、Python、DuckDB、systemd timer、Claude API(application programming interface)を使ってこの構成を構築します。取引の実行は別のジョブとして扱い、障害の種類も異なります。そのため、取引ボット向けに構成した VPSで別途運用します。

4 つの構成要素と、それぞれの役割

フィードは、外部と通信する唯一の構成要素です。ティッカーと日付範囲を指定して取得を要求し、行を返す方法を把握しています。後続の処理はすべてフィードではなくデータベースを読み取るため、フィードの障害が発生しても、画面が壊れるのではなく、新しいデータが 1 日分取得できなくなるだけです。

ストアが、この仕組みの中心です。記録しなかった日次終値は、通常、後から再取得できます。一方、日中の株価、修正前の予測値、または修正再表示前のファンダメンタルズの数値は、再取得できない場合があります。ストアによって、データがその日に実際に示していた内容を記録として残せます。

スケジューラーは、更新を実行する時刻を決定します。サーバーでは systemd timer を使用します。そのため、この処理ではコードよりも VPS が重要になります。

LLM 層は、SQL が生成した短いテキストブロックを読み取り、その要約を書きます。データベースには接続せず、クエリも作成しません。モデルに SQL を生成させると、1 つの誤ったトークンが、流暢な文章の中に誤った数値として入り込み、比較対象がなくなります。SQL が数値を生成すれば、モデルが誤る可能性があるのは文章だけです。送信した行と照合して、その文章を確認できます。

ラップトップではなく VPS で実行する理由

スケジューラーが理由です。米国市場の 16:00 の取引終了時刻は、ニューヨーク時間ではベルリンの 22:00、ジャカルタの翌朝 04:00 です。ラップトップはどちらの時刻にもスリープしています。実行を逃すと、遅れて通知するより大きな問題になります。日足データは通常、後から再取得できます。しかし、改訂されたデータは再取得できない場合があるため、記録の空白が恒久的に残ります。

2 つ目の理由は影響が小さいものの、無視できません。サーバーでは、1 つの API key を 1 つのファイルに保存し、ログインシェルを持たない 1 つのシステムユーザーが、1 つのジョブから使用します。Web 閲覧にも使うラップトップで同じ構成を用意するのは、はるかに困難です。key はコードとモデルの入力の両方から分離してください。詳しくは AI エージェントから API key を分離する方法 を参照してください。

容量設計: ディスク、RAM、トークン

ディスクは簡単です。1 日分のバーは、取引日ごとのティッカー 1 件につき 1 行です。米国市場の 1 年間の取引日は約 252 日です。

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"
  }
]

20 件のティッカーでは、1 年後に 5,040 行になります。500 tickers の行は、10 年後に 1,260,000 行に達します。各行は日付と少数の double で構成され、DuckDB は列を圧縮して格納するため、容量はギガバイトではなく数十メガバイトです。ただし、この見積もりを、私の見積もりも含めて、そのまま信頼しないでください。最初のバックフィル後に du -h /opt/research/data/market.duckdb を実行し、自分の環境で実測した値を使用してください。

小規模な VPS では、RAM が問題になります。DuckDB はデフォルトで、単一のクエリに対してマシンのメモリと全 CPU コアを大きく使用します。これは分析用サーバーでは適切ですが、他の処理も実行する 2 GB のマシンでは適切ではありません。prices テーブル全体に対する集計を 1 回実行しただけで、カーネルがプロセスを強制終了することがあります。その場合、systemd は Main process exited, code=killed, status=9/KILL を報告し、journalctl -k には out of memory による kill が表示されます。memory_limitthreads を明示的に設定すると、クエリは強制終了されず、処理時間が長くなります。

トークンは推測せず、実測してください。Messages API のすべてのレスポンスには、usage オブジェクトが含まれます。このオブジェクトには input_tokensoutput_tokens が含まれます。呼び出しのたびに両方をテーブルへ記録してください。1 週間後には実際の使用量が分かるため、確認時点でモデルに提示されている単価を掛けて費用を計算できます。

計画時に基準にしやすい点が 2 つあります。すべての Claude モデルで、出力トークンの単価は入力トークンより高く設定されています。そのため、ノートを 200 語に制限するほうが、送信するデータを削減するより請求額に大きく影響します。また、prompt caching は 1 日 1 回のジョブには有効ではありません。キャッシュの有効期間は分単位であり、次回の実行時にはキャッシュされたブロックの有効期限が切れて、入力料金を全額支払うことになるためです。1 回の実行で、同じ大きなテキストブロックを使って多数の呼び出しを行う場合は、キャッシュが有効です。

構成要素をインストールする

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

コードを書く前に、インストール状態を確認します。

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

okと表示されます。仮想環境を作成せず、システムの Python に対して pip install を実行した場合、Ubuntu 24.04 は error: externally-managed-environment で処理を停止します。ディストリビューションが /usr/lib/python3 を管理しており、pip にそこへの書き込みを許可しないためです。venv は単なる配慮ではありません。pip が書き込める唯一のディレクトリです。

API key は、サービスユーザーが読み取れ、他のユーザーは読み取れないファイルに保存します。

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

ファイルには、引用符も export も付けずに 1 行だけ記述します。systemd はこのファイルをシェルに渡さず、自身で解析するためです。

ANTHROPIC_API_KEY=sk-ant-your-key-here

ストア: 2 つのテーブル

# /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

(ticker, day) の主キーにより、更新処理を安全に繰り返せます。INSERT OR REPLACE は、そのティッカーと日付に該当する既存の行を上書きするため、バックフィルを 2 回実行してもテーブル内のデータが二重になりません。主キーがない場合、クラッシュ後に再実行すると、すべてのバーが気付かないうちに重複します。その後に計算する平均値もすべて誤りますが、エラーメッセージはどこにも表示されません。

DuckDB では、書き込みのためにファイルを開いたまま保持できるプロセスは 1 つだけです。2 つ目の書き込みプロセスは直ちに失敗し、Could not set lock on file と、そのファイルを保持している PID が表示されます。実際には、別のターミナルで開いたままの対話型 duckdb シェルであることが多いです。読み取り処理は read_only=True を通過します。そのため、connect ではこのフラグを指定します。複数のプロセスが同時に書き込む必要がある場合は、別のエンジンを使うべきです。WAL モードの SQLite では、1 つの書き込み処理がコミットしている間も読み取り処理を実行でき、busy timeout により、他の書き込み処理を失敗させずに待機させられます。サーバー用途での DuckDB と SQLite の比較では両者を比較し、VPS の本番環境で SQLite を実行する方法では、WAL を適切に動作させる設定を説明します。

更新ジョブ

# /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()

手動で 1 回実行します。

sudo -u research /opt/research/venv/bin/python /opt/research/refresh.py

初回実行では数年分のデータを補完するため、各ティッカーについて数千件の件数を示す行が 1 行ずつ出力されます。1 分後にもう一度実行すると、各行は 1 件または 2 件を示します。ジョブが、すでに保存されている最終日から処理を開始するためです。この 2 回目の実行が本当のテストです。件数がまだ数千件の場合、max(day) が何も返しておらず、insert が毎晩履歴全体を書き直しています。

df.empty のチェックは、このファイルで最も重要な行です。Ticker.history() は、シンボルが誤っている場合や上場廃止になっている場合でも例外を発生させません。価格データが見つからず、シンボルが上場廃止になった可能性があるという警告を出力し、空の DataFrame を返します。 このチェックがないジョブは何も書き込まず、終了コード 0 で終了します。そのため、systemd では正常な実行として表示される一方、テーブルの行数は静かに増えなくなります。毎日同じ行を返す画面を数週間後に確認して、初めて気づくことになります。

タイムスタンプの処理にも理由があります。フィードが返すインデックスには、取引所のタイムゾーンが付いている場合があります。オフセットを削除することは、UTC に変換することと同じではありません。東京の取引セッションに付いた現地時間の午前 0 時を UTC に変換すると、UTC では前日の日付になります。そのため、UTC 変換を行うと日本の各バーが気づかないうちに 1 日前へずれ、主キーが壊れます。tz_localize(None) は取引所固有のセッション日を維持します。これが日足データの意味する日付です。

取引終了時に実行するタイマー

米国市場はニューヨーク時間の 16:00 に終了します。最後の約定が確定するまで数分かかるため、ジョブは 16:20 に実行します。UTC ではなく、ニューヨーク時間で記述してください。ニューヨーク時間は冬季には UTC マイナス 5 時間、夏季には UTC マイナス 4 時間です。そのため、固定した UTC の時刻でタイマーを記述すると、年 2 回、実行時刻が 1 時間ずれ、取引終了前に実行され始めます。systemd 252 以降では、OnCalendar にタイムゾーンを直接指定できます。Ubuntu 24.04 には 255 が搭載されています。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 は、複数の ExecStart を受け付ける唯一のサービスタイプです。複数の ExecStart を順番に実行し、いずれかが非ゼロで終了すると停止します。これは必要な動作です。更新に失敗した場合、古いデータを表示する処理を続けてはなりません。Persistent=true は、カーネル更新のために再起動する VPS で重要です。これを指定せずに 16:15 に再起動すると、その回の実行は失われます。指定していれば、マシンが復旧するとすぐにジョブが実行されます。

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

list-timers には、次回の平日実行時刻をサーバー自身のローカル時間に変換して格納する NEXT 列が表示されるはずです。リストが空の場合、タイマーが有効になっていないか、ユニットに [Install] セクションがありません。そのため、enabletimers.target にリンクする対象を取得できませんでした。

画面: SQLを先に、モデルを最後に

SQLは正確で、実行ごとのコストもかかりません。一方、モデルはそうではありません。そのため、クエリで対象範囲を絞り込み、残ったデータだけをモデルに送ります。これを /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;

n > 50 フィルターは飾りではありません。ROWS BETWEEN 49 PRECEDING AND CURRENT ROW は存在する行を対象に平均するため、ティッカーの3行目では3日分の平均を返し、それでも ma50 と呼びます。これを ma20 と比較すると、実際には発生していないクロスオーバーが、すべてのティッカーの履歴開始時点で発生したように見えてしまいます。行番号でフィルタリングすると、ウィンドウが満たされていない行を除外できます。

モデルが見るもの、決して見ないもの

# /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()

system prompt の語数制限により、コストの大きい処理部分の料金を抑えられます。検証が必要なのは、指定された行だけを使うという指示です。この指示をそのまま信頼してはいけません。ブロックから列を1つ削除して再実行し、出力を確認してください。その列の数値がまだ表示される場合、モデルが不足分を補っています。prompt の制約が不十分です。このテストは2分で実行でき、事実を確認する唯一の確実な方法です。

モデルが API key を見ることはありません。database path を見ることもなく、query も実行しません。モデルが受け取るのは行で、返すのは文章です。この境界があるため、出力を検証できます。メモ内のすべての数値は、送信したブロック内にも含まれているはずです。行ごとに比較できます。prompt の使い方については、財務分析で Claude を使うで、モデルが読み取るのを得意とする内容をさらに説明しています。

エンドツーエンドの1回の実行

New York 時間の16:20に、タイマーがサービスを起動します。refresh.pyは保存済みの最終日以降について各ティッカーのデータをフィードに要求し、各ティッカーに1本または2本の新しいバーを書き込み、ティッカーごとに1行を出力します。screen.pyは同じファイルを開き、移動平均のクエリを実行して、数行の結果を取得します。その結果を数百トークンのテキストブロックにします。1回のAPI呼び出しで短いメモに変換し、そのメモをジャーナルに記録します。また、トークン数とヒット数を含む1行がrunsに格納されます。

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

正常なログには、ティッカーごとの1行、メモ、research-refresh.service: Deactivated successfullyの順に記録されます。1週間後、データベースから実際のコストを読み出します。

SELECT count(*) AS runs,
       sum(input_tokens)  AS in_tokens,
       sum(output_tokens) AS out_tokens
FROM runs;

これらの合計に、読み出した日のモデルに記載されている100万トークンあたりの料金を掛けます。これにより、誰かの推定値ではなく、実際の金額が得られます。公開料金は変わります。計算方法は変わりません。

バックテストが過学習する理由と、その過程を監視する方法

スクリーニングを2つのウィンドウ長の関数として書き直し、ペアのグリッドを総当たりして、リターンの順に並べます。最良のペアは非常に優れているように見えます。問題はその結果ではなく、この手順です。200ペアのグリッドは200回の実験に相当し、その中で最も運のよかったものを選んでいるからです。

この過程は10分で確認できます。日付でデータストアを半分に分けます。前半だけでグリッドを総当たりし、勝者を記録します。同じグリッドを後半でも総当たりします。2つの勝者が大きく異なるなら、パラメータはノイズに適合しています。調整に使った期間でしか勝てないペアは、翌日のことを何も示しません。

生存者バイアスは、過学習より深刻です。調整では修正できないためです。ティッカーの一覧が現在の指数構成銘柄なので、生き残った企業しか含まれていません。2019年に上場廃止になったティッカーをフィードに問い合わせても、空のフレームが返ります。そのため、その企業はデータストアにもテストにも入りません。実行するすべてのバックテストで、すでに失敗した銘柄が除外されています。

修正再表示されたファンダメンタルズは、時系列を壊します。APIが現在返す2019年の四半期の売上高は、2019年に公表された数値と常に同じとは限りません。現在のファンダメンタルズと2019年の価格を混在させるスクリーニングは、当時は存在しなかった情報を使用しています。ここでは価格は通常問題ありません。ファンダメンタルズには通常問題があります。

調整済み価格は後から変わります。 auto_adjust=Trueでは、配当と株式分割を反映して終値が過去にさかのぼって調整されるため、同じクエリを翌月に実行すると、わずかに異なる履歴が返ります。実際に使用した行を保存することで、結果を再現できるようになります。これも、ローカルデータストアが必要な理由です。

このバックテストでは、手数料とスリッページも無視しています。また、注文によって価格が動かないと仮定しています。これらは執行に関する事項であり、ここでは対象外です。VPS 上でトレーディングボットを実行する方法で説明します。

障害パターンと表示される文字列

error: externally-managed-environment が pip の実行時に表示される場合。仮想環境の外にいます。/opt/research/venv/bin/pip をフルパスで呼び出してください。

Could not set lock on file の後に PID が表示される場合。別のプロセスが DuckDB ファイルを開いたまま書き込みロックを保持しています。通常は、終了し忘れた対話シェルが原因です。そのプロセスを終了するか、2 つ目の接続を read_only=True で開いてください。

Main process exited, code=killed, status=9/KILLsystemctl status に表示される場合。カーネルがメモリ不足によりジョブを強制終了しました。journalctl -k | grep -i oom で確認し、store.pymemory_limit を下げてください。

実行は成功したように見えるのに何も書き込まれない。 systemctl statusactive (exited) を読み込みますが、テーブルの行数が増えていません。フィードが空のフレームを返しています。この障害は発見に最も時間がかかるため、すべての ticker が空だった場合はジョブを non zero で終了させてください。

市場休日に timer が実行された。 systemd は取引所のカレンダーを認識しないため、Mon-Fri には休日も含まれます。実行自体は行われますが、フィードに新しいデータはありません。この場合、ジョブはエラーではなく正常な状態として扱う必要があります。

API からの 429 rate limit を超えています。Anthropic SDK は自動的に backoff 付きで retry し、Anthropic(max_retries=5) により試行回数が増えます。それでも毎日失敗する場合は、ジョブが 1 回の burst で要求しすぎています。

これは何ではないか

これは調査アシスタントです。提出書類を要約するモデルは、その書類の読み取り結果を生成します。本文に記載された数値を、確信を持って誤ることがあります。そのため、注記内のすべての数値は、送信した行まで追跡できなければなりません。出力は、自分で読むべき項目の候補一覧として扱ってください。ここで提供するものは金融アドバイスではなく、売買シグナルでもありません。

バックテストは、アイデアを退ける用途には有効ですが、裏付ける用途には不十分です。自分のデータで失敗する戦略は、実際に使えません。通過した戦略も、自分のデータを生き残っただけです。これは、午前1時には感じるほど大きな主張ではありません。

注文の執行は、意図的に対象外とします。注文とブローカーの認証情報は、read only の調査用サーバーとは異なるリスク特性を持ちます。両者を混在させると、取引用の鍵を LLM プロンプトと同じマシンに置くことになります。この構成が、自分のサーバーで運用する価値のある他の仕組みとどの位置関係にあるかを確認したい場合は、自分でホストする価値のある AI エージェントで全体像を確認できます。

FAQ

有料の市場データフィードは必要ですか?

プロトタイプには必要ありません。無料の非公式フィードで、システムの構成を学ぶには十分です。ただし、何も保証されていない Web サイトに依存しているため、停止します。多くの場合、例外ではなく空のフレームとして停止するため、ジョブで行数を確認する必要があります。データが意思決定に使われるようになったら、API 仕様とサポート窓口が用意された有料フィードに移行してください。交換を容易にするのが store です。変更するのは fetch 関数だけで、スケジュール、スキーマ、画面はそのまま維持できます。

価格データは SQLite と DuckDB のどちらに保存すべきですか?

DuckDB は列指向で、多数の行をスキャンして集計値を計算する処理に適しています。10 年分のバーに対する移動平均は、まさにこの処理です。SQLite は行指向で、複数のプロセスから小さな読み書きを同時に多数実行する用途に適しています。1 つの定期ジョブが数百行を追加した後、数百万行をスキャンする構成では、DuckDB のほうが適しています。複数のプロセスから同時に書き込む必要がある場合は、SQLite を WAL モードで使用すると、1 つの writer がコミットしている間も reader が処理できます。busy timeout を設定すれば、他の writer は即座に失敗せず待機します。

LLM の呼び出し料金は月額でどの程度かかりますか?

すべてのレスポンスから usage.input_tokensusage.output_tokens を取得してテーブルに記録し、週ごとの合計に、確認日にモデルが示している 100 万 token あたりの料金を掛けてください。次の四半期でも有効な数値はこれだけです。短いスクリーニングを 1 日 1 回実行するだけなら、呼び出し回数は少なくなります。制御しやすいのはノートの長さです。要約を 200 語に制限するほうが、行数を減らして送信するよりも節約になります。すべての Claude モデルでは、出力 token の料金が入力 token より高いためです。

ジョブは成功したのに、新しい行が書き込まれないのはなぜですか?

一般的な原因は 2 つあります。1 つ目は市場が休場していた場合です。systemd の Mon-Fri スケジュールには、取引所の休日が含まれています。2 つ目は、フィードがすべての ticker に対して空のフレームを返した場合です。client library はこれを例外ではなく警告として出力することが多いため、プロセスは 0 で終了し、systemd も実行成功として表示します。SELECT max(day) FROM prices を直近の実際の取引日と比較すれば、両者を区別できます。また、すべての ticker が空だった場合は、ジョブを non zero で終了させてください。

agent に購入対象を決めさせることはできますか?

できません。そう試みるように構築することが、この種のプロジェクトが失敗する原因です。モデルは市場にアクセスできず、保有状況や税務上の状況も把握できません。また、自身の数値を他の情報と照合する手段もありません。モデルが得意なのは、大量のテキストを読み、今日注意を向けるべき少数の項目を示すことです。モデルが生成する内容は金融アドバイスではありません。意思決定とその責任は、あなたにあります。