Merge pull request #4273 from freqtrade/new_release

New release 2021.1
This commit is contained in:
Matthias 2021-01-27 19:02:52 +01:00 committed by GitHub
commit 766c786d90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
97 changed files with 2491 additions and 1624 deletions

View File

@ -3,13 +3,15 @@ FROM freqtradeorg/freqtrade:develop
# Install dependencies # Install dependencies
COPY requirements-dev.txt /freqtrade/ COPY requirements-dev.txt /freqtrade/
RUN apt-get update \ RUN apt-get update \
&& apt-get -y install git sudo vim \ && apt-get -y install git mercurial sudo vim \
&& apt-get clean \ && apt-get clean \
&& pip install autopep8 -r docs/requirements-docs.txt -r requirements-dev.txt --no-cache-dir \ && pip install autopep8 -r docs/requirements-docs.txt -r requirements-dev.txt --no-cache-dir \
&& useradd -u 1000 -U -m ftuser \ && useradd -u 1000 -U -m ftuser \
&& mkdir -p /home/ftuser/.vscode-server /home/ftuser/.vscode-server-insiders /home/ftuser/commandhistory \ && mkdir -p /home/ftuser/.vscode-server /home/ftuser/.vscode-server-insiders /home/ftuser/commandhistory \
&& echo "export PROMPT_COMMAND='history -a'" >> /home/ftuser/.bashrc \ && echo "export PROMPT_COMMAND='history -a'" >> /home/ftuser/.bashrc \
&& echo "export HISTFILE=~/commandhistory/.bash_history" >> /home/ftuser/.bashrc \ && echo "export HISTFILE=~/commandhistory/.bash_history" >> /home/ftuser/.bashrc \
&& mv /root/.local /home/ftuser/.local/ \
&& chown ftuser:ftuser -R /home/ftuser/.local/ \
&& chown ftuser: -R /home/ftuser/ && chown ftuser: -R /home/ftuser/
USER ftuser USER ftuser

View File

@ -79,13 +79,13 @@ jobs:
- name: Backtesting - name: Backtesting
run: | run: |
cp config.json.example config.json cp config_bittrex.json.example config.json
freqtrade create-userdir --userdir user_data freqtrade create-userdir --userdir user_data
freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy
- name: Hyperopt - name: Hyperopt
run: | run: |
cp config.json.example config.json cp config_bittrex.json.example config.json
freqtrade create-userdir --userdir user_data freqtrade create-userdir --userdir user_data
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all
@ -117,7 +117,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ macos-latest ] os: [ macos-latest ]
python-version: [3.7, 3.8] python-version: [3.7, 3.8, 3.9]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -146,8 +146,9 @@ jobs:
run: | run: |
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd .. cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
- name: Installation - *nix - name: Installation - macOS
run: | run: |
brew install hdf5 c-blosc
python -m pip install --upgrade pip python -m pip install --upgrade pip
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_LIBRARY_PATH=${HOME}/dependencies/lib
@ -170,13 +171,13 @@ jobs:
- name: Backtesting - name: Backtesting
run: | run: |
cp config.json.example config.json cp config_bittrex.json.example config.json
freqtrade create-userdir --userdir user_data freqtrade create-userdir --userdir user_data
freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy
- name: Hyperopt - name: Hyperopt
run: | run: |
cp config.json.example config.json cp config_bittrex.json.example config.json
freqtrade create-userdir --userdir user_data freqtrade create-userdir --userdir user_data
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all
@ -237,13 +238,13 @@ jobs:
- name: Backtesting - name: Backtesting
run: | run: |
cp config.json.example config.json cp config_bittrex.json.example config.json
freqtrade create-userdir --userdir user_data freqtrade create-userdir --userdir user_data
freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy
- name: Hyperopt - name: Hyperopt
run: | run: |
cp config.json.example config.json cp config_bittrex.json.example config.json
freqtrade create-userdir --userdir user_data freqtrade create-userdir --userdir user_data
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all

View File

@ -26,12 +26,12 @@ jobs:
# - coveralls || true # - coveralls || true
name: pytest name: pytest
- script: - script:
- cp config.json.example config.json - cp config_bittrex.json.example config.json
- freqtrade create-userdir --userdir user_data - freqtrade create-userdir --userdir user_data
- freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy - freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy
name: backtest name: backtest
- script: - script:
- cp config.json.example config.json - cp config_bittrex.json.example config.json
- freqtrade create-userdir --userdir user_data - freqtrade create-userdir --userdir user_data
- freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily
name: hyperopt name: hyperopt

View File

@ -12,7 +12,7 @@ Few pointers for contributions:
- New features need to contain unit tests, must conform to PEP8 (max-line-length = 100) and should be documented with the introduction PR. - New features need to contain unit tests, must conform to PEP8 (max-line-length = 100) and should be documented with the introduction PR.
- PR's can be declared as `[WIP]` - which signify Work in Progress Pull Requests (which are not finished). - PR's can be declared as `[WIP]` - which signify Work in Progress Pull Requests (which are not finished).
If you are unsure, discuss the feature on our [discord server](https://discord.gg/MA9v74M), on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR. If you are unsure, discuss the feature on our [discord server](https://discord.gg/MA9v74M), on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR.
## Getting started ## Getting started

View File

@ -1,5 +1,4 @@
include LICENSE include LICENSE
include README.md include README.md
include config.json.example
recursive-include freqtrade *.py recursive-include freqtrade *.py
recursive-include freqtrade/templates/ *.j2 *.ipynb recursive-include freqtrade/templates/ *.j2 *.ipynb

View File

@ -113,7 +113,7 @@ Telegram is not mandatory. However, this is a great way to control your bot. Mor
- `/start`: Starts the trader. - `/start`: Starts the trader.
- `/stop`: Stops the trader. - `/stop`: Stops the trader.
- `/stopbuy`: Stop entering new trades. - `/stopbuy`: Stop entering new trades.
- `/status [table]`: Lists all open trades. - `/status <trade_id>|[table]`: Lists all or specific open trades.
- `/profit`: Lists cumulative profit from all finished trades - `/profit`: Lists cumulative profit from all finished trades
- `/forcesell <trade_id>|all`: Instantly sells the given trade (Ignoring `minimum_roi`). - `/forcesell <trade_id>|all`: Instantly sells the given trade (Ignoring `minimum_roi`).
- `/performance`: Show performance of each finished trade grouped by pair - `/performance`: Show performance of each finished trade grouped by pair
@ -138,7 +138,7 @@ For any questions not covered by the documentation or for further information ab
Please check out our [discord server](https://discord.gg/MA9v74M). Please check out our [discord server](https://discord.gg/MA9v74M).
You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA). You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA).
### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) ### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)

View File

@ -30,7 +30,7 @@ if [ $? -ne 0 ]; then
fi fi
# Run backtest # Run backtest
docker run --rm -v $(pwd)/config.json.example:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy DefaultStrategy docker run --rm -v $(pwd)/config_bittrex.json.example:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy DefaultStrategy
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "failed running backtest" echo "failed running backtest"

View File

@ -84,12 +84,13 @@
"enabled": false, "enabled": false,
"listen_ip_address": "127.0.0.1", "listen_ip_address": "127.0.0.1",
"listen_port": 8080, "listen_port": 8080,
"verbosity": "info", "verbosity": "error",
"jwt_secret_key": "somethingrandom", "jwt_secret_key": "somethingrandom",
"CORS_origins": [], "CORS_origins": [],
"username": "", "username": "freqtrader",
"password": "" "password": "SuperSecurePassword"
}, },
"bot_name": "freqtrade",
"initial_state": "running", "initial_state": "running",
"forcebuy_enable": false, "forcebuy_enable": false,
"internals": { "internals": {

View File

@ -79,12 +79,13 @@
"enabled": false, "enabled": false,
"listen_ip_address": "127.0.0.1", "listen_ip_address": "127.0.0.1",
"listen_port": 8080, "listen_port": 8080,
"verbosity": "info", "verbosity": "error",
"jwt_secret_key": "somethingrandom", "jwt_secret_key": "somethingrandom",
"CORS_origins": [], "CORS_origins": [],
"username": "", "username": "freqtrader",
"password": "" "password": "SuperSecurePassword"
}, },
"bot_name": "freqtrade",
"initial_state": "running", "initial_state": "running",
"forcebuy_enable": false, "forcebuy_enable": false,
"internals": { "internals": {

View File

@ -42,6 +42,7 @@
"order_book_max": 1, "order_book_max": 1,
"use_sell_signal": true, "use_sell_signal": true,
"sell_profit_only": false, "sell_profit_only": false,
"sell_profit_offset": 0.0,
"ignore_roi_if_buy_signal": false "ignore_roi_if_buy_signal": false
}, },
"order_types": { "order_types": {
@ -103,7 +104,7 @@
} }
], ],
"exchange": { "exchange": {
"name": "bittrex", "name": "binance",
"sandbox": false, "sandbox": false,
"key": "your_exchange_key", "key": "your_exchange_key",
"secret": "your_exchange_secret", "secret": "your_exchange_secret",
@ -115,16 +116,21 @@
"aiohttp_trust_env": false "aiohttp_trust_env": false
}, },
"pair_whitelist": [ "pair_whitelist": [
"ALGO/BTC",
"ATOM/BTC",
"BAT/BTC",
"BCH/BTC",
"BRD/BTC",
"EOS/BTC",
"ETH/BTC", "ETH/BTC",
"IOTA/BTC",
"LINK/BTC",
"LTC/BTC", "LTC/BTC",
"ETC/BTC", "NEO/BTC",
"DASH/BTC", "NXS/BTC",
"ZEC/BTC", "XMR/BTC",
"XLM/BTC", "XRP/BTC",
"NXT/BTC", "XTZ/BTC"
"TRX/BTC",
"ADA/BTC",
"XMR/BTC"
], ],
"pair_blacklist": [ "pair_blacklist": [
"DOGE/BTC" "DOGE/BTC"
@ -147,7 +153,7 @@
"remove_pumps": false "remove_pumps": false
}, },
"telegram": { "telegram": {
"enabled": true, "enabled": false,
"token": "your_telegram_token", "token": "your_telegram_token",
"chat_id": "your_telegram_chat_id", "chat_id": "your_telegram_chat_id",
"notification_settings": { "notification_settings": {
@ -164,12 +170,14 @@
"enabled": false, "enabled": false,
"listen_ip_address": "127.0.0.1", "listen_ip_address": "127.0.0.1",
"listen_port": 8080, "listen_port": 8080,
"verbosity": "info", "verbosity": "error",
"enable_openapi": false,
"jwt_secret_key": "somethingrandom", "jwt_secret_key": "somethingrandom",
"CORS_origins": [], "CORS_origins": [],
"username": "freqtrader", "username": "freqtrader",
"password": "SuperSecurePassword" "password": "SuperSecurePassword"
}, },
"bot_name": "freqtrade",
"db_url": "sqlite:///tradesv3.sqlite", "db_url": "sqlite:///tradesv3.sqlite",
"initial_state": "running", "initial_state": "running",
"forcebuy_enable": false, "forcebuy_enable": false,

View File

@ -89,12 +89,13 @@
"enabled": false, "enabled": false,
"listen_ip_address": "127.0.0.1", "listen_ip_address": "127.0.0.1",
"listen_port": 8080, "listen_port": 8080,
"verbosity": "info", "verbosity": "error",
"jwt_secret_key": "somethingrandom", "jwt_secret_key": "somethingrandom",
"CORS_origins": [], "CORS_origins": [],
"username": "", "username": "freqtrader",
"password": "" "password": "SuperSecurePassword"
}, },
"bot_name": "freqtrade",
"initial_state": "running", "initial_state": "running",
"forcebuy_enable": false, "forcebuy_enable": false,
"internals": { "internals": {

View File

@ -49,8 +49,9 @@ This loop will be repeated again and again until the bot is stopped.
[backtesting](backtesting.md) or [hyperopt](hyperopt.md) do only part of the above logic, since most of the trading operations are fully simulated. [backtesting](backtesting.md) or [hyperopt](hyperopt.md) do only part of the above logic, since most of the trading operations are fully simulated.
* Load historic data for configured pairlist. * Load historic data for configured pairlist.
* Calculate indicators (calls `populate_indicators()`). * Calls `bot_loop_start()` once.
* Calls `populate_buy_trend()` and `populate_sell_trend()` * Calculate indicators (calls `populate_indicators()` once per pair).
* Calculate buy / sell signals (calls `populate_buy_trend()` and `populate_sell_trend()` once per pair)
* Loops per candle simulating entry and exit points. * Loops per candle simulating entry and exit points.
* Generate backtest report output * Generate backtest report output

View File

@ -16,8 +16,7 @@ In some advanced use cases, multiple configuration files can be specified and us
If you used the [Quick start](installation.md/#quick-start) method for installing If you used the [Quick start](installation.md/#quick-start) method for installing
the bot, the installation script should have already created the default configuration file (`config.json`) for you. the bot, the installation script should have already created the default configuration file (`config.json`) for you.
If default configuration file is not created we recommend you to copy and use the `config.json.example` as a template If default configuration file is not created we recommend you to use `freqtrade new-config --config config.json` to generate a basic configuration file.
for your bot configuration.
The Freqtrade configuration file is to be written in the JSON format. The Freqtrade configuration file is to be written in the JSON format.
@ -72,8 +71,10 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `ask_strategy.order_book_min` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer | `ask_strategy.order_book_min` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
| `ask_strategy.order_book_max` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer | `ask_strategy.order_book_max` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
| `ask_strategy.use_sell_signal` | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `true`.* <br> **Datatype:** Boolean | `ask_strategy.use_sell_signal` | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `true`.* <br> **Datatype:** Boolean
| `ask_strategy.sell_profit_only` | Wait until the bot makes a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean | `ask_strategy.sell_profit_only` | Wait until the bot reaches `ask_strategy.sell_profit_offset` before taking a sell decision. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `ask_strategy.sell_profit_offset` | Sell-signal is only active above this value. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0`.* <br> **Datatype:** Float (as ratio)
| `ask_strategy.ignore_roi_if_buy_signal` | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean | `ask_strategy.ignore_roi_if_buy_signal` | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `ask_strategy.ignore_buying_expired_candle_after` | Specifies the number of seconds until a buy signal is no longer used. <br> **Datatype:** Integer
| `order_types` | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Dict | `order_types` | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Dict
| `order_time_in_force` | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict | `order_time_in_force` | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict
| `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). <br> **Datatype:** String | `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). <br> **Datatype:** String
@ -81,7 +82,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `exchange.key` | API key to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String | `exchange.key` | API key to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
| `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String | `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
| `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String | `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
| `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Not used by VolumePairList (see [below](#pairlists-and-pairlist-handlers)). <br> **Datatype:** List | `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Supports regex pairs as `.*/BTC`. Not used by VolumePairList (see [below](#pairlists-and-pairlist-handlers)). <br> **Datatype:** List
| `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#pairlists-and-pairlist-handlers)). <br> **Datatype:** List | `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#pairlists-and-pairlist-handlers)). <br> **Datatype:** List
| `exchange.ccxt_config` | Additional CCXT parameters passed to both ccxt instances (sync and async). This is usually the correct place for ccxt configurations. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict | `exchange.ccxt_config` | Additional CCXT parameters passed to both ccxt instances (sync and async). This is usually the correct place for ccxt configurations. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
| `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict | `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
@ -91,7 +92,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation. | `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation.
| `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now. <br>*Defaults to `true`.* <br> **Datatype:** Boolean | `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
| `pairlists` | Define one or more pairlists to be used. [More information below](#pairlists-and-pairlist-handlers). <br>*Defaults to `StaticPairList`.* <br> **Datatype:** List of Dicts | `pairlists` | Define one or more pairlists to be used. [More information below](#pairlists-and-pairlist-handlers). <br>*Defaults to `StaticPairList`.* <br> **Datatype:** List of Dicts
| `protections` | Define one or more protections to be used. [More information below](#protections). <br> **Datatype:** List of Dicts | `protections` | Define one or more protections to be used. [More information below](#protections). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** List of Dicts
| `telegram.enabled` | Enable the usage of Telegram. <br> **Datatype:** Boolean | `telegram.enabled` | Enable the usage of Telegram. <br> **Datatype:** Boolean
| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String | `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
| `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String | `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
@ -108,6 +109,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `api_server.verbosity` | Logging verbosity. `info` will print all RPC Calls, while "error" will only display errors. <br>**Datatype:** Enum, either `info` or `error`. Defaults to `info`. | `api_server.verbosity` | Logging verbosity. `info` will print all RPC Calls, while "error" will only display errors. <br>**Datatype:** Enum, either `info` or `error`. Defaults to `info`.
| `api_server.username` | Username for API server. See the [API Server documentation](rest-api.md) for more details. <br>**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. <br>**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. <br>**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. <br>**Keep it in secret, do not disclose publicly.**<br> **Datatype:** String
| `bot_name` | Name of the bot. Passed via API to a client - can be shown to distinguish / name bots.<br> *Defaults to `freqtrade`*<br> **Datatype:** 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 | `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
@ -141,9 +143,12 @@ Values set in the configuration file always overwrite values set in the strategy
* `stake_amount` * `stake_amount`
* `unfilledtimeout` * `unfilledtimeout`
* `disable_dataframe_checks` * `disable_dataframe_checks`
* `protections`
* `use_sell_signal` (ask_strategy) * `use_sell_signal` (ask_strategy)
* `sell_profit_only` (ask_strategy) * `sell_profit_only` (ask_strategy)
* `sell_profit_offset` (ask_strategy)
* `ignore_roi_if_buy_signal` (ask_strategy) * `ignore_roi_if_buy_signal` (ask_strategy)
* `ignore_buying_expired_candle_after` (ask_strategy)
### Configuring amount per trade ### Configuring amount per trade
@ -272,6 +277,22 @@ before asking the strategy if we should buy or a sell an asset. After each wait
every opened trade wether or not we should sell, and for all the remaining pairs (either the dynamic list of pairs or every opened trade wether or not we should sell, and for all the remaining pairs (either the dynamic list of pairs or
the static list of pairs) if we should buy. the static list of pairs) if we should buy.
### Ignoring expired candles
When working with larger timeframes (for example 1h or more) and using a low `max_open_trades` value, the last candle can be processed as soon as a trade slot becomes available. When processing the last candle, this can lead to a situation where it may not be desirable to use the buy signal on that candle. For example, when using a condition in your strategy where you use a cross-over, that point may have passed too long ago for you to start a trade on it.
In these situations, you can enable the functionality to ignore candles that are beyond a specified period by setting `ask_strategy.ignore_buying_expired_candle_after` to a positive number, indicating the number of seconds after which the buy signal becomes expired.
For example, if your strategy is using a 1h timeframe, and you only want to buy within the first 5 minutes when a new candle comes in, you can add the following configuration to your strategy:
``` json
"ask_strategy":{
"ignore_buying_expired_candle_after": 300,
"price_side": "bid",
// ...
},
```
### Understand order_types ### Understand order_types
The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`, `emergencysell`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds. The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`, `emergencysell`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds.
@ -671,32 +692,6 @@ export HTTPS_PROXY="http://addr:port"
freqtrade freqtrade
``` ```
## Embedding Strategies
Freqtrade provides you with with an easy way to embed the strategy into your configuration file.
This is done by utilizing BASE64 encoding and providing this string at the strategy configuration field,
in your chosen config file.
### Encoding a string as BASE64
This is a quick example, how to generate the BASE64 string in python
```python
from base64 import urlsafe_b64encode
with open(file, 'r') as f:
content = f.read()
content = urlsafe_b64encode(content.encode('utf-8'))
```
The variable 'content', will contain the strategy file in a BASE64 encoded form. Which can now be set in your configurations file as following
```json
"strategy": "NameOfStrategy:BASE64String"
```
Please ensure that 'NameOfStrategy' is identical to the strategy name!
## Next step ## Next step
Now you have configured your config.json, the next step is to [start your bot](bot-usage.md). Now you have configured your config.json, the next step is to [start your bot](bot-usage.md).

View File

@ -308,10 +308,13 @@ Since this data is large by default, the files use gzip by default. They are sto
To use this mode, simply add `--dl-trades` to your call. This will swap the download method to download trades, and resamples the data locally. To use this mode, simply add `--dl-trades` to your call. This will swap the download method to download trades, and resamples the data locally.
!!! Warning "do not use"
You should not use this unless you're a kraken user. Most other exchanges provide OHLCV data with sufficient history.
Example call: Example call:
```bash ```bash
freqtrade download-data --exchange binance --pairs XRP/ETH ETH/BTC --days 20 --dl-trades freqtrade download-data --exchange kraken --pairs XRP/EUR ETH/EUR --days 20 --dl-trades
``` ```
!!! Note !!! Note

View File

@ -2,7 +2,7 @@
This page is intended for developers of Freqtrade, people who want to contribute to the Freqtrade codebase or documentation, or people who want to understand the source code of the application they're running. This page is intended for developers of Freqtrade, people who want to contribute to the Freqtrade codebase or documentation, or people who want to understand the source code of the application they're running.
All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel on [discord](https://discord.gg/MA9v74M) or [slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA) where you can ask questions. All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel on [discord](https://discord.gg/MA9v74M) or [slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA) where you can ask questions.
## Documentation ## Documentation

View File

@ -1,201 +0,0 @@
## Freqtrade with docker without docker-compose
!!! Warning
The below documentation is provided for completeness and assumes that you are familiar with running docker containers. If you're just starting out with Docker, we recommend to follow the [Quickstart](docker.md) instructions.
### Download the official Freqtrade docker image
Pull the image from docker hub.
Branches / tags available can be checked out on [Dockerhub tags page](https://hub.docker.com/r/freqtradeorg/freqtrade/tags/).
```bash
docker pull freqtradeorg/freqtrade:stable
# Optionally tag the repository so the run-commands remain shorter
docker tag freqtradeorg/freqtrade:stable freqtrade
```
To update the image, simply run the above commands again and restart your running container.
Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image).
!!! Note "Docker image update frequency"
The official docker images with tags `stable`, `develop` and `latest` are automatically rebuild once a week to keep the base image up-to-date.
In addition to that, every merge to `develop` will trigger a rebuild for `develop` and `latest`.
### Prepare the configuration files
Even though you will use docker, you'll still need some files from the github repository.
#### Clone the git repository
Linux/Mac/Windows with WSL
```bash
git clone https://github.com/freqtrade/freqtrade.git
```
Windows with docker
```bash
git clone --config core.autocrlf=input https://github.com/freqtrade/freqtrade.git
```
#### Copy `config.json.example` to `config.json`
```bash
cd freqtrade
cp -n config.json.example config.json
```
> To understand the configuration options, please refer to the [Bot Configuration](configuration.md) page.
#### Create your database file
=== "Dry-Run"
``` bash
touch tradesv3.dryrun.sqlite
```
=== "Production"
``` bash
touch tradesv3.sqlite
```
!!! Warning "Database File Path"
Make sure to use the path to the correct database file when starting the bot in Docker.
### Build your own Docker image
Best start by pulling the official docker image from dockerhub as explained [here](#download-the-official-docker-image) to speed up building.
To add additional libraries to your docker image, best check out [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/docker/Dockerfile.technical) which adds the [technical](https://github.com/freqtrade/technical) module to the image.
```bash
docker build -t freqtrade -f docker/Dockerfile.technical .
```
If you are developing using Docker, use `docker/Dockerfile.develop` to build a dev Docker image, which will also set up develop dependencies:
```bash
docker build -f docker/Dockerfile.develop -t freqtrade-dev .
```
!!! Warning "Include your config file manually"
For security reasons, your configuration file will not be included in the image, you will need to bind mount it. It is also advised to bind mount an SQLite database file (see [5. Run a restartable docker image](#run-a-restartable-docker-image)") to keep it between updates.
#### Verify the Docker image
After the build process you can verify that the image was created with:
```bash
docker images
```
The output should contain the freqtrade image.
### Run the Docker image
You can run a one-off container that is immediately deleted upon exiting with the following command (`config.json` must be in the current working directory):
```bash
docker run --rm -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
```
!!! Warning
In this example, the database will be created inside the docker instance and will be lost when you refresh your image.
#### Adjust timezone
By default, the container will use UTC timezone.
If you would like to change the timezone use the following commands:
=== "Linux"
``` bash
-v /etc/timezone:/etc/timezone:ro
# Complete command:
docker run --rm -v /etc/timezone:/etc/timezone:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
```
=== "MacOS"
```bash
docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
```
!!! Note "MacOS Issues"
The OSX Docker versions after 17.09.1 have a known issue whereby `/etc/localtime` cannot be shared causing Docker to not start.<br>
A work-around for this is to start with the MacOS command above
More information on this docker issue and work-around can be read [here](https://github.com/docker/for-mac/issues/2396).
### Run a restartable docker image
To run a restartable instance in the background (feel free to place your configuration and database files wherever it feels comfortable on your filesystem).
#### 1. Move your config file and database
The following will assume that you place your configuration / database files to `~/.freqtrade`, which is a hidden directory in your home directory. Feel free to use a different directory and replace the directory in the upcomming commands.
```bash
mkdir ~/.freqtrade
mv config.json ~/.freqtrade
mv tradesv3.sqlite ~/.freqtrade
```
#### 2. Run the docker image
```bash
docker run -d \
--name freqtrade \
-v ~/.freqtrade/config.json:/freqtrade/config.json \
-v ~/.freqtrade/user_data/:/freqtrade/user_data \
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
freqtrade trade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy
```
!!! Note
When using docker, it's best to specify `--db-url` explicitly to ensure that the database URL and the mounted database file match.
!!! Note
All available bot command line parameters can be added to the end of the `docker run` command.
!!! Note
You can define a [restart policy](https://docs.docker.com/config/containers/start-containers-automatically/) in docker. It can be useful in some cases to use the `--restart unless-stopped` flag (crash of freqtrade or reboot of your system).
### Monitor your Docker instance
You can use the following commands to monitor and manage your container:
```bash
docker logs freqtrade
docker logs -f freqtrade
docker restart freqtrade
docker stop freqtrade
docker start freqtrade
```
For more information on how to operate Docker, please refer to the [official Docker documentation](https://docs.docker.com/).
!!! Note
You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container.
### Backtest with docker
The following assumes that the download/setup of the docker image have been completed successfully.
Also, backtest-data should be available at `~/.freqtrade/user_data/`.
```bash
docker run -d \
--name freqtrade \
-v /etc/localtime:/etc/localtime:ro \
-v ~/.freqtrade/config.json:/freqtrade/config.json \
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
-v ~/.freqtrade/user_data/:/freqtrade/user_data/ \
freqtrade backtesting --strategy AwsomelyProfitableStrategy
```
Head over to the [Backtesting Documentation](backtesting.md) for more details.
!!! Note
Additional bot command line parameters can be appended after the image name (`freqtrade` in the above example).

View File

@ -8,9 +8,7 @@ Start by downloading and installing Docker CE for your platform:
* [Windows](https://docs.docker.com/docker-for-windows/install/) * [Windows](https://docs.docker.com/docker-for-windows/install/)
* [Linux](https://docs.docker.com/install/) * [Linux](https://docs.docker.com/install/)
Optionally, [`docker-compose`](https://docs.docker.com/compose/install/) should be installed and available to follow the [docker quick start guide](#docker-quick-start). To simplify running freqtrade, please install [`docker-compose`](https://docs.docker.com/compose/install/) should be installed and available to follow the below [docker quick start guide](#docker-quick-start).
Once you have Docker installed, simply prepare the config file (e.g. `config.json`) and run the image for `freqtrade` as explained below.
## Freqtrade with docker-compose ## Freqtrade with docker-compose
@ -71,7 +69,7 @@ The last 2 steps in the snippet create the directory with `user_data`, as well a
!!! Question "How to edit the bot configuration?" !!! Question "How to edit the bot configuration?"
You can edit the configuration at any time, which is available as `user_data/config.json` (within the directory `ft_userdata`) when using the above configuration. You can edit the configuration at any time, which is available as `user_data/config.json` (within the directory `ft_userdata`) when using the above configuration.
You can also change the both Strategy and commands by editing the `docker-compose.yml` file. You can also change the both Strategy and commands by editing the command section of your `docker-compose.yml` file.
#### Adding a custom strategy #### Adding a custom strategy
@ -83,7 +81,8 @@ The `SampleStrategy` is run by default.
!!! Warning "`SampleStrategy` is just a demo!" !!! Warning "`SampleStrategy` is just a demo!"
The `SampleStrategy` is there for your reference and give you ideas for your own strategy. The `SampleStrategy` is there for your reference and give you ideas for your own strategy.
Please always backtest the strategy and use dry-run for some time before risking real money! Please always backtest your strategy and use dry-run for some time before risking real money!
You will find more information about Strategy development in the [Strategy documentation](strategy-customization.md).
Once this is done, you're ready to launch the bot in trading mode (Dry-run or Live-trading, depending on your answer to the corresponding question you made above). Once this is done, you're ready to launch the bot in trading mode (Dry-run or Live-trading, depending on your answer to the corresponding question you made above).
@ -91,18 +90,23 @@ Once this is done, you're ready to launch the bot in trading mode (Dry-run or Li
docker-compose up -d docker-compose up -d
``` ```
#### Monitoring the bot
You can check for running instances with `docker-compose ps`.
This should list the service `freqtrade` as `running`. If that's not the case, best check the logs (see next point).
#### Docker-compose logs #### Docker-compose logs
Logs will be located at: `user_data/logs/freqtrade.log`. Logs will be written to: `user_data/logs/freqtrade.log`.
You can check the latest log with the command `docker-compose logs -f`. You can also check the latest log with the command `docker-compose logs -f`.
#### Database #### Database
The database will be at: `user_data/tradesv3.sqlite` The database will be located at: `user_data/tradesv3.sqlite`
#### Updating freqtrade with docker-compose #### Updating freqtrade with docker-compose
To update freqtrade when using `docker-compose` is as simple as running the following 2 commands: Updating freqtrade when using `docker-compose` is as simple as running the following 2 commands:
``` bash ``` bash
# Download the latest image # Download the latest image
@ -120,10 +124,10 @@ This will first pull the latest image, and will then restart the container with
Advanced users may edit the docker-compose file further to include all possible options or arguments. Advanced users may edit the docker-compose file further to include all possible options or arguments.
All possible freqtrade arguments will be available by running `docker-compose run --rm freqtrade <command> <optional arguments>`. All freqtrade arguments will be available by running `docker-compose run --rm freqtrade <command> <optional arguments>`.
!!! Note "`docker-compose run --rm`" !!! Note "`docker-compose run --rm`"
Including `--rm` will clean up the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command). Including `--rm` will remove the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command).
#### Example: Download data with docker-compose #### Example: Download data with docker-compose
@ -172,19 +176,19 @@ docker-compose run --rm freqtrade plot-dataframe --strategy AwesomeStrategy -p B
The output will be stored in the `user_data/plot` directory, and can be opened with any modern browser. The output will be stored in the `user_data/plot` directory, and can be opened with any modern browser.
## Data analayis using docker compose ## Data analysis using docker compose
Freqtrade provides a docker-compose file which starts up a jupyter lab server. Freqtrade provides a docker-compose file which starts up a jupyter lab server.
You can run this server using the following command: You can run this server using the following command:
``` bash ``` bash
docker-compose --rm -f docker/docker-compose-jupyter.yml up docker-compose -f docker/docker-compose-jupyter.yml up
``` ```
This will create a dockercontainer running jupyter lab, which will be accessible using `https://127.0.0.1:8888/lab`. This will create a docker-container running jupyter lab, which will be accessible using `https://127.0.0.1:8888/lab`.
Please use the link that's printed in the console after startup for simplified login. Please use the link that's printed in the console after startup for simplified login.
Since part of this image is built on your machine, it is recommended to rebuild the image from time to time to keep freqtrade (and dependencies) uptodate. Since part of this image is built on your machine, it is recommended to rebuild the image from time to time to keep freqtrade (and dependencies) up-to-date.
``` bash ``` bash
docker-compose -f docker/docker-compose-jupyter.yml build --no-cache docker-compose -f docker/docker-compose-jupyter.yml build --no-cache

View File

@ -1,6 +1,6 @@
# Edge positioning # Edge positioning
The `Edge Positioning` module uses probability to calculate your win rate and risk reward ration. It will use these statistics to control your strategy trade entry points, position side and, stoploss. The `Edge Positioning` module uses probability to calculate your win rate and risk reward ratio. It will use these statistics to control your strategy trade entry points, position size and, stoploss.
!!! Warning !!! Warning
`Edge positioning` is not compatible with dynamic (volume-based) whitelist. `Edge positioning` is not compatible with dynamic (volume-based) whitelist.
@ -55,7 +55,7 @@ Similarly, we can discover the set of losing trades $T_{lose}$ as follows:
$$ T_{lose} = \{o \in O | o \leq 0\} $$ $$ T_{lose} = \{o \in O | o \leq 0\} $$
!!! Example !!! Example
In a section where a strategy made three transactions $O = \{3.5, -1, 15, 0\}$:<br> In a section where a strategy made four transactions $O = \{3.5, -1, 15, 0\}$:<br>
$T_{win} = \{3.5, 15\}$<br> $T_{win} = \{3.5, 15\}$<br>
$T_{lose} = \{-1, 0\}$<br> $T_{lose} = \{-1, 0\}$<br>
@ -206,7 +206,7 @@ Let's say the stake currency is **ETH** and there is $10$ **ETH** on the wallet.
- The strategy detects a sell signal in the **XLM/ETH** market. The bot exits **Trade 1** for a profit of $1$ **ETH**. The total capital in the wallet becomes $11$ **ETH** and the available capital for trading becomes $5.5$ **ETH**. - The strategy detects a sell signal in the **XLM/ETH** market. The bot exits **Trade 1** for a profit of $1$ **ETH**. The total capital in the wallet becomes $11$ **ETH** and the available capital for trading becomes $5.5$ **ETH**.
- **Trade 4** The strategy detects a new buy signal int the **XLM/ETH** market. `Edge Positioning` calculates the stoploss of $2%$, and the position size of $0.055 / 0.02 = 2.75$ **ETH**. - **Trade 4** The strategy detects a new buy signal int the **XLM/ETH** market. `Edge Positioning` calculates the stoploss of $2\%$, and the position size of $0.055 / 0.02 = 2.75$ **ETH**.
## Configurations ## Configurations

View File

@ -143,7 +143,7 @@ freqtrade hyperopt --hyperopt SampleHyperopt --hyperopt-loss SharpeHyperOptLossD
### Why does it take a long time to run hyperopt? ### Why does it take a long time to run hyperopt?
* Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA) - or the Freqtrade [discord community](https://discord.gg/X89cVG). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you. * Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/zt-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA) - or the Freqtrade [discord community](https://discord.gg/X89cVG). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you.
* If you wonder why it can take from 20 minutes to days to do 1000 epochs here are some answers: * If you wonder why it can take from 20 minutes to days to do 1000 epochs here are some answers:

View File

@ -10,6 +10,14 @@ If multiple Pairlist Handlers are used, they are chained and a combination of al
Inactive markets are always removed from the resulting pairlist. Explicitly blacklisted pairs (those in the `pair_blacklist` configuration setting) are also always removed from the resulting pairlist. Inactive markets are always removed from the resulting pairlist. Explicitly blacklisted pairs (those in the `pair_blacklist` configuration setting) are also always removed from the resulting pairlist.
### Pair blacklist
The pair blacklist (configured via `exchange.pair_blacklist` in the configuration) disallows certain pairs from trading.
This can be as simple as excluding `DOGE/BTC` - which will remove exactly this pair.
The pair-blacklist does also support wildcards (in regex-style) - so `BNB/.*` will exclude ALL pairs that start with BNB.
You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged tokens (check Pair naming conventions for your exchange!)
### Available Pairlist Handlers ### Available Pairlist Handlers
* [`StaticPairList`](#static-pair-list) (default, if not configured differently) * [`StaticPairList`](#static-pair-list) (default, if not configured differently)
@ -27,7 +35,7 @@ Inactive markets are always removed from the resulting pairlist. Explicitly blac
#### Static Pair List #### Static Pair List
By default, the `StaticPairList` method is used, which uses a statically defined pair whitelist from the configuration. By default, the `StaticPairList` method is used, which uses a statically defined pair whitelist from the configuration. The pairlist also supports wildcards (in regex-style) - so `.*/BTC` will include all pairs with BTC as a stake.
It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`. It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`.

View File

@ -8,6 +8,7 @@ All protection end times are rounded up to the next candle to avoid sudden, unex
!!! Note !!! Note
Not all Protections will work for all strategies, and parameters will need to be tuned for your strategy to improve performance. Not all Protections will work for all strategies, and parameters will need to be tuned for your strategy to improve performance.
To align your protection with your strategy, you can define protections in the strategy.
!!! Tip !!! Tip
Each Protection can be configured multiple times with different parameters, to allow different levels of protection (short-term / long-term). Each Protection can be configured multiple times with different parameters, to allow different levels of protection (short-term / long-term).
@ -39,7 +40,7 @@ All protection end times are rounded up to the next candle to avoid sudden, unex
#### Stoploss Guard #### Stoploss Guard
`StoplossGuard` selects all trades within `lookback_period`, and determines if the amount of trades that resulted in stoploss are above `trade_limit` - in which case trading will stop for `stop_duration`. `StoplossGuard` selects all trades within `lookback_period` in minutes (or in candles when using `lookback_period_candles`), and determines if the amount of trades that resulted in stoploss are above `trade_limit` - in which case trading will stop for `stop_duration` in minutes (or in candles when using `stop_duration_candles`).
This applies across all pairs, unless `only_per_pair` is set to true, which will then only look at one pair at a time. This applies across all pairs, unless `only_per_pair` is set to true, which will then only look at one pair at a time.
The below example stops trading for all pairs for 4 candles after the last trade if the bot hit stoploss 4 times within the last 24 candles. The below example stops trading for all pairs for 4 candles after the last trade if the bot hit stoploss 4 times within the last 24 candles.
@ -57,14 +58,14 @@ The below example stops trading for all pairs for 4 candles after the last trade
``` ```
!!! Note !!! Note
`StoplossGuard` considers all trades with the results `"stop_loss"` and `"trailing_stop_loss"` if the resulting profit was negative. `StoplossGuard` considers all trades with the results `"stop_loss"`, `"stoploss_on_exchange"` and `"trailing_stop_loss"` if the resulting profit was negative.
`trade_limit` and `lookback_period` will need to be tuned for your strategy. `trade_limit` and `lookback_period` will need to be tuned for your strategy.
#### MaxDrawdown #### MaxDrawdown
`MaxDrawdown` uses all trades within `lookback_period` (in minutes) to determine the maximum drawdown. If the drawdown is below `max_allowed_drawdown`, trading will stop for `stop_duration` (in minutes) after the last trade - assuming that the bot needs some time to let markets recover. `MaxDrawdown` uses all trades within `lookback_period` in minutes (or in candles when using `lookback_period_candles`) to determine the maximum drawdown. If the drawdown is below `max_allowed_drawdown`, trading will stop for `stop_duration` in minutes (or in candles when using `stop_duration_candles`) after the last trade - assuming that the bot needs some time to let markets recover.
The below sample stops trading for 12 candles if max-drawdown is > 20% considering all trades within the last 48 candles. The below sample stops trading for 12 candles if max-drawdown is > 20% considering all pairs - with a minimum of `trade_limit` trades - within the last 48 candles. If desired, `lookback_period` and/or `stop_duration` can be used.
```json ```json
"protections": [ "protections": [
@ -76,13 +77,12 @@ The below sample stops trading for 12 candles if max-drawdown is > 20% consideri
"max_allowed_drawdown": 0.2 "max_allowed_drawdown": 0.2
}, },
], ],
``` ```
#### Low Profit Pairs #### Low Profit Pairs
`LowProfitPairs` uses all trades for a pair within `lookback_period` (in minutes) to determine the overall profit ratio. `LowProfitPairs` uses all trades for a pair within `lookback_period` in minutes (or in candles when using `lookback_period_candles`) to determine the overall profit ratio.
If that ratio is below `required_profit`, that pair will be locked for `stop_duration` (in minutes). If that ratio is below `required_profit`, that pair will be locked for `stop_duration` in minutes (or in candles when using `stop_duration_candles`).
The below example will stop trading a pair for 60 minutes if the pair does not have a required profit of 2% (and a minimum of 2 trades) within the last 6 candles. The below example will stop trading a pair for 60 minutes if the pair does not have a required profit of 2% (and a minimum of 2 trades) within the last 6 candles.
@ -100,7 +100,7 @@ The below example will stop trading a pair for 60 minutes if the pair does not h
#### Cooldown Period #### Cooldown Period
`CooldownPeriod` locks a pair for `stop_duration` (in minutes) after selling, avoiding a re-entry for this pair for `stop_duration` minutes. `CooldownPeriod` locks a pair for `stop_duration` in minutes (or in candles when using `stop_duration_candles`) after selling, avoiding a re-entry for this pair for `stop_duration` minutes.
The below example will stop trading a pair for 2 candles after closing a trade, allowing this pair to "cool down". The below example will stop trading a pair for 2 candles after closing a trade, allowing this pair to "cool down".
@ -167,3 +167,47 @@ The below example assumes a timeframe of 1 hour:
} }
], ],
``` ```
You can use the same in your strategy, the syntax is only slightly different:
``` python
from freqtrade.strategy import IStrategy
class AwesomeStrategy(IStrategy)
timeframe = '1h'
protections = [
{
"method": "CooldownPeriod",
"stop_duration_candles": 5
},
{
"method": "MaxDrawdown",
"lookback_period_candles": 48,
"trade_limit": 20,
"stop_duration_candles": 4,
"max_allowed_drawdown": 0.2
},
{
"method": "StoplossGuard",
"lookback_period_candles": 24,
"trade_limit": 4,
"stop_duration_candles": 2,
"only_per_pair": False
},
{
"method": "LowProfitPairs",
"lookback_period_candles": 6,
"trade_limit": 2,
"stop_duration_candles": 60,
"required_profit": 0.02
},
{
"method": "LowProfitPairs",
"lookback_period_candles": 24,
"trade_limit": 4,
"stop_duration_candles": 2,
"required_profit": 0.01
}
]
# ...
```

