Beispiel #1
0
    def set_routes(self, routes):
        self.routes = []

        for r in routes:
            # validate strategy
            import jesse.helpers as jh
            from jesse import exceptions

            strategy_name = r[3]
            if jh.is_unit_testing():
                exists = jh.file_exists(
                    'jesse/strategies/{}/__init__.py'.format(strategy_name))
            else:
                exists = jh.file_exists(
                    'strategies/{}/__init__.py'.format(strategy_name))

            if not exists:
                raise exceptions.InvalidRoutes(
                    'A strategy with the name of "{}" could not be found.'.
                    format(r[3]))

            # validate timeframe
            timeframe = r[2]
            if timeframe not in [
                    '1m', '3m', '5m', '15m', '30m', '1h', '2h', '3h', '4h',
                    '6h', '8h', '1D'
            ]:
                raise exceptions.InvalidRoutes(
                    'Timeframe "{}" is invalid. Supported timeframes are 1m, 3m, 5m, 15m, 30m, 1h, 2h, 3h, 4h, 6h, 8h, 1D'
                    .format(timeframe))

            self.routes.append(Route(*r))
Beispiel #2
0
    def set_routes(self, routes: List[Any]) -> None:
        self._reset()

        self.routes = []

        for r in routes:
            # validate strategy
            strategy_name = r[3]
            if jh.is_unit_testing():
                exists = jh.file_exists(sys.path[0] + '/jesse/strategies/{}/__init__.py'.format(strategy_name))
            else:
                exists = jh.file_exists('strategies/{}/__init__.py'.format(strategy_name))

            if not exists:
                raise exceptions.InvalidRoutes(
                    'A strategy with the name of "{}" could not be found.'.format(r[3]))

            # validate timeframe
            route_timeframe = r[2]
            all_timeframes = [timeframe for timeframe in jh.class_iter(timeframes)]
            if route_timeframe not in all_timeframes:
                raise exceptions.InvalidRoutes(
                    'Timeframe "{}" is invalid. Supported timeframes are {}'.format(
                        route_timeframe, ', '.join(all_timeframes))
                )

            self.routes.append(Route(*r))
Beispiel #3
0
    def set_routes(self, routes: List[Any]) -> None:
        self._reset()

        self.routes = []

        for r in routes:
            # validate strategy that the strategy file exists (if sent as a string)
            if isinstance(r["strategy"], str):
                strategy_name = r["strategy"]
                if jh.is_unit_testing():
                    path = sys.path[0]
                    # live plugin
                    if path.endswith('jesse-live'):
                        strategies_dir = f'{sys.path[0]}/tests/strategies'
                    # main framework
                    else:
                        strategies_dir = f'{sys.path[0]}/jesse/strategies'
                    exists = jh.file_exists(
                        f"{strategies_dir}/{strategy_name}/__init__.py")
                else:
                    exists = jh.file_exists(
                        f'strategies/{strategy_name}/__init__.py')
            else:
                exists = True

            if not exists and isinstance(r["strategy"], str):
                raise exceptions.InvalidRoutes(
                    f'A strategy with the name of "{r["strategy"]}" could not be found.'
                )

            self.routes.append(
                Route(r["exchange"], r["symbol"], r["timeframe"],
                      r["strategy"], None))
Beispiel #4
0
    def _close(self, close_price: float) -> None:
        if self.is_open is False:
            raise EmptyPosition('The position is already closed.')

        # just to prevent confusion
        close_qty = abs(self.qty)

        estimated_profit = jh.estimate_PNL(close_qty, self.entry_price,
                                           close_price, self.type)
        entry = self.entry_price
        trade_type = self.type
        self.exit_price = close_price

        if self.exchange:
            self.exchange.add_realized_pnl(estimated_profit)
            self.exchange.temp_reduced_amount[jh.base_asset(
                self.symbol)] += abs(close_qty * close_price)
        self.qty = 0
        self.entry_price = None
        self.closed_at = jh.now_to_timestamp()

        if not jh.is_unit_testing():
            info_text = 'CLOSED {} position: {}, {}, {}. PNL: ${}, Balance: ${}, entry: {}, exit: {}'.format(
                trade_type, self.exchange_name, self.symbol,
                self.strategy.name, round(estimated_profit, 2),
                jh.format_currency(
                    round(self.exchange.wallet_balance(self.symbol),
                          2)), entry, close_price)

            if jh.is_debuggable('position_closed'):
                logger.info(info_text)

            if jh.is_live(
            ) and config['env']['notifications']['events']['updated_position']:
                notifier.notify(info_text)
