Skip to content
Published on

AI金融 & クオンツトレーディング: FinBERT、強化学習、バックテストまで

Authors

1. 金融データの収集と前処理

クオンツトレーディングの出発点はデータです。OHLCV(始値・高値・安値・終値・出来高)に加え、オーダーブックのスナップショット、ティックデータ、ニュースや衛星画像といった代替データまで活用範囲が広がっています。

yfinanceで株価データをダウンロード

import yfinance as yf
import pandas as pd

# 複数銘柄の日足OHLCVデータを取得
tickers = ["AAPL", "MSFT", "GOOGL", "NVDA"]
df = yf.download(tickers, start="2020-01-01", end="2026-01-01", auto_adjust=True)

# MultiIndex → 銘柄別 DataFrame に変換
close  = df["Close"]
volume = df["Volume"]

# 欠損値処理: 前値補完後、先頭のNaN行を削除
close = close.ffill().dropna()

print(close.tail())

ccxtで暗号資産オーダーブックを取得

import ccxt

exchange  = ccxt.binance()
symbol    = "BTC/USDT"
orderbook = exchange.fetch_order_book(symbol, limit=20)

bids = orderbook["bids"][:5]   # 上位5件 [価格, 数量] の買い注文
asks = orderbook["asks"][:5]   # 上位5件 [価格, 数量] の売り注文

mid_price  = (bids[0][0] + asks[0][0]) / 2
spread_bps = (asks[0][0] - bids[0][0]) / mid_price * 10000

print(f"Mid: {mid_price:.2f}, Spread: {spread_bps:.2f} bps")

代替データ: ニュースヘッドラインの収集

ニュースやSNSデータは、価格データでは捉えられない自然言語アルファを提供します。

import requests
from datetime import datetime, timedelta

API_KEY   = "YOUR_NEWSAPI_KEY"
yesterday = (datetime.today() - timedelta(days=1)).strftime("%Y-%m-%d")

url = (
    f"https://newsapi.org/v2/everything"
    f"?q=NVIDIA+earnings&from={yesterday}&sortBy=publishedAt"
    f"&language=en&apiKey={API_KEY}"
)
resp      = requests.get(url).json()
headlines = [art["title"] for art in resp.get("articles", [])]
print(headlines[:5])

2. テクニカル分析の自動化

TA-Libとpandas-taを使えば、数百種類のテクニカル指標をPython一行で計算できます。

RSI / MACDの計算

import talib
import numpy as np
import yfinance as yf

df    = yf.download("AAPL", start="2023-01-01", end="2026-01-01", auto_adjust=True)
close = df["Close"].squeeze().values.astype(float)

# RSI (14日)
rsi = talib.RSI(close, timeperiod=14)

# MACD
macd, signal, hist = talib.MACD(close, fastperiod=12, slowperiod=26, signalperiod=9)

# pandas-ta の使い方(TA-Lib 不要)
import pandas_ta as ta
df_ta = df["Close"].squeeze().to_frame("close")
df_ta.ta.rsi(length=14, append=True)
df_ta.ta.macd(fast=12, slow=26, signal=9, append=True)
print(df_ta.tail())

ローソク足パターン認識

open_p  = df["Open"].squeeze().values.astype(float)
high_p  = df["High"].squeeze().values.astype(float)
low_p   = df["Low"].squeeze().values.astype(float)
close_p = df["Close"].squeeze().values.astype(float)

hammer       = talib.CDLHAMMER(open_p, high_p, low_p, close_p)
engulfing    = talib.CDLENGULFING(open_p, high_p, low_p, close_p)
morning_star = talib.CDLMORNINGSTAR(open_p, high_p, low_p, close_p)

# 100(強気)/ -100(弱気)/ 0(パターンなし)
print("ハンマー検出数:", (hammer != 0).sum())

3. MLトレーディング戦略: XGBoostアルファファクター

フィーチャーエンジニアリング

import pandas as pd
import numpy as np
import yfinance as yf
import pandas_ta as ta

df = yf.download("SPY", start="2018-01-01", end="2026-01-01", auto_adjust=True)
df.columns = df.columns.droplevel(1) if df.columns.nlevels > 1 else df.columns
df.columns = [c.lower() for c in df.columns]

# リターン特徴量
df["ret_1d"]  = df["close"].pct_change(1)
df["ret_5d"]  = df["close"].pct_change(5)
df["ret_20d"] = df["close"].pct_change(20)

# ボラティリティ特徴量
df["vol_20d"] = df["ret_1d"].rolling(20).std()

# テクニカル指標特徴量
df.ta.rsi(length=14, append=True)
df.ta.macd(fast=12, slow=26, signal=9, append=True)
df.ta.bbands(length=20, append=True)

