【ChatGPT開発入門】FANG+の株価因子を調整し、グラフ化するプロジェクト🎵

## はじめに
この記事では、FANG+の特定の銘柄（今回はMETA）の株価因子を調整し、他の銘柄に与える影響を計算・視覚化するプロジェクトについて説明します。2024年1月2日の値に基づいて因子を規格化し、グラフ化して視覚化しました。

## 使用した技術
- Next.js
- React.js
- Chart.js
- SQLAlchemy
- yFinance

## インストール手順

１. **必要なパッケージのインストール**

   Pythonの依存関係をインストールします。

   ```bash
   pip install -r requirements.txt
   ```

   Node.jsの依存関係をインストールします。

   ```bash
   npm install
   ```

２. **データの取得と格納**

   `data_fetcher.py`スクリプトを実行して、データベースにデータを格納します。

   ```bash
   python scripts/data_fetcher.py
   ```

３. **データベースのリバランス**

   `rebalancer.py`スクリプトを実行して、ポートフォリオを再バランスします。

   ```bash
   python scripts/rebalancer.py
   ```

４. **アプリケーションの起動**

   開発サーバーを起動します。

   ```bash
   npm run dev
   ```

## ディレクトリ構成

以下はプロジェクトのディレクトリ構成の一例です。

```
project-directory/
├── backend/
│   ├── app/
│   │   ├── __init__.py
│   │   ├── models.py
│   │   ├── database.py
│   │   ├── crud.py
│   │   ├── main.py
│   │   └── schemas.py
│   └── alembic/
│       ├── versions/
│       ├── env.py
│       ├── script.py.mako
│       └── alembic.ini
├── frontend/
│   ├── components/
│   │   ├── Chart.js
│   │   └── Navbar.js
│   ├── pages/
│   │   ├── api/
│   │   │   ├── index.js
│   │   ├── _app.js
│   │   ├── index.js
│   ├── public/
│   ├── styles/
│   │   └── global.css
│   └── package.json
├── scripts/
│   ├── data_fetcher.py
│   └── rebalancer.py
├── requirements.txt
└── README.md
```

### backend/

