예제 #1
0
    def on_order_cancellation(self, order: Order):
        base_asset = jh.base_asset(order.symbol)
        quote_asset = jh.quote_asset(order.symbol)

        # used for logging balance change
        temp_old_quote_available_asset = self.available_assets[quote_asset]
        temp_old_base_available_asset = self.available_assets[base_asset]

        if order.side == sides.BUY:
            self.available_assets[quote_asset] += (
                abs(order.qty) * order.price) * (1 + self.fee_rate)
        # sell order
        else:
            self.available_assets[base_asset] += abs(order.qty)

        temp_new_quote_available_asset = self.available_assets[quote_asset]
        if jh.is_debuggable(
                'balance_update'
        ) and temp_old_quote_available_asset != temp_new_quote_available_asset:
            logger.info(
                'Available balance for {} on {} changed from {} to {}'.format(
                    quote_asset, self.name,
                    round(temp_old_quote_available_asset, 2),
                    round(temp_new_quote_available_asset, 2)))
        temp_new_base_available_asset = self.available_assets[base_asset]
        if jh.is_debuggable(
                'balance_update'
        ) and temp_old_base_available_asset != temp_new_base_available_asset:
            logger.info(
                'Available balance for {} on {} changed from {} to {}'.format(
                    base_asset, self.name,
                    round(temp_old_base_available_asset, 2),
                    round(temp_new_base_available_asset, 2)))
예제 #2
0
    def on_order_execution(self, order: Order):
        base_asset = jh.base_asset(order.symbol)
        quote_asset = jh.quote_asset(order.symbol)

        if order.type == order_types.MARKET:
            self.on_order_submission(order, skip_market_order=False)

        # used for logging balance change
        temp_old_quote_asset = self.assets[quote_asset]
        temp_old_quote_available_asset = self.available_assets[quote_asset]
        temp_old_base_asset = self.assets[base_asset]
        temp_old_base_available_asset = self.available_assets[base_asset]

        # works for both buy and sell orders (sell order's qty < 0)
        self.assets[base_asset] += order.qty

        if order.side == sides.BUY:
            self.available_assets[base_asset] += order.qty
            self.assets[quote_asset] -= (abs(order.qty) *
                                         order.price) * (1 + self.fee_rate)
        # sell order
        else:
            self.available_assets[quote_asset] += abs(
                order.qty) * order.price * (1 - self.fee_rate)
            self.assets[quote_asset] += abs(
                order.qty) * order.price * (1 - self.fee_rate)

        temp_new_quote_asset = self.assets[quote_asset]
        if jh.is_debuggable('balance_update'
                            ) and temp_old_quote_asset != temp_new_quote_asset:
            logger.info('Balance for {} on {} changed from {} to {}'.format(
                quote_asset, self.name, round(temp_old_quote_asset, 2),
                round(temp_new_quote_asset, 2)))
        temp_new_quote_available_asset = self.available_assets[quote_asset]
        if jh.is_debuggable(
                'balance_update'
        ) and temp_old_quote_available_asset != temp_new_quote_available_asset:
            logger.info('Balance for {} on {} changed from {} to {}'.format(
                quote_asset, self.name, round(temp_old_quote_available_asset,
                                              2),
                round(temp_new_quote_available_asset, 2)))

        temp_new_base_asset = self.assets[base_asset]
        if jh.is_debuggable('balance_update'
                            ) and temp_old_base_asset != temp_new_base_asset:
            logger.info('Balance for {} on {} changed from {} to {}'.format(
                base_asset, self.name, round(temp_old_base_asset, 2),
                round(temp_new_base_asset, 2)))
        temp_new_base_available_asset = self.available_assets[base_asset]
        if jh.is_debuggable(
                'balance_update'
        ) and temp_old_base_available_asset != temp_new_base_available_asset:
            logger.info('Balance for {} on {} changed from {} to {}'.format(
                base_asset, self.name, round(temp_old_base_available_asset, 2),
                round(temp_new_base_available_asset, 2)))