Beispiel #5
0
    def set_routes(self, routes: List[Any]) -> None:
        self._reset()

        self.routes = []

        for r in routes:
            # validate strategy
            strategy_name = r[3]
            if jh.is_unit_testing():
                exists = jh.file_exists(sys.path[0] + '/jesse/strategies/{}/__init__.py'.format(strategy_name))
            else:
                exists = jh.file_exists('strategies/{}/__init__.py'.format(strategy_name))

            if not exists:
                raise exceptions.InvalidRoutes(
                    'A strategy with the name of "{}" could not be found.'.format(r[3]))

            # validate timeframe
            timeframe = r[2]
            if timeframe not in ['1m', '3m', '5m', '15m', '30m', '45m', '1h', '2h', '3h', '4h', '6h', '8h', '12h', '1D', '3D', '1W'']:
                raise exceptions.InvalidRoutes(
                    'Timeframe "{}" is invalid. Supported timeframes are 1m, 3m, 5m, 15m, 30m, 45m, 1h, 2h, 3h, 4h, 6h, 8h, 12h, 1D, 3D, 1W'.format(
                        timeframe)
                )

            self.routes.append(Route(*r))
Beispiel #6
0
    def open_connection(self) -> None:
        if not jh.is_jesse_project() or jh.is_unit_testing():
            return

        # if it's not None, then we already have a connection
        if self.db is not None:
            return

        options = {
            "keepalives": 1,
            "keepalives_idle": 60,
            "keepalives_interval": 10,
            "keepalives_count": 5
        }

        self.db = PostgresqlExtDatabase(
            ENV_VALUES['POSTGRES_NAME'],
            user=ENV_VALUES['POSTGRES_USERNAME'],
            password=ENV_VALUES['POSTGRES_PASSWORD'],
            host=ENV_VALUES['POSTGRES_HOST'],
            port=int(ENV_VALUES['POSTGRES_PORT']),
            sslmode=ENV_VALUES.get('POSTGRES_SSLMODE', 'disable'),
            **options)

        # connect to the database
        self.db.connect()
Beispiel #7
0
 def initiate(self, routes: list, extra_routes: list = None):
     if extra_routes is None:
         extra_routes = []
     self.set_routes(routes)
     self.set_extra_candles(extra_routes)
     from jesse.store import store
     store.reset(force_install_routes=jh.is_unit_testing())
Beispiel #8
0
    def set_routes(self, routes: List[Any]) -> None:
        self._reset()

        self.routes = []

        for r in routes:
            # validate strategy
            strategy_name = r[3]
            if jh.is_unit_testing():
                exists = jh.file_exists(
                    f"{sys.path[0]}/jesse/strategies/{strategy_name}/__init__.py"
                )
            else:
                exists = jh.file_exists(
                    f'strategies/{strategy_name}/__init__.py')

            if not exists:
                raise exceptions.InvalidRoutes(
                    f'A strategy with the name of "{r[3]}" could not be found.'
                )

            # validate timeframe
            route_timeframe = r[2]
            all_timeframes = [
                timeframe for timeframe in jh.class_iter(timeframes)
            ]
            if route_timeframe not in all_timeframes:
                raise exceptions.InvalidRoutes(
                    f'Timeframe "{route_timeframe}" is invalid. Supported timeframes are {", ".join(all_timeframes)}'
                )

            self.routes.append(Route(*r))
Beispiel #9
0
    def cancel_all_orders(self, symbol):
        orders = filter(lambda o: o.is_new,
                        store.orders.get_orders(self.name, symbol))

        for o in orders:
            o.cancel()

        if not jh.is_unit_testing():
            store.orders.storage[f'{self.name}-{symbol}'].clear()
Beispiel #10
0
    def _check(self) -> None:
        """Based on the newly updated info, check if we should take action or not"""
        if not self._is_initiated:
            self._is_initiated = True

        if jh.is_live() and jh.is_debugging():
            logger.info(
                f'Executing  {self.name}-{self.exchange}-{self.symbol}-{self.timeframe}'
            )

        # for caution to make sure testing on livetrade won't bleed your account
        if jh.is_test_driving() and store.completed_trades.count >= 2:
            logger.info('Maximum allowed trades in test-drive mode is reached')
            return

        if self._open_position_orders != [] and self.is_close and self.should_cancel(
        ):
            self._execute_cancel()

            # make sure order cancellation response is received via WS
            if jh.is_live():
                # sleep a little until cancel is received via WS
                sleep(0.1)
                # just in case, sleep some more if necessary
                for _ in range(20):
                    if store.orders.count_active_orders(
                            self.exchange, self.symbol) == 0:
                        break

                    logger.info('sleeping 0.2 more seconds...')
                    sleep(0.2)

                # If it's still not cancelled, something is wrong. Handle cancellation failure
                if store.orders.count_active_orders(self.exchange,
                                                    self.symbol) != 0:
                    raise exceptions.ExchangeNotResponding(
                        'The exchange did not respond as expected')

        if self.position.is_open:
            self._update_position()

        if jh.is_backtesting() or jh.is_unit_testing():
            store.orders.execute_pending_market_orders()

        if self.position.is_close and self._open_position_orders == []:
            should_short = self.should_short()
            should_long = self.should_long()
            # validation
            if should_short and should_long:
                raise exceptions.ConflictingRules(
                    'should_short and should_long should not be true at the same time.'
                )
            if should_long:
                self._execute_long()
            elif should_short:
                self._execute_short()
Beispiel #11
0
def sync_publish(event: str, msg):
    if jh.is_unit_testing():
        raise EnvironmentError('sync_publish() should be NOT called during testing. There must be something wrong')

    sync_redis.publish(
        f"{ENV_VALUES['APP_PORT']}:channel:1", json.dumps({
            'id': os.getpid(),
            'event': f'{jh.app_mode()}.{event}',
            'data': msg
        }, ignore_nan=True, cls=NpEncoder)
    )
Beispiel #12
0
    def cancel_all_orders(self, symbol):
        """

        :param symbol:
        """
        orders = filter(lambda o: o.is_new,
                        store.orders.get_orders(self.name, symbol))

        for o in orders:
            o.cancel()

        if not jh.is_unit_testing():
            store.orders.storage['{}-{}'.format(self.name, symbol)].clear()
Beispiel #13
0
def process_status(pid=None) -> str:
    if jh.is_unit_testing():
        raise EnvironmentError('process_status() is not meant to be called in unit tests')

    if pid is None:
        pid = jh.get_pid()

    key = f"{ENV_VALUES['APP_PORT']}|process-status:{pid}"

    res: str = jh.str_or_none(sync_redis.get(key))
    if res is None:
        raise ValueError(f'No value exists in Redis for process ID of: {pid}')

    return jh.string_after_character(res, ':')
Beispiel #14
0
    def reset(self, force_install_routes=False):
        """resets all the states within the store
        
        Keyword Arguments:
            force_install_routes {bool} -- used for unit_testing (default: {False})
        """
        if not jh.is_unit_testing() or force_install_routes:
            install_routes()

        self.app = AppState()
        self.orders = OrdersState()
        self.completed_trades = CompletedTrades()
        self.logs = LogsState()
        self.exchanges = ExchangesState()
        self.candles = CandlesState()
        self.positions = PositionsState()
        self.tickers = TickersState()
        self.trades = TradesState()
        self.orderbooks = OrderbookState()
Beispiel #15
0
    def _execute_cancel(self):
        """
        cancels everything so that the strategy can keep looking for new trades.
        """
        # validation
        if self.position.is_open:
            raise Exception('cannot cancel orders when position is still open. there must be a bug somewhere.')

        logger.info('cancel all remaining orders to prepare for a fresh start...')

        self.broker.cancel_all_orders()

        self._reset()

        self._broadcast('route-canceled')

        self.on_cancel()

        if not jh.is_unit_testing() and not jh.is_live():
            store.orders.storage['{}-{}'.format(self.exchange, self.symbol)].clear()
Beispiel #16
0
    def pnl_percentage(self) -> float:
        """
        Alias for self.roi
        """
        return self.roi

    @property
    def roi(self) -> float:
        """
        Return on Investment in percentage
        More at: https://www.binance.com/en/support/faq/5b9ad93cb4854f5990b9fb97c03cfbeb
        """
        return self.pnl / self.total_cost * 100

    @property
    def total_cost(self) -> float:
        """
        How much we paid to open this position (currently does not include fees, should we?!)
        """
        return self.entry_price * abs(self.qty) / self.leverage

    @property
    def holding_period(self) -> int:
        """How many SECONDS has it taken for the trade to be done."""
        return (self.closed_at - self.opened_at) / 1000


if not jh.is_unit_testing():
    # create the table
    CompletedTrade.create_table()
Beispiel #17
0
def run(
        debug_mode,
        user_config: dict,
        routes: List[Dict[str, str]],
        extra_routes: List[Dict[str, str]],
        start_date: str,
        finish_date: str,
        candles: dict = None,
        chart: bool = False,
        tradingview: bool = False,
        full_reports: bool = False,
        csv: bool = False,
        json: bool = False
) -> None:
    if not jh.is_unit_testing():
        # 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()

    from jesse.config import config, set_config
    config['app']['trading_mode'] = 'backtest'

    # debug flag
    config['app']['debug_mode'] = debug_mode

    # inject config
    if not jh.is_unit_testing():
        set_config(user_config)

    # set routes
    router.initiate(routes, extra_routes)

    store.app.set_session_id()

    register_custom_exception_handler()

    # clear the screen
    if not jh.should_execute_silently():
        click.clear()

    # validate routes
    validate_routes(router)

    # initiate candle store
    store.candles.init_storage(5000)

    # load historical candles
    if candles is None:
        candles = load_candles(start_date, finish_date)
        click.clear()

    if not jh.should_execute_silently():
        sync_publish('general_info', {
            'session_id': jh.get_session_id(),
            'debug_mode': str(config['app']['debug_mode']),
        })

        # candles info
        key = f"{config['app']['considering_candles'][0][0]}-{config['app']['considering_candles'][0][1]}"
        sync_publish('candles_info', stats.candles_info(candles[key]['candles']))

        # routes info
        sync_publish('routes_info', stats.routes(router.routes))

    # run backtest simulation
    simulator(candles, run_silently=jh.should_execute_silently())

    # hyperparameters (if any)
    if not jh.should_execute_silently():
        sync_publish('hyperparameters', stats.hyperparameters(router.routes))

    if not jh.should_execute_silently():
        if store.completed_trades.count > 0:
            sync_publish('metrics', report.portfolio_metrics())

            routes_count = len(router.routes)
            more = f"-and-{routes_count - 1}-more" if routes_count > 1 else ""
            study_name = f"{router.routes[0].strategy_name}-{router.routes[0].exchange}-{router.routes[0].symbol}-{router.routes[0].timeframe}{more}-{start_date}-{finish_date}"
            store_logs(study_name, json, tradingview, csv)

            if chart:
                charts.portfolio_vs_asset_returns(study_name)

            sync_publish('equity_curve', charts.equity_curve())

            # QuantStats' report
            if full_reports:
                price_data = []
                # load close candles for Buy and hold and calculate pct_change
                for index, c in enumerate(config['app']['considering_candles']):
                    exchange, symbol = c[0], c[1]
                    if exchange in config['app']['trading_exchanges'] and symbol in config['app']['trading_symbols']:
                        # fetch from database
                        candles_tuple = Candle.select(
                            Candle.timestamp, Candle.close
                        ).where(
                            Candle.timestamp.between(jh.date_to_timestamp(start_date),
                                                     jh.date_to_timestamp(finish_date) - 60000),
                            Candle.exchange == exchange,
                            Candle.symbol == symbol
                        ).order_by(Candle.timestamp.asc()).tuples()

                        candles = np.array(candles_tuple)

                        timestamps = candles[:, 0]
                        price_data.append(candles[:, 1])

                price_data = np.transpose(price_data)
                price_df = pd.DataFrame(price_data, index=pd.to_datetime(timestamps, unit="ms"), dtype=float).resample(
                    'D').mean()
                price_pct_change = price_df.pct_change(1).fillna(0)
                bh_daily_returns_all_routes = price_pct_change.mean(1)
                quantstats.quantstats_tearsheet(bh_daily_returns_all_routes, study_name)
        else:
            sync_publish('equity_curve', None)
            sync_publish('metrics', None)

    # close database connection
    from jesse.services.db import database
    database.close_connection()
Beispiel #18
0
def test_is_unit_testing():
    assert jh.is_unit_testing() is True
Beispiel #19
0
def portfolio_vs_asset_returns(study_name: str = None) -> str:
    if jh.is_unit_testing():
        return 'charts'

    register_matplotlib_converters()
    trades = store.completed_trades.trades
    # create a plot figure
    plt.figure(figsize=(26, 16))

    # daily balance
    plt.subplot(2, 1, 1)
    start_date = datetime.fromtimestamp(store.app.starting_time / 1000)
    date_list = [
        start_date + timedelta(days=x)
        for x in range(len(store.app.daily_balance))
    ]
    plt.xlabel('date')
    plt.ylabel('balance')
    if study_name:
        plt.title(f'Portfolio Daily Return - {study_name}')
    else:
        plt.title('Portfolio Daily Return')
    plt.plot(date_list, store.app.daily_balance)

    # price change%
    plt.subplot(2, 1, 2)
    price_dict = {}
    for r in router.routes:
        key = jh.key(r.exchange, r.symbol)
        price_dict[key] = {'indexes': {}, 'prices': []}
        dates = []
        prices = []
        candles = store.candles.get_candles(r.exchange, r.symbol, '1m')
        max_timeframe = jh.max_timeframe(
            config['app']['considering_timeframes'])
        pre_candles_count = jh.timeframe_to_one_minutes(
            max_timeframe) * jh.get_config('env.data.warmup_candles_num', 210)
        for i, c in enumerate(candles):
            # do not plot prices for required_initial_candles period
            if i < pre_candles_count:
                continue

            dates.append(datetime.fromtimestamp(c[0] / 1000))
            prices.append(c[2])
            # save index of the price instead of the actual price
            price_dict[key]['indexes'][str(int(c[0]))] = len(prices) - 1

        # price => %returns
        price_returns = pd.Series(prices).pct_change(1) * 100
        cumsum_returns = np.cumsum(price_returns)
        if len(router.routes) == 1:
            plt.plot(dates, cumsum_returns, label=r.symbol, c='grey')
        else:
            plt.plot(dates, cumsum_returns, label=r.symbol)
        price_dict[key]['prices'] = cumsum_returns

    # buy and sell plots
    buy_x = []
    buy_y = []
    sell_x = []
    sell_y = []
    for index, t in enumerate(trades):
        key = jh.key(t.exchange, t.symbol)

        # dirty fix for an issue with last trade being an open trade at the end of backtest
        if index == len(trades) - 1 and store.app.total_open_trades > 0:
            continue

        if t.type == 'long':
            # Buy
            if str(int(t.opened_at)) in price_dict[key]['indexes']:
                # add price change%
                buy_y.append(
                    price_dict[key]['prices'][price_dict[key]['indexes'][str(
                        int(t.opened_at))]])
                # add datetime
                buy_x.append(datetime.fromtimestamp(t.opened_at / 1000))

            # Sell: only generate data point if this trade wasn't after the last candle (open position at end)
            if str(int(t.closed_at)) in price_dict[key]['indexes']:
                # add price change%
                sell_y.append(
                    price_dict[key]['prices'][price_dict[key]['indexes'][str(
                        int(t.closed_at))]])
                # add datetime
                sell_x.append(datetime.fromtimestamp(t.closed_at / 1000))

        elif t.type == 'short':
            # Buy: only generate data point if this trade wasn't after the last candle (open position at end)
            if str(int(t.closed_at)) in price_dict[key]['indexes']:
                # add price change%
                buy_y.append(
                    price_dict[key]['prices'][price_dict[key]['indexes'][str(
                        int(t.closed_at))]])
                # add datetime
                buy_x.append(datetime.fromtimestamp(t.closed_at / 1000))

            # Sell
            if str(int(t.opened_at)) in price_dict[key]['indexes']:
                # add price change%
                sell_y.append(
                    price_dict[key]['prices'][price_dict[key]['indexes'][str(
                        int(t.opened_at))]])
                # add datetime
                sell_x.append(datetime.fromtimestamp(t.opened_at / 1000))

    plt.plot(buy_x, np.array(buy_y) * 0.99, '^', color='blue', markersize=7)
    plt.plot(sell_x, np.array(sell_y) * 1.01, 'v', color='red', markersize=7)

    plt.xlabel('date')
    plt.ylabel('price change %')
    plt.title('Asset Daily Return')
    plt.legend(loc='upper left')

    # store final result
    # make sure directories exist
    os.makedirs('./storage/charts', exist_ok=True)
    file_path = f'storage/charts/{jh.get_session_id()}.png'.replace(":", "-")
    plt.savefig(file_path)

    return file_path
Beispiel #20
0
import os
import sys

# fix directory issue
sys.path.insert(0, os.getcwd())

ENV_VALUES = {}

if jh.is_jesse_project():
    # load env
    load_dotenv()

    # create and expose ENV_VALUES
    ENV_VALUES = dotenv_values('.env')

    if jh.is_unit_testing():
        ENV_VALUES['POSTGRES_HOST'] = '127.0.0.1'
        ENV_VALUES['POSTGRES_NAME'] = 'jesse_db'
        ENV_VALUES['POSTGRES_PORT'] = '5432'
        ENV_VALUES['POSTGRES_USERNAME'] = '******'
        ENV_VALUES['POSTGRES_PASSWORD'] = '******'
        ENV_VALUES['REDIS_HOST'] = 'localhost'
        ENV_VALUES['REDIS_PORT'] = '6379'
        ENV_VALUES['REDIS_DB'] = 0
        ENV_VALUES['REDIS_PASSWORD'] = ''

    # validation for existence of .env file
    if len(list(ENV_VALUES.keys())) == 0:
        jh.error(
            '.env file is missing from within your local project. '
            'This usually happens when you\'re in the wrong directory. '
Beispiel #21
0
    def _log_position_update(self, order: Order, role: str):
        """
        A log can be either about opening, adding, reducing, or closing the position.

        Arguments:
            order {order} -- the order object
        """
        if role == order_roles.OPEN_POSITION:
            self.trade = CompletedTrade()
            self.trade.orders = [order]
            self.trade.timeframe = self.timeframe
            self.trade.id = order.id
            self.trade.strategy_name = self.name
            self.trade.exchange = order.exchange
            self.trade.symbol = order.symbol
            self.trade.type = trade_types.LONG if order.side == sides.BUY else trade_types.SHORT
            self.trade.qty = order.qty
            self.trade.opened_at = jh.now()
            self.trade.entry_candle_timestamp = self.current_candle[0]
        elif role == order_roles.INCREASE_POSITION:
            self.trade.orders.append(order)
            self.trade.qty += order.qty
        elif role == order_roles.REDUCE_POSITION:
            self.trade.orders.append(order)
            self.trade.qty += order.qty
        elif role == order_roles.CLOSE_POSITION:
            self.trade.exit_candle_timestamp = self.current_candle[0]
            self.trade.orders.append(order)

            # calculate average stop-loss price
            sum_price = 0
            sum_qty = 0
            if self._log_stop_loss is not None:
                for l in self._log_stop_loss:
                    sum_qty += abs(l[0])
                    sum_price += abs(l[0]) * l[1]
                self.trade.stop_loss_at = sum_price / sum_qty
            else:
                self.trade.stop_loss_at = np.nan

            # calculate average take-profit price
            sum_price = 0
            sum_qty = 0
            if self._log_take_profit is not None:
                for l in self._log_take_profit:
                    sum_qty += abs(l[0])
                    sum_price += abs(l[0]) * l[1]
                self.trade.take_profit_at = sum_price / sum_qty
            else:
                self.trade.take_profit_at = np.nan

            # calculate average entry_price price
            sum_price = 0
            sum_qty = 0
            for l in self.trade.orders:
                if not l.is_executed:
                    continue

                if jh.side_to_type(l.side) != self.trade.type:
                    continue

                sum_qty += abs(l.qty)
                sum_price += abs(l.qty) * l.price
            self.trade.entry_price = sum_price / sum_qty

            # calculate average exit_price
            sum_price = 0
            sum_qty = 0
            for l in self.trade.orders:
                if not l.is_executed:
                    continue

                if jh.side_to_type(l.side) == self.trade.type:
                    continue

                sum_qty += abs(l.qty)
                sum_price += abs(l.qty) * l.price
            self.trade.exit_price = sum_price / sum_qty

            self.trade.closed_at = jh.now()
            self.trade.qty = pydash.sum_by(
                filter(lambda o: o.side == jh.type_to_side(self.trade.type),
                       self.trade.orders), lambda o: abs(o.qty))

            if not jh.is_unit_testing():
                store.orders.storage['{}-{}'.format(self.exchange,
                                                    self.symbol)].clear()

            store.completed_trades.add_trade(self.trade)
            self.trade = None
            self.trades_count += 1