From 184b5ca3fc5ad3d102be4846db52a5b1a469b1b2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Aug 2018 19:59:56 +0200 Subject: [PATCH 001/103] cleanup root dir and create build_helpers --- .../install_ta-lib.sh | 0 build_helpers/publish_docker.sh | 53 ++++++++++++++++++ .../ta-lib-0.4.0-src.tar.gz | Bin 3 files changed, 53 insertions(+) rename install_ta-lib.sh => build_helpers/install_ta-lib.sh (100%) create mode 100755 build_helpers/publish_docker.sh rename ta-lib-0.4.0-src.tar.gz => build_helpers/ta-lib-0.4.0-src.tar.gz (100%) diff --git a/install_ta-lib.sh b/build_helpers/install_ta-lib.sh similarity index 100% rename from install_ta-lib.sh rename to build_helpers/install_ta-lib.sh diff --git a/build_helpers/publish_docker.sh b/build_helpers/publish_docker.sh new file mode 100755 index 000000000..a398a8719 --- /dev/null +++ b/build_helpers/publish_docker.sh @@ -0,0 +1,53 @@ +#!/bin/sh +# Tag with travis build +TAG=$TRAVIS_BUILD_NUMBER +# - export TAG=`if [ "$TRAVIS_BRANCH" == "develop" ]; then echo "latest"; else echo $TRAVIS_BRANCH ; fi` +# Replace / with _ to create a valid tag +TAG=$(echo "${TRAVIS_BRANCH}" | sed -e "s/\//_/") + + +# Pull last build to avoid rebuilding the whole image +docker pull ${REPO}:${TAG} + +docker build --cache-from ${IMAGE_NAME}:${TAG} -t freqtrade:${TAG} . +if [ $? -ne 0 ]; then + echo "failed building image" + return 1 +fi + +# Run backtest +docker run --rm -it -v $(pwd)/config.json.example:/freqtrade/config.json:ro freqtrade:${TAG} --datadir freqtrade/tests/testdata backtesting + +if [ $? -ne 0 ]; then + echo "failed running backtest" + return 1 +fi + +# Tag image for upload +docker tag freqtrade:$TAG ${IMAGE_NAME}:$TAG +if [ $? -ne 0 ]; then + echo "failed tagging image" + return 1 +fi + +# Tag as latest for develop builds +if [ "${TRAVIS_BRANCH}" == "develop" ]; then + docker tag freqtrade:$TAG ${IMAGE_NAME}:latest +fi + +# Login +echo "$DOCKER_PASS" | docker login -u $DOCKER_USER --password-stdin + +if [ $? -ne 0 ]; then + echo "failed login" + return 1 +fi + +# Show all available images +docker images + +docker push ${IMAGE_NAME} +if [ $? -ne 0 ]; then + echo "failed pushing repo" + return 1 +fi diff --git a/ta-lib-0.4.0-src.tar.gz b/build_helpers/ta-lib-0.4.0-src.tar.gz similarity index 100% rename from ta-lib-0.4.0-src.tar.gz rename to build_helpers/ta-lib-0.4.0-src.tar.gz From 98738c482a7e52ecc9d7d55929e21b54ff2558c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Aug 2018 20:01:57 +0200 Subject: [PATCH 002/103] modify install-ta-lib script to support running in docker --- build_helpers/install_ta-lib.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/build_helpers/install_ta-lib.sh b/build_helpers/install_ta-lib.sh index d8ae2eeaa..4d4f37c17 100755 --- a/build_helpers/install_ta-lib.sh +++ b/build_helpers/install_ta-lib.sh @@ -1,7 +1,13 @@ if [ ! -f "ta-lib/CHANGELOG.TXT" ]; then tar zxvf ta-lib-0.4.0-src.tar.gz - cd ta-lib && sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h && ./configure && make && sudo make install && cd .. + cd ta-lib \ + && sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \ + && ./configure \ + && make \ + && which sudo && sudo make install || make install \ + && cd .. else echo "TA-lib already installed, skipping download and build." cd ta-lib && sudo make install && cd .. + fi From 907761f9941e73f931e3b8e97e8d1366539f90ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Aug 2018 20:03:20 +0200 Subject: [PATCH 003/103] Install ta-lib in Docker with script works for travis, works for Docker --- Dockerfile | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2506665ab..24cce0049 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,19 +1,20 @@ FROM python:3.7.0-slim-stretch -# Install TA-lib -RUN apt-get update && apt-get -y install curl build-essential && apt-get clean -RUN curl -L http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz | \ - tar xzvf - && \ - cd ta-lib && \ - sed -i "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h && \ - ./configure && make && make install && \ - cd .. && rm -rf ta-lib -ENV LD_LIBRARY_PATH /usr/local/lib +RUN apt-get update \ + && apt-get -y install curl build-essential \ + && apt-get clean \ + && pip install --upgrade pip # Prepare environment RUN mkdir /freqtrade WORKDIR /freqtrade +# Install TA-lib +COPY build_helpers/* /tmp/ +RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib* + +ENV LD_LIBRARY_PATH /usr/local/lib + # Install dependencies COPY requirements.txt /freqtrade/ RUN pip install numpy --no-cache-dir \ From af7283017b331f6c5f396b7757f7c84674796a3e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Aug 2018 20:04:25 +0200 Subject: [PATCH 004/103] modify travis to build and push docker * name steps * only build for master / develop and this branch (for now) --- .travis.yml | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 981eedcf8..f1192e80c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,15 @@ sudo: true os: - linux +dist: trusty language: python python: - 3.6 +services: + - docker +env: + global: + - IMAGE_NAME=freqtradeorg/freqtrade addons: apt: packages: @@ -11,24 +17,38 @@ addons: - libdw-dev - binutils-dev install: -- ./install_ta-lib.sh +- ./build_helpers/install_ta-lib.sh - export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH - pip install --upgrade flake8 coveralls pytest-random-order pytest-asyncio mypy - pip install -r requirements.txt - pip install -e . jobs: include: - - script: + - stage: tests + script: - pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/ - coveralls + name: pytest - script: - cp config.json.example config.json - python freqtrade/main.py --datadir freqtrade/tests/testdata backtesting + name: backtest - script: - cp config.json.example config.json - python freqtrade/main.py --datadir freqtrade/tests/testdata hyperopt -e 5 + name: hyperopt - script: flake8 freqtrade + name: flake8 - script: mypy freqtrade + name: mypy + + - stage: docker + if: branch in (master, develop, feat/improve_travis) AND (type in (push, cron)) + script: + - build_helpers/publish_docker.sh + name: "Build and test and push docker image" + + notifications: slack: secure: bKLXmOrx8e2aPZl7W8DA5BdPAXWGpI5UzST33oc1G/thegXcDVmHBTJrBs4sZak6bgAclQQrdZIsRd2eFYzHLalJEaw6pk7hoAw8SvLnZO0ZurWboz7qg2+aZZXfK4eKl/VUe4sM9M4e/qxjkK+yWG7Marg69c4v1ypF7ezUi1fPYILYw8u0paaiX0N5UX8XNlXy+PBlga2MxDjUY70MuajSZhPsY2pDUvYnMY1D/7XN3cFW0g+3O8zXjF0IF4q1Z/1ASQe+eYjKwPQacE+O8KDD+ZJYoTOFBAPllrtpO1jnOPFjNGf3JIbVMZw4bFjIL0mSQaiSUaUErbU3sFZ5Or79rF93XZ81V7uEZ55vD8KMfR2CB1cQJcZcj0v50BxLo0InkFqa0Y8Nra3sbpV4fV5Oe8pDmomPJrNFJnX6ULQhQ1gTCe0M5beKgVms5SITEpt4/Y0CmLUr6iHDT0CUiyMIRWAXdIgbGh1jfaWOMksybeRevlgDsIsNBjXmYI1Sw2ZZR2Eo2u4R6zyfyjOMLwYJ3vgq9IrACv2w5nmf0+oguMWHf6iWi2hiOqhlAN1W74+3HsYQcqnuM3LGOmuCnPprV1oGBqkPXjIFGpy21gNx4vHfO1noLUyJnMnlu2L7SSuN1CdLsnjJ1hVjpJjPfqB4nn8g12x87TqM1bOm+3Q= From 0535660db722342d11e3a7a2da4d3aca6880025c Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 Aug 2018 09:44:21 +0200 Subject: [PATCH 005/103] build technical image --- Dockerfile.technical | 6 ++++++ build_helpers/publish_docker.sh | 10 ++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 Dockerfile.technical diff --git a/Dockerfile.technical b/Dockerfile.technical new file mode 100644 index 000000000..5339eb232 --- /dev/null +++ b/Dockerfile.technical @@ -0,0 +1,6 @@ +FROM freqtradeorg/freqtrade:develop + +RUN apt-get update \ + && apt-get -y install git \ + && apt-get clean \ + && pip install git+https://github.com/berlinguyinca/technical diff --git a/build_helpers/publish_docker.sh b/build_helpers/publish_docker.sh index a398a8719..95aae0f4f 100755 --- a/build_helpers/publish_docker.sh +++ b/build_helpers/publish_docker.sh @@ -1,10 +1,8 @@ #!/bin/sh -# Tag with travis build -TAG=$TRAVIS_BUILD_NUMBER # - export TAG=`if [ "$TRAVIS_BRANCH" == "develop" ]; then echo "latest"; else echo $TRAVIS_BRANCH ; fi` # Replace / with _ to create a valid tag TAG=$(echo "${TRAVIS_BRANCH}" | sed -e "s/\//_/") - +TAG_TECH="${TAG}_technical" # Pull last build to avoid rebuilding the whole image docker pull ${REPO}:${TAG} @@ -23,6 +21,10 @@ if [ $? -ne 0 ]; then return 1 fi +# build technical image +sed -i Dockerfile.technical -e "s/FROM freqtradeorg\/freqtrade:develop/FROM freqtradeorg\/freqtrade:${TAG}/" +docker build --cache-from freqtrade:${TAG} -t ${IMAGE_NAME}:${TAG_TECH} -f Dockerfile.technical . + # Tag image for upload docker tag freqtrade:$TAG ${IMAGE_NAME}:$TAG if [ $? -ne 0 ]; then @@ -31,7 +33,7 @@ if [ $? -ne 0 ]; then fi # Tag as latest for develop builds -if [ "${TRAVIS_BRANCH}" == "develop" ]; then +if [ "${TRAVIS_BRANCH}" = "develop" ]; then docker tag freqtrade:$TAG ${IMAGE_NAME}:latest fi From 7301d76cff46c23a0e9b7c7aee04d83ca124bb27 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Oct 2018 13:20:51 +0200 Subject: [PATCH 006/103] Remove autobuild for technical as it's not versioned as it's not versioned and installed from github, we cannot guarantee which version is in the image. --- build_helpers/publish_docker.sh | 4 ---- 1 file changed, 4 deletions(-) diff --git a/build_helpers/publish_docker.sh b/build_helpers/publish_docker.sh index 95aae0f4f..5e0809fcf 100755 --- a/build_helpers/publish_docker.sh +++ b/build_helpers/publish_docker.sh @@ -21,10 +21,6 @@ if [ $? -ne 0 ]; then return 1 fi -# build technical image -sed -i Dockerfile.technical -e "s/FROM freqtradeorg\/freqtrade:develop/FROM freqtradeorg\/freqtrade:${TAG}/" -docker build --cache-from freqtrade:${TAG} -t ${IMAGE_NAME}:${TAG_TECH} -f Dockerfile.technical . - # Tag image for upload docker tag freqtrade:$TAG ${IMAGE_NAME}:$TAG if [ $? -ne 0 ]; then From 39efda19f4df040576ce55f33e60699a5e1dbe73 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Oct 2018 13:33:23 +0200 Subject: [PATCH 007/103] Add freqtradeorg/freqtrade docker images to the documentation --- docs/installation.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index 1000600e6..15cfb1467 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -109,7 +109,25 @@ Dry-Run touch tradesv3.dryrun.sqlite ``` -### 2. Build the Docker image +### 2. Download or build the docker image + +Either use the prebuilt image from docker hub - or build the image yourself if you would like more control on which version is used. + +Branches / tags available can be checked out on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/tags/). + +#### 2.1. Download the docker image + +Pull the image from docker hub and (optionally) change the name of the image + +```bash +docker pull freqtradeorg/freqtrade:develop +# Optionally tag the repository so the run-commands remain shorter +docker tag freqtradeorg/freqtrade:develop freqtrade +``` + +To update the image, simply run the above commands again and restart your running container. + +#### 2.2. Build the Docker image ```bash cd freqtrade From 530d521d78bc9305face56e2fb8308c6b58f328c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 21 Oct 2018 14:00:01 +0200 Subject: [PATCH 008/103] Rebuild complete image on "cron" events --- build_helpers/publish_docker.sh | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/build_helpers/publish_docker.sh b/build_helpers/publish_docker.sh index 5e0809fcf..c2b40ba81 100755 --- a/build_helpers/publish_docker.sh +++ b/build_helpers/publish_docker.sh @@ -2,12 +2,18 @@ # - export TAG=`if [ "$TRAVIS_BRANCH" == "develop" ]; then echo "latest"; else echo $TRAVIS_BRANCH ; fi` # Replace / with _ to create a valid tag TAG=$(echo "${TRAVIS_BRANCH}" | sed -e "s/\//_/") -TAG_TECH="${TAG}_technical" -# Pull last build to avoid rebuilding the whole image -docker pull ${REPO}:${TAG} -docker build --cache-from ${IMAGE_NAME}:${TAG} -t freqtrade:${TAG} . +if [ "${TRAVIS_EVENT_TYPE}" = "cron" ]; then + echo "event ${TRAVIS_EVENT_TYPE}: full rebuild - skipping cache" + docker build -t freqtrade:${TAG} . +else + echo "event ${TRAVIS_EVENT_TYPE}: building with cache" + # Pull last build to avoid rebuilding the whole image + docker pull ${REPO}:${TAG} + docker build --cache-from ${IMAGE_NAME}:${TAG} -t freqtrade:${TAG} . +fi + if [ $? -ne 0 ]; then echo "failed building image" return 1 @@ -29,7 +35,7 @@ if [ $? -ne 0 ]; then fi # Tag as latest for develop builds -if [ "${TRAVIS_BRANCH}" = "develop" ]; then +if [ "${TRAVIS_BRANCH}" = "develop" ]; then docker tag freqtrade:$TAG ${IMAGE_NAME}:latest fi From e94da7ca41c817ac96373232f9da3448c235a330 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Oct 2018 22:02:23 +0200 Subject: [PATCH 009/103] inverse backtest logic to loop over time - not pairs (more realistic) --- freqtrade/optimize/backtesting.py | 40 +++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6fcde64fa..212c675fd 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -66,6 +66,7 @@ class Backtesting(object): if self.config.get('strategy_list', None): # Force one interval self.ticker_interval = str(self.config.get('ticker_interval')) + self.ticker_interval_mins = constants.TICKER_INTERVAL_MINUTES[self.ticker_interval] for strat in list(self.config['strategy_list']): stratconf = deepcopy(self.config) stratconf['strategy'] = strat @@ -86,6 +87,8 @@ class Backtesting(object): """ self.strategy = strategy self.ticker_interval = self.config.get('ticker_interval') + self.ticker_interval_mins = constants.TICKER_INTERVAL_MINUTES[self.ticker_interval] + self.tickerdata_to_dataframe = strategy.tickerdata_to_dataframe self.advise_buy = strategy.advise_buy self.advise_sell = strategy.advise_sell @@ -280,8 +283,13 @@ class Backtesting(object): processed = args['processed'] max_open_trades = args.get('max_open_trades', 0) position_stacking = args.get('position_stacking', False) + start_date = args.get('start_date') + end_date = args.get('end_date') trades = [] trade_count_lock: Dict = {} + ticker: Dict = {} + pairs = [] + # Create ticker dict for pair, pair_data in processed.items(): pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run @@ -296,15 +304,29 @@ class Backtesting(object): # Convert from Pandas to list for performance reasons # (Looping Pandas is slow.) - ticker = [x for x in ticker_data.itertuples()] + ticker[pair] = [x for x in ticker_data.itertuples()] + pairs.append(pair) + + lock_pair_until: Dict = {} + tmp = start_date + timedelta(minutes=self.ticker_interval_mins) + index = 0 + # Loop timerange and test per pair + while tmp < end_date: + # print(f"time: {tmp}") + for i, pair in enumerate(ticker): + try: + row = ticker[pair][index] + except IndexError: + # missing Data for one pair ... + # TODO:howto handle this + # logger.warning(f"i: {index} - {tmp} did not exist for {pair}") + continue - lock_pair_until = None - for index, row in enumerate(ticker): if row.buy == 0 or row.sell == 1: continue # skip rows where no buy signal or that would immediately sell off if not position_stacking: - if lock_pair_until is not None and row.date <= lock_pair_until: + if pair in lock_pair_until and row.date <= lock_pair_until[pair]: continue if max_open_trades > 0: # Check if max_open_trades has already been reached for the given date @@ -313,17 +335,19 @@ class Backtesting(object): trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1 - trade_entry = self._get_sell_trade_entry(pair, row, ticker[index + 1:], + trade_entry = self._get_sell_trade_entry(pair, row, ticker[pair][index + 1:], trade_count_lock, args) if trade_entry: - lock_pair_until = trade_entry.close_time + lock_pair_until[pair] = trade_entry.close_time trades.append(trade_entry) else: # Set lock_pair_until to end of testing period if trade could not be closed # This happens only if the buy-signal was with the last candle - lock_pair_until = ticker_data.iloc[-1].date + lock_pair_until[pair] = end_date + tmp += timedelta(minutes=self.ticker_interval_mins) + index += 1 return DataFrame.from_records(trades, columns=BacktestResult._fields) def start(self) -> None: @@ -390,6 +414,8 @@ class Backtesting(object): 'processed': preprocessed, 'max_open_trades': max_open_trades, 'position_stacking': self.config.get('position_stacking', False), + 'start_date': min_date, + 'end_date': max_date, } ) From 96efd12a312c14afae0034c4dae2d99972059e6f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Oct 2018 19:35:16 +0200 Subject: [PATCH 010/103] add new options to hyperopt --- freqtrade/optimize/hyperopt.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index b2d05d603..9766f5acb 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -276,11 +276,14 @@ class Hyperopt(Backtesting): self.strategy.stoploss = params['stoploss'] processed = load(TICKERDATA_PICKLE) + min_date, max_date = Backtesting.get_timeframe(processed) results = self.backtest( { 'stake_amount': self.config['stake_amount'], 'processed': processed, 'position_stacking': self.config.get('position_stacking', True), + 'start_date': min_date, + 'end_date': max_date, } ) result_explanation = self.format_results(results) From 6729dfa6d3ed8fec31f371f936f475184256b5dd Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Oct 2018 20:32:33 +0200 Subject: [PATCH 011/103] Add get_timeframe mock for hyperopt --- freqtrade/tests/optimize/test_hyperopt.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index c93f2d316..3ea374bf4 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -1,4 +1,5 @@ # pragma pylint: disable=missing-docstring,W0212,C0103 +from datetime import datetime import os from unittest.mock import MagicMock @@ -291,6 +292,10 @@ def test_generate_optimizer(mocker, default_conf) -> None: 'freqtrade.optimize.hyperopt.Hyperopt.backtest', MagicMock(return_value=backtest_result) ) + mocker.patch( + 'freqtrade.optimize.backtesting.Backtesting.get_timeframe', + MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) + ) patch_exchange(mocker) mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock()) From 03cda8e23eccbeccf8f66d618605d2413b3c33f1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Oct 2018 20:09:12 +0200 Subject: [PATCH 012/103] remove meaningless backtesting test --- freqtrade/tests/optimize/test_backtesting.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 20f2a6582..2d2b2d4a7 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -449,7 +449,7 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None: ) default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] - default_conf['ticker_interval'] = "1m" + default_conf['ticker_interval'] = '1m' default_conf['live'] = False default_conf['datadir'] = None default_conf['export'] = None @@ -587,21 +587,6 @@ def test_backtest_pricecontours(default_conf, fee, mocker) -> None: simple_backtest(default_conf, contour, numres, mocker) -# Test backtest using offline data (testdata directory) -def test_backtest_ticks(default_conf, fee, mocker): - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - patch_exchange(mocker) - ticks = [1, 5] - fun = Backtesting(default_conf).advise_buy - for _ in ticks: - backtest_conf = _make_backtest_conf(mocker, conf=default_conf) - backtesting = Backtesting(default_conf) - backtesting.advise_buy = fun # Override - backtesting.advise_sell = fun # Override - results = backtesting.backtest(backtest_conf) - assert not results.empty - - def test_backtest_clash_buy_sell(mocker, default_conf): # Override the default buy trend function in our default_strategy def fun(dataframe=None, pair=None): From db17ccef2b9e89982d20cbfe11623f47a702095d Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Oct 2018 20:34:20 +0200 Subject: [PATCH 013/103] Adapt backtesting-tests to new backtest-logic --- freqtrade/tests/optimize/test_backtesting.py | 32 +++++++++++++++----- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 2d2b2d4a7..e3e078d72 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -86,17 +86,21 @@ def load_data_test(what): def simple_backtest(config, contour, num_results, mocker) -> None: patch_exchange(mocker) + config['ticker_interval'] = '1m' backtesting = Backtesting(config) data = load_data_test(contour) processed = backtesting.strategy.tickerdata_to_dataframe(data) + min_date, max_date = Backtesting.get_timeframe(processed) assert isinstance(processed, dict) results = backtesting.backtest( { 'stake_amount': config['stake_amount'], 'processed': processed, 'max_open_trades': 1, - 'position_stacking': False + 'position_stacking': False, + 'start_date': min_date, + 'end_date': max_date, } ) # results :: @@ -123,12 +127,16 @@ def _make_backtest_conf(mocker, conf=None, pair='UNITTEST/BTC', record=None): data = trim_dictlist(data, -201) patch_exchange(mocker) backtesting = Backtesting(conf) + processed = backtesting.strategy.tickerdata_to_dataframe(data) + min_date, max_date = Backtesting.get_timeframe(processed) return { 'stake_amount': conf['stake_amount'], - 'processed': backtesting.strategy.tickerdata_to_dataframe(data), + 'processed': processed, 'max_open_trades': 10, 'position_stacking': False, - 'record': record + 'record': record, + 'start_date': min_date, + 'end_date': max_date, } @@ -505,12 +513,15 @@ def test_backtest(default_conf, fee, mocker) -> None: data = optimize.load_data(None, ticker_interval='5m', pairs=['UNITTEST/BTC']) data = trim_dictlist(data, -200) data_processed = backtesting.strategy.tickerdata_to_dataframe(data) + min_date, max_date = Backtesting.get_timeframe(data_processed) results = backtesting.backtest( { 'stake_amount': default_conf['stake_amount'], 'processed': data_processed, 'max_open_trades': 10, - 'position_stacking': False + 'position_stacking': False, + 'start_date': min_date, + 'end_date': max_date, } ) assert not results.empty @@ -554,12 +565,16 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None: # Run a backtesting for an exiting 5min ticker_interval data = optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC']) data = trim_dictlist(data, -200) + processed = backtesting.strategy.tickerdata_to_dataframe(data) + min_date, max_date = Backtesting.get_timeframe(processed) results = backtesting.backtest( { 'stake_amount': default_conf['stake_amount'], - 'processed': backtesting.strategy.tickerdata_to_dataframe(data), + 'processed': processed, 'max_open_trades': 1, - 'position_stacking': False + 'position_stacking': False, + 'start_date': min_date, + 'end_date': max_date, } ) assert not results.empty @@ -582,7 +597,10 @@ 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', 19]] + tests = [['raise', 18], ['lower', 0], ['sine', 16]] + # We need to enable sell-signal - otherwise it sells on ROI!! + default_conf['experimental'] = {"use_sell_signal": True} + for [contour, numres] in tests: simple_backtest(default_conf, contour, numres, mocker) From 83a8d79115b1056c1469338da4965817243e33f2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Oct 2018 20:35:08 +0200 Subject: [PATCH 014/103] Fix alternate buy/sell (this should respect the sell signal!) --- freqtrade/tests/optimize/test_backtesting.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index e3e078d72..20c117a8e 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -638,14 +638,16 @@ def test_backtest_only_sell(mocker, default_conf): def test_backtest_alternate_buy_sell(default_conf, fee, mocker): mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC') + # We need to enable sell-signal - otherwise it sells on ROI!! + default_conf['experimental'] = {"use_sell_signal": True} backtesting = Backtesting(default_conf) backtesting.advise_buy = _trend_alternate # Override backtesting.advise_sell = _trend_alternate # Override results = backtesting.backtest(backtest_conf) backtesting._store_backtest_result("test_.json", results) - assert len(results) == 4 + assert len(results) == 21 # One trade was force-closed at the end - assert len(results.loc[results.open_at_end]) == 1 + assert len(results.loc[results.open_at_end]) == 0 def test_backtest_record(default_conf, fee, mocker): From 66487f2a133decf3ef887f732fc58b1927c9079a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Oct 2018 20:36:24 +0200 Subject: [PATCH 015/103] require start/end-date argument in backtest --- freqtrade/optimize/backtesting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 212c675fd..49f6375f7 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -283,8 +283,8 @@ class Backtesting(object): processed = args['processed'] max_open_trades = args.get('max_open_trades', 0) position_stacking = args.get('position_stacking', False) - start_date = args.get('start_date') - end_date = args.get('end_date') + start_date = args['start_date'] + end_date = args['end_date'] trades = [] trade_count_lock: Dict = {} ticker: Dict = {} From 2371d1e696b3a6491cbc2883b0fa523b491b9896 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Oct 2018 06:54:07 +0200 Subject: [PATCH 016/103] Fix backtest test (don't use 8m file if we use 1m tickers) --- freqtrade/tests/optimize/test_backtesting.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 20c117a8e..d0c8a88aa 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -640,12 +640,16 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker): backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC') # We need to enable sell-signal - otherwise it sells on ROI!! default_conf['experimental'] = {"use_sell_signal": True} + default_conf['ticker_interval'] = '1m' backtesting = Backtesting(default_conf) backtesting.advise_buy = _trend_alternate # Override backtesting.advise_sell = _trend_alternate # Override results = backtesting.backtest(backtest_conf) backtesting._store_backtest_result("test_.json", results) - assert len(results) == 21 + # 200 candles in backtest data + # won't buy on first (shifted by 1) + # 100 buys signals + assert len(results) == 99 # One trade was force-closed at the end assert len(results.loc[results.open_at_end]) == 0 From fa4c199aa63d13837688ed0b7f3fa2d0a7d627a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 4 Nov 2018 13:23:59 +0100 Subject: [PATCH 017/103] fix some mismatches after rebase --- freqtrade/tests/optimize/test_backtesting.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index d0c8a88aa..b0d4f2cbd 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -13,6 +13,7 @@ from arrow import Arrow from freqtrade import DependencyException, constants, optimize from freqtrade.arguments import Arguments, TimeRange +from freqtrade.optimize import get_timeframe from freqtrade.optimize.backtesting import (Backtesting, setup_configuration, start) from freqtrade.tests.conftest import log_has, patch_exchange @@ -91,7 +92,7 @@ def simple_backtest(config, contour, num_results, mocker) -> None: data = load_data_test(contour) processed = backtesting.strategy.tickerdata_to_dataframe(data) - min_date, max_date = Backtesting.get_timeframe(processed) + min_date, max_date = get_timeframe(processed) assert isinstance(processed, dict) results = backtesting.backtest( { @@ -128,7 +129,7 @@ def _make_backtest_conf(mocker, conf=None, pair='UNITTEST/BTC', record=None): patch_exchange(mocker) backtesting = Backtesting(conf) processed = backtesting.strategy.tickerdata_to_dataframe(data) - min_date, max_date = Backtesting.get_timeframe(processed) + min_date, max_date = get_timeframe(processed) return { 'stake_amount': conf['stake_amount'], 'processed': processed, @@ -513,7 +514,7 @@ def test_backtest(default_conf, fee, mocker) -> None: data = optimize.load_data(None, ticker_interval='5m', pairs=['UNITTEST/BTC']) data = trim_dictlist(data, -200) data_processed = backtesting.strategy.tickerdata_to_dataframe(data) - min_date, max_date = Backtesting.get_timeframe(data_processed) + min_date, max_date = get_timeframe(data_processed) results = backtesting.backtest( { 'stake_amount': default_conf['stake_amount'], @@ -566,7 +567,7 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None: data = optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC']) data = trim_dictlist(data, -200) processed = backtesting.strategy.tickerdata_to_dataframe(data) - min_date, max_date = Backtesting.get_timeframe(processed) + min_date, max_date = get_timeframe(processed) results = backtesting.backtest( { 'stake_amount': default_conf['stake_amount'], From 9cd2ed5a16ae7ed5c617dd345ade46d7b8d6d18c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 4 Nov 2018 13:43:09 +0100 Subject: [PATCH 018/103] fix hyperopt get_timeframe mock --- freqtrade/optimize/hyperopt.py | 4 ++-- freqtrade/tests/optimize/test_hyperopt.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 9766f5acb..edf73fcf3 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -23,7 +23,7 @@ from skopt.space import Categorical, Dimension, Integer, Real import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration -from freqtrade.optimize import load_data +from freqtrade.optimize import load_data, get_timeframe from freqtrade.optimize.backtesting import Backtesting logger = logging.getLogger(__name__) @@ -276,7 +276,7 @@ class Hyperopt(Backtesting): self.strategy.stoploss = params['stoploss'] processed = load(TICKERDATA_PICKLE) - min_date, max_date = Backtesting.get_timeframe(processed) + min_date, max_date = get_timeframe(processed) results = self.backtest( { 'stake_amount': self.config['stake_amount'], diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 3ea374bf4..703b88fc1 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock import pandas as pd import pytest -from freqtrade.optimize.__init__ import load_tickerdata_file +from freqtrade.optimize import load_tickerdata_file from freqtrade.optimize.hyperopt import Hyperopt, start from freqtrade.strategy.resolver import StrategyResolver from freqtrade.tests.conftest import log_has, patch_exchange @@ -293,7 +293,7 @@ def test_generate_optimizer(mocker, default_conf) -> None: MagicMock(return_value=backtest_result) ) mocker.patch( - 'freqtrade.optimize.backtesting.Backtesting.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timeframe', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) patch_exchange(mocker) From 93429a58b2d605215b4157cb8409cb845ded97ed Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 4 Nov 2018 13:45:22 +0100 Subject: [PATCH 019/103] remove TODO --- freqtrade/optimize/backtesting.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 49f6375f7..4dc3119d3 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -318,8 +318,7 @@ class Backtesting(object): row = ticker[pair][index] except IndexError: # missing Data for one pair ... - # TODO:howto handle this - # logger.warning(f"i: {index} - {tmp} did not exist for {pair}") + # Warnings for this are shown by `validate_backtest_data` continue if row.buy == 0 or row.sell == 1: From 56dcf080a906645df2a2872618c3666c129d8973 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Nov 2018 20:03:04 +0100 Subject: [PATCH 020/103] Add explicit test for parallel trades --- freqtrade/tests/optimize/test_backtesting.py | 71 ++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index b0d4f2cbd..e03e011f3 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -655,6 +655,77 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker): assert len(results.loc[results.open_at_end]) == 0 +def test_backtest_multi_pair(default_conf, fee, mocker): + + def evaluate_result_multi(results, freq, max_open_trades): + # Find overlapping trades by expanding each trade once per period + # and then counting overlaps + dates = [pd.Series(pd.date_range(row[1].open_time, row[1].close_time, freq=freq)) + for row in results[['open_time', 'close_time']].iterrows()] + deltas = [len(x) for x in dates] + dates = pd.Series(pd.concat(dates).values, name='date') + df2 = pd.DataFrame(np.repeat(results.values, deltas, axis=0), columns=results.columns) + + df2 = df2.astype(dtype={"open_time": "datetime64", "close_time": "datetime64"}) + df2 = pd.concat([dates, df2], axis=1) + df2 = df2.set_index('date') + df_final = df2.resample(freq)[['pair']].count() + return df_final[df_final['pair'] > max_open_trades] + + def _trend_alternate_hold(dataframe=None, metadata=None): + """ + Buy every 8th candle - sell every other 8th -2 (hold on to pairs a bit) + """ + multi = 8 + dataframe['buy'] = np.where(dataframe.index % multi == 0, 1, 0) + dataframe['sell'] = np.where((dataframe.index + multi - 2) % multi == 0, 1, 0) + if metadata['pair'] in('ETH/BTC', 'LTC/BTC'): + dataframe['buy'] = dataframe['buy'].shift(-4) + dataframe['sell'] = dataframe['sell'].shift(-4) + return dataframe + + mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + pairs = ['ADA/BTC', 'DASH/BTC', 'ETH/BTC', 'LTC/BTC', 'NXT/BTC'] + data = optimize.load_data(None, ticker_interval='5m', pairs=pairs) + data = trim_dictlist(data, -500) + # We need to enable sell-signal - otherwise it sells on ROI!! + default_conf['experimental'] = {"use_sell_signal": True} + default_conf['ticker_interval'] = '5m' + + backtesting = Backtesting(default_conf) + backtesting.advise_buy = _trend_alternate_hold # Override + backtesting.advise_sell = _trend_alternate_hold # Override + + data_processed = backtesting.strategy.tickerdata_to_dataframe(data) + min_date, max_date = get_timeframe(data_processed) + backtest_conf = { + 'stake_amount': default_conf['stake_amount'], + 'processed': data_processed, + 'max_open_trades': 3, + 'position_stacking': False, + 'start_date': min_date, + 'end_date': max_date, + } + + results = backtesting.backtest(backtest_conf) + + # Make sure we have parallel trades + assert len(evaluate_result_multi(results, '5min', 2)) > 0 + # make sure we don't have trades with more than configured max_open_trades + assert len(evaluate_result_multi(results, '5min', 3)) == 0 + + backtest_conf = { + 'stake_amount': default_conf['stake_amount'], + 'processed': data_processed, + 'max_open_trades': 1, + 'position_stacking': False, + 'start_date': min_date, + 'end_date': max_date, + } + results = backtesting.backtest(backtest_conf) + assert len(evaluate_result_multi(results, '5min', 1)) == 0 + + def test_backtest_record(default_conf, fee, mocker): names = [] records = [] From 272ff51d512d0c06832812f985744a7cbea72719 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Nov 2018 20:37:59 +0100 Subject: [PATCH 021/103] correctly patch exchange --- freqtrade/tests/optimize/test_backtesting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index e03e011f3..aa1f144cd 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -685,6 +685,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker): return dataframe mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + patch_exchange(mocker) pairs = ['ADA/BTC', 'DASH/BTC', 'ETH/BTC', 'LTC/BTC', 'NXT/BTC'] data = optimize.load_data(None, ticker_interval='5m', pairs=pairs) data = trim_dictlist(data, -500) From 5c5fe4c13a4b92be65e0e52903bb804f0e397f09 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Nov 2018 07:14:43 +0100 Subject: [PATCH 022/103] Fix test --- freqtrade/tests/optimize/test_backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index aa1f144cd..03d874f5f 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -598,7 +598,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]] # We need to enable sell-signal - otherwise it sells on ROI!! default_conf['experimental'] = {"use_sell_signal": True} From 292962d64dfb88d9d0b4b55aab9f5883577867b3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Nov 2018 19:34:18 +0100 Subject: [PATCH 023/103] Fix tests --- freqtrade/tests/optimize/__init__.py | 5 +++-- freqtrade/tests/optimize/test_backtest_detail.py | 7 ++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/freqtrade/tests/optimize/__init__.py b/freqtrade/tests/optimize/__init__.py index 2b7222e88..58ea7c343 100644 --- a/freqtrade/tests/optimize/__init__.py +++ b/freqtrade/tests/optimize/__init__.py @@ -4,9 +4,10 @@ import arrow from pandas import DataFrame from freqtrade.strategy.interface import SellType +from freqtrade.constants import TICKER_INTERVAL_MINUTES ticker_start_time = arrow.get(2018, 10, 3) -ticker_interval_in_minute = 60 +tests_ticker_interval = "1h" class BTrade(NamedTuple): @@ -31,7 +32,7 @@ class BTContainer(NamedTuple): def _get_frame_time_from_offset(offset): return ticker_start_time.shift( - minutes=(offset * ticker_interval_in_minute)).datetime + minutes=(offset * TICKER_INTERVAL_MINUTES[tests_ticker_interval])).datetime def _build_backtest_dataframe(ticker_with_signals): diff --git a/freqtrade/tests/optimize/test_backtest_detail.py b/freqtrade/tests/optimize/test_backtest_detail.py index 806c136bc..7db6913f3 100644 --- a/freqtrade/tests/optimize/test_backtest_detail.py +++ b/freqtrade/tests/optimize/test_backtest_detail.py @@ -6,10 +6,11 @@ from pandas import DataFrame import pytest +from freqtrade.optimize import get_timeframe 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) + _get_frame_time_from_offset, tests_ticker_interval) from freqtrade.tests.conftest import patch_exchange @@ -147,6 +148,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: """ default_conf["stoploss"] = data.stop_loss default_conf["minimal_roi"] = {"0": data.roi} + default_conf['ticker_interval'] = tests_ticker_interval mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.0)) patch_exchange(mocker) frame = _build_backtest_dataframe(data.data) @@ -158,11 +160,14 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: pair = 'UNITTEST/BTC' # Dummy data as we mock the analyze functions data_processed = {pair: DataFrame()} + min_date, max_date = get_timeframe({pair: frame}) results = backtesting.backtest( { 'stake_amount': default_conf['stake_amount'], 'processed': data_processed, 'max_open_trades': 10, + 'start_date': min_date, + 'end_date': max_date, } ) print(results.T) From 59cd4fe0ef735bb18d2d3ab484616921670176c0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Nov 2018 19:34:46 +0100 Subject: [PATCH 024/103] Remove boilerplate comments --- freqtrade/tests/optimize/test_backtest_detail.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/freqtrade/tests/optimize/test_backtest_detail.py b/freqtrade/tests/optimize/test_backtest_detail.py index 7db6913f3..e5849319e 100644 --- a/freqtrade/tests/optimize/test_backtest_detail.py +++ b/freqtrade/tests/optimize/test_backtest_detail.py @@ -174,18 +174,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: 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 From b50250139ec3171309ad62dd784b159e965245b9 Mon Sep 17 00:00:00 2001 From: misagh Date: Mon, 19 Nov 2018 20:02:26 +0100 Subject: [PATCH 025/103] Drafting stoploss on exchange --- freqtrade/strategy/interface.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 212559c8c..d1d4703a4 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -67,6 +67,11 @@ class IStrategy(ABC): # associated stoploss stoploss: float + # if the stoploss should be on exchange. + # if this is True then a stoploss order will be placed + # immediately after a successful buy order. + stoploss_on_exchange: bool = False + # associated ticker interval ticker_interval: str @@ -214,7 +219,11 @@ class IStrategy(ABC): # 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, + + if self.stoploss_on_exchange: + stoplossflag = False + else: + 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: From bfbdddff26bd9296da0b046f260db1ce5778b703 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 16:24:40 +0100 Subject: [PATCH 026/103] stoploss limit order added to exchange --- freqtrade/exchange/__init__.py | 18 ++++++++++++++++++ freqtrade/strategy/interface.py | 2 ++ 2 files changed, 20 insertions(+) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index ae07e36e9..59a5da23e 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -333,6 +333,24 @@ class Exchange(object): except ccxt.BaseError as e: raise OperationalException(e) + def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict: + # Only binance is supported + if not self._api.name == 'Binance': + raise NotImplementedError( + 'Stoploss limit orders are implemented only for binance as of now.') + + # Set the precision for amount and price(rate) as accepted by the exchange + amount = self.symbol_amount_prec(pair, amount) + rate = self.symbol_price_prec(pair, rate) + stop_price = self.symbol_price_prec(pair, stop_price) + + # Ensure rate is less than stop price + if stop_price >= rate: + raise OperationalException( + 'In stoploss limit order, stop price should be more than limit price') + + return self._api.create_order(pair, 'stop_loss', 'sell', amount, rate, {'stopPrice': stop_price}) + @retrier def get_balance(self, currency: str) -> float: if self._conf['dry_run']: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 9b7b180cc..df0e3cf72 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -233,8 +233,10 @@ class IStrategy(ABC): 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) From 3b7e05e07b15c1df1bfe62bfbf264feac9877784 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 16:26:24 +0100 Subject: [PATCH 027/103] stop loss order added right after a buy order is executued --- freqtrade/freqtradebot.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 8a2db84a9..f1aae3c3f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -508,6 +508,37 @@ class FreqtradeBot(object): Trade.session.add(trade) Trade.session.flush() + # Check if stoploss should be added on exchange + # If True then here immediately after buy we should + # Add the stoploss order + if self.strategy.stoploss_on_exchange: + stoploss = self.edge.stoploss if self.edge else self.strategy.stoploss + stop_price = buy_limit * (1 + stoploss) + + # limit price should be less than stop price. + # 0.98 is arbitrary here. + limit_price = stop_price * 0.98 + + order_id = self.exchange.stoploss_limit(pair=pair, amount=amount, + stop_price=stop_price, rate=limit_price)['id'] + + trade = Trade( + pair=pair, + stake_amount=stake_amount, + amount=amount, + fee_open=fee, + fee_close=fee, + stoploss=stop_price, + open_date=datetime.utcnow(), + exchange=self.exchange.id, + open_order_id=order_id, + strategy=self.strategy.get_strategy_name(), + ticker_interval=constants.TICKER_INTERVAL_MINUTES[self.config['ticker_interval']] + ) + + Trade.session.add(trade) + Trade.session.flush() + # Updating wallets self.wallets.update() From bb37b56dea02fdf7e5b51e54ae7144e808ab2e08 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 16:47:52 +0100 Subject: [PATCH 028/103] adding stop loss order id to Trade --- freqtrade/persistence.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 51a8129fb..db6d526c7 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -178,6 +178,7 @@ class Trade(_DECL_BASE): # absolute value of the initial stop loss initial_stop_loss = Column(Float, nullable=True, default=0.0) # absolute value of the highest reached price + stoploss_order_id = Column(Integer, nullable=True, index=True) max_rate = Column(Float, nullable=True, default=0.0) sell_reason = Column(String, nullable=True) strategy = Column(String, nullable=True) From fad75939356188b1bf2d8ca7eb82fa5e4201ba4a Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 16:53:50 +0100 Subject: [PATCH 029/103] =?UTF-8?q?doesn=E2=80=99t=20have=20to=20create=20?= =?UTF-8?q?another=20Trade=20for=20SL.=20can=20be=20cumulated=20into=20the?= =?UTF-8?q?=20same.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- freqtrade/freqtradebot.py | 48 ++++++++++++++------------------------- 1 file changed, 17 insertions(+), 31 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f1aae3c3f..f3537f2ab 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -479,6 +479,22 @@ class FreqtradeBot(object): order_id = self.exchange.buy(pair=pair, ordertype=self.strategy.order_types['buy'], amount=amount, rate=buy_limit)['id'] + stoploss_order_id: int = None + + # Check if stoploss should be added on exchange + # If True then here immediately after buy we should + # Add the stoploss order + if self.strategy.stoploss_on_exchange: + stoploss = self.edge.stoploss if self.edge else self.strategy.stoploss + stop_price = buy_limit * (1 + stoploss) + + # limit price should be less than stop price. + # 0.98 is arbitrary here. + limit_price = stop_price * 0.98 + + stoploss_order_id = self.exchange.stoploss_limit(pair=pair, amount=amount, + stop_price=stop_price, rate=limit_price)['id'] + self.rpc.send_msg({ 'type': RPCMessageType.BUY_NOTIFICATION, 'exchange': self.exchange.name.capitalize(), @@ -502,43 +518,13 @@ class FreqtradeBot(object): open_date=datetime.utcnow(), exchange=self.exchange.id, open_order_id=order_id, + stoploss_order_id=stoploss_order_id, strategy=self.strategy.get_strategy_name(), ticker_interval=constants.TICKER_INTERVAL_MINUTES[self.config['ticker_interval']] ) Trade.session.add(trade) Trade.session.flush() - # Check if stoploss should be added on exchange - # If True then here immediately after buy we should - # Add the stoploss order - if self.strategy.stoploss_on_exchange: - stoploss = self.edge.stoploss if self.edge else self.strategy.stoploss - stop_price = buy_limit * (1 + stoploss) - - # limit price should be less than stop price. - # 0.98 is arbitrary here. - limit_price = stop_price * 0.98 - - order_id = self.exchange.stoploss_limit(pair=pair, amount=amount, - stop_price=stop_price, rate=limit_price)['id'] - - trade = Trade( - pair=pair, - stake_amount=stake_amount, - amount=amount, - fee_open=fee, - fee_close=fee, - stoploss=stop_price, - open_date=datetime.utcnow(), - exchange=self.exchange.id, - open_order_id=order_id, - strategy=self.strategy.get_strategy_name(), - ticker_interval=constants.TICKER_INTERVAL_MINUTES[self.config['ticker_interval']] - ) - - Trade.session.add(trade) - Trade.session.flush() - # Updating wallets self.wallets.update() From da5617624c97389b0a1aabd3e95e18258e6b6348 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 17:02:02 +0100 Subject: [PATCH 030/103] cancelling stop loss order before selling --- freqtrade/freqtradebot.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f3537f2ab..744f92156 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -505,6 +505,7 @@ class FreqtradeBot(object): 'stake_currency': stake_currency, 'fiat_currency': fiat_currency }) + # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') trade = Trade( @@ -522,6 +523,7 @@ class FreqtradeBot(object): strategy=self.strategy.get_strategy_name(), ticker_interval=constants.TICKER_INTERVAL_MINUTES[self.config['ticker_interval']] ) + Trade.session.add(trade) Trade.session.flush() @@ -798,6 +800,11 @@ class FreqtradeBot(object): sell_type = 'sell' if sell_reason in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): sell_type = 'stoploss' + + # First cancelling stoploss on exchange ... + if self.strategy.stoploss_on_exchange and trade.stoploss_order_id: + self.exchange.cancel_order(trade.stoploss_order_id, trade.pair) + # Execute sell and update trade record order_id = self.exchange.sell(pair=str(trade.pair), ordertype=self.strategy.order_types[sell_type], From bbe8e4e49456b8ec5de23ed22901dfb1c4c82561 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 17:07:37 +0100 Subject: [PATCH 031/103] flake8 --- freqtrade/exchange/__init__.py | 3 ++- freqtrade/freqtradebot.py | 4 ++-- freqtrade/strategy/interface.py | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 59a5da23e..53ae6c2d7 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -349,7 +349,8 @@ class Exchange(object): raise OperationalException( 'In stoploss limit order, stop price should be more than limit price') - return self._api.create_order(pair, 'stop_loss', 'sell', amount, rate, {'stopPrice': stop_price}) + return self._api.create_order(pair, 'stop_loss', 'sell', + amount, rate, {'stopPrice': stop_price}) @retrier def get_balance(self, currency: str) -> float: diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 744f92156..40734f385 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -492,8 +492,8 @@ class FreqtradeBot(object): # 0.98 is arbitrary here. limit_price = stop_price * 0.98 - stoploss_order_id = self.exchange.stoploss_limit(pair=pair, amount=amount, - stop_price=stop_price, rate=limit_price)['id'] + stoploss_order_id = self.exchange.stoploss_limit( + pair=pair, amount=amount, stop_price=stop_price, rate=limit_price)['id'] self.rpc.send_msg({ 'type': RPCMessageType.BUY_NOTIFICATION, diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index df0e3cf72..9047d8807 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -231,8 +231,8 @@ class IStrategy(ABC): stoplossflag = False else: stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, - current_time=date, current_profit=current_profit, - force_stoploss=force_stoploss) + current_time=date, current_profit=current_profit, + force_stoploss=force_stoploss) if stoplossflag.sell_flag: return stoplossflag From 3a1c378325dc97b48bb1ad767fc5ff8281bdf88c Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 17:14:22 +0100 Subject: [PATCH 032/103] typing bugs --- freqtrade/freqtradebot.py | 4 ++-- freqtrade/strategy/interface.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 40734f385..ac9fd758c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -479,13 +479,13 @@ class FreqtradeBot(object): order_id = self.exchange.buy(pair=pair, ordertype=self.strategy.order_types['buy'], amount=amount, rate=buy_limit)['id'] - stoploss_order_id: int = None + stoploss_order_id = None # Check if stoploss should be added on exchange # If True then here immediately after buy we should # Add the stoploss order if self.strategy.stoploss_on_exchange: - stoploss = self.edge.stoploss if self.edge else self.strategy.stoploss + stoploss = self.edge.stoploss(pair=pair) if self.edge else self.strategy.stoploss stop_price = buy_limit * (1 + stoploss) # limit price should be less than stop price. diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 9047d8807..30fc62f42 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -228,7 +228,7 @@ class IStrategy(ABC): current_profit = trade.calc_profit_percent(current_rate) if self.stoploss_on_exchange: - stoplossflag = False + stoplossflag = SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) else: stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, current_time=date, current_profit=current_profit, From 2461d86c8d2d0dd692edfabf4fd79162eb87fbd4 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 17:24:45 +0100 Subject: [PATCH 033/103] dry run should consider stop loss is hit on limit price --- freqtrade/freqtradebot.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ac9fd758c..281b22bc5 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -805,6 +805,11 @@ class FreqtradeBot(object): if self.strategy.stoploss_on_exchange and trade.stoploss_order_id: self.exchange.cancel_order(trade.stoploss_order_id, trade.pair) + # Dry-run should consider stoploss is executed at the limit price + # So overriding limit in case of dry-run + if self.config['dry_run']: + limit = trade.stop_loss + # Execute sell and update trade record order_id = self.exchange.sell(pair=str(trade.pair), ordertype=self.strategy.order_types[sell_type], From 24df093a85ea65293b6baa653ab515049e4e0e56 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 17:41:01 +0100 Subject: [PATCH 034/103] test: only implemented for binance --- freqtrade/tests/exchange/test_exchange.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 207f14efe..5cbe5b42e 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1171,3 +1171,10 @@ def test_get_fee(default_conf, mocker): ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'get_fee', 'calculate_fee') + +def test_stoploss_limit_available_only_for_binance(default_conf, mocker): + api_mock = MagicMock() + exchange = get_patched_exchange(mocker, default_conf, api_mock) + with pytest.raises(NotImplementedError): + exchange.stoploss_limit('BTC/ETH', 1, 0.8, 0.79) + From 3418592908a8e01699e485993c1d31ffb0d4c291 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 19:25:26 +0100 Subject: [PATCH 035/103] freqtradebot test added for orders on exchange --- freqtrade/exchange/__init__.py | 2 +- freqtrade/tests/conftest.py | 7 ++-- freqtrade/tests/test_freqtradebot.py | 50 ++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 53ae6c2d7..90660c9aa 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -335,7 +335,7 @@ class Exchange(object): def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict: # Only binance is supported - if not self._api.name == 'Binance': + if not self.name == 'Binance': raise NotImplementedError( 'Stoploss limit orders are implemented only for binance as of now.') diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index b6c022b45..3453b4ddf 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -27,12 +27,13 @@ def log_has(line, logs): False) -def patch_exchange(mocker, api_mock=None) -> None: +def patch_exchange(mocker, api_mock=None, id='bittrex') -> None: mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_ordertypes', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value="Bittrex")) - mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value="bittrex")) + mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id)) + mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title())) + if api_mock: mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) else: diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index cef89c250..af4071591 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -880,6 +880,56 @@ def test_execute_buy(mocker, default_conf, fee, markets, limit_buy_order) -> Non assert call_args['rate'] == fix_price assert call_args['amount'] == stake_amount / fix_price +def test_execute_buy_with_stoploss_on_exchange(mocker, default_conf, fee, markets, limit_buy_order) -> None: + default_conf['exchange']['name'] = 'binance' + patch_RPCManager(mocker) + patch_exchange(mocker) + freqtrade = FreqtradeBot(default_conf) + freqtrade.strategy.stoploss_on_exchange = True + freqtrade.strategy.stoploss = -0.05 + stake_amount = 2 + bid = 0.11 + get_bid = MagicMock(return_value=bid) + mocker.patch.multiple( + 'freqtrade.freqtradebot.FreqtradeBot', + get_target_bid=get_bid, + _get_min_pair_stake_amount=MagicMock(return_value=1) + ) + buy_mm = MagicMock(return_value={'id': limit_buy_order['id']}) + stoploss_limit = MagicMock(return_value={'id': 13434334}) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=MagicMock(return_value={ + 'bid': 0.00001172, + 'ask': 0.00001173, + 'last': 0.00001172 + }), + buy=buy_mm, + get_fee=fee, + get_markets=markets, + stoploss_limit=stoploss_limit + ) + pair = 'ETH/BTC' + print(buy_mm.call_args_list) + + assert freqtrade.execute_buy(pair, stake_amount) + assert stoploss_limit.call_count == 1 + assert get_bid.call_count == 1 + assert buy_mm.call_count == 1 + call_args = buy_mm.call_args_list[0][1] + assert call_args['pair'] == pair + assert call_args['rate'] == bid + assert call_args['amount'] == stake_amount / bid + + call_args = stoploss_limit.call_args_list[0][1] + assert call_args['pair'] == pair + assert call_args['amount'] == stake_amount / bid + assert call_args['stop_price'] == 0.11 * 0.95 + assert call_args['rate'] == 0.11 * 0.95 * 0.98 + + trade = Trade.query.first() + assert trade.is_open is True + assert trade.stoploss_order_id == 13434334 def test_process_maybe_execute_buy(mocker, default_conf) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) From cc1422d448f898f5570224dda448c3995dc9d600 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 19:27:32 +0100 Subject: [PATCH 036/103] flake8 --- freqtrade/tests/test_freqtradebot.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index af4071591..03bc68025 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -880,7 +880,9 @@ def test_execute_buy(mocker, default_conf, fee, markets, limit_buy_order) -> Non assert call_args['rate'] == fix_price assert call_args['amount'] == stake_amount / fix_price -def test_execute_buy_with_stoploss_on_exchange(mocker, default_conf, fee, markets, limit_buy_order) -> None: + +def test_execute_buy_with_stoploss_on_exchange(mocker, default_conf, + fee, markets, limit_buy_order) -> None: default_conf['exchange']['name'] = 'binance' patch_RPCManager(mocker) patch_exchange(mocker) @@ -931,6 +933,7 @@ def test_execute_buy_with_stoploss_on_exchange(mocker, default_conf, fee, market assert trade.is_open is True assert trade.stoploss_order_id == 13434334 + def test_process_maybe_execute_buy(mocker, default_conf) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) From ecb2c4dca384d08cc9ebf6a112ebb98cefe0b795 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 19:38:20 +0100 Subject: [PATCH 037/103] bloody flake8 --- freqtrade/tests/exchange/test_exchange.py | 2 +- freqtrade/tests/test_freqtradebot.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 5cbe5b42e..9dbc50a66 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1172,9 +1172,9 @@ def test_get_fee(default_conf, mocker): ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'get_fee', 'calculate_fee') + def test_stoploss_limit_available_only_for_binance(default_conf, mocker): api_mock = MagicMock() exchange = get_patched_exchange(mocker, default_conf, api_mock) with pytest.raises(NotImplementedError): exchange.stoploss_limit('BTC/ETH', 1, 0.8, 0.79) - diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 03bc68025..571c89bc0 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -885,7 +885,7 @@ def test_execute_buy_with_stoploss_on_exchange(mocker, default_conf, fee, markets, limit_buy_order) -> None: default_conf['exchange']['name'] = 'binance' patch_RPCManager(mocker) - patch_exchange(mocker) + patch_exchange(mocker, id='binance') freqtrade = FreqtradeBot(default_conf) freqtrade.strategy.stoploss_on_exchange = True freqtrade.strategy.stoploss = -0.05 From 07ac9024512e7f96185ee447243ee8d6ccd188f4 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 20:30:31 +0100 Subject: [PATCH 038/103] test exchange added --- freqtrade/exchange/__init__.py | 2 +- freqtrade/tests/conftest.py | 4 +-- freqtrade/tests/exchange/test_exchange.py | 36 +++++++++++++++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 90660c9aa..a88fb6ee8 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -345,7 +345,7 @@ class Exchange(object): stop_price = self.symbol_price_prec(pair, stop_price) # Ensure rate is less than stop price - if stop_price >= rate: + if stop_price <= rate: raise OperationalException( 'In stoploss limit order, stop price should be more than limit price') diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 3453b4ddf..f4c263959 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -40,8 +40,8 @@ def patch_exchange(mocker, api_mock=None, id='bittrex') -> None: mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock()) -def get_patched_exchange(mocker, config, api_mock=None) -> Exchange: - patch_exchange(mocker, api_mock) +def get_patched_exchange(mocker, config, api_mock=None, id='bittrex') -> Exchange: + patch_exchange(mocker, api_mock, id) exchange = Exchange(config) return exchange diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 9dbc50a66..53e77d7b6 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1178,3 +1178,39 @@ def test_stoploss_limit_available_only_for_binance(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock) with pytest.raises(NotImplementedError): exchange.stoploss_limit('BTC/ETH', 1, 0.8, 0.79) + + +def test_stoploss_limit_order(default_conf, mocker): + api_mock = MagicMock() + order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) + order_type = 'stop_loss' + + api_mock.create_order = MagicMock(return_value={ + 'id': order_id, + 'info': { + 'foo': 'bar' + } + }) + + default_conf['dry_run'] = False + mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) + mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) + + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') + + with pytest.raises(OperationalException): + order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200) + + api_mock.create_order.reset_mock() + + order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) + + assert 'id' in order + assert 'info' in order + assert order['id'] == order_id + assert api_mock.create_order.call_args[0][0] == 'ETH/BTC' + assert api_mock.create_order.call_args[0][1] == order_type + assert api_mock.create_order.call_args[0][2] == 'sell' + assert api_mock.create_order.call_args[0][3] == 1 + assert api_mock.create_order.call_args[0][4] == 200 + assert api_mock.create_order.call_args[0][5] == {'stopPrice': 220} From 7faafea8a2949a94725b883e89d81a7e854ec7f5 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 21:01:39 +0100 Subject: [PATCH 039/103] added test for cancelling stop loss before sell --- freqtrade/tests/test_freqtradebot.py | 50 ++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 571c89bc0..333b4d51f 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1528,6 +1528,56 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, } == last_msg +def test_execute_sell_with_stoploss_on_exchange(default_conf, + ticker, fee, ticker_sell_up, + markets, mocker) -> None: + + default_conf['exchange']['name'] = 'binance' + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + _load_markets=MagicMock(return_value={}), + get_ticker=ticker, + get_fee=fee, + get_markets=markets + ) + + stoploss_limit = MagicMock(return_value={ + 'id': 123, + 'info': { + 'foo': 'bar' + } + }) + + cancel_order = MagicMock(return_value=True) + + mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) + mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) + mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit) + mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order) + + freqtrade = FreqtradeBot(default_conf) + freqtrade.strategy.stoploss_on_exchange = True + patch_get_signal(freqtrade) + + # Create some test data + freqtrade.create_trade() + + trade = Trade.query.first() + assert trade + + # Increase the price and sell it + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=ticker_sell_up + ) + + freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) + + trade = Trade.query.first() + assert trade + assert cancel_order.call_count == 1 + + def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, mocker) -> None: rpc_mock = patch_RPCManager(mocker) From 6f0025c6de194c9f3bf3dd3ea43c135c63abef04 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 21:07:33 +0100 Subject: [PATCH 040/103] documentation written --- docs/configuration.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/configuration.md b/docs/configuration.md index 62559a41e..64f0a2ea6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -26,6 +26,7 @@ The table below will list all configuration parameters. | `process_only_new_candles` | false | No | If set to true indicators are processed only once a new candle arrives. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. Can be set either in Configuration or in the strategy. | `minimal_roi` | See below | No | Set the threshold in percent the bot will use to sell a trade. More information below. If set, this parameter will override `minimal_roi` from your strategy file. | `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file. +| `stoploss_on_exchange` | false | No | Only for binance users for now: If this parameter is on then stoploss limit order is executed immediately after buy order is done on binance. | `trailing_stop` | false | No | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file). | `trailing_stop_positve` | 0 | No | Changes stop-loss once profit has been reached. | `trailing_stop_positve_offset` | 0 | No | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. From 1dde56790c4650fa7be00108eb807e9c0e806645 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 21:12:49 +0100 Subject: [PATCH 041/103] final broken test fixed --- freqtrade/tests/test_freqtradebot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 333b4d51f..aad5c371f 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1533,6 +1533,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, markets, mocker) -> None: default_conf['exchange']['name'] = 'binance' + rpc_mock = patch_RPCManager(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', _load_markets=MagicMock(return_value={}), @@ -1576,6 +1577,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, trade = Trade.query.first() assert trade assert cancel_order.call_count == 1 + assert rpc_mock.call_count == 2 def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, From fea77824d09a0ebc9c3f1acf09273b799aa39e23 Mon Sep 17 00:00:00 2001 From: misagh Date: Fri, 23 Nov 2018 15:17:36 +0100 Subject: [PATCH 042/103] handle stop loss on exchange added --- freqtrade/freqtradebot.py | 24 +++++++++ freqtrade/persistence.py | 5 +- freqtrade/strategy/interface.py | 1 + freqtrade/tests/test_freqtradebot.py | 75 +++++++++++++++++++++++++++- 4 files changed, 102 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 281b22bc5..1e5dfd175 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -572,6 +572,17 @@ class FreqtradeBot(object): trade.update(order) + # Check if stoploss on exchnage is hit first + if self.strategy.stoploss_on_exchange and trade.stoploss_order_id: + # Check if stoploss is hit + result = self.handle_stoploss_on_exchage(trade) + + # Updating wallets if stoploss is hit + if result: + self.wallets.update() + + return result + if trade.is_open and trade.open_order_id is None: # Check if we can sell our current pair result = self.handle_trade(trade) @@ -676,6 +687,19 @@ class FreqtradeBot(object): logger.info('Found no sell signals for whitelisted currencies. Trying again..') return False + def handle_stoploss_on_exchage(self, trade: Trade) -> bool: + if not trade.is_open: + raise ValueError(f'attempt to handle stoploss on exchnage for a closed trade: {trade}') + + logger.debug('Handling stoploss on exchange %s ...', trade) + order = self.exchange.get_order(trade.stoploss_order_id, trade.pair) + if order['status'] == 'closed': + trade.sell_reason = SellType.STOPLOSS_ON_EXCHNAGE.value + trade.update(order) + return True + else: + return False + def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool: if self.edge: stoploss = self.edge.stoploss(trade.pair) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index db6d526c7..02caeeccd 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -178,7 +178,7 @@ class Trade(_DECL_BASE): # absolute value of the initial stop loss initial_stop_loss = Column(Float, nullable=True, default=0.0) # absolute value of the highest reached price - stoploss_order_id = Column(Integer, nullable=True, index=True) + stoploss_order_id = Column(String, nullable=True, index=True) max_rate = Column(Float, nullable=True, default=0.0) sell_reason = Column(String, nullable=True) strategy = Column(String, nullable=True) @@ -250,6 +250,9 @@ class Trade(_DECL_BASE): self.open_order_id = None elif order_type == 'limit' and order['side'] == 'sell': self.close(order['price']) + elif order_type == 'stop_loss_limit': + self.stoploss_order_id = None + self.close(order['price']) else: raise ValueError(f'Unknown order type: {order_type}') cleanup() diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 30fc62f42..d1e22850c 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -33,6 +33,7 @@ class SellType(Enum): """ ROI = "roi" STOP_LOSS = "stop_loss" + STOPLOSS_ON_EXCHNAGE = "stoploss_on_exchange" TRAILING_STOP_LOSS = "trailing_stop_loss" SELL_SIGNAL = "sell_signal" FORCE_SELL = "force_sell" diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index aad5c371f..48918645d 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -931,7 +931,7 @@ def test_execute_buy_with_stoploss_on_exchange(mocker, default_conf, trade = Trade.query.first() assert trade.is_open is True - assert trade.stoploss_order_id == 13434334 + assert trade.stoploss_order_id == '13434334' def test_process_maybe_execute_buy(mocker, default_conf) -> None: @@ -1572,7 +1572,8 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, get_ticker=ticker_sell_up ) - freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) + freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], + sell_reason=SellType.SELL_SIGNAL) trade = Trade.query.first() assert trade @@ -1580,6 +1581,76 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, assert rpc_mock.call_count == 2 +def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, + ticker, fee, + limit_buy_order, + markets, mocker) -> None: + default_conf['exchange']['name'] = 'binance' + rpc_mock = patch_RPCManager(mocker) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + _load_markets=MagicMock(return_value={}), + get_ticker=ticker, + get_fee=fee, + get_markets=markets + ) + + stoploss_limit = MagicMock(return_value={ + 'id': 123, + 'info': { + 'foo': 'bar' + } + }) + + mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) + mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) + mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit) + + freqtrade = FreqtradeBot(default_conf) + freqtrade.strategy.stoploss_on_exchange = True + patch_get_signal(freqtrade) + + # Create some test data + freqtrade.create_trade() + + trade = Trade.query.first() + assert trade + assert trade.stoploss_order_id == '123' + assert trade.open_order_id is not None + + trade.update(limit_buy_order) + + # Assuming stoploss on exchnage is hit + # stoploss_order_id should become None + # and trade should be sold at the price of stoploss + stoploss_limit_executed = MagicMock(return_value={ + "id": "123", + "timestamp": 1542707426845, + "datetime": "2018-11-20T09:50:26.845Z", + "lastTradeTimestamp": None, + "symbol": "BTC/USDT", + "type": "stop_loss_limit", + "side": "sell", + "price": 1.08801, + "amount": 90.99181074, + "cost": 99.0000000032274, + "average": 1.08801, + "filled": 90.99181074, + "remaining": 0.0, + "status": "closed", + "fee": None, + "trades": None + }) + mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_limit_executed) + + freqtrade.process_maybe_execute_sell(trade) + assert trade.stoploss_order_id is None + assert trade.is_open is False + print(trade.sell_reason) + assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHNAGE.value + assert rpc_mock.call_count == 1 + + def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, mocker) -> None: rpc_mock = patch_RPCManager(mocker) From 5ee2faa182fd8c5fd470e50febf266648fbbaf21 Mon Sep 17 00:00:00 2001 From: misagh Date: Fri, 23 Nov 2018 19:17:36 +0100 Subject: [PATCH 043/103] adding stop loss on exchange after the buy order is fulfilled not before. --- freqtrade/freqtradebot.py | 41 +++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1e5dfd175..c2ef0e406 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -479,22 +479,6 @@ class FreqtradeBot(object): order_id = self.exchange.buy(pair=pair, ordertype=self.strategy.order_types['buy'], amount=amount, rate=buy_limit)['id'] - stoploss_order_id = None - - # Check if stoploss should be added on exchange - # If True then here immediately after buy we should - # Add the stoploss order - if self.strategy.stoploss_on_exchange: - stoploss = self.edge.stoploss(pair=pair) if self.edge else self.strategy.stoploss - stop_price = buy_limit * (1 + stoploss) - - # limit price should be less than stop price. - # 0.98 is arbitrary here. - limit_price = stop_price * 0.98 - - stoploss_order_id = self.exchange.stoploss_limit( - pair=pair, amount=amount, stop_price=stop_price, rate=limit_price)['id'] - self.rpc.send_msg({ 'type': RPCMessageType.BUY_NOTIFICATION, 'exchange': self.exchange.name.capitalize(), @@ -519,7 +503,6 @@ class FreqtradeBot(object): open_date=datetime.utcnow(), exchange=self.exchange.id, open_order_id=order_id, - stoploss_order_id=stoploss_order_id, strategy=self.strategy.get_strategy_name(), ticker_interval=constants.TICKER_INTERVAL_MINUTES[self.config['ticker_interval']] ) @@ -572,16 +555,32 @@ class FreqtradeBot(object): trade.update(order) - # Check if stoploss on exchnage is hit first - if self.strategy.stoploss_on_exchange and trade.stoploss_order_id: + # Check uf trade is fulfulled in which case the stoploss + # on exchange should be added immediately if stoploss on exchnage + # is on + if self.strategy.stoploss_on_exchange and trade.is_open and \ + trade.open_order_id is None and trade.stoploss_order_id is None: + + stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss + stop_price = trade.open_rate * (1 + stoploss) + + # limit price should be less than stop price. + # 0.98 is arbitrary here. + limit_price = stop_price * 0.98 + + stoploss_order_id = self.exchange.stoploss_limit( + pair=trade.pair, amount=trade.amount, stop_price=stop_price, rate=limit_price)['id'] + trade.stoploss_order_id = stoploss_order_id + + # Or Check if there is a stoploss on exchnage and it is hit + elif self.strategy.stoploss_on_exchange and trade.stoploss_order_id: # Check if stoploss is hit result = self.handle_stoploss_on_exchage(trade) # Updating wallets if stoploss is hit if result: self.wallets.update() - - return result + return result if trade.is_open and trade.open_order_id is None: # Check if we can sell our current pair From 9144a8f79df3c3839e294e4d2285a2a123aa3993 Mon Sep 17 00:00:00 2001 From: misagh Date: Fri, 23 Nov 2018 20:28:01 +0100 Subject: [PATCH 044/103] tests fixed --- freqtrade/freqtradebot.py | 14 ++++-- freqtrade/tests/test_freqtradebot.py | 70 +++++++++------------------- 2 files changed, 31 insertions(+), 53 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c2ef0e406..d4ea83d25 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -559,9 +559,13 @@ class FreqtradeBot(object): # on exchange should be added immediately if stoploss on exchnage # is on if self.strategy.stoploss_on_exchange and trade.is_open and \ - trade.open_order_id is None and trade.stoploss_order_id is None: + trade.open_order_id is None and trade.stoploss_order_id is None: + + if self.edge: + stoploss = self.edge.stoploss(pair=trade.pair) + else: + stoploss = self.strategy.stoploss - stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss stop_price = trade.open_rate * (1 + stoploss) # limit price should be less than stop price. @@ -569,8 +573,10 @@ class FreqtradeBot(object): limit_price = stop_price * 0.98 stoploss_order_id = self.exchange.stoploss_limit( - pair=trade.pair, amount=trade.amount, stop_price=stop_price, rate=limit_price)['id'] - trade.stoploss_order_id = stoploss_order_id + pair=trade.pair, amount=trade.amount, stop_price=stop_price, rate=limit_price + )['id'] + + trade.stoploss_order_id = str(stoploss_order_id) # Or Check if there is a stoploss on exchnage and it is hit elif self.strategy.stoploss_on_exchange and trade.stoploss_order_id: diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 48918645d..64c8e9765 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -881,57 +881,29 @@ def test_execute_buy(mocker, default_conf, fee, markets, limit_buy_order) -> Non assert call_args['amount'] == stake_amount / fix_price -def test_execute_buy_with_stoploss_on_exchange(mocker, default_conf, - fee, markets, limit_buy_order) -> None: - default_conf['exchange']['name'] = 'binance' +def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None: patch_RPCManager(mocker) - patch_exchange(mocker, id='binance') + mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) + mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) + mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) + mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', + return_value=limit_buy_order['amount']) + + stoploss_limit = MagicMock(return_value={'id': 13434334}) + mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit) + freqtrade = FreqtradeBot(default_conf) freqtrade.strategy.stoploss_on_exchange = True - freqtrade.strategy.stoploss = -0.05 - stake_amount = 2 - bid = 0.11 - get_bid = MagicMock(return_value=bid) - mocker.patch.multiple( - 'freqtrade.freqtradebot.FreqtradeBot', - get_target_bid=get_bid, - _get_min_pair_stake_amount=MagicMock(return_value=1) - ) - buy_mm = MagicMock(return_value={'id': limit_buy_order['id']}) - stoploss_limit = MagicMock(return_value={'id': 13434334}) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ - 'bid': 0.00001172, - 'ask': 0.00001173, - 'last': 0.00001172 - }), - buy=buy_mm, - get_fee=fee, - get_markets=markets, - stoploss_limit=stoploss_limit - ) - pair = 'ETH/BTC' - print(buy_mm.call_args_list) - assert freqtrade.execute_buy(pair, stake_amount) - assert stoploss_limit.call_count == 1 - assert get_bid.call_count == 1 - assert buy_mm.call_count == 1 - call_args = buy_mm.call_args_list[0][1] - assert call_args['pair'] == pair - assert call_args['rate'] == bid - assert call_args['amount'] == stake_amount / bid + trade = MagicMock() + trade.open_order_id = None + trade.stoploss_order_id = None + trade.is_open = True - call_args = stoploss_limit.call_args_list[0][1] - assert call_args['pair'] == pair - assert call_args['amount'] == stake_amount / bid - assert call_args['stop_price'] == 0.11 * 0.95 - assert call_args['rate'] == 0.11 * 0.95 * 0.98 - - trade = Trade.query.first() - assert trade.is_open is True + freqtrade.process_maybe_execute_sell(trade) assert trade.stoploss_order_id == '13434334' + assert stoploss_limit.call_count == 1 + assert trade.is_open is True def test_process_maybe_execute_buy(mocker, default_conf) -> None: @@ -1566,6 +1538,8 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, trade = Trade.query.first() assert trade + freqtrade.process_maybe_execute_sell(trade) + # Increase the price and sell it mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -1612,13 +1586,11 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, # Create some test data freqtrade.create_trade() - trade = Trade.query.first() + freqtrade.process_maybe_execute_sell(trade) assert trade assert trade.stoploss_order_id == '123' - assert trade.open_order_id is not None - - trade.update(limit_buy_order) + assert trade.open_order_id is None # Assuming stoploss on exchnage is hit # stoploss_order_id should become None From 1c2c19b12cfa60951e2169c77277452e412374b0 Mon Sep 17 00:00:00 2001 From: misagh Date: Fri, 23 Nov 2018 20:47:17 +0100 Subject: [PATCH 045/103] the complex in the life of flake8 resolved --- freqtrade/freqtradebot.py | 69 ++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 37 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d4ea83d25..3d52ffffa 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -555,35 +555,8 @@ class FreqtradeBot(object): trade.update(order) - # Check uf trade is fulfulled in which case the stoploss - # on exchange should be added immediately if stoploss on exchnage - # is on - if self.strategy.stoploss_on_exchange and trade.is_open and \ - trade.open_order_id is None and trade.stoploss_order_id is None: - - if self.edge: - stoploss = self.edge.stoploss(pair=trade.pair) - else: - stoploss = self.strategy.stoploss - - stop_price = trade.open_rate * (1 + stoploss) - - # limit price should be less than stop price. - # 0.98 is arbitrary here. - limit_price = stop_price * 0.98 - - stoploss_order_id = self.exchange.stoploss_limit( - pair=trade.pair, amount=trade.amount, stop_price=stop_price, rate=limit_price - )['id'] - - trade.stoploss_order_id = str(stoploss_order_id) - - # Or Check if there is a stoploss on exchnage and it is hit - elif self.strategy.stoploss_on_exchange and trade.stoploss_order_id: - # Check if stoploss is hit + if self.strategy.stoploss_on_exchange: result = self.handle_stoploss_on_exchage(trade) - - # Updating wallets if stoploss is hit if result: self.wallets.update() return result @@ -693,18 +666,40 @@ class FreqtradeBot(object): return False def handle_stoploss_on_exchage(self, trade: Trade) -> bool: - if not trade.is_open: - raise ValueError(f'attempt to handle stoploss on exchnage for a closed trade: {trade}') + # Check uf trade is fulfulled in which case the stoploss + # on exchange should be added immediately if stoploss on exchnage + # is on + if trade.is_open and trade.open_order_id is None and trade.stoploss_order_id is None: + if self.edge: + stoploss = self.edge.stoploss(pair=trade.pair) + else: + stoploss = self.strategy.stoploss - logger.debug('Handling stoploss on exchange %s ...', trade) - order = self.exchange.get_order(trade.stoploss_order_id, trade.pair) - if order['status'] == 'closed': - trade.sell_reason = SellType.STOPLOSS_ON_EXCHNAGE.value - trade.update(order) - return True - else: + stop_price = trade.open_rate * (1 + stoploss) + + # limit price should be less than stop price. + # 0.98 is arbitrary here. + limit_price = stop_price * 0.98 + + stoploss_order_id = self.exchange.stoploss_limit( + pair=trade.pair, amount=trade.amount, stop_price=stop_price, rate=limit_price + )['id'] + + trade.stoploss_order_id = str(stoploss_order_id) return False + # Or Check if there is a stoploss on exchnage and it is hit + elif trade.stoploss_order_id: + logger.debug('Handling stoploss on exchange %s ...', trade) + order = self.exchange.get_order(trade.stoploss_order_id, trade.pair) + if order['status'] == 'closed': + trade.sell_reason = SellType.STOPLOSS_ON_EXCHNAGE.value + trade.update(order) + return True + else: + return False + + def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool: if self.edge: stoploss = self.edge.stoploss(trade.pair) From 89eb3d9f36d960d8862bf4fb66b02f2e12a4d7c4 Mon Sep 17 00:00:00 2001 From: misagh Date: Fri, 23 Nov 2018 20:49:00 +0100 Subject: [PATCH 046/103] blank line removed --- freqtrade/freqtradebot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 3d52ffffa..bd4e1b9e7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -699,7 +699,6 @@ class FreqtradeBot(object): else: return False - def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool: if self.edge: stoploss = self.edge.stoploss(trade.pair) From dedf1ff70340df99647d0f30cc556dcaedc3c291 Mon Sep 17 00:00:00 2001 From: misagh Date: Fri, 23 Nov 2018 20:51:23 +0100 Subject: [PATCH 047/103] refactoring --- freqtrade/freqtradebot.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index bd4e1b9e7..335a0f76e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -669,6 +669,7 @@ class FreqtradeBot(object): # Check uf trade is fulfulled in which case the stoploss # on exchange should be added immediately if stoploss on exchnage # is on + result = False if trade.is_open and trade.open_order_id is None and trade.stoploss_order_id is None: if self.edge: stoploss = self.edge.stoploss(pair=trade.pair) @@ -684,9 +685,7 @@ class FreqtradeBot(object): stoploss_order_id = self.exchange.stoploss_limit( pair=trade.pair, amount=trade.amount, stop_price=stop_price, rate=limit_price )['id'] - trade.stoploss_order_id = str(stoploss_order_id) - return False # Or Check if there is a stoploss on exchnage and it is hit elif trade.stoploss_order_id: @@ -695,9 +694,10 @@ class FreqtradeBot(object): if order['status'] == 'closed': trade.sell_reason = SellType.STOPLOSS_ON_EXCHNAGE.value trade.update(order) - return True + result = True else: - return False + result = False + return result def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool: if self.edge: From 1a8e9ebc0f8e60239015fc17dbde7ed7bcd0f77d Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 24 Nov 2018 16:53:10 +0100 Subject: [PATCH 048/103] stoploss_order_id added to migration script --- freqtrade/persistence.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 02caeeccd..364af06ce 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -91,6 +91,7 @@ def check_migrate(engine) -> None: close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null') stop_loss = get_column_def(cols, 'stop_loss', '0.0') initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0') + stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null') max_rate = get_column_def(cols, 'max_rate', '0.0') sell_reason = get_column_def(cols, 'sell_reason', 'null') strategy = get_column_def(cols, 'strategy', 'null') @@ -106,7 +107,7 @@ def check_migrate(engine) -> None: (id, exchange, pair, is_open, fee_open, fee_close, open_rate, open_rate_requested, close_rate, close_rate_requested, close_profit, stake_amount, amount, open_date, close_date, open_order_id, - stop_loss, initial_stop_loss, max_rate, sell_reason, strategy, + stop_loss, initial_stop_loss, stoploss_order_id, max_rate, sell_reason, strategy, ticker_interval ) select id, lower(exchange), @@ -122,7 +123,8 @@ def check_migrate(engine) -> None: {close_rate_requested} close_rate_requested, close_profit, stake_amount, amount, open_date, close_date, open_order_id, {stop_loss} stop_loss, {initial_stop_loss} initial_stop_loss, - {max_rate} max_rate, {sell_reason} sell_reason, {strategy} strategy, + {stoploss_order_id} stoploss_order_id, {max_rate} max_rate, + {sell_reason} sell_reason, {strategy} strategy, {ticker_interval} ticker_interval from {table_back_name} """) @@ -177,8 +179,9 @@ class Trade(_DECL_BASE): stop_loss = Column(Float, nullable=True, default=0.0) # absolute value of the initial stop loss initial_stop_loss = Column(Float, nullable=True, default=0.0) - # absolute value of the highest reached price + # stoploss order id which is on exchange stoploss_order_id = Column(String, nullable=True, index=True) + # absolute value of the highest reached price max_rate = Column(Float, nullable=True, default=0.0) sell_reason = Column(String, nullable=True) strategy = Column(String, nullable=True) From a9ec5c66993a00bb8e7d6ec57b3a65079d419144 Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 24 Nov 2018 17:07:35 +0100 Subject: [PATCH 049/103] simplifying if conditions --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 335a0f76e..f197d9117 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -670,7 +670,7 @@ class FreqtradeBot(object): # on exchange should be added immediately if stoploss on exchnage # is on result = False - if trade.is_open and trade.open_order_id is None and trade.stoploss_order_id is None: + if trade.is_open and not trade.open_order_id and not trade.stoploss_order_id: if self.edge: stoploss = self.edge.stoploss(pair=trade.pair) else: From afd0a054b2a6bfdac724d61f6e4a23c80cd0bc71 Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 24 Nov 2018 17:08:12 +0100 Subject: [PATCH 050/103] typo corrected --- freqtrade/freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f197d9117..a8736d6f1 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -556,7 +556,7 @@ class FreqtradeBot(object): trade.update(order) if self.strategy.stoploss_on_exchange: - result = self.handle_stoploss_on_exchage(trade) + result = self.handle_stoploss_on_exchange(trade) if result: self.wallets.update() return result @@ -665,7 +665,7 @@ class FreqtradeBot(object): logger.info('Found no sell signals for whitelisted currencies. Trying again..') return False - def handle_stoploss_on_exchage(self, trade: Trade) -> bool: + def handle_stoploss_on_exchange(self, trade: Trade) -> bool: # Check uf trade is fulfulled in which case the stoploss # on exchange should be added immediately if stoploss on exchnage # is on From 531d9ecd0c8f15f9bf7721d6d0b691486a240740 Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 24 Nov 2018 17:10:51 +0100 Subject: [PATCH 051/103] docstring added --- freqtrade/freqtradebot.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a8736d6f1..369bed173 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -666,9 +666,12 @@ class FreqtradeBot(object): return False def handle_stoploss_on_exchange(self, trade: Trade) -> bool: - # Check uf trade is fulfulled in which case the stoploss - # on exchange should be added immediately if stoploss on exchnage - # is on + """ + Check if trade is fulfilled in which case the stoploss + on exchange should be added immediately if stoploss on exchnage + is enabled. + """ + result = False if trade.is_open and not trade.open_order_id and not trade.stoploss_order_id: if self.edge: From 870631f324eb785fb0faf12c4544b742dc59cb09 Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 24 Nov 2018 17:32:25 +0100 Subject: [PATCH 052/103] 1) comments added to handle_sl 2) dry-run force price removed --- freqtrade/freqtradebot.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 369bed173..cf22ee52a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -673,6 +673,9 @@ class FreqtradeBot(object): """ result = False + + # If trade is open and the buy order is fulfilled but there is no stoploss, + # then we add a stoploss on exchange if trade.is_open and not trade.open_order_id and not trade.stoploss_order_id: if self.edge: stoploss = self.edge.stoploss(pair=trade.pair) @@ -690,7 +693,8 @@ class FreqtradeBot(object): )['id'] trade.stoploss_order_id = str(stoploss_order_id) - # Or Check if there is a stoploss on exchnage and it is hit + # Or there is already a stoploss on exchnage. + # so we check if it is hit ... elif trade.stoploss_order_id: logger.debug('Handling stoploss on exchange %s ...', trade) order = self.exchange.get_order(trade.stoploss_order_id, trade.pair) @@ -831,11 +835,6 @@ class FreqtradeBot(object): if self.strategy.stoploss_on_exchange and trade.stoploss_order_id: self.exchange.cancel_order(trade.stoploss_order_id, trade.pair) - # Dry-run should consider stoploss is executed at the limit price - # So overriding limit in case of dry-run - if self.config['dry_run']: - limit = trade.stop_loss - # Execute sell and update trade record order_id = self.exchange.sell(pair=str(trade.pair), ordertype=self.strategy.order_types[sell_type], From 000711b0256e541bf8dada406ae6ad8d77c24701 Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 24 Nov 2018 18:08:11 +0100 Subject: [PATCH 053/103] added stoploss_limit_order for dry-run --- freqtrade/tests/exchange/test_exchange.py | 36 +++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 53e77d7b6..402596da8 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1214,3 +1214,39 @@ def test_stoploss_limit_order(default_conf, mocker): assert api_mock.create_order.call_args[0][3] == 1 assert api_mock.create_order.call_args[0][4] == 200 assert api_mock.create_order.call_args[0][5] == {'stopPrice': 220} + + +def test_stoploss_limit_order_dry_run(default_conf, mocker): + api_mock = MagicMock() + order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) + order_type = 'stop_loss' + + api_mock.create_order = MagicMock(return_value={ + 'id': order_id, + 'info': { + 'foo': 'bar' + } + }) + + default_conf['dry_run'] = True + mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) + mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) + + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') + + with pytest.raises(OperationalException): + order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200) + + api_mock.create_order.reset_mock() + + order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) + + assert 'id' in order + assert 'info' in order + assert order['id'] == order_id + assert api_mock.create_order.call_args[0][0] == 'ETH/BTC' + assert api_mock.create_order.call_args[0][1] == order_type + assert api_mock.create_order.call_args[0][2] == 'sell' + assert api_mock.create_order.call_args[0][3] == 1 + assert api_mock.create_order.call_args[0][4] == 200 + assert api_mock.create_order.call_args[0][5] == {'stopPrice': 220} From b2c0b20a58c161961d13bb1fa34494516f93332e Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 24 Nov 2018 18:26:04 +0100 Subject: [PATCH 054/103] added real tests for stop on exchange in dry-run --- freqtrade/exchange/__init__.py | 17 +++++++++++++++++ freqtrade/tests/exchange/test_exchange.py | 23 ++++++----------------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index a88fb6ee8..fc28516f4 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -349,6 +349,23 @@ class Exchange(object): raise OperationalException( 'In stoploss limit order, stop price should be more than limit price') + if self._conf['dry_run']: + order_id = f'dry_run_buy_{randint(0, 10**6)}' + self._dry_run_open_orders[order_id] = { + 'info': {}, + 'id': order_id, + 'pair': pair, + 'price': stop_price, + 'amount': amount, + 'type': 'stop_loss_limit', + 'side': 'sell', + 'remaining': amount, + 'datetime': arrow.utcnow().isoformat(), + 'status': 'open', + 'fee': None + } + return self._dry_run_open_orders[order_id] + return self._api.create_order(pair, 'stop_loss', 'sell', amount, rate, {'stopPrice': stop_price}) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 402596da8..5ae6a031a 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1218,16 +1218,7 @@ def test_stoploss_limit_order(default_conf, mocker): def test_stoploss_limit_order_dry_run(default_conf, mocker): api_mock = MagicMock() - order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) - order_type = 'stop_loss' - - api_mock.create_order = MagicMock(return_value={ - 'id': order_id, - 'info': { - 'foo': 'bar' - } - }) - + order_type = 'stop_loss_limit' default_conf['dry_run'] = True mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) @@ -1243,10 +1234,8 @@ def test_stoploss_limit_order_dry_run(default_conf, mocker): assert 'id' in order assert 'info' in order - assert order['id'] == order_id - assert api_mock.create_order.call_args[0][0] == 'ETH/BTC' - assert api_mock.create_order.call_args[0][1] == order_type - assert api_mock.create_order.call_args[0][2] == 'sell' - assert api_mock.create_order.call_args[0][3] == 1 - assert api_mock.create_order.call_args[0][4] == 200 - assert api_mock.create_order.call_args[0][5] == {'stopPrice': 220} + assert 'type' in order + + assert order['type'] == order_type + assert order['price'] == 220 + assert order['amount'] == 1 From fe8927136c6427c3351017256489c9d18a2c2094 Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 24 Nov 2018 18:36:07 +0100 Subject: [PATCH 055/103] typo --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index cf22ee52a..126cc485f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -693,7 +693,7 @@ class FreqtradeBot(object): )['id'] trade.stoploss_order_id = str(stoploss_order_id) - # Or there is already a stoploss on exchnage. + # Or there is already a stoploss on exchange. # so we check if it is hit ... elif trade.stoploss_order_id: logger.debug('Handling stoploss on exchange %s ...', trade) From b5192880df34ef09e6eb8b25dce0d51a64a4a777 Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 24 Nov 2018 19:00:59 +0100 Subject: [PATCH 056/103] [WIP] adding tests for handle_stoploss_on_exchange. --- freqtrade/tests/test_freqtradebot.py | 76 ++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 64c8e9765..4ecd287d6 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -906,6 +906,82 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None assert trade.is_open is True +def test_handle_stoploss_on_exchange(mocker, default_conf, fee, + markets, limit_buy_order, limit_sell_order) -> None: + + freqtrade = get_patched_freqtradebot(mocker, default_conf) + stoploss_limit = MagicMock(return_value={'id': 13434334}) + mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit) + + # First case: when stoploss is not yet set but the order is open + # should get the stoploss order id immediately + # and should return false as no trade actually happened + trade = MagicMock() + trade.is_open = True + trade.open_order_id = None + trade.stoploss_order_id = None + + assert freqtrade.handle_stoploss_on_exchange(trade) is False + assert stoploss_limit.call_count == 1 + assert trade.stoploss_order_id == "13434334" + + trade.reset_mock() + + # Second case: when stoploss is set but it is not yet hit + # should do nothing and return false + trade.is_open = True + trade.open_order_id = None + trade.stoploss_order_id = 100 + + hanging_stoploss_order = MagicMock(return_value={'status': 'open'}) + mocker.patch('freqtrade.exchange.Exchange.get_order', hanging_stoploss_order) + + assert freqtrade.handle_stoploss_on_exchange(trade) is False + assert trade.stoploss_order_id == 100 + + trade.reset_mock() + + # Third case: when stoploss is set and it is hit + # should unset stoploss_order_id and return true + # as a trade actually happened + patch_RPCManager(mocker) + patch_exchange(mocker) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=MagicMock(return_value={ + 'bid': 0.00001172, + 'ask': 0.00001173, + 'last': 0.00001172 + }), + buy=MagicMock(return_value={'id': limit_buy_order['id']}), + sell=MagicMock(return_value={'id': limit_sell_order['id']}), + get_fee=fee, + get_markets=markets + ) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + + freqtrade.create_trade() + + trade = Trade.query.first() + assert trade + + # trade = freqtrade.create + # trade.is_open = True + # trade.open_order_id = None + # trade.stoploss_order_id = 100 + + # stoploss_order_hit = MagicMock(return_value={'status': 'closed'}) + # mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hit) + + # # trade = Trade.query.first() + # # assert trade + # assert freqtrade.handle_stoploss_on_exchange(trade) is True + # time.sleep(0.01) # Race condition fix + # assert trade.is_open is True + # assert trade.stoploss_order_id is None + + def test_process_maybe_execute_buy(mocker, default_conf) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) From c8a0956e1bc34fbb418de13d08eac686b3cd6581 Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 24 Nov 2018 19:12:00 +0100 Subject: [PATCH 057/103] fixed test handle_stoploss_on_exchange --- freqtrade/tests/test_freqtradebot.py | 68 ++++++++++++---------------- 1 file changed, 29 insertions(+), 39 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 4ecd287d6..9b1f40ccf 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -908,10 +908,24 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None def test_handle_stoploss_on_exchange(mocker, default_conf, fee, markets, limit_buy_order, limit_sell_order) -> None: - - freqtrade = get_patched_freqtradebot(mocker, default_conf) stoploss_limit = MagicMock(return_value={'id': 13434334}) - mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit) + patch_RPCManager(mocker) + patch_exchange(mocker) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=MagicMock(return_value={ + 'bid': 0.00001172, + 'ask': 0.00001173, + 'last': 0.00001172 + }), + buy=MagicMock(return_value={'id': limit_buy_order['id']}), + sell=MagicMock(return_value={'id': limit_sell_order['id']}), + get_fee=fee, + get_markets=markets, + stoploss_limit=stoploss_limit + ) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) # First case: when stoploss is not yet set but the order is open # should get the stoploss order id immediately @@ -925,8 +939,6 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, assert stoploss_limit.call_count == 1 assert trade.stoploss_order_id == "13434334" - trade.reset_mock() - # Second case: when stoploss is set but it is not yet hit # should do nothing and return false trade.is_open = True @@ -939,47 +951,25 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, assert freqtrade.handle_stoploss_on_exchange(trade) is False assert trade.stoploss_order_id == 100 - trade.reset_mock() - # Third case: when stoploss is set and it is hit # should unset stoploss_order_id and return true # as a trade actually happened - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ - 'bid': 0.00001172, - 'ask': 0.00001173, - 'last': 0.00001172 - }), - buy=MagicMock(return_value={'id': limit_buy_order['id']}), - sell=MagicMock(return_value={'id': limit_sell_order['id']}), - get_fee=fee, - get_markets=markets - ) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - freqtrade.create_trade() - trade = Trade.query.first() + trade.is_open = True + trade.open_order_id = None + trade.stoploss_order_id = 100 assert trade - # trade = freqtrade.create - # trade.is_open = True - # trade.open_order_id = None - # trade.stoploss_order_id = 100 - - # stoploss_order_hit = MagicMock(return_value={'status': 'closed'}) - # mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hit) - - # # trade = Trade.query.first() - # # assert trade - # assert freqtrade.handle_stoploss_on_exchange(trade) is True - # time.sleep(0.01) # Race condition fix - # assert trade.is_open is True - # assert trade.stoploss_order_id is None + stoploss_order_hit = MagicMock(return_value={ + 'status': 'closed', + 'type': 'stop_loss_limit', + 'price': 2 + }) + mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hit) + assert freqtrade.handle_stoploss_on_exchange(trade) is True + assert trade.stoploss_order_id is None + assert trade.is_open is False def test_process_maybe_execute_buy(mocker, default_conf) -> None: From 519b1f00e2932d034afb76ee999f4273b8c7467a Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 24 Nov 2018 20:12:50 +0100 Subject: [PATCH 058/103] adding strategy config consistency function --- freqtrade/freqtradebot.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 126cc485f..dc1189bb9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -54,11 +54,14 @@ class FreqtradeBot(object): # Init objects self.config = config self.strategy: IStrategy = StrategyResolver(self.config).strategy + self.check_strategy_config_consistency(config, self.strategy) + self.rpc: RPCManager = RPCManager(self) self.persistence = None self.exchange = Exchange(self.config) self.wallets = Wallets(self.exchange) + # Initializing Edge only if enabled self.edge = Edge(self.config, self.exchange, self.strategy) if \ self.config.get('edge', {}).get('enabled', False) else None @@ -66,6 +69,16 @@ class FreqtradeBot(object): self.active_pair_whitelist: List[str] = self.config['exchange']['pair_whitelist'] self._init_modules() + def check_strategy_config_consistency(self, config, strategy: IStrategy) -> None: + """ + checks if config is compatible with the given strategy + """ + + # Stoploss on exchange is only implemented for binance + if strategy.stoploss_on_exchange and config.get('exchange') is not 'binance': + raise OperationalException( + 'Stoploss limit orders are implemented only for binance as of now.') + def _init_modules(self) -> None: """ Initializes all modules and updates the config From 266bd7b9b62d095d7f5d703e2cd8f27439102abd Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 24 Nov 2018 21:42:15 +0100 Subject: [PATCH 059/103] error message improved --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index dc1189bb9..fbabb7d90 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -77,7 +77,7 @@ class FreqtradeBot(object): # Stoploss on exchange is only implemented for binance if strategy.stoploss_on_exchange and config.get('exchange') is not 'binance': raise OperationalException( - 'Stoploss limit orders are implemented only for binance as of now.') + 'On exchange stoploss is not supported for %s.' % config.get('exchange')) def _init_modules(self) -> None: """ From 664b96173eabe2e6472e6a812e631aa6378080eb Mon Sep 17 00:00:00 2001 From: misagh Date: Sun, 25 Nov 2018 10:54:36 +0100 Subject: [PATCH 060/103] removing NotImplementedError from stoploss_limit --- freqtrade/exchange/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index fc28516f4..3ccd2369a 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -334,10 +334,10 @@ class Exchange(object): raise OperationalException(e) def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict: - # Only binance is supported - if not self.name == 'Binance': - raise NotImplementedError( - 'Stoploss limit orders are implemented only for binance as of now.') + """ + creates a stoploss limit order. + NOTICE: it is not supported by all exchanges. only binance is tested for now. + """ # Set the precision for amount and price(rate) as accepted by the exchange amount = self.symbol_amount_prec(pair, amount) From dcae3a26440a07cadb362999003a2d81c6486157 Mon Sep 17 00:00:00 2001 From: misagh Date: Sun, 25 Nov 2018 11:29:04 +0100 Subject: [PATCH 061/103] test of check_consistency added --- freqtrade/freqtradebot.py | 5 ++--- freqtrade/tests/exchange/test_exchange.py | 7 ------- freqtrade/tests/test_freqtradebot.py | 6 ++++++ 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index fbabb7d90..005f698dd 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -61,7 +61,6 @@ class FreqtradeBot(object): self.exchange = Exchange(self.config) self.wallets = Wallets(self.exchange) - # Initializing Edge only if enabled self.edge = Edge(self.config, self.exchange, self.strategy) if \ self.config.get('edge', {}).get('enabled', False) else None @@ -73,11 +72,11 @@ class FreqtradeBot(object): """ checks if config is compatible with the given strategy """ - # Stoploss on exchange is only implemented for binance if strategy.stoploss_on_exchange and config.get('exchange') is not 'binance': raise OperationalException( - 'On exchange stoploss is not supported for %s.' % config.get('exchange')) + 'On exchange stoploss is not supported for %s.' % config['exchange']['name'] + ) def _init_modules(self) -> None: """ diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 5ae6a031a..57be54262 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1173,13 +1173,6 @@ def test_get_fee(default_conf, mocker): 'get_fee', 'calculate_fee') -def test_stoploss_limit_available_only_for_binance(default_conf, mocker): - api_mock = MagicMock() - exchange = get_patched_exchange(mocker, default_conf, api_mock) - with pytest.raises(NotImplementedError): - exchange.stoploss_limit('BTC/ETH', 1, 0.8, 0.79) - - def test_stoploss_limit_order(default_conf, mocker): api_mock = MagicMock() order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 9b1f40ccf..5acf4fdcb 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2516,3 +2516,9 @@ def test_startup_messages(default_conf, mocker): default_conf['dynamic_whitelist'] = 20 freqtrade = get_patched_freqtradebot(mocker, default_conf) assert freqtrade.state is State.RUNNING + + +def test_check_consistency(default_conf, mocker, caplog): + mocker.patch('freqtrade.freqtradebot.IStrategy.stoploss_on_exchange', True) + with pytest.raises(OperationalException): + FreqtradeBot(default_conf) From e4744c1ba4cb428fc40da67c9b960e57b0c275b8 Mon Sep 17 00:00:00 2001 From: misagh Date: Sun, 25 Nov 2018 11:31:30 +0100 Subject: [PATCH 062/103] stop loss on exchanged removed from doc --- docs/configuration.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 64f0a2ea6..62559a41e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -26,7 +26,6 @@ The table below will list all configuration parameters. | `process_only_new_candles` | false | No | If set to true indicators are processed only once a new candle arrives. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. Can be set either in Configuration or in the strategy. | `minimal_roi` | See below | No | Set the threshold in percent the bot will use to sell a trade. More information below. If set, this parameter will override `minimal_roi` from your strategy file. | `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file. -| `stoploss_on_exchange` | false | No | Only for binance users for now: If this parameter is on then stoploss limit order is executed immediately after buy order is done on binance. | `trailing_stop` | false | No | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file). | `trailing_stop_positve` | 0 | No | Changes stop-loss once profit has been reached. | `trailing_stop_positve_offset` | 0 | No | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. From 3e29fbb17a49544c77b06e46776f53dcd6f674bc Mon Sep 17 00:00:00 2001 From: misagh Date: Sun, 25 Nov 2018 17:22:56 +0100 Subject: [PATCH 063/103] stoploss on exchange added as a parameter to order_types --- freqtrade/constants.py | 3 ++- freqtrade/exchange/__init__.py | 6 ++++++ freqtrade/freqtradebot.py | 15 ++------------- freqtrade/strategy/default_strategy.py | 3 ++- freqtrade/strategy/interface.py | 10 +++------- freqtrade/tests/exchange/test_exchange.py | 20 +++++++++++++++++++- freqtrade/tests/test_freqtradebot.py | 12 +++--------- 7 files changed, 37 insertions(+), 32 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 055fee3b2..481a219d6 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -109,7 +109,8 @@ CONF_SCHEMA = { 'properties': { 'buy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, - 'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES} + 'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, + 'stoploss_on_exchange': {'type': 'boolean'} }, 'required': ['buy', 'sell', 'stoploss'] }, diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 3ccd2369a..6e826794a 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -227,6 +227,12 @@ class Exchange(object): raise OperationalException( f'Exchange {self.name} does not support market orders.') + if order_types.get('stoploss_on_exchange', False): + if self.name is not 'Binance': + raise OperationalException( + 'On exchange stoploss is not supported for %s.' % self.name + ) + def exchange_has(self, endpoint: str) -> bool: """ Checks if exchange implements a specific API endpoint. diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 005f698dd..d9a15f56a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -54,7 +54,6 @@ class FreqtradeBot(object): # Init objects self.config = config self.strategy: IStrategy = StrategyResolver(self.config).strategy - self.check_strategy_config_consistency(config, self.strategy) self.rpc: RPCManager = RPCManager(self) self.persistence = None @@ -68,16 +67,6 @@ class FreqtradeBot(object): self.active_pair_whitelist: List[str] = self.config['exchange']['pair_whitelist'] self._init_modules() - def check_strategy_config_consistency(self, config, strategy: IStrategy) -> None: - """ - checks if config is compatible with the given strategy - """ - # Stoploss on exchange is only implemented for binance - if strategy.stoploss_on_exchange and config.get('exchange') is not 'binance': - raise OperationalException( - 'On exchange stoploss is not supported for %s.' % config['exchange']['name'] - ) - def _init_modules(self) -> None: """ Initializes all modules and updates the config @@ -567,7 +556,7 @@ class FreqtradeBot(object): trade.update(order) - if self.strategy.stoploss_on_exchange: + if self.strategy.order_types.get('stoploss_on_exchange'): result = self.handle_stoploss_on_exchange(trade) if result: self.wallets.update() @@ -844,7 +833,7 @@ class FreqtradeBot(object): sell_type = 'stoploss' # First cancelling stoploss on exchange ... - if self.strategy.stoploss_on_exchange and trade.stoploss_order_id: + if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id: self.exchange.cancel_order(trade.stoploss_order_id, trade.pair) # Execute sell and update trade record diff --git a/freqtrade/strategy/default_strategy.py b/freqtrade/strategy/default_strategy.py index b282a5938..9c850a8be 100644 --- a/freqtrade/strategy/default_strategy.py +++ b/freqtrade/strategy/default_strategy.py @@ -32,7 +32,8 @@ class DefaultStrategy(IStrategy): order_types = { 'buy': 'limit', 'sell': 'limit', - 'stoploss': 'limit' + 'stoploss': 'limit', + 'stoploss_on_exchange': False } def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index d1e22850c..1073f8028 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -68,11 +68,6 @@ class IStrategy(ABC): # associated stoploss stoploss: float - # if the stoploss should be on exchange. - # if this is True then a stoploss order will be placed - # immediately after a successful buy order. - stoploss_on_exchange: bool = False - # associated ticker interval ticker_interval: str @@ -80,7 +75,8 @@ class IStrategy(ABC): order_types: Dict = { 'buy': 'limit', 'sell': 'limit', - 'stoploss': 'limit' + 'stoploss': 'limit', + 'stoploss_on_exchange': False } # run "populate_indicators" only for new candle @@ -228,7 +224,7 @@ class IStrategy(ABC): current_rate = low or rate current_profit = trade.calc_profit_percent(current_rate) - if self.stoploss_on_exchange: + if self.order_types.get('stoploss_on_exchange'): stoplossflag = SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) else: stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 57be54262..1a46ff001 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -362,7 +362,14 @@ def test_validate_order_types(default_conf, mocker): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) - default_conf['order_types'] = {'buy': 'limit', 'sell': 'limit', 'stoploss': 'market'} + mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex') + default_conf['order_types'] = { + 'buy': 'limit', + 'sell': 'limit', + 'stoploss': 'market', + 'stoploss_on_exchange': False + } + Exchange(default_conf) type(api_mock).has = PropertyMock(return_value={'createMarketOrder': False}) @@ -374,6 +381,17 @@ def test_validate_order_types(default_conf, mocker): match=r'Exchange .* does not support market orders.'): Exchange(default_conf) + default_conf['order_types'] = { + 'buy': 'limit', + 'sell': 'limit', + 'stoploss': 'limit', + 'stoploss_on_exchange': True + } + + with pytest.raises(OperationalException, + match=r'On exchange stoploss is not supported for .*'): + Exchange(default_conf) + def test_validate_order_types_not_in_config(default_conf, mocker): api_mock = MagicMock() diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 5acf4fdcb..b6b42d1da 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -893,7 +893,7 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit) freqtrade = FreqtradeBot(default_conf) - freqtrade.strategy.stoploss_on_exchange = True + freqtrade.strategy.order_types['stoploss_on_exchange'] = True trade = MagicMock() trade.open_order_id = None @@ -1595,7 +1595,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order) freqtrade = FreqtradeBot(default_conf) - freqtrade.strategy.stoploss_on_exchange = True + freqtrade.strategy.order_types['stoploss_on_exchange'] = True patch_get_signal(freqtrade) # Create some test data @@ -1647,7 +1647,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit) freqtrade = FreqtradeBot(default_conf) - freqtrade.strategy.stoploss_on_exchange = True + freqtrade.strategy.order_types['stoploss_on_exchange'] = True patch_get_signal(freqtrade) # Create some test data @@ -2516,9 +2516,3 @@ def test_startup_messages(default_conf, mocker): default_conf['dynamic_whitelist'] = 20 freqtrade = get_patched_freqtradebot(mocker, default_conf) assert freqtrade.state is State.RUNNING - - -def test_check_consistency(default_conf, mocker, caplog): - mocker.patch('freqtrade.freqtradebot.IStrategy.stoploss_on_exchange', True) - with pytest.raises(OperationalException): - FreqtradeBot(default_conf) From 5e1fb11124cf67df36e29876698450714132a51a Mon Sep 17 00:00:00 2001 From: misagh Date: Sun, 25 Nov 2018 17:30:06 +0100 Subject: [PATCH 064/103] documentation added for stop loss on exchange --- config_full.json.example | 3 ++- docs/configuration.md | 9 +++++---- freqtrade/constants.py | 2 +- freqtrade/exchange/__init__.py | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index b0719bcc6..6134e9cad 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -36,7 +36,8 @@ "order_types": { "buy": "limit", "sell": "limit", - "stoploss": "market" + "stoploss": "market", + "stoploss_on_exchange": "false" }, "exchange": { "name": "bittrex", diff --git a/docs/configuration.md b/docs/configuration.md index 62559a41e..03059e261 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -39,7 +39,7 @@ The table below will list all configuration parameters. | `ask_strategy.use_order_book` | false | No | Allows selling of open traded pair using the rates in Order Book Asks. | `ask_strategy.order_book_min` | 0 | No | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. | `ask_strategy.order_book_max` | 0 | No | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. -| `order_types` | None | No | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`). +| `order_types` | None | No | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). | `exchange.name` | bittrex | Yes | Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). | `exchange.key` | key | No | API key to use for the exchange. Only required when you are in production mode. | `exchange.secret` | secret | No | API secret to use for the exchange. Only required when you are in production mode. @@ -141,17 +141,18 @@ end up paying more then would probably have been necessary. ### Understand order_types -`order_types` contains a dict mapping order-types to market-types. This allows to buy using limit orders, sell using limit-orders, and create stoploss orders using market. +`order_types` contains a dict mapping order-types to market-types as well as stoploss on or off exchange type. This allows to buy using limit orders, sell using limit-orders, and create stoploss orders using market. It also allows to set the stoploss "on exchange" which means stoploss order would be placed immediately once the buy order is fulfilled. This can be set in the configuration or in the strategy. Configuration overwrites strategy configurations. -If this is configured, all 3 values (`"buy"`, `"sell"` and `"stoploss"`) need to be present, otherwise the bot warn about it and will fail to start. +If this is configured, all 4 values (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`) need to be present, otherwise the bot warn about it and will fail to start. The below is the default which is used if this is not configured in either Strategy or configuration. ``` json "order_types": { "buy": "limit", "sell": "limit", - "stoploss": "market" + "stoploss": "market", + "stoploss_on_exchange": "false" }, ``` diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 481a219d6..86067d395 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -13,7 +13,7 @@ DEFAULT_HYPEROPT = 'DefaultHyperOpts' DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_DRYRUN_URL = 'sqlite://' UNLIMITED_STAKE_AMOUNT = 'unlimited' -REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss'] +REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange'] ORDERTYPE_POSSIBILITIES = ['limit', 'market'] diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 6e826794a..a311fd666 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -227,7 +227,7 @@ class Exchange(object): raise OperationalException( f'Exchange {self.name} does not support market orders.') - if order_types.get('stoploss_on_exchange', False): + if order_types.get('stoploss_on_exchange'): if self.name is not 'Binance': raise OperationalException( 'On exchange stoploss is not supported for %s.' % self.name From 92930b2343494c7c2aa99b6511fa91466f03fccd Mon Sep 17 00:00:00 2001 From: misagh Date: Sun, 25 Nov 2018 19:03:28 +0100 Subject: [PATCH 065/103] test fixed --- freqtrade/constants.py | 2 +- freqtrade/tests/exchange/test_exchange.py | 2 +- freqtrade/tests/strategy/test_strategy.py | 13 +++++++------ user_data/strategies/test_strategy.py | 5 +++-- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 86067d395..f8fb91240 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -112,7 +112,7 @@ CONF_SCHEMA = { 'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'stoploss_on_exchange': {'type': 'boolean'} }, - 'required': ['buy', 'sell', 'stoploss'] + 'required': ['buy', 'sell', 'stoploss', 'stoploss_on_exchange'] }, 'exchange': {'$ref': '#/definitions/exchange'}, 'edge': {'$ref': '#/definitions/edge'}, diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 1a46ff001..6ad84585c 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -375,7 +375,7 @@ def test_validate_order_types(default_conf, mocker): type(api_mock).has = PropertyMock(return_value={'createMarketOrder': False}) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - default_conf['order_types'] = {'buy': 'limit', 'sell': 'limit', 'stoploss': 'market'} + default_conf['order_types'] = {'buy': 'limit', 'sell': 'limit', 'stoploss': 'market', 'stoploss_on_exchange': 'false'} with pytest.raises(OperationalException, match=r'Exchange .* does not support market orders.'): diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index a38050f24..66d988075 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -188,7 +188,8 @@ def test_strategy_override_order_types(caplog): order_types = { 'buy': 'market', 'sell': 'limit', - 'stoploss': 'limit' + 'stoploss': 'limit', + 'stoploss_on_exchange': True, } config = { @@ -198,13 +199,13 @@ def test_strategy_override_order_types(caplog): resolver = StrategyResolver(config) assert resolver.strategy.order_types - for method in ['buy', 'sell', 'stoploss']: + for method in ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']: assert resolver.strategy.order_types[method] == order_types[method] assert ('freqtrade.strategy.resolver', logging.INFO, "Override strategy 'order_types' with value in config file:" - " {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit'}." + " {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit', 'stoploss_on_exchange': True}." ) in caplog.record_tuples config = { @@ -262,13 +263,13 @@ def test_call_deprecated_function(result, monkeypatch): assert resolver.strategy._sell_fun_len == 2 indicator_df = resolver.strategy.advise_indicators(result, metadata=metadata) - assert type(indicator_df) is DataFrame + assert isinstance(indicator_df, DataFrame) assert 'adx' in indicator_df.columns buydf = resolver.strategy.advise_buy(result, metadata=metadata) - assert type(buydf) is DataFrame + assert isinstance(buydf, DataFrame) assert 'buy' in buydf.columns selldf = resolver.strategy.advise_sell(result, metadata=metadata) - assert type(selldf) is DataFrame + assert isinstance(selldf, DataFrame) assert 'sell' in selldf diff --git a/user_data/strategies/test_strategy.py b/user_data/strategies/test_strategy.py index fd2e9ab75..e7804e683 100644 --- a/user_data/strategies/test_strategy.py +++ b/user_data/strategies/test_strategy.py @@ -7,7 +7,7 @@ from pandas import DataFrame # Add your lib to import here import talib.abstract as ta import freqtrade.vendor.qtpylib.indicators as qtpylib -import numpy # noqa +import numpy # noqa # This class is a sample. Feel free to customize it. @@ -52,7 +52,8 @@ class TestStrategy(IStrategy): order_types = { 'buy': 'limit', 'sell': 'limit', - 'stoploss': 'market' + 'stoploss': 'market', + 'stoploss_on_exchange': False } def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: From a80c984323ef8dec3aa5468ef572eca0ae966260 Mon Sep 17 00:00:00 2001 From: misagh Date: Sun, 25 Nov 2018 19:09:11 +0100 Subject: [PATCH 066/103] flake8 --- freqtrade/tests/exchange/test_exchange.py | 7 ++++++- freqtrade/tests/strategy/test_strategy.py | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 6ad84585c..d7f70d477 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -375,7 +375,12 @@ def test_validate_order_types(default_conf, mocker): type(api_mock).has = PropertyMock(return_value={'createMarketOrder': False}) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - default_conf['order_types'] = {'buy': 'limit', 'sell': 'limit', 'stoploss': 'market', 'stoploss_on_exchange': 'false'} + default_conf['order_types'] = { + 'buy': 'limit', + 'sell': 'limit', + 'stoploss': 'market', + 'stoploss_on_exchange': 'false' + } with pytest.raises(OperationalException, match=r'Exchange .* does not support market orders.'): diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 66d988075..1ad774ffa 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -205,7 +205,8 @@ def test_strategy_override_order_types(caplog): assert ('freqtrade.strategy.resolver', logging.INFO, "Override strategy 'order_types' with value in config file:" - " {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit', 'stoploss_on_exchange': True}." + " {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit'," + " 'stoploss_on_exchange': True}." ) in caplog.record_tuples config = { From 5c257730a8b104e3d9a0d9b7c92d6ecf36337f3c Mon Sep 17 00:00:00 2001 From: misagh Date: Sun, 25 Nov 2018 20:16:53 +0100 Subject: [PATCH 067/103] test added for dry run stop loss sell --- freqtrade/tests/test_freqtradebot.py | 53 +++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index a36ae2cd8..547a048bf 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1422,7 +1422,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, moc } == last_msg -def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, mocker) -> None: +def test_execute_sell_down_live(default_conf, ticker, fee, ticker_sell_down, markets, mocker) -> None: rpc_mock = patch_RPCManager(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -1468,6 +1468,57 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, } == last_msg +def test_execute_sell_down_dry_run(default_conf, ticker, fee, ticker_sell_down, markets, mocker) -> None: + rpc_mock = patch_RPCManager(mocker) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + _load_markets=MagicMock(return_value={}), + get_ticker=ticker, + get_fee=fee, + get_markets=markets + ) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + + # Create some test data + freqtrade.create_trade() + + trade = Trade.query.first() + assert trade + + # Decrease the price and sell it + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=ticker_sell_down + ) + + default_conf['dry_run'] = True + + # Setting trade stoploss to 0.01 + trade.stop_loss = 0.00001099 * 0.99 + + freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], + sell_reason=SellType.STOP_LOSS) + + assert rpc_mock.call_count == 2 + last_msg = rpc_mock.call_args_list[-1][0][0] + assert { + 'type': RPCMessageType.SELL_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'gain': 'loss', + 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', + 'limit': 1.08801e-05, + 'amount': 90.99181073703367, + 'open_rate': 1.099e-05, + 'current_rate': 1.044e-05, + 'profit_amount': -1.498e-05, + 'profit_percent': -0.01493766, + 'stake_currency': 'BTC', + 'fiat_currency': 'USD', + } == last_msg + + def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, mocker) -> None: rpc_mock = patch_RPCManager(mocker) From b57976861852fe75f45e479ed604b2acf184c8eb Mon Sep 17 00:00:00 2001 From: misagh Date: Sun, 25 Nov 2018 20:20:11 +0100 Subject: [PATCH 068/103] dry run set explicitly to False for live stop loss --- freqtrade/tests/test_freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 547a048bf..a418e3e45 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1445,7 +1445,7 @@ def test_execute_sell_down_live(default_conf, ticker, fee, ticker_sell_down, mar 'freqtrade.exchange.Exchange', get_ticker=ticker_sell_down ) - + default_conf['dry_run'] = False freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], sell_reason=SellType.STOP_LOSS) From 6c38bde24aca4d0b293b52a00994515aa64b5eeb Mon Sep 17 00:00:00 2001 From: misagh Date: Sun, 25 Nov 2018 20:21:50 +0100 Subject: [PATCH 069/103] some formatting fixed --- freqtrade/tests/test_freqtradebot.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index a418e3e45..6dd709dbf 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1422,7 +1422,8 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, moc } == last_msg -def test_execute_sell_down_live(default_conf, ticker, fee, ticker_sell_down, markets, mocker) -> None: +def test_execute_sell_down_live(default_conf, ticker, fee, + ticker_sell_down, markets, mocker) -> None: rpc_mock = patch_RPCManager(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -1468,7 +1469,8 @@ def test_execute_sell_down_live(default_conf, ticker, fee, ticker_sell_down, mar } == last_msg -def test_execute_sell_down_dry_run(default_conf, ticker, fee, ticker_sell_down, markets, mocker) -> None: +def test_execute_sell_down_dry_run(default_conf, ticker, fee, + ticker_sell_down, markets, mocker) -> None: rpc_mock = patch_RPCManager(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', From b2634e8e085d8510740bf16c1293067a8e8a6b73 Mon Sep 17 00:00:00 2001 From: misagh Date: Mon, 26 Nov 2018 18:28:13 +0100 Subject: [PATCH 070/103] typo corrected --- freqtrade/freqtradebot.py | 2 +- freqtrade/strategy/interface.py | 2 +- freqtrade/tests/test_freqtradebot.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d9a15f56a..5425e8736 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -700,7 +700,7 @@ class FreqtradeBot(object): logger.debug('Handling stoploss on exchange %s ...', trade) order = self.exchange.get_order(trade.stoploss_order_id, trade.pair) if order['status'] == 'closed': - trade.sell_reason = SellType.STOPLOSS_ON_EXCHNAGE.value + trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value trade.update(order) result = True else: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 1073f8028..141dd996c 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -33,7 +33,7 @@ class SellType(Enum): """ ROI = "roi" STOP_LOSS = "stop_loss" - STOPLOSS_ON_EXCHNAGE = "stoploss_on_exchange" + STOPLOSS_ON_EXCHANGE = "stoploss_on_exchange" TRAILING_STOP_LOSS = "trailing_stop_loss" SELL_SIGNAL = "sell_signal" FORCE_SELL = "force_sell" diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index b6b42d1da..131d7df99 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1685,7 +1685,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, assert trade.stoploss_order_id is None assert trade.is_open is False print(trade.sell_reason) - assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHNAGE.value + assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value assert rpc_mock.call_count == 1 From 17004a5a72a232b3c80389f09de97fe107439d98 Mon Sep 17 00:00:00 2001 From: misagh Date: Mon, 26 Nov 2018 18:29:41 +0100 Subject: [PATCH 071/103] documentation corrected --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 03059e261..e05405aed 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -152,7 +152,7 @@ The below is the default which is used if this is not configured in either Strat "buy": "limit", "sell": "limit", "stoploss": "market", - "stoploss_on_exchange": "false" + "stoploss_on_exchange": False }, ``` From 1f1770ad5a21e5c994fd46041b183fab6db2cc34 Mon Sep 17 00:00:00 2001 From: misagh Date: Mon, 26 Nov 2018 18:46:59 +0100 Subject: [PATCH 072/103] migration script and and error handling on stop loss order --- freqtrade/exchange/__init__.py | 19 ++++++++++++++++++- freqtrade/persistence.py | 2 +- freqtrade/tests/test_persistence.py | 1 + 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index a311fd666..38e2fa317 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -372,9 +372,26 @@ class Exchange(object): } return self._dry_run_open_orders[order_id] - return self._api.create_order(pair, 'stop_loss', 'sell', + try: + return self._api.create_order(pair, 'stop_loss', 'sell', amount, rate, {'stopPrice': stop_price}) + except ccxt.InsufficientFunds as e: + raise DependencyException( + f'Insufficient funds to place stoploss limit order on market {pair}.' + f'Tried to put a stoploss amount {amount} at rate {rate} (total {rate*amount}).' + f'Message: {e}') + except ccxt.InvalidOrder as e: + raise DependencyException( + f'Could not place stoploss limit order on market {pair}.' + f'Tried to place stoploss amount {amount} at rate {rate} (total {rate*amount}).' + f'Message: {e}') + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not place stoploss limit order due to {e.__class__.__name__}. Message: {e}') + except ccxt.BaseError as e: + raise OperationalException(e) + @retrier def get_balance(self, currency: str) -> float: if self._conf['dry_run']: diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 364af06ce..26b0d9d93 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -82,7 +82,7 @@ def check_migrate(engine) -> None: logger.debug(f'trying {table_back_name}') # Check for latest column - if not has_column(cols, 'ticker_interval'): + if not has_column(cols, 'stoploss_order_id'): logger.info(f'Running database migration - backup available as {table_back_name}') fee_open = get_column_def(cols, 'fee_open', 'fee') diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index 5e0647dff..cdfdef6e6 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -426,6 +426,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): max_rate FLOAT, sell_reason VARCHAR, strategy VARCHAR, + ticker_interval INTEGER, PRIMARY KEY (id), CHECK (is_open IN (0, 1)) );""" From b63535083e66a404724968c5edf6c5841c65b87e Mon Sep 17 00:00:00 2001 From: misagh Date: Mon, 26 Nov 2018 18:47:32 +0100 Subject: [PATCH 073/103] flake8 --- freqtrade/exchange/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 38e2fa317..64875af87 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -374,7 +374,7 @@ class Exchange(object): try: return self._api.create_order(pair, 'stop_loss', 'sell', - amount, rate, {'stopPrice': stop_price}) + amount, rate, {'stopPrice': stop_price}) except ccxt.InsufficientFunds as e: raise DependencyException( From 7f6fc7e90f4edde9757d3f794d396916d935da47 Mon Sep 17 00:00:00 2001 From: misagh Date: Mon, 26 Nov 2018 19:13:36 +0100 Subject: [PATCH 074/103] Lost in git ! --- freqtrade/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 7f4022d1f..3ed478ec3 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -45,7 +45,7 @@ def main(sysargv: List[str]) -> None: freqtrade = FreqtradeBot(config) state = None - while True: + while 1: state = freqtrade.worker(old_state=state) if state == State.RELOAD_CONF: freqtrade = reconfigure(freqtrade, args) From 6351fe7a7f927763aa79e28d941d60ab3e9a7fea Mon Sep 17 00:00:00 2001 From: misagh Date: Mon, 26 Nov 2018 20:24:13 +0100 Subject: [PATCH 075/103] test added: stoploss_order_id should be null after migration --- freqtrade/tests/test_persistence.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index cdfdef6e6..d0a209f40 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -472,6 +472,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert trade.sell_reason is None assert trade.strategy is None assert trade.ticker_interval is None + assert trade.stoploss_order_id is None assert log_has("trying trades_bak1", caplog.record_tuples) assert log_has("trying trades_bak2", caplog.record_tuples) assert log_has("Running database migration - backup available as trades_bak2", From 7832fe7074a403fa7417f7cc38c5156ed1fa64a4 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 27 Nov 2018 13:34:08 +0100 Subject: [PATCH 076/103] Update ccxt from 1.17.539 to 1.17.543 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6f969be06..12c6f5cda 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.17.539 +ccxt==1.17.543 SQLAlchemy==1.2.14 python-telegram-bot==11.1.0 arrow==0.12.1 From 7dbf0fed684e9af10da8c2fda45a03b9f1a3bdcb Mon Sep 17 00:00:00 2001 From: misagh Date: Tue, 27 Nov 2018 17:09:51 +0100 Subject: [PATCH 077/103] stop loss limit order type corrected --- freqtrade/exchange/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index a2857a12a..2480dbe32 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -373,7 +373,7 @@ class Exchange(object): return self._dry_run_open_orders[order_id] try: - return self._api.create_order(pair, 'stop_loss', 'sell', + return self._api.create_order(pair, 'stop_loss_limit', 'sell', amount, rate, {'stopPrice': stop_price}) except ccxt.InsufficientFunds as e: From 29f680ec5d6f5bd837788b935e7b27dc3eee77f7 Mon Sep 17 00:00:00 2001 From: misagh Date: Tue, 27 Nov 2018 17:26:06 +0100 Subject: [PATCH 078/103] fix order type test --- freqtrade/tests/exchange/test_exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 63f1008b9..ec7c2acae 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1150,7 +1150,7 @@ def test_get_fee(default_conf, mocker): def test_stoploss_limit_order(default_conf, mocker): api_mock = MagicMock() order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) - order_type = 'stop_loss' + order_type = 'stop_loss_limit' api_mock.create_order = MagicMock(return_value={ 'id': order_id, From 50a384130fe7830cd5c3edc3ab2ecda5dd030f0a Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 28 Nov 2018 13:34:07 +0100 Subject: [PATCH 079/103] Update ccxt from 1.17.543 to 1.17.545 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 12c6f5cda..15c7d1858 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.17.543 +ccxt==1.17.545 SQLAlchemy==1.2.14 python-telegram-bot==11.1.0 arrow==0.12.1 From da94e97c602861492cf03a38eb37c0d6e382e94e Mon Sep 17 00:00:00 2001 From: misagh Date: Wed, 28 Nov 2018 13:58:53 +0100 Subject: [PATCH 080/103] in case trade is not open, then handle_stoploss_on_exchange should not be called --- freqtrade/freqtradebot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index aeb8ce50c..c72451df6 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -554,7 +554,7 @@ class FreqtradeBot(object): trade.update(order) - if self.strategy.order_types.get('stoploss_on_exchange'): + if self.strategy.order_types.get('stoploss_on_exchange') and trade.is_open: result = self.handle_stoploss_on_exchange(trade) if result: self.wallets.update() @@ -675,7 +675,7 @@ class FreqtradeBot(object): # If trade is open and the buy order is fulfilled but there is no stoploss, # then we add a stoploss on exchange - if trade.is_open and not trade.open_order_id and not trade.stoploss_order_id: + if not trade.open_order_id and not trade.stoploss_order_id: if self.edge: stoploss = self.edge.stoploss(pair=trade.pair) else: @@ -692,7 +692,7 @@ class FreqtradeBot(object): )['id'] trade.stoploss_order_id = str(stoploss_order_id) - # Or there is already a stoploss on exchange. + # Or the trade open and there is already a stoploss on exchange. # so we check if it is hit ... elif trade.stoploss_order_id: logger.debug('Handling stoploss on exchange %s ...', trade) From fb755880fad8cc2e0b0145544117b7243eac26dd Mon Sep 17 00:00:00 2001 From: misagh Date: Wed, 28 Nov 2018 14:16:50 +0100 Subject: [PATCH 081/103] logs added in case stop loss on exchange is hit --- freqtrade/persistence.py | 1 + freqtrade/tests/test_freqtradebot.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 26b0d9d93..64663a2fd 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -255,6 +255,7 @@ class Trade(_DECL_BASE): self.close(order['price']) elif order_type == 'stop_loss_limit': self.stoploss_order_id = None + logger.info('STOP_LOSS_LIMIT is hit for %s.', self) self.close(order['price']) else: raise ValueError(f'Unknown order type: {order_type}') diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 995a3e8ff..81ade608a 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -899,7 +899,7 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None assert trade.is_open is True -def test_handle_stoploss_on_exchange(mocker, default_conf, fee, +def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, markets, limit_buy_order, limit_sell_order) -> None: stoploss_limit = MagicMock(return_value={'id': 13434334}) patch_RPCManager(mocker) @@ -961,6 +961,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, }) mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hit) assert freqtrade.handle_stoploss_on_exchange(trade) is True + assert log_has('STOP_LOSS_LIMIT is hit for {}.'.format(trade), caplog.record_tuples) assert trade.stoploss_order_id is None assert trade.is_open is False From c913fef80c5c31d8007b5d437e9d371196440695 Mon Sep 17 00:00:00 2001 From: misagh Date: Wed, 28 Nov 2018 15:45:11 +0100 Subject: [PATCH 082/103] =?UTF-8?q?stop=20loss=20limit=20when=20hit,=20the?= =?UTF-8?q?=20close=20price=20is=20=E2=80=9Caverage=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- freqtrade/persistence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 64663a2fd..592a88acb 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -256,7 +256,7 @@ class Trade(_DECL_BASE): elif order_type == 'stop_loss_limit': self.stoploss_order_id = None logger.info('STOP_LOSS_LIMIT is hit for %s.', self) - self.close(order['price']) + self.close(order['average']) else: raise ValueError(f'Unknown order type: {order_type}') cleanup() From 1a5465fb508ca20318e36913cfb0cd2f12a995b1 Mon Sep 17 00:00:00 2001 From: misagh Date: Wed, 28 Nov 2018 19:35:10 +0100 Subject: [PATCH 083/103] logs enriched in case of stop loss on exchange, test fixed --- freqtrade/exchange/__init__.py | 8 +++++--- freqtrade/tests/test_freqtradebot.py | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 2480dbe32..baa9d573d 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -378,13 +378,15 @@ class Exchange(object): except ccxt.InsufficientFunds as e: raise DependencyException( - f'Insufficient funds to place stoploss limit order on market {pair}.' - f'Tried to put a stoploss amount {amount} at rate {rate} (total {rate*amount}).' + f'Insufficient funds to place stoploss limit order on market {pair}. ' + f'Tried to put a stoploss amount {amount} with ' + f'stop {stop_price} and limit {rate} (total {rate*amount}).' f'Message: {e}') except ccxt.InvalidOrder as e: raise DependencyException( f'Could not place stoploss limit order on market {pair}.' - f'Tried to place stoploss amount {amount} at rate {rate} (total {rate*amount}).' + f'Tried to place stoploss amount {amount} with ' + f'stop {stop_price} and limit {rate} (total {rate*amount}).' f'Message: {e}') except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 81ade608a..147d47b72 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -957,7 +957,8 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, stoploss_order_hit = MagicMock(return_value={ 'status': 'closed', 'type': 'stop_loss_limit', - 'price': 2 + 'price': 3, + 'average': 2 }) mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hit) assert freqtrade.handle_stoploss_on_exchange(trade) is True From 38592c6fa60f0892d0d32e450b1036830bab2c3c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Nov 2018 07:07:47 +0100 Subject: [PATCH 084/103] Add binance config sample, improve invalid pair message --- config_binance.json.example | 83 ++++++++++++++++++++++++++++++++++ freqtrade/exchange/__init__.py | 3 +- 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 config_binance.json.example diff --git a/config_binance.json.example b/config_binance.json.example new file mode 100644 index 000000000..da000efa7 --- /dev/null +++ b/config_binance.json.example @@ -0,0 +1,83 @@ +{ + "max_open_trades": 3, + "stake_currency": "BTC", + "stake_amount": 0.05, + "fiat_display_currency": "USD", + "ticker_interval" : "5m", + "dry_run": true, + "trailing_stop": false, + "unfilledtimeout": { + "buy": 10, + "sell": 30 + }, + "bid_strategy": { + "ask_last_balance": 0.0, + "use_order_book": false, + "order_book_top": 1, + "check_depth_of_market": { + "enabled": false, + "bids_to_ask_delta": 1 + } + }, + "ask_strategy":{ + "use_order_book": false, + "order_book_min": 1, + "order_book_max": 9 + }, + "exchange": { + "name": "binance", + "key": "your_exchange_key", + "secret": "your_exchange_secret", + "ccxt_config": {"enableRateLimit": true}, + "ccxt_async_config": { + "enableRateLimit": false + }, + "pair_whitelist": [ + "AST/BTC", + "ETC/BTC", + "ETH/BTC", + "EOS/BTC", + "IOTA/BTC", + "LTC/BTC", + "MTH/BTC", + "NCASH/BTC", + "TNT/BTC", + "XMR/BTC", + "XLM/BTC", + "XRP/BTC" + ], + "pair_blacklist": [ + "DOGE/BTC" + ] + }, + "experimental": { + "use_sell_signal": false, + "sell_profit_only": false, + "ignore_roi_if_buy_signal": false + }, + "edge": { + "enabled": false, + "process_throttle_secs": 3600, + "calculate_since_number_of_days": 7, + "total_capital_in_stake_currency": 0.5, + "allowed_risk": 0.01, + "stoploss_range_min": -0.01, + "stoploss_range_max": -0.1, + "stoploss_range_step": -0.01, + "minimum_winrate": 0.60, + "minimum_expectancy": 0.20, + "min_trade_number": 10, + "max_trade_duration_minute": 1440, + "remove_pumps": false + }, + "telegram": { + "enabled": false, + "token": "your_telegram_token", + "chat_id": "your_telegram_chat_id" + }, + "initial_state": "running", + "forcebuy_enable": false, + "internals": { + "process_throttle_secs": 5 + } +} diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 350c730a4..4573e461c 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -207,7 +207,8 @@ class Exchange(object): f'Pair {pair} not compatible with stake_currency: {stake_cur}') if self.markets and pair not in self.markets: raise OperationalException( - f'Pair {pair} is not available at {self.name}') + f'Pair {pair} is not available at {self.name}' + f'Please remove {pair} from your whitelist.') def validate_timeframes(self, timeframe: List[str]) -> None: """ From cb9104fd8a1fb4a9862ae042baae9e5b69f608d6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Nov 2018 07:36:37 +0100 Subject: [PATCH 085/103] Add BNB as blacklist to align to documentation --- config_binance.json.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config_binance.json.example b/config_binance.json.example index da000efa7..7773a8c39 100644 --- a/config_binance.json.example +++ b/config_binance.json.example @@ -47,7 +47,7 @@ "XRP/BTC" ], "pair_blacklist": [ - "DOGE/BTC" + "BNB/BTC" ] }, "experimental": { From bc2f6d3b71722f9703a5423488759a44669fba79 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 29 Nov 2018 13:34:07 +0100 Subject: [PATCH 086/103] Update ccxt from 1.17.545 to 1.17.556 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 15c7d1858..e2a1332ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.17.545 +ccxt==1.17.556 SQLAlchemy==1.2.14 python-telegram-bot==11.1.0 arrow==0.12.1 From 3d37c5d76725e0b56aaf0b2591781e88e408cba8 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 29 Nov 2018 18:31:08 +0100 Subject: [PATCH 087/103] edge non existing stoploss fixed. solves #1370 --- freqtrade/edge/__init__.py | 8 +++++++- freqtrade/tests/edge/test_edge.py | 11 +++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 009b80664..2b1849d19 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -157,7 +157,13 @@ class Edge(): return position_size def stoploss(self, pair: str) -> float: - return self._cached_pairs[pair].stoploss + if pair in self._cached_pairs: + return self._cached_pairs[pair].stoploss + else: + logger.warning('tried to access stoploss of a non-existing pair, ' + 'strategy stoploss is returned instead.') + return self.strategy.stoploss + def adjust(self, pairs) -> list: """ diff --git a/freqtrade/tests/edge/test_edge.py b/freqtrade/tests/edge/test_edge.py index fac055c17..d2b1f8030 100644 --- a/freqtrade/tests/edge/test_edge.py +++ b/freqtrade/tests/edge/test_edge.py @@ -152,6 +152,17 @@ def test_stoploss(mocker, default_conf): assert edge.stoploss('E/F') == -0.01 +def test_nonexisting_stoploss(mocker, default_conf): + freqtrade = get_patched_freqtradebot(mocker, default_conf) + edge = Edge(default_conf, freqtrade.exchange, freqtrade.strategy) + mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( + return_value={ + 'E/F': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60), + } + )) + + assert edge.stoploss('N/O') == -0.1 + def _validate_ohlc(buy_ohlc_sell_matrice): for index, ohlc in enumerate(buy_ohlc_sell_matrice): # if not high < open < low or not high < close < low From 74ca34f2dee9982235478c19e7a86ce74fa89c37 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 29 Nov 2018 18:45:37 +0100 Subject: [PATCH 088/103] flaking8 --- freqtrade/edge/__init__.py | 1 - freqtrade/tests/edge/test_edge.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 2b1849d19..4cb0dbc31 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -164,7 +164,6 @@ class Edge(): 'strategy stoploss is returned instead.') return self.strategy.stoploss - def adjust(self, pairs) -> list: """ Filters out and sorts "pairs" according to Edge calculated pairs diff --git a/freqtrade/tests/edge/test_edge.py b/freqtrade/tests/edge/test_edge.py index d2b1f8030..50c4ade3d 100644 --- a/freqtrade/tests/edge/test_edge.py +++ b/freqtrade/tests/edge/test_edge.py @@ -163,6 +163,7 @@ def test_nonexisting_stoploss(mocker, default_conf): assert edge.stoploss('N/O') == -0.1 + def _validate_ohlc(buy_ohlc_sell_matrice): for index, ohlc in enumerate(buy_ohlc_sell_matrice): # if not high < open < low or not high < close < low From efcec736b55205f732493c381a3d2671b9ae1fa4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Nov 2018 20:02:12 +0100 Subject: [PATCH 089/103] refactor startup_messages to rpc_manger this cleans up freqtradebot slightly --- freqtrade/freqtradebot.py | 34 +------------------------ freqtrade/rpc/rpc_manager.py | 34 ++++++++++++++++++++++++- freqtrade/tests/rpc/test_rpc_manager.py | 20 +++++++++++++++ 3 files changed, 54 insertions(+), 34 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7a974d385..0c6369ef4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -107,7 +107,7 @@ class FreqtradeBot(object): }) logger.info('Changing state to: %s', state.name) if state == State.RUNNING: - self._startup_messages() + self.rpc.startup_messages(self.config) if state == State.STOPPED: time.sleep(1) @@ -121,38 +121,6 @@ class FreqtradeBot(object): min_secs=min_secs) return state - def _startup_messages(self) -> None: - if self.config.get('dry_run', False): - self.rpc.send_msg({ - 'type': RPCMessageType.WARNING_NOTIFICATION, - 'status': 'Dry run is enabled. All trades are simulated.' - }) - stake_currency = self.config['stake_currency'] - stake_amount = self.config['stake_amount'] - minimal_roi = self.config['minimal_roi'] - ticker_interval = self.config['ticker_interval'] - exchange_name = self.config['exchange']['name'] - strategy_name = self.config.get('strategy', '') - self.rpc.send_msg({ - 'type': RPCMessageType.CUSTOM_NOTIFICATION, - 'status': f'*Exchange:* `{exchange_name}`\n' - f'*Stake per trade:* `{stake_amount} {stake_currency}`\n' - f'*Minimum ROI:* `{minimal_roi}`\n' - f'*Ticker Interval:* `{ticker_interval}`\n' - f'*Strategy:* `{strategy_name}`' - }) - if self.config.get('dynamic_whitelist', False): - top_pairs = 'top volume ' + str(self.config.get('dynamic_whitelist', 20)) - specific_pairs = '' - else: - top_pairs = 'whitelisted' - specific_pairs = '\n' + ', '.join(self.config['exchange'].get('pair_whitelist', '')) - self.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': f'Searching for {top_pairs} {stake_currency} pairs to buy and sell...' - f'{specific_pairs}' - }) - def _throttle(self, func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any: """ Throttles the given callable that it diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 022578378..74a4e3bdc 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -4,7 +4,7 @@ This module contains class to manage RPC communications (Telegram, Slack, ...) import logging from typing import List, Dict, Any -from freqtrade.rpc import RPC +from freqtrade.rpc import RPC, RPCMessageType logger = logging.getLogger(__name__) @@ -51,3 +51,35 @@ class RPCManager(object): for mod in self.registered_modules: logger.debug('Forwarding message to rpc.%s', mod.name) mod.send_msg(msg) + + def startup_messages(self, config) -> None: + if config.get('dry_run', False): + self.send_msg({ + 'type': RPCMessageType.WARNING_NOTIFICATION, + 'status': 'Dry run is enabled. All trades are simulated.' + }) + stake_currency = config['stake_currency'] + stake_amount = config['stake_amount'] + minimal_roi = config['minimal_roi'] + ticker_interval = config['ticker_interval'] + exchange_name = config['exchange']['name'] + strategy_name = config.get('strategy', '') + self.send_msg({ + 'type': RPCMessageType.CUSTOM_NOTIFICATION, + 'status': f'*Exchange:* `{exchange_name}`\n' + f'*Stake per trade:* `{stake_amount} {stake_currency}`\n' + f'*Minimum ROI:* `{minimal_roi}`\n' + f'*Ticker Interval:* `{ticker_interval}`\n' + f'*Strategy:* `{strategy_name}`' + }) + if config.get('dynamic_whitelist', False): + top_pairs = 'top volume ' + str(config.get('dynamic_whitelist', 20)) + specific_pairs = '' + else: + top_pairs = 'whitelisted' + specific_pairs = '\n' + ', '.join(config['exchange'].get('pair_whitelist', '')) + self.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': f'Searching for {top_pairs} {stake_currency} pairs to buy and sell...' + f'{specific_pairs}' + }) diff --git a/freqtrade/tests/rpc/test_rpc_manager.py b/freqtrade/tests/rpc/test_rpc_manager.py index 90c693830..cbb858522 100644 --- a/freqtrade/tests/rpc/test_rpc_manager.py +++ b/freqtrade/tests/rpc/test_rpc_manager.py @@ -113,3 +113,23 @@ def test_init_webhook_enabled(mocker, default_conf, caplog) -> None: assert log_has('Enabling rpc.webhook ...', caplog.record_tuples) assert len(rpc_manager.registered_modules) == 1 assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules] + + +def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None: + telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) + mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + rpc_manager = RPCManager(freqtradebot) + rpc_manager.startup_messages(default_conf) + + assert telegram_mock.call_count == 3 + assert "*Exchange:* `bittrex`" in telegram_mock.call_args_list[1][0][0]['status'] + + telegram_mock.reset_mock() + default_conf['dry_run'] = True + default_conf['dynamic_whitelist'] = 20 + + rpc_manager.startup_messages(default_conf) + assert telegram_mock.call_count == 3 + assert "Dry run is enabled." in telegram_mock.call_args_list[0][0][0]['status'] From 42c8888fa1967bf024ed8afb0e4414cb66e142ee Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Fri, 30 Nov 2018 13:34:08 +0100 Subject: [PATCH 090/103] Update ccxt from 1.17.556 to 1.17.563 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e2a1332ba..f7db2ea09 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.17.556 +ccxt==1.17.563 SQLAlchemy==1.2.14 python-telegram-bot==11.1.0 arrow==0.12.1 From f554647efd8e072ddea0bd86b6e9abb33e40daca Mon Sep 17 00:00:00 2001 From: misagh Date: Fri, 30 Nov 2018 14:14:31 +0100 Subject: [PATCH 091/103] =?UTF-8?q?=E2=80=9Cchecking=20sell=E2=80=9D=20mes?= =?UTF-8?q?sage=20removed=20to=20debug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7a974d385..a9a51aae0 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -648,7 +648,7 @@ class FreqtradeBot(object): return True break else: - logger.info('checking sell') + logger.debug('checking sell') if self.check_sell(trade, sell_rate, buy, sell): return True From f04655c012c90ceffb01a562e80a8d8c72f6e10c Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Nov 2018 20:13:50 +0100 Subject: [PATCH 092/103] Test exceptions in sell-stoploss --- freqtrade/tests/exchange/test_exchange.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index ec7c2acae..d1f391266 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1182,6 +1182,27 @@ def test_stoploss_limit_order(default_conf, mocker): assert api_mock.create_order.call_args[0][4] == 200 assert api_mock.create_order.call_args[0][5] == {'stopPrice': 220} + # test exception handling + with pytest.raises(DependencyException): + api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) + + with pytest.raises(DependencyException): + api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) + + with pytest.raises(TemporaryError): + api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) + + with pytest.raises(OperationalException): + api_mock.create_order = MagicMock(side_effect=ccxt.BaseError) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) + def test_stoploss_limit_order_dry_run(default_conf, mocker): api_mock = MagicMock() From d4f83a7516c916ea65e2013f632a75f0686c56c4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Nov 2018 20:15:56 +0100 Subject: [PATCH 093/103] Fix missing mock in test_add_stoploss_on_exchange --- freqtrade/tests/test_freqtradebot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 147d47b72..a3638d08a 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -876,6 +876,7 @@ def test_execute_buy(mocker, default_conf, fee, markets, limit_buy_order) -> Non def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None: patch_RPCManager(mocker) + patch_exchange(mocker) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) From 24f573f3b01e3e59eafc856b03dbf57d38d61a9a Mon Sep 17 00:00:00 2001 From: Misagh Date: Sat, 1 Dec 2018 10:01:11 +0100 Subject: [PATCH 094/103] log "Found no sell signal for whitelisted ..." changed (#1378) * sell log enriched and put modify on debug --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 62b0a0d2c..d85a533b7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -629,7 +629,7 @@ class FreqtradeBot(object): if self.check_sell(trade, sell_rate, buy, sell): return True - logger.info('Found no sell signals for whitelisted currencies. Trying again..') + logger.debug('Found no sell signal for %s.', trade) return False def handle_stoploss_on_exchange(self, trade: Trade) -> bool: From bf990ec59954a722c0803dee02071d8749dcc586 Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 1 Dec 2018 10:50:41 +0100 Subject: [PATCH 095/103] test fixed and flake --- freqtrade/freqtradebot.py | 7 +++++-- freqtrade/tests/test_freqtradebot.py | 7 ++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 53477f0ca..a73a2e98f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -798,8 +798,11 @@ class FreqtradeBot(object): if sell_reason in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): sell_type = 'stoploss' - if self.config.get('dry_run', False) and sell_type == 'stoploss' and self.strategy.order_types['stoploss_on_exchange']: - limit = trade.stop_loss + # if stoploss is on exchange and we are on dry_run mode, + # we consider the sell price stop price + if self.config.get('dry_run', False) and sell_type == 'stoploss' \ + and self.strategy.order_types['stoploss_on_exchange']: + limit = trade.stop_loss # First cancelling stoploss on exchange ... if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id: diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index fb42dd518..912adacaf 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1516,8 +1516,9 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, moc } == last_msg -def test_execute_sell_down_live(default_conf, ticker, fee, - ticker_sell_down, markets, mocker) -> None: +def test_execute_sell_down(default_conf, ticker, fee, + ticker_sell_down, markets, mocker) -> None: + rpc_mock = patch_RPCManager(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -1540,7 +1541,7 @@ def test_execute_sell_down_live(default_conf, ticker, fee, 'freqtrade.exchange.Exchange', get_ticker=ticker_sell_down ) - default_conf['dry_run'] = False + freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], sell_reason=SellType.STOP_LOSS) From 042e631f87720304b71237a03a75d9d74546465d Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 1 Dec 2018 10:52:36 +0100 Subject: [PATCH 096/103] rollback on futile change --- freqtrade/tests/test_freqtradebot.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 912adacaf..bd0651ffc 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1516,8 +1516,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, moc } == last_msg -def test_execute_sell_down(default_conf, ticker, fee, - ticker_sell_down, markets, mocker) -> None: +def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, mocker) -> None: rpc_mock = patch_RPCManager(mocker) mocker.patch.multiple( From b1c81acfcb08ddbc3b8774143e99afa42a32fd6b Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 1 Dec 2018 10:53:21 +0100 Subject: [PATCH 097/103] another futile one --- freqtrade/tests/test_freqtradebot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index bd0651ffc..a9b3ffc5d 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1517,7 +1517,6 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, moc def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, mocker) -> None: - rpc_mock = patch_RPCManager(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', From b594bc7cccaf9c9964fdde218aa1a708a79736f1 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sat, 1 Dec 2018 13:34:06 +0100 Subject: [PATCH 098/103] Update ccxt from 1.17.563 to 1.17.566 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f7db2ea09..e16ca8d37 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.17.563 +ccxt==1.17.566 SQLAlchemy==1.2.14 python-telegram-bot==11.1.0 arrow==0.12.1 From 35d678c5058a0277ad224ac29696bab81f5d2247 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sun, 2 Dec 2018 13:34:06 +0100 Subject: [PATCH 099/103] Update ccxt from 1.17.566 to 1.17.572 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e16ca8d37..cee3e19be 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.17.566 +ccxt==1.17.572 SQLAlchemy==1.2.14 python-telegram-bot==11.1.0 arrow==0.12.1 From bb828c308f9e881d25614553bde6021ae84e19ab Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Dec 2018 16:03:34 +0100 Subject: [PATCH 100/103] Remove unnecessary test-file --- freqtrade/tests/optimize/test_optimize.py | 3 +- freqtrade/tests/strategy/test_interface.py | 8 ++++-- freqtrade/tests/test_dataframe.py | 32 ---------------------- 3 files changed, 8 insertions(+), 35 deletions(-) delete mode 100644 freqtrade/tests/test_dataframe.py diff --git a/freqtrade/tests/optimize/test_optimize.py b/freqtrade/tests/optimize/test_optimize.py index b58c92d5c..d73f31ad5 100644 --- a/freqtrade/tests/optimize/test_optimize.py +++ b/freqtrade/tests/optimize/test_optimize.py @@ -57,7 +57,8 @@ def test_load_data_30min_ticker(ticker_history, mocker, caplog, default_conf) -> mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history) file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-30m.json') _backup_file(file, copy_file=True) - optimize.load_data(None, pairs=['UNITTEST/BTC'], ticker_interval='30m') + ld = optimize.load_data(None, pairs=['UNITTEST/BTC'], ticker_interval='30m') + assert isinstance(ld, dict) assert os.path.isfile(file) is True assert not log_has('Download the pair: "UNITTEST/BTC", Interval: 30m', caplog.record_tuples) _clean_test_file(file) diff --git a/freqtrade/tests/strategy/test_interface.py b/freqtrade/tests/strategy/test_interface.py index fedd355af..79c485590 100644 --- a/freqtrade/tests/strategy/test_interface.py +++ b/freqtrade/tests/strategy/test_interface.py @@ -179,6 +179,10 @@ def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None: strategy.process_only_new_candles = True ret = strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'}) + assert 'high' in ret.columns + assert 'low' in ret.columns + assert 'close' in ret.columns + assert isinstance(ret, DataFrame) assert ind_mock.call_count == 1 assert buy_mock.call_count == 1 assert buy_mock.call_count == 1 @@ -193,8 +197,8 @@ def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None: assert buy_mock.call_count == 1 assert buy_mock.call_count == 1 # only skipped analyze adds buy and sell columns, otherwise it's all mocked - assert 'buy' in ret - assert 'sell' in ret + assert 'buy' in ret.columns + assert 'sell' in ret.columns assert ret['buy'].sum() == 0 assert ret['sell'].sum() == 0 assert not log_has('TA Analysis Launched', caplog.record_tuples) diff --git a/freqtrade/tests/test_dataframe.py b/freqtrade/tests/test_dataframe.py deleted file mode 100644 index 6afb83a3f..000000000 --- a/freqtrade/tests/test_dataframe.py +++ /dev/null @@ -1,32 +0,0 @@ -# pragma pylint: disable=missing-docstring, C0103 - -import pandas - -from freqtrade.optimize import load_data -from freqtrade.resolvers import StrategyResolver - -_pairs = ['ETH/BTC'] - - -def load_dataframe_pair(pairs, strategy): - ld = load_data(None, ticker_interval='5m', pairs=pairs) - assert isinstance(ld, dict) - assert isinstance(pairs[0], str) - dataframe = ld[pairs[0]] - - dataframe = strategy.analyze_ticker(dataframe, {'pair': pairs[0]}) - return dataframe - - -def test_dataframe_load(): - strategy = StrategyResolver({'strategy': 'DefaultStrategy'}).strategy - dataframe = load_dataframe_pair(_pairs, strategy) - assert isinstance(dataframe, pandas.core.frame.DataFrame) - - -def test_dataframe_columns_exists(): - strategy = StrategyResolver({'strategy': 'DefaultStrategy'}).strategy - dataframe = load_dataframe_pair(_pairs, strategy) - assert 'high' in dataframe.columns - assert 'low' in dataframe.columns - assert 'close' in dataframe.columns From 99f7c3752afb0ba03643baa16de046f2b0a0782d Mon Sep 17 00:00:00 2001 From: Pan Long Date: Mon, 3 Dec 2018 07:21:01 +0800 Subject: [PATCH 101/103] Correct Edge links It was pointing to a fork instead of freqtrade/freqtrade --- docs/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index c64b9c188..43890b053 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,12 +21,12 @@ Pull-request. Do not hesitate to reach us on - [Bot commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#bot-commands) - [Backtesting commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands) - [Hyperopt commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands) - - [Edge commands](https://github.com/mishaker/freqtrade/blob/develop/docs/bot-usage.md#edge-commands) + - [Edge commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#edge-commands) - [Bot Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md) - [Change your strategy](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md#change-your-strategy) - [Add more Indicator](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md#add-more-indicator) - [Test your strategy with Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md) - - [Edge positioning](https://github.com/mishaker/freqtrade/blob/money_mgt/docs/edge.md) + - [Edge positioning](https://github.com/freqtrade/freqtrade/blob/money_mgt/docs/edge.md) - [Find optimal parameters with Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md) - [Control the bot with telegram](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.md) - [Receive notifications via webhook](https://github.com/freqtrade/freqtrade/blob/develop/docs/webhook-config.md) From 11da297c25ed32016bba2cb71eda44deb6263e70 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 3 Dec 2018 13:34:08 +0100 Subject: [PATCH 102/103] Update ccxt from 1.17.572 to 1.17.574 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index cee3e19be..592c009d6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.17.572 +ccxt==1.17.574 SQLAlchemy==1.2.14 python-telegram-bot==11.1.0 arrow==0.12.1 From 32b6cd9dff8f025e7400f51b4c14b81806f4f149 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 4 Dec 2018 13:34:07 +0100 Subject: [PATCH 103/103] Update ccxt from 1.17.574 to 1.17.581 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 592c009d6..2befc4d5c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.17.574 +ccxt==1.17.581 SQLAlchemy==1.2.14 python-telegram-bot==11.1.0 arrow==0.12.1