diff --git a/algo/mm.py b/algo/mm.py new file mode 100644 index 0000000..6343af8 --- /dev/null +++ b/algo/mm.py @@ -0,0 +1,70 @@ +# No copyright +import asyncio +import logging + +class MM: + def __init__(self, player, pair, config): + self._player = player + self._pair = pair + + self._lastBuy = "" + self._lastSell = "" + + self._delta = float(config["delta"]) + + async def update(self): + lastBuyAgreed = (self._lastBuy is not None and + self._lastBuy not in self._player.orders) + lastSellAgreed = (self._lastSell is not None and + self._lastSell not in self._player.orders) + + if not lastBuyAgreed and not lastSellAgreed: + return + + cancel_orders = [] + if not lastBuyAgreed and self._lastBuy is not None: + cancel_orders.append(self._lastBuy) + self._lastBuy = None + if not lastSellAgreed and self._lastSell is not None: + cancel_orders.append(self._lastSell) + self._lastSell = None + if len(cancel_orders) != 0: + await self._player.cancel(self._pair.names, cancel_orders) + + sales = self._player.assets[self._pair.names[0]] + crncy = self._player.assets[self._pair.names[1]] + price = self._pair.ticker.price + + lot = (sales.amount + crncy.amount / price) / 4 + pos = sales.amount / lot - 2 + + buy_amount = _quad( pos) * lot + sell_amount = _quad(-pos) * lot + + buy_price = price * (1 - self._delta); + sell_price = price * (1 + self._delta); + + order_unit = pow(10, -sales.precision) + + async def buy(): + return (None if order_unit > buy_amount else + await self._player.orderLimitBuy( + self._pair.names, amount = buy_amount, price = buy_price)) + + async def sell(): + return (None if order_unit > sell_amount else + await self._player.orderLimitSell( + self._pair.names, amount = sell_amount, price = sell_price)) + + orders = await asyncio.gather(buy(), sell()) + self._lastBuy = orders[0] + self._lastSell = orders[1] + +# 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 + diff --git a/main.py b/main.py index 71d9a9b..f3a81a0 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,50 @@ +# No copyright + +import asyncio import json +import logging +import pybotters +import signal + +from util.pair import Pair +from util.player import Player +from algo.mm import MM with open("config.json", "r") as file: config = json.load(file) + +async def main(): + alive = True + logging.basicConfig(level=logging.DEBUG) -print(config) + def teardown(): + nonlocal 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) + + async with pybotters.Client(apis={"bitbank":config["auth"]}) as pb: + player = Player(pb) + pair = Pair(pb, config["pair"]) + algo = MM(player, pair, config["algorithm"]["mm"]) + while alive: + try: + await asyncio.gather( + player.update(), + pair.update()) + await algo.update() + except Exception as e: + print(e) + logging.error(e) + await asyncio.sleep(int(config["interval"])) + +if __name__ == "__main__": + try: + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + finally: + loop.close() diff --git a/util/pair.py b/util/pair.py new file mode 100644 index 0000000..44e0653 --- /dev/null +++ b/util/pair.py @@ -0,0 +1,23 @@ +# No copyright +import logging + +class Pair: + def __init__(self, pb, names): + self.names = names + self._pb = pb + + async def update(self): + ticker = (await self._get("ticker"))["data"] + self.ticker = Ticker(ticker) + + async def _get(self, suffix): + res = await self._pb.get(f"https://public.bitbank.cc/{self.names[0]}_{self.names[1]}/{suffix}") + json = await res.json() + if "success" not in json or 1 != json["success"]: + code = json["data"]["code"] + raise Exception(f"API error: {code}") + return json + +class Ticker: + def __init__(self, json): + self.price = float(json["last"]) diff --git a/util/player.py b/util/player.py new file mode 100644 index 0000000..e5cebf2 --- /dev/null +++ b/util/player.py @@ -0,0 +1,77 @@ +# No copyright +import logging + +class Player: + def __init__(self, pb): + self._pb = pb + self.assets = {} + self.orders = {} + + async def cancel(self, pair, orders): + res = await self._post("user/spot/cancel_orders", data={ + "pair": f"{pair[0]}_{pair[1]}", + "order_ids": orders + }) + self._check(res) + + async def orderLimitSell(self, pair, amount, price): + res = await self._post("user/spot/order", data={ + "pair": f"{pair[0]}_{pair[1]}", + "amount": str(amount), + "price": str(price), + "side": "sell", + "type": "limit", + }) + return self._check(res)["data"]["order_id"] + + async def orderLimitBuy(self, pair, amount, price): + res = await self._post("user/spot/order", data={ + "pair": f"{pair[0]}_{pair[1]}", + "amount": str(amount), + "price": str(price), + "side": "buy", + "type": "limit", + }) + return self._check(res)["data"]["order_id"] + + async def update(self): + self.assets = {} + self.orders = {} + + assets = (await self._get("user/assets"))["data"]["assets"] + for asset in assets: + self.assets[asset["asset"]] = Asset(asset) + + orders = (await self._get("user/spot/active_orders"))["data"]["orders"] + for order in orders: + self.orders[order["order_id"]] = Order(order) + + async def _post(self, suffix, data): + res = await self._pb.post(f"https://api.bitbank.cc/v1/{suffix}", data=data) + return self._check(await res.json()) + + async def _get(self, suffix): + res = await self._pb.get(f"https://api.bitbank.cc/v1/{suffix}") + return self._check(await res.json()) + + def _check(self, json): + if "success" not in json or 1 != json["success"]: + code = json["data"]["code"] + raise Exception(f"API error: {code}") + return json + + +class Asset: + def __init__(self, json): + self.name = json["asset"] + self.amount = float(json["onhand_amount"]) + self.locked = float(json["locked_amount"]) + self.precision = int(json["amount_precision"]) + +class Order: + def __init__(self, json): + self.id = json["order_id"] + self.pair = json["pair"] + self.amount = float(json["start_amount"]) + self.remain = float(json["remaining_amount"]) + self.price = float(json["average_price"])