Exemple #1
0
    def test_ETHBTC(self):
        base, quote = utils.give_base_quote("ETHBTC")
        market, dir = utils.give_pair_market_direction(
            TestGivePairMarketDirection.test_markets, quote, base)

        self.assertEqual(market, "ETHBTC")
        self.assertEqual(dir, const.BUY_ORDER_DIRECTION)

        base, quote = utils.give_base_quote("ETHBTC")
        market, dir = utils.give_pair_market_direction(
            TestGivePairMarketDirection.test_markets, base, quote)

        self.assertEqual(market, "ETHBTC")
        self.assertEqual(dir, const.SELL_ORDER_DIRECTION)
Exemple #2
0
    def test_no_regression(self):
        new_start = time.time()
        for i in range(10000):
            utils.give_base_quote("BTCUSDT")
            utils.give_base_quote("ETHBTC")
        new_end = time.time()
        new_time_taken = new_end - new_start

        vanilla_start = time.time()
        for i in range(10000):
            utils.give_base_quote("BTCUSDT")
            utils.give_base_quote("ETHBTC")
        vanilla_end = time.time()

        vanilla_time_taken = vanilla_end - vanilla_start

        self.assertLess(new_time_taken, vanilla_time_taken)
Exemple #3
0
    def test_BTCUSDT(self):
        base, quote = utils.give_base_quote("BTCUSDT")

        # quote to base is a buy
        market, dir = utils.give_pair_market_direction(
            TestGivePairMarketDirection.test_markets, quote, base)

        self.assertEqual(market, "BTCUSDT")
        self.assertEqual(dir, const.BUY_ORDER_DIRECTION)

        # base to quote is a sell
        market, dir = utils.give_pair_market_direction(
            TestGivePairMarketDirection.test_markets, base, quote)

        self.assertEqual(market, "BTCUSDT")
        self.assertEqual(dir, const.SELL_ORDER_DIRECTION)

        market, dir = utils.give_pair_market_direction(
            TestGivePairMarketDirection.test_markets, base, quote)

        self.assertEqual(market, "BTCUSDT")
        self.assertEqual(dir, const.SELL_ORDER_DIRECTION)
