Merge branch 'develop' into doc/pricing_reasons

This commit is contained in:
Matthias 2019-12-24 06:33:51 +01:00
commit 6688a2c112
38 changed files with 409 additions and 301 deletions

View File

@ -64,19 +64,17 @@ jobs:
pip install -e . pip install -e .
- name: Tests - name: Tests
env:
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
COVERALLS_SERVICE_NAME: travis-ci
TRAVIS: "true"
run: | run: |
pytest --random-order --cov=freqtrade --cov-config=.coveragerc pytest --random-order --cov=freqtrade --cov-config=.coveragerc
- name: Coveralls
if: startsWith(matrix.os, 'ubuntu')
env:
# Coveralls token. Not used as secret due to github not providing secrets to forked repositories
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
run: |
# Allow failure for coveralls # Allow failure for coveralls
# Fake travis environment to get coveralls working correctly coveralls -v || true
export TRAVIS_PULL_REQUEST="https://github.com/${GITHUB_REPOSITORY}/pull/$(cat $GITHUB_EVENT_PATH | jq -r .number)"
export TRAVIS_BRANCH=${GITHUB_REF#"ref/heads"}
export CI_BRANCH=${GITHUB_REF#"ref/heads"}
echo "${TRAVIS_BRANCH}"
coveralls || true
- name: Backtesting - name: Backtesting
run: | run: |

View File

@ -1,4 +1,4 @@
FROM python:3.7.5-slim-stretch FROM python:3.7.6-slim-stretch
RUN apt-get update \ RUN apt-get update \
&& apt-get -y install curl build-essential libssl-dev \ && apt-get -y install curl build-essential libssl-dev \

View File

@ -45,14 +45,17 @@ optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
--db-url PATH Override trades database URL, this is useful in custom --db-url PATH Override trades database URL, this is useful in custom
deployments (default: `sqlite:///tradesv3.sqlite` for deployments (default: `sqlite:///tradesv3.sqlite` for
Live Run mode, `sqlite://` for Dry Run). Live Run mode, `sqlite:///tradesv3.dryrun.sqlite` for
Dry Run).
--sd-notify Notify systemd service manager. --sd-notify Notify systemd service manager.
--dry-run Enforce dry-run for trading (removes Exchange secrets --dry-run Enforce dry-run for trading (removes Exchange secrets
and simulates trades). and simulates trades).
Common arguments: Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages). -v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified. --logfile FILE Log to the file specified. Special values are:
'syslog', 'journald'. See the documentation for more
details.
-V, --version show program's version number and exit -V, --version show program's version number and exit
-c PATH, --config PATH -c PATH, --config PATH
Specify configuration file (default: `config.json`). Specify configuration file (default: `config.json`).
@ -68,6 +71,7 @@ Strategy arguments:
Specify strategy class name which will be used by the Specify strategy class name which will be used by the
bot. bot.
--strategy-path PATH Specify additional strategy lookup path. --strategy-path PATH Specify additional strategy lookup path.
``` ```
### How to specify which configuration file be used? ### How to specify which configuration file be used?
@ -192,8 +196,8 @@ Backtesting also uses the config specified via `-c/--config`.
usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH] usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[-d PATH] [--userdir PATH] [-s NAME] [-d PATH] [--userdir PATH] [-s NAME]
[--strategy-path PATH] [-i TICKER_INTERVAL] [--strategy-path PATH] [-i TICKER_INTERVAL]
[--timerange TIMERANGE] [--max_open_trades INT] [--timerange TIMERANGE] [--max-open-trades INT]
[--stake_amount STAKE_AMOUNT] [--fee FLOAT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT]
[--eps] [--dmmp] [--eps] [--dmmp]
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
[--export EXPORT] [--export-filename PATH] [--export EXPORT] [--export-filename PATH]
@ -205,10 +209,12 @@ optional arguments:
`1d`). `1d`).
--timerange TIMERANGE --timerange TIMERANGE
Specify what timerange of data to use. Specify what timerange of data to use.
--max_open_trades INT --max-open-trades INT
Specify max_open_trades to use. Override the value of the `max_open_trades`
--stake_amount STAKE_AMOUNT configuration setting.
Specify stake_amount. --stake-amount STAKE_AMOUNT
Override the value of the `stake_amount` configuration
setting.
--fee FLOAT Specify fee ratio. Will be applied twice (on trade --fee FLOAT Specify fee ratio. Will be applied twice (on trade
entry and exit). entry and exit).
--eps, --enable-position-stacking --eps, --enable-position-stacking
@ -270,8 +276,8 @@ to find optimal parameter values for your stategy.
usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--userdir PATH] [-s NAME] [--strategy-path PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH]
[-i TICKER_INTERVAL] [--timerange TIMERANGE] [-i TICKER_INTERVAL] [--timerange TIMERANGE]
[--max_open_trades INT] [--max-open-trades INT]
[--stake_amount STAKE_AMOUNT] [--fee FLOAT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT]
[--hyperopt NAME] [--hyperopt-path PATH] [--eps] [--hyperopt NAME] [--hyperopt-path PATH] [--eps]
[-e INT] [-e INT]
[--spaces {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] [--spaces {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]]
@ -286,10 +292,12 @@ optional arguments:
`1d`). `1d`).
--timerange TIMERANGE --timerange TIMERANGE
Specify what timerange of data to use. Specify what timerange of data to use.
--max_open_trades INT --max-open-trades INT
Specify max_open_trades to use. Override the value of the `max_open_trades`
--stake_amount STAKE_AMOUNT configuration setting.
Specify stake_amount. --stake-amount STAKE_AMOUNT
Override the value of the `stake_amount` configuration
setting.
--fee FLOAT Specify fee ratio. Will be applied twice (on trade --fee FLOAT Specify fee ratio. Will be applied twice (on trade
entry and exit). entry and exit).
--hyperopt NAME Specify hyperopt class name which will be used by the --hyperopt NAME Specify hyperopt class name which will be used by the
@ -360,7 +368,7 @@ To know your trade expectancy and winrate against historical data, you can use E
usage: freqtrade edge [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] usage: freqtrade edge [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--userdir PATH] [-s NAME] [--strategy-path PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH]
[-i TICKER_INTERVAL] [--timerange TIMERANGE] [-i TICKER_INTERVAL] [--timerange TIMERANGE]
[--max_open_trades INT] [--stake_amount STAKE_AMOUNT] [--max-open-trades INT] [--stake-amount STAKE_AMOUNT]
[--fee FLOAT] [--stoplosses STOPLOSS_RANGE] [--fee FLOAT] [--stoplosses STOPLOSS_RANGE]
optional arguments: optional arguments:
@ -370,10 +378,12 @@ optional arguments:
`1d`). `1d`).
--timerange TIMERANGE --timerange TIMERANGE
Specify what timerange of data to use. Specify what timerange of data to use.
--max_open_trades INT --max-open-trades INT
Specify max_open_trades to use. Override the value of the `max_open_trades`
--stake_amount STAKE_AMOUNT configuration setting.
Specify stake_amount. --stake-amount STAKE_AMOUNT
Override the value of the `stake_amount` configuration
setting.
--fee FLOAT Specify fee ratio. Will be applied twice (on trade --fee FLOAT Specify fee ratio. Will be applied twice (on trade
entry and exit). entry and exit).
--stoplosses STOPLOSS_RANGE --stoplosses STOPLOSS_RANGE

View File

