Exemplo n.º 1
0
def backtest(trades_list: [dict], settings: dict):
    # trades format is [{price: float, buyer_maker: bool}]

    # no static mode
    grid_spacing = settings['grid_spacing']
    grid_coefficient = settings['grid_coefficient']
    price_step = settings['price_step']
    qty_step = settings['qty_step']
    inverse = settings['inverse']
    liq_diff_threshold = settings['liq_diff_threshold']
    stop_loss_pos_reduction = settings['stop_loss_pos_reduction']
    min_qty = settings['min_qty']
    ddown_factor = settings['ddown_factor']
    leverage = settings['leverage']
    starting_balance = settings['balance']
    maker_fee = settings['maker_fee']
    taker_fee = settings['taker_fee']
    min_markup = settings['min_markup']
    max_markup = settings['max_markup']
    n_close_orders = settings['n_close_orders']
    break_on_loss = settings['break_on_loss']

    min_notional = settings[
        'min_notional'] if 'min_notional' in settings else 0.0

    if inverse:
        calc_cost = lambda qty_, price_: qty_ / price_
        calc_liq_price = lambda balance_, pos_size_, pos_price_: \
            bybit_calc_cross_shrt_liq_price(balance_, pos_size_, pos_price_, leverage=leverage) \
                if pos_size_ < 0.0 else \
                bybit_calc_cross_long_liq_price(balance_, pos_size_, pos_price_, leverage=leverage)
        if settings['default_qty'] <= 0.0:
            calc_default_qty_ = lambda balance_, last_price: \
                calc_default_qty(min_qty, qty_step, balance_ * last_price, settings['default_qty'])
        else:
            calc_default_qty_ = lambda balance_, last_price: settings[
                'default_qty']
        calc_max_pos_size = lambda balance_, price_: balance_ * price_ * leverage
    else:
        calc_cost = lambda qty_, price_: qty_ * price_
        calc_liq_price = lambda balance_, pos_size_, pos_price_: \
            binance_calc_cross_shrt_liq_price(balance_, pos_size_, pos_price_, leverage=leverage) \
                if pos_size_ < 0.0 else \
                binance_calc_cross_long_liq_price(balance_, pos_size_, pos_price_, leverage=leverage)
        if settings['default_qty'] <= 0.0:
            calc_default_qty_ = lambda balance_, last_price: \
                calc_default_qty(max(min_qty, round_up(min_notional / last_price, qty_step)),
                                 qty_step, balance_ / last_price, settings['default_qty'])
        else:
            calc_default_qty_ = lambda balance_, last_price: \
                max(settings['default_qty'], round_up(min_notional / last_price, qty_step))
        calc_max_pos_size = lambda balance_, price_: balance_ / price_ * leverage

    calc_long_reentry_price_ = lambda balance_, pos_margin_, pos_price_, highest_bid_: \
        min(highest_bid_, calc_long_reentry_price(price_step, grid_spacing, grid_coefficient,
                                           balance_, pos_margin_, pos_price_))
    calc_shrt_reentry_price_ = lambda balance_, pos_margin_, pos_price_, lowest_ask_: \
        max(lowest_ask_, calc_shrt_reentry_price(price_step, grid_spacing, grid_coefficient,
                                                 balance_, pos_margin_, pos_price_))
    balance = starting_balance
    trades = []
    ob = [
        min(trades_list[0]['price'], trades_list[1]['price']),
        max(trades_list[0]['price'], trades_list[1]['price'])
    ]

    pos_size = 0.0
    pos_price = 0.0

    bid_price = ob[0]
    ask_price = ob[1]

    liq_price = 0.0

    pnl_sum = 0.0
    loss_sum = 0.0

    ema_alpha = 2 / (settings['ema_span'] + 1)
    ema_alpha_ = 1 - ema_alpha
    ema = trades_list[0]['price']

    k = 0
    prev_len_trades = 0

    for t in trades_list:
        if t['buyer_maker']:
            # buy
            if pos_size == 0.0:
                # no pos
                bid_price = min(ob[0], round_dn(ema, price_step))
                bid_qty = calc_default_qty_(balance, ob[0])
            elif pos_size > 0.0:
                # long pos
                liq_price = calc_liq_price(balance, pos_size, pos_price)
                if calc_diff(liq_price, ob[0]) < liq_diff_threshold:
                    # long soft stop, no reentry
                    bid_price = 0.0
                else:
                    # long reentry
                    bid_qty = calc_entry_qty(qty_step, ddown_factor,
                                             calc_default_qty_(balance, ob[0]),
                                             calc_max_pos_size(balance, ob[0]),
                                             pos_size)
                    if bid_qty >= min_qty:
                        pos_margin = calc_cost(pos_size, pos_price) / leverage
                        bid_price = calc_long_reentry_price_(
                            balance, pos_margin, pos_price, ob[0])
                    else:
                        bid_price = 0.0
            else:
                # short pos
                liq_price = calc_liq_price(balance, pos_size, pos_price)
                if calc_diff(liq_price, ob[0]) < liq_diff_threshold:
                    # short soft stop
                    bid_price = ob[0]
                    bid_qty = round_up(-pos_size * stop_loss_pos_reduction,
                                       qty_step)
                else:
                    if t['price'] <= pos_price:
                        # short close
                        qtys, prices = calc_shrt_closes(
                            price_step, qty_step, min_qty, min_markup,
                            max_markup, pos_size, pos_price, ob[0],
                            n_close_orders)
                        bid_qty = qtys[0]
                        bid_price = prices[0]
                    else:
                        bid_price = 0.0
            ob[0] = t['price']
            if t['price'] < bid_price:
                # filled trade
                cost = calc_cost(bid_qty, bid_price)
                pnl = -cost * maker_fee
                if pos_size >= 0.0:
                    # create or increase long pos
                    trade_side = 'long'
                    trade_type = 'entry'
                    new_pos_size = pos_size + bid_qty
                    pos_price = pos_price * (pos_size / new_pos_size) + \
                        bid_price * (bid_qty / new_pos_size)
                    pos_size = new_pos_size
                    roi = 0.0
                else:
                    # close short pos
                    trade_side = 'shrt'
                    gain = pos_price / bid_price - 1
                    pnl += cost * gain
                    trade_type = 'close' if gain > 0.0 else 'stop_loss'
                    pos_size = pos_size + bid_qty
                    roi = gain * leverage
                balance += pnl
                pnl_sum += pnl
                trades.append({
                    'trade_id':
                    k,
                    'side':
                    trade_side,
                    'type':
                    trade_type,
                    'price':
                    bid_price,
                    'qty':
                    bid_qty,
                    'pnl':
                    pnl,
                    'roi':
                    roi,
                    'pos_size':
                    pos_size,
                    'pos_price':
                    pos_price,
                    'liq_price':
                    calc_liq_price(balance, pos_size, pos_price)
                })
        else:
            # sell
            if pos_size == 0.0:
                # no pos
                ask_price = max(ob[1], round_up(ema, price_step))
                ask_qty = -calc_default_qty_(balance, ob[1])
            elif pos_size > 0.0:
                # long pos
                liq_price = calc_liq_price(balance, pos_size, pos_price)
                if calc_diff(liq_price, ob[1]) < liq_diff_threshold:
                    # long soft stop
                    ask_price = ob[1]
                    ask_qty = -round_up(pos_size * stop_loss_pos_reduction,
                                        qty_step)
                else:
                    if t['price'] >= pos_price:
                        # long close
                        qtys, prices = calc_long_closes(
                            price_step, qty_step, min_qty, min_markup,
                            max_markup, pos_size, pos_price, ob[1],
                            n_close_orders)
                        ask_qty = qtys[0]
                        ask_price = prices[0]
                    else:
                        ask_price = 9e9
            else:
                # short pos
                liq_price = calc_liq_price(balance, pos_size, pos_price)
                if calc_diff(liq_price, ob[1]) < liq_diff_threshold:
                    # shrt soft stop, no reentry
                    ask_price = 9e9
                else:
                    # shrt reentry
                    ask_qty = -calc_entry_qty(
                        qty_step, ddown_factor,
                        calc_default_qty_(balance, ob[1]),
                        calc_max_pos_size(balance, ob[1]), pos_size)
                    if -ask_qty >= min_qty:
                        pos_margin = calc_cost(-pos_size, pos_price) / leverage
                        ask_price = calc_shrt_reentry_price_(
                            balance, pos_margin, pos_price, ob[0])
                    else:
                        ask_price = 9e9
            ob[1] = t['price']
            if t['price'] > ask_price:
                # filled trade
                cost = calc_cost(-ask_qty, ask_price)
                pnl = -cost * maker_fee
                if pos_size <= 0.0:
                    # create or increase shrt pos
                    trade_side = 'shrt'
                    trade_type = 'entry'
                    new_pos_size = pos_size + ask_qty
                    pos_price = pos_price * (pos_size / new_pos_size) + \
                        ask_price * (ask_qty / new_pos_size)
                    pos_size = new_pos_size
                    roi = 0.0
                else:
                    # close long pos
                    trade_side = 'long'
                    gain = ask_price / pos_price - 1
                    pnl += cost * gain
                    trade_type = 'close' if gain > 0.0 else 'stop_loss'
                    pos_size = pos_size + ask_qty
                    roi = gain * leverage
                balance += pnl
                pnl_sum += pnl
                trades.append({
                    'trade_id':
                    k,
                    'side':
                    trade_side,
                    'type':
                    trade_type,
                    'price':
                    ask_price,
                    'qty':
                    ask_qty,
                    'pnl':
                    pnl,
                    'roi':
                    roi,
                    'pos_size':
                    pos_size,
                    'pos_price':
                    pos_price,
                    'liq_price':
                    calc_liq_price(balance, pos_size, pos_price)
                })
        ema = ema * ema_alpha_ + t['price'] * ema_alpha
        k += 1
        if k % 10000 == 0 or len(trades) != prev_len_trades:
            if trades and trades[-1]['type'] == 'stop_loss':
                loss_sum += pnl
                if break_on_loss:
                    print('break on loss')
                    return trades
            balance = max(balance, settings['balance'])
            prev_len_trades = len(trades)
            progress = k / len(trades_list)
            if pnl_sum < 0.0 and progress >= settings['break_on_negative_pnl']:
                print('break on negative pnl')
                return trades
            line = f"\r{progress:.3f} pnl sum {pnl_sum:.8f} "
            line += f"loss sum {loss_sum:.6f} balance {balance:.6f} "
            line += f"qty {calc_default_qty_(balance, ob[0]):.4f} "
            line += f"liq diff {min(1.0, calc_diff(trades[-1]['liq_price'], ob[0])):.3f} "
            line += f"pos size {pos_size:.6f} "
            print(line, end=' ')
    return trades
