Merge branch 'develop' into feat/new_config
This commit is contained in:
commit
abf10aec98
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-18.04, macos-latest ]
|
||||
python-version: [3.7]
|
||||
python-version: [3.7, 3.8]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
@ -68,7 +68,7 @@ jobs:
|
||||
pytest --random-order --cov=freqtrade --cov-config=.coveragerc
|
||||
|
||||
- name: Coveralls
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
if: (startsWith(matrix.os, 'ubuntu') && matrix.python-version == '3.8')
|
||||
env:
|
||||
# Coveralls token. Not used as secret due to github not providing secrets to forked repositories
|
||||
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM python:3.7.6-slim-stretch
|
||||
FROM python:3.8.1-slim-buster
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install curl build-essential libssl-dev \
|
||||
|
@ -62,8 +62,8 @@
|
||||
"refresh_period": 1800
|
||||
},
|
||||
{"method": "PrecisionFilter"},
|
||||
{"method": "PriceFilter", "low_price_ratio": 0.01
|
||||
}
|
||||
{"method": "PriceFilter", "low_price_ratio": 0.01},
|
||||
{"method": "SpreadFilter", "max_spread_ratio": 0.005}
|
||||
],
|
||||
"exchange": {
|
||||
"name": "bittrex",
|
||||
|
@ -237,8 +237,8 @@ There will be an additional table comparing win/losses of the different strategi
|
||||
Detailed output for all strategies one after the other will be available, so make sure to scroll up to see the details per strategy.
|
||||
|
||||
```
|
||||
=========================================================== Strategy Summary ===========================================================
|
||||
| Strategy | buy count | avg profit % | cum profit % | tot profit BTC | tot profit % | avg duration | profit | loss |
|
||||
=========================================================== STRATEGY SUMMARY ===========================================================
|
||||
| Strategy | Buy Count | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Losses |
|
||||
|:------------|------------:|---------------:|---------------:|-----------------:|---------------:|:---------------|---------:|-------:|
|
||||
| Strategy1 | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 243 |
|
||||
| Strategy2 | 1487 | -0.13 | -197.58 | -0.00988917 | -98.79 | 4:43:00 | 662 | 825 |
|
||||
|
@ -337,8 +337,8 @@ optional arguments:
|
||||
generate completely different results, since the
|
||||
target for optimization is different. Built-in
|
||||
Hyperopt-loss-functions are: DefaultHyperOptLoss,
|
||||
OnlyProfitHyperOptLoss, SharpeHyperOptLoss (default:
|
||||
`DefaultHyperOptLoss`).
|
||||
OnlyProfitHyperOptLoss, SharpeHyperOptLoss,
|
||||
SharpeHyperOptLossDaily (default: `DefaultHyperOptLoss`).
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
|
@ -278,7 +278,7 @@ If this is configured, the following 4 values (`buy`, `sell`, `stoploss` and
|
||||
The below is the default which is used if this is not configured in either strategy or configuration file.
|
||||
|
||||
Since `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price.
|
||||
`stoploss` defines the stop-price - and limit should be slightly below this. This defaults to 0.99 / 1%.
|
||||
`stoploss` defines the stop-price - and limit should be slightly below this. This defaults to 0.99 / 1% (configurable via `stoploss_on_exchange_limit_ratio`).
|
||||
Calculation example: we bought the asset at 100$.
|
||||
Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the stoploss will happen between 95$ and 94.05$.
|
||||
|
||||
@ -503,6 +503,7 @@ Inactive markets and blacklisted pairs are always removed from the resulting `pa
|
||||
* [`VolumePairList`](#volume-pair-list)
|
||||
* [`PrecisionFilter`](#precision-filter)
|
||||
* [`PriceFilter`](#price-pair-filter)
|
||||
* [`SpreadFilter`](#spread-filter)
|
||||
|
||||
!!! Tip "Testing pairlists"
|
||||
Pairlist configurations can be quite tricky to get right. Best use the [`test-pairlist`](utils.md#test-pairlist) subcommand to test your configuration quickly.
|
||||
@ -551,6 +552,11 @@ Min price precision is 8 decimals. If price is 0.00000011 - one step would be 0.
|
||||
|
||||
These pairs are dangerous since it may be impossible to place the desired stoploss - and often result in high losses.
|
||||
|
||||
#### Spread Filter
|
||||
Removes pairs that have a difference between asks and bids above the specified ratio (default `0.005`).
|
||||
Example:
|
||||
If `DOGE/BTC` maximum bid is 0.00000026 and minimum ask is 0.00000027 the ratio is calculated as: `1 - bid/ask ~= 0.037` which is `> 0.005`
|
||||
|
||||
### Full Pairlist example
|
||||
|
||||
The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting by `quoteVolume` and applies both [`PrecisionFilter`](#precision-filter) and [`PriceFilter`](#price-pair-filter), filtering all assets where 1 priceunit is > 1%.
|
||||
|
@ -5,7 +5,7 @@ This page combines common gotchas and informations which are exchange-specific a
|
||||
## Binance
|
||||
|
||||
!!! Tip "Stoploss on Exchange"
|
||||
Binance is currently the only exchange supporting `stoploss_on_exchange`. It provides great advantages, so we recommend to benefit from it.
|
||||
Binance supports `stoploss_on_exchange` and uses stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
||||
|
||||
### Blacklists
|
||||
|
||||
@ -22,6 +22,9 @@ Binance has been split into 3, and users must use the correct ccxt exchange ID f
|
||||
|
||||
## Kraken
|
||||
|
||||
!!! Tip "Stoploss on Exchange"
|
||||
Kraken supports `stoploss_on_exchange` and uses stop-loss-market orders. It provides great advantages, so we recommend to benefit from it, however since the resulting order is a stoploss-market order, sell-rates are not guaranteed, which makes this feature less secure than on other exchanges. This limitation is based on kraken's policy [source](https://blog.kraken.com/post/1234/announcement-delisting-pairs-and-temporary-suspension-of-advanced-order-types/) and [source2](https://blog.kraken.com/post/1494/kraken-enables-advanced-orders-and-adds-10-currency-pairs/) - which has stoploss-limit orders disabled.
|
||||
|
||||
### Historic Kraken data
|
||||
|
||||
The Kraken API does only provide 720 historic candles, which is sufficient for Freqtrade dry-run and live trade modes, but is a problem for backtesting.
|
||||
|
@ -75,8 +75,8 @@ Copy the file `user_data/hyperopts/sample_hyperopt.py` into `user_data/hyperopts
|
||||
|
||||
There are two places you need to change in your hyperopt file to add a new buy hyperopt for testing:
|
||||
|
||||
- Inside `indicator_space()` - the parameters hyperopt shall be optimizing.
|
||||
- Inside `populate_buy_trend()` - applying the parameters.
|
||||
* Inside `indicator_space()` - the parameters hyperopt shall be optimizing.
|
||||
* Inside `populate_buy_trend()` - applying the parameters.
|
||||
|
||||
There you have two different types of indicators: 1. `guards` and 2. `triggers`.
|
||||
|
||||
@ -192,6 +192,7 @@ Currently, the following loss functions are builtin:
|
||||
* `DefaultHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function)
|
||||
* `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration)
|
||||
* `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on the trade returns)
|
||||
* `SharpeHyperOptLossDaily` (optimizes Sharpe Ratio calculated on daily trade returns)
|
||||
|
||||
Creation of a custom loss function is covered in the [Advanced Hyperopt](advanced-hyperopt.md) part of the documentation.
|
||||
|
||||
@ -372,6 +373,7 @@ In order to use this best ROI table found by Hyperopt in backtesting and for liv
|
||||
118: 0
|
||||
}
|
||||
```
|
||||
|
||||
As stated in the comment, you can also use it as the value of the `minimal_roi` setting in the configuration file.
|
||||
|
||||
#### Default ROI Search Space
|
||||
@ -379,7 +381,7 @@ As stated in the comment, you can also use it as the value of the `minimal_roi`
|
||||
If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the ticker_interval used. By default the values vary in the following ranges (for some of the most used ticker intervals, values are rounded to 5 digits after the decimal point):
|
||||
|
||||
| # step | 1m | | 5m | | 1h | | 1d | |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| ------ | ------ | ----------------- | -------- | ----------- | ---------- | ----------------- | ------------ | ----------------- |
|
||||
| 1 | 0 | 0.01161...0.11992 | 0 | 0.03...0.31 | 0 | 0.06883...0.71124 | 0 | 0.12178...1.25835 |
|
||||
| 2 | 2...8 | 0.00774...0.04255 | 10...40 | 0.02...0.11 | 120...480 | 0.04589...0.25238 | 2880...11520 | 0.08118...0.44651 |
|
||||
| 3 | 4...20 | 0.00387...0.01547 | 20...100 | 0.01...0.04 | 240...1200 | 0.02294...0.09177 | 5760...28800 | 0.04059...0.16237 |
|
||||
@ -416,6 +418,7 @@ In order to use this best stoploss value found by Hyperopt in backtesting and fo
|
||||
# This attribute will be overridden if the config file contains "stoploss"
|
||||
stoploss = -0.27996
|
||||
```
|
||||
|
||||
As stated in the comment, you can also use it as the value of the `stoploss` setting in the configuration file.
|
||||
|
||||
#### Default Stoploss Search Space
|
||||
@ -452,6 +455,7 @@ In order to use these best trailing stop parameters found by Hyperopt in backtes
|
||||
trailing_stop_positive_offset = 0.06038
|
||||
trailing_only_offset_is_reached = True
|
||||
```
|
||||
|
||||
As stated in the comment, you can also use it as the values of the corresponding settings in the configuration file.
|
||||
|
||||
#### Default Trailing Stop Search Space
|
||||
|
@ -27,7 +27,7 @@ So this parameter will tell the bot how often it should update the stoploss orde
|
||||
This same logic will reapply a stoploss order on the exchange should you cancel it accidentally.
|
||||
|
||||
!!! Note
|
||||
Stoploss on exchange is only supported for Binance as of now.
|
||||
Stoploss on exchange is only supported for Binance (stop-loss-limit) and Kraken (stop-loss-market) as of now.
|
||||
|
||||
## Static Stop Loss
|
||||
|
||||
|
@ -141,9 +141,9 @@ With custom user directory
|
||||
freqtrade new-hyperopt --userdir ~/.freqtrade/ --hyperopt AwesomeHyperopt
|
||||
```
|
||||
|
||||
## List Strategies
|
||||
## List Strategies and List Hyperopts
|
||||
|
||||
Use the `list-strategies` subcommand to see all strategies in one particular directory.
|
||||
Use the `list-strategies` subcommand to see all strategies in one particular directory and the `list-hyperopts` subcommand to list custom Hyperopts.
|
||||
|
||||
```
|
||||
freqtrade list-strategies --help
|
||||
@ -166,22 +166,63 @@ Common arguments:
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
```
|
||||
```
|
||||
freqtrade list-hyperopts --help
|
||||
usage: freqtrade list-hyperopts [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||
[-d PATH] [--userdir PATH]
|
||||
[--hyperopt-path PATH] [-1]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
--hyperopt-path PATH Specify additional lookup path for Hyperopt and
|
||||
Hyperopt Loss functions.
|
||||
-1, --one-column Print output in one column.
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
--logfile FILE Log to the file specified. Special values are:
|
||||
'syslog', 'journald'. See the documentation for more
|
||||
details.
|
||||
-V, --version show program's version number and exit
|
||||
-c PATH, --config PATH
|
||||
Specify configuration file (default: `config.json`).
|
||||
Multiple --config options may be used. Can be set to
|
||||
`-` to read config from stdin.
|
||||
-d PATH, --datadir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
```
|
||||
|
||||
!!! Warning
|
||||
Using this command will try to load all python files from a directory. This can be a security risk if untrusted files reside in this directory, since all module-level code is executed.
|
||||
Using these commands will try to load all python files from a directory. This can be a security risk if untrusted files reside in this directory, since all module-level code is executed.
|
||||
|
||||
Example: search default strategy directory within userdir
|
||||
Example: Search default strategies and hyperopts directories (within the default userdir).
|
||||
|
||||
``` bash
|
||||
freqtrade list-strategies
|
||||
freqtrade list-hyperopts
|
||||
```
|
||||
|
||||
Example: Search strategies and hyperopts directory within the userdir.
|
||||
|
||||
``` bash
|
||||
freqtrade list-strategies --userdir ~/.freqtrade/
|
||||
freqtrade list-hyperopts --userdir ~/.freqtrade/
|
||||
```
|
||||
|
||||
Example: search dedicated strategy path
|
||||
Example: Search dedicated strategy path.
|
||||
|
||||
``` bash
|
||||
freqtrade list-strategies --strategy-path ~/.freqtrade/strategies/
|
||||
```
|
||||
|
||||
Example: Search dedicated hyperopt path.
|
||||
|
||||
``` bash
|
||||
freqtrade list-hyperopt --hyperopt-path ~/.freqtrade/hyperopts/
|
||||
```
|
||||
|
||||
## List Exchanges
|
||||
|
||||
Use the `list-exchanges` subcommand to see the exchanges available for the bot.
|
||||
|
@ -15,6 +15,7 @@ from freqtrade.commands.deploy_commands import (start_create_userdir,
|
||||
from freqtrade.commands.hyperopt_commands import (start_hyperopt_list,
|
||||
start_hyperopt_show)
|
||||
from freqtrade.commands.list_commands import (start_list_exchanges,
|
||||
start_list_hyperopts,
|
||||
start_list_markets,
|
||||
start_list_strategies,
|
||||
start_list_timeframes)
|
||||
|
@ -32,6 +32,8 @@ ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
|
||||
|
||||
ARGS_LIST_STRATEGIES = ["strategy_path", "print_one_column"]
|
||||
|
||||
ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column"]
|
||||
|
||||
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"]
|
||||
|
||||
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"]
|
||||
@ -66,8 +68,8 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop
|
||||
"print_json", "hyperopt_show_no_header"]
|
||||
|
||||
NO_CONF_REQURIED = ["download-data", "list-timeframes", "list-markets", "list-pairs",
|
||||
"list-strategies", "hyperopt-list", "hyperopt-show", "plot-dataframe",
|
||||
"plot-profit"]
|
||||
"list-strategies", "list-hyperopts", "hyperopt-list", "hyperopt-show",
|
||||
"plot-dataframe", "plot-profit"]
|
||||
|
||||
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-hyperopt", "new-strategy"]
|
||||
|
||||
@ -134,9 +136,9 @@ class Arguments:
|
||||
|
||||
from freqtrade.commands import (start_create_userdir, start_download_data,
|
||||
start_hyperopt_list, start_hyperopt_show,
|
||||
start_list_exchanges, start_list_markets,
|
||||
start_list_strategies, start_list_timeframes,
|
||||
start_new_config,
|
||||
start_list_exchanges, start_list_hyperopts,
|
||||
start_list_markets, start_list_strategies,
|
||||
start_list_timeframes, start_new_config,
|
||||
start_new_hyperopt, start_new_strategy,
|
||||
start_plot_dataframe, start_plot_profit,
|
||||
start_backtesting, start_hyperopt, start_edge,
|
||||
@ -207,6 +209,15 @@ class Arguments:
|
||||
list_strategies_cmd.set_defaults(func=start_list_strategies)
|
||||
self._build_args(optionlist=ARGS_LIST_STRATEGIES, parser=list_strategies_cmd)
|
||||
|
||||
# Add list-hyperopts subcommand
|
||||
list_hyperopts_cmd = subparsers.add_parser(
|
||||
'list-hyperopts',
|
||||
help='Print available hyperopt classes.',
|
||||
parents=[_common_parser],
|
||||
)
|
||||
list_hyperopts_cmd.set_defaults(func=start_list_hyperopts)
|
||||
self._build_args(optionlist=ARGS_LIST_HYPEROPTS, parser=list_hyperopts_cmd)
|
||||
|
||||
# Add list-exchanges subcommand
|
||||
list_exchanges_cmd = subparsers.add_parser(
|
||||
'list-exchanges',
|
||||
|
@ -256,7 +256,7 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). '
|
||||
'Different functions can generate completely different results, '
|
||||
'since the target for optimization is different. Built-in Hyperopt-loss-functions are: '
|
||||
'DefaultHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss.'
|
||||
'DefaultHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss, SharpeHyperOptLossDaily.'
|
||||
'(default: `%(default)s`).',
|
||||
metavar='NAME',
|
||||
default=constants.DEFAULT_HYPEROPT_LOSS,
|
||||
|
@ -43,16 +43,18 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
||||
if config.get('download_trades'):
|
||||
pairs_not_available = refresh_backtest_trades_data(
|
||||
exchange, pairs=config["pairs"], datadir=config['datadir'],
|
||||
timerange=timerange, erase=config.get("erase"))
|
||||
timerange=timerange, erase=bool(config.get("erase")))
|
||||
|
||||
# Convert downloaded trade data to different timeframes
|
||||
convert_trades_to_ohlcv(
|
||||
pairs=config["pairs"], timeframes=config["timeframes"],
|
||||
datadir=config['datadir'], timerange=timerange, erase=config.get("erase"))
|
||||
datadir=config['datadir'], timerange=timerange,
|
||||
erase=bool(config.get("erase")))
|
||||
else:
|
||||
pairs_not_available = refresh_backtest_ohlcv_data(
|
||||
exchange, pairs=config["pairs"], timeframes=config["timeframes"],
|
||||
datadir=config['datadir'], timerange=timerange, erase=config.get("erase"))
|
||||
datadir=config['datadir'], timerange=timerange,
|
||||
erase=bool(config.get("erase")))
|
||||
|
||||
except KeyboardInterrupt:
|
||||
sys.exit("SIGINT received, aborting ...")
|
||||
|
@ -6,7 +6,7 @@ from typing import Any, Dict
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.configuration.directory_operations import (copy_sample_files,
|
||||
create_userdata_dir)
|
||||
from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGY
|
||||
from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import render_template
|
||||
from freqtrade.state import RunMode
|
||||
@ -28,7 +28,7 @@ def start_create_userdir(args: Dict[str, Any]) -> None:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def deploy_new_strategy(strategy_name, strategy_path: Path, subtemplate: str):
|
||||
def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: str) -> None:
|
||||
"""
|
||||
Deploy new strategy from template to strategy_path
|
||||
"""
|
||||
@ -57,7 +57,7 @@ def start_new_strategy(args: Dict[str, Any]) -> None:
|
||||
if args["strategy"] == "DefaultStrategy":
|
||||
raise OperationalException("DefaultStrategy is not allowed as name.")
|
||||
|
||||
new_path = config['user_data_dir'] / USERPATH_STRATEGY / (args["strategy"] + ".py")
|
||||
new_path = config['user_data_dir'] / USERPATH_STRATEGIES / (args["strategy"] + ".py")
|
||||
|
||||
if new_path.exists():
|
||||
raise OperationalException(f"`{new_path}` already exists. "
|
||||
@ -69,7 +69,7 @@ def start_new_strategy(args: Dict[str, Any]) -> None:
|
||||
raise OperationalException("`new-strategy` requires --strategy to be set.")
|
||||
|
||||
|
||||
def deploy_new_hyperopt(hyperopt_name, hyperopt_path: Path, subtemplate: str):
|
||||
def deploy_new_hyperopt(hyperopt_name: str, hyperopt_path: Path, subtemplate: str) -> None:
|
||||
"""
|
||||
Deploys a new hyperopt template to hyperopt_path
|
||||
"""
|
||||
|
@ -9,7 +9,7 @@ import rapidjson
|
||||
from tabulate import tabulate
|
||||
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.constants import USERPATH_STRATEGY
|
||||
from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import (available_exchanges, ccxt_exchanges,
|
||||
market_is_active, symbol_is_pair)
|
||||
@ -38,11 +38,11 @@ def start_list_exchanges(args: Dict[str, Any]) -> None:
|
||||
|
||||
def start_list_strategies(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Print Strategies available in a directory
|
||||
Print files with Strategy custom classes available in the directory
|
||||
"""
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
directory = Path(config.get('strategy_path', config['user_data_dir'] / USERPATH_STRATEGY))
|
||||
directory = Path(config.get('strategy_path', config['user_data_dir'] / USERPATH_STRATEGIES))
|
||||
strategies = StrategyResolver.search_all_objects(directory)
|
||||
# Sort alphabetically
|
||||
strategies = sorted(strategies, key=lambda x: x['name'])
|
||||
@ -54,6 +54,26 @@ def start_list_strategies(args: Dict[str, Any]) -> None:
|
||||
print(tabulate(strats_to_print, headers='keys', tablefmt='pipe'))
|
||||
|
||||
|
||||
def start_list_hyperopts(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Print files with HyperOpt custom classes available in the directory
|
||||
"""
|
||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
directory = Path(config.get('hyperopt_path', config['user_data_dir'] / USERPATH_HYPEROPTS))
|
||||
hyperopts = HyperOptResolver.search_all_objects(directory)
|
||||
# Sort alphabetically
|
||||
hyperopts = sorted(hyperopts, key=lambda x: x['name'])
|
||||
hyperopts_to_print = [{'name': s['name'], 'location': s['location'].name} for s in hyperopts]
|
||||
|
||||
if args['print_one_column']:
|
||||
print('\n'.join([s['name'] for s in hyperopts]))
|
||||
else:
|
||||
print(tabulate(hyperopts_to_print, headers='keys', tablefmt='pipe'))
|
||||
|
||||
|
||||
def start_list_timeframes(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Print ticker intervals (timeframes) available on Exchange
|
||||
|
@ -5,7 +5,7 @@ from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
|
||||
def validate_plot_args(args: Dict[str, Any]):
|
||||
def validate_plot_args(args: Dict[str, Any]) -> None:
|
||||
if not args.get('datadir') and not args.get('config'):
|
||||
raise OperationalException(
|
||||
"You need to specify either `--datadir` or `--config` "
|
||||
|
@ -10,7 +10,7 @@ from freqtrade.state import RunMode
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def remove_credentials(config: Dict[str, Any]):
|
||||
def remove_credentials(config: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Removes exchange keys from the configuration and specifies dry-run
|
||||
Used for backtesting / hyperopt / edge and utils.
|
||||
|
@ -13,7 +13,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def check_conflicting_settings(config: Dict[str, Any],
|
||||
section1: str, name1: str,
|
||||
section2: str, name2: str):
|
||||
section2: str, name2: str) -> None:
|
||||
section1_config = config.get(section1, {})
|
||||
section2_config = config.get(section2, {})
|
||||
if name1 in section1_config and name2 in section2_config:
|
||||
@ -28,7 +28,7 @@ def check_conflicting_settings(config: Dict[str, Any],
|
||||
|
||||
def process_deprecated_setting(config: Dict[str, Any],
|
||||
section1: str, name1: str,
|
||||
section2: str, name2: str):
|
||||
section2: str, name2: str) -> None:
|
||||
section2_config = config.get(section2, {})
|
||||
|
||||
if name2 in section2_config:
|
||||
|
@ -23,7 +23,7 @@ def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> Pat
|
||||
return folder
|
||||
|
||||
|
||||
def create_userdata_dir(directory: str, create_dir=False) -> Path:
|
||||
def create_userdata_dir(directory: str, create_dir: bool = False) -> Path:
|
||||
"""
|
||||
Create userdata directory structure.
|
||||
if create_dir is True, then the parent-directory will be created if it does not exist.
|
||||
|
@ -7,6 +7,7 @@ from typing import Optional
|
||||
|
||||
import arrow
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -30,7 +31,7 @@ class TimeRange:
|
||||
return (self.starttype == other.starttype and self.stoptype == other.stoptype
|
||||
and self.startts == other.startts and self.stopts == other.stopts)
|
||||
|
||||
def subtract_start(self, seconds) -> None:
|
||||
def subtract_start(self, seconds: int) -> None:
|
||||
"""
|
||||
Subtracts <seconds> from startts if startts is set.
|
||||
:param seconds: Seconds to subtract from starttime
|
||||
@ -59,7 +60,7 @@ class TimeRange:
|
||||
self.starttype = 'date'
|
||||
|
||||
@staticmethod
|
||||
def parse_timerange(text: Optional[str]):
|
||||
def parse_timerange(text: Optional[str]) -> 'TimeRange':
|
||||
"""
|
||||
Parse the value of the argument --timerange to determine what is the range desired
|
||||
:param text: value from --timerange
|
||||
|
@ -17,20 +17,22 @@ REQUIRED_ORDERTIF = ['buy', 'sell']
|
||||
REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
|
||||
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
||||
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
|
||||
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'PrecisionFilter', 'PriceFilter']
|
||||
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
|
||||
'PrecisionFilter', 'PriceFilter', 'SpreadFilter']
|
||||
DRY_RUN_WALLET = 1000
|
||||
MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons
|
||||
|
||||
USERPATH_HYPEROPTS = 'hyperopts'
|
||||
USERPATH_STRATEGY = 'strategies'
|
||||
USERPATH_STRATEGIES = 'strategies'
|
||||
USERPATH_NOTEBOOKS = 'notebooks'
|
||||
|
||||
# Soure files with destination directories within user-directory
|
||||
USER_DATA_FILES = {
|
||||
'sample_strategy.py': USERPATH_STRATEGY,
|
||||
'sample_strategy.py': USERPATH_STRATEGIES,
|
||||
'sample_hyperopt_advanced.py': USERPATH_HYPEROPTS,
|
||||
'sample_hyperopt_loss.py': USERPATH_HYPEROPTS,
|
||||
'sample_hyperopt.py': USERPATH_HYPEROPTS,
|
||||
'strategy_analysis_example.ipynb': 'notebooks',
|
||||
'strategy_analysis_example.ipynb': USERPATH_NOTEBOOKS,
|
||||
}
|
||||
|
||||
SUPPORTED_FIAT = [
|
||||
|
@ -3,7 +3,7 @@ Helpers when analyzing backtest data
|
||||
"""
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
from typing import Dict, Union
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
@ -20,7 +20,7 @@ BT_DATA_COLUMNS = ["pair", "profitperc", "open_time", "close_time", "index", "du
|
||||
"open_rate", "close_rate", "open_at_end", "sell_reason"]
|
||||
|
||||
|
||||
def load_backtest_data(filename) -> pd.DataFrame:
|
||||
def load_backtest_data(filename: Union[Path, str]) -> pd.DataFrame:
|
||||
"""
|
||||
Load backtest data file.
|
||||
:param filename: pathlib.Path object, or string pointing to the file.
|
||||
@ -151,7 +151,8 @@ def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame) -> p
|
||||
return trades
|
||||
|
||||
|
||||
def combine_tickers_with_mean(tickers: Dict[str, pd.DataFrame], column: str = "close"):
|
||||
def combine_tickers_with_mean(tickers: Dict[str, pd.DataFrame],
|
||||
column: str = "close") -> pd.DataFrame:
|
||||
"""
|
||||
Combine multiple dataframes "column"
|
||||
:param tickers: Dict of Dataframes, dict key should be pair.
|
||||
|
@ -86,7 +86,7 @@ def load_tickerdata_file(datadir: Path, pair: str, timeframe: str,
|
||||
|
||||
|
||||
def store_tickerdata_file(datadir: Path, pair: str,
|
||||
timeframe: str, data: list, is_zip: bool = False):
|
||||
timeframe: str, data: list, is_zip: bool = False) -> None:
|
||||
"""
|
||||
Stores tickerdata to file
|
||||
"""
|
||||
@ -109,7 +109,7 @@ def load_trades_file(datadir: Path, pair: str,
|
||||
|
||||
|
||||
def store_trades_file(datadir: Path, pair: str,
|
||||
data: list, is_zip: bool = True):
|
||||
data: list, is_zip: bool = True) -> None:
|
||||
"""
|
||||
Stores tickerdata to file
|
||||
"""
|
||||
@ -117,7 +117,7 @@ def store_trades_file(datadir: Path, pair: str,
|
||||
misc.file_dump_json(filename, data, is_zip=is_zip)
|
||||
|
||||
|
||||
def _validate_pairdata(pair, pairdata, timerange: TimeRange):
|
||||
def _validate_pairdata(pair: str, pairdata: List[Dict], timerange: TimeRange) -> None:
|
||||
if timerange.starttype == 'date' and pairdata[0][0] > timerange.startts * 1000:
|
||||
logger.warning('Missing data at start for pair %s, data starts at %s',
|
||||
pair, arrow.get(pairdata[0][0] // 1000).strftime('%Y-%m-%d %H:%M:%S'))
|
||||
@ -331,7 +331,7 @@ def _download_pair_history(datadir: Path,
|
||||
|
||||
def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes: List[str],
|
||||
datadir: Path, timerange: Optional[TimeRange] = None,
|
||||
erase=False) -> List[str]:
|
||||
erase: bool = False) -> List[str]:
|
||||
"""
|
||||
Refresh stored ohlcv data for backtesting and hyperopt operations.
|
||||
Used by freqtrade download-data subcommand.
|
||||
@ -401,7 +401,7 @@ def _download_trades_history(datadir: Path,
|
||||
|
||||
|
||||
def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: Path,
|
||||
timerange: TimeRange, erase=False) -> List[str]:
|
||||
timerange: TimeRange, erase: bool = False) -> List[str]:
|
||||
"""
|
||||
Refresh stored trades data for backtesting and hyperopt operations.
|
||||
Used by freqtrade download-data subcommand.
|
||||
@ -428,7 +428,7 @@ def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir:
|
||||
|
||||
|
||||
def convert_trades_to_ohlcv(pairs: List[str], timeframes: List[str],
|
||||
datadir: Path, timerange: TimeRange, erase=False) -> None:
|
||||
datadir: Path, timerange: TimeRange, erase: bool = False) -> None:
|
||||
"""
|
||||
Convert stored trades data to ohlcv data
|
||||
"""
|
||||
|
@ -1,7 +1,7 @@
|
||||
# pragma pylint: disable=W0603
|
||||
""" Edge positioning package """
|
||||
import logging
|
||||
from typing import Any, Dict, NamedTuple
|
||||
from typing import Any, Dict, List, NamedTuple
|
||||
|
||||
import arrow
|
||||
import numpy as np
|
||||
@ -181,7 +181,7 @@ class Edge:
|
||||
'strategy stoploss is returned instead.')
|
||||
return self.strategy.stoploss
|
||||
|
||||
def adjust(self, pairs) -> list:
|
||||
def adjust(self, pairs: List[str]) -> list:
|
||||
"""
|
||||
Filters out and sorts "pairs" according to Edge calculated pairs
|
||||
"""
|
||||
|
@ -32,13 +32,23 @@ class Binance(Exchange):
|
||||
|
||||
return super().get_order_book(pair, limit)
|
||||
|
||||
def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict:
|
||||
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||
"""
|
||||
Verify stop_loss against stoploss-order value (limit or price)
|
||||
Returns True if adjustment is necessary.
|
||||
"""
|
||||
return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice'])
|
||||
|
||||
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
||||
"""
|
||||
creates a stoploss limit order.
|
||||
this stoploss-limit is binance-specific.
|
||||
It may work with a limited number of other exchanges, but this has not been tested yet.
|
||||
|
||||
"""
|
||||
# Limit price threshold: As limit price should always be below stop-price
|
||||
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||
rate = stop_price * limit_price_pct
|
||||
|
||||
ordertype = "stop_loss_limit"
|
||||
|
||||
stop_price = self.price_to_precision(pair, stop_price)
|
||||
@ -61,8 +71,8 @@ class Binance(Exchange):
|
||||
|
||||
rate = self.price_to_precision(pair, rate)
|
||||
|
||||
order = self._api.create_order(pair, ordertype, 'sell',
|
||||
amount, rate, params)
|
||||
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
||||
amount=amount, price=stop_price, params=params)
|
||||
logger.info('stoploss limit order added for %s. '
|
||||
'stop price: %s. limit: %s', pair, stop_price, rate)
|
||||
return order
|
||||
|
@ -24,6 +24,10 @@ from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
||||
from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async
|
||||
from freqtrade.misc import deep_merge_dicts
|
||||
|
||||
|
||||
CcxtModuleType = Any
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -51,7 +55,7 @@ class Exchange:
|
||||
}
|
||||
_ft_has: Dict = {}
|
||||
|
||||
def __init__(self, config: dict, validate: bool = True) -> None:
|
||||
def __init__(self, config: Dict[str, Any], validate: bool = True) -> None:
|
||||
"""
|
||||
Initializes this module with the given config,
|
||||
it does basic validation whether the specified exchange and pairs are valid.
|
||||
@ -135,7 +139,7 @@ class Exchange:
|
||||
if self._api_async and inspect.iscoroutinefunction(self._api_async.close):
|
||||
asyncio.get_event_loop().run_until_complete(self._api_async.close())
|
||||
|
||||
def _init_ccxt(self, exchange_config: dict, ccxt_module=ccxt,
|
||||
def _init_ccxt(self, exchange_config: Dict[str, Any], ccxt_module: CcxtModuleType = ccxt,
|
||||
ccxt_kwargs: dict = None) -> ccxt.Exchange:
|
||||
"""
|
||||
Initialize ccxt with given config and return valid
|
||||
@ -224,13 +228,13 @@ class Exchange:
|
||||
markets = self.markets
|
||||
return sorted(set([x['quote'] for _, x in markets.items()]))
|
||||
|
||||
def klines(self, pair_interval: Tuple[str, str], copy=True) -> DataFrame:
|
||||
def klines(self, pair_interval: Tuple[str, str], copy: bool = True) -> DataFrame:
|
||||
if pair_interval in self._klines:
|
||||
return self._klines[pair_interval].copy() if copy else self._klines[pair_interval]
|
||||
else:
|
||||
return DataFrame()
|
||||
|
||||
def set_sandbox(self, api, exchange_config: dict, name: str):
|
||||
def set_sandbox(self, api: ccxt.Exchange, exchange_config: dict, name: str) -> None:
|
||||
if exchange_config.get('sandbox'):
|
||||
if api.urls.get('test'):
|
||||
api.urls['api'] = api.urls['test']
|
||||
@ -240,7 +244,7 @@ class Exchange:
|
||||
"Please check your config.json")
|
||||
raise OperationalException(f'Exchange {name} does not provide a sandbox api')
|
||||
|
||||
def _load_async_markets(self, reload=False) -> None:
|
||||
def _load_async_markets(self, reload: bool = False) -> None:
|
||||
try:
|
||||
if self._api_async:
|
||||
asyncio.get_event_loop().run_until_complete(
|
||||
@ -273,7 +277,7 @@ class Exchange:
|
||||
except ccxt.BaseError:
|
||||
logger.exception("Could not reload markets.")
|
||||
|
||||
def validate_stakecurrency(self, stake_currency) -> None:
|
||||
def validate_stakecurrency(self, stake_currency: str) -> None:
|
||||
"""
|
||||
Checks stake-currency against available currencies on the exchange.
|
||||
:param stake_currency: Stake-currency to validate
|
||||
@ -319,7 +323,7 @@ class Exchange:
|
||||
f"Please check if you are impacted by this restriction "
|
||||
f"on the exchange and eventually remove {pair} from your whitelist.")
|
||||
|
||||
def get_valid_pair_combination(self, curr_1, curr_2) -> str:
|
||||
def get_valid_pair_combination(self, curr_1: str, curr_2: str) -> str:
|
||||
"""
|
||||
Get valid pair combination of curr_1 and curr_2 by trying both combinations.
|
||||
"""
|
||||
@ -373,7 +377,7 @@ class Exchange:
|
||||
raise OperationalException(
|
||||
f'Time in force policies are not supported for {self.name} yet.')
|
||||
|
||||
def validate_required_startup_candles(self, startup_candles) -> None:
|
||||
def validate_required_startup_candles(self, startup_candles: int) -> None:
|
||||
"""
|
||||
Checks if required startup_candles is more than ohlcv_candle_limit.
|
||||
Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default.
|
||||
@ -392,7 +396,7 @@ class Exchange:
|
||||
"""
|
||||
return endpoint in self._api.has and self._api.has[endpoint]
|
||||
|
||||
def amount_to_precision(self, pair, amount: float) -> float:
|
||||
def amount_to_precision(self, pair: str, amount: float) -> float:
|
||||
'''
|
||||
Returns the amount to buy or sell to a precision the Exchange accepts
|
||||
Reimplementation of ccxt internal methods - ensuring we can test the result is correct
|
||||
@ -406,7 +410,7 @@ class Exchange:
|
||||
|
||||
return amount
|
||||
|
||||
def price_to_precision(self, pair, price: float) -> float:
|
||||
def price_to_precision(self, pair: str, price: float) -> float:
|
||||
'''
|
||||
Returns the price rounded up to the precision the Exchange accepts.
|
||||
Partial Reimplementation of ccxt internal method decimal_to_precision(),
|
||||
@ -494,7 +498,7 @@ class Exchange:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
def buy(self, pair: str, ordertype: str, amount: float,
|
||||
rate: float, time_in_force) -> Dict:
|
||||
rate: float, time_in_force: str) -> Dict:
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.dry_run_order(pair, ordertype, "buy", amount, rate)
|
||||
@ -507,7 +511,7 @@ class Exchange:
|
||||
return self.create_order(pair, ordertype, 'buy', amount, rate, params)
|
||||
|
||||
def sell(self, pair: str, ordertype: str, amount: float,
|
||||
rate: float, time_in_force='gtc') -> Dict:
|
||||
rate: float, time_in_force: str = 'gtc') -> Dict:
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.dry_run_order(pair, ordertype, "sell", amount, rate)
|
||||
@ -519,9 +523,17 @@ class Exchange:
|
||||
|
||||
return self.create_order(pair, ordertype, 'sell', amount, rate, params)
|
||||
|
||||
def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict:
|
||||
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||
"""
|
||||
creates a stoploss limit order.
|
||||
Verify stop_loss against stoploss-order value (limit or price)
|
||||
Returns True if adjustment is necessary.
|
||||
"""
|
||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||
|
||||
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
||||
"""
|
||||
creates a stoploss order.
|
||||
The precise ordertype is determined by the order_types dict or exchange default.
|
||||
Since ccxt does not unify stoploss-limit orders yet, this needs to be implemented in each
|
||||
exchange's subclass.
|
||||
The exception below should never raise, since we disallow
|
||||
@ -529,7 +541,7 @@ class Exchange:
|
||||
Note: Changes to this interface need to be applied to all sub-classes too.
|
||||
"""
|
||||
|
||||
raise OperationalException(f"stoploss_limit is not implemented for {self.name}.")
|
||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||
|
||||
@retrier
|
||||
def get_balance(self, currency: str) -> float:
|
||||
@ -728,10 +740,11 @@ class Exchange:
|
||||
f'Exchange {self._api.name} does not support fetching historical candlestick data.'
|
||||
f'Message: {e}') from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(f'Could not load ticker history due to {e.__class__.__name__}. '
|
||||
f'Message: {e}') from e
|
||||
raise TemporaryError(f'Could not load ticker history for pair {pair} due to '
|
||||
f'{e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(f'Could not fetch ticker data. Msg: {e}') from e
|
||||
raise OperationalException(f'Could not fetch ticker data for pair {pair}. '
|
||||
f'Msg: {e}') from e
|
||||
|
||||
@retrier_async
|
||||
async def _async_fetch_trades(self, pair: str,
|
||||
@ -976,8 +989,8 @@ class Exchange:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def get_fee(self, symbol, type='', side='', amount=1,
|
||||
price=1, taker_or_maker='maker') -> float:
|
||||
def get_fee(self, symbol: str, type: str = '', side: str = '', amount: float = 1,
|
||||
price: float = 1, taker_or_maker: str = 'maker') -> float:
|
||||
try:
|
||||
# validate that markets are loaded before trying to get fee
|
||||
if self._api.markets is None or len(self._api.markets) == 0:
|
||||
@ -1000,7 +1013,7 @@ def get_exchange_bad_reason(exchange_name: str) -> str:
|
||||
return BAD_EXCHANGES.get(exchange_name, "")
|
||||
|
||||
|
||||
def is_exchange_known_ccxt(exchange_name: str, ccxt_module=None) -> bool:
|
||||
def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool:
|
||||
return exchange_name in ccxt_exchanges(ccxt_module)
|
||||
|
||||
|
||||
@ -1008,14 +1021,14 @@ def is_exchange_officially_supported(exchange_name: str) -> bool:
|
||||
return exchange_name in ['bittrex', 'binance']
|
||||
|
||||
|
||||
def ccxt_exchanges(ccxt_module=None) -> List[str]:
|
||||
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
|
||||
"""
|
||||
Return the list of all exchanges known to ccxt
|
||||
"""
|
||||
return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges
|
||||
|
||||
|
||||
def available_exchanges(ccxt_module=None) -> List[str]:
|
||||
def available_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
|
||||
"""
|
||||
Return exchanges available to the bot, i.e. non-bad exchanges in the ccxt list
|
||||
"""
|
||||
@ -1075,7 +1088,8 @@ def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime:
|
||||
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
|
||||
|
||||
|
||||
def symbol_is_pair(market_symbol: str, base_currency: str = None, quote_currency: str = None):
|
||||
def symbol_is_pair(market_symbol: str, base_currency: str = None,
|
||||
quote_currency: str = None) -> bool:
|
||||
"""
|
||||
Check if the market symbol is a pair, i.e. that its symbol consists of the base currency and the
|
||||
quote currency separated by '/' character. If base_currency and/or quote_currency is passed,
|
||||
@ -1088,7 +1102,7 @@ def symbol_is_pair(market_symbol: str, base_currency: str = None, quote_currency
|
||||
(symbol_parts[1] == quote_currency if quote_currency else len(symbol_parts[1]) > 0))
|
||||
|
||||
|
||||
def market_is_active(market):
|
||||
def market_is_active(market: Dict) -> bool:
|
||||
"""
|
||||
Return True if the market is active.
|
||||
"""
|
||||
|
@ -4,7 +4,8 @@ from typing import Dict
|
||||
|
||||
import ccxt
|
||||
|
||||
from freqtrade.exceptions import OperationalException, TemporaryError
|
||||
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.exchange import retrier
|
||||
|
||||
@ -15,6 +16,7 @@ class Kraken(Exchange):
|
||||
|
||||
_params: Dict = {"trading_agreement": "agree"}
|
||||
_ft_has: Dict = {
|
||||
"stoploss_on_exchange": True,
|
||||
"trades_pagination": "id",
|
||||
"trades_pagination_arg": "since",
|
||||
}
|
||||
@ -48,3 +50,51 @@ class Kraken(Exchange):
|
||||
f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||
"""
|
||||
Verify stop_loss against stoploss-order value (limit or price)
|
||||
Returns True if adjustment is necessary.
|
||||
"""
|
||||
return order['type'] == 'stop-loss' and stop_loss > float(order['price'])
|
||||
|
||||
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
||||
"""
|
||||
Creates a stoploss market order.
|
||||
Stoploss market orders is the only stoploss type supported by kraken.
|
||||
"""
|
||||
|
||||
ordertype = "stop-loss"
|
||||
|
||||
stop_price = self.price_to_precision(pair, stop_price)
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.dry_run_order(
|
||||
pair, ordertype, "sell", amount, stop_price)
|
||||
return dry_order
|
||||
|
||||
try:
|
||||
params = self._params.copy()
|
||||
|
||||
amount = self.amount_to_precision(pair, amount)
|
||||
|
||||
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
||||
amount=amount, price=stop_price, params=params)
|
||||
logger.info('stoploss order added for %s. '
|
||||
'stop price: %s.', pair, stop_price)
|
||||
return order
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise DependencyException(
|
||||
f'Insufficient funds to create {ordertype} sell order on market {pair}.'
|
||||
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise InvalidOrderException(
|
||||
f'Could not create {ordertype} sell order on market {pair}. '
|
||||
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
||||
f'Message: {e}') from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
@ -265,7 +265,7 @@ class FreqtradeBot:
|
||||
|
||||
return used_rate
|
||||
|
||||
def get_trade_stake_amount(self, pair) -> float:
|
||||
def get_trade_stake_amount(self, pair: str) -> float:
|
||||
"""
|
||||
Calculate stake amount for the trade
|
||||
:return: float: Stake amount
|
||||
@ -427,16 +427,23 @@ class FreqtradeBot:
|
||||
Checks depth of market before executing a buy
|
||||
"""
|
||||
conf_bids_to_ask_delta = conf.get('bids_to_ask_delta', 0)
|
||||
logger.info('checking depth of market for %s', pair)
|
||||
logger.info(f"Checking depth of market for {pair} ...")
|
||||
order_book = self.exchange.get_order_book(pair, 1000)
|
||||
order_book_data_frame = order_book_to_dataframe(order_book['bids'], order_book['asks'])
|
||||
order_book_bids = order_book_data_frame['b_size'].sum()
|
||||
order_book_asks = order_book_data_frame['a_size'].sum()
|
||||
bids_ask_delta = order_book_bids / order_book_asks
|
||||
logger.info('bids: %s, asks: %s, delta: %s', order_book_bids,
|
||||
order_book_asks, bids_ask_delta)
|
||||
logger.info(
|
||||
f"Bids: {order_book_bids}, Asks: {order_book_asks}, Delta: {bids_ask_delta}, "
|
||||
f"Bid Price: {order_book['bids'][0][0]}, Ask Price: {order_book['asks'][0][0]}, "
|
||||
f"Immediate Bid Quantity: {order_book['bids'][0][1]}, "
|
||||
f"Immediate Ask Quantity: {order_book['asks'][0][1]}."
|
||||
)
|
||||
if bids_ask_delta >= conf_bids_to_ask_delta:
|
||||
logger.info(f"Bids to asks delta for {pair} DOES satisfy condition.")
|
||||
return True
|
||||
else:
|
||||
logger.info(f"Bids to asks delta for {pair} does not satisfy condition.")
|
||||
return False
|
||||
|
||||
def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = None) -> bool:
|
||||
@ -532,7 +539,7 @@ class FreqtradeBot:
|
||||
|
||||
return True
|
||||
|
||||
def _notify_buy(self, trade: Trade, order_type: str):
|
||||
def _notify_buy(self, trade: Trade, order_type: str) -> None:
|
||||
"""
|
||||
Sends rpc notification when a buy occured.
|
||||
"""
|
||||
@ -620,7 +627,7 @@ class FreqtradeBot:
|
||||
self.dataprovider.ohlcv(trade.pair, self.strategy.ticker_interval))
|
||||
|
||||
if config_ask_strategy.get('use_order_book', False):
|
||||
logger.info('Using order book for selling...')
|
||||
logger.debug(f'Using order book for selling {trade.pair}...')
|
||||
# logger.debug('Order book %s',orderBook)
|
||||
order_book_min = config_ask_strategy.get('order_book_min', 1)
|
||||
order_book_max = config_ask_strategy.get('order_book_max', 1)
|
||||
@ -629,7 +636,7 @@ class FreqtradeBot:
|
||||
|
||||
for i in range(order_book_min, order_book_max + 1):
|
||||
order_book_rate = order_book['asks'][i - 1][0]
|
||||
logger.info(' order book asks top %s: %0.8f', i, order_book_rate)
|
||||
logger.debug(' order book asks top %s: %0.8f', i, order_book_rate)
|
||||
sell_rate = order_book_rate
|
||||
|
||||
if self._check_and_execute_sell(trade, sell_rate, buy, sell):
|
||||
@ -651,13 +658,10 @@ class FreqtradeBot:
|
||||
Force-sells the pair (using EmergencySell reason) in case of Problems creating the order.
|
||||
:return: True if the order succeeded, and False in case of problems.
|
||||
"""
|
||||
# Limit price threshold: As limit price should always be below stop-price
|
||||
LIMIT_PRICE_PCT = self.strategy.order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||
|
||||
try:
|
||||
stoploss_order = self.exchange.stoploss_limit(pair=trade.pair, amount=trade.amount,
|
||||
stoploss_order = self.exchange.stoploss(pair=trade.pair, amount=trade.amount,
|
||||
stop_price=stop_price,
|
||||
rate=rate * LIMIT_PRICE_PCT)
|
||||
order_types=self.strategy.order_types)
|
||||
trade.stoploss_order_id = str(stoploss_order['id'])
|
||||
return True
|
||||
except InvalidOrderException as e:
|
||||
@ -689,8 +693,24 @@ class FreqtradeBot:
|
||||
except InvalidOrderException as exception:
|
||||
logger.warning('Unable to fetch stoploss order: %s', exception)
|
||||
|
||||
# We check if stoploss order is fulfilled
|
||||
if stoploss_order and stoploss_order['status'] == 'closed':
|
||||
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
|
||||
trade.update(stoploss_order)
|
||||
# Lock pair for one candle to prevent immediate rebuys
|
||||
self.strategy.lock_pair(trade.pair,
|
||||
timeframe_to_next_date(self.config['ticker_interval']))
|
||||
self._notify_sell(trade, "stoploss")
|
||||
return True
|
||||
|
||||
if trade.open_order_id or not trade.is_open:
|
||||
# Trade has an open Buy or Sell order, Stoploss-handling can't happen in this case
|
||||
# as the Amount on the exchange is tied up in another trade.
|
||||
# The trade can be closed already (sell-order fill confirmation came in this iteration)
|
||||
return False
|
||||
|
||||
# If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange
|
||||
if (not trade.open_order_id and not stoploss_order):
|
||||
if (not stoploss_order):
|
||||
|
||||
stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss
|
||||
|
||||
@ -709,16 +729,6 @@ class FreqtradeBot:
|
||||
trade.stoploss_order_id = None
|
||||
logger.warning('Stoploss order was cancelled, but unable to recreate one.')
|
||||
|
||||
# We check if stoploss order is fulfilled
|
||||
if stoploss_order and stoploss_order['status'] == 'closed':
|
||||
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
|
||||
trade.update(stoploss_order)
|
||||
# Lock pair for one candle to prevent immediate rebuys
|
||||
self.strategy.lock_pair(trade.pair,
|
||||
timeframe_to_next_date(self.config['ticker_interval']))
|
||||
self._notify_sell(trade, "stoploss")
|
||||
return True
|
||||
|
||||
# Finally we check if stoploss on exchange should be moved up because of trailing.
|
||||
if stoploss_order and self.config.get('trailing_stop', False):
|
||||
# if trailing stoploss is enabled we check if stoploss value has changed
|
||||
@ -728,7 +738,7 @@ class FreqtradeBot:
|
||||
|
||||
return False
|
||||
|
||||
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order):
|
||||
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict) -> None:
|
||||
"""
|
||||
Check to see if stoploss on exchange should be updated
|
||||
in case of trailing stoploss on exchange
|
||||
@ -736,8 +746,7 @@ class FreqtradeBot:
|
||||
:param order: Current on exchange stoploss order
|
||||
:return: None
|
||||
"""
|
||||
|
||||
if trade.stop_loss > float(order['info']['stopPrice']):
|
||||
if self.exchange.stoploss_adjust(trade.stop_loss, order):
|
||||
# we check if the update is neccesary
|
||||
update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
|
||||
if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat:
|
||||
@ -751,10 +760,8 @@ class FreqtradeBot:
|
||||
f"for pair {trade.pair}")
|
||||
|
||||
# Create new stoploss order
|
||||
if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss,
|
||||
if not self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss,
|
||||
rate=trade.stop_loss):
|
||||
return False
|
||||
else:
|
||||
logger.warning(f"Could not create trailing stoploss order "
|
||||
f"for pair {trade.pair}.")
|
||||
|
||||
@ -983,7 +990,7 @@ class FreqtradeBot:
|
||||
|
||||
self._notify_sell(trade, order_type)
|
||||
|
||||
def _notify_sell(self, trade: Trade, order_type: str):
|
||||
def _notify_sell(self, trade: Trade, order_type: str) -> None:
|
||||
"""
|
||||
Sends rpc notification when a sell occured.
|
||||
"""
|
||||
@ -1024,7 +1031,7 @@ class FreqtradeBot:
|
||||
# Common update trade state methods
|
||||
#
|
||||
|
||||
def update_trade_state(self, trade, action_order: dict = None):
|
||||
def update_trade_state(self, trade: Trade, action_order: dict = None) -> None:
|
||||
"""
|
||||
Checks trades with open orders and updates the amount if necessary
|
||||
"""
|
||||
|
@ -6,6 +6,7 @@ import logging
|
||||
import re
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing.io import IO
|
||||
|
||||
import numpy as np
|
||||
@ -40,7 +41,7 @@ def datesarray_to_datetimearray(dates: np.ndarray) -> np.ndarray:
|
||||
return dates.dt.to_pydatetime()
|
||||
|
||||
|
||||
def file_dump_json(filename: Path, data, is_zip=False) -> None:
|
||||
def file_dump_json(filename: Path, data: Any, is_zip: bool = False) -> None:
|
||||
"""
|
||||
Dump JSON data into a file
|
||||
:param filename: file to create
|
||||
@ -61,7 +62,7 @@ def file_dump_json(filename: Path, data, is_zip=False) -> None:
|
||||
logger.debug(f'done json to "{filename}"')
|
||||
|
||||
|
||||
def json_load(datafile: IO):
|
||||
def json_load(datafile: IO) -> Any:
|
||||
"""
|
||||
load data with rapidjson
|
||||
Use this to have a consistent experience,
|
||||
@ -125,11 +126,11 @@ def round_dict(d, n):
|
||||
return {k: (round(v, n) if isinstance(v, float) else v) for k, v in d.items()}
|
||||
|
||||
|
||||
def plural(num, singular: str, plural: str = None) -> str:
|
||||
def plural(num: float, singular: str, plural: str = None) -> str:
|
||||
return singular if (num == 1 or num == -1) else plural or singular + 's'
|
||||
|
||||
|
||||
def render_template(templatefile: str, arguments: dict = {}):
|
||||
def render_template(templatefile: str, arguments: dict = {}) -> str:
|
||||
|
||||
from jinja2 import Environment, PackageLoader, select_autoescape
|
||||
|
||||
|
@ -9,6 +9,7 @@ from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, NamedTuple, Optional
|
||||
|
||||
import arrow
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.configuration import (TimeRange, remove_credentials,
|
||||
@ -24,7 +25,7 @@ from freqtrade.optimize.optimize_reports import (
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
from freqtrade.state import RunMode
|
||||
from freqtrade.strategy.interface import IStrategy, SellType
|
||||
from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -148,7 +149,7 @@ class Backtesting:
|
||||
logger.info(f'Dumping backtest results to {recordfilename}')
|
||||
file_dump_json(recordfilename, records)
|
||||
|
||||
def _get_ticker_list(self, processed) -> Dict[str, DataFrame]:
|
||||
def _get_ticker_list(self, processed: Dict) -> Dict[str, DataFrame]:
|
||||
"""
|
||||
Helper function to convert a processed tickerlist into a list for performance reasons.
|
||||
|
||||
@ -175,7 +176,8 @@ class Backtesting:
|
||||
ticker[pair] = [x for x in ticker_data.itertuples()]
|
||||
return ticker
|
||||
|
||||
def _get_close_rate(self, sell_row, trade: Trade, sell, trade_dur) -> float:
|
||||
def _get_close_rate(self, sell_row, trade: Trade, sell: SellCheckTuple,
|
||||
trade_dur: int) -> float:
|
||||
"""
|
||||
Get close rate for backtesting result
|
||||
"""
|
||||
@ -280,7 +282,7 @@ class Backtesting:
|
||||
return None
|
||||
|
||||
def backtest(self, processed: Dict, stake_amount: float,
|
||||
start_date, end_date,
|
||||
start_date: arrow.Arrow, end_date: arrow.Arrow,
|
||||
max_open_trades: int = 0, position_stacking: bool = False) -> DataFrame:
|
||||
"""
|
||||
Implement backtesting functionality
|
||||
@ -426,7 +428,10 @@ class Backtesting:
|
||||
results=results))
|
||||
|
||||
print(' SELL REASON STATS '.center(133, '='))
|
||||
print(generate_text_table_sell_reason(data, results))
|
||||
print(generate_text_table_sell_reason(data,
|
||||
stake_currency=self.config['stake_currency'],
|
||||
max_open_trades=self.config['max_open_trades'],
|
||||
results=results))
|
||||
|
||||
print(' LEFT OPEN TRADES REPORT '.center(133, '='))
|
||||
print(generate_text_table(data,
|
||||
@ -436,7 +441,7 @@ class Backtesting:
|
||||
print()
|
||||
if len(all_results) > 1:
|
||||
# Print Strategy summary table
|
||||
print(' Strategy Summary '.center(133, '='))
|
||||
print(' STRATEGY SUMMARY '.center(133, '='))
|
||||
print(generate_text_table_strategy(self.config['stake_currency'],
|
||||
self.config['max_open_trades'],
|
||||
all_results=all_results))
|
||||
|
@ -59,6 +59,7 @@ class Hyperopt:
|
||||
hyperopt = Hyperopt(config)
|
||||
hyperopt.start()
|
||||
"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any]) -> None:
|
||||
self.config = config
|
||||
|
||||
@ -117,11 +118,11 @@ class Hyperopt:
|
||||
self.print_json = self.config.get('print_json', False)
|
||||
|
||||
@staticmethod
|
||||
def get_lock_filename(config) -> str:
|
||||
def get_lock_filename(config: Dict[str, Any]) -> str:
|
||||
|
||||
return str(config['user_data_dir'] / 'hyperopt.lock')
|
||||
|
||||
def clean_hyperopt(self):
|
||||
def clean_hyperopt(self) -> None:
|
||||
"""
|
||||
Remove hyperopt pickle files to restart hyperopt.
|
||||
"""
|
||||
@ -158,7 +159,7 @@ class Hyperopt:
|
||||
f"saved to '{self.trials_file}'.")
|
||||
|
||||
@staticmethod
|
||||
def _read_trials(trials_file) -> List:
|
||||
def _read_trials(trials_file: Path) -> List:
|
||||
"""
|
||||
Read hyperopt trials file
|
||||
"""
|
||||
@ -189,7 +190,7 @@ class Hyperopt:
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def print_epoch_details(results, total_epochs, print_json: bool,
|
||||
def print_epoch_details(results, total_epochs: int, print_json: bool,
|
||||
no_header: bool = False, header_str: str = None) -> None:
|
||||
"""
|
||||
Display details of the hyperopt result
|
||||
@ -218,7 +219,7 @@ class Hyperopt:
|
||||
Hyperopt._params_pretty_print(params, 'trailing', "Trailing stop:")
|
||||
|
||||
@staticmethod
|
||||
def _params_update_for_json(result_dict, params, space: str):
|
||||
def _params_update_for_json(result_dict, params, space: str) -> None:
|
||||
if space in params:
|
||||
space_params = Hyperopt._space_params(params, space)
|
||||
if space in ['buy', 'sell']:
|
||||
@ -235,7 +236,7 @@ class Hyperopt:
|
||||
result_dict.update(space_params)
|
||||
|
||||
@staticmethod
|
||||
def _params_pretty_print(params, space: str, header: str):
|
||||
def _params_pretty_print(params, space: str, header: str) -> None:
|
||||
if space in params:
|
||||
space_params = Hyperopt._space_params(params, space, 5)
|
||||
if space == 'stoploss':
|
||||
@ -251,7 +252,7 @@ class Hyperopt:
|
||||
return round_dict(d, r) if r else d
|
||||
|
||||
@staticmethod
|
||||
def is_best_loss(results, current_best_loss) -> bool:
|
||||
def is_best_loss(results, current_best_loss: float) -> bool:
|
||||
return results['loss'] < current_best_loss
|
||||
|
||||
def print_results(self, results) -> None:
|
||||
@ -438,7 +439,7 @@ class Hyperopt:
|
||||
random_state=self.random_state,
|
||||
)
|
||||
|
||||
def fix_optimizer_models_list(self):
|
||||
def fix_optimizer_models_list(self) -> None:
|
||||
"""
|
||||
WORKAROUND: Since skopt is not actively supported, this resolves problems with skopt
|
||||
memory usage, see also: https://github.com/scikit-optimize/scikit-optimize/pull/746
|
||||
@ -460,7 +461,7 @@ class Hyperopt:
|
||||
wrap_non_picklable_objects(self.generate_optimizer))(v, i) for v in asked)
|
||||
|
||||
@staticmethod
|
||||
def load_previous_results(trials_file) -> List:
|
||||
def load_previous_results(trials_file: Path) -> List:
|
||||
"""
|
||||
Load data for epochs from the file if we have one
|
||||
"""
|
||||
|
@ -28,18 +28,19 @@ class SharpeHyperOptLoss(IHyperOptLoss):
|
||||
|
||||
Uses Sharpe Ratio calculation.
|
||||
"""
|
||||
total_profit = results.profit_percent
|
||||
total_profit = results["profit_percent"]
|
||||
days_period = (max_date - min_date).days
|
||||
|
||||
# adding slippage of 0.1% per trade
|
||||
total_profit = total_profit - 0.0005
|
||||
expected_yearly_return = total_profit.sum() / days_period
|
||||
expected_returns_mean = total_profit.sum() / days_period
|
||||
up_stdev = np.std(total_profit)
|
||||
|
||||
if (np.std(total_profit) != 0.):
|
||||
sharp_ratio = expected_yearly_return / np.std(total_profit) * np.sqrt(365)
|
||||
sharp_ratio = expected_returns_mean / up_stdev * np.sqrt(365)
|
||||
else:
|
||||
# Define high (negative) sharpe ratio to be clear that this is NOT optimal.
|
||||
sharp_ratio = -20.
|
||||
|
||||
# print(expected_yearly_return, np.std(total_profit), sharp_ratio)
|
||||
# print(expected_returns_mean, up_stdev, sharp_ratio)
|
||||
return -sharp_ratio
|
||||
|
61
freqtrade/optimize/hyperopt_loss_sharpe_daily.py
Normal file
61
freqtrade/optimize/hyperopt_loss_sharpe_daily.py
Normal file
@ -0,0 +1,61 @@
|
||||
"""
|
||||
SharpeHyperOptLossDaily
|
||||
|
||||
This module defines the alternative HyperOptLoss class which can be used for
|
||||
Hyperoptimization.
|
||||
"""
|
||||
import math
|
||||
from datetime import datetime
|
||||
|
||||
from pandas import DataFrame, date_range
|
||||
|
||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||
|
||||
|
||||
class SharpeHyperOptLossDaily(IHyperOptLoss):
|
||||
"""
|
||||
Defines the loss function for hyperopt.
|
||||
|
||||
This implementation uses the Sharpe Ratio calculation.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def hyperopt_loss_function(results: DataFrame, trade_count: int,
|
||||
min_date: datetime, max_date: datetime,
|
||||
*args, **kwargs) -> float:
|
||||
"""
|
||||
Objective function, returns smaller number for more optimal results.
|
||||
|
||||
Uses Sharpe Ratio calculation.
|
||||
"""
|
||||
resample_freq = '1D'
|
||||
slippage_per_trade_ratio = 0.0005
|
||||
days_in_year = 365
|
||||
annual_risk_free_rate = 0.0
|
||||
risk_free_rate = annual_risk_free_rate / days_in_year
|
||||
|
||||
# apply slippage per trade to profit_percent
|
||||
results.loc[:, 'profit_percent_after_slippage'] = \
|
||||
results['profit_percent'] - slippage_per_trade_ratio
|
||||
|
||||
# create the index within the min_date and end max_date
|
||||
t_index = date_range(start=min_date, end=max_date, freq=resample_freq)
|
||||
|
||||
sum_daily = (
|
||||
results.resample(resample_freq, on='close_time').agg(
|
||||
{"profit_percent_after_slippage": sum}).reindex(t_index).fillna(0)
|
||||
)
|
||||
|
||||
total_profit = sum_daily["profit_percent_after_slippage"] - risk_free_rate
|
||||
expected_returns_mean = total_profit.mean()
|
||||
up_stdev = total_profit.std()
|
||||
|
||||
if (up_stdev != 0.):
|
||||
sharp_ratio = expected_returns_mean / up_stdev * math.sqrt(days_in_year)
|
||||
else:
|
||||
# Define high (negative) sharpe ratio to be clear that this is NOT optimal.
|
||||
sharp_ratio = -20.
|
||||
|
||||
# print(t_index, sum_daily, total_profit)
|
||||
# print(risk_free_rate, expected_returns_mean, up_stdev, sharp_ratio)
|
||||
return -sharp_ratio
|
@ -19,9 +19,17 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra
|
||||
|
||||
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f')
|
||||
tabular_data = []
|
||||
headers = ['pair', 'buy count', 'avg profit %', 'cum profit %',
|
||||
f'tot profit {stake_currency}', 'tot profit %', 'avg duration',
|
||||
'profit', 'loss']
|
||||
headers = [
|
||||
'Pair',
|
||||
'Buy Count',
|
||||
'Avg Profit %',
|
||||
'Cum Profit %',
|
||||
f'Tot Profit {stake_currency}',
|
||||
'Tot Profit %',
|
||||
'Avg Duration',
|
||||
'Wins',
|
||||
'Losses'
|
||||
]
|
||||
for pair in data:
|
||||
result = results[results.pair == pair]
|
||||
if skip_nan and result.profit_abs.isnull().all():
|
||||
@ -58,7 +66,9 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra
|
||||
floatfmt=floatfmt, tablefmt="pipe") # type: ignore
|
||||
|
||||
|
||||
def generate_text_table_sell_reason(data: Dict[str, Dict], results: DataFrame) -> str:
|
||||
def generate_text_table_sell_reason(
|
||||
data: Dict[str, Dict], stake_currency: str, max_open_trades: int, results: DataFrame
|
||||
) -> str:
|
||||
"""
|
||||
Generate small table outlining Backtest results
|
||||
:param data: Dict of <pair: dataframe> containing data that was used during backtesting.
|
||||
@ -66,13 +76,36 @@ def generate_text_table_sell_reason(data: Dict[str, Dict], results: DataFrame) -
|
||||
:return: pretty printed table with tabulate as string
|
||||
"""
|
||||
tabular_data = []
|
||||
headers = ['Sell Reason', 'Count', 'Profit', 'Loss', 'Profit %']
|
||||
headers = [
|
||||
"Sell Reason",
|
||||
"Sell Count",
|
||||
"Wins",
|
||||
"Losses",
|
||||
"Avg Profit %",
|
||||
"Cum Profit %",
|
||||
f"Tot Profit {stake_currency}",
|
||||
"Tot Profit %",
|
||||
]
|
||||
for reason, count in results['sell_reason'].value_counts().iteritems():
|
||||
result = results.loc[results['sell_reason'] == reason]
|
||||
profit = len(result[result['profit_abs'] >= 0])
|
||||
loss = len(result[result['profit_abs'] < 0])
|
||||
profit_mean = round(result['profit_percent'].mean() * 100.0, 2)
|
||||
tabular_data.append([reason.value, count, profit, loss, profit_mean])
|
||||
profit_sum = round(result["profit_percent"].sum() * 100.0, 2)
|
||||
profit_tot = result['profit_abs'].sum()
|
||||
profit_percent_tot = round(result['profit_percent'].sum() * 100.0 / max_open_trades, 2)
|
||||
tabular_data.append(
|
||||
[
|
||||
reason.value,
|
||||
count,
|
||||
profit,
|
||||
loss,
|
||||
profit_mean,
|
||||
profit_sum,
|
||||
profit_tot,
|
||||
profit_percent_tot,
|
||||
]
|
||||
)
|
||||
return tabulate(tabular_data, headers=headers, tablefmt="pipe")
|
||||
|
||||
|
||||
@ -88,9 +121,9 @@ def generate_text_table_strategy(stake_currency: str, max_open_trades: str,
|
||||
|
||||
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f')
|
||||
tabular_data = []
|
||||
headers = ['Strategy', 'buy count', 'avg profit %', 'cum profit %',
|
||||
f'tot profit {stake_currency}', 'tot profit %', 'avg duration',
|
||||
'profit', 'loss']
|
||||
headers = ['Strategy', 'Buy Count', 'Avg Profit %', 'Cum Profit %',
|
||||
f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration',
|
||||
'Wins', 'Losses']
|
||||
for strategy, results in all_results.items():
|
||||
tabular_data.append([
|
||||
strategy,
|
||||
|
@ -7,7 +7,7 @@ Provides lists as configured in config.json
|
||||
import logging
|
||||
from abc import ABC, abstractmethod, abstractproperty
|
||||
from copy import deepcopy
|
||||
from typing import Dict, List
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from freqtrade.exchange import market_is_active
|
||||
|
||||
@ -16,7 +16,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class IPairList(ABC):
|
||||
|
||||
def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict,
|
||||
def __init__(self, exchange, pairlistmanager,
|
||||
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int) -> None:
|
||||
"""
|
||||
:param exchange: Exchange instance
|
||||
|
@ -48,10 +48,10 @@ class PrecisionFilter(IPairList):
|
||||
"""
|
||||
Filters and sorts pairlists and assigns and returns them again.
|
||||
"""
|
||||
stoploss = None
|
||||
if self._config.get('stoploss') is not None:
|
||||
stoploss = self._config.get('stoploss')
|
||||
if stoploss is not None:
|
||||
# Precalculate sanitized stoploss value to avoid recalculation for every pair
|
||||
stoploss = 1 - abs(self._config.get('stoploss'))
|
||||
stoploss = 1 - abs(stoploss)
|
||||
# Copy list since we're modifying this list
|
||||
for p in deepcopy(pairlist):
|
||||
ticker = tickers.get(p)
|
||||
|
@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from typing import Dict, List
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from freqtrade.pairlist.IPairList import IPairList
|
||||
|
||||
@ -9,7 +9,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class PriceFilter(IPairList):
|
||||
|
||||
def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict,
|
||||
def __init__(self, exchange, pairlistmanager,
|
||||
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
|
||||
|
59
freqtrade/pairlist/SpreadFilter.py
Normal file
59
freqtrade/pairlist/SpreadFilter.py
Normal file
@ -0,0 +1,59 @@
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from typing import Dict, List
|
||||
|
||||
from freqtrade.pairlist.IPairList import IPairList
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SpreadFilter(IPairList):
|
||||
|
||||
def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict,
|
||||
pairlist_pos: int) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
|
||||
self._max_spread_ratio = pairlistconfig.get('max_spread_ratio', 0.005)
|
||||
|
||||
@property
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
Boolean property defining if tickers are necessary.
|
||||
If no Pairlist requries tickers, an empty List is passed
|
||||
as tickers argument to filter_pairlist
|
||||
"""
|
||||
return True
|
||||
|
||||
def short_desc(self) -> str:
|
||||
"""
|
||||
Short whitelist method description - used for startup-messages
|
||||
"""
|
||||
return (f"{self.name} - Filtering pairs with ask/bid diff above "
|
||||
f"{self._max_spread_ratio * 100}%.")
|
||||
|
||||
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
|
||||
|
||||
"""
|
||||
Filters and sorts pairlist and returns the whitelist again.
|
||||
Called on each bot iteration - please use internal caching if necessary
|
||||
:param pairlist: pairlist to filter or sort
|
||||
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
|
||||
:return: new whitelist
|
||||
"""
|
||||
# Copy list since we're modifying this list
|
||||
|
||||
spread = None
|
||||
for p in deepcopy(pairlist):
|
||||
ticker = tickers.get(p)
|
||||
assert ticker is not None
|
||||
if 'bid' in ticker and 'ask' in ticker:
|
||||
spread = 1 - ticker['bid'] / ticker['ask']
|
||||
if not ticker or spread > self._max_spread_ratio:
|
||||
logger.info(f"Removed {ticker['symbol']} from whitelist, "
|
||||
f"because spread {spread * 100:.3f}% >"
|
||||
f"{self._max_spread_ratio * 100}%")
|
||||
pairlist.remove(p)
|
||||
else:
|
||||
pairlist.remove(p)
|
||||
|
||||
return pairlist
|
@ -6,7 +6,7 @@ Provides lists as configured in config.json
|
||||
"""
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, List
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.pairlist.IPairList import IPairList
|
||||
@ -18,7 +18,7 @@ SORT_VALUES = ['askVolume', 'bidVolume', 'quoteVolume']
|
||||
|
||||
class VolumePairList(IPairList):
|
||||
|
||||
def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict,
|
||||
def __init__(self, exchange, pairlistmanager, config: Dict[str, Any], pairlistconfig: dict,
|
||||
pairlist_pos: int) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
|
||||
@ -28,6 +28,7 @@ class VolumePairList(IPairList):
|
||||
'for "pairlist.config.number_assets"')
|
||||
self._number_pairs = self._pairlistconfig['number_assets']
|
||||
self._sort_key = self._pairlistconfig.get('sort_key', 'quoteVolume')
|
||||
self._min_value = self._pairlistconfig.get('min_value', 0)
|
||||
self.refresh_period = self._pairlistconfig.get('refresh_period', 1800)
|
||||
|
||||
if not self._exchange.exchange_has('fetchTickers'):
|
||||
@ -73,11 +74,13 @@ class VolumePairList(IPairList):
|
||||
tickers,
|
||||
self._config['stake_currency'],
|
||||
self._sort_key,
|
||||
self._min_value
|
||||
)
|
||||
else:
|
||||
return pairlist
|
||||
|
||||
def _gen_pair_whitelist(self, pairlist, tickers, base_currency: str, key: str) -> List[str]:
|
||||
def _gen_pair_whitelist(self, pairlist: List[str], tickers: Dict,
|
||||
base_currency: str, key: str, min_val: int) -> List[str]:
|
||||
"""
|
||||
Updates the whitelist with with a dynamically generated list
|
||||
:param base_currency: base currency as str
|
||||
@ -96,6 +99,9 @@ class VolumePairList(IPairList):
|
||||
# If other pairlist is in front, use the incomming pairlist.
|
||||
filtered_tickers = [v for k, v in tickers.items() if k in pairlist]
|
||||
|
||||
if min_val > 0:
|
||||
filtered_tickers = list(filter(lambda t: t[key] > min_val, filtered_tickers))
|
||||
|
||||
sorted_tickers = sorted(filtered_tickers, reverse=True, key=lambda t: t[key])
|
||||
|
||||
# Validate whitelist to only have active market pairs
|
||||
|
@ -64,11 +64,11 @@ def init(db_url: str, clean_open_orders: bool = False) -> None:
|
||||
clean_dry_run_db()
|
||||
|
||||
|
||||
def has_column(columns, searchname: str) -> bool:
|
||||
def has_column(columns: List, searchname: str) -> bool:
|
||||
return len(list(filter(lambda x: x["name"] == searchname, columns))) == 1
|
||||
|
||||
|
||||
def get_column_def(columns, column: str, default: str) -> str:
|
||||
def get_column_def(columns: List, column: str, default: str) -> str:
|
||||
return default if not has_column(columns, column) else column
|
||||
|
||||
|
||||
@ -246,14 +246,15 @@ class Trade(_DECL_BASE):
|
||||
if self.initial_stop_loss_pct else None),
|
||||
}
|
||||
|
||||
def adjust_min_max_rates(self, current_price: float):
|
||||
def adjust_min_max_rates(self, current_price: float) -> None:
|
||||
"""
|
||||
Adjust the max_rate and min_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)
|
||||
|
||||
def adjust_stop_loss(self, current_price: float, stoploss: float, initial: bool = False):
|
||||
def adjust_stop_loss(self, current_price: float, stoploss: float,
|
||||
initial: bool = False) -> None:
|
||||
"""
|
||||
This adjusts the stop loss to it's most recently observed setting
|
||||
:param current_price: Current rate the asset is traded
|
||||
|
@ -370,7 +370,7 @@ def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame],
|
||||
return fig
|
||||
|
||||
|
||||
def generate_plot_filename(pair, timeframe) -> str:
|
||||
def generate_plot_filename(pair: str, timeframe: str) -> str:
|
||||
"""
|
||||
Generate filenames per pair/timeframe to be used for storing plots
|
||||
"""
|
||||
|
@ -25,7 +25,7 @@ class IResolver:
|
||||
initial_search_path: Path
|
||||
|
||||
@classmethod
|
||||
def build_search_paths(cls, config, user_subdir: Optional[str] = None,
|
||||
def build_search_paths(cls, config: Dict[str, Any], user_subdir: Optional[str] = None,
|
||||
extra_dir: Optional[str] = None) -> List[Path]:
|
||||
|
||||
abs_paths: List[Path] = [cls.initial_search_path]
|
||||
|
@ -9,10 +9,10 @@ from base64 import urlsafe_b64decode
|
||||
from collections import OrderedDict
|
||||
from inspect import getfullargspec
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from freqtrade.constants import (REQUIRED_ORDERTIF, REQUIRED_ORDERTYPES,
|
||||
USERPATH_STRATEGY)
|
||||
USERPATH_STRATEGIES)
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.resolvers import IResolver
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
@ -26,11 +26,11 @@ class StrategyResolver(IResolver):
|
||||
"""
|
||||
object_type = IStrategy
|
||||
object_type_str = "Strategy"
|
||||
user_subdir = USERPATH_STRATEGY
|
||||
user_subdir = USERPATH_STRATEGIES
|
||||
initial_search_path = Path(__file__).parent.parent.joinpath('strategy').resolve()
|
||||
|
||||
@staticmethod
|
||||
def load_strategy(config: Optional[Dict] = None) -> IStrategy:
|
||||
def load_strategy(config: Dict[str, Any] = None) -> IStrategy:
|
||||
"""
|
||||
Load the custom class from config parameter
|
||||
:param config: configuration dictionary or None
|
||||
@ -96,7 +96,8 @@ class StrategyResolver(IResolver):
|
||||
return strategy
|
||||
|
||||
@staticmethod
|
||||
def _override_attribute_helper(strategy, config, attribute: str, default):
|
||||
def _override_attribute_helper(strategy, config: Dict[str, Any],
|
||||
attribute: str, default: Any):
|
||||
"""
|
||||
Override attributes in the strategy.
|
||||
Prevalence:
|
||||
@ -140,7 +141,7 @@ class StrategyResolver(IResolver):
|
||||
"""
|
||||
|
||||
abs_paths = StrategyResolver.build_search_paths(config,
|
||||
user_subdir=USERPATH_STRATEGY,
|
||||
user_subdir=USERPATH_STRATEGIES,
|
||||
extra_dir=extra_dir)
|
||||
|
||||
if ":" in strategy_name:
|
||||
|
@ -139,7 +139,8 @@ class RPC:
|
||||
results.append(trade_dict)
|
||||
return results
|
||||
|
||||
def _rpc_status_table(self, stake_currency, fiat_display_currency: str) -> Tuple[List, List]:
|
||||
def _rpc_status_table(self, stake_currency: str,
|
||||
fiat_display_currency: str) -> Tuple[List, List]:
|
||||
trades = Trade.get_open_trades()
|
||||
if not trades:
|
||||
raise RPCException('no active trade')
|
||||
@ -385,7 +386,7 @@ class RPC:
|
||||
|
||||
return {'status': 'No more buy will occur from now. Run /reload_conf to reset.'}
|
||||
|
||||
def _rpc_forcesell(self, trade_id) -> Dict[str, str]:
|
||||
def _rpc_forcesell(self, trade_id: str) -> Dict[str, str]:
|
||||
"""
|
||||
Handler for forcesell <id>.
|
||||
Sells the given trade at current price
|
||||
|
@ -61,7 +61,7 @@ class RPCManager:
|
||||
except NotImplementedError:
|
||||
logger.error(f"Message type {msg['type']} not implemented by handler {mod.name}.")
|
||||
|
||||
def startup_messages(self, config, pairlist) -> None:
|
||||
def startup_messages(self, config: Dict[str, Any], pairlist) -> None:
|
||||
if config['dry_run']:
|
||||
self.send_msg({
|
||||
'type': RPCMessageType.WARNING_NOTIFICATION,
|
||||
|
@ -180,7 +180,7 @@ class IStrategy(ABC):
|
||||
if pair not in self._pair_locked_until or self._pair_locked_until[pair] < until:
|
||||
self._pair_locked_until[pair] = until
|
||||
|
||||
def unlock_pair(self, pair) -> None:
|
||||
def unlock_pair(self, pair: str) -> None:
|
||||
"""
|
||||
Unlocks a pair previously locked using lock_pair.
|
||||
Not used by freqtrade itself, but intended to be used if users lock pairs
|
||||
|
4
freqtrade/vendor/qtpylib/indicators.py
vendored
4
freqtrade/vendor/qtpylib/indicators.py
vendored
@ -288,9 +288,9 @@ def rolling_min(series, window=14, min_periods=None):
|
||||
def rolling_max(series, window=14, min_periods=None):
|
||||
min_periods = window if min_periods is None else min_periods
|
||||
try:
|
||||
return series.rolling(window=window, min_periods=min_periods).min()
|
||||
return series.rolling(window=window, min_periods=min_periods).max()
|
||||
except Exception as e: # noqa: F841
|
||||
return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
|
||||
return pd.Series(series).rolling(window=window, min_periods=min_periods).max()
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
@ -30,24 +30,21 @@ class Wallets:
|
||||
self._last_wallet_refresh = 0
|
||||
self.update()
|
||||
|
||||
def get_free(self, currency) -> float:
|
||||
|
||||
def get_free(self, currency: str) -> float:
|
||||
balance = self._wallets.get(currency)
|
||||
if balance and balance.free:
|
||||
return balance.free
|
||||
else:
|
||||
return 0
|
||||
|
||||
def get_used(self, currency) -> float:
|
||||
|
||||
def get_used(self, currency: str) -> float:
|
||||
balance = self._wallets.get(currency)
|
||||
if balance and balance.used:
|
||||
return balance.used
|
||||
else:
|
||||
return 0
|
||||
|
||||
def get_total(self, currency) -> float:
|
||||
|
||||
def get_total(self, currency: str) -> float:
|
||||
balance = self._wallets.get(currency)
|
||||
if balance and balance.total:
|
||||
return balance.total
|
||||
@ -87,7 +84,6 @@ class Wallets:
|
||||
self._wallets = _wallets
|
||||
|
||||
def _update_live(self) -> None:
|
||||
|
||||
balances = self._exchange.get_balances()
|
||||
|
||||
for currency in balances:
|
||||
|
@ -22,7 +22,7 @@ class Worker:
|
||||
Freqtradebot worker class
|
||||
"""
|
||||
|
||||
def __init__(self, args: Dict[str, Any], config=None) -> None:
|
||||
def __init__(self, args: Dict[str, Any], config: Dict[str, Any] = None) -> None:
|
||||
"""
|
||||
Init all variables and objects the bot needs to work
|
||||
"""
|
||||
@ -56,14 +56,6 @@ class Worker:
|
||||
self._sd_notify = sdnotify.SystemdNotifier() if \
|
||||
self._config.get('internals', {}).get('sd_notify', False) else None
|
||||
|
||||
@property
|
||||
def state(self) -> State:
|
||||
return self.freqtrade.state
|
||||
|
||||
@state.setter
|
||||
def state(self, value: State) -> None:
|
||||
self.freqtrade.state = value
|
||||
|
||||
def run(self) -> None:
|
||||
state = None
|
||||
while True:
|
||||
|
@ -1,6 +1,6 @@
|
||||
# requirements without requirements installable via conda
|
||||
# mainly used for Raspberry pi installs
|
||||
ccxt==1.21.91
|
||||
ccxt==1.22.30
|
||||
SQLAlchemy==1.3.13
|
||||
python-telegram-bot==12.3.0
|
||||
arrow==0.15.5
|
||||
@ -12,7 +12,7 @@ jsonschema==3.2.0
|
||||
TA-Lib==0.4.17
|
||||
tabulate==0.8.6
|
||||
coinmarketcap==5.0.3
|
||||
jinja2==2.10.3
|
||||
jinja2==2.11.1
|
||||
|
||||
# find first, C search in arrays
|
||||
py_find_1st==1.1.4
|
||||
|
@ -8,7 +8,7 @@ flake8==3.7.9
|
||||
flake8-type-annotations==0.1.0
|
||||
flake8-tidy-imports==4.0.0
|
||||
mypy==0.761
|
||||
pytest==5.3.4
|
||||
pytest==5.3.5
|
||||
pytest-asyncio==0.10.0
|
||||
pytest-cov==2.8.1
|
||||
pytest-mock==2.0.0
|
||||
|
@ -4,6 +4,6 @@
|
||||
# Required for hyperopt
|
||||
scipy==1.4.1
|
||||
scikit-learn==0.22.1
|
||||
scikit-optimize==0.5.2
|
||||
scikit-optimize==0.7.1
|
||||
filelock==3.0.12
|
||||
joblib==0.14.1
|
||||
|
@ -2,4 +2,4 @@
|
||||
-r requirements-common.txt
|
||||
|
||||
numpy==1.18.1
|
||||
pandas==0.25.3
|
||||
pandas==1.0.0
|
||||
|
@ -7,7 +7,8 @@ import pytest
|
||||
from freqtrade.commands import (start_create_userdir, start_download_data,
|
||||
start_hyperopt_list, start_hyperopt_show,
|
||||
start_list_exchanges, start_list_markets,
|
||||
start_list_strategies, start_list_timeframes,
|
||||
start_list_hyperopts, start_list_strategies,
|
||||
start_list_timeframes,
|
||||
start_new_hyperopt, start_new_strategy,
|
||||
start_test_pairlist, start_trading)
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
@ -665,6 +666,39 @@ def test_start_list_strategies(mocker, caplog, capsys):
|
||||
assert "DefaultStrategy" in captured.out
|
||||
|
||||
|
||||
def test_start_list_hyperopts(mocker, caplog, capsys):
|
||||
|
||||
args = [
|
||||
"list-hyperopts",
|
||||
"--hyperopt-path",
|
||||
str(Path(__file__).parent.parent / "optimize"),
|
||||
"-1"
|
||||
]
|
||||
pargs = get_args(args)
|
||||
# pargs['config'] = None
|
||||
start_list_hyperopts(pargs)
|
||||
captured = capsys.readouterr()
|
||||
assert "TestHyperoptLegacy" not in captured.out
|
||||
assert "legacy_hyperopt.py" not in captured.out
|
||||
assert "DefaultHyperOpt" in captured.out
|
||||
assert "test_hyperopt.py" not in captured.out
|
||||
|
||||
# Test regular output
|
||||
args = [
|
||||
"list-hyperopts",
|
||||
"--hyperopt-path",
|
||||
str(Path(__file__).parent.parent / "optimize"),
|
||||
]
|
||||
pargs = get_args(args)
|
||||
# pargs['config'] = None
|
||||
start_list_hyperopts(pargs)
|
||||
captured = capsys.readouterr()
|
||||
assert "TestHyperoptLegacy" not in captured.out
|
||||
assert "legacy_hyperopt.py" not in captured.out
|
||||
assert "DefaultHyperOpt" in captured.out
|
||||
assert "test_hyperopt.py" in captured.out
|
||||
|
||||
|
||||
def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
|
||||
patch_exchange(mocker, mock_markets=True)
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||
|
@ -640,6 +640,31 @@ def shitcoinmarkets(markets):
|
||||
},
|
||||
'info': {},
|
||||
},
|
||||
'NANO/USDT': {
|
||||
"percentage": True,
|
||||
"tierBased": False,
|
||||
"taker": 0.001,
|
||||
"maker": 0.001,
|
||||
"precision": {
|
||||
"base": 8,
|
||||
"quote": 8,
|
||||
"amount": 2,
|
||||
"price": 4
|
||||
},
|
||||
"limits": {
|
||||
},
|
||||
"id": "NANOUSDT",
|
||||
"symbol": "NANO/USDT",
|
||||
"base": "NANO",
|
||||
"quote": "USDT",
|
||||
"baseId": "NANO",
|
||||
"quoteId": "USDT",
|
||||
"info": {},
|
||||
"type": "spot",
|
||||
"spot": True,
|
||||
"future": False,
|
||||
"active": True
|
||||
},
|
||||
})
|
||||
return shitmarkets
|
||||
|
||||
@ -1114,6 +1139,28 @@ def tickers():
|
||||
'quoteVolume': 1154.19266394,
|
||||
'info': {}
|
||||
},
|
||||
"NANO/USDT": {
|
||||
"symbol": "NANO/USDT",
|
||||
"timestamp": 1580469388244,
|
||||
"datetime": "2020-01-31T11:16:28.244Z",
|
||||
"high": 0.7519,
|
||||
"low": 0.7154,
|
||||
"bid": 0.7305,
|
||||
"bidVolume": 300.3,
|
||||
"ask": 0.7342,
|
||||
"askVolume": 15.14,
|
||||
"vwap": 0.73645591,
|
||||
"open": 0.7154,
|
||||
"close": 0.7342,
|
||||
"last": 0.7342,
|
||||
"previousClose": 0.7189,
|
||||
"change": 0.0188,
|
||||
"percentage": 2.628,
|
||||
"average": None,
|
||||
"baseVolume": 439472.44,
|
||||
"quoteVolume": 323652.075405,
|
||||
"info": {}
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
|
@ -163,8 +163,8 @@ def test_edge_results(edge_conf, mocker, caplog, data) -> None:
|
||||
for c, trade in enumerate(data.trades):
|
||||
res = results.iloc[c]
|
||||
assert res.exit_type == trade.sell_reason
|
||||
assert arrow.get(res.open_time) == _get_frame_time_from_offset(trade.open_tick)
|
||||
assert arrow.get(res.close_time) == _get_frame_time_from_offset(trade.close_tick)
|
||||
assert res.open_time == np.datetime64(_get_frame_time_from_offset(trade.open_tick))
|
||||
assert res.close_time == np.datetime64(_get_frame_time_from_offset(trade.close_tick))
|
||||
|
||||
|
||||
def test_adjust(mocker, edge_conf):
|
||||
|
@ -9,7 +9,7 @@ from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
||||
from tests.conftest import get_patched_exchange
|
||||
|
||||
|
||||
def test_stoploss_limit_order(default_conf, mocker):
|
||||
def test_stoploss_order_binance(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||
order_type = 'stop_loss_limit'
|
||||
@ -28,46 +28,47 @@ def test_stoploss_limit_order(default_conf, mocker):
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200)
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||
order_types={'stoploss_on_exchange_limit_ratio': 1.05})
|
||||
|
||||
api_mock.create_order.reset_mock()
|
||||
|
||||
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
assert 'id' in order
|
||||
assert 'info' in order
|
||||
assert order['id'] == order_id
|
||||
assert api_mock.create_order.call_args[0][0] == 'ETH/BTC'
|
||||
assert api_mock.create_order.call_args[0][1] == order_type
|
||||
assert api_mock.create_order.call_args[0][2] == 'sell'
|
||||
assert api_mock.create_order.call_args[0][3] == 1
|
||||
assert api_mock.create_order.call_args[0][4] == 200
|
||||
assert api_mock.create_order.call_args[0][5] == {'stopPrice': 220}
|
||||
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
|
||||
assert api_mock.create_order.call_args_list[0][1]['type'] == order_type
|
||||
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
|
||||
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
||||
assert api_mock.create_order.call_args_list[0][1]['price'] == 220
|
||||
assert api_mock.create_order.call_args_list[0][1]['params'] == {'stopPrice': 220}
|
||||
|
||||
# test exception handling
|
||||
with pytest.raises(DependencyException):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
with pytest.raises(InvalidOrderException):
|
||||
api_mock.create_order = MagicMock(
|
||||
side_effect=ccxt.InvalidOrder("binance Order would trigger immediately."))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
with pytest.raises(TemporaryError):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection"))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
with pytest.raises(OperationalException, match=r".*DeadBeef.*"):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
|
||||
def test_stoploss_limit_order_dry_run(default_conf, mocker):
|
||||
def test_stoploss_order_dry_run_binance(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
order_type = 'stop_loss_limit'
|
||||
default_conf['dry_run'] = True
|
||||
@ -77,11 +78,12 @@ def test_stoploss_limit_order_dry_run(default_conf, mocker):
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200)
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||
order_types={'stoploss_on_exchange_limit_ratio': 1.05})
|
||||
|
||||
api_mock.create_order.reset_mock()
|
||||
|
||||
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
assert 'id' in order
|
||||
assert 'info' in order
|
||||
@ -90,3 +92,17 @@ def test_stoploss_limit_order_dry_run(default_conf, mocker):
|
||||
assert order['type'] == order_type
|
||||
assert order['price'] == 220
|
||||
assert order['amount'] == 1
|
||||
|
||||
|
||||
def test_stoploss_adjust_binance(mocker, default_conf):
|
||||
exchange = get_patched_exchange(mocker, default_conf, id='binance')
|
||||
order = {
|
||||
'type': 'stop_loss_limit',
|
||||
'price': 1500,
|
||||
'info': {'stopPrice': 1500},
|
||||
}
|
||||
assert exchange.stoploss_adjust(1501, order)
|
||||
assert not exchange.stoploss_adjust(1499, order)
|
||||
# Test with invalid order case
|
||||
order['type'] = 'stop_loss'
|
||||
assert not exchange.stoploss_adjust(1501, order)
|
||||
|
@ -76,9 +76,11 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog):
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||
caplog.set_level(logging.INFO)
|
||||
conf = copy.deepcopy(default_conf)
|
||||
conf['exchange']['ccxt_async_config'] = {'aiohttp_trust_env': True}
|
||||
conf['exchange']['ccxt_async_config'] = {'aiohttp_trust_env': True, 'asyncio_loop': True}
|
||||
ex = Exchange(conf)
|
||||
assert log_has("Applying additional ccxt config: {'aiohttp_trust_env': True}", caplog)
|
||||
assert log_has(
|
||||
"Applying additional ccxt config: {'aiohttp_trust_env': True, 'asyncio_loop': True}",
|
||||
caplog)
|
||||
assert ex._api_async.aiohttp_trust_env
|
||||
assert not ex._api.aiohttp_trust_env
|
||||
|
||||
@ -86,6 +88,8 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog):
|
||||
caplog.clear()
|
||||
conf = copy.deepcopy(default_conf)
|
||||
conf['exchange']['ccxt_config'] = {'TestKWARG': 11}
|
||||
conf['exchange']['ccxt_async_config'] = {'asyncio_loop': True}
|
||||
|
||||
ex = Exchange(conf)
|
||||
assert not log_has("Applying additional ccxt config: {'aiohttp_trust_env': True}", caplog)
|
||||
assert not ex._api_async.aiohttp_trust_env
|
||||
@ -1758,10 +1762,13 @@ def test_get_fee(default_conf, mocker, exchange_name):
|
||||
'get_fee', 'calculate_fee', symbol="ETH/BTC")
|
||||
|
||||
|
||||
def test_stoploss_limit_order_unsupported_exchange(default_conf, mocker):
|
||||
def test_stoploss_order_unsupported_exchange(default_conf, mocker):
|
||||
exchange = get_patched_exchange(mocker, default_conf, 'bittrex')
|
||||
with pytest.raises(OperationalException, match=r"stoploss_limit is not implemented .*"):
|
||||
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||
with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"):
|
||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"):
|
||||
exchange.stoploss_adjust(1, {})
|
||||
|
||||
|
||||
def test_merge_ft_has_dict(default_conf, mocker):
|
||||
|
@ -3,6 +3,11 @@
|
||||
from random import randint
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import ccxt
|
||||
import pytest
|
||||
|
||||
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from tests.conftest import get_patched_exchange
|
||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||
|
||||
@ -149,3 +154,98 @@ def test_get_balances_prod(default_conf, mocker):
|
||||
assert balances['4ST']['used'] == 0.0
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken",
|
||||
"get_balances", "fetch_balance")
|
||||
|
||||
|
||||
def test_stoploss_order_kraken(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||
order_type = 'stop-loss'
|
||||
|
||||
api_mock.create_order = MagicMock(return_value={
|
||||
'id': order_id,
|
||||
'info': {
|
||||
'foo': 'bar'
|
||||
}
|
||||
})
|
||||
|
||||
default_conf['dry_run'] = False
|
||||
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||
|
||||
# stoploss_on_exchange_limit_ratio is irrelevant for kraken market orders
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||
order_types={'stoploss_on_exchange_limit_ratio': 1.05})
|
||||
assert api_mock.create_order.call_count == 1
|
||||
|
||||
api_mock.create_order.reset_mock()
|
||||
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
assert 'id' in order
|
||||
assert 'info' in order
|
||||
assert order['id'] == order_id
|
||||
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
|
||||
assert api_mock.create_order.call_args_list[0][1]['type'] == order_type
|
||||
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
|
||||
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
||||
assert api_mock.create_order.call_args_list[0][1]['price'] == 220
|
||||
assert api_mock.create_order.call_args_list[0][1]['params'] == {'trading_agreement': 'agree'}
|
||||
|
||||
# test exception handling
|
||||
with pytest.raises(DependencyException):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
with pytest.raises(InvalidOrderException):
|
||||
api_mock.create_order = MagicMock(
|
||||
side_effect=ccxt.InvalidOrder("kraken Order would trigger immediately."))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
with pytest.raises(TemporaryError):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection"))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
with pytest.raises(OperationalException, match=r".*DeadBeef.*"):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
|
||||
def test_stoploss_order_dry_run_kraken(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
order_type = 'stop-loss'
|
||||
default_conf['dry_run'] = True
|
||||
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||
|
||||
api_mock.create_order.reset_mock()
|
||||
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
assert 'id' in order
|
||||
assert 'info' in order
|
||||
assert 'type' in order
|
||||
|
||||
assert order['type'] == order_type
|
||||
assert order['price'] == 220
|
||||
assert order['amount'] == 1
|
||||
|
||||
|
||||
def test_stoploss_adjust_kraken(mocker, default_conf):
|
||||
exchange = get_patched_exchange(mocker, default_conf, id='kraken')
|
||||
order = {
|
||||
'type': 'stop-loss',
|
||||
'price': 1500,
|
||||
}
|
||||
assert exchange.stoploss_adjust(1501, order)
|
||||
assert not exchange.stoploss_adjust(1499, order)
|
||||
# Test with invalid order case ...
|
||||
order['type'] = 'stop_loss_limit'
|
||||
assert not exchange.stoploss_adjust(1501, order)
|
||||
|
@ -42,7 +42,13 @@ def hyperopt_results():
|
||||
'profit_percent': [-0.1, 0.2, 0.3],
|
||||
'profit_abs': [-0.2, 0.4, 0.6],
|
||||
'trade_duration': [10, 30, 10],
|
||||
'sell_reason': [SellType.STOP_LOSS, SellType.ROI, SellType.ROI]
|
||||
'sell_reason': [SellType.STOP_LOSS, SellType.ROI, SellType.ROI],
|
||||
'close_time':
|
||||
[
|
||||
datetime(2019, 1, 1, 9, 26, 3, 478039),
|
||||
datetime(2019, 2, 1, 9, 26, 3, 478039),
|
||||
datetime(2019, 3, 1, 9, 26, 3, 478039)
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
@ -336,6 +342,24 @@ def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> N
|
||||
assert under > correct
|
||||
|
||||
|
||||
def test_sharpe_loss_daily_prefers_higher_profits(default_conf, hyperopt_results) -> None:
|
||||
results_over = hyperopt_results.copy()
|
||||
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
|
||||
results_under = hyperopt_results.copy()
|
||||
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
|
||||
|
||||
default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'})
|
||||
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
|
||||
correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results),
|
||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||
over = hl.hyperopt_loss_function(results_over, len(hyperopt_results),
|
||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||
under = hl.hyperopt_loss_function(results_under, len(hyperopt_results),
|
||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||
assert over < correct
|
||||
assert under > correct
|
||||
|
||||
|
||||
def test_onlyprofit_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None:
|
||||
results_over = hyperopt_results.copy()
|
||||
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
|
||||
|
@ -21,14 +21,14 @@ def test_generate_text_table(default_conf, mocker):
|
||||
)
|
||||
|
||||
result_str = (
|
||||
'| pair | buy count | avg profit % | cum profit % | '
|
||||
'tot profit BTC | tot profit % | avg duration | profit | loss |\n'
|
||||
'|:--------|------------:|---------------:|---------------:|'
|
||||
'-----------------:|---------------:|:---------------|---------:|-------:|\n'
|
||||
'| ETH/BTC | 2 | 15.00 | 30.00 | '
|
||||
'0.60000000 | 15.00 | 0:20:00 | 2 | 0 |\n'
|
||||
'| TOTAL | 2 | 15.00 | 30.00 | '
|
||||
'0.60000000 | 15.00 | 0:20:00 | 2 | 0 |'
|
||||
'| Pair | Buy Count | Avg Profit % | Cum Profit % | Tot Profit BTC '
|
||||
'| Tot Profit % | Avg Duration | Wins | Losses |\n'
|
||||
'|:--------|------------:|---------------:|---------------:|-----------------:'
|
||||
'|---------------:|:---------------|-------:|---------:|\n'
|
||||
'| ETH/BTC | 2 | 15.00 | 30.00 | 0.60000000 '
|
||||
'| 15.00 | 0:20:00 | 2 | 0 |\n'
|
||||
'| TOTAL | 2 | 15.00 | 30.00 | 0.60000000 '
|
||||
'| 15.00 | 0:20:00 | 2 | 0 |'
|
||||
)
|
||||
assert generate_text_table(data={'ETH/BTC': {}},
|
||||
stake_currency='BTC', max_open_trades=2,
|
||||
@ -50,13 +50,19 @@ def test_generate_text_table_sell_reason(default_conf, mocker):
|
||||
)
|
||||
|
||||
result_str = (
|
||||
'| Sell Reason | Count | Profit | Loss | Profit % |\n'
|
||||
'|:--------------|--------:|---------:|-------:|-----------:|\n'
|
||||
'| roi | 2 | 2 | 0 | 15 |\n'
|
||||
'| Sell Reason | Sell Count | Wins | Losses | Avg Profit % |'
|
||||
' Cum Profit % | Tot Profit BTC | Tot Profit % |\n'
|
||||
'|:--------------|-------------:|-------:|---------:|---------------:|'
|
||||
'---------------:|-----------------:|---------------:|\n'
|
||||
'| roi | 2 | 2 | 0 | 15 |'
|
||||
' 30 | 0.6 | 15 |\n'
|
||||
'| stop_loss | 1 | 0 | 1 | -10 |'
|
||||
' -10 | -0.2 | -5 |'
|
||||
)
|
||||
assert generate_text_table_sell_reason(
|
||||
data={'ETH/BTC': {}}, results=results) == result_str
|
||||
data={'ETH/BTC': {}},
|
||||
stake_currency='BTC', max_open_trades=2,
|
||||
results=results) == result_str
|
||||
|
||||
|
||||
def test_generate_text_table_strategy(default_conf, mocker):
|
||||
@ -85,10 +91,10 @@ def test_generate_text_table_strategy(default_conf, mocker):
|
||||
)
|
||||
|
||||
result_str = (
|
||||
'| Strategy | buy count | avg profit % | cum profit % '
|
||||
'| tot profit BTC | tot profit % | avg duration | profit | loss |\n'
|
||||
'|:-----------|------------:|---------------:|---------------:'
|
||||
'|-----------------:|---------------:|:---------------|---------:|-------:|\n'
|
||||
'| Strategy | Buy Count | Avg Profit % | Cum Profit % | Tot Profit BTC '
|
||||
'| Tot Profit % | Avg Duration | Wins | Losses |\n'
|
||||
'|:-----------|------------:|---------------:|---------------:|-----------------:'
|
||||
'|---------------:|:---------------|-------:|---------:|\n'
|
||||
'| ETH/BTC | 3 | 20.00 | 60.00 '
|
||||
'| 1.10000000 | 30.00 | 0:17:00 | 3 | 0 |\n'
|
||||
'| LTC/BTC | 3 | 30.00 | 90.00 '
|
||||
|
@ -141,7 +141,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
|
||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}],
|
||||
"BTC", ['HOT/BTC', 'FUEL/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']),
|
||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}],
|
||||
"USDT", ['ETH/USDT']),
|
||||
"USDT", ['ETH/USDT', 'NANO/USDT']),
|
||||
# No pair for ETH ...
|
||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}],
|
||||
"ETH", []),
|
||||
@ -160,6 +160,10 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
|
||||
{"method": "PrecisionFilter"},
|
||||
{"method": "PriceFilter", "low_price_ratio": 0.02}
|
||||
], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']),
|
||||
# HOT and XRP are removed because below 1250 quoteVolume
|
||||
([{"method": "VolumePairList", "number_assets": 5,
|
||||
"sort_key": "quoteVolume", "min_value": 1250}],
|
||||
"BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']),
|
||||
# StaticPairlist Only
|
||||
([{"method": "StaticPairList"},
|
||||
], "BTC", ['ETH/BTC', 'TKN/BTC']),
|
||||
@ -167,6 +171,10 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
|
||||
([{"method": "StaticPairList"},
|
||||
{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"},
|
||||
], "BTC", ['TKN/BTC', 'ETH/BTC']),
|
||||
# SpreadFilter
|
||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
|
||||
{"method": "SpreadFilter", "max_spread": 0.005}
|
||||
], "USDT", ['ETH/USDT']),
|
||||
])
|
||||
def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers,
|
||||
pairlists, base_currency, whitelist_result,
|
||||
|
@ -797,10 +797,10 @@ def test_process_operational_exception(default_conf, ticker, mocker) -> None:
|
||||
worker = Worker(args=None, config=default_conf)
|
||||
patch_get_signal(worker.freqtrade)
|
||||
|
||||
assert worker.state == State.RUNNING
|
||||
assert worker.freqtrade.state == State.RUNNING
|
||||
|
||||
worker._process()
|
||||
assert worker.state == State.STOPPED
|
||||
assert worker.freqtrade.state == State.STOPPED
|
||||
assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]['status']
|
||||
|
||||
|
||||
@ -1023,8 +1023,8 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None
|
||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
|
||||
return_value=limit_buy_order['amount'])
|
||||
|
||||
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
||||
mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit)
|
||||
stoploss = MagicMock(return_value={'id': 13434334})
|
||||
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
|
||||
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
||||
@ -1037,13 +1037,13 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None
|
||||
|
||||
freqtrade.exit_positions(trades)
|
||||
assert trade.stoploss_order_id == '13434334'
|
||||
assert stoploss_limit.call_count == 1
|
||||
assert stoploss.call_count == 1
|
||||
assert trade.is_open is True
|
||||
|
||||
|
||||
def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
||||
limit_buy_order, limit_sell_order) -> None:
|
||||
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
||||
stoploss = MagicMock(return_value={'id': 13434334})
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
@ -1056,7 +1056,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||
get_fee=fee,
|
||||
stoploss_limit=stoploss_limit
|
||||
stoploss=stoploss
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtrade)
|
||||
@ -1070,7 +1070,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
||||
trade.stoploss_order_id = None
|
||||
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
assert stoploss_limit.call_count == 1
|
||||
assert stoploss.call_count == 1
|
||||
assert trade.stoploss_order_id == "13434334"
|
||||
|
||||
# Second case: when stoploss is set but it is not yet hit
|
||||
@ -1094,10 +1094,10 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
||||
|
||||
canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'})
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_order', canceled_stoploss_order)
|
||||
stoploss_limit.reset_mock()
|
||||
stoploss.reset_mock()
|
||||
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
assert stoploss_limit.call_count == 1
|
||||
assert stoploss.call_count == 1
|
||||
assert trade.stoploss_order_id == "13434334"
|
||||
|
||||
# Fourth case: when stoploss is set and it is hit
|
||||
@ -1124,9 +1124,10 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
||||
assert trade.is_open is False
|
||||
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.stoploss_limit',
|
||||
'freqtrade.exchange.Exchange.stoploss',
|
||||
side_effect=DependencyException()
|
||||
)
|
||||
trade.is_open = True
|
||||
freqtrade.handle_stoploss_on_exchange(trade)
|
||||
assert log_has('Unable to place a stoploss order on exchange.', caplog)
|
||||
assert trade.stoploss_order_id is None
|
||||
@ -1134,11 +1135,21 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
||||
# Fifth case: get_order returns InvalidOrder
|
||||
# It should try to add stoploss order
|
||||
trade.stoploss_order_id = 100
|
||||
stoploss_limit.reset_mock()
|
||||
stoploss.reset_mock()
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_order', side_effect=InvalidOrderException())
|
||||
mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit)
|
||||
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
|
||||
freqtrade.handle_stoploss_on_exchange(trade)
|
||||
assert stoploss_limit.call_count == 1
|
||||
assert stoploss.call_count == 1
|
||||
|
||||
# Sixth case: Closed Trade
|
||||
# Should not create new order
|
||||
trade.stoploss_order_id = None
|
||||
trade.is_open = False
|
||||
stoploss.reset_mock()
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_order')
|
||||
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
assert stoploss.call_count == 0
|
||||
|
||||
|
||||
def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog,
|
||||
@ -1157,7 +1168,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog,
|
||||
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||
get_fee=fee,
|
||||
get_order=MagicMock(return_value={'status': 'canceled'}),
|
||||
stoploss_limit=MagicMock(side_effect=DependencyException()),
|
||||
stoploss=MagicMock(side_effect=DependencyException()),
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtrade)
|
||||
@ -1165,7 +1176,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog,
|
||||
freqtrade.enter_positions()
|
||||
trade = Trade.query.first()
|
||||
trade.is_open = True
|
||||
trade.open_order_id = '12345'
|
||||
trade.open_order_id = None
|
||||
trade.stoploss_order_id = 100
|
||||
assert trade
|
||||
|
||||
@ -1191,7 +1202,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee,
|
||||
sell=sell_mock,
|
||||
get_fee=fee,
|
||||
get_order=MagicMock(return_value={'status': 'canceled'}),
|
||||
stoploss_limit=MagicMock(side_effect=InvalidOrderException()),
|
||||
stoploss=MagicMock(side_effect=InvalidOrderException()),
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtrade)
|
||||
@ -1221,7 +1232,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee,
|
||||
def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
|
||||
limit_buy_order, limit_sell_order) -> None:
|
||||
# When trailing stoploss is set
|
||||
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
||||
stoploss = MagicMock(return_value={'id': 13434334})
|
||||
patch_RPCManager(mocker)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
@ -1233,7 +1244,8 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
|
||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||
get_fee=fee,
|
||||
stoploss_limit=stoploss_limit
|
||||
stoploss=stoploss,
|
||||
stoploss_adjust=MagicMock(return_value=True),
|
||||
)
|
||||
|
||||
# enabling TSL
|
||||
@ -1288,7 +1300,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
|
||||
cancel_order_mock = MagicMock()
|
||||
stoploss_order_mock = MagicMock()
|
||||
mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock)
|
||||
mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_order_mock)
|
||||
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss_order_mock)
|
||||
|
||||
# stoploss should not be updated as the interval is 60 seconds
|
||||
assert freqtrade.handle_trade(trade) is False
|
||||
@ -1307,7 +1319,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
|
||||
cancel_order_mock.assert_called_once_with(100, 'ETH/BTC')
|
||||
stoploss_order_mock.assert_called_once_with(amount=85.25149190110828,
|
||||
pair='ETH/BTC',
|
||||
rate=0.00002344 * 0.95 * 0.99,
|
||||
order_types=freqtrade.strategy.order_types,
|
||||
stop_price=0.00002344 * 0.95)
|
||||
|
||||
# price fell below stoploss, so dry-run sells trade.
|
||||
@ -1322,7 +1334,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
|
||||
def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog,
|
||||
limit_buy_order, limit_sell_order) -> None:
|
||||
# When trailing stoploss is set
|
||||
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
||||
stoploss = MagicMock(return_value={'id': 13434334})
|
||||
patch_exchange(mocker)
|
||||
|
||||
mocker.patch.multiple(
|
||||
@ -1335,7 +1347,8 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
|
||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||
get_fee=fee,
|
||||
stoploss_limit=stoploss_limit
|
||||
stoploss=stoploss,
|
||||
stoploss_adjust=MagicMock(return_value=True),
|
||||
)
|
||||
|
||||
# enabling TSL
|
||||
@ -1375,12 +1388,12 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
|
||||
assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog)
|
||||
|
||||
# Still try to create order
|
||||
assert stoploss_limit.call_count == 1
|
||||
assert stoploss.call_count == 1
|
||||
|
||||
# Fail creating stoploss order
|
||||
caplog.clear()
|
||||
cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_order", MagicMock())
|
||||
mocker.patch("freqtrade.exchange.Exchange.stoploss_limit", side_effect=DependencyException())
|
||||
mocker.patch("freqtrade.exchange.Exchange.stoploss", side_effect=DependencyException())
|
||||
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
|
||||
assert cancel_mock.call_count == 1
|
||||
assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog)
|
||||
@ -1390,12 +1403,13 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
||||
limit_buy_order, limit_sell_order) -> None:
|
||||
|
||||
# When trailing stoploss is set
|
||||
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
||||
stoploss = MagicMock(return_value={'id': 13434334})
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
patch_edge(mocker)
|
||||
edge_conf['max_open_trades'] = float('inf')
|
||||
edge_conf['dry_run_wallet'] = 999.9
|
||||
edge_conf['exchange']['name'] = 'binance'
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=MagicMock(return_value={
|
||||
@ -1406,7 +1420,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||
get_fee=fee,
|
||||
stoploss_limit=stoploss_limit
|
||||
stoploss=stoploss,
|
||||
)
|
||||
|
||||
# enabling TSL
|
||||
@ -1459,7 +1473,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
||||
cancel_order_mock = MagicMock()
|
||||
stoploss_order_mock = MagicMock()
|
||||
mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock)
|
||||
mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_order_mock)
|
||||
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss_order_mock)
|
||||
|
||||
# price goes down 5%
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={
|
||||
@ -1492,7 +1506,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
||||
cancel_order_mock.assert_called_once_with(100, 'NEO/BTC')
|
||||
stoploss_order_mock.assert_called_once_with(amount=2131074.168797954,
|
||||
pair='NEO/BTC',
|
||||
rate=0.00002344 * 0.99 * 0.99,
|
||||
order_types=freqtrade.strategy.order_types,
|
||||
stop_price=0.00002344 * 0.99)
|
||||
|
||||
|
||||
@ -2423,7 +2437,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke
|
||||
default_conf['exchange']['name'] = 'binance'
|
||||
rpc_mock = patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
stoploss_limit = MagicMock(return_value={
|
||||
stoploss = MagicMock(return_value={
|
||||
'id': 123,
|
||||
'info': {
|
||||
'foo': 'bar'
|
||||
@ -2437,7 +2451,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke
|
||||
get_fee=fee,
|
||||
amount_to_precision=lambda s, x, y: y,
|
||||
price_to_precision=lambda s, x, y: y,
|
||||
stoploss_limit=stoploss_limit,
|
||||
stoploss=stoploss,
|
||||
cancel_order=cancel_order,
|
||||
)
|
||||
|
||||
@ -2482,14 +2496,14 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f
|
||||
price_to_precision=lambda s, x, y: y,
|
||||
)
|
||||
|
||||
stoploss_limit = MagicMock(return_value={
|
||||
stoploss = MagicMock(return_value={
|
||||
'id': 123,
|
||||
'info': {
|
||||
'foo': 'bar'
|
||||
}
|
||||
})
|
||||
|
||||
mocker.patch('freqtrade.exchange.Binance.stoploss_limit', stoploss_limit)
|
||||
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
|
||||
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
||||
@ -2507,7 +2521,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f
|
||||
# Assuming stoploss on exchnage is hit
|
||||
# stoploss_order_id should become None
|
||||
# and trade should be sold at the price of stoploss
|
||||
stoploss_limit_executed = MagicMock(return_value={
|
||||
stoploss_executed = MagicMock(return_value={
|
||||
"id": "123",
|
||||
"timestamp": 1542707426845,
|
||||
"datetime": "2018-11-20T09:50:26.845Z",
|
||||
@ -2525,7 +2539,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f
|
||||
"fee": None,
|
||||
"trades": None
|
||||
})
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_limit_executed)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_executed)
|
||||
|
||||
freqtrade.exit_positions(trades)
|
||||
assert trade.stoploss_order_id is None
|
||||
@ -3631,7 +3645,7 @@ def test_startup_state(default_conf, mocker):
|
||||
}
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||
worker = get_patched_worker(mocker, default_conf)
|
||||
assert worker.state is State.RUNNING
|
||||
assert worker.freqtrade.state is State.RUNNING
|
||||
|
||||
|
||||
def test_startup_trade_reinit(default_conf, edge_conf, mocker):
|
||||
|
@ -20,7 +20,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
||||
default_conf['max_open_trades'] = 3
|
||||
default_conf['exchange']['name'] = 'binance'
|
||||
|
||||
stoploss_limit = {
|
||||
stoploss = {
|
||||
'id': 123,
|
||||
'info': {}
|
||||
}
|
||||
@ -53,7 +53,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
||||
SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)]
|
||||
)
|
||||
cancel_order_mock = MagicMock()
|
||||
mocker.patch('freqtrade.exchange.Binance.stoploss_limit', stoploss_limit)
|
||||
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker,
|
||||
|
@ -11,11 +11,11 @@ from tests.conftest import get_patched_worker, log_has
|
||||
def test_worker_state(mocker, default_conf, markets) -> None:
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
|
||||
worker = get_patched_worker(mocker, default_conf)
|
||||
assert worker.state is State.RUNNING
|
||||
assert worker.freqtrade.state is State.RUNNING
|
||||
|
||||
default_conf.pop('initial_state')
|
||||
worker = Worker(args=None, config=default_conf)
|
||||
assert worker.state is State.STOPPED
|
||||
assert worker.freqtrade.state is State.STOPPED
|
||||
|
||||
|
||||
def test_worker_running(mocker, default_conf, caplog) -> None:
|
||||
@ -41,7 +41,7 @@ def test_worker_stopped(mocker, default_conf, caplog) -> None:
|
||||
mock_sleep = mocker.patch('time.sleep', return_value=None)
|
||||
|
||||
worker = get_patched_worker(mocker, default_conf)
|
||||
worker.state = State.STOPPED
|
||||
worker.freqtrade.state = State.STOPPED
|
||||
state = worker._worker(old_state=State.RUNNING)
|
||||
assert state is State.STOPPED
|
||||
assert log_has('Changing state to: STOPPED', caplog)
|
||||
|
Loading…
Reference in New Issue
Block a user