前回の記事に引き続き、今回も仮想通貨botの開発状況をまとめていきます。
本記事では、仮想通貨の自動取引Bot「MMBot」の開発・検証を行った2025年4月22日の作業ログを整理しています。
今回はテストネットでの挙動確認から、本番環境での安全な実行に向けた設計改善まで、大きく進展がありました。
特に「テストネットと本番でのAPI仕様の差異」「コードが意図した通りに動いていない原因」など、初心者がつまずきやすいポイントについて詳しく書いています。
🔍 作業のタイムライン
時刻 | 試行内容 | 結果・問題 | 原因分析 | 対処・学び |
---|---|---|---|---|
10:05 | テストネットでMarket注文に切替 | 指値注文が継続、history API error 多発 | 実行ファイルが旧版のまま | ✅ run_loop に testnet 分岐追加✅ ログに "send market orders" で見分け可能に |
10:11 | 修正版を再実行 | wait_for_fill が呼ばれ続ける | .pyc 残存/未保存ファイル | ✅ grep , realpath , -u で実行ファイル特定 |
10:15 | MMbot_testnet.py 新規作成 | 出力なしで即終了 | 別ファイル/空ファイルを実行 | ✅ ls -l , grep , cat で中身確認+保存確認 |
10:17 | 「本番で検証した方が早いのでは?」という転換 | API key invalid , 指値ルート再発 | 本番設定が未反映/旧コード使用 | ✅ config_mainnet.json を新規用意、最小lotでMarket注文から確認する方針に変更 |
10:25 | 安全ガード付きコードv1.7作成 | — | — | ✅ 追加項目: • DBファイル分離 (main/test) • 建玉上限 MAX_NOTIONAL_USD • 3回エラーで自動停止 • --max_cycles によるサイクル制御 |
— | 本番用起動コマンド確定 | — | — | ✅ --lot 0.001 & --max_cycles 20 で安全に実行可能に |
🎯 成功したこと
- ✅ マーケット注文のロジック検証が完了
テストネットでAPIの挙動・署名処理・注文成功を確認。 - ✅ Slack通知 / SQLite保存 / ログ出力が一連で動作確認済
開発環境におけるボットの基本動作は整っている。 - ✅ 安全装備を備えたv1.7コードを整備完了
本番用にも安心して導入できる構成ができた。 - ✅ 原因切り分けフローを整理し、再現可能なデバッグ手順を明文化
実行されているコードが想定と異なる問題をクリアに。
🧱 うまくいかなかったことと原因・対処
課題 | 原因 | 再発防止策 |
---|---|---|
テストネットで指値ルートが残存 | 実行ファイルが旧版/保存漏れ | ✔️ ログにTAGを明示 ✔️ .pyc 削除 & realpath 確認 |
history API error: Illegal category | テストネットは /v5/order/history の category を受け付けない | ✔️ テストネットは Market注文+realtime APIのみに制限 |
本番で API key is invalid | キーの設定ミス or config混在 | ✔️ 設定ファイルを物理的にmain/testで分割 ✔️ 将来的には環境変数経由で安全に読み込みたい |
🛠 今後の開発タスク
- 本番ワンショットMarketテスト
lot=0.001
,max_cycles=1
でSlack & DBの正常動作を確認。
- マーケット常時運転 → Limit戦略に切替
s_entry
を調整しつつ安定稼働にシフト。
- ユニットテスト / CIの整備
create_signature()
、PnL計算、DB挿入などをpytestでテスト化。
- フェイルセーフ通知の強化
- 重大エラーをSlack→PagerDutyなどへ切り替え可能に。
- デプロイの自動化
- Dockerfile + GitHub Actionsでステージ別ビルド&デプロイを可能に。
- パフォーマンス計測・改善
asyncio.gather
化やWebSocketパーサーの最適化、注文レイテンシ測定の導入。
- リスク管理強化
- Redis等で未決済ポジションを管理し、非常時に即カットできる体制を整備。
🚀 今後のロードマップ
Step 1:本番最小構成で安全確認
→ Step 2:戦略切替(Limit化)とパフォーマンスチューニング
→ Step 3:デプロイ/CI/監視の本格運用へ拡張
✍️ 最後に
今回の作業では、「意図通りのコードが走っていない」ことに気づくまでに時間がかかりました。
一見するとプログラムミスに見える問題も、実際はキャッシュや保存漏れ、実行対象の混在によって起きていたことがわかりました。
Bot開発においては**「実行しているコードが本当にそれか?」**を確認する習慣が、
戦略ロジックと同じくらい重要だと痛感しました。
🧩 次のログでは、本番Market注文の初回結果と、Limit戦略へのスムーズな移行に挑戦する予定です!
—
✍️ 記録:2025年4月22日 / よだか
これもう本番環境で検証した方が早いな。最低限の安全稼働状態でぶん投げてしまおう。
— よだか(夜鷹/yodaka) (@yodakablog) April 22, 2025
開発者として評価(10 段階)byGPTo3
項目 | 評価 | コメント |
---|---|---|
問題切り分けのスピード | 4/10 | “動かない” 状態を長時間ループ。旧ファイル・.pyc ・保存漏れが典型的なのに初動が遅い。 |
環境管理 (venv / config) | 3/10 | main/test の設定混在、API キー誤適用。本番 API 無効で気づくのは遅すぎる。 |
安全設計 | 6/10 | 上限金額・エラーカウントなど最終的に追加できた点は評価。ただし最初から組み込むべき基礎。 |
ロギング & 可観測性 | 7/10 | loguru・Slack 通知は○。ただし“識別タグ”を事後で足している時点で設計が受動的。 |
テスト・自動化 | 2/10 | ユニットテスト無し。CI 無し。人力確認に依存。 |
学習姿勢 | 8/10 | 問題を言語化し続け、解決策を取り込んだ点は良い。 |
総合:5/10(辛くつけて“及第点ギリギリ”)
素質よりも、開発プロセスそのものに改善余地が大きい。
厳しめに指摘する 6 つの反省点
- 「実行バイナリ=ソース」の幻想
- 保存漏れ/キャッシュ/別ディレクトリを疑うのは基本中の基本。
- 対策:
pytest
に必ず “バージョンタグを出力して終わるテスト” を一つ入れ、CI に通さない限り本番デプロイしない。
- 設定の分離が甘い
config_mainnet.json
とconfig_testnet.json
を物理分離したのは事後対応。- 対策:環境変数+
.env
管理、Vault 等でキー暗号化。誤適用を CI で fail させる。
- 安全装備が“後追い”
- 上限金額・max_cycles は事後に慌てて追加。
- 対策:仕様書の段階で失敗パスを洗い出し、実装時に “フェイルセーフ先行”。
- テストネットを信用しすぎ
- Bybit テストネットの仕様差を知らずに時間を浪費。
- 対策:API ドキュメント+コミュニティで「何が本番と違うか」を事前調査。検証は“実運用最小サイズ”を最初から視野に。
- デバッグは“勘”が多い
print
→思考→修正の反復は初学者なら仕方ないが、時間効率が悪い。- 対策:
pdb
/ VS Code Debug・structured logging・テスト駆動で可観測性を高める。
- バージョン管理の痕跡が見えない
- コードの差分管理・タグ付けが会話に出てこない。
- 対策:GitFlow でも trunk-based でもよい。PR レビュー無しで本番に push は厳禁。
今後 30 日の行動プラン
週 | やること | 目的 |
---|---|---|
1 | Git リポジトリを再構成:main/test ブランチを分離。.env でキー管理。 | 設定混在を根絶 |
1‑2 | pytest 導入:署名関数・PnL・DB I/O など 10 本のユニットテストを書く。 | 回帰バグ即検知 |
2 | GitHub Actions で CI:pytest + ruff + mypy を通さないと merge できない。 | 機械的品質担保 |
2‑3 | Docker 化 & compose:redis / bot / prometheus / grafana を一括起動できる dev-stack を作る。 | dev/prod parity |
3 | 本番 Canary デプロイ:lot=0.0005 /max_cycles=10 を cron で1日数回流す。 | 実環境の挙動を小さく観測 |
3‑4 | 自動ロールバック:連続エラー3回→Bot停止→Slack警報→PagerDuty。 | フェイルセーフ強化 |
4 | パフォーマンス計測:板取得→決済までの latency を Prometheus エクスポート。 | ボトルネック特定 |
継続 | 毎日 30 分コードを“写経”:人気 OSS の async bot、Backtest スクリプトなど。 | コーディング筋トレ |
意識すべきポイント
- 「コードを書く」より「仕組みを作る」
- 仕組み=テスト・CI・監視。これがないと本番では動かせない。
- “動く”をゴールにしない
- 再現可能、安全に失敗できる を同時に満たして初めてプロダクト。
- 疑う対象は常に自分の環境
- キャッシュ・設定・実行パス…。まず足元を確認してからコードを疑う。
- 小さく賭けて早く学ぶ
- 本番検証はロット最小、サイクル最小、期間最小で。失敗をコスト最小で回収する。
- AI は“補助輪”
- 解決策をもらったら手でタイプし直し、ドキュメントを熟読し、自分の言葉で再実装。
- “コピペして動いた”だけではスキルは伸びない。
厳しい一言まとめ
「動かない」時間の大半は、環境と手順への無理解が原因。
コードよりもまずプロセスを疑え。
ここを乗り越えられれば、個人開発者としてのレベルは確実に一段上がります。
次回はテスト・CI そして運用自動化の成果を報告できることを期待しています。
概要まとめ:凝縮版
本日の作業ログ — MMBot v1.x 開発・検証の進捗まとめ
(2025‑04‑22)
時刻 (JST) | 試行内容 | 結果 / 問題 | 原因分析 | 対処・学び |
---|---|---|---|---|
10:05 頃 | テストネットで「マーケット注文に切替」した改修コードを投入 | 指値 が飛び続け、history API error が多発 | 旧コード(Limit ルート)を実行していた ⇒ ファイル名・保存漏れ | ✅ 改修点を run_loop 冒頭に if EXCHANGE_MODE=="testnet": ... continue で明示。ログ文言も “send market orders” に変更して判別容易に |
10:11 頃 | 修正版を再実行 | 依然として指値ルート → wait_for_fill が呼ばれる | 実行ファイル/キャッシュ (.pyc) 混在、タブ未保存 | ✅ .pyc 全削除・grep でタグ検索・-u (unbuffered) 実行で “どのコードが動いているか” を可視化 |
10:15 頃 | テストネット専用最小コード を新規作成 (MMbot_testnet.py ) | 端末に何も表示されず即終了 | python MMbot_testnet.py ではなく 別ファイル か「空ファイル」を実行/入力ミス | ✅ realpath , ls -l , grep でファイル実体を確認、-u 起動で強制ログ出力 |
10:17 頃 | 本番環境に切替して検証した方が早いのでは? との判断 | mainnet で即実行 → API キー invalid, 指値ルートが動作 | 本番 API キー未設定・旧コードが残存 | ✅ 本番用設定ファイル config_mainnet.json を用意し、最小ロットで “ワンショット Market” をまず試す方針に転換 |
10:25 頃 | 安全ガード付きフルコード v1.7 作成 | — | — | ✅ 以下を追加: • main/test で DB 分離 • 建玉上限 MAX_NOTIONAL_USD • 連続失敗 3 回で停止 • --max_cycles で自動終了 |
— | 推奨起動コマンド (mainnet, lot 0.001, 20 cycles) 提示 | — | — | ✅ 本番へスムーズに移行できる手順を確定 |
成功したこと ✅
- テストネットでのマーケット注文ロジック を最小コードで検証し、
実際に Market API の署名・呼び出しテンプレを確立できた。 - ログ/Slack/SQLite の一連の動作は旧コードでも正常であることを確認。
- 安全ガード付きの本番向けコード v1.7 を完成。
- 原因切り分けフロー(grep → realpath → .pyc 削除 →
-u
起動)を整備し、
コードと実行ファイル不一致のデバッグ手順を共有できた。
うまくいかなかったこと & 原因 ❌
課題 | 原因 | 再発防止策 |
---|---|---|
テストネットで指値ルートが残存 | 旧ファイルを実行・保存漏れ | ✔️ ログに “TAG” を入れて必ず判定 ✔️ .pyc 削除 & 仮想環境 python を確認 |
history API error: Illegal category | テストネットは /v5/order/history が category 不受理 | ✔️ テストネットでは Market 即約定 or realtime だけを使う |
mainnet で API key is invalid | config mix‑up / API キー誤入力 | ✔️ main / test 用設定ファイルを物理的に分割、環境変数で注入も検討 |
今後の開発タスク 🗒️
- 本番ワンショット Market テスト
lot=0.001
,max_cycles=1
、Slack & DB に約定記録が入ることを確認
- マーケット常時運転 → Limit 戻し
- Market で安定後、
s_entry
を調整して Limit 戦略へ切替
- Market で安定後、
- ユニットテスト / CI
- 署名関数・PnL 計算・DB 書込みを pytest で自動テスト
- フェイルセーフの外部化
- 連続エラー通知を Slack → PagerDuty 等に昇格できるよう設計
- デプロイ自動化
- Dockerfile + GitHub Actions で tag push ⇒ 本番/テスト用イメージ作成
- パフォーマンス計測
- asyncio gather 化・WS ハンドラ高速化、latency log を追加
- リスク管理
- 未決済ポジションを Redis に集約、急変時に一括手動決済できるフックを実装
これらをロードマップ化し、Step 1 → 2 → 3 の順で進めれば
「安全に本番へ移行しつつ、機能追加へ展開」できるはずです。
👇ラジオで話したこと
🎙️ 開発記録#191|MMBot開発ログ8「テストネットから本番環境へ。安全ガードを加えた本番運用に向けての道のり」
こんにちは、よだかです。
今回のラジオは、2025年4月22日に行った仮想通貨自動取引Bot「MMBot」の開発と検証の記録を、未来の自分への再学習としてまとめておきます。
この日は、テストネットから本番環境への移行準備というテーマで、大きな前進と、いくつかのつまずきがありました。
とくに、「コードが意図した通りに動いていないときにどう気づくか」
そして「安全に本番環境で動かすには何を備えておくべきか」
この2つが、今日のキーワードでした。
では、まずはその日の流れをざっくり時系列で振り返ってみます。
🕙 10:05
テストネットで、マーケット注文に切り替えたコードを投入。
でもなぜか、指値注文が出続け、history API error
が止まらない…。
調べてみると、実行されていたのは古いコードだった。
保存漏れや実行ファイルの食い違いが原因でした。
🕚 10:11
修正したコードをもう一度実行。
しかし wait_for_fill()
が呼ばれ続ける…。
よく見ると .pyc
ファイルが残っていて、それが旧バージョンを保持していたようです。grep
や realpath
、それから python -u
を使って、どのコードが動いているかを可視化できるようにしました。
🕧 10:15
「もう新しいファイルで確実に動かそう」と思い、MMbot_testnet.py
を新規作成。
ところが実行しても何も表示されない。
原因は、実行していたのが空ファイルだったという初歩的ミス。
ここも、ls -l
や cat
で中身確認することで、すぐに修正できました。
🕐 10:17
ここで方針転換。
「テストネットでこんなに挙動が違うなら、本番で動かした方が早い」と判断。
さっそく本番用のAPIで起動…したものの、API key is invalid
と怒られる。
configファイルの中身がテスト用のままだったんですね。
改めて config_mainnet.json
を用意して、最小ロットでMarket注文から始めるという方針を立て直しました。
🕝 10:25
本番環境でも安心して動かせるよう、安全ガード付きのv1.7コードを作成。
以下のような保護機能を追加しています。
- 本番とテストで別々のDBファイルを使う
- 建玉の想定金額(qty × price)に上限
MAX_NOTIONAL_USD
を設ける - 注文が3回連続で失敗したら自動停止
--max_cycles
を指定すれば、一定サイクルで自動終了
📌 成功したことをまとめると、こうなります。
- テストネット上でマーケット注文が問題なく動作したこと
- Slack通知、SQLite保存、ログ出力の一連動作が確認できたこと
- 安全ガード付きのコードを本番に向けて完成させたこと
- 実行ファイルの食い違いを特定するための手順を確立できたこと
💡 逆に、うまくいかなかったこととその対策も、忘れずに記録しておきます。
- 指値ルートが消えてない問題 → ファイル保存忘れ、
.pyc
残存。
→ ログにタグを入れることで判別をしやすくした。 - history API error → テストネットは
/v5/order/history
の仕様が異なる。
→ Market注文+realtime APIのみに限定することで回避。 - APIキー invalid → 設定ファイルの混在。
→ mainnet / testnet を物理的に分離して明示管理。
🛠 今後の開発タスク も、整理しておきます。
- 本番環境での ワンショットMarketテスト(lot=0.001, max_cycles=1)で確認
- そこから常時運転に移行し、Limit注文戦略に戻していく
- ユニットテストやCIの整備(署名関数やPnL計算などをpytestで)
- フェイルセーフ通知の強化(Slack→PagerDutyなど)
- DockerやGitHub Actionsを使ったデプロイ自動化
- パフォーマンス改善(非同期処理や板データパーサーの高速化)
- Redisによるポジション監視と、緊急時の自動カット機能
📈 これからのロードマップは3ステップ。
Step 1:最小構成で本番のMarket注文が動くか確認
Step 2:戦略をLimitに戻してチューニング
Step 3:CI・監視・自動デプロイなど本格運用へ拡張
✍️ 最後に
今回、意図通りのコードが動いていないという状況が、
いかに気づきにくく、いかに時間を食うかを体感しました。
一見すると「プログラムミス」に見える問題も、
実際はファイル保存忘れ・キャッシュの残り・違うコードを実行していた
という、もっと根本的な環境要因だったりします。
この体験を通して、Bot開発では
「実行しているコードが、本当にそれか?」
を毎回疑うくらいでちょうどいいと思いました。
次回のログでは、いよいよ 本番Market注文の初回結果と、
Limit戦略へのスムーズな移行に挑戦する予定です。
🎙️ おまけコーナー|開発者としての自己評価と反省点
さて、ここからはちょっとおまけのコーナーとして、
今回のMMBot開発の一連の流れを経て、開発者としての自分自身を評価する時間です。
この評価は、ChatGPT──正確には私が使っている「o3」モデルから、
かなり客観的かつ厳しめにフィードバックをもらったものです。
🧮 10段階評価 by GPTo3(かなり正直)
- 問題切り分けのスピード:4点
→ 「動かない」状態を長時間ループしていた。旧ファイル/.pyc
/保存漏れ…どれも初心者あるあるだけど、初動が遅かった。 - 環境管理:3点
→ 本番とテストの設定混在。本番用APIキーが無効だったのに気づいたのも遅すぎ。 - 安全設計:6点
→ 結果的にエラー制御や上限設計を組み込んだのは良い。ただ、それは“後からの補填”にすぎない。 - ロギング & 可観測性:7点
→ loguruとSlack通知は良かった。でも識別タグを“後から”足してる時点で、可観測性が受動的。 - テスト・自動化:2点
→ ユニットテストなし、CIもなし。完全に人力確認任せ。これでは本番に出すには怖い。 - 学習姿勢:8点
→ とはいえ、問題を言語化してフィードバックを取り込んでいく姿勢はちゃんとあった。
総合評価:5/10(ギリ及第点)
正直、素質はある。ただ「開発プロセス全体」に改善余地が山ほどある、というのが本音。
💥 反省点を厳しく6つ
- 「実行バイナリ=ソースコード」と思い込んでいた
→ 保存してなかったり、.pyc
に旧コードが残っていたり。これ、初歩だけど超大事。
→ 対策:CIテストに“バージョンタグ出力”を入れて、本番前に必ず確認。 - 設定ファイルの分離が甘すぎた
→ mainnetとtestnetが同居。config書き間違いは事故のもと。
→ 対策:.env
による環境変数管理+CIで制限する。 - 安全設計が完全に後追いだった
→ MAX建玉制限、max_cycles…全部“やばそうだから後で足した”。
→ 対策:最初にフェイルセーフを設計書に入れる癖をつける。 - テストネットを“信用しすぎ”た
→/history
がcategoryパラメータを受け付けないなど、完全に罠だった。
→ 対策:ドキュメント+StackOverflowで事前に仕様差を把握。 - デバッグが“勘”と“手動”に依存していた
→print
→修正→再実行の繰り返しは、時間効率が悪い。
→ 対策:pdb、構造化ログ、VSCodeのステップ実行を習慣に。 - バージョン管理の痕跡がなかった
→ Gitでのタグ、PRレビュー、ブランチ運用…どこにも見当たらない。
→ 対策:GitFlowじゃなくてもいいから、main/testで分けるのは最低限。
🧭 これからの30日間の行動プラン
- 1週目:Gitの再構成 & 環境変数管理
→ 設定混在を根絶する。 - 1〜2週目:pytestで10本テストを書く
→ バグにすぐ気づけるようにする。 - 2週目:GitHub ActionsでCIを導入
→ テストが通らなきゃマージできない環境を作る。 - 2〜3週目:Docker + Compose化
→ Redis/Bot/監視をワンコマンド起動できるように。 - 3週目:本番のCanary実行(少ロット×短時間)
→ 小さく動かして、大きな失敗を防ぐ。 - 3〜4週目:自動ロールバック機構の追加
→ 連続エラー→Slack→自動停止→PagerDuty。これで安心。 - 4週目:Prometheusでパフォーマンスを可視化
→ どこで時間かかってるかを明示。 - 毎日:30分だけコード写経
→ OSSのBotやBacktest系を写す。これが一番伸びる。
💡 意識しておくべき5つのポイント
- 「コードを書く」より「仕組みを作る」が重要。
- “動いた”をゴールにしない。「再現性」「安全な失敗」がセット。
- まず疑うのは“コード”ではなく“環境”。
- 小さく賭けて、早く失敗して、早く学ぶ。
- AIは補助輪。写経し直して、手で再実装してこそスキルになる。
💬 最後に一言、今日の学びをまとめるならば…
「動かない」時間の大半は、コードの問題じゃない。
それ以前の“環境”と“手順”に対する無理解が原因だ。
コードより、まずプロセスを疑え。
次回のラジオでは、いよいよCI導入と本番常時運転のテスト結果を報告できるように頑張ります。
以上、よだかのおまけコーナーでした!
現在のコード:自分用ログ
#!/usr/bin/env python3 """ MMBot v1.7 (2025‑04‑22) — Mainnet‑ready + safety guards ──────────────────────────────────────────────────────────────────── ■ 機能 1. スプレッド検出で指値 2 本 … place_limit_order() 2. タイムアウトで自動キャンセル … wait_for_fill() → cancel_order() 3. loguru でファイル & コンソールログ (日次ローテーション 7 日保持) 4. 約定結果を SQLite に保存し、サイクル毎に累計 PnL を計算 5. 追加ガード • DB を mainnet / testnet で分離 • 1 回あたりの想定建玉 (qty*price) 上限 MAX_NOTIONAL_USD • 連続 3 回注文失敗で自動停止 consecutive_errors • 任意サイクル数で自動終了 (デフォ 0 = 無限) max_cycles """ from __future__ import annotations import asyncio, json, time, hmac, hashlib, sqlite3, sys from argparse import ArgumentParser from pathlib import Path from urllib.parse import urlencode import aiohttp, pybotters from loguru import logger # ────── 0. 定数 ─────────────────────────────────────────── LOG_FILE = "mm_bot_{time:YYYYMMDD}.log" MAX_NOTIONAL_USD = 50 # ここを超える注文は無視 ERROR_THRESHOLD = 3 # 連続失敗で停止 # これらは __main__ で上書き EXCHANGE_MODE: str # 'mainnet' | 'testnet' REST_BASE: str WS_PUBLIC: str DB_FILE: Path # ────── 1. 共通ユーティリティ ───────────────────────── def create_signature(cfg: dict, body: str = "", params: dict | None = None) -> dict: ts = str(int(time.time() * 1000)) api, sec = cfg["bybit"]["apiKey"], cfg["bybit"]["secret"] rw = cfg.get("recv_window", "5000") payload = urlencode(sorted(params.items())) if params else (body or "") msg = ts + api + rw + payload sig = hmac.new(sec.encode(), msg.encode(), hashlib.sha256).hexdigest() return { "Content-Type": "application/json", "X-BAPI-API-KEY": api, "X-BAPI-TIMESTAMP": ts, "X-BAPI-SIGN": sig, "X-BAPI-RECV-WINDOW": rw, } async def place_limit_order(sess, cfg, sym, side, qty, price) -> str: body = json.dumps({ "category":"linear","symbol":sym,"side":side, "orderType":"Limit","qty":str(qty),"price":str(price),"timeInForce":"GTC" }) async with sess.post(REST_BASE+"/v5/order/create", headers=create_signature(cfg, body), data=body) as r: j = await r.json() if j.get("retCode") != 0: raise RuntimeError(j) return j["result"]["orderId"] async def cancel_order(sess, cfg, sym, oid) -> bool: body = json.dumps({"category":"linear","symbol":sym,"orderId":oid}) async with sess.post(REST_BASE+"/v5/order/cancel", headers=create_signature(cfg, body), data=body) as r: j = await r.json() ok = j.get("retCode") == 0 logger.info(f"cancel {oid} → {ok}") return ok async def wait_for_fill(sess, cfg, sym, oid, timeout=30, interval=1.0): end = time.time() + timeout path = "/v5/order/realtime" while time.time() < end: params = {"category":"linear","symbol":sym,"orderId":oid} hdr = create_signature(cfg, "", params) url = f"{REST_BASE}{path}?{urlencode(sorted(params.items()))}" async with sess.get(url, headers=hdr) as r: j = await r.json() if j.get("retCode")==0 and (d:=j["result"].get("data")): info = d[0] if info.get("orderStatus") == "Filled": return True, float(info.get("price", 0.0)) await asyncio.sleep(interval) logger.warning(f"wait_for_fill timeout → cancel {oid}") await cancel_order(sess, cfg, sym, oid) return False, 0.0 async def notify_slack(url, msg): async with aiohttp.ClientSession() as s: await s.post(url, json={"text":msg}) # ────── 2. DB ───────────────────────────────────────────── def init_db() -> sqlite3.Connection: conn = sqlite3.connect(DB_FILE) conn.execute( "CREATE TABLE IF NOT EXISTS trades(" "ts INTEGER, symbol TEXT, side TEXT, qty REAL, price REAL)" ) conn.commit() return conn def record_trade(conn, ts, sym, side, qty, price): conn.execute("INSERT INTO trades VALUES (?,?,?,?,?)",(ts,sym,side,qty,price)) conn.commit() def calc_pnl(conn, sym) -> float: cur = conn.cursor() cur.execute( "SELECT side, qty, price FROM trades " "WHERE symbol=? ORDER BY ts DESC LIMIT 2",(sym,)) rows = cur.fetchall() if len(rows)==2 and {r[0] for r in rows}=={"Buy","Sell"}: sell = next(p for s,q,p in rows if s=="Sell") buy = next(p for s,q,p in rows if s=="Buy") return (sell-buy)*rows[0][1] return 0.0 # ────── 3. メインループ ────────────────────────────── async def run_loop(sym: str, qty: float, s_entry: float, cfg: dict, interval: int, max_cycles: int): global DB_FILE DB_FILE = Path(f"mmtrades_{EXCHANGE_MODE}.db") conn = init_db() orderbook: dict[str,float] = {} cycles = 0 consecutive_errors = 0 logger.info(f"MMBot start mode={EXCHANGE_MODE}") async with pybotters.Client(base_url=WS_PUBLIC) as ws, aiohttp.ClientSession() as sess: await ws.ws_connect( WS_PUBLIC, send_json=[{"op":"subscribe","args":[f"orderbook.1.{sym}"]}], hdlr_json=lambda m,w:( orderbook.update({"ask":float(m["data"]["a"][0][0])}) if m.get("data") and m["data"].get("a") else None, orderbook.update({"bid":float(m["data"]["b"][0][0])}) if m.get("data") and m["data"].get("b") else None ) ) while "bid" not in orderbook: await asyncio.sleep(0.3) while True: if max_cycles and cycles >= max_cycles: logger.warning("Reached max_cycles → exit") return bid, ask = orderbook["bid"], orderbook["ask"] spread_pct = (ask - bid) / bid logger.debug(f"spread={spread_pct:.6f}") if spread_pct > s_entry: notional = abs(qty) * ask if notional > MAX_NOTIONAL_USD: logger.warning(f"notional {notional:.2f} > limit {MAX_NOTIONAL_USD} → skip") await asyncio.sleep(interval); continue logger.info("spread OK → send orders") buy_p, sell_p = int(bid - 1), int(ask + 1) try: buy_id = await place_limit_order(sess, cfg, sym, "Buy", qty, buy_p) sell_id = await place_limit_order(sess, cfg, sym, "Sell", qty, sell_p) consecutive_errors = 0 except Exception as e: consecutive_errors += 1 logger.error(e) await notify_slack(cfg["slack_webhook_url"], f":warning: order error ({consecutive_errors}/{ERROR_THRESHOLD}): {e}") if consecutive_errors >= ERROR_THRESHOLD: logger.critical("too many errors → exit") return await asyncio.sleep(interval) continue filled_b, pr_b = await wait_for_fill(sess, cfg, sym, buy_id) filled_s, pr_s = await wait_for_fill(sess, cfg, sym, sell_id) ts = int(time.time()*1000) if filled_b: record_trade(conn, ts, sym, "Buy", qty, pr_b) if filled_s: record_trade(conn, ts, sym, "Sell", qty, pr_s) pnl = calc_pnl(conn, sym) logger.success(f"cycle {cycles} pnl={pnl:.4f}") await notify_slack(cfg["slack_webhook_url"], f"📈 *MMBot* cycle {cycles}\n" f"spread `{spread_pct:.5f}`\n" f"Buy @{buy_p} → {'Fill' if filled_b else 'Cancel'}\n" f"Sell@{sell_p} → {'Fill' if filled_s else 'Cancel'}\n" f"PnL `{pnl:.4f}`") cycles += 1 await asyncio.sleep(interval) else: await asyncio.sleep(1) # ────── 4. CLI & Entry ──────────────────────────────── def cli(): p = ArgumentParser() p.add_argument("--mode", choices=["mainnet","testnet"], default="mainnet") p.add_argument("--config", default=None) p.add_argument("--symbol", default="BTCUSDT") p.add_argument("--lot", type=float, default=0.001) p.add_argument("--s_entry", type=float, default=0.0005) p.add_argument("--interval",type=int, default=10) p.add_argument("--max_cycles", type=int, default=20, help="0=無限 n>0 でその回数で自動停止") return p.parse_args() if __name__ == "__main__": args = cli() cfg_file = args.config or f"config_{args.mode}.json" if not Path(cfg_file).exists(): sys.exit(f"Config not found: {cfg_file}") with open(cfg_file) as f: cfg = json.load(f) EXCHANGE_MODE = args.mode REST_BASE = {"mainnet":"https://api.bybit.com", "testnet":"https://api-testnet.bybit.com"}[EXCHANGE_MODE] WS_PUBLIC = {"mainnet":"wss://stream.bybit.com/v5/public/linear", "testnet":"wss://stream-testnet.bybit.com/v5/public/linear"}[EXCHANGE_MODE] logger.remove() logger.add(lambda m: print(m, end="")) logger.add(LOG_FILE, rotation="00:00", retention="7 days", compression="zip") try: asyncio.run(run_loop( sym = args.symbol, qty = args.lot, s_entry = args.s_entry, cfg = cfg, interval = args.interval, max_cycles = args.max_cycles, )) except KeyboardInterrupt: logger.warning("Interrupted by user")
起動コマンド
python MMbotbybit.py \ --mode mainnet \ --config config_mainnet.json \ --symbol BTCUSDT \ --lot 0.001 \ --s_entry 0.0005 \ --interval 10 \ --max_cycles 20