owned this note
owned this note
Published
Linked with GitHub
# fin-pyコードリーディング会#5 :bookmark_tabs:
## 概要
### fin-pyについて
https://github.com/fin-py/guideline
#### 次回のイベント
[fin-pyもくもく会 #50](https://fin-py.connpass.com/event/227921/)
### Slack
https://docs.google.com/forms/d/e/1FAIpQLSd9oVlrCMHEuD3PN0x3QcMgeQGy6Sj90d6uP1CQXQnArX9YqQ/viewform
### 参加方法
:::info
Zoom:
https://us02web.zoom.us/j/81839197616?pwd=Q21ZZFpNM2dWSk12UlVqVll1amdMZz09
ミーティングID: 818 3919 7616
パスコード: 024180
:::
:::warning
**重要**
Zoomに参加したら、[出欠確認](https://hackmd.io/hthf2yeNRGipHzrER5_1uQ#%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)
- [#4](https://hackmd.io/bFBFaPbYS1Kqfc97HMlp7Q?view)
### タイムテーブル :clock1:
1. 発表者枠が話す: 各自20-30min
1. クロージング(コードを読んだ感想など): 20min
## コードリーディング
### 読むコード
[auth.py](https://github.com/MtkN1/pybotters/blob/main/pybotters/auth.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は長く書いてるわりに分かってないひと。クラスとかいまだ苦手
- 感想
---
### やろころ
- 概要説明担当
---
## 発表者
### どりらん
#### [hmac](https://docs.python.org/ja/3/library/hmac.html)
- メッセージ認証のための鍵付きハッシュ化
- `Auth` クラスではAPIの電子署名を生成する目的で使われている
> [【Python】プライベートAPIの利用に必要なAPI認証や電子署名をAPIドキュメントから実装する(後半)](https://tkstock.site/2019/11/26/python-private-api-access-hashlib-requests/)
#### [aiohttp.formdata.Formdata](https://docs.aiohttp.org/en/stable/client_reference.html?highlight=FormData#formdata)
- フォームデータを処理するクラス
- エンコードの処理も行う
- `multipart/form-data`
- `application/x-www-form-urlencoded`
- `multipart/form-data` が使われる条件
- フィールドに`io.IOBase` オブジェクトが含まれている
- `add_field` にオプショナルな引数が追加されていること
- `application/x-www-form-urlencoded` が使われる条件
- `multipart/form-data` 以外
#### [ウェブフォームとは何か?](https://developer.mozilla.org/ja/docs/Learn/Forms/Your_first_form#what_are_html_forms)
- ユーザーとウェブサイトやアプリケーションとの対話の要となるもののひとつ
- フォームによって、ユーザーはウェブサイトへデータを送れる
- 1つ以上のフォームコントロール(ウィジェットともいう)と、フォーム全体を構成するのに役立つ追加要素(HTMLフォームと呼ばる)とで作られる
#### application/x-www-form-urlencoded
- キーと値は、その間に `=` がある形でキーと値の組になる
- `&` で区切られてエンコードされる
- キーや値の英数字以外の文字は、パーセントエンコーディングされる
- バイナリデータを扱うのには向かない
- 代わりに `multipart/form-data` を使用する
フォームの例
```
POST /test HTTP/1.1
Host: foo.example
Content-Type: application/x-www-form-urlencoded
Content-Length: 27
field1=value1&field2=value2
```
#### [multipart/form-data](https://developer.mozilla.org/ja/docs/Web/HTTP/Basics_of_HTTP/MIME_types#multipartform-data)
- それぞれの値はデータのブロック ("body part") として送信される
- ユーザーエージェントが定義するデリミター ("boundary") がそれぞれの部分を区切る
- キーはそれぞれの部分の `Content-Disposition` ヘッダーの中で与えられる
フォームの例
```
POST /test HTTP/1.1
Host: foo.example
Content-Type: multipart/form-data;boundary="boundary"
--boundary
Content-Disposition: form-data; name="field1"
value1
--boundary
Content-Disposition: form-data; name="field2"; filename="example.txt"
value2
--boundary--
```
> 参考: https://developer.mozilla.org/ja/docs/Web/HTTP/Methods/POST
#### aiohttp.hdrs
> https://github.com/aio-libs/aiohttp/blob/master/aiohttp/hdrs.py
HTTPヘッダの定数
#### aiohttp.payload.JsonPayload
> https://github.com/aio-libs/aiohttp/blob/e445011aebc670d78163eeeb1e5da58fa851221f/aiohttp/payload.py#L384
- [BytesPayload](https://github.com/aio-libs/aiohttp/blob/e445011aebc670d78163eeeb1e5da58fa851221f/aiohttp/payload.py#L220)のサブクラス
- BytesPayloadは[Payload](https://github.com/aio-libs/aiohttp/blob/e445011aebc670d78163eeeb1e5da58fa851221f/aiohttp/payload.py#L134)のサブクラス
- Payloadは抽象基底クラス(ABCを継承!)
- コンストラクタの引数にJSON用のデフォルト値が設定されている
- [ソースコード](https://github.com/aio-libs/aiohttp/blob/e445011aebc670d78163eeeb1e5da58fa851221f/aiohttp/payload.py#L388)
```python=388
encoding: str = "utf-8",
content_type: str = "application/json",
dumps: JSONEncoder = json.dumps,
```
`Payload` クラスのプロパティ
size
: ペイロードのサイズ
filename
: ペイロードのファイル名
headers
: カスタムアイテムヘッダ
encoding
: ペイロードのエンコーディング
content_type
: コンテンツタイプ
`Payload` クラスのメソッド
set_content_disposition
: [Content-Disposition](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Content-Disposition) ヘッダをセット
write
: [AbstractStreamWriter](https://github.com/aio-libs/aiohttp/blob/09ac1cbb4c07b4b3e1872374808512c9bc52f2ab/aiohttp/abc.py#L159) でペイロードを書き込むコルーチン
#### ペイロード
- ネットワークパケットのユーザデータ部分
- 可変長のデータ
- プロトコルによって長さに制限
- HTTP通信においてはHTTPヘッダ以降の本文に該当する部分
> 参考:
> - [パケット](https://developer.mozilla.org/ja/docs/Glossary/Packet#user_data_-_payload)
> - [HTTPメッセージ](https://developer.mozilla.org/ja/docs/Web/HTTP/Messages)
#### [multidict.CIMultiDict](https://multidict.readthedocs.io/en/stable/multidict.html?highlight=CIMultiDict#multidict.CIMultiDict)
大文字/小文字を区別しない [MultiDict](https://multidict.readthedocs.io/en/stable/multidict.html?highlight=CIMultiDict#multidict.MultiDict)
#### pybotters.auth.Auth
> https://github.com/MtkN1/pybotters/blob/f90a05b151b483dd2a8ccd110a4a09896eef3d67/pybotters/auth.py#L17
- 各取引所の認可(認証?)をするメソッドの集合
- エンドポイントのホスト名から呼び出されるメソッドを判別
:::info
`Auth` クラス内でもし共通する処理があれば、親クラスで抽象化して、各取引所のクラスをつくる実装でもできそう?
:::
:::info
- `Auth` クラスの処理はおそらく認可(Authorization)と思われる
- 名前が「Auth」だと、認証(Authentication)も連想するので、どちらかがわかる名称のほうがよさそう
- ...と思ったけど、認証に依存した認可にもみえるのでうーん...
> [よくわかる認証と認可](https://dev.classmethod.jp/articles/authentication-and-authorization/)
:::
#### pybotters.auth.Item
> https://github.com/MtkN1/pybotters/blob/f90a05b151b483dd2a8ccd110a4a09896eef3d67/pybotters/auth.py#L302
- 取引所名とメソッド名をメンバとしたクラス
- 辞書でもよさそう?
#### pybotters.auth.Hosts
- ホスト名をキー、 `Item` インスタンスを値とした辞書( `items` )をもつクラス
- 辞書でもよさそう?
---
### しんせいたろう
#### auth.py
- Auth クラス
- 各取引所の認証に必要なデータをセットするためのメソッドが `@staticmethod` で定義
- Hosts クラス
- `items` という辞書が定義
- キーに取引所のホスト、値に `Item` データクラスを持つ
- Items クラス
- 文字列型の `name` と Any型の `func` のデータクラス
- dataclass に関しては [第3回のメモを参照](https://hackmd.io/L2p6b7obSzq0EbUYi7Fh0w?view#%E5%88%9D%E3%82%81%E3%81%A6%E4%BD%BF%E3%81%86%E6%A8%99%E6%BA%96%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA)
#### どのように使われている?
- client作成時
```python
import asyncio
import pybotters
apis = {
"ftx": ["YOUR-FTX-API-KEY", "YOUR-FTX-API-SECRET",],
}
async def main():
# api 情報を渡して client オブジェクト作成
async with pybotters.Client(apis=apis, base_url="https://ftx.com/api") as client:
resp = await client.get("/account")
data = await resp.json()
print(data)
asyncio.run(main())
```
- 認証情報をセット
```python=18
# client.py 34行目
class Client:
_session: aiohttp.ClientSession
_base_url: str
def __init__(
self,
apis: Union[Dict[str, List[str]], str] = {},
base_url: str = "",
**kwargs: Any,
) -> None:
self._session = aiohttp.ClientSession(
request_class=ClientRequest,
ws_response_class=ClientWebSocketResponse,
**kwargs,
)
# ここでapiがsession にセットされる
apis = self._load_apis(apis)
self._session.__dict__["_apis"] = self._encode_apis(apis)
:
:
```
- 最初に client を作る時、`request_class=` で指定されている `ClientRequest`がインスタンス化されている
```python=18
# request.py 25行目
class ClientRequest(aiohttp.ClientRequest):
def __init__(self, *args, **kwargs) -> None:
method: str = args[0]
url: URL = args[1]
:
:
# Auth 情報がある場合は
if kwargs["auth"] is Auth:
# 一回だけしかしなくていいのでNoneを入れるのかな?
kwargs["auth"] = None
# 取引所に応じて auth.py で定義した メソッドを呼び出す
if url.host in Hosts.items:
if Hosts.items[url.host].name in kwargs["session"].__dict__["_apis"]:
args = Hosts.items[url.host].func(args, kwargs)
# 親クラスの init に渡される
super().__init__(*args, **kwargs)
```
- Auth クラスの ftx メソッドを抜粋して読む
- https://docs.ftx.com/#authentication に沿って情報をセット
```python
@staticmethod
def ftx(args: Tuple[str, URL], kwargs: Dict[str, Any]) -> Tuple[str, URL]:
method: str = args[0]
url: URL = args[1]
data: Dict[str, Any] = kwargs['data'] or {}
headers: CIMultiDict = kwargs['headers']
session: aiohttp.ClientSession = kwargs['session']
key: str = session.__dict__['_apis'][Hosts.items[url.host].name][0]
secret: bytes = session.__dict__['_apis'][Hosts.items[url.host].name][1]
path = url.raw_path_qs
body = JsonPayload(data) if data else FormData(data)()
ts = str(int(time.time() * 1000))
text = f'{ts}{method}{path}'.encode() + body._value
signature = hmac.new(secret, text, hashlib.sha256).hexdigest()
kwargs.update({'data': body})
headers.update({'FTX-KEY': key, 'FTX-SIGN': signature, 'FTX-TS': ts})
return args
```
---
### やろころ
#### 概要
- Authクラスに各取引毎の認証処理を実装
- [Data class](https://qiita.com/tag1216/items/13b032348c893667862a)を準備
- URLをKey、データクラス(取引所、認証処理のstatic method)を値とした辞書)がvalueの辞書を準備
##### コード抜粋
```
class Auth:
@staticmethod
def bybit(args: Tuple[str, URL], kwargs: Dict[str, Any]) -> Tuple[str, URL]:
@staticmethod
def binance(args: Tuple[str, URL], kwargs: Dict[str, Any]) -> Tuple[str, URL]:
@staticmethod
def bitflyer(args: Tuple[str, URL], kwargs: Dict[str, Any]) -> Tuple[str, URL]
@staticmethod
def gmocoin(args: Tuple[str, URL], kwargs: Dict[str, Any]) -> Tuple[str, URL]:
@staticmethod
def liquid(args: Tuple[str, URL], kwargs: Dict[str, Any]) -> Tuple[str, URL]:
@staticmethod
def bitbank(args: Tuple[str, URL], kwargs: Dict[str, Any]) -> Tuple[str, URL]:
@staticmethod
def ftx(args: Tuple[str, URL], kwargs: Dict[str, Any]) -> Tuple[str, URL]:
@staticmethod
def bitmex(args: Tuple[str, URL], kwargs: Dict[str, Any]) -> Tuple[str, URL]:
@staticmethod
def phemex(args: Tuple[str, URL], kwargs: Dict[str, Any]) -> Tuple[str, URL]:
@staticmethod
def coincheck(args: Tuple[str, URL], kwargs: Dict[str, Any]) -> Tuple[str, URL]:
@dataclass
class Item:
name: str
func: Any
class Hosts:
items = {
'api.bybit.com': Item('bybit', Auth.bybit),
```
#### 対応状況確認と認証処理のリーディング
https://github.com/MtkN1/pybotters/blob/9533e359c63ccc25cf18c69b4aa988b57accd5cd/README.md#-exchanges
Bybitに狙いを定める
- 引数 args
- str: HTTP Method(e.g. GET)
- URL: 取引所のURL
- 引数 kwargs
- str: ??
- Any: ??
- key, secretを準備
- HTTP Methodに応じた処理
- GETの場合
- HTTP Methodと新しいURLを返却
- [hmac](https://docs.python.org/ja/3/library/hmac.html):メッセージ認証のための鍵付きハッシュ化
```
@staticmethod
def bybit(args: Tuple[str, URL], kwargs: Dict[str, Any]) -> Tuple[str, URL]:
method: str = args[0]
url: URL = args[1]
data: Dict[str, Any] = kwargs['data'] or {}
session: aiohttp.ClientSession = kwargs['session']
key: str = session.__dict__['_apis'][Hosts.items[url.host].name][0]
secret: bytes = session.__dict__['_apis'][Hosts.items[url.host].name][1]
expires = str(int((time.time() - 1.0) * 1000))
if method == METH_GET:
query = MultiDict(url.query)
if url.scheme == 'https':
query.extend({'api_key': key, 'timestamp': expires})
query_string = '&'.join(f'{k}={v}' for k, v in sorted(query.items()))
sign = hmac.new(
secret, query_string.encode(), hashlib.sha256
).hexdigest()
query.extend({'sign': sign})
else:
expires = str(int((time.time() + 1.0) * 1000))
path = f'{method}/realtime{expires}'
signature = hmac.new(secret, path.encode(), hashlib.sha256).hexdigest()
query.extend(
{'api_key': key, 'expires': expires, 'signature': signature}
)
url = url.with_query(query)
args = (
method,
url,
)
else:
data.update({'api_key': key, 'timestamp': expires})
body = FormData(sorted(data.items()))()
sign = hmac.new(secret, body._value, hashlib.sha256).hexdigest()
body._value += f'&sign={sign}'.encode()
body._size = len(body._value)
kwargs.update({'data': body})
return args
```
---
### 発表者4
---
### 発表者5
---
## 出席確認
## 次回
- [ ] `__init__.py`
- [x] `auth.py`
- [x] `client.py`
- [x] `request.py`
- [x] `store.py`
- [ ] `typedefs.py`
- [x] `ws.py`
- [ ] `models`
- [ ] `tests`
- [ ] `docs` (ドキュメント関連)
- [ ] `pyproject.toml` (パッケージ関連)
- [ ] `pytest.yml` (CI, GitHub Actions)
pybottersを使ってみるイベント
- どこかの取引所でやる
- デモトレードができる取引所
- [bybit](https://testnet.bybit.com/trade/inverse/BTCUSD)
- カスタマサポートにチャットするとテストコインがもらえるらしい
- https://crypto-worldwide.xyz/bybit-demo-testnet/
- 参加条件
- テストコインを送った人だけ参加できる
- 本当にできるか要確認
- 残高APIの結果をアンケートに記入
- 免責事項をしっかりと書いておく
- アンケートに同意しましたのチェックしてもらう
11/19(金) 19:00-20:00