Exemple #4
0
def main(argv):

    rates = collections.defaultdict(dict)
    graph = collections.defaultdict(dict)

    orderbook_dicts = {}
    markets_info = {}

    client = Client(CONFIG.API_PUB, CONFIG.API_PRI, {"timeout": 5})

    # set up our orderbook_dicts with orderbook instances.
    # this list will be used to check that we have some data about every market
    # before executing trades.
    orderbook_dicts = {m: LimitOrderBook(m) for m in CONFIG.MARKETS}

    # Retrieve data about markets, IE minimum quantity, minimum price increment etc
    # Do the moving around before we even start the bot so that we get O(1)
    # Access later.

    executed_trades = {}

    markets_info = initialise_markets_info()
    start_time = time.time()

    execute_lock = True
    execute_trade_queue = multiprocessing.Queue()

    report_queue = multiprocessing.Queue()

    execute_process = multiprocessing.Process(target=execute.execute_loop,
                                              args=(
                                                  execute_trade_queue,
                                                  report_queue,
                                                  markets_info,
                                                  CONFIG.EXECUTE_IP,
                                              ))

    reporting_process = multiprocessing.Process(
        target=reporting.reporting_loop, args=(report_queue, ))

    listen_process = multiprocessing.Process(target=listen.listen_orderbook,
                                             args=(markets_info.keys(), ))

    execute_process.start()
    reporting_process.start()

    if CONFIG.MODE == const.DRY_MODE:
        listen_process.start()

    if FLAGS.input:
        raw_data_queue = Queue()
        for line in open(FLAGS.input):
            element_list = json.loads(line)
            LOGGER.info(len(element_list))
            for element in element_list:
                element[0] = element[0].encode()
                element[1] = element[1].encode()
                raw_data_queue.put(element)

        class FakeSocket(object):
            pass

        socket = FakeSocket()
        socket.recv_multipart = raw_data_queue.get_nowait
    else:
        LOGGER.info("LIVE DATA FEED")
        # zmq - subscribe to our zmq server
        ctx = zmq.Context()
        socket = ctx.socket(zmq.SUB)
        socket.connect(CONFIG.ZMQ_SOCKET)
        socket.setsockopt(zmq.SUBSCRIBE, CONFIG.ZMQ_TOPIC)
        socket.linger = 10

    b = client.get_asset_balance("USDT")
    start_bal = float(b["free"])
    current_bal = float(b["free"])

    while True:
        # check if the execute_lock is False, check for execution results, update
        # balances and profits.
        if not execute_lock:
            if not execute_trade_queue.empty():
                check_exec = execute_trade_queue.get(block=False)
            else:
                check_exec = None

            if check_exec:
                if check_exec["type"] == const.EXECUTE_RESULT_TYPE:
                    execute_lock = True
                if check_exec["type"] == const.EXECUTE_TOO_SOON_TYPE:
                    execute_lock = True

                LOGGER.info(str(check_exec))
                execute_lock = True
                # Should the execute_handler update orderbooks with trade results?
                # We have moved the market; we should be sure not to try and execute on
                # oppurtunities we've already taken.

        # We need to keep getting messages if they exist here..
        # this implementation for retrieving messages is not ideal. We should
        # receive messages until there are no more, then perform analysis, and
        # queue any trades. Then look for more messages. Instead we find one
        # message and perform analysis / trades on potentially stale info.
        # LOGGER.info("TEST")
        # LOGGER.info(markets_info.keys())

        msg = None
        try:
            topic, msg = socket.recv_multipart(zmq.NOBLOCK)
        except zmq.error.Again:
            # LOGGER.info("zmq.error.Again".format(msg))
            continue

        if not msg:
            continue

        msg = json.loads(msg.decode("utf-8"))
        topic = topic.decode("utf-8")

        # topic = (
        #     topic[WEB_SOCKETS_TOPIC_HEAD_LENGTH: -WEB_SOCKETS_TOPIC_TAIL_LENGTH]
        #     .upper()
        # )

        if not topic:
            try:
                topic = msg["market"]
            except KeyError as e:
                LOGGER.error(
                    "We weren't able to find the market for this message {}"\
                    .format(msg)
                )
                continue

        market = topic

        base, quote = give_base_quote(market)
        if market not in orderbook_dicts.keys():
            continue

        orderbook_dicts[market].update_levels_from_partial(msg)
        rate = orderbook_dicts[market].best_price["price"]

        add_to_graph(graph, rates, base, rate, quote, TRANSACTION_COST)

        # ensure all orderbooks are populated before continuing... Not a check
        # that should really be in the exec loop.
        orderbooks_populated = True
        for m, lob in orderbook_dicts.items():
            if not orderbooks_populated:
                break
            if not lob.last_update:
                orderbooks_populated = False
                LOGGER.info("{} still not populated".format(m))

        if not orderbooks_populated:
            continue

        max_q = 0
        path_profit = 0

        # find_cycles will return cycles sorted by profit multiplier, however actual
        # profit may be 0 because the quantites available are too small. We should
        # loop through arbs to see if they provide some profit.
        # this should be modified to the most profitable path we haven't run this
        # second. This implementation is PoC of this being the problem.

        for arbitrage in find_cycles(graph, rates):
            path = arbitrage["currencies"]
            trades = []

            # get trade market and directions for the path
            for i in range(len(path) - 1):
                market, direction = give_pair_market_direction(
                    CONFIG.MARKETS, path[i], path[i + 1])
                trades.append({"market": market, "direction": direction})

            path_str = "".join(sorted([c["market"] for c in trades]))
            try:
                path_last_executed = executed_trades[path_str]
            except KeyError as e:
                executed_trades[path_str] = 0
                path_last_executed = executed_trades[path_str]

            if time.time() - path_last_executed < 3:
                continue

            executed_trades[path_str] = time.time()

            max_q = round(
                give_max_quantity_through_path(trades, rates, orderbook_dicts,
                                               50), 2)

            # hopefully less quantity equals less risk
            if max_q > 50.0:
                max_q = 50.00

            # calculate some things
            path_return = round(max_q * arbitrage["value"], 2)
            path_profit = round(path_return - max_q, 2)
            trade_str = "->".join([x["market"] for x in trades])

            if path_profit < 0.05:
                continue
            if path_profit > 0.05:
                break

        # CHECK THE PATH ACTUALLY ADDS UP
        if max_q > 1 and path_profit > 0.01:
            # max_q IS FIAT QUANTITY. NEED CRYPTO QUANTITY.
            base, quote = give_base_quote(trades[0]["market"])
            max_q_fiat = max_q
            max_q = max_q / rates[base][quote]

            try:
                max_q = closest_tradeable_quantity(
                    markets_info[trades[0]["market"]], max_q)
            except QuantityTooSmallError:
                LOGGER.info("QUANTITY_TOO_SMALL for market".format(
                    trades[0]["market"]))
                continue

            # 25% is a little optimistic, let's sanity check this!
            if arbitrage["value"] > 1.25:
                LOGGER.info("SKIPPING UNREALISTIC PATH VALUE: ".format(
                    arbitrage["currencies"], arbitrage["value"]))
                LOGGER.info(
                    "ORDERBOOK BEST PRICES: ",
                    [
                        orderbook_dicts[x["market"]].best_price["price"]
                        for x in trades
                    ],
                )
                # here investigate the orderbooks for markets in the path with
                # particular emphasis on their content and last_update timestamp

            opportunity_tag = str(uuid.uuid4())
            LOGGER.info("{} {} {} {}".format(str(datetime.datetime.utcnow()),
                                             trade_str, max_q_fiat,
                                             path_profit))
            if CONFIG.EXECUTE:
                LOGGER.info("{} {} {} {}".format(
                    str(datetime.datetime.utcnow()), trade_str, max_q_fiat,
                    path_profit))
                report_queue.put({
                    "obj_type": const.OBJ_TYPE_OPPPORTUNITY,
                    "path": str(trades),
                    "max_q_fiat": max_q_fiat,
                    "path_profit": path_profit,
                    "timestamp": time.time(),
                    "opportunity_tag": opportunity_tag
                })

                # We need a pre-execution check to ensure that the orderbooks were
                # up to date when the path was generated.

                now = time.time()
                break_execute = False
                for t in trades:
                    if (now - orderbook_dicts[t["market"]].last_update >
                            CONFIG.STALE_TIMEOUT):
                        break_execute = True

                if break_execute:
                    LOGGER.info("STALE PATH")
                    LOGGER.info(
                        str(now - orderbook_dicts[t["market"]].last_update))
                    continue

                del (now)

                # Update the balance; We've allocated this money.
                current_bal = current_bal - max_q

                # put a job onto the executor.

                if execute_lock:
                    execute_lock = False

                    execute_trade_queue.put({
                        "type":
                        const.EXECUTE_PATH_TYPE,
                        "path":
                        trades,
                        "rates":
                        rates,
                        "quantity":
                        closest_tradeable_quantity(
                            markets_info[trades[0]["market"]], max_q),
                        "initial_quantity":
                        max_q_fiat,
                        "opportunity_tag":
                        opportunity_tag
                    })

                    report_queue.put({
                        "obj_type": const.OBJ_TYPE_EVENT,
                        "event": const.EVENT_OPPORTUNITY_SENT_TO_EXECUTOR,
                        "timestamp": time.time(),
                        "opportunity_tag": opportunity_tag
                    })
                else:
                    continue
            else:
                report_queue.put({
                    "obj_type": const.OBJ_TYPE_OPPPORTUNITY,
                    "path": str(trades),
                    "max_q_fiat": max_q_fiat,
                    "path_profit": path_profit,
                    "timestamp": time.time(),
                    "opportunity_tag": opportunity_tag
                })
                LOGGER.info("{} {} {} {}".format(
                    str(datetime.datetime.utcnow()), trade_str, max_q_fiat,
                    path_profit))