View File

@ -65,7 +65,7 @@ For any questions not covered by the documentation or for further information ab
Please check out our [discord server](https://discord.gg/MA9v74M). Please check out our [discord server](https://discord.gg/MA9v74M).
You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA). You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA).
## Ready to try? ## Ready to try?

View File

@ -2,7 +2,7 @@
This page explains how to prepare your environment for running the bot. This page explains how to prepare your environment for running the bot.
Please consider using the prebuilt [docker images](docker.md) to get started quickly while trying out freqtrade evaluating how it operates. Please consider using the prebuilt [docker images](docker_quickstart.md) to get started quickly while evaluating how freqtrade works.
## Prerequisite ## Prerequisite
@ -35,6 +35,7 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito
!!! Note !!! Note
Python3.7 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository. Python3.7 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository.
Also, python headers (`python<yourversion>-dev` / `python<yourversion>-devel`) must be available for the installation to complete successfully.
This can be achieved with the following commands: This can be achieved with the following commands:
@ -209,7 +210,7 @@ If this is the first time you run the bot, ensure you are running it in Dry-run
freqtrade trade -c config.json freqtrade trade -c config.json
``` ```
*Note*: If you run the bot on a server, you should consider using [Docker](docker.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout. *Note*: If you run the bot on a server, you should consider using [Docker compose](docker_quickstart.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout.
#### 7. (Optional) Post-installation Tasks #### 7. (Optional) Post-installation Tasks
@ -244,6 +245,19 @@ open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10
If this file is inexistent, then you're probably on a different version of MacOS, so you may need to consult the internet for specific resolution details. If this file is inexistent, then you're probably on a different version of MacOS, so you may need to consult the internet for specific resolution details.
### MacOS installation error with python 3.9
When using python 3.9 on macOS, it's currently necessary to install some os-level modules to allow dependencies to compile.
The errors you'll see happen during installation and are related to the installation of `tables` or `blosc`.
You can install the necessary libraries with the following command:
``` bash
brew install hdf5 c-blosc
```
After this, please run the installation (script) again.
----- -----
Now you have an environment ready, the next step is Now you have an environment ready, the next step is

View File

@ -208,6 +208,7 @@ Sample configuration with inline comments explaining the process:
} }
``` ```
!!! Note !!! Note
The above configuration assumes that `ema10`, `ema50`, `senkou_a`, `senkou_b`, The above configuration assumes that `ema10`, `ema50`, `senkou_a`, `senkou_b`,
`macd`, `macdsignal`, `macdhist` and `rsi` are columns in the DataFrame created by the strategy. `macd`, `macdsignal`, `macdhist` and `rsi` are columns in the DataFrame created by the strategy.

View File

@ -1,3 +1,3 @@
mkdocs-material==6.2.3 mkdocs-material==6.2.5
mdx_truly_sane_lists==1.2 mdx_truly_sane_lists==1.2
pymdown-extensions==8.1 pymdown-extensions==8.1

View File

@ -11,7 +11,8 @@ Sample configuration:
"enabled": true, "enabled": true,
"listen_ip_address": "127.0.0.1", "listen_ip_address": "127.0.0.1",
"listen_port": 8080, "listen_port": 8080,
"verbosity": "info", "verbosity": "error",
"enable_openapi": false,
"jwt_secret_key": "somethingrandom", "jwt_secret_key": "somethingrandom",
"CORS_origins": [], "CORS_origins": [],
"username": "Freqtrader", "username": "Freqtrader",
@ -263,6 +264,11 @@ whitelist
``` ```
## OpenAPI interface
To enable the builtin openAPI interface (Swagger UI), specify `"enable_openapi": true` in the api_server configuration.
This will enable the Swagger UI at the `/docs` endpoint. By default, that's running at http://localhost:8080/docs/ - but it'll depend on your settings.
## Advanced API usage using JWT tokens ## Advanced API usage using JWT tokens
!!! Note !!! Note

View File

@ -78,6 +78,7 @@ At this stage the bot contains the following stoploss support modes:
2. Trailing stop loss. 2. Trailing stop loss.
3. Trailing stop loss, custom positive loss. 3. Trailing stop loss, custom positive loss.
4. Trailing stop loss only once the trade has reached a certain offset. 4. Trailing stop loss only once the trade has reached a certain offset.
5. [Custom stoploss function](strategy-advanced.md#custom-stoploss)
### Static Stop Loss ### Static Stop Loss

View File

@ -8,9 +8,183 @@ If you're just getting started, please be familiar with the methods described in
!!! Note !!! Note
All callback methods described below should only be implemented in a strategy if they are actually used. All callback methods described below should only be implemented in a strategy if they are actually used.
!!! Tip
You can get a strategy template containing all below methods by running `freqtrade new-strategy --strategy MyAwesomeStrategy --template advanced`
## Custom stoploss
A stoploss can only ever move upwards - so if you set it to an absolute profit of 2%, you can never move it below this price.
Also, the traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss.
The usage of the custom stoploss method must be enabled by setting `use_custom_stoploss=True` on the strategy object.
The method must return a stoploss value (float / number) with a relative ratio below the current price.
E.g. `current_profit = 0.05` (5% profit) - stoploss returns `0.02` - then you "locked in" a profit of 3% (`0.05 - 0.02 = 0.03`).
To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum reached price) you would use the following very simple method:
``` python
# additional imports required
from datetime import datetime
from freqtrade.persistence import Trade
class AwesomeStrategy(IStrategy):
# ... populate_* methods
use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
current_profit: float, **kwargs) -> float:
"""
Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
e.g. returning -0.05 would create a stoploss 5% below current_rate.
The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, returns the initial stoploss value
Only called when use_custom_stoploss is set to True.
:param pair: Pair that's currently analyzed
:param trade: trade object.
:param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
:param current_profit: Current profit (as ratio), calculated based on current_rate.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New stoploss value, relative to the currentrate
"""
return -0.04
```
Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchange-freqtrade)).
!!! Note "Use of dates"
All time-based calculations should be done based on `current_time` - using `datetime.now()` or `datetime.utcnow()` is discouraged, as this will break backtesting support.
!!! Tip "Trailing stoploss"
It's recommended to disable `trailing_stop` when using custom stoploss values. Both can work in tandem, but you might encounter the trailing stop to move the price higher while your custom function would not want this, causing conflicting behavior.
### Custom stoploss examples
The next section will show some examples on what's possible with the custom stoploss function.
Of course, many more things are possible, and all examples can be combined at will.
#### Time based trailing stop
Use the initial stoploss for the first 60 minutes, after this change to 10% trailing stoploss, and after 2 hours (120 minutes) we use a 5% trailing stoploss.
``` python
from datetime import datetime, timedelta
from freqtrade.persistence import Trade
class AwesomeStrategy(IStrategy):
# ... populate_* methods
use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
current_profit: float, **kwargs) -> float:
# Make sure you have the longest interval first - these conditions are evaluated from top to bottom.
if current_time - timedelta(minutes=120) > trade.open_date:
return -0.05
elif current_time - timedelta(minutes=60) > trade.open_date:
return -0.10
return 1
```
#### Different stoploss per pair
Use a different stoploss depending on the pair.
In this example, we'll trail the highest price with 10% trailing stoploss for `ETH/BTC` and `XRP/BTC`, with 5% trailing stoploss for `LTC/BTC` and with 15% for all other pairs.
``` python
from datetime import datetime
from freqtrade.persistence import Trade
class AwesomeStrategy(IStrategy):
# ... populate_* methods
use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
current_profit: float, **kwargs) -> float:
if pair in ('ETH/BTC', 'XRP/BTC'):
return -0.10
elif pair in ('LTC/BTC'):
return -0.05
return -0.15
```
#### Trailing stoploss with positive offset
Use the initial stoploss until the profit is above 4%, then use a trailing stoploss of 50% of the current profit with a minimum of 2.5% and a maximum of 5%.
Please note that the stoploss can only increase, values lower than the current stoploss are ignored.
``` python
from datetime import datetime, timedelta
from freqtrade.persistence import Trade
class AwesomeStrategy(IStrategy):
# ... populate_* methods
use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
current_profit: float, **kwargs) -> float:
if current_profit < 0.04:
return -1 # return a value bigger than the inital stoploss to keep using the inital stoploss
# After reaching the desired offset, allow the stoploss to trail by half the profit
desired_stoploss = current_profit / 2
# Use a minimum of 2.5% and a maximum of 5%
return max(min(desired_stoploss, 0.05), 0.025)
```
#### Absolute stoploss
The below example sets absolute profit levels based on the current profit.
* Use the regular stoploss until 20% profit is reached
* Once profit is > 40%, stoploss will be at 25%, locking in at least 25% of the profit.
* Once profit is > 25% - stoploss will be 15%.
* Once profit is > 20% - stoploss will be set to 7%.
``` python
from datetime import datetime
from freqtrade.persistence import Trade
class AwesomeStrategy(IStrategy):
# ... populate_* methods
use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
current_profit: float, **kwargs) -> float:
# Calculate as `-desired_stop_from_open + current_profit` to get the distance between current_profit and initial price
if current_profit > 0.40:
return (-0.25 + current_profit)
if current_profit > 0.25:
return (-0.15 + current_profit)
if current_profit > 0.20:
return (-0.7 + current_profit)
return 1
```
---
## Custom order timeout rules ## Custom order timeout rules
Simple, timebased order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section. Simple, time-based order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section.
However, freqtrade also offers a custom callback for both order types, which allows you to decide based on custom criteria if a order did time out or not. However, freqtrade also offers a custom callback for both order types, which allows you to decide based on custom criteria if a order did time out or not.
@ -28,7 +202,7 @@ The function must return either `True` (cancel order) or `False` (keep order ali
from datetime import datetime, timedelta from datetime import datetime, timedelta
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
class Awesomestrategy(IStrategy): class AwesomeStrategy(IStrategy):
# ... populate_* methods # ... populate_* methods
@ -67,7 +241,7 @@ class Awesomestrategy(IStrategy):
from datetime import datetime from datetime import datetime
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
class Awesomestrategy(IStrategy): class AwesomeStrategy(IStrategy):
# ... populate_* methods # ... populate_* methods
@ -95,6 +269,8 @@ class Awesomestrategy(IStrategy):
return False return False
``` ```
---
## Bot loop start callback ## Bot loop start callback
A simple callback which is called once at the start of every bot throttling iteration. A simple callback which is called once at the start of every bot throttling iteration.
@ -103,7 +279,7 @@ This can be used to perform calculations which are pair independent (apply to al
``` python ``` python
import requests import requests
class Awesomestrategy(IStrategy): class AwesomeStrategy(IStrategy):
# ... populate_* methods # ... populate_* methods
@ -128,7 +304,7 @@ class Awesomestrategy(IStrategy):
`confirm_trade_entry()` can be used to abort a trade entry at the latest second (maybe because the price is not what we expect). `confirm_trade_entry()` can be used to abort a trade entry at the latest second (maybe because the price is not what we expect).
``` python ``` python
class Awesomestrategy(IStrategy): class AwesomeStrategy(IStrategy):
# ... populate_* methods # ... populate_* methods
@ -164,7 +340,7 @@ class Awesomestrategy(IStrategy):
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
class Awesomestrategy(IStrategy): class AwesomeStrategy(IStrategy):
# ... populate_* methods # ... populate_* methods
@ -200,6 +376,8 @@ class Awesomestrategy(IStrategy):
``` ```
---
## Derived strategies ## Derived strategies
The strategies can be derived from other strategies. This avoids duplication of your custom strategy code. You can use this technique to override small parts of your main strategy, leaving the rest untouched: The strategies can be derived from other strategies. This avoids duplication of your custom strategy code. You can use this technique to override small parts of your main strategy, leaving the rest untouched:
@ -219,4 +397,30 @@ class MyAwesomeStrategy2(MyAwesomeStrategy):
trailing_stop = True trailing_stop = True
``` ```
Both attributes and methods may be overriden, altering behavior of the original strategy in a way you need. Both attributes and methods may be overridden, altering behavior of the original strategy in a way you need.
## Embedding Strategies
Freqtrade provides you with with an easy way to embed the strategy into your configuration file.
This is done by utilizing BASE64 encoding and providing this string at the strategy configuration field,
in your chosen config file.
### Encoding a string as BASE64
This is a quick example, how to generate the BASE64 string in python
```python
from base64 import urlsafe_b64encode
with open(file, 'r') as f:
content = f.read()
content = urlsafe_b64encode(content.encode('utf-8'))
```
The variable 'content', will contain the strategy file in a BASE64 encoded form. Which can now be set in your configurations file as following
```json
"strategy": "NameOfStrategy:BASE64String"
```
Please ensure that 'NameOfStrategy' is identical to the strategy name!

View File

@ -309,7 +309,7 @@ Storing information can be accomplished by creating a new dictionary within the
The name of the variable can be chosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables. The name of the variable can be chosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables.
```python ```python
class Awesomestrategy(IStrategy): class AwesomeStrategy(IStrategy):
# Create custom dictionary # Create custom dictionary
cust_info = {} cust_info = {}
@ -653,7 +653,7 @@ The following example queries for the current pair and trades from today, howeve
if self.config['runmode'].value in ('live', 'dry_run'): if self.config['runmode'].value in ('live', 'dry_run'):
trades = Trade.get_trades([Trade.pair == metadata['pair'], trades = Trade.get_trades([Trade.pair == metadata['pair'],
Trade.open_date > datetime.utcnow() - timedelta(days=1), Trade.open_date > datetime.utcnow() - timedelta(days=1),
Trade.is_open == False, Trade.is_open.is_(False),
]).order_by(Trade.close_date).all() ]).order_by(Trade.close_date).all()
# Summarize profit for this pair. # Summarize profit for this pair.
curdayprofit = sum(trade.close_profit for trade in trades) curdayprofit = sum(trade.close_profit for trade in trades)
@ -719,7 +719,7 @@ if self.config['runmode'].value in ('live', 'dry_run'):
# fetch closed trades for the last 2 days # fetch closed trades for the last 2 days
trades = Trade.get_trades([Trade.pair == metadata['pair'], trades = Trade.get_trades([Trade.pair == metadata['pair'],
Trade.open_date > datetime.utcnow() - timedelta(days=2), Trade.open_date > datetime.utcnow() - timedelta(days=2),
Trade.is_open == False, Trade.is_open.is_(False),
]).all() ]).all()
# Analyze the conditions you'd like to lock the pair .... will probably be different for every strategy # 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) sumprofit = sum(trade.close_profit for trade in trades)

View File

@ -137,6 +137,7 @@ official commands. You can ask at any moment for help with `/help`.
| `/show_config` | Shows part of the current configuration with relevant settings to operation | `/show_config` | Shows part of the current configuration with relevant settings to operation
| `/logs [limit]` | Show last log messages. | `/logs [limit]` | Show last log messages.
| `/status` | Lists all open trades | `/status` | Lists all open trades
| `/status <trade_id>` | Lists one or more specific trade. Separate multiple <trade_id> with a blank space.
| `/status table` | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**) | `/status table` | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**)
| `/trades [limit]` | List all recently closed trades in a table format. | `/trades [limit]` | List all recently closed trades in a table format.
| `/delete <trade_id>` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange. | `/delete <trade_id>` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange.

View File

@ -1,4 +1,4 @@
We **strongly** recommend that Windows users use [Docker](docker.md) as this will work much easier and smoother (also more secure). We **strongly** recommend that Windows users use [Docker](docker_quickstart.md) as this will work much easier and smoother (also more secure).
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work. If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work.
Otherwise, try the instructions below. Otherwise, try the instructions below.
@ -52,6 +52,6 @@ error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++
Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use. Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use.
The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building C code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker](docker.md) first. The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building C code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker compose](docker_quickstart.md) first.
--- ---

View File

@ -1,5 +1,5 @@
""" Freqtrade bot """ """ Freqtrade bot """
__version__ = '2020.12' __version__ = '2021.1'
if __version__ == 'develop': if __version__ == 'develop':

View File

@ -10,6 +10,7 @@ from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_oh
refresh_backtest_trades_data) refresh_backtest_trades_data)
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.resolvers import ExchangeResolver from freqtrade.resolvers import ExchangeResolver
from freqtrade.state import RunMode from freqtrade.state import RunMode
@ -42,15 +43,17 @@ def start_download_data(args: Dict[str, Any]) -> None:
"Downloading data requires a list of pairs. " "Downloading data requires a list of pairs. "
"Please check the documentation on how to configure this.") "Please check the documentation on how to configure this.")
logger.info(f"About to download pairs: {config['pairs']}, "
f"intervals: {config['timeframes']} to {config['datadir']}")
pairs_not_available: List[str] = [] pairs_not_available: List[str] = []
# Init exchange # Init exchange
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False) exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
# Manual validations of relevant settings # Manual validations of relevant settings
exchange.validate_pairs(config['pairs']) exchange.validate_pairs(config['pairs'])
expanded_pairs = expand_pairlist(config['pairs'], list(exchange.markets))
logger.info(f"About to download pairs: {expanded_pairs}, "
f"intervals: {config['timeframes']} to {config['datadir']}")
for timeframe in config['timeframes']: for timeframe in config['timeframes']:
exchange.validate_timeframes(timeframe) exchange.validate_timeframes(timeframe)
@ -58,20 +61,20 @@ def start_download_data(args: Dict[str, Any]) -> None:
if config.get('download_trades'): if config.get('download_trades'):
pairs_not_available = refresh_backtest_trades_data( pairs_not_available = refresh_backtest_trades_data(
exchange, pairs=config['pairs'], datadir=config['datadir'], exchange, pairs=expanded_pairs, datadir=config['datadir'],
timerange=timerange, erase=bool(config.get('erase')), timerange=timerange, erase=bool(config.get('erase')),
data_format=config['dataformat_trades']) data_format=config['dataformat_trades'])
# Convert downloaded trade data to different timeframes # Convert downloaded trade data to different timeframes
convert_trades_to_ohlcv( convert_trades_to_ohlcv(
pairs=config['pairs'], timeframes=config['timeframes'], pairs=expanded_pairs, timeframes=config['timeframes'],
datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')), datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')),
data_format_ohlcv=config['dataformat_ohlcv'], data_format_ohlcv=config['dataformat_ohlcv'],
data_format_trades=config['dataformat_trades'], data_format_trades=config['dataformat_trades'],
) )
else: else:
pairs_not_available = refresh_backtest_ohlcv_data( pairs_not_available = refresh_backtest_ohlcv_data(
exchange, pairs=config['pairs'], timeframes=config['timeframes'], exchange, pairs=expanded_pairs, timeframes=config['timeframes'],
datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')), datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')),
data_format=config['dataformat_ohlcv']) data_format=config['dataformat_ohlcv'])

View File

