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)))
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)))
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)))
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'] }
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)))
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
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'
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)
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)))
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)
def test_quote_asset(): assert jh.quote_asset('BTCUSDT') == 'USDT' assert jh.quote_asset('DEFIUSDT') == 'USDT'
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
def wallet_balance(self, symbol=''): if symbol == '': raise ValueError quote_asset = jh.quote_asset(symbol) return self.assets[quote_asset]
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()