conflict resolved => new backtest low and high params
This commit is contained in:
commit
6838ae0591
@ -206,21 +206,37 @@ class Backtesting(object):
|
||||
|
||||
buy_signal = sell_row.buy
|
||||
sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, buy_signal,
|
||||
sell_row.sell)
|
||||
sell_row.sell, low=sell_row.low, high=sell_row.high)
|
||||
if sell.sell_flag:
|
||||
|
||||
trade_dur = int((sell_row.date - buy_row.date).total_seconds() // 60)
|
||||
# Special handling if high or low hit STOP_LOSS or ROI
|
||||
if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
|
||||
# Set close_rate to stoploss
|
||||
closerate = trade.stop_loss
|
||||
elif sell.sell_type == (SellType.ROI):
|
||||
# get entry in min_roi >= to trade duration
|
||||
roi_entry = max(list(filter(lambda x: trade_dur >= x,
|
||||
self.strategy.minimal_roi.keys())))
|
||||
roi = self.strategy.minimal_roi[roi_entry]
|
||||
|
||||
# - (Expected abs profit + open_rate + open_fee) / (fee_close -1)
|
||||
closerate = - (trade.open_rate * roi + trade.open_rate *
|
||||
(1 + trade.fee_open)) / (trade.fee_close - 1)
|
||||
else:
|
||||
closerate = sell_row.open
|
||||
|
||||
return BacktestResult(pair=pair,
|
||||
profit_percent=trade.calc_profit_percent(rate=sell_row.open),
|
||||
profit_abs=trade.calc_profit(rate=sell_row.open),
|
||||
profit_percent=trade.calc_profit_percent(rate=closerate),
|
||||
profit_abs=trade.calc_profit(rate=closerate),
|
||||
open_time=buy_row.date,
|
||||
close_time=sell_row.date,
|
||||
trade_duration=int((
|
||||
sell_row.date - buy_row.date).total_seconds() // 60),
|
||||
trade_duration=trade_dur,
|
||||
open_index=buy_row.Index,
|
||||
close_index=sell_row.Index,
|
||||
open_at_end=False,
|
||||
open_rate=buy_row.open,
|
||||
close_rate=sell_row.open,
|
||||
close_rate=closerate,
|
||||
sell_reason=sell.sell_type
|
||||
)
|
||||
if partial_ticker:
|
||||
@ -260,7 +276,7 @@ class Backtesting(object):
|
||||
position_stacking: do we allow position stacking? (default: False)
|
||||
:return: DataFrame
|
||||
"""
|
||||
headers = ['date', 'buy', 'open', 'close', 'sell']
|
||||
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high']
|
||||
processed = args['processed']
|
||||
max_open_trades = args.get('max_open_trades', 0)
|
||||
position_stacking = args.get('position_stacking', False)
|
||||
|
@ -272,10 +272,10 @@ class Trade(_DECL_BASE):
|
||||
self,
|
||||
fee: Optional[float] = None) -> float:
|
||||
"""
|
||||
Calculate the open_rate in BTC
|
||||
Calculate the open_rate including fee.
|
||||
:param fee: fee to use on the open rate (optional).
|
||||
If rate is not set self.fee will be used
|
||||
:return: Price in BTC of the open trade
|
||||
:return: Price in of the open trade incl. Fees
|
||||
"""
|
||||
|
||||
buy_trade = (Decimal(self.amount) * Decimal(self.open_rate))
|
||||
@ -287,7 +287,7 @@ class Trade(_DECL_BASE):
|
||||
rate: Optional[float] = None,
|
||||
fee: Optional[float] = None) -> float:
|
||||
"""
|
||||
Calculate the close_rate in BTC
|
||||
Calculate the close_rate including fee
|
||||
:param fee: fee to use on the close rate (optional).
|
||||
If rate is not set self.fee will be used
|
||||
:param rate: rate to compare with (optional).
|
||||
@ -307,12 +307,12 @@ class Trade(_DECL_BASE):
|
||||
rate: Optional[float] = None,
|
||||
fee: Optional[float] = None) -> float:
|
||||
"""
|
||||
Calculate the profit in BTC between Close and Open trade
|
||||
Calculate the absolute profit in stake currency between Close and Open trade
|
||||
:param fee: fee to use on the close rate (optional).
|
||||
If rate is not set self.fee will be used
|
||||
:param rate: close rate to compare with (optional).
|
||||
If rate is not set self.close_rate will be used
|
||||
:return: profit in BTC as float
|
||||
:return: profit in stake currency as float
|
||||
"""
|
||||
open_trade_price = self.calc_open_trade_price()
|
||||
close_trade_price = self.calc_close_trade_price(
|
||||
|
@ -203,23 +203,24 @@ class IStrategy(ABC):
|
||||
return buy, sell
|
||||
|
||||
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool,
|
||||
sell: bool, force_stoploss: float=0) -> SellCheckTuple:
|
||||
sell: bool, low: float = None, high: float = None, force_stoploss: float = 0) -> SellCheckTuple:
|
||||
"""
|
||||
This function evaluate if on the condition required to trigger a sell has been reached
|
||||
if the threshold is reached and updates the trade record.
|
||||
:return: True if trade should be sold, False otherwise
|
||||
"""
|
||||
current_profit = trade.calc_profit_percent(rate)
|
||||
stoplossflag = self.stop_loss_reached(
|
||||
current_rate=rate,
|
||||
trade=trade,
|
||||
current_time=date,
|
||||
current_profit=current_profit,
|
||||
force_stoploss=force_stoploss)
|
||||
|
||||
# Set current rate to low for backtesting sell
|
||||
current_rate = low or rate
|
||||
current_profit = trade.calc_profit_percent(current_rate)
|
||||
stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade,
|
||||
current_time=date, current_profit=current_profit,
|
||||
force_stoploss=force_stoploss)
|
||||
if stoplossflag.sell_flag:
|
||||
return stoplossflag
|
||||
|
||||
# Set current rate to low for backtesting sell
|
||||
current_rate = high or rate
|
||||
current_profit = trade.calc_profit_percent(current_rate)
|
||||
experimental = self.config.get('experimental', {})
|
||||
|
||||
if buy and experimental.get('ignore_roi_if_buy_signal', False):
|
||||
|
@ -0,0 +1,45 @@
|
||||
from typing import NamedTuple, List
|
||||
|
||||
import arrow
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.strategy.interface import SellType
|
||||
|
||||
ticker_start_time = arrow.get(2018, 10, 3)
|
||||
ticker_interval_in_minute = 60
|
||||
|
||||
|
||||
class BTrade(NamedTuple):
|
||||
"""
|
||||
Minimalistic Trade result used for functional backtesting
|
||||
"""
|
||||
sell_reason: SellType
|
||||
open_tick: int
|
||||
close_tick: int
|
||||
|
||||
|
||||
class BTContainer(NamedTuple):
|
||||
"""
|
||||
Minimal BacktestContainer defining Backtest inputs and results.
|
||||
"""
|
||||
data: List[float]
|
||||
stop_loss: float
|
||||
roi: float
|
||||
trades: List[BTrade]
|
||||
profit_perc: float
|
||||
|
||||
|
||||
def _get_frame_time_from_offset(offset):
|
||||
return ticker_start_time.shift(
|
||||
minutes=(offset * ticker_interval_in_minute)).datetime
|
||||
|
||||
|
||||
def _build_backtest_dataframe(ticker_with_signals):
|
||||
columns = ['date', 'open', 'high', 'low', 'close', 'volume', 'buy', 'sell']
|
||||
|
||||
frame = DataFrame.from_records(ticker_with_signals, columns=columns)
|
||||
frame['date'] = frame['date'].apply(_get_frame_time_from_offset)
|
||||
# Ensure floats are in place
|
||||
for column in ['open', 'high', 'low', 'close', 'volume']:
|
||||
frame[column] = frame[column].astype('float64')
|
||||
return frame
|
188
freqtrade/tests/optimize/test_backtest_detail.py
Normal file
188
freqtrade/tests/optimize/test_backtest_detail.py
Normal file
@ -0,0 +1,188 @@
|
||||
# pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, unused-argument
|
||||
import logging
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from pandas import DataFrame
|
||||
import pytest
|
||||
|
||||
|
||||
from freqtrade.optimize.backtesting import Backtesting
|
||||
from freqtrade.strategy.interface import SellType
|
||||
from freqtrade.tests.optimize import (BTrade, BTContainer, _build_backtest_dataframe,
|
||||
_get_frame_time_from_offset)
|
||||
from freqtrade.tests.conftest import patch_exchange
|
||||
|
||||
|
||||
# Test 0 Minus 8% Close
|
||||
# Test with Stop-loss at 1%
|
||||
# TC1: Stop-Loss Triggered 1% loss
|
||||
tc0 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle)
|
||||
[2, 4987, 5012, 4600, 4600, 6172, 0, 0], # exit with stoploss hit
|
||||
[3, 4975, 5000, 4980, 4977, 6172, 0, 0],
|
||||
[4, 4977, 4987, 4977, 4995, 6172, 0, 0],
|
||||
[5, 4995, 4995, 4995, 4950, 6172, 0, 0]],
|
||||
stop_loss=-0.01, roi=1, profit_perc=-0.01,
|
||||
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)]
|
||||
)
|
||||
|
||||
|
||||
# Test 1 Minus 4% Low, minus 1% close
|
||||
# Test with Stop-Loss at 3%
|
||||
# TC2: Stop-Loss Triggered 3% Loss
|
||||
tc1 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle)
|
||||
[2, 4987, 5012, 4962, 4975, 6172, 0, 0],
|
||||
[3, 4975, 5000, 4800, 4962, 6172, 0, 0], # exit with stoploss hit
|
||||
[4, 4962, 4987, 4937, 4950, 6172, 0, 0],
|
||||
[5, 4950, 4975, 4925, 4950, 6172, 0, 0]],
|
||||
stop_loss=-0.03, roi=1, profit_perc=-0.03,
|
||||
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=3)]
|
||||
)
|
||||
|
||||
|
||||
# Test 3 Candle drops 4%, Recovers 1%.
|
||||
# Entry Criteria Met
|
||||
# Candle drops 20%
|
||||
# Candle Data for test 3
|
||||
# Test with Stop-Loss at 2%
|
||||
# TC3: Trade-A: Stop-Loss Triggered 2% Loss
|
||||
# Trade-B: Stop-Loss Triggered 2% Loss
|
||||
tc2 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle)
|
||||
[2, 4987, 5012, 4800, 4975, 6172, 0, 0], # exit with stoploss hit
|
||||
[3, 4975, 5000, 4950, 4962, 6172, 1, 0],
|
||||
[4, 4975, 5000, 4950, 4962, 6172, 0, 0], # enter trade 2 (signal on last candle)
|
||||
[5, 4962, 4987, 4000, 4000, 6172, 0, 0], # exit with stoploss hit
|
||||
[6, 4950, 4975, 4975, 4950, 6172, 0, 0]],
|
||||
stop_loss=-0.02, roi=1, profit_perc=-0.04,
|
||||
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2),
|
||||
BTrade(sell_reason=SellType.STOP_LOSS, open_tick=4, close_tick=5)]
|
||||
)
|
||||
|
||||
# Test 4 Minus 3% / recovery +15%
|
||||
# Candle Data for test 3 – Candle drops 3% Closed 15% up
|
||||
# Test with Stop-loss at 2% ROI 6%
|
||||
# TC4: Stop-Loss Triggered 2% Loss
|
||||
tc3 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle)
|
||||
[2, 4987, 5750, 4850, 5750, 6172, 0, 0], # Exit with stoploss hit
|
||||
[3, 4975, 5000, 4950, 4962, 6172, 0, 0],
|
||||
[4, 4962, 4987, 4937, 4950, 6172, 0, 0],
|
||||
[5, 4950, 4975, 4925, 4950, 6172, 0, 0]],
|
||||
stop_loss=-0.02, roi=0.06, profit_perc=-0.02,
|
||||
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)]
|
||||
)
|
||||
|
||||
# Test 4 / Drops 0.5% Closes +20%
|
||||
# Set stop-loss at 1% ROI 3%
|
||||
# TC5: ROI triggers 3% Gain
|
||||
tc4 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4980, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4980, 4987, 6172, 0, 0], # enter trade (signal on last candle)
|
||||
[2, 4987, 5025, 4975, 4987, 6172, 0, 0],
|
||||
[3, 4975, 6000, 4975, 6000, 6172, 0, 0], # ROI
|
||||
[4, 4962, 4987, 4972, 4950, 6172, 0, 0],
|
||||
[5, 4950, 4975, 4925, 4950, 6172, 0, 0]],
|
||||
stop_loss=-0.01, roi=0.03, profit_perc=0.03,
|
||||
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)]
|
||||
)
|
||||
|
||||
# Test 6 / Drops 3% / Recovers 6% Positive / Closes 1% positve
|
||||
# Candle Data for test 6
|
||||
# Set stop-loss at 2% ROI at 5%
|
||||
# TC6: Stop-Loss triggers 2% Loss
|
||||
tc5 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle)
|
||||
[2, 4987, 5300, 4850, 5050, 6172, 0, 0], # Exit with stoploss
|
||||
[3, 4975, 5000, 4950, 4962, 6172, 0, 0],
|
||||
[4, 4962, 4987, 4972, 4950, 6172, 0, 0],
|
||||
[5, 4950, 4975, 4925, 4950, 6172, 0, 0]],
|
||||
stop_loss=-0.02, roi=0.05, profit_perc=-0.02,
|
||||
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)]
|
||||
)
|
||||
|
||||
# Test 7 - 6% Positive / 1% Negative / Close 1% Positve
|
||||
# Candle Data for test 7
|
||||
# Set stop-loss at 2% ROI at 3%
|
||||
# TC7: ROI Triggers 3% Gain
|
||||
tc6 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 0],
|
||||
[2, 4987, 5300, 4950, 5050, 6172, 0, 0],
|
||||
[3, 4975, 5000, 4950, 4962, 6172, 0, 0],
|
||||
[4, 4962, 4987, 4972, 4950, 6172, 0, 0],
|
||||
[5, 4950, 4975, 4925, 4950, 6172, 0, 0]],
|
||||
stop_loss=-0.02, roi=0.03, profit_perc=0.03,
|
||||
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)]
|
||||
)
|
||||
|
||||
TESTS = [
|
||||
tc0,
|
||||
tc1,
|
||||
tc2,
|
||||
tc3,
|
||||
tc4,
|
||||
tc5,
|
||||
tc6,
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("data", TESTS)
|
||||
def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
|
||||
"""
|
||||
run functional tests
|
||||
"""
|
||||
default_conf["stoploss"] = data.stop_loss
|
||||
default_conf["minimal_roi"] = {"0": data.roi}
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.0))
|
||||
patch_exchange(mocker)
|
||||
frame = _build_backtest_dataframe(data.data)
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting.advise_buy = lambda a, m: frame
|
||||
backtesting.advise_sell = lambda a, m: frame
|
||||
caplog.set_level(logging.DEBUG)
|
||||
|
||||
pair = 'UNITTEST/BTC'
|
||||
# Dummy data as we mock the analyze functions
|
||||
data_processed = {pair: DataFrame()}
|
||||
results = backtesting.backtest(
|
||||
{
|
||||
'stake_amount': default_conf['stake_amount'],
|
||||
'processed': data_processed,
|
||||
'max_open_trades': 10,
|
||||
}
|
||||
)
|
||||
print(results.T)
|
||||
|
||||
assert len(results) == len(data.trades)
|
||||
assert round(results["profit_percent"].sum(), 3) == round(data.profit_perc, 3)
|
||||
# if data.sell_r == SellType.STOP_LOSS:
|
||||
# assert log_has("Stop loss hit.", caplog.record_tuples)
|
||||
# else:
|
||||
# assert not log_has("Stop loss hit.", caplog.record_tuples)
|
||||
# log_test = (f'Force_selling still open trade UNITTEST/BTC with '
|
||||
# f'{results.iloc[-1].profit_percent} perc - {results.iloc[-1].profit_abs}')
|
||||
# if data.sell_r == SellType.FORCE_SELL:
|
||||
# assert log_has(log_test,
|
||||
# caplog.record_tuples)
|
||||
# else:
|
||||
# assert not log_has(log_test,
|
||||
# caplog.record_tuples)
|
||||
for c, trade in enumerate(data.trades):
|
||||
res = results.iloc[c]
|
||||
assert res.sell_reason == trade.sell_reason
|
||||
assert res.open_time == _get_frame_time_from_offset(trade.open_tick)
|
||||
assert res.close_time == _get_frame_time_from_offset(trade.close_tick)
|
@ -518,18 +518,18 @@ def test_backtest(default_conf, fee, mocker) -> None:
|
||||
|
||||
expected = pd.DataFrame(
|
||||
{'pair': [pair, pair],
|
||||
'profit_percent': [0.00029977, 0.00056716],
|
||||
'profit_abs': [1.49e-06, 7.6e-07],
|
||||
'profit_percent': [0.0, 0.0],
|
||||
'profit_abs': [0.0, 0.0],
|
||||
'open_time': [Arrow(2018, 1, 29, 18, 40, 0).datetime,
|
||||
Arrow(2018, 1, 30, 3, 30, 0).datetime],
|
||||
'close_time': [Arrow(2018, 1, 29, 22, 40, 0).datetime,
|
||||
Arrow(2018, 1, 30, 4, 20, 0).datetime],
|
||||
'close_time': [Arrow(2018, 1, 29, 22, 35, 0).datetime,
|
||||
Arrow(2018, 1, 30, 4, 15, 0).datetime],
|
||||
'open_index': [77, 183],
|
||||
'close_index': [125, 193],
|
||||
'trade_duration': [240, 50],
|
||||
'close_index': [124, 192],
|
||||
'trade_duration': [235, 45],
|
||||
'open_at_end': [False, False],
|
||||
'open_rate': [0.104445, 0.10302485],
|
||||
'close_rate': [0.105, 0.10359999],
|
||||
'close_rate': [0.104969, 0.103541],
|
||||
'sell_reason': [SellType.ROI, SellType.ROI]
|
||||
})
|
||||
pd.testing.assert_frame_equal(results, expected)
|
||||
@ -539,9 +539,11 @@ def test_backtest(default_conf, fee, mocker) -> None:
|
||||
# Check open trade rate alignes to open rate
|
||||
assert ln is not None
|
||||
assert round(ln.iloc[0]["open"], 6) == round(t["open_rate"], 6)
|
||||
# check close trade rate alignes to close rate
|
||||
# check close trade rate alignes to close rate or is between high and low
|
||||
ln = data_pair.loc[data_pair["date"] == t["close_time"]]
|
||||
assert round(ln.iloc[0]["open"], 6) == round(t["close_rate"], 6)
|
||||
assert (round(ln.iloc[0]["open"], 6) == round(t["close_rate"], 6) or
|
||||
round(ln.iloc[0]["low"], 6) < round(
|
||||
t["close_rate"], 6) < round(ln.iloc[0]["high"], 6))
|
||||
|
||||
|
||||
def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
|
||||
@ -580,7 +582,7 @@ def test_processed(default_conf, mocker) -> None:
|
||||
|
||||
def test_backtest_pricecontours(default_conf, fee, mocker) -> None:
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
tests = [['raise', 18], ['lower', 0], ['sine', 16]]
|
||||
tests = [['raise', 18], ['lower', 0], ['sine', 19]]
|
||||
for [contour, numres] in tests:
|
||||
simple_backtest(default_conf, contour, numres, mocker)
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
ccxt==1.17.464
|
||||
ccxt==1.17.480
|
||||
SQLAlchemy==1.2.13
|
||||
python-telegram-bot==11.1.0
|
||||
arrow==0.12.1
|
||||
cachetools==2.1.0
|
||||
cachetools==3.0.0
|
||||
requests==2.20.0
|
||||
urllib3==1.24.1
|
||||
wrapt==1.10.11
|
||||
@ -10,9 +10,9 @@ pandas==0.23.4
|
||||
scikit-learn==0.20.0
|
||||
scipy==1.1.0
|
||||
jsonschema==2.6.0
|
||||
numpy==1.15.3
|
||||
numpy==1.15.4
|
||||
TA-Lib==0.4.17
|
||||
pytest==3.9.3
|
||||
pytest==3.10.0
|
||||
pytest-mock==1.10.0
|
||||
pytest-asyncio==0.9.0
|
||||
pytest-cov==2.6.0
|
||||
|
Loading…
Reference in New Issue
Block a user