Skip to content

Commit

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

## Issues

✅ 各取引所のDataStoreを実装する #20
✅ DataStoreのリバースイテレーション可能にする #113
✅ PhemexDataStoreで認証エラーのWarningを表示する #114
✅ BybitInverseDataStoreにwalletを追加する #118
✅ GMOCoinDataStoreのtickerが上書きされない #120
✅ GMOCoinDataStoreがTypeErrorで落ちる。 #122
✅ GMOコインのPrivate WebSocketアクセストークンを自動延長する #124
✅ bitbankのDatastoreのDepthで、sorted()の返り値の型の定義が正しくない #126
  • Loading branch information
MtkN1 committed Feb 4, 2022
2 parents 5f3558a + 37c6ebf commit 4174779
Show file tree
Hide file tree
Showing 10 changed files with 171 additions and 15 deletions.
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -44,7 +44,7 @@ An advanced api client for python botters.
| GMO Coin ||| [Official](https://api.coin.z.com/docs/) |
| Liquid || WIP | [Official](https://document.liquid.com/) |
| bitbank ||| [Official](https://docs.bitbank.cc/) |
| Coincheck || WIP | [Official](https://coincheck.com/documents/exchange/api) |
| Coincheck || | [Official](https://coincheck.com/documents/exchange/api) |

## 🐍 Requires

Expand Down
2 changes: 2 additions & 0 deletions pybotters/__init__.py
Expand Up @@ -14,6 +14,7 @@
from .models.bitflyer import bitFlyerDataStore
from .models.bitmex import BitMEXDataStore
from .models.bybit import BybitDataStore
from .models.coincheck import CoincheckDataStore
from .models.experimental.bybit import BybitInverseDataStore, BybitUSDTDataStore
from .models.ftx import FTXDataStore
from .models.gmocoin import GMOCoinDataStore
Expand All @@ -28,6 +29,7 @@
'put',
'delete',
'BybitDataStore',
'CoincheckDataStore',
'BybitInverseDataStore',
'BybitUSDTDataStore',
'FTXDataStore',
Expand Down
2 changes: 1 addition & 1 deletion pybotters/models/bitbank.py
Expand Up @@ -53,7 +53,7 @@ class Depth(DataStore):
_KEYS = ['pair', 'side', 'price']
_BDSIDE = {'sell': 'asks', 'buy': 'bids'}

def sorted(self, query: Optional[Item] = None) -> dict[str, list[float]]:
def sorted(self, query: Optional[Item] = None) -> dict[str, list[list[str]]]:
if query is None:
query = {}
result = {'asks': [], 'bids': []}
Expand Down
81 changes: 81 additions & 0 deletions pybotters/models/coincheck.py
@@ -0,0 +1,81 @@
from __future__ import annotations

import asyncio
from typing import Any, Awaitable, Optional

import aiohttp

from ..store import DataStore, DataStoreManager
from ..typedefs import Item
from ..ws import ClientWebSocketResponse


class CoincheckDataStore(DataStoreManager):
def _init(self) -> None:
self.create("trades", datastore_class=Trades)
self.create("orderbook", datastore_class=Orderbook)

async def initialize(self, *aws: Awaitable[aiohttp.ClientResponse]) -> None:
for f in asyncio.as_completed(aws):
resp = await f
data = await resp.json()
if resp.url.path == "/api/order_books":
symbol = resp.url.query.get("symbol")
self.orderbook._onresponse(symbol, data)

def _onmessage(self, msg: Any, ws: ClientWebSocketResponse) -> None:
if len(msg) == 5:
self.trades._onmessage(*msg)
elif len(msg) == 2:
self.orderbook._onmessage(*msg)

@property
def trades(self) -> "Trades":
return self.get("trades", Trades)

@property
def orderbook(self) -> "Orderbook":
return self.get("orderbook", Orderbook)


class Trades(DataStore):
_MAXLEN = 99999

def _onmessage(self, id: int, pair: str, rate: str, amount: str, side: str) -> None:
self._insert(
[{"id": id, "pair": pair, "rate": rate, "amount": amount, "side": side}]
)


class Orderbook(DataStore):
_KEYS = ["side", "rate"]

def sorted(self, query: Optional[Item] = None) -> dict[str, list[float]]:
if query is None:
query = {}
result = {"asks": [], "bids": []}
for item in self:
if all(k in item and query[k] == item[k] for k in query):
result[item["side"]].append([item["rate"], item["amount"]])
result["asks"].sort(key=lambda x: float(x[0]))
result["bids"].sort(key=lambda x: float(x[0]), reverse=True)
return result

def _onresponse(self, symbol: Optional[str], data: dict[list[str]]) -> None:
if symbol is None:
symbol = "btc_jpy"
result = []
for side in data:
for rate, amount in data[side]:
result.append(
{"symbol": symbol, "side": side, "rate": rate, "amount": amount}
)
self._insert(result)

def _onmessage(self, pair: str, data: dict[str, list[list[str]]]) -> None:
for side in data:
for rate, amount in data[side]:
if amount == "0":
self._delete([{"side": side, "rate": rate}])
else:
self._update([{"side": side, "rate": rate, "amount": amount}])
46 changes: 39 additions & 7 deletions pybotters/models/experimental/bybit.py
Expand Up @@ -29,6 +29,7 @@ def _init(self) -> None:
self.create("execution", datastore_class=ExecutionInverse)
self.create("order", datastore_class=OrderInverse)
self.create("stoporder", datastore_class=StopOrderInverse)
self.create("wallet", datastore_class=WalletInverse)
self.timestamp_e6: Optional[int] = None

async def initialize(self, *aws: Awaitable[aiohttp.ClientResponse]) -> None:
Expand All @@ -41,6 +42,7 @@ async def initialize(self, *aws: Awaitable[aiohttp.ClientResponse]) -> None:
- GET /futures/private/stop-order (DataStore: stoporder)
- GET /v2/private/position/list (DataStore: position)
- GET /futures/private/position/list (DataStore: position)
- GET /v2/private/wallet/balance (DataStore: wallet)
"""
for f in asyncio.as_completed(aws):
resp = await f
Expand Down Expand Up @@ -68,6 +70,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 @@ -101,6 +105,8 @@ def _onmessage(self, msg: Item, ws: ClientWebSocketResponse) -> None:
self.order._onmessage(data)
elif topic == "stop_order":
self.stoporder._onmessage(data)
elif topic == "wallet":
self.wallet._onmessage(data)
if "timestamp_e6" in msg:
self.timestamp_e6 = int(msg["timestamp_e6"])

Expand Down Expand Up @@ -153,6 +159,10 @@ def stoporder(self) -> "StopOrderInverse":
"""
return self.get("stoporder", StopOrderInverse)

@property
def wallet(self) -> "WalletInverse":
return self.get("wallet", WalletInverse)


class BybitUSDTDataStore(DataStoreManager):
"""
Expand All @@ -170,7 +180,7 @@ def _init(self) -> None:
self.create("execution", datastore_class=ExecutionUSDT)
self.create("order", datastore_class=OrderUSDT)
self.create("stoporder", datastore_class=StopOrderUSDT)
self.create("wallet", datastore_class=Wallet)
self.create("wallet", datastore_class=WalletUSDT)
self.timestamp_e6: Optional[int] = None

async def initialize(self, *aws: Awaitable[aiohttp.ClientResponse]) -> None:
Expand All @@ -180,6 +190,8 @@ async def initialize(self, *aws: Awaitable[aiohttp.ClientResponse]) -> None:
- GET /private/linear/order/search (DataStore: order)
- GET /private/linear/stop-order/search (DataStore: stoporder)
- GET /private/linear/position/list (DataStore: position)
- GET /private/linear/position/list (DataStore: position)
- GET /v2/private/wallet/balance (DataStore: wallet)
"""
for f in asyncio.as_completed(aws):
resp = await f
Expand Down Expand Up @@ -282,8 +294,8 @@ def stoporder(self) -> "StopOrderUSDT":
return self.get("stoporder", StopOrderUSDT)

@property
def wallet(self) -> "Wallet":
return self.get("wallet", Wallet)
def wallet(self) -> "WalletUSDT":
return self.get("wallet", WalletUSDT)


class OrderBookInverse(DataStore):
Expand Down Expand Up @@ -490,19 +502,39 @@ class StopOrderUSDT(StopOrderInverse):
_KEYS = ["stop_order_id"]


class Wallet(DataStore):
class WalletInverse(DataStore):
_KEYS = ["coin"]

def _onresponse(self, data: dict[str, Item]) -> None:
data.pop("USDT", None)
for coin in data:
self._update(
[
{
"coin": coin,
"available_balance": data[coin]["available_balance"],
"wallet_balance": data[coin]["wallet_balance"],
}
]
)

def _onmessage(self, data: list[Item]) -> None:
self._update(data)


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

def _onmessage(self, data: list[Item]) -> None:
self._clear()
self._update(data)
for item in data:
self._update([{"coin": "USDT", **item}])
27 changes: 22 additions & 5 deletions pybotters/models/gmocoin.py
Expand Up @@ -12,6 +12,7 @@
from pybotters.store import DataStore, DataStoreManager
from pybotters.typedefs import Item

from ..auth import Auth
from ..ws import ClientWebSocketResponse

try:
Expand Down Expand Up @@ -297,6 +298,8 @@ class PositionSummary(TypedDict):


class TickerStore(DataStore):
_KEYS = ["symbol"]

def _onmessage(self, mes: Ticker) -> None:
self._update([cast(Item, mes)])

Expand Down Expand Up @@ -325,7 +328,7 @@ def _onmessage(self, mes: OrderBook) -> None:
data = mes["asks"] + mes["bids"]
result = self.find({"symbol": mes["symbol"]})
self._delete(result)
self._insert(cast(list[Item], data))
self._insert(cast("list[Item]", data))
self.timestamp = mes["timestamp"]


Expand All @@ -338,7 +341,7 @@ class OrderStore(DataStore):
_KEYS = ["order_id"]

def _onresponse(self, data: list[Order]) -> None:
self._insert(cast(list[Item], data))
self._insert(cast("list[Item]", data))

def _onmessage(self, mes: Order) -> None:
if mes["order_status"] in (OrderStatus.WAITING, OrderStatus.ORDERED):
Expand Down Expand Up @@ -375,7 +378,7 @@ def sorted(self, query: Optional[Item] = None) -> list[Execution]:
return result

def _onresponse(self, data: list[Execution]) -> None:
self._insert(cast(list[Item], data))
self._insert(cast("list[Item]", data))

def _onmessage(self, mes: Execution) -> None:
self._insert([cast(Item, mes)])
Expand All @@ -385,7 +388,7 @@ class PositionStore(DataStore):
_KEYS = ["position_id"]

def _onresponse(self, data: list[Position]) -> None:
self._update(cast(list[Item], data))
self._update(cast("list[Item]", data))

def _onmessage(self, mes: Position, type: MessageType) -> None:
if type == MessageType.OPR:
Expand All @@ -400,7 +403,7 @@ class PositionSummaryStore(DataStore):
_KEYS = ["symbol", "side"]

def _onresponse(self, data: list[PositionSummary]) -> None:
self._update(cast(list[Item], data))
self._update(cast("list[Item]", data))

def _onmessage(self, mes: PositionSummary) -> None:
self._update([cast(Item, mes)])
Expand Down Expand Up @@ -575,6 +578,7 @@ def _init(self) -> None:
self.create("positions", datastore_class=PositionStore)
self.create("executions", datastore_class=ExecutionStore)
self.create("position_summary", datastore_class=PositionSummaryStore)
self.token: Optional[str] = None

async def initialize(self, *aws: Awaitable[aiohttp.ClientResponse]) -> None:
"""
Expand All @@ -584,6 +588,7 @@ async def initialize(self, *aws: Awaitable[aiohttp.ClientResponse]) -> None:
- GET /private/v1/activeOrders (DataStore: orders)
- GET /private/v1/openPositions (DataStore: positions)
- GET /private/v1/positionSummary (DataStore: position_summary)
- POST /private/v1/ws-auth (Property: token)
"""
for f in asyncio.as_completed(aws):
resp = await f
Expand All @@ -608,6 +613,9 @@ async def initialize(self, *aws: Awaitable[aiohttp.ClientResponse]) -> None:
self.position_summary._onresponse(
MessageHelper.to_position_summaries(data["data"]["list"])
)
if resp.url.path == "/private/v1/ws-auth":
self.token = data["data"]
asyncio.create_task(self._token(resp.__dict__['_raw_session']))

def _onmessage(self, msg: Item, ws: ClientWebSocketResponse) -> None:
if "channel" in msg:
Expand All @@ -631,6 +639,15 @@ def _onmessage(self, msg: Item, ws: ClientWebSocketResponse) -> None:
elif channel == Channel.POSITION_SUMMARY_EVENTS:
self.position_summary._onmessage(MessageHelper.to_position_summary(msg))

async def _token(self, session: aiohttp.ClientSession):
while not session.closed:
await session.put(
'https://api.coin.z.com/private/v1/ws-auth',
data={"token": self.token},
auth=Auth,
)
await asyncio.sleep(1800.0) # 30 minutes

@property
def ticker(self) -> TickerStore:
return self.get("ticker", TickerStore)
Expand Down
3 changes: 3 additions & 0 deletions pybotters/models/phemex.py
Expand Up @@ -65,6 +65,9 @@ def _onmessage(self, msg: Item, ws: ClientWebSocketResponse) -> None:
if 'positions' in msg:
self.positions._onmessage(msg.get('positions'))

if msg.get('error'):
logger.warning(msg)

@property
def trade(self) -> 'Trade':
return self.get('trade', Trade)
Expand Down
3 changes: 3 additions & 0 deletions pybotters/store.py
Expand Up @@ -36,6 +36,9 @@ def __len__(self) -> int:
def __iter__(self) -> Iterator[Item]:
return iter(self._data.values())

def __reversed__(self) -> Iterator[Item]:
return reversed(self._data.values())

@staticmethod
def _hash(item: dict[str, Hashable]) -> int:
return hash(tuple(item.items()))
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pybotters"
version = "0.9.0"
version = "0.10.0"
description = "An advanced api client for python botters."
license = "MIT"
authors = ["MtkN1 <51289448+MtkN1@users.noreply.github.com>"]
Expand Down
18 changes: 18 additions & 0 deletions tests/test_store.py
@@ -1,4 +1,5 @@
import asyncio
import sys
import uuid

import pytest
Expand Down Expand Up @@ -285,6 +286,23 @@ def test__iter__():
next(data_iter)


def test__reversed__():
data = [{'foo': f'bar{i}'} for i in range(5)]
ds = pybotters.store.DataStore(keys=['foo'], data=data)
if sys.version_info.major == 3 and sys.version_info.minor >= 8:
data_iter = reversed(ds)
assert next(data_iter) == {'foo': 'bar4'}
assert next(data_iter) == {'foo': 'bar3'}
assert next(data_iter) == {'foo': 'bar2'}
assert next(data_iter) == {'foo': 'bar1'}
assert next(data_iter) == {'foo': 'bar0'}
with pytest.raises(StopIteration):
next(data_iter)
else:
with pytest.raises(TypeError):
data_iter = reversed(ds)


def test_set():
ds = pybotters.store.DataStore()
event = asyncio.Event()
Expand Down

0 comments on commit 4174779

Please sign in to comment.