「DEXで手数料0%のまま24時間自動売買しながらSeason3 XPも稼ぐ方法を、実装コードで解説します」

実は私自身、ParadexでXP蓄積を目的としたファームボットを数ヶ月間稼働させてきました。DEXにありがちな面倒な認証や独特のAPIレイヤーに最初はハマったが、一度動き出せばCEXより安定している印象です。Season3が継続中の今、ボットで24時間稼働させることで手動では到底集めきれないXPが自然と積み上がっていく。

この記事ではParadexボットをゼロから作る手順を、実際に動くPythonコードで解説します。Claude Codeを使えば実装速度が格段に上がるので、その活用法も合わせて紹介します。

⚠️ 注意・免責事項
本記事は自動売買ボットの実装手順を解説するものであり、投資を勧誘するものではありません。仮想通貨取引は元本割れのリスクがあります。ボットの誤作動・バグ・API障害による損失について筆者は責任を負いません。少額から検証し、リスク管理を徹底してください。Paradexはデリバティブ取引所であり、日本居住者はご自身の判断と責任でご利用ください。

Paradexの基本的な使い方をまだ知らない方は先にこちらを読んでおくと実装がスムーズになります。
Paradexの使い方・登録方法【2026年版】

📝 この記事でわかること
・paradex_pyライブラリを使ったAPIへの接続方法
・L2ウォレットキーを使った安全な認証の実装
・EMAゴールデンクロスに基づくエントリー・エグジットロジック
・Season3 XPを効率的に積み上げる取引頻度の設定
・本番環境でも使えるリスク遮断器の実装パターン
・実際に遭遇したエラーと対処法

Paradexボットを作る前に知っておくべきこと

ボット運用のメリット:手数料0%・XP自動蓄積・24時間稼働

Paradexでボットを動かす最大のメリットはMaker手数料が0%であることだ(2026年7月時点)。指値注文でポジションを建てる限りコストがかからないため、高頻度な売買でも手数料がPnLを圧迫しません。CEXのボットと比べてコスト構造が根本的に違います。

加えてSeason3が進行中の現在、取引ボリュームに応じてXPが付与されます。手動取引ではどうしても夜間や休日が手薄になるが、ボットは文字通り24時間稼働します。その差が数週間で大きなXP差になり、エアドロップ報酬に直結します。

前提知識と必要なもの

実装に入る前に以下を揃えておく必要があります。

  • Paradexアカウント:MetaMaskなどのEVMウォレットで登録済みであること
  • USDC証拠金:最低でも50 USDC程度。テスト段階は小額で
  • Python 3.10以上:バージョン依存のエラーを避けるため3.10以上を推奨
  • 基本的なPython知識:async/awaitの概念が分かると実装しやすい
⚠️ L1キーは絶対に使いません
Paradexには「L1ウォレットキー(MetaMaskの秘密鍵)」と「L2エージェントキー」の2種類があります。ボット実装ではL2エージェントキーのみを使います。L1キーはオンチェーン資産すべてのコントロールを持つため、コード・.envファイル・GitHubに書かれた時点で全財産喪失のリスクがあります。絶対に使わないこと。

STEP1: 開発環境の構築とParadex APIキー発行

Pythonバージョンの確認とvenv作成

# バージョン確認
python --version  # Python 3.10.x 以上であることを確認

# プロジェクトディレクトリを作成
mkdir paradex-bot
cd paradex-bot

# 仮想環境を作成・有効化
python -m venv venv
# Windowsの場合
venv\Scripts\activate
# Mac/Linuxの場合
source venv/bin/activate

paradex_pyのインストール

Paradex公式のPythonクライアントライブラリを使います。これにより認証の複雑な処理が抽象化され、APIアクセスが簡潔に書けます。

pip install paradex_py python-dotenv

L2エージェントキーの発行手順

ParadexのダッシュボードからAPI設定を開きます。「Generate API Key」でL2エージェントキーのペア(公開鍵・秘密鍵)が生成されます。この画面を閉じると秘密鍵は二度と表示されないので、必ずコピーして保管すること。

環境変数の設定(.envファイル)

秘密鍵をコードに直書きするのは厳禁です。.envファイルに分離し、.gitignoreに追加します。

# .envファイルの内容
PARADEX_ACCOUNT_ADDRESS=0xYOUR_ACCOUNT_ADDRESS
PARADEX_L2_PRIVATE_KEY=0xYOUR_L2_PRIVATE_KEY
PARADEX_NETWORK=prod  # または testnet
# .gitignoreに追加(絶対に忘れない)
echo ".env" >> .gitignore