@ -54,7 +54,7 @@ def validate_config_schema(conf: Dict[str, Any]) -> Dict[str, Any]:
return conf return conf
except ValidationError as e: except ValidationError as e:
logger.critical( logger.critical(
f"Invalid configuration. See config.json.example. Reason: {e}" f"Invalid configuration. Reason: {e}"
) )
raise ValidationError( raise ValidationError(
best_match(Draft4Validator(conf_schema).iter_errors(conf)).message best_match(Draft4Validator(conf_schema).iter_errors(conf)).message

View File

@ -116,6 +116,7 @@ CONF_SCHEMA = {
'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1}, 'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1},
'trailing_stop_positive_offset': {'type': 'number', 'minimum': 0, 'maximum': 1}, 'trailing_stop_positive_offset': {'type': 'number', 'minimum': 0, 'maximum': 1},
'trailing_only_offset_is_reached': {'type': 'boolean'}, 'trailing_only_offset_is_reached': {'type': 'boolean'},
'bot_name': {'type': 'string'},
'unfilledtimeout': { 'unfilledtimeout': {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
@ -154,6 +155,7 @@ CONF_SCHEMA = {
'order_book_max': {'type': 'integer', 'minimum': 1, 'maximum': 50}, 'order_book_max': {'type': 'integer', 'minimum': 1, 'maximum': 50},
'use_sell_signal': {'type': 'boolean'}, 'use_sell_signal': {'type': 'boolean'},
'sell_profit_only': {'type': 'boolean'}, 'sell_profit_only': {'type': 'boolean'},
'sell_profit_offset': {'type': 'number', 'minimum': 0.0},
'ignore_roi_if_buy_signal': {'type': 'boolean'} 'ignore_roi_if_buy_signal': {'type': 'boolean'}
} }
}, },

View File

@ -347,7 +347,7 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
# Resample to timeframe to make sure trades match candles # Resample to timeframe to make sure trades match candles
_trades_sum = trades.resample(f'{timeframe_minutes}min', on='close_date' _trades_sum = trades.resample(f'{timeframe_minutes}min', on='close_date'
)[['profit_percent']].sum() )[['profit_percent']].sum()
df.loc[:, col_name] = _trades_sum.cumsum() df.loc[:, col_name] = _trades_sum['profit_percent'].cumsum()
# Set first value to 0 # Set first value to 0
df.loc[df.iloc[0].name, col_name] = 0 df.loc[df.iloc[0].name, col_name] = 0
# FFill to get continuous # FFill to get continuous

View File

@ -12,6 +12,7 @@ from freqtrade.configuration import TimeRange
from freqtrade.constants import DATETIME_PRINT_FORMAT, UNLIMITED_STAKE_AMOUNT from freqtrade.constants import DATETIME_PRINT_FORMAT, UNLIMITED_STAKE_AMOUNT
from freqtrade.data.history import get_timerange, load_data, refresh_data from freqtrade.data.history import get_timerange, load_data, refresh_data
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.strategy.interface import SellType from freqtrade.strategy.interface import SellType
@ -80,10 +81,12 @@ class Edge:
if config.get('fee'): if config.get('fee'):
self.fee = config['fee'] self.fee = config['fee']
else: else:
self.fee = self.exchange.get_fee(symbol=self.config['exchange']['pair_whitelist'][0]) self.fee = self.exchange.get_fee(symbol=expand_pairlist(
self.config['exchange']['pair_whitelist'], list(self.exchange.markets))[0])
def calculate(self) -> bool: def calculate(self) -> bool:
pairs = self.config['exchange']['pair_whitelist'] pairs = expand_pairlist(self.config['exchange']['pair_whitelist'],
list(self.exchange.markets))
heartbeat = self.edge_config.get('process_throttle_secs') heartbeat = self.edge_config.get('process_throttle_secs')
if (self._last_updated > 0) and ( if (self._last_updated > 0) and (

View File

@ -21,6 +21,7 @@ BAD_EXCHANGES = {
"hitbtc": "This API cannot be used with Freqtrade. " "hitbtc": "This API cannot be used with Freqtrade. "
"Use `hitbtc2` exchange id to access this exchange.", "Use `hitbtc2` exchange id to access this exchange.",
"phemex": "Does not provide history. ", "phemex": "Does not provide history. ",
"poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders.",
**dict.fromkeys([ **dict.fromkeys([
'adara', 'adara',
'anxpro', 'anxpro',

View File

@ -25,6 +25,7 @@ from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFun
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES, retrier, from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES, retrier,
retrier_async) retrier_async)
from freqtrade.misc import deep_merge_dicts, safe_value_fallback2 from freqtrade.misc import deep_merge_dicts, safe_value_fallback2
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
CcxtModuleType = Any CcxtModuleType = Any
@ -208,7 +209,7 @@ class Exchange:
return self._api.precisionMode return self._api.precisionMode
def get_markets(self, base_currencies: List[str] = None, quote_currencies: List[str] = None, def get_markets(self, base_currencies: List[str] = None, quote_currencies: List[str] = None,
pairs_only: bool = False, active_only: bool = False) -> Dict: pairs_only: bool = False, active_only: bool = False) -> Dict[str, Any]:
""" """
Return exchange ccxt markets, filtered out by base currency and quote currency Return exchange ccxt markets, filtered out by base currency and quote currency
if this was requested in parameters. if this was requested in parameters.
@ -335,8 +336,9 @@ class Exchange:
if not self.markets: if not self.markets:
logger.warning('Unable to validate pairs (assuming they are correct).') logger.warning('Unable to validate pairs (assuming they are correct).')
return return
extended_pairs = expand_pairlist(pairs, list(self.markets), keep_invalid=True)
invalid_pairs = [] invalid_pairs = []
for pair in pairs: for pair in extended_pairs:
# Note: ccxt has BaseCurrency/QuoteCurrency format for pairs # Note: ccxt has BaseCurrency/QuoteCurrency format for pairs
# TODO: add a support for having coins in BTC/USDT format # TODO: add a support for having coins in BTC/USDT format
if self.markets and pair not in self.markets: if self.markets and pair not in self.markets:
@ -936,7 +938,7 @@ class Exchange:
while True: while True:
t = await self._async_fetch_trades(pair, since=since) t = await self._async_fetch_trades(pair, since=since)
if len(t): if len(t):
since = t[-1][1] since = t[-1][0]
trades.extend(t) trades.extend(t)
# Reached the end of the defined-download period # Reached the end of the defined-download period
if until and t[-1][0] > until: if until and t[-1][0] > until:

View File

@ -48,7 +48,7 @@ class Kraken(Exchange):
orders = self._api.fetch_open_orders() orders = self._api.fetch_open_orders()
order_list = [(x["symbol"].split("/")[0 if x["side"] == "sell" else 1], order_list = [(x["symbol"].split("/")[0 if x["side"] == "sell" else 1],
x["remaining"], x["remaining"] if x["side"] == "sell" else x["remaining"] * x["price"],
# Don't remove the below comment, this can be important for debuggung # Don't remove the below comment, this can be important for debuggung
# x["side"], x["amount"], # x["side"], x["amount"],
) for x in orders] ) for x in orders]

View File

@ -200,7 +200,7 @@ class FreqtradeBot(LoggingMixin):
Notify the user when the bot is stopped Notify the user when the bot is stopped
and there are still open trades active. and there are still open trades active.
""" """
open_trades = Trade.get_trades([Trade.is_open == 1]).all() open_trades = Trade.get_trades([Trade.is_open.is_(True)]).all()
if len(open_trades) != 0: if len(open_trades) != 0:
msg = { msg = {
@ -246,6 +246,10 @@ class FreqtradeBot(LoggingMixin):
Updates open orders based on order list kept in the database. Updates open orders based on order list kept in the database.
Mainly updates the state of orders - but may also close trades Mainly updates the state of orders - but may also close trades
""" """
if self.config['dry_run']:
# Updating open orders in dry-run does not make sense and will fail.
return
orders = Order.get_open_orders() orders = Order.get_open_orders()
logger.info(f"Updating {len(orders)} open orders.") logger.info(f"Updating {len(orders)} open orders.")
for order in orders: for order in orders:
@ -256,6 +260,7 @@ class FreqtradeBot(LoggingMixin):
self.update_trade_state(order.trade, order.order_id, fo) self.update_trade_state(order.trade, order.order_id, fo)
except ExchangeError as e: except ExchangeError as e:
logger.warning(f"Error updating Order {order.order_id} due to {e}") logger.warning(f"Error updating Order {order.order_id} due to {e}")
def update_closed_trades_without_assigned_fees(self): def update_closed_trades_without_assigned_fees(self):
@ -263,6 +268,10 @@ class FreqtradeBot(LoggingMixin):
Update closed trades without close fees assigned. Update closed trades without close fees assigned.
Only acts when Orders are in the database, otherwise the last orderid is unknown. Only acts when Orders are in the database, otherwise the last orderid is unknown.
""" """
if self.config['dry_run']:
# Updating open orders in dry-run does not make sense and will fail.
return
trades: List[Trade] = Trade.get_sold_trades_without_assigned_fees() trades: List[Trade] = Trade.get_sold_trades_without_assigned_fees()
for trade in trades: for trade in trades:

View File

@ -6,7 +6,7 @@ This module contains the backtesting logic
import logging import logging
from collections import defaultdict from collections import defaultdict
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, NamedTuple, Optional, Tuple from typing import Any, Dict, List, NamedTuple, Optional, Tuple
from pandas import DataFrame from pandas import DataFrame
@ -26,6 +26,7 @@ from freqtrade.plugins.pairlistmanager import PairListManager
from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.plugins.protectionmanager import ProtectionManager
from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -76,6 +77,8 @@ 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.all_results: Dict[str, Dict] = {}
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
dataprovider = DataProvider(self.config, self.exchange) dataprovider = DataProvider(self.config, self.exchange)
@ -150,6 +153,10 @@ class Backtesting:
self.strategy.order_types['stoploss_on_exchange'] = False self.strategy.order_types['stoploss_on_exchange'] = False
def load_bt_data(self) -> Tuple[Dict[str, DataFrame], TimeRange]: def load_bt_data(self) -> Tuple[Dict[str, DataFrame], TimeRange]:
"""
Loads backtest data and returns the data combined with the timerange
as tuple.
"""
timerange = TimeRange.parse_timerange(None if self.config.get( 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')))
@ -180,6 +187,7 @@ class Backtesting:
Backtesting setup method - called once for every call to "backtest()". Backtesting setup method - called once for every call to "backtest()".
""" """
PairLocks.use_db = False PairLocks.use_db = False
PairLocks.timeframe = self.config['timeframe']
Trade.use_db = False Trade.use_db = False
if enable_protections: if enable_protections:
# Reset persisted data - used for protections only # Reset persisted data - used for protections only
@ -423,25 +431,13 @@ class Backtesting:
return DataFrame.from_records(trades, columns=BacktestResult._fields) return DataFrame.from_records(trades, columns=BacktestResult._fields)
def start(self) -> None: def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, Any], timerange: TimeRange):
"""
Run backtesting end-to-end
:return: None
"""
data: Dict[str, Any] = {}
logger.info('Using stake_currency: %s ...', self.config['stake_currency'])
logger.info('Using stake_amount: %s ...', self.config['stake_amount'])
position_stacking = self.config.get('position_stacking', False)
data, timerange = self.load_bt_data()
all_results = {}
for strat in self.strategylist:
logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) logger.info("Running backtesting for Strategy %s", strat.get_strategy_name())
backtest_start_time = datetime.now(timezone.utc)
self._set_strategy(strat) self._set_strategy(strat)
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)()
# Use max_open_trades in backtesting, except --disable-max-market-positions is set # Use max_open_trades in backtesting, except --disable-max-market-positions is set
if self.config.get('use_max_market_positions', True): if self.config.get('use_max_market_positions', True):
# Must come from strategy config, as the strategy may modify this setting. # Must come from strategy config, as the strategy may modify this setting.
@ -462,23 +458,42 @@ class Backtesting:
logger.info(f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' logger.info(f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} '
f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} '
f'({(max_date - min_date).days} days)..') f'({(max_date - min_date).days} days)..')
# Execute backtest and print results # Execute backtest and store results
results = self.backtest( results = self.backtest(
processed=preprocessed, processed=preprocessed,
stake_amount=self.config['stake_amount'], stake_amount=self.config['stake_amount'],
start_date=min_date.datetime, start_date=min_date.datetime,
end_date=max_date.datetime, end_date=max_date.datetime,
max_open_trades=max_open_trades, max_open_trades=max_open_trades,
position_stacking=position_stacking, position_stacking=self.config.get('position_stacking', False),
enable_protections=self.config.get('enable_protections', False), enable_protections=self.config.get('enable_protections', False),
) )
all_results[self.strategy.get_strategy_name()] = { backtest_end_time = datetime.now(timezone.utc)
self.all_results[self.strategy.get_strategy_name()] = {
'results': results, 'results': results,
'config': self.strategy.config, 'config': self.strategy.config,
'locks': PairLocks.locks, 'locks': PairLocks.locks,
'backtest_start_time': int(backtest_start_time.timestamp()),
'backtest_end_time': int(backtest_end_time.timestamp()),
} }
return min_date, max_date
stats = generate_backtest_stats(data, all_results, min_date=min_date, max_date=max_date) def start(self) -> None:
"""
Run backtesting end-to-end
:return: None
"""
data: Dict[str, Any] = {}
data, timerange = self.load_bt_data()
min_date = None
max_date = None
for strat in self.strategylist:
min_date, max_date = self.backtest_one_strategy(strat, data, timerange)
stats = generate_backtest_stats(data, self.all_results,
min_date=min_date, max_date=max_date)
if self.config.get('export', False): if self.config.get('export', False):
store_backtest_stats(self.config['exportfilename'], stats) store_backtest_stats(self.config['exportfilename'], stats)

View File

@ -650,7 +650,7 @@ class Hyperopt:
# Trim startup period from analyzed dataframe # Trim startup period from analyzed dataframe
for pair, df in preprocessed.items(): for pair, df in preprocessed.items():
preprocessed[pair] = trim_dataframe(df, timerange) preprocessed[pair] = trim_dataframe(df, timerange)
min_date, max_date = get_timerange(data) min_date, max_date = get_timerange(preprocessed)
logger.info(f'Hyperopting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' logger.info(f'Hyperopting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} '
f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} '

View File

@ -282,6 +282,9 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
'backtest_end_ts': max_date.int_timestamp * 1000, 'backtest_end_ts': max_date.int_timestamp * 1000,
'backtest_days': backtest_days, 'backtest_days': backtest_days,
'backtest_run_start_ts': content['backtest_start_time'],
'backtest_run_end_ts': content['backtest_end_time'],
'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else 0, 'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else 0,
'market_change': market_change, 'market_change': market_change,
'pairlist': list(btdata.keys()), 'pairlist': list(btdata.keys()),
@ -290,15 +293,20 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
'max_open_trades': (config['max_open_trades'] 'max_open_trades': (config['max_open_trades']
if config['max_open_trades'] != float('inf') else -1), if config['max_open_trades'] != float('inf') else -1),
'timeframe': config['timeframe'], 'timeframe': config['timeframe'],
'timerange': config.get('timerange', ''),
'enable_protections': config.get('enable_protections', False),
'strategy_name': strategy,
# Parameters relevant for backtesting # Parameters relevant for backtesting
'stoploss': config['stoploss'], 'stoploss': config['stoploss'],
'trailing_stop': config.get('trailing_stop', False), 'trailing_stop': config.get('trailing_stop', False),
'trailing_stop_positive': config.get('trailing_stop_positive'), 'trailing_stop_positive': config.get('trailing_stop_positive'),
'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset', 0.0), 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset', 0.0),
'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached', False), 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached', False),
'use_custom_stoploss': config.get('use_custom_stoploss', False),
'minimal_roi': config['minimal_roi'], 'minimal_roi': config['minimal_roi'],
'use_sell_signal': config['ask_strategy']['use_sell_signal'], 'use_sell_signal': config['ask_strategy']['use_sell_signal'],
'sell_profit_only': config['ask_strategy']['sell_profit_only'], 'sell_profit_only': config['ask_strategy']['sell_profit_only'],
'sell_profit_offset': config['ask_strategy']['sell_profit_offset'],
'ignore_roi_if_buy_signal': config['ask_strategy']['ignore_roi_if_buy_signal'], 'ignore_roi_if_buy_signal': config['ask_strategy']['ignore_roi_if_buy_signal'],
**daily_stats, **daily_stats,
} }

View File

@ -342,6 +342,12 @@ class Trade(_DECL_BASE):
self.max_rate = max(current_price, self.max_rate or self.open_rate) self.max_rate = max(current_price, self.max_rate or self.open_rate)
self.min_rate = min(current_price, self.min_rate or self.open_rate) self.min_rate = min(current_price, self.min_rate or self.open_rate)
def _set_new_stoploss(self, new_loss: float, stoploss: float):
"""Assign new stop value"""
self.stop_loss = new_loss
self.stop_loss_pct = -1 * abs(stoploss)
self.stoploss_last_update = datetime.utcnow()
def adjust_stop_loss(self, current_price: float, stoploss: float, def adjust_stop_loss(self, current_price: float, stoploss: float,
initial: bool = False) -> None: initial: bool = False) -> None:
""" """
@ -360,19 +366,15 @@ class Trade(_DECL_BASE):
# no stop loss assigned yet # no stop loss assigned yet
if not self.stop_loss: if not self.stop_loss:
logger.debug(f"{self.pair} - Assigning new stoploss...") logger.debug(f"{self.pair} - Assigning new stoploss...")
self.stop_loss = new_loss self._set_new_stoploss(new_loss, stoploss)
self.stop_loss_pct = -1 * abs(stoploss)
self.initial_stop_loss = new_loss self.initial_stop_loss = new_loss
self.initial_stop_loss_pct = -1 * abs(stoploss) self.initial_stop_loss_pct = -1 * abs(stoploss)
self.stoploss_last_update = datetime.utcnow()
# evaluate if the stop loss needs to be updated # evaluate if the stop loss needs to be updated
else: else:
if new_loss > self.stop_loss: # stop losses only walk up, never down! if new_loss > self.stop_loss: # stop losses only walk up, never down!
logger.debug(f"{self.pair} - Adjusting stoploss...") logger.debug(f"{self.pair} - Adjusting stoploss...")
self.stop_loss = new_loss self._set_new_stoploss(new_loss, stoploss)
self.stop_loss_pct = -1 * abs(stoploss)
self.stoploss_last_update = datetime.utcnow()
else: else:
logger.debug(f"{self.pair} - Keeping current stoploss...") logger.debug(f"{self.pair} - Keeping current stoploss...")

View File

@ -13,6 +13,7 @@ from freqtrade.data.history import get_timerange, load_data
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_prev_date, timeframe_to_seconds from freqtrade.exchange import timeframe_to_prev_date, timeframe_to_seconds
from freqtrade.misc import pair_to_filename from freqtrade.misc import pair_to_filename
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.strategy import IStrategy from freqtrade.strategy import IStrategy
@ -29,16 +30,16 @@ except ImportError:
exit(1) exit(1)
def init_plotscript(config, startup_candles: int = 0): def init_plotscript(config, markets: List, startup_candles: int = 0):
""" """
Initialize objects needed for plotting Initialize objects needed for plotting
:return: Dict with candle (OHLCV) data, trades and pairs :return: Dict with candle (OHLCV) data, trades and pairs
""" """
if "pairs" in config: if "pairs" in config:
pairs = config['pairs'] pairs = expand_pairlist(config['pairs'], markets)
else: else:
pairs = config['exchange']['pair_whitelist'] pairs = expand_pairlist(config['exchange']['pair_whitelist'], markets)
# Set timerange to use # Set timerange to use
timerange = TimeRange.parse_timerange(config.get('timerange')) timerange = TimeRange.parse_timerange(config.get('timerange'))
@ -444,6 +445,8 @@ def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame],
# Trim trades to available OHLCV data # Trim trades to available OHLCV data
trades = extract_trades_of_period(df_comb, trades, date_index=True) trades = extract_trades_of_period(df_comb, trades, date_index=True)
if len(trades) == 0:
raise OperationalException('No trades found in selected timerange.')
# Add combined cumulative profit # Add combined cumulative profit
df_comb = create_cum_profit(df_comb, trades, 'cum_profit', timeframe) df_comb = create_cum_profit(df_comb, trades, 'cum_profit', timeframe)
@ -525,7 +528,7 @@ def load_and_plot_trades(config: Dict[str, Any]):
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config) exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
IStrategy.dp = DataProvider(config, exchange) IStrategy.dp = DataProvider(config, exchange)
plot_elements = init_plotscript(config, strategy.startup_candle_count) plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count)
timerange = plot_elements['timerange'] timerange = plot_elements['timerange']
trades = plot_elements['trades'] trades = plot_elements['trades']
pair_counter = 0 pair_counter = 0
@ -560,7 +563,8 @@ def plot_profit(config: Dict[str, Any]) -> None:
But should be somewhat proportional, and therefor useful But should be somewhat proportional, and therefor useful
in helping out to find a good algorithm. in helping out to find a good algorithm.
""" """
plot_elements = init_plotscript(config) exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
plot_elements = init_plotscript(config, list(exchange.markets))
trades = plot_elements['trades'] trades = plot_elements['trades']
# Filter trades to relevant pairs # Filter trades to relevant pairs
# Remove open pairs - we don't know the profit yet so can't calculate profit for these. # Remove open pairs - we don't know the profit yet so can't calculate profit for these.

View File

@ -124,10 +124,21 @@ class IPairList(LoggingMixin, ABC):
""" """
return self._pairlistmanager.verify_blacklist(pairlist, logmethod) return self._pairlistmanager.verify_blacklist(pairlist, logmethod)
def verify_whitelist(self, pairlist: List[str], logmethod,
keep_invalid: bool = False) -> List[str]:
"""
Proxy method to verify_whitelist for easy access for child classes.
:param pairlist: Pairlist to validate
:param logmethod: Function that'll be called, `logger.info` or `logger.warning`
:param keep_invalid: If sets to True, drops invalid pairs silently while expanding regexes.
:return: pairlist - whitelisted pairs
"""
return self._pairlistmanager.verify_whitelist(pairlist, logmethod, keep_invalid)
def _whitelist_for_active_markets(self, pairlist: List[str]) -> List[str]: def _whitelist_for_active_markets(self, pairlist: List[str]) -> List[str]:
""" """
Check available markets and remove pair from whitelist if necessary Check available markets and remove pair from whitelist if necessary
:param whitelist: the sorted list of pairs the user might want to trade :param pairlist: the sorted list of pairs the user might want to trade
:return: the list of pairs the user wants to trade without those unavailable or :return: the list of pairs the user wants to trade without those unavailable or
black_listed black_listed
""" """

View File

@ -43,7 +43,7 @@ class SpreadFilter(IPairList):
:param ticker: ticker dict as returned from ccxt.load_markets() :param ticker: ticker dict as returned from ccxt.load_markets()
:return: True if the pair can stay, false if it should be removed :return: True if the pair can stay, false if it should be removed
""" """
if 'bid' in ticker and 'ask' in ticker: if 'bid' in ticker and 'ask' in ticker and ticker['ask']:
spread = 1 - ticker['bid'] / ticker['ask'] spread = 1 - ticker['bid'] / ticker['ask']
if spread > self._max_spread_ratio: if spread > self._max_spread_ratio:
self.log_once(f"Removed {pair} from whitelist, because spread " self.log_once(f"Removed {pair} from whitelist, because spread "
@ -52,4 +52,6 @@ class SpreadFilter(IPairList):
return False return False
else: else:
return True return True
self.log_once(f"Removed {pair} from whitelist due to invalid ticker data: {ticker}",
logger.info)
return False return False

View File

@ -50,9 +50,12 @@ class StaticPairList(IPairList):
:return: List of pairs :return: List of pairs
""" """
if self._allow_inactive: if self._allow_inactive:
return self._config['exchange']['pair_whitelist'] return self.verify_whitelist(
self._config['exchange']['pair_whitelist'], logger.info, keep_invalid=True
)
else: else:
return self._whitelist_for_active_markets(self._config['exchange']['pair_whitelist']) return self._whitelist_for_active_markets(
self.verify_whitelist(self._config['exchange']['pair_whitelist'], logger.info))
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
""" """

View File

@ -0,0 +1,42 @@
import re
from typing import List
def expand_pairlist(wildcardpl: List[str], available_pairs: List[str],
keep_invalid: bool = False) -> List[str]:
"""
Expand pairlist potentially containing wildcards based on available markets.
This will implicitly filter all pairs in the wildcard-list which are not in available_pairs.
:param wildcardpl: List of Pairlists, which may contain regex
:param available_pairs: List of all available pairs (`exchange.get_markets().keys()`)
:param keep_invalid: If sets to True, drops invalid pairs silently while expanding regexes
:return expanded pairlist, with Regexes from wildcardpl applied to match all available pairs.
:raises: ValueError if a wildcard is invalid (like '*/BTC' - which should be `.*/BTC`)
"""
result = []
if keep_invalid:
for pair_wc in wildcardpl:
try:
comp = re.compile(pair_wc)
result_partial = [
pair for pair in available_pairs if re.fullmatch(comp, pair)
]
# Add all matching pairs.
# If there are no matching pairs (Pair not on exchange) keep it.
result += result_partial or [pair_wc]
except re.error as err:
raise ValueError(f"Wildcard error in {pair_wc}, {err}")
for element in result:
if not re.fullmatch(r'^[A-Za-z0-9/-]+$', element):
result.remove(element)
else:
for pair_wc in wildcardpl:
try:
comp = re.compile(pair_wc)
result += [
pair for pair in available_pairs if re.fullmatch(comp, pair)
]
except re.error as err:
raise ValueError(f"Wildcard error in {pair_wc}, {err}")
return result

View File

@ -10,6 +10,7 @@ from cachetools import TTLCache, cached
from freqtrade.constants import ListPairsWithTimeframes from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.plugins.pairlist.IPairList import IPairList from freqtrade.plugins.pairlist.IPairList import IPairList
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.resolvers import PairListResolver from freqtrade.resolvers import PairListResolver
@ -42,30 +43,40 @@ class PairListManager():
@property @property
def whitelist(self) -> List[str]: def whitelist(self) -> List[str]:
""" """The current whitelist"""
Has the current whitelist
"""
return self._whitelist return self._whitelist
@property @property
def blacklist(self) -> List[str]: def blacklist(self) -> List[str]:
""" """
Has the current blacklist The current blacklist
-> no need to overwrite in subclasses -> no need to overwrite in subclasses
""" """
return self._blacklist return self._blacklist
@property
def expanded_blacklist(self) -> List[str]:
"""The expanded blacklist (including wildcard expansion)"""
return expand_pairlist(self._blacklist, self._exchange.get_markets().keys())
@property
def expanded_whitelist_keep_invalid(self) -> List[str]:
"""The expanded whitelist (including wildcard expansion), maintaining invalid pairs"""
return expand_pairlist(self._whitelist, self._exchange.get_markets().keys(),
keep_invalid=True)
@property
def expanded_whitelist(self) -> List[str]:
"""The expanded whitelist (including wildcard expansion), filtering invalid pairs"""
return expand_pairlist(self._whitelist, self._exchange.get_markets().keys())
@property @property
def name_list(self) -> List[str]: def name_list(self) -> List[str]:
""" """Get list of loaded Pairlist Handler names"""
Get list of loaded Pairlist Handler names
"""
return [p.name for p in self._pairlist_handlers] return [p.name for p in self._pairlist_handlers]
def short_desc(self) -> List[Dict]: def short_desc(self) -> List[Dict]:
""" """List of short_desc for each Pairlist Handler"""
List of short_desc for each Pairlist Handler
"""
return [{p.name: p.short_desc()} for p in self._pairlist_handlers] return [{p.name: p.short_desc()} for p in self._pairlist_handlers]
@cached(TTLCache(maxsize=1, ttl=1800)) @cached(TTLCache(maxsize=1, ttl=1800))
@ -73,9 +84,7 @@ class PairListManager():
return self._exchange.get_tickers() return self._exchange.get_tickers()
def refresh_pairlist(self) -> None: def refresh_pairlist(self) -> None:
""" """Run pairlist through all configured Pairlist Handlers."""
Run pairlist through all configured Pairlist Handlers.
"""
# Tickers should be cached to avoid calling the exchange on each call. # Tickers should be cached to avoid calling the exchange on each call.
tickers: Dict = {} tickers: Dict = {}
if self._tickers_needed: if self._tickers_needed:
@ -120,12 +129,39 @@ class PairListManager():
:param logmethod: Function that'll be called, `logger.info` or `logger.warning`. :param logmethod: Function that'll be called, `logger.info` or `logger.warning`.
:return: pairlist - blacklisted pairs :return: pairlist - blacklisted pairs
""" """
try:
blacklist = self.expanded_blacklist
except ValueError as err:
logger.error(f"Pair blacklist contains an invalid Wildcard: {err}")
return []
for pair in deepcopy(pairlist): for pair in deepcopy(pairlist):
if pair in self._blacklist: if pair in blacklist:
logmethod(f"Pair {pair} in your blacklist. Removing it from whitelist...") logmethod(f"Pair {pair} in your blacklist. Removing it from whitelist...")
pairlist.remove(pair) pairlist.remove(pair)
return pairlist return pairlist
def verify_whitelist(self, pairlist: List[str], logmethod,
keep_invalid: bool = False) -> List[str]:
"""
Verify and remove items from pairlist - returning a filtered pairlist.
Logs a warning or info depending on `aswarning`.
Pairlist Handlers explicitly using this method shall use
`logmethod=logger.info` to avoid spamming with warning messages
:param pairlist: Pairlist to validate
:param logmethod: Function that'll be called, `logger.info` or `logger.warning`
:param keep_invalid: If sets to True, drops invalid pairs silently while expanding regexes.
:return: pairlist - whitelisted pairs
"""
try:
if keep_invalid:
whitelist = self.expanded_whitelist_keep_invalid
else:
whitelist = self.expanded_whitelist
except ValueError as err:
logger.error(f"Pair whitelist contains an invalid Wildcard: {err}")
return []
return whitelist
def create_pair_list(self, pairs: List[str], timeframe: str = None) -> ListPairsWithTimeframes: def create_pair_list(self, pairs: List[str], timeframe: str = None) -> ListPairsWithTimeframes:
""" """
Create list of pair tuples with (pair, timeframe) Create list of pair tuples with (pair, timeframe)

View File

@ -53,8 +53,9 @@ class StoplossGuard(IProtection):
# trades = Trade.get_trades(filters).all() # trades = Trade.get_trades(filters).all()
trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until) trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until)
trades = [trade for trade in trades1 if str(trade.sell_reason) == SellType.STOP_LOSS.value trades = [trade for trade in trades1 if (str(trade.sell_reason) in (
or (str(trade.sell_reason) == SellType.TRAILING_STOP_LOSS.value SellType.TRAILING_STOP_LOSS.value, SellType.STOP_LOSS.value,
SellType.STOPLOSS_ON_EXCHANGE.value)
and trade.close_profit < 0)] and trade.close_profit < 0)]
if len(trades) > self._trade_limit: if len(trades) > self._trade_limit:

View File

@ -73,12 +73,15 @@ class StrategyResolver(IResolver):
("order_time_in_force", None, None), ("order_time_in_force", None, None),
("stake_currency", None, None), ("stake_currency", None, None),
("stake_amount", None, None), ("stake_amount", None, None),
("protections", None, None),
("startup_candle_count", None, None), ("startup_candle_count", None, None),
("unfilledtimeout", None, None), ("unfilledtimeout", None, None),
("use_sell_signal", True, 'ask_strategy'), ("use_sell_signal", True, 'ask_strategy'),
("sell_profit_only", False, 'ask_strategy'), ("sell_profit_only", False, 'ask_strategy'),
("ignore_roi_if_buy_signal", False, 'ask_strategy'), ("ignore_roi_if_buy_signal", False, 'ask_strategy'),
("sell_profit_offset", 0.0, 'ask_strategy'),
("disable_dataframe_checks", False, None), ("disable_dataframe_checks", False, None),
("ignore_buying_expired_candle_after", 0, 'ask_strategy')
] ]
for attribute, default, subkey in attributes: for attribute, default, subkey in attributes:
if subkey: if subkey:

View File