@ -55,8 +55,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br> ***Datatype:*** *Float* | `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br> ***Datatype:*** *Float*
| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0` (no offset).* <br> ***Datatype:*** *Float* | `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0` (no offset).* <br> ***Datatype:*** *Float*
| `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> ***Datatype:*** *Boolean* | `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> ***Datatype:*** *Boolean*
| `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled. <br> ***Datatype:*** *Integer* | `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).<br> ***Datatype:*** *Integer*
| `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. <br> ***Datatype:*** *Integer* | `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).<br> ***Datatype:*** *Integer*
| `bid_strategy.ask_last_balance` | **Required.** Set the bidding price. More information [below](#buy-price-without-orderbook). | `bid_strategy.ask_last_balance` | **Required.** Set the bidding price. More information [below](#buy-price-without-orderbook).
| `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled). <br> ***Datatype:*** *Boolean* | `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled). <br> ***Datatype:*** *Boolean*
| `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book Bids to buy. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in [Order Book Bids](#buy-price-with-orderbook-enabled). <br>*Defaults to `1`.* <br> ***Datatype:*** *Positive Integer* | `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book Bids to buy. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in [Order Book Bids](#buy-price-with-orderbook-enabled). <br>*Defaults to `1`.* <br> ***Datatype:*** *Positive Integer*
@ -96,7 +96,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `api_server.listen_port` | Bind Port. See the [API Server documentation](rest-api.md) for more details. <br> ***Datatype:*** *Integer between 1024 and 65535* | `api_server.listen_port` | Bind Port. See the [API Server documentation](rest-api.md) for more details. <br> ***Datatype:*** *Integer between 1024 and 65535*
| `api_server.username` | Username for API server. See the [API Server documentation](rest-api.md) for more details. **Keep it in secret, do not disclose publicly.**<br> ***Datatype:*** *String* | `api_server.username` | Username for API server. See the [API Server documentation](rest-api.md) for more details. **Keep it in secret, do not disclose publicly.**<br> ***Datatype:*** *String*
| `api_server.password` | Password for API server. See the [API Server documentation](rest-api.md) for more details. **Keep it in secret, do not disclose publicly.**<br> ***Datatype:*** *String* | `api_server.password` | Password for API server. See the [API Server documentation](rest-api.md) for more details. **Keep it in secret, do not disclose publicly.**<br> ***Datatype:*** *String*
| `db_url` | Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `true`, and to `sqlite:///tradesv3.sqlite` for production instances. <br> ***Datatype:*** *String, SQLAlchemy connect string* | `db_url` | Declares database URL to use. NOTE: This defaults to `sqlite:///tradesv3.dryrun.sqlite` if `dry_run` is `true`, and to `sqlite:///tradesv3.sqlite` for production instances. <br> ***Datatype:*** *String, SQLAlchemy connect string*
| `initial_state` | Defines the initial application state. More information below. <br>*Defaults to `stopped`.* <br> ***Datatype:*** *Enum, either `stopped` or `running`* | `initial_state` | Defines the initial application state. More information below. <br>*Defaults to `stopped`.* <br> ***Datatype:*** *Enum, either `stopped` or `running`*
| `forcebuy_enable` | Enables the RPC Commands to force a buy. More information below. <br> ***Datatype:*** *Boolean* | `forcebuy_enable` | Enables the RPC Commands to force a buy. More information below. <br> ***Datatype:*** *Boolean*
| `strategy` | **Required** Defines Strategy class to use. Recommended to be set via `--strategy NAME`. <br> ***Datatype:*** *ClassName* | `strategy` | **Required** Defines Strategy class to use. Recommended to be set via `--strategy NAME`. <br> ***Datatype:*** *ClassName*
@ -124,6 +124,7 @@ Values set in the configuration file always overwrite values set in the strategy
* `order_time_in_force` * `order_time_in_force`
* `stake_currency` * `stake_currency`
* `stake_amount` * `stake_amount`
* `unfilledtimeout`
* `use_sell_signal` (ask_strategy) * `use_sell_signal` (ask_strategy)
* `sell_profit_only` (ask_strategy) * `sell_profit_only` (ask_strategy)
* `ignore_roi_if_buy_signal` (ask_strategy) * `ignore_roi_if_buy_signal` (ask_strategy)

View File

@ -266,4 +266,29 @@ Once the PR against master is merged (best right after merging):
* Use the button "Draft a new release" in the Github UI (subsection releases). * Use the button "Draft a new release" in the Github UI (subsection releases).
* Use the version-number specified as tag. * Use the version-number specified as tag.
* Use "master" as reference (this step comes after the above PR is merged). * Use "master" as reference (this step comes after the above PR is merged).
* Use the above changelog as release comment (as codeblock). * Use the above changelog as release comment (as codeblock)
### After-release
* Update version in develop by postfixing that with `-dev` (`2019.6 -> 2019.6-dev`).
* Create a PR against develop to update that branch.
## Releases
### pypi
To create a pypi release, please run the following commands:
Additional requirement: `wheel`, `twine` (for uploading), account on pypi with proper permissions.
``` bash
python setup.py sdist bdist_wheel
# For pypi test (to check if some change to the installation did work)
twine upload --repository-url https://test.pypi.org/legacy/ dist/*
# For production:
twine upload dist/*
```
Please don't push non-releases to the productive / real pypi instance.

View File

@ -164,8 +164,7 @@ docker run -d \
``` ```
!!! Note !!! Note
db-url defaults to `sqlite:///tradesv3.sqlite` but it defaults to `sqlite://` if `dry_run=True` is being used. When using docker, it's best to specify `--db-url` explicitly to ensure that the database URL and the mounted database file match.
To override this behaviour use a custom db-url value: i.e.: `--db-url sqlite:///tradesv3.dryrun.sqlite`
!!! Note !!! Note
All available bot command line parameters can be added to the end of the `docker run` command. All available bot command line parameters can be added to the end of the `docker run` command.

View File

@ -23,58 +23,43 @@ The `freqtrade plot-dataframe` subcommand shows an interactive graph with three
Possible arguments: Possible arguments:
``` ```
usage: freqtrade plot-dataframe [-h] [-v] [--logfile FILE] [-V] [-c PATH] usage: freqtrade plot-dataframe [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME]
[-d PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH] [-p PAIRS [PAIRS ...]] [--indicators1 INDICATORS1 [INDICATORS1 ...]]
[--strategy-path PATH] [-p PAIRS [PAIRS ...]] [--indicators2 INDICATORS2 [INDICATORS2 ...]] [--plot-limit INT] [--db-url PATH]
[--indicators1 INDICATORS1 [INDICATORS1 ...]] [--trade-source {DB,file}] [--export EXPORT] [--export-filename PATH] [--timerange TIMERANGE]
[--indicators2 INDICATORS2 [INDICATORS2 ...]] [-i TICKER_INTERVAL]
[--plot-limit INT] [--db-url PATH]
[--trade-source {DB,file}] [--export EXPORT]
[--export-filename PATH]
[--timerange TIMERANGE] [-i TICKER_INTERVAL]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
Show profits for only these pairs. Pairs are space- Show profits for only these pairs. Pairs are space-separated.
separated.
--indicators1 INDICATORS1 [INDICATORS1 ...] --indicators1 INDICATORS1 [INDICATORS1 ...]
Set indicators from your strategy you want in the Set indicators from your strategy you want in the first row of the graph. Space-separated list. Example:
first row of the graph. Space-separated list. Example:
`ema3 ema5`. Default: `['sma', 'ema3', 'ema5']`. `ema3 ema5`. Default: `['sma', 'ema3', 'ema5']`.
--indicators2 INDICATORS2 [INDICATORS2 ...] --indicators2 INDICATORS2 [INDICATORS2 ...]
Set indicators from your strategy you want in the Set indicators from your strategy you want in the third row of the graph. Space-separated list. Example:
third row of the graph. Space-separated list. Example:
`fastd fastk`. Default: `['macd', 'macdsignal']`. `fastd fastk`. Default: `['macd', 'macdsignal']`.
--plot-limit INT Specify tick limit for plotting. Notice: too high --plot-limit INT Specify tick limit for plotting. Notice: too high values cause huge files. Default: 750.
values cause huge files. Default: 750. --db-url PATH Override trades database URL, this is useful in custom deployments (default: `sqlite:///tradesv3.sqlite`
--db-url PATH Override trades database URL, this is useful in custom for Live Run mode, `sqlite:///tradesv3.dryrun.sqlite` for Dry Run).
deployments (default: `sqlite:///tradesv3.sqlite` for
Live Run mode, `sqlite://` for Dry Run).
--trade-source {DB,file} --trade-source {DB,file}
Specify the source for trades (Can be DB or file Specify the source for trades (Can be DB or file (backtest file)) Default: file
(backtest file)) Default: file --export EXPORT Export backtest results, argument are: trades. Example: `--export=trades`
--export EXPORT Export backtest results, argument are: trades.
Example: `--export=trades`
--export-filename PATH --export-filename PATH
Save backtest results to the file with this filename Save backtest results to the file with this filename. Requires `--export` to be set as well. Example:
(default: `user_data/backtest_results/backtest- `--export-filename=user_data/backtest_results/backtest_today.json`
result.json`). Requires `--export` to be set as well.
Example: `--export-filename=user_data/backtest_results
/backtest_today.json`
--timerange TIMERANGE --timerange TIMERANGE
Specify what timerange of data to use. Specify what timerange of data to use.
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL -i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
Specify ticker interval (`1m`, `5m`, `30m`, `1h`, Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`).
`1d`).
Common arguments: Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages). -v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified. --logfile FILE Log to the file specified. Special values are: 'syslog', 'journald'. See the documentation for more
details.
-V, --version show program's version number and exit -V, --version show program's version number and exit
-c PATH, --config PATH -c PATH, --config PATH
Specify configuration file (default: `config.json`). Specify configuration file (default: `config.json`). Multiple --config options may be used. Can be set to
Multiple --config options may be used. Can be set to
`-` to read config from stdin. `-` to read config from stdin.
-d PATH, --datadir PATH -d PATH, --datadir PATH
Path to directory with historical backtesting data. Path to directory with historical backtesting data.
@ -83,8 +68,7 @@ Common arguments:
Strategy arguments: Strategy arguments:
-s NAME, --strategy NAME -s NAME, --strategy NAME
Specify strategy class name (default: Specify strategy class name which will be used by the bot.
`DefaultStrategy`).
--strategy-path PATH Specify additional strategy lookup path. --strategy-path PATH Specify additional strategy lookup path.
``` ```
@ -173,14 +157,14 @@ optional arguments:
--export EXPORT Export backtest results, argument are: trades. --export EXPORT Export backtest results, argument are: trades.
Example: `--export=trades` Example: `--export=trades`
--export-filename PATH --export-filename PATH
Save backtest results to the file with this filename Save backtest results to the file with this filename.
(default: `user_data/backtest_results/backtest- Requires `--export` to be set as well. Example:
result.json`). Requires `--export` to be set as well. `--export-filename=user_data/backtest_results/backtest
Example: `--export-filename=user_data/backtest_results _today.json`
/backtest_today.json`
--db-url PATH Override trades database URL, this is useful in custom --db-url PATH Override trades database URL, this is useful in custom
deployments (default: `sqlite:///tradesv3.sqlite` for deployments (default: `sqlite:///tradesv3.sqlite` for
Live Run mode, `sqlite://` for Dry Run). Live Run mode, `sqlite:///tradesv3.dryrun.sqlite` for
Dry Run).
--trade-source {DB,file} --trade-source {DB,file}
Specify the source for trades (Can be DB or file Specify the source for trades (Can be DB or file
(backtest file)) Default: file (backtest file)) Default: file
@ -190,7 +174,9 @@ optional arguments:
Common arguments: Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages). -v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified. --logfile FILE Log to the file specified. Special values are:
'syslog', 'journald'. See the documentation for more
details.
-V, --version show program's version number and exit -V, --version show program's version number and exit
-c PATH, --config PATH -c PATH, --config PATH
Specify configuration file (default: `config.json`). Specify configuration file (default: `config.json`).

View File

