そろそろ来るかも？投資信託の“兆し”を分析して自動ポストする仕組みを作った話

# 1．はじめに
どうも、趣味でデータ分析している猫背なエンジニアです。

今回は、学生時代からやりたかった「X(旧Twitter)にテクニカル分析したチャートを自動投稿してくれる」システムを開発したいと思い、数か月温めまくって一段落したので記録として投稿したいと思います。
精度高めて一攫千金なんかできたら、おいしいご飯とかLet's Noteの最新機種買いたいです（笑）

# 2．テクニカル分析とは？
テクニカル分析とは、過去の株価や出来高などのチャートパターンを分析し、投資判断を助けるテクニカル指標です。
代表的なもので、ローソク足,移動平均線,ゴールデンクロス／デッドクロス などです。詳細は[Wiki](https://ja.wikipedia.org/wiki/%E3%83%86%E3%82%AF%E3%83%8B%E3%82%AB%E3%83%AB%E6%8C%87%E6%A8%99%E4%B8%80%E8%A6%A7)頼みで👏

本記事では、チャートにゴールデンクロスとデッドクロス、移動平均線を重ねて可視化したものを紹介します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3528290/05d95ba5-8f6b-4c3c-bf9b-e0001fad2f2c.png)

# 3．処理概要
全部書くと見飽きるので、重要部分だけ記録します。

### 1. データ収集
まずは `yfinance` を使って、対象銘柄の過去データを取得します。

```python
import yfinance as yf

ticker = "1489.T"  # 例：日経平均高配当株50指数連動型ETF
dataset = ticker.history(period="max", interval="1d")
```

### 2. テクニカル分析（移動平均線とクロス判定）
テクニカル分析に関しては、ライブラリは使っていません。
単純移動平均の手法をとっていますが、これを設計したときは何も考えていませんでした（笑）
ですが、インデックス投資で予測する場合は急な価格変動があまりなく、過去データに均等に重みを置くため、これでいいんじゃないかなと思います。（個人的意見）

```python
# 移動平均
dataset["Short_MA"] = dataset['Close'].rolling(window=5).mean()
dataset["Medium_MA"] = dataset['Close'].rolling(window=25).mean()
dataset["Long_MA"] = dataset['Close'].rolling(window=75).mean()

# ゴールデンクロスとデッドクロスの判定
dataset["Golden_Cross"] = (dataset["Short_MA"] > dataset["Long_MA"]) & (dataset["Short_MA"].shift(1) <= dataset["Long_MA"].shift(1))
dataset["Golden_Dead_Cross"] = (dataset["Short_MA"] < dataset["Long_MA"]) & (dataset["Short_MA"].shift(1) >= dataset["Long_MA"].shift(1))
```