@ -1,665 +0,0 @@
import logging
import threading
from copy import deepcopy
from datetime import date, datetime
from ipaddress import IPv4Address
from pathlib import Path
from typing import Any, Callable, Dict
from arrow import Arrow
from flask import Flask, jsonify, request
from flask.json import JSONEncoder
from flask_cors import CORS
from flask_jwt_extended import (JWTManager, create_access_token, create_refresh_token,
get_jwt_identity, jwt_refresh_token_required,
verify_jwt_in_request_optional)
from werkzeug.security import safe_str_cmp
from werkzeug.serving import make_server
from freqtrade.__init__ import __version__
from freqtrade.constants import DATETIME_PRINT_FORMAT, USERPATH_STRATEGIES
from freqtrade.exceptions import OperationalException
from freqtrade.persistence import Trade
from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler
logger = logging.getLogger(__name__)
BASE_URI = "/api/v1"
class FTJSONEncoder(JSONEncoder):
def default(self, obj):
try:
if isinstance(obj, Arrow):
return obj.for_json()
elif isinstance(obj, datetime):
return obj.strftime(DATETIME_PRINT_FORMAT)
elif isinstance(obj, date):
return obj.strftime("%Y-%m-%d")
iterable = iter(obj)
except TypeError:
pass
else:
return list(iterable)
return JSONEncoder.default(self, obj)
# Type should really be Callable[[ApiServer, Any], Any], but that will create a circular dependency
def require_login(func: Callable[[Any, Any], Any]):
def func_wrapper(obj, *args, **kwargs):
verify_jwt_in_request_optional()
auth = request.authorization
if get_jwt_identity() or auth and obj.check_auth(auth.username, auth.password):
return func(obj, *args, **kwargs)
else:
return jsonify({"error": "Unauthorized"}), 401
return func_wrapper
# Type should really be Callable[[ApiServer], Any], but that will create a circular dependency
def rpc_catch_errors(func: Callable[..., Any]):
def func_wrapper(obj, *args, **kwargs):
try:
return func(obj, *args, **kwargs)
except RPCException as e:
logger.exception("API Error calling %s: %s", func.__name__, e)
return obj.rest_error(f"Error querying {func.__name__}: {e}")
return func_wrapper
def shutdown_session(exception=None):
# Remove scoped session
Trade.session.remove()
class ApiServer(RPCHandler):
"""
This class runs api server and provides rpc.rpc functionality to it
This class starts a non-blocking thread the api server runs within
"""
def check_auth(self, username, password):
return (safe_str_cmp(username, self._config['api_server'].get('username')) and
safe_str_cmp(password, self._config['api_server'].get('password')))
def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None:
"""
Init the api server, and init the super class RPCHandler
:param rpc: instance of RPC Helper class
:param config: Configuration object
:return: None
"""
super().__init__(rpc, config)
self.app = Flask(__name__)
self._cors = CORS(self.app,
resources={r"/api/*": {
"supports_credentials": True,
"origins": self._config['api_server'].get('CORS_origins', [])}}
)
# Setup the Flask-JWT-Extended extension
self.app.config['JWT_SECRET_KEY'] = self._config['api_server'].get(
'jwt_secret_key', 'super-secret')
self.jwt = JWTManager(self.app)
self.app.json_encoder = FTJSONEncoder
self.app.teardown_appcontext(shutdown_session)
# Register application handling
self.register_rest_rpc_urls()
thread = threading.Thread(target=self.run, daemon=True)
thread.start()
def cleanup(self) -> None:
logger.info("Stopping API Server")
self.srv.shutdown()
def run(self):
"""
Method that runs flask app in its own thread forever.
Section to handle configuration and running of the Rest server
also to check and warn if not bound to a loopback, warn on security risk.
"""
rest_ip = self._config['api_server']['listen_ip_address']
rest_port = self._config['api_server']['listen_port']
logger.info(f'Starting HTTP Server at {rest_ip}:{rest_port}')
if not IPv4Address(rest_ip).is_loopback:
logger.warning("SECURITY WARNING - Local Rest Server listening to external connections")
logger.warning("SECURITY WARNING - This is insecure please set to your loopback,"
"e.g 127.0.0.1 in config.json")
if not self._config['api_server'].get('password'):
logger.warning("SECURITY WARNING - No password for local REST Server defined. "
"Please make sure that this is intentional!")
# Run the Server
logger.info('Starting Local Rest Server.')
try:
self.srv = make_server(rest_ip, rest_port, self.app)
self.srv.serve_forever()
except Exception:
logger.exception("Api server failed to start.")
logger.info('Local Rest Server started.')
def send_msg(self, msg: Dict[str, str]) -> None:
"""
We don't push to endpoints at the moment.
Take a look at webhooks for that functionality.
"""
pass
def rest_error(self, error_msg, error_code=502):
return jsonify({"error": error_msg}), error_code
def register_rest_rpc_urls(self):
"""
Registers flask app URLs that are calls to functionality in rpc.rpc.
First two arguments passed are /URL and 'Label'
Label can be used as a shortcut when refactoring
:return:
"""
self.app.register_error_handler(404, self.page_not_found)
# Actions to control the bot
self.app.add_url_rule(f'{BASE_URI}/token/login', 'login',
view_func=self._token_login, methods=['POST'])
self.app.add_url_rule(f'{BASE_URI}/token/refresh', 'token_refresh',
view_func=self._token_refresh, methods=['POST'])
self.app.add_url_rule(f'{BASE_URI}/start', 'start',
view_func=self._start, methods=['POST'])
self.app.add_url_rule(f'{BASE_URI}/stop', 'stop', view_func=self._stop, methods=['POST'])
self.app.add_url_rule(f'{BASE_URI}/stopbuy', 'stopbuy',
view_func=self._stopbuy, methods=['POST'])
self.app.add_url_rule(f'{BASE_URI}/reload_config', 'reload_config',
view_func=self._reload_config, methods=['POST'])
# Info commands
self.app.add_url_rule(f'{BASE_URI}/balance', 'balance',
view_func=self._balance, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/count', 'count', view_func=self._count, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/locks', 'locks', view_func=self._locks, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/daily', 'daily', view_func=self._daily, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/edge', 'edge', view_func=self._edge, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/logs', 'log', view_func=self._get_logs, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/profit', 'profit',
view_func=self._profit, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/stats', 'stats',
view_func=self._stats, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/performance', 'performance',
view_func=self._performance, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/status', 'status',
view_func=self._status, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/version', 'version',
view_func=self._version, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/show_config', 'show_config',
view_func=self._show_config, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/ping', 'ping',
view_func=self._ping, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/trades', 'trades',
view_func=self._trades, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/trades/<int:tradeid>', 'trades_delete',
view_func=self._trades_delete, methods=['DELETE'])
self.app.add_url_rule(f'{BASE_URI}/pair_candles', 'pair_candles',
view_func=self._analysed_candles, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/pair_history', 'pair_history',
view_func=self._analysed_history, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/plot_config', 'plot_config',
view_func=self._plot_config, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/strategies', 'strategies',
view_func=self._list_strategies, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/strategy/<string:strategy>', 'strategy',
view_func=self._get_strategy, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/available_pairs', 'pairs',
view_func=self._list_available_pairs, methods=['GET'])
# Combined actions and infos
self.app.add_url_rule(f'{BASE_URI}/blacklist', 'blacklist', view_func=self._blacklist,
methods=['GET', 'POST'])
self.app.add_url_rule(f'{BASE_URI}/whitelist', 'whitelist', view_func=self._whitelist,
methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/forcebuy', 'forcebuy',
view_func=self._forcebuy, methods=['POST'])
self.app.add_url_rule(f'{BASE_URI}/forcesell', 'forcesell', view_func=self._forcesell,
methods=['POST'])
@require_login
def page_not_found(self, error):
"""
Return "404 not found", 404.
"""
return jsonify({
'status': 'error',
'reason': f"There's no API call for {request.base_url}.",
'code': 404
}), 404
@require_login
@rpc_catch_errors
def _token_login(self):
"""
Handler for /token/login
Returns a JWT token
"""
auth = request.authorization
if auth and self.check_auth(auth.username, auth.password):
keystuff = {'u': auth.username}
ret = {
'access_token': create_access_token(identity=keystuff),
'refresh_token': create_refresh_token(identity=keystuff),
}
return jsonify(ret)
return jsonify({"error": "Unauthorized"}), 401
@jwt_refresh_token_required
@rpc_catch_errors
def _token_refresh(self):
"""
Handler for /token/refresh
Returns a JWT token based on a JWT refresh token
"""
current_user = get_jwt_identity()
new_token = create_access_token(identity=current_user, fresh=False)
ret = {'access_token': new_token}
return jsonify(ret)
@require_login
@rpc_catch_errors
def _start(self):
"""
Handler for /start.
Starts TradeThread in bot if stopped.
"""
msg = self._rpc._rpc_start()
return jsonify(msg)
@require_login
@rpc_catch_errors
def _stop(self):
"""
Handler for /stop.
Stops TradeThread in bot if running
"""
msg = self._rpc._rpc_stop()
return jsonify(msg)
@require_login
@rpc_catch_errors
def _stopbuy(self):
"""
Handler for /stopbuy.
Sets max_open_trades to 0 and gracefully sells all open trades
"""
msg = self._rpc._rpc_stopbuy()
return jsonify(msg)
@rpc_catch_errors
def _ping(self):
"""
simple ping version
"""
return jsonify({"status": "pong"})
@require_login
@rpc_catch_errors
def _version(self):
"""
Prints the bot's version
"""
return jsonify({"version": __version__})
@require_login
@rpc_catch_errors
def _show_config(self):
"""
Prints the bot's version
"""
return jsonify(RPC._rpc_show_config(self._config, self._rpc._freqtrade.state))
@require_login
@rpc_catch_errors
def _reload_config(self):
"""
Handler for /reload_config.
Triggers a config file reload
"""
msg = self._rpc._rpc_reload_config()
return jsonify(msg)
@require_login
@rpc_catch_errors
def _count(self):
"""
Handler for /count.
Returns the number of trades running
"""
msg = self._rpc._rpc_count()
return jsonify(msg)
@require_login
@rpc_catch_errors
def _locks(self):
"""
Handler for /locks.
Returns the currently active locks.
"""
return jsonify(self._rpc._rpc_locks())
@require_login
@rpc_catch_errors
def _daily(self):
"""
Returns the last X days trading stats summary.
:return: stats
"""
timescale = request.args.get('timescale', 7)
timescale = int(timescale)
stats = self._rpc._rpc_daily_profit(timescale,
self._config['stake_currency'],
self._config.get('fiat_display_currency', '')
)
return jsonify(stats)
@require_login
@rpc_catch_errors
def _get_logs(self):
"""
Returns latest logs
get:
param:
limit: Only get a certain number of records
"""
limit = int(request.args.get('limit', 0)) or None
return jsonify(RPC._rpc_get_logs(limit))
@require_login
@rpc_catch_errors
def _edge(self):
"""
Returns information related to Edge.
:return: edge stats
"""
stats = self._rpc._rpc_edge()
return jsonify(stats)
@require_login
@rpc_catch_errors
def _profit(self):
"""
Handler for /profit.
Returns a cumulative profit statistics
:return: stats
"""
stats = self._rpc._rpc_trade_statistics(self._config['stake_currency'],
self._config.get('fiat_display_currency')
)
return jsonify(stats)
@require_login
@rpc_catch_errors
def _stats(self):
"""
Handler for /stats.
Returns a Object with "durations" and "sell_reasons" as keys.
"""
stats = self._rpc._rpc_stats()
return jsonify(stats)
@require_login
@rpc_catch_errors
def _performance(self):
"""
Handler for /performance.
Returns a cumulative performance statistics
:return: stats
"""
stats = self._rpc._rpc_performance()
return jsonify(stats)
@require_login
@rpc_catch_errors
def _status(self):
"""
Handler for /status.
Returns the current status of the trades in json format
"""
try:
results = self._rpc._rpc_trade_status()
return jsonify(results)
except RPCException:
return jsonify([])
@require_login
@rpc_catch_errors
def _balance(self):
"""
Handler for /balance.
Returns the current status of the trades in json format
"""
results = self._rpc._rpc_balance(self._config['stake_currency'],
self._config.get('fiat_display_currency', ''))
return jsonify(results)
@require_login
@rpc_catch_errors
def _trades(self):
"""
Handler for /trades.
Returns the X last trades in json format
"""
limit = int(request.args.get('limit', 0))
results = self._rpc._rpc_trade_history(limit)
return jsonify(results)
@require_login
@rpc_catch_errors
def _trades_delete(self, tradeid: int):
"""
Handler for DELETE /trades/<tradeid> endpoint.
Removes the trade from the database (tries to cancel open orders first!)
get:
param:
tradeid: Numeric trade-id assigned to the trade.
"""
result = self._rpc._rpc_delete(tradeid)
return jsonify(result)
@require_login
@rpc_catch_errors
def _whitelist(self):
"""
Handler for /whitelist.
"""
results = self._rpc._rpc_whitelist()
return jsonify(results)
@require_login
@rpc_catch_errors
def _blacklist(self):
"""
Handler for /blacklist.
"""
add = request.json.get("blacklist", None) if request.method == 'POST' else None
results = self._rpc._rpc_blacklist(add)
return jsonify(results)
@require_login
@rpc_catch_errors
def _forcebuy(self):
"""
Handler for /forcebuy.
"""
asset = request.json.get("pair")
price = request.json.get("price", None)
price = float(price) if price is not None else price
trade = self._rpc._rpc_forcebuy(asset, price)
if trade:
return jsonify(trade.to_json())
else:
return jsonify({"status": f"Error buying pair {asset}."})
@require_login
@rpc_catch_errors
def _forcesell(self):
"""
Handler for /forcesell.
"""
tradeid = request.json.get("tradeid")
results = self._rpc._rpc_forcesell(tradeid)
return jsonify(results)
@require_login
@rpc_catch_errors
def _analysed_candles(self):
"""
Handler for /pair_candles.
Returns the dataframe the bot is using during live/dry operations.
Takes the following get arguments:
get:
parameters:
- pair: Pair
- timeframe: Timeframe to get data for (should be aligned to strategy.timeframe)
- limit: Limit return length to the latest X candles
"""
pair = request.args.get("pair")
timeframe = request.args.get("timeframe")
limit = request.args.get("limit", type=int)
if not pair or not timeframe:
return self.rest_error("Mandatory parameter missing.", 400)
results = self._rpc._rpc_analysed_dataframe(pair, timeframe, limit)
return jsonify(results)
@require_login
@rpc_catch_errors
def _analysed_history(self):
"""
Handler for /pair_history.
Returns the dataframe of a given timerange
Takes the following get arguments:
get:
parameters:
- pair: Pair
- timeframe: Timeframe to get data for (should be aligned to strategy.timeframe)
- strategy: Strategy to use - Must exist in configured strategy-path!
- timerange: timerange in the format YYYYMMDD-YYYYMMDD (YYYYMMDD- or (-YYYYMMDD))
are als possible. If omitted uses all available data.
"""
pair = request.args.get("pair")
timeframe = request.args.get("timeframe")
timerange = request.args.get("timerange")
strategy = request.args.get("strategy")
if not pair or not timeframe or not timerange or not strategy:
return self.rest_error("Mandatory parameter missing.", 400)
config = deepcopy(self._config)
config.update({
'strategy': strategy,
})
results = RPC._rpc_analysed_history_full(config, pair, timeframe, timerange)
return jsonify(results)
@require_login
@rpc_catch_errors
def _plot_config(self):
"""
Handler for /plot_config.
"""
return jsonify(self._rpc._rpc_plot_config())
@require_login
@rpc_catch_errors
def _list_strategies(self):
directory = Path(self._config.get(
'strategy_path', self._config['user_data_dir'] / USERPATH_STRATEGIES))
from freqtrade.resolvers.strategy_resolver import StrategyResolver
strategy_objs = StrategyResolver.search_all_objects(directory, False)
strategy_objs = sorted(strategy_objs, key=lambda x: x['name'])
return jsonify({'strategies': [x['name'] for x in strategy_objs]})
@require_login
@rpc_catch_errors
def _get_strategy(self, strategy: str):
"""
Get a single strategy
get:
parameters:
- strategy: Only get this strategy
"""
config = deepcopy(self._config)
from freqtrade.resolvers.strategy_resolver import StrategyResolver
try:
strategy_obj = StrategyResolver._load_strategy(strategy, config,
extra_dir=config.get('strategy_path'))
except OperationalException:
return self.rest_error("Strategy not found.", 404)
return jsonify({
'strategy': strategy_obj.get_strategy_name(),
'code': strategy_obj.__source__,
})
@require_login
@rpc_catch_errors
def _list_available_pairs(self):
"""
Handler for /available_pairs.
Returns an object, with pairs, available pair length and pair_interval combinations
Takes the following get arguments:
get:
parameters:
- stake_currency: Filter on this stake currency
- timeframe: Timeframe to get data for Filter elements to this timeframe
"""
timeframe = request.args.get("timeframe")
stake_currency = request.args.get("stake_currency")
from freqtrade.data.history import get_datahandler
dh = get_datahandler(self._config['datadir'], self._config.get('dataformat_ohlcv', None))
pair_interval = dh.ohlcv_get_available_data(self._config['datadir'])
if timeframe:
pair_interval = [pair for pair in pair_interval if pair[1] == timeframe]
if stake_currency:
pair_interval = [pair for pair in pair_interval if pair[0].endswith(stake_currency)]
pair_interval = sorted(pair_interval, key=lambda x: x[0])
pairs = list({x[0] for x in pair_interval})
result = {
'length': len(pairs),
'pairs': pairs,
'pair_interval': pair_interval,
}
return jsonify(result)

View File

@ -0,0 +1,2 @@
# flake8: noqa: F401
from .webserver import ApiServer

View File

@ -0,0 +1,106 @@
import secrets
from datetime import datetime, timedelta
import jwt
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from fastapi.security.http import HTTPBasic, HTTPBasicCredentials
from freqtrade.rpc.api_server.api_schemas import AccessAndRefreshToken, AccessToken
from freqtrade.rpc.api_server.deps import get_api_config
ALGORITHM = "HS256"
router_login = APIRouter()
def verify_auth(api_config, username: str, password: str):
"""Verify username/password"""
return (secrets.compare_digest(username, api_config.get('username')) and
secrets.compare_digest(password, api_config.get('password')))
httpbasic = HTTPBasic(auto_error=False)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False)
def get_user_from_token(token, secret_key: str, token_type: str = "access"):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, secret_key, algorithms=[ALGORITHM])
username: str = payload.get("identity", {}).get('u')
if username is None:
raise credentials_exception
if payload.get("type") != token_type:
raise credentials_exception
except jwt.PyJWTError:
raise credentials_exception
return username
def create_token(data: dict, secret_key: str, token_type: str = "access") -> str:
to_encode = data.copy()
if token_type == "access":
expire = datetime.utcnow() + timedelta(minutes=15)
elif token_type == "refresh":
expire = datetime.utcnow() + timedelta(days=30)
else:
raise ValueError()
to_encode.update({
"exp": expire,
"iat": datetime.utcnow(),
"type": token_type,
})
encoded_jwt = jwt.encode(to_encode, secret_key, algorithm=ALGORITHM)
return encoded_jwt
def http_basic_or_jwt_token(form_data: HTTPBasicCredentials = Depends(httpbasic),
token: str = Depends(oauth2_scheme),
api_config=Depends(get_api_config)):
if token:
return get_user_from_token(token, api_config.get('jwt_secret_key', 'super-secret'))
elif form_data and verify_auth(api_config, form_data.username, form_data.password):
return form_data.username
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Unauthorized",
)
@router_login.post('/token/login', response_model=AccessAndRefreshToken)
def token_login(form_data: HTTPBasicCredentials = Depends(HTTPBasic()),
api_config=Depends(get_api_config)):
if verify_auth(api_config, form_data.username, form_data.password):
token_data = {'identity': {'u': form_data.username}}
access_token = create_token(token_data, api_config.get('jwt_secret_key', 'super-secret'))
refresh_token = create_token(token_data, api_config.get('jwt_secret_key', 'super-secret'),
token_type="refresh")
return {
"access_token": access_token,
"refresh_token": refresh_token,
}
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
)
@router_login.post('/token/refresh', response_model=AccessToken)
def token_refresh(token: str = Depends(oauth2_scheme), api_config=Depends(get_api_config)):
# Refresh token
u = get_user_from_token(token, api_config.get(
'jwt_secret_key', 'super-secret'), 'refresh')
token_data = {'identity': {'u': u}}
access_token = create_token(token_data, api_config.get('jwt_secret_key', 'super-secret'),
token_type="access")
return {'access_token': access_token}

View File

@ -0,0 +1,307 @@
from datetime import date, datetime
from typing import Any, Dict, List, Optional, TypeVar, Union
from pydantic import BaseModel
from freqtrade.constants import DATETIME_PRINT_FORMAT
class Ping(BaseModel):
status: str
class AccessToken(BaseModel):
access_token: str
class AccessAndRefreshToken(AccessToken):
refresh_token: str
class Version(BaseModel):
version: str
class StatusMsg(BaseModel):
status: str
class ResultMsg(BaseModel):
result: str
class Balance(BaseModel):
currency: str
free: float
balance: float
used: float
est_stake: float
stake: str
class Balances(BaseModel):
currencies: List[Balance]
total: float
symbol: str
value: float
stake: str
note: str
class Count(BaseModel):
current: int
max: int
total_stake: float
class PerformanceEntry(BaseModel):
pair: str
profit: float
count: int
class Profit(BaseModel):
profit_closed_coin: float
profit_closed_percent: float
profit_closed_percent_mean: float
profit_closed_ratio_mean: float
profit_closed_percent_sum: float
profit_closed_ratio_sum: float
profit_closed_fiat: float
profit_all_coin: float
profit_all_percent: float
profit_all_percent_mean: float
profit_all_ratio_mean: float
profit_all_percent_sum: float
profit_all_ratio_sum: float
profit_all_fiat: float
trade_count: int
closed_trade_count: int
first_trade_date: str
first_trade_timestamp: int
latest_trade_date: str
latest_trade_timestamp: int
avg_duration: str
best_pair: str
best_rate: float
winning_trades: int
losing_trades: int
class SellReason(BaseModel):
wins: int
losses: int
draws: int
class Stats(BaseModel):
sell_reasons: Dict[str, SellReason]
durations: Dict[str, Union[str, float]]
class DailyRecord(BaseModel):
date: date
abs_profit: float
fiat_value: float
trade_count: int
class Daily(BaseModel):
data: List[DailyRecord]
fiat_display_currency: str
stake_currency: str
class ShowConfig(BaseModel):
dry_run: str
stake_currency: str
stake_amount: Union[float, str]
max_open_trades: int
minimal_roi: Dict[str, Any]
stoploss: float
trailing_stop: bool
trailing_stop_positive: Optional[float]
trailing_stop_positive_offset: Optional[float]
trailing_only_offset_is_reached: Optional[bool]
timeframe: str
timeframe_ms: int
timeframe_min: int
exchange: str
strategy: str
forcebuy_enabled: bool
ask_strategy: Dict[str, Any]
bid_strategy: Dict[str, Any]
bot_name: str
state: str
runmode: str
class TradeSchema(BaseModel):
trade_id: int
pair: str
is_open: bool
exchange: str
amount: float
amount_requested: float
stake_amount: float
strategy: str
timeframe: int
fee_open: Optional[float]
fee_open_cost: Optional[float]
fee_open_currency: Optional[str]
fee_close: Optional[float]
fee_close_cost: Optional[float]
fee_close_currency: Optional[str]
open_date_hum: str
open_date: str
open_timestamp: int
open_rate: float
open_rate_requested: Optional[float]
open_trade_value: float
close_date_hum: Optional[str]
close_date: Optional[str]
close_timestamp: Optional[int]
close_rate: Optional[float]
close_rate_requested: Optional[float]
close_profit: Optional[float]
close_profit_pct: Optional[float]
close_profit_abs: Optional[float]
profit_ratio: Optional[float]
profit_pct: Optional[float]
profit_abs: Optional[float]
sell_reason: Optional[str]
sell_order_status: Optional[str]
stop_loss_abs: Optional[float]
stop_loss_ratio: Optional[float]
stop_loss_pct: Optional[float]
stoploss_order_id: Optional[str]
stoploss_last_update: Optional[str]
stoploss_last_update_timestamp: Optional[int]
initial_stop_loss_abs: Optional[float]
initial_stop_loss_ratio: Optional[float]
initial_stop_loss_pct: Optional[float]
min_rate: Optional[float]
max_rate: Optional[float]
open_order_id: Optional[str]
class OpenTradeSchema(TradeSchema):
stoploss_current_dist: Optional[float]
stoploss_current_dist_pct: Optional[float]
stoploss_current_dist_ratio: Optional[float]
stoploss_entry_dist: Optional[float]
stoploss_entry_dist_ratio: Optional[float]
base_currency: str
current_profit: float
current_profit_abs: float
current_profit_pct: float
current_rate: float
open_order: Optional[str]
class TradeResponse(BaseModel):
trades: List[TradeSchema]
trades_count: int
ForceBuyResponse = TypeVar('ForceBuyResponse', TradeSchema, StatusMsg)
class LockModel(BaseModel):
active: bool
lock_end_time: str
lock_end_timestamp: int
lock_time: str
lock_timestamp: int
pair: str
reason: str
class Locks(BaseModel):
lock_count: int
locks: List[LockModel]
class Logs(BaseModel):
log_count: int
logs: List[List]
class ForceBuyPayload(BaseModel):
pair: str
price: Optional[float]
class ForceSellPayload(BaseModel):
tradeid: str
class BlacklistPayload(BaseModel):
blacklist: List[str]
class BlacklistResponse(BaseModel):
blacklist: List[str]
blacklist_expanded: List[str]
errors: Dict
length: int
method: List[str]
class WhitelistResponse(BaseModel):
whitelist: List[str]
length: int
method: List[str]
class DeleteTrade(BaseModel):
cancel_order_count: int
result: str
result_msg: str
trade_id: int
class PlotConfig_(BaseModel):
main_plot: Dict[str, Any]
subplots: Optional[Dict[str, Any]]
PlotConfig = TypeVar('PlotConfig', PlotConfig_, Dict)
class StrategyListResponse(BaseModel):
strategies: List[str]
class StrategyResponse(BaseModel):
strategy: str
code: str
class AvailablePairs(BaseModel):
length: int
pairs: List[str]
pair_interval: List[List[str]]
class PairHistory(BaseModel):
strategy: str
pair: str
timeframe: str
timeframe_ms: int
columns: List[str]
data: List[Any]
length: int
buy_signals: int
sell_signals: int
last_analyzed: datetime
last_analyzed_ts: int
data_start_ts: int
data_start: str
data_stop: str
data_stop_ts: int
class Config:
json_encoders = {
datetime: lambda v: v.strftime(DATETIME_PRINT_FORMAT),
}

View File

@ -0,0 +1,238 @@
from copy import deepcopy
from pathlib import Path
from typing import List, Optional
from fastapi import APIRouter, Depends
from fastapi.exceptions import HTTPException
from freqtrade import __version__
from freqtrade.constants import USERPATH_STRATEGIES
from freqtrade.data.history import get_datahandler
from freqtrade.exceptions import OperationalException
from freqtrade.rpc import RPC
from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload,
BlacklistResponse, Count, Daily, DeleteTrade,
ForceBuyPayload, ForceBuyResponse,
ForceSellPayload, Locks, Logs, OpenTradeSchema,
PairHistory, PerformanceEntry, Ping, PlotConfig,
Profit, ResultMsg, ShowConfig, Stats, StatusMsg,
StrategyListResponse, StrategyResponse,
TradeResponse, Version, WhitelistResponse)
from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional
from freqtrade.rpc.rpc import RPCException
# Public API, requires no auth.
router_public = APIRouter()
# Private API, protected by authentication
router = APIRouter()
@router_public.get('/ping', response_model=Ping)
def ping():
"""simple ping"""
return {"status": "pong"}
@router.get('/version', response_model=Version, tags=['info'])
def version():
""" Bot Version info"""
return {"version": __version__}
@router.get('/balance', response_model=Balances, tags=['info'])
def balance(rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
"""Account Balances"""
return rpc._rpc_balance(config['stake_currency'], config.get('fiat_display_currency', ''),)
@router.get('/count', response_model=Count, tags=['info'])
def count(rpc: RPC = Depends(get_rpc)):
return rpc._rpc_count()
@router.get('/performance', response_model=List[PerformanceEntry], tags=['info'])
def performance(rpc: RPC = Depends(get_rpc)):
return rpc._rpc_performance()
@router.get('/profit', response_model=Profit, tags=['info'])
def profit(rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
return rpc._rpc_trade_statistics(config['stake_currency'],
config.get('fiat_display_currency')
)
@router.get('/stats', response_model=Stats, tags=['info'])
def stats(rpc: RPC = Depends(get_rpc)):
return rpc._rpc_stats()
@router.get('/daily', response_model=Daily, tags=['info'])
def daily(timescale: int = 7, rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
return rpc._rpc_daily_profit(timescale, config['stake_currency'],
config.get('fiat_display_currency', ''))
@router.get('/status', response_model=List[OpenTradeSchema], tags=['info'])
def status(rpc: RPC = Depends(get_rpc)):
try:
return rpc._rpc_trade_status()
except RPCException:
return []
@router.get('/trades', response_model=TradeResponse, tags=['info', 'trading'])
def trades(limit: int = 0, rpc: RPC = Depends(get_rpc)):
return rpc._rpc_trade_history(limit)
@router.delete('/trades/{tradeid}', response_model=DeleteTrade, tags=['info', 'trading'])
def trades_delete(tradeid: int, rpc: RPC = Depends(get_rpc)):
return rpc._rpc_delete(tradeid)
# TODO: Missing response model
@router.get('/edge', tags=['info'])
def edge(rpc: RPC = Depends(get_rpc)):
return rpc._rpc_edge()
@router.get('/show_config', response_model=ShowConfig, tags=['info'])
def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(get_config)):
state = ''
if rpc:
state = rpc._freqtrade.state
return RPC._rpc_show_config(config, state)
@router.post('/forcebuy', response_model=ForceBuyResponse, tags=['trading'])
def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)):
trade = rpc._rpc_forcebuy(payload.pair, payload.price)
if trade:
return trade.to_json()
else:
return {"status": f"Error buying pair {payload.pair}."}
@router.post('/forcesell', response_model=ResultMsg, tags=['trading'])
def forcesell(payload: ForceSellPayload, rpc: RPC = Depends(get_rpc)):
return rpc._rpc_forcesell(payload.tradeid)
@router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist'])
def blacklist(rpc: RPC = Depends(get_rpc)):
return rpc._rpc_blacklist()
@router.post('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist'])
def blacklist_post(payload: BlacklistPayload, rpc: RPC = Depends(get_rpc)):
return rpc._rpc_blacklist(payload.blacklist)
@router.get('/whitelist', response_model=WhitelistResponse, tags=['info', 'pairlist'])
def whitelist(rpc: RPC = Depends(get_rpc)):
return rpc._rpc_whitelist()
@router.get('/locks', response_model=Locks, tags=['info'])
def locks(rpc: RPC = Depends(get_rpc)):
return rpc._rpc_locks()
@router.get('/logs', response_model=Logs, tags=['info'])
def logs(limit: Optional[int] = None, rpc: RPC = Depends(get_rpc)):
return rpc._rpc_get_logs(limit)
@router.post('/start', response_model=StatusMsg, tags=['botcontrol'])
def start(rpc: RPC = Depends(get_rpc)):
return rpc._rpc_start()
@router.post('/stop', response_model=StatusMsg, tags=['botcontrol'])
def stop(rpc: RPC = Depends(get_rpc)):
return rpc._rpc_stop()
@router.post('/stopbuy', response_model=StatusMsg, tags=['botcontrol'])
def stop_buy(rpc: RPC = Depends(get_rpc)):
return rpc._rpc_stopbuy()
@router.post('/reload_config', response_model=StatusMsg, tags=['botcontrol'])
def reload_config(rpc: RPC = Depends(get_rpc)):
return rpc._rpc_reload_config()
@router.get('/pair_candles', response_model=PairHistory, tags=['candle data'])
def pair_candles(pair: str, timeframe: str, limit: Optional[int], rpc=Depends(get_rpc)):
return rpc._rpc_analysed_dataframe(pair, timeframe, limit)
@router.get('/pair_history', response_model=PairHistory, tags=['candle data'])
def pair_history(pair: str, timeframe: str, timerange: str, strategy: str,
config=Depends(get_config)):
config = deepcopy(config)
config.update({
'strategy': strategy,
})
return RPC._rpc_analysed_history_full(config, pair, timeframe, timerange)
@router.get('/plot_config', response_model=PlotConfig, tags=['candle data'])
def plot_config(rpc: RPC = Depends(get_rpc)):
return rpc._rpc_plot_config()
@router.get('/strategies', response_model=StrategyListResponse, tags=['strategy'])
def list_strategies(config=Depends(get_config)):
directory = Path(config.get(
'strategy_path', config['user_data_dir'] / USERPATH_STRATEGIES))
from freqtrade.resolvers.strategy_resolver import StrategyResolver
strategies = StrategyResolver.search_all_objects(directory, False)
strategies = sorted(strategies, key=lambda x: x['name'])
return {'strategies': [x['name'] for x in strategies]}
@router.get('/strategy/{strategy}', response_model=StrategyResponse, tags=['strategy'])
def get_strategy(strategy: str, config=Depends(get_config)):
config = deepcopy(config)
from freqtrade.resolvers.strategy_resolver import StrategyResolver
try:
strategy_obj = StrategyResolver._load_strategy(strategy, config,
extra_dir=config.get('strategy_path'))
except OperationalException:
raise HTTPException(status_code=404, detail='Strategy not found')
return {
'strategy': strategy_obj.get_strategy_name(),
'code': strategy_obj.__source__,
}
@router.get('/available_pairs', response_model=AvailablePairs, tags=['candle data'])
def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Optional[str] = None,
config=Depends(get_config)):
dh = get_datahandler(config['datadir'], config.get('dataformat_ohlcv', None))
pair_interval = dh.ohlcv_get_available_data(config['datadir'])
if timeframe:
pair_interval = [pair for pair in pair_interval if pair[1] == timeframe]
if stake_currency:
pair_interval = [pair for pair in pair_interval if pair[0].endswith(stake_currency)]
pair_interval = sorted(pair_interval, key=lambda x: x[0])
pairs = list({x[0] for x in pair_interval})
result = {
'length': len(pairs),
'pairs': pairs,
'pair_interval': pair_interval,
}
return result

View File

@ -0,0 +1,27 @@
from typing import Any, Dict, Optional
from freqtrade.rpc.rpc import RPC, RPCException
from .webserver import ApiServer
def get_rpc_optional() -> Optional[RPC]:
if ApiServer._has_rpc:
return ApiServer._rpc
return None
def get_rpc() -> Optional[RPC]:
_rpc = get_rpc_optional()
if _rpc:
return _rpc
else:
raise RPCException('Bot is not in the correct state')
def get_config() -> Dict[str, Any]:
return ApiServer._config
def get_api_config() -> Dict[str, Any]:
return ApiServer._config['api_server']

View File

@ -0,0 +1,27 @@
import contextlib
import threading
import time
import uvicorn
class UvicornServer(uvicorn.Server):
"""
Multithreaded server - as found in https://github.com/encode/uvicorn/issues/742
"""
def install_signal_handlers(self):
"""
In the parent implementation, this starts the thread, therefore we must patch it away here.
"""
pass
@contextlib.contextmanager
def run_in_thread(self):
self.thread = threading.Thread(target=self.run)
self.thread.start()
while not self.started:
time.sleep(1e-3)
def cleanup(self):
self.should_exit = True
self.thread.join()

View File

@ -0,0 +1,115 @@
import logging
from ipaddress import IPv4Address
from typing import Any, Dict
import uvicorn
from fastapi import Depends, FastAPI
from fastapi.middleware.cors import CORSMiddleware
from starlette.responses import JSONResponse
from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer
from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler
logger = logging.getLogger(__name__)
class ApiServer(RPCHandler):
_rpc: RPC
_has_rpc: bool = False
_config: Dict[str, Any] = {}
def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None:
super().__init__(rpc, config)
self._server = None
ApiServer._rpc = rpc
ApiServer._has_rpc = True
ApiServer._config = config
api_config = self._config['api_server']
self.app = FastAPI(title="Freqtrade API",
docs_url='/docs' if api_config.get('enable_openapi', False) else None,
redoc_url=None,
)
self.configure_app(self.app, self._config)
self.start_api()
def cleanup(self) -> None:
""" Cleanup pending module resources """
if self._server:
logger.info("Stopping API Server")
self._server.cleanup()
def send_msg(self, msg: Dict[str, str]) -> None:
pass
def handle_rpc_exception(self, request, exc):
logger.exception(f"API Error calling: {exc}")
return JSONResponse(
status_code=502,
content={'error': f"Error querying {request.url.path}: {exc.message}"}
)
def configure_app(self, app: FastAPI, config):
from freqtrade.rpc.api_server.api_auth import http_basic_or_jwt_token, router_login
from freqtrade.rpc.api_server.api_v1 import router as api_v1
from freqtrade.rpc.api_server.api_v1 import router_public as api_v1_public
app.include_router(api_v1_public, prefix="/api/v1")
app.include_router(api_v1, prefix="/api/v1",
dependencies=[Depends(http_basic_or_jwt_token)],
)
app.include_router(router_login, prefix="/api/v1", tags=["auth"])
app.add_middleware(
CORSMiddleware,
allow_origins=config['api_server'].get('CORS_origins', []),
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.add_exception_handler(RPCException, self.handle_rpc_exception)
def start_api(self):
"""
Start API ... should be run in thread.
"""
rest_ip = self._config['api_server']['listen_ip_address']
rest_port = self._config['api_server']['listen_port']
logger.info(f'Starting HTTP Server at {rest_ip}:{rest_port}')
if not IPv4Address(rest_ip).is_loopback:
logger.warning("SECURITY WARNING - Local Rest Server listening to external connections")
logger.warning("SECURITY WARNING - This is insecure please set to your loopback,"
"e.g 127.0.0.1 in config.json")
if not self._config['api_server'].get('password'):
logger.warning("SECURITY WARNING - No password for local REST Server defined. "
"Please make sure that this is intentional!")
if (self._config['api_server'].get('jwt_secret_key', 'super-secret')
in ('super-secret, somethingrandom')):
logger.warning("SECURITY WARNING - `jwt_secret_key` seems to be default."
"Others may be able to log into your bot.")
logger.info('Starting Local Rest Server.')
verbosity = self._config['api_server'].get('verbosity', 'error')
log_config = uvicorn.config.LOGGING_CONFIG
# Change logging of access logs to stderr
log_config["handlers"]["access"]["stream"] = log_config["handlers"]["default"]["stream"]
uvconfig = uvicorn.Config(self.app,
port=rest_port,
host=rest_ip,
use_colors=False,
log_config=log_config,
access_log=True if verbosity != 'error' else False,
)
try:
self._server = UvicornServer(uvconfig)
self._server.run_in_thread()
except Exception:
logger.exception("Api server failed to start.")

View File

@ -20,6 +20,7 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
from freqtrade.loggers import bufferHandler from freqtrade.loggers import bufferHandler
from freqtrade.misc import shorten_date from freqtrade.misc import shorten_date
from freqtrade.persistence import PairLocks, Trade from freqtrade.persistence import PairLocks, Trade
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
from freqtrade.state import State from freqtrade.state import State
from freqtrade.strategy.interface import SellType from freqtrade.strategy.interface import SellType
@ -110,7 +111,7 @@ class RPC:
self._fiat_converter = CryptoToFiatConverter() self._fiat_converter = CryptoToFiatConverter()
@staticmethod @staticmethod
def _rpc_show_config(config, botstate: State) -> Dict[str, Any]: def _rpc_show_config(config, botstate: Union[State, str]) -> Dict[str, Any]:
""" """
Return a dict of config options. Return a dict of config options.
Explicitly does NOT return the full config to avoid leakage of sensitive Explicitly does NOT return the full config to avoid leakage of sensitive
@ -120,13 +121,15 @@ class RPC:
'dry_run': config['dry_run'], 'dry_run': config['dry_run'],
'stake_currency': config['stake_currency'], 'stake_currency': config['stake_currency'],
'stake_amount': config['stake_amount'], 'stake_amount': config['stake_amount'],
'max_open_trades': config['max_open_trades'], 'max_open_trades': (config['max_open_trades']
if config['max_open_trades'] != float('inf') else -1),
'minimal_roi': config['minimal_roi'].copy() if 'minimal_roi' in config else {}, 'minimal_roi': config['minimal_roi'].copy() if 'minimal_roi' in config else {},
'stoploss': config.get('stoploss'), 'stoploss': config.get('stoploss'),
'trailing_stop': config.get('trailing_stop'), 'trailing_stop': config.get('trailing_stop'),
'trailing_stop_positive': config.get('trailing_stop_positive'), 'trailing_stop_positive': config.get('trailing_stop_positive'),
'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'), 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'),
'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'), 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'),
'bot_name': config.get('bot_name', 'freqtrade'),
'timeframe': config.get('timeframe'), 'timeframe': config.get('timeframe'),
'timeframe_ms': timeframe_to_msecs(config['timeframe'] 'timeframe_ms': timeframe_to_msecs(config['timeframe']
) if 'timeframe' in config else '', ) if 'timeframe' in config else '',
@ -142,13 +145,17 @@ class RPC:
} }
return val return val
def _rpc_trade_status(self) -> List[Dict[str, Any]]: def _rpc_trade_status(self, trade_ids: List[int] = []) -> List[Dict[str, Any]]:
""" """
Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is
a remotely exposed function a remotely exposed function
""" """
# Fetch open trade # Fetch open trades
if trade_ids:
trades = Trade.get_trades(trade_filter=Trade.id.in_(trade_ids)).all()
else:
trades = Trade.get_open_trades() trades = Trade.get_open_trades()
if not trades: if not trades:
raise RPCException('no active trade') raise RPCException('no active trade')
else: else:
@ -648,7 +655,8 @@ class RPC:
trades = Trade.get_open_trades() trades = Trade.get_open_trades()
return { return {
'current': len(trades), 'current': len(trades),
'max': float(self._freqtrade.config['max_open_trades']), 'max': (int(self._freqtrade.config['max_open_trades'])
if self._freqtrade.config['max_open_trades'] != float('inf') else -1),
'total_stake': sum((trade.open_rate * trade.amount) for trade in trades) 'total_stake': sum((trade.open_rate * trade.amount) for trade in trades)
} }
@ -673,23 +681,23 @@ class RPC:
""" Returns the currently active blacklist""" """ Returns the currently active blacklist"""
errors = {} errors = {}
if add: if add:
stake_currency = self._freqtrade.config.get('stake_currency')
for pair in add: for pair in add:
if self._freqtrade.exchange.get_pair_quote_currency(pair) == stake_currency:
if pair not in self._freqtrade.pairlists.blacklist: if pair not in self._freqtrade.pairlists.blacklist:
try:
expand_pairlist([pair], self._freqtrade.exchange.get_markets().keys())
self._freqtrade.pairlists.blacklist.append(pair) self._freqtrade.pairlists.blacklist.append(pair)
except ValueError:
errors[pair] = {
'error_msg': f'Pair {pair} is not a valid wildcard.'}
else: else:
errors[pair] = { errors[pair] = {
'error_msg': f'Pair {pair} already in pairlist.'} 'error_msg': f'Pair {pair} already in pairlist.'}
else:
errors[pair] = {
'error_msg': f"Pair {pair} does not match stake currency."
}
res = {'method': self._freqtrade.pairlists.name_list, res = {'method': self._freqtrade.pairlists.name_list,
'length': len(self._freqtrade.pairlists.blacklist), 'length': len(self._freqtrade.pairlists.blacklist),
'blacklist': self._freqtrade.pairlists.blacklist, 'blacklist': self._freqtrade.pairlists.blacklist,
'blacklist_expanded': self._freqtrade.pairlists.expanded_blacklist,
'errors': errors, 'errors': errors,
} }
return res return res
@ -787,6 +795,8 @@ class RPC:
timerange=timerange_parsed, timerange=timerange_parsed,
data_format=config.get('dataformat_ohlcv', 'json'), data_format=config.get('dataformat_ohlcv', 'json'),
) )
if pair not in _data:
raise RPCException(f"No data for {pair}, {timeframe} in {timerange} found.")
from freqtrade.resolvers.strategy_resolver import StrategyResolver from freqtrade.resolvers.strategy_resolver import StrategyResolver
strategy = StrategyResolver.load_strategy(config) strategy = StrategyResolver.load_strategy(config)
df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair}) df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair})

