Skip to content

Commit

Permalink
Merge pull request #112 from MtkN1/develop
Browse files Browse the repository at this point in the history
✨v0.9.0リリース

## Issues

✅ Bybit SpotのDELETE認証方式を修正する #95
✅ BinanceのWebSocket制限に対応する #96
✅ bitFlyerの親注文APIがエラーになる #98
✅ BybitUSDTDataStoreのウォレット実装が漏れている #99
✅ HTTPリクエストのメソッドにおけるパラメーター指定時の警告を削除する #101
✅ Bybitの認証タイムスタンプについて変更を検討する #102
✅ Bybit USDT無期限のワンウェイモードに対応する #105
✅ GMOコインのタイムスタンプにミリ秒がないレコードがパースできない #106
✅ Binance USDⓈ-M Futuresの新しいWebSocketエンドポイントに対応する #108
✅ bitbankの認証タイムスタンプを修正する #109
✅ 旧BybitDataStoreクラスがauto_castを受け付けない #111

## Pull requests

✅ Phemexのデータストアをサポートする #97
✅ GMOコインのタイムスタンプのミリ秒をパースする #107
  • Loading branch information
MtkN1 committed Jan 3, 2022
2 parents a409121 + 428fa7b commit 5f3558a
Show file tree
Hide file tree
Showing 13 changed files with 369 additions and 62 deletions.
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -38,7 +38,7 @@ An advanced api client for python botters.
| Bybit ||| [Official](https://bybit-exchange.github.io/docs/inverse) |
| Binance || ✅(USDⓈ-M) | [Official](https://binance-docs.github.io/apidocs/spot/en/) |
| FTX ||| [Official](https://docs.ftx.com/) |
| Phemex || WIP | [Official](https://github.com/phemex/phemex-api-docs) |
| Phemex || | [Official](https://github.com/phemex/phemex-api-docs) |
| BitMEX ||| [Official](https://www.bitmex.com/app/apiOverview) |
| bitFlyer ||| [Official](https://lightning.bitflyer.com/docs) |
| GMO Coin ||| [Official](https://api.coin.z.com/docs/) |
Expand Down
29 changes: 28 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pybotters/__init__.py
Expand Up @@ -17,6 +17,7 @@
from .models.experimental.bybit import BybitInverseDataStore, BybitUSDTDataStore
from .models.ftx import FTXDataStore
from .models.gmocoin import GMOCoinDataStore
from .models.phemex import PhemexDataStore
from .typedefs import WsJsonHandler, WsStrHandler

__all__: Tuple[str, ...] = (
Expand All @@ -35,6 +36,7 @@
'bitFlyerDataStore',
'BitMEXDataStore',
'GMOCoinDataStore',
'PhemexDataStore',
'experimental',
'print',
'print_handler',
Expand Down
53 changes: 28 additions & 25 deletions pybotters/auth.py
Expand Up @@ -10,7 +10,7 @@

import aiohttp
from aiohttp.formdata import FormData
from aiohttp.hdrs import METH_GET
from aiohttp.hdrs import METH_DELETE, METH_GET
from aiohttp.payload import JsonPayload
from multidict import CIMultiDict, MultiDict
from yarl import URL
Expand All @@ -27,35 +27,37 @@ def bybit(args: tuple[str, URL], kwargs: dict[str, Any]) -> tuple[str, URL]:
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})
if url.scheme == 'https':
expires = str(int((time.time() - 5.0) * 1000))
recv_window = (
'recv_window' if not url.path.startswith("/spot") else "recvWindow"
)
auth_params = {'api_key': key, 'timestamp': expires, recv_window: 10000}
if method in (METH_GET, METH_DELETE):
query = MultiDict(url.query)
query.extend(auth_params)
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})
url = url.with_query(query)
args = (method, url)
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}
)
data.update(auth_params)
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})
elif url.scheme == 'wss':
query = MultiDict(url.query)
expires = str(int((time.time() + 5.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})
args = (method, url)

return args

Expand Down Expand Up @@ -108,7 +110,7 @@ def bitflyer(args: tuple[str, URL], kwargs: dict[str, Any]) -> tuple[str, URL]:
secret: bytes = session.__dict__['_apis'][Hosts.items[url.host].name][1]

path = url.raw_path_qs
body = FormData(data)()
body = JsonPayload(data) if data else FormData(data)()
timestamp = str(int(time.time()))
text = f'{timestamp}{method}{path}'.encode() + body._value
signature = hmac.new(secret, text, hashlib.sha256).hexdigest()
Expand Down Expand Up @@ -195,7 +197,7 @@ def bitbank(args: tuple[str, URL], kwargs: dict[str, Any]) -> tuple[str, URL]:

path = url.raw_path_qs
body = JsonPayload(data) if data else FormData(data)()
nonce = str(int(time.time()))
nonce = str(int(time.time() * 1000))
if method == METH_GET:
text = f'{nonce}{path}'.encode()
else:
Expand Down Expand Up @@ -322,6 +324,7 @@ class Hosts:
'stream.binance.com': Item('binance', Auth.binance),
'fapi.binance.com': Item('binance', Auth.binance),
'fstream.binance.com': Item('binance', Auth.binance),
'fstream-auth.binance.com': Item('binance', Auth.binance),
'dapi.binance.com': Item('binance', Auth.binance),
'dstream.binance.com': Item('binance', Auth.binance),
'vapi.binance.com': Item('binance', Auth.binance),
Expand Down
4 changes: 0 additions & 4 deletions pybotters/client.py
Expand Up @@ -115,10 +115,6 @@ def _request(
auth: Optional[Auth] = Auth,
**kwargs: Any,
) -> _RequestContextManager:
if method == hdrs.METH_GET and data:
logger.warning('Send a GET request, but data argument is set')
elif method != hdrs.METH_GET and params:
logger.warning(f'Send a {method} request, but params argument is set')
return self._session.request(
method=method,
url=self._base_url + url,
Expand Down
4 changes: 2 additions & 2 deletions pybotters/models/bitbank.py
Expand Up @@ -60,8 +60,8 @@ def sorted(self, query: Optional[Item] = None) -> dict[str, list[float]]:
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)
result['asks'].sort(key=lambda x: float(x[0]))
result['bids'].sort(key=lambda x: float(x[0]), reverse=True)
return result

def _onmessage(self, room_name: str, data: list[Item]) -> None:
Expand Down
2 changes: 1 addition & 1 deletion pybotters/models/bybit.py
Expand Up @@ -18,7 +18,7 @@ class BybitDataStore(DataStoreManager):
Bybitのデータストアマネージャー
"""

def __new__(cls) -> BybitDataStore:
def __new__(cls, *args, **kwargs) -> BybitDataStore:
logger.warning(
'DEPRECATION WARNING: BybitDataStore will be changed to '
'BybitInverseDataStore and BybitUSDTDataStore'
Expand Down
31 changes: 18 additions & 13 deletions pybotters/models/experimental/bybit.py
Expand Up @@ -198,6 +198,8 @@ async def initialize(self, *aws: Awaitable[aiohttp.ClientResponse]) -> None:
self.position._onresponse(data["result"])
elif resp.url.path == "/v2/public/kline/list":
self.kline._onresponse(data["result"])
elif resp.url.path == "/v2/private/wallet/balance":
self.wallet._onresponse(data["result"])

def _onmessage(self, msg: Item, ws: ClientWebSocketResponse) -> None:
if "success" in msg:
Expand Down Expand Up @@ -429,19 +431,10 @@ def _onmessage(self, data: list[Item]) -> None:


class PositionUSDT(PositionInverse):
_KEYS = ["symbol", "side"]

def one(self, symbol: str) -> dict[str, Optional[Item]]:
return {
"Sell": self.get({"symbol": symbol, "side": "Sell"}),
"Buy": self.get({"symbol": symbol, "side": "Buy"}),
}

def both(self, symbol: str) -> dict[str, Optional[Item]]:
return {
"Sell": self.get({"symbol": symbol, "side": "Sell"}),
"Buy": self.get({"symbol": symbol, "side": "Buy"}),
}
def _onmessage(self, data: list[Item]) -> None:
for item in data:
item['position_idx'] = int(item['position_idx'])
self._update([item])


class ExecutionInverse(DataStore):
Expand Down Expand Up @@ -498,6 +491,18 @@ class StopOrderUSDT(StopOrderInverse):


class Wallet(DataStore):
def _onresponse(self, data: dict[str, Item]) -> None:
if "USDT" in data:
self._clear()
self._update(
[
{
"wallet_balance": data["USDT"]["wallet_balance"],
"available_balance": data["USDT"]["available_balance"],
}
]
)

def _onmessage(self, data: list[Item]) -> None:
self._clear()
self._update(data)
17 changes: 16 additions & 1 deletion pybotters/models/gmocoin.py
Expand Up @@ -3,6 +3,7 @@
import asyncio
import logging
from datetime import datetime, timezone
from dateutil import parser
from decimal import Decimal
from enum import Enum, auto
from typing import Any, Awaitable, Optional, cast
Expand All @@ -23,7 +24,21 @@

def parse_datetime(x: Any) -> datetime:
if isinstance(x, str):
return datetime.strptime(x, "%Y-%m-%dT%H:%M:%S.%f%z")
try:
exec_date = x.replace('T', ' ')[:-1]
exec_date = exec_date + '00000000'
dt = datetime(
int(exec_date[0:4]),
int(exec_date[5:7]),
int(exec_date[8:10]),
int(exec_date[11:13]),
int(exec_date[14:16]),
int(exec_date[17:19]),
int(exec_date[20:26]),
)
except Exception:
dt = parser.parse(x)
return dt
else:
raise ValueError(f'x only support str, but {type(x)} passed.')

Expand Down

0 comments on commit 5f3558a

Please sign in to comment.