TradingViewのWebhookによる自動売買とは、TradingView上で作ったアラートが発火した瞬間に、指定したサーバーへHTTPSリクエストを飛ばし、それを受け取ったプログラムが取引所APIへ発注する仕組みのことである。結論から言うと、TradingViewからBitgetへ直接注文を出す公式連携は存在しないため、間に自分の中継サーバーを1台立てて「アラートを受け取って署名付きで発注する」役割を担わせるのが標準的な構成になる。この記事はその中継サーバーのコードと設定を、そのままコピーして動かせる形で最後まで通す。

日本語で「TradingView webhook 自動売買」を調べると、上位に出てくる解説の多くがBybitを前提にしている。しかしBybitは日本居住者向けサービスを終了しており、2026年3月23日にクローズオンリー(新規建玉不可)、2026年7月22日正午に未決済ポジションの強制決済という扱いになった。乗り換え先を探している人のために、この記事はBitget版の手順を一次情報ベースで書き直したものである。

この構成でできること・できないこと

項目 できること できないこと
発注のトリガー Pine Scriptの条件成立と同時に成行・指値を出す ティック単位の高頻度売買(アラートは足の確定などに依存)
対応市場 Bitgetの現物・USDT建て先物 TradingView側で扱いのない銘柄
注文の種類 成行・指値・決済・ポジションのクローズ 複雑な条件付き注文をTradingView側だけで完結させること
サーバー 自前の中継サーバーで完全に制御できる サーバーなしでTradingViewからBitgetへ直接発注すること
信頼性 受信ログを自分で残せる TradingView側の配信遅延・欠落をゼロにすること
コスト 月額はVPS代+TradingViewの有料プラン代 無料プランでのWebhook利用

「できないこと」の3行目が要点である。Webhookは「シグナルの通知」であって「注文の保証」ではない。TradingViewは送信先が3秒以上応答しないとリクエストを中止する仕様で、リトライの保証も公表されていない。したがって受信側は「3秒以内に必ず200を返して、発注は非同期で行う」設計にしなければならない。ここを外すと、相場が動いた場面ほど注文が抜ける。

事前に必要なもの(2026年7月時点)

  1. TradingViewの有料プラン:Webhook通知は有料プランの機能である。月額は年間請求時の特別価格でEssentialが12.95ドル、Plusが29.95ドル、Premiumが59.95ドル、Ultimateが199.95ドル。アラートの上限はEssentialが価格・テクニカルとも20件、Plusが100件、Premiumが400件、Ultimateが1,000件で、Webhook通知はいずれの有料プランでも利用できる。1〜2戦略を回すだけならEssentialで足りる
  2. 二段階認証(2FA)の有効化:TradingViewのWebhookは2FAを有効にしているアカウントでのみ使用できる。
  3. 常時稼働のサーバー:中継サーバーを置く場所。国内VPSの2GBプランなら月1,100〜1,600円程度から借りられる。
  4. ドメイン名:TradingViewはポート80と443以外へのリクエストを拒否し、IPv6にも対応していない。TLS証明書を自動取得させるためにもドメインを1つ用意する。
  5. Bitgetの口座とAPIキー:発注権限あり、出金権限なし、IP許可リストに中継サーバーのIPのみを登録したもの。

国内取引所との使い分けについて

海外取引所を使う場合の前提として、日本円の入出金は金融庁登録の国内取引所で行い、Bitgetはその先の運用先として位置づけるのが基本である。国内取引所で日本円を入金して暗号資産を購入し、それを送金して運用する。国内取引所はUSDTを扱っていないことが多いため、海外へはUSDT/USDCのまま、国内へ戻すときはBTCやETHに変換して送るのが定石になる。送金ネットワーク(TRC20/ERC20など)の選択を誤ると資産が戻らないことがあるため、初回は必ず少額でテストする。

全体の流れを1枚で理解する

[TradingView]
   Pine Script の条件成立
        ↓ アラート発火(JSON文字列を送信)
   Webhook POST  →  ポート443のみ / タイムアウト3秒
        ↓
