開発史(8) 構成篇——Rustのエンジン1本と、7つの仕事と、小さなVPS3台

開発史(8) 構成篇——Rustのエンジン1本と、7つの仕事と、小さなVPS3台

前回(開発史7)の末尾で予告したとおり、今回はこのBotの作りを書きます。これまでの記事は成績の話ばかりで、何がどう動いているのかを一度も書いていませんでした。

先に1枚の図にまとめます。

Phantom Hunterの構成。Rustの実行エンジン1本に7つの非同期タスクが同居し、理論値(DryRun)と実弾は同じプログラムで動く。置き場所は1 vCPU・1GBのVPS3台
Phantom Hunterの構成。Rustの実行エンジン1本に7つの非同期タスクが同居し、理論値(DryRun)と実弾は同じプログラムで動く。置き場所は1 vCPU・1GBのVPS3台

以下、図の左から右へ説明します。

目次

入口: gRPCのストリーム1本に、フィルター2枚

Botが見ているのは、Solanaのチェーン上で起きるPump.fun関連のトランザクションです。これをRPCで何度も問い合わせるのではなく、gRPCのストリームで押し出してもらう形にしています。ストリームは1本で、フィルターが2枚かかっています。

  1. 全Pump.fun — 新しい銘柄の発行、買い、売り。成功したトランザクションだけ。
  2. 自分のウォレット — 自分が送ったトランザクションの着地。成功も失敗も両方。

2枚目が、後で出てくる「第2エンジン」の目になります。

ストリームは切れます。切れたら3秒待って張り直すループは最初からありましたが、8月に「切れたことに気づけない」障害が起きました。接続は生きているように見えて、何も流れてこない。これは接続の状態を見ても分かりません。対策は2つで、通信路の生存確認(keepalive)を入れたことと、「45秒イベントが来なかったら切れたとみなして張り直す」見張り番をエンジン側に置いたことです。10月には深夜に短い切断が57回続いた日がありましたが、見張り番が毎回張り直して、エンジン自体は止まりませんでした。

本体: Rustの1プロセスに、7つの仕事

実行エンジンはRustで書いた1つのプログラムです。中では、役目の違う7つの非同期タスクが同時に走っています。

記号仕事中身
A受信ストリームを読んで、エンジン内部のイベントに変換する
B指揮銘柄ごとの板(仮想のSOLとトークンの残高)とホルダー数を更新し、入口の条件を判定し、保有中は毎イベントで出口を判定する
C発注買い・売りのトランザクションを組んで送る。DryRunのときは送らずに記録する
Dブロックハッシュ1秒ごとに最新のブロックハッシュを取り直す(送信に必要)
E残玉そうじ売り損ねて残ったトークンを後から売る。実弾のときだけ動く
F確認送ったトランザクションをRPCで取り直し、実際に受け取った数量と支払ったSOLを確定する
G第2エンジン自分のトランザクションがチェーンに載った瞬間を、ストリームのほうから検知して知らせる

この7つの間は、Rustのチャネル(片方向のメッセージの通り道)でつながっています。

設計で一番大事にしているのは、状態を書き換えられるのはBだけ、という決まりです。「いま買っている最中か」「保有中か」「売っている最中か」という状態と、保有しているポジションの中身は、Bの中にしかありません。他のタスクはBにメッセージを送るだけで、Bの持ち物を直接いじることはできません。これを守ると、同時に走る7つのタスクが互いの足を踏まなくなります。ロックの取り合いもありません。

第2エンジン: 着地を「先に」知る

図のGが、このBotを「ツインエンジン」と呼んでいる理由です。

買いのトランザクションを送ったあと、Botは「本当に載ったのか」「いくらで約定したのか」を知る必要があります。普通にやると、RPCに問い合わせて返事を待つ(Fの仕事)ことになり、これが遅い。4月の計測では、買いの確認に平均2,069ms、売りの確認に2,075msかかっていました。

そこで、ストリームの2枚目のフィルターで自分のウォレットのトランザクションを拾い、チェーンに載った瞬間にBへ「着地した」と知らせる専用のタスクを足しました。これがGです。Gは状態を一切持ちません。拾って、分類して、知らせるだけです。

Gを足したあとの計測では、買いの確認が267ms、売りの確認が159msになりました。Fは廃止せず、Gの後ろで実際の約定値を取り直す役に回っています。先に知って動き、後から正確な数字で帳尻を合わせる、という二段構えです。計測した9回すべてで、Gのほうが先に着いていました。

理論値と実弾は、同じプログラム

これまでの記事で「理論値(DryRun)」と書いてきたものの正体は、このエンジンそのものです。

起動時の設定が1つあり、DryRunのときは、Cが「送る」代わりに「送ったことにして記録する」だけになります。入口の判定も出口の判定も、板の更新も、実弾のときと同じコードが同じ順番で走ります。違うのは最後の1歩だけです。

