pmorissette/bt

Potential infinite loop in `allocate` due to high precision requirement for non-integer positions

0xEljh opened this issue · 0 comments

Description:

I have identified an issue with the allocate method of SecurityBase in core.py. When integer_positions is set to False and the method attempts to find an optimal q such that the total outlay is as close as possible to the amount, it potentially triggers the infinite loop error condition due to the high precision requirements (rtol is set to 0.0):

while not np.isclose(full_outlay, amount, rtol=0.0) and q != 0:
    dq_wout_considering_tx_costs = (full_outlay - amount) / (
        self._price * self.multiplier
    )

This results in the following error being thrown in a setup that involves no commissions:

if i > 1e4:
                    raise Exception(
                        "Potentially infinite loop detected. This occurred "
                        "while trying to reduce the amount of shares purchased"
                        " to respect the outlay <= amount rule. This is most "
                        "likely due to a commission function that outputs a "
                        "commission that is greater than the amount of cash "
                        "a short sale can raise."
                    )

Reproduction

This issue is hard to reproduce without providing data, but is an issue I frequently encounter when backtesting cryptocurrencies, whereby it makes sense to have integer_positions set to False and where price decimal precision is high, causing this error to sometimes emerge when allocating into crypto assets.

def setup_backtest(name: str, algos: list, prices: pd.DataFrame):
    strat = bt.Strategy(name, algos)
    return bt.Backtest(strat, prices, integer_positions=False, commissions=None)

total_count = 25_000
batch_size = 10

for i in tqdm(range(0, total_count, batch_size)):
    backtests = []
    for x in range(batch_size):
        backtests.append(setup_backtest(f"pick_1_{i + x}", [
            bt.algos.RunMonthly(),
            bt.algos.SelectRandomly(n=1),
            bt.algos.WeighEqually(),
            bt.algos.Rebalance(),
            ],
            price_df
            ))
    results = bt.run(*backtests)

Expected behavior

The allocate method should find q without entering an infinite loop since no commissions are involved. Otherwise, the error thrown should be more informative of potential causes.

Solutions

I've found that relaxing the precision requirement from rtol=0.0 to rtol=1e-8 in the np.isclose function seems to mitigate the issue. However, I would like to open up a discussion on the potential impacts of this change.

  • Could relaxing this condition have any unexpected negative impact?
  • Perhaps this can be constrained to when integer_positions=False.
  • Should rtol be set as an argument to backtest?
  • Is there a better way to handle this issue?