예제 #3
0
    def on_order_submission(self,
                            order: Order,
                            skip_market_order: bool = True) -> None:
        base_asset = jh.base_asset(order.symbol)
        quote_asset = jh.quote_asset(order.symbol)

        # skip market order at the time of submission because we don't have
        # the exact order.price. Instead, we call on_order_submission() one
        # more time at time of execution without "skip_market_order=False".
        if order.type == order_types.MARKET and skip_market_order:
            return

        # used for logging balance change
        temp_old_quote_available_asset = self.available_assets[quote_asset]
        temp_old_base_available_asset = self.available_assets[base_asset]

        if order.side == sides.BUY:
            quote_balance = self.available_assets[quote_asset]
            self.available_assets[quote_asset] -= (
                abs(order.qty) * order.price) * (1 + self.fee_rate)
            if self.available_assets[quote_asset] < 0:
                raise NegativeBalance(
                    "Balance cannot go below zero in spot market. Available capital at {} for {} is {} but you're trying to sell {}"
                    .format(self.name, quote_asset, quote_balance,
                            abs(order.qty * order.price)))
        # sell order
        else:
            base_balance = self.available_assets[base_asset]
            new_base_balance = base_balance + order.qty
            if new_base_balance < 0:
                raise NegativeBalance(
                    "Balance cannot go below zero in spot market. Available capital at {} for {} is {} but you're trying to sell {}"
                    .format(self.name, base_asset, base_balance,
                            abs(order.qty)))

            self.available_assets[base_asset] -= abs(order.qty)

        temp_new_quote_available_asset = self.available_assets[quote_asset]
        if jh.is_debuggable(
                'balance_update'
        ) and temp_old_quote_available_asset != temp_new_quote_available_asset:
            logger.info(
                'Available balance for {} on {} changed from {} to {}'.format(
                    quote_asset, self.name,
                    round(temp_old_quote_available_asset, 2),
                    round(temp_new_quote_available_asset, 2)))
        temp_new_base_available_asset = self.available_assets[base_asset]
        if jh.is_debuggable(
                'balance_update'
        ) and temp_old_base_available_asset != temp_new_base_available_asset:
            logger.info(
                'Available balance for {} on {} changed from {} to {}'.format(
                    base_asset, self.name,
                    round(temp_old_base_available_asset, 2),
                    round(temp_new_base_available_asset, 2)))
예제 #4
0
파일: fitness.py 프로젝트: jesse-ai/jesse
def _formatted_inputs_for_isolated_backtest(user_config, routes):
    return {
        'starting_balance': user_config['exchange']['balance'],
        'fee': user_config['exchange']['fee'],
        'futures_leverage': user_config['exchange']['futures_leverage'],
        'futures_leverage_mode':
        user_config['exchange']['futures_leverage_mode'],
        'exchange': routes[0]['exchange'],
        'settlement_currency': jh.quote_asset(routes[0]['symbol']),
        'warm_up_candles': user_config['warmup_candles_num']
    }
예제 #5
0
    def on_order_cancellation(self, order: Order):
        base_asset = jh.base_asset(order.symbol)
        quote_asset = jh.quote_asset(order.symbol)

        # in margin, we only update available_asset's value which is used for detecting reduce_only orders
        if self.type == 'margin':
            self.available_assets[base_asset] -= order.qty
            # self.available_assets[quote_asset] += order.qty * order.price
            if order.side == sides.BUY:
                # find and set order to [0, 0] (same as removing it)
                for index, item in enumerate(self.buy_orders[base_asset]):
                    if item[0] == order.qty and item[1] == order.price:
                        self.buy_orders[base_asset][index] = np.array([0, 0])
                        break
            else:
                # find and set order to [0, 0] (same as removing it)
                for index, item in enumerate(self.sell_orders[base_asset]):
                    if item[0] == order.qty and item[1] == order.price:
                        self.sell_orders[base_asset][index] = np.array([0, 0])
                        break
            return

        # used for logging balance change
        temp_old_quote_available_asset = self.available_assets[quote_asset]
        temp_old_base_available_asset = self.available_assets[base_asset]

        if order.side == sides.BUY:
            self.available_assets[quote_asset] += (
                abs(order.qty) * order.price) * (1 + self.fee_rate)
        # sell order
        else:
            self.available_assets[base_asset] += abs(order.qty)

        temp_new_quote_available_asset = self.available_assets[quote_asset]
        if jh.is_debuggable(
                'balance_update'
        ) and temp_old_quote_available_asset != temp_new_quote_available_asset:
            logger.info(
                'Available balance for {} on {} changed from {} to {}'.format(
                    quote_asset, self.name,
                    round(temp_old_quote_available_asset, 2),
                    round(temp_new_quote_available_asset, 2)))
        temp_new_base_available_asset = self.available_assets[base_asset]
        if jh.is_debuggable(
                'balance_update'
        ) and temp_old_base_available_asset != temp_new_base_available_asset:
            logger.info(
                'Available balance for {} on {} changed from {} to {}'.format(
                    base_asset, self.name,
                    round(temp_old_base_available_asset, 2),
                    round(temp_new_base_available_asset, 2)))