[自分の中継サーバー(VPS)]
   1. 送信元IPが TradingView の4つのIPか検証
   2. JSON内の共有シークレットを検証
   3. 即座に 200 OK を返す(3秒制限を守る)
   4. 背後のワーカーが Bitget API を署名付きで叩く
        ↓
[Bitget]
   ACCESS-KEY / ACCESS-SIGN / ACCESS-TIMESTAMP / ACCESS-PASSPHRASE
   で認証し、注文が板に乗る

TradingViewからのリクエストは、次の4つのIPアドレスから送信される。これは公式に公開されている値である。

52.89.214.238
34.212.75.30
54.218.53.128
52.32.178.7

手順1:Bitgetで口座とAPIキーを用意する

まず口座を作り、本人確認を済ませ、証拠金となるUSDTを入金する。Bitgetは2018年創業の取引所で、現物手数料は0.1%(BGB払いで0.08%)、先物はメイカー0.02%・テイカー0.06%、レバレッジは最大125倍、日本語UIに対応している(いずれも2026年7月時点)。REST APIとWebSocket APIを提供し、APIキーにIP許可リストを設定できる点が、この構成では特に重要になる。

APIキーは管理画面から作成する。設定は次の通りにする。

  • 権限:現物取引・先物取引の「取引」権限のみを付与する。出金権限は付けない
  • IP許可リスト:中継サーバーのグローバルIPアドレスを1つだけ登録する。
  • パスフレーズ:APIキー作成時に自分で決める文字列で、後から確認できない。作成直後に安全な場所へ控える。

発行されるのは「APIキー」「シークレットキー」「パスフレーズ」の3点セットで、この3つがそろって初めてリクエストに署名できる。

手順2:中継サーバーにTLSを設定する

TradingViewはポート80と443しか許可しないため、443番でHTTPSを受ける必要がある。証明書の自動取得と更新を任せられるCaddyを使うのが最も手数が少ない。