Exemple #5
0
 def test_all(self):
     for m in ALL_MARKETS:
         b, q = utils.give_base_quote(m)
         self.assertTrue(b != "")
         self.assertIn(q, ["ETH", "BTC", "USDT", "BNB"])
Exemple #6
0
 def test_five(self):
     b, q = utils.give_base_quote("BTCGBP")
     self.assertEqual(b, "BTC")
     self.assertEqual(q, "GBP")
Exemple #7
0
 def test_four(self):
     b, q = utils.give_base_quote("BTCCHF")
     self.assertEqual(b, "BTC")
     self.assertEqual(q, "CHF")
Exemple #8
0
 def test_three(self):
     b, q = utils.give_base_quote("BNBETH")
     self.assertEqual(b, "BNB")
     self.assertEqual(q, "ETH")
Exemple #9
0
 def test_two(self):
     b, q = utils.give_base_quote("BNBUSDT")
     self.assertEqual(b, "BNB")
     self.assertEqual(q, "USDT")
Exemple #10
0
 def test_one(self):
     b, q = utils.give_base_quote("BTCUSDT")
     self.assertEqual(b, "BTC")
     self.assertEqual(q, "USDT")
Exemple #11
0
def execute_loop(execute_queue, report_queue, market_info, this_ip=None):
    client = Client(CONFIG.API_PUB, CONFIG.API_PRI)

    which_order = {
        "dry": client.create_test_order,
        "production": client.create_order,
    }

    make_order = which_order["production"]
    nwmon = utils.SimpleNwmon()

    while True:
        try:
            execorder = json.loads(execute_queue.get(False))
        except Exception as e:
            continue
        if execorder["type"] == "SHUTDOWN":
            quit()

        if "rates" not in execorder:
            continue

        limits_ok = nwmon.can_complete_path(len(execorder["path"]))
        if not limits_ok: continue

        rates = execorder["rates"]
        orders = []

        EXPECTED_PATH = True
        # for loops are slow, use a while len trades -1 or something
        for trade in execorder["path"]:
            # determine the quantity dependant on the previous quantities
            this_base, this_quote = utils.give_base_quote(trade["market"])
            if len(orders) > 0:
                prev_base, prev_quote = utils.give_base_quote(orders[-1]["symbol"])


                if this_base == prev_base:
                    this_quantity = float(orders[-1]["executedQty"])
                else:
                    # we need to work out the order quantity based on the
                    # previously executed quantities.
                    if trade["direction"] == BUY:
                        # previously_executed / base/quote rate
                        this_quantity = utils.closest_tradeable_quantity(
                            market_info[trade["market"]],
                            float(orders[-1]["executedQty"])
                            / rates[this_base][this_quote],
                        )
                    if trade["direction"] == SELL:
                        # prev qty * avg_price / base/quote rate
                        exec_prices = [
                            float(execorder["qty"]) * float(execorder["price"])
                            for x in orders[-1]["fills"]
                        ]
                        avg_exec_price = round(
                            sum(exec_prices) / float(orders[-1]["executedQty"]), 8
                        )
                        # * for sell not /
                        this_quantity = (
                            float(orders[-1]["executedQty"])
                            * avg_exec_price
                            * rates[this_base][this_quote]
                        )
                        this_quantity = utils.closest_tradeable_quantity(
                            market_info[trade["market"]], this_quantity
                        )
            else:
                this_quantity = float(execorder["quantity"])

            try:
                order = make_order(
                    symbol=trade["market"], side=trade["direction"],
                    type="LIMIT",
                    quantity=utils.closest_tradeable_quantity(
                        market_info[trade["market"]], this_quantity
                    ),
                    price=str(round(rates[this_base][this_quote], 8)),
                    timeInForce=("FOK"), # Fill immediately or cancel
                    newOrderRespType="FULL",
                )

                if order["status"] == "FILLED":
                    orders.append(order)
                    nwmon.iterate_order()
                    continue
                else:
                    nwmon.iterate_order()
                    raise TooSlowException("Too slow.")


            except (BinanceAPIException, TooSlowException, Exception) as e:
                # should log error
                EXPECTED_PATH = False

                if len(orders) > 0:
                    prev_base, prev_quote = utils.give_base_quote(orders[-1]["symbol"])

                    direction = orders[-1]["side"]
                    if direction == BUY:
                        currency = prev_base
                    if direction == SELL:
                        currency = prev_quote

                    # membership check quicker if config.ALL_MARKETS was a dict
                    if currency + "USDT" not in config.ALL_MARKETS:
                        order = order_method(
                            symbol=currency + "BTC",
                            side=SELL,
                            type="MARKET",
                            quantity=closest_tradeable_quantity(
                                market_info[str(currency + "BTC")],
                                float(orders[-1]["executedQty"])
                            ),
                            newOrderRespType="FULL",
                        )

                        # Next currency will be in > BTC to USDT
                        currency = "BTC"
                        orders.append(order)
                        nwmon.iterate_order()


                    order = order_method(
                        symbol=currency + "USDT",
                        side=SELL,
                        type="MARKET",
                        quantity=closest_tradeable_quantity(
                            market_info[str(currency + "USDT")],
                            float(orders[-1]["executedQty"]),
                        ),
                        newOrderRespType="FULL",
                    )

                    orders.append(order)
                    nwmon.iterate_order()


                # this break will exit the for each trade in trades loop
                # but finally will still be executed.
                break  # cancel this path

            finally:
                # calculate profit
                input_q = execorder["quantity"]
                output_q = 0


                if len(orders) > 1:
                    for fill in orders[-1]["fills"]:
                        # Commission will always be the source
                        proceeds = proceeds + float(fill["price"]) * float(fill["qty"])

                    profit = output_q - input_q
                    profit_percent = proit / input_q * 100
                else:
                    profit = 0
                    profit_percent = 0
                    proceeds = exec_prices["initial_quantity"]

                result = {
                    "type": const.EXECUTE_RESULT_TYPE,
                    "obj_type": const.OBJ_TYPE_RESULT,
                    "path": execorder["path"],
                    "orders": orders,
                    "completed_path": EXPECTED_PATH,
                    "profit": profit,
                    "profit_percent": profit_percent,
                    "opportunity_tag": execorder["opportunity_tag"]
                }

                report_queue.put(result)
                execute_queue.put(result)