40行で支度しな

「仮想通貨botter Advent Calendar 2023」14日目の記事になります。

https://qiita.com/advent-calendar/2023/botter

おはようございます。こんにちは。こんばんは。
仮想通貨Botを初めて３年目、なぜか初心者マークが外れないままのヘソの下のポニョと申します。

「天空の城ラピュタ」で登場した台詞である「40秒で支度しな」より「40行で（バブルに向けてbotの）支度しな」ということで40行以内のbotを作成するという試みです。
こういう機会がないと開発しないので自分に鞭を打って開発しようと思います。
記事が出来上がる頃、儲かっているかは分かりませんが、bot作成日記なので生暖かい目で見守ってください。
**儲かるbotできたらください。**

:::note warn
注意
　あくまでも本記事は日記のようなものです。
　本記事を参考に取引をされて損害が出た場合に責任を負えません。
　悪しからずご了承ください。
:::

### 今回のbotの方針
今回はGas代が非常に安いSOLを使ったbotを作成します。
Solanaを主戦場としたbotterが少なさそうなのも良いですよね。
レッドオーシャンに突っ込んで勝つには並々ならぬ努力が必要です。
最低限の努力で最小限の利益をつかみ取ろうと思います。

# CEX-DEX bot
~~CEX-DEXの戦略はいくつかありますが、ここでは一種の三角裁定のように作ります。
こんなイメージです。~~(2023年11月27日修正)
![スクリーンショット 0005-11-13 3.41.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2311466/47277aa3-8642-98b6-3a7b-f4608fbcf7a0.png)

40行に収まらなかったので、シンプルにDEX買い、CEX売りのシンプルbotです。
まあこういう~~行き当たりばったり~~試行錯誤もbot作成の醍醐味というところで。
## CEXの選定
2023年11月9日現在、国内取引所でSOLの取り扱いがあるのは以下の４社です。

・FTX Japan株式会社
・GMOコイン株式会社
・SBI VCトレード株式会社
・Binance Japan株式会社