STEP2: Paradex APIへの接続と認証テスト

接続サンプルコード

以下が最小限の接続テストコードです。実際に私がParadexファームボット実装時に最初に確認したのがこのパターンに近い。

import asyncio
import os
from dotenv import load_dotenv
from paradex_py import Paradex
from paradex_py.environment import Environment

load_dotenv()

async def test_connection():
    # 環境設定 (prod または testnet)
    env = Environment.PROD if os.getenv("PARADEX_NETWORK") == "prod" else Environment.TESTNET

    # Paradexクライアントの初期化
    # L1キーは渡さない - L2キーのみ使用
    pdx = Paradex(
        env=env,
        l2_private_key=os.getenv("PARADEX_L2_PRIVATE_KEY"),
    )

    # JWT認証トークンを取得
    await pdx.account.get_jwt_token()
    print("[OK] 認証成功")

    # アカウント情報を取得
    account_info = await pdx.account.fetch_account_summary()
    print(f"[OK] アカウント取得")
    print(f"  総残高: {account_info.get('account_value', 'N/A')} USDC")
    print(f"  フリーマージン: {account_info.get('free_collateral', 'N/A')} USDC")

    # マーケット一覧を取得
    markets = await pdx.markets.fetch_markets()
    print(f"[OK] マーケット数: {len(markets)}件")

    return pdx

if __name__ == "__main__":
    asyncio.run(test_connection())

ポジションと残高の確認コード

async def check_positions(pdx):
    # 現在のオープンポジションを確認する
    positions = await pdx.positions.fetch_positions()

    if not positions:
        print("[INFO] オープンポジションなし")
        return

    for pos in positions:
        market = pos.get("market", "")
        side = pos.get("side", "")
        size = pos.get("size", 0)
        entry_price = pos.get("avg_entry_price", 0)
        unrealized_pnl = pos.get("unrealized_pnl", 0)

        print(f"  {market}: {side} {size} @ {entry_price} | 含み損益: {unrealized_pnl}")
💡 Claude Codeへの指示例

「Paradex APIを使って、ETH-USD-PERPのEMA(14)とEMA(28)のゴールデンクロス時にLongエントリー、デッドクロス時にShortエントリーするボットを作って。ポジションサイズは証拠金の10%、SLは2%で設定して。.envからL2キーを読み込んで、既存ポジションがある場合は新規エントリーしないようにして」

STEP3: 基本的なオーダー戦略の実装

EMAベースの方向性判断ロジック

Paradexボットで方向性判断を行う際の一例として、EMAクロスを使った実装を紹介します。以下のコードは実際に動くものだが、パラメータはバックテストで調整すること。

from collections import deque
from typing import Optional

class EMACalculator:
    # 指数移動平均を計算するクラス

    def __init__(self, fast_period: int = 14, slow_period: int = 28):
        self.fast_period = fast_period
        self.slow_period = slow_period
        self.prices = deque(maxlen=slow_period + 10)
        self.fast_ema: Optional[float] = None
        self.slow_ema: Optional[float] = None

    def _calc_ema(self, prices: list, period: int) -> float:
        # EMA計算(初回はSMAで初期化)
        if len(prices) < period:
            return sum(prices) / len(prices)

        k = 2.0 / (period + 1)
        ema = sum(prices[:period]) / period  # 初期値をSMAで設定

        for price in prices[period:]:
            ema = price * k + ema * (1 - k)

        return ema

    def update(self, price: float) -> dict:
        # 新しい価格でEMAを更新し、シグナルを返す
        self.prices.append(price)
        prices_list = list(self.prices)

        prev_fast = self.fast_ema
        prev_slow = self.slow_ema

        if len(prices_list) >= self.slow_period:
            self.fast_ema = self._calc_ema(prices_list, self.fast_period)
            self.slow_ema = self._calc_ema(prices_list, self.slow_period)

        signal = "HOLD"
        if prev_fast is not None and prev_slow is not None:
            if prev_fast <= prev_slow and self.fast_ema > self.slow_ema:
                signal = "BUY"   # ゴールデンクロス
            elif prev_fast >= prev_slow and self.fast_ema < self.slow_ema:
                signal = "SELL"  # デッドクロス

        return {
            "signal": signal,
            "fast_ema": self.fast_ema,
            "slow_ema": self.slow_ema,
            "price": price,
        }

指値注文の送信とポジション管理