Exemplo n.º 2
0
def backtest(trades_list: [dict], settings: dict):
    # trades format is [{price: float, buyer_maker: bool}]

    # no static mode
    grid_spacing = settings['grid_spacing']
    grid_coefficient = settings['grid_coefficient']
    price_step = settings['price_step']
    qty_step = settings['qty_step']
    inverse = settings['inverse']
    liq_diff_threshold = settings['liq_diff_threshold']
    stop_loss_pos_reduction = settings['stop_loss_pos_reduction']
    min_qty = settings['min_qty']
    ddown_factor = settings['ddown_factor']
    leverage = settings['leverage']
    max_leverage = settings['max_leverage']
    maker_fee = settings['maker_fee']
    taker_fee = settings['taker_fee']
    min_markup = settings['min_markup']
    max_markup = settings['max_markup']
    n_close_orders = settings['n_close_orders']

    do_long = settings['do_long']
    do_shrt = settings['do_shrt']

    min_notional = settings[
        'min_notional'] if 'min_notional' in settings else 0.0
    cross_mode = settings['cross_mode'] if 'cross_mode' in settings else True

    if inverse:
        calc_cost = lambda qty_, price_: qty_ / price_
        if settings['cross_mode']:
            calc_liq_price = lambda balance_, pos_size_, pos_price_: \
                bybit_calc_cross_shrt_liq_price(balance_,
                                                pos_size_,
                                                pos_price_,
                                                leverage=max_leverage) \
                    if pos_size_ < 0.0 else \
                    bybit_calc_cross_long_liq_price(balance_,
                                                    pos_size_,
                                                    pos_price_,
                                                    leverage=max_leverage)
        else:
            calc_liq_price = lambda balance_, pos_size_, pos_price_: \
                bybit_calc_isolated_shrt_liq_price(balance_,
                                                   pos_size_,
                                                   pos_price_,
                                                   leverage=leverage) \
                    if pos_size_ < 0.0 else \
                    bybit_calc_isolated_long_liq_price(balance_,
                                                       pos_size_,
                                                       pos_price_,
                                                       leverage=leverage)
        if settings['default_qty'] <= 0.0:
            calc_default_qty_ = lambda balance_, last_price: \
                calc_default_qty(min_qty, qty_step, balance_ * last_price, settings['default_qty'])
        else:
            calc_default_qty_ = lambda balance_, last_price: settings[
                'default_qty']
        calc_max_pos_size = lambda balance_, price_: balance_ * price_ * leverage
    else:
        calc_cost = lambda qty_, price_: qty_ * price_
        if settings['cross_mode']:
            calc_liq_price = lambda balance_, pos_size_, pos_price_: \
                binance_calc_cross_shrt_liq_price(balance_,
                                                  pos_size_,
                                                  pos_price_,
                                                  leverage=leverage) \
                    if pos_size_ < 0.0 else \
                    binance_calc_cross_long_liq_price(balance_,
                                                      pos_size_,
                                                      pos_price_,
                                                      leverage=leverage)
        else:
            calc_liq_price = lambda balance_, pos_size_, pos_price_: \
                binance_calc_isolated_shrt_liq_price(balance_,
                                                     pos_size_,
                                                     pos_price_,
                                                     leverage=leverage) \
                    if pos_size_ < 0.0 else \
                    binance_calc_isolated_long_liq_price(balance_,
                                                         pos_size_,
                                                         pos_price_,
                                                         leverage=leverage)
        if settings['default_qty'] <= 0.0:
            calc_default_qty_ = lambda balance_, last_price: \
                calc_default_qty(max(min_qty, round_up(min_notional / last_price, qty_step)),
                                 qty_step, balance_ / last_price, settings['default_qty'])
        else:
            calc_default_qty_ = lambda balance_, last_price: \
                max(settings['default_qty'], round_up(min_notional / last_price, qty_step))
        calc_max_pos_size = lambda balance_, price_: balance_ / price_ * leverage

    calc_long_reentry_price_ = lambda balance_, pos_margin_, pos_price_, highest_bid_: \
        min(highest_bid_, calc_long_reentry_price(price_step, grid_spacing, grid_coefficient,
                                           balance_, pos_margin_, pos_price_))
    calc_shrt_reentry_price_ = lambda balance_, pos_margin_, pos_price_, lowest_ask_: \
        max(lowest_ask_, calc_shrt_reentry_price(price_step, grid_spacing, grid_coefficient,
                                                 balance_, pos_margin_, pos_price_))
    balance = settings['starting_balance']
    trades = []
    ob = [
        min(trades_list[0]['price'], trades_list[1]['price']),
        max(trades_list[0]['price'], trades_list[1]['price'])
    ]

    pos_size = 0.0
    pos_price = 0.0

    bid_price = ob[0]
    ask_price = ob[1]

    liq_price = 0.0

    pnl_sum = 0.0
    loss_sum = 0.0
    profit_sum = 0.0

    ema_alpha = 2 / (settings['ema_span'] + 1)
    ema_alpha_ = 1 - ema_alpha
    ema = trades_list[0]['price']

    k = 0
    break_on = {
        e[0]: eval(e[1])
        for e in settings['break_on'] if e[0].startswith('ON:')
    }
    for t in trades_list:
        append_trade = False
        if t['buyer_maker']:
            # buy
            if pos_size == 0.0:
                # no pos
                if do_long:
                    bid_price = min(ob[0], round_dn(ema, price_step))
                    bid_qty = calc_default_qty_(balance, ob[0])
                else:
                    bid_price = 0.0
                    bid_qty = 0.0
            elif pos_size > 0.0:
                if calc_diff(
                        liq_price, ob[1]
                ) < liq_diff_threshold and t['price'] <= liq_price:
                    # long liq
                    print(f'break on long liquidation, liq price: {liq_price}')
                    return []
                # long reentry
                bid_qty = calc_entry_qty(qty_step, ddown_factor,
                                         calc_default_qty_(balance, ob[0]),
                                         calc_max_pos_size(balance, ob[0]),
                                         pos_size)
                if bid_qty >= max(min_qty, min_notional / t['price']):
                    pos_margin = calc_cost(pos_size, pos_price) / leverage
                    bid_price = calc_long_reentry_price_(
                        balance, pos_margin, pos_price, ob[0])
                else:
                    bid_price = 0.0
            else:
                # short pos
                if calc_diff(liq_price, ob[0]) < liq_diff_threshold:
                    # short soft stop
                    bid_price = ob[0]
                    bid_qty = round_up(-pos_size * stop_loss_pos_reduction,
                                       qty_step)
                else:
                    if t['price'] <= pos_price:
                        # short close
                        min_close_qty = \
                            max(min_qty,
                                round_dn(calc_default_qty_(balance, ob[0]) * 0.5, qty_step))
                        qtys, prices = calc_shrt_closes(
                            price_step, qty_step, min_qty, min_markup,
                            max_markup, min_close_qty, pos_size, pos_price,
                            ob[0], n_close_orders)
                        if len(qtys) > 0:
                            bid_qty = qtys[0]
                            bid_price = prices[0]
                        else:
                            bid_price = 0.0
                    else:
                        bid_price = 0.0
            ob[0] = t['price']
            if t['price'] < bid_price and bid_qty >= min_qty:
                # filled trade
                qty = bid_qty
                price = bid_price
                cost = calc_cost(bid_qty, bid_price)
                pnl = -cost * maker_fee
                if pos_size >= 0.0:
                    # create or increase long pos
                    trade_side = 'long'
                    trade_type = 'entry'
                    new_pos_size = pos_size + bid_qty
                    pos_price = pos_price * (pos_size / new_pos_size) + \
                        bid_price * (bid_qty / new_pos_size)
                    pos_size = new_pos_size
                    roi = 0.0
                else:
                    # close short pos
                    trade_side = 'shrt'
                    gain = pos_price / bid_price - 1
                    pnl += cost * gain
                    if gain > 0.0:
                        trade_type = 'close'
                        profit_sum += pnl
                    else:
                        trade_type = 'stop_loss'
                        loss_sum += pnl
                    pos_size = pos_size + bid_qty
                    roi = gain * leverage
                balance += pnl
                pnl_sum += pnl
                liq_price = calc_liq_price(balance, pos_size, pos_price)
                append_trade = True
        else:
            # sell
            if pos_size == 0.0:
                # no pos
                if do_shrt:
                    ask_price = max(ob[1], round_up(ema, price_step))
                    ask_qty = -calc_default_qty_(balance, ob[1])
                else:
                    ask_price = 9e9
                    ask_qty = 0.0
            elif pos_size > 0.0:
                # long pos
                if calc_diff(liq_price, ob[1]) < liq_diff_threshold:
                    # long soft stop
                    ask_price = ob[1]
                    ask_qty = -round_up(pos_size * stop_loss_pos_reduction,
                                        qty_step)
                else:
                    if t['price'] >= pos_price:
                        # long close
                        min_close_qty = \
                            max(min_qty,
                                round_dn(calc_default_qty_(balance, ob[1]) * 0.5, qty_step))
                        qtys, prices = calc_long_closes(
                            price_step, qty_step, min_qty, min_markup,
                            max_markup, min_close_qty, pos_size, pos_price,
                            ob[1], n_close_orders)
                        if len(qtys) > 0:
                            ask_qty = qtys[0]
                            ask_price = prices[0]
                        else:
                            ask_price = 9e9
                    else:
                        ask_price = 9e9
            else:
                if calc_diff(
                        liq_price, ob[1]
                ) < liq_diff_threshold and t['price'] >= liq_price:
                    # shrt liq
                    print(f'break on shrt liquidation, liq price: {liq_price}')
                    return []
                # shrt reentry
                ask_qty = -calc_entry_qty(
                    qty_step, ddown_factor, calc_default_qty_(balance, ob[1]),
                    calc_max_pos_size(balance, ob[1]), pos_size)
                if -ask_qty >= max(min_qty, min_notional / t['price']):
                    pos_margin = calc_cost(-pos_size, pos_price) / leverage
                    ask_price = calc_shrt_reentry_price_(
                        balance, pos_margin, pos_price, ob[0])
                else:
                    ask_price = 9e9
            ob[1] = t['price']
            if t['price'] > ask_price and abs(ask_qty) >= min_qty:
                # filled trade
                qty = ask_qty
                price = ask_price
                cost = calc_cost(-ask_qty, ask_price)
                pnl = -cost * maker_fee
                if pos_size <= 0.0:
                    # create or increase shrt pos
                    trade_side = 'shrt'
                    trade_type = 'entry'
                    new_pos_size = pos_size + ask_qty
                    pos_price = pos_price * (pos_size / new_pos_size) + \
                        ask_price * (ask_qty / new_pos_size)
                    pos_size = new_pos_size
                    roi = 0.0
                else:
                    # close long pos
                    trade_side = 'long'
                    gain = ask_price / pos_price - 1
                    pnl += cost * gain
                    if gain > 0.0:
                        trade_type = 'close'
                        profit_sum += pnl
                    else:
                        trade_type = 'stop_loss'
                        loss_sum += pnl
                    pos_size = pos_size + ask_qty
                    roi = gain * leverage
                balance += pnl
                pnl_sum += pnl
                liq_price = calc_liq_price(balance, pos_size, pos_price)
                append_trade = True
        if append_trade:
            progress = k / len(trades_list)
            total_gain = (pnl_sum + settings['starting_balance']
                          ) / settings['starting_balance']
            n_days_ = (t['timestamp'] -
                       trades_list[0]['timestamp']) / (1000 * 60 * 60 * 24)
            adg = total_gain**(1 / n_days_)
            trades.append({
                'trade_id':
                k,
                'side':
                trade_side,
                'type':
                trade_type,
                'price':
                price,
                'qty':
                qty,
                'pnl':
                pnl,
                'roi':
                roi,
                'pos_size':
                pos_size,
                'pos_price':
                pos_price,
                'balance':
                balance,
                'max_pos_size':
                calc_max_pos_size(balance, t['price']),
                'pnl_sum':
                pnl_sum,
                'loss_sum':
                loss_sum,
                'profit_sum':
                profit_sum,
                'progress':
                progress,
                'liq_price':
                liq_price,
                'gain':
                total_gain,
                'n_days':
                n_days_,
                'average_daily_gain':
                adg,
                'liq_diff':
                min(1.0, calc_diff(liq_price, t['price'])),
                'timestamp':
                t['timestamp']
            })
            balance = max(balance, settings['starting_balance'])
            line = f"\r{progress:.3f} net pnl {pnl_sum:.8f} "
            line += f"profit sum {profit_sum:.5f} "
            line += f"loss sum {loss_sum:.5f} "
            line += f"balance {balance:.5f} qty {calc_default_qty_(balance, ob[0]):.4f} "
            line += f"adg {trades[-1]['average_daily_gain']:.3f} "
            line += f"max pos pct {abs(pos_size) / calc_max_pos_size(balance, t['price']):.3f} "
            line += f"liq diff {min(1.0, calc_diff(trades[-1]['liq_price'], ob[0])):.3f} "
            line += f"pos size {pos_size:.4f} "
            print(line, end=' ')
            for key, condition in break_on.items():
                if condition(trades[-1], t):
                    print('break on', key)
                    return []
        ema = ema * ema_alpha_ + t['price'] * ema_alpha
        k += 1
    return trades
