1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

自動売買システムの「アラートメールが届かない」に泣いた話 ― SendGridで学んだ到達率改善のノウハウ

1
Posted at

はじめに

個人でPythonによる株式デイトレードの自動売買プログラムを開発・運用しています。相場が動いている間は基本的にシステム任せで売買判断をさせているため、「システムエラーで発注が止まった」「想定外の挙動をした」といった異常を即座に人間(自分)に伝える手段が生命線になります。

その通知手段として長らくメールを使っていたのですが、運用を続ける中で「アラートメールが届かない」「気づいたら数時間ノーガード状態だった」というヒヤリとする経験を何度かしました。この記事では、その苦労と、SendGridに乗り換えて改善した話をまとめます。

何に使っているか

自動売買システムから送っているメールはだいたい以下の3種類です。

  • エラー通知: 発注失敗、API接続断、想定外の例外など、即座に気づきたいもの
  • 日次サマリー: 当日の約定・損益・モデルの再学習結果などをまとめたレポート
  • 異常検知アラート: ポジションサイズや損失が閾値を超えた場合の警告

このうち特に困っていたのが「エラー通知」です。エラーが起きているのにメールが届かない、あるいは届いても数十分遅れる、といった状態では通知の意味がありません。

最初にやっていたこと(そして失敗した話)

最初は自宅のメールサーバーではなく、契約しているレンタルサーバーのSMTPをそのまま使って smtplib で送信していました。

import smtplib
from email.mime.text import MIMEText

def send_alert(subject: str, body: str):
    msg = MIMEText(body)
    msg["Subject"] = subject
    msg["From"] = "alert@example.com"
    msg["To"] = "myaddress@example.com"

    with smtplib.SMTP("smtp.example.com", 587) as server:
        server.starttls()
        server.login("alert@example.com", "password")
        server.send_message(msg)

一見動いているように見えるのですが、しばらく運用していると以下のような問題が出てきました。

  • 送信元ドメインにSPF/DKIMの設定をしておらず、受信側(Gmail)で迷惑メール判定されてGmailの迷惑メールフォルダに入ることが頻発
  • レンタルサーバー側の送信制限に引っかかり、短時間にエラーが連発した際(=一番通知が欲しいタイミング)に送信自体が制限されて届かない
  • 送信の成否をログでしか確認できず、「本当に届いたか」を後から検証する手段がなかった

一番怖かったのは2つ目です。システムが異常な挙動をしている時ほどエラーメールを連投したくなるのに、まさにそのタイミングで送信制限に引っかかって通知が止まる、という本末転倒な状態になっていました。

SendGridに移行した理由