@ -455,6 +455,51 @@ Sample return value: ETH/BTC had 5 trades, with a total profit of 1.5% (ratio of
!!! Warning !!! Warning
Trade history is not available during backtesting or hyperopt. Trade history is not available during backtesting or hyperopt.
### Prevent trades from happening for a specific pair
Freqtrade locks pairs automatically for the current candle (until that candle is over) when a pair is sold, preventing an immediate re-buy of that pair.
Locked pairs will show the message `Pair <pair> is currently locked.`.
#### Locking pairs from within the strategy
Sometimes it may be desired to lock a pair after certain events happen (e.g. multiple losing trades in a row).
Freqtrade has an easy method to do this from within the strategy, by calling `self.lock_pair(pair, until)`.
`until` must be a datetime object in the future, after which trading will be reenabled for that pair.
Locks can also be lifted manually, by calling `self.unlock_pair(pair)`.
To verify if a pair is currently locked, use `self.is_pair_locked(pair)`.
!!! Note
Locked pairs are not persisted, so a restart of the bot, or calling `/reload_conf` will reset locked pairs.
!!! Warning
Locking pairs is not functioning during backtesting.
##### Pair locking example
``` python
from freqtrade.persistence import Trade
from datetime import timedelta, datetime, timezone
# Put the above lines a the top of the strategy file, next to all the other imports
# --------
# Within populate indicators (or populate_buy):
if self.config['runmode'] in ('live', 'dry_run'):
# fetch closed trades for the last 2 days
trades = Trade.get_trades([Trade.pair == metadata['pair'],
Trade.open_date > datetime.utcnow() - timedelta(days=2),
Trade.is_open == False,
]).all()
# Analyze the conditions you'd like to lock the pair .... will probably be different for every strategy
sumprofit = sum(trade.close_profit for trade in trades)
if sumprofit < 0:
# Lock pair for 12 hours
self.lock_pair(metadata['pair'], until=datetime.now(timezone.utc) + timedelta(hours=12))
```
### Print created dataframe ### Print created dataframe
To inspect the created dataframe, you can issue a print-statement in either `populate_buy_trend()` or `populate_sell_trend()`. To inspect the created dataframe, you can issue a print-statement in either `populate_buy_trend()` or `populate_sell_trend()`.
@ -479,11 +524,6 @@ def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
Printing more than a few rows is also possible (simply use `print(dataframe)` instead of `print(dataframe.tail())`), however not recommended, as that will be very verbose (~500 lines per pair every 5 seconds). Printing more than a few rows is also possible (simply use `print(dataframe)` instead of `print(dataframe.tail())`), however not recommended, as that will be very verbose (~500 lines per pair every 5 seconds).
### Where can i find a strategy template?
The strategy template is located in the file
[user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_strategy.py).
### Specify custom strategy location ### Specify custom strategy location
If you want to use a strategy from a different directory you can pass `--strategy-path` If you want to use a strategy from a different directory you can pass `--strategy-path`

View File

@ -44,9 +44,9 @@ candles.head()
```python ```python
# Load strategy using values set above # Load strategy using values set above
from freqtrade.resolvers import StrategyResolver from freqtrade.resolvers import StrategyResolver
strategy = StrategyResolver({'strategy': strategy_name, strategy = StrategyResolver.load_strategy({'strategy': strategy_name,
'user_data_dir': user_data_dir, 'user_data_dir': user_data_dir,
'strategy_path': strategy_location}).strategy 'strategy_path': strategy_location})
# Generate buy/sell signals using strategy # Generate buy/sell signals using strategy
df = strategy.analyze_ticker(candles, {'pair': pair}) df = strategy.analyze_ticker(candles, {'pair': pair})

View File

@ -118,14 +118,14 @@ AVAILABLE_CLI_OPTIONS = {
help='Specify what timerange of data to use.', help='Specify what timerange of data to use.',
), ),
"max_open_trades": Arg( "max_open_trades": Arg(
'--max_open_trades', '--max-open-trades',
help='Specify max_open_trades to use.', help='Override the value of the `max_open_trades` configuration setting.',
type=int, type=int,
metavar='INT', metavar='INT',
), ),
"stake_amount": Arg( "stake_amount": Arg(
'--stake_amount', '--stake-amount',
help='Specify stake_amount.', help='Override the value of the `stake_amount` configuration setting.',
type=float, type=float,
), ),
# Backtesting # Backtesting

View File