예제 #6
0
    def tradable_balance(self, symbol=''):
        if self.type == 'spot':
            if symbol == '':
                raise ValueError
            quote_asset = jh.quote_asset(symbol)
            return self.available_assets[quote_asset]
        else:
            temp_credit = self.assets[self.settlement_currency]
            # we need to consider buy and sell orders of ALL pairs
            # also, consider the value of all open positions
            for asset in self.assets:
                if asset == self.settlement_currency:
                    continue

                position = selectors.get_position(
                    self.name, asset + "-" + self.settlement_currency)
                if position is None:
                    continue

                if position.is_open:
                    # add unrealized PNL
                    temp_credit += position.pnl

                # subtract worst scenario orders' used margin
                sum_buy_orders = (self.buy_orders[asset][:][:, 0] *
                                  self.buy_orders[asset][:][:, 1]).sum()
                sum_sell_orders = (self.sell_orders[asset][:][:, 0] *
                                   self.sell_orders[asset][:][:, 1]).sum()
                if position.is_open:
                    if position.type == 'long':
                        sum_buy_orders += position.value
                    else:
                        sum_sell_orders -= abs(position.value)
                temp_credit -= max(abs(sum_buy_orders), abs(sum_sell_orders))

            return temp_credit
예제 #7
0
def test_quote_asset():
    assert jh.quote_asset('BTC-USDT') == 'USDT'
    assert jh.quote_asset('DEFI-USDT') == 'USDT'
    assert jh.quote_asset('DEFI-EUR') == 'EUR'
예제 #8
0
파일: __init__.py 프로젝트: jesse-ai/jesse
def install_routes() -> None:
    considering_candles = set()

    # when importing market data, considering_candles is all we need
    if jh.is_collecting_data():
        for r in router.market_data:
            considering_candles.add((r.exchange, r.symbol))

        config['app']['considering_candles'] = tuple(considering_candles)
        return

    # validate routes for duplicates:
    # each exchange-symbol pair can be traded only once.
    for r in router.routes:
        considering_candles.add((r.exchange, r.symbol))

        exchange = r.exchange
        symbol = r.symbol
        count = sum(ro.exchange == exchange and ro.symbol == symbol
                    for ro in router.routes)

        if count != 1:
            raise InvalidRoutes(
                'each exchange-symbol pair can be traded only once. \nMore info: https://docs.jesse.trade/docs/routes.html#trading-multiple-routes'
            )

    # check to make sure if trading more than one route, they all have the same quote
    # currency because otherwise we cannot calculate the correct performance metrics
    first_routes_quote = jh.quote_asset(router.routes[0].symbol)
    for r in router.routes:
        if jh.quote_asset(r.symbol) != first_routes_quote:
            raise InvalidRoutes(
                'All trading routes must have the same quote asset.')

    trading_exchanges = set()
    trading_timeframes = set()
    trading_symbols = set()

    for r in router.routes:
        trading_exchanges.add(r.exchange)
        trading_timeframes.add(r.timeframe)
        trading_symbols.add(r.symbol)

    considering_exchanges = trading_exchanges.copy()
    considering_timeframes = trading_timeframes.copy()
    considering_symbols = trading_symbols.copy()

    for e in router.extra_candles:
        considering_candles.add((e['exchange'], e['symbol']))
        considering_exchanges.add(e['exchange'])
        considering_symbols.add(e['symbol'])
        considering_timeframes.add(e['timeframe'])

    # 1m must be present at all times
    considering_timeframes.add('1m')

    config['app']['considering_candles'] = tuple(considering_candles)
    config['app']['considering_exchanges'] = tuple(considering_exchanges)

    config['app']['considering_symbols'] = tuple(considering_symbols)
    config['app']['considering_timeframes'] = tuple(considering_timeframes)
    config['app']['trading_exchanges'] = tuple(trading_exchanges)
    config['app']['trading_symbols'] = tuple(trading_symbols)
    config['app']['trading_timeframes'] = tuple(trading_timeframes)