DryRunの損益は、板の式(定数積の自動マーケットメーカー)で「このとき0.2 SOL買っていたら、売ったときいくら戻るか」を計算した値です。決済のたびに1件ずつJSONで保存され、それを並べたのが前回までの累積カーブです。

実弾のときは、Cが本当に署名して送ります。送り先はRPCで、売りには逃げ道が3段あります。まずPump.funの板に直接、だめならDFlow、それもだめならJupiterです。ボンディングカーブを卒業した銘柄は板が変わるので、この順番が要ります。

同じプログラムで動くことの効用は、「DryRunで良かったのに実弾で違う動きをした」という疑いを、コードの差ではなく、開発史(4)で書いた三つの溝(固定費・執行・地合い)に絞れることです。

通知は、UDPで1行投げるだけ

エンジンはDiscordを直接叩きません。決済や入場のたびに、ローカルのUDPポートへJSONを1行投げるだけです。

受け取るのはPythonの監視役で、こちらが文面を整え、損益グラフなどを描いて、Discordに送ります。エンジンの側に「通知が送れなかったら」という分岐を持たせたくなかったので、こうなっています。Discordが落ちても、グラフの描画で何かが壊れても、エンジンの売買には影響しません。実際、10月末に監視役のグラフ表示を直したときも、監視役だけを14秒止めて差し替え、エンジンは触っていません。

置き場所: 1 vCPU・1GBのVPSが3台

これだけの仕事をしているエンジンですが、置き場所は小さなVPSです。3台とも1 vCPU・1GB RAMで、重い計算(過去の記録の再生、検定)はMacBook側でやります。

役割は分けています。

  • 記録機: 同じエンジンをDryRunで回し続ける係。鍵を持ちません。前回の1,251本は、この1台が36日間止まらずに書いた帳簿です。
  • 実弾機: 同じバイナリをLiveで武装する係。鍵はこの1台にしかありません。今は止めています。
  • 予備・分析: 過去のストリーム記録の再生と検定。

「記録機と実弾機で同じバイナリが動いていること」は、ファイルのハッシュを突き合わせて確かめています。片方だけに手が入ると、理論値と実弾の比較が意味を失うからです。

壊れたところの記録

この構成は、一度にできたものではありません。壊れた順に書きます。

  • 売りが2.8秒止まる(7月): 売りの手前に別の処理が割り込んでいた。順番を入れ替えて根絶し、売りの送信までが0.04〜0.07秒になった。
  • 「切れたことに気づけない」ストリーム(8月): 上で書いたとおり。keepaliveと見張り番で対処。
  • メモリ不足でビルドが止まる(1GBのVPSの宿命): 動かすのは平気だが、コンパイルの山で足りなくなる。ビルドのときだけ一時的にスワップを足し、終わったら外す運用に。
  • 監視役が別のプロセスとポートを取り合って止まる(7月): 通知が途絶えたが、エンジンは無事だった。分離しておいてよかった例。

まだ直していないものもあります。入口の手前の数値の上限が広すぎて、本来通すべき銘柄の一部を門前で弾いている疑いが10月から残っています。影響の大きさは測定中で、直すときは理論値の記録に段差が入るので、日付を書き添えて直します。

なぜこの作りなのか

速さのためだけではありません。試す場所と、動き続ける場所を分けるためです。

入口や出口の条件を変える実験は、過去の記録の再生と理論値の記録でやります。実弾機のバイナリは、検証を通ったものしか置きません。状態を書けるのはBだけ、通知はエンジンの外、鍵は1台だけ、同じバイナリをハッシュで確認——どれも「思いつきの変更が本番の売買を壊さない」ための柵です。

開発史(1)で「アーキテクチャは速度のためでなく、規律のためにある」と書きました。この図は、その1行を具体にしたものです。

まとめ

  • 入口はgRPCのストリーム1本・フィルター2枚。切れたことに気づく見張り番つき。
  • 本体はRustの1プロセスに7つの非同期タスク。状態を書けるのは指揮役の1つだけ。
  • 第2エンジンが自分の取引の着地を先に知らせ、確認が2秒から0.2〜0.3秒になった。
  • 理論値と実弾は同じプログラム。違うのは「送るか、記録するか」の最後の1歩だけ。
  • 通知はUDPで1行投げて、Pythonの監視役がDiscordへ。エンジンは外に依存しない。
  • 置き場所は1 vCPU・1GBのVPS3台。記録機・実弾機・予備で役割を分け、同じバイナリをハッシュで確認している。

次は、実弾機を動かすときに書きます。


本記事の内容は2026年4月〜11月の当方の記録に基づきます。数字は当方の環境での計測値です。鍵の置き方・接続先・入口の条件の数値は非公開です。本記事は投資助言ではありません。暗号資産の自動売買には元本を失うリスクがあります。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

目次