async def place_limit_order(
    pdx,
    market: str,
    side: str,
    size: float,
    price: float,
) -> Optional[str]:
    # 指値注文を送信してオーダーIDを返す

    try:
        order = await pdx.orders.create_order(
            market=market,
            side=side,
            type="LIMIT",
            size=str(size),
            price=str(round(price, 2)),
            time_in_force="GTC",  # Good Till Cancel
        )

        order_id = order.get("id")
        print(f"[ORDER] {side} {size} {market} @ {price} | ID: {order_id}")

        return order_id

    except Exception as e:
        print(f"[ERROR] 注文送信失敗: {e}")
        return None


async def calculate_position_size(pdx, risk_pct: float = 0.10) -> float:
    # 証拠金に対するポジションサイズを計算する
    account = await pdx.account.fetch_account_summary()
    free_collateral = float(account.get("free_collateral", 0))
    position_size = free_collateral * risk_pct
    return max(10.0, round(position_size, 1))  # 最低10 USDC

ポジションのクローズ確認パターン

実運用で痛感したのが、クローズ注文を送っただけでポジションが消えたと思い込む事故です。私の実装では「送信→確認→確認失敗時リトライ」のパターンを必ず使っています。

import asyncio

async def close_position_with_verify(
    pdx,
    market: str,
    max_retries: int = 3,
) -> bool:
    # ポジションをクローズし、消滅を確認する

    for attempt in range(max_retries):
        positions = await pdx.positions.fetch_positions()
        pos = next((p for p in positions if p.get("market") == market), None)

        if pos is None:
            print(f"[OK] {market} ポジションなし(クローズ済みまたは未保有)")
            return True

        close_side = "SELL" if pos["side"] == "LONG" else "BUY"
        size = abs(float(pos["size"]))
        current_price = float(pos.get("mark_price", 0))

        # 成行に近い指値でクローズ
        close_price = current_price * 0.995 if close_side == "SELL" else current_price * 1.005

        await pdx.orders.create_order(
            market=market,
            side=close_side,
            type="LIMIT",
            size=str(size),
            price=str(round(close_price, 2)),
            time_in_force="IOC",  # Immediate or Cancel
            reduce_only=True,
        )

        # 3秒待ってポジション消滅を確認
        await asyncio.sleep(3)
        positions_after = await pdx.positions.fetch_positions()
        still_open = any(p.get("market") == market for p in positions_after)

        if not still_open:
            print(f"[OK] {market} クローズ確認完了")
            return True

        print(f"[WARN] クローズ確認失敗 ({attempt + 1}/{max_retries})、リトライ中...")

    print(f"[ERROR] {market} クローズ失敗、手動対応が必要")
    return False

STEP4: XP最大化のための最適化

取引ボリュームとXPの関係

Season3のXP計算は「取引ボリューム(USD建て)」がベースになっています。つまり、ポジションサイズが大きいほど・取引回数が多いほどXPが増えます。ただし証拠金残高に見合わないレバレッジは清算リスクを高めるため、ボリュームを稼ぐための無謀なポジション拡大は避けます。

✅ XP効率を上げる実践ポイント
・Maker注文(指値)を使う:手数料0%でボリュームだけが積み上がります
・スプレッドが狭いペアを選ぶ:ETH-USD-PERP、BTC-USD-PERPが安定
・紹介コードで5%XPボーナスを確実に受け取る(登録時のみ適用)
・ポジションを短い周期でロールオーバーすることでボリュームを積み上げます

効率的な取引頻度の設定

import time
from datetime import datetime

class BotScheduler:
    # 取引頻度を管理するスケジューラー

    def __init__(self, scan_interval_sec: int = 60):
        self.scan_interval = scan_interval_sec
        self.last_scan = 0

    def should_scan(self) -> bool:
        # スキャンのタイミングかどうかを返す
        now = time.time()
        if now - self.last_scan >= self.scan_interval:
            self.last_scan = now
            return True
        return False

    def log(self, msg: str):
        ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        print(f"[{ts}] {msg}")

紹介コードによる5%XPボーナスの活用

Paradexには紹介制度があり、紹介リンク経由で登録した場合に5%のXPボーナスが付与されます。ボットを複数ウォレットで運用する場合、新しいウォレットを登録する際は必ず紹介リンクを経由すること。後から適用することはできません。

STEP5: リスク管理と遮断器の実装

最大ロスリミットと遮断器

ファームボットをParadexで稼働させた際、一番設計に気を使ったのがここです。ボットは止まらないから、損失も止めなければ永遠に続きます。