View File

@ -35,6 +35,7 @@ class RPCManager:
if config.get('api_server', {}).get('enabled', False): if config.get('api_server', {}).get('enabled', False):
logger.info('Enabling rpc.api_server') logger.info('Enabling rpc.api_server')
from freqtrade.rpc.api_server import ApiServer from freqtrade.rpc.api_server import ApiServer
self.registered_modules.append(ApiServer(self._rpc, config)) self.registered_modules.append(ApiServer(self._rpc, config))
def cleanup(self) -> None: def cleanup(self) -> None:

View File

@ -277,7 +277,14 @@ class Telegram(RPCHandler):
return return
try: try:
results = self._rpc._rpc_trade_status()
# Check if there's at least one numerical ID provided.
# If so, try to get only these trades.
trade_ids = []
if context.args and len(context.args) > 0:
trade_ids = [int(i) for i in context.args if i.isnumeric()]
results = self._rpc._rpc_trade_status(trade_ids=trade_ids)
messages = [] messages = []
for r in results: for r in results:
@ -815,7 +822,9 @@ class Telegram(RPCHandler):
"Optionally takes a rate at which to buy.` \n") "Optionally takes a rate at which to buy.` \n")
message = ("*/start:* `Starts the trader`\n" message = ("*/start:* `Starts the trader`\n"
"*/stop:* `Stops the trader`\n" "*/stop:* `Stops the trader`\n"
"*/status [table]:* `Lists all open trades`\n" "*/status <trade_id>|[table]:* `Lists all open trades`\n"
" *<trade_id> :* `Lists one or more specific trades.`\n"
" `Separate multiple <trade_id> with a blank space.`\n"
" *table :* `will display trades in a table`\n" " *table :* `will display trades in a table`\n"
" `pending buy orders are marked with an asterisk (*)`\n" " `pending buy orders are marked with an asterisk (*)`\n"
" `pending sell orders are marked with a double asterisk (**)`\n" " `pending sell orders are marked with a double asterisk (**)`\n"

View File

@ -5,7 +5,7 @@ This module defines the interface to apply for strategies
import logging import logging
import warnings import warnings
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from datetime import datetime, timezone from datetime import datetime, timedelta, timezone
from enum import Enum from enum import Enum
from typing import Dict, List, NamedTuple, Optional, Tuple from typing import Dict, List, NamedTuple, Optional, Tuple
@ -15,7 +15,7 @@ from pandas import DataFrame
from freqtrade.constants import ListPairsWithTimeframes from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exceptions import OperationalException, StrategyError
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.exchange.exchange import timeframe_to_next_date
from freqtrade.persistence import PairLocks, Trade from freqtrade.persistence import PairLocks, Trade
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
@ -89,6 +89,7 @@ class IStrategy(ABC):
trailing_stop_positive: Optional[float] = None trailing_stop_positive: Optional[float] = None
trailing_stop_positive_offset: float = 0.0 trailing_stop_positive_offset: float = 0.0
trailing_only_offset_is_reached = False trailing_only_offset_is_reached = False
use_custom_stoploss: bool = False
# associated timeframe # associated timeframe
ticker_interval: str # DEPRECATED ticker_interval: str # DEPRECATED
@ -112,12 +113,18 @@ class IStrategy(ABC):
# run "populate_indicators" only for new candle # run "populate_indicators" only for new candle
process_only_new_candles: bool = False process_only_new_candles: bool = False
# Number of seconds after which the candle will no longer result in a buy on expired candles
ignore_buying_expired_candle_after: int = 0
# Disable checking the dataframe (converts the error into a warning message) # Disable checking the dataframe (converts the error into a warning message)
disable_dataframe_checks: bool = False disable_dataframe_checks: bool = False
# Count of candles the strategy requires before producing valid signals # Count of candles the strategy requires before producing valid signals
startup_candle_count: int = 0 startup_candle_count: int = 0
# Protections
protections: List
# Class level variables (intentional) containing # Class level variables (intentional) containing
# the dataprovider (dp) (access to other candles, historic data, ...) # the dataprovider (dp) (access to other candles, historic data, ...)
# and wallets - access to the current balance. # and wallets - access to the current balance.
@ -254,6 +261,28 @@ class IStrategy(ABC):
""" """
return True return True
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
current_profit: float, **kwargs) -> float:
"""
Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
e.g. returning -0.05 would create a stoploss 5% below current_rate.
The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, returns the initial stoploss value
Only called when use_custom_stoploss is set to True.
:param pair: Pair that's currently analyzed
:param trade: trade object.
:param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
:param current_profit: Current profit (as ratio), calculated based on current_rate.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New stoploss value, relative to the currentrate
"""
return self.stoploss
def informative_pairs(self) -> ListPairsWithTimeframes: def informative_pairs(self) -> ListPairsWithTimeframes:
""" """
Define additional, informative pair/interval combinations to be cached from the exchange. Define additional, informative pair/interval combinations to be cached from the exchange.
@ -453,8 +482,22 @@ class IStrategy(ABC):
(buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1 (buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1
logger.debug('trigger: %s (pair=%s) buy=%s sell=%s', logger.debug('trigger: %s (pair=%s) buy=%s sell=%s',
latest['date'], pair, str(buy), str(sell)) latest['date'], pair, str(buy), str(sell))
timeframe_seconds = timeframe_to_seconds(timeframe)
if self.ignore_expired_candle(latest_date=latest_date,
current_time=datetime.now(timezone.utc),
timeframe_seconds=timeframe_seconds,
buy=buy):
return False, sell
return buy, sell return buy, sell
def ignore_expired_candle(self, latest_date: datetime, current_time: datetime,
timeframe_seconds: int, buy: bool):
if self.ignore_buying_expired_candle_after and buy:
time_delta = current_time - (latest_date + timedelta(seconds=timeframe_seconds))
return time_delta.total_seconds() > self.ignore_buying_expired_candle_after
else:
return False
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool,
sell: bool, low: float = None, high: float = None, sell: bool, low: float = None, high: float = None,
force_stoploss: float = 0) -> SellCheckTuple: force_stoploss: float = 0) -> SellCheckTuple:
@ -479,18 +522,19 @@ class IStrategy(ABC):
# Set current rate to high for backtesting sell # Set current rate to high for backtesting sell
current_rate = high or rate current_rate = high or rate
current_profit = trade.calc_profit_ratio(current_rate) current_profit = trade.calc_profit_ratio(current_rate)
config_ask_strategy = self.config.get('ask_strategy', {}) ask_strategy = self.config.get('ask_strategy', {})
# if buy signal and ignore_roi is set, we don't need to evaluate min_roi. # if buy signal and ignore_roi is set, we don't need to evaluate min_roi.
roi_reached = (not (buy and config_ask_strategy.get('ignore_roi_if_buy_signal', False)) roi_reached = (not (buy and ask_strategy.get('ignore_roi_if_buy_signal', False))
and self.min_roi_reached(trade=trade, current_profit=current_profit, and self.min_roi_reached(trade=trade, current_profit=current_profit,
current_time=date)) current_time=date))
if config_ask_strategy.get('sell_profit_only', False) and trade.calc_profit(rate=rate) <= 0: if (ask_strategy.get('sell_profit_only', False)
and trade.calc_profit(rate=rate) <= ask_strategy.get('sell_profit_offset', 0)):
# Negative profits and sell_profit_only - ignore sell signal # Negative profits and sell_profit_only - ignore sell signal
sell_signal = False sell_signal = False
else: else:
sell_signal = sell and not buy and config_ask_strategy.get('use_sell_signal', True) sell_signal = sell and not buy and ask_strategy.get('use_sell_signal', True)
# TODO: return here if sell-signal should be favored over ROI # TODO: return here if sell-signal should be favored over ROI
# Start evaluations # Start evaluations
@ -531,6 +575,19 @@ class IStrategy(ABC):
# Initiate stoploss with open_rate. Does nothing if stoploss is already set. # Initiate stoploss with open_rate. Does nothing if stoploss is already set.
trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True) trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True)
if self.use_custom_stoploss:
stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None
)(pair=trade.pair, trade=trade,
current_time=current_time,
current_rate=current_rate,
current_profit=current_profit)
# Sanity check - error cases will return None
if stop_loss_value:
# logger.info(f"{trade.pair} {stop_loss_value=} {current_profit=}")
trade.adjust_stop_loss(current_rate, stop_loss_value)
else:
logger.warning("CustomStoploss function did not return valid stoploss")
if self.trailing_stop: if self.trailing_stop:
# trailing stoploss handling # trailing stoploss handling
sl_offset = self.trailing_stop_positive_offset sl_offset = self.trailing_stop_positive_offset
@ -636,6 +693,7 @@ class IStrategy(ABC):
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
logger.debug(f"Populating buy signals for pair {metadata.get('pair')}.") logger.debug(f"Populating buy signals for pair {metadata.get('pair')}.")
if self._buy_fun_len == 2: if self._buy_fun_len == 2:
warnings.warn("deprecated - check out the Sample strategy to see " warnings.warn("deprecated - check out the Sample strategy to see "
"the current function headers!", DeprecationWarning) "the current function headers!", DeprecationWarning)

View File

@ -24,15 +24,24 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
:param timeframe: Timeframe of the original pair sample. :param timeframe: Timeframe of the original pair sample.
:param timeframe_inf: Timeframe of the informative pair sample. :param timeframe_inf: Timeframe of the informative pair sample.
:param ffill: Forwardfill missing values - optional but usually required :param ffill: Forwardfill missing values - optional but usually required
:return: Merged dataframe
:raise: ValueError if the secondary timeframe is shorter than the dataframe timeframe
""" """
minutes_inf = timeframe_to_minutes(timeframe_inf) minutes_inf = timeframe_to_minutes(timeframe_inf)
minutes = timeframe_to_minutes(timeframe) minutes = timeframe_to_minutes(timeframe)
if minutes >= minutes_inf: if minutes == minutes_inf:
# No need to forwardshift if the timeframes are identical # No need to forwardshift if the timeframes are identical
informative['date_merge'] = informative["date"] informative['date_merge'] = informative["date"]
elif minutes < minutes_inf:
# Subtract "small" timeframe so merging is not delayed by 1 small candle
# Detailed explanation in https://github.com/freqtrade/freqtrade/issues/4073
informative['date_merge'] = (
informative["date"] + pd.to_timedelta(minutes_inf, 'm') - pd.to_timedelta(minutes, 'm')
)
else: else:
informative['date_merge'] = informative["date"] + pd.to_timedelta(minutes_inf, 'm') raise ValueError("Tried to merge a faster timeframe to a slower timeframe."
"This would create new rows, and can throw off your regular indicators.")
# Rename columns to be unique # Rename columns to be unique
informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns] informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns]

View File

@ -63,6 +63,7 @@
"username": "", "username": "",
"password": "" "password": ""
}, },
"bot_name": "freqtrade",
"initial_state": "running", "initial_state": "running",
"forcebuy_enable": false, "forcebuy_enable": false,
"internals": { "internals": {

View File

@ -12,6 +12,30 @@ def bot_loop_start(self, **kwargs) -> None:
""" """
pass pass
use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
current_profit: float, **kwargs) -> float:
"""
Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
e.g. returning -0.05 would create a stoploss 5% below current_rate.
The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, returns the initial stoploss value
Only called when use_custom_stoploss is set to True.
:param pair: Pair that's about to be sold.
:param trade: trade object.
:param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
:param current_profit: Current profit (as ratio), calculated based on current_rate.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New stoploss value, relative to the currentrate
"""
return self.stoploss
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, **kwargs) -> bool: time_in_force: str, **kwargs) -> bool:
""" """
@ -45,7 +69,7 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount:
When not implemented by a strategy, returns True (always confirming). When not implemented by a strategy, returns True (always confirming).
:param pair: Pair that's about to be sold. :param pair: Pair that's currently analyzed
:param trade: trade object. :param trade: trade object.
:param order_type: Order type (as configured in order_types). usually limit or market. :param order_type: Order type (as configured in order_types). usually limit or market.
:param amount: Amount in quote currency. :param amount: Amount in quote currency.

View File

@ -3,7 +3,6 @@ nav:
- Home: index.md - Home: index.md
- Quickstart with Docker: docker_quickstart.md - Quickstart with Docker: docker_quickstart.md
- Installation: - Installation:
- Docker without docker-compose: docker.md
- Linux/MacOS/Raspberry: installation.md - Linux/MacOS/Raspberry: installation.md
- Windows: windows_installation.md - Windows: windows_installation.md
- Freqtrade Basics: bot-basics.md - Freqtrade Basics: bot-basics.md

View File

@ -3,17 +3,17 @@
-r requirements-plot.txt -r requirements-plot.txt
-r requirements-hyperopt.txt -r requirements-hyperopt.txt
coveralls==2.2.0 coveralls==3.0.0
flake8==3.8.4 flake8==3.8.4
flake8-type-annotations==0.1.0 flake8-type-annotations==0.1.0
flake8-tidy-imports==4.2.1 flake8-tidy-imports==4.2.1
mypy==0.790 mypy==0.790
pytest==6.2.1 pytest==6.2.1
pytest-asyncio==0.14.0 pytest-asyncio==0.14.0
pytest-cov==2.10.1 pytest-cov==2.11.1
pytest-mock==3.4.0 pytest-mock==3.5.1
pytest-random-order==1.0.4 pytest-random-order==1.0.4
isort==5.6.4 isort==5.7.0
# Convert jupyter notebooks to markdown documents # Convert jupyter notebooks to markdown documents
nbconvert==6.0.7 nbconvert==6.0.7

View File

@ -2,8 +2,8 @@
-r requirements.txt -r requirements.txt
# Required for hyperopt # Required for hyperopt
scipy==1.5.4 scipy==1.6.0
scikit-learn==0.23.2 scikit-learn==0.24.1
scikit-optimize==0.8.1 scikit-optimize==0.8.1
filelock==3.0.12 filelock==3.0.12
joblib==1.0.0 joblib==1.0.0

View File

@ -1,5 +1,5 @@
# Include all requirements to run the bot. # Include all requirements to run the bot.
-r requirements.txt -r requirements.txt
plotly==4.14.1 plotly==4.14.3

View File

@ -1,12 +1,12 @@
numpy==1.19.4 numpy==1.19.5
pandas==1.1.5 pandas==1.2.1
ccxt==1.39.79 ccxt==1.40.99
aiohttp==3.7.3 aiohttp==3.7.3
SQLAlchemy==1.3.22 SQLAlchemy==1.3.22
python-telegram-bot==13.1 python-telegram-bot==13.1
arrow==0.17.0 arrow==0.17.0
cachetools==4.2.0 cachetools==4.2.1
requests==2.25.1 requests==2.25.1
urllib3==1.26.2 urllib3==1.26.2
wrapt==1.12.1 wrapt==1.12.1
@ -16,7 +16,7 @@ tabulate==0.8.7
pycoingecko==1.4.0 pycoingecko==1.4.0
jinja2==2.11.2 jinja2==2.11.2
tables==3.6.1 tables==3.6.1
blosc==1.10.1 blosc==1.10.2
# find first, C search in arrays # find first, C search in arrays
py_find_1st==1.1.4 py_find_1st==1.1.4
@ -27,13 +27,13 @@ python-rapidjson==1.0
# Notify systemd # Notify systemd
sdnotify==0.3.2 sdnotify==0.3.2
# Api server # API Server
flask==1.1.2 fastapi==0.63.0
flask-jwt-extended==3.25.0 uvicorn==0.13.3
flask-cors==3.0.9 pyjwt==2.0.1
# Support for colorized terminal output # Support for colorized terminal output
colorama==0.4.4 colorama==0.4.4
# Building config files interactively # Building config files interactively
questionary==1.9.0 questionary==1.9.0
prompt-toolkit==3.0.8 prompt-toolkit==3.0.14

View File

@ -202,52 +202,6 @@ function test_and_fix_python_on_mac() {
fi fi
} }
function config_generator() {
echo "Starting to generate config.json"
echo
echo "Generating General configuration"
echo "-------------------------"
default_max_trades=3
read -p "Max open trades: (Default: $default_max_trades) " max_trades
max_trades=${max_trades:-$default_max_trades}
default_stake_amount=0.05
read -p "Stake amount: (Default: $default_stake_amount) " stake_amount
stake_amount=${stake_amount:-$default_stake_amount}
default_stake_currency="BTC"
read -p "Stake currency: (Default: $default_stake_currency) " stake_currency
stake_currency=${stake_currency:-$default_stake_currency}
default_fiat_currency="USD"
read -p "Fiat currency: (Default: $default_fiat_currency) " fiat_currency
fiat_currency=${fiat_currency:-$default_fiat_currency}
echo
echo "Generating exchange config "
echo "------------------------"
read -p "Exchange API key: " api_key
read -p "Exchange API Secret: " api_secret
echo
echo "Generating Telegram config"
echo "-------------------------"
read -p "Telegram Token: " token
read -p "Telegram Chat_id: " chat_id
sed -e "s/\"max_open_trades\": 3,/\"max_open_trades\": $max_trades,/g" \
-e "s/\"stake_amount\": 0.05,/\"stake_amount\": $stake_amount,/g" \
-e "s/\"stake_currency\": \"BTC\",/\"stake_currency\": \"$stake_currency\",/g" \
-e "s/\"fiat_display_currency\": \"USD\",/\"fiat_display_currency\": \"$fiat_currency\",/g" \
-e "s/\"your_exchange_key\"/\"$api_key\"/g" \
-e "s/\"your_exchange_secret\"/\"$api_secret\"/g" \
-e "s/\"your_telegram_token\"/\"$token\"/g" \
-e "s/\"your_telegram_chat_id\"/\"$chat_id\"/g" \
-e "s/\"dry_run\": false,/\"dry_run\": true,/g" config.json.example > config.json
}
function config() { function config() {
echo "-------------------------" echo "-------------------------"

View File

@ -21,7 +21,7 @@ from tests.conftest_trades import MOCK_TRADE_COUNT
def test_setup_utils_configuration(): def test_setup_utils_configuration():
args = [ args = [
'list-exchanges', '--config', 'config.json.example', 'list-exchanges', '--config', 'config_bittrex.json.example',
] ]
config = setup_utils_configuration(get_args(args), RunMode.OTHER) config = setup_utils_configuration(get_args(args), RunMode.OTHER)
@ -40,7 +40,7 @@ def test_start_trading_fail(mocker, caplog):
exitmock = mocker.patch("freqtrade.worker.Worker.exit", MagicMock()) exitmock = mocker.patch("freqtrade.worker.Worker.exit", MagicMock())
args = [ args = [
'trade', 'trade',
'-c', 'config.json.example' '-c', 'config_bittrex.json.example'
] ]
start_trading(get_args(args)) start_trading(get_args(args))
assert exitmock.call_count == 1 assert exitmock.call_count == 1
@ -122,10 +122,10 @@ def test_list_timeframes(mocker, capsys):
match=r"This command requires a configured exchange.*"): match=r"This command requires a configured exchange.*"):
start_list_timeframes(pargs) start_list_timeframes(pargs)
# Test with --config config.json.example # Test with --config config_bittrex.json.example
args = [ args = [
"list-timeframes", "list-timeframes",
'--config', 'config.json.example', '--config', 'config_bittrex.json.example',
] ]
start_list_timeframes(get_args(args)) start_list_timeframes(get_args(args))
captured = capsys.readouterr() captured = capsys.readouterr()
@ -169,7 +169,7 @@ def test_list_timeframes(mocker, capsys):
# Test with --one-column # Test with --one-column
args = [ args = [
"list-timeframes", "list-timeframes",
'--config', 'config.json.example', '--config', 'config_bittrex.json.example',
"--one-column", "--one-column",
] ]
start_list_timeframes(get_args(args)) start_list_timeframes(get_args(args))
@ -209,10 +209,10 @@ def test_list_markets(mocker, markets, capsys):
match=r"This command requires a configured exchange.*"): match=r"This command requires a configured exchange.*"):
start_list_markets(pargs, False) start_list_markets(pargs, False)
# Test with --config config.json.example # Test with --config config_bittrex.json.example
args = [ args = [
"list-markets", "list-markets",
'--config', 'config.json.example', '--config', 'config_bittrex.json.example',
"--print-list", "--print-list",
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
@ -239,7 +239,7 @@ def test_list_markets(mocker, markets, capsys):
# Test with --all: all markets # Test with --all: all markets
args = [ args = [
"list-markets", "--all", "list-markets", "--all",
'--config', 'config.json.example', '--config', 'config_bittrex.json.example',
"--print-list", "--print-list",
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
@ -252,7 +252,7 @@ def test_list_markets(mocker, markets, capsys):
# Test list-pairs subcommand: active pairs # Test list-pairs subcommand: active pairs
args = [ args = [
"list-pairs", "list-pairs",
'--config', 'config.json.example', '--config', 'config_bittrex.json.example',
"--print-list", "--print-list",
] ]
start_list_markets(get_args(args), True) start_list_markets(get_args(args), True)
@ -264,7 +264,7 @@ def test_list_markets(mocker, markets, capsys):
# Test list-pairs subcommand with --all: all pairs # Test list-pairs subcommand with --all: all pairs
args = [ args = [
"list-pairs", "--all", "list-pairs", "--all",
'--config', 'config.json.example', '--config', 'config_bittrex.json.example',
"--print-list", "--print-list",
] ]
start_list_markets(get_args(args), True) start_list_markets(get_args(args), True)
@ -277,7 +277,7 @@ def test_list_markets(mocker, markets, capsys):
# active markets, base=ETH, LTC # active markets, base=ETH, LTC
args = [ args = [
"list-markets", "list-markets",
'--config', 'config.json.example', '--config', 'config_bittrex.json.example',
"--base", "ETH", "LTC", "--base", "ETH", "LTC",
"--print-list", "--print-list",
] ]
@ -290,7 +290,7 @@ def test_list_markets(mocker, markets, capsys):
# active markets, base=LTC # active markets, base=LTC
args = [ args = [
"list-markets", "list-markets",
'--config', 'config.json.example', '--config', 'config_bittrex.json.example',
"--base", "LTC", "--base", "LTC",
"--print-list", "--print-list",
] ]
@ -303,7 +303,7 @@ def test_list_markets(mocker, markets, capsys):
# active markets, quote=USDT, USD # active markets, quote=USDT, USD
args = [ args = [
"list-markets", "list-markets",
'--config', 'config.json.example', '--config', 'config_bittrex.json.example',
"--quote", "USDT", "USD", "--quote", "USDT", "USD",
"--print-list", "--print-list",
] ]
@ -316,7 +316,7 @@ def test_list_markets(mocker, markets, capsys):
# active markets, quote=USDT # active markets, quote=USDT
args = [ args = [
"list-markets", "list-markets",
'--config', 'config.json.example', '--config', 'config_bittrex.json.example',
"--quote", "USDT", "--quote", "USDT",
"--print-list", "--print-list",
] ]
@ -329,7 +329,7 @@ def test_list_markets(mocker, markets, capsys):
# active markets, base=LTC, quote=USDT # active markets, base=LTC, quote=USDT
args = [ args = [
"list-markets", "list-markets",
'--config', 'config.json.example', '--config', 'config_bittrex.json.example',
"--base", "LTC", "--quote", "USDT", "--base", "LTC", "--quote", "USDT",
"--print-list", "--print-list",
] ]
@ -342,7 +342,7 @@ def test_list_markets(mocker, markets, capsys):
# active pairs, base=LTC, quote=USDT # active pairs, base=LTC, quote=USDT
args = [ args = [
"list-pairs", "list-pairs",
'--config', 'config.json.example', '--config', 'config_bittrex.json.example',
"--base", "LTC", "--quote", "USD", "--base", "LTC", "--quote", "USD",
"--print-list", "--print-list",
] ]
@ -355,7 +355,7 @@ def test_list_markets(mocker, markets, capsys):
# active markets, base=LTC, quote=USDT, NONEXISTENT # active markets, base=LTC, quote=USDT, NONEXISTENT
args = [ args = [
"list-markets", "list-markets",
'--config', 'config.json.example', '--config', 'config_bittrex.json.example',
"--base", "LTC", "--quote", "USDT", "NONEXISTENT", "--base", "LTC", "--quote", "USDT", "NONEXISTENT",
"--print-list", "--print-list",
] ]
@ -368,7 +368,7 @@ def test_list_markets(mocker, markets, capsys):
# active markets, base=LTC, quote=NONEXISTENT # active markets, base=LTC, quote=NONEXISTENT
args = [ args = [
"list-markets", "list-markets",
'--config', 'config.json.example', '--config', 'config_bittrex.json.example',
"--base", "LTC", "--quote", "NONEXISTENT", "--base", "LTC", "--quote", "NONEXISTENT",
"--print-list", "--print-list",
] ]
@ -381,7 +381,7 @@ def test_list_markets(mocker, markets, capsys):
# Test tabular output # Test tabular output
args = [ args = [
"list-markets", "list-markets",
'--config', 'config.json.example', '--config', 'config_bittrex.json.example',
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
captured = capsys.readouterr() captured = capsys.readouterr()
@ -391,7 +391,7 @@ def test_list_markets(mocker, markets, capsys):
# Test tabular output, no markets found # Test tabular output, no markets found
args = [ args = [
"list-markets", "list-markets",
'--config', 'config.json.example', '--config', 'config_bittrex.json.example',
"--base", "LTC", "--quote", "NONEXISTENT", "--base", "LTC", "--quote", "NONEXISTENT",
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
@ -403,7 +403,7 @@ def test_list_markets(mocker, markets, capsys):
# Test --print-json # Test --print-json
args = [ args = [
"list-markets", "list-markets",
'--config', 'config.json.example', '--config', 'config_bittrex.json.example',
"--print-json" "--print-json"
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
@ -415,7 +415,7 @@ def test_list_markets(mocker, markets, capsys):
# Test --print-csv # Test --print-csv
args = [ args = [
"list-markets", "list-markets",
'--config', 'config.json.example', '--config', 'config_bittrex.json.example',
"--print-csv" "--print-csv"
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
@ -427,7 +427,7 @@ def test_list_markets(mocker, markets, capsys):
# Test --one-column # Test --one-column
args = [ args = [
"list-markets", "list-markets",
'--config', 'config.json.example', '--config', 'config_bittrex.json.example',
"--one-column" "--one-column"
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
@ -439,7 +439,7 @@ def test_list_markets(mocker, markets, capsys):
# Test --one-column # Test --one-column
args = [ args = [
"list-markets", "list-markets",
'--config', 'config.json.example', '--config', 'config_bittrex.json.example',
"--one-column" "--one-column"
] ]
with pytest.raises(OperationalException, match=r"Cannot get markets.*"): with pytest.raises(OperationalException, match=r"Cannot get markets.*"):
@ -781,7 +781,7 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
args = [ args = [
'test-pairlist', 'test-pairlist',
'-c', 'config.json.example' '-c', 'config_bittrex.json.example'
] ]
start_test_pairlist(get_args(args)) start_test_pairlist(get_args(args))
@ -795,7 +795,7 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
args = [ args = [
'test-pairlist', 'test-pairlist',
'-c', 'config.json.example', '-c', 'config_bittrex.json.example',
'--one-column', '--one-column',
] ]
start_test_pairlist(get_args(args)) start_test_pairlist(get_args(args))
@ -804,7 +804,7 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
args = [ args = [
'test-pairlist', 'test-pairlist',
'-c', 'config.json.example', '-c', 'config_bittrex.json.example',
'--print-json', '--print-json',
] ]
start_test_pairlist(get_args(args)) start_test_pairlist(get_args(args))

View File

@ -1492,11 +1492,11 @@ def trades_for_order():
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def trades_history(): def trades_history():
return [[1565798389463, '126181329', None, 'buy', 0.019627, 0.04, 0.00078508], return [[1565798389463, '12618132aa9', None, 'buy', 0.019627, 0.04, 0.00078508],
[1565798399629, '126181330', None, 'buy', 0.019627, 0.244, 0.004788987999999999], [1565798399629, '1261813bb30', None, 'buy', 0.019627, 0.244, 0.004788987999999999],
[1565798399752, '126181331', None, 'sell', 0.019626, 0.011, 0.00021588599999999999], [1565798399752, '1261813cc31', None, 'sell', 0.019626, 0.011, 0.00021588599999999999],
[1565798399862, '126181332', None, 'sell', 0.019626, 0.011, 0.00021588599999999999], [1565798399862, '126181cc332', None, 'sell', 0.019626, 0.011, 0.00021588599999999999],
[1565798399872, '126181333', None, 'sell', 0.019626, 0.011, 0.00021588599999999999]] [1565798399872, '1261aa81333', None, 'sell', 0.019626, 0.011, 0.00021588599999999999]]
@pytest.fixture(scope="function") @pytest.fixture(scope="function")

View File

@ -32,6 +32,7 @@ def mock_trade_1(fee):
exchange='bittrex', exchange='bittrex',
open_order_id='dry_run_buy_12345', open_order_id='dry_run_buy_12345',
strategy='DefaultStrategy', strategy='DefaultStrategy',
timeframe=5,
) )
o = Order.parse_from_ccxt_object(mock_order_1(), 'ETH/BTC', 'buy') o = Order.parse_from_ccxt_object(mock_order_1(), 'ETH/BTC', 'buy')
trade.orders.append(o) trade.orders.append(o)
@ -84,6 +85,7 @@ def mock_trade_2(fee):
is_open=False, is_open=False,
open_order_id='dry_run_sell_12345', open_order_id='dry_run_sell_12345',
strategy='DefaultStrategy', strategy='DefaultStrategy',
timeframe=5,
sell_reason='sell_signal', sell_reason='sell_signal',
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc), close_date=datetime.now(tz=timezone.utc),
@ -132,6 +134,7 @@ def mock_trade_3(fee):
pair='XRP/BTC', pair='XRP/BTC',
stake_amount=0.001, stake_amount=0.001,
amount=123.0, amount=123.0,
amount_requested=123.0,
fee_open=fee.return_value, fee_open=fee.return_value,
fee_close=fee.return_value, fee_close=fee.return_value,
open_rate=0.05, open_rate=0.05,
@ -139,6 +142,8 @@ def mock_trade_3(fee):
close_profit=0.01, close_profit=0.01,
exchange='bittrex', exchange='bittrex',
is_open=False, is_open=False,
strategy='DefaultStrategy',
timeframe=5,
sell_reason='roi', sell_reason='roi',
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc), close_date=datetime.now(tz=timezone.utc),
@ -179,6 +184,7 @@ def mock_trade_4(fee):
exchange='bittrex', exchange='bittrex',
open_order_id='prod_buy_12345', open_order_id='prod_buy_12345',
strategy='DefaultStrategy', strategy='DefaultStrategy',
timeframe=5,
) )
o = Order.parse_from_ccxt_object(mock_order_4(), 'ETC/BTC', 'buy') o = Order.parse_from_ccxt_object(mock_order_4(), 'ETC/BTC', 'buy')
trade.orders.append(o) trade.orders.append(o)

View File

@ -128,7 +128,7 @@ def test_load_trades_from_db(default_conf, fee, mocker):
if col not in ['index', 'open_at_end']: if col not in ['index', 'open_at_end']:
assert col in trades.columns assert col in trades.columns
trades = load_trades_from_db(db_url=default_conf['db_url'], strategy='DefaultStrategy') trades = load_trades_from_db(db_url=default_conf['db_url'], strategy='DefaultStrategy')
assert len(trades) == 3 assert len(trades) == 4
trades = load_trades_from_db(db_url=default_conf['db_url'], strategy='NoneStrategy') trades = load_trades_from_db(db_url=default_conf['db_url'], strategy='NoneStrategy')
assert len(trades) == 0 assert len(trades) == 0

View File

@ -508,7 +508,7 @@ def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs d
def test_validate_pairs_not_available(default_conf, mocker): def test_validate_pairs_not_available(default_conf, mocker):
api_mock = MagicMock() api_mock = MagicMock()
type(api_mock).markets = PropertyMock(return_value={ type(api_mock).markets = PropertyMock(return_value={
'XRP/BTC': {'inactive': True} 'XRP/BTC': {'inactive': True, 'base': 'XRP', 'quote': 'BTC'}
}) })
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
@ -1718,8 +1718,8 @@ async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name,
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
async def test__async_get_trade_history_id(default_conf, mocker, caplog, exchange_name, async def test__async_get_trade_history_id(default_conf, mocker, exchange_name,
trades_history): fetch_trades_result):
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
pagination_arg = exchange._trades_pagination_arg pagination_arg = exchange._trades_pagination_arg
@ -1727,28 +1727,29 @@ async def test__async_get_trade_history_id(default_conf, mocker, caplog, exchang
async def mock_get_trade_hist(pair, *args, **kwargs): async def mock_get_trade_hist(pair, *args, **kwargs):
if 'since' in kwargs: if 'since' in kwargs:
# Return first 3 # Return first 3
return trades_history[:-2] return fetch_trades_result[:-2]
elif kwargs.get('params', {}).get(pagination_arg) == trades_history[-3][1]: elif kwargs.get('params', {}).get(pagination_arg) == fetch_trades_result[-3]['id']:
# Return 2 # Return 2
return trades_history[-3:-1] return fetch_trades_result[-3:-1]
else: else:
# Return last 2 # Return last 2
return trades_history[-2:] return fetch_trades_result[-2:]
# Monkey-patch async function # Monkey-patch async function
exchange._async_fetch_trades = MagicMock(side_effect=mock_get_trade_hist) exchange._api_async.fetch_trades = MagicMock(side_effect=mock_get_trade_hist)
pair = 'ETH/BTC' pair = 'ETH/BTC'
ret = await exchange._async_get_trade_history_id(pair, since=trades_history[0][0], ret = await exchange._async_get_trade_history_id(pair,
until=trades_history[-1][0]-1) since=fetch_trades_result[0]['timestamp'],
until=fetch_trades_result[-1]['timestamp'] - 1)
assert type(ret) is tuple assert type(ret) is tuple
assert ret[0] == pair assert ret[0] == pair
assert type(ret[1]) is list assert type(ret[1]) is list
assert len(ret[1]) == len(trades_history) assert len(ret[1]) == len(fetch_trades_result)
assert exchange._async_fetch_trades.call_count == 3 assert exchange._api_async.fetch_trades.call_count == 3
fetch_trades_cal = exchange._async_fetch_trades.call_args_list fetch_trades_cal = exchange._api_async.fetch_trades.call_args_list
# first call (using since, not fromId) # first call (using since, not fromId)
assert fetch_trades_cal[0][0][0] == pair assert fetch_trades_cal[0][0][0] == pair
assert fetch_trades_cal[0][1]['since'] == trades_history[0][0] assert fetch_trades_cal[0][1]['since'] == fetch_trades_result[0]['timestamp']
# 2nd call # 2nd call
assert fetch_trades_cal[1][0][0] == pair assert fetch_trades_cal[1][0][0] == pair
@ -1759,36 +1760,37 @@ async def test__async_get_trade_history_id(default_conf, mocker, caplog, exchang
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
async def test__async_get_trade_history_time(default_conf, mocker, caplog, exchange_name, async def test__async_get_trade_history_time(default_conf, mocker, caplog, exchange_name,
trades_history): fetch_trades_result):
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
async def mock_get_trade_hist(pair, *args, **kwargs): async def mock_get_trade_hist(pair, *args, **kwargs):
if kwargs['since'] == trades_history[0][0]: if kwargs['since'] == fetch_trades_result[0]['timestamp']:
return trades_history[:-1] return fetch_trades_result[:-1]
else: else:
return trades_history[-1:] return fetch_trades_result[-1:]
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
# Monkey-patch async function # Monkey-patch async function
exchange._async_fetch_trades = MagicMock(side_effect=mock_get_trade_hist) exchange._api_async.fetch_trades = MagicMock(side_effect=mock_get_trade_hist)
pair = 'ETH/BTC' pair = 'ETH/BTC'
ret = await exchange._async_get_trade_history_time(pair, since=trades_history[0][0], ret = await exchange._async_get_trade_history_time(pair,
until=trades_history[-1][0]-1) since=fetch_trades_result[0]['timestamp'],
until=fetch_trades_result[-1]['timestamp']-1)
assert type(ret) is tuple assert type(ret) is tuple
assert ret[0] == pair assert ret[0] == pair
assert type(ret[1]) is list assert type(ret[1]) is list
assert len(ret[1]) == len(trades_history) assert len(ret[1]) == len(fetch_trades_result)
assert exchange._async_fetch_trades.call_count == 2 assert exchange._api_async.fetch_trades.call_count == 2
fetch_trades_cal = exchange._async_fetch_trades.call_args_list fetch_trades_cal = exchange._api_async.fetch_trades.call_args_list
# first call (using since, not fromId) # first call (using since, not fromId)
assert fetch_trades_cal[0][0][0] == pair assert fetch_trades_cal[0][0][0] == pair
assert fetch_trades_cal[0][1]['since'] == trades_history[0][0] assert fetch_trades_cal[0][1]['since'] == fetch_trades_result[0]['timestamp']
# 2nd call # 2nd call
assert fetch_trades_cal[1][0][0] == pair assert fetch_trades_cal[1][0][0] == pair
assert fetch_trades_cal[0][1]['since'] == trades_history[0][0] assert fetch_trades_cal[1][1]['since'] == fetch_trades_result[-2]['timestamp']
assert log_has_re(r"Stopping because until was reached.*", caplog) assert log_has_re(r"Stopping because until was reached.*", caplog)

View File

@ -89,6 +89,7 @@ def test_get_balances_prod(default_conf, mocker):
'2ST': balance_item.copy(), '2ST': balance_item.copy(),
'3ST': balance_item.copy(), '3ST': balance_item.copy(),
'4ST': balance_item.copy(), '4ST': balance_item.copy(),
'EUR': balance_item.copy(),
}) })
kraken_open_orders = [{'symbol': '1ST/EUR', kraken_open_orders = [{'symbol': '1ST/EUR',
'type': 'limit', 'type': 'limit',
@ -123,21 +124,22 @@ def test_get_balances_prod(default_conf, mocker):
'remaining': 2.0, 'remaining': 2.0,
}, },
{'status': 'open', {'status': 'open',
'symbol': 'BTC/3ST', 'symbol': '3ST/EUR',
'type': 'limit', 'type': 'limit',
'side': 'buy', 'side': 'buy',
'price': 20, 'price': 0.02,
'cost': 0.0, 'cost': 0.0,
'amount': 3.0, 'amount': 100.0,
'filled': 0.0, 'filled': 0.0,
'average': 0.0, 'average': 0.0,
'remaining': 3.0, 'remaining': 100.0,
}] }]
api_mock.fetch_open_orders = MagicMock(return_value=kraken_open_orders) api_mock.fetch_open_orders = MagicMock(return_value=kraken_open_orders)
default_conf['dry_run'] = False default_conf['dry_run'] = False
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken") exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken")
balances = exchange.get_balances() balances = exchange.get_balances()
assert len(balances) == 4 assert len(balances) == 5
assert balances['1ST']['free'] == 9.0 assert balances['1ST']['free'] == 9.0
assert balances['1ST']['total'] == 10.0 assert balances['1ST']['total'] == 10.0
assert balances['1ST']['used'] == 1.0 assert balances['1ST']['used'] == 1.0
@ -146,13 +148,17 @@ def test_get_balances_prod(default_conf, mocker):
assert balances['2ST']['total'] == 10.0 assert balances['2ST']['total'] == 10.0
assert balances['2ST']['used'] == 4.0 assert balances['2ST']['used'] == 4.0
assert balances['3ST']['free'] == 7.0 assert balances['3ST']['free'] == 10.0
assert balances['3ST']['total'] == 10.0 assert balances['3ST']['total'] == 10.0
assert balances['3ST']['used'] == 3.0 assert balances['3ST']['used'] == 0.0
assert balances['4ST']['free'] == 10.0 assert balances['4ST']['free'] == 10.0
assert balances['4ST']['total'] == 10.0 assert balances['4ST']['total'] == 10.0
assert balances['4ST']['used'] == 0.0 assert balances['4ST']['used'] == 0.0
assert balances['EUR']['free'] == 8.0
assert balances['EUR']['total'] == 10.0
assert balances['EUR']['used'] == 2.0
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken", ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken",
"get_balances", "fetch_balance") "get_balances", "fetch_balance")

View File

@ -350,17 +350,17 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
default_conf['timerange'] = '-1510694220' default_conf['timerange'] = '-1510694220'
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting.strategy.bot_loop_start = MagicMock()
backtesting.start() backtesting.start()
# check the logs, that will contain the backtest result # check the logs, that will contain the backtest result
exists = [ exists = [
'Using stake_currency: BTC ...',
'Using stake_amount: 0.001 ...',
'Backtesting with data from 2017-11-14 21:17:00 ' 'Backtesting with data from 2017-11-14 21:17:00 '
'up to 2017-11-14 22:59:00 (0 days)..' 'up to 2017-11-14 22:59:00 (0 days)..'
] ]
for line in exists: for line in exists:
assert log_has(line, caplog) assert log_has(line, caplog)
assert backtesting.strategy.dp._pairlists is not None assert backtesting.strategy.dp._pairlists is not None
assert backtesting.strategy.bot_loop_start.call_count == 1
def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> None: def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> None:
@ -722,8 +722,6 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir):
'Ignoring max_open_trades (--disable-max-market-positions was used) ...', 'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
'Parameter --timerange detected: 1510694220-1510700340 ...', 'Parameter --timerange detected: 1510694220-1510700340 ...',
f'Using data directory: {testdatadir} ...', f'Using data directory: {testdatadir} ...',
'Using stake_currency: BTC ...',
'Using stake_amount: 0.001 ...',
'Loading data from 2017-11-14 20:57:00 ' 'Loading data from 2017-11-14 20:57:00 '
'up to 2017-11-14 22:58:00 (0 days)..', 'up to 2017-11-14 22:58:00 (0 days)..',
'Backtesting with data from 2017-11-14 21:17:00 ' 'Backtesting with data from 2017-11-14 21:17:00 '
@ -786,8 +784,6 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
'Ignoring max_open_trades (--disable-max-market-positions was used) ...', 'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
'Parameter --timerange detected: 1510694220-1510700340 ...', 'Parameter --timerange detected: 1510694220-1510700340 ...',
f'Using data directory: {testdatadir} ...', f'Using data directory: {testdatadir} ...',
'Using stake_currency: BTC ...',
'Using stake_amount: 0.001 ...',
'Loading data from 2017-11-14 20:57:00 ' 'Loading data from 2017-11-14 20:57:00 '
'up to 2017-11-14 22:58:00 (0 days)..', 'up to 2017-11-14 22:58:00 (0 days)..',
'Backtesting with data from 2017-11-14 21:17:00 ' 'Backtesting with data from 2017-11-14 21:17:00 '
@ -865,8 +861,6 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
'Ignoring max_open_trades (--disable-max-market-positions was used) ...', 'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
'Parameter --timerange detected: 1510694220-1510700340 ...', 'Parameter --timerange detected: 1510694220-1510700340 ...',
f'Using data directory: {testdatadir} ...', f'Using data directory: {testdatadir} ...',
'Using stake_currency: BTC ...',
'Using stake_amount: 0.001 ...',
'Loading data from 2017-11-14 20:57:00 ' 'Loading data from 2017-11-14 20:57:00 '
'up to 2017-11-14 22:58:00 (0 days)..', 'up to 2017-11-14 22:58:00 (0 days)..',
'Backtesting with data from 2017-11-14 21:17:00 ' 'Backtesting with data from 2017-11-14 21:17:00 '

View File

@ -20,7 +20,7 @@ def test_hyperoptlossresolver(mocker, default_conf) -> None:
hl = ShortTradeDurHyperOptLoss hl = ShortTradeDurHyperOptLoss
mocker.patch( mocker.patch(
'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver.load_object', 'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver.load_object',
MagicMock(return_value=hl) MagicMock(return_value=hl())
) )
default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'}) default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'})
x = HyperOptLossResolver.load_hyperoptloss(default_conf) x = HyperOptLossResolver.load_hyperoptloss(default_conf)