예제 #9
0
    def on_order_execution(self, order: Order):
        base_asset = jh.base_asset(order.symbol)
        quote_asset = jh.quote_asset(order.symbol)

        if order.type == order_types.MARKET:
            self.on_order_submission(order, skip_market_order=False)

        if self.type == 'margin':
            if order.side == sides.BUY:
                # find and set order to [0, 0] (same as removing it)
                for index, item in enumerate(self.buy_orders[base_asset]):
                    if item[0] == order.qty and item[1] == order.price:
                        self.buy_orders[base_asset][index] = np.array([0, 0])
                        break
            else:
                # find and set order to [0, 0] (same as removing it)
                for index, item in enumerate(self.sell_orders[base_asset]):
                    if item[0] == order.qty and item[1] == order.price:
                        self.sell_orders[base_asset][index] = np.array([0, 0])
                        break
            return

        # used for logging balance change
        temp_old_quote_asset = self.assets[quote_asset]
        temp_old_quote_available_asset = self.available_assets[quote_asset]
        temp_old_base_asset = self.assets[base_asset]
        temp_old_base_available_asset = self.available_assets[base_asset]

        # works for both buy and sell orders (sell order's qty < 0)
        self.assets[base_asset] += order.qty

        if order.side == sides.BUY:
            self.available_assets[base_asset] += order.qty
            self.assets[quote_asset] -= (abs(order.qty) *
                                         order.price) * (1 + self.fee_rate)
        # sell order
        else:
            self.available_assets[quote_asset] += abs(
                order.qty) * order.price * (1 - self.fee_rate)
            self.assets[quote_asset] += abs(
                order.qty) * order.price * (1 - self.fee_rate)

        temp_new_quote_asset = self.assets[quote_asset]
        if jh.is_debuggable('balance_update'
                            ) and temp_old_quote_asset != temp_new_quote_asset:
            logger.info('Balance for {} on {} changed from {} to {}'.format(
                quote_asset, self.name, round(temp_old_quote_asset, 2),
                round(temp_new_quote_asset, 2)))
        temp_new_quote_available_asset = self.available_assets[quote_asset]
        if jh.is_debuggable(
                'balance_update'
        ) and temp_old_quote_available_asset != temp_new_quote_available_asset:
            logger.info('Balance for {} on {} changed from {} to {}'.format(
                quote_asset, self.name, round(temp_old_quote_available_asset,
                                              2),
                round(temp_new_quote_available_asset, 2)))

        temp_new_base_asset = self.assets[base_asset]
        if jh.is_debuggable('balance_update'
                            ) and temp_old_base_asset != temp_new_base_asset:
            logger.info('Balance for {} on {} changed from {} to {}'.format(
                base_asset, self.name, round(temp_old_base_asset, 2),
                round(temp_new_base_asset, 2)))
        temp_new_base_available_asset = self.available_assets[base_asset]
        if jh.is_debuggable(
                'balance_update'
        ) and temp_old_base_available_asset != temp_new_base_available_asset:
            logger.info('Balance for {} on {} changed from {} to {}'.format(
                base_asset, self.name, round(temp_old_base_available_asset, 2),
                round(temp_new_base_available_asset, 2)))
