Shortcutsアプリからbitbank APIでbtc_jpyの指値注文するPythonコードを実行するショートカットについて

## 追加情報(2023/06/24)
以下の投稿でa-Shellアプリでgitコマンドを利用する方法についてを投稿しました。

https://qiita.com/7rikazhexde/items/a8d2cebdb57cc28a801a

実際にgitが使えることが分かったので、[python-bitbankcc](https://github.com/bitbankinc/python-bitbankcc)を使用するコードを下記のGitHubリポジトリにコミットしました。iCloudの共有リンクも記載していますので、詳細はREADME.mdを参照してください。

:::note warn
注意事項
※コードはa-Shellで使用する前提です。
※本記事に記載されたコードはgifの通り動作しますので変更はせずとします。
:::

https://github.com/7rikazhexde/python-bitbankcc-ios-shortcuts

## 経緯
* bitbankでは[公式のAPI](https://github.com/bitbankinc/bitbank-api-docs/blob/master/README_JP.md)を使用して[認証情報](https://github.com/bitbankinc/bitbank-api-docs/blob/master/rest-api_JP.md#%E8%AA%8D%E8%A8%BC)をHTTPリクエストヘッダーに含めることでアセットの確認や新規注文をすることができますが、個人的にスマートフォンから指値注文する方法としてShortcutsアプリで実行する方法を検討していました。
* 調査したところ、Public & Private API をPythonで扱うライブラリ([python-bitbankcc](https://github.com/bitbankinc/python-bitbankcc))を使用することで指値注文ができること。また、ShortcutsアプリでPythonコードで実行するアプリとして、[a-Shell](https://holzschu.github.io/a-Shell_iOS/)と[Pyto](https://pyto.app/)の2つで実行できることが分かりました。
* そこで、本記事では調査内容について、実際にa-ShellとPytoでpython-bitbankccライブラリを使用して作成したbtc_jpyを指値注文するショートカットと合わせて紹介します。
* ShortcutsアプリでPythonコードを実行したいと考えている方の参考になればと思います。

## 記事について
* 実行環境は下記の通りです。
  iOS 16.1.2
  iPadOS 15.7
* APIの仕様(Public,Private,APIキー等)については[bitbank.cc 公式APIドキュメント](https://github.com/bitbankinc/bitbank-api-docs/blob/master/README_JP.md)や[こちらの方の記事](https://qiita.com/kaketechjapan/items/38ea0eca12642f0e9ad3)にてわかりやすく紹介されていますので割愛します。
* 記載されているコードは必ずしも期待された動作を保証するわけではありません。
* 本コードを実行したこと、参考にしたことによって被るあらゆる損害について責任を負いかねますので自己責任でご利用ください。


## ShortcutsアプリでPythonコードを実行する方法について
AppStoreにはPythonコードをiOSで実行するアプリがあります。
アプリにはShortcutsと連携をサポートしている場合、ショートカット内のアクションとしてPythonコードを実行することができます。調べるとa-ShellとPytoの2つがありました。
もしかすると他にも使用できるアプリはあるかもしれませんが、本記事ではa-ShellとPytoの2つのアプリを使用して作成したショートカットを紹介します。

## アプリの特徴について
### a-Shell
詳細は[公式サイト](https://holzschu.github.io/a-Shell_iOS/)を参照してください。主な特徴は下記の通りです。
* 価格は無料
* Pythonではpipコマンドを使用可能
* venvによる仮想環境作成が可能([詳細](https://github.com/holzschu/a-shell/issues/516))
* 2022/12/17時点のpythonのバージョンは3.11.0
```
$  python -V
Python 3.11.0
```
* ShortcutsアプリからPythonコードを実行する場合はa-Shellの**コマンドを実行**アクションでコマンドライン指定で実行する。
* Shortcutsアプリでは値を参照する際は標準出力結果、関数の戻り値を参照できないため後述するファイル書き出し処理を行うことで参照することができる。
* a-Shellアプリではコマンドを実行後にShortcutsアプリに遷移する設定がないため、**コマンドを実行**アクションで```open shortcuts://```を実行することで遷移することができる。([詳細](https://github.com/holzschu/a-shell#shortcuts))

### Pyto
詳細は[公式サイト](https://pyto.app/)と[公式ドキュメント](https://pyto.readthedocs.io/en/latest/)を参照してください。主な特徴は下記の通りです。
* 価格は有料（3日間のお試し利用あり。外部モジュールを使用する場合はFull Versionの購入が必要。）
* Pythonコードはコマンドライン指定かファイル起動画面から**Run with arguments**を選択して実行する。
* 2022/12/17時点のpythonのバージョンは3.10.0
* pipコマンドが使用可能(PyPIの管理画面あり)
* venvは使えない?(venvコマンドでエラーは出ませんが、```ENV_DIR```で指定するターゲットディレクトリは作成されませんでした。)
* ShortcutsアプリからPythonコードを実行する場合はコードをShortcutsアプリ内に記述する**Run Code**指定、またはスクリプトを指定する**Run Script**(引数指定可能)アクションで実行する。
* Shortcutsアプリで値を参照する際はコード側で```print関数```で標準出力し、**Get Script Output**アクションで実行する。

## 作成したもの
bitbankのAPIを使用してbtc_jpyを指定価格で指値注文するショートカットを作成しました。
ショートカットではbitbankの指値注文結果(json)を受けて表示したいデータを取得して表示しています。
なお、レスポンスデータはAPIドキュメント記載の[新規注文を行う / Response](https://github.com/bitbankinc/bitbank-api-docs/blob/master/rest-api_JP.md#%E6%96%B0%E8%A6%8F%E6%B3%A8%E6%96%87%E3%82%92%E8%A1%8C%E3%81%86)に従い送信されます。タイムスタンプはunixtimeのためローカル時間に変換しています。
表示したいデータはコード側でまとめて必要なデータを取得する形式にするとアクションが複雑にならずに済みます。

### a-Shellのアクションを使用したショートカット
![bitbank_spot_order_a-Shell.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1832768/9712992b-c0c3-374a-b0ee-510b0d7506fb.gif)

### Pytoのアクションを使用したショートカット
![bitbank_spot_order_Pyto.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1832768/9ad8a7a6-c61f-2745-d111-cd3884514cb2.gif)

## 仕様とソースコードについて
* ショートカットアプリのアクションとソースコード(注文処理)の流れをアクティビティ図で作成しました。
  緑の部分はa-ShellとPytoで異なる部分になります。
* 価格情報はa-Shell,Pyto共に**入力を要求**アクションで数字で入力し、コマンドライン引数指定でPythonコード側で参照して処理します。
* Pythonコードでは価格情報として**現在の買い注文の最高値**を取得し、指値注文はPublic & Private API をPythonで扱うライブラリである[python-bitbankcc](https://github.com/bitbankinc/python-bitbankcc)と認証時に指定するAPIキーとAPIシークレットキーを環境変数として設定するために[python-dotenvモジュール](https://pypi.org/project/python-dotenv/)を使用します。認証情報は端末内で保存することでセキュリティに配慮しています。
* python-bitbankccとpython-dotenvは標準では組み込まれてはいないため、pipコマンドでインストールする必要がありました。python-bitbankccはPyPIには登録されておらず、また、a-Shell,Pyto共にgitコマンドをサポートしていなかったため、python-bitbankccのGitHubページに記載されたインストール手順を行うことができませんでした。そこでPC側でgit cloneしたコードをiPhoneに送ることで対応しました。
* [2023/06/24追記] a-Shellでは可能でした。([詳細](https://qiita.com/7rikazhexde/items/a8d2cebdb57cc28a801a)) 
* python-dotenvはa-Shellでは```pip install python-dotenv```、PytoではPyPlページで検索してinstall可能です。
* **注文数量**は**現在の買い注文の最高値**と**購入したい価格**から計算して注文用のリクエストデータとして作成しますが、実装の都合で注文時に即約定しないようにコードでは現在の買い注文の最高値から```ADJUST_PRICE```という定数分引いた金額を基準に計算しています。(定数は0とすれば現在の買い注文の最高値で注文数量を計算します。)

### ファイル構成について
git cloneしてファイルコピーしたpython-bitbankccに[追加]のファイルを格納します。
```
python-bitbankcc % tree -t -a -L 2
.
├── .gitignore
├── LICENSE
├── README.md
├── setup.cfg
├── setup.py
├── python_bitbankcc
│   ├── __init__.py
│   ├── private_api.py
│   ├── public_api.py
│   └── utils.py
├── .git(省略)
├── .env //[追加]APIキーとAPIキーシークレットを環境変数として定義
├── response.txt //a-Shellでコード実行時に作成するため追加不要
└── input_spot_order_env.py //[追加]指値注文用のファイル
```

### a-Shellの場合
値を参照する際は標準出力結果、関数の戻り値を参照できないため、ファイル(response.txt)に```open()関数```の引数```mode```を```'w'```で指定して上書き（既存の内容は削除）で保存します。その後、```open shortcuts://```コマンドを実行することでShortcutsアプリに遷移し、**フォルダからファイルを取得**アクションを実行してファイルのデータを読み込みます。
#### アクティビティ図
![bitbank_spot_order_a-Shell.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1832768/68f1965b-ccdf-4e9f-ce0a-f7c6b5ae7eef.png)

#### ソースコード
```input_spot_order_env.py
import sys, os, json, datetime
from python_bitbankcc import public, private
from dotenv import load_dotenv

class UseBitbankApi:
    def __init__(self):
        # コマンドラインより送信するパラメータを設定する
        self._args = sys.argv
        # 要素数で判別する
        if(len(self._args) == 1):
            # 引数指定がない場合は500円で固定する
            self._adjust_order_num = 500
        elif(len(self._args) > 2):
            # 長さが2より大きい場合は価格の指定誤りとしてプログラム終了
            # ただし、ショートカットから実行する場合は通らない
            print('価格は一つのみ指定してください')
            sys.exit()
        else:
            # コマンドライン引数で指定された数字で設定する
            self._adjust_order_num = int(self._args[1])

        # 指値取引のために最新取引価格から調整するための価格(値は0以上で指定してください。)
        self._adjust_price = 5000
        # BTC取引時の小数点以下の桁数
        self._decimal_digits_btc = 8

        # API設定
        load_dotenv()
        self._api_key = os.environ['API_KEY']
        self._api_secret = os.environ['API_SECRET']

        # public API classのオブジェクトを取得
        self._pub = public()

        # private API classのオブジェクトを取得
        self._prv = private(self._api_key, self._api_secret)

    # public APIを使用してティッカー情報を取得するメソッド
    def get_ticker(self,ticker_ptn,pair):
        value = self._pub.get_ticker(pair)
        return value[ticker_ptn]

    # 注文価格と注文数量を返すメソッド
    def set_order_data(self,ticker_ptn,pair):
        buy_price = int(self.get_ticker(ticker_ptn,pair))
        order_num = (self._adjust_order_num / (buy_price - self._adjust_price))
        order_num = round(order_num, 8)
        order_price = buy_price - self._adjust_price
        return order_num,order_price

    # private APIを使用して指値注文するメソッド
    def spot_order(self,ticker_ptn,pair):
        order_num,order_price = self.set_order_data(ticker_ptn,pair)
        order_num = str(order_num)
        #print('order_num =',order_num)
        order_price = str(order_price)
        #print('order_price =',order_price)
        response = self._prv.order(
            pair, # ペア
            order_price, # 価格 (成行注文の場合は None にする)
            order_num, # 注文枚数
            ticker_ptn, # 注文サイド (buy|sell)
            'limit', # 注文タイプ (limit|market|stop|stop_limit)
            # 以降は任意の引数
            False, # post_only 注文、デフォルトは False, None も可能で Falseと同じ挙動
            False # trigger_price 逆指値などのトリガー価格
        )
        return response

    # 注文日時(UnixTimeのミリ秒)をローカル時間に変換するメソッド
    def conv_unixtime(self,unixtime):
        data = unixtime / 1000
        dt = datetime.datetime.fromtimestamp(data).strftime('%Y/%m/%d %H:%M:%S')
        return dt

    # ファイルを書き出すメソッド(a-Shellの場合に使用する)
    def file_write(self,data):
        f = open('./response.txt', 'w')
        f.write(data)
        f.close()

if __name__ == '__main__':
    # インスタンス生成
    use_bitbank_api = UseBitbankApi()
    # 指値注文時の例外処理判定
    try:
        # btc_jpy指定で指値注文する
        response = use_bitbank_api.spot_order('buy','btc_jpy')
    except Exception as e:
        # エラーメッセージをファイル出力する(a-Shellの場合)
        use_bitbank_api.file_write(str(e))
    else:
        # 注文日時(UnixTimeのミリ秒)をローカル時間に変換する
        response['ordered_at'] = use_bitbank_api.conv_unixtime(response['ordered_at'])
        # jsonエンコード
        response_json = json.dumps(response)
        # レスポンスデータをファイル出力する(a-Shellの場合)
        use_bitbank_api.file_write(response_json)
```

### Pytoの場合
Pytoではショートカットで**Get Script Output**でレスポンスデータを参照するためにprint()関数で標準出力しています。ファイル出力処理とprint()関数以外は同じ処理を実装しています。
#### アクティビティ図
![bitbank_spot_order_Pyto.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1832768/be2c8ade-015e-7dda-bf9a-6f19e20cb88b.png)

#### ソースコード
```input_spot_order_env.py
import sys, os, json, datetime
from python_bitbankcc import public, private
from dotenv import load_dotenv

class UseBitbankApi:
    def __init__(self):
        # コマンドラインより送信するパラメータを設定する
        self._args = sys.argv
        # 要素数で判別する
        if(len(self._args) == 1):
            # 引数指定がない場合は500円で固定する
            self._adjust_order_num = 500
        elif(len(self._args) > 2):
            # 長さが2より大きい場合は価格の指定誤りとしてプログラム終了
            # ただし、ショートカットから実行する場合は通らない
            print('価格は一つのみ指定してください')
            sys.exit()
        else:
            # コマンドライン引数で指定された数字で設定する
            self._adjust_order_num = int(self._args[1])

        # 指値取引のために最新取引価格から調整するための価格(値は0以上で指定してください。)
        self._adjust_price = 5000
        # BTC取引時の小数点以下の桁数
        self._decimal_digits_btc = 8

        # API設定
        load_dotenv()
        self._api_key = os.environ['API_KEY']
        self._api_secret = os.environ['API_SECRET']

        # public API classのオブジェクトを取得
        self._pub = public()

        # private API classのオブジェクトを取得
        self._prv = private(self._api_key, self._api_secret)

    # public APIを使用してティッカー情報を取得するメソッド
    def get_ticker(self,ticker_ptn,pair):
        value = self._pub.get_ticker(pair)
        return value[ticker_ptn]

    # 注文価格と注文数量を返すメソッド
    def set_order_data(self,ticker_ptn,pair):
        buy_price = int(self.get_ticker(ticker_ptn,pair))
        order_num = (self._adjust_order_num / (buy_price - self._adjust_price))
        order_num = round(order_num, 8)
        order_price = buy_price - self._adjust_price
        return order_num,order_price

    # private APIを使用して指値注文するメソッド
    def spot_order(self,ticker_ptn,pair):
        order_num,order_price = self.set_order_data(ticker_ptn,pair)
        order_num = str(order_num)
        order_price = str(order_price)
        response = self._prv.order(
            pair, # ペア
            order_price, # 価格 (成行注文の場合は None にする)
            order_num, # 注文枚数
            ticker_ptn, # 注文サイド (buy|sell)
            'limit', # 注文タイプ (limit|market|stop|stop_limit)
            # 以降は任意の引数
            False, # post_only 注文、デフォルトは False, None も可能で Falseと同じ挙動
            False # trigger_price 逆指値などのトリガー価格
        )
        return response

    # 注文日時(UnixTimeのミリ秒)をローカル時間に変換するメソッド
    def conv_unixtime(self,unixtime):
        data = unixtime / 1000
        dt = datetime.datetime.fromtimestamp(data).strftime('%Y/%m/%d %H:%M:%S')
        return dt

if __name__ == '__main__':
    # インスタンス生成
    use_bitbank_api = UseBitbankApi()
    # 指値注文時の例外処理判定
    try:
        # btc_jpy指定で指値注文する
        response = use_bitbank_api.spot_order('buy','btc_jpy')
    except Exception as e:
        # エラーメッセージ標準出力(Pytoの場合)
        print(e)
    else:
        # 注文日時(UnixTimeのミリ秒)をローカル時間に変換する
        response['ordered_at'] = use_bitbank_api.conv_unixtime(response['ordered_at'])
        # jsonエンコード
        response_json = json.dumps(response)
        # shortcutアプリでpytoの実行結果を取得(Get Script Output)するためには
        # print関数で出力する(Pytoの場合)
        print(response_json)
```

## まとめ
* bitbankのAPIを使用してbtc_jpyの価格を参照し、指定価格で指値注文するPythonコードを作成し、そのコードをShortcutsアプリから実行することで指値注文を行うショートカット(a-Shell,Pyto)について紹介しました。
* a-ShellではShortcutsアプリとの連携のしづらさはありますが、無料であり、Python以外のコードも実行できるため十分な機能をサポートしていると思います。
* PytoではShortcutsアプリ用に入力と出力に対応したアクションがあるためショートカットを作成しやすい点がありました。アプリは有料ですが、Pyto LibraryやExample等、ドキュメントがまとめられているため実装のしやすさはあります。
* 本記事では指値注文のコードを実行していますが、オートメーションと連携すれば定期実行することで積立にも対応できると思います。
* その他、指値注文するコードとしてGoogle Apps Scriptベースのコードも作成していますので、興味があれば確認してみてください。([GitHub / gasBitbankSpotOrder](https://github.com/7rikazhexde/gasBitbankSpotOrder))