View File

@ -1,5 +1,5 @@
import re import re
from datetime import timedelta from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
import pandas as pd import pandas as pd
@ -77,7 +77,10 @@ def test_generate_backtest_stats(default_conf, testdatadir):
SellType.ROI, SellType.FORCE_SELL] SellType.ROI, SellType.FORCE_SELL]
}), }),
'config': default_conf, 'config': default_conf,
'locks': []} 'locks': [],
'backtest_start_time': Arrow.utcnow().int_timestamp,
'backtest_end_time': Arrow.utcnow().int_timestamp,
}
} }
timerange = TimeRange.parse_timerange('1510688220-1510700340') timerange = TimeRange.parse_timerange('1510688220-1510700340')
min_date = Arrow.fromtimestamp(1510688220) min_date = Arrow.fromtimestamp(1510688220)
@ -121,8 +124,8 @@ def test_generate_backtest_stats(default_conf, testdatadir):
} }
assert strat_stats['max_drawdown'] == 0.0 assert strat_stats['max_drawdown'] == 0.0
assert strat_stats['drawdown_start'] == Arrow.fromtimestamp(0).datetime assert strat_stats['drawdown_start'] == datetime(1970, 1, 1, tzinfo=timezone.utc)
assert strat_stats['drawdown_end'] == Arrow.fromtimestamp(0).datetime assert strat_stats['drawdown_end'] == datetime(1970, 1, 1, tzinfo=timezone.utc)
assert strat_stats['drawdown_end_ts'] == 0 assert strat_stats['drawdown_end_ts'] == 0
assert strat_stats['drawdown_start_ts'] == 0 assert strat_stats['drawdown_start_ts'] == 0
assert strat_stats['pairlist'] == ['UNITTEST/BTC'] assert strat_stats['pairlist'] == ['UNITTEST/BTC']

View File