- **app/**: APIサーバーの主要なコードを含むディレクトリ。
  - **models.py**: データベースのモデル定義。
  - **database.py**: データベース接続の設定。
  - **crud.py**: データベース操作のための関数。
  - **main.py**: APIのエントリーポイント。
  - **schemas.py**: リクエストおよびレスポンスのスキーマ。
- **alembic/**: データベースのマイグレーション設定。

### frontend/

- **components/**: Reactコンポーネント。
  - **Chart.js**: グラフ描画コンポーネント。
  - **Navbar.js**: ナビゲーションバーコンポーネント。
- **pages/**: Next.jsのページ。
  - **api/**: APIルート。
    - **index.js**: フロントエンドのメインページ。
  - **_app.js**: グローバルな設定。
  - **index.js**: フロントエンドのエントリーポイント。
- **public/**: 公開リソース（画像、フォントなど）。
- **styles/**: CSSファイル。

### scripts/

- **data_fetcher.py**: データを取得してデータベースに格納するスクリプト。
- **rebalancer.py**: ポートフォリオを再バランスするスクリプト。

### その他

- **requirements.txt**: Pythonの依存関係。
以下が`requirements.txt`の内容です。このファイルには、Pythonプロジェクトで使用される依存関係がリストされています。

```plaintext
fastapi==0.68.2
uvicorn==0.15.0
sqlalchemy==1.4.25
pandas==1.3.3
yfinance==0.1.63
alembic==1.7.3
```

これらのライブラリは、バックエンドのAPIサーバーの構築、データベースの操作、データの取得および解析に使用されます。以下は各ライブラリの簡単な説明です：

- **fastapi**: 高速なAPIを構築するためのPythonフレームワーク。
- **uvicorn**: ASGIサーバーの実装。
- **sqlalchemy**: SQLツールキットとオブジェクトリレーショナルマッパー (ORM)。
- **pandas**: データ解析ライブラリ。
- **yfinance**: Yahoo Financeのデータを取得するためのライブラリ。
- **alembic**: SQLAlchemy用のデータベースマイグレーションツール。

この`requirements.txt`ファイルを使用して、以下のコマンドで依存関係をインストールできます。

```bash
pip install -r requirements.txt
```

これでプロジェクトの準備が整います。

- **package.json**: Node.jsの依存関係。
以下が`package.json`の内容です。このファイルには、Node.jsプロジェクトで使用される依存関係がリストされています。

```json
{
  "name": "my-nextjs-project",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "axios": "^0.21.1",
    "chart.js": "^3.5.1",
    "next": "11.1.2",
    "react": "17.0.2",
    "react-dom": "17.0.2",
    "react-chartjs-2": "^3.0.3"
  },
  "devDependencies": {
    "eslint": "7.32.0",
    "eslint-config-next": "11.1.2"
  }
}
```

以下は各依存関係の簡単な説明です：

- **axios**: HTTPクライアントライブラリ。
- **chart.js**: データを視覚化するためのJavaScriptライブラリ。
- **next**: Reactベースのフレームワークで、サーバーサイドレンダリングや静的サイト生成をサポート。
- **react**: Reactライブラリ。
- **react-dom**: ReactとDOMの連携ライブラリ。
- **react-chartjs-2**: React用のChart.jsラッパー。

これらの依存関係をインストールするために、`npm install`コマンドを実行します。

これでプロジェクトの準備が整います。

- **README.md**: プロジェクトの説明。
省略
---

以上がこのプロジェクトのインストール手順とディレクトリ構成の詳細です。

## 1. データの取得と初期設定

まず、FANG+のデータをデータベースに格納します。この部分には、`data_fetcher.py`と`rebalancer.py`の2つのスクリプトを使用します。

### `scripts/data_fetcher.py`
以下のスクリプトは、指定された銘柄のデータを取得し、データベースに格納します。

```python
import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'backend'))

from sqlalchemy.orm import Session
from app import models, database, crud
import yfinance as yf
import pandas as pd

def fetch_and_store_data(db: Session, symbols: str, start: str, end: str):
    symbol_list = symbols.split(',')

    for symbol in symbol_list:
        stock_data = yf.download(symbol, start=start, end=end)
        if stock_data.empty:
            print(f"No data found for symbol: {symbol}")
            continue
        stock_data = stock_data.reset_index()
        stock_data['Date'] = stock_data['Date'].dt.date

        fx_data = yf.download('JPY=X', start=start, end=end)
        fx_data = fx_data.reset_index()
        fx_data['Date'] = fx_data['Date'].dt.date

        merged_data = pd.merge(stock_data, fx_data, on='Date', suffixes=('_stock', '_fx'))

        for _, row in merged_data.iterrows():
            stock_price = models.StockPrice(
                symbol=symbol,
                date=row['Date'],
                close_price=row['Close_stock'],
                fx_rate=row['Close_fx'],
                normalized_close_price_usd=None,
                normalized_close_price_jpy=None
            )
            crud.create_stock_price(db, stock_price)

    print("Data fetched and stored successfully")

if __name__ == "__main__":
    db = database.SessionLocal()
    fetch_and_store_data(db, "META,AMZN,AAPL,NFLX,GOOGL,TSLA,NVDA,MSFT,X,BABA", "2024-01-01", "2025-01-01")
    db.close()
```

### `scripts/rebalancer.py`
次に、ポートフォリオを再バランスして、データベースに規格化された価格を保存します。

```python
import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'backend'))

from sqlalchemy.orm import Session
from app import models, database, crud
import pandas as pd

def rebalance_portfolio(db: Session, target_date: str):
    stock_prices = db.query(models.StockPrice).all()  # 全データを取得
    
    print("Stock Prices Data:")
    for stock_price in stock_prices:
        print(stock_price.symbol, stock_price.date, stock_price.close_price, stock_price.fx_rate)

    df = pd.DataFrame([{
        "symbol": stock_price.symbol,
        "close_price": stock_price.close_price,
        "fx_rate": stock_price.fx_rate,
        "date": stock_price.date
    } for stock_price in stock_prices])
    
    print("DataFrame Columns:", df.columns)

    if not df.empty:
        target_date = pd.to_datetime(target_date)
        df['date'] = pd.to_datetime(df['date'])
        closest_date = df.iloc[(df['date'] - target_date).abs().argsort()[:1]]['date'].values[0]
        print(f"Using closest date: {closest_date}")

        if 'close_price' in df.columns:
            target_value = 14029.12
            base_value_usd = df['close_price'].sum()
            df['normalized_close_price_usd'] = df['close_price'] / base_value_usd * target_value
            df['normalized_close_price_jpy'] = df['normalized_close_price_usd'] * df['fx_rate']
            
            for _, row in df.iterrows():
                stock_price = db.query(models.StockPrice).filter(
                    models.StockPrice.symbol == row['symbol'],
                    models.StockPrice.date == pd.to_datetime(closest_date).date()
                ).first()
                if stock_price:
                    stock_price.normalized_close_price_usd = row['normalized_close_price_usd']
                    stock_price.normalized_close_price_jpy = row['normalized_close_price_jpy']
                    db.add(stock_price)
            db.commit()
        else:
            print("Error: 'close_price' column not found in DataFrame.")
    else:
        print("No data available in DataFrame.")

if __name__ == "__main__":
    db = database.SessionLocal()
    rebalance_portfolio(db, "2025-02-14")
    db.close()
```

### `app/main.py`の解説と起動方法

```main.py
# app/main.py

from fastapi import FastAPI, HTTPException, Depends, Query
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
import yfinance as yf
import pandas as pd
import numpy as np
import logging
from typing import List

from . import models, crud, database

# テーブルの作成を確認
models.Base.metadata.create_all(bind=database.engine)

app = FastAPI()

origins = [
    "http://localhost:3000",  # Next.jsのデフォルトのポート
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# データベースセッションの依存関係
def get_db():
    db = database.SessionLocal()
    try:
        yield db
    finally:
        db.close()

@app.get("/data/")
async def fetch_data(
    symbols: str,
    start: str,
    end: str,
    db: Session = Depends(get_db)
):
    symbol_list = symbols.split(',')
    data = []

    try:
        for symbol in symbol_list:
            stock_data = yf.download(symbol, start=start, end=end)
            if stock_data.empty:
                logger.warning(f"No data found for symbol: {symbol}")
                continue
            stock_data = stock_data.reset_index()
            stock_data['Date'] = stock_data['Date'].dt.date
            
            fx_data = yf.download('JPY=X', start=start, end=end)
            fx_data = fx_data.reset_index()
            fx_data['Date'] = fx_data['Date'].dt.date
            
            merged_data = pd.merge(stock_data, fx_data, on='Date', suffixes=('_stock', '_fx'))

            for _, row in merged_data.iterrows():
                data.append({
                    "Date": row['Date'].isoformat(),
                    "Close_stock": row['Close_stock'],
                    "Close_fx": row['Close_fx']
                })
        
        return data
    except Exception as e:
        logger.error(f"Error fetching data: {e}", exc_info=True)
        raise HTTPException(status_code=500, detail=str(e))
```

#### 解説

`main.py`はFastAPIを使用してAPIサーバーを構築するファイルです。このファイルでは、クロスオリジンリソースシェア (CORS) ミドルウェアの設定、データベースセッションの管理、およびデータ取得のためのエンドポイントが定義されています。

1. **インポート**:
   - 必要なライブラリとモジュールをインポートします。`FastAPI`, `HTTPException`, `Depends`, `CORSMiddleware`, `Session`, `yfinance`, `pandas`, `logging`, `List` などをインポートしています。

2. **データベースの初期設定**:
   - `models.Base.metadata.create_all(bind=database.engine)` を使用して、データベースのテーブルが作成されることを確認します。

3. **アプリケーションの設定**:
   - `FastAPI`インスタンスを作成し、CORSミドルウェアを設定します。これにより、`http://localhost:3000` からのリクエストが許可されます。

4. **データベースセッションの管理**:
   - `get_db`関数を定義し、データベースセッションの依存関係を設定します。セッションが使用された後に確実に閉じるための構造になっています。

5. **データ取得エンドポイント**:
   - `/data/` エンドポイントを定義し、指定されたシンボルの株価データと為替レートデータを取得します。データはYahoo Financeからダウンロードされ、株価データと為替データがマージされます。エラー処理も含まれており、例外が発生した場合には500エラーを返します。

#### 起動方法

1. **必要なパッケージをインストール**:
   ```bash
   pip install fastapi uvicorn sqlalchemy yfinance pandas
   ```

2. **APIサーバーの起動**:
   以下のコマンドでAPIサーバーを起動します。
   ```bash
   uvicorn app.main:app --reload
   ```

3. **エンドポイントへのアクセス**:
   ブラウザまたはHTTPクライアント（例：Postman）を使用して、APIエンドポイントにアクセスします。例えば、以下のURLにアクセスしてデータを取得できます。
   ```
   http://localhost:8000/data/?symbols=META,AMZN,AAPL&start=2024-01-01&end=2025-01-01
   ```

これにより、指定されたシンボルの株価データと為替レートデータがJSON形式で返されます。




## 2. データの取得と初期設定（React）

次に、Reactを使用してデータを取得し、グラフを表示します。

### `api.js`の内容
以下に、必要なAPI関数を示します。`fetchWeightedData`関数は、指定されたシンボルとウェイトを使用してデータを取得します。

```javascript
export async function fetchWeightedData(symbols, weights) {
    const weightedData = {};
    const baseValue = 13120.21;

    for (const symbol of symbols) {
        const res = await fetch(`http://localhost:8000/data?symbols=${symbol}&start=2024-01-01&end=2025-01-01`);
        const data = await res.json();

        data.forEach(item => {
            const date = item.Date;
            const weightedPrice = item.Close_stock * weights[symbol];

            if (!weightedData[date]) {
                weightedData[date] = { totalPrice: 0, fxRate: item.Close_fx };
            }
            weightedData[date].totalPrice += weightedPrice;
        });
    }

    const availableDates = Object.keys(weightedData);

    let baseDate = null;
    let baseTotalPrice = null;
    for (const date of availableDates) {
        const totalPrice = weightedData[date]?.totalPrice;
        if (totalPrice !== undefined && !isNaN(totalPrice)) {
            baseDate = date;
            baseTotalPrice = totalPrice;
            break;
        }
    }

    if (baseDate === null || baseTotalPrice === null || isNaN(baseTotalPrice)) {
        console.error(`Error: No valid base total price found`);
        return { labels: [], totalPrices: [], fxRates: [] };
    }

    const labels = availableDates;
    const totalPrices = labels.map(date => {
        const totalPrice = weightedData[date]?.totalPrice;
        return (totalPrice / baseTotalPrice) * baseValue;
    });
    const fxRates = labels.map(date => weightedData[date]?.fxRate);

    return {
        labels,
        totalPrices,
        fxRates
    };
}
```

### 株価の因子を調整し、因子を求める関数
2024年1月2日の値を基準に、METAの因子を調整して他の因子を求めます。

```javascript
export async function adjustStockPricesAndCalculateFactors(symbols, dates, basePriceOriginal) {
    const initialWeights = symbols.reduce((acc, symbol) => {
        acc[symbol] = 1.0;
        return acc;
    }, {});

    const weightsList = [];
    for (let i = 0; i <= 10; i++) {
        const adjustedWeights = { ...initialWeights };
        adjustedWeights['META'] = 1.0 + (i - 5) * 0.01;

        // 残りのウェイトを全体の合計が1.0になるように調整
        const totalWeight = Object.values(adjustedWeights).reduce((sum, weight) => sum + weight, 0);
        const normalizationFactor = 1.0 / totalWeight;
        Object.keys(adjustedWeights).forEach(symbol => {
            adjustedWeights[symbol] *= normalizationFactor;
        });

        const adjustedData = await fetchWeightedData(symbols, adjustedWeights);

        const basePriceAdjusted = adjustedData.totalPrices[0];
        const adjustmentFactor = basePriceOriginal / basePriceAdjusted;
        const adjustedPrices = adjustedData.totalPrices.map(price => price * adjustmentFactor);

        const factors = symbols.reduce((acc, symbol) => {
            acc[symbol] = adjustedWeights[symbol] * adjustmentFactor;
            return acc;
        }, {});

        weightsList.push({
            weights: factors,
            adjustedPrices,
        });
    }
    return weightsList;
}
```

chart.jsの内容
グラフを描画するためのChart.jsとそのReactラッパーを設定します。

```chart.js
// chart.js
import { Line } from 'react-chartjs-2';
import 'chartjs-adapter-date-fns';
import {
  Chart as ChartJS,
  CategoryScale,
  LinearScale,
  PointElement,
  LineElement,
  TimeScale,
  Title,
  Tooltip,
  Legend,
} from 'chart.js';

ChartJS.register(
  CategoryScale,
  LinearScale,
  PointElement,
  LineElement,
  TimeScale,
  Title,
  Tooltip,
  Legend
);

export function renderChart(chartData) {
  return (
    <Line 
      data={chartData}
      options={{
        scales: {
          y: {
            type: 'linear',
            position: 'left',
          },
          y1: {
            type: 'linear',
            position: 'right',
          },
        },
      }}
    />
  );
}
```

## 3. グラフの表示と因子の出力

### `index.js`の内容
取得したデータを基にグラフを表示し、因子を画面に出力します。

```javascript
import { useEffect, useState } from 'react';
import { fetchWeightedData, adjustStockPricesAndCalculateFactors } from './api';
import { renderChart } from './chart';

export default function Home() {
    const [chartData, setChartData] = useState(null);
    const [loading, setLoading] = useState(true);
    const [factors, setFactors] = useState([]);

    const symbols = ['META', 'AMZN', 'AAPL', 'NFLX', 'GOOGL', 'TSLA', 'NVDA', 'MSFT', 'X', 'BABA'];

    useEffect(() => {
        async function fetchData() {
            try {
                const dates = [
                    '2024-01-01',
                    '2024-02-01',
                    '2024-03-01',
                ];

                const originalWeightsList = symbols.reduce((acc, symbol) => {
                    acc[symbol] = 1.0;
                    return acc;
                }, {});

                const originalData = await fetchWeightedData(symbols, originalWeightsList);

                const endIndex = originalData.labels.indexOf('2024-12-31');
                const adjustmentFactorOriginal = 13120.21 / originalData.totalPrices[endIndex];
                originalData.totalPrices = originalData.totalPrices.map(price => price * adjustmentFactorOriginal);

                const basePriceOriginal = 8484.70;
                originalData.totalPrices = originalData.totalPrices.map(price => price * (basePriceOriginal / originalData.totalPrices[0]));

                const adjustedWeightsList = await adjustStockPricesAndCalculateFactors(symbols, dates, basePriceOriginal);

                const calculatedFactors = adjustedWeightsList.map(item => item.weights);
                setFactors(calculatedFactors);

                const datasets = [
                    {
                        label: 'Original Weighted Close Price (Normalized, USD)',
                        data: originalData.totalPrices,
                        borderColor: 'rgba(0, 0, 0, 1)',
                        fill: false,
                        yAxisID: 'y',
                    },
                    {
                        label: 'FX Rate (JPY/USD)',
                        data: originalData.fxRates,
                        borderColor: 'rgba(255,99,132,1)',
                        fill: false,
                        yAxisID: 'y1',
                    },
                ];

                adjustedWeightsList.forEach((item, index) => {
                    const metaFactor = item.weights['META'];
                    datasets.push({
                        label: `Optimized Weighted Close Price (Normalized, USD) META: ${metaFactor.toFixed(5)}`,
                        data: item.adjustedPrices,
                        borderColor: `rgba(${75 + index * 10}, ${192 - index * 10}, 192, 1)`,
                        fill: false,
                        yAxisID: 'y',
                    });
                });

                const chartData = {
                    labels: originalData.labels,
                    datasets,
                };

                setChartData(chartData);
                setLoading(false);
            } catch (error) {
                console.error('Error fetching data:', error);
                setLoading(false);
            }
        }
        fetchData();
    }, []);

    return (
        <div>
            {loading ? (
                <p>Loading...</p>
            ) : (
                <>
                    <h1>因子の一覧</h1>
                    {factors.map((factorSet, index) => (
                        <div key={index}>
                            <h2>因子セット {index + 1}</h2>
                            <ul>
                                {symbols.map(symbol => (
                                    <li key={symbol}>{symbol}: {factorSet[symbol].toFixed(5)}</li>
                                ))}
                            </ul>
                        </div>
                    ))}
                    {renderChart(chartData)}
                </>
            )}
        </div>
    );
}
```

## まとめ
この記事では、特定の銘柄の因子を調整し、その影響を計算・視覚化する手法を紹介しました。2024年1月2日の値に基づいて因子を規格化し、グラフ化することで、株価の変動を視覚的に理解しやすくしました。

### 苦労したところ

最後に、このプロジェクトで特に苦労した点をいくつか挙げます。

1. **データの取得とマージ**:
   - 各銘柄のデータを取得し、為替データとマージするプロセスでは、データの欠損やダウンロードのエラーに対処する必要がありました。
   - データを適切に正規化し、整合性を保つために多くのデバッグを行いました。

2. **ファイルの分割**:
   - プロジェクトが進むにつれて、ファイルが非常に長くなり、管理が難しくなりました。
   - 可読性とメンテナンス性を向上させるために、`api.js`や`index.js`などのファイルを複数のモジュールに分割しました。

3. **因子の調整**:
   - METAの因子を調整する際、他の銘柄の因子も自動的に再計算されるようにするロジックの実装が複雑でした。
   - これにより、他の銘柄に対する影響を正確に反映させるためのアルゴリズムの設計とデバッグに時間を要しました。

4. **グラフの描画**:
   - Chart.jsとReact Chart.jsを使用してデータを視覚化する際、複数のデータセットと軸の設定を正確に行うのが難しかったです。
   - 特に、異なる軸でデータを表示する際のラベルやスタイルの調整に手間取りました。

これらの課題を解決することで、プロジェクト全体の品質とパフォーマンスを向上させることができました。プロジェクトを通じて、多くの学びがありました。