@ -223,13 +223,13 @@ class Configuration:
logger.info('max_open_trades set to unlimited ...') logger.info('max_open_trades set to unlimited ...')
elif 'max_open_trades' in self.args and self.args["max_open_trades"]: elif 'max_open_trades' in self.args and self.args["max_open_trades"]:
config.update({'max_open_trades': self.args["max_open_trades"]}) config.update({'max_open_trades': self.args["max_open_trades"]})
logger.info('Parameter --max_open_trades detected, ' logger.info('Parameter --max-open-trades detected, '
'overriding max_open_trades to: %s ...', config.get('max_open_trades')) 'overriding max_open_trades to: %s ...', config.get('max_open_trades'))
elif config['runmode'] in NON_UTIL_MODES: elif config['runmode'] in NON_UTIL_MODES:
logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
self._args_to_config(config, argname='stake_amount', self._args_to_config(config, argname='stake_amount',
logstring='Parameter --stake_amount detected, ' logstring='Parameter --stake-amount detected, '
'overriding stake_amount to: {} ...') 'overriding stake_amount to: {} ...')
self._args_to_config(config, argname='fee', self._args_to_config(config, argname='fee',

View File

@ -10,7 +10,7 @@ HYPEROPT_EPOCH = 100 # epochs
RETRY_TIMEOUT = 30 # sec RETRY_TIMEOUT = 30 # sec
DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss' DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss'
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
DEFAULT_DB_DRYRUN_URL = 'sqlite://' DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite'
UNLIMITED_STAKE_AMOUNT = 'unlimited' UNLIMITED_STAKE_AMOUNT = 'unlimited'
DEFAULT_AMOUNT_RESERVE_PERCENT = 0.05 DEFAULT_AMOUNT_RESERVE_PERCENT = 0.05
REQUIRED_ORDERTIF = ['buy', 'sell'] REQUIRED_ORDERTIF = ['buy', 'sell']

View File

@ -55,12 +55,12 @@ class FreqtradeBot:
self.heartbeat_interval = self.config.get('internals', {}).get('heartbeat_interval', 60) self.heartbeat_interval = self.config.get('internals', {}).get('heartbeat_interval', 60)
self.strategy: IStrategy = StrategyResolver(self.config).strategy self.strategy: IStrategy = StrategyResolver.load_strategy(self.config)
# Check config consistency here since strategies can set certain options # Check config consistency here since strategies can set certain options
validate_config_consistency(config) validate_config_consistency(config)
self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
persistence.init(self.config.get('db_url', None), persistence.init(self.config.get('db_url', None),
clean_open_orders=self.config.get('dry_run', False)) clean_open_orders=self.config.get('dry_run', False))

View File

@ -60,7 +60,7 @@ class Backtesting:
# Reset keys for backtesting # Reset keys for backtesting
remove_credentials(self.config) remove_credentials(self.config)
self.strategylist: List[IStrategy] = [] self.strategylist: List[IStrategy] = []
self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
if config.get('fee'): if config.get('fee'):
self.fee = config['fee'] self.fee = config['fee']
@ -75,12 +75,12 @@ class Backtesting:
for strat in list(self.config['strategy_list']): for strat in list(self.config['strategy_list']):
stratconf = deepcopy(self.config) stratconf = deepcopy(self.config)
stratconf['strategy'] = strat stratconf['strategy'] = strat
self.strategylist.append(StrategyResolver(stratconf).strategy) self.strategylist.append(StrategyResolver.load_strategy(stratconf))
validate_config_consistency(stratconf) validate_config_consistency(stratconf)
else: else:
# No strategy list specified, only one strategy # No strategy list specified, only one strategy
self.strategylist.append(StrategyResolver(self.config).strategy) self.strategylist.append(StrategyResolver.load_strategy(self.config))
validate_config_consistency(self.config) validate_config_consistency(self.config)
if "ticker_interval" not in self.config: if "ticker_interval" not in self.config:

View File

@ -34,7 +34,7 @@ class EdgeCli:
remove_credentials(self.config) remove_credentials(self.config)
self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
self.exchange = Exchange(self.config) self.exchange = Exchange(self.config)
self.strategy = StrategyResolver(self.config).strategy self.strategy = StrategyResolver.load_strategy(self.config)
validate_config_consistency(self.config) validate_config_consistency(self.config)
@ -42,11 +42,9 @@ class EdgeCli:
# Set refresh_pairs to false for edge-cli (it must be true for edge) # Set refresh_pairs to false for edge-cli (it must be true for edge)
self.edge._refresh_pairs = False self.edge._refresh_pairs = False
self.timerange = TimeRange.parse_timerange(None if self.config.get( self.edge._timerange = TimeRange.parse_timerange(None if self.config.get(
'timerange') is None else str(self.config.get('timerange'))) 'timerange') is None else str(self.config.get('timerange')))
self.edge._timerange = self.timerange
def _generate_edge_table(self, results: dict) -> str: def _generate_edge_table(self, results: dict) -> str:
floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', '.d') floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', '.d')

View File

@ -64,9 +64,9 @@ class Hyperopt:
self.backtesting = Backtesting(self.config) self.backtesting = Backtesting(self.config)
self.custom_hyperopt = HyperOptResolver(self.config).hyperopt self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config)
self.custom_hyperoptloss = HyperOptLossResolver(self.config).hyperoptloss self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config)
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
self.trials_file = (self.config['user_data_dir'] / self.trials_file = (self.config['user_data_dir'] /

View File

@ -28,13 +28,13 @@ class PairListManager():
if 'method' not in pl: if 'method' not in pl:
logger.warning(f"No method in {pl}") logger.warning(f"No method in {pl}")
continue continue
pairl = PairListResolver(pl.get('method'), pairl = PairListResolver.load_pairlist(pl.get('method'),
exchange=exchange, exchange=exchange,
pairlistmanager=self, pairlistmanager=self,
config=config, config=config,
pairlistconfig=pl, pairlistconfig=pl,
pairlist_pos=len(self._pairlists) pairlist_pos=len(self._pairlists)
).pairlist )
self._tickers_needed = pairl.needstickers or self._tickers_needed self._tickers_needed = pairl.needstickers or self._tickers_needed
self._pairlists.append(pairl) self._pairlists.append(pairl)

View File

@ -340,7 +340,7 @@ def load_and_plot_trades(config: Dict[str, Any]):
- Generate plot files - Generate plot files
:return: None :return: None
""" """
strategy = StrategyResolver(config).strategy strategy = StrategyResolver.load_strategy(config)
plot_elements = init_plotscript(config) plot_elements = init_plotscript(config)
trades = plot_elements['trades'] trades = plot_elements['trades']

View File

@ -15,9 +15,8 @@ class ExchangeResolver(IResolver):
This class contains all the logic to load a custom exchange class This class contains all the logic to load a custom exchange class
""" """
__slots__ = ['exchange'] @staticmethod
def load_exchange(exchange_name: str, config: dict, validate: bool = True) -> Exchange:
def __init__(self, exchange_name: str, config: dict, validate: bool = True) -> None:
""" """
Load the custom class from config parameter Load the custom class from config parameter
:param config: configuration dictionary :param config: configuration dictionary
@ -25,17 +24,20 @@ class ExchangeResolver(IResolver):
# Map exchange name to avoid duplicate classes for identical exchanges # Map exchange name to avoid duplicate classes for identical exchanges
exchange_name = MAP_EXCHANGE_CHILDCLASS.get(exchange_name, exchange_name) exchange_name = MAP_EXCHANGE_CHILDCLASS.get(exchange_name, exchange_name)
exchange_name = exchange_name.title() exchange_name = exchange_name.title()
exchange = None
try: try:
self.exchange = self._load_exchange(exchange_name, kwargs={'config': config, exchange = ExchangeResolver._load_exchange(exchange_name,
'validate': validate}) kwargs={'config': config,
'validate': validate})
except ImportError: except ImportError:
logger.info( logger.info(
f"No {exchange_name} specific subclass found. Using the generic class instead.") f"No {exchange_name} specific subclass found. Using the generic class instead.")
if not hasattr(self, "exchange"): if not exchange:
self.exchange = Exchange(config, validate=validate) exchange = Exchange(config, validate=validate)
return exchange
def _load_exchange( @staticmethod
self, exchange_name: str, kwargs: dict) -> Exchange: def _load_exchange(exchange_name: str, kwargs: dict) -> Exchange:
""" """
Loads the specified exchange. Loads the specified exchange.
Only checks for exchanges exported in freqtrade.exchanges Only checks for exchanges exported in freqtrade.exchanges

View File

@ -20,11 +20,11 @@ class HyperOptResolver(IResolver):
""" """
This class contains all the logic to load custom hyperopt class This class contains all the logic to load custom hyperopt class
""" """
__slots__ = ['hyperopt']
def __init__(self, config: Dict) -> None: @staticmethod
def load_hyperopt(config: Dict) -> IHyperOpt:
""" """
Load the custom class from config parameter Load the custom hyperopt class from config parameter
:param config: configuration dictionary :param config: configuration dictionary
""" """
if not config.get('hyperopt'): if not config.get('hyperopt'):
@ -33,21 +33,23 @@ class HyperOptResolver(IResolver):
hyperopt_name = config['hyperopt'] hyperopt_name = config['hyperopt']
self.hyperopt = self._load_hyperopt(hyperopt_name, config, hyperopt = HyperOptResolver._load_hyperopt(hyperopt_name, config,
extra_dir=config.get('hyperopt_path')) extra_dir=config.get('hyperopt_path'))
if not hasattr(self.hyperopt, 'populate_indicators'): if not hasattr(hyperopt, 'populate_indicators'):
logger.warning("Hyperopt class does not provide populate_indicators() method. " logger.warning("Hyperopt class does not provide populate_indicators() method. "
"Using populate_indicators from the strategy.") "Using populate_indicators from the strategy.")
if not hasattr(self.hyperopt, 'populate_buy_trend'): if not hasattr(hyperopt, 'populate_buy_trend'):
logger.warning("Hyperopt class does not provide populate_buy_trend() method. " logger.warning("Hyperopt class does not provide populate_buy_trend() method. "
"Using populate_buy_trend from the strategy.") "Using populate_buy_trend from the strategy.")
if not hasattr(self.hyperopt, 'populate_sell_trend'): if not hasattr(hyperopt, 'populate_sell_trend'):
logger.warning("Hyperopt class does not provide populate_sell_trend() method. " logger.warning("Hyperopt class does not provide populate_sell_trend() method. "
"Using populate_sell_trend from the strategy.") "Using populate_sell_trend from the strategy.")
return hyperopt
@staticmethod
def _load_hyperopt( def _load_hyperopt(
self, hyperopt_name: str, config: Dict, extra_dir: Optional[str] = None) -> IHyperOpt: hyperopt_name: str, config: Dict, extra_dir: Optional[str] = None) -> IHyperOpt:
""" """
Search and loads the specified hyperopt. Search and loads the specified hyperopt.
:param hyperopt_name: name of the module to import :param hyperopt_name: name of the module to import
@ -57,11 +59,12 @@ class HyperOptResolver(IResolver):
""" """
current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() current_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
abs_paths = self.build_search_paths(config, current_path=current_path, abs_paths = IResolver.build_search_paths(config, current_path=current_path,
user_subdir=USERPATH_HYPEROPTS, extra_dir=extra_dir) user_subdir=USERPATH_HYPEROPTS,
extra_dir=extra_dir)
hyperopt = self._load_object(paths=abs_paths, object_type=IHyperOpt, hyperopt = IResolver._load_object(paths=abs_paths, object_type=IHyperOpt,
object_name=hyperopt_name, kwargs={'config': config}) object_name=hyperopt_name, kwargs={'config': config})
if hyperopt: if hyperopt:
return hyperopt return hyperopt
raise OperationalException( raise OperationalException(
@ -74,9 +77,9 @@ class HyperOptLossResolver(IResolver):
""" """
This class contains all the logic to load custom hyperopt loss class This class contains all the logic to load custom hyperopt loss class
""" """
__slots__ = ['hyperoptloss']
def __init__(self, config: Dict) -> None: @staticmethod
def load_hyperoptloss(config: Dict) -> IHyperOptLoss:
""" """
Load the custom class from config parameter Load the custom class from config parameter
:param config: configuration dictionary :param config: configuration dictionary
@ -86,20 +89,21 @@ class HyperOptLossResolver(IResolver):
# default hyperopt loss # default hyperopt loss
hyperoptloss_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS hyperoptloss_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS
self.hyperoptloss = self._load_hyperoptloss( hyperoptloss = HyperOptLossResolver._load_hyperoptloss(
hyperoptloss_name, config, extra_dir=config.get('hyperopt_path')) hyperoptloss_name, config, extra_dir=config.get('hyperopt_path'))
# Assign ticker_interval to be used in hyperopt # Assign ticker_interval to be used in hyperopt
self.hyperoptloss.__class__.ticker_interval = str(config['ticker_interval']) hyperoptloss.__class__.ticker_interval = str(config['ticker_interval'])
if not hasattr(self.hyperoptloss, 'hyperopt_loss_function'): if not hasattr(hyperoptloss, 'hyperopt_loss_function'):
raise OperationalException( raise OperationalException(
f"Found HyperoptLoss class {hyperoptloss_name} does not " f"Found HyperoptLoss class {hyperoptloss_name} does not "
"implement `hyperopt_loss_function`.") "implement `hyperopt_loss_function`.")
return hyperoptloss
def _load_hyperoptloss( @staticmethod
self, hyper_loss_name: str, config: Dict, def _load_hyperoptloss(hyper_loss_name: str, config: Dict,
extra_dir: Optional[str] = None) -> IHyperOptLoss: extra_dir: Optional[str] = None) -> IHyperOptLoss:
""" """
Search and loads the specified hyperopt loss class. Search and loads the specified hyperopt loss class.
:param hyper_loss_name: name of the module to import :param hyper_loss_name: name of the module to import
@ -109,11 +113,12 @@ class HyperOptLossResolver(IResolver):
""" """
current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() current_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
abs_paths = self.build_search_paths(config, current_path=current_path, abs_paths = IResolver.build_search_paths(config, current_path=current_path,
user_subdir=USERPATH_HYPEROPTS, extra_dir=extra_dir) user_subdir=USERPATH_HYPEROPTS,
extra_dir=extra_dir)
hyperoptloss = self._load_object(paths=abs_paths, object_type=IHyperOptLoss, hyperoptloss = IResolver._load_object(paths=abs_paths, object_type=IHyperOptLoss,
object_name=hyper_loss_name) object_name=hyper_loss_name)
if hyperoptloss: if hyperoptloss:
return hyperoptloss return hyperoptloss

View File

@ -17,7 +17,8 @@ class IResolver:
This class contains all the logic to load custom classes This class contains all the logic to load custom classes
""" """
def build_search_paths(self, config, current_path: Path, user_subdir: Optional[str] = None, @staticmethod
def build_search_paths(config, current_path: Path, user_subdir: Optional[str] = None,
extra_dir: Optional[str] = None) -> List[Path]: extra_dir: Optional[str] = None) -> List[Path]:
abs_paths: List[Path] = [current_path] abs_paths: List[Path] = [current_path]

View File

@ -18,23 +18,29 @@ class PairListResolver(IResolver):
This class contains all the logic to load custom PairList class This class contains all the logic to load custom PairList class
""" """
__slots__ = ['pairlist'] @staticmethod
def load_pairlist(pairlist_name: str, exchange, pairlistmanager,
def __init__(self, pairlist_name: str, exchange, pairlistmanager, config: dict, pairlistconfig: dict, pairlist_pos: int) -> IPairList:
config: dict, pairlistconfig: dict, pairlist_pos: int) -> None:
""" """
Load the custom class from config parameter Load the pairlist with pairlist_name
:param config: configuration dictionary or None :param pairlist_name: Classname of the pairlist
:param exchange: Initialized exchange class
:param pairlistmanager: Initialized pairlist manager
:param config: configuration dictionary
:param pairlistconfig: Configuration dedicated to this pairlist
:param pairlist_pos: Position of the pairlist in the list of pairlists
:return: initialized Pairlist class
""" """
self.pairlist = self._load_pairlist(pairlist_name, config,
kwargs={'exchange': exchange,
'pairlistmanager': pairlistmanager,
'config': config,
'pairlistconfig': pairlistconfig,
'pairlist_pos': pairlist_pos})
def _load_pairlist( return PairListResolver._load_pairlist(pairlist_name, config,
self, pairlist_name: str, config: dict, kwargs: dict) -> IPairList: kwargs={'exchange': exchange,
'pairlistmanager': pairlistmanager,
'config': config,
'pairlistconfig': pairlistconfig,
'pairlist_pos': pairlist_pos})
@staticmethod
def _load_pairlist(pairlist_name: str, config: dict, kwargs: dict) -> IPairList:
""" """
Search and loads the specified pairlist. Search and loads the specified pairlist.
:param pairlist_name: name of the module to import :param pairlist_name: name of the module to import
@ -44,11 +50,11 @@ class PairListResolver(IResolver):
""" """
current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve() current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve()
abs_paths = self.build_search_paths(config, current_path=current_path, abs_paths = IResolver.build_search_paths(config, current_path=current_path,
user_subdir=None, extra_dir=None) user_subdir=None, extra_dir=None)
pairlist = self._load_object(paths=abs_paths, object_type=IPairList, pairlist = IResolver._load_object(paths=abs_paths, object_type=IPairList,
object_name=pairlist_name, kwargs=kwargs) object_name=pairlist_name, kwargs=kwargs)
if pairlist: if pairlist:
return pairlist return pairlist
raise OperationalException( raise OperationalException(

View File

@ -20,12 +20,11 @@ logger = logging.getLogger(__name__)
class StrategyResolver(IResolver): class StrategyResolver(IResolver):
""" """
This class contains all the logic to load custom strategy class This class contains the logic to load custom strategy class
""" """
__slots__ = ['strategy'] @staticmethod
def load_strategy(config: Optional[Dict] = None) -> IStrategy:
def __init__(self, config: Optional[Dict] = None) -> None:
""" """
Load the custom class from config parameter Load the custom class from config parameter
:param config: configuration dictionary or None :param config: configuration dictionary or None
@ -37,9 +36,9 @@ class StrategyResolver(IResolver):
"the strategy class to use.") "the strategy class to use.")
strategy_name = config['strategy'] strategy_name = config['strategy']
self.strategy: IStrategy = self._load_strategy(strategy_name, strategy: IStrategy = StrategyResolver._load_strategy(
config=config, strategy_name, config=config,
extra_dir=config.get('strategy_path')) extra_dir=config.get('strategy_path'))
# make sure ask_strategy dict is available # make sure ask_strategy dict is available
if 'ask_strategy' not in config: if 'ask_strategy' not in config:
@ -61,15 +60,18 @@ class StrategyResolver(IResolver):
("stake_currency", None, False), ("stake_currency", None, False),
("stake_amount", None, False), ("stake_amount", None, False),
("startup_candle_count", None, False), ("startup_candle_count", None, False),
("unfilledtimeout", None, False),
("use_sell_signal", True, True), ("use_sell_signal", True, True),
("sell_profit_only", False, True), ("sell_profit_only", False, True),
("ignore_roi_if_buy_signal", False, True), ("ignore_roi_if_buy_signal", False, True),
] ]
for attribute, default, ask_strategy in attributes: for attribute, default, ask_strategy in attributes:
if ask_strategy: if ask_strategy:
self._override_attribute_helper(config['ask_strategy'], attribute, default) StrategyResolver._override_attribute_helper(strategy, config['ask_strategy'],
attribute, default)
else: else:
self._override_attribute_helper(config, attribute, default) StrategyResolver._override_attribute_helper(strategy, config,
attribute, default)
# Loop this list again to have output combined # Loop this list again to have output combined
for attribute, _, exp in attributes: for attribute, _, exp in attributes:
@ -79,14 +81,16 @@ class StrategyResolver(IResolver):
logger.info("Strategy using %s: %s", attribute, config[attribute]) logger.info("Strategy using %s: %s", attribute, config[attribute])
# Sort and apply type conversions # Sort and apply type conversions
self.strategy.minimal_roi = OrderedDict(sorted( strategy.minimal_roi = OrderedDict(sorted(
{int(key): value for (key, value) in self.strategy.minimal_roi.items()}.items(), {int(key): value for (key, value) in strategy.minimal_roi.items()}.items(),
key=lambda t: t[0])) key=lambda t: t[0]))
self.strategy.stoploss = float(self.strategy.stoploss) strategy.stoploss = float(strategy.stoploss)
self._strategy_sanity_validations() StrategyResolver._strategy_sanity_validations(strategy)
return strategy
def _override_attribute_helper(self, config, attribute: str, default): @staticmethod
def _override_attribute_helper(strategy, config, attribute: str, default):
""" """
Override attributes in the strategy. Override attributes in the strategy.
Prevalence: Prevalence:
@ -95,30 +99,32 @@ class StrategyResolver(IResolver):
- default (if not None) - default (if not None)
""" """
if attribute in config: if attribute in config:
setattr(self.strategy, attribute, config[attribute]) setattr(strategy, attribute, config[attribute])
logger.info("Override strategy '%s' with value in config file: %s.", logger.info("Override strategy '%s' with value in config file: %s.",
attribute, config[attribute]) attribute, config[attribute])
elif hasattr(self.strategy, attribute): elif hasattr(strategy, attribute):
val = getattr(self.strategy, attribute) val = getattr(strategy, attribute)
# None's cannot exist in the config, so do not copy them # None's cannot exist in the config, so do not copy them
if val is not None: if val is not None:
config[attribute] = val config[attribute] = val
# Explicitly check for None here as other "falsy" values are possible # Explicitly check for None here as other "falsy" values are possible
elif default is not None: elif default is not None:
setattr(self.strategy, attribute, default) setattr(strategy, attribute, default)
config[attribute] = default config[attribute] = default
def _strategy_sanity_validations(self): @staticmethod
if not all(k in self.strategy.order_types for k in constants.REQUIRED_ORDERTYPES): def _strategy_sanity_validations(strategy):
raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. " if not all(k in strategy.order_types for k in constants.REQUIRED_ORDERTYPES):
raise ImportError(f"Impossible to load Strategy '{strategy.__class__.__name__}'. "
f"Order-types mapping is incomplete.") f"Order-types mapping is incomplete.")
if not all(k in self.strategy.order_time_in_force for k in constants.REQUIRED_ORDERTIF): if not all(k in strategy.order_time_in_force for k in constants.REQUIRED_ORDERTIF):
raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. " raise ImportError(f"Impossible to load Strategy '{strategy.__class__.__name__}'. "
f"Order-time-in-force mapping is incomplete.") f"Order-time-in-force mapping is incomplete.")
def _load_strategy( @staticmethod
self, strategy_name: str, config: dict, extra_dir: Optional[str] = None) -> IStrategy: def _load_strategy(strategy_name: str,
config: dict, extra_dir: Optional[str] = None) -> IStrategy:
""" """
Search and loads the specified strategy. Search and loads the specified strategy.
:param strategy_name: name of the module to import :param strategy_name: name of the module to import
@ -128,9 +134,9 @@ class StrategyResolver(IResolver):
""" """
current_path = Path(__file__).parent.parent.joinpath('strategy').resolve() current_path = Path(__file__).parent.parent.joinpath('strategy').resolve()
abs_paths = self.build_search_paths(config, current_path=current_path, abs_paths = IResolver.build_search_paths(config, current_path=current_path,
user_subdir=constants.USERPATH_STRATEGY, user_subdir=constants.USERPATH_STRATEGY,
extra_dir=extra_dir) extra_dir=extra_dir)
if ":" in strategy_name: if ":" in strategy_name:
logger.info("loading base64 encoded strategy") logger.info("loading base64 encoded strategy")
@ -148,8 +154,8 @@ class StrategyResolver(IResolver):
# register temp path with the bot # register temp path with the bot
abs_paths.insert(0, temp.resolve()) abs_paths.insert(0, temp.resolve())
strategy = self._load_object(paths=abs_paths, object_type=IStrategy, strategy = IResolver._load_object(paths=abs_paths, object_type=IStrategy,
object_name=strategy_name, kwargs={'config': config}) object_name=strategy_name, kwargs={'config': config})
if strategy: if strategy:
strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args) strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args)
strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)