# 出来高特徴量
df["vol_ratio"] = df["volume"] / df["volume"].rolling(20).mean()

# ターゲット: 5日先リターンの符号 (1: 上昇, 0: 下落)
df["target"] = (df["close"].pct_change(5).shift(-5) > 0).astype(int)

df.dropna(inplace=True)
print(df.shape)

ウォークフォワード検証でXGBoostを学習

from xgboost import XGBClassifier
from sklearn.metrics import accuracy_score, roc_auc_score
import warnings
warnings.filterwarnings("ignore")

feature_cols = [
    "ret_1d", "ret_5d", "ret_20d", "vol_20d",
    "RSI_14", "MACD_12_26_9", "MACDs_12_26_9",
    "BBL_20_2.0", "BBM_20_2.0", "BBU_20_2.0",
    "vol_ratio"
]
target_col = "target"

results    = []
train_years = 2
test_months = 3

dates      = df.index
start_year = dates[0].year + train_years

for year in range(start_year, 2026):
    for q in range(1, 5):
        train_end  = pd.Timestamp(f"{year}-{(q-1)*3+1:02d}-01") if q > 1 else pd.Timestamp(f"{year}-01-01")
        test_start = train_end
        test_end   = test_start + pd.DateOffset(months=test_months)

        train_df = df[df.index < test_start].tail(504)
        test_df  = df[(df.index >= test_start) & (df.index < test_end)]

        if len(train_df) < 100 or len(test_df) < 10:
            continue

        X_train, y_train = train_df[feature_cols], train_df[target_col]
        X_test,  y_test  = test_df[feature_cols],  test_df[target_col]

        model = XGBClassifier(
            n_estimators=200, max_depth=4,
            learning_rate=0.05, subsample=0.8,
            eval_metric="logloss", random_state=42
        )
        model.fit(X_train, y_train)

        preds = model.predict(X_test)
        proba = model.predict_proba(X_test)[:, 1]

        acc = accuracy_score(y_test, preds)
        auc = roc_auc_score(y_test, proba)
        results.append({"period": str(test_start.date()), "acc": acc, "auc": auc})

result_df = pd.DataFrame(results)
print(result_df.tail(8))
print(f"\n平均AUC: {result_df['auc'].mean():.4f}")

4. 金融向け深層学習: LSTMとTemporal Fusion Transformer

LSTM株価予測

import numpy as np
import torch
import torch.nn as nn
from sklearn.preprocessing import MinMaxScaler

scaler = MinMaxScaler()
scaled = scaler.fit_transform(df[["close"]].values)

SEQ_LEN = 60

def make_sequences(data, seq_len):
    X, y = [], []
    for i in range(len(data) - seq_len):
        X.append(data[i:i+seq_len])
        y.append(data[i+seq_len])
    return np.array(X), np.array(y)

X, y   = make_sequences(scaled, SEQ_LEN)
split  = int(len(X) * 0.8)
X_train, X_test = X[:split], X[split:]
y_train, y_test = y[:split], y[split:]

X_train_t = torch.tensor(X_train, dtype=torch.float32)
y_train_t = torch.tensor(y_train, dtype=torch.float32)

class LSTMModel(nn.Module):
    def __init__(self, input_size=1, hidden_size=64, num_layers=2):
        super().__init__()
        self.lstm = nn.LSTM(input_size, hidden_size, num_layers,
                            batch_first=True, dropout=0.2)
        self.fc   = nn.Linear(hidden_size, 1)

    def forward(self, x):
        out, _ = self.lstm(x)
        return self.fc(out[:, -1, :])

model     = LSTMModel()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
criterion = nn.MSELoss()

for epoch in range(30):
    model.train()
    pred = model(X_train_t)
    loss = criterion(pred, y_train_t)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    if (epoch + 1) % 10 == 0:
        print(f"Epoch {epoch+1}, Loss: {loss.item():.6f}")

FinRL強化学習トレーディングエージェント

FinRLはOpenAI Gymベースの環境で株式トレーディングのRLエージェントを学習させるフレームワークです。

# pip install finrl
from finrl.meta.env_stock_trading.env_stocktrading import StockTradingEnv
from finrl.agents.stablebaselines3.models import DRLAgent
import pandas as pd

# FinRL形式に前処理済みの金融データ
# 必須カラム: date, tic, open, high, low, close, volume + テクニカル指標
processed_df = pd.read_csv("processed_stock_data.csv")

env_kwargs = {
    "hmax": 100,               # 最大保有株数
    "initial_amount": 100000,  # 初期資金(ドル)
    "buy_cost_pct": 0.001,     # 売買手数料 0.1%
    "sell_cost_pct": 0.001,
    "reward_scaling": 1e-4,
    "state_space": 181,
    "action_space": 30,
    "tech_indicator_list": ["macd", "rsi_30", "cci_30", "dx_30"],
}

