owned this note
owned this note
Published
Linked with GitHub
# fin-pyコードリーディング会#4 :bookmark_tabs:
## 概要
### fin-pyについて
https://github.com/fin-py/guideline
#### 次回のイベント
TBD
### Slack
https://docs.google.com/forms/d/e/1FAIpQLSd9oVlrCMHEuD3PN0x3QcMgeQGy6Sj90d6uP1CQXQnArX9YqQ/viewform
### 参加方法
:::info
fin-pyコードリーディング会#4
終了
:::
:::warning
**重要**
Zoomに参加したら、[出欠確認](https://hackmd.io/bFBFaPbYS1Kqfc97HMlp7Q?both#%E5%87%BA%E5%B8%AD%E7%A2%BA%E8%AA%8D)にチェックを入れてください
:::
### 前回の様子
- [#1](https://hackmd.io/F8-MqnTSQNyaUI1fR7twMQ?view)
- [#2](https://hackmd.io/Lph_Z6Z2RYeRr1x6_HOopQ?view)
- [#3](https://hackmd.io/L2p6b7obSzq0EbUYi7Fh0w?view)
### タイムテーブル :clock1:
1. 発表者枠が話す: 各自20-30min
1. クロージング(コードを読んだ感想など): 20min
## コードリーディング
### 読むコード
[store.py](https://github.com/MtkN1/pybotters/blob/main/pybotters/store.py)
発表者のかたは、コードを読んだ際のメモ書きなどを下記に記載してください
ほかの形式でまとめたかたは要点(もし可能であれば資料のリンク)を記載してください
### 参考情報
- [botterのためのasyncio](https://zenn.dev/mtkn1/articles/c61e77c1d221aa#fn8)
- [AIOHTTPハンズオン ドキュメント](https://aiohttp-hands-on.readthedocs.io/)
## 自己紹介・コメント・メモ
- にぎやかし枠、発表者枠のかたは下記に自己紹介を記入してください
- pybottersを使っているかなどの情報があると嬉しいです
- イベントのフィードバックや感想などを追記していただけると助かります
### どりらん
- Twitter: [patraqushe](https://twitter.com/patraqushe)
- オプションをトレードしてます
- 感想
---
### しんせいたろう
- Twitter: [shinseitaro](https://twitter.com/shinseitaro)
- 米国株とか売ったり買ったりしてるひと。pythonは長く書いてるわりに分かってないひと。クラスとかいまだ苦手
- 感想
---
### やろころ
- FX、米国株、オプションをちょっとやってます
---
### AAACC
- FXをちょっとやってました。Pythonの基礎を勉強中です。
- 同時にpybottersのサンプルを見ながら、まちゅけんさんの環境構築Note(VScode+Fedora)などを参考に動かしはじめています。
- 上手に使うには、取引所の仕様に合わてデータの扱い方(DataStoreなど?)を自分で作っていく必要があるようなので、そのためには中身も理解していかなければと痛感しており、勉強していきたいと思っております。
- 将来的には、金融に関わらず、自分でパッケージをつくれるようになりたいため、そのためにも構造などを勉強していきたいです。
---
### まちゅけん
- Twitter: [MtkN1XBt](https://twitter.com/MtkN1XBt)
- pybottersの開発をしたりシストレをしています💪 本業はSEです。
---
### もりもと
- [BizPy](https://bizpy.connpass.com/) 主催者、しばらくお休みしてたけど活動を再開
- マイクロ法人でお仕事している
- 最近お仕事が空いたので自社のコンテンツ/プロダクト作りのための調べものしたりしている
---
## 発表者
### もりもと
* リビジョン: f90a05b1 (v0.7.1)
#### DataStore
##### 抽象基底クラス
DataStore を直接インスタンス化して使うことを意図していないなら [abc](https://docs.python.org/ja/3/library/abc.html) を使って抽象基底クラスにすると、利用者に継承して使うものであることを設計者の意図として伝えられる。
```python=
from abc import ABC
class DataStore(ABC):
...
```
抽象基底クラスは、設計者の意図を伝えられるというメリットだけでなく、例えば `@abstractmethod` でサブクラスのモデルそれぞれで実装が必要なメソッドを明示して、未実装の場合は、インスタンス化したときにエラーにしたりできる。テストとも相性がよいはず。
動作確認。
```python=
>>> from abc import *
>>> class A(ABC):
... @abstractmethod
... def myfunc(self):
... pass
...
>>>
>>> class B(A): pass
...
>>> b = B()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class B with abstract methods myfunc
```
##### 未使用のクラス変数
クラス変数の `_KEYS` からインスタンス変数を初期化するのはあまり意義があるようにはみえないかな。明示的に `__init__` で keys を渡すか、`_init` で渡すようなやり方の方がいいのでは? `self._KEYS` を直接参照しているコードもなさそうだし。
```python=
_KEYS = []
...
self._keys: Tuple[str, ...] = tuple(keys if keys else self._KEYS)
```
##### iter のラッピング
`iter` いるのかな?と思ったけど、これは型チェックのために必要なのかな?
```python=
def __iter__(self) -> Iterator[Item]:
return iter(self._data.values())
```
```
FAILED tests/test_store.py::test__iter__ - TypeError: iter() returned non-iterator of type 'dict_values'
```
##### KeyError のロギング
一般論として例外を無視するコードはよくない。
* https://ja.wikisource.org/wiki/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9E%E3%81%8C%E7%9F%A5%E3%82%8B%E3%81%B9%E3%81%8D97%E3%81%AE%E3%81%93%E3%81%A8/%E3%82%A8%E3%83%A9%E3%83%BC%E3%82%92%E7%84%A1%E8%A6%96%E3%81%99%E3%82%8B%E3%81%AA
どうしても仕方ないときは [logging.debug](https://docs.python.org/ja/3/library/logging.html#logging.debug) などを使って、通常 (infoレベル) はログ出力しないけど、調査したいときに切り替えられるようにしておくとまだよいとは思う。
```python=
try:
keyitem = {k: item[k] for k in self._keys}
except KeyError:
pass
else:
...
```
##### wait 処理
wait を呼び出したときに `await event.wait()` でコルーチンが切り替わっているときに ret が変更されることを期待しているコードにみえる。イベントの登録と値の取得は queue のようなものでやった方が保守しやすいコードになるような気がする。
少なくともこのコードだけ読んだら ret は空リストが返るようにしか読めない。
```python=
async def wait(self) -> List[Item]:
event = asyncio.Event()
ret = []
self._events[event] = ret
await event.wait()
del self._events[event]
return ret
```
#### DataStoreManager
これも一般論として `Manager` という名前は役割が曖昧になるのであまり使われない名前になりつつある気がしている。このクラスは DataStore と asyncio.Event の扱いがセットになっていてその役割の曖昧さが `Manager` という名前に現れている気がする。DataStore からみたらコレクションにみえるし、Event からみたらイベントハンドラーにみえる。リファクタリングするなら2つに分割して、それぞれの責務に限定した方が保守はしやすくなる気がする。
##### iscoroutinefunction
これはどこで使っている?
```python=
self._iscorofunc = asyncio.iscoroutinefunction(self._onmessage)
```
##### DataStore 要素の取得
この処理は同じにみえるので Pythonic な概念だと、どちらかのやり方に統一した方がよいのではないか?
```python=
def __getitem__(self, name: str) -> Optional['DataStore']:
return self._stores[name]
def get(self, name: str, type: Type[TDataStore]) -> TDataStore:
return cast(type, self._stores.get(name))
```
動作確認。
```python=
>>> class A:
... def __init__(self):
... self.data = {}
... def __getitem__(self, name):
... return self.data[name]
... def get(self, name):
... return self.data.get(name)
...
>>> a = A()
>>> a.data['x'] = 1
>>> a['x']
1
>>> a.get('x')
1
```
---
### どりらん
> https://github.com/MtkN1/pybotters/blob/f90a05b151b483dd2a8ccd110a4a09896eef3d67/pybotters/store.py#L24
```python=24
def __init__(self, keys: List[str] = [], data: List[Item] = []) -> None:
self._data: Dict[uuid.UUID, Item] = {}
self._index: Dict[int, uuid.UUID] = {}
self._keys: Tuple[str, ...] = tuple(keys if keys else self._KEYS)
self._events: Dict[asyncio.Event, List[Item]] = {}
self._insert(data)
```
- Python3.9以降では `list[str]` のように書ける
- typingを利用した書き方deprecatedになる
- Python3.8-3.7では `from __future__ import annotations` でジェネリクスが使える
#### [Ellipsis](https://docs.python.org/ja/3/library/constants.html#Ellipsis)
- 組み込み定数
- `...` と書ける
- スライス構文で省略するときなどに使われる
numpyの例
```python
>>> import numpy as np
>>>
>>> data = np.arange(16).reshape((2, 2, 2, 2))
>>> data[0, :, :, 1]
array([[1, 3],
[5, 7]])
```
`...` で省略できる
```python
>>> data[0, ..., 1]
array([[1, 3],
[5, 7]])
```
> https://github.com/MtkN1/pybotters/blob/f90a05b151b483dd2a8ccd110a4a09896eef3d67/pybotters/store.py#L43
```python=43
def _insert(self, data: List[Item]) -> None:
if self._keys:
for item in data:
try:
keyitem = {k: item[k] for k in self._keys}
except KeyError:
pass
```
- いくつかのメソッドで同じ処理をしているのでまとめられそう
- `_insert`
- `_update`
- `_delete`
- `_sweep_with_key`
- `_sweep_without_key`
- `get`
- `pop`
- `keyitem` はコンストラクタに入れてインスタンス属性にはできない?
> https://github.com/MtkN1/pybotters/blob/f90a05b151b483dd2a8ccd110a4a09896eef3d67/pybotters/store.py#L115
```python=115
keys = [next(_iter) for _ in range(over)]
for k in keys:
del self._data[self._index[k]]
del self._index[k]
```
- forは1回だけでよさそう?
```python
for _ in range(over):
k = next(_iter)
del self._data[self._index[k]]
del self._index[k]
```
- なんとなくだけど、1要素ごとに削除するのはオーバヘッドが大きい気がする
- for文を使わないで辞書の前半をカットするサンプル
```python
>>> import itertools
>>>
>>> di = {k: v for k, v in zip(range(5), "abcde")}
>>> di
{0: 'a', 1: 'b', 2: 'c', 3: 'd', 4: 'e'}
>>> over = 3
>>> dict(islice(di.items(), over, len(di)))
>>> {3: 'd', 4: 'e'}
```
- `_sweep_with_key` と `_sweep_without_key` は同様な処理なので、1つのメソッドにまとめられそう
> https://github.com/MtkN1/pybotters/blob/f90a05b151b483dd2a8ccd110a4a09896eef3d67/pybotters/store.py#L139
```python=
def _pop(self, item: Item) -> Optional[Item]:
if self._keys:
try:
keyitem = {k: item[k] for k in self._keys}
except KeyError:
pass
else:
keyhash = self._hash(keyitem)
if keyhash in self._index:
ret = self._data[self._index[keyhash]]
del self._data[self._index[keyhash]]
del self._index[keyhash]
return ret
```
- メソッド名が `_pop` で、処理もpopに近いので、 `del` よりは `pop` をにしたほうがわかりやすそう
> https://github.com/MtkN1/pybotters/blob/f90a05b151b483dd2a8ccd110a4a09896eef3d67/pybotters/store.py#L153
```python=153
def find(self, query: Item = {}) -> List[Item]:
if query:
return [
item
for item in self
if all(k in item and query[k] == item[k] for k in query)
]
else:
return list(self)
```
- 詳しくは追いきれていないけど、繰り返し処理をしなくてもset型に変換したら必要なデータは取れそうな気がした
> https://github.com/MtkN1/pybotters/blob/f90a05b151b483dd2a8ccd110a4a09896eef3d67/pybotters/store.py#L182
```python=182
async def wait(self) -> List[Item]:
event = asyncio.Event()
ret = []
self._events[event] = ret
await event.wait()
del self._events[event]
return ret
```
- よく理解できなかったのであとで読み直す
#### [typing.TypeVar](https://docs.python.org/ja/3/library/typing.html#typing.TypeVar)
- 型変数
- 引数 `bound` で `DataStore` クラスが親クラスであることを指定
> https://github.com/MtkN1/pybotters/blob/f90a05b151b483dd2a8ccd110a4a09896eef3d67/pybotters/store.py#L194
#### DataStoreManager
- `models` の親クラス
- `__getitem__` と `__contains__` が実装されているので、コンテナ型として扱える
#### 参考情報
- [2021年版Pythonの型ヒントの書き方 (for Python 3.9)](https://future-architect.github.io/articles/20201223/)
---
### しんせいたろう
#### 動作確認
- スクリプト
```python=
import asyncio
import pybotters
from logzero import logger
# apis
apis = {"ftx": ["", ""]}
async def main(market):
async with pybotters.Client(apis=apis) as client:
# DataStoreManagerインスタンス化
store = pybotters.FTXDataStore()
wstask = await client.ws_connect(
"wss://ftx.com/ws/",
send_json=[
{"op": "subscribe", "channel": "orderbook", "market": market},
# {"op": "subscribe", "channel": "trades", "market": market},
# {"op": "subscribe", "channel": "ticker", "market": market},
],
# ウェブソケットデータを受信したら、onmessage関数で処理するように設定
hdlr_json=store.onmessage,
)
while True:
await store.wait()
# https://docs.ftx.com/#orderbooks
# デフォルトでBest100が流れてくるので今回は見やすさ重視で1つだけ表示
# debug, warn, info には意味はなく、ただログ出力時の見やすさ(ログ色分け)のために使用
logger.debug(store.orderbook.find({"side": "buy"})[:1])
logger.warn(store.orderbook.find({"side": "sell"})[:1])
logger.info(store.ticker.find())
# logger.info(list(store.orderbook))
# 非同期メイン関数を実行(Ctrl+Cで終了)
if __name__ == "__main__":
market = "FTT-PERP"
try:
asyncio.run(main(market))
except KeyboardInterrupt:
pass
```
- 出力
```
[D 211012 18:36:46 test:27] [{'market': 'FTT-PERP', 'side': 'buy', 'price': 53.2035, 'size': 14.3}]
[W 211012 18:36:46 test:28] [{'market': 'FTT-PERP', 'side': 'sell', 'price': 53.2155, 'size': 58.0}]
[I 211012 18:36:46 test:29] [{'market': 'FTT-PERP', 'bid': 53.2035, 'ask': 53.2155, 'bidSize': 14.3, 'askSize': 58.0, 'last': 53.215, 'time': 1634031406.137918}]
```
- 読みたいところ:
- https://docs.ftx.com/#orderbooks によると、orderbook の 出力フォーマットは `[[best price, size], [next best price, size]... ]`
- これを上記のようなフォーマット(`{'market': 'FTT-PERP', 'side': 'buy', 'price': 53.2035, 'size': 14.3}`)に変換しているところを読む
#### 読む
- `hdlr_json=store.onmessage` して、ウェブソケットデータを受信したら、onmessage関数で処理するように設定してあるので
- `pybotters.FTXDataStore()`を読む
- `models/ftx.py` 15行目
```python
class FTXDataStore(DataStoreManager):
def _init(self) -> None:
self.create('ticker', datastore_class=Ticker)
self.create('markets', datastore_class=Markets)
self.create('trades', datastore_class=Trades)
self.create('orderbook', datastore_class=OrderBook)
:
:
```
1. `FTXDataStore()` をインスタンス化する時に、`create`メソッドを使って、チャネル名ごとに `datastore_class`を指定
1. 'orderbook'の`datastore_class`は `OrderBook`
1. createメソッド: `store.py` 208行目
```python
def create(
self,
name: str,
*,
keys: List[str] = [],
data: List[Item] = [],
datastore_class: Type[DataStore] = DataStore,
) -> None:
self._stores[name] = datastore_class(keys, data)
```
- name (必須)とdatastore_class(オプション)で、`self._stores` に、データストアオブジェクトを格納
1. OrderBook データストアクラス: `models/ftx.py` 117行目
```python
class OrderBook(DataStore):
_KEYS = ['market', 'side', 'price']
_BDSIDE = {'sell': 'asks', 'buy': 'bids'}
def sorted(self, query: Item = {}) -> Dict[str, List[float]]:
result = {'asks': [], 'bids': []}
for item in self:
if all(k in item and query[k] == item[k] for k in query):
result[self._BDSIDE[item['side']]].append([item['price'], item['size']])
result['asks'].sort(key=lambda x: x[0])
result['bids'].sort(key=lambda x: x[0], reverse=True)
return result
def _onmessage(self, market: str, data: List[Item]) -> None:
if data['action'] == 'partial':
result = self.find({'market': market})
self._delete(result)
for boardside, side in (('bids', 'buy'), ('asks', 'sell')):
for item in data[boardside]:
if item[1]:
self._update(
[
{
'market': market,
'side': side,
'price': item[0],
'size': item[1],
}
]
)
else:
self._delete([{'market': market, 'side': side, 'price': item[0]}])
```
- この _onmessage に渡される dataのフォーマットは下記の通り
```python
{'time': 1634088862.3278034,
'checksum': 1908033768,
'bids': [[53.5175, 19.0], [53.5055, 6.4], ...], # [price, size] で、Best 100 の 板情報が 入る
'asks': [[53.524, 7.7], [53.5245, 43.2], ...], # 〃
'action': 'partial'}
```
- FTXの仕様で、bids/ask はBest 100 の順番に並んでいることが保証されているので そのままループに渡してよし
- ここで、最初に見たフォーマット `{'market': 'FTT-PERP', 'side': 'buy', 'price': 53.2035, 'size': 14.3}` に変換されている。
- (`sorted` 関数は使われてないっぽい)
- `self._update()`: `store.py` 67行目
```python
def _update(self, data: List[Item]) -> None:
if self._keys:
for item in data:
try:
keyitem = {k: item[k] for k in self._keys}
# {'market': 'FTT-PERP', 'side': 'buy', 'price': 52.9075}
except KeyError:
pass
else:
keyhash = self._hash(keyitem)
if keyhash in self._index:
self._data[self._index[keyhash]].update(item)
else:
_id = uuid.uuid4()
self._data[_id] = item
self._index[keyhash] = _id
self._sweep_with_key()
else:
for item in data:
_id = uuid.uuid4()
self._data[_id] = item
self._sweep_without_key()
# !TODO! This behaviour might be undesirable.
self._set(data)
```
- `self._keys` は `OrderBook` で定義されている `_KEYS = ['market', 'side', 'price']`
- `{'market': 'FTT-PERP', 'side': 'buy', 'price': 52.9075}` という形のデータから keyhash を作成
- `self._data` に、keyhash と 板情報のデータを追加
- `self._data` は
```python
{UUID('ac7be6f4-e2cf-4082-b89e-55ac63ee0387'): {'market': 'FTT-PERP', 'side': 'buy', 'price': 52.733, 'size': 27.0},
UUID('fb27567f-b809-4fa2-8683-0f15eda14e93'): {'market': 'FTT-PERP', 'side': 'buy', 'price': 52.7325, 'size': 1.5},
UUID('c74b5ec0-1014-4387-be7b-a56677a5161c'): {'market': 'FTT-PERP', 'side': 'buy', 'price': 52.7255, 'size': 40.8},.... }
```
- このデータは、DataStoreの `__iter__` で return されているので、ユーザは `store.orderbook` でアクセス可
- `store.orderbook.find({"side": "buy"}` : `store.py` 153行目
```python
def find(self, query: Item = {}) -> List[Item]:
if query:
return [
item
for item in self
if all(k in item and query[k] == item[k] for k in query)
]
else:
return list(self)
```
- self は、`__iter__` で取得できるデータ。つまり `self._data.values()`
- そこから、query(`{'side': 'buy'}`)にマッチしたデータだけフィルタして返す
- まとめ:
- 各取引所のAPIに対応しているDataStoreを、`DataStore` と `DataStoreManager` を継承しながら定義してある。(`models` 配下)
- 当該のデータが来た時に .onmessage メソッドを呼んでデータを処理
- 共通のインターフェイスを用意するのって大変だなーと思いました
---
### やろころ
[UUIDとは](https://wa3.i-3-i.info/word13163.html)
class DataStore:
def __init__(self, keys: List[str] = [], data: List[Item] = []) -> None: 各種インスタンス変数の初期化
def __len__(self) -> int: データの長さを返却
def __len__(self) -> int: データのイテレータを返却
@staticmethod
def _hash(item: Dict[str, Hashable]) -> int: itemのハッシュ値を返却
def _insert(self, data: List[Item]) -> None: dataへのデータ挿入
def _update(self, data: List[Item]) -> None: dataへの要素更新
def _delete(self, data: List[Item]) -> None: dataの要素削除
def _clear(self) -> None: data等の削除
def _sweep_with_key(self) -> None: dataがMAXLENより大きいときのkeyによるsweep処理?(_insert()や_update()で利用)
def _sweep_without_key(self) -> None: dataがMAXLENより大きいときのkeyによらないsweep処理?
def get(self, item: Item) -> Optional[Item]:
def _pop(self, item: Item) -> Optional[Item]:
def find(self, query: Item = {}) -> List[Item]:
def _find_and_delete(self, query: Item = {}) -> List[Item]:
def _set(self, data: List[Item] = None) -> None:
async def wait(self) -> List[Item]:
TDataStore = TypeVar('TDataStore', bound=DataStore)
class DataStoreManager:
def __init__(self) -> None:
def __getitem__(self, name: str) -> Optional['DataStore']:
def __contains__(self, name: str) -> bool:
def create( 中略 ) -> None:
def get(self, name: str, type: Type[TDataStore]) -> TDataStore:
def _onmessage(self, msg: Any, ws: ClientWebSocketResponse) -> None:
def onmessage(self, msg: Any, ws: ClientWebSocketResponse) -> None:
def _set(self) -> None:
async def wait(self) -> None:
---
### 発表者5
---
## 出席確認
- [x] shinseitaro
- [x] driller
- [x] t2y
- [x] yarokoro
- [x] MtkN1
- [x] aaacc
## 次回
- [ ] `__init__.py`
- [ ] `auth.py`
- [x] `client.py`
- [x] `request.py`
- [x] `store.py`
- [ ] `typedefs.py`
- [x] `ws.py`
- [ ] `tests`
- [ ] `docs` (ドキュメント関連)
- [ ] `pyproject.toml` (パッケージ関連)
- [ ] `pytest.yml` (CI, GitHub Actions)
`auth.py` を読む
- 10/29(金) 19:00-20:00