View File

@ -168,11 +168,24 @@ class IStrategy(ABC):
""" """
Locks pair until a given timestamp happens. Locks pair until a given timestamp happens.
Locked pairs are not analyzed, and are prevented from opening new trades. Locked pairs are not analyzed, and are prevented from opening new trades.
Locks can only count up (allowing users to lock pairs for a longer period of time).
To remove a lock from a pair, use `unlock_pair()`
:param pair: Pair to lock :param pair: Pair to lock
:param until: datetime in UTC until the pair should be blocked from opening new trades. :param until: datetime in UTC until the pair should be blocked from opening new trades.
Needs to be timezone aware `datetime.now(timezone.utc)` Needs to be timezone aware `datetime.now(timezone.utc)`
""" """
self._pair_locked_until[pair] = until if pair not in self._pair_locked_until or self._pair_locked_until[pair] < until:
self._pair_locked_until[pair] = until
def unlock_pair(self, pair) -> None:
"""
Unlocks a pair previously locked using lock_pair.
Not used by freqtrade itself, but intended to be used if users lock pairs
manually from within the strategy, to allow an easy way to unlock pairs.
:param pair: Unlock pair to allow trading again
"""
if pair in self._pair_locked_until:
del self._pair_locked_until[pair]
def is_pair_locked(self, pair: str) -> bool: def is_pair_locked(self, pair: str) -> bool:
""" """

View File

@ -73,9 +73,9 @@
"source": [ "source": [
"# Load strategy using values set above\n", "# Load strategy using values set above\n",
"from freqtrade.resolvers import StrategyResolver\n", "from freqtrade.resolvers import StrategyResolver\n",
"strategy = StrategyResolver({'strategy': strategy_name,\n", "strategy = StrategyResolver.load_strategy({'strategy': strategy_name,\n",
" 'user_data_dir': user_data_dir,\n", " 'user_data_dir': user_data_dir,\n",
" 'strategy_path': strategy_location}).strategy\n", " 'strategy_path': strategy_location})\n",
"\n", "\n",
"# Generate buy/sell signals using strategy\n", "# Generate buy/sell signals using strategy\n",
"df = strategy.analyze_ticker(candles, {'pair': pair})\n", "df = strategy.analyze_ticker(candles, {'pair': pair})\n",

View File

@ -198,7 +198,7 @@ def start_download_data(args: Dict[str, Any]) -> None:
pairs_not_available: List[str] = [] pairs_not_available: List[str] = []
# Init exchange # Init exchange
exchange = ExchangeResolver(config['exchange']['name'], config).exchange exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
try: try:
if config.get('download_trades'): if config.get('download_trades'):
@ -233,7 +233,7 @@ def start_list_timeframes(args: Dict[str, Any]) -> None:
config['ticker_interval'] = None config['ticker_interval'] = None
# Init exchange # Init exchange
exchange = ExchangeResolver(config['exchange']['name'], config, validate=False).exchange exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
if args['print_one_column']: if args['print_one_column']:
print('\n'.join(exchange.timeframes)) print('\n'.join(exchange.timeframes))
@ -252,7 +252,7 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
# Init exchange # Init exchange
exchange = ExchangeResolver(config['exchange']['name'], config, validate=False).exchange exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
# By default only active pairs/markets are to be shown # By default only active pairs/markets are to be shown
active_only = not args.get('list_pairs_all', False) active_only = not args.get('list_pairs_all', False)
@ -333,7 +333,7 @@ def start_test_pairlist(args: Dict[str, Any]) -> None:
from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.pairlist.pairlistmanager import PairListManager
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
exchange = ExchangeResolver(config['exchange']['name'], config, validate=False).exchange exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
quote_currencies = args.get('quote_currencies') quote_currencies = args.get('quote_currencies')
if not quote_currencies: if not quote_currencies:

View File

@ -1,7 +1,7 @@
# requirements without requirements installable via conda # requirements without requirements installable via conda
# mainly used for Raspberry pi installs # mainly used for Raspberry pi installs
ccxt==1.20.84 ccxt==1.21.12
SQLAlchemy==1.3.11 SQLAlchemy==1.3.12
python-telegram-bot==12.2.0 python-telegram-bot==12.2.0
arrow==0.15.4 arrow==0.15.4
cachetools==4.0.0 cachetools==4.0.0

View File

@ -7,7 +7,7 @@ coveralls==1.9.2
flake8==3.7.9 flake8==3.7.9
flake8-type-annotations==0.1.0 flake8-type-annotations==0.1.0
flake8-tidy-imports==3.1.0 flake8-tidy-imports==3.1.0
mypy==0.750 mypy==0.761
pytest==5.3.2 pytest==5.3.2
pytest-asyncio==0.10.0 pytest-asyncio==0.10.0
pytest-cov==2.8.1 pytest-cov==2.8.1

View File

@ -2,7 +2,7 @@
-r requirements.txt -r requirements.txt
# Required for hyperopt # Required for hyperopt
scipy==1.3.3 scipy==1.4.1
scikit-learn==0.22 scikit-learn==0.22
scikit-optimize==0.5.2 scikit-optimize==0.5.2
filelock==3.0.12 filelock==3.0.12

View File

@ -1,5 +1,5 @@
# Load common requirements # Load common requirements
-r requirements-common.txt -r requirements-common.txt
numpy==1.17.4 numpy==1.18.0
pandas==0.25.3 pandas==0.25.3

View File

@ -59,7 +59,7 @@ setup(name='freqtrade',
license='GPLv3', license='GPLv3',
packages=['freqtrade'], packages=['freqtrade'],
setup_requires=['pytest-runner', 'numpy'], setup_requires=['pytest-runner', 'numpy'],
tests_require=['pytest', 'pytest-mock', 'pytest-cov'], tests_require=['pytest', 'pytest-asyncio', 'pytest-cov', 'pytest-mock', ],
install_requires=[ install_requires=[
# from requirements-common.txt # from requirements-common.txt
'ccxt>=1.18.1080', 'ccxt>=1.18.1080',
@ -99,8 +99,12 @@ setup(name='freqtrade',
], ],
}, },
classifiers=[ classifiers=[
'Programming Language :: Python :: 3.6', 'Environment :: Console',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Topic :: Office/Business :: Financial :: Investment',
'Intended Audience :: Science/Research', 'Intended Audience :: Science/Research',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Operating System :: MacOS',
'Operating System :: Unix',
'Topic :: Office/Business :: Financial :: Investment',
]) ])