上記の問題を踏まえて、メール配信を専業でやっているサービスに任せることにしました。SendGridを選んだ理由は以下です。

  • 送信ドメイン認証(SPF/DKIM/DMARC)の設定手順が整理されており、迷惑メール判定のリスクを下げやすい
  • 送信ログ・到達状況・開封/クリックのイベントがAPIで取得できるため、「本当に届いたか」を後から検証できる
  • 60日間の無料トライアルがあり、個人開発の検証用途にも試しやすい(参考: https://sendgrid.kke.co.jp/)

実装

1. Pythonからの送信部分

smtplib直叩きから、SendGridのAPI経由に変更しました。エラー通知は即時性が命なので、Web APIでの送信にしています。

import os
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail

SENDGRID_API_KEY = os.environ["SENDGRID_API_KEY"]

def send_alert(subject: str, body: str, to_addr: str = "myaddress@example.com"):
    message = Mail(
        from_email="alert@yourdomain.com",
        to_emails=to_addr,
        subject=subject,
        plain_text_content=body,
    )
    sg = SendGridAPIClient(SENDGRID_API_KEY)
    response = sg.send(message)
    return response.status_code

2. 送信失敗時のリトライとフォールバック

「通知そのものが失敗した」場合に気づけないと本末転倒なので、送信失敗時はローカルログにも必ず残し、さらに数回リトライする設計にしました。

import logging
import time

logger = logging.getLogger("trading_alert")

def send_alert_with_retry(subject: str, body: str, max_retries: int = 3):
    for attempt in range(1, max_retries + 1):
        try:
            status = send_alert(subject, body)
            if status in (200, 202):
                return True
            logger.warning(f"send_alert unexpected status={status} attempt={attempt}")
        except Exception as e:
            logger.error(f"send_alert failed attempt={attempt}: {e}")
        time.sleep(2 ** attempt)  # 指数バックオフ

    # 全リトライ失敗時は、最終防衛ラインとしてローカルログに致命的として残す
    logger.critical(f"ALERT DELIVERY FAILED: {subject} / {body}")
    return False

メール送信を「投げっぱなし」にせず、失敗した場合の最終防衛ライン(ローカルログへの記録)を必ず用意しておく、というのが実運用で効いた設計でした。

3. 送信ドメイン認証(SPF/DKIM/DMARC)

Gmail宛の到達率が悪化していた根本原因はここでした。SendGridの管理画面でドメイン認証を行い、DNS側に以下のようなレコードを追加しています。

  • SPF: どのサーバーからの送信を正当とするかをDNSに記載
  • DKIM: SendGrid側で発行される鍵をDNSのCNAMEとして登録し、メールへの電子署名を有効化
  • DMARC: SPF/DKIMの検証結果に基づく振る舞い(隔離/拒否など)をポリシーとして宣言

この3点セットを設定してから、Gmail宛の迷惑メール振り分けはほぼなくなりました。設定の考え方は以下の記事が分かりやすかったです。

▼ステップごとに解説!SPF/DKIM/DMARC設定をチェックする方法
https://sendgrid.kke.co.jp

4. 到達状況をイベントAPIで検証する

「送信した」と「届いた」はイコールではないので、SendGridのEvent Webhookを使って配信結果を記録するようにしました。

from flask import Flask, request

app = Flask(__name__)

@app.route("/sendgrid/webhook", methods=["POST"])
def sendgrid_webhook():
    events = request.get_json()
    for event in events:
        event_type = event.get("event")  # delivered, bounce, dropped, open, etc.
        email = event.get("email")
        logger.info(f"sendgrid event: {event_type} to={email}")

        if event_type in ("bounce", "dropped", "deferred"):
            logger.warning(f"delivery issue: {event_type} to={email} reason={event.get('reason')}")

    return "", 200

これで、bouncedroppedといった配信失敗イベントを検知できるようになり、「アラートメールを送ったつもりが実は届いていなかった」という事態にすぐ気づけるようになりました。日次でdelivered件数と送信件数を突き合わせて、到達率が下がっていないかを簡易チェックしています。

運用してみて学んだこと

  • メール送信そのものを1個の「外部依存コンポーネント」として扱うべきだった。自作サーバーでの直接送信は、SMTPの制限やIPレピュテーションの問題を全部自分で背負い込むことになり、コアロジック(売買判断)とは別のところで足を引っ張られる
  • SPF/DKIM/DMARCの設定は「一度やれば終わり」ではなく、送信ドメインの信頼性を保ち続けるための継続的なメンテナンス項目として捉える必要がある
  • 通知の失敗をどう検知するかまで含めて設計しないと、「通知が来ないこと」に気づけない。特にアラート用途では、送信失敗時のフォールバック(ローカルログ・別チャネルへの通知など)は必須
  • エラー通知のような「バーストしやすい」用途では、送信元の送信制限・レート制限を事前に把握しておくことが重要。まさに一番送りたいタイミングで送信が絞られる、というのは実運用でありがちな落とし穴

今後試したいこと

  • エラー通知はメール一本足打法をやめ、SendGridのEvent Webhookで配信失敗を検知した場合に、別チャネル(Slack Webhookなど)へフォールバックする多重化構成にする
  • 日次サマリーメールの開封率をトラッキングし、本当に見られているかを検証する
  • SendGridのSuppression List(バウンス・スパム報告のあったアドレスの管理)を使って、通知先アドレスの健全性を定期チェックする

まとめ

自作のSMTP直叩きから、SendGridのAPI経由送信・ドメイン認証・Event Webhookによる到達検証まで一通り整えたことで、「アラートメールが届かず異常に気づけない」という個人開発では地味に致命的なリスクをかなり減らせました。通知系の実装は動いているうちは気づきにくい分、いざという時に信頼できるかどうかが重要だと痛感しています。

60日間の無料トライアルもあるので、(https://sendgrid.kke.co.jp/)
自作の通知基盤に不安がある方は一度試してみることをおすすめします。

参考リンク

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?