# Caddy のインストール(Ubuntu)
# (Ubuntu 22.04 以降の apt は TLS 通信に追加パッケージが不要です)
sudo apt install -y debian-keyring debian-archive-keyring curl
sudo apt update && sudo apt install -y caddy
# /etc/caddy/Caddyfile
# 証明書は起動時に自動取得・自動更新されます
tv-relay.example.com {
    # TradingView の送信元IPのみ通す
    @tradingview {
        remote_ip 52.89.214.238 34.212.75.30 54.218.53.128 52.32.178.7
        path /webhook/*
    }
    handle @tradingview {
        reverse_proxy 127.0.0.1:8080
    }
    # それ以外は即座に拒否
    handle {
        respond "forbidden" 403
    }
    log {
        output file /var/log/caddy/relay.log
    }
}
sudo systemctl reload caddy
sudo ufw allow 443/tcp comment 'tradingview webhook'
sudo ufw allow 80/tcp  comment 'acme challenge'

IP制限をCaddy側で先に効かせておくと、素性の知れないリクエストがアプリケーションに届く前に落ちる。これだけでインターネット上の無差別スキャンはほぼ遮断できる。

手順3:Pine Scriptとアラートメッセージを書く

TradingViewはアラートメッセージが有効なJSONであれば application/json ヘッダーで送信し、そうでなければ text/plain で送信する。受信側の実装を単純にするために、必ずJSONで書く

エントリーとエグジットの両方でアラートを出す最小構成のPine Scriptは次の通りである。

//@version=5
strategy("Bitget Webhook Sample", overlay=true,
     default_qty_type=strategy.percent_of_equity, default_qty_value=100)

fastLen = input.int(20,  "短期EMA")
slowLen = input.int(60,  "長期EMA")
fast = ta.ema(close, fastLen)
slow = ta.ema(close, slowLen)

longCond  = ta.crossover(fast,  slow)
shortCond = ta.crossunder(fast, slow)

// アラートメッセージはJSON文字列で組み立てる
secret = "ここに自分で決めた長いランダム文字列"

msgLong = '{"secret":"' + secret + '","symbol":"{{ticker}}",' +
          '"action":"buy","order_type":"market","size":"0.01",' +
          '"price":"{{close}}","time":"{{timenow}}"}'

msgShort = '{"secret":"' + secret + '","symbol":"{{ticker}}",' +
           '"action":"sell","order_type":"market","size":"0.01",' +
           '"price":"{{close}}","time":"{{timenow}}"}'

if longCond
    strategy.entry("L", strategy.long,  alert_message = msgLong)
if shortCond
    strategy.entry("S", strategy.short, alert_message = msgShort)

plot(fast, color=color.aqua)
plot(slow, color=color.orange)

戦略ではなくインジケーター側でアラートを出す場合は、アラート作成画面の「メッセージ」欄に次のJSONを直接貼り付ける。

{
  "secret": "ここに自分で決めた長いランダム文字列",
  "symbol": "BTCUSDT",
  "product_type": "USDT-FUTURES",
  "action": "buy",
  "order_type": "market",
  "size": "0.01",
  "reduce_only": false,
  "price": "{{close}}",
  "time": "{{timenow}}"
}

アラートの発火条件は必ず「足の確定時」に設定する。「一度でも条件を満たしたら」にすると、足の途中で条件を満たして戻った場合にも発注され、いわゆるリペイントによる誤発注が起きる。

手順4:Bitgetの署名処理を実装する

BitgetのREST APIは、すべてのリクエストに次のヘッダーを要求する。

ヘッダー 内容
ACCESS-KEY APIキー
ACCESS-SIGN 署名文字列をHMAC SHA256で処理しBase64化したもの
ACCESS-TIMESTAMP エポックからのミリ秒
ACCESS-PASSPHRASE APIキー作成時に設定したパスフレーズ
Content-Type POSTの場合は application/json
locale 言語指定(en-US など)

署名する文字列の組み立て方は、クエリパラメータがある場合が timestamp + METHOD + requestPath + "?" + queryString + body、ない場合が timestamp + METHOD + requestPath + body である。この文字列をシークレットキーでHMAC SHA256にかけ、結果をBase64エンコードした値をACCESS-SIGNに入れる。

Pythonでの実装はそのまま次を使えばよい。

# bitget_client.py
import base64, hmac, hashlib, json, time
import requests

BASE = "HTTPS://api.bitget.com"   # スキームの大文字小文字は区別されません

class BitgetClient:
    def __init__(self, api_key: str, secret: str, passphrase: str):
        self.api_key = api_key
        self.secret = secret.encode()
        self.passphrase = passphrase
        self.session = requests.Session()

    def _sign(self, ts: str, method: str, request_path: str, body: str) -> str:
        # timestamp + METHOD + requestPath(+?query) + body
        message = f"{ts}{method.upper()}{request_path}{body}"
        digest = hmac.new(self.secret, message.encode(), hashlib.sha256).digest()
        return base64.b64encode(digest).decode()

    def request(self, method: str, path: str, params=None, body=None):
        ts = str(int(time.time() * 1000))
        query = ""
        if params:
            query = "?" + "&".join(f"{k}={v}" for k, v in sorted(params.items()))
        body_str = json.dumps(body, separators=(",", ":")) if body else ""
        sign = self._sign(ts, method, path + query, body_str)
        headers = {
            "ACCESS-KEY": self.api_key,
            "ACCESS-SIGN": sign,
            "ACCESS-TIMESTAMP": ts,
            "ACCESS-PASSPHRASE": self.passphrase,
            "Content-Type": "application/json",
            "locale": "en-US",
        }
        url = BASE + path + query
        res = self.session.request(method, url, headers=headers,
                                   data=body_str or None, timeout=10)
        res.raise_for_status()
        return res.json()

サーバーの時刻がずれていると署名が通らないsudo timedatectl set-ntp true を必ず有効にしておく。認証エラーが出たときは、キーの間違いを疑う前に時刻同期を確認するほうが早い。

手順5:Webhookを受けるサーバーを立てる

受信側の設計方針は3つある。3秒以内に必ず200を返す共有シークレットで正当性を検証する同じアラートの重複配信で二重発注しない。この3点を満たす最小実装が次のコードである。

# relay.py
import os, json, queue, threading, hashlib, time
from fastapi import FastAPI, Request, Response
from bitget_client import BitgetClient

SECRET = os.environ["WEBHOOK_SECRET"]
client = BitgetClient(
    os.environ["BITGET_API_KEY"],
    os.environ["BITGET_API_SECRET"],
    os.environ["BITGET_API_PASSPHRASE"],
)

app = FastAPI()
work_q: "queue.Queue[dict]" = queue.Queue()
seen: dict[str, float] = {}          # 重複配信の抑止
SEEN_TTL = 120                        # 秒

@app.post("/webhook/tv")
async def webhook(req: Request):
    raw = await req.body()
    try:
        payload = json.loads(raw)
    except Exception:
        return Response("bad json", status_code=400)

    if payload.get("secret") != SECRET:
        return Response("forbidden", status_code=403)

    # 同一内容の再送を弾く(TradingView側の重複配信対策)
    key = hashlib.sha256(raw).hexdigest()
    now = time.time()
    for k, t in list(seen.items()):
        if now - t > SEEN_TTL:
            seen.pop(k, None)
    if key in seen:
        return Response("duplicate", status_code=200)
    seen[key] = now

    work_q.put(payload)           # 発注は非同期に回す
    return Response("ok", status_code=200)   # 3秒制限を守るため即返す


def place_order(p: dict):
    body = {
        "symbol":      p["symbol"],
        "productType": p.get("product_type", "USDT-FUTURES"),
        "marginMode":  "isolated",
        "marginCoin":  "USDT",
        "size":        str(p["size"]),
        "side":        p["action"],            # buy / sell
        "orderType":   p.get("order_type", "market"),
        "reduceOnly":  "YES" if p.get("reduce_only") else "NO",
        "clientOid":   f"tv-{int(time.time()*1000)}",
    }
    if body["orderType"] == "limit":
        body["price"] = str(p["price"])
    res = client.request("POST", "/api/v2/mix/order/place-order", body=body)
    print("[order]", json.dumps(res, ensure_ascii=False), flush=True)


def worker():
    while True:
        p = work_q.get()
        for attempt in range(3):                 # 一時的な失敗は3回まで再試行
            try:
                place_order(p)
                break
            except Exception as e:
                print(f"[error] attempt={attempt} {e}", flush=True)
                time.sleep(1.5 * (attempt + 1))
        work_q.task_done()


threading.Thread(target=worker, daemon=True).start()

起動と常駐化はsystemdに任せる。

# /etc/systemd/system/tv-relay.service
[Unit]
Description=TradingView -> Bitget relay
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=botuser
WorkingDirectory=/home/botuser/relay
EnvironmentFile=/etc/tv-relay/env
ExecStart=/home/botuser/relay/.venv/bin/uvicorn relay:app --host 127.0.0.1 --port 8080
Restart=always
RestartSec=5
StandardOutput=append:/var/log/tv-relay/out.log
StandardError=append:/var/log/tv-relay/err.log
NoNewPrivileges=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target
sudo mkdir -p /etc/tv-relay /var/log/tv-relay
sudo tee /etc/tv-relay/env > /dev/null <<'EOF'
WEBHOOK_SECRET=ここにPine Scriptと同じ長いランダム文字列
BITGET_API_KEY=
BITGET_API_SECRET=
BITGET_API_PASSPHRASE=
EOF
sudo chmod 600 /etc/tv-relay/env
sudo chown botuser:botuser /etc/tv-relay/env /var/log/tv-relay
sudo systemctl daemon-reload && sudo systemctl enable --now tv-relay

手順6:TradingView側でアラートを登録する

  1. チャート右側のアラートパネルから「アラート作成」を開く。
  2. 条件に自分のインジケーターまたはストラテジーを指定し、トリガーを「足の確定時(Once Per Bar Close)」に設定する。
  3. 「通知」タブで「Webhook URL」にチェックを入れ、tv-relay.example.com/webhook/tv の形式でURLを入力する(先頭にHTTPSのスキームを付ける)。
  4. 「メッセージ」欄に手順3のJSONを貼り付ける。ストラテジー側で alert_message を設定している場合は {{strategy.order.alert_message}} と書く。
  5. 有効期限を「無期限」にして保存する。

登録できたら、必ずテスト発火を行う。TradingViewのアラート画面にはログが残るので、送信が成功しているか(配信ステータス)を確認し、同時にサーバー側で journalctl -u tv-relay -f を眺めて受信とレスポンスを見る。ここで200が返っていない場合、原因はほぼIP制限かシークレットの不一致である。

やってはいけないこと・つまずきやすい失敗例

ここが実運用で最も差がつく部分なので、手順の途中に置く。

  1. Webhookの受信処理の中で発注まで済ませる:Bitget側の応答が遅れると3秒を超え、TradingViewがリクエストを中止する。受信は即200、発注は非同期が鉄則である。
  2. URLだけを秘密にして認証をかけない:URLは推測されうるし、ログや共有画面から漏れる。JSON本文に共有シークレットを入れて検証する設計にする。IP制限との二重化が望ましい。
  3. 足の途中でアラートを発火させる:条件が成立と解除を繰り返すたびに発注が飛ぶ。バックテストと実運用の成績が乖離する最大の原因がこれである。
  4. 重複配信を想定しない:ネットワーク事情で同じアラートが二度届くことはありうる。本文のハッシュで短時間の重複を弾く仕組みを入れる。
  5. サーバーの時刻がずれている:署名にミリ秒のタイムスタンプを使うため、数秒のずれで全リクエストが認証エラーになる。NTPを有効化する。
  6. APIキーに出金権限を付ける:発注に出金権限は不要である。付ける理由がない。
  7. 数量の単位を取り違える:Bitgetの先物では発注数量をコイン建てで指定する。USDT建ての金額をそのまま入れると、桁違いの注文になる。最初は必ず最小ロットで実弾テストする
  8. エラー時に無限リトライする:残高不足や銘柄名の誤りのように、何度送っても通らないエラーで再試行を繰り返すと、レート制限に抵触して他の注文まで通らなくなる。再試行は3回程度で打ち切り、通知に回す。
  9. ログにAPIキーを出力する:デバッグ中にヘッダーごとログへ吐く実装は事故のもとである。出力前にキーをマスクする。

編集部でもClaude Codeによる自動売買を実口座で運用しており、初期の実装では発注の状態管理に不具合があって特定銘柄で意図しない挙動が出た。発注監視の仕組みを差し替えて解消したが、「異常時に何が起きるか」を先に設計していない実装は必ず事故るというのが実感である。検証結果は運用実績ページで公開している。

対決:この構成と他の選択肢

対 Bitgetの先物シグナルボット

Bitgetは取引所内にトレーディングボットを揃えており、スポットグリッド・スポットマーチンゲール・スポット自動積立・スポットトレンド追従・スマートポートフォリオ・先物グリッド・先物ポジショングリッド・先物マーチンゲール・先物トレンド追従・先物シグナルボットが提供されている。このうち先物シグナルボットは、外部からのシグナルを受けて発注する用途に用意された機能である。サーバーを持たずに済む点は明確な利点で、稼働は取引所側が担保する。

一方で、外部サーバーを立てる方式には「注文前に自分でリスク判定を挟める」「複数取引所へ同時に流せる」「受信ログを完全に自分で保持できる」という利点がある。まずコードを書かずに試したいなら取引所内蔵のボット、ロジックを自分で制御したいなら中継サーバー、という切り分けが実務的である。

対 サードパーティの自動売買サービス

TradingViewのアラートを受けて各取引所へ発注してくれる有料サービスも存在する。サーバー構築が不要で導入は速いが、月額費用がかかり、そして何より取引権限のあるAPIキーを第三者に預けることになる。預けた先の運営がどうなるかは自分の管理外である。この記事の構成はキーが自分のサーバーから出ない点が本質的な違いになる。

対 取引所APIを直接ポーリングする自作bot

TradingViewを介さず、自分で価格を取得して判定するbotを書く方法もある。TradingViewの月額が不要になり、足の確定を待たずに秒単位で判断できる。ただしPine Scriptで数分で書ける指標を、自分でゼロから実装して検証する手間がかかる。チャート上で目視検証しながら戦略を育てたい段階では、TradingView経由のほうが試行回数を稼げる

海外取引所を使う以上、理解しておくべきリスク

  1. 金融庁登録がない:Bitgetは日本の暗号資産交換業者として登録されていない。日本の法規制による保護や補償の対象外である。
  2. 出金が制限される可能性:本人確認の追加要求やメンテナンスで、出金が一時的に止まることがある。全資産を一か所に置かないことが唯一の実務的な対策になる。
  3. サービス方針の変更:日本居住者向けサービスの縮小・終了は実際に起きている。Bybitの事例がまさにそれで、乗り換え先を常に把握しておく必要がある。
  4. 利益は確定申告が必要:暗号資産の利益は課税対象である。Webhook経由の自動売買は取引回数が膨らみやすく、海外取引所は年間取引報告書が出ないことが多いため、取引履歴のCSVを毎月保管しておく。税務の扱いは個別事情で変わるため、税理士など専門家に相談することを勧める。
  5. レバレッジの上限に引きずられない:最大125倍という数字は「使える上限」であって「使うべき倍率」ではない。検証段階では低倍率から始める。
  6. サーバー停止中もポジションは残る:中継サーバーが落ちてもBitget上の建玉は市場に晒され続ける。損切りは取引所側の注文として置いておくのが基本で、Webhookの到達だけに頼らない。

本番稼働前チェックリスト

  • TradingViewで2FAを有効にし、有料プランに加入している
  • アラートのトリガーが「足の確定時」になっている
  • Webhook URLが443番で応答し、TLS証明書が有効である
  • TradingViewの4つのIPアドレス以外からのリクエストが403で落ちる
  • JSON本文の共有シークレットが一致しない場合に403を返す
  • 受信は3秒以内に200を返し、発注は非同期で行われている
  • 同一内容の重複配信が短時間で弾かれる
  • APIキーに出金権限がなく、IP許可リストにサーバーのIPのみが入っている
  • サーバーの時刻同期(NTP)が有効である
  • 最小ロットでの実弾テストで、注文が意図した数量・方向で通ることを確認した
  • 損切り注文が取引所側に置かれ、サーバー停止中も有効である
  • systemdでの自動再起動とログローテーションを設定した

まとめ

TradingViewのWebhookでBitgetを動かす構成の本体は、「アラートを受けて署名付きで発注する中継サーバー」を1台きちんと作れるかどうかに尽きる。TradingView側は有料プラン・2FA・足の確定時トリガー・JSONメッセージという4点を押さえれば設定は10分で終わる。難所はすべてサーバー側にあり、3秒以内の応答、共有シークレットとIPによる二重の認証、重複配信の抑止、非同期の発注キュー、この4つを実装できていれば実運用に耐える。

Bybit前提の解説が使えなくなった今、同じ構成をBitgetへ移すこと自体は難しくない。署名の作り方とエンドポイントが変わるだけである。ただし移行時こそ最小ロットでの実弾テストを飛ばさないこと、そして損切りを取引所側に置いておくこと。この2点を守れば、乗り換えに伴う事故の大半は防げる。

<!-- INTERNAL_LINKS_RELATED -->

関連記事

<!-- INTERNAL_LINKS_RELATED -->