View File

@ -77,7 +77,7 @@ def get_patched_exchange(mocker, config, api_mock=None, id='bittrex',
patch_exchange(mocker, api_mock, id, mock_markets) patch_exchange(mocker, api_mock, id, mock_markets)
config["exchange"]["name"] = id config["exchange"]["name"] = id
try: try:
exchange = ExchangeResolver(id, config).exchange exchange = ExchangeResolver.load_exchange(id, config)
except ImportError: except ImportError:
exchange = Exchange(config) exchange = Exchange(config)
return exchange return exchange

View File

@ -124,19 +124,19 @@ def test_exchange_resolver(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
exchange = ExchangeResolver('Bittrex', default_conf).exchange exchange = ExchangeResolver.load_exchange('Bittrex', default_conf)
assert isinstance(exchange, Exchange) assert isinstance(exchange, Exchange)
assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog) assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog)
caplog.clear() caplog.clear()
exchange = ExchangeResolver('kraken', default_conf).exchange exchange = ExchangeResolver.load_exchange('kraken', default_conf)
assert isinstance(exchange, Exchange) assert isinstance(exchange, Exchange)
assert isinstance(exchange, Kraken) assert isinstance(exchange, Kraken)
assert not isinstance(exchange, Binance) assert not isinstance(exchange, Binance)
assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.", assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.",
caplog) caplog)
exchange = ExchangeResolver('binance', default_conf).exchange exchange = ExchangeResolver.load_exchange('binance', default_conf)
assert isinstance(exchange, Exchange) assert isinstance(exchange, Exchange)
assert isinstance(exchange, Binance) assert isinstance(exchange, Binance)
assert not isinstance(exchange, Kraken) assert not isinstance(exchange, Kraken)
@ -145,7 +145,7 @@ def test_exchange_resolver(default_conf, mocker, caplog):
caplog) caplog)
# Test mapping # Test mapping
exchange = ExchangeResolver('binanceus', default_conf).exchange exchange = ExchangeResolver.load_exchange('binanceus', default_conf)
assert isinstance(exchange, Exchange) assert isinstance(exchange, Exchange)
assert isinstance(exchange, Binance) assert isinstance(exchange, Binance)
assert not isinstance(exchange, Kraken) assert not isinstance(exchange, Kraken)

View File