train_env = StockTradingEnv(df=processed_df, **env_kwargs)

agent       = DRLAgent(env=train_env)
model_ppo   = agent.get_model("ppo")
trained_ppo = agent.train_model(
    model=model_ppo,
    tb_log_name="ppo_stock",
    total_timesteps=50000
)

5. LLM for Finance: FinBERT感情分析

FinBERTは金融ニュースや決算発表に特化したBERTの事前学習モデルで、Positive / Negative / Neutralの3クラス分類を行います。

from transformers import BertTokenizer, BertForSequenceClassification
import torch
import torch.nn.functional as F

model_name = "ProsusAI/finbert"
tokenizer  = BertTokenizer.from_pretrained(model_name)
model      = BertForSequenceClassification.from_pretrained(model_name)
model.eval()

def finbert_sentiment(texts):
    inputs = tokenizer(texts, padding=True, truncation=True,
                       max_length=512, return_tensors="pt")
    with torch.no_grad():
        logits = model(**inputs).logits
    probs  = F.softmax(logits, dim=-1).numpy()
    labels = ["positive", "negative", "neutral"]
    return [
        {"text": t, "label": labels[p.argmax()], "score": float(p.max())}
        for t, p in zip(texts, probs)
    ]

headlines = [
    "NVIDIA beats Q4 earnings estimates by 15%, raises guidance",
    "Fed signals higher-for-longer rates amid sticky inflation",
    "Apple reports record services revenue despite iPhone slowdown",
]
results = finbert_sentiment(headlines)
for r in results:
    print(f"[{r['label'].upper():8s}] {r['score']:.3f} | {r['text']}")

決算発表の数値/ガイダンスとテキストトーンの分離分析

決算発表には(1) EPS・売上などの定量数値、(2) 将来ガイダンス数値、(3) 経営陣発言のテキストトーンという3種類の情報が含まれます。LLMはトーンの把握が得意ですが、数値が含まれる文章は誤判定することがあります。数値パース(regex/構造化NLP)とテキスト感情分析を分離してアンサンブルすると、より強い自然言語アルファを得られます。


6. リスク管理: VaR、CVaR、ケリー基準

VaR / CVaRの計算

import numpy as np

def calculate_var_cvar(returns, confidence=0.95):
    """
    ヒストリカルVaR/CVaRの計算
    returns: 日次リターン配列
    """
    sorted_returns = np.sort(returns)
    index = int((1 - confidence) * len(sorted_returns))
    var  = -sorted_returns[index]
    cvar = -sorted_returns[:index].mean()
    return var, cvar

daily_returns = df["close"].pct_change().dropna().values
var_95, cvar_95 = calculate_var_cvar(daily_returns, 0.95)
var_99, cvar_99 = calculate_var_cvar(daily_returns, 0.99)

print(f"VaR  95%: {var_95:.4f} ({var_95*100:.2f}%)")
print(f"CVaR 95%: {cvar_95:.4f} ({cvar_95*100:.2f}%)")
print(f"VaR  99%: {var_99:.4f} ({var_99*100:.2f}%)")
print(f"CVaR 99%: {cvar_99:.4f} ({cvar_99*100:.2f}%)")

主要リスク指標の比較表

指標計算式特徴限界
シャープレシオ(Rp - Rf) / sigma_p標準化されたリスク調整後リターン上昇変動性にもペナルティ
ソルティノレシオ(Rp - Rf) / sigma_d下方変動性のみにペナルティ分母の計算が直感的でない
最大ドローダウン最高値からの最大損失極端な損失を捕捉回復期間を反映しない
VaR 95%下位5%損失分位規制標準テールリスクを過小評価
CVaR 95%VaR超過損失の期待値テールリスクを反映分布仮定に感応
カルマーレシオCAGR / MDD下落対比の成長性短期分析には不向き

ケリー基準によるポジションサイジング

def kelly_fraction(win_rate, win_loss_ratio):
    """
    f* = W - (1 - W) / R
    W: 勝率, R: 平均損益比
    """
    return win_rate - (1 - win_rate) / win_loss_ratio

# 例: 勝率55%、損益比1.5
f_full = kelly_fraction(0.55, 1.5)
f_half = f_full * 0.5   # fractional Kelly(変動性を低減)

print(f"フルケリー:     {f_full:.2%}")
print(f"ハーフケリー:   {f_half:.2%}")

fractional Kelly(通常は0.25〜0.5倍)を使う理由: 勝率・損益比の推定誤差が大きく、フルケリーは破産確率を過小評価するため、安全マージンを設けます。


7. バックテスト: Vectorbtで戦略を検証

Vectorbtで移動平均クロスオーバーをバックテスト

import vectorbt as vbt
import pandas as pd
import yfinance as yf