예제 #10
0
def run(exchange: str, symbol: str, start_date_str: str, skip_confirmation: bool = False) -> None:
    try:
        start_timestamp = jh.arrow_to_timestamp(arrow.get(start_date_str, 'YYYY-MM-DD'))
    except:
        raise ValueError('start_date must be a string representing a date before today. ex: 2020-01-17')

    # more start_date validations
    today = arrow.utcnow().floor('day').int_timestamp * 1000
    if start_timestamp == today:
        raise ValueError("Today's date is not accepted. start_date must be a string a representing date BEFORE today.")
    elif start_timestamp > today:
        raise ValueError("Future's date is not accepted. start_date must be a string a representing date BEFORE today.")

    # We just call this to throw a exception in case of a symbol without dash
    jh.quote_asset(symbol)

    click.clear()
    symbol = symbol.upper()

    until_date = arrow.utcnow().floor('day')
    start_date = arrow.get(start_timestamp / 1000)
    days_count = jh.date_diff_in_days(start_date, until_date)
    candles_count = days_count * 1440

    try:
        driver: CandleExchange = drivers[exchange]()
    except KeyError:
        raise ValueError(f'{exchange} is not a supported exchange')
    except TypeError:
        raise FileNotFoundError('You are missing the "plugins.py" file')

    loop_length = int(candles_count / driver.count) + 1
    # ask for confirmation
    if not skip_confirmation:
        click.confirm(
            f'Importing {days_count} days candles from "{exchange}" for "{symbol}". Duplicates will be skipped. All good?', abort=True, default=True)

    with click.progressbar(length=loop_length, label='Importing candles...') as progressbar:
        for _ in range(candles_count):
            temp_start_timestamp = start_date.int_timestamp * 1000
            temp_end_timestamp = temp_start_timestamp + (driver.count - 1) * 60000

            # to make sure it won't try to import candles from the future! LOL
            if temp_start_timestamp > jh.now_to_timestamp():
                break

            # prevent duplicates calls to boost performance
            count = Candle.select().where(
                Candle.timestamp.between(temp_start_timestamp, temp_end_timestamp),
                Candle.symbol == symbol,
                Candle.exchange == exchange
            ).count()
            already_exists = count == driver.count

            if not already_exists:
                # it's today's candles if temp_end_timestamp < now
                if temp_end_timestamp > jh.now_to_timestamp():
                    temp_end_timestamp = arrow.utcnow().floor('minute').int_timestamp * 1000 - 60000

                # fetch from market
                candles = driver.fetch(symbol, temp_start_timestamp)

                # check if candles have been returned and check those returned start with the right timestamp.
                # Sometimes exchanges just return the earliest possible candles if the start date doesn't exist.
                if not len(candles) or arrow.get(candles[0]['timestamp'] / 1000) > start_date:
                    click.clear()
                    first_existing_timestamp = driver.get_starting_time(symbol)

                    # if driver can't provide accurate get_starting_time()
                    if first_existing_timestamp is None:
                        raise CandleNotFoundInExchange(
                            f'No candles exists in the market for this day: {jh.timestamp_to_time(temp_start_timestamp)[:10]} \n'
                            'Try another start_date'
                        )

                    # handle when there's missing candles during the period
                    if temp_start_timestamp > first_existing_timestamp:
                        # see if there are candles for the same date for the backup exchange,
                        # if so, get those, if not, download from that exchange.
                        if driver.backup_exchange is not None:
                            candles = _get_candles_from_backup_exchange(
                                exchange, driver.backup_exchange, symbol, temp_start_timestamp, temp_end_timestamp
                            )

                    else:
                        if not skip_confirmation:
                            print(jh.color(f'No candle exists in the market for {jh.timestamp_to_time(temp_start_timestamp)[:10]}\n', 'yellow'))
                            click.confirm(
                                f'First present candle is since {jh.timestamp_to_time(first_existing_timestamp)[:10]}. Would you like to continue?', abort=True, default=True)

                        run(exchange, symbol, jh.timestamp_to_time(first_existing_timestamp)[:10], True)
                        return

                # fill absent candles (if there's any)
                candles = _fill_absent_candles(candles, temp_start_timestamp, temp_end_timestamp)

                # store in the database
                if skip_confirmation:
                    store_candles(candles)
                else:
                    threading.Thread(target=store_candles, args=[candles]).start()

            # add as much as driver's count to the temp_start_time
            start_date = start_date.shift(minutes=driver.count)

            progressbar.update(1)

            # sleep so that the exchange won't get angry at us
            if not already_exists:
                time.sleep(driver.sleep_time)
예제 #11
0
def test_quote_asset():
    assert jh.quote_asset('BTCUSDT') == 'USDT'
    assert jh.quote_asset('DEFIUSDT') == 'USDT'