class RiskCircuitBreaker:
    # ボット全体のリスク遮断器。
    # 設計不変条件: 遮断器は実際に起動されるランナーに配線すること。

    def __init__(
        self,
        max_daily_loss_usd: float = 50.0,
        max_consecutive_losses: int = 3,
        max_position_usd: float = 200.0,
    ):
        self.max_daily_loss = max_daily_loss_usd
        self.max_consecutive_losses = max_consecutive_losses
        self.max_position = max_position_usd

        self.daily_pnl = 0.0
        self.consecutive_losses = 0
        self.is_halted = False
        self.halt_reason = ""

    def record_trade(self, pnl: float):
        # 取引結果を記録し、必要なら遮断する
        self.daily_pnl += pnl

        if pnl < 0:
            self.consecutive_losses += 1
        else:
            self.consecutive_losses = 0

        if abs(self.daily_pnl) >= self.max_daily_loss and self.daily_pnl < 0:
            self._halt(f"日次損失上限 {self.max_daily_loss} USD 到達 (現在: {self.daily_pnl:.2f})")

        if self.consecutive_losses >= self.max_consecutive_losses:
            self._halt(f"連続損失 {self.consecutive_losses} 回到達")

    def check_position_size(self, size_usd: float) -> bool:
        # ポジションサイズが上限内かチェックする
        if size_usd > self.max_position:
            print(f"[RISK] ポジションサイズ超過: {size_usd:.2f} > {self.max_position:.2f} USD")
            return False
        return True

    def _halt(self, reason: str):
        self.is_halted = True
        self.halt_reason = reason
        print(f"[CIRCUIT BREAKER] 遮断発動: {reason}")

    def reset_daily(self):
        # 毎日0時にリセット(デイリーカウンタのみ)
        self.daily_pnl = 0.0
        # 連続損失とhalted状態は手動リセット

ループ保護とエラーハンドリング

while True の中でscan()がtry/except外にあると、未捕捉例外でプロセスが全停止します。実運用ボットでは必ずループ全体をtry/exceptで包む。

async def main_loop(pdx, risk: RiskCircuitBreaker):
    # メインループ(ループ保護付き)

    ema_calc = EMACalculator(fast_period=14, slow_period=28)
    scheduler = BotScheduler(scan_interval_sec=60)
    market = "ETH-USD-PERP"
    open_order_id = None

    print("[START] Paradexボット起動")

    while True:
        try:
            # 遮断器チェック
            if risk.is_halted:
                print(f"[HALT] 遮断中: {risk.halt_reason}")
                await asyncio.sleep(60)
                continue

            if not scheduler.should_scan():
                await asyncio.sleep(1)
                continue

            # 価格取得
            ticker = await pdx.markets.fetch_ticker(market)
            price = float(ticker.get("mark_price", 0))

            if price <= 0:
                scheduler.log("[WARN] 価格取得失敗")
                continue

            # EMAシグナル計算
            signal_data = ema_calc.update(price)
            signal = signal_data["signal"]

            scheduler.log(
                f"価格: {price:.2f} | "
                f"EMA14: {signal_data['fast_ema']:.2f if signal_data['fast_ema'] else 'N/A'} | "
                f"シグナル: {signal}"
            )

            # エントリーシグナル処理
            if signal in ("BUY", "SELL") and open_order_id is None:
                size_usd = await calculate_position_size(pdx, risk_pct=0.10)

                if risk.check_position_size(size_usd):
                    size = size_usd / price
                    entry_price = price * 0.999 if signal == "BUY" else price * 1.001

                    open_order_id = await place_limit_order(
                        pdx, market, signal, round(size, 4), round(entry_price, 2)
                    )

        except KeyboardInterrupt:
            print("[STOP] 手動停止")
            break
        except Exception as e:
            # 予期しないエラーはログを出してループを継続(プロセスを落とさない)
            print(f"[ERROR] 予期しないエラー: {type(e).__name__}: {e}")
            await asyncio.sleep(10)
📝 メモ:Discord通知を追加する場合
ボットの状態をDiscordに通知する場合は、urllib.requestを使って送信する際に必ずUser-Agent: DiscordBot (crypto-beat-bot, 1.0)ヘッダーを付けること。これがないとDiscord側で一定確率でブロックされます。また通知関数が実際にsend()を呼んでいるか、スタブ化されていないかを必ず確認すること。

動作確認とトラブルシューティング

実際に遭遇したエラーと対処法

Paradexボットの実装時に実際にハマったエラーをまとめた。