詳細は以下リンクよりご確認ください。
[金融庁「暗号資産交換業者登録一覧」](https://www.fsa.go.jp/menkyo/menkyoj/kasoutuka.pdf)

このうち、2023年11月9日時点でトレード可能な取引所はGMOコイン株式会社とSBI VCトレード株式会社です。
どちらでも良いとは思いますが、今回はGMOコインを例に作成します。

## DEX選定
SolanaにはJupiter AggregatorというDEX Aggregatorがあります。
最適価格を探索してくれること、APIがあることからJupiter Aggregatorを利用します。

## bot作成(2023/11/27版)
1\.CEX（GMOコイン）よりDEX（Jupiter）が安い時に
2\.DEX（Jupiter）で買って、
3\.CEX（GMOコイン）で売る！
とりあえずこんな感じで。



```python:main.py
import base64
import requests
from solana.rpc.api import Client
from solana.rpc.types import TxOpts
from solders.keypair import Keypair
from solders.transaction import VersionedTransaction
import json
import solders
import hmac
import hashlib
import time
from datetime import datetime

def get_sol_jpy():
    sol_response = requests.get('https://quote-api.jup.ag/v6/quote?inputMint=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v&outputMint=So11111111111111111111111111111111111111112&amount=1000000&slippageBps=50').json()
    gmo_response = requests.get('https://api.coin.z.com/public/v1/ticker?symbol=SOL').json()
    gmo_usdjpy = requests.get('https://forex-api.coin.z.com/public/v1/ticker').json()
    sol_ask, sol_bid, usdjpy_ask, usdjpy_bid = gmo_response['data'][0]['ask'], gmo_response['data'][0]['bid'], gmo_usdjpy['data'][0]['ask'], gmo_usdjpy['data'][0]['bid']
    sol_price = 1/(int(sol_response["outAmount"])/1000000000)
    return float(sol_price), float(sol_ask), float(sol_bid), float(usdjpy_ask), float(usdjpy_bid), sol_response

def send_request(api_key, secret_key, method, end_point, path, req_body):
    timestamp = f"{int(time.mktime(datetime.now().timetuple()))}000"
    sign = hmac.new(secret_key.encode(), f"{timestamp}{method}{path}{json.dumps(req_body)}".encode(), hashlib.sha256).hexdigest()
    return requests.post(end_point + path, headers={"API-KEY": api_key, "API-TIMESTAMP": timestamp, "API-SIGN": sign}, data=json.dumps(req_body)).json()

def swap_sol(sol_response):
    wallet = Keypair.from_base58_string("YOUR_SECRET_KEY")
    raw_tx = VersionedTransaction.from_bytes(base64.b64decode(requests.post('https://quote-api.jup.ag/v6/swap', headers={'Content-Type': 'application/json'}, data=json.dumps({'quoteResponse': sol_response,'userPublicKey': "{}".format(wallet.pubkey()),'wrapAndUnwrapSol': True})).json()['swapTransaction']))
    signed_tx = solders.transaction.VersionedTransaction.populate(raw_tx.message, [wallet.sign_message(solders.message.to_bytes_versioned(raw_tx.message))])
    client = Client("https://api.mainnet-beta.solana.com")
    txid = client.send_transaction(signed_tx, opts=TxOpts(skip_confirmation=False, skip_preflight=True, max_retries=3),).value

def main():
    crypto_api_key, crypto_secret_key = 'YOUR_CRYPTO_API_KEY', 'YOUR_CRYPTO_SECRET_KEY'
    while True:
        sol_price, sol_ask, sol_bid, usdjpy_ask, usdjpy_bid, sol_response = get_sol_jpy()
        if sol_price * usdjpy_ask * 1.0005 < sol_bid:
            swap_sol(sol_response)
            send_request(crypto_api_key, crypto_secret_key, 'POST', 'https://api.coin.z.com/private', '/v1/order', {"symbol": "SOL", "side": "SELL", "executionType": "MARKET", "size": "0.1"})
        time.sleep(60)

main()
```
間違えてたらごめんなさい。お察しの通りいろいろな機能が足りてませんね。

### 修正すべき点
・可読性が低い
　→　40行だと仕方ないっすね。
・現物送付していない
　→ DEX→CEXは簡単に追加できそう。
・ドル円のヘッジが出来てない
　→ GMOにドル円のFXがあるから利用する？
・GMOコインはAPI出金できない
　→ CEX→DEXの時は手作業？めんどくさいからBinance Japanに期待
・エラーハンドリングがない
　→ 追加する

### 感想
Pythonを使ってSolanaでいろいろとしようとするためには勉強が必要で涙が出そうでした。
日本語で解説してる記事が少なくにわかbotterにはちょっとした壁だったのもあります。

solana.pyの一部機能がsoldersへ移行？プログラミングはChatGPTに託す勢には厳しい戦いでした。

## 細かいところを見てみる

```python:swap_sol.py
import base64
import requests
import json
import solders
from solana.rpc.api import Client
from solana.rpc.types import TxOpts
from solders.keypair import Keypair
from solders.transaction import VersionedTransaction
from solana.transaction import Transaction
from solders.system_program import TransferParams, transfer
from solders.pubkey import Pubkey

def swap_sol():
    # JupiterのAPIを使って見積もりを取得します。1SOL=1000000000、1USDC=1000000です。
    # API詳細についてはhttps://station.jup.ag/docs/apis/swap-apiを参照。
    # ここでは1USDCをSOLへスワップ時の見積もりを取得します。
    sol_addr = "So11111111111111111111111111111111111111112"
    usdc_addr = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
    amount = 1000000
    sol_response = requests.get(f'https://quote-api.jup.ag/v6/quote?inputMint={usdc_addr}&outputMint={sol_addr}&amount={amount}&slippageBps=50').json()
    out_amount = int(sol_response["outAmount"])

    # 今回使用するウォレットの秘密鍵からを指定。
    secret_key = "your_solana_secret_key"
    wallet = Keypair.from_base58_string(secret_key)

    # JupiterのAPIを使ってトランザクションを作成します。
    # APIが指定できるパラメーターの詳細についてはhttps://station.jup.ag/docs/apis/swap-apiを参照。
    headers={'Content-Type': 'application/json'}
    data = {
        'quoteResponse': sol_response,
        'userPublicKey': "{}".format(wallet.pubkey()),
        'wrapAndUnwrapSol': True
        }
    swap_tx_response = requests.post("https://quote-api.jup.ag/v6/swap", headers=headers, data=json.dumps(data))
    # JupiterのAPIから返ってきたトランザクションをSolanaのトランザクションに変換します。
    swap_instruction = swap_tx_response.json()["swapTransaction"]
    raw_tx = VersionedTransaction.from_bytes(base64.b64decode(swap_instruction))
    signature = wallet.sign_message(solders.message.to_bytes_versioned(raw_tx.message))
    signed_tx = VersionedTransaction.populate(raw_tx.message, [signature])

    # トランザクションを送信します。
    client = Client("https://api.mainnet-beta.solana.com")
    txid = client.send_transaction(signed_tx, opts=TxOpts(skip_confirmation=False, skip_preflight=True, max_retries=3),).value
    print(f'https://solscan.io/tx/{txid}')

    # CEXへ送金します。
    cex_addr = Pubkey.from_string('CEXの入金アドレス')
    transaction = Transaction().add(transfer(TransferParams( from_pubkey=wallet.pubkey(), to_pubkey=cex_addr.pubkey(), lamports=out_amount)))
    txid = client.send_transaction(transaction, wallet)
    print(f'https://solscan.io/tx/{txid}')

swap_sol()
```
ほぼ以下URLの内容からいただきました。

https://solana.stackexchange.com/questions/6393/how-do-i-implement-jupiters-versionedtransactions-into-python

Solanaに関するbot制作はハードルが高いけどやってみたらなんとかなりそうです。
でもPythonはやめた方がいいですね。TypeScriptかRustが良さそう。

# これがあれば今日から君もソラナーだ！！！（参考文献）

### Softgate Limited様「Solana での BOT 開発の概要」

https://zenn.dev/softgate/articles/8ee1e3e3f1ce33

控えめに言って神でした。Solanaのbotを作成するための基礎知識が詰まってます。
~~この記事を読みながら何度寝落ちしたことか~~
SolanaのSPLやアカウント、プログラムという概念はとっつきにくかったです。

### Solana Cookbook

https://solanacookbook.com/ja/references/local-development.html

Solana界のバイブル。何気に和訳してあるのも嬉しい。
チュートリアルの途中までPythonがあるのに途中でなくなります。
SolanaのbotはTypeScriptかRustが推奨されているんでしょうね。

### Jupiter API

https://station.jup.ag/docs/api

雑に投げてもいろいろやってくれるからオススメ。



こうやって締め切りを決めてbot開発しないと何もやる気がしなかったので、良い機会になりました。
ありがとうございました。