チャート見るのがだるいのでTradingViewを使って自動売買してみた件：Bitget編

# 趣旨
TradingViewを見ていちいち取引所のサイトに移動して注文を出すのがだるい！という時に全部自動化してトレード放棄したいという邪な考えから作ったbotです。
同じことを全く別の方法で実現することも可能です。

Macでもラズパイでもレンタルサーバーでも実装できますが、実際に動かすとなるとレンタルサーバーでやるのが良いと思います。またサーバーで実装した場合はSmeeを使わなくて済むので簡単ですし、より安全です。

:::note alert
以下のコードを実行する場合は全て自己責任でお願い致します。
本記事は教育目的での共有であり、投資を勧誘するものではありません。
:::

# 自動売買に必要なもの
* Smee
* TradingView有料アカウント
* Bitgetアカウント
* その他CEXアカウント（応用）

# 仮想環境設定
仮想環境のことをよく知らなくても念のためにやっておいた方が良いです。
```bash
python3 -m venv myenv
source myenv/bin/activate
```
専用のディレクトリを構築します。
```bash
mkdir tradingview
cd tradingview
```
# SmeeとWebhook
### Smeeとは
外部からのWebhookをローカルサーバーにデリバリーするサービスです。

SMEEをインストールします。
公式サイトに行けば必要なコマンドが記載されています。

https://smee.io/

公式サイトでランダムなプロキシーURLが表示されるので、以下のコマンドの「プロキシーURL」の部分をそのURLに書き換えてコマンドを打ちます。
```
smee -u https://smee.io/プロキシーURL -t http://127.0.0.1:5000/webhook
```

:::note info
Smeeを使う理由
サーバーを外部に公開するのは危険だから。
Smeeを使えばローカルサーバーのままWebhookが使えます。
:::

### Webhookとは
Webhookが何か全くわからない場合はKintoneさんの記事を参照すると良いかもです。

https://kintone-blog.cybozu.co.jp/developer/000283.html

簡単に言えば、イベント発生時にPOSTでデータを送信する機能です。
今回はTradingViewのWebhook機能を利用するので送信側の実装はやりません。
受信する側のみ実装します。

Flaskというフレームワークで簡易Webアプリを作ってそのアプリで受信するようにセットします。

# コード（コピペ可）
では早速コードの準備に取り掛かります。

## Flask設定

必要なライブラリをインストールします。
```bash
pip install Flask
pip install requests
```

Flaskのためのファイルを作成します。
```
touch flask.py
```
以下のコードをflask.pyにコピペします。

```python: flask.py
from flask import Flask, request
import json
import subprocess

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def handle_webhook():
    try:
        data = request.json
        strategy = data.get("strategy")

        if strategy == "bitget":
            side = data.get("side")
            symbol = data.get("symbol")
            amount = data.get("amount")
            subprocess.run(["python", "bitget.py", "--side", side, "--symbol", symbol, "--amount", amount])

        elif strategy == "okx":
            side = data.get("side")
            symbol = data.get("symbol")
            amount = data.get("amount")
            subprocess.run(["python", "bybit.py", "--side", side, "--symbol", symbol, "--amount", amount])

        else:
            # raise an exception if strategy is not recognized
            raise ValueError("Invalid strategy")

        return "OK"

    except Exception as e:
        # log the error message
        app.logger.error(f"Error: {str(e)}")

        # return an error response to the client
        return {"error": str(e)}, 400

if __name__ == '__main__':
    app.run()
```

## CCXT設定
ccxtは複数の取引所のAPI接続を簡単にするためのライブラリです。
これがなかったら全ての取引所のAPIドキュメントを全て読み込んで設定しないといけなくなるので作業量がかなり増えてしまいます。