### 3. Prophet
こちらに関しては、おまけで設計しています。
以前、簡単なチャート予測に[facebook社（現Meta）が開発したモデル](https://facebook.github.io/prophet/docs/quick_start.html)を使用したことがあったので、ほぼデフォルト設定で実装しました。今後はこの部分をいろんなモデルに変えて比較しながら予測精度を高めていけたらなと思います。
```python
# Prophet用にカラム名を変更（Date → ds, Close → y）
df = dataset.rename(columns={'Date': 'ds', 'Close': 'y'})

# dfに予測の上限値(cap), 下限値(floor)カラムを追加
df["cap"] = 4000
df["floor"] = 1000

# Prophetモデルを作成（線形成長、年間・週間の季節性あり）
model = Prophet(growth="linear", yearly_seasonality=True, weekly_seasonality=True)

# 過去のデータで学習
model.fit(df)

# 未来365日分の予測用データフレームを作成
future = model.make_future_dataframe(periods=365)

# 未来データにもcapとfloorを設定
future["cap"] = 4000
future["floor"] = 1000

# 予測実行
forecast = model.predict(future)
```

### 4．過去のチャートの可視化
可視化の一つ目に全体のチャート表示をするプログラムを組んでいます。
長期的に見たときに右肩上がりなのか下がっているのかっていうのが見れると、今後の予測もしやすいので。
```python
fig, ax1 = plt.subplots(figsize=(15, 7))

# 株価の終値（Close）をプロット
ax1.plot(dataset["Date"], dataset["Close"], label=stock_dict[stock_code])

# 各移動平均線（短期・中期・長期）をプロット
ax1.plot(dataset["Date"], dataset["Short_MA"], label="Short MA")    # 短期移動平均線
ax1.plot(dataset["Date"], dataset["Medium_MA"], label="Medium MA")  # 中期移動平均線
ax1.plot(dataset["Date"], dataset["Long_MA"], label="Long MA")      # 長期移動平均線

# タイトル・ラベルなどを設定
ax1.set_title(f"{stock_code} - All Historical Data")  # グラフタイトル
ax1.set_ylabel("Close Price")                         # Y軸ラベル（終値）
ax1.legend()                                          # 凡例の表示
ax1.grid(True)                                        # グリッドを表示

# 画像として保存
plt.savefig('all_historical_data.png')
```
### 5．売買シグナルの可視化
ここでは直近1年分のチャートにテクニカル分析を重ねて、売買シグナルのタイミングがわかるようにしています。
```python
fig, ax2 = plt.subplots(figsize=(15, 7))

# 終値と移動平均線をプロット（過去365日分）
ax2.plot(dataset_last_365["Date"], dataset_last_365["Close"], label=stock_dict[stock_code])
ax2.plot(dataset_last_365["Date"], dataset_last_365["Short_MA"], label="Short MA")
ax2.plot(dataset_last_365["Date"], dataset_last_365["Medium_MA"], label="Medium MA")
ax2.plot(dataset_last_365["Date"], dataset_last_365["Long_MA"], label="Long MA")

# ゴールデンクロス（赤い縦線）を表示
for index, row in dataset_last_365[dataset_last_365["Golden_Cross"]].iterrows():
    ax2.axvline(row["Date"], color="red", linestyle="--", alpha=0.7)

# デッドクロス（青い縦線）を表示
for index, row in dataset_last_365[dataset_last_365["Golden_Dead_Cross"]].iterrows():
    ax2.axvline(row["Date"], color="blue", linestyle="--", alpha=0.7)

# タイトル・ラベルなどを設定
ax2.set_title(f"{stock_code} - Past 1-Year Data")  # タイトル
ax2.set_ylabel("Close Price")                     # Y軸ラベル
ax2.legend()                                      # 凡例
ax2.grid(True)                                    # グリッド表示

# 画像として保存
plt.savefig('past_one_year_data.png')
```

### 6．prophet予測結果の可視化
最後にProphetの予測結果を表示するプログラムを書いています。
```python
# 過去365日分の元データ（実績）
dataset_last_365 = dataset.tail(365)

# グラフ描画の準備（サイズ指定）
fig, ax3 = plt.subplots(figsize=(15, 7))

# 実績データを青線で描画
ax3.plot(dataset["Date"], dataset["Close"], label="Historical Data", color='blue')

# 予測中央値（yhat）をオレンジ線で描画（直近365日分）
ax3.plot(forecast["ds"].tail(365), forecast["yhat"].tail(365), label="Forecast", color='orange')

# 予測の上下限（信頼区間）を薄いオレンジで塗りつぶし
ax3.fill_between(forecast["ds"].tail(365), forecast["yhat_lower"].tail(365), forecast["yhat_upper"].tail(365), color='orange', alpha=0.3)

# Y軸ラベルとグラフタイトルを設定
ax3.set_ylabel("Close Price")
ax3.set_title(f"{stock_code} - 365-Day Forecast with Prophet")

# 凡例とグリッドを表示
ax3.legend()
ax3.grid(True)

# 画像として保存
plt.savefig('stock_forecast_prophet.png')
```

### 7. Twitterに投稿
ここでは言うまでもなくTwitter(X)に自動投稿する部分を書いています。
詳細は私が以前書いた[こちら](https://qiita.com/Necoze/items/d39ea4cf45fcb1a9e9b3)を見ていただきたいです！
```Python
# 環境変数からTwitter APIの認証情報を取得（.envファイルに記載）
consumer_key = os.getenv("CONSUMER_KEY")
consumer_secret = os.getenv("CONSUMER_KEY_SECRET")
bearer_token = os.getenv("BEARER_TOKEN")
access_token = os.getenv("ACCESS_TOKEN")
access_token_secret = os.getenv("ACCESS_TOKEN_SECRET")

# いずれかのキーが取得できていない場合はエラーを出して終了
if None in (consumer_key, consumer_secret, access_token, access_token_secret):
    raise ValueError("環境変数が正しく設定されていません。'.env' ファイルを確認してください。")

# tweepy の Client インスタンスを作成
client = tweepy.Client(
    bearer_token=bearer_token,
    consumer_key=consumer_key,
    consumer_secret=consumer_secret,
    access_token=access_token,
    access_token_secret=access_token_secret
)

# API 認証
auth = tweepy.OAuth1UserHandler(consumer_key, consumer_secret, access_token, access_token_secret)
api = tweepy.API(auth)

# アップロード対象の画像ファイルをリスト化
image_paths = ["all_historical_data.png", "past_one_year_data.png", "stock_forecast_prophet.png"]

# 各画像をアップロードして、media_id を取得
media_ids = [api.media_upload(image).media_id_string for image in image_paths]

# 現在の日付を取得し、表示形式を整える
dt_now = datetime.datetime.now()
date_str = dt_now.strftime('%Y年%m月%d日')

# 投稿するテキストのフォーマット
tweet_text = (
    f"Post Day : {date_str}\n"
    f"This Post is KK Chart Prediction.\n"
    f"※ Trial Operation Now\n"
    f"#Python #KK_Adam"
)

# Twitterへ画像付きポストを投稿
client.create_tweet(text=tweet_text, media_ids=media_ids)
```

### 8. タスクスケジューラの設定
タスクスケジューラの設定についても私の以前の[記事](https://qiita.com/Necoze/items/cddd33ae273dd3edbf4a)があるので、そちらを参考にしてみてください。
定期実行のタイミングは、平日18:30に設定しています。

# 3．いざ、ぶん回し！
いやあ、すばらしい…自己満ですがずっとやりたかったんですよね。
売買シグナルに関しては、急落前に青ライン（デッドクロス）が入っています。個人的には「安い時に多く買う」タイミングの前触れだと思っていて、これは良い兆しです！
Prophetの予測に関しては…信頼区間が広く出てしまい、まだ予測精度に課題があります。今後はモデルを比較しながらチューニングしていきたいと思います。
[最新の自動投稿はこちら](https://x.com/SupportBot_01)


https://x.com/SupportBot_01/status/1933456834444353769


# 4．おわりに
チャート予測はまだ完璧とは言えませんが、骨組みとしては完成しました。一攫千金なんてもんは遠い話になりそうですが、「兆し」っていう部分ではわかるようになってきたのではないでしょうか。
ぜひソースコードを参考にして、自分なりに改良してみてください👍

## 参考文献

https://ja.wikipedia.org/wiki/%E3%83%86%E3%82%AF%E3%83%8B%E3%82%AB%E3%83%AB%E6%8C%87%E6%A8%99%E4%B8%80%E8%A6%A7

https://ranaroussi.github.io/yfinance/

https://facebook.github.io/prophet/docs/quick_start.html

https://qiita.com/Necoze/items/d39ea4cf45fcb1a9e9b3