@ -163,7 +163,7 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
MagicMock(return_value=hyperopt(default_conf)) MagicMock(return_value=hyperopt(default_conf))
) )
default_conf.update({'hyperopt': 'DefaultHyperOpt'}) default_conf.update({'hyperopt': 'DefaultHyperOpt'})
x = HyperOptResolver(default_conf).hyperopt x = HyperOptResolver.load_hyperopt(default_conf)
assert not hasattr(x, 'populate_indicators') assert not hasattr(x, 'populate_indicators')
assert not hasattr(x, 'populate_buy_trend') assert not hasattr(x, 'populate_buy_trend')
assert not hasattr(x, 'populate_sell_trend') assert not hasattr(x, 'populate_sell_trend')
@ -180,7 +180,7 @@ def test_hyperoptresolver_wrongname(mocker, default_conf, caplog) -> None:
default_conf.update({'hyperopt': "NonExistingHyperoptClass"}) default_conf.update({'hyperopt': "NonExistingHyperoptClass"})
with pytest.raises(OperationalException, match=r'Impossible to load Hyperopt.*'): with pytest.raises(OperationalException, match=r'Impossible to load Hyperopt.*'):
HyperOptResolver(default_conf).hyperopt HyperOptResolver.load_hyperopt(default_conf)
def test_hyperoptresolver_noname(default_conf): def test_hyperoptresolver_noname(default_conf):
@ -188,7 +188,7 @@ def test_hyperoptresolver_noname(default_conf):
with pytest.raises(OperationalException, with pytest.raises(OperationalException,
match="No Hyperopt set. Please use `--hyperopt` to specify " match="No Hyperopt set. Please use `--hyperopt` to specify "
"the Hyperopt class to use."): "the Hyperopt class to use."):
HyperOptResolver(default_conf) HyperOptResolver.load_hyperopt(default_conf)
def test_hyperoptlossresolver(mocker, default_conf, caplog) -> None: def test_hyperoptlossresolver(mocker, default_conf, caplog) -> None:
@ -198,7 +198,7 @@ def test_hyperoptlossresolver(mocker, default_conf, caplog) -> None:
'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver._load_hyperoptloss', 'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver._load_hyperoptloss',
MagicMock(return_value=hl) MagicMock(return_value=hl)
) )
x = HyperOptLossResolver(default_conf).hyperoptloss x = HyperOptLossResolver.load_hyperoptloss(default_conf)
assert hasattr(x, "hyperopt_loss_function") assert hasattr(x, "hyperopt_loss_function")
@ -206,7 +206,7 @@ def test_hyperoptlossresolver_wrongname(mocker, default_conf, caplog) -> None:
default_conf.update({'hyperopt_loss': "NonExistingLossClass"}) default_conf.update({'hyperopt_loss': "NonExistingLossClass"})
with pytest.raises(OperationalException, match=r'Impossible to load HyperoptLoss.*'): with pytest.raises(OperationalException, match=r'Impossible to load HyperoptLoss.*'):
HyperOptLossResolver(default_conf).hyperopt HyperOptLossResolver.load_hyperoptloss(default_conf)
def test_start_not_installed(mocker, default_conf, caplog, import_fails) -> None: def test_start_not_installed(mocker, default_conf, caplog, import_fails) -> None:
@ -286,7 +286,7 @@ def test_start_filelock(mocker, default_conf, caplog) -> None:
def test_loss_calculation_prefer_correct_trade_count(default_conf, hyperopt_results) -> None: def test_loss_calculation_prefer_correct_trade_count(default_conf, hyperopt_results) -> None:
hl = HyperOptLossResolver(default_conf).hyperoptloss hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
correct = hl.hyperopt_loss_function(hyperopt_results, 600) correct = hl.hyperopt_loss_function(hyperopt_results, 600)
over = hl.hyperopt_loss_function(hyperopt_results, 600 + 100) over = hl.hyperopt_loss_function(hyperopt_results, 600 + 100)
under = hl.hyperopt_loss_function(hyperopt_results, 600 - 100) under = hl.hyperopt_loss_function(hyperopt_results, 600 - 100)
@ -298,7 +298,7 @@ def test_loss_calculation_prefer_shorter_trades(default_conf, hyperopt_results)
resultsb = hyperopt_results.copy() resultsb = hyperopt_results.copy()
resultsb.loc[1, 'trade_duration'] = 20 resultsb.loc[1, 'trade_duration'] = 20
hl = HyperOptLossResolver(default_conf).hyperoptloss hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
longer = hl.hyperopt_loss_function(hyperopt_results, 100) longer = hl.hyperopt_loss_function(hyperopt_results, 100)
shorter = hl.hyperopt_loss_function(resultsb, 100) shorter = hl.hyperopt_loss_function(resultsb, 100)
assert shorter < longer assert shorter < longer
@ -310,7 +310,7 @@ def test_loss_calculation_has_limited_profit(default_conf, hyperopt_results) ->
results_under = hyperopt_results.copy() results_under = hyperopt_results.copy()
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
hl = HyperOptLossResolver(default_conf).hyperoptloss hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
correct = hl.hyperopt_loss_function(hyperopt_results, 600) correct = hl.hyperopt_loss_function(hyperopt_results, 600)
over = hl.hyperopt_loss_function(results_over, 600) over = hl.hyperopt_loss_function(results_over, 600)
under = hl.hyperopt_loss_function(results_under, 600) under = hl.hyperopt_loss_function(results_under, 600)
@ -325,7 +325,7 @@ def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> N
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
default_conf.update({'hyperopt_loss': 'SharpeHyperOptLoss'}) default_conf.update({'hyperopt_loss': 'SharpeHyperOptLoss'})
hl = HyperOptLossResolver(default_conf).hyperoptloss hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results),
datetime(2019, 1, 1), datetime(2019, 5, 1)) datetime(2019, 1, 1), datetime(2019, 5, 1))
over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), over = hl.hyperopt_loss_function(results_over, len(hyperopt_results),
@ -343,7 +343,7 @@ def test_onlyprofit_loss_prefers_higher_profits(default_conf, hyperopt_results)
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
default_conf.update({'hyperopt_loss': 'OnlyProfitHyperOptLoss'}) default_conf.update({'hyperopt_loss': 'OnlyProfitHyperOptLoss'})
hl = HyperOptLossResolver(default_conf).hyperoptloss hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results),
datetime(2019, 1, 1), datetime(2019, 5, 1)) datetime(2019, 1, 1), datetime(2019, 5, 1))
over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), over = hl.hyperopt_loss_function(results_over, len(hyperopt_results),

View File

@ -53,7 +53,8 @@ def test_load_pairlist_noexist(mocker, markets, default_conf):
with pytest.raises(OperationalException, with pytest.raises(OperationalException,
match=r"Impossible to load Pairlist 'NonexistingPairList'. " match=r"Impossible to load Pairlist 'NonexistingPairList'. "
r"This class does not exist or contains Python code errors."): r"This class does not exist or contains Python code errors."):
PairListResolver('NonexistingPairList', bot.exchange, plm, default_conf, {}, 1) PairListResolver.load_pairlist('NonexistingPairList', bot.exchange, plm,
default_conf, {}, 1)
def test_refresh_market_pair_not_in_whitelist(mocker, markets, static_pl_conf): def test_refresh_market_pair_not_in_whitelist(mocker, markets, static_pl_conf):

View File

@ -302,6 +302,19 @@ def test_is_pair_locked(default_conf):
# ETH/BTC locked for 4 minutes # ETH/BTC locked for 4 minutes
assert strategy.is_pair_locked(pair) assert strategy.is_pair_locked(pair)
# Test lock does not change
lock = strategy._pair_locked_until[pair]
strategy.lock_pair(pair, arrow.utcnow().shift(minutes=2).datetime)
assert lock == strategy._pair_locked_until[pair]
# XRP/BTC should not be locked now # XRP/BTC should not be locked now
pair = 'XRP/BTC' pair = 'XRP/BTC'
assert not strategy.is_pair_locked(pair) assert not strategy.is_pair_locked(pair)
# Unlocking a pair that's not locked should not raise an error
strategy.unlock_pair(pair)
# Unlock original pair
pair = 'ETH/BTC'
strategy.unlock_pair(pair)
assert not strategy.is_pair_locked(pair)

View File

@ -39,8 +39,8 @@ def test_load_strategy(default_conf, result):
default_conf.update({'strategy': 'SampleStrategy', default_conf.update({'strategy': 'SampleStrategy',
'strategy_path': str(Path(__file__).parents[2] / 'freqtrade/templates') 'strategy_path': str(Path(__file__).parents[2] / 'freqtrade/templates')
}) })
resolver = StrategyResolver(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
assert 'rsi' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) assert 'rsi' in strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
def test_load_strategy_base64(result, caplog, default_conf): def test_load_strategy_base64(result, caplog, default_conf):
@ -48,8 +48,8 @@ def test_load_strategy_base64(result, caplog, default_conf):
encoded_string = urlsafe_b64encode(file.read()).decode("utf-8") encoded_string = urlsafe_b64encode(file.read()).decode("utf-8")
default_conf.update({'strategy': 'SampleStrategy:{}'.format(encoded_string)}) default_conf.update({'strategy': 'SampleStrategy:{}'.format(encoded_string)})
resolver = StrategyResolver(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
assert 'rsi' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) assert 'rsi' in strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
# Make sure strategy was loaded from base64 (using temp directory)!! # Make sure strategy was loaded from base64 (using temp directory)!!
assert log_has_re(r"Using resolved strategy SampleStrategy from '" assert log_has_re(r"Using resolved strategy SampleStrategy from '"
r".*(/|\\).*(/|\\)SampleStrategy\.py'\.\.\.", caplog) r".*(/|\\).*(/|\\)SampleStrategy\.py'\.\.\.", caplog)
@ -57,13 +57,13 @@ def test_load_strategy_base64(result, caplog, default_conf):
def test_load_strategy_invalid_directory(result, caplog, default_conf): def test_load_strategy_invalid_directory(result, caplog, default_conf):
default_conf['strategy'] = 'DefaultStrategy' default_conf['strategy'] = 'DefaultStrategy'
resolver = StrategyResolver(default_conf)
extra_dir = Path.cwd() / 'some/path' extra_dir = Path.cwd() / 'some/path'
resolver._load_strategy('DefaultStrategy', config=default_conf, extra_dir=extra_dir) strategy = StrategyResolver._load_strategy('DefaultStrategy', config=default_conf,
extra_dir=extra_dir)
assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog) assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog)
assert 'rsi' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) assert 'rsi' in strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
def test_load_not_found_strategy(default_conf): def test_load_not_found_strategy(default_conf):
@ -71,7 +71,7 @@ def test_load_not_found_strategy(default_conf):
with pytest.raises(OperationalException, with pytest.raises(OperationalException,
match=r"Impossible to load Strategy 'NotFoundStrategy'. " match=r"Impossible to load Strategy 'NotFoundStrategy'. "
r"This class does not exist or contains Python code errors."): r"This class does not exist or contains Python code errors."):
StrategyResolver(default_conf) StrategyResolver.load_strategy(default_conf)
def test_load_strategy_noname(default_conf): def test_load_strategy_noname(default_conf):
@ -79,30 +79,30 @@ def test_load_strategy_noname(default_conf):
with pytest.raises(OperationalException, with pytest.raises(OperationalException,
match="No strategy set. Please use `--strategy` to specify " match="No strategy set. Please use `--strategy` to specify "
"the strategy class to use."): "the strategy class to use."):
StrategyResolver(default_conf) StrategyResolver.load_strategy(default_conf)
def test_strategy(result, default_conf): def test_strategy(result, default_conf):
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'DefaultStrategy'})
resolver = StrategyResolver(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
metadata = {'pair': 'ETH/BTC'} metadata = {'pair': 'ETH/BTC'}
assert resolver.strategy.minimal_roi[0] == 0.04 assert strategy.minimal_roi[0] == 0.04
assert default_conf["minimal_roi"]['0'] == 0.04 assert default_conf["minimal_roi"]['0'] == 0.04
assert resolver.strategy.stoploss == -0.10 assert strategy.stoploss == -0.10
assert default_conf['stoploss'] == -0.10 assert default_conf['stoploss'] == -0.10
assert resolver.strategy.ticker_interval == '5m' assert strategy.ticker_interval == '5m'
assert default_conf['ticker_interval'] == '5m' assert default_conf['ticker_interval'] == '5m'
df_indicators = resolver.strategy.advise_indicators(result, metadata=metadata) df_indicators = strategy.advise_indicators(result, metadata=metadata)
assert 'adx' in df_indicators assert 'adx' in df_indicators
dataframe = resolver.strategy.advise_buy(df_indicators, metadata=metadata) dataframe = strategy.advise_buy(df_indicators, metadata=metadata)
assert 'buy' in dataframe.columns assert 'buy' in dataframe.columns
dataframe = resolver.strategy.advise_sell(df_indicators, metadata=metadata) dataframe = strategy.advise_sell(df_indicators, metadata=metadata)
assert 'sell' in dataframe.columns assert 'sell' in dataframe.columns
@ -114,9 +114,9 @@ def test_strategy_override_minimal_roi(caplog, default_conf):
"0": 0.5 "0": 0.5
} }
}) })
resolver = StrategyResolver(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
assert resolver.strategy.minimal_roi[0] == 0.5 assert strategy.minimal_roi[0] == 0.5
assert log_has("Override strategy 'minimal_roi' with value in config file: {'0': 0.5}.", caplog) assert log_has("Override strategy 'minimal_roi' with value in config file: {'0': 0.5}.", caplog)
@ -126,9 +126,9 @@ def test_strategy_override_stoploss(caplog, default_conf):
'strategy': 'DefaultStrategy', 'strategy': 'DefaultStrategy',
'stoploss': -0.5 'stoploss': -0.5
}) })
resolver = StrategyResolver(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
assert resolver.strategy.stoploss == -0.5 assert strategy.stoploss == -0.5
assert log_has("Override strategy 'stoploss' with value in config file: -0.5.", caplog) assert log_has("Override strategy 'stoploss' with value in config file: -0.5.", caplog)
@ -138,10 +138,10 @@ def test_strategy_override_trailing_stop(caplog, default_conf):
'strategy': 'DefaultStrategy', 'strategy': 'DefaultStrategy',
'trailing_stop': True 'trailing_stop': True
}) })
resolver = StrategyResolver(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
assert resolver.strategy.trailing_stop assert strategy.trailing_stop
assert isinstance(resolver.strategy.trailing_stop, bool) assert isinstance(strategy.trailing_stop, bool)
assert log_has("Override strategy 'trailing_stop' with value in config file: True.", caplog) assert log_has("Override strategy 'trailing_stop' with value in config file: True.", caplog)
@ -153,13 +153,13 @@ def test_strategy_override_trailing_stop_positive(caplog, default_conf):
'trailing_stop_positive_offset': -0.2 'trailing_stop_positive_offset': -0.2
}) })
resolver = StrategyResolver(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
assert resolver.strategy.trailing_stop_positive == -0.1 assert strategy.trailing_stop_positive == -0.1
assert log_has("Override strategy 'trailing_stop_positive' with value in config file: -0.1.", assert log_has("Override strategy 'trailing_stop_positive' with value in config file: -0.1.",
caplog) caplog)
assert resolver.strategy.trailing_stop_positive_offset == -0.2 assert strategy.trailing_stop_positive_offset == -0.2
assert log_has("Override strategy 'trailing_stop_positive' with value in config file: -0.1.", assert log_has("Override strategy 'trailing_stop_positive' with value in config file: -0.1.",
caplog) caplog)
@ -172,10 +172,10 @@ def test_strategy_override_ticker_interval(caplog, default_conf):
'ticker_interval': 60, 'ticker_interval': 60,
'stake_currency': 'ETH' 'stake_currency': 'ETH'
}) })
resolver = StrategyResolver(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
assert resolver.strategy.ticker_interval == 60 assert strategy.ticker_interval == 60
assert resolver.strategy.stake_currency == 'ETH' assert strategy.stake_currency == 'ETH'
assert log_has("Override strategy 'ticker_interval' with value in config file: 60.", assert log_has("Override strategy 'ticker_interval' with value in config file: 60.",
caplog) caplog)
@ -187,9 +187,9 @@ def test_strategy_override_process_only_new_candles(caplog, default_conf):
'strategy': 'DefaultStrategy', 'strategy': 'DefaultStrategy',
'process_only_new_candles': True 'process_only_new_candles': True
}) })
resolver = StrategyResolver(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
assert resolver.strategy.process_only_new_candles assert strategy.process_only_new_candles
assert log_has("Override strategy 'process_only_new_candles' with value in config file: True.", assert log_has("Override strategy 'process_only_new_candles' with value in config file: True.",
caplog) caplog)
@ -207,11 +207,11 @@ def test_strategy_override_order_types(caplog, default_conf):
'strategy': 'DefaultStrategy', 'strategy': 'DefaultStrategy',
'order_types': order_types 'order_types': order_types
}) })
resolver = StrategyResolver(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
assert resolver.strategy.order_types assert strategy.order_types
for method in ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']: for method in ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']:
assert resolver.strategy.order_types[method] == order_types[method] assert strategy.order_types[method] == order_types[method]
assert log_has("Override strategy 'order_types' with value in config file:" assert log_has("Override strategy 'order_types' with value in config file:"
" {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit'," " {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit',"
@ -225,7 +225,7 @@ def test_strategy_override_order_types(caplog, default_conf):
with pytest.raises(ImportError, with pytest.raises(ImportError,
match=r"Impossible to load Strategy 'DefaultStrategy'. " match=r"Impossible to load Strategy 'DefaultStrategy'. "
r"Order-types mapping is incomplete."): r"Order-types mapping is incomplete."):
StrategyResolver(default_conf) StrategyResolver.load_strategy(default_conf)
def test_strategy_override_order_tif(caplog, default_conf): def test_strategy_override_order_tif(caplog, default_conf):
@ -240,11 +240,11 @@ def test_strategy_override_order_tif(caplog, default_conf):
'strategy': 'DefaultStrategy', 'strategy': 'DefaultStrategy',
'order_time_in_force': order_time_in_force 'order_time_in_force': order_time_in_force
}) })
resolver = StrategyResolver(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
assert resolver.strategy.order_time_in_force assert strategy.order_time_in_force
for method in ['buy', 'sell']: for method in ['buy', 'sell']:
assert resolver.strategy.order_time_in_force[method] == order_time_in_force[method] assert strategy.order_time_in_force[method] == order_time_in_force[method]
assert log_has("Override strategy 'order_time_in_force' with value in config file:" assert log_has("Override strategy 'order_time_in_force' with value in config file:"
" {'buy': 'fok', 'sell': 'gtc'}.", caplog) " {'buy': 'fok', 'sell': 'gtc'}.", caplog)
@ -257,7 +257,7 @@ def test_strategy_override_order_tif(caplog, default_conf):
with pytest.raises(ImportError, with pytest.raises(ImportError,
match=r"Impossible to load Strategy 'DefaultStrategy'. " match=r"Impossible to load Strategy 'DefaultStrategy'. "
r"Order-time-in-force mapping is incomplete."): r"Order-time-in-force mapping is incomplete."):
StrategyResolver(default_conf) StrategyResolver.load_strategy(default_conf)
def test_strategy_override_use_sell_signal(caplog, default_conf): def test_strategy_override_use_sell_signal(caplog, default_conf):
@ -265,9 +265,9 @@ def test_strategy_override_use_sell_signal(caplog, default_conf):
default_conf.update({ default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'DefaultStrategy',
}) })
resolver = StrategyResolver(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
assert resolver.strategy.use_sell_signal assert strategy.use_sell_signal
assert isinstance(resolver.strategy.use_sell_signal, bool) assert isinstance(strategy.use_sell_signal, bool)
# must be inserted to configuration # must be inserted to configuration
assert 'use_sell_signal' in default_conf['ask_strategy'] assert 'use_sell_signal' in default_conf['ask_strategy']
assert default_conf['ask_strategy']['use_sell_signal'] assert default_conf['ask_strategy']['use_sell_signal']
@ -278,10 +278,10 @@ def test_strategy_override_use_sell_signal(caplog, default_conf):
'use_sell_signal': False, 'use_sell_signal': False,
}, },
}) })
resolver = StrategyResolver(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
assert not resolver.strategy.use_sell_signal assert not strategy.use_sell_signal
assert isinstance(resolver.strategy.use_sell_signal, bool) assert isinstance(strategy.use_sell_signal, bool)
assert log_has("Override strategy 'use_sell_signal' with value in config file: False.", caplog) assert log_has("Override strategy 'use_sell_signal' with value in config file: False.", caplog)
@ -290,9 +290,9 @@ def test_strategy_override_use_sell_profit_only(caplog, default_conf):
default_conf.update({ default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'DefaultStrategy',
}) })
resolver = StrategyResolver(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
assert not resolver.strategy.sell_profit_only assert not strategy.sell_profit_only
assert isinstance(resolver.strategy.sell_profit_only, bool) assert isinstance(strategy.sell_profit_only, bool)
# must be inserted to configuration # must be inserted to configuration
assert 'sell_profit_only' in default_conf['ask_strategy'] assert 'sell_profit_only' in default_conf['ask_strategy']
assert not default_conf['ask_strategy']['sell_profit_only'] assert not default_conf['ask_strategy']['sell_profit_only']
@ -303,10 +303,10 @@ def test_strategy_override_use_sell_profit_only(caplog, default_conf):
'sell_profit_only': True, 'sell_profit_only': True,
}, },
}) })
resolver = StrategyResolver(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
assert resolver.strategy.sell_profit_only assert strategy.sell_profit_only
assert isinstance(resolver.strategy.sell_profit_only, bool) assert isinstance(strategy.sell_profit_only, bool)
assert log_has("Override strategy 'sell_profit_only' with value in config file: True.", caplog) assert log_has("Override strategy 'sell_profit_only' with value in config file: True.", caplog)
@ -315,11 +315,11 @@ def test_deprecate_populate_indicators(result, default_conf):
default_location = path.join(path.dirname(path.realpath(__file__))) default_location = path.join(path.dirname(path.realpath(__file__)))
default_conf.update({'strategy': 'TestStrategyLegacy', default_conf.update({'strategy': 'TestStrategyLegacy',
'strategy_path': default_location}) 'strategy_path': default_location})
resolver = StrategyResolver(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
with warnings.catch_warnings(record=True) as w: with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered. # Cause all warnings to always be triggered.
warnings.simplefilter("always") warnings.simplefilter("always")
indicators = resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) indicators = strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
assert len(w) == 1 assert len(w) == 1
assert issubclass(w[-1].category, DeprecationWarning) assert issubclass(w[-1].category, DeprecationWarning)
assert "deprecated - check out the Sample strategy to see the current function headers!" \ assert "deprecated - check out the Sample strategy to see the current function headers!" \
@ -328,7 +328,7 @@ def test_deprecate_populate_indicators(result, default_conf):
with warnings.catch_warnings(record=True) as w: with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered. # Cause all warnings to always be triggered.
warnings.simplefilter("always") warnings.simplefilter("always")
resolver.strategy.advise_buy(indicators, {'pair': 'ETH/BTC'}) strategy.advise_buy(indicators, {'pair': 'ETH/BTC'})
assert len(w) == 1 assert len(w) == 1
assert issubclass(w[-1].category, DeprecationWarning) assert issubclass(w[-1].category, DeprecationWarning)
assert "deprecated - check out the Sample strategy to see the current function headers!" \ assert "deprecated - check out the Sample strategy to see the current function headers!" \
@ -337,7 +337,7 @@ def test_deprecate_populate_indicators(result, default_conf):
with warnings.catch_warnings(record=True) as w: with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered. # Cause all warnings to always be triggered.
warnings.simplefilter("always") warnings.simplefilter("always")
resolver.strategy.advise_sell(indicators, {'pair': 'ETH_BTC'}) strategy.advise_sell(indicators, {'pair': 'ETH_BTC'})
assert len(w) == 1 assert len(w) == 1
assert issubclass(w[-1].category, DeprecationWarning) assert issubclass(w[-1].category, DeprecationWarning)
assert "deprecated - check out the Sample strategy to see the current function headers!" \ assert "deprecated - check out the Sample strategy to see the current function headers!" \
@ -349,47 +349,47 @@ def test_call_deprecated_function(result, monkeypatch, default_conf):
default_location = path.join(path.dirname(path.realpath(__file__))) default_location = path.join(path.dirname(path.realpath(__file__)))
default_conf.update({'strategy': 'TestStrategyLegacy', default_conf.update({'strategy': 'TestStrategyLegacy',
'strategy_path': default_location}) 'strategy_path': default_location})
resolver = StrategyResolver(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
metadata = {'pair': 'ETH/BTC'} metadata = {'pair': 'ETH/BTC'}
# Make sure we are using a legacy function # Make sure we are using a legacy function
assert resolver.strategy._populate_fun_len == 2 assert strategy._populate_fun_len == 2
assert resolver.strategy._buy_fun_len == 2 assert strategy._buy_fun_len == 2
assert resolver.strategy._sell_fun_len == 2 assert strategy._sell_fun_len == 2
assert resolver.strategy.INTERFACE_VERSION == 1 assert strategy.INTERFACE_VERSION == 1
indicator_df = resolver.strategy.advise_indicators(result, metadata=metadata) indicator_df = strategy.advise_indicators(result, metadata=metadata)
assert isinstance(indicator_df, DataFrame) assert isinstance(indicator_df, DataFrame)
assert 'adx' in indicator_df.columns assert 'adx' in indicator_df.columns
buydf = resolver.strategy.advise_buy(result, metadata=metadata) buydf = strategy.advise_buy(result, metadata=metadata)
assert isinstance(buydf, DataFrame) assert isinstance(buydf, DataFrame)
assert 'buy' in buydf.columns assert 'buy' in buydf.columns
selldf = resolver.strategy.advise_sell(result, metadata=metadata) selldf = strategy.advise_sell(result, metadata=metadata)
assert isinstance(selldf, DataFrame) assert isinstance(selldf, DataFrame)
assert 'sell' in selldf assert 'sell' in selldf
def test_strategy_interface_versioning(result, monkeypatch, default_conf): def test_strategy_interface_versioning(result, monkeypatch, default_conf):
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'DefaultStrategy'})
resolver = StrategyResolver(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
metadata = {'pair': 'ETH/BTC'} metadata = {'pair': 'ETH/BTC'}
# Make sure we are using a legacy function # Make sure we are using a legacy function
assert resolver.strategy._populate_fun_len == 3 assert strategy._populate_fun_len == 3
assert resolver.strategy._buy_fun_len == 3 assert strategy._buy_fun_len == 3
assert resolver.strategy._sell_fun_len == 3 assert strategy._sell_fun_len == 3
assert resolver.strategy.INTERFACE_VERSION == 2 assert strategy.INTERFACE_VERSION == 2
indicator_df = resolver.strategy.advise_indicators(result, metadata=metadata) indicator_df = strategy.advise_indicators(result, metadata=metadata)
assert isinstance(indicator_df, DataFrame) assert isinstance(indicator_df, DataFrame)
assert 'adx' in indicator_df.columns assert 'adx' in indicator_df.columns
buydf = resolver.strategy.advise_buy(result, metadata=metadata) buydf = strategy.advise_buy(result, metadata=metadata)
assert isinstance(buydf, DataFrame) assert isinstance(buydf, DataFrame)
assert 'buy' in buydf.columns assert 'buy' in buydf.columns
selldf = resolver.strategy.advise_sell(result, metadata=metadata) selldf = strategy.advise_sell(result, metadata=metadata)
assert isinstance(selldf, DataFrame) assert isinstance(selldf, DataFrame)
assert 'sell' in selldf assert 'sell' in selldf

View File

@ -100,7 +100,7 @@ def test_init_dryrun_db(default_conf, mocker):
init(default_conf['db_url'], default_conf['dry_run']) init(default_conf['db_url'], default_conf['dry_run'])
assert create_engine_mock.call_count == 1 assert create_engine_mock.call_count == 1
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite://' assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.dryrun.sqlite'
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")