Compare commits
4 Commits
main-old
...
becffcd1fb
| Author | SHA1 | Date | |
|---|---|---|---|
| becffcd1fb | |||
| 3d2a454e96 | |||
| a5f951dea9 | |||
| ecb9ad08e1 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,2 @@
|
||||
__pycache__/
|
||||
/secret.json
|
||||
secret.json
|
||||
|
||||
2
algorithm/__init__.py
Normal file
2
algorithm/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from algorithm.ma import SMA, WMA, EMA, MA_Cross
|
||||
from algorithm.status import Status
|
||||
64
algorithm/ma.py
Normal file
64
algorithm/ma.py
Normal file
@@ -0,0 +1,64 @@
|
||||
class SMA:
|
||||
def __init__(self):
|
||||
return
|
||||
|
||||
def calc(self, cands, period):
|
||||
sum = 0
|
||||
n = min(period, len(cands))
|
||||
for i in range(n):
|
||||
sum += cands[i][3]
|
||||
return sum / n
|
||||
|
||||
class EMA:
|
||||
def __init__(self):
|
||||
return
|
||||
|
||||
def calc(self, cands, period):
|
||||
n = min(period, len(cands))
|
||||
ret = 0
|
||||
for i in range(n):
|
||||
ret += cands[i][3]
|
||||
ret /= n
|
||||
for i in range(n):
|
||||
ret = (2/(1+n))*(cands[n-i-1][3] - ret) + ret
|
||||
return ret
|
||||
|
||||
class WMA:
|
||||
def __init__(self):
|
||||
return
|
||||
|
||||
def calc(self, cands, period):
|
||||
sum = 0
|
||||
den = 0
|
||||
for i in range(min(period, len(cands))):
|
||||
sum += cands[i][3] * (i+1)
|
||||
den += i+1
|
||||
return sum / den
|
||||
|
||||
|
||||
class MA_Cross:
|
||||
def __init__(self, ma, unit, width):
|
||||
self._ma = ma
|
||||
self._unit = unit
|
||||
self._width = width
|
||||
|
||||
self._period_short = 5
|
||||
self._period_mid = 21
|
||||
self._period_long = 55
|
||||
|
||||
self._posi_amp = 0.1
|
||||
|
||||
def judge(self, status):
|
||||
cands = status.candles[self._unit]
|
||||
short = self._ma.calc(cands, self._period_short)
|
||||
mid = self._ma.calc(cands, self._period_mid)
|
||||
long = self._ma.calc(cands, self._period_long)
|
||||
|
||||
width = mid * self._width
|
||||
posi = max(-1, min((short-long)/(2.5*width), 1)) * self._posi_amp
|
||||
|
||||
diff = short - mid
|
||||
if diff < 0:
|
||||
return max(-1, diff/width+posi) # sell
|
||||
else:
|
||||
return min(1, diff/width+posi) # buy
|
||||
4
algorithm/status.py
Normal file
4
algorithm/status.py
Normal file
@@ -0,0 +1,4 @@
|
||||
class Status:
|
||||
def __init__(self):
|
||||
self.price = 0
|
||||
self.candles = {"1m": [], "1h": []}
|
||||
493
bitbank.py
493
bitbank.py
@@ -1,376 +1,163 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
import logging
|
||||
import pybotters
|
||||
import json
|
||||
import python_bitbankcc
|
||||
import random
|
||||
import sys
|
||||
import time
|
||||
|
||||
import logic
|
||||
import util.candlestick
|
||||
import util.depth
|
||||
import util.pair
|
||||
import util.ticker
|
||||
import algorithm as algo
|
||||
|
||||
|
||||
PUBLIC_API_ENTRYPOINT = "https://public.bitbank.cc"
|
||||
PRIVATE_API_ENTRYPOINT = "https://api.bitbank.cc/v1"
|
||||
FETCH_INTERVAL = 5
|
||||
|
||||
ORDER_UNITS = {
|
||||
"btc_jpy": 0.0001,
|
||||
"eth_jpy": 0.0001,
|
||||
}
|
||||
PRICE_UNITS = {
|
||||
"btc_jpy": 1,
|
||||
"eth_jpy": 1,
|
||||
}
|
||||
status = {}
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_k = asyncio.Lock()
|
||||
|
||||
async def init(cli):
|
||||
#pair = Pair(cli, "mona_jpy")
|
||||
#logic.Hige(
|
||||
# logging.getLogger("bitbank/mona/Hige"),
|
||||
# pair, 4, 0.2)
|
||||
|
||||
pair = Pair(cli, "btc_jpy")
|
||||
logic.EMA_Chicken(
|
||||
logging.getLogger("bitbank/btc/EMA_Chicken"),
|
||||
pair, pair.candlestick_1m, 0.001, 0.005)
|
||||
|
||||
pair = Pair(cli, "eth_jpy")
|
||||
logic.MM(
|
||||
logging.getLogger("bitbank/eth/MM"),
|
||||
pair, 0.01, 1, 0.15)
|
||||
logic.EMA_Chicken(
|
||||
logging.getLogger("bitbank/eth/EMA_Chicken"),
|
||||
pair, pair.candlestick_1m, 0.01, 0.005)
|
||||
|
||||
|
||||
class Pair(util.pair.Pair):
|
||||
def __init__(self, cli, name):
|
||||
super().__init__(name)
|
||||
self._cli = cli
|
||||
self._store = pybotters.bitbankDataStore()
|
||||
|
||||
self.ticker = Ticker(self._store, name)
|
||||
|
||||
self.order_unit = ORDER_UNITS[name]
|
||||
self.price_unit = PRICE_UNITS[name]
|
||||
|
||||
#self.candlestick_1s = Candlestick(cli, self.ticker, None, 1)
|
||||
self.candlestick_1m = Candlestick(cli, self.ticker, "1min", 60)
|
||||
#self.candlestick_1h = Candlestick(cli, self.ticker, "1hour", 60*60)
|
||||
|
||||
self.depth = Depth(self._store, self.order_unit)
|
||||
|
||||
asyncio.create_task(cli.ws_connect(
|
||||
'wss://stream.bitbank.cc/socket.io/?EIO=3&transport=websocket',
|
||||
send_str=[
|
||||
f'42["join-room","ticker_{name}"]',
|
||||
f'42["join-room","transactions_{name}"]',
|
||||
f'42["join-room","depth_whole_{name}"]',
|
||||
f'42["join-room","depth_diff_{name}"]',
|
||||
],
|
||||
hdlr_str = self._store.onmessage,
|
||||
))
|
||||
|
||||
async def _order(self, amount, side, type, trigger = None, price = None, post_only = None):
|
||||
data = {
|
||||
"pair" : self.name,
|
||||
"amount": str(amount),
|
||||
"side" : side,
|
||||
"type" : type,
|
||||
}
|
||||
if price is not None: data["price"] = price
|
||||
if trigger is not None: data["trigger_price"] = trigger
|
||||
if post_only is not None: data["post_only"] = post_only
|
||||
async with _k:
|
||||
res = await self._cli.post(PRIVATE_API_ENTRYPOINT+"/user/spot/order", data=data)
|
||||
res = await res.json()
|
||||
_check_response(res)
|
||||
order = Order(self._cli, self, res["data"]["order_id"])
|
||||
order.amount = amount
|
||||
order.price = price
|
||||
return order
|
||||
|
||||
def buy_market(self, amount):
|
||||
return self._order(amount, "buy", "market")
|
||||
|
||||
def sell_market(self, amount):
|
||||
return self._order(amount, "sell", "market")
|
||||
|
||||
def buy_limit(self, amount, price, post_only = False):
|
||||
return self._order(amount, "buy", "limit", None, price, post_only)
|
||||
|
||||
def sell_limit(self, amount, price, post_only = False):
|
||||
return self._order(amount, "sell", "limit", None, price, post_only)
|
||||
|
||||
def buy_stop(self, amount, trigger):
|
||||
return self._order(amount, "buy", "stop", trigger)
|
||||
|
||||
def sell_stop(self, amount, trigger):
|
||||
return self._order(amount, "sell", "stop", trigger)
|
||||
|
||||
def buy_stop_limit(self, amount, trigger, price):
|
||||
return self._order(amount, "buy", "stop", trigger, price)
|
||||
|
||||
def sell_stop_limit(self, amount, trigger, price):
|
||||
return self._order(amount, "sell", "stop", trigger, price)
|
||||
|
||||
|
||||
class Order(util.pair.Order):
|
||||
def __init__(self, cli, pair, id):
|
||||
super().__init__()
|
||||
self._cli = cli
|
||||
self._pair = pair
|
||||
self._id = id
|
||||
|
||||
async def update(self):
|
||||
if self.done: return
|
||||
for i in range(5):
|
||||
async with _k:
|
||||
res = await self._cli.get(PRIVATE_API_ENTRYPOINT+"/user/spot/order", params={
|
||||
"pair": self._pair.name,
|
||||
"order_id": self._id,
|
||||
})
|
||||
res = await res.json()
|
||||
if res["success"] == 1: break
|
||||
|
||||
# sometimes fails without a reason, so it does retry :(
|
||||
if res["data"]["code"] != 50009 and str(res["data"]["code"]) in ERROR_CODES:
|
||||
_check_response(res)
|
||||
await asyncio.sleep(1)
|
||||
if res["success"] != 1: raise Exception("failed to update")
|
||||
|
||||
res = res["data"]
|
||||
self.amount = float(res["start_amount"])
|
||||
self.remain = float(res["remaining_amount"])
|
||||
self.price = float(res["average_price"])
|
||||
self.done = \
|
||||
res["status"] == "FULLY_FILLED" or \
|
||||
res["status"] == "CANCELED_UNFILLED" or \
|
||||
res["status"] == "CANCELED_PARTIALLY_FILLED"
|
||||
|
||||
async def cancel(self):
|
||||
for i in range(5):
|
||||
async with _k:
|
||||
res = await self._cli.post(PRIVATE_API_ENTRYPOINT+"/user/spot/cancel_order", data={
|
||||
"pair": self._pair.name,
|
||||
"order_id": self._id,
|
||||
})
|
||||
res = await res.json()
|
||||
if res["success"] == 1:
|
||||
res = res["data"]
|
||||
self.amount = float(res["start_amount"])
|
||||
self.remain = float(res["remaining_amount"])
|
||||
self.price = float(res["average_price"])
|
||||
self.done = True
|
||||
return
|
||||
|
||||
code = res["data"]["code"]
|
||||
if code == 50027:
|
||||
await self.update()
|
||||
return
|
||||
|
||||
# sometimes fails without a reason, so it does retry :(
|
||||
retry = \
|
||||
code == 50009 or \
|
||||
str(code) not in ERROR_CODES
|
||||
|
||||
if not retry:
|
||||
_check_response(res)
|
||||
await asyncio.sleep(1)
|
||||
raise Exception("failed to cancel")
|
||||
|
||||
|
||||
class Ticker(util.ticker.Ticker):
|
||||
def __init__(self, store, pair):
|
||||
super().__init__(pair)
|
||||
self._store = store
|
||||
asyncio.create_task(self._main())
|
||||
|
||||
async def _main(self):
|
||||
while True:
|
||||
await self._store.ticker.wait()
|
||||
ticker = self._store.ticker.find()
|
||||
for i in range(len(ticker)):
|
||||
if ticker[i]["pair"] == self.pair:
|
||||
self.price = float(ticker[i]["last"])
|
||||
self.sell = float(ticker[i]["sell"])
|
||||
self.buy = float(ticker[i]["buy"])
|
||||
self.volume = float(ticker[i]["vol"])
|
||||
self.ts = int(ticker[i]["timestamp"])
|
||||
self._event.set()
|
||||
self._event.clear()
|
||||
break
|
||||
|
||||
|
||||
class Candlestick(util.candlestick.Candlestick):
|
||||
def __init__(self, cli, ticker, unit, interval):
|
||||
super().__init__(ticker, interval)
|
||||
self._cli = cli
|
||||
self._unit = unit
|
||||
|
||||
async def _fetch(self, date, depth = 0, rem = util.candlestick.MAX_CANDLES):
|
||||
if self._unit is None:
|
||||
def init():
|
||||
try:
|
||||
sec = json.load(open("secret.json"))
|
||||
sec = sec["bitbank"]
|
||||
pub = python_bitbankcc.public()
|
||||
pri = python_bitbankcc.private(sec["key"], sec["secret"])
|
||||
except Exception as e:
|
||||
print("failed to initialize bitbank features: ", e)
|
||||
return []
|
||||
|
||||
long_term = \
|
||||
self._unit == "4hour" or \
|
||||
self._unit == "8hour" or \
|
||||
self._unit == "12hour" or \
|
||||
self._unit == "1day" or \
|
||||
self._unit == "1week" or \
|
||||
self._unit == "1month"
|
||||
if long_term:
|
||||
strdate = date.strftime("%Y")
|
||||
else:
|
||||
strdate = date.strftime("%Y%m%d")
|
||||
procs = []
|
||||
procs.append(Fetcher(pub, "xlm_jpy", 10*60))
|
||||
procs.append(Proc(pri, "xlm_jpy", 5000, algo.MA_Cross(algo.EMA(), "1h", 0.01), 10*60))
|
||||
|
||||
pair = self._ticker.pair
|
||||
async with _k:
|
||||
data = await self._cli.get(PUBLIC_API_ENTRYPOINT+f"/{pair}/candlestick/{self._unit}/{strdate}")
|
||||
data = await data.json()
|
||||
_check_response(data)
|
||||
data = data["data"]["candlestick"][0]["ohlcv"]
|
||||
procs.append(Fetcher(pub, "btc_jpy", 10*60))
|
||||
procs.append(Proc(pri, "btc_jpy", 5000, algo.MA_Cross(algo.EMA(), "1h", 0.01), 10*60))
|
||||
|
||||
first_index = 0
|
||||
if depth == 0: first_index = 1
|
||||
procs.append(Fetcher(pub, "eth_jpy", 10*60))
|
||||
procs.append(Proc(pri, "eth_jpy", 5000, algo.MA_Cross(algo.EMA(), "1h", 0.01), 10*60))
|
||||
|
||||
procs.append(Fetcher(pub, "matic_jpy", 5))
|
||||
procs.append(Proc(pri, "matic_jpy", 1000, algo.MA_Cross(algo.EMA(), "1m", 0.01), 5))
|
||||
return procs
|
||||
|
||||
|
||||
class Fetcher:
|
||||
def __init__(self, pub, pair, interval):
|
||||
self._interval = interval
|
||||
self._last_update = 0
|
||||
|
||||
self._pub = pub
|
||||
self._pair = pair
|
||||
|
||||
self._candle_prev = {}
|
||||
|
||||
def tick(self):
|
||||
now = time.time()
|
||||
if now-self._last_update < self._interval:
|
||||
return
|
||||
try:
|
||||
st = algo.Status()
|
||||
|
||||
st.candles["1m"] = self._get_candle("1min", 60)
|
||||
st.candles["1h"] = self._get_candle("1hour", 60)
|
||||
|
||||
st.price = st.candles["1m"][0][3]
|
||||
|
||||
global status
|
||||
status[self._pair] = st
|
||||
except Exception as e:
|
||||
print("fetce error:", e)
|
||||
self._last_update = now
|
||||
|
||||
def _get_candle(self, unit, n, t = datetime.datetime.now(tz=datetime.timezone.utc)):
|
||||
data = self._pub.get_candlestick(
|
||||
self._pair,
|
||||
unit,
|
||||
t.strftime("%Y%m%d"))
|
||||
data = data["candlestick"][0]["ohlcv"]
|
||||
|
||||
ret = []
|
||||
for i in range(first_index, len(data)):
|
||||
ret.append([
|
||||
float(data[i][0]),
|
||||
float(data[i][1]),
|
||||
float(data[i][2]),
|
||||
float(data[i][3]),
|
||||
])
|
||||
for i in range(min(n, len(data))):
|
||||
j = len(data)-i-1
|
||||
ret.append([float(data[j][0]), float(data[j][1]), float(data[j][2]), float(data[j][3])])
|
||||
|
||||
if len(ret) < rem:
|
||||
back = datetime.timedelta(days=1)
|
||||
if long_term:
|
||||
back = datetime.timedelta(years=1)
|
||||
ret = [*ret, *await self._fetch(date-back, depth+1, rem-len(ret))]
|
||||
if len(ret) < n:
|
||||
t = t - datetime.timedelta(days=1)
|
||||
data = self._get_candle(unit, n-len(ret), t)
|
||||
ret = [*ret, *data]
|
||||
return ret
|
||||
|
||||
|
||||
class Depth(util.depth.Depth):
|
||||
def __init__(self, store, order_unit):
|
||||
super().__init__(order_unit)
|
||||
self._store = store
|
||||
asyncio.create_task(self._main())
|
||||
class Proc:
|
||||
def __init__(self, pri, pair, asset, algo, interval):
|
||||
self._pri = pri
|
||||
self._pair = pair
|
||||
|
||||
async def _main(self):
|
||||
while True:
|
||||
await self._store.depth.wait()
|
||||
items = self._store.depth.sorted()
|
||||
asks = items["asks"]
|
||||
bids = items["bids"]
|
||||
self._last_update = 0
|
||||
self._now = 0
|
||||
|
||||
self.asks = []
|
||||
for i in range(len(asks)):
|
||||
self.asks.append([float(asks[i][0]), float(asks[i][1])])
|
||||
self.bids = []
|
||||
for i in range(len(bids)):
|
||||
self.bids.append([float(bids[i][0]), float(bids[i][1])])
|
||||
self._event.set()
|
||||
self._event.clear()
|
||||
self._bsi = 0
|
||||
self._algo = algo
|
||||
self._interval = interval
|
||||
|
||||
self._last_order = None
|
||||
self._buy_price = 0
|
||||
|
||||
def _check_response(res):
|
||||
if res["success"] == 1:
|
||||
self._asset = asset
|
||||
|
||||
def tick(self):
|
||||
self._now = time.time()
|
||||
if self._now-self._last_update > self._interval:
|
||||
self._update()
|
||||
self._last_update = self._now
|
||||
|
||||
def _update(self):
|
||||
global status
|
||||
st = status[self._pair]
|
||||
|
||||
pbsi = self._bsi
|
||||
bsi = self._algo.judge(status[self._pair])
|
||||
if bsi != 0:
|
||||
self._bsi = bsi
|
||||
|
||||
print(f"[bitbank: {self._pair}] BSI {pbsi} -> {bsi}")
|
||||
if bsi*pbsi < 0:
|
||||
if bsi > 0:
|
||||
try:
|
||||
amount = self._asset/st.price
|
||||
self._order_market(amount, "buy")
|
||||
self._buy_price = st.price
|
||||
print(f"[bitbank: {self._pair}] <BUY> +{amount} / -{self._asset}")
|
||||
except Exception as e:
|
||||
print(f"[bitbank: {self._pair}] buy error", e)
|
||||
self._buy_price = 0
|
||||
elif self._buy_price > 0:
|
||||
try:
|
||||
amount = self._asset/self._buy_price
|
||||
self._order_market(amount, "sell")
|
||||
print(f"[bitbank: {self._pair}] <SELL> -{amount} / +{amount*st.price}")
|
||||
except Exception as e:
|
||||
print(f"[bitbank: {self._pair}] sell error", e)
|
||||
|
||||
def _order_market(self, amount, sell_or_buy):
|
||||
order = self._pri.order(self._pair, None, str(amount), sell_or_buy, "market")
|
||||
self._last_order = order
|
||||
|
||||
def _order_limit(self, price, sell_or_buy):
|
||||
try_price = price
|
||||
for i in range(10):
|
||||
amount = self._asset/price
|
||||
order = self._pri.order(self._pair, str(price), str(amount), sell_or_buy, "limit", True)
|
||||
time.sleep(.1)
|
||||
|
||||
order = self._pri.get_order(self._pair, order["order_id"])
|
||||
if order["status"] == "UNFILLED":
|
||||
return
|
||||
|
||||
code = str(res["data"]["code"])
|
||||
if code in ERROR_CODES:
|
||||
raise Exception(ERROR_CODES[code])
|
||||
raise Exception("unknown error")
|
||||
move = random.random()*10+1
|
||||
if sell_or_buy == "sell":
|
||||
price = price-move
|
||||
else:
|
||||
price = price+move
|
||||
|
||||
ERROR_CODES = {
|
||||
'10000': 'URLが存在しません',
|
||||
'10001': 'システムエラーが発生しました。サポートにお問い合わせ下さい',
|
||||
'10002': '不正なJSON形式です。送信内容をご確認下さい',
|
||||
'10003': 'システムエラーが発生しました。サポートにお問い合わせ下さい',
|
||||
'10005': 'タイムアウトエラーが発生しました。しばらく間をおいて再度実行して下さい',
|
||||
'20001': 'API認証に失敗しました',
|
||||
'20002': 'APIキーが不正です',
|
||||
'20003': 'APIキーが存在しません',
|
||||
'20004': 'API Nonceが存在しません',
|
||||
'20005': 'APIシグネチャが存在しません',
|
||||
'20011': '2段階認証に失敗しました',
|
||||
'20014': 'SMS認証に失敗しました',
|
||||
'20023': '2段階認証コードを入力して下さい',
|
||||
'20024': 'SMS認証コードを入力して下さい',
|
||||
'20025': '2段階認証コードとSMS認証コードを入力して下さい',
|
||||
'20026': '一定回数以上2段階認証に失敗したためロックしました。60秒待ってから再度お試しください',
|
||||
'30001': '注文数量を指定して下さい',
|
||||
'30006': '注文IDを指定して下さい',
|
||||
'30007': '注文ID配列を指定して下さい',
|
||||
'30009': '銘柄を指定して下さい',
|
||||
'30012': '注文価格を指定して下さい',
|
||||
'30013': '売買どちらかを指定して下さい',
|
||||
'30015': '注文タイプを指定して下さい',
|
||||
'30016': 'アセット名を指定して下さい',
|
||||
'30019': 'uuidを指定して下さい',
|
||||
'30039': '出金額を指定して下さい',
|
||||
'30101': 'トリガー価格を指定してください',
|
||||
'40001': '注文数量が不正です',
|
||||
'40006': 'count値が不正です',
|
||||
'40007': '終了時期が不正です',
|
||||
'40008': 'end_id値が不正です',
|
||||
'40009': 'from_id値が不正です',
|
||||
'40013': '注文IDが不正です',
|
||||
'40014': '注文ID配列が不正です',
|
||||
'40015': '指定された注文が多すぎます',
|
||||
'40017': '銘柄名が不正です',
|
||||
'40020': '注文価格が不正です',
|
||||
'40021': '売買区分が不正です',
|
||||
'40022': '開始時期が不正です',
|
||||
'40024': '注文タイプが不正です',
|
||||
'40025': 'アセット名が不正です',
|
||||
'40028': 'uuidが不正です',
|
||||
'40048': '出金額が不正です',
|
||||
'40112': 'トリガー価格が不正です',
|
||||
'40113': 'post_only値が不正です',
|
||||
'40114': 'Post Onlyはご指定の注文タイプでは指定できません',
|
||||
'50003': '現在、このアカウントはご指定の操作を実行できない状態となっております。サポートにお問い合わせ下さい',
|
||||
'50004': '現在、このアカウントは仮登録の状態となっております。アカウント登録完了後、再度お試し下さい',
|
||||
'50005': '現在、このアカウントはロックされております。サポートにお問い合わせ下さい',
|
||||
'50006': '現在、このアカウントはロックされております。サポートにお問い合わせ下さい',
|
||||
'50008': 'ユーザの本人確認が完了していません',
|
||||
'50009': 'ご指定の注文は存在しません',
|
||||
'50010': 'ご指定の注文はキャンセルできません',
|
||||
'50011': 'APIが見つかりません',
|
||||
'50026': 'ご指定の注文は既にキャンセル済みです',
|
||||
'50027': 'ご指定の注文は既に約定済みです',
|
||||
'60001': '保有数量が不足しています',
|
||||
'60002': '成行買い注文の数量上限を上回っています',
|
||||
'60003': '指定した数量が制限を超えています',
|
||||
'60004': '指定した数量がしきい値を下回っています',
|
||||
'60005': '指定した価格が上限を上回っています',
|
||||
'60006': '指定した価格が下限を下回っています',
|
||||
'60011': '同時発注制限件数(30件)を上回っています',
|
||||
'60016': '指定したトリガー価格が上限を上回っています',
|
||||
'70001': 'システムエラーが発生しました。サポートにお問い合わせ下さい',
|
||||
'70002': 'システムエラーが発生しました。サポートにお問い合わせ下さい',
|
||||
'70003': 'システムエラーが発生しました。サポートにお問い合わせ下さい',
|
||||
'70004': '現在取引停止中のため、注文を承ることができません',
|
||||
'70005': '現在買い注文停止中のため、注文を承ることができません',
|
||||
'70006': '現在売り注文停止中のため、注文を承ることができません',
|
||||
'70009': 'ただいま成行注文を一時的に制限しています。指値注文をご利用ください',
|
||||
'70010': 'ただいまシステム負荷が高まっているため、最小注文数量を一時的に引き上げています',
|
||||
'70011': 'ただいまリクエストが混雑してます。しばらく時間を空けてから再度リクエストをお願いします',
|
||||
'70012': 'システムエラーが発生しました。サポートにお問い合わせ下さい',
|
||||
'70013': 'ただいまシステム負荷が高まっているため、注文および注文キャンセルを一時的に制限しています',
|
||||
'70014': 'ただいまシステム負荷が高まっているため、出金申請および出金申請キャンセルを一時的に制限しています',
|
||||
'70015': 'ただいまシステム負荷が高まっているため、貸出申請および貸出申請キャンセルを一時的に制限しています',
|
||||
'70016': '貸出申請および貸出申請キャンセル停止中のため、リクエストを承ることができません',
|
||||
'70017': '指定された銘柄は注文停止中のため、リクエストを承ることができません',
|
||||
'70018': '指定された銘柄は注文およびキャンセル停止中のため、リクエストを承ることができません',
|
||||
'70019': '注文はキャンセル中です',
|
||||
'70020': '現在成行注文停止中のため、注文を承ることができません',
|
||||
'70021': '指値注文価格が乖離率を超過しています',
|
||||
'70022': '現在逆指値指値注文停止中のため、注文を承ることができません',
|
||||
'70023': '現在逆指値成行注文停止中のため、注文を承ることができません'
|
||||
}
|
||||
if sell_or_buy == "sell":
|
||||
self._pri.order(self._pair, None, str(amount), sell_or_buy, "market")
|
||||
print(f"[bitbank: {self._pair}] <MARKET-SELL> amount: {amount}")
|
||||
else:
|
||||
raise Exception("tried 10 times.... X(")
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from logic.mm import MM
|
||||
from logic.hige import Hige
|
||||
from logic.ema_chicken import EMA_Chicken
|
||||
@@ -1,66 +0,0 @@
|
||||
import asyncio
|
||||
|
||||
class EMA_Chicken:
|
||||
def __init__(self, logger, pair, candle, lot, limit_rate):
|
||||
self._logger = logger
|
||||
self._pair = pair
|
||||
self._candle = candle
|
||||
|
||||
self._remain = 0
|
||||
self._total = 0
|
||||
|
||||
self._lot = lot
|
||||
self._limit_rate = limit_rate
|
||||
|
||||
self._total = 0
|
||||
|
||||
asyncio.create_task(self._main())
|
||||
|
||||
async def _main(self):
|
||||
self._logger.info("started")
|
||||
while True:
|
||||
await self._pair.ticker.wait()
|
||||
if len(self._candle.values) == 0:
|
||||
continue
|
||||
|
||||
price = self._pair.ticker.price
|
||||
ema = self._candle.EMA(200)
|
||||
|
||||
if price > ema:
|
||||
if self._remain < self._lot*3:
|
||||
buy = await self._pair.buy_market(self._lot)
|
||||
await asyncio.sleep(0.5)
|
||||
await buy.update()
|
||||
outgo = buy.price * self._lot
|
||||
self._total -= outgo
|
||||
self._remain += self._lot
|
||||
self._logger.info(f"<BUY> {self._lot} / {outgo} ({self._remain} / {self._total})")
|
||||
|
||||
limit_price = buy.price*(1+self._limit_rate)
|
||||
stop_price = buy.price*(1-self._limit_rate)
|
||||
sell = await self._pair.sell_limit(self._lot, limit_price)
|
||||
while True:
|
||||
await asyncio.sleep(0.5)
|
||||
await sell.update()
|
||||
|
||||
if sell.done:
|
||||
amount = sell.amount - sell.remain
|
||||
income = sell.price * amount
|
||||
self._total += income
|
||||
self._remain -= amount
|
||||
if amount > 0:
|
||||
self._logger.info(f"[WIN] <SELL> {amount} / {income} ({self._remain} / {self._total})")
|
||||
break
|
||||
|
||||
if self._pair.ticker.price < stop_price:
|
||||
await sell.cancel()
|
||||
sell = await self._pair.sell_market(self._lot)
|
||||
await asyncio.sleep(0.5)
|
||||
await sell.update()
|
||||
amount = sell.amount
|
||||
income = sell.price * sell.amount
|
||||
self._total += income
|
||||
self._remain -= amount
|
||||
if amount > 0:
|
||||
self._logger.info(f"[LOSE] <SELL> {amount} / {income} ({self._remain} / {self._total})")
|
||||
break
|
||||
@@ -1,76 +0,0 @@
|
||||
import asyncio
|
||||
|
||||
|
||||
class Hige:
|
||||
def __init__(self, logger, pair, lot, delta):
|
||||
self._logger = logger
|
||||
self._pair = pair
|
||||
self._lot = lot
|
||||
self._delta = delta
|
||||
|
||||
self._remain = 0
|
||||
|
||||
self._sell_price = 0
|
||||
self._buy_price = 0
|
||||
self._sell = None
|
||||
self._buy = None
|
||||
|
||||
self._total = 0
|
||||
|
||||
asyncio.create_task(self._main())
|
||||
|
||||
async def _main(self):
|
||||
self._logger.info("started")
|
||||
while True:
|
||||
pair = self._pair
|
||||
depth = pair.depth
|
||||
|
||||
if len(depth.bids) == 0 or len(depth.asks) == 0:
|
||||
await depth.wait()
|
||||
continue
|
||||
|
||||
expected_sell_price = depth.asks[0][0] + self._delta
|
||||
expected_buy_price = depth.bids[0][0] - self._delta
|
||||
|
||||
if self._sell is not None:
|
||||
await self._sell.update()
|
||||
if self._sell.done:
|
||||
amount = self._sell.amount - self._sell.remain
|
||||
income = amount * self._sell.price
|
||||
self._remain -= amount
|
||||
self._total += income
|
||||
self._sell = None
|
||||
if amount > 0:
|
||||
self._logger.info(f"<SELL> {amount} / {income} ({self._remain} / {self._total})")
|
||||
continue
|
||||
elif self._sell_price != expected_sell_price:
|
||||
await self._sell.cancel()
|
||||
continue
|
||||
elif self._remain > pair.order_unit:
|
||||
amount = min(self._lot, self._remain)
|
||||
self._sell_price = expected_sell_price
|
||||
self._sell = await pair.sell_limit(amount, self._sell_price)
|
||||
|
||||
if self._buy is not None:
|
||||
await self._buy.update()
|
||||
if self._buy.done:
|
||||
amount = self._buy.amount - self._buy.remain
|
||||
outgo = amount * self._buy.price
|
||||
self._total -= outgo
|
||||
self._remain += amount
|
||||
self._buy = None
|
||||
if amount > 0:
|
||||
self._logger.info(f"<BUY> {amount} / {outgo} ({self._remain} / {self._total})")
|
||||
continue
|
||||
elif self._buy_price != expected_buy_price:
|
||||
await self._buy.cancel()
|
||||
continue
|
||||
elif self._remain < self._lot*3:
|
||||
self._buy_price = expected_buy_price
|
||||
self._buy = await pair.buy_limit(self._lot, self._buy_price)
|
||||
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def _reset_order(order, price):
|
||||
if not order.done and order.price != price:
|
||||
await order.cancel()
|
||||
135
logic/mm.py
135
logic/mm.py
@@ -1,135 +0,0 @@
|
||||
import asyncio
|
||||
|
||||
|
||||
class MM:
|
||||
def __init__(self, logger, pair, lot, spread_amount, spread_band):
|
||||
self._logger = logger
|
||||
self._pair = pair
|
||||
self._lot = lot
|
||||
self._remain = 0
|
||||
|
||||
self._buy_price = None
|
||||
self._sell_price = None
|
||||
self._buy = None
|
||||
self._sell = None
|
||||
|
||||
self._spread_amount = spread_amount
|
||||
self._spread_band = spread_band
|
||||
|
||||
self._total = 0
|
||||
|
||||
asyncio.create_task(self._main())
|
||||
|
||||
async def _main(self):
|
||||
self._logger.info("started")
|
||||
while True:
|
||||
pair = self._pair
|
||||
depth = pair.depth
|
||||
|
||||
await depth.wait()
|
||||
if len(depth.bids) == 0 or len(depth.asks) == 0:
|
||||
continue
|
||||
|
||||
# calculate best amount to buy/sell
|
||||
pos = self._remain / self._lot - 2
|
||||
buy_amount = _quad( pos) * self._lot
|
||||
sell_amount = _quad(-pos) * self._lot
|
||||
|
||||
# calculate ideal spread
|
||||
ask_delta_amount = self._spread_amount * (_quad( pos) + self._spread_band)
|
||||
bid_delta_amount = self._spread_amount * (_quad(-pos) + self._spread_band)
|
||||
|
||||
# calculate best sell price
|
||||
if self._sell is not None and self._sell.done:
|
||||
ask_delta_amount += self._sell.amount
|
||||
ask = 0
|
||||
prev = -1e100
|
||||
for i in range(len(depth.asks)):
|
||||
if (depth.asks[i][0]-prev) > pair.price_unit*1.5:
|
||||
ask = depth.asks[i][0] - pair.price_unit
|
||||
my_order = \
|
||||
self._sell is not None and \
|
||||
not self._sell.done and \
|
||||
abs(self._sell_price-depth.asks[i][0]) < pair.price_unit
|
||||
if my_order:
|
||||
ask_delta_amount -= depth.asks[i][1] - self._sell.amount
|
||||
prev = -1e100
|
||||
else:
|
||||
ask_delta_amount -= depth.asks[i][1]
|
||||
prev = depth.asks[i][0]
|
||||
if ask_delta_amount < 0:
|
||||
break
|
||||
|
||||
# calculate best buy price
|
||||
if self._buy is not None and self._buy.done:
|
||||
bid_delta_amount += self._buy.amount
|
||||
bid = 0
|
||||
prev = 1e100
|
||||
for i in range(len(depth.bids)):
|
||||
if (prev-depth.bids[i][0]) > pair.price_unit*1.5:
|
||||
bid = depth.bids[i][0] + pair.price_unit
|
||||
my_order = \
|
||||
self._buy is not None and \
|
||||
not self._buy.done and \
|
||||
abs(self._buy_price-depth.bids[i][0]) < pair.price_unit
|
||||
if my_order:
|
||||
bid_delta_amount -= depth.bids[i][1] - self._buy.amount
|
||||
prev = 1e100
|
||||
else:
|
||||
bid_delta_amount -= depth.bids[i][1]
|
||||
prev = depth.bids[i][0]
|
||||
if bid_delta_amount < 0:
|
||||
break
|
||||
|
||||
if self._sell is not None:
|
||||
# check current SELL order
|
||||
await asyncio.sleep(0.5)
|
||||
await self._sell.update()
|
||||
|
||||
if self._sell.done:
|
||||
amount = self._sell.amount - self._sell.remain
|
||||
self._remain -= amount
|
||||
if amount > 0:
|
||||
income = amount*self._sell.price
|
||||
self._total += income
|
||||
self._logger.info(f"<SELL> {amount} / {income} ({self._remain} / {self._total})")
|
||||
self._sell = None
|
||||
elif abs(self._sell_price-ask) >= pair.price_unit:
|
||||
await self._sell.cancel()
|
||||
elif sell_amount >= pair.order_unit:
|
||||
# order SELL
|
||||
self._sell_price = ask
|
||||
self._sell = await self._order_sell(sell_amount)
|
||||
|
||||
if self._buy is not None:
|
||||
# check current BUY order
|
||||
await self._buy.update()
|
||||
|
||||
if self._buy.done:
|
||||
amount = self._buy.amount - self._buy.remain
|
||||
self._remain += amount
|
||||
if amount > 0:
|
||||
outgo = amount*self._buy.price
|
||||
self._total -= outgo
|
||||
self._logger.info(f"<BUY> {amount} / {outgo} ({self._remain} / {self._total})")
|
||||
self._buy = None
|
||||
elif abs(self._buy_price-bid) >= pair.price_unit:
|
||||
await self._buy.cancel()
|
||||
elif buy_amount >= pair.order_unit:
|
||||
# order BUY
|
||||
self._buy_price = bid
|
||||
self._buy = await self._order_buy(buy_amount)
|
||||
|
||||
async def _order_sell(self, amount):
|
||||
return await self._pair.sell_limit(amount, self._sell_price, True)
|
||||
async def _order_buy(self, amount):
|
||||
return await self._pair.buy_limit(amount, self._buy_price, True)
|
||||
|
||||
|
||||
# https://kijitora-2018.hatenablog.com/entry/2018/12/23/102913
|
||||
def _quad(x):
|
||||
if x < -1:
|
||||
return 1
|
||||
if x <= 1:
|
||||
return -1/4 * (x+1)**2 + 1
|
||||
return 0
|
||||
136
main.py
136
main.py
@@ -1,32 +1,124 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import pybotters
|
||||
import multiprocessing
|
||||
import os
|
||||
import signal
|
||||
import time
|
||||
|
||||
import bitbank
|
||||
|
||||
|
||||
async def main():
|
||||
alive = True
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
alive = True
|
||||
|
||||
def teardown():
|
||||
nonlocal alive
|
||||
def main():
|
||||
global alive
|
||||
procs = [*bitbank.init()]
|
||||
while alive:
|
||||
for i in range(len(procs)):
|
||||
procs[i].tick()
|
||||
time.sleep(0.5)
|
||||
|
||||
def on_exit(sig, x):
|
||||
global alive
|
||||
alive = False
|
||||
for t in asyncio.all_tasks():
|
||||
t.cancel()
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.add_signal_handler(signal.SIGTERM, teardown)
|
||||
loop.add_signal_handler(signal.SIGINT, teardown)
|
||||
proc = multiprocessing.Process(target = main)
|
||||
proc.start()
|
||||
signal.signal(signal.SIGTERM, on_exit)
|
||||
signal.pause()
|
||||
proc.join()
|
||||
|
||||
async with pybotters.Client(apis="secret.json") as cli:
|
||||
await asyncio.gather(bitbank.init(cli))
|
||||
while alive: await asyncio.sleep(0.5)
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
loop = asyncio.new_event_loop()
|
||||
loop.run_until_complete(main())
|
||||
finally:
|
||||
loop.close()
|
||||
# while True:
|
||||
# print("start tick")
|
||||
#
|
||||
# # ---- get assets
|
||||
# assets = pri.get_asset()["assets"]
|
||||
# onhand = {}
|
||||
# locked = {}
|
||||
# for i in range(len(assets)):
|
||||
# coin = assets[i]["asset"]
|
||||
# onhand[coin] = float(assets[i]["onhand_amount"])
|
||||
# locked[coin] = float(assets[i]["locked_amount"])
|
||||
#
|
||||
# # ---- get current price
|
||||
# candles = pub.get_candlestick("btc_jpy", "1min", now.strftime("%Y%m%d"))["candlestick"][0]["ohlcv"];
|
||||
# latest_candle = candles[len(candles)-1]
|
||||
# begin = int(latest_candle[0])
|
||||
# high = int(latest_candle[1])
|
||||
# low = int(latest_candle[2])
|
||||
# end = int(latest_candle[3])
|
||||
#
|
||||
# # ---- cancel existing orderes
|
||||
# prev_orders = pri.get_active_orders("btc_jpy")["orders"]
|
||||
# cancel_orders = []
|
||||
# for i in range(len(prev_orders)):
|
||||
# id = prev_orders[i]["order_id"]
|
||||
# status = prev_orders[i]["status"]
|
||||
# side = prev_orders[i]["side"]
|
||||
# price = int(prev_orders[i]["price"])
|
||||
#
|
||||
# if status == "UNFILLED":
|
||||
# diff = abs(end-price)/end
|
||||
# if side == "buy":
|
||||
# if diff < -0.0005:
|
||||
# cancel_orders.append(str(id))
|
||||
# elif side == "sell":
|
||||
# if diff > 0.003:
|
||||
# cancel_orders.append(str(id))
|
||||
#
|
||||
# if len(cancel_orders) > 0:
|
||||
# pri.cancel_orders("btc_jpy", cancel_orders)
|
||||
# print("cancelled", len(cancel_orders), "orders")
|
||||
# if len(prev_orders)-len(cancel_orders) > 8:
|
||||
# print("skip making an order because there are too many orderes")
|
||||
# time.sleep(30)
|
||||
# continue
|
||||
#
|
||||
# # ---- get transactions
|
||||
# trans = pub.get_transactions("btc_jpy")["transactions"]
|
||||
# buy_min = low
|
||||
# sell_max = high
|
||||
# for i in range(len(trans)):
|
||||
# exec_at = datetime.datetime.fromtimestamp(trans[i]["executed_at"]/1000)
|
||||
# side = trans[i]["side"]
|
||||
# price = int(trans[i]["price"])
|
||||
# if now-exec_at < datetime.timedelta(minutes=1):
|
||||
# if side == "buy":
|
||||
# buy_min = min(buy_min, price)
|
||||
# elif side == "sell":
|
||||
# sell_max = max(sell_max, price)
|
||||
#
|
||||
# # ---- make an order
|
||||
# buy_coe = 0.5
|
||||
# ben_coe = 0.8
|
||||
#
|
||||
# diff = (end-begin)/end
|
||||
# if diff > 0.002:
|
||||
# buy_coe -= clamp(100*diff/end, 0, 1)*0.2
|
||||
# elif diff < -0.002:
|
||||
# buy_coe += clamp(100*diff, 0, 1)*0.2
|
||||
#
|
||||
# print("buy-min:", buy_min, "| sell-max:", sell_max, "| diff:", sell_max-buy_min)
|
||||
# diff = sell_max-buy_min
|
||||
# avg = (sell_max+buy_min)/2
|
||||
#
|
||||
# buy_price = avg - diff*buy_coe*ben_coe
|
||||
# sell_price = avg + diff*(1-buy_coe)*ben_coe
|
||||
#
|
||||
# buy_jpy = max((onhand["jpy"]-locked["jpy"])*0.1, (onhand["btc"]-locked["btc"])*end*0.1)
|
||||
# buy_btc = buy_jpy/buy_price
|
||||
# sell_jpy = buy_btc*sell_price
|
||||
# sell_btc = buy_btc
|
||||
#
|
||||
# try:
|
||||
# pri.order("btc_jpy", str(buy_price), str(buy_btc), "buy", "limit", True)
|
||||
# print("[buy] btc:", buy_btc, "| jpy:", -buy_jpy, "| price:", buy_price)
|
||||
# except Exception as e:
|
||||
# print(e)
|
||||
#
|
||||
# try:
|
||||
# pri.order("btc_jpy", str(sell_price), str(sell_btc), "sell", "limit", True)
|
||||
# print("[sel] btc:", -sell_btc, "| jpy:", sell_jpy, "| price:", sell_price)
|
||||
# except Exception as e:
|
||||
# print(e)
|
||||
#
|
||||
# time.sleep(10)
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pybotters
|
||||
git+https://github.com/bitbankinc/python-bitbankcc@3aee8a6ef9d4616e11f044c2a1574ec389671675#egg=python-bitbankcc
|
||||
requests
|
||||
|
||||
6
secret.json.default
Normal file
6
secret.json.default
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"bitbank": {
|
||||
"key": "your key",
|
||||
"secret": "your secret"
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
|
||||
|
||||
MAX_CANDLES = 200
|
||||
|
||||
OPEN = 0
|
||||
HIGH = 0
|
||||
LOW = 0
|
||||
CLOSE = 0
|
||||
|
||||
class Candlestick:
|
||||
def __init__(self, ticker, interval):
|
||||
self.values = []
|
||||
self.latest = None
|
||||
self.interval = interval
|
||||
|
||||
self._ticker = ticker
|
||||
self._event = asyncio.Event()
|
||||
|
||||
asyncio.create_task(self._refresh_and_start())
|
||||
|
||||
async def _refresh_and_start(self):
|
||||
self.values = await self._fetch(datetime.datetime.now(tz=datetime.timezone.utc))
|
||||
asyncio.create_task(self._ticker_watcher())
|
||||
asyncio.create_task(self._main())
|
||||
|
||||
async def _fetch(self, data, depth = 0, rem = MAX_CANDLES):
|
||||
assert(False)
|
||||
|
||||
async def _ticker_watcher(self):
|
||||
while True:
|
||||
await self._ticker.wait()
|
||||
v = self._ticker.price
|
||||
if self.latest is None:
|
||||
self.latest = [v, -1e100, 1e100, None]
|
||||
self.latest[1] = max(self.latest[1], v)
|
||||
self.latest[2] = min(self.latest[2], v)
|
||||
self.latest[3] = v
|
||||
|
||||
async def _main(self):
|
||||
while True:
|
||||
await asyncio.sleep(self.interval)
|
||||
if self.latest is None:
|
||||
if len(self.values) == 0:
|
||||
continue
|
||||
self.latest = self.values[0]
|
||||
n = min(MAX_CANDLES-1, len(self.values))
|
||||
self.values = [self.latest, *self.values[0:n]]
|
||||
self.latest = None
|
||||
self._event.set()
|
||||
self._event.clear()
|
||||
|
||||
async def wait(self):
|
||||
await self._event.wait()
|
||||
|
||||
def SMA(self, period, offset=0, type = CLOSE):
|
||||
sum = 0
|
||||
end = min(period+offset, len(self.values))
|
||||
for i in range(offset, end):
|
||||
sum += self.values[i][type]
|
||||
return sum / (end - offset)
|
||||
|
||||
def EMA(self, period, offset=0, type = CLOSE):
|
||||
end = min(period+offset, len(self.values))
|
||||
n = end - offset
|
||||
|
||||
ret = 0
|
||||
for i in range(offset, end):
|
||||
ret += self.values[i][type]
|
||||
ret /= n
|
||||
for i in range(offset, end):
|
||||
ret += (2/(n+1)) * (self.values[i][type] - ret)
|
||||
return ret
|
||||
@@ -1,15 +0,0 @@
|
||||
import asyncio
|
||||
|
||||
|
||||
MAX_ITEMS = 10
|
||||
|
||||
class Depth:
|
||||
def __init__(self, order_unit):
|
||||
self.bids = []
|
||||
self.asks = []
|
||||
|
||||
self._event = asyncio.Event()
|
||||
self._order_unit = order_unit
|
||||
|
||||
async def wait(self):
|
||||
await self._event.wait()
|
||||
51
util/pair.py
51
util/pair.py
@@ -1,51 +0,0 @@
|
||||
import asyncio
|
||||
|
||||
class Pair:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.ticker = None
|
||||
|
||||
self.candlestick_1s = None
|
||||
self.candlestick_1m = None
|
||||
self.candlestick_1h = None
|
||||
|
||||
self.order_unit = None
|
||||
self.price_unit = None
|
||||
|
||||
def buy_market(amount):
|
||||
assert(False)
|
||||
|
||||
def sell_market(amount):
|
||||
assert(False)
|
||||
|
||||
def buy_limit(amount, price):
|
||||
assert(False)
|
||||
|
||||
def sell_limit(amount, price):
|
||||
assert(False)
|
||||
|
||||
def buy_stop(amount, price):
|
||||
assert(False)
|
||||
|
||||
def sell_stop(amount, price):
|
||||
assert(False)
|
||||
|
||||
def buy_stop_limit(amount, trigger, price):
|
||||
assert(False)
|
||||
|
||||
def sell_stop_limit(amount, trigger, price):
|
||||
assert(False)
|
||||
|
||||
|
||||
class Order:
|
||||
def __init__(self):
|
||||
self.price = None
|
||||
self.amount = None
|
||||
self.remain = None
|
||||
self.done = False
|
||||
|
||||
async def update(self):
|
||||
assert(False)
|
||||
|
||||
async def cancel(self):
|
||||
assert(False)
|
||||
@@ -1,14 +0,0 @@
|
||||
import asyncio
|
||||
|
||||
class Ticker:
|
||||
def __init__(self, pair):
|
||||
self.pair = pair
|
||||
self.price = None
|
||||
self.sell = None
|
||||
self.buy = None
|
||||
self.volume = None
|
||||
self.ts = None
|
||||
self._event = asyncio.Event()
|
||||
|
||||
async def wait(self):
|
||||
await self._event.wait()
|
||||
Reference in New Issue
Block a user