@ -6,6 +6,7 @@ import pytest
from freqtrade.constants import AVAILABLE_PAIRLISTS from freqtrade.constants import AVAILABLE_PAIRLISTS
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.pairlistmanager import PairListManager
from freqtrade.resolvers import PairListResolver from freqtrade.resolvers import PairListResolver
from tests.conftest import get_patched_freqtradebot, log_has, log_has_re from tests.conftest import get_patched_freqtradebot, log_has, log_has_re
@ -155,6 +156,47 @@ def test_refresh_static_pairlist(mocker, markets, static_pl_conf):
assert static_pl_conf['exchange']['pair_blacklist'] == freqtrade.pairlists.blacklist assert static_pl_conf['exchange']['pair_blacklist'] == freqtrade.pairlists.blacklist
@pytest.mark.parametrize('pairs,expected', [
(['NOEXIST/BTC', r'\+WHAT/BTC'],
['ETH/BTC', 'TKN/BTC', 'TRST/BTC', 'NOEXIST/BTC', 'SWT/BTC', 'BCC/BTC', 'HOT/BTC']),
(['NOEXIST/BTC', r'*/BTC'], # This is an invalid regex
[]),
])
def test_refresh_static_pairlist_noexist(mocker, markets, static_pl_conf, pairs, expected, caplog):
static_pl_conf['pairlists'][0]['allow_inactive'] = True
static_pl_conf['exchange']['pair_whitelist'] += pairs
freqtrade = get_patched_freqtradebot(mocker, static_pl_conf)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
exchange_has=MagicMock(return_value=True),
markets=PropertyMock(return_value=markets),
)
freqtrade.pairlists.refresh_pairlist()
# Ensure all except those in whitelist are removed
assert set(expected) == set(freqtrade.pairlists.whitelist)
assert static_pl_conf['exchange']['pair_blacklist'] == freqtrade.pairlists.blacklist
if not expected:
assert log_has_re(r'Pair whitelist contains an invalid Wildcard: Wildcard error.*', caplog)
def test_invalid_blacklist(mocker, markets, static_pl_conf, caplog):
static_pl_conf['exchange']['pair_blacklist'] = ['*/BTC']
freqtrade = get_patched_freqtradebot(mocker, static_pl_conf)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
exchange_has=MagicMock(return_value=True),
markets=PropertyMock(return_value=markets),
)
freqtrade.pairlists.refresh_pairlist()
whitelist = []
# Ensure all except those in whitelist are removed
assert set(whitelist) == set(freqtrade.pairlists.whitelist)
assert static_pl_conf['exchange']['pair_blacklist'] == freqtrade.pairlists.blacklist
log_has_re(r"Pair blacklist contains an invalid Wildcard.*", caplog)
def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_conf): def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_conf):
mocker.patch.multiple( mocker.patch.multiple(
@ -677,6 +719,32 @@ def test_rangestabilityfilter_caching(mocker, markets, default_conf, tickers, oh
assert freqtrade.exchange.refresh_latest_ohlcv.call_count == previous_call_count assert freqtrade.exchange.refresh_latest_ohlcv.call_count == previous_call_count
def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplog):
default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10},
{'method': 'SpreadFilter', 'max_spread_ratio': 0.1}]
mocker.patch.multiple('freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets),
exchange_has=MagicMock(return_value=True),
get_tickers=tickers
)
ftbot = get_patched_freqtradebot(mocker, default_conf)
ftbot.pairlists.refresh_pairlist()
assert len(ftbot.pairlists.whitelist) == 5
tickers.return_value['ETH/BTC']['ask'] = 0.0
del tickers.return_value['TKN/BTC']
del tickers.return_value['LTC/BTC']
mocker.patch.multiple('freqtrade.exchange.Exchange', get_tickers=tickers)
ftbot.pairlists.refresh_pairlist()
assert log_has_re(r'Removed .* invalid ticker data.*', caplog)
assert len(ftbot.pairlists.whitelist) == 2
@pytest.mark.parametrize("pairlistconfig,desc_expected,exception_expected", [ @pytest.mark.parametrize("pairlistconfig,desc_expected,exception_expected", [
({"method": "PriceFilter", "low_price_ratio": 0.001, "min_price": 0.00000010, ({"method": "PriceFilter", "low_price_ratio": 0.001, "min_price": 0.00000010,
"max_price": 1.0}, "max_price": 1.0},
@ -804,3 +872,73 @@ def test_performance_filter(mocker, whitelist_conf, pairlists, pair_allowlist, o
freqtrade.pairlists.refresh_pairlist() freqtrade.pairlists.refresh_pairlist()
allowlist = freqtrade.pairlists.whitelist allowlist = freqtrade.pairlists.whitelist
assert allowlist == allowlist_result assert allowlist == allowlist_result
@pytest.mark.parametrize('wildcardlist,pairs,expected', [
(['BTC/USDT'],
['BTC/USDT'],
['BTC/USDT']),
(['BTC/USDT', 'ETH/USDT'],
['BTC/USDT', 'ETH/USDT'],
['BTC/USDT', 'ETH/USDT']),
(['BTC/USDT', 'ETH/USDT'],
['BTC/USDT'], ['BTC/USDT']), # Test one too many
(['.*/USDT'],
['BTC/USDT', 'ETH/USDT'], ['BTC/USDT', 'ETH/USDT']), # Wildcard simple
(['.*C/USDT'],
['BTC/USDT', 'ETC/USDT', 'ETH/USDT'], ['BTC/USDT', 'ETC/USDT']), # Wildcard exclude one
(['.*UP/USDT', 'BTC/USDT', 'ETH/USDT'],
['BTC/USDT', 'ETC/USDT', 'ETH/USDT', 'BTCUP/USDT', 'XRPUP/USDT', 'XRPDOWN/USDT'],
['BTC/USDT', 'ETH/USDT', 'BTCUP/USDT', 'XRPUP/USDT']), # Wildcard exclude one
(['BTC/.*', 'ETH/.*'],
['BTC/USDT', 'ETC/USDT', 'ETH/USDT', 'BTC/USD', 'ETH/EUR', 'BTC/GBP'],
['BTC/USDT', 'ETH/USDT', 'BTC/USD', 'ETH/EUR', 'BTC/GBP']), # Wildcard exclude one
(['*UP/USDT', 'BTC/USDT', 'ETH/USDT'],
['BTC/USDT', 'ETC/USDT', 'ETH/USDT', 'BTCUP/USDT', 'XRPUP/USDT', 'XRPDOWN/USDT'],
None),
(['BTC/USD'],
['BTC/USD', 'BTC/USDT'],
['BTC/USD']),
])
def test_expand_pairlist(wildcardlist, pairs, expected):
if expected is None:
with pytest.raises(ValueError, match=r'Wildcard error in \*UP/USDT,'):
expand_pairlist(wildcardlist, pairs)
else:
assert sorted(expand_pairlist(wildcardlist, pairs)) == sorted(expected)
@pytest.mark.parametrize('wildcardlist,pairs,expected', [
(['BTC/USDT'],
['BTC/USDT'],
['BTC/USDT']),
(['BTC/USDT', 'ETH/USDT'],
['BTC/USDT', 'ETH/USDT'],
['BTC/USDT', 'ETH/USDT']),
(['BTC/USDT', 'ETH/USDT'],
['BTC/USDT'], ['BTC/USDT', 'ETH/USDT']), # Test one too many
(['.*/USDT'],
['BTC/USDT', 'ETH/USDT'], ['BTC/USDT', 'ETH/USDT']), # Wildcard simple
(['.*C/USDT'],
['BTC/USDT', 'ETC/USDT', 'ETH/USDT'], ['BTC/USDT', 'ETC/USDT']), # Wildcard exclude one
(['.*UP/USDT', 'BTC/USDT', 'ETH/USDT'],
['BTC/USDT', 'ETC/USDT', 'ETH/USDT', 'BTCUP/USDT', 'XRPUP/USDT', 'XRPDOWN/USDT'],
['BTC/USDT', 'ETH/USDT', 'BTCUP/USDT', 'XRPUP/USDT']), # Wildcard exclude one
(['BTC/.*', 'ETH/.*'],
['BTC/USDT', 'ETC/USDT', 'ETH/USDT', 'BTC/USD', 'ETH/EUR', 'BTC/GBP'],
['BTC/USDT', 'ETH/USDT', 'BTC/USD', 'ETH/EUR', 'BTC/GBP']), # Wildcard exclude one
(['*UP/USDT', 'BTC/USDT', 'ETH/USDT'],
['BTC/USDT', 'ETC/USDT', 'ETH/USDT', 'BTCUP/USDT', 'XRPUP/USDT', 'XRPDOWN/USDT'],
None),
(['HELLO/WORLD'], [], ['HELLO/WORLD']), # Invalid pair kept
(['BTC/USD'],
['BTC/USD', 'BTC/USDT'],
['BTC/USD']),
])
def test_expand_pairlist_keep_invalid(wildcardlist, pairs, expected):
if expected is None:
with pytest.raises(ValueError, match=r'Wildcard error in \*UP/USDT,'):
expand_pairlist(wildcardlist, pairs, keep_invalid=True)
else:
assert sorted(expand_pairlist(wildcardlist, pairs, keep_invalid=True)) == sorted(expected)

View File

@ -11,11 +11,10 @@ from freqtrade.persistence.models import PairLock
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
def test_PairLocks(use_db): def test_PairLocks(use_db):
PairLocks.timeframe = '5m' PairLocks.timeframe = '5m'
PairLocks.use_db = use_db
# No lock should be present # No lock should be present
if use_db: if use_db:
assert len(PairLock.query.all()) == 0 assert len(PairLock.query.all()) == 0
else:
PairLocks.use_db = False
assert PairLocks.use_db == use_db assert PairLocks.use_db == use_db
@ -88,10 +87,9 @@ def test_PairLocks(use_db):
def test_PairLocks_getlongestlock(use_db): def test_PairLocks_getlongestlock(use_db):
PairLocks.timeframe = '5m' PairLocks.timeframe = '5m'
# No lock should be present # No lock should be present
PairLocks.use_db = use_db
if use_db: if use_db:
assert len(PairLock.query.all()) == 0 assert len(PairLock.query.all()) == 0
else:
PairLocks.use_db = False
assert PairLocks.use_db == use_db assert PairLocks.use_db == use_db

View File

@ -957,14 +957,24 @@ def test_rpc_blacklist(mocker, default_conf) -> None:
assert isinstance(ret['errors'], dict) assert isinstance(ret['errors'], dict)
assert ret['errors']['ETH/BTC']['error_msg'] == 'Pair ETH/BTC already in pairlist.' assert ret['errors']['ETH/BTC']['error_msg'] == 'Pair ETH/BTC already in pairlist.'
ret = rpc._rpc_blacklist(["ETH/ETH"]) ret = rpc._rpc_blacklist(["*/BTC"])
assert 'StaticPairList' in ret['method'] assert 'StaticPairList' in ret['method']
assert len(ret['blacklist']) == 3 assert len(ret['blacklist']) == 3
assert ret['blacklist'] == default_conf['exchange']['pair_blacklist'] assert ret['blacklist'] == default_conf['exchange']['pair_blacklist']
assert ret['blacklist'] == ['DOGE/BTC', 'HOT/BTC', 'ETH/BTC'] assert ret['blacklist'] == ['DOGE/BTC', 'HOT/BTC', 'ETH/BTC']
assert ret['blacklist_expanded'] == ['ETH/BTC']
assert 'errors' in ret
assert isinstance(ret['errors'], dict)
assert ret['errors'] == {'*/BTC': {'error_msg': 'Pair */BTC is not a valid wildcard.'}}
ret = rpc._rpc_blacklist(["XRP/.*"])
assert 'StaticPairList' in ret['method']
assert len(ret['blacklist']) == 4
assert ret['blacklist'] == default_conf['exchange']['pair_blacklist']
assert ret['blacklist'] == ['DOGE/BTC', 'HOT/BTC', 'ETH/BTC', 'XRP/.*']
assert ret['blacklist_expanded'] == ['ETH/BTC', 'XRP/BTC']
assert 'errors' in ret assert 'errors' in ret
assert isinstance(ret['errors'], dict) assert isinstance(ret['errors'], dict)
assert ret['errors']['ETH/ETH']['error_msg'] == 'Pair ETH/ETH does not match stake currency.'
def test_rpc_edge_disabled(mocker, default_conf) -> None: def test_rpc_edge_disabled(mocker, default_conf) -> None:

View File

@ -7,18 +7,25 @@ from pathlib import Path
from unittest.mock import ANY, MagicMock, PropertyMock from unittest.mock import ANY, MagicMock, PropertyMock
import pytest import pytest
from flask import Flask import uvicorn
from fastapi import FastAPI
from fastapi.exceptions import HTTPException
from fastapi.testclient import TestClient
from requests.auth import _basic_auth_str from requests.auth import _basic_auth_str
from freqtrade.__init__ import __version__ from freqtrade.__init__ import __version__
from freqtrade.loggers import setup_logging, setup_logging_pre from freqtrade.loggers import setup_logging, setup_logging_pre
from freqtrade.persistence import PairLocks, Trade from freqtrade.persistence import PairLocks, Trade
from freqtrade.rpc import RPC from freqtrade.rpc import RPC
from freqtrade.rpc.api_server import BASE_URI, ApiServer from freqtrade.rpc.api_server import ApiServer
from freqtrade.rpc.api_server.api_auth import create_token, get_user_from_token
from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer
from freqtrade.state import RunMode, State from freqtrade.state import RunMode, State
from tests.conftest import create_mock_trades, get_patched_freqtradebot, log_has, patch_get_signal from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, log_has_re,
patch_get_signal)
BASE_URI = "/api/v1"
_TEST_USER = "FreqTrader" _TEST_USER = "FreqTrader"
_TEST_PASS = "SuperSecurePassword1!" _TEST_PASS = "SuperSecurePassword1!"
@ -38,18 +45,19 @@ def botclient(default_conf, mocker):
ftbot = get_patched_freqtradebot(mocker, default_conf) ftbot = get_patched_freqtradebot(mocker, default_conf)
rpc = RPC(ftbot) rpc = RPC(ftbot)
mocker.patch('freqtrade.rpc.api_server.ApiServer.run', MagicMock()) mocker.patch('freqtrade.rpc.api_server.ApiServer.start_api', MagicMock())
apiserver = ApiServer(rpc, default_conf) apiserver = ApiServer(rpc, default_conf)
yield ftbot, apiserver.app.test_client() yield ftbot, TestClient(apiserver.app)
# Cleanup ... ? # Cleanup ... ?
def client_post(client, url, data={}): def client_post(client, url, data={}):
return client.post(url, return client.post(url,
content_type="application/json",
data=data, data=data,
headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS), headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS),
'Origin': 'http://example.com'}) 'Origin': 'http://example.com',
'content-type': 'application/json'
})
def client_get(client, url): def client_get(client, url):
@ -66,10 +74,10 @@ def client_delete(client, url):
def assert_response(response, expected_code=200, needs_cors=True): def assert_response(response, expected_code=200, needs_cors=True):
assert response.status_code == expected_code assert response.status_code == expected_code
assert response.content_type == "application/json" assert response.headers.get('content-type') == "application/json"
if needs_cors: if needs_cors:
assert ('Access-Control-Allow-Credentials', 'true') in response.headers._list assert ('access-control-allow-credentials', 'true') in response.headers.items()
assert ('Access-Control-Allow-Origin', 'http://example.com') in response.headers._list assert ('access-control-allow-origin', 'http://example.com') in response.headers.items()
def test_api_not_found(botclient): def test_api_not_found(botclient):
@ -77,55 +85,76 @@ def test_api_not_found(botclient):
rc = client_post(client, f"{BASE_URI}/invalid_url") rc = client_post(client, f"{BASE_URI}/invalid_url")
assert_response(rc, 404) assert_response(rc, 404)
assert rc.json == {"status": "error", assert rc.json() == {"detail": "Not Found"}
"reason": f"There's no API call for http://localhost{BASE_URI}/invalid_url.",
"code": 404
} def test_api_auth():
with pytest.raises(ValueError):
create_token({'identity': {'u': 'Freqtrade'}}, 'secret1234', token_type="NotATokenType")
token = create_token({'identity': {'u': 'Freqtrade'}}, 'secret1234')
assert isinstance(token, str)
u = get_user_from_token(token, 'secret1234')
assert u == 'Freqtrade'
with pytest.raises(HTTPException):
get_user_from_token(token, 'secret1234', token_type='refresh')
# Create invalid token
token = create_token({'identity': {'u1': 'Freqrade'}}, 'secret1234')
with pytest.raises(HTTPException):
get_user_from_token(token, 'secret1234')
with pytest.raises(HTTPException):
get_user_from_token(b'not_a_token', 'secret1234')
def test_api_unauthorized(botclient): def test_api_unauthorized(botclient):
ftbot, client = botclient ftbot, client = botclient
rc = client.get(f"{BASE_URI}/ping") rc = client.get(f"{BASE_URI}/ping")
assert_response(rc, needs_cors=False) assert_response(rc, needs_cors=False)
assert rc.json == {'status': 'pong'} assert rc.json() == {'status': 'pong'}
# Don't send user/pass information # Don't send user/pass information
rc = client.get(f"{BASE_URI}/version") rc = client.get(f"{BASE_URI}/version")
assert_response(rc, 401, needs_cors=False) assert_response(rc, 401, needs_cors=False)
assert rc.json == {'error': 'Unauthorized'} assert rc.json() == {'detail': 'Unauthorized'}
# Change only username # Change only username
ftbot.config['api_server']['username'] = 'Ftrader' ftbot.config['api_server']['username'] = 'Ftrader'
rc = client_get(client, f"{BASE_URI}/version") rc = client_get(client, f"{BASE_URI}/version")
assert_response(rc, 401) assert_response(rc, 401)
assert rc.json == {'error': 'Unauthorized'} assert rc.json() == {'detail': 'Unauthorized'}
# Change only password # Change only password
ftbot.config['api_server']['username'] = _TEST_USER ftbot.config['api_server']['username'] = _TEST_USER
ftbot.config['api_server']['password'] = 'WrongPassword' ftbot.config['api_server']['password'] = 'WrongPassword'
rc = client_get(client, f"{BASE_URI}/version") rc = client_get(client, f"{BASE_URI}/version")
assert_response(rc, 401) assert_response(rc, 401)
assert rc.json == {'error': 'Unauthorized'} assert rc.json() == {'detail': 'Unauthorized'}
ftbot.config['api_server']['username'] = 'Ftrader' ftbot.config['api_server']['username'] = 'Ftrader'
ftbot.config['api_server']['password'] = 'WrongPassword' ftbot.config['api_server']['password'] = 'WrongPassword'
rc = client_get(client, f"{BASE_URI}/version") rc = client_get(client, f"{BASE_URI}/version")
assert_response(rc, 401) assert_response(rc, 401)
assert rc.json == {'error': 'Unauthorized'} assert rc.json() == {'detail': 'Unauthorized'}
def test_api_token_login(botclient): def test_api_token_login(botclient):
ftbot, client = botclient ftbot, client = botclient
rc = client.post(f"{BASE_URI}/token/login",
data=None,
headers={'Authorization': _basic_auth_str('WRONG_USER', 'WRONG_PASS'),
'Origin': 'http://example.com'})
assert_response(rc, 401)
rc = client_post(client, f"{BASE_URI}/token/login") rc = client_post(client, f"{BASE_URI}/token/login")
assert_response(rc) assert_response(rc)
assert 'access_token' in rc.json assert 'access_token' in rc.json()
assert 'refresh_token' in rc.json assert 'refresh_token' in rc.json()
# test Authentication is working with JWT tokens too # test Authentication is working with JWT tokens too
rc = client.get(f"{BASE_URI}/count", rc = client.get(f"{BASE_URI}/count",
content_type="application/json", headers={'Authorization': f'Bearer {rc.json()["access_token"]}',
headers={'Authorization': f'Bearer {rc.json["access_token"]}',
'Origin': 'http://example.com'}) 'Origin': 'http://example.com'})
assert_response(rc) assert_response(rc)
@ -135,13 +164,12 @@ def test_api_token_refresh(botclient):
rc = client_post(client, f"{BASE_URI}/token/login") rc = client_post(client, f"{BASE_URI}/token/login")
assert_response(rc) assert_response(rc)
rc = client.post(f"{BASE_URI}/token/refresh", rc = client.post(f"{BASE_URI}/token/refresh",
content_type="application/json",
data=None, data=None,
headers={'Authorization': f'Bearer {rc.json["refresh_token"]}', headers={'Authorization': f'Bearer {rc.json()["refresh_token"]}',
'Origin': 'http://example.com'}) 'Origin': 'http://example.com'})
assert_response(rc) assert_response(rc)
assert 'access_token' in rc.json assert 'access_token' in rc.json()
assert 'refresh_token' not in rc.json assert 'refresh_token' not in rc.json()
def test_api_stop_workflow(botclient): def test_api_stop_workflow(botclient):
@ -149,24 +177,24 @@ def test_api_stop_workflow(botclient):
assert ftbot.state == State.RUNNING assert ftbot.state == State.RUNNING
rc = client_post(client, f"{BASE_URI}/stop") rc = client_post(client, f"{BASE_URI}/stop")
assert_response(rc) assert_response(rc)
assert rc.json == {'status': 'stopping trader ...'} assert rc.json() == {'status': 'stopping trader ...'}
assert ftbot.state == State.STOPPED assert ftbot.state == State.STOPPED
# Stop bot again # Stop bot again
rc = client_post(client, f"{BASE_URI}/stop") rc = client_post(client, f"{BASE_URI}/stop")
assert_response(rc) assert_response(rc)
assert rc.json == {'status': 'already stopped'} assert rc.json() == {'status': 'already stopped'}
# Start bot # Start bot
rc = client_post(client, f"{BASE_URI}/start") rc = client_post(client, f"{BASE_URI}/start")
assert_response(rc) assert_response(rc)
assert rc.json == {'status': 'starting trader ...'} assert rc.json() == {'status': 'starting trader ...'}
assert ftbot.state == State.RUNNING assert ftbot.state == State.RUNNING
# Call start again # Call start again
rc = client_post(client, f"{BASE_URI}/start") rc = client_post(client, f"{BASE_URI}/start")
assert_response(rc) assert_response(rc)
assert rc.json == {'status': 'already running'} assert rc.json() == {'status': 'already running'}
def test_api__init__(default_conf, mocker): def test_api__init__(default_conf, mocker):
@ -180,11 +208,29 @@ def test_api__init__(default_conf, mocker):
"password": "testPass", "password": "testPass",
}}) }})
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock())
mocker.patch('freqtrade.rpc.api_server.ApiServer.run', MagicMock()) mocker.patch('freqtrade.rpc.api_server.webserver.ApiServer.start_api', MagicMock())
apiserver = ApiServer(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) apiserver = ApiServer(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf)
assert apiserver._config == default_conf assert apiserver._config == default_conf
def test_api_UvicornServer(default_conf, mocker):
thread_mock = mocker.patch('freqtrade.rpc.api_server.uvicorn_threaded.threading.Thread')
s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1'))
assert thread_mock.call_count == 0
s.install_signal_handlers()
# Original implementation starts a thread - make sure that's not the case
assert thread_mock.call_count == 0
# Fake started to avoid sleeping forever
s.started = True
s.run_in_thread()
assert thread_mock.call_count == 1
s.cleanup()
assert s.should_exit is True
def test_api_run(default_conf, mocker, caplog): def test_api_run(default_conf, mocker, caplog):
default_conf.update({"api_server": {"enabled": True, default_conf.update({"api_server": {"enabled": True,
"listen_ip_address": "127.0.0.1", "listen_ip_address": "127.0.0.1",
@ -193,20 +239,19 @@ def test_api_run(default_conf, mocker, caplog):
"password": "testPass", "password": "testPass",
}}) }})
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock())
mocker.patch('freqtrade.rpc.api_server.threading.Thread', MagicMock())
server_mock = MagicMock() server_mock = MagicMock()
mocker.patch('freqtrade.rpc.api_server.make_server', server_mock) mocker.patch('freqtrade.rpc.api_server.webserver.UvicornServer', server_mock)
apiserver = ApiServer(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) apiserver = ApiServer(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf)
assert apiserver._config == default_conf
apiserver.run()
assert server_mock.call_count == 1 assert server_mock.call_count == 1
assert server_mock.call_args_list[0][0][0] == "127.0.0.1" assert apiserver._config == default_conf
assert server_mock.call_args_list[0][0][1] == 8080 apiserver.start_api()
assert isinstance(server_mock.call_args_list[0][0][2], Flask) assert server_mock.call_count == 2
assert hasattr(apiserver, "srv") assert server_mock.call_args_list[0][0][0].host == "127.0.0.1"
assert server_mock.call_args_list[0][0][0].port == 8080
assert isinstance(server_mock.call_args_list[0][0][0].app, FastAPI)
assert log_has("Starting HTTP Server at 127.0.0.1:8080", caplog) assert log_has("Starting HTTP Server at 127.0.0.1:8080", caplog)
assert log_has("Starting Local Rest Server.", caplog) assert log_has("Starting Local Rest Server.", caplog)
@ -219,12 +264,12 @@ def test_api_run(default_conf, mocker, caplog):
"listen_port": 8089, "listen_port": 8089,
"password": "", "password": "",
}}) }})
apiserver.run() apiserver.start_api()
assert server_mock.call_count == 1 assert server_mock.call_count == 1
assert server_mock.call_args_list[0][0][0] == "0.0.0.0" assert server_mock.call_args_list[0][0][0].host == "0.0.0.0"
assert server_mock.call_args_list[0][0][1] == 8089 assert server_mock.call_args_list[0][0][0].port == 8089
assert isinstance(server_mock.call_args_list[0][0][2], Flask) assert isinstance(server_mock.call_args_list[0][0][0].app, FastAPI)
assert log_has("Starting HTTP Server at 0.0.0.0:8089", caplog) assert log_has("Starting HTTP Server at 0.0.0.0:8089", caplog)
assert log_has("Starting Local Rest Server.", caplog) assert log_has("Starting Local Rest Server.", caplog)
assert log_has("SECURITY WARNING - Local Rest Server listening to external connections", assert log_has("SECURITY WARNING - Local Rest Server listening to external connections",
@ -233,11 +278,13 @@ def test_api_run(default_conf, mocker, caplog):
"e.g 127.0.0.1 in config.json", caplog) "e.g 127.0.0.1 in config.json", caplog)
assert log_has("SECURITY WARNING - No password for local REST Server defined. " assert log_has("SECURITY WARNING - No password for local REST Server defined. "
"Please make sure that this is intentional!", caplog) "Please make sure that this is intentional!", caplog)
assert log_has_re("SECURITY WARNING - `jwt_secret_key` seems to be default.*", caplog)
# Test crashing flask # Test crashing flask
caplog.clear() caplog.clear()
mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock(side_effect=Exception)) mocker.patch('freqtrade.rpc.api_server.webserver.UvicornServer',
apiserver.run() MagicMock(side_effect=Exception))
apiserver.start_api()
assert log_has("Api server failed to start.", caplog) assert log_has("Api server failed to start.", caplog)
@ -249,17 +296,15 @@ def test_api_cleanup(default_conf, mocker, caplog):
"password": "testPass", "password": "testPass",
}}) }})
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock())
mocker.patch('freqtrade.rpc.api_server.threading.Thread', MagicMock())
mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock()) server_mock = MagicMock()
server_mock.cleanup = MagicMock()
mocker.patch('freqtrade.rpc.api_server.webserver.UvicornServer', server_mock)
apiserver = ApiServer(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) apiserver = ApiServer(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf)
apiserver.run()
stop_mock = MagicMock()
stop_mock.shutdown = MagicMock()
apiserver.srv = stop_mock
apiserver.cleanup() apiserver.cleanup()
assert stop_mock.shutdown.call_count == 1 assert apiserver._server.cleanup.call_count == 1
assert log_has("Stopping API Server", caplog) assert log_has("Stopping API Server", caplog)
@ -268,7 +313,7 @@ def test_api_reloadconf(botclient):
rc = client_post(client, f"{BASE_URI}/reload_config") rc = client_post(client, f"{BASE_URI}/reload_config")
assert_response(rc) assert_response(rc)
assert rc.json == {'status': 'Reloading config ...'} assert rc.json() == {'status': 'Reloading config ...'}
assert ftbot.state == State.RELOAD_CONFIG assert ftbot.state == State.RELOAD_CONFIG
@ -278,7 +323,7 @@ def test_api_stopbuy(botclient):
rc = client_post(client, f"{BASE_URI}/stopbuy") rc = client_post(client, f"{BASE_URI}/stopbuy")
assert_response(rc) assert_response(rc)
assert rc.json == {'status': 'No more buy will occur from now. Run /reload_config to reset.'} assert rc.json() == {'status': 'No more buy will occur from now. Run /reload_config to reset.'}
assert ftbot.config['max_open_trades'] == 0 assert ftbot.config['max_open_trades'] == 0
@ -293,9 +338,9 @@ def test_api_balance(botclient, mocker, rpc_balance):
rc = client_get(client, f"{BASE_URI}/balance") rc = client_get(client, f"{BASE_URI}/balance")
assert_response(rc) assert_response(rc)
assert "currencies" in rc.json assert "currencies" in rc.json()
assert len(rc.json["currencies"]) == 5 assert len(rc.json()["currencies"]) == 5
assert rc.json['currencies'][0] == { assert rc.json()['currencies'][0] == {
'currency': 'BTC', 'currency': 'BTC',
'free': 12.0, 'free': 12.0,
'balance': 12.0, 'balance': 12.0,
@ -318,15 +363,19 @@ def test_api_count(botclient, mocker, ticker, fee, markets):
rc = client_get(client, f"{BASE_URI}/count") rc = client_get(client, f"{BASE_URI}/count")
assert_response(rc) assert_response(rc)
assert rc.json["current"] == 0 assert rc.json()["current"] == 0
assert rc.json["max"] == 1.0 assert rc.json()["max"] == 1
# Create some test data # Create some test data
ftbot.enter_positions() ftbot.enter_positions()
rc = client_get(client, f"{BASE_URI}/count") rc = client_get(client, f"{BASE_URI}/count")
assert_response(rc) assert_response(rc)
assert rc.json["current"] == 1.0 assert rc.json()["current"] == 1
assert rc.json["max"] == 1.0 assert rc.json()["max"] == 1
ftbot.config['max_open_trades'] = float('inf')
rc = client_get(client, f"{BASE_URI}/count")
assert rc.json()["max"] == -1
def test_api_locks(botclient): def test_api_locks(botclient):
@ -335,10 +384,10 @@ def test_api_locks(botclient):
rc = client_get(client, f"{BASE_URI}/locks") rc = client_get(client, f"{BASE_URI}/locks")
assert_response(rc) assert_response(rc)
assert 'locks' in rc.json assert 'locks' in rc.json()
assert rc.json['lock_count'] == 0 assert rc.json()['lock_count'] == 0
assert rc.json['lock_count'] == len(rc.json['locks']) assert rc.json()['lock_count'] == len(rc.json()['locks'])
PairLocks.lock_pair('ETH/BTC', datetime.now(timezone.utc) + timedelta(minutes=4), 'randreason') PairLocks.lock_pair('ETH/BTC', datetime.now(timezone.utc) + timedelta(minutes=4), 'randreason')
PairLocks.lock_pair('XRP/BTC', datetime.now(timezone.utc) + timedelta(minutes=20), 'deadbeef') PairLocks.lock_pair('XRP/BTC', datetime.now(timezone.utc) + timedelta(minutes=20), 'deadbeef')
@ -346,11 +395,11 @@ def test_api_locks(botclient):
rc = client_get(client, f"{BASE_URI}/locks") rc = client_get(client, f"{BASE_URI}/locks")
assert_response(rc) assert_response(rc)
assert rc.json['lock_count'] == 2 assert rc.json()['lock_count'] == 2
assert rc.json['lock_count'] == len(rc.json['locks']) assert rc.json()['lock_count'] == len(rc.json()['locks'])
assert 'ETH/BTC' in (rc.json['locks'][0]['pair'], rc.json['locks'][1]['pair']) assert 'ETH/BTC' in (rc.json()['locks'][0]['pair'], rc.json()['locks'][1]['pair'])
assert 'randreason' in (rc.json['locks'][0]['reason'], rc.json['locks'][1]['reason']) assert 'randreason' in (rc.json()['locks'][0]['reason'], rc.json()['locks'][1]['reason'])
assert 'deadbeef' in (rc.json['locks'][0]['reason'], rc.json['locks'][1]['reason']) assert 'deadbeef' in (rc.json()['locks'][0]['reason'], rc.json()['locks'][1]['reason'])
def test_api_show_config(botclient, mocker): def test_api_show_config(botclient, mocker):
@ -359,15 +408,16 @@ def test_api_show_config(botclient, mocker):
rc = client_get(client, f"{BASE_URI}/show_config") rc = client_get(client, f"{BASE_URI}/show_config")
assert_response(rc) assert_response(rc)
assert 'dry_run' in rc.json assert 'dry_run' in rc.json()
assert rc.json['exchange'] == 'bittrex' assert rc.json()['exchange'] == 'bittrex'
assert rc.json['timeframe'] == '5m' assert rc.json()['timeframe'] == '5m'
assert rc.json['timeframe_ms'] == 300000 assert rc.json()['timeframe_ms'] == 300000
assert rc.json['timeframe_min'] == 5 assert rc.json()['timeframe_min'] == 5
assert rc.json['state'] == 'running' assert rc.json()['state'] == 'running'
assert not rc.json['trailing_stop'] assert rc.json()['bot_name'] == 'freqtrade'
assert 'bid_strategy' in rc.json assert not rc.json()['trailing_stop']
assert 'ask_strategy' in rc.json assert 'bid_strategy' in rc.json()
assert 'ask_strategy' in rc.json()
def test_api_daily(botclient, mocker, ticker, fee, markets): def test_api_daily(botclient, mocker, ticker, fee, markets):
@ -382,10 +432,10 @@ def test_api_daily(botclient, mocker, ticker, fee, markets):
) )
rc = client_get(client, f"{BASE_URI}/daily") rc = client_get(client, f"{BASE_URI}/daily")
assert_response(rc) assert_response(rc)
assert len(rc.json['data']) == 7 assert len(rc.json()['data']) == 7
assert rc.json['stake_currency'] == 'BTC' assert rc.json()['stake_currency'] == 'BTC'
assert rc.json['fiat_display_currency'] == 'USD' assert rc.json()['fiat_display_currency'] == 'USD'
assert rc.json['data'][0]['date'] == str(datetime.utcnow().date()) assert rc.json()['data'][0]['date'] == str(datetime.utcnow().date())
def test_api_trades(botclient, mocker, fee, markets): def test_api_trades(botclient, mocker, fee, markets):
@ -397,19 +447,20 @@ def test_api_trades(botclient, mocker, fee, markets):
) )
rc = client_get(client, f"{BASE_URI}/trades") rc = client_get(client, f"{BASE_URI}/trades")
assert_response(rc) assert_response(rc)
assert len(rc.json) == 2 assert len(rc.json()) == 2
assert rc.json['trades_count'] == 0 assert rc.json()['trades_count'] == 0
create_mock_trades(fee) create_mock_trades(fee)
Trade.session.flush()
rc = client_get(client, f"{BASE_URI}/trades") rc = client_get(client, f"{BASE_URI}/trades")
assert_response(rc) assert_response(rc)
assert len(rc.json['trades']) == 2 assert len(rc.json()['trades']) == 2
assert rc.json['trades_count'] == 2 assert rc.json()['trades_count'] == 2
rc = client_get(client, f"{BASE_URI}/trades?limit=1") rc = client_get(client, f"{BASE_URI}/trades?limit=1")
assert_response(rc) assert_response(rc)
assert len(rc.json['trades']) == 1 assert len(rc.json()['trades']) == 1
assert rc.json['trades_count'] == 1 assert rc.json()['trades_count'] == 1
def test_api_delete_trade(botclient, mocker, fee, markets): def test_api_delete_trade(botclient, mocker, fee, markets):
@ -428,6 +479,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets):
assert_response(rc, 502) assert_response(rc, 502)
create_mock_trades(fee) create_mock_trades(fee)
Trade.session.flush()
ftbot.strategy.order_types['stoploss_on_exchange'] = True ftbot.strategy.order_types['stoploss_on_exchange'] = True
trades = Trade.query.all() trades = Trade.query.all()
trades[1].stoploss_order_id = '1234' trades[1].stoploss_order_id = '1234'
@ -435,7 +487,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets):
rc = client_delete(client, f"{BASE_URI}/trades/1") rc = client_delete(client, f"{BASE_URI}/trades/1")
assert_response(rc) assert_response(rc)
assert rc.json['result_msg'] == 'Deleted trade 1. Closed 1 open orders.' assert rc.json()['result_msg'] == 'Deleted trade 1. Closed 1 open orders.'
assert len(trades) - 1 == len(Trade.query.all()) assert len(trades) - 1 == len(Trade.query.all())
assert cancel_mock.call_count == 1 assert cancel_mock.call_count == 1
@ -448,7 +500,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets):
assert len(trades) - 1 == len(Trade.query.all()) assert len(trades) - 1 == len(Trade.query.all())
rc = client_delete(client, f"{BASE_URI}/trades/2") rc = client_delete(client, f"{BASE_URI}/trades/2")
assert_response(rc) assert_response(rc)
assert rc.json['result_msg'] == 'Deleted trade 2. Closed 2 open orders.' assert rc.json()['result_msg'] == 'Deleted trade 2. Closed 2 open orders.'
assert len(trades) - 2 == len(Trade.query.all()) assert len(trades) - 2 == len(Trade.query.all())
assert stoploss_mock.call_count == 1 assert stoploss_mock.call_count == 1
@ -457,28 +509,32 @@ def test_api_logs(botclient):
ftbot, client = botclient ftbot, client = botclient
rc = client_get(client, f"{BASE_URI}/logs") rc = client_get(client, f"{BASE_URI}/logs")
assert_response(rc) assert_response(rc)
assert len(rc.json) == 2 assert len(rc.json()) == 2
assert 'logs' in rc.json assert 'logs' in rc.json()
# Using a fixed comparison here would make this test fail! # Using a fixed comparison here would make this test fail!
assert rc.json['log_count'] > 1 assert rc.json()['log_count'] > 1
assert len(rc.json['logs']) == rc.json['log_count'] assert len(rc.json()['logs']) == rc.json()['log_count']
assert isinstance(rc.json['logs'][0], list) assert isinstance(rc.json()['logs'][0], list)
# date # date
assert isinstance(rc.json['logs'][0][0], str) assert isinstance(rc.json()['logs'][0][0], str)
# created_timestamp # created_timestamp
assert isinstance(rc.json['logs'][0][1], float) assert isinstance(rc.json()['logs'][0][1], float)
assert isinstance(rc.json['logs'][0][2], str) assert isinstance(rc.json()['logs'][0][2], str)
assert isinstance(rc.json['logs'][0][3], str) assert isinstance(rc.json()['logs'][0][3], str)
assert isinstance(rc.json['logs'][0][4], str) assert isinstance(rc.json()['logs'][0][4], str)
rc = client_get(client, f"{BASE_URI}/logs?limit=5") rc1 = client_get(client, f"{BASE_URI}/logs?limit=5")
assert_response(rc) assert_response(rc1)
assert len(rc.json) == 2 assert len(rc1.json()) == 2
assert 'logs' in rc.json assert 'logs' in rc1.json()
# Using a fixed comparison here would make this test fail! # Using a fixed comparison here would make this test fail!
assert rc.json['log_count'] == 5 if rc1.json()['log_count'] < 5:
assert len(rc.json['logs']) == rc.json['log_count'] # Help debugging random test failure
print(f"rc={rc.json()}")
print(f"rc1={rc1.json()}")
assert rc1.json()['log_count'] == 5
assert len(rc1.json()['logs']) == rc1.json()['log_count']
def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
@ -493,7 +549,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
) )
rc = client_get(client, f"{BASE_URI}/edge") rc = client_get(client, f"{BASE_URI}/edge")
assert_response(rc, 502) assert_response(rc, 502)
assert rc.json == {"error": "Error querying _edge: Edge is not enabled."} assert rc.json() == {"error": "Error querying /api/v1/edge: Edge is not enabled."}
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
@ -510,7 +566,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li
rc = client_get(client, f"{BASE_URI}/profit") rc = client_get(client, f"{BASE_URI}/profit")
assert_response(rc, 200) assert_response(rc, 200)
assert rc.json['trade_count'] == 0 assert rc.json()['trade_count'] == 0
ftbot.enter_positions() ftbot.enter_positions()
trade = Trade.query.first() trade = Trade.query.first()
@ -520,9 +576,9 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li
rc = client_get(client, f"{BASE_URI}/profit") rc = client_get(client, f"{BASE_URI}/profit")
assert_response(rc, 200) assert_response(rc, 200)
# One open trade # One open trade
assert rc.json['trade_count'] == 1 assert rc.json()['trade_count'] == 1
assert rc.json['best_pair'] == '' assert rc.json()['best_pair'] == ''
assert rc.json['best_rate'] == 0 assert rc.json()['best_rate'] == 0
trade = Trade.query.first() trade = Trade.query.first()
trade.update(limit_sell_order) trade.update(limit_sell_order)
@ -532,7 +588,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li
rc = client_get(client, f"{BASE_URI}/profit") rc = client_get(client, f"{BASE_URI}/profit")
assert_response(rc) assert_response(rc)
assert rc.json == {'avg_duration': '0:00:00', assert rc.json() == {'avg_duration': '0:00:00',
'best_pair': 'ETH/BTC', 'best_pair': 'ETH/BTC',
'best_rate': 6.2, 'best_rate': 6.2,
'first_trade_date': 'just now', 'first_trade_date': 'just now',
@ -574,19 +630,19 @@ def test_api_stats(botclient, mocker, ticker, fee, markets,):
rc = client_get(client, f"{BASE_URI}/stats") rc = client_get(client, f"{BASE_URI}/stats")
assert_response(rc, 200) assert_response(rc, 200)
assert 'durations' in rc.json assert 'durations' in rc.json()
assert 'sell_reasons' in rc.json assert 'sell_reasons' in rc.json()
create_mock_trades(fee) create_mock_trades(fee)
rc = client_get(client, f"{BASE_URI}/stats") rc = client_get(client, f"{BASE_URI}/stats")
assert_response(rc, 200) assert_response(rc, 200)
assert 'durations' in rc.json assert 'durations' in rc.json()
assert 'sell_reasons' in rc.json assert 'sell_reasons' in rc.json()
assert 'wins' in rc.json['durations'] assert 'wins' in rc.json()['durations']
assert 'losses' in rc.json['durations'] assert 'losses' in rc.json()['durations']
assert 'draws' in rc.json['durations'] assert 'draws' in rc.json()['durations']
def test_api_performance(botclient, mocker, ticker, fee): def test_api_performance(botclient, mocker, ticker, fee):
@ -627,8 +683,8 @@ def test_api_performance(botclient, mocker, ticker, fee):
rc = client_get(client, f"{BASE_URI}/performance") rc = client_get(client, f"{BASE_URI}/performance")
assert_response(rc) assert_response(rc)
assert len(rc.json) == 2 assert len(rc.json()) == 2
assert rc.json == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61}, assert rc.json() == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61},
{'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57}] {'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57}]
@ -645,17 +701,19 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
rc = client_get(client, f"{BASE_URI}/status") rc = client_get(client, f"{BASE_URI}/status")
assert_response(rc, 200) assert_response(rc, 200)
assert rc.json == [] assert rc.json() == []
ftbot.enter_positions() ftbot.enter_positions()
trades = Trade.get_open_trades() trades = Trade.get_open_trades()
trades[0].open_order_id = None trades[0].open_order_id = None
ftbot.exit_positions(trades) ftbot.exit_positions(trades)
Trade.session.flush()
rc = client_get(client, f"{BASE_URI}/status") rc = client_get(client, f"{BASE_URI}/status")
assert_response(rc) assert_response(rc)
assert len(rc.json) == 1 assert len(rc.json()) == 1
assert rc.json == [{'amount': 91.07468123, assert rc.json() == [{
'amount': 91.07468123,
'amount_requested': 91.07468123, 'amount_requested': 91.07468123,
'base_currency': 'BTC', 'base_currency': 'BTC',
'close_date': None, 'close_date': None,
@ -722,7 +780,7 @@ def test_api_version(botclient):
rc = client_get(client, f"{BASE_URI}/version") rc = client_get(client, f"{BASE_URI}/version")
assert_response(rc) assert_response(rc)
assert rc.json == {"version": __version__} assert rc.json() == {"version": __version__}
def test_api_blacklist(botclient, mocker): def test_api_blacklist(botclient, mocker):
@ -730,7 +788,9 @@ def test_api_blacklist(botclient, mocker):
rc = client_get(client, f"{BASE_URI}/blacklist") rc = client_get(client, f"{BASE_URI}/blacklist")
assert_response(rc) assert_response(rc)
assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC"], # DOGE and HOT are not in the markets mock!
assert rc.json() == {"blacklist": ["DOGE/BTC", "HOT/BTC"],
"blacklist_expanded": [],
"length": 2, "length": 2,
"method": ["StaticPairList"], "method": ["StaticPairList"],
"errors": {}, "errors": {},
@ -740,21 +800,34 @@ def test_api_blacklist(botclient, mocker):
rc = client_post(client, f"{BASE_URI}/blacklist", rc = client_post(client, f"{BASE_URI}/blacklist",
data='{"blacklist": ["ETH/BTC"]}') data='{"blacklist": ["ETH/BTC"]}')
assert_response(rc) assert_response(rc)
assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC"], assert rc.json() == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC"],
"blacklist_expanded": ["ETH/BTC"],
"length": 3, "length": 3,
"method": ["StaticPairList"], "method": ["StaticPairList"],
"errors": {}, "errors": {},
} }
rc = client_post(client, f"{BASE_URI}/blacklist",
data='{"blacklist": ["XRP/.*"]}')
assert_response(rc)
assert rc.json() == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC", "XRP/.*"],
"blacklist_expanded": ["ETH/BTC", "XRP/BTC"],
"length": 4,
"method": ["StaticPairList"],
"errors": {},
}
def test_api_whitelist(botclient): def test_api_whitelist(botclient):
ftbot, client = botclient ftbot, client = botclient
rc = client_get(client, f"{BASE_URI}/whitelist") rc = client_get(client, f"{BASE_URI}/whitelist")
assert_response(rc) assert_response(rc)
assert rc.json == {"whitelist": ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'], assert rc.json() == {
"whitelist": ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'],
"length": 4, "length": 4,
"method": ["StaticPairList"]} "method": ["StaticPairList"]
}
def test_api_forcebuy(botclient, mocker, fee): def test_api_forcebuy(botclient, mocker, fee):
@ -763,7 +836,7 @@ def test_api_forcebuy(botclient, mocker, fee):
rc = client_post(client, f"{BASE_URI}/forcebuy", rc = client_post(client, f"{BASE_URI}/forcebuy",
data='{"pair": "ETH/BTC"}') data='{"pair": "ETH/BTC"}')
assert_response(rc, 502) assert_response(rc, 502)
assert rc.json == {"error": "Error querying _forcebuy: Forcebuy not enabled."} assert rc.json() == {"error": "Error querying /api/v1/forcebuy: Forcebuy not enabled."}
# enable forcebuy # enable forcebuy
ftbot.config['forcebuy_enable'] = True ftbot.config['forcebuy_enable'] = True
@ -773,9 +846,9 @@ def test_api_forcebuy(botclient, mocker, fee):
rc = client_post(client, f"{BASE_URI}/forcebuy", rc = client_post(client, f"{BASE_URI}/forcebuy",
data='{"pair": "ETH/BTC"}') data='{"pair": "ETH/BTC"}')
assert_response(rc) assert_response(rc)
assert rc.json == {"status": "Error buying pair ETH/BTC."} assert rc.json() == {"status": "Error buying pair ETH/BTC."}
# Test creating trae # Test creating trade
fbuy_mock = MagicMock(return_value=Trade( fbuy_mock = MagicMock(return_value=Trade(
pair='ETH/ETH', pair='ETH/ETH',
amount=1, amount=1,
@ -789,15 +862,19 @@ def test_api_forcebuy(botclient, mocker, fee):
fee_close=fee.return_value, fee_close=fee.return_value,
fee_open=fee.return_value, fee_open=fee.return_value,
close_rate=0.265441, close_rate=0.265441,
id=22,
timeframe=5,
strategy="DefaultStrategy"
)) ))
mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock) mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock)
rc = client_post(client, f"{BASE_URI}/forcebuy", rc = client_post(client, f"{BASE_URI}/forcebuy",
data='{"pair": "ETH/BTC"}') data='{"pair": "ETH/BTC"}')
assert_response(rc) assert_response(rc)
assert rc.json == {'amount': 1, assert rc.json() == {
'amount': 1,
'amount_requested': 1, 'amount_requested': 1,
'trade_id': None, 'trade_id': 22,
'close_date': None, 'close_date': None,
'close_date_hum': None, 'close_date_hum': None,
'close_timestamp': None, 'close_timestamp': None,
@ -838,8 +915,8 @@ def test_api_forcebuy(botclient, mocker, fee):
'open_trade_value': 0.24605460, 'open_trade_value': 0.24605460,
'sell_reason': None, 'sell_reason': None,
'sell_order_status': None, 'sell_order_status': None,
'strategy': None, 'strategy': 'DefaultStrategy',
'timeframe': None, 'timeframe': 5,
'exchange': 'bittrex', 'exchange': 'bittrex',
} }
@ -858,14 +935,14 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets):
rc = client_post(client, f"{BASE_URI}/forcesell", rc = client_post(client, f"{BASE_URI}/forcesell",
data='{"tradeid": "1"}') data='{"tradeid": "1"}')
assert_response(rc, 502) assert_response(rc, 502)
assert rc.json == {"error": "Error querying _forcesell: invalid argument"} assert rc.json() == {"error": "Error querying /api/v1/forcesell: invalid argument"}
ftbot.enter_positions() ftbot.enter_positions()
rc = client_post(client, f"{BASE_URI}/forcesell", rc = client_post(client, f"{BASE_URI}/forcesell",
data='{"tradeid": "1"}') data='{"tradeid": "1"}')
assert_response(rc) assert_response(rc)
assert rc.json == {'result': 'Created sell order for trade 1.'} assert rc.json() == {'result': 'Created sell order for trade 1.'}
def test_api_pair_candles(botclient, ohlcv_history): def test_api_pair_candles(botclient, ohlcv_history):
@ -876,22 +953,22 @@ def test_api_pair_candles(botclient, ohlcv_history):
# No pair # No pair
rc = client_get(client, rc = client_get(client,
f"{BASE_URI}/pair_candles?limit={amount}&timeframe={timeframe}") f"{BASE_URI}/pair_candles?limit={amount}&timeframe={timeframe}")
assert_response(rc, 400) assert_response(rc, 422)
# No timeframe # No timeframe
rc = client_get(client, rc = client_get(client,
f"{BASE_URI}/pair_candles?pair=XRP%2FBTC") f"{BASE_URI}/pair_candles?pair=XRP%2FBTC")
assert_response(rc, 400) assert_response(rc, 422)
rc = client_get(client, rc = client_get(client,
f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}") f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}")
assert_response(rc) assert_response(rc)
assert 'columns' in rc.json assert 'columns' in rc.json()
assert 'data_start_ts' in rc.json assert 'data_start_ts' in rc.json()
assert 'data_start' in rc.json assert 'data_start' in rc.json()
assert 'data_stop' in rc.json assert 'data_stop' in rc.json()
assert 'data_stop_ts' in rc.json assert 'data_stop_ts' in rc.json()
assert len(rc.json['data']) == 0 assert len(rc.json()['data']) == 0
ohlcv_history['sma'] = ohlcv_history['close'].rolling(2).mean() ohlcv_history['sma'] = ohlcv_history['close'].rolling(2).mean()
ohlcv_history['buy'] = 0 ohlcv_history['buy'] = 0
ohlcv_history.loc[1, 'buy'] = 1 ohlcv_history.loc[1, 'buy'] = 1
@ -902,34 +979,34 @@ def test_api_pair_candles(botclient, ohlcv_history):
rc = client_get(client, rc = client_get(client,
f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}") f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}")
assert_response(rc) assert_response(rc)
assert 'strategy' in rc.json assert 'strategy' in rc.json()
assert rc.json['strategy'] == 'DefaultStrategy' assert rc.json()['strategy'] == 'DefaultStrategy'
assert 'columns' in rc.json assert 'columns' in rc.json()
assert 'data_start_ts' in rc.json assert 'data_start_ts' in rc.json()
assert 'data_start' in rc.json assert 'data_start' in rc.json()
assert 'data_stop' in rc.json assert 'data_stop' in rc.json()
assert 'data_stop_ts' in rc.json assert 'data_stop_ts' in rc.json()
assert rc.json['data_start'] == '2017-11-26 08:50:00+00:00' assert rc.json()['data_start'] == '2017-11-26 08:50:00+00:00'
assert rc.json['data_start_ts'] == 1511686200000 assert rc.json()['data_start_ts'] == 1511686200000
assert rc.json['data_stop'] == '2017-11-26 09:00:00+00:00' assert rc.json()['data_stop'] == '2017-11-26 09:00:00+00:00'
assert rc.json['data_stop_ts'] == 1511686800000 assert rc.json()['data_stop_ts'] == 1511686800000
assert isinstance(rc.json['columns'], list) assert isinstance(rc.json()['columns'], list)
assert rc.json['columns'] == ['date', 'open', 'high', assert rc.json()['columns'] == ['date', 'open', 'high',
'low', 'close', 'volume', 'sma', 'buy', 'sell', 'low', 'close', 'volume', 'sma', 'buy', 'sell',
'__date_ts', '_buy_signal_open', '_sell_signal_open'] '__date_ts', '_buy_signal_open', '_sell_signal_open']
assert 'pair' in rc.json assert 'pair' in rc.json()
assert rc.json['pair'] == 'XRP/BTC' assert rc.json()['pair'] == 'XRP/BTC'
assert 'data' in rc.json assert 'data' in rc.json()
assert len(rc.json['data']) == amount assert len(rc.json()['data']) == amount
assert (rc.json['data'] == assert (rc.json()['data'] ==
[['2017-11-26 08:50:00', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869, [['2017-11-26 08:50:00', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869,
None, 0, 0, 1511686200000, None, None], None, 0, 0, 1511686200000, None, None],
['2017-11-26 08:55:00', 8.88e-05, 8.942e-05, 8.88e-05, ['2017-11-26 08:55:00', 8.88e-05, 8.942e-05, 8.88e-05,
8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0, 1511686500000, 8.88e-05, None], 8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0, 1511686500000, 8.88e-05, None],
['2017-11-26 09:00:00', 8.891e-05, 8.893e-05, 8.875e-05, 8.877e-05, ['2017-11-26 09:00:00', 8.891e-05, 8.893e-05, 8.875e-05, 8.877e-05,
0.7039405, 8.885000000000002e-05, 0, 0, 1511686800000, None, None] 0.7039405, 8.885e-05, 0, 0, 1511686800000, None, None]
]) ])
@ -942,41 +1019,49 @@ def test_api_pair_history(botclient, ohlcv_history):
rc = client_get(client, rc = client_get(client,
f"{BASE_URI}/pair_history?timeframe={timeframe}" f"{BASE_URI}/pair_history?timeframe={timeframe}"
"&timerange=20180111-20180112&strategy=DefaultStrategy") "&timerange=20180111-20180112&strategy=DefaultStrategy")
assert_response(rc, 400) assert_response(rc, 422)
# No Timeframe # No Timeframe
rc = client_get(client, rc = client_get(client,
f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC" f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC"
"&timerange=20180111-20180112&strategy=DefaultStrategy") "&timerange=20180111-20180112&strategy=DefaultStrategy")
assert_response(rc, 400) assert_response(rc, 422)
# No timerange # No timerange
rc = client_get(client, rc = client_get(client,
f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}"
"&strategy=DefaultStrategy") "&strategy=DefaultStrategy")
assert_response(rc, 400) assert_response(rc, 422)
# No strategy # No strategy
rc = client_get(client, rc = client_get(client,
f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}"
"&timerange=20180111-20180112") "&timerange=20180111-20180112")
assert_response(rc, 400) assert_response(rc, 422)
# Working # Working
rc = client_get(client, rc = client_get(client,
f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}"
"&timerange=20180111-20180112&strategy=DefaultStrategy") "&timerange=20180111-20180112&strategy=DefaultStrategy")
assert_response(rc, 200) assert_response(rc, 200)
assert rc.json['length'] == 289 assert rc.json()['length'] == 289
assert len(rc.json['data']) == rc.json['length'] assert len(rc.json()['data']) == rc.json()['length']
assert 'columns' in rc.json assert 'columns' in rc.json()
assert 'data' in rc.json assert 'data' in rc.json()
assert rc.json['pair'] == 'UNITTEST/BTC' assert rc.json()['pair'] == 'UNITTEST/BTC'
assert rc.json['strategy'] == 'DefaultStrategy' assert rc.json()['strategy'] == 'DefaultStrategy'
assert rc.json['data_start'] == '2018-01-11 00:00:00+00:00' assert rc.json()['data_start'] == '2018-01-11 00:00:00+00:00'
assert rc.json['data_start_ts'] == 1515628800000 assert rc.json()['data_start_ts'] == 1515628800000
assert rc.json['data_stop'] == '2018-01-12 00:00:00+00:00' assert rc.json()['data_stop'] == '2018-01-12 00:00:00+00:00'
assert rc.json['data_stop_ts'] == 1515715200000 assert rc.json()['data_stop_ts'] == 1515715200000
# No data found
rc = client_get(client,
f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}"
"&timerange=20200111-20200112&strategy=DefaultStrategy")
assert_response(rc, 502)
assert rc.json()['error'] == ("Error querying /api/v1/pair_history: "
"No data for UNITTEST/BTC, 5m in 20200111-20200112 found.")
def test_api_plot_config(botclient): def test_api_plot_config(botclient):
@ -984,14 +1069,14 @@ def test_api_plot_config(botclient):
rc = client_get(client, f"{BASE_URI}/plot_config") rc = client_get(client, f"{BASE_URI}/plot_config")
assert_response(rc) assert_response(rc)
assert rc.json == {} assert rc.json() == {}
ftbot.strategy.plot_config = {'main_plot': {'sma': {}}, ftbot.strategy.plot_config = {'main_plot': {'sma': {}},
'subplots': {'RSI': {'rsi': {'color': 'red'}}}} 'subplots': {'RSI': {'rsi': {'color': 'red'}}}}
rc = client_get(client, f"{BASE_URI}/plot_config") rc = client_get(client, f"{BASE_URI}/plot_config")
assert_response(rc) assert_response(rc)
assert rc.json == ftbot.strategy.plot_config assert rc.json() == ftbot.strategy.plot_config
assert isinstance(rc.json['main_plot'], dict) assert isinstance(rc.json()['main_plot'], dict)
def test_api_strategies(botclient): def test_api_strategies(botclient):
@ -1000,7 +1085,7 @@ def test_api_strategies(botclient):
rc = client_get(client, f"{BASE_URI}/strategies") rc = client_get(client, f"{BASE_URI}/strategies")
assert_response(rc) assert_response(rc)
assert rc.json == {'strategies': ['DefaultStrategy', 'TestStrategyLegacy']} assert rc.json() == {'strategies': ['DefaultStrategy', 'TestStrategyLegacy']}
def test_api_strategy(botclient): def test_api_strategy(botclient):
@ -1009,10 +1094,10 @@ def test_api_strategy(botclient):
rc = client_get(client, f"{BASE_URI}/strategy/DefaultStrategy") rc = client_get(client, f"{BASE_URI}/strategy/DefaultStrategy")
assert_response(rc) assert_response(rc)
assert rc.json['strategy'] == 'DefaultStrategy' assert rc.json()['strategy'] == 'DefaultStrategy'
data = (Path(__file__).parents[1] / "strategy/strats/default_strategy.py").read_text() data = (Path(__file__).parents[1] / "strategy/strats/default_strategy.py").read_text()
assert rc.json['code'] == data assert rc.json()['code'] == data
rc = client_get(client, f"{BASE_URI}/strategy/NoStrat") rc = client_get(client, f"{BASE_URI}/strategy/NoStrat")
assert_response(rc, 404) assert_response(rc, 404)
@ -1024,21 +1109,21 @@ def test_list_available_pairs(botclient):
rc = client_get(client, f"{BASE_URI}/available_pairs") rc = client_get(client, f"{BASE_URI}/available_pairs")
assert_response(rc) assert_response(rc)
assert rc.json['length'] == 12 assert rc.json()['length'] == 12
assert isinstance(rc.json['pairs'], list) assert isinstance(rc.json()['pairs'], list)
rc = client_get(client, f"{BASE_URI}/available_pairs?timeframe=5m") rc = client_get(client, f"{BASE_URI}/available_pairs?timeframe=5m")
assert_response(rc) assert_response(rc)
assert rc.json['length'] == 12 assert rc.json()['length'] == 12
rc = client_get(client, f"{BASE_URI}/available_pairs?stake_currency=ETH") rc = client_get(client, f"{BASE_URI}/available_pairs?stake_currency=ETH")
assert_response(rc) assert_response(rc)
assert rc.json['length'] == 1 assert rc.json()['length'] == 1
assert rc.json['pairs'] == ['XRP/ETH'] assert rc.json()['pairs'] == ['XRP/ETH']
assert len(rc.json['pair_interval']) == 2 assert len(rc.json()['pair_interval']) == 2
rc = client_get(client, f"{BASE_URI}/available_pairs?stake_currency=ETH&timeframe=5m") rc = client_get(client, f"{BASE_URI}/available_pairs?stake_currency=ETH&timeframe=5m")
assert_response(rc) assert_response(rc)
assert rc.json['length'] == 1 assert rc.json()['length'] == 1
assert rc.json['pairs'] == ['XRP/ETH'] assert rc.json()['pairs'] == ['XRP/ETH']
assert len(rc.json['pair_interval']) == 1 assert len(rc.json()['pair_interval']) == 1

