Compare commits

..

6 Commits

Author SHA1 Message Date
314d60792e fix an issue that the MM-algorithm dies after losscut 2023-08-03 02:52:23 +09:00
ccb8c2967c fix errors 2023-07-23 16:55:32 +09:00
9784f8627b fix an issue that start message is not shown 2023-07-23 09:04:06 +09:00
e376eea76b implement losscut in MM algorithm 2023-07-23 08:59:04 +09:00
f6413a3389 add MM algorithm for bitbank 2023-07-23 01:06:38 +09:00
b5cb84828d create new project 2023-07-23 01:06:25 +09:00
7 changed files with 304 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
__pycache__/
/config.json

111
algo/mm.py Normal file
View File

@@ -0,0 +1,111 @@
# 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._lastPrice = None
self._delta = float(config["delta"])
self._positiveCut = float(config["+cut"])
self._negativeCut = float(config["-cut"])
async def update(self):
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) / 2
pos = sales.amount / lot - 1
# evaluate if cutting should be executed
negCut = False
posCut = False
if self._lastPrice is not None:
change = (price - self._lastPrice) / self._lastPrice
negCut = pos > 0.5 and change < -abs(self._negativeCut)
posCut = pos < -0.5 and change > abs(self._positiveCut)
cutting = negCut or posCut
# check if the past orders are active
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 and not cutting:
return
# cancel active orders
cancel_orders = []
if not lastBuyAgreed and self._lastBuy is not None:
logging.info(f"cancel buy")
cancel_orders.append(self._lastBuy)
elif lastBuyAgreed:
logging.info(f"agree buy")
if not lastSellAgreed and self._lastSell is not None:
logging.info(f"cancel sell")
cancel_orders.append(self._lastSell)
elif lastSellAgreed:
logging.info(f"agree sell")
if len(cancel_orders) != 0:
await self._player.cancel(self._pair.names, cancel_orders)
self._lastBuy = None
self._lastSell = None
# execute cutting
if cutting:
if negCut:
logging.info(f"-cut (change: {change}, pos: {pos} -> 0)")
if posCut:
logging.info(f"+cut (change: {change}, pos: {pos} -> 0)")
self._lastBuy = ""
self._lastSell = ""
sell_amount = pos * lot
if sell_amount > 0:
await self._player.orderMarketSell(self._pair.names, amount=sell_amount)
else:
await self._player.orderMarketBuy(self._pair.names, amount=-buy_amount)
return
# execute new cycle
self._lastPrice = None
buy_price = price * (1 - self._delta);
sell_price = price * (1 + self._delta);
buy_amount = min(_quad( pos) * lot, crncy.amount / buy_price)
sell_amount = min(_quad(-pos) * lot, sales.amount)
order_unit = pow(10, -sales.precision)
logging.info(f"new cycle (pos: {pos}, price: {price}, buy: {buy_amount}, sell: {sell_amount})")
async def buy():
self._lastBuy = (
None if order_unit > buy_amount else
await self._player.orderLimitBuy(
self._pair.names, amount = buy_amount, price = buy_price))
async def sell():
self._lastSell = (
None if order_unit > sell_amount else
await self._player.orderLimitSell(
self._pair.names, amount = sell_amount, price = sell_price))
await asyncio.gather(buy(), sell())
self._lastPrice = price
# 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

9
docker-compose.yml Normal file
View File

@@ -0,0 +1,9 @@
version: '3'
services:
env:
image: python:3-alpine
command: sh -c "pip3 install -q -r requirements.txt && python3 main.py"
working_dir: /repo/
volumes:
- ./:/repo/

51
main.py Normal file
View File

@@ -0,0 +1,51 @@
# 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)
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)
logging.info("#### TMM ####")
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()

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
pybotters

23
util/pair.py Normal file
View File

@@ -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"])

107
util/player.py Normal file
View File

@@ -0,0 +1,107 @@
# No copyright
import asyncio
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
})
async def orderMarketSell(self, pair, amount):
res = await self._post("user/spot/order", data={
"pair": f"{pair[0]}_{pair[1]}",
"amount": str(amount),
"side": "sell",
"type": "market",
})
return res["data"]["order_id"]
async def orderMarketBuy(self, pair, amount):
res = await self._post("user/spot/order", data={
"pair": f"{pair[0]}_{pair[1]}",
"amount": str(amount),
"side": "buy",
"type": "market",
})
return res["data"]["order_id"]
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 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 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):
for i in range(10):
try:
res = await self._pb.post(f"https://api.bitbank.cc/v1/{suffix}", data=data)
return self._check(await res.json())
except Exception as e:
err = e
await asyncio.sleep(1)
raise Exception(f"API error: {suffix} ({err})")
async def _get(self, suffix):
for i in range(10):
try:
res = await self._pb.get(f"https://api.bitbank.cc/v1/{suffix}")
return self._check(await res.json())
except Exception as e:
err = e
await asyncio.sleep(1)
raise Exception(f"API error: {suffix} ({err})")
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"])