예제 #12
0
def livetrade():
    # sum up balance of all trading exchanges
    starting_balance = 0
    current_balance = 0
    for e in store.exchanges.storage:
        starting_balance += store.exchanges.storage[e].starting_assets[
            jh.app_currency()]
        current_balance += store.exchanges.storage[e].assets[jh.app_currency()]
    starting_balance = round(starting_balance, 2)
    current_balance = round(current_balance, 2)

    arr = [[
        'started at',
        jh.timestamp_to_arrow(store.app.starting_time).humanize()
    ], ['current time',
        jh.timestamp_to_time(jh.now_to_timestamp())[:19]],
           [
               'errors/info', '{}/{}'.format(len(store.logs.errors),
                                             len(store.logs.info))
           ], ['active orders',
               store.orders.count_all_active_orders()],
           ['open positions',
            store.positions.count_open_positions()]]

    # TODO: for now, we assume that we trade on one exchange only. Later, we need to support for more than one exchange at a time
    first_exchange = selectors.get_exchange(router.routes[0].exchange)

    if first_exchange.type == 'futures':
        arr.append([
            'started/current balance', '{}/{}'.format(starting_balance,
                                                      current_balance)
        ])
    else:
        # loop all trading exchanges
        for exchange in selectors.get_all_exchanges():
            # loop all assets
            for asset_name, asset_balance in exchange.assets.items():
                if asset_name == jh.base_asset(router.routes[0].symbol):
                    current_price = selectors.get_current_price(
                        router.routes[0].exchange, router.routes[0].symbol)
                    arr.append([
                        '{}'.format(asset_name), '{}/{} ({} {})'.format(
                            round(exchange.available_assets[asset_name], 5),
                            round(asset_balance, 5),
                            jh.format_currency(
                                round(asset_balance * current_price, 2)),
                            jh.quote_asset(router.routes[0].symbol))
                    ])
                else:
                    arr.append([
                        '{}'.format(asset_name), '{}/{}'.format(
                            round(exchange.available_assets[asset_name], 5),
                            round(asset_balance, 5),
                        )
                    ])

    # short trades summary
    if len(store.completed_trades.trades):
        df = pd.DataFrame.from_records(
            [t.to_dict() for t in store.completed_trades.trades])
        total = len(df)
        winning_trades = df.loc[df['PNL'] > 0]
        losing_trades = df.loc[df['PNL'] < 0]
        pnl = round(df['PNL'].sum(), 2)
        pnl_percentage = round((pnl / starting_balance) * 100, 2)

        arr.append([
            'total/winning/losing trades',
            '{}/{}/{}'.format(total, len(winning_trades), len(losing_trades))
        ])
        arr.append(['PNL (%)', '${} ({}%)'.format(pnl, pnl_percentage)])

    if config['app']['debug_mode']:
        arr.append(['debug mode', config['app']['debug_mode']])

    if config['app']['is_test_driving']:
        arr.append(['Test Drive', config['app']['is_test_driving']])

    return arr
예제 #13
0
 def wallet_balance(self, symbol=''):
     if symbol == '':
         raise ValueError
     quote_asset = jh.quote_asset(symbol)
     return self.assets[quote_asset]