View File

@ -160,7 +160,7 @@ def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None:
def test_init_apiserver_disabled(mocker, default_conf, caplog) -> None: def test_init_apiserver_disabled(mocker, default_conf, caplog) -> None:
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
run_mock = MagicMock() run_mock = MagicMock()
mocker.patch('freqtrade.rpc.api_server.ApiServer.run', run_mock) mocker.patch('freqtrade.rpc.api_server.ApiServer.start_api', run_mock)
default_conf['telegram']['enabled'] = False default_conf['telegram']['enabled'] = False
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
@ -172,7 +172,7 @@ def test_init_apiserver_disabled(mocker, default_conf, caplog) -> None:
def test_init_apiserver_enabled(mocker, default_conf, caplog) -> None: def test_init_apiserver_enabled(mocker, default_conf, caplog) -> None:
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
run_mock = MagicMock() run_mock = MagicMock()
mocker.patch('freqtrade.rpc.api_server.ApiServer.run', run_mock) mocker.patch('freqtrade.rpc.api_server.ApiServer.start_api', run_mock)
default_conf["telegram"]["enabled"] = False default_conf["telegram"]["enabled"] = False
default_conf["api_server"] = {"enabled": True, default_conf["api_server"] = {"enabled": True,

View File

@ -205,13 +205,14 @@ def test_telegram_status(default_conf, update, mocker) -> None:
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
context = MagicMock() context = MagicMock()
# /status table 2 3 # /status table
context.args = ["table", "2", "3"] context.args = ["table"]
telegram._status(update=update, context=context) telegram._status(update=update, context=context)
assert status_table.call_count == 1 assert status_table.call_count == 1
def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: def test_status_handle(default_conf, update, ticker, fee, mocker) -> None:
default_conf['max_open_trades'] = 3
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker, fetch_ticker=ticker,
@ -252,8 +253,23 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None:
assert 'Close Rate' not in ''.join(lines) assert 'Close Rate' not in ''.join(lines)
assert 'Close Profit' not in ''.join(lines) assert 'Close Profit' not in ''.join(lines)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 3
assert 'ETH/BTC' in msg_mock.call_args_list[0][0][0] assert 'ETH/BTC' in msg_mock.call_args_list[0][0][0]
assert 'LTC/BTC' in msg_mock.call_args_list[1][0][0]
msg_mock.reset_mock()
context = MagicMock()
context.args = ["2", "3"]
telegram._status(update=update, context=context)
lines = msg_mock.call_args_list[0][0][0].split('\n')
assert '' not in lines
assert 'Close Rate' not in ''.join(lines)
assert 'Close Profit' not in ''.join(lines)
assert msg_mock.call_count == 2
assert 'LTC/BTC' in msg_mock.call_args_list[0][0][0]
def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None:
@ -1011,15 +1027,13 @@ def test_blacklist_static(default_conf, update, mocker) -> None:
msg_mock.reset_mock() msg_mock.reset_mock()
context = MagicMock() context = MagicMock()
context.args = ["ETH/ETH"] context.args = ["XRP/.*"]
telegram._blacklist(update=update, context=context) telegram._blacklist(update=update, context=context)
assert msg_mock.call_count == 2 assert msg_mock.call_count == 1
assert ("Error adding `ETH/ETH` to blacklist: `Pair ETH/ETH does not match stake currency.`"
in msg_mock.call_args_list[0][0][0])
assert ("Blacklist contains 3 pairs\n`DOGE/BTC, HOT/BTC, ETH/BTC`" assert ("Blacklist contains 4 pairs\n`DOGE/BTC, HOT/BTC, ETH/BTC, XRP/.*`"
in msg_mock.call_args_list[1][0][0]) in msg_mock.call_args_list[0][0][0])
assert freqtradebot.pairlists.blacklist == ["DOGE/BTC", "HOT/BTC", "ETH/BTC"] assert freqtradebot.pairlists.blacklist == ["DOGE/BTC", "HOT/BTC", "ETH/BTC", "XRP/.*"]
def test_telegram_logs(default_conf, update, mocker) -> None: def test_telegram_logs(default_conf, update, mocker) -> None:

View File

@ -1,5 +1,9 @@
from datetime import datetime
from pandas import DataFrame from pandas import DataFrame
from freqtrade.persistence.models import Trade
from .strats.default_strategy import DefaultStrategy from .strats.default_strategy import DefaultStrategy
@ -12,7 +16,7 @@ def test_default_strategy_structure():
assert hasattr(DefaultStrategy, 'populate_sell_trend') assert hasattr(DefaultStrategy, 'populate_sell_trend')
def test_default_strategy(result): def test_default_strategy(result, fee):
strategy = DefaultStrategy({}) strategy = DefaultStrategy({})
metadata = {'pair': 'ETH/BTC'} metadata = {'pair': 'ETH/BTC'}
@ -23,3 +27,18 @@ def test_default_strategy(result):
assert type(indicators) is DataFrame assert type(indicators) is DataFrame
assert type(strategy.populate_buy_trend(indicators, metadata)) is DataFrame assert type(strategy.populate_buy_trend(indicators, metadata)) is DataFrame
assert type(strategy.populate_sell_trend(indicators, metadata)) is DataFrame assert type(strategy.populate_sell_trend(indicators, metadata)) is DataFrame
trade = Trade(
open_rate=19_000,
amount=0.1,
pair='ETH/BTC',
fee_open=fee.return_value
)
assert strategy.confirm_trade_entry(pair='ETH/BTC', order_type='limit', amount=0.1,
rate=20000, time_in_force='gtc') is True
assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=trade, order_type='limit', amount=0.1,
rate=20000, time_in_force='gtc', sell_reason='roi') is True
assert strategy.custom_stoploss(pair='ETH/BTC', trade=trade, current_time=datetime.now(),
current_rate=20_000, current_profit=0.05) == strategy.stoploss

View File

@ -13,6 +13,7 @@ from freqtrade.data.history import load_data
from freqtrade.exceptions import StrategyError from freqtrade.exceptions import StrategyError
from freqtrade.persistence import PairLocks, Trade from freqtrade.persistence import PairLocks, Trade
from freqtrade.resolvers import StrategyResolver from freqtrade.resolvers import StrategyResolver
from freqtrade.strategy.interface import SellCheckTuple, SellType
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from tests.conftest import log_has, log_has_re from tests.conftest import log_has, log_has_re
@ -105,9 +106,29 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history):
assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog) assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog)
def test_assert_df_raise(default_conf, mocker, caplog, ohlcv_history): def test_ignore_expired_candle(default_conf):
# default_conf defines a 5m interval. we check interval * 2 + 5m default_conf.update({'strategy': 'DefaultStrategy'})
# this is necessary as the last candle is removed (partial candles) by default strategy = StrategyResolver.load_strategy(default_conf)
strategy.ignore_buying_expired_candle_after = 60
latest_date = datetime(2020, 12, 30, 7, 0, 0, tzinfo=timezone.utc)
# Add 1 candle length as the "latest date" defines candle open.
current_time = latest_date + timedelta(seconds=80 + 300)
assert strategy.ignore_expired_candle(latest_date=latest_date,
current_time=current_time,
timeframe_seconds=300,
buy=True) is True
current_time = latest_date + timedelta(seconds=30 + 300)
assert not strategy.ignore_expired_candle(latest_date=latest_date,
current_time=current_time,
timeframe_seconds=300,
buy=True) is True
def test_assert_df_raise(mocker, caplog, ohlcv_history):
ohlcv_history.loc[1, 'date'] = arrow.utcnow().shift(minutes=-16) ohlcv_history.loc[1, 'date'] = arrow.utcnow().shift(minutes=-16)
# Take a copy to correctly modify the call # Take a copy to correctly modify the call
mocked_history = ohlcv_history.copy() mocked_history = ohlcv_history.copy()
@ -127,7 +148,7 @@ def test_assert_df_raise(default_conf, mocker, caplog, ohlcv_history):
caplog) caplog)
def test_assert_df(default_conf, mocker, ohlcv_history, caplog): def test_assert_df(ohlcv_history, caplog):
df_len = len(ohlcv_history) - 1 df_len = len(ohlcv_history) - 1
# Ensure it's running when passed correctly # Ensure it's running when passed correctly
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
@ -288,6 +309,77 @@ def test_min_roi_reached3(default_conf, fee) -> None:
assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime) assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime)
@pytest.mark.parametrize(
'profit,adjusted,expected,trailing,custom,profit2,adjusted2,expected2,custom_stop', [
# Profit, adjusted stoploss(absolute), profit for 2nd call, enable trailing,
# enable custom stoploss, expected after 1st call, expected after 2nd call
(0.2, 0.9, SellType.NONE, False, False, 0.3, 0.9, SellType.NONE, None),
(0.2, 0.9, SellType.NONE, False, False, -0.2, 0.9, SellType.STOP_LOSS, None),
(0.2, 1.14, SellType.NONE, True, False, 0.05, 1.14, SellType.TRAILING_STOP_LOSS, None),
(0.01, 0.96, SellType.NONE, True, False, 0.05, 1, SellType.NONE, None),
(0.05, 1, SellType.NONE, True, False, -0.01, 1, SellType.TRAILING_STOP_LOSS, None),
# Default custom case - trails with 10%
(0.05, 0.95, SellType.NONE, False, True, -0.02, 0.95, SellType.NONE, None),
(0.05, 0.95, SellType.NONE, False, True, -0.06, 0.95, SellType.TRAILING_STOP_LOSS, None),
(0.05, 1, SellType.NONE, False, True, -0.06, 1, SellType.TRAILING_STOP_LOSS,
lambda **kwargs: -0.05),
(0.05, 1, SellType.NONE, False, True, 0.09, 1.04, SellType.NONE,
lambda **kwargs: -0.05),
(0.05, 0.95, SellType.NONE, False, True, 0.09, 0.98, SellType.NONE,
lambda current_profit, **kwargs: -0.1 if current_profit < 0.6 else -(current_profit * 2)),
# Error case - static stoploss in place
(0.05, 0.9, SellType.NONE, False, True, 0.09, 0.9, SellType.NONE,
lambda **kwargs: None),
])
def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, trailing, custom,
profit2, adjusted2, expected2, custom_stop) -> None:
default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf)
trade = Trade(
pair='ETH/BTC',
stake_amount=0.01,
amount=1,
open_date=arrow.utcnow().shift(hours=-1).datetime,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
open_rate=1,
)
trade.adjust_min_max_rates(trade.open_rate)
strategy.trailing_stop = trailing
strategy.trailing_stop_positive = -0.05
strategy.use_custom_stoploss = custom
original_stopvalue = strategy.custom_stoploss
if custom_stop:
strategy.custom_stoploss = custom_stop
now = arrow.utcnow().datetime
sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit), trade=trade,
current_time=now, current_profit=profit,
force_stoploss=0, high=None)
assert isinstance(sl_flag, SellCheckTuple)
assert sl_flag.sell_type == expected
if expected == SellType.NONE:
assert sl_flag.sell_flag is False
else:
assert sl_flag.sell_flag is True
assert round(trade.stop_loss, 2) == adjusted
sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit2), trade=trade,
current_time=now, current_profit=profit2,
force_stoploss=0, high=None)
assert sl_flag.sell_type == expected2
if expected2 == SellType.NONE:
assert sl_flag.sell_flag is False
else:
assert sl_flag.sell_flag is True
assert round(trade.stop_loss, 2) == adjusted2
strategy.custom_stoploss = original_stopvalue
def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None: def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None:
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
ind_mock = MagicMock(side_effect=lambda x, meta: x) ind_mock = MagicMock(side_effect=lambda x, meta: x)

View File

@ -1,5 +1,6 @@
import numpy as np import numpy as np
import pandas as pd import pandas as pd
import pytest
from freqtrade.strategy import merge_informative_pair, timeframe_to_minutes from freqtrade.strategy import merge_informative_pair, timeframe_to_minutes
@ -47,17 +48,17 @@ def test_merge_informative_pair():
assert 'volume_1h' in result.columns assert 'volume_1h' in result.columns
assert result['volume'].equals(data['volume']) assert result['volume'].equals(data['volume'])
# First 4 rows are empty # First 3 rows are empty
assert result.iloc[0]['date_1h'] is pd.NaT assert result.iloc[0]['date_1h'] is pd.NaT
assert result.iloc[1]['date_1h'] is pd.NaT assert result.iloc[1]['date_1h'] is pd.NaT
assert result.iloc[2]['date_1h'] is pd.NaT assert result.iloc[2]['date_1h'] is pd.NaT
assert result.iloc[3]['date_1h'] is pd.NaT
# Next 4 rows contain the starting date (0:00) # Next 4 rows contain the starting date (0:00)
assert result.iloc[3]['date_1h'] == result.iloc[0]['date']
assert result.iloc[4]['date_1h'] == result.iloc[0]['date'] assert result.iloc[4]['date_1h'] == result.iloc[0]['date']
assert result.iloc[5]['date_1h'] == result.iloc[0]['date'] assert result.iloc[5]['date_1h'] == result.iloc[0]['date']
assert result.iloc[6]['date_1h'] == result.iloc[0]['date'] assert result.iloc[6]['date_1h'] == result.iloc[0]['date']
assert result.iloc[7]['date_1h'] == result.iloc[0]['date']
# Next 4 rows contain the next Hourly date original date row 4 # Next 4 rows contain the next Hourly date original date row 4
assert result.iloc[7]['date_1h'] == result.iloc[4]['date']
assert result.iloc[8]['date_1h'] == result.iloc[4]['date'] assert result.iloc[8]['date_1h'] == result.iloc[4]['date']
@ -86,3 +87,11 @@ def test_merge_informative_pair_same():
# Dates match 1:1 # Dates match 1:1
assert result['date_15m'].equals(result['date']) assert result['date_15m'].equals(result['date'])
def test_merge_informative_pair_lower():
data = generate_test_data('1h', 40)
informative = generate_test_data('15m', 40)
with pytest.raises(ValueError, match=r"Tried to merge a faster timeframe .*"):
merge_informative_pair(data, informative, '1h', '15m', ffill=True)

View File

@ -172,7 +172,7 @@ def test_download_data_options() -> None:
def test_plot_dataframe_options() -> None: def test_plot_dataframe_options() -> None:
args = [ args = [
'plot-dataframe', 'plot-dataframe',
'-c', 'config.json.example', '-c', 'config_bittrex.json.example',
'--indicators1', 'sma10', 'sma100', '--indicators1', 'sma10', 'sma100',
'--indicators2', 'macd', 'fastd', 'fastk', '--indicators2', 'macd', 'fastd', 'fastk',
'--plot-limit', '30', '--plot-limit', '30',

View File

@ -3065,6 +3065,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy
default_conf['ask_strategy'] = { default_conf['ask_strategy'] = {
'use_sell_signal': True, 'use_sell_signal': True,
'sell_profit_only': True, 'sell_profit_only': True,
'sell_profit_offset': 0.1,
} }
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade) patch_get_signal(freqtrade)
@ -3076,7 +3077,11 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy
trade.update(limit_buy_order) trade.update(limit_buy_order)
freqtrade.wallets.update() freqtrade.wallets.update()
patch_get_signal(freqtrade, value=(False, True)) patch_get_signal(freqtrade, value=(False, True))
assert freqtrade.handle_trade(trade) is False
freqtrade.config['ask_strategy']['sell_profit_offset'] = 0.0
assert freqtrade.handle_trade(trade) is True assert freqtrade.handle_trade(trade) is True
assert trade.sell_reason == SellType.SELL_SIGNAL.value assert trade.sell_reason == SellType.SELL_SIGNAL.value
@ -4313,6 +4318,11 @@ def test_update_open_orders(mocker, default_conf, fee, caplog):
create_mock_trades(fee) create_mock_trades(fee)
freqtrade.update_open_orders() freqtrade.update_open_orders()
assert not log_has_re(r"Error updating Order .*", caplog)
freqtrade.config['dry_run'] = False
freqtrade.update_open_orders()
assert log_has_re(r"Error updating Order .*", caplog) assert log_has_re(r"Error updating Order .*", caplog)
caplog.clear() caplog.clear()
@ -4358,6 +4368,19 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee):
freqtrade.update_closed_trades_without_assigned_fees() freqtrade.update_closed_trades_without_assigned_fees()
# Does nothing for dry-run
trades = Trade.get_trades().all()
assert len(trades) == MOCK_TRADE_COUNT
for trade in trades:
assert trade.fee_open_cost is None
assert trade.fee_open_currency is None
assert trade.fee_close_cost is None
assert trade.fee_close_currency is None
freqtrade.config['dry_run'] = False
freqtrade.update_closed_trades_without_assigned_fees()
trades = Trade.get_trades().all() trades = Trade.get_trades().all()
assert len(trades) == MOCK_TRADE_COUNT assert len(trades) == MOCK_TRADE_COUNT

View File

@ -67,12 +67,12 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.init_db', MagicMock()) mocker.patch('freqtrade.freqtradebot.init_db', MagicMock())
args = ['trade', '-c', 'config.json.example'] args = ['trade', '-c', 'config_bittrex.json.example']
# Test Main + the KeyboardInterrupt exception # Test Main + the KeyboardInterrupt exception
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
main(args) main(args)
assert log_has('Using config: config.json.example ...', caplog) assert log_has('Using config: config_bittrex.json.example ...', caplog)
assert log_has('Fatal exception!', caplog) assert log_has('Fatal exception!', caplog)
@ -85,12 +85,12 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.wallets.Wallets.update', MagicMock()) mocker.patch('freqtrade.wallets.Wallets.update', MagicMock())
mocker.patch('freqtrade.freqtradebot.init_db', MagicMock()) mocker.patch('freqtrade.freqtradebot.init_db', MagicMock())
args = ['trade', '-c', 'config.json.example'] args = ['trade', '-c', 'config_bittrex.json.example']
# Test Main + the KeyboardInterrupt exception # Test Main + the KeyboardInterrupt exception
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
main(args) main(args)
assert log_has('Using config: config.json.example ...', caplog) assert log_has('Using config: config_bittrex.json.example ...', caplog)
assert log_has('SIGINT received, aborting ...', caplog) assert log_has('SIGINT received, aborting ...', caplog)
@ -106,12 +106,12 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.init_db', MagicMock()) mocker.patch('freqtrade.freqtradebot.init_db', MagicMock())
args = ['trade', '-c', 'config.json.example'] args = ['trade', '-c', 'config_bittrex.json.example']
# Test Main + the KeyboardInterrupt exception # Test Main + the KeyboardInterrupt exception
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
main(args) main(args)
assert log_has('Using config: config.json.example ...', caplog) assert log_has('Using config: config_bittrex.json.example ...', caplog)
assert log_has('Oh snap!', caplog) assert log_has('Oh snap!', caplog)
@ -157,12 +157,12 @@ def test_main_reload_config(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.init_db', MagicMock()) mocker.patch('freqtrade.freqtradebot.init_db', MagicMock())
args = Arguments(['trade', '-c', 'config.json.example']).get_parsed_arg() args = Arguments(['trade', '-c', 'config_bittrex.json.example']).get_parsed_arg()
worker = Worker(args=args, config=default_conf) worker = Worker(args=args, config=default_conf)
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
main(['trade', '-c', 'config.json.example']) main(['trade', '-c', 'config_bittrex.json.example'])
assert log_has('Using config: config.json.example ...', caplog) assert log_has('Using config: config_bittrex.json.example ...', caplog)
assert worker_mock.call_count == 4 assert worker_mock.call_count == 4
assert reconfigure_mock.call_count == 1 assert reconfigure_mock.call_count == 1
assert isinstance(worker.freqtrade, FreqtradeBot) assert isinstance(worker.freqtrade, FreqtradeBot)
@ -180,7 +180,7 @@ def test_reconfigure(mocker, default_conf) -> None:
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.init_db', MagicMock()) mocker.patch('freqtrade.freqtradebot.init_db', MagicMock())
args = Arguments(['trade', '-c', 'config.json.example']).get_parsed_arg() args = Arguments(['trade', '-c', 'config_bittrex.json.example']).get_parsed_arg()
worker = Worker(args=args, config=default_conf) worker = Worker(args=args, config=default_conf)
freqtrade = worker.freqtrade freqtrade = worker.freqtrade

View File

@ -47,14 +47,15 @@ def test_init_plotscript(default_conf, mocker, testdatadir):
default_conf['timeframe'] = "5m" default_conf['timeframe'] = "5m"
default_conf["datadir"] = testdatadir default_conf["datadir"] = testdatadir
default_conf['exportfilename'] = testdatadir / "backtest-result_test.json" default_conf['exportfilename'] = testdatadir / "backtest-result_test.json"
ret = init_plotscript(default_conf) supported_markets = ["TRX/BTC", "ADA/BTC"]
ret = init_plotscript(default_conf, supported_markets)
assert "ohlcv" in ret assert "ohlcv" in ret
assert "trades" in ret assert "trades" in ret
assert "pairs" in ret assert "pairs" in ret
assert 'timerange' in ret assert 'timerange' in ret
default_conf['pairs'] = ["TRX/BTC", "ADA/BTC"] default_conf['pairs'] = ["TRX/BTC", "ADA/BTC"]
ret = init_plotscript(default_conf, 20) ret = init_plotscript(default_conf, supported_markets, 20)
assert "ohlcv" in ret assert "ohlcv" in ret
assert "TRX/BTC" in ret["ohlcv"] assert "TRX/BTC" in ret["ohlcv"]
assert "ADA/BTC" in ret["ohlcv"] assert "ADA/BTC" in ret["ohlcv"]
@ -353,12 +354,16 @@ def test_generate_profit_graph(testdatadir):
profit_pair = find_trace_in_fig_data(figure.data, f"Profit {pair}") profit_pair = find_trace_in_fig_data(figure.data, f"Profit {pair}")
assert isinstance(profit_pair, go.Scatter) assert isinstance(profit_pair, go.Scatter)
with pytest.raises(OperationalException, match=r"No trades found.*"):
# Pair cannot be empty - so it's an empty dataframe.
generate_profit_graph(pairs, data, trades.loc[trades['pair'].isnull()], timeframe="5m")
def test_start_plot_dataframe(mocker): def test_start_plot_dataframe(mocker):
aup = mocker.patch("freqtrade.plot.plotting.load_and_plot_trades", MagicMock()) aup = mocker.patch("freqtrade.plot.plotting.load_and_plot_trades", MagicMock())
args = [ args = [
"plot-dataframe", "plot-dataframe",
"--config", "config.json.example", "--config", "config_bittrex.json.example",
"--pairs", "ETH/BTC" "--pairs", "ETH/BTC"
] ]
start_plot_dataframe(get_args(args)) start_plot_dataframe(get_args(args))
@ -402,7 +407,7 @@ def test_start_plot_profit(mocker):
aup = mocker.patch("freqtrade.plot.plotting.plot_profit", MagicMock()) aup = mocker.patch("freqtrade.plot.plotting.plot_profit", MagicMock())
args = [ args = [
"plot-profit", "plot-profit",
"--config", "config.json.example", "--config", "config_bittrex.json.example",
"--pairs", "ETH/BTC" "--pairs", "ETH/BTC"
] ]
start_plot_profit(get_args(args)) start_plot_profit(get_args(args))