ruff format: update more tests

This commit is contained in:
Matthias
2024-05-12 16:04:01 +02:00
parent 40e161a5b9
commit 02075b15e3
4 changed files with 524 additions and 499 deletions
+103 -112
View File
@@ -19,13 +19,10 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
* 2nd trade is kept * 2nd trade is kept
* 3rd trade is sold via sell-signal * 3rd trade is sold via sell-signal
""" """
default_conf['max_open_trades'] = 3 default_conf["max_open_trades"] = 3
default_conf['exchange']['name'] = 'binance' default_conf["exchange"]["name"] = "binance"
stoploss = { stoploss = {"id": 123, "info": {}}
'id': 123,
'info': {}
}
stoploss_order_open = { stoploss_order_open = {
"id": "123", "id": "123",
"timestamp": 1542707426845, "timestamp": 1542707426845,
@@ -42,21 +39,17 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
"remaining": 0.0, "remaining": 0.0,
"status": "open", "status": "open",
"fee": None, "fee": None,
"trades": None "trades": None,
} }
stoploss_order_closed = stoploss_order_open.copy() stoploss_order_closed = stoploss_order_open.copy()
stoploss_order_closed['status'] = 'closed' stoploss_order_closed["status"] = "closed"
stoploss_order_closed['filled'] = stoploss_order_closed['amount'] stoploss_order_closed["filled"] = stoploss_order_closed["amount"]
# Sell first trade based on stoploss, keep 2nd and 3rd trade open # Sell first trade based on stoploss, keep 2nd and 3rd trade open
stop_orders = [stoploss_order_closed, stoploss_order_open.copy(), stoploss_order_open.copy()] stop_orders = [stoploss_order_closed, stoploss_order_open.copy(), stoploss_order_open.copy()]
stoploss_order_mock = MagicMock( stoploss_order_mock = MagicMock(side_effect=stop_orders)
side_effect=stop_orders)
# Sell 3rd trade (not called for the first trade) # Sell 3rd trade (not called for the first trade)
should_sell_mock = MagicMock(side_effect=[ should_sell_mock = MagicMock(side_effect=[[], [ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL)]])
[],
[ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL)]]
)
cancel_order_mock = MagicMock() cancel_order_mock = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(
EXMS, EXMS,
@@ -70,7 +63,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
) )
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot', "freqtrade.freqtradebot.FreqtradeBot",
create_stoploss_order=MagicMock(return_value=True), create_stoploss_order=MagicMock(return_value=True),
_notify_exit=MagicMock(), _notify_exit=MagicMock(),
) )
@@ -80,9 +73,9 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
mocker.patch("freqtrade.wallets.Wallets.check_exit_amount", return_value=True) mocker.patch("freqtrade.wallets.Wallets.check_exit_amount", return_value=True)
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
freqtrade.strategy.order_types['stoploss_on_exchange'] = True freqtrade.strategy.order_types["stoploss_on_exchange"] = True
# Switch ordertype to market to close trade immediately # Switch ordertype to market to close trade immediately
freqtrade.strategy.order_types['exit'] = 'market' freqtrade.strategy.order_types["exit"] = "market"
freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True) freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True)
freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True) freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True)
patch_get_signal(freqtrade) patch_get_signal(freqtrade)
@@ -98,8 +91,8 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
# Make sure stoploss-order is open and trade is bought # Make sure stoploss-order is open and trade is bought
for idx, trade in enumerate(trades): for idx, trade in enumerate(trades):
stop_order = stop_orders[idx] stop_order = stop_orders[idx]
stop_order['id'] = f"stop{idx}" stop_order["id"] = f"stop{idx}"
oobj = Order.parse_from_ccxt_object(stop_order, trade.pair, 'stoploss') oobj = Order.parse_from_ccxt_object(stop_order, trade.pair, "stoploss")
oobj.ft_is_open = True oobj.ft_is_open = True
trade.orders.append(oobj) trade.orders.append(oobj)
@@ -132,10 +125,13 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
assert not trade.is_open assert not trade.is_open
@pytest.mark.parametrize("balance_ratio,result1", [ @pytest.mark.parametrize(
(1, 200), "balance_ratio,result1",
(0.99, 198), [
]) (1, 200),
(0.99, 198),
],
)
def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_ratio, result1) -> None: def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_ratio, result1) -> None:
""" """
Tests workflow unlimited stake-amount Tests workflow unlimited stake-amount
@@ -143,14 +139,14 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_rati
Sell one trade, calculated stake amount should now be lower than before since Sell one trade, calculated stake amount should now be lower than before since
one trade was sold at a loss. one trade was sold at a loss.
""" """
default_conf['max_open_trades'] = 5 default_conf["max_open_trades"] = 5
default_conf['force_entry_enable'] = True default_conf["force_entry_enable"] = True
default_conf['stake_amount'] = 'unlimited' default_conf["stake_amount"] = "unlimited"
default_conf['tradable_balance_ratio'] = balance_ratio default_conf["tradable_balance_ratio"] = balance_ratio
default_conf['dry_run_wallet'] = 1000 default_conf["dry_run_wallet"] = 1000
default_conf['exchange']['name'] = 'binance' default_conf["exchange"]["name"] = "binance"
default_conf['telegram']['enabled'] = True default_conf["telegram"]["enabled"] = True
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch("freqtrade.rpc.telegram.Telegram", MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
EXMS, EXMS,
fetch_ticker=ticker, fetch_ticker=ticker,
@@ -160,24 +156,20 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_rati
) )
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot', "freqtrade.freqtradebot.FreqtradeBot",
create_stoploss_order=MagicMock(return_value=True), create_stoploss_order=MagicMock(return_value=True),
_notify_exit=MagicMock(), _notify_exit=MagicMock(),
) )
should_sell_mock = MagicMock(side_effect=[ should_sell_mock = MagicMock(
[], side_effect=[[], [ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL)], [], [], []]
[ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL)],
[],
[],
[]]
) )
mocker.patch("freqtrade.strategy.interface.IStrategy.should_exit", should_sell_mock) mocker.patch("freqtrade.strategy.interface.IStrategy.should_exit", should_sell_mock)
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
rpc = RPC(freqtrade) rpc = RPC(freqtrade)
freqtrade.strategy.order_types['stoploss_on_exchange'] = True freqtrade.strategy.order_types["stoploss_on_exchange"] = True
# Switch ordertype to market to close trade immediately # Switch ordertype to market to close trade immediately
freqtrade.strategy.order_types['exit'] = 'market' freqtrade.strategy.order_types["exit"] = "market"
patch_get_signal(freqtrade) patch_get_signal(freqtrade)
# Create 4 trades # Create 4 trades
@@ -186,9 +178,9 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_rati
trades = Trade.session.scalars(select(Trade)).all() trades = Trade.session.scalars(select(Trade)).all()
assert len(trades) == 4 assert len(trades) == 4
assert freqtrade.wallets.get_trade_stake_amount('XRP/BTC', 5) == result1 assert freqtrade.wallets.get_trade_stake_amount("XRP/BTC", 5) == result1
rpc._rpc_force_entry('TKN/BTC', None) rpc._rpc_force_entry("TKN/BTC", None)
trades = Trade.session.scalars(select(Trade)).all() trades = Trade.session.scalars(select(Trade)).all()
assert len(trades) == 5 assert len(trades) == 5
@@ -206,18 +198,18 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_rati
# One trade sold # One trade sold
assert len(trades) == 4 assert len(trades) == 4
# stake-amount should now be reduced, since one trade was sold at a loss. # stake-amount should now be reduced, since one trade was sold at a loss.
assert freqtrade.wallets.get_trade_stake_amount('XRP/BTC', 5) < result1 assert freqtrade.wallets.get_trade_stake_amount("XRP/BTC", 5) < result1
# Validate that balance of sold trade is not in dry-run balances anymore. # Validate that balance of sold trade is not in dry-run balances anymore.
bals2 = freqtrade.wallets.get_all_balances() bals2 = freqtrade.wallets.get_all_balances()
assert bals != bals2 assert bals != bals2
assert len(bals) == 6 assert len(bals) == 6
assert len(bals2) == 5 assert len(bals2) == 5
assert 'LTC' in bals assert "LTC" in bals
assert 'LTC' not in bals2 assert "LTC" not in bals2
def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None: def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
default_conf_usdt['position_adjustment_enable'] = True default_conf_usdt["position_adjustment_enable"] = True
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
mocker.patch.multiple( mocker.patch.multiple(
@@ -242,8 +234,8 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
# Reduce bid amount # Reduce bid amount
ticker_usdt_modif = ticker_usdt.return_value ticker_usdt_modif = ticker_usdt.return_value
ticker_usdt_modif['bid'] = ticker_usdt_modif['bid'] * 0.995 ticker_usdt_modif["bid"] = ticker_usdt_modif["bid"] * 0.995
mocker.patch(f'{EXMS}.fetch_ticker', return_value=ticker_usdt_modif) mocker.patch(f"{EXMS}.fetch_ticker", return_value=ticker_usdt_modif)
# additional buy order # additional buy order
freqtrade.process() freqtrade.process()
@@ -263,7 +255,7 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
assert len(trade.orders) == 2 assert len(trade.orders) == 2
assert pytest.approx(trade.stake_amount) == 120 assert pytest.approx(trade.stake_amount) == 120
assert trade.orders[0].amount == 30 assert trade.orders[0].amount == 30
assert pytest.approx(trade.orders[1].amount) == 60 / ticker_usdt_modif['bid'] assert pytest.approx(trade.orders[1].amount) == 60 / ticker_usdt_modif["bid"]
assert pytest.approx(trade.amount) == trade.orders[0].amount + trade.orders[1].amount assert pytest.approx(trade.amount) == trade.orders[0].amount + trade.orders[1].amount
assert trade.nr_of_successful_buys == 2 assert trade.nr_of_successful_buys == 2
@@ -275,10 +267,10 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
trade = Trade.get_trades().first() trade = Trade.get_trades().first()
assert trade.is_open is False assert trade.is_open is False
assert trade.orders[0].amount == 30 assert trade.orders[0].amount == 30
assert trade.orders[0].side == 'buy' assert trade.orders[0].side == "buy"
assert pytest.approx(trade.orders[1].amount) == 60 / ticker_usdt_modif['bid'] assert pytest.approx(trade.orders[1].amount) == 60 / ticker_usdt_modif["bid"]
# Sold everything # Sold everything
assert trade.orders[-1].side == 'sell' assert trade.orders[-1].side == "sell"
assert trade.orders[2].amount == trade.amount assert trade.orders[2].amount == trade.amount
assert trade.nr_of_successful_buys == 2 assert trade.nr_of_successful_buys == 2
@@ -286,7 +278,7 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None: def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
default_conf_usdt['position_adjustment_enable'] = True default_conf_usdt["position_adjustment_enable"] = True
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
mocker.patch.multiple( mocker.patch.multiple(
@@ -314,8 +306,8 @@ def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
# Reduce bid amount # Reduce bid amount
ticker_usdt_modif = ticker_usdt.return_value ticker_usdt_modif = ticker_usdt.return_value
ticker_usdt_modif['ask'] = ticker_usdt_modif['ask'] * 1.004 ticker_usdt_modif["ask"] = ticker_usdt_modif["ask"] * 1.004
mocker.patch(f'{EXMS}.fetch_ticker', return_value=ticker_usdt_modif) mocker.patch(f"{EXMS}.fetch_ticker", return_value=ticker_usdt_modif)
# additional buy order # additional buy order
freqtrade.process() freqtrade.process()
@@ -334,7 +326,7 @@ def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
trade = Trade.get_trades().first() trade = Trade.get_trades().first()
assert len(trade.orders) == 2 assert len(trade.orders) == 2
assert pytest.approx(trade.stake_amount) == 120 assert pytest.approx(trade.stake_amount) == 120
assert trade.orders[1].amount == round(60 / ticker_usdt_modif['ask'], 4) assert trade.orders[1].amount == round(60 / ticker_usdt_modif["ask"], 4)
assert trade.amount == trade.orders[0].amount + trade.orders[1].amount assert trade.amount == trade.orders[0].amount + trade.orders[1].amount
assert trade.nr_of_successful_entries == 2 assert trade.nr_of_successful_entries == 2
@@ -345,23 +337,21 @@ def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
trade = Trade.get_trades().first() trade = Trade.get_trades().first()
assert trade.is_open is False assert trade.is_open is False
# assert trade.orders[0].amount == 30 # assert trade.orders[0].amount == 30
assert trade.orders[0].side == 'sell' assert trade.orders[0].side == "sell"
assert trade.orders[1].amount == round(60 / ticker_usdt_modif['ask'], 4) assert trade.orders[1].amount == round(60 / ticker_usdt_modif["ask"], 4)
# Sold everything # Sold everything
assert trade.orders[-1].side == 'buy' assert trade.orders[-1].side == "buy"
assert trade.orders[2].amount == trade.amount assert trade.orders[2].amount == trade.amount
assert trade.nr_of_successful_entries == 2 assert trade.nr_of_successful_entries == 2
assert trade.nr_of_successful_exits == 1 assert trade.nr_of_successful_exits == 1
@pytest.mark.parametrize('leverage', [ @pytest.mark.parametrize("leverage", [1, 2])
1, 2
])
def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) -> None: def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) -> None:
default_conf_usdt['position_adjustment_enable'] = True default_conf_usdt["position_adjustment_enable"] = True
default_conf_usdt['trading_mode'] = 'futures' default_conf_usdt["trading_mode"] = "futures"
default_conf_usdt['margin_mode'] = 'isolated' default_conf_usdt["margin_mode"] = "isolated"
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
mocker.patch.multiple( mocker.patch.multiple(
@@ -371,13 +361,13 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
amount_to_precision=lambda s, x, y: y, amount_to_precision=lambda s, x, y: y,
price_to_precision=lambda s, x, y: y, price_to_precision=lambda s, x, y: y,
) )
mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=False) mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=False)
mocker.patch(f"{EXMS}.get_max_leverage", return_value=10) mocker.patch(f"{EXMS}.get_max_leverage", return_value=10)
mocker.patch(f"{EXMS}.get_funding_fees", return_value=0) mocker.patch(f"{EXMS}.get_funding_fees", return_value=0)
mocker.patch(f"{EXMS}.get_maintenance_ratio_and_amt", return_value=(0, 0)) mocker.patch(f"{EXMS}.get_maintenance_ratio_and_amt", return_value=(0, 0))
patch_get_signal(freqtrade) patch_get_signal(freqtrade)
freqtrade.strategy.custom_entry_price = lambda **kwargs: ticker_usdt['ask'] * 0.96 freqtrade.strategy.custom_entry_price = lambda **kwargs: ticker_usdt["ask"] * 0.96
freqtrade.strategy.leverage = MagicMock(return_value=leverage) freqtrade.strategy.leverage = MagicMock(return_value=leverage)
freqtrade.strategy.minimal_roi = {0: 0.2} freqtrade.strategy.minimal_roi = {0: 0.2}
@@ -417,7 +407,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
assert trade.initial_stop_loss_pct == -0.1 assert trade.initial_stop_loss_pct == -0.1
# Fill order # Fill order
mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=True) mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=True)
freqtrade.process() freqtrade.process()
trade = Trade.get_trades().first() trade = Trade.get_trades().first()
assert len(trade.orders) == 2 assert len(trade.orders) == 2
@@ -433,7 +423,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
# 2nd order - not filling # 2nd order - not filling
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=120) freqtrade.strategy.adjust_trade_position = MagicMock(return_value=120)
mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=False) mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=False)
freqtrade.process() freqtrade.process()
trade = Trade.get_trades().first() trade = Trade.get_trades().first()
@@ -458,7 +448,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
# Fill DCA order # Fill DCA order
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=None) freqtrade.strategy.adjust_trade_position = MagicMock(return_value=None)
mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=True) mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=True)
freqtrade.strategy.adjust_entry_price = MagicMock(side_effect=ValueError) freqtrade.strategy.adjust_entry_price = MagicMock(side_effect=ValueError)
freqtrade.process() freqtrade.process()
@@ -468,7 +458,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
assert pytest.approx(trade.open_rate) == 1.963153456 assert pytest.approx(trade.open_rate) == 1.963153456
assert trade.orders[-1].price == 1.95 assert trade.orders[-1].price == 1.95
assert pytest.approx(trade.orders[-1].cost) == 120 * leverage assert pytest.approx(trade.orders[-1].cost) == 120 * leverage
assert trade.orders[-1].status == 'closed' assert trade.orders[-1].status == "closed"
assert pytest.approx(trade.amount) == 91.689215 * leverage assert pytest.approx(trade.amount) == 91.689215 * leverage
# Check the 2 filled orders equal the above amount # Check the 2 filled orders equal the above amount
@@ -476,14 +466,14 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
assert pytest.approx(trade.orders[-1].amount) == 61.538461232 * leverage assert pytest.approx(trade.orders[-1].amount) == 61.538461232 * leverage
# Full exit # Full exit
mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=False) mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=False)
freqtrade.strategy.custom_exit = MagicMock(return_value='Exit now') freqtrade.strategy.custom_exit = MagicMock(return_value="Exit now")
freqtrade.strategy.adjust_entry_price = MagicMock(return_value=2.02) freqtrade.strategy.adjust_entry_price = MagicMock(return_value=2.02)
freqtrade.process() freqtrade.process()
trade = Trade.get_trades().first() trade = Trade.get_trades().first()
assert len(trade.orders) == 5 assert len(trade.orders) == 5
assert trade.orders[-1].side == trade.exit_side assert trade.orders[-1].side == trade.exit_side
assert trade.orders[-1].status == 'open' assert trade.orders[-1].status == "open"
assert trade.orders[-1].price == 2.02 assert trade.orders[-1].price == 2.02
assert pytest.approx(trade.amount) == 91.689215 * leverage assert pytest.approx(trade.amount) == 91.689215 * leverage
assert pytest.approx(trade.orders[-1].amount) == 91.689215 * leverage assert pytest.approx(trade.orders[-1].amount) == 91.689215 * leverage
@@ -492,23 +482,23 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
freqtrade.process() freqtrade.process()
trade = Trade.get_trades().first() trade = Trade.get_trades().first()
assert len(trade.orders) == 5 assert len(trade.orders) == 5
assert trade.orders[-1].status == 'open' assert trade.orders[-1].status == "open"
assert trade.orders[-1].price == 2.02 assert trade.orders[-1].price == 2.02
# Adjust entry price cannot be called - this is an exit order # Adjust entry price cannot be called - this is an exit order
assert freqtrade.strategy.adjust_entry_price.call_count == 0 assert freqtrade.strategy.adjust_entry_price.call_count == 0
@pytest.mark.parametrize('leverage', [1, 2]) @pytest.mark.parametrize("leverage", [1, 2])
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
def test_dca_order_adjust_entry_replace_fails( def test_dca_order_adjust_entry_replace_fails(
default_conf_usdt, ticker_usdt, fee, mocker, caplog, is_short, leverage default_conf_usdt, ticker_usdt, fee, mocker, caplog, is_short, leverage
) -> None: ) -> None:
spot = leverage == 1 spot = leverage == 1
if not spot: if not spot:
default_conf_usdt['trading_mode'] = 'futures' default_conf_usdt["trading_mode"] = "futures"
default_conf_usdt['margin_mode'] = 'isolated' default_conf_usdt["margin_mode"] = "isolated"
default_conf_usdt['position_adjustment_enable'] = True default_conf_usdt["position_adjustment_enable"] = True
default_conf_usdt['max_open_trades'] = 2 default_conf_usdt["max_open_trades"] = 2
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
mocker.patch.multiple( mocker.patch.multiple(
EXMS, EXMS,
@@ -518,7 +508,7 @@ def test_dca_order_adjust_entry_replace_fails(
) )
# no order fills. # no order fills.
mocker.patch(f'{EXMS}._dry_is_price_crossed', side_effect=[False, True]) mocker.patch(f"{EXMS}._dry_is_price_crossed", side_effect=[False, True])
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
freqtrade.enter_positions() freqtrade.enter_positions()
@@ -527,10 +517,10 @@ def test_dca_order_adjust_entry_replace_fails(
.where(Order.ft_is_open.is_(True)) .where(Order.ft_is_open.is_(True))
.where(Order.ft_order_side != "stoploss") .where(Order.ft_order_side != "stoploss")
.where(Order.ft_trade_id == Trade.id) .where(Order.ft_trade_id == Trade.id)
).all() ).all()
assert len(trades) == 1 assert len(trades) == 1
mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=False) mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=False)
# Timeout to not interfere # Timeout to not interfere
freqtrade.strategy.ft_check_timed_out = MagicMock(return_value=False) freqtrade.strategy.ft_check_timed_out = MagicMock(return_value=False)
@@ -538,7 +528,7 @@ def test_dca_order_adjust_entry_replace_fails(
# Create DCA order for 2nd trade (so we have 2 open orders on 2 trades) # Create DCA order for 2nd trade (so we have 2 open orders on 2 trades)
# this 2nd order won't fill. # this 2nd order won't fill.
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=(20, 'PeNF')) freqtrade.strategy.adjust_trade_position = MagicMock(return_value=(20, "PeNF"))
freqtrade.process() freqtrade.process()
@@ -548,7 +538,7 @@ def test_dca_order_adjust_entry_replace_fails(
.where(Order.ft_is_open.is_(True)) .where(Order.ft_is_open.is_(True))
.where(Order.ft_order_side != "stoploss") .where(Order.ft_order_side != "stoploss")
.where(Order.ft_trade_id == Trade.id) .where(Order.ft_trade_id == Trade.id)
).all() ).all()
assert len(trades) == 2 assert len(trades) == 2
# We now have 2 orders open # We now have 2 orders open
@@ -559,7 +549,7 @@ def test_dca_order_adjust_entry_replace_fails(
.where(Order.ft_is_open.is_(True)) .where(Order.ft_is_open.is_(True))
.where(Order.ft_order_side != "stoploss") .where(Order.ft_order_side != "stoploss")
.where(Order.ft_trade_id == Trade.id) .where(Order.ft_trade_id == Trade.id)
).all() ).all()
assert len(trades) == 2 assert len(trades) == 2
assert len(Order.get_open_orders()) == 2 assert len(Order.get_open_orders()) == 2
# Entry adjustment is called # Entry adjustment is called
@@ -568,8 +558,9 @@ def test_dca_order_adjust_entry_replace_fails(
# Attempt order replacement - fails. # Attempt order replacement - fails.
freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1234) freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1234)
entry_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_entry', entry_mock = mocker.patch(
return_value=False) "freqtrade.freqtradebot.FreqtradeBot.execute_entry", return_value=False
)
msg = r"Could not replace order for.*" msg = r"Could not replace order for.*"
assert not log_has_re(msg, caplog) assert not log_has_re(msg, caplog)
freqtrade.manage_open_orders() freqtrade.manage_open_orders()
@@ -580,13 +571,13 @@ def test_dca_order_adjust_entry_replace_fails(
assert len(Order.get_open_orders()) == 0 assert len(Order.get_open_orders()) == 0
@pytest.mark.parametrize('leverage', [1, 2]) @pytest.mark.parametrize("leverage", [1, 2])
def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, leverage) -> None: def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, leverage) -> None:
default_conf_usdt['position_adjustment_enable'] = True default_conf_usdt["position_adjustment_enable"] = True
spot = leverage == 1 spot = leverage == 1
if not spot: if not spot:
default_conf_usdt['trading_mode'] = 'futures' default_conf_usdt["trading_mode"] = "futures"
default_conf_usdt['margin_mode'] = 'isolated' default_conf_usdt["margin_mode"] = "isolated"
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
assert freqtrade.trading_mode == TradingMode.FUTURES if not spot else TradingMode.SPOT assert freqtrade.trading_mode == TradingMode.FUTURES if not spot else TradingMode.SPOT
mocker.patch.multiple( mocker.patch.multiple(
@@ -599,7 +590,7 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera
get_funding_fees=MagicMock(return_value=0), get_funding_fees=MagicMock(return_value=0),
) )
mocker.patch(f"{EXMS}.get_max_leverage", return_value=10) mocker.patch(f"{EXMS}.get_max_leverage", return_value=10)
starting_amount = freqtrade.wallets.get_total('USDT') starting_amount = freqtrade.wallets.get_total("USDT")
assert starting_amount == 1000 assert starting_amount == 1000
patch_get_signal(freqtrade) patch_get_signal(freqtrade)
@@ -613,11 +604,11 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera
assert trade.leverage == leverage assert trade.leverage == leverage
assert pytest.approx(trade.amount) == 30.0 * leverage assert pytest.approx(trade.amount) == 30.0 * leverage
assert trade.open_rate == 2.0 assert trade.open_rate == 2.0
assert pytest.approx(freqtrade.wallets.get_free('USDT')) == starting_amount - 60 assert pytest.approx(freqtrade.wallets.get_free("USDT")) == starting_amount - 60
if spot: if spot:
assert pytest.approx(freqtrade.wallets.get_total('USDT')) == starting_amount - 60 assert pytest.approx(freqtrade.wallets.get_total("USDT")) == starting_amount - 60
else: else:
assert freqtrade.wallets.get_total('USDT') == starting_amount assert freqtrade.wallets.get_total("USDT") == starting_amount
# Too small size # Too small size
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-59) freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-59)
@@ -627,28 +618,29 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera
assert pytest.approx(trade.stake_amount) == 60 assert pytest.approx(trade.stake_amount) == 60
assert pytest.approx(trade.amount) == 30.0 * leverage assert pytest.approx(trade.amount) == 30.0 * leverage
assert log_has_re( assert log_has_re(
r"Remaining amount of \d\.\d+.* would be smaller than the minimum of 10.", caplog) r"Remaining amount of \d\.\d+.* would be smaller than the minimum of 10.", caplog
)
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=(-20, 'PES')) freqtrade.strategy.adjust_trade_position = MagicMock(return_value=(-20, "PES"))
freqtrade.process() freqtrade.process()
trade = Trade.get_trades().first() trade = Trade.get_trades().first()
assert len(trade.orders) == 2 assert len(trade.orders) == 2
assert trade.orders[-1].ft_order_side == 'sell' assert trade.orders[-1].ft_order_side == "sell"
assert trade.orders[-1].ft_order_tag == 'PES' assert trade.orders[-1].ft_order_tag == "PES"
assert pytest.approx(trade.stake_amount) == 40 assert pytest.approx(trade.stake_amount) == 40
assert pytest.approx(trade.amount) == 20 * leverage assert pytest.approx(trade.amount) == 20 * leverage
assert trade.open_rate == 2.0 assert trade.open_rate == 2.0
assert trade.is_open assert trade.is_open
assert trade.realized_profit > 0.098 * leverage assert trade.realized_profit > 0.098 * leverage
expected_profit = starting_amount - 40 + trade.realized_profit expected_profit = starting_amount - 40 + trade.realized_profit
assert pytest.approx(freqtrade.wallets.get_free('USDT')) == expected_profit assert pytest.approx(freqtrade.wallets.get_free("USDT")) == expected_profit
if spot: if spot:
assert pytest.approx(freqtrade.wallets.get_total('USDT')) == expected_profit assert pytest.approx(freqtrade.wallets.get_total("USDT")) == expected_profit
else: else:
# total won't change in futures mode, only free / used will. # total won't change in futures mode, only free / used will.
assert freqtrade.wallets.get_total('USDT') == starting_amount + trade.realized_profit assert freqtrade.wallets.get_total("USDT") == starting_amount + trade.realized_profit
caplog.clear() caplog.clear()
# Sell more than what we got (we got ~20 coins left) # Sell more than what we got (we got ~20 coins left)
@@ -666,14 +658,13 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera
assert len(trade.orders) == 2 assert len(trade.orders) == 2
# Amount exactly comes out as exactly 0 # Amount exactly comes out as exactly 0
freqtrade.strategy.adjust_trade_position = MagicMock( freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-trade.stake_amount)
return_value=-trade.stake_amount)
freqtrade.process() freqtrade.process()
trade = Trade.get_trades().first() trade = Trade.get_trades().first()
assert len(trade.orders) == 3 assert len(trade.orders) == 3
assert trade.orders[-1].ft_order_side == 'sell' assert trade.orders[-1].ft_order_side == "sell"
assert pytest.approx(trade.stake_amount) == 40 assert pytest.approx(trade.stake_amount) == 40
assert trade.is_open is False assert trade.is_open is False
@@ -683,14 +674,14 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera
freqtrade.process() freqtrade.process()
trade = Trade.get_trades().first() trade = Trade.get_trades().first()
assert len(trade.orders) == 3 assert len(trade.orders) == 3
assert trade.orders[-1].ft_order_side == 'sell' assert trade.orders[-1].ft_order_side == "sell"
assert pytest.approx(trade.stake_amount) == 40 assert pytest.approx(trade.stake_amount) == 40
assert trade.is_open is False assert trade.is_open is False
assert log_has_re('Amount to exit is 0.0 due to exchange limits - not exiting.', caplog) assert log_has_re("Amount to exit is 0.0 due to exchange limits - not exiting.", caplog)
expected_profit = starting_amount - 60 + trade.realized_profit expected_profit = starting_amount - 60 + trade.realized_profit
assert pytest.approx(freqtrade.wallets.get_free('USDT')) == expected_profit assert pytest.approx(freqtrade.wallets.get_free("USDT")) == expected_profit
if spot: if spot:
assert pytest.approx(freqtrade.wallets.get_total('USDT')) == expected_profit assert pytest.approx(freqtrade.wallets.get_total("USDT")) == expected_profit
else: else:
# total won't change in futures mode, only free / used will. # total won't change in futures mode, only free / used will.
assert freqtrade.wallets.get_total('USDT') == starting_amount + trade.realized_profit assert freqtrade.wallets.get_total("USDT") == starting_amount + trade.realized_profit
+58 -51
View File
@@ -9,96 +9,103 @@ from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver
def test_hyperoptlossresolver_noname(default_conf): def test_hyperoptlossresolver_noname(default_conf):
with pytest.raises(OperationalException, with pytest.raises(
match="No Hyperopt loss set. Please use `--hyperopt-loss` to specify " OperationalException,
"the Hyperopt-Loss class to use."): match="No Hyperopt loss set. Please use `--hyperopt-loss` to specify "
"the Hyperopt-Loss class to use.",
):
HyperOptLossResolver.load_hyperoptloss(default_conf) HyperOptLossResolver.load_hyperoptloss(default_conf)
def test_hyperoptlossresolver(mocker, default_conf) -> None: def test_hyperoptlossresolver(mocker, default_conf) -> None:
hl = ShortTradeDurHyperOptLoss hl = ShortTradeDurHyperOptLoss
mocker.patch( mocker.patch(
'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver.load_object', "freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver.load_object",
MagicMock(return_value=hl()) MagicMock(return_value=hl()),
) )
default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'}) default_conf.update({"hyperopt_loss": "SharpeHyperOptLossDaily"})
x = HyperOptLossResolver.load_hyperoptloss(default_conf) x = HyperOptLossResolver.load_hyperoptloss(default_conf)
assert hasattr(x, "hyperopt_loss_function") assert hasattr(x, "hyperopt_loss_function")
def test_hyperoptlossresolver_wrongname(default_conf) -> None: def test_hyperoptlossresolver_wrongname(default_conf) -> None:
default_conf.update({'hyperopt_loss': "NonExistingLossClass"}) default_conf.update({"hyperopt_loss": "NonExistingLossClass"})
with pytest.raises(OperationalException, match=r'Impossible to load HyperoptLoss.*'): with pytest.raises(OperationalException, match=r"Impossible to load HyperoptLoss.*"):
HyperOptLossResolver.load_hyperoptloss(default_conf) HyperOptLossResolver.load_hyperoptloss(default_conf)
def test_loss_calculation_prefer_correct_trade_count(hyperopt_conf, hyperopt_results) -> None: def test_loss_calculation_prefer_correct_trade_count(hyperopt_conf, hyperopt_results) -> None:
hyperopt_conf.update({'hyperopt_loss': "ShortTradeDurHyperOptLoss"}) hyperopt_conf.update({"hyperopt_loss": "ShortTradeDurHyperOptLoss"})
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf) hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
correct = hl.hyperopt_loss_function(hyperopt_results, 600, correct = hl.hyperopt_loss_function(
datetime(2019, 1, 1), datetime(2019, 5, 1)) hyperopt_results, 600, datetime(2019, 1, 1), datetime(2019, 5, 1)
over = hl.hyperopt_loss_function(hyperopt_results, 600 + 100, )
datetime(2019, 1, 1), datetime(2019, 5, 1)) over = hl.hyperopt_loss_function(
under = hl.hyperopt_loss_function(hyperopt_results, 600 - 100, hyperopt_results, 600 + 100, datetime(2019, 1, 1), datetime(2019, 5, 1)
datetime(2019, 1, 1), datetime(2019, 5, 1)) )
under = hl.hyperopt_loss_function(
hyperopt_results, 600 - 100, datetime(2019, 1, 1), datetime(2019, 5, 1)
)
assert over > correct assert over > correct
assert under > correct assert under > correct
def test_loss_calculation_prefer_shorter_trades(hyperopt_conf, hyperopt_results) -> None: def test_loss_calculation_prefer_shorter_trades(hyperopt_conf, hyperopt_results) -> None:
resultsb = hyperopt_results.copy() resultsb = hyperopt_results.copy()
resultsb.loc[1, 'trade_duration'] = 20 resultsb.loc[1, "trade_duration"] = 20
hyperopt_conf.update({'hyperopt_loss': "ShortTradeDurHyperOptLoss"}) hyperopt_conf.update({"hyperopt_loss": "ShortTradeDurHyperOptLoss"})
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf) hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
longer = hl.hyperopt_loss_function(hyperopt_results, 100, longer = hl.hyperopt_loss_function(
datetime(2019, 1, 1), datetime(2019, 5, 1)) hyperopt_results, 100, datetime(2019, 1, 1), datetime(2019, 5, 1)
shorter = hl.hyperopt_loss_function(resultsb, 100, )
datetime(2019, 1, 1), datetime(2019, 5, 1)) shorter = hl.hyperopt_loss_function(resultsb, 100, datetime(2019, 1, 1), datetime(2019, 5, 1))
assert shorter < longer assert shorter < longer
def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) -> None: def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) -> None:
results_over = hyperopt_results.copy() results_over = hyperopt_results.copy()
results_over['profit_ratio'] = hyperopt_results['profit_ratio'] * 2 results_over["profit_ratio"] = hyperopt_results["profit_ratio"] * 2
results_under = hyperopt_results.copy() results_under = hyperopt_results.copy()
results_under['profit_ratio'] = hyperopt_results['profit_ratio'] / 2 results_under["profit_ratio"] = hyperopt_results["profit_ratio"] / 2
hyperopt_conf.update({'hyperopt_loss': "ShortTradeDurHyperOptLoss"}) hyperopt_conf.update({"hyperopt_loss": "ShortTradeDurHyperOptLoss"})
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf) hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
correct = hl.hyperopt_loss_function(hyperopt_results, 600, correct = hl.hyperopt_loss_function(
datetime(2019, 1, 1), datetime(2019, 5, 1)) hyperopt_results, 600, datetime(2019, 1, 1), datetime(2019, 5, 1)
over = hl.hyperopt_loss_function(results_over, 600, )
datetime(2019, 1, 1), datetime(2019, 5, 1)) over = hl.hyperopt_loss_function(results_over, 600, datetime(2019, 1, 1), datetime(2019, 5, 1))
under = hl.hyperopt_loss_function(results_under, 600, under = hl.hyperopt_loss_function(
datetime(2019, 1, 1), datetime(2019, 5, 1)) results_under, 600, datetime(2019, 1, 1), datetime(2019, 5, 1)
)
assert over < correct assert over < correct
assert under > correct assert under > correct
@pytest.mark.parametrize('lossfunction', [ @pytest.mark.parametrize(
"OnlyProfitHyperOptLoss", "lossfunction",
"SortinoHyperOptLoss", [
"SortinoHyperOptLossDaily", "OnlyProfitHyperOptLoss",
"SharpeHyperOptLoss", "SortinoHyperOptLoss",
"SharpeHyperOptLossDaily", "SortinoHyperOptLossDaily",
"MaxDrawDownHyperOptLoss", "SharpeHyperOptLoss",
"MaxDrawDownRelativeHyperOptLoss", "SharpeHyperOptLossDaily",
"CalmarHyperOptLoss", "MaxDrawDownHyperOptLoss",
"ProfitDrawDownHyperOptLoss", "MaxDrawDownRelativeHyperOptLoss",
"CalmarHyperOptLoss",
]) "ProfitDrawDownHyperOptLoss",
],
)
def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunction) -> None: def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunction) -> None:
results_over = hyperopt_results.copy() results_over = hyperopt_results.copy()
results_over['profit_abs'] = hyperopt_results['profit_abs'] * 2 + 0.2 results_over["profit_abs"] = hyperopt_results["profit_abs"] * 2 + 0.2
results_over['profit_ratio'] = hyperopt_results['profit_ratio'] * 2 results_over["profit_ratio"] = hyperopt_results["profit_ratio"] * 2
results_under = hyperopt_results.copy() results_under = hyperopt_results.copy()
results_under['profit_abs'] = hyperopt_results['profit_abs'] / 2 - 0.2 results_under["profit_abs"] = hyperopt_results["profit_abs"] / 2 - 0.2
results_under['profit_ratio'] = hyperopt_results['profit_ratio'] / 2 results_under["profit_ratio"] = hyperopt_results["profit_ratio"] / 2
default_conf.update({'hyperopt_loss': lossfunction}) default_conf.update({"hyperopt_loss": lossfunction})
hl = HyperOptLossResolver.load_hyperoptloss(default_conf) hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
correct = hl.hyperopt_loss_function( correct = hl.hyperopt_loss_function(
hyperopt_results, hyperopt_results,
@@ -107,7 +114,7 @@ def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunct
max_date=datetime(2019, 5, 1), max_date=datetime(2019, 5, 1),
config=default_conf, config=default_conf,
processed=None, processed=None,
backtest_stats={'profit_total': hyperopt_results['profit_abs'].sum()} backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
) )
over = hl.hyperopt_loss_function( over = hl.hyperopt_loss_function(
results_over, results_over,
@@ -116,7 +123,7 @@ def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunct
max_date=datetime(2019, 5, 1), max_date=datetime(2019, 5, 1),
config=default_conf, config=default_conf,
processed=None, processed=None,
backtest_stats={'profit_total': results_over['profit_abs'].sum()} backtest_stats={"profit_total": results_over["profit_abs"].sum()},
) )
under = hl.hyperopt_loss_function( under = hl.hyperopt_loss_function(
results_under, results_under,
@@ -125,7 +132,7 @@ def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunct
max_date=datetime(2019, 5, 1), max_date=datetime(2019, 5, 1),
config=default_conf, config=default_conf,
processed=None, processed=None,
backtest_stats={'profit_total': results_under['profit_abs'].sum()} backtest_stats={"profit_total": results_under["profit_abs"].sum()},
) )
assert over < correct assert over < correct
assert under > correct assert under > correct
+297 -271
View File
@@ -51,7 +51,7 @@ def _backup_file(file: Path, copy_file: bool = False) -> None:
:param copy_file: keep file in place too. :param copy_file: keep file in place too.
:return: None :return: None
""" """
file_swp = str(file) + '.swp' file_swp = str(file) + ".swp"
if file.is_file(): if file.is_file():
file.rename(file_swp) file.rename(file_swp)
@@ -60,149 +60,172 @@ def _backup_file(file: Path, copy_file: bool = False) -> None:
def test_text_table_bt_results(): def test_text_table_bt_results():
results = pd.DataFrame( results = pd.DataFrame(
{ {
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], "pair": ["ETH/BTC", "ETH/BTC", "ETH/BTC"],
'profit_ratio': [0.1, 0.2, -0.05], "profit_ratio": [0.1, 0.2, -0.05],
'profit_abs': [0.2, 0.4, -0.1], "profit_abs": [0.2, 0.4, -0.1],
'trade_duration': [10, 30, 20], "trade_duration": [10, 30, 20],
} }
) )
result_str = ( result_str = (
'| Pair | Entries | Avg Profit % | Tot Profit BTC | ' "| Pair | Entries | Avg Profit % | Tot Profit BTC | "
'Tot Profit % | Avg Duration | Win Draw Loss Win% |\n' "Tot Profit % | Avg Duration | Win Draw Loss Win% |\n"
'|---------+-----------+----------------+------------------+' "|---------+-----------+----------------+------------------+"
'----------------+----------------+-------------------------|\n' "----------------+----------------+-------------------------|\n"
'| ETH/BTC | 3 | 8.33 | 0.50000000 | ' "| ETH/BTC | 3 | 8.33 | 0.50000000 | "
'12.50 | 0:20:00 | 2 0 1 66.7 |\n' "12.50 | 0:20:00 | 2 0 1 66.7 |\n"
'| TOTAL | 3 | 8.33 | 0.50000000 | ' "| TOTAL | 3 | 8.33 | 0.50000000 | "
'12.50 | 0:20:00 | 2 0 1 66.7 |' "12.50 | 0:20:00 | 2 0 1 66.7 |"
) )
pair_results = generate_pair_metrics(['ETH/BTC'], stake_currency='BTC', pair_results = generate_pair_metrics(
starting_balance=4, results=results) ["ETH/BTC"], stake_currency="BTC", starting_balance=4, results=results
assert text_table_bt_results(pair_results, stake_currency='BTC') == result_str )
assert text_table_bt_results(pair_results, stake_currency="BTC") == result_str
def test_generate_backtest_stats(default_conf, testdatadir, tmp_path): def test_generate_backtest_stats(default_conf, testdatadir, tmp_path):
default_conf.update({'strategy': CURRENT_TEST_STRATEGY}) default_conf.update({"strategy": CURRENT_TEST_STRATEGY})
StrategyResolver.load_strategy(default_conf) StrategyResolver.load_strategy(default_conf)
results = {'DefStrat': { results = {
'results': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", "DefStrat": {
"UNITTEST/BTC", "UNITTEST/BTC"], "results": pd.DataFrame(
"profit_ratio": [0.003312, 0.010801, 0.013803, 0.002780], {
"profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], "pair": ["UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC"],
"open_date": [dt_utc(2017, 11, 14, 19, 32, 00), "profit_ratio": [0.003312, 0.010801, 0.013803, 0.002780],
dt_utc(2017, 11, 14, 21, 36, 00), "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003],
dt_utc(2017, 11, 14, 22, 12, 00), "open_date": [
dt_utc(2017, 11, 14, 22, 44, 00)], dt_utc(2017, 11, 14, 19, 32, 00),
"close_date": [dt_utc(2017, 11, 14, 21, 35, 00), dt_utc(2017, 11, 14, 21, 36, 00),
dt_utc(2017, 11, 14, 22, 10, 00), dt_utc(2017, 11, 14, 22, 12, 00),
dt_utc(2017, 11, 14, 22, 43, 00), dt_utc(2017, 11, 14, 22, 44, 00),
dt_utc(2017, 11, 14, 22, 58, 00)], ],
"open_rate": [0.002543, 0.003003, 0.003089, 0.003214], "close_date": [
"close_rate": [0.002546, 0.003014, 0.003103, 0.003217], dt_utc(2017, 11, 14, 21, 35, 00),
"trade_duration": [123, 34, 31, 14], dt_utc(2017, 11, 14, 22, 10, 00),
"is_open": [False, False, False, True], dt_utc(2017, 11, 14, 22, 43, 00),
"is_short": [False, False, False, False], dt_utc(2017, 11, 14, 22, 58, 00),
"stake_amount": [0.01, 0.01, 0.01, 0.01], ],
"exit_reason": [ExitType.ROI, ExitType.STOP_LOSS, "open_rate": [0.002543, 0.003003, 0.003089, 0.003214],
ExitType.ROI, ExitType.FORCE_EXIT] "close_rate": [0.002546, 0.003014, 0.003103, 0.003217],
}), "trade_duration": [123, 34, 31, 14],
'config': default_conf, "is_open": [False, False, False, True],
'locks': [], "is_short": [False, False, False, False],
'final_balance': 1000.02, "stake_amount": [0.01, 0.01, 0.01, 0.01],
'rejected_signals': 20, "exit_reason": [
'timedout_entry_orders': 0, ExitType.ROI,
'timedout_exit_orders': 0, ExitType.STOP_LOSS,
'canceled_trade_entries': 0, ExitType.ROI,
'canceled_entry_orders': 0, ExitType.FORCE_EXIT,
'replaced_entry_orders': 0, ],
'backtest_start_time': dt_ts() // 1000, }
'backtest_end_time': dt_ts() // 1000, ),
'run_id': '123', "config": default_conf,
"locks": [],
"final_balance": 1000.02,
"rejected_signals": 20,
"timedout_entry_orders": 0,
"timedout_exit_orders": 0,
"canceled_trade_entries": 0,
"canceled_entry_orders": 0,
"replaced_entry_orders": 0,
"backtest_start_time": dt_ts() // 1000,
"backtest_end_time": dt_ts() // 1000,
"run_id": "123",
} }
} }
timerange = TimeRange.parse_timerange('1510688220-1510700340') timerange = TimeRange.parse_timerange("1510688220-1510700340")
min_date = dt_from_ts(1510688220) min_date = dt_from_ts(1510688220)
max_date = dt_from_ts(1510700340) max_date = dt_from_ts(1510700340)
btdata = history.load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange, btdata = history.load_data(
fill_up_missing=True) testdatadir, "1m", ["UNITTEST/BTC"], timerange=timerange, fill_up_missing=True
)
stats = generate_backtest_stats(btdata, results, min_date, max_date) stats = generate_backtest_stats(btdata, results, min_date, max_date)
assert isinstance(stats, dict) assert isinstance(stats, dict)
assert 'strategy' in stats assert "strategy" in stats
assert 'DefStrat' in stats['strategy'] assert "DefStrat" in stats["strategy"]
assert 'strategy_comparison' in stats assert "strategy_comparison" in stats
strat_stats = stats['strategy']['DefStrat'] strat_stats = stats["strategy"]["DefStrat"]
assert strat_stats['backtest_start'] == min_date.strftime(DATETIME_PRINT_FORMAT) assert strat_stats["backtest_start"] == min_date.strftime(DATETIME_PRINT_FORMAT)
assert strat_stats['backtest_end'] == max_date.strftime(DATETIME_PRINT_FORMAT) assert strat_stats["backtest_end"] == max_date.strftime(DATETIME_PRINT_FORMAT)
assert strat_stats['total_trades'] == len(results['DefStrat']['results']) assert strat_stats["total_trades"] == len(results["DefStrat"]["results"])
# Above sample had no losing trade # Above sample had no losing trade
assert strat_stats['max_drawdown_account'] == 0.0 assert strat_stats["max_drawdown_account"] == 0.0
# Retry with losing trade # Retry with losing trade
results = {'DefStrat': { results = {
'results': pd.DataFrame( "DefStrat": {
{"pair": ["UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC"], "results": pd.DataFrame(
"profit_ratio": [0.003312, 0.010801, -0.013803, 0.002780], {
"profit_abs": [0.000003, 0.000011, -0.000014, 0.000003], "pair": ["UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC"],
"open_date": [dt_utc(2017, 11, 14, 19, 32, 00), "profit_ratio": [0.003312, 0.010801, -0.013803, 0.002780],
dt_utc(2017, 11, 14, 21, 36, 00), "profit_abs": [0.000003, 0.000011, -0.000014, 0.000003],
dt_utc(2017, 11, 14, 22, 12, 00), "open_date": [
dt_utc(2017, 11, 14, 22, 44, 00)], dt_utc(2017, 11, 14, 19, 32, 00),
"close_date": [dt_utc(2017, 11, 14, 21, 35, 00), dt_utc(2017, 11, 14, 21, 36, 00),
dt_utc(2017, 11, 14, 22, 10, 00), dt_utc(2017, 11, 14, 22, 12, 00),
dt_utc(2017, 11, 14, 22, 43, 00), dt_utc(2017, 11, 14, 22, 44, 00),
dt_utc(2017, 11, 14, 22, 58, 00)], ],
"open_rate": [0.002543, 0.003003, 0.003089, 0.003214], "close_date": [
"close_rate": [0.002546, 0.003014, 0.0032903, 0.003217], dt_utc(2017, 11, 14, 21, 35, 00),
"trade_duration": [123, 34, 31, 14], dt_utc(2017, 11, 14, 22, 10, 00),
"is_open": [False, False, False, True], dt_utc(2017, 11, 14, 22, 43, 00),
"is_short": [False, False, False, False], dt_utc(2017, 11, 14, 22, 58, 00),
"stake_amount": [0.01, 0.01, 0.01, 0.01], ],
"exit_reason": [ExitType.ROI, ExitType.ROI, "open_rate": [0.002543, 0.003003, 0.003089, 0.003214],
ExitType.STOP_LOSS, ExitType.FORCE_EXIT] "close_rate": [0.002546, 0.003014, 0.0032903, 0.003217],
}), "trade_duration": [123, 34, 31, 14],
'config': default_conf, "is_open": [False, False, False, True],
'locks': [], "is_short": [False, False, False, False],
'final_balance': 1000.02, "stake_amount": [0.01, 0.01, 0.01, 0.01],
'rejected_signals': 20, "exit_reason": [
'timedout_entry_orders': 0, ExitType.ROI,
'timedout_exit_orders': 0, ExitType.ROI,
'canceled_trade_entries': 0, ExitType.STOP_LOSS,
'canceled_entry_orders': 0, ExitType.FORCE_EXIT,
'replaced_entry_orders': 0, ],
'backtest_start_time': dt_ts() // 1000, }
'backtest_end_time': dt_ts() // 1000, ),
'run_id': '124', "config": default_conf,
"locks": [],
"final_balance": 1000.02,
"rejected_signals": 20,
"timedout_entry_orders": 0,
"timedout_exit_orders": 0,
"canceled_trade_entries": 0,
"canceled_entry_orders": 0,
"replaced_entry_orders": 0,
"backtest_start_time": dt_ts() // 1000,
"backtest_end_time": dt_ts() // 1000,
"run_id": "124",
} }
} }
stats = generate_backtest_stats(btdata, results, min_date, max_date) stats = generate_backtest_stats(btdata, results, min_date, max_date)
assert isinstance(stats, dict) assert isinstance(stats, dict)
assert 'strategy' in stats assert "strategy" in stats
assert 'DefStrat' in stats['strategy'] assert "DefStrat" in stats["strategy"]
assert 'strategy_comparison' in stats assert "strategy_comparison" in stats
strat_stats = stats['strategy']['DefStrat'] strat_stats = stats["strategy"]["DefStrat"]
assert pytest.approx(strat_stats['max_drawdown_account']) == 1.399999e-08 assert pytest.approx(strat_stats["max_drawdown_account"]) == 1.399999e-08
assert strat_stats['drawdown_start'] == '2017-11-14 22:10:00' assert strat_stats["drawdown_start"] == "2017-11-14 22:10:00"
assert strat_stats['drawdown_end'] == '2017-11-14 22:43:00' assert strat_stats["drawdown_end"] == "2017-11-14 22:43:00"
assert strat_stats['drawdown_end_ts'] == 1510699380000 assert strat_stats["drawdown_end_ts"] == 1510699380000
assert strat_stats['drawdown_start_ts'] == 1510697400000 assert strat_stats["drawdown_start_ts"] == 1510697400000
assert strat_stats['pairlist'] == ['UNITTEST/BTC'] assert strat_stats["pairlist"] == ["UNITTEST/BTC"]
# Test storing stats # Test storing stats
filename = tmp_path / 'btresult.json' filename = tmp_path / "btresult.json"
filename_last = tmp_path / LAST_BT_RESULT_FN filename_last = tmp_path / LAST_BT_RESULT_FN
_backup_file(filename_last, copy_file=True) _backup_file(filename_last, copy_file=True)
assert not filename.is_file() assert not filename.is_file()
store_backtest_stats(filename, stats, '2022_01_01_15_05_13') store_backtest_stats(filename, stats, "2022_01_01_15_05_13")
# get real Filename (it's btresult-<date>.json) # get real Filename (it's btresult-<date>.json)
last_fn = get_latest_backtest_filename(filename_last.parent) last_fn = get_latest_backtest_filename(filename_last.parent)
@@ -211,9 +234,9 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmp_path):
filename1 = tmp_path / last_fn filename1 = tmp_path / last_fn
assert filename1.is_file() assert filename1.is_file()
content = filename1.read_text() content = filename1.read_text()
assert 'max_drawdown_account' in content assert "max_drawdown_account" in content
assert 'strategy' in content assert "strategy" in content
assert 'pairlist' in content assert "pairlist" in content
assert filename_last.is_file() assert filename_last.is_file()
@@ -222,149 +245,146 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmp_path):
def test_store_backtest_stats(testdatadir, mocker): def test_store_backtest_stats(testdatadir, mocker):
dump_mock = mocker.patch("freqtrade.optimize.optimize_reports.bt_storage.file_dump_json")
dump_mock = mocker.patch('freqtrade.optimize.optimize_reports.bt_storage.file_dump_json') data = {"metadata": {}, "strategy": {}, "strategy_comparison": []}
store_backtest_stats(testdatadir, data, "2022_01_01_15_05_13")
data = {'metadata': {}, 'strategy': {}, 'strategy_comparison': []}
store_backtest_stats(testdatadir, data, '2022_01_01_15_05_13')
assert dump_mock.call_count == 3 assert dump_mock.call_count == 3
assert isinstance(dump_mock.call_args_list[0][0][0], Path) assert isinstance(dump_mock.call_args_list[0][0][0], Path)
assert str(dump_mock.call_args_list[0][0][0]).startswith(str(testdatadir / 'backtest-result')) assert str(dump_mock.call_args_list[0][0][0]).startswith(str(testdatadir / "backtest-result"))
dump_mock.reset_mock() dump_mock.reset_mock()
filename = testdatadir / 'testresult.json' filename = testdatadir / "testresult.json"
store_backtest_stats(filename, data, '2022_01_01_15_05_13') store_backtest_stats(filename, data, "2022_01_01_15_05_13")
assert dump_mock.call_count == 3 assert dump_mock.call_count == 3
assert isinstance(dump_mock.call_args_list[0][0][0], Path) assert isinstance(dump_mock.call_args_list[0][0][0], Path)
# result will be testdatadir / testresult-<timestamp>.json # result will be testdatadir / testresult-<timestamp>.json
assert str(dump_mock.call_args_list[0][0][0]).startswith(str(testdatadir / 'testresult')) assert str(dump_mock.call_args_list[0][0][0]).startswith(str(testdatadir / "testresult"))
def test_store_backtest_stats_real(tmp_path): def test_store_backtest_stats_real(tmp_path):
data = {'metadata': {}, 'strategy': {}, 'strategy_comparison': []} data = {"metadata": {}, "strategy": {}, "strategy_comparison": []}
store_backtest_stats(tmp_path, data, '2022_01_01_15_05_13') store_backtest_stats(tmp_path, data, "2022_01_01_15_05_13")
assert (tmp_path / 'backtest-result-2022_01_01_15_05_13.json').is_file() assert (tmp_path / "backtest-result-2022_01_01_15_05_13.json").is_file()
assert (tmp_path / 'backtest-result-2022_01_01_15_05_13.meta.json').is_file() assert (tmp_path / "backtest-result-2022_01_01_15_05_13.meta.json").is_file()
assert not (tmp_path / 'backtest-result-2022_01_01_15_05_13_market_change.feather').is_file() assert not (tmp_path / "backtest-result-2022_01_01_15_05_13_market_change.feather").is_file()
assert (tmp_path / LAST_BT_RESULT_FN).is_file() assert (tmp_path / LAST_BT_RESULT_FN).is_file()
fn = get_latest_backtest_filename(tmp_path) fn = get_latest_backtest_filename(tmp_path)
assert fn == 'backtest-result-2022_01_01_15_05_13.json' assert fn == "backtest-result-2022_01_01_15_05_13.json"
store_backtest_stats(tmp_path, data, '2024_01_01_15_05_25', market_change_data=pd.DataFrame()) store_backtest_stats(tmp_path, data, "2024_01_01_15_05_25", market_change_data=pd.DataFrame())
assert (tmp_path / 'backtest-result-2024_01_01_15_05_25.json').is_file() assert (tmp_path / "backtest-result-2024_01_01_15_05_25.json").is_file()
assert (tmp_path / 'backtest-result-2024_01_01_15_05_25.meta.json').is_file() assert (tmp_path / "backtest-result-2024_01_01_15_05_25.meta.json").is_file()
assert (tmp_path / 'backtest-result-2024_01_01_15_05_25_market_change.feather').is_file() assert (tmp_path / "backtest-result-2024_01_01_15_05_25_market_change.feather").is_file()
assert (tmp_path / LAST_BT_RESULT_FN).is_file() assert (tmp_path / LAST_BT_RESULT_FN).is_file()
# Last file reference should be updated # Last file reference should be updated
fn = get_latest_backtest_filename(tmp_path) fn = get_latest_backtest_filename(tmp_path)
assert fn == 'backtest-result-2024_01_01_15_05_25.json' assert fn == "backtest-result-2024_01_01_15_05_25.json"
def test_store_backtest_candles(testdatadir, mocker): def test_store_backtest_candles(testdatadir, mocker):
dump_mock = mocker.patch("freqtrade.optimize.optimize_reports.bt_storage.file_dump_joblib")
dump_mock = mocker.patch( candle_dict = {"DefStrat": {"UNITTEST/BTC": pd.DataFrame()}}
'freqtrade.optimize.optimize_reports.bt_storage.file_dump_joblib')
candle_dict = {'DefStrat': {'UNITTEST/BTC': pd.DataFrame()}}
# mock directory exporting # mock directory exporting
store_backtest_analysis_results(testdatadir, candle_dict, {}, '2022_01_01_15_05_13') store_backtest_analysis_results(testdatadir, candle_dict, {}, "2022_01_01_15_05_13")
assert dump_mock.call_count == 2 assert dump_mock.call_count == 2
assert isinstance(dump_mock.call_args_list[0][0][0], Path) assert isinstance(dump_mock.call_args_list[0][0][0], Path)
assert str(dump_mock.call_args_list[0][0][0]).endswith('_signals.pkl') assert str(dump_mock.call_args_list[0][0][0]).endswith("_signals.pkl")
dump_mock.reset_mock() dump_mock.reset_mock()
# mock file exporting # mock file exporting
filename = Path(testdatadir / 'testresult') filename = Path(testdatadir / "testresult")
store_backtest_analysis_results(filename, candle_dict, {}, '2022_01_01_15_05_13') store_backtest_analysis_results(filename, candle_dict, {}, "2022_01_01_15_05_13")
assert dump_mock.call_count == 2 assert dump_mock.call_count == 2
assert isinstance(dump_mock.call_args_list[0][0][0], Path) assert isinstance(dump_mock.call_args_list[0][0][0], Path)
# result will be testdatadir / testresult-<timestamp>_signals.pkl # result will be testdatadir / testresult-<timestamp>_signals.pkl
assert str(dump_mock.call_args_list[0][0][0]).endswith('_signals.pkl') assert str(dump_mock.call_args_list[0][0][0]).endswith("_signals.pkl")
dump_mock.reset_mock() dump_mock.reset_mock()
def test_write_read_backtest_candles(tmp_path): def test_write_read_backtest_candles(tmp_path):
candle_dict = {"DefStrat": {"UNITTEST/BTC": pd.DataFrame()}}
candle_dict = {'DefStrat': {'UNITTEST/BTC': pd.DataFrame()}}
# test directory exporting # test directory exporting
sample_date = '2022_01_01_15_05_13' sample_date = "2022_01_01_15_05_13"
store_backtest_analysis_results(tmp_path, candle_dict, {}, sample_date) store_backtest_analysis_results(tmp_path, candle_dict, {}, sample_date)
stored_file = tmp_path / f'backtest-result-{sample_date}_signals.pkl' stored_file = tmp_path / f"backtest-result-{sample_date}_signals.pkl"
with stored_file.open("rb") as scp: with stored_file.open("rb") as scp:
pickled_signal_candles = joblib.load(scp) pickled_signal_candles = joblib.load(scp)
assert pickled_signal_candles.keys() == candle_dict.keys() assert pickled_signal_candles.keys() == candle_dict.keys()
assert pickled_signal_candles['DefStrat'].keys() == pickled_signal_candles['DefStrat'].keys() assert pickled_signal_candles["DefStrat"].keys() == pickled_signal_candles["DefStrat"].keys()
assert pickled_signal_candles['DefStrat']['UNITTEST/BTC'] \ assert pickled_signal_candles["DefStrat"]["UNITTEST/BTC"].equals(
.equals(pickled_signal_candles['DefStrat']['UNITTEST/BTC']) pickled_signal_candles["DefStrat"]["UNITTEST/BTC"]
)
_clean_test_file(stored_file) _clean_test_file(stored_file)
# test file exporting # test file exporting
filename = tmp_path / 'testresult' filename = tmp_path / "testresult"
store_backtest_analysis_results(filename, candle_dict, {}, sample_date) store_backtest_analysis_results(filename, candle_dict, {}, sample_date)
stored_file = tmp_path / f'testresult-{sample_date}_signals.pkl' stored_file = tmp_path / f"testresult-{sample_date}_signals.pkl"
with stored_file.open("rb") as scp: with stored_file.open("rb") as scp:
pickled_signal_candles = joblib.load(scp) pickled_signal_candles = joblib.load(scp)
assert pickled_signal_candles.keys() == candle_dict.keys() assert pickled_signal_candles.keys() == candle_dict.keys()
assert pickled_signal_candles['DefStrat'].keys() == pickled_signal_candles['DefStrat'].keys() assert pickled_signal_candles["DefStrat"].keys() == pickled_signal_candles["DefStrat"].keys()
assert pickled_signal_candles['DefStrat']['UNITTEST/BTC'] \ assert pickled_signal_candles["DefStrat"]["UNITTEST/BTC"].equals(
.equals(pickled_signal_candles['DefStrat']['UNITTEST/BTC']) pickled_signal_candles["DefStrat"]["UNITTEST/BTC"]
)
_clean_test_file(stored_file) _clean_test_file(stored_file)
def test_generate_pair_metrics(): def test_generate_pair_metrics():
results = pd.DataFrame( results = pd.DataFrame(
{ {
'pair': ['ETH/BTC', 'ETH/BTC'], "pair": ["ETH/BTC", "ETH/BTC"],
'profit_ratio': [0.1, 0.2], "profit_ratio": [0.1, 0.2],
'profit_abs': [0.2, 0.4], "profit_abs": [0.2, 0.4],
'trade_duration': [10, 30], "trade_duration": [10, 30],
'wins': [2, 0], "wins": [2, 0],
'draws': [0, 0], "draws": [0, 0],
'losses': [0, 0] "losses": [0, 0],
} }
) )
pair_results = generate_pair_metrics(['ETH/BTC'], stake_currency='BTC', pair_results = generate_pair_metrics(
starting_balance=2, results=results) ["ETH/BTC"], stake_currency="BTC", starting_balance=2, results=results
)
assert isinstance(pair_results, list) assert isinstance(pair_results, list)
assert len(pair_results) == 2 assert len(pair_results) == 2
assert pair_results[-1]['key'] == 'TOTAL' assert pair_results[-1]["key"] == "TOTAL"
assert ( assert (
pytest.approx(pair_results[-1]['profit_mean_pct']) == pair_results[-1]['profit_mean'] * 100) pytest.approx(pair_results[-1]["profit_mean_pct"]) == pair_results[-1]["profit_mean"] * 100
assert ( )
pytest.approx(pair_results[-1]['profit_sum_pct']) == pair_results[-1]['profit_sum'] * 100) assert pytest.approx(pair_results[-1]["profit_sum_pct"]) == pair_results[-1]["profit_sum"] * 100
def test_generate_daily_stats(testdatadir): def test_generate_daily_stats(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json" filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename) bt_data = load_backtest_data(filename)
res = generate_daily_stats(bt_data) res = generate_daily_stats(bt_data)
assert isinstance(res, dict) assert isinstance(res, dict)
assert round(res['backtest_best_day'], 4) == 0.1796 assert round(res["backtest_best_day"], 4) == 0.1796
assert round(res['backtest_worst_day'], 4) == -0.1468 assert round(res["backtest_worst_day"], 4) == -0.1468
assert res['winning_days'] == 19 assert res["winning_days"] == 19
assert res['draw_days'] == 0 assert res["draw_days"] == 0
assert res['losing_days'] == 2 assert res["losing_days"] == 2
# Select empty dataframe! # Select empty dataframe!
res = generate_daily_stats(bt_data.loc[bt_data['open_date'] == '2000-01-01', :]) res = generate_daily_stats(bt_data.loc[bt_data["open_date"] == "2000-01-01", :])
assert isinstance(res, dict) assert isinstance(res, dict)
assert round(res['backtest_best_day'], 4) == 0.0 assert round(res["backtest_best_day"], 4) == 0.0
assert res['winning_days'] == 0 assert res["winning_days"] == 0
assert res['draw_days'] == 0 assert res["draw_days"] == 0
assert res['losing_days'] == 0 assert res["losing_days"] == 0
def test_generate_trading_stats(testdatadir): def test_generate_trading_stats(testdatadir):
@@ -372,22 +392,24 @@ def test_generate_trading_stats(testdatadir):
bt_data = load_backtest_data(filename) bt_data = load_backtest_data(filename)
res = generate_trading_stats(bt_data) res = generate_trading_stats(bt_data)
assert isinstance(res, dict) assert isinstance(res, dict)
assert res['winner_holding_avg'] == timedelta(seconds=1440) assert res["winner_holding_avg"] == timedelta(seconds=1440)
assert res['loser_holding_avg'] == timedelta(days=1, seconds=21420) assert res["loser_holding_avg"] == timedelta(days=1, seconds=21420)
assert 'wins' in res assert "wins" in res
assert 'losses' in res assert "losses" in res
assert 'draws' in res assert "draws" in res
# Select empty dataframe! # Select empty dataframe!
res = generate_trading_stats(bt_data.loc[bt_data['open_date'] == '2000-01-01', :]) res = generate_trading_stats(bt_data.loc[bt_data["open_date"] == "2000-01-01", :])
assert res['wins'] == 0 assert res["wins"] == 0
assert res['losses'] == 0 assert res["losses"] == 0
def test_calc_streak(testdatadir): def test_calc_streak(testdatadir):
df = pd.DataFrame({ df = pd.DataFrame(
'profit_ratio': [0.05, -0.02, -0.03, -0.05, 0.01, 0.02, 0.03, 0.04, -0.02, -0.03], {
}) "profit_ratio": [0.05, -0.02, -0.03, -0.05, 0.01, 0.02, 0.03, 0.04, -0.02, -0.03],
}
)
# 4 consecutive wins, 3 consecutive losses # 4 consecutive wins, 3 consecutive losses
res = calc_streak(df) res = calc_streak(df)
assert res == (4, 3) assert res == (4, 3)
@@ -396,12 +418,14 @@ def test_calc_streak(testdatadir):
# invert situation # invert situation
df1 = df.copy() df1 = df.copy()
df1['profit_ratio'] = df1['profit_ratio'] * -1 df1["profit_ratio"] = df1["profit_ratio"] * -1
assert calc_streak(df1) == (3, 4) assert calc_streak(df1) == (3, 4)
df_empty = pd.DataFrame({ df_empty = pd.DataFrame(
'profit_ratio': [], {
}) "profit_ratio": [],
}
)
assert df_empty.empty assert df_empty.empty
assert calc_streak(df_empty) == (0, 0) assert calc_streak(df_empty) == (0, 0)
@@ -411,131 +435,133 @@ def test_calc_streak(testdatadir):
def test_text_table_exit_reason(): def test_text_table_exit_reason():
results = pd.DataFrame( results = pd.DataFrame(
{ {
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], "pair": ["ETH/BTC", "ETH/BTC", "ETH/BTC"],
'profit_ratio': [0.1, 0.2, -0.1], "profit_ratio": [0.1, 0.2, -0.1],
'profit_abs': [0.2, 0.4, -0.2], "profit_abs": [0.2, 0.4, -0.2],
'trade_duration': [10, 30, 10], "trade_duration": [10, 30, 10],
'wins': [2, 0, 0], "wins": [2, 0, 0],
'draws': [0, 0, 0], "draws": [0, 0, 0],
'losses': [0, 0, 1], "losses": [0, 0, 1],
'exit_reason': [ExitType.ROI, ExitType.ROI, ExitType.STOP_LOSS] "exit_reason": [ExitType.ROI, ExitType.ROI, ExitType.STOP_LOSS],
} }
) )
result_str = ( result_str = (
'| Exit Reason | Exits | Avg Profit % | Tot Profit BTC | Tot Profit % |' "| Exit Reason | Exits | Avg Profit % | Tot Profit BTC | Tot Profit % |"
' Avg Duration | Win Draw Loss Win% |\n' " Avg Duration | Win Draw Loss Win% |\n"
'|---------------+---------+----------------+------------------+----------------+' "|---------------+---------+----------------+------------------+----------------+"
'----------------+-------------------------|\n' "----------------+-------------------------|\n"
'| roi | 2 | 15.00 | 0.60000000 | 2.73 |' "| roi | 2 | 15.00 | 0.60000000 | 2.73 |"
' 0:20:00 | 2 0 0 100 |\n' " 0:20:00 | 2 0 0 100 |\n"
'| stop_loss | 1 | -10.00 | -0.20000000 | -0.91 |' "| stop_loss | 1 | -10.00 | -0.20000000 | -0.91 |"
' 0:10:00 | 0 0 1 0 |\n' " 0:10:00 | 0 0 1 0 |\n"
'| TOTAL | 3 | 6.67 | 0.40000000 | 1.82 |' "| TOTAL | 3 | 6.67 | 0.40000000 | 1.82 |"
' 0:17:00 | 2 0 1 66.7 |' " 0:17:00 | 2 0 1 66.7 |"
) )
exit_reason_stats = generate_tag_metrics('exit_reason', starting_balance=22, exit_reason_stats = generate_tag_metrics(
results=results, skip_nan=False) "exit_reason", starting_balance=22, results=results, skip_nan=False
assert text_table_tags('exit_tag', exit_reason_stats, 'BTC') == result_str )
assert text_table_tags("exit_tag", exit_reason_stats, "BTC") == result_str
def test_generate_sell_reason_stats(): def test_generate_sell_reason_stats():
results = pd.DataFrame( results = pd.DataFrame(
{ {
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], "pair": ["ETH/BTC", "ETH/BTC", "ETH/BTC"],
'profit_ratio': [0.1, 0.2, -0.1], "profit_ratio": [0.1, 0.2, -0.1],
'profit_abs': [0.2, 0.4, -0.2], "profit_abs": [0.2, 0.4, -0.2],
'trade_duration': [10, 30, 10], "trade_duration": [10, 30, 10],
'wins': [2, 0, 0], "wins": [2, 0, 0],
'draws': [0, 0, 0], "draws": [0, 0, 0],
'losses': [0, 0, 1], "losses": [0, 0, 1],
'exit_reason': [ExitType.ROI.value, ExitType.ROI.value, ExitType.STOP_LOSS.value] "exit_reason": [ExitType.ROI.value, ExitType.ROI.value, ExitType.STOP_LOSS.value],
} }
) )
exit_reason_stats = generate_tag_metrics('exit_reason', starting_balance=22, exit_reason_stats = generate_tag_metrics(
results=results, skip_nan=False) "exit_reason", starting_balance=22, results=results, skip_nan=False
)
roi_result = exit_reason_stats[0] roi_result = exit_reason_stats[0]
assert roi_result['key'] == 'roi' assert roi_result["key"] == "roi"
assert roi_result['trades'] == 2 assert roi_result["trades"] == 2
assert pytest.approx(roi_result['profit_mean']) == 0.15 assert pytest.approx(roi_result["profit_mean"]) == 0.15
assert roi_result['profit_mean_pct'] == round(roi_result['profit_mean'] * 100, 2) assert roi_result["profit_mean_pct"] == round(roi_result["profit_mean"] * 100, 2)
assert pytest.approx(roi_result['profit_mean']) == 0.15 assert pytest.approx(roi_result["profit_mean"]) == 0.15
assert roi_result['profit_mean_pct'] == round(roi_result['profit_mean'] * 100, 2) assert roi_result["profit_mean_pct"] == round(roi_result["profit_mean"] * 100, 2)
stop_result = exit_reason_stats[1] stop_result = exit_reason_stats[1]
assert stop_result['key'] == 'stop_loss' assert stop_result["key"] == "stop_loss"
assert stop_result['trades'] == 1 assert stop_result["trades"] == 1
assert pytest.approx(stop_result['profit_mean']) == -0.1 assert pytest.approx(stop_result["profit_mean"]) == -0.1
assert stop_result['profit_mean_pct'] == round(stop_result['profit_mean'] * 100, 2) assert stop_result["profit_mean_pct"] == round(stop_result["profit_mean"] * 100, 2)
assert pytest.approx(stop_result['profit_mean']) == -0.1 assert pytest.approx(stop_result["profit_mean"]) == -0.1
assert stop_result['profit_mean_pct'] == round(stop_result['profit_mean'] * 100, 2) assert stop_result["profit_mean_pct"] == round(stop_result["profit_mean"] * 100, 2)
def test_text_table_strategy(testdatadir): def test_text_table_strategy(testdatadir):
filename = testdatadir / "backtest_results/backtest-result_multistrat.json" filename = testdatadir / "backtest_results/backtest-result_multistrat.json"
bt_res_data = load_backtest_stats(filename) bt_res_data = load_backtest_stats(filename)
bt_res_data_comparison = bt_res_data.pop('strategy_comparison') bt_res_data_comparison = bt_res_data.pop("strategy_comparison")
result_str = ( result_str = (
'| Strategy | Entries | Avg Profit % | Tot Profit BTC |' "| Strategy | Entries | Avg Profit % | Tot Profit BTC |"
' Tot Profit % | Avg Duration | Win Draw Loss Win% | Drawdown |\n' " Tot Profit % | Avg Duration | Win Draw Loss Win% | Drawdown |\n"
'|----------------+-----------+----------------+------------------+' "|----------------+-----------+----------------+------------------+"
'----------------+----------------+-------------------------+-----------------------|\n' "----------------+----------------+-------------------------+-----------------------|\n"
'| StrategyTestV2 | 179 | 0.08 | 0.02608550 |' "| StrategyTestV2 | 179 | 0.08 | 0.02608550 |"
' 260.85 | 3:40:00 | 170 0 9 95.0 | 0.00308222 BTC 8.67% |\n' " 260.85 | 3:40:00 | 170 0 9 95.0 | 0.00308222 BTC 8.67% |\n"
'| TestStrategy | 179 | 0.08 | 0.02608550 |' "| TestStrategy | 179 | 0.08 | 0.02608550 |"
' 260.85 | 3:40:00 | 170 0 9 95.0 | 0.00308222 BTC 8.67% |' " 260.85 | 3:40:00 | 170 0 9 95.0 | 0.00308222 BTC 8.67% |"
) )
strategy_results = generate_strategy_comparison(bt_stats=bt_res_data['strategy']) strategy_results = generate_strategy_comparison(bt_stats=bt_res_data["strategy"])
assert strategy_results == bt_res_data_comparison assert strategy_results == bt_res_data_comparison
assert text_table_strategy(strategy_results, 'BTC') == result_str assert text_table_strategy(strategy_results, "BTC") == result_str
def test_generate_edge_table(): def test_generate_edge_table():
results = {} results = {}
results['ETH/BTC'] = PairInfo(-0.01, 0.60, 2, 1, 3, 10, 60) results["ETH/BTC"] = PairInfo(-0.01, 0.60, 2, 1, 3, 10, 60)
assert generate_edge_table(results).count('+') == 7 assert generate_edge_table(results).count("+") == 7
assert generate_edge_table(results).count('| ETH/BTC |') == 1 assert generate_edge_table(results).count("| ETH/BTC |") == 1
assert generate_edge_table(results).count( assert (
'| Risk Reward Ratio | Required Risk Reward | Expectancy |') == 1 generate_edge_table(results).count(
"| Risk Reward Ratio | Required Risk Reward | Expectancy |"
)
== 1
)
def test_generate_periodic_breakdown_stats(testdatadir): def test_generate_periodic_breakdown_stats(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json" filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename).to_dict(orient='records') bt_data = load_backtest_data(filename).to_dict(orient="records")
res = generate_periodic_breakdown_stats(bt_data, 'day') res = generate_periodic_breakdown_stats(bt_data, "day")
assert isinstance(res, list) assert isinstance(res, list)
assert len(res) == 21 assert len(res) == 21
day = res[0] day = res[0]
assert 'date' in day assert "date" in day
assert 'draws' in day assert "draws" in day
assert 'loses' in day assert "loses" in day
assert 'wins' in day assert "wins" in day
assert 'profit_abs' in day assert "profit_abs" in day
# Select empty dataframe! # Select empty dataframe!
res = generate_periodic_breakdown_stats([], 'day') res = generate_periodic_breakdown_stats([], "day")
assert res == [] assert res == []
def test__get_resample_from_period(): def test__get_resample_from_period():
assert _get_resample_from_period("day") == "1d"
assert _get_resample_from_period('day') == '1d' assert _get_resample_from_period("week") == "1W-MON"
assert _get_resample_from_period('week') == '1W-MON' assert _get_resample_from_period("month") == "1ME"
assert _get_resample_from_period('month') == '1ME'
with pytest.raises(ValueError, match=r"Period noooo is not supported."): with pytest.raises(ValueError, match=r"Period noooo is not supported."):
_get_resample_from_period('noooo') _get_resample_from_period("noooo")
for period in BACKTEST_BREAKDOWNS: for period in BACKTEST_BREAKDOWNS:
assert isinstance(_get_resample_from_period(period), str) assert isinstance(_get_resample_from_period(period), str)
@@ -544,11 +570,11 @@ def test__get_resample_from_period():
def test_show_sorted_pairlist(testdatadir, default_conf, capsys): def test_show_sorted_pairlist(testdatadir, default_conf, capsys):
filename = testdatadir / "backtest_results/backtest-result.json" filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_stats(filename) bt_data = load_backtest_stats(filename)
default_conf['backtest_show_pair_list'] = True default_conf["backtest_show_pair_list"] = True
show_sorted_pairlist(default_conf, bt_data) show_sorted_pairlist(default_conf, bt_data)
out, _err = capsys.readouterr() out, _err = capsys.readouterr()
assert 'Pairs for Strategy StrategyTestV3: \n[' in out assert "Pairs for Strategy StrategyTestV3: \n[" in out
assert 'TOTAL' not in out assert "TOTAL" not in out
assert '"ETH/BTC", // ' in out assert '"ETH/BTC", // ' in out
+66 -65
View File
@@ -20,7 +20,7 @@ from tests.conftest import (
@pytest.mark.parametrize("use_db", [True, False]) @pytest.mark.parametrize("use_db", [True, False])
def test_trade_custom_data(fee, use_db): def test_trade_custom_data(fee, use_db):
if not use_db: if not use_db:
disable_database_use('5m') disable_database_use("5m")
Trade.reset_trades() Trade.reset_trades()
CustomDataWrapper.reset_custom_data() CustomDataWrapper.reset_custom_data()
@@ -31,120 +31,121 @@ def test_trade_custom_data(fee, use_db):
trade1.id = 1 trade1.id = 1
assert trade1.get_all_custom_data() == [] assert trade1.get_all_custom_data() == []
trade1.set_custom_data('test_str', 'test_value') trade1.set_custom_data("test_str", "test_value")
trade1.set_custom_data('test_int', 1) trade1.set_custom_data("test_int", 1)
trade1.set_custom_data('test_float', 1.55) trade1.set_custom_data("test_float", 1.55)
trade1.set_custom_data('test_bool', True) trade1.set_custom_data("test_bool", True)
trade1.set_custom_data('test_dict', {'test': 'dict'}) trade1.set_custom_data("test_dict", {"test": "dict"})
assert len(trade1.get_all_custom_data()) == 5 assert len(trade1.get_all_custom_data()) == 5
assert trade1.get_custom_data('test_str') == 'test_value' assert trade1.get_custom_data("test_str") == "test_value"
trade1.set_custom_data('test_str', 'test_value_updated') trade1.set_custom_data("test_str", "test_value_updated")
assert trade1.get_custom_data('test_str') == 'test_value_updated' assert trade1.get_custom_data("test_str") == "test_value_updated"
assert trade1.get_custom_data('test_int') == 1 assert trade1.get_custom_data("test_int") == 1
assert isinstance(trade1.get_custom_data('test_int'), int) assert isinstance(trade1.get_custom_data("test_int"), int)
assert trade1.get_custom_data('test_float') == 1.55 assert trade1.get_custom_data("test_float") == 1.55
assert isinstance(trade1.get_custom_data('test_float'), float) assert isinstance(trade1.get_custom_data("test_float"), float)
assert trade1.get_custom_data('test_bool') is True assert trade1.get_custom_data("test_bool") is True
assert isinstance(trade1.get_custom_data('test_bool'), bool) assert isinstance(trade1.get_custom_data("test_bool"), bool)
assert trade1.get_custom_data('test_dict') == {'test': 'dict'} assert trade1.get_custom_data("test_dict") == {"test": "dict"}
assert isinstance(trade1.get_custom_data('test_dict'), dict) assert isinstance(trade1.get_custom_data("test_dict"), dict)
if not use_db: if not use_db:
enable_database_use() enable_database_use()
def test_trade_custom_data_strategy_compat(mocker, default_conf_usdt, fee): def test_trade_custom_data_strategy_compat(mocker, default_conf_usdt, fee):
mocker.patch(f"{EXMS}.get_rate", return_value=0.50)
mocker.patch(f'{EXMS}.get_rate', return_value=0.50) mocker.patch("freqtrade.freqtradebot.FreqtradeBot.get_real_amount", return_value=None)
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=None) default_conf_usdt["minimal_roi"] = {"0": 100}
default_conf_usdt["minimal_roi"] = {"0": 100}
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
create_mock_trades_usdt(fee) create_mock_trades_usdt(fee)
trade1 = Trade.get_trades_proxy(pair='ADA/USDT')[0] trade1 = Trade.get_trades_proxy(pair="ADA/USDT")[0]
trade1.set_custom_data('test_str', 'test_value') trade1.set_custom_data("test_str", "test_value")
trade1.set_custom_data('test_int', 1) trade1.set_custom_data("test_int", 1)
def custom_exit(pair, trade, **kwargs): def custom_exit(pair, trade, **kwargs):
if pair == "ADA/USDT":
if pair == 'ADA/USDT': custom_val = trade.get_custom_data("test_str")
custom_val = trade.get_custom_data('test_str') custom_val_i = trade.get_custom_data("test_int")
custom_val_i = trade.get_custom_data('test_int')
return f"{custom_val}_{custom_val_i}" return f"{custom_val}_{custom_val_i}"
freqtrade.strategy.custom_exit = custom_exit freqtrade.strategy.custom_exit = custom_exit
ff_spy = mocker.spy(freqtrade.strategy, 'custom_exit') ff_spy = mocker.spy(freqtrade.strategy, "custom_exit")
trades = Trade.get_open_trades() trades = Trade.get_open_trades()
freqtrade.exit_positions(trades) freqtrade.exit_positions(trades)
Trade.commit() Trade.commit()
trade_after = Trade.get_trades_proxy(pair='ADA/USDT')[0] trade_after = Trade.get_trades_proxy(pair="ADA/USDT")[0]
assert trade_after.get_custom_data('test_str') == 'test_value' assert trade_after.get_custom_data("test_str") == "test_value"
assert trade_after.get_custom_data('test_int') == 1 assert trade_after.get_custom_data("test_int") == 1
# 2 open pairs eligible for exit # 2 open pairs eligible for exit
assert ff_spy.call_count == 2 assert ff_spy.call_count == 2
assert trade_after.exit_reason == 'test_value_1' assert trade_after.exit_reason == "test_value_1"
def test_trade_custom_data_strategy_backtest_compat(mocker, default_conf_usdt, fee): def test_trade_custom_data_strategy_backtest_compat(mocker, default_conf_usdt, fee):
mocker.patch(f"{EXMS}.get_fee", fee)
mocker.patch(f'{EXMS}.get_fee', fee)
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=10) mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=10)
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float("inf"))
mocker.patch(f"{EXMS}.get_max_leverage", return_value=10) mocker.patch(f"{EXMS}.get_max_leverage", return_value=10)
mocker.patch(f"{EXMS}.get_maintenance_ratio_and_amt", return_value=(0.1, 0.1)) mocker.patch(f"{EXMS}.get_maintenance_ratio_and_amt", return_value=(0.1, 0.1))
mocker.patch('freqtrade.optimize.backtesting.Backtesting._run_funding_fees') mocker.patch("freqtrade.optimize.backtesting.Backtesting._run_funding_fees")
patch_exchange(mocker) patch_exchange(mocker)
default_conf_usdt.update({ default_conf_usdt.update(
"stake_amount": 100.0, {
"max_open_trades": 2, "stake_amount": 100.0,
"dry_run_wallet": 1000.0, "max_open_trades": 2,
"strategy": "StrategyTestV3", "dry_run_wallet": 1000.0,
"trading_mode": "futures", "strategy": "StrategyTestV3",
"margin_mode": "isolated", "trading_mode": "futures",
"stoploss": -2, "margin_mode": "isolated",
"minimal_roi": {"0": 100}, "stoploss": -2,
}) "minimal_roi": {"0": 100},
default_conf_usdt['pairlists'] = [{'method': 'StaticPairList', 'allow_inactive': True}] }
)
default_conf_usdt["pairlists"] = [{"method": "StaticPairList", "allow_inactive": True}]
backtesting = Backtesting(default_conf_usdt) backtesting = Backtesting(default_conf_usdt)
df = generate_test_data(default_conf_usdt['timeframe'], 100, '2022-01-01 00:00:00+00:00') df = generate_test_data(default_conf_usdt["timeframe"], 100, "2022-01-01 00:00:00+00:00")
pair_exp = 'XRP/USDT:USDT' pair_exp = "XRP/USDT:USDT"
def custom_exit(pair, trade, **kwargs): def custom_exit(pair, trade, **kwargs):
custom_val = trade.get_custom_data('test_str') custom_val = trade.get_custom_data("test_str")
custom_val_i = trade.get_custom_data('test_int', 0) custom_val_i = trade.get_custom_data("test_int", 0)
if pair == pair_exp: if pair == pair_exp:
trade.set_custom_data('test_str', 'test_value') trade.set_custom_data("test_str", "test_value")
trade.set_custom_data('test_int', custom_val_i + 1) trade.set_custom_data("test_int", custom_val_i + 1)
if custom_val_i >= 2: if custom_val_i >= 2:
return f"{custom_val}_{custom_val_i}" return f"{custom_val}_{custom_val_i}"
backtesting._set_strategy(backtesting.strategylist[0]) backtesting._set_strategy(backtesting.strategylist[0])
processed = backtesting.strategy.advise_all_indicators({ processed = backtesting.strategy.advise_all_indicators(
pair_exp: df, {
'BTC/USDT:USDT': df, pair_exp: df,
}) "BTC/USDT:USDT": df,
}
)
def fun(dataframe, *args, **kwargs): def fun(dataframe, *args, **kwargs):
dataframe.loc[dataframe.index == 50, 'enter_long'] = 1 dataframe.loc[dataframe.index == 50, "enter_long"] = 1
return dataframe return dataframe
backtesting.strategy.advise_entry = fun backtesting.strategy.advise_entry = fun
backtesting.strategy.leverage = MagicMock(return_value=1) backtesting.strategy.leverage = MagicMock(return_value=1)
backtesting.strategy.custom_exit = custom_exit backtesting.strategy.custom_exit = custom_exit
ff_spy = mocker.spy(backtesting.strategy, 'custom_exit') ff_spy = mocker.spy(backtesting.strategy, "custom_exit")
min_date, max_date = get_timerange(processed) min_date, max_date = get_timerange(processed)
@@ -153,13 +154,13 @@ def test_trade_custom_data_strategy_backtest_compat(mocker, default_conf_usdt, f
start_date=min_date, start_date=min_date,
end_date=max_date, end_date=max_date,
) )
results = result['results'] results = result["results"]
assert not results.empty assert not results.empty
assert len(results) == 2 assert len(results) == 2
assert results['pair'][0] == pair_exp assert results["pair"][0] == pair_exp
assert results['pair'][1] == 'BTC/USDT:USDT' assert results["pair"][1] == "BTC/USDT:USDT"
assert results['exit_reason'][0] == 'test_value_2' assert results["exit_reason"][0] == "test_value_2"
assert results['exit_reason'][1] == 'exit_signal' assert results["exit_reason"][1] == "exit_signal"
assert ff_spy.call_count == 7 assert ff_spy.call_count == 7
Backtesting.cleanup() Backtesting.cleanup()