【Python×Discord】ラズパイトレーダー×Discord!定期レポート&スラッシュコマンド遠隔操作で手元から完全制御する方法
こんにちは!
我が家では、Raspberry Pi(ラズパイ)を使ってGMOコインのFX自動売買プログラムを24時間フル稼働させています。
以前は稼働状況や収支の定期レポートを 旧Twitter(現X) に自動投稿させてスマホからチェックしていたのですが、APIの仕様変更や制限・有料化の波に押され、動かなくなってしまいました……。
そこで今回、通知基盤を Discord(ディスコード) へ完全移行しました!
単に移行しただけでなく、Discordの「スラッシュコマンド(/)」を使った双方向コントロール(稼働状況の確認、自動売買のON/OFF切り替え、ラズパイの再起動など)まで実装したところ、「あれ、これTwitter時代より圧倒的に便利じゃないか……!?」という状態になりました。
今回は、実際の稼働画面やシステムアーキテクチャの工夫、Discord側の設定手順、そしてPythonによる実装コードの抜粋を分かりやすくご紹介します!
1. 実際の稼働画面(こんな感じで届きます!)
まずは実際に運用しているDiscordの画面をご覧ください。
① 毎時の定期レポート(Webhook配信)
毎時0分になると、現在の総資産や評価損益、証拠金維持率、稼働日数がリッチなEmbed形式で自動投稿されます。
② スラッシュコマンド(/)で手元から操作
Discordの入力欄で / を打つと、登録したコマンド一覧がサジェスト表示されます。
③ 取引・発注履歴の確認(/trades)
コマンドを実行すると、Botが即座に応答して直近の発注・約定結果を表示してくれます。
2. なぜDiscordへの移行が正解だったのか?
XからDiscordに移行して実感した4つの大きなメリットです。
- 完全無料・API制限なし: 自作BotやWebhookを制限を気にせず24時間いつでも気兼ねなく常時稼働できます。
- 自分専用のプライベート空間: 残高や詳細な約定履歴など、公開SNSでは流しにくい生データを安全に流せます。
- リッチな埋め込み(Embed): 成功(緑)・失敗(赤)・約定(金)など色分けやアイコンで圧倒的に見やすいUIが作れます。
- スラッシュコマンドで双方向操作: スマホから
/trade state: OFFと打つだけで、外出先からでも即座に売買を停止できます。
3. 落ちない・安全なシステムアーキテクチャ
自動売買のような「実弾のお金が動くシステム」で最も恐ろしいのは、「Discord側の通信エラーやBotのクラッシュに巻き込まれて取引本体が止まること」です。
そのため、今回はマルチプロセスによる疎結合(ファイル共有)アーキテクチャを採用しました。
【ラズパイ内のプロセス構成】
[自動売買本体: Trade.py] (常駐)
│ 5秒ごとのループで最新状況をアトミックにJSON書き出し
▼
./LogData/bot_status.json (状態データ)
./LogData/trade_control.json (制御フラグ)
▲
├─ [定期レポート: discord_report.py] (毎時0分にsystemdタイマー起動)
│ Webhook経由で最新状況を1回POSTして即終了 (超軽量)
│
└─ [双方向Bot: discord_bot.py] (常駐)
・スラッシュコマンド受信(/status, /trade, /service, /reboot)
・15秒ごとの新着ポーリングで発注完了・実約定を即時通知
設計のポイント:
- 本体とDiscord側を完全分離:取引本体(
Trade.py)とDiscord Bot(discord_bot.py)は別プロセス。仮にDiscord Botがエラーで落ちても、取引ループはビクともしません。 - 2つの通知経路を使い分け:
- Webhook(送信専用):毎時の定期レポートや週次サマリー用。Python標準ライブラリ(
urllib.request)のみで動き、実行したら即終了する軽量設計。 - Bot(双方向通信):スラッシュコマンド応答や発注・約定の即時プッシュ通知用(
discord.pyを使用)。
- Webhook(送信専用):毎時の定期レポートや週次サマリー用。Python標準ライブラリ(
4. Discord側の事前準備・設定手順
DiscordでBotとWebhookを使うための準備手順です。数分で簡単に設定できます!
- Step 1: Discordサーバー&チャンネルの作成
Discordで自分専用サーバーを作成し、通知を受信するテキストチャンネル(例:#fx-report)を用意します。 - Step 2: Webhook URLの作成(定期通知用)
チャンネル設定(歯車)→「連携」→「ウェブフック」→「新しいウェブフック」を作成し、「ウェブフックURLをコピー」します。 - Step 3: Discord Developer PortalでBotを作成
Discord Developer Portal で「New Application」を作成し、「Bot」タブから Bot Token を取得します。 - Step 4: Botをサーバーへ招待する
「OAuth2」→「OAuth2 URL Generator」でbotとapplications.commandsにチェックを入れ、必要な権限(Send Messages, Embed Links等)を選んで招待URLを発行・参加させます。 - Step 5: チャンネルIDの取得
ユーザー設定の「詳細設定」で「開発者モード」をONにし、通知先チャンネルを右クリックして「チャンネルIDをコピー」します。
今回のように
.env ファイルや環境変数として外部に逃がし、.gitignore で除外するのが鉄則です。
5. 実装コードの抜粋とポイント
実際にラズパイ上で動かしているPythonコードのコア部分を抜粋してご紹介します。
① Webhookを使った定期レポート送信 (discord_report.py)
外部ライブラリを一切使わず、Python標準ライブラリ(urllib.request)だけで送信できるようにしています。
import json
import urllib.request
import os
# Webhook URLは環境変数から取得
WEBHOOK_URL = os.environ.get("DISCORD_WEBHOOK_URL")
def send_discord_report(status_data):
"""口座状態をEmbed形式でDiscordに送信する"""
if not WEBHOOK_URL:
print("Webhook URLが設定されていません")
return
# リッチな埋め込み(Embed)カードを作成
embed = {
"title": "📊 FX定期稼働レポート",
"color": 0x2ecc71, # 鮮やかなグリーン
"fields": [
{
"name": "総資産 / 評価損益",
"value": f"**{status_data['total_balance']:,}円** (損益: `{status_data['profit']:+,.0f}円`)",
"inline": False
},
{
"name": "証拠金 / 維持率",
"value": f"拘束: `{status_data['used_margin']:,}円` | 維持率: **{status_data['margin_rate']:.1f}%**",
"inline": False
},
{
"name": "建玉数 / スワップ累計",
"value": f"建玉: **{status_data['total_trades']}件** | スワップ: `{status_data['total_swap']:+,.0f}円`",
"inline": False
}
],
"footer": {
"text": f"最終更新: {status_data['updated_at']}"
}
}
payload = {"embeds": [embed]}
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
WEBHOOK_URL,
data=data,
headers={
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0 (compatible; FX-Trader-Bot/1.0)",
},
method="POST"
)
try:
with urllib.request.urlopen(req, timeout=15) as resp:
print(f"レポート送信成功: HTTP {resp.status}")
except Exception as e:
print(f"レポート送信エラー: {e}")
② スラッシュコマンド対応&即時通知Bot (discord_bot.py)
こちらは discord.py(ver 2.x)を使った本格的な常駐Botです。スマホから /status や /trade で操作でき、15秒ごとのループで約定を即時検知します。
import os
import json
import asyncio
import subprocess
import discord
from discord import app_commands
from discord.ext import tasks
BOT_TOKEN = os.environ.get("DISCORD_BOT_TOKEN")
CHANNEL_ID = int(os.environ.get("DISCORD_CHANNEL_ID", "0"))
STATUS_FILE = "./LogData/bot_status.json"
CONTROL_FILE = "./LogData/trade_control.json"
class FXBotClient(discord.Client):
def __init__(self):
intents = discord.Intents.default()
super().__init__(intents=intents)
self.tree = app_commands.CommandTree(self)
self.last_exec_time = ""
client = FXBotClient()
def load_status():
"""取引本体が書き出したJSONを読み込む"""
try:
with open(STATUS_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return None
# スラッシュコマンド: /status (口座状況確認)
@client.tree.command(name="status", description="現在の口座残高と稼働状況を確認します")
async def status_command(interaction: discord.Interaction):
status = load_status()
if not status:
await interaction.response.send_message("⚠️ 状態ファイルを取得できませんでした。")
return
embed = discord.Embed(title="📊 口座・稼働ステータス", color=discord.Color.green())
embed.add_field(name="総資産", value=f"**{status.get('total_balance', 0):,}円**", inline=True)
embed.add_field(name="証拠金維持率", value=f"**{status.get('margin_rate', 0):.1f}%**", inline=True)
embed.add_field(name="保有建玉", value=f"{status.get('total_trades', 0)}件", inline=True)
embed.set_footer(text=f"更新日時: {status.get('updated_at', '-')}")
await interaction.response.send_message(embed=embed)
# スラッシュコマンド: /trade (自動売買ON/OFF切り替え)
@client.tree.command(name="trade", description="自動売買の有効/無効を切り替えます")
@app_commands.choices(state=[
app_commands.Choice(name="ON (自動売買有効)", value="ON"),
app_commands.Choice(name="OFF (自動売買停止)", value="OFF")
])
async def trade_command(interaction: discord.Interaction, state: app_commands.Choice[str]):
is_on = (state.value == "ON")
try:
with open(CONTROL_FILE, "w", encoding="utf-8") as f:
json.dump({"trade_on": is_on}, f)
emoji = "✅" if is_on else "⏸️"
await interaction.response.send_message(f"{emoji} 自動売買を **【{state.value}】** に変更しました。")
except Exception as e:
await interaction.response.send_message(f"❌ 変更失敗: {e}")
# スラッシュコマンド: /reboot (ラズパイ再起動)
@client.tree.command(name="reboot", description="ラズパイ本体を再起動します")
async def reboot_command(interaction: discord.Interaction):
await interaction.response.send_message("🔄 **ラズパイを再起動します...**")
async def _do_reboot():
await asyncio.sleep(2)
subprocess.run(["sudo", "/sbin/reboot"])
asyncio.create_task(_do_reboot())
# バックグラウンド監視ループ (約定の即時通知)
@tasks.loop(seconds=15)
async def check_new_executions():
if not CHANNEL_ID:
return
channel = client.get_channel(CHANNEL_ID)
if not channel:
return
status = load_status()
if not status:
return
exec_history = status.get("execution_history", [])
if not exec_history:
return
latest = exec_history[-1]
if latest.get("time", "") > client.last_exec_time:
if client.last_exec_time != "":
pair = latest.get("pair", "")
lot = latest.get("lot_diff", 0)
is_buy = (latest.get("type") == "BuyFill")
embed = discord.Embed(
title=f"🎯 【約定通知】{pair} {'新規買い' if is_buy else '決済売'}",
description=f"数量: `{abs(lot):g}Lot` @ `{latest.get('avg_price', 0):.3f}`\n時刻: {latest.get('time')}",
color=discord.Color.blue() if is_buy else discord.Color.gold()
)
await channel.send(embed=embed)
client.last_exec_time = latest.get("time", "")
@client.event()
async def on_ready():
print(f"ログイン完了: {client.user}")
await client.tree.sync()
if not check_new_executions.is_running():
check_new_executions.start()
if __name__ == "__main__":
client.run(BOT_TOKEN)
6. ラズパイ側の安全な権限設定 (sudoers)
Botからラズパイのサービス再起動やOS再起動(/reboot)を行えるようにする場合、Bot実行ユーザーにパスワードなしで特定のコマンドだけ実行できる権限を与えます。
ラズパイ上で /etc/sudoers.d/fx-trader を作成し、以下のように実行可能なコマンドを厳格に限定します。
# piユーザーに対して特定のsystemctlとrebootのみパスワード不要にする
pi ALL=(ALL) NOPASSWD: /bin/systemctl stop gmocoin-trader.service, /bin/systemctl start gmocoin-trader.service, /bin/systemctl restart gmocoin-trader.service, /bin/systemctl restart gmocoin-trader.service, /bin/systemctl is-active gmocoin-trader.service, /sbin/reboot, /sbin/shutdown
ALL=(ALL) NOPASSWD: ALL)を与えてしまうとセキュリティ上危険ですが、上記のようにフルパスで実行対象を限定することで、安全にDiscord経由のリモート管理が可能になります。
7. 運用してみた感想・まとめ
X(Twitter)からDiscordへ乗り換えて数ヶ月が経ちましたが、「もっと早く移行していればよかった!」というのが率直な感想です。
/trade state: OFF を送るだけで、即座に新規売買をストップできます。
ラズパイで自動売買やスクレイピング、IoT機器の監視などをされている方は、ぜひDiscord連携(Webhook & スラッシュコマンド)を試してみてください!運用が劇的に快適になりますよ!
コメント
0 件のコメント :
コメントを投稿