예제 #14
0
def run(exchange: str,
        symbol: str,
        start_date_str: str,
        skip_confirmation: bool = False,
        mode: str = 'candles') -> None:
    config['app']['trading_mode'] = mode

    # first, create and set session_id
    store.app.set_session_id()

    register_custom_exception_handler()

    # close database connection
    from jesse.services.db import database
    database.open_connection()

    # at every second, we check to see if it's time to execute stuff
    status_checker = Timeloop()

    @status_checker.job(interval=timedelta(seconds=1))
    def handle_time():
        if process_status() != 'started':
            raise exceptions.Termination

    status_checker.start()

    try:
        start_timestamp = jh.arrow_to_timestamp(
            arrow.get(start_date_str, 'YYYY-MM-DD'))
    except:
        raise ValueError(
            'start_date must be a string representing a date before today. ex: 2020-01-17'
        )

    # more start_date validations
    today = arrow.utcnow().floor('day').int_timestamp * 1000
    if start_timestamp == today:
        raise ValueError(
            "Today's date is not accepted. start_date must be a string a representing date BEFORE today."
        )
    elif start_timestamp > today:
        raise ValueError(
            "Future's date is not accepted. start_date must be a string a representing date BEFORE today."
        )

    # We just call this to throw a exception in case of a symbol without dash
    jh.quote_asset(symbol)

    click.clear()
    symbol = symbol.upper()

    until_date = arrow.utcnow().floor('day')
    start_date = arrow.get(start_timestamp / 1000)
    days_count = jh.date_diff_in_days(start_date, until_date)
    candles_count = days_count * 1440

    try:
        driver: CandleExchange = drivers[exchange]()
    except KeyError:
        raise ValueError(f'{exchange} is not a supported exchange')
    except TypeError:
        raise FileNotFoundError('You are missing the "plugins.py" file')

    loop_length = int(candles_count / driver.count) + 1
    # ask for confirmation
    if not skip_confirmation:
        click.confirm(
            f'Importing {days_count} days candles from "{exchange}" for "{symbol}". Duplicates will be skipped. All good?',
            abort=True,
            default=True)

    progressbar = Progressbar(loop_length)
    for i in range(candles_count):
        temp_start_timestamp = start_date.int_timestamp * 1000
        temp_end_timestamp = temp_start_timestamp + (driver.count - 1) * 60000

        # to make sure it won't try to import candles from the future! LOL
        if temp_start_timestamp > jh.now_to_timestamp():
            break

        # prevent duplicates calls to boost performance
        count = Candle.select().where(
            Candle.timestamp.between(temp_start_timestamp, temp_end_timestamp),
            Candle.symbol == symbol, Candle.exchange == exchange).count()
        already_exists = count == driver.count

        if not already_exists:
            # it's today's candles if temp_end_timestamp < now
            if temp_end_timestamp > jh.now_to_timestamp():
                temp_end_timestamp = arrow.utcnow().floor(
                    'minute').int_timestamp * 1000 - 60000

            # fetch from market
            candles = driver.fetch(symbol, temp_start_timestamp)

            # check if candles have been returned and check those returned start with the right timestamp.
            # Sometimes exchanges just return the earliest possible candles if the start date doesn't exist.
            if not len(candles) or arrow.get(
                    candles[0]['timestamp'] / 1000) > start_date:
                click.clear()
                first_existing_timestamp = driver.get_starting_time(symbol)

                # if driver can't provide accurate get_starting_time()
                if first_existing_timestamp is None:
                    raise CandleNotFoundInExchange(
                        f'No candles exists in the market for this day: {jh.timestamp_to_time(temp_start_timestamp)[:10]} \n'
                        'Try another start_date')

                # handle when there's missing candles during the period
                if temp_start_timestamp > first_existing_timestamp:
                    # see if there are candles for the same date for the backup exchange,
                    # if so, get those, if not, download from that exchange.
                    if driver.backup_exchange is not None:
                        candles = _get_candles_from_backup_exchange(
                            exchange, driver.backup_exchange, symbol,
                            temp_start_timestamp, temp_end_timestamp)

                else:
                    temp_start_time = jh.timestamp_to_time(
                        temp_start_timestamp)[:10]
                    temp_existing_time = jh.timestamp_to_time(
                        first_existing_timestamp)[:10]
                    sync_publish(
                        'alert', {
                            'message':
                            f'No candle exists in the market for {temp_start_time}. So '
                            f'Jesse started importing since the first existing date which is {temp_existing_time}',
                            'type':
                            'success'
                        })
                    run(exchange, symbol,
                        jh.timestamp_to_time(first_existing_timestamp)[:10],
                        True)
                    return

            # fill absent candles (if there's any)
            candles = _fill_absent_candles(candles, temp_start_timestamp,
                                           temp_end_timestamp)

            # store in the database
            if skip_confirmation:
                store_candles(candles)
            else:
                threading.Thread(target=store_candles, args=[candles]).start()

        # add as much as driver's count to the temp_start_time
        start_date = start_date.shift(minutes=driver.count)

        progressbar.update()
        sync_publish(
            'progressbar', {
                'current':
                progressbar.current,
                'estimated_remaining_seconds':
                progressbar.estimated_remaining_seconds
            })

        # sleep so that the exchange won't get angry at us
        if not already_exists:
            time.sleep(driver.sleep_time)

    # stop the status_checker time loop
    status_checker.stop()

    sync_publish(
        'alert', {
            'message':
            f'Successfully imported candles since {jh.timestamp_to_date(start_timestamp)} until today ({days_count} days). ',
            'type': 'success'
        })

    # if it is to skip, then it's being called from another process hence we should leave the database be
    if not skip_confirmation:
        # close database connection
        from jesse.services.db import database
        database.close_connection()