FIREして市場に落ちている金を拾うブログ

30代でFIREし、プログラムを勉強して米国株式と仮想通貨で落ちている金をせっせと拾う記録。

Websocket実装に向けて非同期処理を勉強する(WebSocket)

前回に引き継ぎ、まちゅけんさんの記事を見ながら勉強。今回はWebSocket。
WebSocketの記事は流し読みして、pybotterライブラリを使うところにいきなり突入。

これまでWebSocketがうまく動作しなかった理由がようやくわかる

これまで、いろんなラッパーを使おうとするも、どうしてもエラーが出て使えなかったが、ようやくわかった。
Jupyter NotebookとPythonでは、微妙に非同期処理の使い方が違うらしい。

import asyncio
import pybotters

apis = {"bybit": ["YOUR_API_KEY", "YOUR_API_SECRET"]}

async def main():
    async with pybotters.Client(base_url="https://api.bybit.com", apis=apis) as client:
        r = await client.get("/v2/public/tickers", params={"symbol": "BTCUSD"})
        data = await r.json()
        price = round(float(data["result"][0]["bid_price"]) - 300.0, 1)

        r = await client.post(
            "/v2/private/order/create",
            data={"symbol": "BTCUSD", "side": "Buy", "order_type": "Limit", "qty": 1, "price": price, "time_in_force": "PostOnly"},
        )
        data = await r.json()
        print(data)
 
asyncio.run(main())

上記コードをFTX用に書き換えて実行したら、下記エラーが出た。

asyncio.run() cannot be called from a running event loop

なんでか調べたところ、こちらに行き着いた。

The asyncio.run() documentation says:

This function cannot be called when another asyncio event loop is running in the same thread.

The problem in your case is that jupyter (IPython) is already running an event loop (for IPython ≥ 7.0):

You can now use async/await at the top level in the IPython terminal and in the notebook, it should — in most of the cases — “just work”. Update IPython to version 7+, IPykernel to version 5+, and you’re off to the races.

That's the reason why you don't need to start the event loop yourself in jupyter and you can directly call await main(url) even outside asynchronous functions.

In jupyter

async def main():
print(1)

await main()
In plain Python (≥3.7)

import asyncio

async def main():
print(1)

asyncio.run(main())

Jupyter Notebookでは、プログラムとは別にEvent Loopを回しているようで、
それとは別にEvent Loopにタスクを突っ込んで回そうとしてもだめらしい。
今回は最終的にPythonとして実行するため、Jupyter NotebookではなくPythonで開発するように変更した。

エンドポイント設定でハマる

Pythonでサンプルコードを変えて入れていたが、ずっとNot Logged Inとエラーが出てハマる。
FTX側のホワイトリスト設定も見てみるが、特に問題はない。
どうもわからなかったので、もしかしてPybottersのAuthentificationにミスが有るのでは?と思って見るがバグを発見できず。

しばらく試行錯誤して、base_url = 'https://ftx.com/api/'となっており、r = await client.get('/positions')としていたことに気づく。
もしかして"/"が重複してるのでは?と思い片方削ると無事成功。
えー、、、URL間違えてもNot Logged Inって出るの・・・。Authentificationミスのときだけそのエラーが出てほしかった。

self設定でハマる

なんか引数が足りんと言われるなぁ・・・と思ってると、Classの中の関数で、selfを引数に設定し忘れていた。

そんなわけて一応WebSocket動作

import time
import asyncio
import pybotters

class FTX():
    # 定数
    URLS = {'REST': 'https://ftx.com/api/',
           'WebSocket': 'wss://ftx.com/ws/',
          } 

    def __init__(self, api_key = None, api_secret = None, subaccount = None):
        # APIキー、Secretをセット
        self.api_key = api_key
        self.api_secret = api_secret
        self.subaccount = subaccount    
        self.apis = {
            'ftx': [self.api_key, self.api_secret],
        }

    # ------------------------------------------------ #
    # WebSocket
    # ------------------------------------------------ #
    async def main(self):
        async with pybotters.Client(base_url = self.URLS['REST'], apis = self.apis, headers={'FTX-SUBACCOUNT': self.subaccount}) as client:
            symbol = 'BTC-PERP'
            await client.ws_connect(
                self.URLS['WebSocket'],
                send_json=[
                    {'op': 'subscribe', 'channel': 'orders'},
                    {'op': 'subscribe', 'channel': 'fills'},
                ],
                hdlr_json=store.onmessage,)

            while True:
                start = time.time()
                await store.orders.wait()
                orders = store.orders.find()
                print(orders)
                await asyncio.sleep(5)
                end = time.time()
                elpsed_time = end - start
                print(elpsed_time)

try:
    _api_key = ''
    _api_secret = ''
    _subaccount = ''
    
    ftx = FTX(_api_key, _api_secret, _subaccount)
    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())  # need windowsOS python3.8
    asyncio.run(ftx.main())

except Exception as e:
    print('=== エラー内容 ===')
    print(f'{str(e)}')

except KeyboardInterrupt:
    pass

あとはこいつを使ってラッパー化していくつもり。