0件ヒットしました

    シンプルなbotで裁量トレードを補助する環境を作ってみよう!という記事です。 「サーバーレス」 がキーワードになります。

    はじめに

    こんにちは。暗号通貨バブル楽しんでますか?紫藤かもめです。

    この記事は毎年恒例の仮想通貨botter Advent Calendarの三日目に寄稿しています。とりまとめは大物botterの @hohetoさんです。いつもありがとうございます!

    去年のアドベントカレンダーも非常に良い記事が多かったので、未読の方はぜひ一読をお勧めします!

    去年の記事の中では、個人的には、なかなか目にすることのできないガチ勢のハイレベルな記事として特に Rosさん の書かれた記事がおすすめです!

    自己紹介

    暗号通貨 + トレードな人間 @shidokamo です。 昨年はDEX-CEXアビトラについて「DEXトレードの思ひ出」という記事を投稿し好評を頂きました。

    簡単に言うと

    今年はガチ勢ではなくても動かせるような、裁量トレードを手軽に自動化する手法について語ってみたいと思います。

    目標としては、

    1. とにかくシンプルに。簡素なコードで
    2. クラウドベンダーの便利なサービスをフルに活用する
    3. 各自応用が出来るように。汎用的なアイディアを紹介
    4. 間口を広く。複雑なプログラミングが苦手な人でも使えるように

    です。

    少し詳しい人向けに言うとbotの動作のイメージとしては、かつてFTXと言う取引所で、 一世を風靡した Quant Zone と言う機能のようなもの実現することを目指します。 一定のルールに従って、常に定期的に動作をさせます。 ご存知のように、FTXは資金管理の問題から巨額の負債を残し経営破綻しましたが、 取引所としては使いやすい機能が多く、Quant Zoneという機能は他に類を見ない野心的な機能で、 一部から大きな支持を得ていました。

    サーバーレスbotとは

    通常のbotはサーバーなりPCなりでコードを走らせて起動するかと思います。

    ここを本botではまずクラウドサービスにコードをデプロイします。 そしてそのデプロイしたコードのHTTPエンドポイントを発行してもらいます。 HTTPエンドポイントへアクセスすることがトリガになってコードを実行します。 自分のコードをサービス化する訳ですね。自分のサーバーでコードを実行しないからサーバーレスです。

    実はアドベントカレンダーを昨日担当された QASHさん の書かれた記事でもサーバーレスbotのことが軽く紹介されていました。 この記事は更にこれを深掘りするような形になりますので、図らずもコラボを実現してしまったということで嬉しく思います!

    使用するクラウドサービス

    本記事ではサーバーレスなコード実行サービスとして定評のある Google Cloud Functions を使っていきたいと思います。 またCloud Functionsには第一世代と第二世代があるのですが、本記事では 第二世代(Gen-2) を使います。

    Gen-2は内部的にはCloud Runというコンテナ実行サービスです。 botのコードをデプロイすると実際はGCPがコンテナをデプロイして実行環境を整えてくれます。 特にその辺を意識する必要はありません。 ただ、コードを実行する便利な環境をGoogleが提供してくれると考えればオッケーです!

    その他にもGCPの便利な機能を使っていきます。 本記事のような単純なbotであれば、基本的に無料枠の範囲で実行できるはずです。

    全体の動作のイメージ

    Image from Gyazo

    サーバーレスの利点

    • コードを実行している時しか課金されない
    • 実行環境の信頼性が高い
    • ログ監視環境が整備されている

    こんな辺りが利点となると思います。

    反面、状態を持たせる処理や低遅延を重視する処理などは苦手であると思います。 基本的にはシンプルな処理に使って行くもの かと思いますので、本記事のbotもシンプルなものとします。

    botの仕様

    今回設計するbotは簡単に言うと以下の様な仕様で動作させます。

    1. 種銭となるUSDTを用意する
    2. BTC/USDT市場で買えるだけビットコインを買う
    3. 買ったビットコインを担保にUSDTを規定のレバレッジまで借りる
    4. 借りたUSDTでビットコインを買う
    5. ビットコインが値上がりしたらさらにUSDTを規定のレバレッジまで借りる
    6. 借りたUSDTでビットコインを買う
    7. 以下ループ
    8. ある値段以上になったら注文をやめる。また、ビットコインは常に規定の売り指し値を出しておく。

    当然ならが、ビットコインが値下がりしすぎると、担保が精算され全ての資金を失います。 この知的レベルの低さを戒めるため私は単にこれを Apeボット と呼んでいます。

    ちなみに、マイクロストラテジーというビットコイン界隈で大変有名な、 ビットコイン狂信者がCEOを務めていた会社が似たような手法で大量のビットコインを保有していますね。 (彼はCEOを辞任しましたが実質的には未だに議決権を掌握して、同じ手法を取り続けているようです。)

    常に値上がりを監視して、このような処理を適切に手動で行うのは面倒なので、自動化する意味はあります。

    ちなみに、「USDTを借りてビットコインを買う」と書くと面倒そうに思えますが、 現在の取引所では、現物マージン取引市場でビットコインを買うと、 自動で借金をしてレバレッジをかけて現物を買えるような機能 があり、それを使えば特に難しいことはありません。 (「自動で借金をしてレバレッジをかけて」と書くとやってることのヤバさが引き立ちますね。)

    (余談)なぜこのようなボットを運用するか?

    ビットコインは一番星の生まれ変わりの究極のアイドルだからです!

    僕としてはクリプト投資の魅力はやはり投機的なアップサイドの大きさにあると思っています多くのアセットと違い、利回りのないビットコインなどの暗号通貨は悪く言えばその場の雰囲気でフィアット建ての価格付けが行われるので、時にとんでもないミスプライスも絡んで猛烈な価格上昇をすることが度々あるのは皆さんもご存知の通りですよねしかし、近々価格が大きく上昇しそうだと考えたとしても、それがいつ起こるのかは基本的には誰にもわからない、そうですよね?ですから許容できる最大の範囲で常にエクスポージャーを高めておくというのは投資としては王道ではないしょうか!?

    嘘です。ギャンブルがしたいだけです。

    ペーパーシミュレーション

    bot を作る前に表計算でペーパーシミュレーションをしてみます。

    Google Spread Sheet

    シートの使い方ですが、まず右端に必要な情報を入れます。

    Image from Gyazo

    すると、推定精算価格や予想される利益などを計算してれます。

    Image from Gyazo

    このシートの特徴ですが、

    • 値上がりに応じた区間ごとに資産の変動について粗く計算
    • 区間ごとに金利もある程度計算してコストとして算出
    • レバレッジを徐々に下げていくようシミュレーションが可能

    となっております。(かなり先の区間まで計算されていますが、実際に運用する場合は当然早めに利確をすることになります)

    実際に運用した場合とシミュレーションの差があまりないことを確認していますので、大きな間違いはないと思います。 (精算価格は実際はずれると思います。)もしよかったら少し値を変えて遊んでみてください。

    レバレッジは徐々に下げていくのが実用上は大事だと思います。ほんの少し下がるだけでもかなり違いますからね。

    コード

    さて前置きも終わったところで早速コードを紹介します

    GitHubのレポジトリはこちら

    main.py がデプロイされるコードです。この記事にもコードを貼っておきます。

    • 少し丁寧なログ処理を入れてあるのでコードが長くなっていますが構成は非常に単純です。runという関数だけ見てください。
    • 環境変数で設定を変更します。基本的に先ほどのスプレッドシートに対応した変数やAPIキー等を入れるだけです。
    • 1回だけ状態の確認と発注を行なって終了します。

    クライアントもシンプルにベタ書きしています。 最新版ではもっとスマートになっていますが、安定版の方のコードを載せました。 (既知の問題:売り指値のキャンセルや変更処理がないので、指し値を変えて再起動しても反映されません)

    import base64
    import hmac
    import datetime
    import urllib.parse
    from typing import Optional, Dict, Any, List
    
    from requests import Request, Session, Response
    import os
    import sys
    import logging
    import json
    import traceback
    
    class FormatterJSON(logging.Formatter):
        def format(self, record):
            json_msg = {'message': record.msg}
    
            # Practically, we should add timestamp. However, cloud functions will add their own timestamp so it's commented out
            # record.asctime = self.formatTime(record, self.datefmt)
            # json_msg['time'] = record.asctime
    
            json_msg['level'] = record.levelname
            json_msg['severity'] = record.levelname
            return json.dumps(json_msg, ensure_ascii=False)
    
    #
    # Caution: This will overwrite record itself so if you are using multiple logger, they will be also affected.
    #
    RESET_SEQ = "\x1b[0m"
    class FormatterColor(logging.Formatter):
        def color(self, level):
            match level:
                case 'WARNING':
                    return "\x1b[1;43m" + level + RESET_SEQ
                case 'INFO':
                    return "\x1b[1;42m" + level + RESET_SEQ
                case 'DEBUG':
                    return "\x1b[1;47m" + level + RESET_SEQ
                case 'CRITICAL':
                    return "\x1b[1;41m" + level + RESET_SEQ
                case 'ERROR':
                    return "\x1b[1;41m" + level + RESET_SEQ
                case _:
                    # If it's already colored. Do nothing.
                    return level
    
        def format(self, record):
            record.levelname = self.color(record.levelname)
            return super().format(record)
    
    logger = logging.getLogger('console')
    if os.environ.get("PROD"):
        h = logging.StreamHandler()
        fmt = FormatterJSON()
        h.setFormatter(fmt)
        logger.addHandler(h)
    else:
        h = logging.StreamHandler()
        fmt = FormatterColor('[%(asctime)s] [%(levelname)s] %(message)s')
        h.setFormatter(fmt)
        logger.addHandler(h)
    logger.setLevel(logging.DEBUG)
    
    class OkxClient:
        _ENDPOINT = 'https://www.okx.com/api/v5/' # Don't forget last '/'
    
        def __init__(self, api_key=None, api_secret=None, api_pass=None) -> None:
            self._session = Session()
            self._api_key = api_key
            self._api_secret = api_secret
            self._api_pass = api_pass
    
        def _get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
            return self._request('GET', path, params=params)
    
        def _post(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
            return self._request('POST', path, json=params)
    
        def _delete(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
            return self._request('DELETE', path, json=params)
    
        def _request(self, method: str, path: str, **kwargs) -> Any:
            request = Request(method, self._ENDPOINT + path, **kwargs)
            self._sign_request(request)
            response = self._session.send(request.prepare())
            return self._process_response(response)
    
        def _sign_request(self, request: Request) -> None:
            ts = datetime.datetime.utcnow().isoformat(timespec='milliseconds') + 'Z'
            prepared = request.prepare()
            signature_payload = f'{ts}{prepared.method}{prepared.path_url}'.encode()
            if prepared.body:
                signature_payload += prepared.body
            signature = hmac.new(self._api_secret.encode(), signature_payload, 'sha256').digest()
            signature = base64.b64encode(signature)
            request.headers['OK-ACCESS-KEY'] = self._api_key
            request.headers['OK-ACCESS-SIGN'] = signature
            request.headers['OK-ACCESS-TIMESTAMP'] = str(ts)
            request.headers['OK-ACCESS-PASSPHRASE'] = self._api_pass
    
        def _process_response(self, response: Response) -> Any:
            try:
                data = response.json()
            except ValueError:
                response.raise_for_status()
                raise
            else:
                return data['data']
    
        def get_balance(self, coin: str) -> dict:
            return self._get(f'account/balance', {'ccy':coin})
    
        def place_limit_order(self, market: str, side: str, size: str, mode: str, price: str, post_only:bool) -> dict:
            return self._post('trade/order', {
                                         'instId': market,
                                         'tdMode': mode,
                                         'side': side,
                                         'sz': size,
                                         'px': price,
                                         'ordType': 'post_only',
                                         })
    
        def place_market_order(self, market: str, side: str, size: str, mode: str) -> dict:
            return self._post('trade/order', {
                                         'instId': market,
                                         'tdMode': mode,
                                         'side': side,
                                         'sz': size,
                                         'ordType': 'market'
                                         })
    
        def get_balances(self) -> List[dict]:
            return self._get('account/balance')[0]
    
        def get_positions(self, show_avg_price: bool = False) -> List[dict]:
            position_list = self._get('account/positions')
            return {position['instId']:position for position in position_list}
    
        def get_account_position_risk(self, type) -> List[dict]:
            return self._get('account/account-position-risk', {'instType': type})
    
        def get_order_book(self, market:str) -> dict:
            return self._get('market/books', {'instId': market, 'sz':'100'})[0]
    
    # Note:
    # In this case, we don't need to take care of performance but if is the best practice to use global variable
    # for reusable objects like client object so that functions can cache it for next call.
    key = os.environ.get("API_KEY")
    if not key:
        raise Exception("API key is not provided")
    secret = os.environ.get("API_SECRET")
    if not secret:
        raise Exception("API secret is not provided")
    passphrase = os.environ.get("API_PASS")
    if not passphrase:
        raise Exception("API passphrase is not provided")
    leverage_init = float(os.environ.get("LEVERAGE_INIT"))
    if not leverage_init:
        raise Exception("Leverage init value is not provided.")
    leverage_ref_price = float(os.environ.get("LEVERAGE_REF_PRICE"))
    if not leverage_ref_price:
        raise Exception("Leverage reference price is not provided.")
    leverage_decay = float(os.environ.get("LEVERAGE_DECAY"))
    if not leverage_decay:
        raise Exception("Leverage decay parameter not provided.")
    leverage_min = float(os.environ.get("LEVERAGE_MIN"))
    if not leverage_min:
        raise Exception("Minimum leverage is not provided.")
    max_init_price = os.environ.get("MAX_INIT_PRICE")
    if not max_init_price:
        raise Exception("Max init price is not provided.")
    quote = os.environ.get("QUOTE")
    if not quote:
        raise Exception("Quote coin name is not provided")
    if not quote in ['USDT']:
        raise Exception("Quote coin %s is not supported" % quote)
    base = os.environ.get("BASE")
    if not base:
        raise Exception("Base coin name is not provided")
    if not base in ['BTC', 'ETH']:
        raise Exception("Base coin  %s is not supported" % base)
    order_size = os.environ.get("ORDER_SIZE")
    if not order_size:
        raise Exception("Order size is not provided.")
    no_order_limit = os.environ.get("NO_ORDER_LIMIT")
    if not no_order_limit:
        raise Exception("No order limit is not specified.")
    take_profit_limit = os.environ.get("TAKE_PROFIT_LIMIT_PRICE")
    if not take_profit_limit:
        raise Exception("take profit limit is not specified.")
    
    client = OkxClient(key, secret, passphrase)
    
    def run(requests=None) -> None:
        global client
        global key
        global secret
        global passphrase
        global leverage_init
        global leverage_ref_price
        global leverage_decay
        global leverage_min
        global quote
        global base
        global order_size
        global no_orderr_limit
        global take_profit_limit
        try:
            spot_market =  base + '-' + quote
            logger.info("Spot market: %s" % spot_market)
    
            balances = client.get_balances()
            positions = client.get_positions()
            risks = client.get_account_position_risk('MARGIN')
    
            def get_coin_balance(balances) -> List[dict]:
                return {coin['ccy']:coin for coin in balances['details']}
            coins = get_coin_balance(balances)
            logger.debug(balances)
            logger.debug(coins)
            logger.debug(positions)
            logger.debug(risks)
    
            def get_leverage(balances) -> float:
                return float(balances['notionalUsd']) / float(balances['totalEq'])
            leverage_est = get_leverage(balances)
    
            def get_liq_price(balances, risks, coins) -> float:
                # Use cashBal for caluculation. Otherwise, it becomes huge.
                return (float(balances['mmr']) - float([x for x in risks[0]['balData'] if x['ccy'] == quote][0]['eq'])) / float(coins[base]['cashBal'])
    
            market = "%s-%s"%(base,quote)
    
            b = client.get_order_book(market)
            ask_best = b['asks'][0][0]
            bid_best = b['bids'][0][0]
    
            def get_desired_leverage():
                return max(leverage_init * ((leverage_ref_price / float(ask_best)) ** leverage_decay), leverage_min)
            leverage = get_desired_leverage()
    
            # Account is flesh or very few coin is remaining
            if (not base in coins or float(coins[base]['eqUsd']) < 1) and float(bid_best) > float(max_init_price):
                logger.warning("Init order can't be placed. Best bid > MAX_INIT_PRICE:%s" % max_init_price)
            # No order threshold
            elif float(bid_best) > float(no_order_limit):
                logger.warning("Best bid is greater than no order limit price. No buying order is made.")
            elif leverage_est / leverage < 0.99:
                logger.warning("There is buying power available. Put order")
                r = client.place_market_order(market=market, side="buy", size=order_size, mode='cross')
                logger.debug(r)
    
            # Sell orders
            if base in coins:
                if float(coins[base]['availBal'])*float(bid_best) < 1:
                    logger.warning("There is no coins available for sell. Maybe we already placed limit order for all coins.")
                else:
                    r = client.place_limit_order(market=market, side="sell", price=take_profit_limit, size=coins[base]['availBal'], post_only=False, mode='cross')
                    logger.debug(r)
    
            def show_info() -> None:
                # Important information last
                logger.info("Initial Margin USD             : %s" % balances['imr'])
                logger.info("Maintenance Margin USD         : %s" % balances['mmr'])
                if balances['mgnRatio']:
                    logger.info("Margin Ratio %%                 : %f" % (float(balances['mgnRatio']) * 100))
                if base in coins:
                    logger.info("%-10s balance free        : %s" % (base, coins[base]['availBal']))
                    logger.info("%-10s balance total       : %s" % (base, coins[base]['cashBal']))
                    logger.info("%-10s balance USD         : %s" % (base, coins[base]['eqUsd']))
                if quote in coins:
                    logger.info("%-10s balance free        : %s" % (quote, coins[quote]['availBal']))
                    logger.info("%-10s balance total       : %s" % (quote, coins[quote]['cashBal']))
                    logger.info("%-10s balance USD         : %s" % (quote, coins[quote]['eqUsd']))
                logger.info("Total notional value USD       : %s" % balances['notionalUsd'])
                logger.info("Total equity USD               : %s" % balances['totalEq'])
                logger.info("Best ask                       : %s" % ask_best)
                logger.info("Best bid                       : %s" % bid_best)
                logger.info("Sell order price               : %s" % take_profit_limit)
                if base in coins and quote in coins:
                    liq_price_est = get_liq_price(balances, risks, coins)
                    logger.info("Estimated Liq Price            : %f" % liq_price_est)
                logger.info("Current target leverage        : %s" % leverage)
                logger.info("Leverage                       : %f" % leverage_est)
                logger.info("Total net USD                  : %s" % balances['adjEq'])
            show_info()
    
    
            return 'OK' # HTTP request return value should be specifi
        except Exception as e:
            logger.exception("Exception in run command.")
            exc_info = sys.exc_info()
            logger.exception(traceback.format_exception(*exc_info))
            return 'ERROR' # HTTP request return value should be specifi
    
    if __name__ == "__main__":
        run()

    ローカルでの実行

    以下が必要です。

    • 必要な環境変数をセット(僕は .env というファイルに書いています)
    • ライブラリをインストール (requests だけ必要です。僕は、pipenv という仮想環境を使っています。pipenv.env も自動で読み込んで変数をセットしてくれます。

    ローカルでの実行結果

    うまくいくと以下のようなログが出ます。 ここでポイントは自分にとって重要なログほど後に出すことだと思います(運用時にパッとわかるので)。 以下の実行結果では、注文余力があったので10ドル分成行注文をして終了しました。

    Image from Gyazo

    Cloud Functionsは基本的に状態を持たせずにステートレスな処理を実行するものだと思いますので、 このbotもステートレスな処理を一度だけ実行します。 状態は全て取引所側から起動後に取得することを期待しています。 (そのため取引所側がおかしくなったらまずいことになります。祈りましょう🙏)

    もう一度実行します。

    Image from Gyazo

    10ドル分ビットコインが増えていますね。想定通りです。

    デプロイ

    コマンドでサクッとデプロイします。僕は Makefile からデプロイしています。 これで、bot をサービス化して、実行用のHTTPエンドポイントをゲットできます。

    Image from Gyazo

    設定の確認

    ブラウザで Web コンソールに行くと環境変数の確認などが出来ます。 ここから、環境変数を変更して再度デプロイなどもできるのでとても便利です。

    Image from Gyazo

    スケジューラの登録

    これもコマンドでサクッと登録するか、もしくは手動で登録できます。 内部的には cron を使ってるみたいなので、最小で1分間隔で実行できます。それ以下には出来ません。

    実行権限周りでハマるかもしれません。GCP全体として、権限周りはかなり機能が多いので…

    Image from Gyazo

    ちなみに bot を止めたくなったら、このスケジューラを停止すればいいだけなので簡単です。 このスケジューラを登録するともうbotは早速1分間隔で実行され始めます。 ちなみに、5分間隔やもっと長く間隔を取って実行してもこのbotは問題ないと思います。

    Image from Gyazo

    高値を更新するたびにがんがんビットコインを買ってくれます。がんばれ〜。

    実行ログの確認

    GCPの Web コンソールから実行ログを確認できます。

    Image from Gyazo

    はい。順調に借金を増やしてくれていますね。(USDTの残高に注目)

    ちなみに JSON 型のログは自動でパースしてくれてこんな風に見れます。

    Image from Gyazo

    また、スマホにGCPのアプリを入れちゃったりして同様のログを携帯からこまめに確認したりも出来ます。 この辺りから大分クラウドの恩恵が出て来ますね。

    ログの解析

    さて、GCPは、Botが出力したログを自動でパースしてデータベースに入れてくれます。 これが大変便利ですので最後にこれを紹介したいと思います。

    ログエクスプローラというWebインターフェースに行くと、色々な機能を使う事ができるんですが、 ここで、シンプルなクエリ機能を使う事ができます。

    例えば、こんな感じでメッセージから特定の文字列を抽出したりできます。こんなクエリを書くと、

    Image from Gyazo

    実行結果がこれ。

    Image from Gyazo

    想定通り BTC を1分間隔で買って行って、ある時点で買うのをやめているようですね。

    清算価格とレバレッジを見てみましょう。

    Image from Gyazo

    Image from Gyazo

    大体想定通りに動いているようです。(資金に対する注文サイズが大きすぎるのでレバレッジは設定より高くなっています)

    コマンドからログを取得

    当然ながら、このクエリにはAPIも用意されていて、スクリプトから取得することも出来ます。

    例えばbotが記録している以下の構造化ログから、BTCの残高を抜き出して、PandasのDataFrameを作ってみます。 (これは流石にGoogleの提供しているクライアントを使っています)

    このようなログを抜き出すために、

    Image from Gyazo

    このコードで

     from google.cloud import logging
     import pandas as pd
     
     client = logging.Client()
     logger = client.logger("test")
     
     entries = client.list_entries(
         max_results=100,
         filter_ = 'resource.labels.service_name="YOUR_FUNCTIONS_NAME" AND jsonPayload.message.BTC:*'
    )
     
     data = []
     for e in entries:
         time = e.timestamp
         balance = e.payload["message"]["BTC"]["cashBal"]
         data.append((time, float(balance)))
     
     df = pd.DataFrame(data, columns=["time", "BTC balance"])
     df.set_index("time", inplace=True)
     df.to_pickle("data.pkl")
     print(df)

    実行結果がこれ。

    Image from Gyazo

    問題なくDataFrameが作れていますね。

    このようなクエリをスケジューラから定期的に実行して何か処理を行うのも良いですね。 ちなみにクエリにレートリミットあるので、ほどほどに実行するのが吉です。

    まとめ

    お手軽サーバーレスbotを紹介しました。 明日は更にこのbotにChat-GPTを組み合わせて遊んでみる記事を紹介したいと思います!

    注意

    このbotはあくまで遊びのためのものですので、運用したことによる損失については一才責任を負いません。

    遊びは遊びとして、全損しても構わない金額だけを証拠金にしてく遊びましょう。 また取引所については可能な限り信頼性の高く流動性の大きい取引所を使いましょう。 取引所のハッキングや諸々のリスクなどを考えてきちんと資金を分散してください。 また、サブアカウント等を使って資金を適切に隔離することも言うまでもありません。

    レバレッジの上限はbotで指定されていますが、取引所側でもきちんと設定しましょう。

    補足:先物(無期限先物)でやらないの?

    先ずは現物を買いその上でUSDTを借りる方が、金利コスト面で有利なのではないかと考えます。 これが成り立つのはビットコインが担保として優秀だからです。 ビットコインはCollateral Ratioが基本的に100%であることが多いので、担保としてフルに活用できます。 (ちなみに忘れたんですが、FTXはBTCの Collateral Ratio が100%じゃなかった様な気がします…)

    補足:クライアント

    自前で取引所のクライアントを用意していますが、 普通であれば まちゅけん さんが中心となって開発されている pybotters のようなライブラリを使って効率していくのが王道です。

    今回は、これぐらいシンプルなものであれば自前でクライアントを用意しておくのもフットワークが軽くていいかなと思いベタ書きしています。 例えば、何か急にチャンスが目の前に転がり込んできたとして、 「ライブラリが対応してないからできません」ということになっては非常にもったいないですからね。

    © 紫藤かもめ. All rights reserved.