Implement Kucoin auth and last trades call
parent
dc02c115ba
commit
2a68ce0657
|
@ -18,32 +18,28 @@
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from dataclasses import field
|
from logging import warning
|
||||||
from typing import Any, Optional, Literal
|
from typing import Any, Optional, Literal
|
||||||
from contextlib import asynccontextmanager as acm
|
from contextlib import asynccontextmanager as acm
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import time
|
import time
|
||||||
import math
|
import math
|
||||||
from os import path, walk
|
import base64
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
|
||||||
import asks
|
import asks
|
||||||
import tractor
|
import tractor
|
||||||
import trio
|
import trio
|
||||||
from trio_typing import TaskStatus
|
from trio_typing import TaskStatus
|
||||||
from fuzzywuzzy import process as fuzzy
|
from fuzzywuzzy import process as fuzzy
|
||||||
from cryptofeed.defines import KUCOIN, TRADES, L2_BOOK
|
|
||||||
from cryptofeed.symbols import Symbol
|
|
||||||
import pendulum
|
import pendulum
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from piker.data.cryptofeeds import (
|
|
||||||
fqsn_to_cf_sym,
|
|
||||||
mk_stream_quotes,
|
|
||||||
get_config,
|
|
||||||
)
|
|
||||||
from piker._cacheables import open_cached_client
|
from piker._cacheables import open_cached_client
|
||||||
from piker.log import get_logger
|
from piker.log import get_logger
|
||||||
from piker.pp import config
|
|
||||||
from ._util import DataUnavailable
|
from ._util import DataUnavailable
|
||||||
|
from piker.pp import config
|
||||||
|
|
||||||
_spawn_kwargs = {
|
_spawn_kwargs = {
|
||||||
"infect_asyncio": True,
|
"infect_asyncio": True,
|
||||||
|
@ -51,45 +47,80 @@ _spawn_kwargs = {
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
_ohlc_dtype = [
|
_ohlc_dtype = [
|
||||||
('index', int),
|
("index", int),
|
||||||
('time', int),
|
("time", int),
|
||||||
('open', float),
|
("open", float),
|
||||||
('high', float),
|
("high", float),
|
||||||
('low', float),
|
("low", float),
|
||||||
('close', float),
|
("close", float),
|
||||||
('volume', float),
|
("volume", float),
|
||||||
('bar_wap', float), # will be zeroed by sampler if not filled
|
("bar_wap", float), # will be zeroed by sampler if not filled
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class Client:
|
class Client:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._pairs: dict[str, Symbol] = {}
|
self._pairs: dict[str, any] = {}
|
||||||
self._bars: list[list] = []
|
self._bars: list[list] = []
|
||||||
# TODO" Shouldn't have to write kucoin twice here
|
self._key_id: str
|
||||||
|
self._key_secret: str
|
||||||
|
self._key_passphrase: str
|
||||||
|
self._authenticated: bool = False
|
||||||
|
|
||||||
config = get_config("kucoin").get("kucoin", {})
|
config = get_config()
|
||||||
#
|
breakpoint()
|
||||||
if ("key_id" in config) and ("key_secret" in config):
|
if ("key_id" in config) and \
|
||||||
|
("key_secret" in config) and \
|
||||||
|
("key_passphrase" in config):
|
||||||
|
self._authenticated = True
|
||||||
self._key_id = config["key_id"]
|
self._key_id = config["key_id"]
|
||||||
self._key_secret = config["key_secret"]
|
self._key_secret = config["key_secret"]
|
||||||
|
self._key_passphrase = config["key_passphrase"]
|
||||||
else:
|
|
||||||
self._key_id = None
|
|
||||||
self._key_secret = None
|
|
||||||
|
|
||||||
async def _request(
|
async def _request(
|
||||||
self,
|
self,
|
||||||
action: Literal["POST", "GET", "PUT", "DELETE"],
|
action: Literal["POST", "GET", "PUT", "DELETE"],
|
||||||
route: str,
|
endpoint: str,
|
||||||
api_v: str = "v2",
|
api_v: str = "v2",
|
||||||
) -> Any:
|
) -> Any:
|
||||||
api_url = f"https://api.kucoin.com/api/{api_v}{route}"
|
|
||||||
res = await asks.request(action, api_url)
|
now = int(time.time() * 1000)
|
||||||
|
path = f'/api/{api_v}{endpoint}'
|
||||||
|
str_to_sign = str(now) + action + path
|
||||||
|
headers = {}
|
||||||
|
|
||||||
|
# Add headers to request if authenticated
|
||||||
|
if self._authenticated:
|
||||||
|
signature = base64.b64encode(
|
||||||
|
hmac.new(
|
||||||
|
self._key_secret.encode('utf-8'),
|
||||||
|
str_to_sign.encode('utf-8'),
|
||||||
|
hashlib.sha256
|
||||||
|
).digest()
|
||||||
|
)
|
||||||
|
|
||||||
|
passphrase = base64.b64encode(
|
||||||
|
hmac.new(
|
||||||
|
self._key_secret.encode('utf-8'),
|
||||||
|
self._key_passphrase.encode('utf-8'),
|
||||||
|
hashlib.sha256
|
||||||
|
).digest()
|
||||||
|
)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"KC-API-SIGN": signature,
|
||||||
|
"KC-API-TIMESTAMP": str(now),
|
||||||
|
"KC-API-KEY": self._key_id,
|
||||||
|
"KC-API-PASSPHRASE": passphrase,
|
||||||
|
"KC-API-KEY-VERSION": "2"
|
||||||
|
}
|
||||||
|
|
||||||
|
api_url = f"https://api.kucoin.com{path}"
|
||||||
|
res = await asks.request(action, api_url, headers=headers)
|
||||||
# breakpoint()
|
# breakpoint()
|
||||||
try:
|
if "data" in res.json():
|
||||||
return res.json()["data"]
|
return res.json()["data"]
|
||||||
except KeyError as e:
|
else:
|
||||||
print(f'KUCOIN ERROR: {res.json()["msg"]}')
|
print(f'KUCOIN ERROR: {res.json()["msg"]}')
|
||||||
breakpoint()
|
breakpoint()
|
||||||
|
|
||||||
|
@ -106,14 +137,14 @@ class Client:
|
||||||
async def cache_pairs(
|
async def cache_pairs(
|
||||||
self,
|
self,
|
||||||
normalize: bool = True,
|
normalize: bool = True,
|
||||||
) -> dict[str, Symbol]:
|
) -> dict[str, any]:
|
||||||
if not self._pairs:
|
if not self._pairs:
|
||||||
self._pairs = await self.get_pairs()
|
self._pairs = await self.get_pairs()
|
||||||
if normalize:
|
if normalize:
|
||||||
self._pairs = self.normalize_pairs(self._pairs)
|
self._pairs = self.normalize_pairs(self._pairs)
|
||||||
return self._pairs
|
return self._pairs
|
||||||
|
|
||||||
def normalize_pairs(self, pairs: dict[str, Symbol]) -> dict[str, Symbol]:
|
def normalize_pairs(self, pairs: dict[str, any]) -> dict[str, any]:
|
||||||
"""
|
"""
|
||||||
Map crypfeeds symbols to fqsn strings
|
Map crypfeeds symbols to fqsn strings
|
||||||
|
|
||||||
|
@ -137,6 +168,10 @@ class Client:
|
||||||
# repack in dict form
|
# repack in dict form
|
||||||
return {item[0]["name"].lower(): item[0] for item in matches}
|
return {item[0]["name"].lower(): item[0] for item in matches}
|
||||||
|
|
||||||
|
async def last_trades(self, sym: str):
|
||||||
|
trades = await self._request("GET", f"/accounts/ledgers?currency={sym}", "v1")
|
||||||
|
return trades.items
|
||||||
|
|
||||||
async def get_bars(
|
async def get_bars(
|
||||||
self,
|
self,
|
||||||
fqsn: str,
|
fqsn: str,
|
||||||
|
@ -172,23 +207,22 @@ class Client:
|
||||||
# TODO: implement struct/typecasting/validation here
|
# TODO: implement struct/typecasting/validation here
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'index': i,
|
"index": i,
|
||||||
'time': bar[0],
|
"time": bar[0],
|
||||||
'open': bar[1],
|
"open": bar[1],
|
||||||
'close': bar[2],
|
"close": bar[2],
|
||||||
'high': bar[3],
|
"high": bar[3],
|
||||||
'low': bar[4],
|
"low": bar[4],
|
||||||
'volume': bar[5],
|
"volume": bar[5],
|
||||||
'amount': bar [6],
|
"amount": bar[6],
|
||||||
'bar_wap': 0.0,
|
"bar_wap": 0.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
row = []
|
row = []
|
||||||
for j, (field_name, field_type) in enumerate(_ohlc_dtype):
|
for j, (field_name, field_type) in enumerate(_ohlc_dtype):
|
||||||
|
|
||||||
value = data[field_name]
|
value = data[field_name]
|
||||||
match field_name:
|
match field_name:
|
||||||
case 'index' | 'time':
|
case "index" | "time":
|
||||||
row.append(int(value))
|
row.append(int(value))
|
||||||
# case 'time':
|
# case 'time':
|
||||||
# dt_from_unix_ts = datetime.utcfromtimestamp(int(value))
|
# dt_from_unix_ts = datetime.utcfromtimestamp(int(value))
|
||||||
|
@ -203,6 +237,26 @@ class Client:
|
||||||
return array
|
return array
|
||||||
|
|
||||||
|
|
||||||
|
def fqsn_to_cf_sym(fqsn: str, pairs: dict[str, any]) -> str:
|
||||||
|
pair_data = pairs[fqsn]
|
||||||
|
return pair_data["baseCurrency"] + "-" + pair_data["quoteCurrency"]
|
||||||
|
|
||||||
|
|
||||||
|
def get_config() -> dict[str, Any]:
|
||||||
|
conf, path = config.load()
|
||||||
|
|
||||||
|
section = conf.get('kucoin')
|
||||||
|
|
||||||
|
# TODO: document why we send this, basically because logging params for cryptofeed
|
||||||
|
conf["log"] = {}
|
||||||
|
conf["log"]["disabled"] = True
|
||||||
|
|
||||||
|
if section is None:
|
||||||
|
log.warning("No config section found for deribit in kucoin")
|
||||||
|
|
||||||
|
return section
|
||||||
|
|
||||||
|
|
||||||
@acm
|
@acm
|
||||||
async def get_client():
|
async def get_client():
|
||||||
client = Client()
|
client = Client()
|
||||||
|
@ -217,7 +271,7 @@ async def open_symbol_search(
|
||||||
):
|
):
|
||||||
async with open_cached_client("kucoin") as client:
|
async with open_cached_client("kucoin") as client:
|
||||||
# load all symbols locally for fast search
|
# load all symbols locally for fast search
|
||||||
cache = await client.cache_pairs()
|
await client.cache_pairs()
|
||||||
await ctx.started()
|
await ctx.started()
|
||||||
|
|
||||||
async with ctx.open_stream() as stream:
|
async with ctx.open_stream() as stream:
|
||||||
|
@ -234,15 +288,24 @@ async def stream_quotes(
|
||||||
# startup sync
|
# startup sync
|
||||||
task_status: TaskStatus[tuple[dict, dict]] = trio.TASK_STATUS_IGNORED,
|
task_status: TaskStatus[tuple[dict, dict]] = trio.TASK_STATUS_IGNORED,
|
||||||
):
|
):
|
||||||
return await mk_stream_quotes(
|
sym = symbols[0]
|
||||||
KUCOIN,
|
|
||||||
[L2_BOOK, TRADES],
|
async with open_cached_client("kucoin") as client:
|
||||||
send_chan,
|
init_msgs = {
|
||||||
symbols,
|
# pass back token, and bool, signalling if we're the writer
|
||||||
feed_is_live,
|
# and that history has been written
|
||||||
loglevel,
|
sym: {
|
||||||
task_status,
|
"symbol_info": {
|
||||||
)
|
"asset_type": "option",
|
||||||
|
"price_tick_size": 0.0005,
|
||||||
|
"lot_tick_size": 0.1,
|
||||||
|
},
|
||||||
|
"shm_write_opts": {"sum_tick_vml": False},
|
||||||
|
"fqsn": sym,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
last_trades = await client.last_trades(sym)
|
||||||
|
|
||||||
|
|
||||||
@acm
|
@acm
|
||||||
|
@ -256,13 +319,9 @@ async def open_history_client(
|
||||||
timeframe: float,
|
timeframe: float,
|
||||||
end_dt: datetime | None = None,
|
end_dt: datetime | None = None,
|
||||||
start_dt: datetime | None = None,
|
start_dt: datetime | None = None,
|
||||||
) -> tuple[
|
) -> tuple[np.ndarray, datetime | None, datetime | None,]: # start # end
|
||||||
np.ndarray,
|
|
||||||
datetime | None, # start
|
|
||||||
datetime | None, # end
|
|
||||||
]:
|
|
||||||
if timeframe != 60:
|
if timeframe != 60:
|
||||||
raise DataUnavailable('Only 1m bars are supported')
|
raise DataUnavailable("Only 1m bars are supported")
|
||||||
|
|
||||||
array = await client.get_bars(
|
array = await client.get_bars(
|
||||||
symbol,
|
symbol,
|
||||||
|
@ -270,13 +329,13 @@ async def open_history_client(
|
||||||
end_dt=end_dt,
|
end_dt=end_dt,
|
||||||
)
|
)
|
||||||
|
|
||||||
times = array['time']
|
times = array["time"]
|
||||||
|
|
||||||
if (
|
if end_dt is None:
|
||||||
end_dt is None
|
|
||||||
):
|
|
||||||
inow = round(time.time())
|
inow = round(time.time())
|
||||||
print(f'difference in time between load and processing {inow - times[-1]}')
|
print(
|
||||||
|
f"difference in time between load and processing {inow - times[-1]}"
|
||||||
|
)
|
||||||
if (inow - times[-1]) > 60:
|
if (inow - times[-1]) > 60:
|
||||||
await tractor.breakpoint()
|
await tractor.breakpoint()
|
||||||
|
|
||||||
|
@ -284,4 +343,4 @@ async def open_history_client(
|
||||||
end_dt = pendulum.from_timestamp(times[-1])
|
end_dt = pendulum.from_timestamp(times[-1])
|
||||||
return array, start_dt, end_dt
|
return array, start_dt, end_dt
|
||||||
|
|
||||||
yield get_ohlc_history, {'erlangs': 3, 'rate': 3}
|
yield get_ohlc_history, {"erlangs": 3, "rate": 3}
|
||||||
|
|
Loading…
Reference in New Issue