エラー 原因 対処法
401 Unauthorized JWTトークンの期限切れ(約1時間) get_jwt_token()を定期的に再呼び出し(45分ごと推奨)
InvalidSignature L2キーのフォーマット不正(0x有無) 0xプレフィックスを付けた形式で渡す
InsufficientMargin 注文サイズが証拠金に対して大きすぎる free_collateralを確認してサイズを減らします
OrderNotFound 約定済みまたはキャンセル済みの注文をキャンセルしようとしました キャンセル前にオーダーステータスを確認します
注文が通らない(無音) WebSocket接続が切断されています REST APIで直接確認、接続を再初期化します

JWTトークン自動更新の実装

import time

class TokenManager:
    # JWTトークンを自動更新するマネージャー

    def __init__(self, pdx, refresh_interval: int = 2700):  # 45分
        self.pdx = pdx
        self.refresh_interval = refresh_interval
        self.last_refresh = 0

    async def ensure_valid_token(self):
        # 必要な場合にトークンを更新する
        now = time.time()
        if now - self.last_refresh >= self.refresh_interval:
            await self.pdx.account.get_jwt_token()
            self.last_refresh = now
            print("[TOKEN] JWTトークン更新完了")

よくある質問

Q: ボットを動かすにはどれくらいの資金が必要ですか?
A: 技術的な最低額はありませんが、実用的な観点では証拠金100〜200 USDC以上を推奨します。証拠金が少なすぎると、ポジションサイズが最低注文単位を下回って注文が通らないケースがあります。また、XPを効率的に積み上げるためには取引ボリュームが必要なので、資金が多いほど有利です。最初は50 USDCで動作確認してから、問題なければ増やすアプローチが安全です。
Q: XPはボットの取引でも獲得できますか?
A: はい、獲得できます。Paradexのシーズン報酬は取引ボリュームが基準なので、手動・ボット問わず取引が発生すればXPが付与されます。むしろボットは24時間稼働するため、手動取引より効率的にXPを積み上げられます。ただし、意図的にボリュームを作るだけのウォッシュトレード(自己取引)はルール違反になる可能性があるため、正常な売買戦略に基づいた取引であることが前提です。
Q: Paradex APIは無料で使えますか?
A: はい、APIの利用自体は無料です。アカウントを持っていればL2エージェントキーを発行でき、REST APIとWebSocket APIのいずれも追加費用なしで利用できます。取引に伴う手数料(Taker手数料)はかかりますが、指値注文(Maker)であれば手数料0%です。APIのレート制限はありますが、通常のボット運用では問題になる水準ではありません。
Q: ボットが停止したらポジションはどうなりますか?
A: ボットが停止してもポジションは残ります。これがDEXボット運用で最も注意すべき点です。プロセスがクラッシュしても、オープンポジションはParadex上に残り続け、価格が動いて証拠金比率が悪化すると清算されます。対策として、①SL注文を必ずセットする(ボットが落ちても清算前に決済されます)②起動時にまず既存ポジションを確認してから動作を開始するロジックを入れる、の2点が必須です。
Q: Claude Codeを使わずに実装できますか?
A: もちろんできます。この記事のコードはPythonの基本的な知識があれば理解・改変できます。Claude Codeを使うと「○○の機能を追加して」という自然言語の指示でコードを生成・修正できるので開発速度が上がりますが、必須ではありません。ただし、paradex_pyライブラリの使い方やAsync Pythonに慣れていない場合はClaude Codeで壁打ちしながら実装するのが時間的に一番効率的です。

まとめ:ParadexボットでXPを稼ぎながら自動売買します

✅ この記事のポイントまとめ
paradex_pyでAPIへ接続できます。L2キーのみ使用、L1キーは絶対に使いません
EMAクロス戦略で方向性を判断し、指値注文でMaker手数料0%を維持します
close_with_verifyパターンでクローズ確認を必ず行う(送信だけでは不十分)
RiskCircuitBreakerで日次損失上限・連続損失上限を設定して遮断器を配線します
while Trueの全体をtry/exceptで包み、未捕捉例外でプロセスが落ちないようにします
JWTトークンは45分ごとに更新する(1時間で期限切れになります)
紹介コード経由での登録で5%XPボーナス(登録時のみ適用)

Paradexは手数料構造とAPIの品質の両面でDEXボット向きのプラットフォームだと実感しています。CEXと違ってウォレット管理は自己責任だが、その分カストディリスクがなく、Season3が続く限りXPというリターンもあります。

最初のボットを動かすのに数時間もあれば十分です。まずは接続テストから始めて、testnetで動作確認してから本番に移行する順番を守れば、大きなミスは避けられます。