ccxtをインストールします。
```bash
install ccxt
```
bitget.pyを作成します。
```
touch bitget.py
```
bitget.pyに以下のコードをコピペします。
```python: bitget.py
import ccxt
import argparse
import config

parser = argparse.ArgumentParser()
parser.add_argument("--side", help="Trade side (buy or sell)", choices=["buy", "sell"])
parser.add_argument("--symbol", help="Symbol")
parser.add_argument("--amount", help="Amount")
args = parser.parse_args()
side = args.side.lower()
symbol = args.symbol.upper()
amount = float(args.amount)

bitget_exchange = ccxt.bitget({
    "apiKey": config.bitget_api_key,
    "secret": config.bitget_secret_key,
    "password": config.bitget_password,
    "enableRateLimit": True
})

bitget_exchange.options["defaultType"] = "future"

try:
    positions = bitget_exchange.fetch_positions(symbol)
except ccxt.BaseError as e:
    print(f"Error fetching positions: {e}")
    exit(1)

has_long_position = False
has_short_position = False

for position in positions:
    if position['info']['symbol'] == symbol:
        if position['info']['holdSide'] == 'long' and float(position['info']['available']) > 0:
            has_long_position = True
        elif position['info']['holdSide'] == 'short' and float(position['info']['available']) > 0:
            has_short_position = True

if (side == "buy" and has_short_position) or (side == "sell" and has_long_position):
    bitget_exchange.create_order(symbol, "market", side, amount, params={"reduceOnly": True})
    bitget_exchange.create_order(symbol, "market", side, amount)
else:
    bitget_exchange.create_order(symbol, "market", side, amount)
```

## Config設定

config.pyを作成します。

```
touch config.py
```
config.pyに以下の内容をコピぺして、該当する情報をダブルクオーテンションの中に記載します。
```python: config.py
bitget_api_key = ""
bitget_secret_key = ""
bitget_password = ""
```

:::note alert
APIキーを直に書いて保存しているのでセキュリティに問題があるかも知れません。気になる方は暗号化の方法を学んでご自身で暗号化してください。
:::

あとはSmeeが起動しているのを確認してbotを起動します。
```
python flask.py
```
Smeeの起動確認は以下のコマンドで出来ます。
```
sudo systemctl status smee.service
```


# TradingViewのアラート
以下はサンプルです。
銘柄に応じてDOGEUSDT_UMCBLのDOGEの部分を別の銘柄に変更します。
```json
{"strategy":"bitget","side":"{{strategy.order.action}}","symbol":"DOGEUSDT_UMCBL","amount":"{{strategy.order.contracts}}"}
```

例えば以下のようにセットします。
![Screenshot 2024-01-27 at 18.11.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3670541/8db0d9cc-feed-60a2-7ee1-0d1bc246779e.png)

次にWebhookのURLの箇所にSmeeのプロキシーURLをセットします。
![Screenshot 2024-01-27 at 18.15.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3670541/e92e0a05-5708-5b67-7606-aa32947ed280.png)

:::note info
プロキシーURLはSmeeのページを訪れた時に付与されるURLです。
:::

## おまけ

例として既存のストラテジーをアレンジして作ったオリジナルストラテジーのコードを記載します。
BTCの日足に対して適用させると以下のような結果になります。

![Screenshot 2024-01-27 at 18.05.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3670541/3eaddcc0-b413-0482-d51a-cbfd1ee63236.png)

以下のPineScriptコードをTradingViewでコピペすると使えます。
```
//@version=4
strategy("danieltanaka", overlay=true)

len = input(14, minval=1, title="DI Length")
lensig = input(14, title="ADX Smoothing", minval=1, maxval=50)

up = change(high)
down = -change(low)
plusDM = na(up) ? na : (up > down and up > 0 ? up : 0)
minusDM = na(down) ? na : (down > up and down > 0 ? down : 0)
trur = rma(tr, len)
plus = fixnan(100 * rma(plusDM, len) / trur)
minus = fixnan(100 * rma(minusDM, len) / trur)
sum = plus + minus
adx = 100 * rma(abs(plus - minus) / (sum == 0 ? 1 : sum), lensig)
DMX = (adx/25)*(plus-minus)

longCondition = crossover(DMX, 0)
if (longCondition)
    strategy.entry("Long", strategy.long)

shortCondition = crossunder(DMX, 0)
if (shortCondition)
    strategy.entry("Short", strategy.short)

plot(DMX, color=#d0b170, style=plot.style_area, transp=40, title="DMX")
plot(plus, color=color.green, title="+DI")
plot(minus, color=color.red, title="-DI")
```