price = yf.download("SPY", start="2018-01-01", end="2026-01-01",
                    auto_adjust=True)["Close"].squeeze()

fast_ma = vbt.MA.run(price, 20)
slow_ma = vbt.MA.run(price, 60)

entries = fast_ma.ma_crossed_above(slow_ma)
exits   = fast_ma.ma_crossed_below(slow_ma)

portfolio = vbt.Portfolio.from_signals(
    price,
    entries,
    exits,
    init_cash=100_000,
    fees=0.001,       # 手数料 0.1%
    slippage=0.001,   # スリッページ 0.1%
    freq="D",
)

stats = portfolio.stats()
print(stats[["Total Return [%]", "Sharpe Ratio", "Max Drawdown [%]",
             "Win Rate [%]", "Profit Factor"]])

バックテストのバイアスチェックリスト

バックテストで好成績が出たときに必ず確認すべき項目:

バイアスの種類原因対策
ルックアヘッドバイアス未来データで現在のシグナルを計算shift(-1)を確認し特徴量生成タイミングを見直す
サバイバーシップバイアス上場廃止銘柄がユニバースから除外ポイントインタイムの全銘柄データを使用
最適化バイアスインサンプルパラメータの過学習ウォークフォワード検証、アウトオブサンプル保留
マーケットインパクト無視大口注文の価格影響を無視スリッページモデル、出来高制限付きサイジング
取引コストの過小評価実際のスプレッド・手数料を含まない現実的な手数料とスリッページを設定

クイズ

Q1. ウォークフォワード検証がk分割交差検証より金融時系列に適している理由は?

答え: 金融時系列には時間的依存性があり、ランダムに分割するとフォールド間で未来データが訓練に混入するルックアヘッドバイアスが発生します。

解説: ウォークフォワード検証は常に過去データで学習し未来データでテストするため、時系列の順序を保持します。k分割では一部のフォールドにテスト期より後の観測値が訓練データとして入り込み、モデルが「未来を知っている」ように見えて性能が水増しされます。金融時系列は自己相関やレジームシフトがあるため、時間順序を保った検証が不可欠です。

Q2. シャープレシオの限界と、ソルティノレシオがより適切な状況は?

答え: シャープレシオは上昇・下降の変動性を同等にペナルティとして扱うため、大きな上昇リターンによる変動性の高い戦略を不当に低く評価します。

解説: ソルティノレシオは分母を下方標準偏差に置き換え、投資家が本当に嫌う損失方向の変動性のみにペナルティを与えます。オプション売り、モメンタム戦略など非対称なリターン分布を持つ戦略や、右裾が厚い戦略の評価にソルティノレシオがより適しています。

Q3. ルックアヘッドバイアスがバックテスト結果を楽観的に歪める仕組みは?

答え: シグナルの生成時点では存在しなかった未来のデータ(当日の終値、翌四半期の決算など)がフィーチャーやラベルの計算に含まれ、モデルが実際よりはるかに高い予測精度を示します。

解説: 具体的な原因として、当日終値で計算したRSIを同日始値のエントリーシグナルに使う、shift(-n)を適用し忘れる、ローリング統計にmin_periodsの設定ミスがある、指数移動平均が未来の情報を逆方向に漏洩させるなどが挙げられます。

Q4. ケリー基準がポジションサイジングに最適な数学的根拠と、fractional Kellyを使う理由は?

答え: ケリー公式 f* = W - (1 - W) / R は、期待対数リターンを最大化し、長期的な幾何平均成長率を最大にする賭け比率を導出します。

解説: E[log(資産)]を最大化するため、理論上は他のいかなる固定比率戦略より長期的に資産を最速で増やせます。しかし勝率・損益比の推定誤差が大きいと、過剰なレバレッジで大きなドローダウンが発生します。fractional Kelly(f*の25〜50%)は漸近成長率を若干犠牲にしながら分散とドローダウンを大幅に削減し、実運用でより現実的です。

Q5. 決算発表をLLMで感情分析する際に数値/ガイダンスとテキストトーンを分離すべき理由は?

答え: 経営陣が数値は良好に発表しながら翌四半期のガイダンスを保守的に示したり、逆に悪い実績をポジティブな表現で包むケースが頻繁にあり、混合分析ではシグナルが希薄化します。

解説: 決算発表には(1) EPS・売上などの実績数値、(2) 将来ガイダンス数値、(3) 経営陣コメントのテキストトーンという3種類の情報が含まれます。FinBERTなどのLLMはテキストのトーンを得意としますが、「売上が8%未達」のように数値が埋め込まれた文は文脈によって誤分類することがあります。数値の抽出(regex/構造化NLP)とテキスト感情分析を分離してアンサンブルすることで、より精度の高い自然言語アルファシグナルを得られます。