Exemplo n.º 3
0
def backtest(df: pd.DataFrame, settings: dict):

    grid_step = settings['grid_step']
    price_step = settings['price_step']
    qty_step = settings['qty_step']
    inverse = settings['inverse']
    break_on_loss = settings['break_on_loss']

    if inverse:
        calc_cost = lambda qty_, price_: qty_ / price_
    else:
        calc_cost = lambda qty_, price_: qty_ * price_

    maker_fee = settings['maker_fee']

    min_qty = settings['min_qty']
    min_markup = sorted(settings['markups'])[0]
    max_markup = sorted(settings['markups'])[-1]
    n_close_orders = settings['n_close_orders']

    default_qty = settings['default_qty']

    leverage = settings['leverage']
    margin_limit = settings['margin_limit']

    trades = []

    ob = [df.iloc[:2].price.min(), df.iloc[:2].price.max()]

    pos_size = 0.0
    pos_price = 0.0

    bid_price = round_dn(ob[0], grid_step)
    ask_price = round_up(ob[1], grid_step)

    pnl_sum = 0.0

    for row in df.itertuples():
        if row.buyer_maker:
            if pos_size == 0.0:  # no pos
                bid_qty = default_qty
                bid_price = round_dn(ob[0], grid_step)
            elif pos_size > 0.0:  # long pos
                if calc_cost(pos_size, pos_price) / leverage > margin_limit:
                    # limit reached; enter no more
                    bid_qty = 0.0
                    bid_price = 0.0
                else:  # long reentry
                    bid_qty = default_qty
                    bid_price = round_dn(min(ob[0], pos_price), grid_step)
            else:  # shrt pos
                if row.price <= pos_price:  # shrt close
                    qtys, prices = calc_shrt_closes(price_step, qty_step,
                                                    default_qty, min_markup,
                                                    max_markup, pos_size,
                                                    pos_price, ob[0],
                                                    n_close_orders)
                    bid_qty = qtys[0]
                    bid_price = prices[0]
                elif -calc_cost(pos_size, pos_price) / leverage > margin_limit:
                    if break_on_loss:
                        return []
                    # controlled shrt loss
                    bid_qty = default_qty
                    bid_price = round_dn(ob[0], grid_step)
                else:  # no shrt close
                    bid_qty = 0.0
                    bid_price = 0.0
            ob[0] = row.price
            if row.price < bid_price:
                if pos_size >= 0.0:
                    # create or add to long pos
                    cost = calc_cost(bid_qty, bid_price)
                    margin_cost = cost / leverage
                    pnl = -cost * maker_fee
                    new_pos_size = pos_size + bid_qty
                    pos_price = pos_price * (pos_size / new_pos_size) + \
                        bid_price * (bid_qty / new_pos_size)
                    pos_size = new_pos_size
                    trades.append({
                        'trade_id': row.Index,
                        'side': 'long',
                        'type': 'entry',
                        'price': bid_price,
                        'qty': bid_qty,
                        'pnl': pnl,
                        'pos_size': pos_size,
                        'pos_price': pos_price,
                        'roe': np.nan,
                        'margin_cost': margin_cost
                    })
                    pnl_sum += pnl
                    print(
                        f'\r{row.Index / len(df):.2f} pnl sum {pnl_sum:.4f} pos_size {pos_size:.3f} ',
                        end='    ')
                else:
                    # close shrt pos
                    cost = calc_cost(bid_qty, bid_price)
                    margin_cost = cost / leverage
                    gain = (pos_price / bid_price - 1)
                    pnl = cost * gain - cost * maker_fee
                    pos_size += bid_qty
                    roe = gain * leverage
                    trades.append({
                        'trade_id': row.Index,
                        'side': 'shrt',
                        'type': 'close',
                        'price': bid_price,
                        'qty': bid_qty,
                        'pnl': pnl,
                        'pos_size': pos_size,
                        'pos_price': pos_price,
                        'roe': roe,
                        'margin_cost': margin_cost
                    })
                    pnl_sum += pnl
                    print(
                        f'\r{row.Index / len(df):.2f} pnl sum {pnl_sum:.4f} pos_size {pos_size:.3f} ',
                        end='    ')
        else:
            if pos_size == 0.0:  # no pos
                ask_qty = -default_qty
                ask_price = round_up(ob[1], grid_step)
            elif pos_size < 0.0:  # shrt pos
                if -calc_cost(pos_size, pos_price) / leverage > margin_limit:
                    # limit reached; enter no more
                    ask_qty = 0.0
                    ask_price = 9.9e9
                else:  # shrt reentry
                    ask_qty = -default_qty
                    ask_price = round_up(max(ob[1], pos_price), grid_step)
            else:  # long pos
                if row.price >= pos_price:  # close long pos
                    qtys, prices = calc_long_closes(price_step, qty_step,
                                                    default_qty, min_markup,
                                                    max_markup, pos_size,
                                                    pos_price, ob[1],
                                                    n_close_orders)
                    ask_qty = qtys[0]
                    ask_price = prices[0]
                elif calc_cost(pos_size, pos_price) / leverage > margin_limit:
                    if break_on_loss:
                        return []
                    # controlled long loss
                    ask_qty = -default_qty
                    ask_price = round_up(ob[1], grid_step)
                else:  # no close
                    ask_qty = 0.0
                    ask_price = 9.9e9
            ob[1] = row.price
            if row.price > ask_price:
                if pos_size <= 0.0:
                    # add to or create short pos
                    cost = -calc_cost(ask_qty, ask_price)
                    margin_cost = cost / leverage
                    pnl = -cost * maker_fee
                    new_pos_size = pos_size + ask_qty
                    pos_price = pos_price * (pos_size / new_pos_size) + \
                        ask_price * (ask_qty / new_pos_size)
                    pos_size = new_pos_size
                    trades.append({
                        'trade_id': row.Index,
                        'side': 'shrt',
                        'type': 'entry',
                        'price': ask_price,
                        'qty': ask_qty,
                        'pnl': pnl,
                        'pos_size': pos_size,
                        'pos_price': pos_price,
                        'roe': np.nan,
                        'margin_cost': margin_cost
                    })
                    pnl_sum += pnl
                    print(
                        f'\r{row.Index / len(df):.2f} pnl sum {pnl_sum:.4f} pos_size {pos_size:.3f} ',
                        end='    ')
                else:
                    # close long pos
                    cost = -calc_cost(ask_qty, ask_price)
                    margin_cost = cost / leverage
                    gain = (ask_price / pos_price - 1)
                    pnl = cost * gain - cost * maker_fee
                    pos_size += ask_qty
                    roe = gain * leverage
                    trades.append({
                        'trade_id': row.Index,
                        'side': 'long',
                        'type': 'close',
                        'price': ask_price,
                        'qty': ask_qty,
                        'pnl': pnl,
                        'pos_size': pos_size,
                        'pos_price': pos_price,
                        'roe': roe,
                        'margin_cost': margin_cost
                    })
                    pnl_sum += pnl
                    print(
                        f'\r{row.Index / len(df):.2f} pnl sum {pnl_sum:.4f} pos_size {pos_size:.3f} ',
                        end='    ')
    return trades