Merge branch 'freqtrade:feat/short' into feat/short
This commit is contained in:
commit
9d21261d25
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -2,14 +2,16 @@ Thank you for sending your pull request. But first, have you included
|
|||||||
unit tests, and is your code PEP8 conformant? [More details](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
unit tests, and is your code PEP8 conformant? [More details](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
Explain in one sentence the goal of this PR
|
Explain in one sentence the goal of this PR
|
||||||
|
|
||||||
Solve the issue: #___
|
Solve the issue: #___
|
||||||
|
|
||||||
## Quick changelog
|
## Quick changelog
|
||||||
|
|
||||||
- <change log #1>
|
- <change log 1>
|
||||||
- <change log #2>
|
- <change log 1>
|
||||||
|
|
||||||
## What's new?
|
## What's new?
|
||||||
|
|
||||||
*Explain in details what this PR solve or improve. You can include visuals.*
|
*Explain in details what this PR solve or improve. You can include visuals.*
|
||||||
|
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@ -87,7 +87,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cp config_examples/config_bittrex.example.json config.json
|
cp config_examples/config_bittrex.example.json config.json
|
||||||
freqtrade create-userdir --userdir user_data
|
freqtrade create-userdir --userdir user_data
|
||||||
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
||||||
|
|
||||||
- name: Flake8
|
- name: Flake8
|
||||||
run: |
|
run: |
|
||||||
@ -180,7 +180,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cp config_examples/config_bittrex.example.json config.json
|
cp config_examples/config_bittrex.example.json config.json
|
||||||
freqtrade create-userdir --userdir user_data
|
freqtrade create-userdir --userdir user_data
|
||||||
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
||||||
|
|
||||||
- name: Flake8
|
- name: Flake8
|
||||||
run: |
|
run: |
|
||||||
@ -247,7 +247,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cp config_examples/config_bittrex.example.json config.json
|
cp config_examples/config_bittrex.example.json config.json
|
||||||
freqtrade create-userdir --userdir user_data
|
freqtrade create-userdir --userdir user_data
|
||||||
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
||||||
|
|
||||||
- name: Flake8
|
- name: Flake8
|
||||||
run: |
|
run: |
|
||||||
|
@ -33,7 +33,7 @@ jobs:
|
|||||||
- script:
|
- script:
|
||||||
- cp config_examples/config_bittrex.example.json config.json
|
- cp config_examples/config_bittrex.example.json config.json
|
||||||
- freqtrade create-userdir --userdir user_data
|
- freqtrade create-userdir --userdir user_data
|
||||||
- freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily
|
- freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily
|
||||||
name: hyperopt
|
name: hyperopt
|
||||||
- script: flake8
|
- script: flake8
|
||||||
name: flake8
|
name: flake8
|
||||||
|
@ -13,7 +13,7 @@ RUN mkdir /freqtrade \
|
|||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get -y install sudo libatlas3-base curl sqlite3 libhdf5-serial-dev \
|
&& apt-get -y install sudo libatlas3-base curl sqlite3 libhdf5-serial-dev \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& useradd -u 1000 -G sudo -U -m ftuser \
|
&& useradd -u 1000 -G sudo -U -m -s /bin/bash ftuser \
|
||||||
&& chown ftuser:ftuser /freqtrade \
|
&& chown ftuser:ftuser /freqtrade \
|
||||||
# Allow sudoers
|
# Allow sudoers
|
||||||
&& echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers
|
&& echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers
|
||||||
|
@ -30,6 +30,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even
|
|||||||
- [X] [Bittrex](https://bittrex.com/)
|
- [X] [Bittrex](https://bittrex.com/)
|
||||||
- [X] [Kraken](https://kraken.com/)
|
- [X] [Kraken](https://kraken.com/)
|
||||||
- [X] [FTX](https://ftx.com)
|
- [X] [FTX](https://ftx.com)
|
||||||
|
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||||
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||||
|
|
||||||
### Community tested
|
### Community tested
|
||||||
@ -78,22 +79,22 @@ For any other type of installation please refer to [Installation doc](https://ww
|
|||||||
|
|
||||||
```
|
```
|
||||||
usage: freqtrade [-h] [-V]
|
usage: freqtrade [-h] [-V]
|
||||||
{trade,create-userdir,new-config,new-hyperopt,new-strategy,download-data,convert-data,convert-trade-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,plot-dataframe,plot-profit}
|
{trade,create-userdir,new-config,new-strategy,download-data,convert-data,convert-trade-data,list-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,install-ui,plot-dataframe,plot-profit,webserver}
|
||||||
...
|
...
|
||||||
|
|
||||||
Free, open source crypto trading bot
|
Free, open source crypto trading bot
|
||||||
|
|
||||||
positional arguments:
|
positional arguments:
|
||||||
{trade,create-userdir,new-config,new-hyperopt,new-strategy,download-data,convert-data,convert-trade-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,plot-dataframe,plot-profit}
|
{trade,create-userdir,new-config,new-strategy,download-data,convert-data,convert-trade-data,list-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,install-ui,plot-dataframe,plot-profit,webserver}
|
||||||
trade Trade module.
|
trade Trade module.
|
||||||
create-userdir Create user-data directory.
|
create-userdir Create user-data directory.
|
||||||
new-config Create new config
|
new-config Create new config
|
||||||
new-hyperopt Create new hyperopt
|
|
||||||
new-strategy Create new strategy
|
new-strategy Create new strategy
|
||||||
download-data Download backtesting data.
|
download-data Download backtesting data.
|
||||||
convert-data Convert candle (OHLCV) data from one format to
|
convert-data Convert candle (OHLCV) data from one format to
|
||||||
another.
|
another.
|
||||||
convert-trade-data Convert trade data from one format to another.
|
convert-trade-data Convert trade data from one format to another.
|
||||||
|
list-data List downloaded data.
|
||||||
backtesting Backtesting module.
|
backtesting Backtesting module.
|
||||||
edge Edge module.
|
edge Edge module.
|
||||||
hyperopt Hyperopt module.
|
hyperopt Hyperopt module.
|
||||||
@ -107,8 +108,10 @@ positional arguments:
|
|||||||
list-timeframes Print available timeframes for the exchange.
|
list-timeframes Print available timeframes for the exchange.
|
||||||
show-trades Show trades.
|
show-trades Show trades.
|
||||||
test-pairlist Test your pairlist configuration.
|
test-pairlist Test your pairlist configuration.
|
||||||
|
install-ui Install FreqUI
|
||||||
plot-dataframe Plot candles with indicators.
|
plot-dataframe Plot candles with indicators.
|
||||||
plot-profit Generate plot showing profits.
|
plot-profit Generate plot showing profits.
|
||||||
|
webserver Webserver module.
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
|
@ -12,9 +12,12 @@ if [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then
|
|||||||
&& curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub \
|
&& curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub \
|
||||||
&& ./configure --prefix=${INSTALL_LOC}/ \
|
&& ./configure --prefix=${INSTALL_LOC}/ \
|
||||||
&& make -j$(nproc) \
|
&& make -j$(nproc) \
|
||||||
&& which sudo && sudo make install || make install \
|
&& which sudo && sudo make install || make install
|
||||||
&& cd ..
|
if [ -x "$(command -v apt-get)" ]; then
|
||||||
|
echo "Updating library path using ldconfig"
|
||||||
|
sudo ldconfig
|
||||||
|
fi
|
||||||
|
cd .. && rm -rf ./ta-lib/
|
||||||
else
|
else
|
||||||
echo "TA-lib already installed, skipping installation"
|
echo "TA-lib already installed, skipping installation"
|
||||||
fi
|
fi
|
||||||
# && sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \
|
|
||||||
|
@ -67,10 +67,10 @@ Currently, the arguments are:
|
|||||||
This function needs to return a floating point number (`float`). Smaller numbers will be interpreted as better results. The parameters and balancing for this is up to you.
|
This function needs to return a floating point number (`float`). Smaller numbers will be interpreted as better results. The parameters and balancing for this is up to you.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
This function is called once per iteration - so please make sure to have this as optimized as possible to not slow hyperopt down unnecessarily.
|
This function is called once per epoch - so please make sure to have this as optimized as possible to not slow hyperopt down unnecessarily.
|
||||||
|
|
||||||
!!! Note
|
!!! Note "`*args` and `**kwargs`"
|
||||||
Please keep the arguments `*args` and `**kwargs` in the interface to allow us to extend this interface later.
|
Please keep the arguments `*args` and `**kwargs` in the interface to allow us to extend this interface in the future.
|
||||||
|
|
||||||
## Overriding pre-defined spaces
|
## Overriding pre-defined spaces
|
||||||
|
|
||||||
@ -80,10 +80,24 @@ To override a pre-defined space (`roi_space`, `generate_roi_table`, `stoploss_sp
|
|||||||
class MyAwesomeStrategy(IStrategy):
|
class MyAwesomeStrategy(IStrategy):
|
||||||
class HyperOpt:
|
class HyperOpt:
|
||||||
# Define a custom stoploss space.
|
# Define a custom stoploss space.
|
||||||
def stoploss_space(self):
|
def stoploss_space():
|
||||||
return [SKDecimal(-0.05, -0.01, decimals=3, name='stoploss')]
|
return [SKDecimal(-0.05, -0.01, decimals=3, name='stoploss')]
|
||||||
|
|
||||||
|
# Define custom ROI space
|
||||||
|
def roi_space() -> List[Dimension]:
|
||||||
|
return [
|
||||||
|
Integer(10, 120, name='roi_t1'),
|
||||||
|
Integer(10, 60, name='roi_t2'),
|
||||||
|
Integer(10, 40, name='roi_t3'),
|
||||||
|
SKDecimal(0.01, 0.04, decimals=3, name='roi_p1'),
|
||||||
|
SKDecimal(0.01, 0.07, decimals=3, name='roi_p2'),
|
||||||
|
SKDecimal(0.01, 0.20, decimals=3, name='roi_p3'),
|
||||||
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
All overrides are optional and can be mixed/matched as necessary.
|
||||||
|
|
||||||
## Space options
|
## Space options
|
||||||
|
|
||||||
For the additional spaces, scikit-optimize (in combination with Freqtrade) provides the following space types:
|
For the additional spaces, scikit-optimize (in combination with Freqtrade) provides the following space types:
|
||||||
@ -105,281 +119,3 @@ from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal,
|
|||||||
Assuming the definition of a rather small space (`SKDecimal(0.10, 0.15, decimals=2, name='xxx')`) - SKDecimal will have 5 possibilities (`[0.10, 0.11, 0.12, 0.13, 0.14, 0.15]`).
|
Assuming the definition of a rather small space (`SKDecimal(0.10, 0.15, decimals=2, name='xxx')`) - SKDecimal will have 5 possibilities (`[0.10, 0.11, 0.12, 0.13, 0.14, 0.15]`).
|
||||||
|
|
||||||
A corresponding real space `Real(0.10, 0.15 name='xxx')` on the other hand has an almost unlimited number of possibilities (`[0.10, 0.010000000001, 0.010000000002, ... 0.014999999999, 0.01500000000]`).
|
A corresponding real space `Real(0.10, 0.15 name='xxx')` on the other hand has an almost unlimited number of possibilities (`[0.10, 0.010000000001, 0.010000000002, ... 0.014999999999, 0.01500000000]`).
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Legacy Hyperopt
|
|
||||||
|
|
||||||
This Section explains the configuration of an explicit Hyperopt file (separate to the strategy).
|
|
||||||
|
|
||||||
!!! Warning "Deprecated / legacy mode"
|
|
||||||
Since the 2021.4 release you no longer have to write a separate hyperopt class, but all strategies can be hyperopted.
|
|
||||||
Please read the [main hyperopt page](hyperopt.md) for more details.
|
|
||||||
|
|
||||||
### Prepare hyperopt file
|
|
||||||
|
|
||||||
Configuring an explicit hyperopt file is similar to writing your own strategy, and many tasks will be similar.
|
|
||||||
|
|
||||||
!!! Tip "About this page"
|
|
||||||
For this page, we will be using a fictional strategy called `AwesomeStrategy` - which will be optimized using the `AwesomeHyperopt` class.
|
|
||||||
|
|
||||||
#### Create a Custom Hyperopt File
|
|
||||||
|
|
||||||
The simplest way to get started is to use the following command, which will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`.
|
|
||||||
|
|
||||||
Let assume you want a hyperopt file `AwesomeHyperopt.py`:
|
|
||||||
|
|
||||||
``` bash
|
|
||||||
freqtrade new-hyperopt --hyperopt AwesomeHyperopt
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Legacy Hyperopt checklist
|
|
||||||
|
|
||||||
Checklist on all tasks / possibilities in hyperopt
|
|
||||||
|
|
||||||
Depending on the space you want to optimize, only some of the below are required:
|
|
||||||
|
|
||||||
* fill `buy_strategy_generator` - for buy signal optimization
|
|
||||||
* fill `indicator_space` - for buy signal optimization
|
|
||||||
* fill `sell_strategy_generator` - for sell signal optimization
|
|
||||||
* fill `sell_indicator_space` - for sell signal optimization
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
`populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work.
|
|
||||||
|
|
||||||
Optional in hyperopt - can also be loaded from a strategy (recommended):
|
|
||||||
|
|
||||||
* `populate_indicators` - fallback to create indicators
|
|
||||||
* `populate_buy_trend` - fallback if not optimizing for buy space. should come from strategy
|
|
||||||
* `populate_sell_trend` - fallback if not optimizing for sell space. should come from strategy
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
You always have to provide a strategy to Hyperopt, even if your custom Hyperopt class contains all methods.
|
|
||||||
Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead.
|
|
||||||
|
|
||||||
Rarely you may also need to override:
|
|
||||||
|
|
||||||
* `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default)
|
|
||||||
* `generate_roi_table` - for custom ROI optimization (if you need the ranges for the values in the ROI table that differ from default or the number of entries (steps) in the ROI table which differs from the default 4 steps)
|
|
||||||
* `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default)
|
|
||||||
* `trailing_space` - for custom trailing stop optimization (if you need the ranges for the trailing stop parameters in the optimization hyperspace that differ from default)
|
|
||||||
|
|
||||||
#### Defining a buy signal optimization
|
|
||||||
|
|
||||||
Let's say you are curious: should you use MACD crossings or lower Bollinger
|
|
||||||
Bands to trigger your buys. And you also wonder should you use RSI or ADX to
|
|
||||||
help with those buy decisions. If you decide to use RSI or ADX, which values
|
|
||||||
should I use for them? So let's use hyperparameter optimization to solve this
|
|
||||||
mystery.
|
|
||||||
|
|
||||||
We will start by defining a search space:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def indicator_space() -> List[Dimension]:
|
|
||||||
"""
|
|
||||||
Define your Hyperopt space for searching strategy parameters
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
Integer(20, 40, name='adx-value'),
|
|
||||||
Integer(20, 40, name='rsi-value'),
|
|
||||||
Categorical([True, False], name='adx-enabled'),
|
|
||||||
Categorical([True, False], name='rsi-enabled'),
|
|
||||||
Categorical(['bb_lower', 'macd_cross_signal'], name='trigger')
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Above definition says: I have five parameters I want you to randomly combine
|
|
||||||
to find the best combination. Two of them are integer values (`adx-value` and `rsi-value`) and I want you test in the range of values 20 to 40.
|
|
||||||
Then we have three category variables. First two are either `True` or `False`.
|
|
||||||
We use these to either enable or disable the ADX and RSI guards.
|
|
||||||
The last one we call `trigger` and use it to decide which buy trigger we want to use.
|
|
||||||
|
|
||||||
So let's write the buy strategy generator using these values:
|
|
||||||
|
|
||||||
```python
|
|
||||||
@staticmethod
|
|
||||||
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
|
||||||
"""
|
|
||||||
Define the buy strategy parameters to be used by Hyperopt.
|
|
||||||
"""
|
|
||||||
def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
conditions = []
|
|
||||||
# GUARDS AND TRENDS
|
|
||||||
if 'adx-enabled' in params and params['adx-enabled']:
|
|
||||||
conditions.append(dataframe['adx'] > params['adx-value'])
|
|
||||||
if 'rsi-enabled' in params and params['rsi-enabled']:
|
|
||||||
conditions.append(dataframe['rsi'] < params['rsi-value'])
|
|
||||||
|
|
||||||
# TRIGGERS
|
|
||||||
if 'trigger' in params:
|
|
||||||
if params['trigger'] == 'bb_lower':
|
|
||||||
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
|
|
||||||
if params['trigger'] == 'macd_cross_signal':
|
|
||||||
conditions.append(qtpylib.crossed_above(
|
|
||||||
dataframe['macd'], dataframe['macdsignal']
|
|
||||||
))
|
|
||||||
|
|
||||||
# Check that volume is not 0
|
|
||||||
conditions.append(dataframe['volume'] > 0)
|
|
||||||
|
|
||||||
if conditions:
|
|
||||||
dataframe.loc[
|
|
||||||
reduce(lambda x, y: x & y, conditions),
|
|
||||||
'buy'] = 1
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
return populate_buy_trend
|
|
||||||
```
|
|
||||||
|
|
||||||
Hyperopt will now call `populate_buy_trend()` many times (`epochs`) with different value combinations.
|
|
||||||
It will use the given historical data and make buys based on the buy signals generated with the above function.
|
|
||||||
Based on the results, hyperopt will tell you which parameter combination produced the best results (based on the configured [loss function](#loss-functions)).
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators.
|
|
||||||
When you want to test an indicator that isn't used by the bot currently, remember to
|
|
||||||
add it to the `populate_indicators()` method in your strategy or hyperopt file.
|
|
||||||
|
|
||||||
#### Sell optimization
|
|
||||||
|
|
||||||
Similar to the buy-signal above, sell-signals can also be optimized.
|
|
||||||
Place the corresponding settings into the following methods
|
|
||||||
|
|
||||||
* Inside `sell_indicator_space()` - the parameters hyperopt shall be optimizing.
|
|
||||||
* Within `sell_strategy_generator()` - populate the nested method `populate_sell_trend()` to apply the parameters.
|
|
||||||
|
|
||||||
The configuration and rules are the same than for buy signals.
|
|
||||||
To avoid naming collisions in the search-space, please prefix all sell-spaces with `sell-`.
|
|
||||||
|
|
||||||
### Execute Hyperopt
|
|
||||||
|
|
||||||
Once you have updated your hyperopt configuration you can run it.
|
|
||||||
Because hyperopt tries a lot of combinations to find the best parameters it will take time to get a good result. More time usually results in better results.
|
|
||||||
|
|
||||||
We strongly recommend to use `screen` or `tmux` to prevent any connection loss.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
freqtrade hyperopt --config config.json --hyperopt <hyperoptname> --hyperopt-loss <hyperoptlossname> --strategy <strategyname> -e 500 --spaces all
|
|
||||||
```
|
|
||||||
|
|
||||||
Use `<hyperoptname>` as the name of the custom hyperopt used.
|
|
||||||
|
|
||||||
The `-e` option will set how many evaluations hyperopt will do. Since hyperopt uses Bayesian search, running too many epochs at once may not produce greater results. Experience has shown that best results are usually not improving much after 500-1000 epochs.
|
|
||||||
Doing multiple runs (executions) with a few 1000 epochs and different random state will most likely produce different results.
|
|
||||||
|
|
||||||
The `--spaces all` option determines that all possible parameters should be optimized. Possibilities are listed below.
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
Hyperopt will store hyperopt results with the timestamp of the hyperopt start time.
|
|
||||||
Reading commands (`hyperopt-list`, `hyperopt-show`) can use `--hyperopt-filename <filename>` to read and display older hyperopt results.
|
|
||||||
You can find a list of filenames with `ls -l user_data/hyperopt_results/`.
|
|
||||||
|
|
||||||
#### Running Hyperopt using methods from a strategy
|
|
||||||
|
|
||||||
Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
freqtrade hyperopt --hyperopt AwesomeHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy AwesomeStrategy
|
|
||||||
```
|
|
||||||
|
|
||||||
### Understand the Hyperopt Result
|
|
||||||
|
|
||||||
Once Hyperopt is completed you can use the result to create a new strategy.
|
|
||||||
Given the following result from hyperopt:
|
|
||||||
|
|
||||||
```
|
|
||||||
Best result:
|
|
||||||
|
|
||||||
44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722%). Avg duration 180.4 mins. Objective: 1.94367
|
|
||||||
|
|
||||||
Buy hyperspace params:
|
|
||||||
{ 'adx-value': 44,
|
|
||||||
'rsi-value': 29,
|
|
||||||
'adx-enabled': False,
|
|
||||||
'rsi-enabled': True,
|
|
||||||
'trigger': 'bb_lower'}
|
|
||||||
```
|
|
||||||
|
|
||||||
You should understand this result like:
|
|
||||||
|
|
||||||
* The buy trigger that worked best was `bb_lower`.
|
|
||||||
* You should not use ADX because `adx-enabled: False`)
|
|
||||||
* You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`)
|
|
||||||
|
|
||||||
You have to look inside your strategy file into `buy_strategy_generator()`
|
|
||||||
method, what those values match to.
|
|
||||||
|
|
||||||
So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that translates to the following code block:
|
|
||||||
|
|
||||||
```python
|
|
||||||
(dataframe['rsi'] < 29.0)
|
|
||||||
```
|
|
||||||
|
|
||||||
Translating your whole hyperopt result as the new buy-signal would then look like:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
|
||||||
dataframe.loc[
|
|
||||||
(
|
|
||||||
(dataframe['rsi'] < 29.0) & # rsi-value
|
|
||||||
dataframe['close'] < dataframe['bb_lowerband'] # trigger
|
|
||||||
),
|
|
||||||
'buy'] = 1
|
|
||||||
return dataframe
|
|
||||||
```
|
|
||||||
|
|
||||||
### Validate backtesting results
|
|
||||||
|
|
||||||
Once the optimized parameters and conditions have been implemented into your strategy, you should backtest the strategy to make sure everything is working as expected.
|
|
||||||
|
|
||||||
To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting.
|
|
||||||
|
|
||||||
Should results not match, please double-check to make sure you transferred all conditions correctly.
|
|
||||||
Pay special care to the stoploss (and trailing stoploss) parameters, as these are often set in configuration files, which override changes to the strategy.
|
|
||||||
You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss` or `trailing_stop`).
|
|
||||||
|
|
||||||
### Sharing methods with your strategy
|
|
||||||
|
|
||||||
Hyperopt classes provide access to the Strategy via the `strategy` class attribute.
|
|
||||||
This can be a great way to reduce code duplication if used correctly, but will also complicate usage for inexperienced users.
|
|
||||||
|
|
||||||
``` python
|
|
||||||
from pandas import DataFrame
|
|
||||||
from freqtrade.strategy.interface import IStrategy
|
|
||||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
|
||||||
|
|
||||||
class MyAwesomeStrategy(IStrategy):
|
|
||||||
|
|
||||||
buy_params = {
|
|
||||||
'rsi-value': 30,
|
|
||||||
'adx-value': 35,
|
|
||||||
}
|
|
||||||
|
|
||||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
return self.buy_strategy_generator(self.buy_params, dataframe, metadata)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def buy_strategy_generator(params, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
dataframe.loc[
|
|
||||||
(
|
|
||||||
qtpylib.crossed_above(dataframe['rsi'], params['rsi-value']) &
|
|
||||||
dataframe['adx'] > params['adx-value']) &
|
|
||||||
dataframe['volume'] > 0
|
|
||||||
)
|
|
||||||
, 'buy'] = 1
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
class MyAwesomeHyperOpt(IHyperOpt):
|
|
||||||
...
|
|
||||||
@staticmethod
|
|
||||||
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
|
||||||
"""
|
|
||||||
Define the buy strategy parameters to be used by Hyperopt.
|
|
||||||
"""
|
|
||||||
def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
# Call strategy's buy strategy generator
|
|
||||||
return self.StrategyClass.buy_strategy_generator(params, dataframe, metadata)
|
|
||||||
|
|
||||||
return populate_buy_trend
|
|
||||||
```
|
|
||||||
|
@ -12,22 +12,22 @@ This page explains the different parameters of the bot and how to run it.
|
|||||||
|
|
||||||
```
|
```
|
||||||
usage: freqtrade [-h] [-V]
|
usage: freqtrade [-h] [-V]
|
||||||
{trade,create-userdir,new-config,new-hyperopt,new-strategy,download-data,convert-data,convert-trade-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,plot-dataframe,plot-profit}
|
{trade,create-userdir,new-config,new-strategy,download-data,convert-data,convert-trade-data,list-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,install-ui,plot-dataframe,plot-profit,webserver}
|
||||||
...
|
...
|
||||||
|
|
||||||
Free, open source crypto trading bot
|
Free, open source crypto trading bot
|
||||||
|
|
||||||
positional arguments:
|
positional arguments:
|
||||||
{trade,create-userdir,new-config,new-hyperopt,new-strategy,download-data,convert-data,convert-trade-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,plot-dataframe,plot-profit}
|
{trade,create-userdir,new-config,new-strategy,download-data,convert-data,convert-trade-data,list-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,install-ui,plot-dataframe,plot-profit,webserver}
|
||||||
trade Trade module.
|
trade Trade module.
|
||||||
create-userdir Create user-data directory.
|
create-userdir Create user-data directory.
|
||||||
new-config Create new config
|
new-config Create new config
|
||||||
new-hyperopt Create new hyperopt
|
|
||||||
new-strategy Create new strategy
|
new-strategy Create new strategy
|
||||||
download-data Download backtesting data.
|
download-data Download backtesting data.
|
||||||
convert-data Convert candle (OHLCV) data from one format to
|
convert-data Convert candle (OHLCV) data from one format to
|
||||||
another.
|
another.
|
||||||
convert-trade-data Convert trade data from one format to another.
|
convert-trade-data Convert trade data from one format to another.
|
||||||
|
list-data List downloaded data.
|
||||||
backtesting Backtesting module.
|
backtesting Backtesting module.
|
||||||
edge Edge module.
|
edge Edge module.
|
||||||
hyperopt Hyperopt module.
|
hyperopt Hyperopt module.
|
||||||
@ -41,8 +41,10 @@ positional arguments:
|
|||||||
list-timeframes Print available timeframes for the exchange.
|
list-timeframes Print available timeframes for the exchange.
|
||||||
show-trades Show trades.
|
show-trades Show trades.
|
||||||
test-pairlist Test your pairlist configuration.
|
test-pairlist Test your pairlist configuration.
|
||||||
|
install-ui Install FreqUI
|
||||||
plot-dataframe Plot candles with indicators.
|
plot-dataframe Plot candles with indicators.
|
||||||
plot-profit Generate plot showing profits.
|
plot-profit Generate plot showing profits.
|
||||||
|
webserver Webserver module.
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
|
@ -444,8 +444,8 @@ The possible values are: `gtc` (default), `fok` or `ioc`.
|
|||||||
```
|
```
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
This is ongoing work. For now, it is supported only for binance.
|
This is ongoing work. For now, it is supported only for binance and kucoin.
|
||||||
Please don't change the default value unless you know what you are doing and have researched the impact of using different values.
|
Please don't change the default value unless you know what you are doing and have researched the impact of using different values for your particular exchange.
|
||||||
|
|
||||||
### Exchange configuration
|
### Exchange configuration
|
||||||
|
|
||||||
|
@ -38,3 +38,8 @@ Since only quoteVolume can be compared between assets, the other options (bidVol
|
|||||||
|
|
||||||
Using `order_book_min` and `order_book_max` used to allow stepping the orderbook and trying to find the next ROI slot - trying to place sell-orders early.
|
Using `order_book_min` and `order_book_max` used to allow stepping the orderbook and trying to find the next ROI slot - trying to place sell-orders early.
|
||||||
As this does however increase risk and provides no benefit, it's been removed for maintainability purposes in 2021.7.
|
As this does however increase risk and provides no benefit, it's been removed for maintainability purposes in 2021.7.
|
||||||
|
|
||||||
|
### Legacy Hyperopt mode
|
||||||
|
|
||||||
|
Using separate hyperopt files was deprecated in 2021.4 and was removed in 2021.9.
|
||||||
|
Please switch to the new [Parametrized Strategies](hyperopt.md) to benefit from the new hyperopt interface.
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
The `Edge Positioning` module uses probability to calculate your win rate and risk reward ratio. It will use these statistics to control your strategy trade entry points, position size and, stoploss.
|
The `Edge Positioning` module uses probability to calculate your win rate and risk reward ratio. It will use these statistics to control your strategy trade entry points, position size and, stoploss.
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
WHen using `Edge positioning` with a dynamic whitelist (VolumePairList), make sure to also use `AgeFilter` and set it to at least `calculate_since_number_of_days` to avoid problems with missing data.
|
When using `Edge positioning` with a dynamic whitelist (VolumePairList), make sure to also use `AgeFilter` and set it to at least `calculate_since_number_of_days` to avoid problems with missing data.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
`Edge Positioning` only considers *its own* buy/sell/stoploss signals. It ignores the stoploss, trailing stoploss, and ROI settings in the strategy configuration file.
|
`Edge Positioning` only considers *its own* buy/sell/stoploss signals. It ignores the stoploss, trailing stoploss, and ROI settings in the strategy configuration file.
|
||||||
|
@ -4,6 +4,8 @@ This page combines common gotchas and informations which are exchange-specific a
|
|||||||
|
|
||||||
## Binance
|
## Binance
|
||||||
|
|
||||||
|
Binance supports [time_in_force](configuration.md#understand-order_time_in_force).
|
||||||
|
|
||||||
!!! Tip "Stoploss on Exchange"
|
!!! Tip "Stoploss on Exchange"
|
||||||
Binance supports `stoploss_on_exchange` and uses stop-loss-limit orders. 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.
|
||||||
|
|
||||||
@ -56,6 +58,12 @@ Bittrex does not support market orders. If you have a message at the bot startup
|
|||||||
Bittrex also does not support `VolumePairlist` due to limited / split API constellation at the moment.
|
Bittrex also does not support `VolumePairlist` due to limited / split API constellation at the moment.
|
||||||
Please use `StaticPairlist`. Other pairlists (other than `VolumePairlist`) should not be affected.
|
Please use `StaticPairlist`. Other pairlists (other than `VolumePairlist`) should not be affected.
|
||||||
|
|
||||||
|
### Volume pairlist
|
||||||
|
|
||||||
|
Bittrex does not support the direct usage of VolumePairList. This can however be worked around by using the advanced mode with `lookback_days: 1` (or more), which will emulate 24h volume.
|
||||||
|
|
||||||
|
Read more in the [pairlist documentation](plugins.md#volumepairlist-advanced-mode).
|
||||||
|
|
||||||
### Restricted markets
|
### Restricted markets
|
||||||
|
|
||||||
Bittrex split its exchange into US and International versions.
|
Bittrex split its exchange into US and International versions.
|
||||||
@ -113,8 +121,12 @@ Kucoin requires a passphrase for each api key, you will therefore need to add th
|
|||||||
"key": "your_exchange_key",
|
"key": "your_exchange_key",
|
||||||
"secret": "your_exchange_secret",
|
"secret": "your_exchange_secret",
|
||||||
"password": "your_exchange_api_key_password",
|
"password": "your_exchange_api_key_password",
|
||||||
|
// ...
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force).
|
||||||
|
|
||||||
### Kucoin Blacklists
|
### Kucoin Blacklists
|
||||||
|
|
||||||
For Kucoin, please add `"KCS/<STAKE>"` to your blacklist to avoid issues.
|
For Kucoin, please add `"KCS/<STAKE>"` to your blacklist to avoid issues.
|
||||||
@ -158,6 +170,8 @@ For example, to test the order type `FOK` with Kraken, and modify candle limit t
|
|||||||
"order_time_in_force": ["gtc", "fok"],
|
"order_time_in_force": ["gtc", "fok"],
|
||||||
"ohlcv_candle_limit": 200
|
"ohlcv_candle_limit": 200
|
||||||
}
|
}
|
||||||
|
//...
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
|
@ -167,7 +167,7 @@ Since hyperopt uses Bayesian search, running for too many epochs may not produce
|
|||||||
It's therefore recommended to run between 500-1000 epochs over and over until you hit at least 10.000 epochs in total (or are satisfied with the result). You can best judge by looking at the results - if the bot keeps discovering better strategies, it's best to keep on going.
|
It's therefore recommended to run between 500-1000 epochs over and over until you hit at least 10.000 epochs in total (or are satisfied with the result). You can best judge by looking at the results - if the bot keeps discovering better strategies, it's best to keep on going.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
freqtrade hyperopt --hyperopt SampleHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy SampleStrategy -e 1000
|
freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy SampleStrategy -e 1000
|
||||||
```
|
```
|
||||||
|
|
||||||
### Why does it take a long time to run hyperopt?
|
### Why does it take a long time to run hyperopt?
|
||||||
|
@ -44,9 +44,8 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
|||||||
[--data-format-ohlcv {json,jsongz,hdf5}]
|
[--data-format-ohlcv {json,jsongz,hdf5}]
|
||||||
[--max-open-trades INT]
|
[--max-open-trades INT]
|
||||||
[--stake-amount STAKE_AMOUNT] [--fee FLOAT]
|
[--stake-amount STAKE_AMOUNT] [--fee FLOAT]
|
||||||
[-p PAIRS [PAIRS ...]] [--hyperopt NAME]
|
[-p PAIRS [PAIRS ...]] [--hyperopt-path PATH]
|
||||||
[--hyperopt-path PATH] [--eps] [--dmmp]
|
[--eps] [--dmmp] [--enable-protections]
|
||||||
[--enable-protections]
|
|
||||||
[--dry-run-wallet DRY_RUN_WALLET] [-e INT]
|
[--dry-run-wallet DRY_RUN_WALLET] [-e INT]
|
||||||
[--spaces {all,buy,sell,roi,stoploss,trailing,protection,default} [{all,buy,sell,roi,stoploss,trailing,protection,default} ...]]
|
[--spaces {all,buy,sell,roi,stoploss,trailing,protection,default} [{all,buy,sell,roi,stoploss,trailing,protection,default} ...]]
|
||||||
[--print-all] [--no-color] [--print-json] [-j JOBS]
|
[--print-all] [--no-color] [--print-json] [-j JOBS]
|
||||||
@ -73,10 +72,8 @@ optional arguments:
|
|||||||
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
||||||
Limit command to these pairs. Pairs are space-
|
Limit command to these pairs. Pairs are space-
|
||||||
separated.
|
separated.
|
||||||
--hyperopt NAME Specify hyperopt class name which will be used by the
|
--hyperopt-path PATH Specify additional lookup path for Hyperopt Loss
|
||||||
bot.
|
functions.
|
||||||
--hyperopt-path PATH Specify additional lookup path for Hyperopt and
|
|
||||||
Hyperopt Loss functions.
|
|
||||||
--eps, --enable-position-stacking
|
--eps, --enable-position-stacking
|
||||||
Allow buying the same pair multiple times (position
|
Allow buying the same pair multiple times (position
|
||||||
stacking).
|
stacking).
|
||||||
@ -558,7 +555,7 @@ For example, to use one month of data, pass `--timerange 20210101-20210201` (fro
|
|||||||
Full command:
|
Full command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
freqtrade hyperopt --hyperopt <hyperoptname> --strategy <strategyname> --timerange 20210101-20210201
|
freqtrade hyperopt --strategy <strategyname> --timerange 20210101-20210201
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running Hyperopt with Smaller Search Space
|
### Running Hyperopt with Smaller Search Space
|
||||||
@ -684,7 +681,7 @@ If you have the `generate_roi_table()` and `roi_space()` methods in your custom
|
|||||||
|
|
||||||
Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps).
|
Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps).
|
||||||
|
|
||||||
A sample for these methods can be found in [sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py).
|
A sample for these methods can be found in the [overriding pre-defined spaces section](advanced-hyperopt.md#overriding-pre-defined-spaces).
|
||||||
|
|
||||||
!!! Note "Reduced search space"
|
!!! Note "Reduced search space"
|
||||||
To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs.
|
To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs.
|
||||||
@ -726,7 +723,7 @@ If you are optimizing stoploss values, Freqtrade creates the 'stoploss' optimiza
|
|||||||
|
|
||||||
If you have the `stoploss_space()` method in your custom hyperopt file, remove it in order to utilize Stoploss hyperoptimization space generated by Freqtrade by default.
|
If you have the `stoploss_space()` method in your custom hyperopt file, remove it in order to utilize Stoploss hyperoptimization space generated by Freqtrade by default.
|
||||||
|
|
||||||
Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py).
|
Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in the [overriding pre-defined spaces section](advanced-hyperopt.md#overriding-pre-defined-spaces).
|
||||||
|
|
||||||
!!! Note "Reduced search space"
|
!!! Note "Reduced search space"
|
||||||
To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs.
|
To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs.
|
||||||
@ -764,10 +761,10 @@ As stated in the comment, you can also use it as the values of the corresponding
|
|||||||
|
|
||||||
If you are optimizing trailing stop values, Freqtrade creates the 'trailing' optimization hyperspace for you. By default, the `trailing_stop` parameter is always set to True in that hyperspace, the value of the `trailing_only_offset_is_reached` vary between True and False, the values of the `trailing_stop_positive` and `trailing_stop_positive_offset` parameters vary in the ranges 0.02...0.35 and 0.01...0.1 correspondingly, which is sufficient in most cases.
|
If you are optimizing trailing stop values, Freqtrade creates the 'trailing' optimization hyperspace for you. By default, the `trailing_stop` parameter is always set to True in that hyperspace, the value of the `trailing_only_offset_is_reached` vary between True and False, the values of the `trailing_stop_positive` and `trailing_stop_positive_offset` parameters vary in the ranges 0.02...0.35 and 0.01...0.1 correspondingly, which is sufficient in most cases.
|
||||||
|
|
||||||
Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py).
|
Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in the [overriding pre-defined spaces section](advanced-hyperopt.md#overriding-pre-defined-spaces).
|
||||||
|
|
||||||
!!! Note "Reduced search space"
|
!!! Note "Reduced search space"
|
||||||
To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs.
|
To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#overriding-pre-defined-spaces) to change this to your needs.
|
||||||
|
|
||||||
### Reproducible results
|
### Reproducible results
|
||||||
|
|
||||||
|
@ -82,6 +82,8 @@ Filtering instances (not the first position in the list) will not apply any cach
|
|||||||
|
|
||||||
You can define a minimum volume with `min_value` - which will filter out pairs with a volume lower than the specified value in the specified timerange.
|
You can define a minimum volume with `min_value` - which will filter out pairs with a volume lower than the specified value in the specified timerange.
|
||||||
|
|
||||||
|
### VolumePairList Advanced mode
|
||||||
|
|
||||||
`VolumePairList` can also operate in an advanced mode to build volume over a given timerange of specified candle size. It utilizes exchange historical candle data, builds a typical price (calculated by (open+high+low)/3) and multiplies the typical price with every candle's volume. The sum is the `quoteVolume` over the given range. This allows different scenarios, for a more smoothened volume, when using longer ranges with larger candle sizes, or the opposite when using a short range with small candles.
|
`VolumePairList` can also operate in an advanced mode to build volume over a given timerange of specified candle size. It utilizes exchange historical candle data, builds a typical price (calculated by (open+high+low)/3) and multiplies the typical price with every candle's volume. The sum is the `quoteVolume` over the given range. This allows different scenarios, for a more smoothened volume, when using longer ranges with larger candle sizes, or the opposite when using a short range with small candles.
|
||||||
|
|
||||||
For convenience `lookback_days` can be specified, which will imply that 1d candles will be used for the lookback. In the example below the pairlist would be created based on the last 7 days:
|
For convenience `lookback_days` can be specified, which will imply that 1d candles will be used for the lookback. In the example below the pairlist would be created based on the last 7 days:
|
||||||
@ -105,6 +107,24 @@ For convenience `lookback_days` can be specified, which will imply that 1d candl
|
|||||||
!!! Warning "Performance implications when using lookback range"
|
!!! Warning "Performance implications when using lookback range"
|
||||||
If used in first position in combination with lookback, the computation of the range based volume can be time and resource consuming, as it downloads candles for all tradable pairs. Hence it's highly advised to use the standard approach with `VolumeFilter` to narrow the pairlist down for further range volume calculation.
|
If used in first position in combination with lookback, the computation of the range based volume can be time and resource consuming, as it downloads candles for all tradable pairs. Hence it's highly advised to use the standard approach with `VolumeFilter` to narrow the pairlist down for further range volume calculation.
|
||||||
|
|
||||||
|
??? Tip "Unsupported exchanges (Bittrex, Gemini)"
|
||||||
|
On some exchanges (like Bittrex and Gemini), regular VolumePairList does not work as the api does not natively provide 24h volume. This can be worked around by using candle data to build the volume.
|
||||||
|
To roughly simulate 24h volume, you can use the following configuration.
|
||||||
|
Please note that These pairlists will only refresh once per day.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"pairlists": [
|
||||||
|
{
|
||||||
|
"method": "VolumePairList",
|
||||||
|
"number_assets": 20,
|
||||||
|
"sort_key": "quoteVolume",
|
||||||
|
"min_value": 0,
|
||||||
|
"refresh_period": 86400,
|
||||||
|
"lookback_days": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
More sophisticated approach can be used, by using `lookback_timeframe` for candle size and `lookback_period` which specifies the amount of candles. This example will build the volume pairs based on a rolling period of 3 days of 1h candles:
|
More sophisticated approach can be used, by using `lookback_timeframe` for candle size and `lookback_period` which specifies the amount of candles. This example will build the volume pairs based on a rolling period of 3 days of 1h candles:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
@ -40,6 +40,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual,
|
|||||||
- [X] [Bittrex](https://bittrex.com/)
|
- [X] [Bittrex](https://bittrex.com/)
|
||||||
- [X] [FTX](https://ftx.com)
|
- [X] [FTX](https://ftx.com)
|
||||||
- [X] [Kraken](https://kraken.com/)
|
- [X] [Kraken](https://kraken.com/)
|
||||||
|
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||||
- [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
- [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||||
|
|
||||||
### Community tested
|
### Community tested
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
mkdocs==1.2.2
|
mkdocs==1.2.2
|
||||||
mkdocs-material==7.2.5
|
mkdocs-material==7.2.6
|
||||||
mdx_truly_sane_lists==1.2
|
mdx_truly_sane_lists==1.2
|
||||||
pymdown-extensions==8.2
|
pymdown-extensions==8.2
|
||||||
|
@ -26,9 +26,7 @@ optional arguments:
|
|||||||
├── data
|
├── data
|
||||||
├── hyperopt_results
|
├── hyperopt_results
|
||||||
├── hyperopts
|
├── hyperopts
|
||||||
│ ├── sample_hyperopt_advanced.py
|
|
||||||
│ ├── sample_hyperopt_loss.py
|
│ ├── sample_hyperopt_loss.py
|
||||||
│ └── sample_hyperopt.py
|
|
||||||
├── notebooks
|
├── notebooks
|
||||||
│ └── strategy_analysis_example.ipynb
|
│ └── strategy_analysis_example.ipynb
|
||||||
├── plot
|
├── plot
|
||||||
@ -111,46 +109,11 @@ Using the advanced template (populates all optional functions and methods)
|
|||||||
freqtrade new-strategy --strategy AwesomeStrategy --template advanced
|
freqtrade new-strategy --strategy AwesomeStrategy --template advanced
|
||||||
```
|
```
|
||||||
|
|
||||||
## Create new hyperopt
|
## List Strategies
|
||||||
|
|
||||||
Creates a new hyperopt from a template similar to SampleHyperopt.
|
Use the `list-strategies` subcommand to see all strategies in one particular directory.
|
||||||
The file will be named inline with your class name, and will not overwrite existing files.
|
|
||||||
|
|
||||||
Results will be located in `user_data/hyperopts/<classname>.py`.
|
This subcommand is useful for finding problems in your environment with loading strategies: modules with strategies that contain errors and failed to load are printed in red (LOAD FAILED), while strategies with duplicate names are printed in yellow (DUPLICATE NAME).
|
||||||
|
|
||||||
``` output
|
|
||||||
usage: freqtrade new-hyperopt [-h] [--userdir PATH] [--hyperopt NAME]
|
|
||||||
[--template {full,minimal,advanced}]
|
|
||||||
|
|
||||||
optional arguments:
|
|
||||||
-h, --help show this help message and exit
|
|
||||||
--userdir PATH, --user-data-dir PATH
|
|
||||||
Path to userdata directory.
|
|
||||||
--hyperopt NAME Specify hyperopt class name which will be used by the
|
|
||||||
bot.
|
|
||||||
--template {full,minimal,advanced}
|
|
||||||
Use a template which is either `minimal`, `full`
|
|
||||||
(containing multiple sample indicators) or `advanced`.
|
|
||||||
Default: `full`.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Sample usage of new-hyperopt
|
|
||||||
|
|
||||||
```bash
|
|
||||||
freqtrade new-hyperopt --hyperopt AwesomeHyperopt
|
|
||||||
```
|
|
||||||
|
|
||||||
With custom user directory
|
|
||||||
|
|
||||||
```bash
|
|
||||||
freqtrade new-hyperopt --userdir ~/.freqtrade/ --hyperopt AwesomeHyperopt
|
|
||||||
```
|
|
||||||
|
|
||||||
## List Strategies and List Hyperopts
|
|
||||||
|
|
||||||
Use the `list-strategies` subcommand to see all strategies in one particular directory and the `list-hyperopts` subcommand to list custom Hyperopts.
|
|
||||||
|
|
||||||
These subcommands are useful for finding problems in your environment with loading strategies or hyperopt classes: modules with strategies or hyperopt classes that contain errors and failed to load are printed in red (LOAD FAILED), while strategies or hyperopt classes with duplicate names are printed in yellow (DUPLICATE NAME).
|
|
||||||
|
|
||||||
```
|
```
|
||||||
usage: freqtrade list-strategies [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
usage: freqtrade list-strategies [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||||
@ -164,34 +127,6 @@ optional arguments:
|
|||||||
--no-color Disable colorization of hyperopt results. May be
|
--no-color Disable colorization of hyperopt results. May be
|
||||||
useful if you are redirecting output to a file.
|
useful if you are redirecting output to a file.
|
||||||
|
|
||||||
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.
|
|
||||||
```
|
|
||||||
```
|
|
||||||
usage: freqtrade list-hyperopts [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
|
||||||
[-d PATH] [--userdir PATH]
|
|
||||||
[--hyperopt-path PATH] [-1] [--no-color]
|
|
||||||
|
|
||||||
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.
|
|
||||||
--no-color Disable colorization of hyperopt results. May be
|
|
||||||
useful if you are redirecting output to a file.
|
|
||||||
|
|
||||||
Common arguments:
|
Common arguments:
|
||||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
--logfile FILE Log to the file specified. Special values are:
|
--logfile FILE Log to the file specified. Special values are:
|
||||||
@ -211,18 +146,16 @@ Common arguments:
|
|||||||
!!! Warning
|
!!! Warning
|
||||||
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.
|
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 strategies and hyperopts directories (within the default userdir).
|
Example: Search default strategies directories (within the default userdir).
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade list-strategies
|
freqtrade list-strategies
|
||||||
freqtrade list-hyperopts
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Example: Search strategies and hyperopts directory within the userdir.
|
Example: Search strategies directory within the userdir.
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade list-strategies --userdir ~/.freqtrade/
|
freqtrade list-strategies --userdir ~/.freqtrade/
|
||||||
freqtrade list-hyperopts --userdir ~/.freqtrade/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Example: Search dedicated strategy path.
|
Example: Search dedicated strategy path.
|
||||||
@ -231,12 +164,6 @@ Example: Search dedicated strategy path.
|
|||||||
freqtrade list-strategies --strategy-path ~/.freqtrade/strategies/
|
freqtrade list-strategies --strategy-path ~/.freqtrade/strategies/
|
||||||
```
|
```
|
||||||
|
|
||||||
Example: Search dedicated hyperopt path.
|
|
||||||
|
|
||||||
``` bash
|
|
||||||
freqtrade list-hyperopt --hyperopt-path ~/.freqtrade/hyperopts/
|
|
||||||
```
|
|
||||||
|
|
||||||
## List Exchanges
|
## List Exchanges
|
||||||
|
|
||||||
Use the `list-exchanges` subcommand to see the exchanges available for the bot.
|
Use the `list-exchanges` subcommand to see the exchanges available for the bot.
|
||||||
|
@ -22,7 +22,7 @@ if __version__ == 'develop':
|
|||||||
# subprocess.check_output(
|
# subprocess.check_output(
|
||||||
# ['git', 'log', '--format="%h"', '-n 1'],
|
# ['git', 'log', '--format="%h"', '-n 1'],
|
||||||
# stderr=subprocess.DEVNULL).decode("utf-8").rstrip().strip('"')
|
# stderr=subprocess.DEVNULL).decode("utf-8").rstrip().strip('"')
|
||||||
except Exception:
|
except Exception: # pragma: no cover
|
||||||
# git not available, ignore
|
# git not available, ignore
|
||||||
try:
|
try:
|
||||||
# Try Fallback to freqtrade_commit file (created by CI while building docker image)
|
# Try Fallback to freqtrade_commit file (created by CI while building docker image)
|
||||||
|
@ -11,11 +11,11 @@ from freqtrade.commands.build_config_commands import start_new_config
|
|||||||
from freqtrade.commands.data_commands import (start_convert_data, start_download_data,
|
from freqtrade.commands.data_commands import (start_convert_data, start_download_data,
|
||||||
start_list_data)
|
start_list_data)
|
||||||
from freqtrade.commands.deploy_commands import (start_create_userdir, start_install_ui,
|
from freqtrade.commands.deploy_commands import (start_create_userdir, start_install_ui,
|
||||||
start_new_hyperopt, start_new_strategy)
|
start_new_strategy)
|
||||||
from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show
|
from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show
|
||||||
from freqtrade.commands.list_commands import (start_list_exchanges, start_list_hyperopts,
|
from freqtrade.commands.list_commands import (start_list_exchanges, start_list_markets,
|
||||||
start_list_markets, start_list_strategies,
|
start_list_strategies, start_list_timeframes,
|
||||||
start_list_timeframes, start_show_trades)
|
start_show_trades)
|
||||||
from freqtrade.commands.optimize_commands import start_backtesting, start_edge, start_hyperopt
|
from freqtrade.commands.optimize_commands import start_backtesting, start_edge, start_hyperopt
|
||||||
from freqtrade.commands.pairlist_commands import start_test_pairlist
|
from freqtrade.commands.pairlist_commands import start_test_pairlist
|
||||||
from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit
|
from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit
|
||||||
|
@ -55,8 +55,6 @@ ARGS_BUILD_CONFIG = ["config"]
|
|||||||
|
|
||||||
ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"]
|
ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"]
|
||||||
|
|
||||||
ARGS_BUILD_HYPEROPT = ["user_data_dir", "hyperopt", "template"]
|
|
||||||
|
|
||||||
ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase"]
|
ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase"]
|
||||||
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"]
|
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"]
|
||||||
|
|
||||||
@ -92,10 +90,10 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop
|
|||||||
|
|
||||||
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
|
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
|
||||||
"list-markets", "list-pairs", "list-strategies", "list-data",
|
"list-markets", "list-pairs", "list-strategies", "list-data",
|
||||||
"list-hyperopts", "hyperopt-list", "hyperopt-show",
|
"hyperopt-list", "hyperopt-show",
|
||||||
"plot-dataframe", "plot-profit", "show-trades"]
|
"plot-dataframe", "plot-profit", "show-trades"]
|
||||||
|
|
||||||
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-hyperopt", "new-strategy"]
|
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
|
||||||
|
|
||||||
|
|
||||||
class Arguments:
|
class Arguments:
|
||||||
@ -174,12 +172,11 @@ class Arguments:
|
|||||||
from freqtrade.commands import (start_backtesting, start_convert_data, start_create_userdir,
|
from freqtrade.commands import (start_backtesting, start_convert_data, start_create_userdir,
|
||||||
start_download_data, start_edge, start_hyperopt,
|
start_download_data, start_edge, start_hyperopt,
|
||||||
start_hyperopt_list, start_hyperopt_show, start_install_ui,
|
start_hyperopt_list, start_hyperopt_show, start_install_ui,
|
||||||
start_list_data, start_list_exchanges, start_list_hyperopts,
|
start_list_data, start_list_exchanges, start_list_markets,
|
||||||
start_list_markets, start_list_strategies,
|
start_list_strategies, start_list_timeframes,
|
||||||
start_list_timeframes, start_new_config, start_new_hyperopt,
|
start_new_config, start_new_strategy, start_plot_dataframe,
|
||||||
start_new_strategy, start_plot_dataframe, start_plot_profit,
|
start_plot_profit, start_show_trades, start_test_pairlist,
|
||||||
start_show_trades, start_test_pairlist, start_trading,
|
start_trading, start_webserver)
|
||||||
start_webserver)
|
|
||||||
|
|
||||||
subparsers = self.parser.add_subparsers(dest='command',
|
subparsers = self.parser.add_subparsers(dest='command',
|
||||||
# Use custom message when no subhandler is added
|
# Use custom message when no subhandler is added
|
||||||
@ -206,12 +203,6 @@ class Arguments:
|
|||||||
build_config_cmd.set_defaults(func=start_new_config)
|
build_config_cmd.set_defaults(func=start_new_config)
|
||||||
self._build_args(optionlist=ARGS_BUILD_CONFIG, parser=build_config_cmd)
|
self._build_args(optionlist=ARGS_BUILD_CONFIG, parser=build_config_cmd)
|
||||||
|
|
||||||
# add new-hyperopt subcommand
|
|
||||||
build_hyperopt_cmd = subparsers.add_parser('new-hyperopt',
|
|
||||||
help="Create new hyperopt")
|
|
||||||
build_hyperopt_cmd.set_defaults(func=start_new_hyperopt)
|
|
||||||
self._build_args(optionlist=ARGS_BUILD_HYPEROPT, parser=build_hyperopt_cmd)
|
|
||||||
|
|
||||||
# add new-strategy subcommand
|
# add new-strategy subcommand
|
||||||
build_strategy_cmd = subparsers.add_parser('new-strategy',
|
build_strategy_cmd = subparsers.add_parser('new-strategy',
|
||||||
help="Create new strategy")
|
help="Create new strategy")
|
||||||
@ -300,15 +291,6 @@ class Arguments:
|
|||||||
list_exchanges_cmd.set_defaults(func=start_list_exchanges)
|
list_exchanges_cmd.set_defaults(func=start_list_exchanges)
|
||||||
self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd)
|
self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_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-markets subcommand
|
# Add list-markets subcommand
|
||||||
list_markets_cmd = subparsers.add_parser(
|
list_markets_cmd = subparsers.add_parser(
|
||||||
'list-markets',
|
'list-markets',
|
||||||
|
@ -61,13 +61,13 @@ def ask_user_config() -> Dict[str, Any]:
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"name": "stake_currency",
|
"name": "stake_currency",
|
||||||
"message": "Please insert your stake currency:",
|
"message": "Please insert your stake currency:",
|
||||||
"default": 'BTC',
|
"default": 'USDT',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"name": "stake_amount",
|
"name": "stake_amount",
|
||||||
"message": f"Please insert your stake amount (Number or '{UNLIMITED_STAKE_AMOUNT}'):",
|
"message": f"Please insert your stake amount (Number or '{UNLIMITED_STAKE_AMOUNT}'):",
|
||||||
"default": "0.01",
|
"default": "100",
|
||||||
"validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_float(val),
|
"validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_float(val),
|
||||||
"filter": lambda val: '"' + UNLIMITED_STAKE_AMOUNT + '"'
|
"filter": lambda val: '"' + UNLIMITED_STAKE_AMOUNT + '"'
|
||||||
if val == UNLIMITED_STAKE_AMOUNT
|
if val == UNLIMITED_STAKE_AMOUNT
|
||||||
@ -105,6 +105,8 @@ def ask_user_config() -> Dict[str, Any]:
|
|||||||
"bittrex",
|
"bittrex",
|
||||||
"kraken",
|
"kraken",
|
||||||
"ftx",
|
"ftx",
|
||||||
|
"kucoin",
|
||||||
|
"gateio",
|
||||||
Separator(),
|
Separator(),
|
||||||
"other",
|
"other",
|
||||||
],
|
],
|
||||||
@ -128,6 +130,12 @@ def ask_user_config() -> Dict[str, Any]:
|
|||||||
"message": "Insert Exchange Secret",
|
"message": "Insert Exchange Secret",
|
||||||
"when": lambda x: not x['dry_run']
|
"when": lambda x: not x['dry_run']
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "password",
|
||||||
|
"name": "exchange_key_password",
|
||||||
|
"message": "Insert Exchange API Key password",
|
||||||
|
"when": lambda x: not x['dry_run'] and x['exchange_name'] == 'kucoin'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "confirm",
|
"type": "confirm",
|
||||||
"name": "telegram",
|
"name": "telegram",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Definition of cli arguments used in arguments.py
|
Definition of cli arguments used in arguments.py
|
||||||
"""
|
"""
|
||||||
from argparse import ArgumentTypeError
|
from argparse import SUPPRESS, ArgumentTypeError
|
||||||
|
|
||||||
from freqtrade import __version__, constants
|
from freqtrade import __version__, constants
|
||||||
from freqtrade.constants import HYPEROPT_LOSS_BUILTIN
|
from freqtrade.constants import HYPEROPT_LOSS_BUILTIN
|
||||||
@ -203,13 +203,13 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
# Hyperopt
|
# Hyperopt
|
||||||
"hyperopt": Arg(
|
"hyperopt": Arg(
|
||||||
'--hyperopt',
|
'--hyperopt',
|
||||||
help='Specify hyperopt class name which will be used by the bot.',
|
help=SUPPRESS,
|
||||||
metavar='NAME',
|
metavar='NAME',
|
||||||
required=False,
|
required=False,
|
||||||
),
|
),
|
||||||
"hyperopt_path": Arg(
|
"hyperopt_path": Arg(
|
||||||
'--hyperopt-path',
|
'--hyperopt-path',
|
||||||
help='Specify additional lookup path for Hyperopt and Hyperopt Loss functions.',
|
help='Specify additional lookup path for Hyperopt Loss functions.',
|
||||||
metavar='PATH',
|
metavar='PATH',
|
||||||
),
|
),
|
||||||
"epochs": Arg(
|
"epochs": Arg(
|
||||||
|
@ -7,7 +7,7 @@ import requests
|
|||||||
|
|
||||||
from freqtrade.configuration import setup_utils_configuration
|
from freqtrade.configuration import setup_utils_configuration
|
||||||
from freqtrade.configuration.directory_operations import copy_sample_files, create_userdata_dir
|
from freqtrade.configuration.directory_operations import copy_sample_files, create_userdata_dir
|
||||||
from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES
|
from freqtrade.constants import USERPATH_STRATEGIES
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.misc import render_template, render_template_with_fallback
|
from freqtrade.misc import render_template, render_template_with_fallback
|
||||||
@ -87,56 +87,6 @@ def start_new_strategy(args: Dict[str, Any]) -> None:
|
|||||||
raise OperationalException("`new-strategy` requires --strategy to be set.")
|
raise OperationalException("`new-strategy` requires --strategy to be set.")
|
||||||
|
|
||||||
|
|
||||||
def deploy_new_hyperopt(hyperopt_name: str, hyperopt_path: Path, subtemplate: str) -> None:
|
|
||||||
"""
|
|
||||||
Deploys a new hyperopt template to hyperopt_path
|
|
||||||
"""
|
|
||||||
fallback = 'full'
|
|
||||||
buy_guards = render_template_with_fallback(
|
|
||||||
templatefile=f"subtemplates/hyperopt_buy_guards_{subtemplate}.j2",
|
|
||||||
templatefallbackfile=f"subtemplates/hyperopt_buy_guards_{fallback}.j2",
|
|
||||||
)
|
|
||||||
sell_guards = render_template_with_fallback(
|
|
||||||
templatefile=f"subtemplates/hyperopt_sell_guards_{subtemplate}.j2",
|
|
||||||
templatefallbackfile=f"subtemplates/hyperopt_sell_guards_{fallback}.j2",
|
|
||||||
)
|
|
||||||
buy_space = render_template_with_fallback(
|
|
||||||
templatefile=f"subtemplates/hyperopt_buy_space_{subtemplate}.j2",
|
|
||||||
templatefallbackfile=f"subtemplates/hyperopt_buy_space_{fallback}.j2",
|
|
||||||
)
|
|
||||||
sell_space = render_template_with_fallback(
|
|
||||||
templatefile=f"subtemplates/hyperopt_sell_space_{subtemplate}.j2",
|
|
||||||
templatefallbackfile=f"subtemplates/hyperopt_sell_space_{fallback}.j2",
|
|
||||||
)
|
|
||||||
|
|
||||||
strategy_text = render_template(templatefile='base_hyperopt.py.j2',
|
|
||||||
arguments={"hyperopt": hyperopt_name,
|
|
||||||
"buy_guards": buy_guards,
|
|
||||||
"sell_guards": sell_guards,
|
|
||||||
"buy_space": buy_space,
|
|
||||||
"sell_space": sell_space,
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.info(f"Writing hyperopt to `{hyperopt_path}`.")
|
|
||||||
hyperopt_path.write_text(strategy_text)
|
|
||||||
|
|
||||||
|
|
||||||
def start_new_hyperopt(args: Dict[str, Any]) -> None:
|
|
||||||
|
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
|
||||||
|
|
||||||
if 'hyperopt' in args and args['hyperopt']:
|
|
||||||
|
|
||||||
new_path = config['user_data_dir'] / USERPATH_HYPEROPTS / (args['hyperopt'] + '.py')
|
|
||||||
|
|
||||||
if new_path.exists():
|
|
||||||
raise OperationalException(f"`{new_path}` already exists. "
|
|
||||||
"Please choose another Hyperopt Name.")
|
|
||||||
deploy_new_hyperopt(args['hyperopt'], new_path, args['template'])
|
|
||||||
else:
|
|
||||||
raise OperationalException("`new-hyperopt` requires --hyperopt to be set.")
|
|
||||||
|
|
||||||
|
|
||||||
def clean_ui_subdir(directory: Path):
|
def clean_ui_subdir(directory: Path):
|
||||||
if directory.is_dir():
|
if directory.is_dir():
|
||||||
logger.info("Removing UI directory content.")
|
logger.info("Removing UI directory content.")
|
||||||
|
@ -102,3 +102,4 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
HyperoptTools.show_epoch_details(val, total_epochs, print_json, no_header,
|
HyperoptTools.show_epoch_details(val, total_epochs, print_json, no_header,
|
||||||
header_str="Epoch details")
|
header_str="Epoch details")
|
||||||
|
# TODO-lev: Hyperopt optimal leverage
|
||||||
|
@ -10,7 +10,7 @@ from colorama import init as colorama_init
|
|||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
|
|
||||||
from freqtrade.configuration import setup_utils_configuration
|
from freqtrade.configuration import setup_utils_configuration
|
||||||
from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES
|
from freqtrade.constants import USERPATH_STRATEGIES
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import market_is_active, validate_exchanges
|
from freqtrade.exchange import market_is_active, validate_exchanges
|
||||||
@ -92,25 +92,6 @@ def start_list_strategies(args: Dict[str, Any]) -> None:
|
|||||||
_print_objs_tabular(strategy_objs, config.get('print_colorized', False))
|
_print_objs_tabular(strategy_objs, config.get('print_colorized', False))
|
||||||
|
|
||||||
|
|
||||||
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))
|
|
||||||
hyperopt_objs = HyperOptResolver.search_all_objects(directory, not args['print_one_column'])
|
|
||||||
# Sort alphabetically
|
|
||||||
hyperopt_objs = sorted(hyperopt_objs, key=lambda x: x['name'])
|
|
||||||
|
|
||||||
if args['print_one_column']:
|
|
||||||
print('\n'.join([s['name'] for s in hyperopt_objs]))
|
|
||||||
else:
|
|
||||||
_print_objs_tabular(hyperopt_objs, config.get('print_colorized', False))
|
|
||||||
|
|
||||||
|
|
||||||
def start_list_timeframes(args: Dict[str, Any]) -> None:
|
def start_list_timeframes(args: Dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
Print timeframes available on Exchange
|
Print timeframes available on Exchange
|
||||||
@ -148,6 +129,7 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
|
|||||||
quote_currencies = args.get('quote_currencies', [])
|
quote_currencies = args.get('quote_currencies', [])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# TODO-lev: Add leverage amount to get markets that support a certain leverage
|
||||||
pairs = exchange.get_markets(base_currencies=base_currencies,
|
pairs = exchange.get_markets(base_currencies=base_currencies,
|
||||||
quote_currencies=quote_currencies,
|
quote_currencies=quote_currencies,
|
||||||
pairs_only=pairs_only,
|
pairs_only=pairs_only,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# flake8: noqa: F401
|
# flake8: noqa: F401
|
||||||
|
|
||||||
from freqtrade.configuration.check_exchange import check_exchange, remove_credentials
|
from freqtrade.configuration.check_exchange import check_exchange
|
||||||
from freqtrade.configuration.config_setup import setup_utils_configuration
|
from freqtrade.configuration.config_setup import setup_utils_configuration
|
||||||
from freqtrade.configuration.config_validation import validate_config_consistency
|
from freqtrade.configuration.config_validation import validate_config_consistency
|
||||||
from freqtrade.configuration.configuration import Configuration
|
from freqtrade.configuration.configuration import Configuration
|
||||||
|
@ -10,19 +10,6 @@ from freqtrade.exchange import (available_exchanges, is_exchange_known_ccxt,
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
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.
|
|
||||||
Modifies the input dict!
|
|
||||||
"""
|
|
||||||
config['exchange']['key'] = ''
|
|
||||||
config['exchange']['secret'] = ''
|
|
||||||
config['exchange']['password'] = ''
|
|
||||||
config['exchange']['uid'] = ''
|
|
||||||
config['dry_run'] = True
|
|
||||||
|
|
||||||
|
|
||||||
def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool:
|
def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if the exchange name in the config file is supported by Freqtrade
|
Check if the exchange name in the config file is supported by Freqtrade
|
||||||
|
@ -3,7 +3,6 @@ from typing import Any, Dict
|
|||||||
|
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
|
|
||||||
from .check_exchange import remove_credentials
|
|
||||||
from .config_validation import validate_config_consistency
|
from .config_validation import validate_config_consistency
|
||||||
from .configuration import Configuration
|
from .configuration import Configuration
|
||||||
|
|
||||||
@ -21,8 +20,8 @@ def setup_utils_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str
|
|||||||
configuration = Configuration(args, method)
|
configuration = Configuration(args, method)
|
||||||
config = configuration.get_config()
|
config = configuration.get_config()
|
||||||
|
|
||||||
# Ensure we do not use Exchange credentials
|
# Ensure these modes are using Dry-run
|
||||||
remove_credentials(config)
|
config['dry_run'] = True
|
||||||
validate_config_consistency(config)
|
validate_config_consistency(config)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
@ -69,9 +69,7 @@ DUST_PER_COIN = {
|
|||||||
# Source files with destination directories within user-directory
|
# Source files with destination directories within user-directory
|
||||||
USER_DATA_FILES = {
|
USER_DATA_FILES = {
|
||||||
'sample_strategy.py': USERPATH_STRATEGIES,
|
'sample_strategy.py': USERPATH_STRATEGIES,
|
||||||
'sample_hyperopt_advanced.py': USERPATH_HYPEROPTS,
|
|
||||||
'sample_hyperopt_loss.py': USERPATH_HYPEROPTS,
|
'sample_hyperopt_loss.py': USERPATH_HYPEROPTS,
|
||||||
'sample_hyperopt.py': USERPATH_HYPEROPTS,
|
|
||||||
'strategy_analysis_example.ipynb': USERPATH_NOTEBOOKS,
|
'strategy_analysis_example.ipynb': USERPATH_NOTEBOOKS,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,7 +197,8 @@ def _download_pair_history(pair: str, *,
|
|||||||
timeframe=timeframe,
|
timeframe=timeframe,
|
||||||
since_ms=since_ms if since_ms else
|
since_ms=since_ms if since_ms else
|
||||||
arrow.utcnow().shift(
|
arrow.utcnow().shift(
|
||||||
days=-new_pairs_days).int_timestamp * 1000
|
days=-new_pairs_days).int_timestamp * 1000,
|
||||||
|
is_new_pair=data.empty
|
||||||
)
|
)
|
||||||
# TODO: Maybe move parsing to exchange class (?)
|
# TODO: Maybe move parsing to exchange class (?)
|
||||||
new_dataframe = ohlcv_to_dataframe(new_data, timeframe, pair,
|
new_dataframe = ohlcv_to_dataframe(new_data, timeframe, pair,
|
||||||
|
@ -3,7 +3,7 @@ from enum import Enum
|
|||||||
|
|
||||||
class SignalType(Enum):
|
class SignalType(Enum):
|
||||||
"""
|
"""
|
||||||
Enum to distinguish between buy and sell signals
|
Enum to distinguish between enter and exit signals
|
||||||
"""
|
"""
|
||||||
BUY = "buy"
|
BUY = "buy"
|
||||||
SELL = "sell"
|
SELL = "sell"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# flake8: noqa: F401
|
# flake8: noqa: F401
|
||||||
# isort: off
|
# isort: off
|
||||||
from freqtrade.exchange.common import MAP_EXCHANGE_CHILDCLASS
|
from freqtrade.exchange.common import remove_credentials, MAP_EXCHANGE_CHILDCLASS
|
||||||
from freqtrade.exchange.exchange import Exchange
|
from freqtrade.exchange.exchange import Exchange
|
||||||
# isort: on
|
# isort: on
|
||||||
from freqtrade.exchange.bibox import Bibox
|
from freqtrade.exchange.bibox import Bibox
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
""" Binance exchange subclass """
|
""" Binance exchange subclass """
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict
|
from typing import Dict, List
|
||||||
|
|
||||||
|
import arrow
|
||||||
import ccxt
|
import ccxt
|
||||||
|
|
||||||
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
|
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
|
||||||
@ -18,6 +19,7 @@ class Binance(Exchange):
|
|||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
||||||
|
"time_in_force_parameter": "timeInForce",
|
||||||
"ohlcv_candle_limit": 1000,
|
"ohlcv_candle_limit": 1000,
|
||||||
"trades_pagination": "id",
|
"trades_pagination": "id",
|
||||||
"trades_pagination_arg": "fromId",
|
"trades_pagination_arg": "fromId",
|
||||||
@ -89,3 +91,20 @@ class Binance(Exchange):
|
|||||||
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
|
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||||
|
since_ms: int, is_new_pair: bool
|
||||||
|
) -> List:
|
||||||
|
"""
|
||||||
|
Overwrite to introduce "fast new pair" functionality by detecting the pair's listing date
|
||||||
|
Does not work for other exchanges, which don't return the earliest data when called with "0"
|
||||||
|
"""
|
||||||
|
if is_new_pair:
|
||||||
|
x = await self._async_get_candle_history(pair, timeframe, 0)
|
||||||
|
if x and x[2] and x[2][0] and x[2][0][0] > since_ms:
|
||||||
|
# Set starting date to first available candle.
|
||||||
|
since_ms = x[2][0][0]
|
||||||
|
logger.info(f"Candle-data for {pair} available starting with "
|
||||||
|
f"{arrow.get(since_ms // 1000).isoformat()}.")
|
||||||
|
return await super()._async_get_historic_ohlcv(
|
||||||
|
pair=pair, timeframe=timeframe, since_ms=since_ms, is_new_pair=is_new_pair)
|
||||||
|
@ -51,6 +51,19 @@ EXCHANGE_HAS_OPTIONAL = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def remove_credentials(config) -> None:
|
||||||
|
"""
|
||||||
|
Removes exchange keys from the configuration and specifies dry-run
|
||||||
|
Used for backtesting / hyperopt / edge and utils.
|
||||||
|
Modifies the input dict!
|
||||||
|
"""
|
||||||
|
if config.get('dry_run', False):
|
||||||
|
config['exchange']['key'] = ''
|
||||||
|
config['exchange']['secret'] = ''
|
||||||
|
config['exchange']['password'] = ''
|
||||||
|
config['exchange']['uid'] = ''
|
||||||
|
|
||||||
|
|
||||||
def calculate_backoff(retrycount, max_retries):
|
def calculate_backoff(retrycount, max_retries):
|
||||||
"""
|
"""
|
||||||
Calculate backoff
|
Calculate backoff
|
||||||
|
@ -26,9 +26,9 @@ from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFun
|
|||||||
InvalidOrderException, OperationalException, PricingError,
|
InvalidOrderException, OperationalException, PricingError,
|
||||||
RetryableOrderError, TemporaryError)
|
RetryableOrderError, TemporaryError)
|
||||||
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES,
|
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES,
|
||||||
EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED, retrier,
|
EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED,
|
||||||
retrier_async)
|
remove_credentials, retrier, retrier_async)
|
||||||
from freqtrade.misc import deep_merge_dicts, safe_value_fallback2
|
from freqtrade.misc import chunks, deep_merge_dicts, safe_value_fallback2
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||||
|
|
||||||
|
|
||||||
@ -54,12 +54,16 @@ class Exchange:
|
|||||||
# Parameters to add directly to buy/sell calls (like agreeing to trading agreement)
|
# Parameters to add directly to buy/sell calls (like agreeing to trading agreement)
|
||||||
_params: Dict = {}
|
_params: Dict = {}
|
||||||
|
|
||||||
|
# Additional headers - added to the ccxt object
|
||||||
|
_headers: Dict = {}
|
||||||
|
|
||||||
# Dict to specify which options each exchange implements
|
# Dict to specify which options each exchange implements
|
||||||
# This defines defaults, which can be selectively overridden by subclasses using _ft_has
|
# This defines defaults, which can be selectively overridden by subclasses using _ft_has
|
||||||
# or by specifying them in the configuration.
|
# or by specifying them in the configuration.
|
||||||
_ft_has_default: Dict = {
|
_ft_has_default: Dict = {
|
||||||
"stoploss_on_exchange": False,
|
"stoploss_on_exchange": False,
|
||||||
"order_time_in_force": ["gtc"],
|
"order_time_in_force": ["gtc"],
|
||||||
|
"time_in_force_parameter": "timeInForce",
|
||||||
"ohlcv_params": {},
|
"ohlcv_params": {},
|
||||||
"ohlcv_candle_limit": 500,
|
"ohlcv_candle_limit": 500,
|
||||||
"ohlcv_partial_candle": True,
|
"ohlcv_partial_candle": True,
|
||||||
@ -100,6 +104,7 @@ class Exchange:
|
|||||||
|
|
||||||
# Holds all open sell orders for dry_run
|
# Holds all open sell orders for dry_run
|
||||||
self._dry_run_open_orders: Dict[str, Any] = {}
|
self._dry_run_open_orders: Dict[str, Any] = {}
|
||||||
|
remove_credentials(config)
|
||||||
|
|
||||||
if config['dry_run']:
|
if config['dry_run']:
|
||||||
logger.info('Instance is running with dry_run enabled')
|
logger.info('Instance is running with dry_run enabled')
|
||||||
@ -169,7 +174,7 @@ class Exchange:
|
|||||||
asyncio.get_event_loop().run_until_complete(self._api_async.close())
|
asyncio.get_event_loop().run_until_complete(self._api_async.close())
|
||||||
|
|
||||||
def _init_ccxt(self, exchange_config: Dict[str, Any], ccxt_module: CcxtModuleType = ccxt,
|
def _init_ccxt(self, exchange_config: Dict[str, Any], ccxt_module: CcxtModuleType = ccxt,
|
||||||
ccxt_kwargs: dict = None) -> ccxt.Exchange:
|
ccxt_kwargs: Dict = {}) -> ccxt.Exchange:
|
||||||
"""
|
"""
|
||||||
Initialize ccxt with given config and return valid
|
Initialize ccxt with given config and return valid
|
||||||
ccxt instance.
|
ccxt instance.
|
||||||
@ -188,6 +193,10 @@ class Exchange:
|
|||||||
}
|
}
|
||||||
if ccxt_kwargs:
|
if ccxt_kwargs:
|
||||||
logger.info('Applying additional ccxt config: %s', ccxt_kwargs)
|
logger.info('Applying additional ccxt config: %s', ccxt_kwargs)
|
||||||
|
if self._headers:
|
||||||
|
# Inject static headers after the above output to not confuse users.
|
||||||
|
ccxt_kwargs = deep_merge_dicts({'headers': self._headers}, ccxt_kwargs)
|
||||||
|
if ccxt_kwargs:
|
||||||
ex_config.update(ccxt_kwargs)
|
ex_config.update(ccxt_kwargs)
|
||||||
try:
|
try:
|
||||||
|
|
||||||
@ -716,7 +725,8 @@ class Exchange:
|
|||||||
|
|
||||||
params = self._params.copy()
|
params = self._params.copy()
|
||||||
if time_in_force != 'gtc' and ordertype != 'market':
|
if time_in_force != 'gtc' and ordertype != 'market':
|
||||||
params.update({'timeInForce': time_in_force})
|
param = self._ft_has.get('time_in_force_parameter', '')
|
||||||
|
params.update({param: time_in_force})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Set the precision for amount and price(rate) as accepted by the exchange
|
# Set the precision for amount and price(rate) as accepted by the exchange
|
||||||
@ -1185,7 +1195,7 @@ class Exchange:
|
|||||||
# Historic data
|
# Historic data
|
||||||
|
|
||||||
def get_historic_ohlcv(self, pair: str, timeframe: str,
|
def get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||||
since_ms: int) -> List:
|
since_ms: int, is_new_pair: bool = False) -> List:
|
||||||
"""
|
"""
|
||||||
Get candle history using asyncio and returns the list of candles.
|
Get candle history using asyncio and returns the list of candles.
|
||||||
Handles all async work for this.
|
Handles all async work for this.
|
||||||
@ -1197,7 +1207,7 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
return asyncio.get_event_loop().run_until_complete(
|
return asyncio.get_event_loop().run_until_complete(
|
||||||
self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe,
|
self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe,
|
||||||
since_ms=since_ms))
|
since_ms=since_ms, is_new_pair=is_new_pair))
|
||||||
|
|
||||||
def get_historic_ohlcv_as_df(self, pair: str, timeframe: str,
|
def get_historic_ohlcv_as_df(self, pair: str, timeframe: str,
|
||||||
since_ms: int) -> DataFrame:
|
since_ms: int) -> DataFrame:
|
||||||
@ -1212,11 +1222,12 @@ class Exchange:
|
|||||||
return ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True,
|
return ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True,
|
||||||
drop_incomplete=self._ohlcv_partial_candle)
|
drop_incomplete=self._ohlcv_partial_candle)
|
||||||
|
|
||||||
async def _async_get_historic_ohlcv(self, pair: str,
|
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||||
timeframe: str,
|
since_ms: int, is_new_pair: bool
|
||||||
since_ms: int) -> List:
|
) -> List:
|
||||||
"""
|
"""
|
||||||
Download historic ohlcv
|
Download historic ohlcv
|
||||||
|
:param is_new_pair: used by binance subclass to allow "fast" new pair downloading
|
||||||
"""
|
"""
|
||||||
|
|
||||||
one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe)
|
one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe)
|
||||||
@ -1229,21 +1240,22 @@ class Exchange:
|
|||||||
pair, timeframe, since) for since in
|
pair, timeframe, since) for since in
|
||||||
range(since_ms, arrow.utcnow().int_timestamp * 1000, one_call)]
|
range(since_ms, arrow.utcnow().int_timestamp * 1000, one_call)]
|
||||||
|
|
||||||
results = await asyncio.gather(*input_coroutines, return_exceptions=True)
|
|
||||||
|
|
||||||
# Combine gathered results
|
|
||||||
data: List = []
|
data: List = []
|
||||||
for res in results:
|
# Chunk requests into batches of 100 to avoid overwelming ccxt Throttling
|
||||||
if isinstance(res, Exception):
|
for input_coro in chunks(input_coroutines, 100):
|
||||||
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
|
|
||||||
continue
|
results = await asyncio.gather(*input_coro, return_exceptions=True)
|
||||||
# Deconstruct tuple if it's not an exception
|
for res in results:
|
||||||
p, _, new_data = res
|
if isinstance(res, Exception):
|
||||||
if p == pair:
|
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
|
||||||
data.extend(new_data)
|
continue
|
||||||
|
# Deconstruct tuple if it's not an exception
|
||||||
|
p, _, new_data = res
|
||||||
|
if p == pair:
|
||||||
|
data.extend(new_data)
|
||||||
# Sort data again after extending the result - above calls return in "async order"
|
# Sort data again after extending the result - above calls return in "async order"
|
||||||
data = sorted(data, key=lambda x: x[0])
|
data = sorted(data, key=lambda x: x[0])
|
||||||
logger.info("Downloaded data for %s with length %s.", pair, len(data))
|
logger.info(f"Downloaded data for {pair} with length {len(data)}.")
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *,
|
def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *,
|
||||||
|
@ -21,3 +21,5 @@ class Gateio(Exchange):
|
|||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"ohlcv_candle_limit": 1000,
|
"ohlcv_candle_limit": 1000,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_headers = {'X-Gate-Channel-Id': 'freqtrade'}
|
||||||
|
@ -21,4 +21,6 @@ class Kucoin(Exchange):
|
|||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"l2_limit_range": [20, 100],
|
"l2_limit_range": [20, 100],
|
||||||
"l2_limit_range_required": False,
|
"l2_limit_range_required": False,
|
||||||
|
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
||||||
|
"time_in_force_parameter": "timeInForce",
|
||||||
}
|
}
|
||||||
|
@ -66,6 +66,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
init_db(self.config.get('db_url', None), clean_open_orders=self.config['dry_run'])
|
init_db(self.config.get('db_url', None), clean_open_orders=self.config['dry_run'])
|
||||||
|
|
||||||
|
# TODO-lev: Do anything with this?
|
||||||
self.wallets = Wallets(self.config, self.exchange)
|
self.wallets = Wallets(self.config, self.exchange)
|
||||||
|
|
||||||
PairLocks.timeframe = self.config['timeframe']
|
PairLocks.timeframe = self.config['timeframe']
|
||||||
@ -77,6 +78,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# so anything in the Freqtradebot instance should be ready (initialized), including
|
# so anything in the Freqtradebot instance should be ready (initialized), including
|
||||||
# the initial state of the bot.
|
# the initial state of the bot.
|
||||||
# Keep this at the end of this initialization method.
|
# Keep this at the end of this initialization method.
|
||||||
|
# TODO-lev: Do I need to consider the rpc, pairlists or dataprovider?
|
||||||
self.rpc: RPCManager = RPCManager(self)
|
self.rpc: RPCManager = RPCManager(self)
|
||||||
|
|
||||||
self.pairlists = PairListManager(self.exchange, self.config)
|
self.pairlists = PairListManager(self.exchange, self.config)
|
||||||
@ -99,7 +101,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
self.state = State[initial_state.upper()] if initial_state else State.STOPPED
|
self.state = State[initial_state.upper()] if initial_state else State.STOPPED
|
||||||
|
|
||||||
# Protect sell-logic from forcesell and vice versa
|
# Protect sell-logic from forcesell and vice versa
|
||||||
self._sell_lock = Lock()
|
self._exit_lock = Lock()
|
||||||
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
|
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
|
||||||
|
|
||||||
def notify_status(self, msg: str) -> None:
|
def notify_status(self, msg: str) -> None:
|
||||||
@ -166,14 +168,14 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
self.strategy.analyze(self.active_pair_whitelist)
|
self.strategy.analyze(self.active_pair_whitelist)
|
||||||
|
|
||||||
with self._sell_lock:
|
with self._exit_lock:
|
||||||
# Check and handle any timed out open orders
|
# Check and handle any timed out open orders
|
||||||
self.check_handle_timedout()
|
self.check_handle_timedout()
|
||||||
|
|
||||||
# Protect from collisions with forcesell.
|
# Protect from collisions with forceexit.
|
||||||
# Without this, freqtrade my try to recreate stoploss_on_exchange orders
|
# Without this, freqtrade my try to recreate stoploss_on_exchange orders
|
||||||
# while selling is in process, since telegram messages arrive in an different thread.
|
# while selling is in process, since telegram messages arrive in an different thread.
|
||||||
with self._sell_lock:
|
with self._exit_lock:
|
||||||
trades = Trade.get_open_trades()
|
trades = Trade.get_open_trades()
|
||||||
# First process current opened trades (positions)
|
# First process current opened trades (positions)
|
||||||
self.exit_positions(trades)
|
self.exit_positions(trades)
|
||||||
@ -289,16 +291,16 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
def handle_insufficient_funds(self, trade: Trade):
|
def handle_insufficient_funds(self, trade: Trade):
|
||||||
"""
|
"""
|
||||||
Determine if we ever opened a sell order for this trade.
|
Determine if we ever opened a exiting order for this trade.
|
||||||
If not, try update buy fees - otherwise "refind" the open order we obviously lost.
|
If not, try update entering fees - otherwise "refind" the open order we obviously lost.
|
||||||
"""
|
"""
|
||||||
sell_order = trade.select_order('sell', None)
|
sell_order = trade.select_order('sell', None)
|
||||||
if sell_order:
|
if sell_order:
|
||||||
self.refind_lost_order(trade)
|
self.refind_lost_order(trade)
|
||||||
else:
|
else:
|
||||||
self.reupdate_buy_order_fees(trade)
|
self.reupdate_enter_order_fees(trade)
|
||||||
|
|
||||||
def reupdate_buy_order_fees(self, trade: Trade):
|
def reupdate_enter_order_fees(self, trade: Trade):
|
||||||
"""
|
"""
|
||||||
Get buy order from database, and try to reupdate.
|
Get buy order from database, and try to reupdate.
|
||||||
Handles trades where the initial fee-update did not work.
|
Handles trades where the initial fee-update did not work.
|
||||||
@ -312,7 +314,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
def refind_lost_order(self, trade):
|
def refind_lost_order(self, trade):
|
||||||
"""
|
"""
|
||||||
Try refinding a lost trade.
|
Try refinding a lost trade.
|
||||||
Only used when InsufficientFunds appears on sell orders (stoploss or sell).
|
Only used when InsufficientFunds appears on exit orders (stoploss or long sell/short buy).
|
||||||
Tries to walk the stored orders and sell them off eventually.
|
Tries to walk the stored orders and sell them off eventually.
|
||||||
"""
|
"""
|
||||||
logger.info(f"Trying to refind lost order for {trade}")
|
logger.info(f"Trying to refind lost order for {trade}")
|
||||||
@ -323,7 +325,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
logger.debug(f"Order {order} is no longer open.")
|
logger.debug(f"Order {order} is no longer open.")
|
||||||
continue
|
continue
|
||||||
if order.ft_order_side == 'buy':
|
if order.ft_order_side == 'buy':
|
||||||
# Skip buy side - this is handled by reupdate_buy_order_fees
|
# Skip buy side - this is handled by reupdate_enter_order_fees
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair,
|
fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair,
|
||||||
@ -350,7 +352,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
def enter_positions(self) -> int:
|
def enter_positions(self) -> int:
|
||||||
"""
|
"""
|
||||||
Tries to execute buy orders for new trades (positions)
|
Tries to execute entry orders for new trades (positions)
|
||||||
"""
|
"""
|
||||||
trades_created = 0
|
trades_created = 0
|
||||||
|
|
||||||
@ -366,7 +368,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
if not whitelist:
|
if not whitelist:
|
||||||
logger.info("No currency pair in active pair whitelist, "
|
logger.info("No currency pair in active pair whitelist, "
|
||||||
"but checking to sell open trades.")
|
"but checking to exit open trades.")
|
||||||
return trades_created
|
return trades_created
|
||||||
if PairLocks.is_global_lock():
|
if PairLocks.is_global_lock():
|
||||||
lock = PairLocks.get_pair_longest_lock('*')
|
lock = PairLocks.get_pair_longest_lock('*')
|
||||||
@ -385,7 +387,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
logger.warning('Unable to create trade for %s: %s', pair, exception)
|
logger.warning('Unable to create trade for %s: %s', pair, exception)
|
||||||
|
|
||||||
if not trades_created:
|
if not trades_created:
|
||||||
logger.debug("Found no buy signals for whitelisted currencies. Trying again...")
|
logger.debug("Found no enter signals for whitelisted currencies. Trying again...")
|
||||||
|
|
||||||
return trades_created
|
return trades_created
|
||||||
|
|
||||||
@ -476,21 +478,21 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
time_in_force = self.strategy.order_time_in_force['buy']
|
time_in_force = self.strategy.order_time_in_force['buy']
|
||||||
|
|
||||||
if price:
|
if price:
|
||||||
buy_limit_requested = price
|
enter_limit_requested = price
|
||||||
else:
|
else:
|
||||||
# Calculate price
|
# Calculate price
|
||||||
proposed_buy_rate = self.exchange.get_rate(pair, refresh=True, side="buy")
|
proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side="buy")
|
||||||
custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price,
|
custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price,
|
||||||
default_retval=proposed_buy_rate)(
|
default_retval=proposed_enter_rate)(
|
||||||
pair=pair, current_time=datetime.now(timezone.utc),
|
pair=pair, current_time=datetime.now(timezone.utc),
|
||||||
proposed_rate=proposed_buy_rate)
|
proposed_rate=proposed_enter_rate)
|
||||||
|
|
||||||
buy_limit_requested = self.get_valid_price(custom_entry_price, proposed_buy_rate)
|
enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate)
|
||||||
|
|
||||||
if not buy_limit_requested:
|
if not enter_limit_requested:
|
||||||
raise PricingError('Could not determine buy price.')
|
raise PricingError('Could not determine buy price.')
|
||||||
|
|
||||||
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, buy_limit_requested,
|
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, enter_limit_requested,
|
||||||
self.strategy.stoploss)
|
self.strategy.stoploss)
|
||||||
|
|
||||||
if not self.edge:
|
if not self.edge:
|
||||||
@ -498,7 +500,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
|
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
|
||||||
default_retval=stake_amount)(
|
default_retval=stake_amount)(
|
||||||
pair=pair, current_time=datetime.now(timezone.utc),
|
pair=pair, current_time=datetime.now(timezone.utc),
|
||||||
current_rate=buy_limit_requested, proposed_stake=stake_amount,
|
current_rate=enter_limit_requested, proposed_stake=stake_amount,
|
||||||
min_stake=min_stake_amount, max_stake=max_stake_amount)
|
min_stake=min_stake_amount, max_stake=max_stake_amount)
|
||||||
stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount)
|
stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount)
|
||||||
|
|
||||||
@ -508,27 +510,29 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: "
|
logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: "
|
||||||
f"{stake_amount} ...")
|
f"{stake_amount} ...")
|
||||||
|
|
||||||
amount = stake_amount / buy_limit_requested
|
amount = stake_amount / enter_limit_requested
|
||||||
order_type = self.strategy.order_types['buy']
|
order_type = self.strategy.order_types['buy']
|
||||||
if forcebuy:
|
if forcebuy:
|
||||||
# Forcebuy can define a different ordertype
|
# Forcebuy can define a different ordertype
|
||||||
|
# TODO-lev: get a forceshort? What is this
|
||||||
order_type = self.strategy.order_types.get('forcebuy', order_type)
|
order_type = self.strategy.order_types.get('forcebuy', order_type)
|
||||||
|
# TODO-lev: Will this work for shorting?
|
||||||
|
|
||||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
|
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
|
||||||
pair=pair, order_type=order_type, amount=amount, rate=buy_limit_requested,
|
pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
|
||||||
time_in_force=time_in_force, current_time=datetime.now(timezone.utc)):
|
time_in_force=time_in_force, current_time=datetime.now(timezone.utc)):
|
||||||
logger.info(f"User requested abortion of buying {pair}")
|
logger.info(f"User requested abortion of buying {pair}")
|
||||||
return False
|
return False
|
||||||
amount = self.exchange.amount_to_precision(pair, amount)
|
amount = self.exchange.amount_to_precision(pair, amount)
|
||||||
order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy",
|
order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy",
|
||||||
amount=amount, rate=buy_limit_requested,
|
amount=amount, rate=enter_limit_requested,
|
||||||
time_in_force=time_in_force)
|
time_in_force=time_in_force)
|
||||||
order_obj = Order.parse_from_ccxt_object(order, pair, 'buy')
|
order_obj = Order.parse_from_ccxt_object(order, pair, 'buy')
|
||||||
order_id = order['id']
|
order_id = order['id']
|
||||||
order_status = order.get('status', None)
|
order_status = order.get('status', None)
|
||||||
|
|
||||||
# we assume the order is executed at the price requested
|
# we assume the order is executed at the price requested
|
||||||
buy_limit_filled_price = buy_limit_requested
|
enter_limit_filled_price = enter_limit_requested
|
||||||
amount_requested = amount
|
amount_requested = amount
|
||||||
|
|
||||||
if order_status == 'expired' or order_status == 'rejected':
|
if order_status == 'expired' or order_status == 'rejected':
|
||||||
@ -551,13 +555,13 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
)
|
)
|
||||||
stake_amount = order['cost']
|
stake_amount = order['cost']
|
||||||
amount = safe_value_fallback(order, 'filled', 'amount')
|
amount = safe_value_fallback(order, 'filled', 'amount')
|
||||||
buy_limit_filled_price = safe_value_fallback(order, 'average', 'price')
|
enter_limit_filled_price = safe_value_fallback(order, 'average', 'price')
|
||||||
|
|
||||||
# in case of FOK the order may be filled immediately and fully
|
# in case of FOK the order may be filled immediately and fully
|
||||||
elif order_status == 'closed':
|
elif order_status == 'closed':
|
||||||
stake_amount = order['cost']
|
stake_amount = order['cost']
|
||||||
amount = safe_value_fallback(order, 'filled', 'amount')
|
amount = safe_value_fallback(order, 'filled', 'amount')
|
||||||
buy_limit_filled_price = safe_value_fallback(order, 'average', 'price')
|
enter_limit_filled_price = safe_value_fallback(order, 'average', 'price')
|
||||||
|
|
||||||
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
||||||
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
||||||
@ -569,8 +573,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
amount_requested=amount_requested,
|
amount_requested=amount_requested,
|
||||||
fee_open=fee,
|
fee_open=fee,
|
||||||
fee_close=fee,
|
fee_close=fee,
|
||||||
open_rate=buy_limit_filled_price,
|
open_rate=enter_limit_filled_price,
|
||||||
open_rate_requested=buy_limit_requested,
|
open_rate_requested=enter_limit_requested,
|
||||||
open_date=datetime.utcnow(),
|
open_date=datetime.utcnow(),
|
||||||
exchange=self.exchange.id,
|
exchange=self.exchange.id,
|
||||||
open_order_id=order_id,
|
open_order_id=order_id,
|
||||||
@ -590,13 +594,13 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# Updating wallets
|
# Updating wallets
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
|
|
||||||
self._notify_buy(trade, order_type)
|
self._notify_enter(trade, order_type)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _notify_buy(self, trade: Trade, order_type: str) -> None:
|
def _notify_enter(self, trade: Trade, order_type: str) -> None:
|
||||||
"""
|
"""
|
||||||
Sends rpc notification when a buy occurred.
|
Sends rpc notification when a entry order occurred.
|
||||||
"""
|
"""
|
||||||
msg = {
|
msg = {
|
||||||
'trade_id': trade.id,
|
'trade_id': trade.id,
|
||||||
@ -617,9 +621,9 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# Send the message
|
# Send the message
|
||||||
self.rpc.send_msg(msg)
|
self.rpc.send_msg(msg)
|
||||||
|
|
||||||
def _notify_buy_cancel(self, trade: Trade, order_type: str, reason: str) -> None:
|
def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str) -> None:
|
||||||
"""
|
"""
|
||||||
Sends rpc notification when a buy cancel occurred.
|
Sends rpc notification when a entry order cancel occurred.
|
||||||
"""
|
"""
|
||||||
current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="buy")
|
current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="buy")
|
||||||
|
|
||||||
@ -643,7 +647,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# Send the message
|
# Send the message
|
||||||
self.rpc.send_msg(msg)
|
self.rpc.send_msg(msg)
|
||||||
|
|
||||||
def _notify_buy_fill(self, trade: Trade) -> None:
|
def _notify_enter_fill(self, trade: Trade) -> None:
|
||||||
msg = {
|
msg = {
|
||||||
'trade_id': trade.id,
|
'trade_id': trade.id,
|
||||||
'type': RPCMessageType.BUY_FILL,
|
'type': RPCMessageType.BUY_FILL,
|
||||||
@ -665,7 +669,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
def exit_positions(self, trades: List[Any]) -> int:
|
def exit_positions(self, trades: List[Any]) -> int:
|
||||||
"""
|
"""
|
||||||
Tries to execute sell orders for open trades (positions)
|
Tries to execute exit orders for open trades (positions)
|
||||||
"""
|
"""
|
||||||
trades_closed = 0
|
trades_closed = 0
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
@ -681,7 +685,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
trades_closed += 1
|
trades_closed += 1
|
||||||
|
|
||||||
except DependencyException as exception:
|
except DependencyException as exception:
|
||||||
logger.warning('Unable to sell trade %s: %s', trade.pair, exception)
|
logger.warning('Unable to exit trade %s: %s', trade.pair, exception)
|
||||||
|
|
||||||
# Updating wallets if any trade occurred
|
# Updating wallets if any trade occurred
|
||||||
if trades_closed:
|
if trades_closed:
|
||||||
@ -691,8 +695,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
def handle_trade(self, trade: Trade) -> bool:
|
def handle_trade(self, trade: Trade) -> bool:
|
||||||
"""
|
"""
|
||||||
Sells the current pair if the threshold is reached and updates the trade record.
|
Sells/exits_short the current pair if the threshold is reached and updates the trade record.
|
||||||
:return: True if trade has been sold, False otherwise
|
:return: True if trade has been sold/exited_short, False otherwise
|
||||||
"""
|
"""
|
||||||
if not trade.is_open:
|
if not trade.is_open:
|
||||||
raise DependencyException(f'Attempt to handle closed trade: {trade}')
|
raise DependencyException(f'Attempt to handle closed trade: {trade}')
|
||||||
@ -700,7 +704,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
logger.debug('Handling %s ...', trade)
|
logger.debug('Handling %s ...', trade)
|
||||||
|
|
||||||
(buy, sell) = (False, False)
|
(buy, sell) = (False, False)
|
||||||
|
# TODO-lev: change to use_exit_signal, ignore_roi_if_enter_signal
|
||||||
if (self.config.get('use_sell_signal', True) or
|
if (self.config.get('use_sell_signal', True) or
|
||||||
self.config.get('ignore_roi_if_buy_signal', False)):
|
self.config.get('ignore_roi_if_buy_signal', False)):
|
||||||
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
|
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
|
||||||
@ -713,8 +717,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
logger.debug('checking sell')
|
logger.debug('checking sell')
|
||||||
sell_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell")
|
exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell")
|
||||||
if self._check_and_execute_sell(trade, sell_rate, buy, sell):
|
if self._check_and_execute_exit(trade, exit_rate, buy, sell):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
logger.debug('Found no sell signal for %s.', trade)
|
logger.debug('Found no sell signal for %s.', trade)
|
||||||
@ -744,7 +748,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
except InvalidOrderException as e:
|
except InvalidOrderException as e:
|
||||||
trade.stoploss_order_id = None
|
trade.stoploss_order_id = None
|
||||||
logger.error(f'Unable to place a stoploss order on exchange. {e}')
|
logger.error(f'Unable to place a stoploss order on exchange. {e}')
|
||||||
logger.warning('Selling the trade forcefully')
|
logger.warning('Exiting the trade forcefully')
|
||||||
self.execute_trade_exit(trade, trade.stop_loss, sell_reason=SellCheckTuple(
|
self.execute_trade_exit(trade, trade.stop_loss, sell_reason=SellCheckTuple(
|
||||||
sell_type=SellType.EMERGENCY_SELL))
|
sell_type=SellType.EMERGENCY_SELL))
|
||||||
|
|
||||||
@ -758,6 +762,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
Check if trade is fulfilled in which case the stoploss
|
Check if trade is fulfilled in which case the stoploss
|
||||||
on exchange should be added immediately if stoploss on exchange
|
on exchange should be added immediately if stoploss on exchange
|
||||||
is enabled.
|
is enabled.
|
||||||
|
# TODO-lev: liquidation price will always be on exchange, even though
|
||||||
|
# TODO-lev: stoploss_on_exchange might not be enabled
|
||||||
"""
|
"""
|
||||||
|
|
||||||
logger.debug('Handling stoploss on exchange %s ...', trade)
|
logger.debug('Handling stoploss on exchange %s ...', trade)
|
||||||
@ -776,13 +782,14 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
# We check if stoploss order is fulfilled
|
# We check if stoploss order is fulfilled
|
||||||
if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'):
|
if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'):
|
||||||
|
# TODO-lev: Update to exit reason
|
||||||
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
|
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
|
||||||
self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order,
|
self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order,
|
||||||
stoploss_order=True)
|
stoploss_order=True)
|
||||||
# Lock pair for one candle to prevent immediate rebuys
|
# Lock pair for one candle to prevent immediate rebuys
|
||||||
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
||||||
reason='Auto lock')
|
reason='Auto lock')
|
||||||
self._notify_sell(trade, "stoploss")
|
self._notify_exit(trade, "stoploss")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if trade.open_order_id or not trade.is_open:
|
if trade.open_order_id or not trade.is_open:
|
||||||
@ -791,7 +798,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# The trade can be closed already (sell-order fill confirmation came in this iteration)
|
# The trade can be closed already (sell-order fill confirmation came in this iteration)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange
|
# If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange
|
||||||
if not stoploss_order:
|
if not stoploss_order:
|
||||||
stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss
|
stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss
|
||||||
stop_price = trade.open_rate * (1 + stoploss)
|
stop_price = trade.open_rate * (1 + stoploss)
|
||||||
@ -851,19 +858,19 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
logger.warning(f"Could not create trailing stoploss order "
|
logger.warning(f"Could not create trailing stoploss order "
|
||||||
f"for pair {trade.pair}.")
|
f"for pair {trade.pair}.")
|
||||||
|
|
||||||
def _check_and_execute_sell(self, trade: Trade, sell_rate: float,
|
def _check_and_execute_exit(self, trade: Trade, exit_rate: float,
|
||||||
buy: bool, sell: bool) -> bool:
|
buy: bool, sell: bool) -> bool:
|
||||||
"""
|
"""
|
||||||
Check and execute sell
|
Check and execute exit
|
||||||
"""
|
"""
|
||||||
should_sell = self.strategy.should_sell(
|
should_sell = self.strategy.should_sell(
|
||||||
trade, sell_rate, datetime.now(timezone.utc), buy, sell,
|
trade, exit_rate, datetime.now(timezone.utc), buy, sell,
|
||||||
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
|
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
|
||||||
)
|
)
|
||||||
|
|
||||||
if should_sell.sell_flag:
|
if should_sell.sell_flag:
|
||||||
logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}')
|
logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}')
|
||||||
self.execute_trade_exit(trade, sell_rate, should_sell)
|
self.execute_trade_exit(trade, exit_rate, should_sell)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -906,7 +913,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
default_retval=False)(pair=trade.pair,
|
default_retval=False)(pair=trade.pair,
|
||||||
trade=trade,
|
trade=trade,
|
||||||
order=order))):
|
order=order))):
|
||||||
self.handle_cancel_buy(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||||
|
|
||||||
elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and (
|
elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and (
|
||||||
fully_cancelled
|
fully_cancelled
|
||||||
@ -915,7 +922,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
default_retval=False)(pair=trade.pair,
|
default_retval=False)(pair=trade.pair,
|
||||||
trade=trade,
|
trade=trade,
|
||||||
order=order))):
|
order=order))):
|
||||||
self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||||
|
|
||||||
def cancel_all_open_orders(self) -> None:
|
def cancel_all_open_orders(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -931,17 +938,18 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if order['side'] == 'buy':
|
if order['side'] == 'buy':
|
||||||
self.handle_cancel_buy(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
|
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
|
||||||
|
|
||||||
elif order['side'] == 'sell':
|
elif order['side'] == 'sell':
|
||||||
self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
|
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
|
||||||
Trade.commit()
|
Trade.commit()
|
||||||
|
|
||||||
def handle_cancel_buy(self, trade: Trade, order: Dict, reason: str) -> bool:
|
def handle_cancel_enter(self, trade: Trade, order: Dict, reason: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Buy cancel - cancel order
|
Buy cancel - cancel order
|
||||||
:return: True if order was fully cancelled
|
:return: True if order was fully cancelled
|
||||||
"""
|
"""
|
||||||
|
# TODO-lev: Pay back borrowed/interest and transfer back on leveraged trades
|
||||||
was_trade_fully_canceled = False
|
was_trade_fully_canceled = False
|
||||||
|
|
||||||
# Cancelled orders may have the status of 'canceled' or 'closed'
|
# Cancelled orders may have the status of 'canceled' or 'closed'
|
||||||
@ -986,6 +994,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# to the order dict acquired before cancelling.
|
# to the order dict acquired before cancelling.
|
||||||
# we need to fall back to the values from order if corder does not contain these keys.
|
# we need to fall back to the values from order if corder does not contain these keys.
|
||||||
trade.amount = filled_amount
|
trade.amount = filled_amount
|
||||||
|
# TODO-lev: Check edge cases, we don't want to make leverage > 1.0 if we don't have to
|
||||||
|
|
||||||
trade.stake_amount = trade.amount * trade.open_rate
|
trade.stake_amount = trade.amount * trade.open_rate
|
||||||
self.update_trade_state(trade, trade.open_order_id, corder)
|
self.update_trade_state(trade, trade.open_order_id, corder)
|
||||||
|
|
||||||
@ -994,13 +1004,13 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
|
reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
|
||||||
|
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
self._notify_buy_cancel(trade, order_type=self.strategy.order_types['buy'],
|
self._notify_enter_cancel(trade, order_type=self.strategy.order_types['buy'],
|
||||||
reason=reason)
|
reason=reason)
|
||||||
return was_trade_fully_canceled
|
return was_trade_fully_canceled
|
||||||
|
|
||||||
def handle_cancel_sell(self, trade: Trade, order: Dict, reason: str) -> str:
|
def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> str:
|
||||||
"""
|
"""
|
||||||
Sell cancel - cancel order and update trade
|
exit order cancel - cancel order and update trade
|
||||||
:return: Reason for cancel
|
:return: Reason for cancel
|
||||||
"""
|
"""
|
||||||
# if trade is not partially completed, just cancel the order
|
# if trade is not partially completed, just cancel the order
|
||||||
@ -1032,14 +1042,14 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
||||||
|
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
self._notify_sell_cancel(
|
self._notify_exit_cancel(
|
||||||
trade,
|
trade,
|
||||||
order_type=self.strategy.order_types['sell'],
|
order_type=self.strategy.order_types['sell'],
|
||||||
reason=reason
|
reason=reason
|
||||||
)
|
)
|
||||||
return reason
|
return reason
|
||||||
|
|
||||||
def _safe_sell_amount(self, pair: str, amount: float) -> float:
|
def _safe_exit_amount(self, pair: str, amount: float) -> float:
|
||||||
"""
|
"""
|
||||||
Get sellable amount.
|
Get sellable amount.
|
||||||
Should be trade.amount - but will fall back to the available amount if necessary.
|
Should be trade.amount - but will fall back to the available amount if necessary.
|
||||||
@ -1050,6 +1060,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
:return: amount to sell
|
:return: amount to sell
|
||||||
:raise: DependencyException: if available balance is not within 2% of the available amount.
|
:raise: DependencyException: if available balance is not within 2% of the available amount.
|
||||||
"""
|
"""
|
||||||
|
# TODO-lev Maybe update?
|
||||||
# Update wallets to ensure amounts tied up in a stoploss is now free!
|
# Update wallets to ensure amounts tied up in a stoploss is now free!
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
trade_base_currency = self.exchange.get_pair_base_currency(pair)
|
trade_base_currency = self.exchange.get_pair_base_currency(pair)
|
||||||
@ -1062,7 +1073,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
return wallet_amount
|
return wallet_amount
|
||||||
else:
|
else:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}")
|
f"Not enough amount to exit trade. Trade-amount: {amount}, Wallet: {wallet_amount}")
|
||||||
|
|
||||||
def execute_trade_exit(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool:
|
def execute_trade_exit(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -1072,7 +1083,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
:param sell_reason: Reason the sell was triggered
|
:param sell_reason: Reason the sell was triggered
|
||||||
:return: True if it succeeds (supported) False (not supported)
|
:return: True if it succeeds (supported) False (not supported)
|
||||||
"""
|
"""
|
||||||
sell_type = 'sell'
|
sell_type = 'sell' # TODO-lev: Update to exit
|
||||||
if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
|
if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
|
||||||
sell_type = 'stoploss'
|
sell_type = 'stoploss'
|
||||||
|
|
||||||
@ -1111,23 +1122,26 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# but we allow this value to be changed)
|
# but we allow this value to be changed)
|
||||||
order_type = self.strategy.order_types.get("forcesell", order_type)
|
order_type = self.strategy.order_types.get("forcesell", order_type)
|
||||||
|
|
||||||
amount = self._safe_sell_amount(trade.pair, trade.amount)
|
amount = self._safe_exit_amount(trade.pair, trade.amount)
|
||||||
time_in_force = self.strategy.order_time_in_force['sell']
|
time_in_force = self.strategy.order_time_in_force['sell']
|
||||||
|
|
||||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
|
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
|
||||||
pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit,
|
pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit,
|
||||||
time_in_force=time_in_force, sell_reason=sell_reason.sell_reason,
|
time_in_force=time_in_force, sell_reason=sell_reason.sell_reason,
|
||||||
current_time=datetime.now(timezone.utc)):
|
current_time=datetime.now(timezone.utc)): # TODO-lev: Update to exit
|
||||||
logger.info(f"User requested abortion of selling {trade.pair}")
|
logger.info(f"User requested abortion of exiting {trade.pair}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Execute sell and update trade record
|
# Execute sell and update trade record
|
||||||
order = self.exchange.create_order(pair=trade.pair,
|
order = self.exchange.create_order(
|
||||||
ordertype=order_type, side="sell",
|
pair=trade.pair,
|
||||||
amount=amount, rate=limit,
|
ordertype=order_type,
|
||||||
time_in_force=time_in_force
|
side="sell",
|
||||||
)
|
amount=amount,
|
||||||
|
rate=limit,
|
||||||
|
time_in_force=time_in_force
|
||||||
|
)
|
||||||
except InsufficientFundsError as e:
|
except InsufficientFundsError as e:
|
||||||
logger.warning(f"Unable to place order {e}.")
|
logger.warning(f"Unable to place order {e}.")
|
||||||
# Try to figure out what went wrong
|
# Try to figure out what went wrong
|
||||||
@ -1146,15 +1160,15 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
self.update_trade_state(trade, trade.open_order_id, order)
|
self.update_trade_state(trade, trade.open_order_id, order)
|
||||||
Trade.commit()
|
Trade.commit()
|
||||||
|
|
||||||
# Lock pair for one candle to prevent immediate re-buys
|
# Lock pair for one candle to prevent immediate re-trading
|
||||||
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
||||||
reason='Auto lock')
|
reason='Auto lock')
|
||||||
|
|
||||||
self._notify_sell(trade, order_type)
|
self._notify_exit(trade, order_type)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _notify_sell(self, trade: Trade, order_type: str, fill: bool = False) -> None:
|
def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False) -> None:
|
||||||
"""
|
"""
|
||||||
Sends rpc notification when a sell occurred.
|
Sends rpc notification when a sell occurred.
|
||||||
"""
|
"""
|
||||||
@ -1196,7 +1210,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# Send the message
|
# Send the message
|
||||||
self.rpc.send_msg(msg)
|
self.rpc.send_msg(msg)
|
||||||
|
|
||||||
def _notify_sell_cancel(self, trade: Trade, order_type: str, reason: str) -> None:
|
def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str) -> None:
|
||||||
"""
|
"""
|
||||||
Sends rpc notification when a sell cancel occurred.
|
Sends rpc notification when a sell cancel occurred.
|
||||||
"""
|
"""
|
||||||
@ -1291,13 +1305,13 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# Updating wallets when order is closed
|
# Updating wallets when order is closed
|
||||||
if not trade.is_open:
|
if not trade.is_open:
|
||||||
if not stoploss_order and not trade.open_order_id:
|
if not stoploss_order and not trade.open_order_id:
|
||||||
self._notify_sell(trade, '', True)
|
self._notify_exit(trade, '', True)
|
||||||
self.protections.stop_per_pair(trade.pair)
|
self.protections.stop_per_pair(trade.pair)
|
||||||
self.protections.global_stop()
|
self.protections.global_stop()
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
elif not trade.open_order_id:
|
elif not trade.open_order_id:
|
||||||
# Buy fill
|
# Buy fill
|
||||||
self._notify_buy_fill(trade)
|
self._notify_enter_fill(trade)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -1310,6 +1324,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount:
|
if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount:
|
||||||
# Eat into dust if we own more than base currency
|
# Eat into dust if we own more than base currency
|
||||||
|
# TODO-lev: won't be in "base"(quote) currency for shorts
|
||||||
logger.info(f"Fee amount for {trade} was in base currency - "
|
logger.info(f"Fee amount for {trade} was in base currency - "
|
||||||
f"Eating Fee {fee_abs} into dust.")
|
f"Eating Fee {fee_abs} into dust.")
|
||||||
elif fee_abs != 0:
|
elif fee_abs != 0:
|
||||||
@ -1386,6 +1401,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', ''))
|
trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', ''))
|
||||||
|
|
||||||
if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC):
|
if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC):
|
||||||
|
# TODO-lev: leverage?
|
||||||
logger.warning(f"Amount {amount} does not match amount {trade.amount}")
|
logger.warning(f"Amount {amount} does not match amount {trade.amount}")
|
||||||
raise DependencyException("Half bought? Amounts don't match")
|
raise DependencyException("Half bought? Amounts don't match")
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ def setup_logging(config: Dict[str, Any]) -> None:
|
|||||||
# syslog config. The messages should be equal for this.
|
# syslog config. The messages should be equal for this.
|
||||||
handler_sl.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s'))
|
handler_sl.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s'))
|
||||||
logging.root.addHandler(handler_sl)
|
logging.root.addHandler(handler_sl)
|
||||||
elif s[0] == 'journald':
|
elif s[0] == 'journald': # pragma: no cover
|
||||||
try:
|
try:
|
||||||
from systemd.journal import JournaldLogHandler
|
from systemd.journal import JournaldLogHandler
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -9,7 +9,7 @@ from typing import Any, List
|
|||||||
|
|
||||||
|
|
||||||
# check min. python version
|
# check min. python version
|
||||||
if sys.version_info < (3, 7):
|
if sys.version_info < (3, 7): # pragma: no cover
|
||||||
sys.exit("Freqtrade requires Python version >= 3.7")
|
sys.exit("Freqtrade requires Python version >= 3.7")
|
||||||
|
|
||||||
from freqtrade.commands import Arguments
|
from freqtrade.commands import Arguments
|
||||||
@ -46,7 +46,7 @@ def main(sysargv: List[str] = None) -> None:
|
|||||||
"`freqtrade --help` or `freqtrade <command> --help`."
|
"`freqtrade --help` or `freqtrade <command> --help`."
|
||||||
)
|
)
|
||||||
|
|
||||||
except SystemExit as e:
|
except SystemExit as e: # pragma: no cover
|
||||||
return_code = e
|
return_code = e
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.info('SIGINT received, aborting ...')
|
logger.info('SIGINT received, aborting ...')
|
||||||
@ -60,5 +60,5 @@ def main(sysargv: List[str] = None) -> None:
|
|||||||
sys.exit(return_code)
|
sys.exit(return_code)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__': # pragma: no cover
|
||||||
main()
|
main()
|
||||||
|
@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
|||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency
|
from freqtrade.configuration import TimeRange, validate_config_consistency
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
||||||
from freqtrade.data import history
|
from freqtrade.data import history
|
||||||
from freqtrade.data.btanalysis import trade_list_to_dataframe
|
from freqtrade.data.btanalysis import trade_list_to_dataframe
|
||||||
@ -61,8 +61,7 @@ class Backtesting:
|
|||||||
self.config = config
|
self.config = config
|
||||||
self.results: Optional[Dict[str, Any]] = None
|
self.results: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
# Reset keys for backtesting
|
config['dry_run'] = True
|
||||||
remove_credentials(self.config)
|
|
||||||
self.strategylist: List[IStrategy] = []
|
self.strategylist: List[IStrategy] = []
|
||||||
self.all_results: Dict[str, Dict] = {}
|
self.all_results: Dict[str, Dict] = {}
|
||||||
|
|
||||||
@ -386,7 +385,7 @@ class Backtesting:
|
|||||||
detail_data = detail_data.loc[
|
detail_data = detail_data.loc[
|
||||||
(detail_data['date'] >= sell_candle_time) &
|
(detail_data['date'] >= sell_candle_time) &
|
||||||
(detail_data['date'] < sell_candle_end)
|
(detail_data['date'] < sell_candle_end)
|
||||||
]
|
]
|
||||||
if len(detail_data) == 0:
|
if len(detail_data) == 0:
|
||||||
# Fall back to "regular" data if no detail data was found for this candle
|
# Fall back to "regular" data if no detail data was found for this candle
|
||||||
return self._get_sell_trade_entry_for_candle(trade, sell_row)
|
return self._get_sell_trade_entry_for_candle(trade, sell_row)
|
||||||
|
@ -7,7 +7,7 @@ import logging
|
|||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency
|
from freqtrade.configuration import TimeRange, validate_config_consistency
|
||||||
from freqtrade.edge import Edge
|
from freqtrade.edge import Edge
|
||||||
from freqtrade.optimize.optimize_reports import generate_edge_table
|
from freqtrade.optimize.optimize_reports import generate_edge_table
|
||||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||||
@ -28,8 +28,8 @@ class EdgeCli:
|
|||||||
def __init__(self, config: Dict[str, Any]) -> None:
|
def __init__(self, config: Dict[str, Any]) -> None:
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
# Reset keys for edge
|
# Ensure using dry-run
|
||||||
remove_credentials(self.config)
|
self.config['dry_run'] = True
|
||||||
self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
||||||
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
||||||
self.strategy = StrategyResolver.load_strategy(self.config)
|
self.strategy = StrategyResolver.load_strategy(self.config)
|
||||||
|
@ -22,6 +22,7 @@ from pandas import DataFrame
|
|||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN
|
from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN
|
||||||
from freqtrade.data.converter import trim_dataframes
|
from freqtrade.data.converter import trim_dataframes
|
||||||
from freqtrade.data.history import get_timerange
|
from freqtrade.data.history import get_timerange
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.misc import deep_merge_dicts, file_dump_json, plural
|
from freqtrade.misc import deep_merge_dicts, file_dump_json, plural
|
||||||
from freqtrade.optimize.backtesting import Backtesting
|
from freqtrade.optimize.backtesting import Backtesting
|
||||||
# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules
|
# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules
|
||||||
@ -30,7 +31,7 @@ from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401
|
|||||||
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401
|
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401
|
||||||
from freqtrade.optimize.hyperopt_tools import HyperoptTools, hyperopt_serializer
|
from freqtrade.optimize.hyperopt_tools import HyperoptTools, hyperopt_serializer
|
||||||
from freqtrade.optimize.optimize_reports import generate_strategy_stats
|
from freqtrade.optimize.optimize_reports import generate_strategy_stats
|
||||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver
|
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver
|
||||||
|
|
||||||
|
|
||||||
# Suppress scikit-learn FutureWarnings from skopt
|
# Suppress scikit-learn FutureWarnings from skopt
|
||||||
@ -78,10 +79,10 @@ class Hyperopt:
|
|||||||
|
|
||||||
if not self.config.get('hyperopt'):
|
if not self.config.get('hyperopt'):
|
||||||
self.custom_hyperopt = HyperOptAuto(self.config)
|
self.custom_hyperopt = HyperOptAuto(self.config)
|
||||||
self.auto_hyperopt = True
|
|
||||||
else:
|
else:
|
||||||
self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config)
|
raise OperationalException(
|
||||||
self.auto_hyperopt = False
|
"Using separate Hyperopt files has been removed in 2021.9. Please convert "
|
||||||
|
"your existing Hyperopt file to the new Hyperoptable strategy interface")
|
||||||
|
|
||||||
self.backtesting._set_strategy(self.backtesting.strategylist[0])
|
self.backtesting._set_strategy(self.backtesting.strategylist[0])
|
||||||
self.custom_hyperopt.strategy = self.backtesting.strategy
|
self.custom_hyperopt.strategy = self.backtesting.strategy
|
||||||
@ -103,31 +104,6 @@ class Hyperopt:
|
|||||||
self.num_epochs_saved = 0
|
self.num_epochs_saved = 0
|
||||||
self.current_best_epoch: Optional[Dict[str, Any]] = None
|
self.current_best_epoch: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
if not self.auto_hyperopt:
|
|
||||||
# Populate "fallback" functions here
|
|
||||||
# (hasattr is slow so should not be run during "regular" operations)
|
|
||||||
if hasattr(self.custom_hyperopt, 'populate_indicators'):
|
|
||||||
logger.warning(
|
|
||||||
"DEPRECATED: Using `populate_indicators()` in the hyperopt file is deprecated. "
|
|
||||||
"Please move these methods to your strategy."
|
|
||||||
)
|
|
||||||
self.backtesting.strategy.populate_indicators = ( # type: ignore
|
|
||||||
self.custom_hyperopt.populate_indicators) # type: ignore
|
|
||||||
if hasattr(self.custom_hyperopt, 'populate_buy_trend'):
|
|
||||||
logger.warning(
|
|
||||||
"DEPRECATED: Using `populate_buy_trend()` in the hyperopt file is deprecated. "
|
|
||||||
"Please move these methods to your strategy."
|
|
||||||
)
|
|
||||||
self.backtesting.strategy.populate_buy_trend = ( # type: ignore
|
|
||||||
self.custom_hyperopt.populate_buy_trend) # type: ignore
|
|
||||||
if hasattr(self.custom_hyperopt, 'populate_sell_trend'):
|
|
||||||
logger.warning(
|
|
||||||
"DEPRECATED: Using `populate_sell_trend()` in the hyperopt file is deprecated. "
|
|
||||||
"Please move these methods to your strategy."
|
|
||||||
)
|
|
||||||
self.backtesting.strategy.populate_sell_trend = ( # type: ignore
|
|
||||||
self.custom_hyperopt.populate_sell_trend) # type: ignore
|
|
||||||
|
|
||||||
# Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
|
# Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
|
||||||
if self.config.get('use_max_market_positions', True):
|
if self.config.get('use_max_market_positions', True):
|
||||||
self.max_open_trades = self.config['max_open_trades']
|
self.max_open_trades = self.config['max_open_trades']
|
||||||
@ -256,7 +232,7 @@ class Hyperopt:
|
|||||||
"""
|
"""
|
||||||
Assign the dimensions in the hyperoptimization space.
|
Assign the dimensions in the hyperoptimization space.
|
||||||
"""
|
"""
|
||||||
if self.auto_hyperopt and HyperoptTools.has_space(self.config, 'protection'):
|
if HyperoptTools.has_space(self.config, 'protection'):
|
||||||
# Protections can only be optimized when using the Parameter interface
|
# Protections can only be optimized when using the Parameter interface
|
||||||
logger.debug("Hyperopt has 'protection' space")
|
logger.debug("Hyperopt has 'protection' space")
|
||||||
# Enable Protections if protection space is selected.
|
# Enable Protections if protection space is selected.
|
||||||
@ -285,6 +261,15 @@ class Hyperopt:
|
|||||||
self.dimensions = (self.buy_space + self.sell_space + self.protection_space
|
self.dimensions = (self.buy_space + self.sell_space + self.protection_space
|
||||||
+ self.roi_space + self.stoploss_space + self.trailing_space)
|
+ self.roi_space + self.stoploss_space + self.trailing_space)
|
||||||
|
|
||||||
|
def assign_params(self, params_dict: Dict, category: str) -> None:
|
||||||
|
"""
|
||||||
|
Assign hyperoptable parameters
|
||||||
|
"""
|
||||||
|
for attr_name, attr in self.backtesting.strategy.enumerate_parameters(category):
|
||||||
|
if attr.optimize:
|
||||||
|
# noinspection PyProtectedMember
|
||||||
|
attr.value = params_dict[attr_name]
|
||||||
|
|
||||||
def generate_optimizer(self, raw_params: List[Any], iteration=None) -> Dict:
|
def generate_optimizer(self, raw_params: List[Any], iteration=None) -> Dict:
|
||||||
"""
|
"""
|
||||||
Used Optimize function.
|
Used Optimize function.
|
||||||
@ -296,18 +281,13 @@ class Hyperopt:
|
|||||||
|
|
||||||
# Apply parameters
|
# Apply parameters
|
||||||
if HyperoptTools.has_space(self.config, 'buy'):
|
if HyperoptTools.has_space(self.config, 'buy'):
|
||||||
self.backtesting.strategy.advise_buy = ( # type: ignore
|
self.assign_params(params_dict, 'buy')
|
||||||
self.custom_hyperopt.buy_strategy_generator(params_dict))
|
|
||||||
|
|
||||||
if HyperoptTools.has_space(self.config, 'sell'):
|
if HyperoptTools.has_space(self.config, 'sell'):
|
||||||
self.backtesting.strategy.advise_sell = ( # type: ignore
|
self.assign_params(params_dict, 'sell')
|
||||||
self.custom_hyperopt.sell_strategy_generator(params_dict))
|
|
||||||
|
|
||||||
if HyperoptTools.has_space(self.config, 'protection'):
|
if HyperoptTools.has_space(self.config, 'protection'):
|
||||||
for attr_name, attr in self.backtesting.strategy.enumerate_parameters('protection'):
|
self.assign_params(params_dict, 'protection')
|
||||||
if attr.optimize:
|
|
||||||
# noinspection PyProtectedMember
|
|
||||||
attr.value = params_dict[attr_name]
|
|
||||||
|
|
||||||
if HyperoptTools.has_space(self.config, 'roi'):
|
if HyperoptTools.has_space(self.config, 'roi'):
|
||||||
self.backtesting.strategy.minimal_roi = ( # type: ignore
|
self.backtesting.strategy.minimal_roi = ( # type: ignore
|
||||||
@ -517,11 +497,10 @@ class Hyperopt:
|
|||||||
f"saved to '{self.results_file}'.")
|
f"saved to '{self.results_file}'.")
|
||||||
|
|
||||||
if self.current_best_epoch:
|
if self.current_best_epoch:
|
||||||
if self.auto_hyperopt:
|
HyperoptTools.try_export_params(
|
||||||
HyperoptTools.try_export_params(
|
self.config,
|
||||||
self.config,
|
self.backtesting.strategy.get_strategy_name(),
|
||||||
self.backtesting.strategy.get_strategy_name(),
|
self.current_best_epoch)
|
||||||
self.current_best_epoch)
|
|
||||||
|
|
||||||
HyperoptTools.show_epoch_details(self.current_best_epoch, self.total_epochs,
|
HyperoptTools.show_epoch_details(self.current_best_epoch, self.total_epochs,
|
||||||
self.print_json)
|
self.print_json)
|
||||||
|
@ -4,9 +4,9 @@ This module implements a convenience auto-hyperopt class, which can be used toge
|
|||||||
that implement IHyperStrategy interface.
|
that implement IHyperStrategy interface.
|
||||||
"""
|
"""
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from typing import Any, Callable, Dict, List
|
from typing import Callable, Dict, List
|
||||||
|
|
||||||
from pandas import DataFrame
|
from freqtrade.exceptions import OperationalException
|
||||||
|
|
||||||
|
|
||||||
with suppress(ImportError):
|
with suppress(ImportError):
|
||||||
@ -15,6 +15,14 @@ with suppress(ImportError):
|
|||||||
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
||||||
|
|
||||||
|
|
||||||
|
def _format_exception_message(space: str) -> str:
|
||||||
|
raise OperationalException(
|
||||||
|
f"The '{space}' space is included into the hyperoptimization "
|
||||||
|
f"but no parameter for this space was not found in your Strategy. "
|
||||||
|
f"Please make sure to have parameters for this space enabled for optimization "
|
||||||
|
f"or remove the '{space}' space from hyperoptimization.")
|
||||||
|
|
||||||
|
|
||||||
class HyperOptAuto(IHyperOpt):
|
class HyperOptAuto(IHyperOpt):
|
||||||
"""
|
"""
|
||||||
This class delegates functionality to Strategy(IHyperStrategy) and Strategy.HyperOpt classes.
|
This class delegates functionality to Strategy(IHyperStrategy) and Strategy.HyperOpt classes.
|
||||||
@ -22,26 +30,6 @@ class HyperOptAuto(IHyperOpt):
|
|||||||
sell_indicator_space methods, but other hyperopt methods can be overridden as well.
|
sell_indicator_space methods, but other hyperopt methods can be overridden as well.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable:
|
|
||||||
def populate_buy_trend(dataframe: DataFrame, metadata: dict):
|
|
||||||
for attr_name, attr in self.strategy.enumerate_parameters('buy'):
|
|
||||||
if attr.optimize:
|
|
||||||
# noinspection PyProtectedMember
|
|
||||||
attr.value = params[attr_name]
|
|
||||||
return self.strategy.populate_buy_trend(dataframe, metadata)
|
|
||||||
|
|
||||||
return populate_buy_trend
|
|
||||||
|
|
||||||
def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable:
|
|
||||||
def populate_sell_trend(dataframe: DataFrame, metadata: dict):
|
|
||||||
for attr_name, attr in self.strategy.enumerate_parameters('sell'):
|
|
||||||
if attr.optimize:
|
|
||||||
# noinspection PyProtectedMember
|
|
||||||
attr.value = params[attr_name]
|
|
||||||
return self.strategy.populate_sell_trend(dataframe, metadata)
|
|
||||||
|
|
||||||
return populate_sell_trend
|
|
||||||
|
|
||||||
def _get_func(self, name) -> Callable:
|
def _get_func(self, name) -> Callable:
|
||||||
"""
|
"""
|
||||||
Return a function defined in Strategy.HyperOpt class, or one defined in super() class.
|
Return a function defined in Strategy.HyperOpt class, or one defined in super() class.
|
||||||
@ -60,21 +48,22 @@ class HyperOptAuto(IHyperOpt):
|
|||||||
if attr.optimize:
|
if attr.optimize:
|
||||||
yield attr.get_space(attr_name)
|
yield attr.get_space(attr_name)
|
||||||
|
|
||||||
def _get_indicator_space(self, category, fallback_method_name):
|
def _get_indicator_space(self, category):
|
||||||
|
# TODO: is this necessary, or can we call "generate_space" directly?
|
||||||
indicator_space = list(self._generate_indicator_space(category))
|
indicator_space = list(self._generate_indicator_space(category))
|
||||||
if len(indicator_space) > 0:
|
if len(indicator_space) > 0:
|
||||||
return indicator_space
|
return indicator_space
|
||||||
else:
|
else:
|
||||||
return self._get_func(fallback_method_name)()
|
_format_exception_message(category)
|
||||||
|
|
||||||
def indicator_space(self) -> List['Dimension']:
|
def indicator_space(self) -> List['Dimension']:
|
||||||
return self._get_indicator_space('buy', 'indicator_space')
|
return self._get_indicator_space('buy')
|
||||||
|
|
||||||
def sell_indicator_space(self) -> List['Dimension']:
|
def sell_indicator_space(self) -> List['Dimension']:
|
||||||
return self._get_indicator_space('sell', 'sell_indicator_space')
|
return self._get_indicator_space('sell')
|
||||||
|
|
||||||
def protection_space(self) -> List['Dimension']:
|
def protection_space(self) -> List['Dimension']:
|
||||||
return self._get_indicator_space('protection', 'protection_space')
|
return self._get_indicator_space('protection')
|
||||||
|
|
||||||
def generate_roi_table(self, params: Dict) -> Dict[int, float]:
|
def generate_roi_table(self, params: Dict) -> Dict[int, float]:
|
||||||
return self._get_func('generate_roi_table')(params)
|
return self._get_func('generate_roi_table')(params)
|
||||||
|
@ -5,11 +5,10 @@ This module defines the interface to apply for hyperopt
|
|||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
from typing import Any, Callable, Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
from skopt.space import Categorical, Dimension, Integer
|
from skopt.space import Categorical, Dimension, Integer
|
||||||
|
|
||||||
from freqtrade.exceptions import OperationalException
|
|
||||||
from freqtrade.exchange import timeframe_to_minutes
|
from freqtrade.exchange import timeframe_to_minutes
|
||||||
from freqtrade.misc import round_dict
|
from freqtrade.misc import round_dict
|
||||||
from freqtrade.optimize.space import SKDecimal
|
from freqtrade.optimize.space import SKDecimal
|
||||||
@ -19,13 +18,6 @@ from freqtrade.strategy import IStrategy
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _format_exception_message(method: str, space: str) -> str:
|
|
||||||
return (f"The '{space}' space is included into the hyperoptimization "
|
|
||||||
f"but {method}() method is not found in your "
|
|
||||||
f"custom Hyperopt class. You should either implement this "
|
|
||||||
f"method or remove the '{space}' space from hyperoptimization.")
|
|
||||||
|
|
||||||
|
|
||||||
class IHyperOpt(ABC):
|
class IHyperOpt(ABC):
|
||||||
"""
|
"""
|
||||||
Interface for freqtrade hyperopt
|
Interface for freqtrade hyperopt
|
||||||
@ -45,37 +37,6 @@ class IHyperOpt(ABC):
|
|||||||
IHyperOpt.ticker_interval = str(config['timeframe']) # DEPRECATED
|
IHyperOpt.ticker_interval = str(config['timeframe']) # DEPRECATED
|
||||||
IHyperOpt.timeframe = str(config['timeframe'])
|
IHyperOpt.timeframe = str(config['timeframe'])
|
||||||
|
|
||||||
def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable:
|
|
||||||
"""
|
|
||||||
Create a buy strategy generator.
|
|
||||||
"""
|
|
||||||
raise OperationalException(_format_exception_message('buy_strategy_generator', 'buy'))
|
|
||||||
|
|
||||||
def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable:
|
|
||||||
"""
|
|
||||||
Create a sell strategy generator.
|
|
||||||
"""
|
|
||||||
raise OperationalException(_format_exception_message('sell_strategy_generator', 'sell'))
|
|
||||||
|
|
||||||
def protection_space(self) -> List[Dimension]:
|
|
||||||
"""
|
|
||||||
Create a protection space.
|
|
||||||
Only supported by the Parameter interface.
|
|
||||||
"""
|
|
||||||
raise OperationalException(_format_exception_message('indicator_space', 'protection'))
|
|
||||||
|
|
||||||
def indicator_space(self) -> List[Dimension]:
|
|
||||||
"""
|
|
||||||
Create an indicator space.
|
|
||||||
"""
|
|
||||||
raise OperationalException(_format_exception_message('indicator_space', 'buy'))
|
|
||||||
|
|
||||||
def sell_indicator_space(self) -> List[Dimension]:
|
|
||||||
"""
|
|
||||||
Create a sell indicator space.
|
|
||||||
"""
|
|
||||||
raise OperationalException(_format_exception_message('sell_indicator_space', 'sell'))
|
|
||||||
|
|
||||||
def generate_roi_table(self, params: Dict) -> Dict[int, float]:
|
def generate_roi_table(self, params: Dict) -> Dict[int, float]:
|
||||||
"""
|
"""
|
||||||
Create a ROI table.
|
Create a ROI table.
|
||||||
|
@ -549,7 +549,7 @@ class LocalTrade():
|
|||||||
if self.is_open:
|
if self.is_open:
|
||||||
payment = "BUY" if self.is_short else "SELL"
|
payment = "BUY" if self.is_short else "SELL"
|
||||||
# TODO-lev: On shorts, you buy a little bit more than the amount (amount + interest)
|
# TODO-lev: On shorts, you buy a little bit more than the amount (amount + interest)
|
||||||
# This wll only print the original amount
|
# TODO-lev: This wll only print the original amount
|
||||||
logger.info(f'{order_type.upper()}_{payment} has been fulfilled for {self}.')
|
logger.info(f'{order_type.upper()}_{payment} has been fulfilled for {self}.')
|
||||||
# TODO-lev: Double check this
|
# TODO-lev: Double check this
|
||||||
self.close(safe_value_fallback(order, 'average', 'price'))
|
self.close(safe_value_fallback(order, 'average', 'price'))
|
||||||
|
@ -18,6 +18,7 @@ class PrecisionFilter(IPairList):
|
|||||||
pairlist_pos: int) -> None:
|
pairlist_pos: int) -> None:
|
||||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||||
|
|
||||||
|
# TODO-lev: Liquidation price?
|
||||||
if 'stoploss' not in self._config:
|
if 'stoploss' not in self._config:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
'PrecisionFilter can only work with stoploss defined. Please add the '
|
'PrecisionFilter can only work with stoploss defined. Please add the '
|
||||||
|
@ -123,7 +123,7 @@ class VolumePairList(IPairList):
|
|||||||
filtered_tickers = [
|
filtered_tickers = [
|
||||||
v for k, v in tickers.items()
|
v for k, v in tickers.items()
|
||||||
if (self._exchange.get_pair_quote_currency(k) == self._stake_currency
|
if (self._exchange.get_pair_quote_currency(k) == self._stake_currency
|
||||||
and v[self._sort_key] is not None)]
|
and (self._use_range or v[self._sort_key] is not None))]
|
||||||
pairlist = [s['symbol'] for s in filtered_tickers]
|
pairlist = [s['symbol'] for s in filtered_tickers]
|
||||||
|
|
||||||
pairlist = self.filter_pairlist(pairlist, tickers)
|
pairlist = self.filter_pairlist(pairlist, tickers)
|
||||||
|
@ -17,7 +17,7 @@ def expand_pairlist(wildcardpl: List[str], available_pairs: List[str],
|
|||||||
if keep_invalid:
|
if keep_invalid:
|
||||||
for pair_wc in wildcardpl:
|
for pair_wc in wildcardpl:
|
||||||
try:
|
try:
|
||||||
comp = re.compile(pair_wc)
|
comp = re.compile(pair_wc, re.IGNORECASE)
|
||||||
result_partial = [
|
result_partial = [
|
||||||
pair for pair in available_pairs if re.fullmatch(comp, pair)
|
pair for pair in available_pairs if re.fullmatch(comp, pair)
|
||||||
]
|
]
|
||||||
@ -33,7 +33,7 @@ def expand_pairlist(wildcardpl: List[str], available_pairs: List[str],
|
|||||||
else:
|
else:
|
||||||
for pair_wc in wildcardpl:
|
for pair_wc in wildcardpl:
|
||||||
try:
|
try:
|
||||||
comp = re.compile(pair_wc)
|
comp = re.compile(pair_wc, re.IGNORECASE)
|
||||||
result += [
|
result += [
|
||||||
pair for pair in available_pairs if re.fullmatch(comp, pair)
|
pair for pair in available_pairs if re.fullmatch(comp, pair)
|
||||||
]
|
]
|
||||||
|
@ -127,7 +127,7 @@ class PairListManager():
|
|||||||
:return: pairlist - whitelisted pairs
|
:return: pairlist - whitelisted pairs
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
# TODO-lev: filter for pairlists that are able to trade at the desired leverage
|
||||||
whitelist = expand_pairlist(pairlist, self._exchange.get_markets().keys(), keep_invalid)
|
whitelist = expand_pairlist(pairlist, self._exchange.get_markets().keys(), keep_invalid)
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
logger.error(f"Pair whitelist contains an invalid Wildcard: {err}")
|
logger.error(f"Pair whitelist contains an invalid Wildcard: {err}")
|
||||||
|
@ -36,6 +36,7 @@ class MaxDrawdown(IProtection):
|
|||||||
"""
|
"""
|
||||||
LockReason to use
|
LockReason to use
|
||||||
"""
|
"""
|
||||||
|
# TODO-lev: < for shorts?
|
||||||
return (f'{drawdown} > {self._max_allowed_drawdown} in {self.lookback_period_str}, '
|
return (f'{drawdown} > {self._max_allowed_drawdown} in {self.lookback_period_str}, '
|
||||||
f'locking for {self.stop_duration_str}.')
|
f'locking for {self.stop_duration_str}.')
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@ class StoplossGuard(IProtection):
|
|||||||
def _reason(self) -> str:
|
def _reason(self) -> str:
|
||||||
"""
|
"""
|
||||||
LockReason to use
|
LockReason to use
|
||||||
|
#TODO-lev: check if min is the right word for shorts
|
||||||
"""
|
"""
|
||||||
return (f'{self._trade_limit} stoplosses in {self._lookback_period} min, '
|
return (f'{self._trade_limit} stoplosses in {self._lookback_period} min, '
|
||||||
f'locking for {self._stop_duration} min.')
|
f'locking for {self._stop_duration} min.')
|
||||||
@ -51,6 +52,7 @@ class StoplossGuard(IProtection):
|
|||||||
# if pair:
|
# if pair:
|
||||||
# filters.append(Trade.pair == pair)
|
# filters.append(Trade.pair == pair)
|
||||||
# trades = Trade.get_trades(filters).all()
|
# trades = Trade.get_trades(filters).all()
|
||||||
|
# TODO-lev: Liquidation price?
|
||||||
|
|
||||||
trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until)
|
trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until)
|
||||||
trades = [trade for trade in trades1 if (str(trade.sell_reason) in (
|
trades = [trade for trade in trades1 if (str(trade.sell_reason) in (
|
||||||
|
@ -9,7 +9,6 @@ from typing import Dict
|
|||||||
|
|
||||||
from freqtrade.constants import HYPEROPT_LOSS_BUILTIN, USERPATH_HYPEROPTS
|
from freqtrade.constants import HYPEROPT_LOSS_BUILTIN, USERPATH_HYPEROPTS
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
|
||||||
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss
|
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss
|
||||||
from freqtrade.resolvers import IResolver
|
from freqtrade.resolvers import IResolver
|
||||||
|
|
||||||
@ -17,43 +16,6 @@ from freqtrade.resolvers import IResolver
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class HyperOptResolver(IResolver):
|
|
||||||
"""
|
|
||||||
This class contains all the logic to load custom hyperopt class
|
|
||||||
"""
|
|
||||||
object_type = IHyperOpt
|
|
||||||
object_type_str = "Hyperopt"
|
|
||||||
user_subdir = USERPATH_HYPEROPTS
|
|
||||||
initial_search_path = None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def load_hyperopt(config: Dict) -> IHyperOpt:
|
|
||||||
"""
|
|
||||||
Load the custom hyperopt class from config parameter
|
|
||||||
:param config: configuration dictionary
|
|
||||||
"""
|
|
||||||
if not config.get('hyperopt'):
|
|
||||||
raise OperationalException("No Hyperopt set. Please use `--hyperopt` to specify "
|
|
||||||
"the Hyperopt class to use.")
|
|
||||||
|
|
||||||
hyperopt_name = config['hyperopt']
|
|
||||||
|
|
||||||
hyperopt = HyperOptResolver.load_object(hyperopt_name, config,
|
|
||||||
kwargs={'config': config},
|
|
||||||
extra_dir=config.get('hyperopt_path'))
|
|
||||||
|
|
||||||
if not hasattr(hyperopt, 'populate_indicators'):
|
|
||||||
logger.info("Hyperopt class does not provide populate_indicators() method. "
|
|
||||||
"Using populate_indicators from the strategy.")
|
|
||||||
if not hasattr(hyperopt, 'populate_buy_trend'):
|
|
||||||
logger.info("Hyperopt class does not provide populate_buy_trend() method. "
|
|
||||||
"Using populate_buy_trend from the strategy.")
|
|
||||||
if not hasattr(hyperopt, 'populate_sell_trend'):
|
|
||||||
logger.info("Hyperopt class does not provide populate_sell_trend() method. "
|
|
||||||
"Using populate_sell_trend from the strategy.")
|
|
||||||
return hyperopt
|
|
||||||
|
|
||||||
|
|
||||||
class HyperOptLossResolver(IResolver):
|
class HyperOptLossResolver(IResolver):
|
||||||
"""
|
"""
|
||||||
This class contains all the logic to load custom hyperopt loss class
|
This class contains all the logic to load custom hyperopt loss class
|
||||||
|
@ -5,6 +5,20 @@ import time
|
|||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
|
|
||||||
|
def asyncio_setup() -> None: # pragma: no cover
|
||||||
|
# Set eventloop for win32 setups
|
||||||
|
# Reverts a change done in uvicorn 0.15.0 - which now sets the eventloop
|
||||||
|
# via policy.
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 8) and sys.platform == "win32":
|
||||||
|
import asyncio
|
||||||
|
import selectors
|
||||||
|
selector = selectors.SelectSelector()
|
||||||
|
loop = asyncio.SelectorEventLoop(selector)
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
|
|
||||||
class UvicornServer(uvicorn.Server):
|
class UvicornServer(uvicorn.Server):
|
||||||
"""
|
"""
|
||||||
Multithreaded server - as found in https://github.com/encode/uvicorn/issues/742
|
Multithreaded server - as found in https://github.com/encode/uvicorn/issues/742
|
||||||
@ -28,7 +42,7 @@ class UvicornServer(uvicorn.Server):
|
|||||||
try:
|
try:
|
||||||
import uvloop # noqa
|
import uvloop # noqa
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
from uvicorn.loops.asyncio import asyncio_setup
|
|
||||||
asyncio_setup()
|
asyncio_setup()
|
||||||
else:
|
else:
|
||||||
asyncio.set_event_loop(uvloop.new_event_loop())
|
asyncio.set_event_loop(uvloop.new_event_loop())
|
||||||
|
@ -36,6 +36,7 @@ class RPCException(Exception):
|
|||||||
|
|
||||||
raise RPCException('*Status:* `no active trade`')
|
raise RPCException('*Status:* `no active trade`')
|
||||||
"""
|
"""
|
||||||
|
# TODO-lev: Add new configuration options introduced with leveraged/short trading
|
||||||
|
|
||||||
def __init__(self, message: str) -> None:
|
def __init__(self, message: str) -> None:
|
||||||
super().__init__(self)
|
super().__init__(self)
|
||||||
@ -403,8 +404,11 @@ class RPC:
|
|||||||
# Doing the sum is not right - overall profit needs to be based on initial capital
|
# Doing the sum is not right - overall profit needs to be based on initial capital
|
||||||
profit_all_ratio_sum = sum(profit_all_ratio) if profit_all_ratio else 0.0
|
profit_all_ratio_sum = sum(profit_all_ratio) if profit_all_ratio else 0.0
|
||||||
starting_balance = self._freqtrade.wallets.get_starting_balance()
|
starting_balance = self._freqtrade.wallets.get_starting_balance()
|
||||||
profit_closed_ratio_fromstart = profit_closed_coin_sum / starting_balance
|
profit_closed_ratio_fromstart = 0
|
||||||
profit_all_ratio_fromstart = profit_all_coin_sum / starting_balance
|
profit_all_ratio_fromstart = 0
|
||||||
|
if starting_balance:
|
||||||
|
profit_closed_ratio_fromstart = profit_closed_coin_sum / starting_balance
|
||||||
|
profit_all_ratio_fromstart = profit_all_coin_sum / starting_balance
|
||||||
|
|
||||||
profit_all_fiat = self._fiat_converter.convert_amount(
|
profit_all_fiat = self._fiat_converter.convert_amount(
|
||||||
profit_all_coin_sum,
|
profit_all_coin_sum,
|
||||||
@ -545,12 +549,12 @@ class RPC:
|
|||||||
order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
|
order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
|
||||||
|
|
||||||
if order['side'] == 'buy':
|
if order['side'] == 'buy':
|
||||||
fully_canceled = self._freqtrade.handle_cancel_buy(
|
fully_canceled = self._freqtrade.handle_cancel_enter(
|
||||||
trade, order, CANCEL_REASON['FORCE_SELL'])
|
trade, order, CANCEL_REASON['FORCE_SELL'])
|
||||||
|
|
||||||
if order['side'] == 'sell':
|
if order['side'] == 'sell':
|
||||||
# Cancel order - so it is placed anew with a fresh price.
|
# Cancel order - so it is placed anew with a fresh price.
|
||||||
self._freqtrade.handle_cancel_sell(trade, order, CANCEL_REASON['FORCE_SELL'])
|
self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_SELL'])
|
||||||
|
|
||||||
if not fully_canceled:
|
if not fully_canceled:
|
||||||
# Get current rate and execute sell
|
# Get current rate and execute sell
|
||||||
@ -563,7 +567,7 @@ class RPC:
|
|||||||
if self._freqtrade.state != State.RUNNING:
|
if self._freqtrade.state != State.RUNNING:
|
||||||
raise RPCException('trader is not running')
|
raise RPCException('trader is not running')
|
||||||
|
|
||||||
with self._freqtrade._sell_lock:
|
with self._freqtrade._exit_lock:
|
||||||
if trade_id == 'all':
|
if trade_id == 'all':
|
||||||
# Execute sell for all open orders
|
# Execute sell for all open orders
|
||||||
for trade in Trade.get_open_trades():
|
for trade in Trade.get_open_trades():
|
||||||
@ -625,7 +629,7 @@ class RPC:
|
|||||||
Handler for delete <id>.
|
Handler for delete <id>.
|
||||||
Delete the given trade and close eventually existing open orders.
|
Delete the given trade and close eventually existing open orders.
|
||||||
"""
|
"""
|
||||||
with self._freqtrade._sell_lock:
|
with self._freqtrade._exit_lock:
|
||||||
c_count = 0
|
c_count = 0
|
||||||
trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first()
|
trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first()
|
||||||
if not trade:
|
if not trade:
|
||||||
|
@ -168,7 +168,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
"""
|
"""
|
||||||
Check buy enter timeout function callback.
|
Check buy enter timeout function callback.
|
||||||
This method can be used to override the enter-timeout.
|
This method can be used to override the enter-timeout.
|
||||||
It is called whenever a limit buy/short order has been created,
|
It is called whenever a limit entry order has been created,
|
||||||
and is not yet fully filled.
|
and is not yet fully filled.
|
||||||
Configuration options in `unfilledtimeout` will be verified before this,
|
Configuration options in `unfilledtimeout` will be verified before this,
|
||||||
so ensure to set these timeouts high enough.
|
so ensure to set these timeouts high enough.
|
||||||
@ -178,7 +178,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
:param trade: trade object.
|
:param trade: trade object.
|
||||||
:param order: Order dictionary as returned from CCXT.
|
:param order: Order dictionary as returned from CCXT.
|
||||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
:return bool: When True is returned, then the buy/short-order is cancelled.
|
:return bool: When True is returned, then the entry order is cancelled.
|
||||||
"""
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -212,7 +212,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
|
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
|
||||||
time_in_force: str, current_time: datetime, **kwargs) -> bool:
|
time_in_force: str, current_time: datetime, **kwargs) -> bool:
|
||||||
"""
|
"""
|
||||||
Called right before placing a buy/short order.
|
Called right before placing a entry order.
|
||||||
Timing for this function is critical, so avoid doing heavy computations or
|
Timing for this function is critical, so avoid doing heavy computations or
|
||||||
network requests in this method.
|
network requests in this method.
|
||||||
|
|
||||||
@ -236,7 +236,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
rate: float, time_in_force: str, sell_reason: str,
|
rate: float, time_in_force: str, sell_reason: str,
|
||||||
current_time: datetime, **kwargs) -> bool:
|
current_time: datetime, **kwargs) -> bool:
|
||||||
"""
|
"""
|
||||||
Called right before placing a regular sell/exit_short order.
|
Called right before placing a regular exit order.
|
||||||
Timing for this function is critical, so avoid doing heavy computations or
|
Timing for this function is critical, so avoid doing heavy computations or
|
||||||
network requests in this method.
|
network requests in this method.
|
||||||
|
|
||||||
@ -410,7 +410,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
Checks if a pair is currently locked
|
Checks if a pair is currently locked
|
||||||
The 2nd, optional parameter ensures that locks are applied until the new candle arrives,
|
The 2nd, optional parameter ensures that locks are applied until the new candle arrives,
|
||||||
and not stop at 14:00:00 - while the next candle arrives at 14:00:02 leaving a gap
|
and not stop at 14:00:00 - while the next candle arrives at 14:00:02 leaving a gap
|
||||||
of 2 seconds for a buy/short to happen on an old signal.
|
of 2 seconds for an entry order to happen on an old signal.
|
||||||
:param pair: "Pair to check"
|
:param pair: "Pair to check"
|
||||||
:param candle_date: Date of the last candle. Optional, defaults to current date
|
:param candle_date: Date of the last candle. Optional, defaults to current date
|
||||||
:returns: locking state of the pair in question.
|
:returns: locking state of the pair in question.
|
||||||
@ -426,7 +426,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Parses the given candle (OHLCV) data and returns a populated DataFrame
|
Parses the given candle (OHLCV) data and returns a populated DataFrame
|
||||||
add several TA indicators and buy/short signal to it
|
add several TA indicators and entry order signal to it
|
||||||
:param dataframe: Dataframe containing data from exchange
|
:param dataframe: Dataframe containing data from exchange
|
||||||
:param metadata: Metadata dictionary with additional data (e.g. 'pair')
|
:param metadata: Metadata dictionary with additional data (e.g. 'pair')
|
||||||
:return: DataFrame of candle (OHLCV) data with indicator data and signals added
|
:return: DataFrame of candle (OHLCV) data with indicator data and signals added
|
||||||
@ -541,7 +541,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
dataframe: DataFrame
|
dataframe: DataFrame
|
||||||
) -> Tuple[bool, bool, Optional[str]]:
|
) -> Tuple[bool, bool, Optional[str]]:
|
||||||
"""
|
"""
|
||||||
Calculates current signal based based on the buy/short or sell/exit_short
|
Calculates current signal based based on the entry order or exit order
|
||||||
columns of the dataframe.
|
columns of the dataframe.
|
||||||
Used by Bot to get the signal to buy, sell, short, or exit_short
|
Used by Bot to get the signal to buy, sell, short, or exit_short
|
||||||
:param pair: pair in format ANT/BTC
|
:param pair: pair in format ANT/BTC
|
||||||
@ -606,7 +606,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
sell: bool, low: float = None, high: float = None,
|
sell: bool, low: float = None, high: float = None,
|
||||||
force_stoploss: float = 0) -> SellCheckTuple:
|
force_stoploss: float = 0) -> SellCheckTuple:
|
||||||
"""
|
"""
|
||||||
This function evaluates if one of the conditions required to trigger a sell/exit_short
|
This function evaluates if one of the conditions required to trigger an exit order
|
||||||
has been reached, which can either be a stop-loss, ROI or exit-signal.
|
has been reached, which can either be a stop-loss, ROI or exit-signal.
|
||||||
:param low: Only used during backtesting to simulate (long)stoploss/(short)ROI
|
:param low: Only used during backtesting to simulate (long)stoploss/(short)ROI
|
||||||
:param high: Only used during backtesting, to simulate (short)stoploss/(long)ROI
|
:param high: Only used during backtesting, to simulate (short)stoploss/(long)ROI
|
||||||
@ -810,7 +810,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
|
|
||||||
def advise_buy(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def advise_buy(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Based on TA indicators, populates the buy/short signal for the given dataframe
|
Based on TA indicators, populates the entry order signal for the given dataframe
|
||||||
This method should not be overridden.
|
This method should not be overridden.
|
||||||
:param dataframe: DataFrame
|
:param dataframe: DataFrame
|
||||||
:param metadata: Additional information dictionary, with details like the
|
:param metadata: Additional information dictionary, with details like the
|
||||||
@ -829,7 +829,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
|
|
||||||
def advise_sell(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def advise_sell(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Based on TA indicators, populates the sell/exit_short signal for the given dataframe
|
Based on TA indicators, populates the exit order signal for the given dataframe
|
||||||
This method should not be overridden.
|
This method should not be overridden.
|
||||||
:param dataframe: DataFrame
|
:param dataframe: DataFrame
|
||||||
:param metadata: Additional information dictionary, with details like the
|
:param metadata: Additional information dictionary, with details like the
|
||||||
|
@ -1,3 +1,10 @@
|
|||||||
|
{%set volume_pairlist = '{
|
||||||
|
"method": "VolumePairList",
|
||||||
|
"number_assets": 20,
|
||||||
|
"sort_key": "quoteVolume",
|
||||||
|
"min_value": 0,
|
||||||
|
"refresh_period": 1800
|
||||||
|
}' %}
|
||||||
{
|
{
|
||||||
"max_open_trades": {{ max_open_trades }},
|
"max_open_trades": {{ max_open_trades }},
|
||||||
"stake_currency": "{{ stake_currency }}",
|
"stake_currency": "{{ stake_currency }}",
|
||||||
@ -29,7 +36,7 @@
|
|||||||
},
|
},
|
||||||
{{ exchange | indent(4) }},
|
{{ exchange | indent(4) }},
|
||||||
"pairlists": [
|
"pairlists": [
|
||||||
{"method": "StaticPairList"}
|
{{ '{"method": "StaticPairList"}' if exchange_name == 'bittrex' else volume_pairlist }}
|
||||||
],
|
],
|
||||||
"edge": {
|
"edge": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
|
@ -1,137 +0,0 @@
|
|||||||
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
|
||||||
|
|
||||||
# --- Do not remove these libs ---
|
|
||||||
from functools import reduce
|
|
||||||
from typing import Any, Callable, Dict, List
|
|
||||||
|
|
||||||
import numpy as np # noqa
|
|
||||||
import pandas as pd # noqa
|
|
||||||
from pandas import DataFrame
|
|
||||||
from skopt.space import Categorical, Dimension, Integer, Real # noqa
|
|
||||||
|
|
||||||
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
|
||||||
|
|
||||||
# --------------------------------
|
|
||||||
# Add your lib to import here
|
|
||||||
import talib.abstract as ta # noqa
|
|
||||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
|
||||||
|
|
||||||
|
|
||||||
class {{ hyperopt }}(IHyperOpt):
|
|
||||||
"""
|
|
||||||
This is a Hyperopt template to get you started.
|
|
||||||
|
|
||||||
More information in the documentation: https://www.freqtrade.io/en/latest/hyperopt/
|
|
||||||
|
|
||||||
You should:
|
|
||||||
- Add any lib you need to build your hyperopt.
|
|
||||||
|
|
||||||
You must keep:
|
|
||||||
- The prototypes for the methods: populate_indicators, indicator_space, buy_strategy_generator.
|
|
||||||
|
|
||||||
The methods roi_space, generate_roi_table and stoploss_space are not required
|
|
||||||
and are provided by default.
|
|
||||||
However, you may override them if you need 'roi' and 'stoploss' spaces that
|
|
||||||
differ from the defaults offered by Freqtrade.
|
|
||||||
Sample implementation of these methods will be copied to `user_data/hyperopts` when
|
|
||||||
creating the user-data directory using `freqtrade create-userdir --userdir user_data`,
|
|
||||||
or is available online under the following URL:
|
|
||||||
https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def indicator_space() -> List[Dimension]:
|
|
||||||
"""
|
|
||||||
Define your Hyperopt space for searching buy strategy parameters.
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
{{ buy_space | indent(12) }}
|
|
||||||
]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
|
||||||
"""
|
|
||||||
Define the buy strategy parameters to be used by Hyperopt.
|
|
||||||
"""
|
|
||||||
def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Buy strategy Hyperopt will build and use.
|
|
||||||
"""
|
|
||||||
conditions = []
|
|
||||||
|
|
||||||
# GUARDS AND TRENDS
|
|
||||||
{{ buy_guards | indent(12) }}
|
|
||||||
|
|
||||||
# TRIGGERS
|
|
||||||
if 'trigger' in params:
|
|
||||||
if params['trigger'] == 'bb_lower':
|
|
||||||
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
|
|
||||||
if params['trigger'] == 'macd_cross_signal':
|
|
||||||
conditions.append(qtpylib.crossed_above(
|
|
||||||
dataframe['macd'], dataframe['macdsignal']
|
|
||||||
))
|
|
||||||
if params['trigger'] == 'sar_reversal':
|
|
||||||
conditions.append(qtpylib.crossed_above(
|
|
||||||
dataframe['close'], dataframe['sar']
|
|
||||||
))
|
|
||||||
|
|
||||||
# Check that the candle had volume
|
|
||||||
conditions.append(dataframe['volume'] > 0)
|
|
||||||
|
|
||||||
if conditions:
|
|
||||||
dataframe.loc[
|
|
||||||
reduce(lambda x, y: x & y, conditions),
|
|
||||||
'buy'] = 1
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
return populate_buy_trend
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def sell_indicator_space() -> List[Dimension]:
|
|
||||||
"""
|
|
||||||
Define your Hyperopt space for searching sell strategy parameters.
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
{{ sell_space | indent(12) }}
|
|
||||||
]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def sell_strategy_generator(params: Dict[str, Any]) -> Callable:
|
|
||||||
"""
|
|
||||||
Define the sell strategy parameters to be used by Hyperopt.
|
|
||||||
"""
|
|
||||||
def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Sell strategy Hyperopt will build and use.
|
|
||||||
"""
|
|
||||||
conditions = []
|
|
||||||
|
|
||||||
# GUARDS AND TRENDS
|
|
||||||
{{ sell_guards | indent(12) }}
|
|
||||||
|
|
||||||
# TRIGGERS
|
|
||||||
if 'sell-trigger' in params:
|
|
||||||
if params['sell-trigger'] == 'sell-bb_upper':
|
|
||||||
conditions.append(dataframe['close'] > dataframe['bb_upperband'])
|
|
||||||
if params['sell-trigger'] == 'sell-macd_cross_signal':
|
|
||||||
conditions.append(qtpylib.crossed_above(
|
|
||||||
dataframe['macdsignal'], dataframe['macd']
|
|
||||||
))
|
|
||||||
if params['sell-trigger'] == 'sell-sar_reversal':
|
|
||||||
conditions.append(qtpylib.crossed_above(
|
|
||||||
dataframe['sar'], dataframe['close']
|
|
||||||
))
|
|
||||||
|
|
||||||
# Check that the candle had volume
|
|
||||||
conditions.append(dataframe['volume'] > 0)
|
|
||||||
|
|
||||||
if conditions:
|
|
||||||
dataframe.loc[
|
|
||||||
reduce(lambda x, y: x & y, conditions),
|
|
||||||
'sell'] = 1
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
return populate_sell_trend
|
|
||||||
|
|
@ -1,180 +0,0 @@
|
|||||||
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
|
||||||
# isort: skip_file
|
|
||||||
|
|
||||||
# --- Do not remove these libs ---
|
|
||||||
from functools import reduce
|
|
||||||
from typing import Any, Callable, Dict, List
|
|
||||||
|
|
||||||
import numpy as np # noqa
|
|
||||||
import pandas as pd # noqa
|
|
||||||
from pandas import DataFrame
|
|
||||||
from skopt.space import Categorical, Dimension, Integer, Real # noqa
|
|
||||||
|
|
||||||
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
|
||||||
|
|
||||||
# --------------------------------
|
|
||||||
# Add your lib to import here
|
|
||||||
import talib.abstract as ta # noqa
|
|
||||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
|
||||||
|
|
||||||
|
|
||||||
class SampleHyperOpt(IHyperOpt):
|
|
||||||
"""
|
|
||||||
This is a sample Hyperopt to inspire you.
|
|
||||||
|
|
||||||
More information in the documentation: https://www.freqtrade.io/en/latest/hyperopt/
|
|
||||||
|
|
||||||
You should:
|
|
||||||
- Rename the class name to some unique name.
|
|
||||||
- Add any methods you want to build your hyperopt.
|
|
||||||
- Add any lib you need to build your hyperopt.
|
|
||||||
|
|
||||||
An easier way to get a new hyperopt file is by using
|
|
||||||
`freqtrade new-hyperopt --hyperopt MyCoolHyperopt`.
|
|
||||||
|
|
||||||
You must keep:
|
|
||||||
- The prototypes for the methods: populate_indicators, indicator_space, buy_strategy_generator.
|
|
||||||
|
|
||||||
The methods roi_space, generate_roi_table and stoploss_space are not required
|
|
||||||
and are provided by default.
|
|
||||||
However, you may override them if you need 'roi' and 'stoploss' spaces that
|
|
||||||
differ from the defaults offered by Freqtrade.
|
|
||||||
Sample implementation of these methods will be copied to `user_data/hyperopts` when
|
|
||||||
creating the user-data directory using `freqtrade create-userdir --userdir user_data`,
|
|
||||||
or is available online under the following URL:
|
|
||||||
https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def indicator_space() -> List[Dimension]:
|
|
||||||
"""
|
|
||||||
Define your Hyperopt space for searching buy strategy parameters.
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
Integer(10, 25, name='mfi-value'),
|
|
||||||
Integer(15, 45, name='fastd-value'),
|
|
||||||
Integer(20, 50, name='adx-value'),
|
|
||||||
Integer(20, 40, name='rsi-value'),
|
|
||||||
Categorical([True, False], name='mfi-enabled'),
|
|
||||||
Categorical([True, False], name='fastd-enabled'),
|
|
||||||
Categorical([True, False], name='adx-enabled'),
|
|
||||||
Categorical([True, False], name='rsi-enabled'),
|
|
||||||
Categorical(['boll', 'macd_cross_signal', 'sar_reversal'], name='trigger')
|
|
||||||
]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
|
||||||
"""
|
|
||||||
Define the buy strategy parameters to be used by Hyperopt.
|
|
||||||
"""
|
|
||||||
def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Buy strategy Hyperopt will build and use.
|
|
||||||
"""
|
|
||||||
long_conditions = []
|
|
||||||
|
|
||||||
# GUARDS AND TRENDS
|
|
||||||
if 'mfi-enabled' in params and params['mfi-enabled']:
|
|
||||||
long_conditions.append(dataframe['mfi'] < params['mfi-value'])
|
|
||||||
if 'fastd-enabled' in params and params['fastd-enabled']:
|
|
||||||
long_conditions.append(dataframe['fastd'] < params['fastd-value'])
|
|
||||||
if 'adx-enabled' in params and params['adx-enabled']:
|
|
||||||
long_conditions.append(dataframe['adx'] > params['adx-value'])
|
|
||||||
if 'rsi-enabled' in params and params['rsi-enabled']:
|
|
||||||
long_conditions.append(dataframe['rsi'] < params['rsi-value'])
|
|
||||||
|
|
||||||
# TRIGGERS
|
|
||||||
if 'trigger' in params:
|
|
||||||
if params['trigger'] == 'boll':
|
|
||||||
long_conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
|
|
||||||
if params['trigger'] == 'macd_cross_signal':
|
|
||||||
long_conditions.append(qtpylib.crossed_above(
|
|
||||||
dataframe['macd'],
|
|
||||||
dataframe['macdsignal']
|
|
||||||
))
|
|
||||||
if params['trigger'] == 'sar_reversal':
|
|
||||||
long_conditions.append(qtpylib.crossed_above(
|
|
||||||
dataframe['close'],
|
|
||||||
dataframe['sar']
|
|
||||||
))
|
|
||||||
|
|
||||||
# Check that volume is not 0
|
|
||||||
long_conditions.append(dataframe['volume'] > 0)
|
|
||||||
|
|
||||||
if long_conditions:
|
|
||||||
dataframe.loc[
|
|
||||||
reduce(lambda x, y: x & y, long_conditions),
|
|
||||||
'buy'] = 1
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
return populate_buy_trend
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def sell_indicator_space() -> List[Dimension]:
|
|
||||||
"""
|
|
||||||
Define your Hyperopt space for searching sell strategy parameters.
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
Integer(75, 100, name='sell-mfi-value'),
|
|
||||||
Integer(50, 100, name='sell-fastd-value'),
|
|
||||||
Integer(50, 100, name='sell-adx-value'),
|
|
||||||
Integer(60, 100, name='sell-rsi-value'),
|
|
||||||
Categorical([True, False], name='sell-mfi-enabled'),
|
|
||||||
Categorical([True, False], name='sell-fastd-enabled'),
|
|
||||||
Categorical([True, False], name='sell-adx-enabled'),
|
|
||||||
Categorical([True, False], name='sell-rsi-enabled'),
|
|
||||||
Categorical(['sell-boll',
|
|
||||||
'sell-macd_cross_signal',
|
|
||||||
'sell-sar_reversal'],
|
|
||||||
name='sell-trigger'
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def sell_strategy_generator(params: Dict[str, Any]) -> Callable:
|
|
||||||
"""
|
|
||||||
Define the sell strategy parameters to be used by Hyperopt.
|
|
||||||
"""
|
|
||||||
def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Sell strategy Hyperopt will build and use.
|
|
||||||
"""
|
|
||||||
exit_long_conditions = []
|
|
||||||
|
|
||||||
# GUARDS AND TRENDS
|
|
||||||
if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']:
|
|
||||||
exit_long_conditions.append(dataframe['mfi'] > params['sell-mfi-value'])
|
|
||||||
if 'sell-fastd-enabled' in params and params['sell-fastd-enabled']:
|
|
||||||
exit_long_conditions.append(dataframe['fastd'] > params['sell-fastd-value'])
|
|
||||||
if 'sell-adx-enabled' in params and params['sell-adx-enabled']:
|
|
||||||
exit_long_conditions.append(dataframe['adx'] < params['sell-adx-value'])
|
|
||||||
if 'sell-rsi-enabled' in params and params['sell-rsi-enabled']:
|
|
||||||
exit_long_conditions.append(dataframe['rsi'] > params['sell-rsi-value'])
|
|
||||||
|
|
||||||
# TRIGGERS
|
|
||||||
if 'sell-trigger' in params:
|
|
||||||
if params['sell-trigger'] == 'sell-boll':
|
|
||||||
exit_long_conditions.append(dataframe['close'] > dataframe['bb_upperband'])
|
|
||||||
if params['sell-trigger'] == 'sell-macd_cross_signal':
|
|
||||||
exit_long_conditions.append(qtpylib.crossed_above(
|
|
||||||
dataframe['macdsignal'],
|
|
||||||
dataframe['macd']
|
|
||||||
))
|
|
||||||
if params['sell-trigger'] == 'sell-sar_reversal':
|
|
||||||
exit_long_conditions.append(qtpylib.crossed_above(
|
|
||||||
dataframe['sar'],
|
|
||||||
dataframe['close']
|
|
||||||
))
|
|
||||||
|
|
||||||
# Check that volume is not 0
|
|
||||||
exit_long_conditions.append(dataframe['volume'] > 0)
|
|
||||||
|
|
||||||
if exit_long_conditions:
|
|
||||||
dataframe.loc[
|
|
||||||
reduce(lambda x, y: x & y, exit_long_conditions),
|
|
||||||
'sell'] = 1
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
return populate_sell_trend
|
|
@ -1,272 +0,0 @@
|
|||||||
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
|
||||||
# isort: skip_file
|
|
||||||
# --- Do not remove these libs ---
|
|
||||||
from functools import reduce
|
|
||||||
from typing import Any, Callable, Dict, List
|
|
||||||
|
|
||||||
import numpy as np # noqa
|
|
||||||
import pandas as pd # noqa
|
|
||||||
from pandas import DataFrame
|
|
||||||
from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal, Real # noqa
|
|
||||||
|
|
||||||
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
|
||||||
|
|
||||||
# --------------------------------
|
|
||||||
# Add your lib to import here
|
|
||||||
import talib.abstract as ta # noqa
|
|
||||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
|
||||||
|
|
||||||
|
|
||||||
class AdvancedSampleHyperOpt(IHyperOpt):
|
|
||||||
"""
|
|
||||||
This is a sample hyperopt to inspire you.
|
|
||||||
Feel free to customize it.
|
|
||||||
|
|
||||||
More information in the documentation: https://www.freqtrade.io/en/latest/hyperopt/
|
|
||||||
|
|
||||||
You should:
|
|
||||||
- Rename the class name to some unique name.
|
|
||||||
- Add any methods you want to build your hyperopt.
|
|
||||||
- Add any lib you need to build your hyperopt.
|
|
||||||
|
|
||||||
You must keep:
|
|
||||||
- The prototypes for the methods: populate_indicators, indicator_space, buy_strategy_generator.
|
|
||||||
|
|
||||||
The methods roi_space, generate_roi_table and stoploss_space are not required
|
|
||||||
and are provided by default.
|
|
||||||
However, you may override them if you need the
|
|
||||||
'roi' and the 'stoploss' spaces that differ from the defaults offered by Freqtrade.
|
|
||||||
|
|
||||||
This sample illustrates how to override these methods.
|
|
||||||
"""
|
|
||||||
@staticmethod
|
|
||||||
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
"""
|
|
||||||
This method can also be loaded from the strategy, if it doesn't exist in the hyperopt class.
|
|
||||||
"""
|
|
||||||
dataframe['adx'] = ta.ADX(dataframe)
|
|
||||||
macd = ta.MACD(dataframe)
|
|
||||||
dataframe['macd'] = macd['macd']
|
|
||||||
dataframe['macdsignal'] = macd['macdsignal']
|
|
||||||
dataframe['mfi'] = ta.MFI(dataframe)
|
|
||||||
dataframe['rsi'] = ta.RSI(dataframe)
|
|
||||||
stoch_fast = ta.STOCHF(dataframe)
|
|
||||||
dataframe['fastd'] = stoch_fast['fastd']
|
|
||||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
|
||||||
# Bollinger bands
|
|
||||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
|
||||||
dataframe['bb_lowerband'] = bollinger['lower']
|
|
||||||
dataframe['bb_upperband'] = bollinger['upper']
|
|
||||||
dataframe['sar'] = ta.SAR(dataframe)
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def indicator_space() -> List[Dimension]:
|
|
||||||
"""
|
|
||||||
Define your Hyperopt space for searching buy strategy parameters.
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
Integer(10, 25, name='mfi-value'),
|
|
||||||
Integer(15, 45, name='fastd-value'),
|
|
||||||
Integer(20, 50, name='adx-value'),
|
|
||||||
Integer(20, 40, name='rsi-value'),
|
|
||||||
Categorical([True, False], name='mfi-enabled'),
|
|
||||||
Categorical([True, False], name='fastd-enabled'),
|
|
||||||
Categorical([True, False], name='adx-enabled'),
|
|
||||||
Categorical([True, False], name='rsi-enabled'),
|
|
||||||
Categorical(['boll', 'macd_cross_signal', 'sar_reversal'], name='trigger')
|
|
||||||
]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
|
||||||
"""
|
|
||||||
Define the buy strategy parameters to be used by hyperopt
|
|
||||||
"""
|
|
||||||
def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Buy strategy Hyperopt will build and use
|
|
||||||
"""
|
|
||||||
long_conditions = []
|
|
||||||
# GUARDS AND TRENDS
|
|
||||||
if 'mfi-enabled' in params and params['mfi-enabled']:
|
|
||||||
long_conditions.append(dataframe['mfi'] < params['mfi-value'])
|
|
||||||
if 'fastd-enabled' in params and params['fastd-enabled']:
|
|
||||||
long_conditions.append(dataframe['fastd'] < params['fastd-value'])
|
|
||||||
if 'adx-enabled' in params and params['adx-enabled']:
|
|
||||||
long_conditions.append(dataframe['adx'] > params['adx-value'])
|
|
||||||
if 'rsi-enabled' in params and params['rsi-enabled']:
|
|
||||||
long_conditions.append(dataframe['rsi'] < params['rsi-value'])
|
|
||||||
|
|
||||||
# TRIGGERS
|
|
||||||
if 'trigger' in params:
|
|
||||||
if params['trigger'] == 'boll':
|
|
||||||
long_conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
|
|
||||||
if params['trigger'] == 'macd_cross_signal':
|
|
||||||
long_conditions.append(qtpylib.crossed_above(
|
|
||||||
dataframe['macd'], dataframe['macdsignal']
|
|
||||||
))
|
|
||||||
if params['trigger'] == 'sar_reversal':
|
|
||||||
long_conditions.append(qtpylib.crossed_above(
|
|
||||||
dataframe['close'], dataframe['sar']
|
|
||||||
))
|
|
||||||
|
|
||||||
# Check that volume is not 0
|
|
||||||
long_conditions.append(dataframe['volume'] > 0)
|
|
||||||
|
|
||||||
if long_conditions:
|
|
||||||
dataframe.loc[
|
|
||||||
reduce(lambda x, y: x & y, long_conditions),
|
|
||||||
'buy'] = 1
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
return populate_buy_trend
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def sell_indicator_space() -> List[Dimension]:
|
|
||||||
"""
|
|
||||||
Define your Hyperopt space for searching sell strategy parameters.
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
Integer(75, 100, name='sell-mfi-value'),
|
|
||||||
Integer(50, 100, name='sell-fastd-value'),
|
|
||||||
Integer(50, 100, name='sell-adx-value'),
|
|
||||||
Integer(60, 100, name='sell-rsi-value'),
|
|
||||||
Categorical([True, False], name='sell-mfi-enabled'),
|
|
||||||
Categorical([True, False], name='sell-fastd-enabled'),
|
|
||||||
Categorical([True, False], name='sell-adx-enabled'),
|
|
||||||
Categorical([True, False], name='sell-rsi-enabled'),
|
|
||||||
Categorical(['sell-boll',
|
|
||||||
'sell-macd_cross_signal',
|
|
||||||
'sell-sar_reversal'],
|
|
||||||
name='sell-trigger')
|
|
||||||
]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def sell_strategy_generator(params: Dict[str, Any]) -> Callable:
|
|
||||||
"""
|
|
||||||
Define the sell strategy parameters to be used by hyperopt
|
|
||||||
"""
|
|
||||||
def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Sell strategy Hyperopt will build and use
|
|
||||||
"""
|
|
||||||
# print(params)
|
|
||||||
exit_long_conditions = []
|
|
||||||
# GUARDS AND TRENDS
|
|
||||||
if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']:
|
|
||||||
exit_long_conditions.append(dataframe['mfi'] > params['sell-mfi-value'])
|
|
||||||
if 'sell-fastd-enabled' in params and params['sell-fastd-enabled']:
|
|
||||||
exit_long_conditions.append(dataframe['fastd'] > params['sell-fastd-value'])
|
|
||||||
if 'sell-adx-enabled' in params and params['sell-adx-enabled']:
|
|
||||||
exit_long_conditions.append(dataframe['adx'] < params['sell-adx-value'])
|
|
||||||
if 'sell-rsi-enabled' in params and params['sell-rsi-enabled']:
|
|
||||||
exit_long_conditions.append(dataframe['rsi'] > params['sell-rsi-value'])
|
|
||||||
|
|
||||||
# TRIGGERS
|
|
||||||
if 'sell-trigger' in params:
|
|
||||||
if params['sell-trigger'] == 'sell-boll':
|
|
||||||
exit_long_conditions.append(dataframe['close'] > dataframe['bb_upperband'])
|
|
||||||
if params['sell-trigger'] == 'sell-macd_cross_signal':
|
|
||||||
exit_long_conditions.append(qtpylib.crossed_above(
|
|
||||||
dataframe['macdsignal'],
|
|
||||||
dataframe['macd']
|
|
||||||
))
|
|
||||||
if params['sell-trigger'] == 'sell-sar_reversal':
|
|
||||||
exit_long_conditions.append(qtpylib.crossed_above(
|
|
||||||
dataframe['sar'],
|
|
||||||
dataframe['close']
|
|
||||||
))
|
|
||||||
|
|
||||||
# Check that volume is not 0
|
|
||||||
exit_long_conditions.append(dataframe['volume'] > 0)
|
|
||||||
|
|
||||||
if exit_long_conditions:
|
|
||||||
dataframe.loc[
|
|
||||||
reduce(lambda x, y: x & y, exit_long_conditions),
|
|
||||||
'sell'] = 1
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
return populate_sell_trend
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def generate_roi_table(params: Dict) -> Dict[int, float]:
|
|
||||||
"""
|
|
||||||
Generate the ROI table that will be used by Hyperopt
|
|
||||||
|
|
||||||
This implementation generates the default legacy Freqtrade ROI tables.
|
|
||||||
|
|
||||||
Change it if you need different number of steps in the generated
|
|
||||||
ROI tables or other structure of the ROI tables.
|
|
||||||
|
|
||||||
Please keep it aligned with parameters in the 'roi' optimization
|
|
||||||
hyperspace defined by the roi_space method.
|
|
||||||
"""
|
|
||||||
roi_table = {}
|
|
||||||
roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3']
|
|
||||||
roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2']
|
|
||||||
roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1']
|
|
||||||
roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0
|
|
||||||
|
|
||||||
return roi_table
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def roi_space() -> List[Dimension]:
|
|
||||||
"""
|
|
||||||
Values to search for each ROI steps
|
|
||||||
|
|
||||||
Override it if you need some different ranges for the parameters in the
|
|
||||||
'roi' optimization hyperspace.
|
|
||||||
|
|
||||||
Please keep it aligned with the implementation of the
|
|
||||||
generate_roi_table method.
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
Integer(10, 120, name='roi_t1'),
|
|
||||||
Integer(10, 60, name='roi_t2'),
|
|
||||||
Integer(10, 40, name='roi_t3'),
|
|
||||||
SKDecimal(0.01, 0.04, decimals=3, name='roi_p1'),
|
|
||||||
SKDecimal(0.01, 0.07, decimals=3, name='roi_p2'),
|
|
||||||
SKDecimal(0.01, 0.20, decimals=3, name='roi_p3'),
|
|
||||||
]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def stoploss_space() -> List[Dimension]:
|
|
||||||
"""
|
|
||||||
Stoploss Value to search
|
|
||||||
|
|
||||||
Override it if you need some different range for the parameter in the
|
|
||||||
'stoploss' optimization hyperspace.
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
SKDecimal(-0.35, -0.02, decimals=3, name='stoploss'),
|
|
||||||
]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def trailing_space() -> List[Dimension]:
|
|
||||||
"""
|
|
||||||
Create a trailing stoploss space.
|
|
||||||
|
|
||||||
You may override it in your custom Hyperopt class.
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
# It was decided to always set trailing_stop is to True if the 'trailing' hyperspace
|
|
||||||
# is used. Otherwise hyperopt will vary other parameters that won't have effect if
|
|
||||||
# trailing_stop is set False.
|
|
||||||
# This parameter is included into the hyperspace dimensions rather than assigning
|
|
||||||
# it explicitly in the code in order to have it printed in the results along with
|
|
||||||
# other 'trailing' hyperspace parameters.
|
|
||||||
Categorical([True], name='trailing_stop'),
|
|
||||||
|
|
||||||
SKDecimal(0.01, 0.35, decimals=3, name='trailing_stop_positive'),
|
|
||||||
|
|
||||||
# 'trailing_stop_positive_offset' should be greater than 'trailing_stop_positive',
|
|
||||||
# so this intermediate parameter is used as the value of the difference between
|
|
||||||
# them. The value of the 'trailing_stop_positive_offset' is constructed in the
|
|
||||||
# generate_trailing_params() method.
|
|
||||||
# This is similar to the hyperspace dimensions used for constructing the ROI tables.
|
|
||||||
SKDecimal(0.001, 0.1, decimals=3, name='trailing_stop_positive_offset_p1'),
|
|
||||||
|
|
||||||
Categorical([True, False], name='trailing_only_offset_is_reached'),
|
|
||||||
]
|
|
@ -8,34 +8,8 @@
|
|||||||
"rateLimit": 200
|
"rateLimit": 200
|
||||||
},
|
},
|
||||||
"pair_whitelist": [
|
"pair_whitelist": [
|
||||||
"ALGO/BTC",
|
|
||||||
"ATOM/BTC",
|
|
||||||
"BAT/BTC",
|
|
||||||
"BCH/BTC",
|
|
||||||
"BRD/BTC",
|
|
||||||
"EOS/BTC",
|
|
||||||
"ETH/BTC",
|
|
||||||
"IOTA/BTC",
|
|
||||||
"LINK/BTC",
|
|
||||||
"LTC/BTC",
|
|
||||||
"NEO/BTC",
|
|
||||||
"NXS/BTC",
|
|
||||||
"XMR/BTC",
|
|
||||||
"XRP/BTC",
|
|
||||||
"XTZ/BTC"
|
|
||||||
],
|
],
|
||||||
"pair_blacklist": [
|
"pair_blacklist": [
|
||||||
"BNB/BTC",
|
"BNB/.*"
|
||||||
"BNB/BUSD",
|
|
||||||
"BNB/ETH",
|
|
||||||
"BNB/EUR",
|
|
||||||
"BNB/NGN",
|
|
||||||
"BNB/PAX",
|
|
||||||
"BNB/RUB",
|
|
||||||
"BNB/TRY",
|
|
||||||
"BNB/TUSD",
|
|
||||||
"BNB/USDC",
|
|
||||||
"BNB/USDS",
|
|
||||||
"BNB/USDT"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -15,16 +15,6 @@
|
|||||||
"rateLimit": 500
|
"rateLimit": 500
|
||||||
},
|
},
|
||||||
"pair_whitelist": [
|
"pair_whitelist": [
|
||||||
"ETH/BTC",
|
|
||||||
"LTC/BTC",
|
|
||||||
"ETC/BTC",
|
|
||||||
"DASH/BTC",
|
|
||||||
"ZEC/BTC",
|
|
||||||
"XLM/BTC",
|
|
||||||
"XRP/BTC",
|
|
||||||
"TRX/BTC",
|
|
||||||
"ADA/BTC",
|
|
||||||
"XMR/BTC"
|
|
||||||
],
|
],
|
||||||
"pair_blacklist": [
|
"pair_blacklist": [
|
||||||
]
|
]
|
||||||
|
@ -7,28 +7,10 @@
|
|||||||
"ccxt_async_config": {
|
"ccxt_async_config": {
|
||||||
"enableRateLimit": true,
|
"enableRateLimit": true,
|
||||||
"rateLimit": 1000
|
"rateLimit": 1000
|
||||||
|
// Enable the below for downoading data.
|
||||||
|
//"rateLimit": 3100
|
||||||
},
|
},
|
||||||
"pair_whitelist": [
|
"pair_whitelist": [
|
||||||
"ADA/EUR",
|
|
||||||
"ATOM/EUR",
|
|
||||||
"BAT/EUR",
|
|
||||||
"BCH/EUR",
|
|
||||||
"BTC/EUR",
|
|
||||||
"DAI/EUR",
|
|
||||||
"DASH/EUR",
|
|
||||||
"EOS/EUR",
|
|
||||||
"ETC/EUR",
|
|
||||||
"ETH/EUR",
|
|
||||||
"LINK/EUR",
|
|
||||||
"LTC/EUR",
|
|
||||||
"QTUM/EUR",
|
|
||||||
"REP/EUR",
|
|
||||||
"WAVES/EUR",
|
|
||||||
"XLM/EUR",
|
|
||||||
"XMR/EUR",
|
|
||||||
"XRP/EUR",
|
|
||||||
"XTZ/EUR",
|
|
||||||
"ZEC/EUR"
|
|
||||||
],
|
],
|
||||||
"pair_blacklist": [
|
"pair_blacklist": [
|
||||||
|
|
||||||
|
18
freqtrade/templates/subtemplates/exchange_kucoin.j2
Normal file
18
freqtrade/templates/subtemplates/exchange_kucoin.j2
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
"exchange": {
|
||||||
|
"name": "{{ exchange_name | lower }}",
|
||||||
|
"key": "{{ exchange_key }}",
|
||||||
|
"secret": "{{ exchange_secret }}",
|
||||||
|
"password": "{{ exchange_key_password }}",
|
||||||
|
"ccxt_config": {
|
||||||
|
"enableRateLimit": true
|
||||||
|
"rateLimit": 200
|
||||||
|
},
|
||||||
|
"ccxt_async_config": {
|
||||||
|
"enableRateLimit": true,
|
||||||
|
"rateLimit": 200
|
||||||
|
},
|
||||||
|
"pair_whitelist": [
|
||||||
|
],
|
||||||
|
"pair_blacklist": [
|
||||||
|
]
|
||||||
|
}
|
@ -1,8 +0,0 @@
|
|||||||
if params.get('mfi-enabled'):
|
|
||||||
conditions.append(dataframe['mfi'] < params['mfi-value'])
|
|
||||||
if params.get('fastd-enabled'):
|
|
||||||
conditions.append(dataframe['fastd'] < params['fastd-value'])
|
|
||||||
if params.get('adx-enabled'):
|
|
||||||
conditions.append(dataframe['adx'] > params['adx-value'])
|
|
||||||
if params.get('rsi-enabled'):
|
|
||||||
conditions.append(dataframe['rsi'] < params['rsi-value'])
|
|
@ -1,2 +0,0 @@
|
|||||||
if params.get('rsi-enabled'):
|
|
||||||
conditions.append(dataframe['rsi'] < params['rsi-value'])
|
|
@ -1,9 +0,0 @@
|
|||||||
Integer(10, 25, name='mfi-value'),
|
|
||||||
Integer(15, 45, name='fastd-value'),
|
|
||||||
Integer(20, 50, name='adx-value'),
|
|
||||||
Integer(20, 40, name='rsi-value'),
|
|
||||||
Categorical([True, False], name='mfi-enabled'),
|
|
||||||
Categorical([True, False], name='fastd-enabled'),
|
|
||||||
Categorical([True, False], name='adx-enabled'),
|
|
||||||
Categorical([True, False], name='rsi-enabled'),
|
|
||||||
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')
|
|
@ -1,3 +0,0 @@
|
|||||||
Integer(20, 40, name='rsi-value'),
|
|
||||||
Categorical([True, False], name='rsi-enabled'),
|
|
||||||
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')
|
|
@ -1,8 +0,0 @@
|
|||||||
if params.get('sell-mfi-enabled'):
|
|
||||||
conditions.append(dataframe['mfi'] > params['sell-mfi-value'])
|
|
||||||
if params.get('sell-fastd-enabled'):
|
|
||||||
conditions.append(dataframe['fastd'] > params['sell-fastd-value'])
|
|
||||||
if params.get('sell-adx-enabled'):
|
|
||||||
conditions.append(dataframe['adx'] < params['sell-adx-value'])
|
|
||||||
if params.get('sell-rsi-enabled'):
|
|
||||||
conditions.append(dataframe['rsi'] > params['sell-rsi-value'])
|
|
@ -1,2 +0,0 @@
|
|||||||
if params.get('sell-rsi-enabled'):
|
|
||||||
conditions.append(dataframe['rsi'] > params['sell-rsi-value'])
|
|
@ -1,11 +0,0 @@
|
|||||||
Integer(75, 100, name='sell-mfi-value'),
|
|
||||||
Integer(50, 100, name='sell-fastd-value'),
|
|
||||||
Integer(50, 100, name='sell-adx-value'),
|
|
||||||
Integer(60, 100, name='sell-rsi-value'),
|
|
||||||
Categorical([True, False], name='sell-mfi-enabled'),
|
|
||||||
Categorical([True, False], name='sell-fastd-enabled'),
|
|
||||||
Categorical([True, False], name='sell-adx-enabled'),
|
|
||||||
Categorical([True, False], name='sell-rsi-enabled'),
|
|
||||||
Categorical(['sell-bb_upper',
|
|
||||||
'sell-macd_cross_signal',
|
|
||||||
'sell-sar_reversal'], name='sell-trigger')
|
|
@ -1,5 +0,0 @@
|
|||||||
Integer(60, 100, name='sell-rsi-value'),
|
|
||||||
Categorical([True, False], name='sell-rsi-enabled'),
|
|
||||||
Categorical(['sell-bb_upper',
|
|
||||||
'sell-macd_cross_signal',
|
|
||||||
'sell-sar_reversal'], name='sell-trigger')
|
|
72
mkdocs.yml
72
mkdocs.yml
@ -3,42 +3,42 @@ site_url: https://www.freqtrade.io/
|
|||||||
repo_url: https://github.com/freqtrade/freqtrade
|
repo_url: https://github.com/freqtrade/freqtrade
|
||||||
use_directory_urls: True
|
use_directory_urls: True
|
||||||
nav:
|
nav:
|
||||||
- Home: index.md
|
- Home: index.md
|
||||||
- Quickstart with Docker: docker_quickstart.md
|
- Quickstart with Docker: docker_quickstart.md
|
||||||
- Installation:
|
- Installation:
|
||||||
- Linux/MacOS/Raspberry: installation.md
|
- Linux/MacOS/Raspberry: installation.md
|
||||||
- Windows: windows_installation.md
|
- Windows: windows_installation.md
|
||||||
- Freqtrade Basics: bot-basics.md
|
- Freqtrade Basics: bot-basics.md
|
||||||
- Configuration: configuration.md
|
- Configuration: configuration.md
|
||||||
- Strategy Customization: strategy-customization.md
|
- Strategy Customization: strategy-customization.md
|
||||||
- Plugins: plugins.md
|
- Plugins: plugins.md
|
||||||
- Stoploss: stoploss.md
|
- Stoploss: stoploss.md
|
||||||
- Start the bot: bot-usage.md
|
- Start the bot: bot-usage.md
|
||||||
- Control the bot:
|
- Control the bot:
|
||||||
- Telegram: telegram-usage.md
|
- Telegram: telegram-usage.md
|
||||||
- REST API & FreqUI: rest-api.md
|
- REST API & FreqUI: rest-api.md
|
||||||
- Web Hook: webhook-config.md
|
- Web Hook: webhook-config.md
|
||||||
- Data Downloading: data-download.md
|
- Data Downloading: data-download.md
|
||||||
- Backtesting: backtesting.md
|
- Backtesting: backtesting.md
|
||||||
- Leverage: leverage.md
|
- Hyperopt: hyperopt.md
|
||||||
- Hyperopt: hyperopt.md
|
- Leverage: leverage.md
|
||||||
- Utility Sub-commands: utils.md
|
- Utility Sub-commands: utils.md
|
||||||
- Plotting: plotting.md
|
- Plotting: plotting.md
|
||||||
- Data Analysis:
|
- Exchange-specific Notes: exchanges.md
|
||||||
- Jupyter Notebooks: data-analysis.md
|
- Data Analysis:
|
||||||
- Strategy analysis: strategy_analysis_example.md
|
- Jupyter Notebooks: data-analysis.md
|
||||||
- Exchange-specific Notes: exchanges.md
|
- Strategy analysis: strategy_analysis_example.md
|
||||||
- Advanced Topics:
|
- Advanced Topics:
|
||||||
- Advanced Post-installation Tasks: advanced-setup.md
|
- Advanced Post-installation Tasks: advanced-setup.md
|
||||||
- Edge Positioning: edge.md
|
- Edge Positioning: edge.md
|
||||||
- Advanced Strategy: strategy-advanced.md
|
- Advanced Strategy: strategy-advanced.md
|
||||||
- Advanced Hyperopt: advanced-hyperopt.md
|
- Advanced Hyperopt: advanced-hyperopt.md
|
||||||
- Sandbox Testing: sandbox-testing.md
|
- Sandbox Testing: sandbox-testing.md
|
||||||
- FAQ: faq.md
|
- FAQ: faq.md
|
||||||
- SQL Cheat-sheet: sql_cheatsheet.md
|
- SQL Cheat-sheet: sql_cheatsheet.md
|
||||||
- Updating Freqtrade: updating.md
|
- Updating Freqtrade: updating.md
|
||||||
- Deprecated Features: deprecated.md
|
- Deprecated Features: deprecated.md
|
||||||
- Contributors Guide: developer.md
|
- Contributors Guide: developer.md
|
||||||
theme:
|
theme:
|
||||||
name: material
|
name: material
|
||||||
logo: "images/logo.png"
|
logo: "images/logo.png"
|
||||||
|
@ -8,7 +8,7 @@ flake8==3.9.2
|
|||||||
flake8-type-annotations==0.1.0
|
flake8-type-annotations==0.1.0
|
||||||
flake8-tidy-imports==4.4.1
|
flake8-tidy-imports==4.4.1
|
||||||
mypy==0.910
|
mypy==0.910
|
||||||
pytest==6.2.4
|
pytest==6.2.5
|
||||||
pytest-asyncio==0.15.1
|
pytest-asyncio==0.15.1
|
||||||
pytest-cov==2.12.1
|
pytest-cov==2.12.1
|
||||||
pytest-mock==3.6.1
|
pytest-mock==3.6.1
|
||||||
|
@ -8,4 +8,4 @@ scikit-optimize==0.8.1
|
|||||||
filelock==3.0.12
|
filelock==3.0.12
|
||||||
joblib==1.0.1
|
joblib==1.0.1
|
||||||
psutil==5.8.0
|
psutil==5.8.0
|
||||||
progressbar2==3.53.1
|
progressbar2==3.53.2
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Include all requirements to run the bot.
|
# Include all requirements to run the bot.
|
||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
plotly==5.3.0
|
plotly==5.3.1
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
numpy==1.21.2
|
numpy==1.21.2
|
||||||
pandas==1.3.2
|
pandas==1.3.3
|
||||||
|
|
||||||
ccxt==1.55.56
|
ccxt==1.56.30
|
||||||
# Pin cryptography for now due to rust build errors with piwheels
|
# Pin cryptography for now due to rust build errors with piwheels
|
||||||
cryptography==3.4.8
|
cryptography==3.4.8
|
||||||
aiohttp==3.7.4.post0
|
aiohttp==3.7.4.post0
|
||||||
|
14
setup.sh
14
setup.sh
@ -95,19 +95,7 @@ function install_talib() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cd build_helpers
|
cd build_helpers && ./install_ta-lib.sh && cd ..
|
||||||
tar zxvf ta-lib-0.4.0-src.tar.gz
|
|
||||||
cd ta-lib
|
|
||||||
sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h
|
|
||||||
./configure --prefix=/usr/local
|
|
||||||
make
|
|
||||||
sudo make install
|
|
||||||
if [ -x "$(command -v apt-get)" ]; then
|
|
||||||
echo "Updating library path using ldconfig"
|
|
||||||
sudo ldconfig
|
|
||||||
fi
|
|
||||||
cd .. && rm -rf ./ta-lib/
|
|
||||||
cd ..
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function install_mac_newer_python_dependencies() {
|
function install_mac_newer_python_dependencies() {
|
||||||
|
@ -10,10 +10,10 @@ import pytest
|
|||||||
|
|
||||||
from freqtrade.commands import (start_convert_data, start_create_userdir, start_download_data,
|
from freqtrade.commands import (start_convert_data, start_create_userdir, start_download_data,
|
||||||
start_hyperopt_list, start_hyperopt_show, start_install_ui,
|
start_hyperopt_list, start_hyperopt_show, start_install_ui,
|
||||||
start_list_data, start_list_exchanges, start_list_hyperopts,
|
start_list_data, start_list_exchanges, start_list_markets,
|
||||||
start_list_markets, start_list_strategies, start_list_timeframes,
|
start_list_strategies, start_list_timeframes, start_new_strategy,
|
||||||
start_new_hyperopt, start_new_strategy, start_show_trades,
|
start_show_trades, start_test_pairlist, start_trading,
|
||||||
start_test_pairlist, start_trading, start_webserver)
|
start_webserver)
|
||||||
from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_install_ui,
|
from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_install_ui,
|
||||||
get_ui_download_url, read_ui_version)
|
get_ui_download_url, read_ui_version)
|
||||||
from freqtrade.configuration import setup_utils_configuration
|
from freqtrade.configuration import setup_utils_configuration
|
||||||
@ -32,8 +32,6 @@ def test_setup_utils_configuration():
|
|||||||
config = setup_utils_configuration(get_args(args), RunMode.OTHER)
|
config = setup_utils_configuration(get_args(args), RunMode.OTHER)
|
||||||
assert "exchange" in config
|
assert "exchange" in config
|
||||||
assert config['dry_run'] is True
|
assert config['dry_run'] is True
|
||||||
assert config['exchange']['key'] == ''
|
|
||||||
assert config['exchange']['secret'] == ''
|
|
||||||
|
|
||||||
|
|
||||||
def test_start_trading_fail(mocker, caplog):
|
def test_start_trading_fail(mocker, caplog):
|
||||||
@ -519,37 +517,6 @@ def test_start_new_strategy_no_arg(mocker, caplog):
|
|||||||
start_new_strategy(get_args(args))
|
start_new_strategy(get_args(args))
|
||||||
|
|
||||||
|
|
||||||
def test_start_new_hyperopt(mocker, caplog):
|
|
||||||
wt_mock = mocker.patch.object(Path, "write_text", MagicMock())
|
|
||||||
mocker.patch.object(Path, "exists", MagicMock(return_value=False))
|
|
||||||
|
|
||||||
args = [
|
|
||||||
"new-hyperopt",
|
|
||||||
"--hyperopt",
|
|
||||||
"CoolNewhyperopt"
|
|
||||||
]
|
|
||||||
start_new_hyperopt(get_args(args))
|
|
||||||
|
|
||||||
assert wt_mock.call_count == 1
|
|
||||||
assert "CoolNewhyperopt" in wt_mock.call_args_list[0][0][0]
|
|
||||||
assert log_has_re("Writing hyperopt to .*", caplog)
|
|
||||||
|
|
||||||
mocker.patch('freqtrade.commands.deploy_commands.setup_utils_configuration')
|
|
||||||
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
|
|
||||||
with pytest.raises(OperationalException,
|
|
||||||
match=r".* already exists. Please choose another Hyperopt Name\."):
|
|
||||||
start_new_hyperopt(get_args(args))
|
|
||||||
|
|
||||||
|
|
||||||
def test_start_new_hyperopt_no_arg(mocker):
|
|
||||||
args = [
|
|
||||||
"new-hyperopt",
|
|
||||||
]
|
|
||||||
with pytest.raises(OperationalException,
|
|
||||||
match="`new-hyperopt` requires --hyperopt to be set."):
|
|
||||||
start_new_hyperopt(get_args(args))
|
|
||||||
|
|
||||||
|
|
||||||
def test_start_install_ui(mocker):
|
def test_start_install_ui(mocker):
|
||||||
clean_mock = mocker.patch('freqtrade.commands.deploy_commands.clean_ui_subdir')
|
clean_mock = mocker.patch('freqtrade.commands.deploy_commands.clean_ui_subdir')
|
||||||
get_url_mock = mocker.patch('freqtrade.commands.deploy_commands.get_ui_download_url',
|
get_url_mock = mocker.patch('freqtrade.commands.deploy_commands.get_ui_download_url',
|
||||||
@ -824,37 +791,20 @@ def test_start_list_strategies(mocker, caplog, capsys):
|
|||||||
assert "legacy_strategy_v1.py" in captured.out
|
assert "legacy_strategy_v1.py" in captured.out
|
||||||
assert "StrategyTestV2" in captured.out
|
assert "StrategyTestV2" in captured.out
|
||||||
|
|
||||||
|
# Test color output
|
||||||
def test_start_list_hyperopts(mocker, caplog, capsys):
|
|
||||||
|
|
||||||
args = [
|
args = [
|
||||||
"list-hyperopts",
|
"list-strategies",
|
||||||
"--hyperopt-path",
|
"--strategy-path",
|
||||||
str(Path(__file__).parent.parent / "optimize" / "hyperopts"),
|
str(Path(__file__).parent.parent / "strategy" / "strats"),
|
||||||
"-1"
|
|
||||||
]
|
]
|
||||||
pargs = get_args(args)
|
pargs = get_args(args)
|
||||||
# pargs['config'] = None
|
# pargs['config'] = None
|
||||||
start_list_hyperopts(pargs)
|
start_list_strategies(pargs)
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert "TestHyperoptLegacy" not in captured.out
|
assert "TestStrategyLegacyV1" in captured.out
|
||||||
assert "legacy_hyperopt.py" not in captured.out
|
assert "legacy_strategy_v1.py" in captured.out
|
||||||
assert "HyperoptTestSepFile" in captured.out
|
assert "StrategyTestV2" in captured.out
|
||||||
assert "test_hyperopt.py" not in captured.out
|
assert "LOAD FAILED" in captured.out
|
||||||
|
|
||||||
# Test regular output
|
|
||||||
args = [
|
|
||||||
"list-hyperopts",
|
|
||||||
"--hyperopt-path",
|
|
||||||
str(Path(__file__).parent.parent / "optimize" / "hyperopts"),
|
|
||||||
]
|
|
||||||
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 "HyperoptTestSepFile" in captured.out
|
|
||||||
|
|
||||||
|
|
||||||
def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
|
def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from datetime import datetime, timezone
|
||||||
from random import randint
|
from random import randint
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
@ -5,7 +6,7 @@ import ccxt
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
|
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
|
||||||
from tests.conftest import get_patched_exchange
|
from tests.conftest import get_mock_coro, get_patched_exchange, log_has_re
|
||||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||||
|
|
||||||
|
|
||||||
@ -105,3 +106,35 @@ def test_stoploss_adjust_binance(mocker, default_conf):
|
|||||||
# Test with invalid order case
|
# Test with invalid order case
|
||||||
order['type'] = 'stop_loss'
|
order['type'] = 'stop_loss'
|
||||||
assert not exchange.stoploss_adjust(1501, order)
|
assert not exchange.stoploss_adjust(1501, order)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog):
|
||||||
|
ohlcv = [
|
||||||
|
[
|
||||||
|
int((datetime.now(timezone.utc).timestamp() - 1000) * 1000),
|
||||||
|
1, # open
|
||||||
|
2, # high
|
||||||
|
3, # low
|
||||||
|
4, # close
|
||||||
|
5, # volume (in quote currency)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id='binance')
|
||||||
|
# Monkey-patch async function
|
||||||
|
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
|
||||||
|
|
||||||
|
pair = 'ETH/BTC'
|
||||||
|
res = await exchange._async_get_historic_ohlcv(pair, "5m",
|
||||||
|
1500000000000, is_new_pair=False)
|
||||||
|
# Call with very old timestamp - causes tons of requests
|
||||||
|
assert exchange._api_async.fetch_ohlcv.call_count > 400
|
||||||
|
# assert res == ohlcv
|
||||||
|
exchange._api_async.fetch_ohlcv.reset_mock()
|
||||||
|
res = await exchange._async_get_historic_ohlcv(pair, "5m", 1500000000000, is_new_pair=True)
|
||||||
|
|
||||||
|
# Called twice - one "init" call - and one to get the actual data.
|
||||||
|
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
||||||
|
assert res == ohlcv
|
||||||
|
assert log_has_re(r"Candle-data for ETH/BTC available starting with .*", caplog)
|
||||||
|
@ -54,6 +54,8 @@ EXCHANGES = {
|
|||||||
def exchange_conf():
|
def exchange_conf():
|
||||||
config = get_default_conf((Path(__file__).parent / "testdata").resolve())
|
config = get_default_conf((Path(__file__).parent / "testdata").resolve())
|
||||||
config['exchange']['pair_whitelist'] = []
|
config['exchange']['pair_whitelist'] = []
|
||||||
|
config['exchange']['key'] = ''
|
||||||
|
config['exchange']['secret'] = ''
|
||||||
config['dry_run'] = False
|
config['dry_run'] = False
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
|
from copy import deepcopy
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from math import isclose
|
from math import isclose
|
||||||
from random import randint
|
from random import randint
|
||||||
@ -14,7 +15,7 @@ from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOr
|
|||||||
OperationalException, PricingError, TemporaryError)
|
OperationalException, PricingError, TemporaryError)
|
||||||
from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken
|
from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken
|
||||||
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT,
|
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT,
|
||||||
calculate_backoff)
|
calculate_backoff, remove_credentials)
|
||||||
from freqtrade.exchange.exchange import (market_is_active, timeframe_to_minutes, timeframe_to_msecs,
|
from freqtrade.exchange.exchange import (market_is_active, timeframe_to_minutes, timeframe_to_msecs,
|
||||||
timeframe_to_next_date, timeframe_to_prev_date,
|
timeframe_to_next_date, timeframe_to_prev_date,
|
||||||
timeframe_to_seconds)
|
timeframe_to_seconds)
|
||||||
@ -78,6 +79,22 @@ def test_init(default_conf, mocker, caplog):
|
|||||||
assert log_has('Instance is running with dry_run enabled', caplog)
|
assert log_has('Instance is running with dry_run enabled', caplog)
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_credentials(default_conf, caplog) -> None:
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf['dry_run'] = False
|
||||||
|
remove_credentials(conf)
|
||||||
|
|
||||||
|
assert conf['exchange']['key'] != ''
|
||||||
|
assert conf['exchange']['secret'] != ''
|
||||||
|
|
||||||
|
conf['dry_run'] = True
|
||||||
|
remove_credentials(conf)
|
||||||
|
assert conf['exchange']['key'] == ''
|
||||||
|
assert conf['exchange']['secret'] == ''
|
||||||
|
assert conf['exchange']['password'] == ''
|
||||||
|
assert conf['exchange']['uid'] == ''
|
||||||
|
|
||||||
|
|
||||||
def test_init_ccxt_kwargs(default_conf, mocker, caplog):
|
def test_init_ccxt_kwargs(default_conf, mocker, caplog):
|
||||||
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||||
@ -108,6 +125,13 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog):
|
|||||||
assert hasattr(ex._api_async, 'TestKWARG')
|
assert hasattr(ex._api_async, 'TestKWARG')
|
||||||
assert log_has("Applying additional ccxt config: {'TestKWARG': 11, 'TestKWARG44': 11}", caplog)
|
assert log_has("Applying additional ccxt config: {'TestKWARG': 11, 'TestKWARG44': 11}", caplog)
|
||||||
assert log_has(asynclogmsg, caplog)
|
assert log_has(asynclogmsg, caplog)
|
||||||
|
# Test additional headers case
|
||||||
|
Exchange._headers = {'hello': 'world'}
|
||||||
|
ex = Exchange(conf)
|
||||||
|
|
||||||
|
assert log_has("Applying additional ccxt config: {'TestKWARG': 11, 'TestKWARG44': 11}", caplog)
|
||||||
|
assert ex._api.headers == {'hello': 'world'}
|
||||||
|
Exchange._headers = {}
|
||||||
|
|
||||||
|
|
||||||
def test_destroy(default_conf, mocker, caplog):
|
def test_destroy(default_conf, mocker, caplog):
|
||||||
@ -178,7 +202,7 @@ def test_exchange_resolver(default_conf, mocker, caplog):
|
|||||||
|
|
||||||
def test_validate_order_time_in_force(default_conf, mocker, caplog):
|
def test_validate_order_time_in_force(default_conf, mocker, caplog):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
# explicitly test bittrex, exchanges implementing other policies need seperate tests
|
# explicitly test bittrex, exchanges implementing other policies need separate tests
|
||||||
ex = get_patched_exchange(mocker, default_conf, id="bittrex")
|
ex = get_patched_exchange(mocker, default_conf, id="bittrex")
|
||||||
tif = {
|
tif = {
|
||||||
"buy": "gtc",
|
"buy": "gtc",
|
||||||
@ -1544,6 +1568,32 @@ def test_get_historic_ohlcv_as_df(default_conf, mocker, exchange_name):
|
|||||||
assert 'high' in ret.columns
|
assert 'high' in ret.columns
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
|
async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name):
|
||||||
|
ohlcv = [
|
||||||
|
[
|
||||||
|
int((datetime.now(timezone.utc).timestamp() - 1000) * 1000),
|
||||||
|
1, # open
|
||||||
|
2, # high
|
||||||
|
3, # low
|
||||||
|
4, # close
|
||||||
|
5, # volume (in quote currency)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||||
|
# Monkey-patch async function
|
||||||
|
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
|
||||||
|
|
||||||
|
pair = 'ETH/USDT'
|
||||||
|
res = await exchange._async_get_historic_ohlcv(pair, "5m",
|
||||||
|
1500000000000, is_new_pair=False)
|
||||||
|
# Call with very old timestamp - causes tons of requests
|
||||||
|
assert exchange._api_async.fetch_ohlcv.call_count > 200
|
||||||
|
assert res[0] == ohlcv[0]
|
||||||
|
assert log_has_re(r'Downloaded data for .* with length .*\.', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
|
def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
|
||||||
ohlcv = [
|
ohlcv = [
|
||||||
[
|
[
|
||||||
@ -2431,7 +2481,7 @@ def test_fetch_order(default_conf, mocker, exchange_name, caplog):
|
|||||||
|
|
||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
def test_fetch_stoploss_order(default_conf, mocker, exchange_name):
|
def test_fetch_stoploss_order(default_conf, mocker, exchange_name):
|
||||||
# Don't test FTX here - that needs a seperate test
|
# Don't test FTX here - that needs a separate test
|
||||||
if exchange_name == 'ftx':
|
if exchange_name == 'ftx':
|
||||||
return
|
return
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
|
@ -16,7 +16,7 @@ def hyperopt_conf(default_conf):
|
|||||||
hyperconf.update({
|
hyperconf.update({
|
||||||
'datadir': Path(default_conf['datadir']),
|
'datadir': Path(default_conf['datadir']),
|
||||||
'runmode': RunMode.HYPEROPT,
|
'runmode': RunMode.HYPEROPT,
|
||||||
'hyperopt': 'HyperoptTestSepFile',
|
'strategy': 'HyperoptableStrategy',
|
||||||
'hyperopt_loss': 'ShortTradeDurHyperOptLoss',
|
'hyperopt_loss': 'ShortTradeDurHyperOptLoss',
|
||||||
'hyperopt_path': str(Path(__file__).parent / 'hyperopts'),
|
'hyperopt_path': str(Path(__file__).parent / 'hyperopts'),
|
||||||
'epochs': 1,
|
'epochs': 1,
|
||||||
|
@ -1,207 +0,0 @@
|
|||||||
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
|
||||||
|
|
||||||
from functools import reduce
|
|
||||||
from typing import Any, Callable, Dict, List
|
|
||||||
|
|
||||||
import talib.abstract as ta
|
|
||||||
from pandas import DataFrame
|
|
||||||
from skopt.space import Categorical, Dimension, Integer
|
|
||||||
|
|
||||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
|
||||||
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
|
||||||
|
|
||||||
|
|
||||||
class HyperoptTestSepFile(IHyperOpt):
|
|
||||||
"""
|
|
||||||
Default hyperopt provided by the Freqtrade bot.
|
|
||||||
You can override it with your own Hyperopt
|
|
||||||
"""
|
|
||||||
@staticmethod
|
|
||||||
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Add several indicators needed for buy and sell strategies defined below.
|
|
||||||
"""
|
|
||||||
# ADX
|
|
||||||
dataframe['adx'] = ta.ADX(dataframe)
|
|
||||||
# MACD
|
|
||||||
macd = ta.MACD(dataframe)
|
|
||||||
dataframe['macd'] = macd['macd']
|
|
||||||
dataframe['macdsignal'] = macd['macdsignal']
|
|
||||||
# MFI
|
|
||||||
dataframe['mfi'] = ta.MFI(dataframe)
|
|
||||||
# RSI
|
|
||||||
dataframe['rsi'] = ta.RSI(dataframe)
|
|
||||||
# Stochastic Fast
|
|
||||||
stoch_fast = ta.STOCHF(dataframe)
|
|
||||||
dataframe['fastd'] = stoch_fast['fastd']
|
|
||||||
# Minus-DI
|
|
||||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
|
||||||
# Bollinger bands
|
|
||||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
|
||||||
dataframe['bb_lowerband'] = bollinger['lower']
|
|
||||||
dataframe['bb_upperband'] = bollinger['upper']
|
|
||||||
# SAR
|
|
||||||
dataframe['sar'] = ta.SAR(dataframe)
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
|
||||||
"""
|
|
||||||
Define the buy strategy parameters to be used by Hyperopt.
|
|
||||||
"""
|
|
||||||
def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Buy strategy Hyperopt will build and use.
|
|
||||||
"""
|
|
||||||
conditions = []
|
|
||||||
|
|
||||||
# GUARDS AND TRENDS
|
|
||||||
if 'mfi-enabled' in params and params['mfi-enabled']:
|
|
||||||
conditions.append(dataframe['mfi'] < params['mfi-value'])
|
|
||||||
if 'fastd-enabled' in params and params['fastd-enabled']:
|
|
||||||
conditions.append(dataframe['fastd'] < params['fastd-value'])
|
|
||||||
if 'adx-enabled' in params and params['adx-enabled']:
|
|
||||||
conditions.append(dataframe['adx'] > params['adx-value'])
|
|
||||||
if 'rsi-enabled' in params and params['rsi-enabled']:
|
|
||||||
conditions.append(dataframe['rsi'] < params['rsi-value'])
|
|
||||||
|
|
||||||
# TRIGGERS
|
|
||||||
if 'trigger' in params:
|
|
||||||
if params['trigger'] == 'boll':
|
|
||||||
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
|
|
||||||
if params['trigger'] == 'macd_cross_signal':
|
|
||||||
conditions.append(qtpylib.crossed_above(
|
|
||||||
dataframe['macd'],
|
|
||||||
dataframe['macdsignal']
|
|
||||||
))
|
|
||||||
if params['trigger'] == 'sar_reversal':
|
|
||||||
conditions.append(qtpylib.crossed_above(
|
|
||||||
dataframe['close'],
|
|
||||||
dataframe['sar']
|
|
||||||
))
|
|
||||||
|
|
||||||
if conditions:
|
|
||||||
dataframe.loc[
|
|
||||||
reduce(lambda x, y: x & y, conditions),
|
|
||||||
'buy'] = 1
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
return populate_buy_trend
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def indicator_space() -> List[Dimension]:
|
|
||||||
"""
|
|
||||||
Define your Hyperopt space for searching buy strategy parameters.
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
Integer(10, 25, name='mfi-value'),
|
|
||||||
Integer(15, 45, name='fastd-value'),
|
|
||||||
Integer(20, 50, name='adx-value'),
|
|
||||||
Integer(20, 40, name='rsi-value'),
|
|
||||||
Categorical([True, False], name='mfi-enabled'),
|
|
||||||
Categorical([True, False], name='fastd-enabled'),
|
|
||||||
Categorical([True, False], name='adx-enabled'),
|
|
||||||
Categorical([True, False], name='rsi-enabled'),
|
|
||||||
Categorical(['boll', 'macd_cross_signal', 'sar_reversal'], name='trigger')
|
|
||||||
]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def sell_strategy_generator(params: Dict[str, Any]) -> Callable:
|
|
||||||
"""
|
|
||||||
Define the sell strategy parameters to be used by Hyperopt.
|
|
||||||
"""
|
|
||||||
def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Sell strategy Hyperopt will build and use.
|
|
||||||
"""
|
|
||||||
conditions = []
|
|
||||||
|
|
||||||
# GUARDS AND TRENDS
|
|
||||||
if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']:
|
|
||||||
conditions.append(dataframe['mfi'] > params['sell-mfi-value'])
|
|
||||||
if 'sell-fastd-enabled' in params and params['sell-fastd-enabled']:
|
|
||||||
conditions.append(dataframe['fastd'] > params['sell-fastd-value'])
|
|
||||||
if 'sell-adx-enabled' in params and params['sell-adx-enabled']:
|
|
||||||
conditions.append(dataframe['adx'] < params['sell-adx-value'])
|
|
||||||
if 'sell-rsi-enabled' in params and params['sell-rsi-enabled']:
|
|
||||||
conditions.append(dataframe['rsi'] > params['sell-rsi-value'])
|
|
||||||
|
|
||||||
# TRIGGERS
|
|
||||||
if 'sell-trigger' in params:
|
|
||||||
if params['sell-trigger'] == 'sell-boll':
|
|
||||||
conditions.append(dataframe['close'] > dataframe['bb_upperband'])
|
|
||||||
if params['sell-trigger'] == 'sell-macd_cross_signal':
|
|
||||||
conditions.append(qtpylib.crossed_above(
|
|
||||||
dataframe['macdsignal'],
|
|
||||||
dataframe['macd']
|
|
||||||
))
|
|
||||||
if params['sell-trigger'] == 'sell-sar_reversal':
|
|
||||||
conditions.append(qtpylib.crossed_above(
|
|
||||||
dataframe['sar'],
|
|
||||||
dataframe['close']
|
|
||||||
))
|
|
||||||
|
|
||||||
if conditions:
|
|
||||||
dataframe.loc[
|
|
||||||
reduce(lambda x, y: x & y, conditions),
|
|
||||||
'sell'] = 1
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
return populate_sell_trend
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def sell_indicator_space() -> List[Dimension]:
|
|
||||||
"""
|
|
||||||
Define your Hyperopt space for searching sell strategy parameters.
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
Integer(75, 100, name='sell-mfi-value'),
|
|
||||||
Integer(50, 100, name='sell-fastd-value'),
|
|
||||||
Integer(50, 100, name='sell-adx-value'),
|
|
||||||
Integer(60, 100, name='sell-rsi-value'),
|
|
||||||
Categorical([True, False], name='sell-mfi-enabled'),
|
|
||||||
Categorical([True, False], name='sell-fastd-enabled'),
|
|
||||||
Categorical([True, False], name='sell-adx-enabled'),
|
|
||||||
Categorical([True, False], name='sell-rsi-enabled'),
|
|
||||||
Categorical(['sell-boll',
|
|
||||||
'sell-macd_cross_signal',
|
|
||||||
'sell-sar_reversal'],
|
|
||||||
name='sell-trigger')
|
|
||||||
]
|
|
||||||
|
|
||||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Based on TA indicators. Should be a copy of same method from strategy.
|
|
||||||
Must align to populate_indicators in this file.
|
|
||||||
Only used when --spaces does not include buy space.
|
|
||||||
"""
|
|
||||||
dataframe.loc[
|
|
||||||
(
|
|
||||||
(dataframe['close'] < dataframe['bb_lowerband']) &
|
|
||||||
(dataframe['mfi'] < 16) &
|
|
||||||
(dataframe['adx'] > 25) &
|
|
||||||
(dataframe['rsi'] < 21)
|
|
||||||
),
|
|
||||||
'buy'] = 1
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Based on TA indicators. Should be a copy of same method from strategy.
|
|
||||||
Must align to populate_indicators in this file.
|
|
||||||
Only used when --spaces does not include sell space.
|
|
||||||
"""
|
|
||||||
dataframe.loc[
|
|
||||||
(
|
|
||||||
(qtpylib.crossed_above(
|
|
||||||
dataframe['macdsignal'], dataframe['macd']
|
|
||||||
)) &
|
|
||||||
(dataframe['fastd'] > 54)
|
|
||||||
),
|
|
||||||
'sell'] = 1
|
|
||||||
|
|
||||||
return dataframe
|
|
@ -17,13 +17,10 @@ from freqtrade.optimize.hyperopt_auto import HyperOptAuto
|
|||||||
from freqtrade.optimize.hyperopt_tools import HyperoptTools
|
from freqtrade.optimize.hyperopt_tools import HyperoptTools
|
||||||
from freqtrade.optimize.optimize_reports import generate_strategy_stats
|
from freqtrade.optimize.optimize_reports import generate_strategy_stats
|
||||||
from freqtrade.optimize.space import SKDecimal
|
from freqtrade.optimize.space import SKDecimal
|
||||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver
|
|
||||||
from freqtrade.strategy.hyper import IntParameter
|
from freqtrade.strategy.hyper import IntParameter
|
||||||
from tests.conftest import (get_args, log_has, log_has_re, patch_exchange,
|
from tests.conftest import (get_args, log_has, log_has_re, patch_exchange,
|
||||||
patched_configuration_load_config_file)
|
patched_configuration_load_config_file)
|
||||||
|
|
||||||
from .hyperopts.hyperopt_test_sep_file import HyperoptTestSepFile
|
|
||||||
|
|
||||||
|
|
||||||
def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
||||||
patched_configuration_load_config_file(mocker, default_conf)
|
patched_configuration_load_config_file(mocker, default_conf)
|
||||||
@ -31,7 +28,7 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca
|
|||||||
args = [
|
args = [
|
||||||
'hyperopt',
|
'hyperopt',
|
||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
'--hyperopt', 'HyperoptTestSepFile',
|
'--strategy', 'HyperoptableStrategy',
|
||||||
]
|
]
|
||||||
|
|
||||||
config = setup_optimize_configuration(get_args(args), RunMode.HYPEROPT)
|
config = setup_optimize_configuration(get_args(args), RunMode.HYPEROPT)
|
||||||
@ -63,7 +60,7 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo
|
|||||||
args = [
|
args = [
|
||||||
'hyperopt',
|
'hyperopt',
|
||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
'--hyperopt', 'HyperoptTestSepFile',
|
'--strategy', 'HyperoptableStrategy',
|
||||||
'--datadir', '/foo/bar',
|
'--datadir', '/foo/bar',
|
||||||
'--timeframe', '1m',
|
'--timeframe', '1m',
|
||||||
'--timerange', ':100',
|
'--timerange', ':100',
|
||||||
@ -115,7 +112,7 @@ def test_setup_hyperopt_configuration_stake_amount(mocker, default_conf) -> None
|
|||||||
args = [
|
args = [
|
||||||
'hyperopt',
|
'hyperopt',
|
||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
'--hyperopt', 'HyperoptTestSepFile',
|
'--strategy', 'HyperoptableStrategy',
|
||||||
'--stake-amount', '1',
|
'--stake-amount', '1',
|
||||||
'--starting-balance', '2'
|
'--starting-balance', '2'
|
||||||
]
|
]
|
||||||
@ -133,47 +130,6 @@ def test_setup_hyperopt_configuration_stake_amount(mocker, default_conf) -> None
|
|||||||
setup_optimize_configuration(get_args(args), RunMode.HYPEROPT)
|
setup_optimize_configuration(get_args(args), RunMode.HYPEROPT)
|
||||||
|
|
||||||
|
|
||||||
def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
|
|
||||||
patched_configuration_load_config_file(mocker, default_conf)
|
|
||||||
|
|
||||||
hyperopt = HyperoptTestSepFile
|
|
||||||
delattr(hyperopt, 'populate_indicators')
|
|
||||||
delattr(hyperopt, 'populate_buy_trend')
|
|
||||||
delattr(hyperopt, 'populate_sell_trend')
|
|
||||||
mocker.patch(
|
|
||||||
'freqtrade.resolvers.hyperopt_resolver.HyperOptResolver.load_object',
|
|
||||||
MagicMock(return_value=hyperopt(default_conf))
|
|
||||||
)
|
|
||||||
default_conf.update({'hyperopt': 'HyperoptTestSepFile'})
|
|
||||||
x = HyperOptResolver.load_hyperopt(default_conf)
|
|
||||||
assert not hasattr(x, 'populate_indicators')
|
|
||||||
assert not hasattr(x, 'populate_buy_trend')
|
|
||||||
assert not hasattr(x, 'populate_sell_trend')
|
|
||||||
assert log_has("Hyperopt class does not provide populate_indicators() method. "
|
|
||||||
"Using populate_indicators from the strategy.", caplog)
|
|
||||||
assert log_has("Hyperopt class does not provide populate_sell_trend() method. "
|
|
||||||
"Using populate_sell_trend from the strategy.", caplog)
|
|
||||||
assert log_has("Hyperopt class does not provide populate_buy_trend() method. "
|
|
||||||
"Using populate_buy_trend from the strategy.", caplog)
|
|
||||||
assert hasattr(x, "ticker_interval") # DEPRECATED
|
|
||||||
assert hasattr(x, "timeframe")
|
|
||||||
|
|
||||||
|
|
||||||
def test_hyperoptresolver_wrongname(default_conf) -> None:
|
|
||||||
default_conf.update({'hyperopt': "NonExistingHyperoptClass"})
|
|
||||||
|
|
||||||
with pytest.raises(OperationalException, match=r'Impossible to load Hyperopt.*'):
|
|
||||||
HyperOptResolver.load_hyperopt(default_conf)
|
|
||||||
|
|
||||||
|
|
||||||
def test_hyperoptresolver_noname(default_conf):
|
|
||||||
default_conf['hyperopt'] = ''
|
|
||||||
with pytest.raises(OperationalException,
|
|
||||||
match="No Hyperopt set. Please use `--hyperopt` to specify "
|
|
||||||
"the Hyperopt class to use."):
|
|
||||||
HyperOptResolver.load_hyperopt(default_conf)
|
|
||||||
|
|
||||||
|
|
||||||
def test_start_not_installed(mocker, default_conf, import_fails) -> None:
|
def test_start_not_installed(mocker, default_conf, import_fails) -> None:
|
||||||
start_mock = MagicMock()
|
start_mock = MagicMock()
|
||||||
patched_configuration_load_config_file(mocker, default_conf)
|
patched_configuration_load_config_file(mocker, default_conf)
|
||||||
@ -184,9 +140,7 @@ def test_start_not_installed(mocker, default_conf, import_fails) -> None:
|
|||||||
args = [
|
args = [
|
||||||
'hyperopt',
|
'hyperopt',
|
||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
'--hyperopt', 'HyperoptTestSepFile',
|
'--strategy', 'HyperoptableStrategy',
|
||||||
'--hyperopt-path',
|
|
||||||
str(Path(__file__).parent / "hyperopts"),
|
|
||||||
'--epochs', '5',
|
'--epochs', '5',
|
||||||
'--hyperopt-loss', 'SharpeHyperOptLossDaily',
|
'--hyperopt-loss', 'SharpeHyperOptLossDaily',
|
||||||
]
|
]
|
||||||
@ -196,7 +150,7 @@ def test_start_not_installed(mocker, default_conf, import_fails) -> None:
|
|||||||
start_hyperopt(pargs)
|
start_hyperopt(pargs)
|
||||||
|
|
||||||
|
|
||||||
def test_start(mocker, hyperopt_conf, caplog) -> None:
|
def test_start_no_hyperopt_allowed(mocker, hyperopt_conf, caplog) -> None:
|
||||||
start_mock = MagicMock()
|
start_mock = MagicMock()
|
||||||
patched_configuration_load_config_file(mocker, hyperopt_conf)
|
patched_configuration_load_config_file(mocker, hyperopt_conf)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
|
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
|
||||||
@ -210,10 +164,8 @@ def test_start(mocker, hyperopt_conf, caplog) -> None:
|
|||||||
'--epochs', '5'
|
'--epochs', '5'
|
||||||
]
|
]
|
||||||
pargs = get_args(args)
|
pargs = get_args(args)
|
||||||
start_hyperopt(pargs)
|
with pytest.raises(OperationalException, match=r"Using separate Hyperopt files has been.*"):
|
||||||
|
start_hyperopt(pargs)
|
||||||
assert log_has('Starting freqtrade in Hyperopt mode', caplog)
|
|
||||||
assert start_mock.call_count == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_start_no_data(mocker, hyperopt_conf) -> None:
|
def test_start_no_data(mocker, hyperopt_conf) -> None:
|
||||||
@ -225,11 +177,11 @@ def test_start_no_data(mocker, hyperopt_conf) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
# TODO: migrate to strategy-based hyperopt
|
||||||
args = [
|
args = [
|
||||||
'hyperopt',
|
'hyperopt',
|
||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
'--hyperopt', 'HyperoptTestSepFile',
|
'--strategy', 'HyperoptableStrategy',
|
||||||
'--hyperopt-loss', 'SharpeHyperOptLossDaily',
|
'--hyperopt-loss', 'SharpeHyperOptLossDaily',
|
||||||
'--epochs', '5'
|
'--epochs', '5'
|
||||||
]
|
]
|
||||||
@ -247,7 +199,7 @@ def test_start_filelock(mocker, hyperopt_conf, caplog) -> None:
|
|||||||
args = [
|
args = [
|
||||||
'hyperopt',
|
'hyperopt',
|
||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
'--hyperopt', 'HyperoptTestSepFile',
|
'--strategy', 'HyperoptableStrategy',
|
||||||
'--hyperopt-loss', 'SharpeHyperOptLossDaily',
|
'--hyperopt-loss', 'SharpeHyperOptLossDaily',
|
||||||
'--epochs', '5'
|
'--epochs', '5'
|
||||||
]
|
]
|
||||||
@ -427,66 +379,14 @@ def test_hyperopt_format_results(hyperopt):
|
|||||||
def test_populate_indicators(hyperopt, testdatadir) -> None:
|
def test_populate_indicators(hyperopt, testdatadir) -> None:
|
||||||
data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True)
|
data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True)
|
||||||
dataframes = hyperopt.backtesting.strategy.advise_all_indicators(data)
|
dataframes = hyperopt.backtesting.strategy.advise_all_indicators(data)
|
||||||
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
|
dataframe = dataframes['UNITTEST/BTC']
|
||||||
{'pair': 'UNITTEST/BTC'})
|
|
||||||
|
|
||||||
# Check if some indicators are generated. We will not test all of them
|
# Check if some indicators are generated. We will not test all of them
|
||||||
assert 'adx' in dataframe
|
assert 'adx' in dataframe
|
||||||
assert 'mfi' in dataframe
|
assert 'macd' in dataframe
|
||||||
assert 'rsi' in dataframe
|
assert 'rsi' in dataframe
|
||||||
|
|
||||||
|
|
||||||
def test_buy_strategy_generator(hyperopt, testdatadir) -> None:
|
|
||||||
data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True)
|
|
||||||
dataframes = hyperopt.backtesting.strategy.advise_all_indicators(data)
|
|
||||||
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
|
|
||||||
{'pair': 'UNITTEST/BTC'})
|
|
||||||
|
|
||||||
populate_buy_trend = hyperopt.custom_hyperopt.buy_strategy_generator(
|
|
||||||
{
|
|
||||||
'adx-value': 20,
|
|
||||||
'fastd-value': 20,
|
|
||||||
'mfi-value': 20,
|
|
||||||
'rsi-value': 20,
|
|
||||||
'adx-enabled': True,
|
|
||||||
'fastd-enabled': True,
|
|
||||||
'mfi-enabled': True,
|
|
||||||
'rsi-enabled': True,
|
|
||||||
'trigger': 'bb_lower'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
result = populate_buy_trend(dataframe, {'pair': 'UNITTEST/BTC'})
|
|
||||||
# Check if some indicators are generated. We will not test all of them
|
|
||||||
assert 'buy' in result
|
|
||||||
assert 1 in result['buy']
|
|
||||||
|
|
||||||
|
|
||||||
def test_sell_strategy_generator(hyperopt, testdatadir) -> None:
|
|
||||||
data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True)
|
|
||||||
dataframes = hyperopt.backtesting.strategy.advise_all_indicators(data)
|
|
||||||
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
|
|
||||||
{'pair': 'UNITTEST/BTC'})
|
|
||||||
|
|
||||||
populate_sell_trend = hyperopt.custom_hyperopt.sell_strategy_generator(
|
|
||||||
{
|
|
||||||
'sell-adx-value': 20,
|
|
||||||
'sell-fastd-value': 75,
|
|
||||||
'sell-mfi-value': 80,
|
|
||||||
'sell-rsi-value': 20,
|
|
||||||
'sell-adx-enabled': True,
|
|
||||||
'sell-fastd-enabled': True,
|
|
||||||
'sell-mfi-enabled': True,
|
|
||||||
'sell-rsi-enabled': True,
|
|
||||||
'sell-trigger': 'sell-bb_upper'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
result = populate_sell_trend(dataframe, {'pair': 'UNITTEST/BTC'})
|
|
||||||
# Check if some indicators are generated. We will not test all of them
|
|
||||||
print(result)
|
|
||||||
assert 'sell' in result
|
|
||||||
assert 1 in result['sell']
|
|
||||||
|
|
||||||
|
|
||||||
def test_generate_optimizer(mocker, hyperopt_conf) -> None:
|
def test_generate_optimizer(mocker, hyperopt_conf) -> None:
|
||||||
hyperopt_conf.update({'spaces': 'all',
|
hyperopt_conf.update({'spaces': 'all',
|
||||||
'hyperopt_min_trades': 1,
|
'hyperopt_min_trades': 1,
|
||||||
@ -527,24 +427,12 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
|
|||||||
mocker.patch('freqtrade.optimize.hyperopt.load', return_value={'XRP/BTC': None})
|
mocker.patch('freqtrade.optimize.hyperopt.load', return_value={'XRP/BTC': None})
|
||||||
|
|
||||||
optimizer_param = {
|
optimizer_param = {
|
||||||
'adx-value': 0,
|
'buy_plusdi': 0.02,
|
||||||
'fastd-value': 35,
|
'buy_rsi': 35,
|
||||||
'mfi-value': 0,
|
'sell_minusdi': 0.02,
|
||||||
'rsi-value': 0,
|
'sell_rsi': 75,
|
||||||
'adx-enabled': False,
|
'protection_cooldown_lookback': 20,
|
||||||
'fastd-enabled': True,
|
'protection_enabled': True,
|
||||||
'mfi-enabled': False,
|
|
||||||
'rsi-enabled': False,
|
|
||||||
'trigger': 'macd_cross_signal',
|
|
||||||
'sell-adx-value': 0,
|
|
||||||
'sell-fastd-value': 75,
|
|
||||||
'sell-mfi-value': 0,
|
|
||||||
'sell-rsi-value': 0,
|
|
||||||
'sell-adx-enabled': False,
|
|
||||||
'sell-fastd-enabled': True,
|
|
||||||
'sell-mfi-enabled': False,
|
|
||||||
'sell-rsi-enabled': False,
|
|
||||||
'sell-trigger': 'macd_cross_signal',
|
|
||||||
'roi_t1': 60.0,
|
'roi_t1': 60.0,
|
||||||
'roi_t2': 30.0,
|
'roi_t2': 30.0,
|
||||||
'roi_t3': 20.0,
|
'roi_t3': 20.0,
|
||||||
@ -564,29 +452,19 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
|
|||||||
'0.00003100 BTC ( 0.00%). '
|
'0.00003100 BTC ( 0.00%). '
|
||||||
'Avg duration 0:50:00 min.'
|
'Avg duration 0:50:00 min.'
|
||||||
),
|
),
|
||||||
'params_details': {'buy': {'adx-enabled': False,
|
'params_details': {'buy': {'buy_plusdi': 0.02,
|
||||||
'adx-value': 0,
|
'buy_rsi': 35,
|
||||||
'fastd-enabled': True,
|
},
|
||||||
'fastd-value': 35,
|
|
||||||
'mfi-enabled': False,
|
|
||||||
'mfi-value': 0,
|
|
||||||
'rsi-enabled': False,
|
|
||||||
'rsi-value': 0,
|
|
||||||
'trigger': 'macd_cross_signal'},
|
|
||||||
'roi': {"0": 0.12000000000000001,
|
'roi': {"0": 0.12000000000000001,
|
||||||
"20.0": 0.02,
|
"20.0": 0.02,
|
||||||
"50.0": 0.01,
|
"50.0": 0.01,
|
||||||
"110.0": 0},
|
"110.0": 0},
|
||||||
'protection': {},
|
'protection': {'protection_cooldown_lookback': 20,
|
||||||
'sell': {'sell-adx-enabled': False,
|
'protection_enabled': True,
|
||||||
'sell-adx-value': 0,
|
},
|
||||||
'sell-fastd-enabled': True,
|
'sell': {'sell_minusdi': 0.02,
|
||||||
'sell-fastd-value': 75,
|
'sell_rsi': 75,
|
||||||
'sell-mfi-enabled': False,
|
},
|
||||||
'sell-mfi-value': 0,
|
|
||||||
'sell-rsi-enabled': False,
|
|
||||||
'sell-rsi-value': 0,
|
|
||||||
'sell-trigger': 'macd_cross_signal'},
|
|
||||||
'stoploss': {'stoploss': -0.4},
|
'stoploss': {'stoploss': -0.4},
|
||||||
'trailing': {'trailing_only_offset_is_reached': False,
|
'trailing': {'trailing_only_offset_is_reached': False,
|
||||||
'trailing_stop': True,
|
'trailing_stop': True,
|
||||||
@ -808,11 +686,6 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non
|
|||||||
hyperopt.backtesting.strategy.advise_all_indicators = MagicMock()
|
hyperopt.backtesting.strategy.advise_all_indicators = MagicMock()
|
||||||
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
|
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
|
||||||
|
|
||||||
del hyperopt.custom_hyperopt.__class__.buy_strategy_generator
|
|
||||||
del hyperopt.custom_hyperopt.__class__.sell_strategy_generator
|
|
||||||
del hyperopt.custom_hyperopt.__class__.indicator_space
|
|
||||||
del hyperopt.custom_hyperopt.__class__.sell_indicator_space
|
|
||||||
|
|
||||||
hyperopt.start()
|
hyperopt.start()
|
||||||
|
|
||||||
parallel.assert_called_once()
|
parallel.assert_called_once()
|
||||||
@ -843,16 +716,14 @@ def test_simplified_interface_all_failed(mocker, hyperopt_conf) -> None:
|
|||||||
|
|
||||||
hyperopt_conf.update({'spaces': 'all', })
|
hyperopt_conf.update({'spaces': 'all', })
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt_auto.HyperOptAuto._generate_indicator_space',
|
||||||
|
return_value=[])
|
||||||
|
|
||||||
hyperopt = Hyperopt(hyperopt_conf)
|
hyperopt = Hyperopt(hyperopt_conf)
|
||||||
hyperopt.backtesting.strategy.advise_all_indicators = MagicMock()
|
hyperopt.backtesting.strategy.advise_all_indicators = MagicMock()
|
||||||
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
|
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
|
||||||
|
|
||||||
del hyperopt.custom_hyperopt.__class__.buy_strategy_generator
|
with pytest.raises(OperationalException, match=r"The 'protection' space is included into *"):
|
||||||
del hyperopt.custom_hyperopt.__class__.sell_strategy_generator
|
|
||||||
del hyperopt.custom_hyperopt.__class__.indicator_space
|
|
||||||
del hyperopt.custom_hyperopt.__class__.sell_indicator_space
|
|
||||||
|
|
||||||
with pytest.raises(OperationalException, match=r"The 'buy' space is included into *"):
|
|
||||||
hyperopt.start()
|
hyperopt.start()
|
||||||
|
|
||||||
|
|
||||||
@ -889,11 +760,6 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:
|
|||||||
hyperopt.backtesting.strategy.advise_all_indicators = MagicMock()
|
hyperopt.backtesting.strategy.advise_all_indicators = MagicMock()
|
||||||
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
|
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
|
||||||
|
|
||||||
# TODO: sell_strategy_generator() is actually not called because
|
|
||||||
# run_optimizer_parallel() is mocked
|
|
||||||
del hyperopt.custom_hyperopt.__class__.sell_strategy_generator
|
|
||||||
del hyperopt.custom_hyperopt.__class__.sell_indicator_space
|
|
||||||
|
|
||||||
hyperopt.start()
|
hyperopt.start()
|
||||||
|
|
||||||
parallel.assert_called_once()
|
parallel.assert_called_once()
|
||||||
@ -943,11 +809,6 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None:
|
|||||||
hyperopt.backtesting.strategy.advise_all_indicators = MagicMock()
|
hyperopt.backtesting.strategy.advise_all_indicators = MagicMock()
|
||||||
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
|
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
|
||||||
|
|
||||||
# TODO: buy_strategy_generator() is actually not called because
|
|
||||||
# run_optimizer_parallel() is mocked
|
|
||||||
del hyperopt.custom_hyperopt.__class__.buy_strategy_generator
|
|
||||||
del hyperopt.custom_hyperopt.__class__.indicator_space
|
|
||||||
|
|
||||||
hyperopt.start()
|
hyperopt.start()
|
||||||
|
|
||||||
parallel.assert_called_once()
|
parallel.assert_called_once()
|
||||||
@ -964,13 +825,12 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None:
|
|||||||
assert hasattr(hyperopt, "position_stacking")
|
assert hasattr(hyperopt, "position_stacking")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("method,space", [
|
@pytest.mark.parametrize("space", [
|
||||||
('buy_strategy_generator', 'buy'),
|
('buy'),
|
||||||
('indicator_space', 'buy'),
|
('sell'),
|
||||||
('sell_strategy_generator', 'sell'),
|
('protection'),
|
||||||
('sell_indicator_space', 'sell'),
|
|
||||||
])
|
])
|
||||||
def test_simplified_interface_failed(mocker, hyperopt_conf, method, space) -> None:
|
def test_simplified_interface_failed(mocker, hyperopt_conf, space) -> None:
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
|
mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
|
mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
|
||||||
@ -979,6 +839,8 @@ def test_simplified_interface_failed(mocker, hyperopt_conf, method, space) -> No
|
|||||||
'freqtrade.optimize.hyperopt.get_timerange',
|
'freqtrade.optimize.hyperopt.get_timerange',
|
||||||
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
|
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
|
||||||
)
|
)
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt_auto.HyperOptAuto._generate_indicator_space',
|
||||||
|
return_value=[])
|
||||||
|
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
|
||||||
@ -988,8 +850,6 @@ def test_simplified_interface_failed(mocker, hyperopt_conf, method, space) -> No
|
|||||||
hyperopt.backtesting.strategy.advise_all_indicators = MagicMock()
|
hyperopt.backtesting.strategy.advise_all_indicators = MagicMock()
|
||||||
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
|
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
|
||||||
|
|
||||||
delattr(hyperopt.custom_hyperopt.__class__, method)
|
|
||||||
|
|
||||||
with pytest.raises(OperationalException, match=f"The '{space}' space is included into *"):
|
with pytest.raises(OperationalException, match=f"The '{space}' space is included into *"):
|
||||||
hyperopt.start()
|
hyperopt.start()
|
||||||
|
|
||||||
@ -999,7 +859,6 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
|
|||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
(Path(tmpdir) / 'hyperopt_results').mkdir(parents=True)
|
(Path(tmpdir) / 'hyperopt_results').mkdir(parents=True)
|
||||||
# No hyperopt needed
|
# No hyperopt needed
|
||||||
del hyperopt_conf['hyperopt']
|
|
||||||
hyperopt_conf.update({
|
hyperopt_conf.update({
|
||||||
'strategy': 'HyperoptableStrategy',
|
'strategy': 'HyperoptableStrategy',
|
||||||
'user_data_dir': Path(tmpdir),
|
'user_data_dir': Path(tmpdir),
|
||||||
|
@ -68,7 +68,7 @@ def test_PairLocks(use_db):
|
|||||||
# Global lock
|
# Global lock
|
||||||
PairLocks.lock_pair('*', lock_time)
|
PairLocks.lock_pair('*', lock_time)
|
||||||
assert PairLocks.is_global_lock(lock_time + timedelta(minutes=-50))
|
assert PairLocks.is_global_lock(lock_time + timedelta(minutes=-50))
|
||||||
# Global lock also locks every pair seperately
|
# Global lock also locks every pair separately
|
||||||
assert PairLocks.is_pair_locked(pair, lock_time + timedelta(minutes=-50))
|
assert PairLocks.is_pair_locked(pair, lock_time + timedelta(minutes=-50))
|
||||||
assert PairLocks.is_pair_locked('XRP/USDT', lock_time + timedelta(minutes=-50))
|
assert PairLocks.is_pair_locked('XRP/USDT', lock_time + timedelta(minutes=-50))
|
||||||
|
|
||||||
|
@ -739,11 +739,16 @@ def test_auto_hyperopt_interface(default_conf):
|
|||||||
PairLocks.timeframe = default_conf['timeframe']
|
PairLocks.timeframe = default_conf['timeframe']
|
||||||
strategy = StrategyResolver.load_strategy(default_conf)
|
strategy = StrategyResolver.load_strategy(default_conf)
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
next(strategy.enumerate_parameters('deadBeef'))
|
||||||
|
|
||||||
assert strategy.buy_rsi.value == strategy.buy_params['buy_rsi']
|
assert strategy.buy_rsi.value == strategy.buy_params['buy_rsi']
|
||||||
# PlusDI is NOT in the buy-params, so default should be used
|
# PlusDI is NOT in the buy-params, so default should be used
|
||||||
assert strategy.buy_plusdi.value == 0.5
|
assert strategy.buy_plusdi.value == 0.5
|
||||||
assert strategy.sell_rsi.value == strategy.sell_params['sell_rsi']
|
assert strategy.sell_rsi.value == strategy.sell_params['sell_rsi']
|
||||||
|
|
||||||
|
assert repr(strategy.sell_rsi) == 'IntParameter(74)'
|
||||||
|
|
||||||
# Parameter is disabled - so value from sell_param dict will NOT be used.
|
# Parameter is disabled - so value from sell_param dict will NOT be used.
|
||||||
assert strategy.sell_minusdi.value == 0.5
|
assert strategy.sell_minusdi.value == 0.5
|
||||||
all_params = strategy.detect_all_parameters()
|
all_params = strategy.detect_all_parameters()
|
||||||
|
@ -11,8 +11,7 @@ import pytest
|
|||||||
from jsonschema import ValidationError
|
from jsonschema import ValidationError
|
||||||
|
|
||||||
from freqtrade.commands import Arguments
|
from freqtrade.commands import Arguments
|
||||||
from freqtrade.configuration import (Configuration, check_exchange, remove_credentials,
|
from freqtrade.configuration import Configuration, check_exchange, validate_config_consistency
|
||||||
validate_config_consistency)
|
|
||||||
from freqtrade.configuration.config_validation import validate_config_schema
|
from freqtrade.configuration.config_validation import validate_config_schema
|
||||||
from freqtrade.configuration.deprecated_settings import (check_conflicting_settings,
|
from freqtrade.configuration.deprecated_settings import (check_conflicting_settings,
|
||||||
process_deprecated_setting,
|
process_deprecated_setting,
|
||||||
@ -617,18 +616,6 @@ def test_check_exchange(default_conf, caplog) -> None:
|
|||||||
check_exchange(default_conf)
|
check_exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
def test_remove_credentials(default_conf, caplog) -> None:
|
|
||||||
conf = deepcopy(default_conf)
|
|
||||||
conf['dry_run'] = False
|
|
||||||
remove_credentials(conf)
|
|
||||||
|
|
||||||
assert conf['dry_run'] is True
|
|
||||||
assert conf['exchange']['key'] == ''
|
|
||||||
assert conf['exchange']['secret'] == ''
|
|
||||||
assert conf['exchange']['password'] == ''
|
|
||||||
assert conf['exchange']['uid'] == ''
|
|
||||||
|
|
||||||
|
|
||||||
def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None:
|
def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None:
|
||||||
patched_configuration_load_config_file(mocker, default_conf)
|
patched_configuration_load_config_file(mocker, default_conf)
|
||||||
|
|
||||||
|
@ -74,16 +74,12 @@ def test_copy_sample_files(mocker, default_conf, caplog) -> None:
|
|||||||
copymock = mocker.patch('shutil.copy', MagicMock())
|
copymock = mocker.patch('shutil.copy', MagicMock())
|
||||||
|
|
||||||
copy_sample_files(Path('/tmp/bar'))
|
copy_sample_files(Path('/tmp/bar'))
|
||||||
assert copymock.call_count == 5
|
assert copymock.call_count == 3
|
||||||
assert copymock.call_args_list[0][0][1] == str(
|
assert copymock.call_args_list[0][0][1] == str(
|
||||||
Path('/tmp/bar') / 'strategies/sample_strategy.py')
|
Path('/tmp/bar') / 'strategies/sample_strategy.py')
|
||||||
assert copymock.call_args_list[1][0][1] == str(
|
assert copymock.call_args_list[1][0][1] == str(
|
||||||
Path('/tmp/bar') / 'hyperopts/sample_hyperopt_advanced.py')
|
|
||||||
assert copymock.call_args_list[2][0][1] == str(
|
|
||||||
Path('/tmp/bar') / 'hyperopts/sample_hyperopt_loss.py')
|
Path('/tmp/bar') / 'hyperopts/sample_hyperopt_loss.py')
|
||||||
assert copymock.call_args_list[3][0][1] == str(
|
assert copymock.call_args_list[2][0][1] == str(
|
||||||
Path('/tmp/bar') / 'hyperopts/sample_hyperopt.py')
|
|
||||||
assert copymock.call_args_list[4][0][1] == str(
|
|
||||||
Path('/tmp/bar') / 'notebooks/strategy_analysis_example.ipynb')
|
Path('/tmp/bar') / 'notebooks/strategy_analysis_example.ipynb')
|
||||||
|
|
||||||
|
|
||||||
|
@ -518,6 +518,7 @@ def test_enter_positions_global_pairlock(default_conf, ticker, limit_buy_order,
|
|||||||
# 0 trades, but it's not because of pairlock.
|
# 0 trades, but it's not because of pairlock.
|
||||||
assert n == 0
|
assert n == 0
|
||||||
assert not log_has_re(message, caplog)
|
assert not log_has_re(message, caplog)
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
PairLocks.lock_pair('*', arrow.utcnow().shift(minutes=20).datetime, 'Just because')
|
PairLocks.lock_pair('*', arrow.utcnow().shift(minutes=20).datetime, 'Just because')
|
||||||
n = freqtrade.enter_positions()
|
n = freqtrade.enter_positions()
|
||||||
@ -1086,6 +1087,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
|||||||
assert log_has_re(r'STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.', caplog)
|
assert log_has_re(r'STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.', caplog)
|
||||||
assert trade.stoploss_order_id is None
|
assert trade.stoploss_order_id is None
|
||||||
assert trade.is_open is False
|
assert trade.is_open is False
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.exchange.Binance.stoploss',
|
'freqtrade.exchange.Binance.stoploss',
|
||||||
@ -1190,7 +1192,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee,
|
|||||||
assert trade.stoploss_order_id is None
|
assert trade.stoploss_order_id is None
|
||||||
assert trade.sell_reason == SellType.EMERGENCY_SELL.value
|
assert trade.sell_reason == SellType.EMERGENCY_SELL.value
|
||||||
assert log_has("Unable to place a stoploss order on exchange. ", caplog)
|
assert log_has("Unable to place a stoploss order on exchange. ", caplog)
|
||||||
assert log_has("Selling the trade forcefully", caplog)
|
assert log_has("Exiting the trade forcefully", caplog)
|
||||||
|
|
||||||
# Should call a market sell
|
# Should call a market sell
|
||||||
assert create_order_mock.call_count == 2
|
assert create_order_mock.call_count == 2
|
||||||
@ -1659,7 +1661,7 @@ def test_enter_positions(mocker, default_conf, caplog) -> None:
|
|||||||
MagicMock(return_value=False))
|
MagicMock(return_value=False))
|
||||||
n = freqtrade.enter_positions()
|
n = freqtrade.enter_positions()
|
||||||
assert n == 0
|
assert n == 0
|
||||||
assert log_has('Found no buy signals for whitelisted currencies. Trying again...', caplog)
|
assert log_has('Found no enter signals for whitelisted currencies. Trying again...', caplog)
|
||||||
# create_trade should be called once for every pair in the whitelist.
|
# create_trade should be called once for every pair in the whitelist.
|
||||||
assert mock_ct.call_count == len(default_conf['exchange']['pair_whitelist'])
|
assert mock_ct.call_count == len(default_conf['exchange']['pair_whitelist'])
|
||||||
|
|
||||||
@ -1720,7 +1722,7 @@ def test_exit_positions_exception(mocker, default_conf, limit_buy_order, caplog)
|
|||||||
)
|
)
|
||||||
n = freqtrade.exit_positions(trades)
|
n = freqtrade.exit_positions(trades)
|
||||||
assert n == 0
|
assert n == 0
|
||||||
assert log_has('Unable to sell trade ETH/BTC: ', caplog)
|
assert log_has('Unable to exit trade ETH/BTC: ', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> None:
|
def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> None:
|
||||||
@ -1743,10 +1745,12 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No
|
|||||||
)
|
)
|
||||||
assert not freqtrade.update_trade_state(trade, None)
|
assert not freqtrade.update_trade_state(trade, None)
|
||||||
assert log_has_re(r'Orderid for trade .* is empty.', caplog)
|
assert log_has_re(r'Orderid for trade .* is empty.', caplog)
|
||||||
|
caplog.clear()
|
||||||
# Add datetime explicitly since sqlalchemy defaults apply only once written to database
|
# Add datetime explicitly since sqlalchemy defaults apply only once written to database
|
||||||
freqtrade.update_trade_state(trade, '123')
|
freqtrade.update_trade_state(trade, '123')
|
||||||
# Test amount not modified by fee-logic
|
# Test amount not modified by fee-logic
|
||||||
assert not log_has_re(r'Applying fee to .*', caplog)
|
assert not log_has_re(r'Applying fee to .*', caplog)
|
||||||
|
caplog.clear()
|
||||||
assert trade.open_order_id is None
|
assert trade.open_order_id is None
|
||||||
assert trade.amount == limit_buy_order['amount']
|
assert trade.amount == limit_buy_order['amount']
|
||||||
|
|
||||||
@ -2453,8 +2457,8 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke
|
|||||||
|
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.FreqtradeBot',
|
'freqtrade.freqtradebot.FreqtradeBot',
|
||||||
handle_cancel_buy=MagicMock(),
|
handle_cancel_enter=MagicMock(),
|
||||||
handle_cancel_sell=MagicMock(),
|
handle_cancel_exit=MagicMock(),
|
||||||
)
|
)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
@ -2475,7 +2479,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke
|
|||||||
caplog)
|
caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> None:
|
def test_handle_cancel_enter(mocker, caplog, default_conf, limit_buy_order) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
cancel_buy_order = deepcopy(limit_buy_order)
|
cancel_buy_order = deepcopy(limit_buy_order)
|
||||||
@ -2486,7 +2490,7 @@ def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> Non
|
|||||||
mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock)
|
mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock)
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
freqtrade._notify_buy_cancel = MagicMock()
|
freqtrade._notify_enter_cancel = MagicMock()
|
||||||
|
|
||||||
trade = MagicMock()
|
trade = MagicMock()
|
||||||
trade.pair = 'LTC/USDT'
|
trade.pair = 'LTC/USDT'
|
||||||
@ -2494,46 +2498,46 @@ def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> Non
|
|||||||
limit_buy_order['filled'] = 0.0
|
limit_buy_order['filled'] = 0.0
|
||||||
limit_buy_order['status'] = 'open'
|
limit_buy_order['status'] = 'open'
|
||||||
reason = CANCEL_REASON['TIMEOUT']
|
reason = CANCEL_REASON['TIMEOUT']
|
||||||
assert freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
|
assert freqtrade.handle_cancel_enter(trade, limit_buy_order, reason)
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
|
|
||||||
cancel_order_mock.reset_mock()
|
cancel_order_mock.reset_mock()
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
limit_buy_order['filled'] = 0.01
|
limit_buy_order['filled'] = 0.01
|
||||||
assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
|
assert not freqtrade.handle_cancel_enter(trade, limit_buy_order, reason)
|
||||||
assert cancel_order_mock.call_count == 0
|
assert cancel_order_mock.call_count == 0
|
||||||
assert log_has_re("Order .* for .* not cancelled, as the filled amount.* unsellable.*", caplog)
|
assert log_has_re("Order .* for .* not cancelled, as the filled amount.* unsellable.*", caplog)
|
||||||
|
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
cancel_order_mock.reset_mock()
|
cancel_order_mock.reset_mock()
|
||||||
limit_buy_order['filled'] = 2
|
limit_buy_order['filled'] = 2
|
||||||
assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
|
assert not freqtrade.handle_cancel_enter(trade, limit_buy_order, reason)
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
|
|
||||||
# Order remained open for some reason (cancel failed)
|
# Order remained open for some reason (cancel failed)
|
||||||
cancel_buy_order['status'] = 'open'
|
cancel_buy_order['status'] = 'open'
|
||||||
cancel_order_mock = MagicMock(return_value=cancel_buy_order)
|
cancel_order_mock = MagicMock(return_value=cancel_buy_order)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock)
|
mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock)
|
||||||
assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
|
assert not freqtrade.handle_cancel_enter(trade, limit_buy_order, reason)
|
||||||
assert log_has_re(r"Order .* for .* not cancelled.", caplog)
|
assert log_has_re(r"Order .* for .* not cancelled.", caplog)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'ftx', 'kraken', 'bittrex'],
|
@pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'ftx', 'kraken', 'bittrex'],
|
||||||
indirect=['limit_buy_order_canceled_empty'])
|
indirect=['limit_buy_order_canceled_empty'])
|
||||||
def test_handle_cancel_buy_exchanges(mocker, caplog, default_conf,
|
def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf,
|
||||||
limit_buy_order_canceled_empty) -> None:
|
limit_buy_order_canceled_empty) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
cancel_order_mock = mocker.patch(
|
cancel_order_mock = mocker.patch(
|
||||||
'freqtrade.exchange.Exchange.cancel_order_with_result',
|
'freqtrade.exchange.Exchange.cancel_order_with_result',
|
||||||
return_value=limit_buy_order_canceled_empty)
|
return_value=limit_buy_order_canceled_empty)
|
||||||
nofiy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot._notify_buy_cancel')
|
nofiy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot._notify_enter_cancel')
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
|
||||||
reason = CANCEL_REASON['TIMEOUT']
|
reason = CANCEL_REASON['TIMEOUT']
|
||||||
trade = MagicMock()
|
trade = MagicMock()
|
||||||
trade.pair = 'LTC/ETH'
|
trade.pair = 'LTC/ETH'
|
||||||
assert freqtrade.handle_cancel_buy(trade, limit_buy_order_canceled_empty, reason)
|
assert freqtrade.handle_cancel_enter(trade, limit_buy_order_canceled_empty, reason)
|
||||||
assert cancel_order_mock.call_count == 0
|
assert cancel_order_mock.call_count == 0
|
||||||
assert log_has_re(r'Buy order fully cancelled. Removing .* from database\.', caplog)
|
assert log_has_re(r'Buy order fully cancelled. Removing .* from database\.', caplog)
|
||||||
assert nofiy_mock.call_count == 1
|
assert nofiy_mock.call_count == 1
|
||||||
@ -2545,8 +2549,8 @@ def test_handle_cancel_buy_exchanges(mocker, caplog, default_conf,
|
|||||||
'String Return value',
|
'String Return value',
|
||||||
123
|
123
|
||||||
])
|
])
|
||||||
def test_handle_cancel_buy_corder_empty(mocker, default_conf, limit_buy_order,
|
def test_handle_cancel_enter_corder_empty(mocker, default_conf, limit_buy_order,
|
||||||
cancelorder) -> None:
|
cancelorder) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
cancel_order_mock = MagicMock(return_value=cancelorder)
|
cancel_order_mock = MagicMock(return_value=cancelorder)
|
||||||
@ -2556,7 +2560,7 @@ def test_handle_cancel_buy_corder_empty(mocker, default_conf, limit_buy_order,
|
|||||||
)
|
)
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
freqtrade._notify_buy_cancel = MagicMock()
|
freqtrade._notify_enter_cancel = MagicMock()
|
||||||
|
|
||||||
trade = MagicMock()
|
trade = MagicMock()
|
||||||
trade.pair = 'LTC/USDT'
|
trade.pair = 'LTC/USDT'
|
||||||
@ -2564,16 +2568,16 @@ def test_handle_cancel_buy_corder_empty(mocker, default_conf, limit_buy_order,
|
|||||||
limit_buy_order['filled'] = 0.0
|
limit_buy_order['filled'] = 0.0
|
||||||
limit_buy_order['status'] = 'open'
|
limit_buy_order['status'] = 'open'
|
||||||
reason = CANCEL_REASON['TIMEOUT']
|
reason = CANCEL_REASON['TIMEOUT']
|
||||||
assert freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
|
assert freqtrade.handle_cancel_enter(trade, limit_buy_order, reason)
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
|
|
||||||
cancel_order_mock.reset_mock()
|
cancel_order_mock.reset_mock()
|
||||||
limit_buy_order['filled'] = 1.0
|
limit_buy_order['filled'] = 1.0
|
||||||
assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
|
assert not freqtrade.handle_cancel_enter(trade, limit_buy_order, reason)
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_handle_cancel_sell_limit(mocker, default_conf, fee) -> None:
|
def test_handle_cancel_exit_limit(mocker, default_conf, fee) -> None:
|
||||||
send_msg_mock = patch_RPCManager(mocker)
|
send_msg_mock = patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
cancel_order_mock = MagicMock()
|
cancel_order_mock = MagicMock()
|
||||||
@ -2599,26 +2603,26 @@ def test_handle_cancel_sell_limit(mocker, default_conf, fee) -> None:
|
|||||||
'amount': 1,
|
'amount': 1,
|
||||||
'status': "open"}
|
'status': "open"}
|
||||||
reason = CANCEL_REASON['TIMEOUT']
|
reason = CANCEL_REASON['TIMEOUT']
|
||||||
assert freqtrade.handle_cancel_sell(trade, order, reason)
|
assert freqtrade.handle_cancel_exit(trade, order, reason)
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
assert send_msg_mock.call_count == 1
|
assert send_msg_mock.call_count == 1
|
||||||
|
|
||||||
send_msg_mock.reset_mock()
|
send_msg_mock.reset_mock()
|
||||||
|
|
||||||
order['amount'] = 2
|
order['amount'] = 2
|
||||||
assert freqtrade.handle_cancel_sell(trade, order, reason
|
assert freqtrade.handle_cancel_exit(trade, order, reason
|
||||||
) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
||||||
# Assert cancel_order was not called (callcount remains unchanged)
|
# Assert cancel_order was not called (callcount remains unchanged)
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
assert send_msg_mock.call_count == 1
|
assert send_msg_mock.call_count == 1
|
||||||
assert freqtrade.handle_cancel_sell(trade, order, reason
|
assert freqtrade.handle_cancel_exit(trade, order, reason
|
||||||
) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
||||||
# Message should not be iterated again
|
# Message should not be iterated again
|
||||||
assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
||||||
assert send_msg_mock.call_count == 1
|
assert send_msg_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_handle_cancel_sell_cancel_exception(mocker, default_conf) -> None:
|
def test_handle_cancel_exit_cancel_exception(mocker, default_conf) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
@ -2631,7 +2635,7 @@ def test_handle_cancel_sell_cancel_exception(mocker, default_conf) -> None:
|
|||||||
order = {'remaining': 1,
|
order = {'remaining': 1,
|
||||||
'amount': 1,
|
'amount': 1,
|
||||||
'status': "open"}
|
'status': "open"}
|
||||||
assert freqtrade.handle_cancel_sell(trade, order, reason) == 'error cancelling order'
|
assert freqtrade.handle_cancel_exit(trade, order, reason) == 'error cancelling order'
|
||||||
|
|
||||||
|
|
||||||
def test_execute_trade_exit_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> None:
|
def test_execute_trade_exit_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> None:
|
||||||
@ -3303,7 +3307,7 @@ def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_
|
|||||||
assert trade.amount != amnt
|
assert trade.amount != amnt
|
||||||
|
|
||||||
|
|
||||||
def test__safe_sell_amount(default_conf, fee, caplog, mocker):
|
def test__safe_exit_amount(default_conf, fee, caplog, mocker):
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
amount = 95.33
|
amount = 95.33
|
||||||
@ -3323,17 +3327,17 @@ def test__safe_sell_amount(default_conf, fee, caplog, mocker):
|
|||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
|
|
||||||
wallet_update.reset_mock()
|
wallet_update.reset_mock()
|
||||||
assert freqtrade._safe_sell_amount(trade.pair, trade.amount) == amount_wallet
|
assert freqtrade._safe_exit_amount(trade.pair, trade.amount) == amount_wallet
|
||||||
assert log_has_re(r'.*Falling back to wallet-amount.', caplog)
|
assert log_has_re(r'.*Falling back to wallet-amount.', caplog)
|
||||||
assert wallet_update.call_count == 1
|
assert wallet_update.call_count == 1
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
wallet_update.reset_mock()
|
wallet_update.reset_mock()
|
||||||
assert freqtrade._safe_sell_amount(trade.pair, amount_wallet) == amount_wallet
|
assert freqtrade._safe_exit_amount(trade.pair, amount_wallet) == amount_wallet
|
||||||
assert not log_has_re(r'.*Falling back to wallet-amount.', caplog)
|
assert not log_has_re(r'.*Falling back to wallet-amount.', caplog)
|
||||||
assert wallet_update.call_count == 1
|
assert wallet_update.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test__safe_sell_amount_error(default_conf, fee, caplog, mocker):
|
def test__safe_exit_amount_error(default_conf, fee, caplog, mocker):
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
amount = 95.33
|
amount = 95.33
|
||||||
@ -3350,8 +3354,8 @@ def test__safe_sell_amount_error(default_conf, fee, caplog, mocker):
|
|||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
with pytest.raises(DependencyException, match=r"Not enough amount to sell."):
|
with pytest.raises(DependencyException, match=r"Not enough amount to exit."):
|
||||||
assert freqtrade._safe_sell_amount(trade.pair, trade.amount)
|
assert freqtrade._safe_exit_amount(trade.pair, trade.amount)
|
||||||
|
|
||||||
|
|
||||||
def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplog) -> None:
|
def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplog) -> None:
|
||||||
@ -3525,6 +3529,7 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, limit_buy_or
|
|||||||
assert log_has("ETH/BTC - Using positive stoploss: 0.01 offset: 0 profit: 0.2666%", caplog)
|
assert log_has("ETH/BTC - Using positive stoploss: 0.01 offset: 0 profit: 0.2666%", caplog)
|
||||||
assert log_has("ETH/BTC - Adjusting stoploss...", caplog)
|
assert log_has("ETH/BTC - Adjusting stoploss...", caplog)
|
||||||
assert trade.stop_loss == 0.0000138501
|
assert trade.stop_loss == 0.0000138501
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
|
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
|
||||||
MagicMock(return_value={
|
MagicMock(return_value={
|
||||||
@ -3585,6 +3590,7 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, limit_buy_orde
|
|||||||
assert log_has("ETH/BTC - Using positive stoploss: 0.01 offset: 0.011 profit: 0.2666%", caplog)
|
assert log_has("ETH/BTC - Using positive stoploss: 0.01 offset: 0.011 profit: 0.2666%", caplog)
|
||||||
assert log_has("ETH/BTC - Adjusting stoploss...", caplog)
|
assert log_has("ETH/BTC - Adjusting stoploss...", caplog)
|
||||||
assert trade.stop_loss == 0.0000138501
|
assert trade.stop_loss == 0.0000138501
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
|
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
|
||||||
MagicMock(return_value={
|
MagicMock(return_value={
|
||||||
@ -3649,6 +3655,7 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, limit_buy_order_
|
|||||||
|
|
||||||
assert not log_has("ETH/BTC - Adjusting stoploss...", caplog)
|
assert not log_has("ETH/BTC - Adjusting stoploss...", caplog)
|
||||||
assert trade.stop_loss == 0.0000098910
|
assert trade.stop_loss == 0.0000098910
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
# price rises above the offset (rises 12% when the offset is 5.5%)
|
# price rises above the offset (rises 12% when the offset is 5.5%)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
|
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
|
||||||
@ -4316,8 +4323,8 @@ def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limi
|
|||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_order',
|
mocker.patch('freqtrade.exchange.Exchange.fetch_order',
|
||||||
side_effect=[
|
side_effect=[
|
||||||
ExchangeError(), limit_sell_order, limit_buy_order, limit_sell_order])
|
ExchangeError(), limit_sell_order, limit_buy_order, limit_sell_order])
|
||||||
buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_buy')
|
buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_enter')
|
||||||
sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_sell')
|
sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_exit')
|
||||||
|
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
create_mock_trades(fee)
|
create_mock_trades(fee)
|
||||||
@ -4351,6 +4358,7 @@ def test_update_open_orders(mocker, default_conf, fee, caplog):
|
|||||||
|
|
||||||
freqtrade.update_open_orders()
|
freqtrade.update_open_orders()
|
||||||
assert not log_has_re(r"Error updating Order .*", caplog)
|
assert not log_has_re(r"Error updating Order .*", caplog)
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
freqtrade.config['dry_run'] = False
|
freqtrade.config['dry_run'] = False
|
||||||
freqtrade.update_open_orders()
|
freqtrade.update_open_orders()
|
||||||
@ -4432,14 +4440,14 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog):
|
def test_reupdate_enter_order_fees(mocker, default_conf, fee, caplog):
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state')
|
mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state')
|
||||||
|
|
||||||
create_mock_trades(fee)
|
create_mock_trades(fee)
|
||||||
trades = Trade.get_trades().all()
|
trades = Trade.get_trades().all()
|
||||||
|
|
||||||
freqtrade.reupdate_buy_order_fees(trades[0])
|
freqtrade.reupdate_enter_order_fees(trades[0])
|
||||||
assert log_has_re(r"Trying to reupdate buy fees for .*", caplog)
|
assert log_has_re(r"Trying to reupdate buy fees for .*", caplog)
|
||||||
assert mock_uts.call_count == 1
|
assert mock_uts.call_count == 1
|
||||||
assert mock_uts.call_args_list[0][0][0] == trades[0]
|
assert mock_uts.call_args_list[0][0][0] == trades[0]
|
||||||
@ -4462,7 +4470,7 @@ def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog):
|
|||||||
)
|
)
|
||||||
Trade.query.session.add(trade)
|
Trade.query.session.add(trade)
|
||||||
|
|
||||||
freqtrade.reupdate_buy_order_fees(trade)
|
freqtrade.reupdate_enter_order_fees(trade)
|
||||||
assert log_has_re(r"Trying to reupdate buy fees for .*", caplog)
|
assert log_has_re(r"Trying to reupdate buy fees for .*", caplog)
|
||||||
assert mock_uts.call_count == 0
|
assert mock_uts.call_count == 0
|
||||||
assert not log_has_re(r"Updating buy-fee on trade .* for order .*\.", caplog)
|
assert not log_has_re(r"Updating buy-fee on trade .* for order .*\.", caplog)
|
||||||
@ -4472,7 +4480,7 @@ def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog):
|
|||||||
def test_handle_insufficient_funds(mocker, default_conf, fee):
|
def test_handle_insufficient_funds(mocker, default_conf, fee):
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
mock_rlo = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.refind_lost_order')
|
mock_rlo = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.refind_lost_order')
|
||||||
mock_bof = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.reupdate_buy_order_fees')
|
mock_bof = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.reupdate_enter_order_fees')
|
||||||
create_mock_trades(fee)
|
create_mock_trades(fee)
|
||||||
trades = Trade.get_trades().all()
|
trades = Trade.get_trades().all()
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.FreqtradeBot',
|
'freqtrade.freqtradebot.FreqtradeBot',
|
||||||
create_stoploss_order=MagicMock(return_value=True),
|
create_stoploss_order=MagicMock(return_value=True),
|
||||||
_notify_sell=MagicMock(),
|
_notify_exit=MagicMock(),
|
||||||
)
|
)
|
||||||
mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock)
|
mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock)
|
||||||
wallets_mock = mocker.patch("freqtrade.wallets.Wallets.update", MagicMock())
|
wallets_mock = mocker.patch("freqtrade.wallets.Wallets.update", MagicMock())
|
||||||
@ -154,7 +154,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.FreqtradeBot',
|
'freqtrade.freqtradebot.FreqtradeBot',
|
||||||
create_stoploss_order=MagicMock(return_value=True),
|
create_stoploss_order=MagicMock(return_value=True),
|
||||||
_notify_sell=MagicMock(),
|
_notify_exit=MagicMock(),
|
||||||
)
|
)
|
||||||
should_sell_mock = MagicMock(side_effect=[
|
should_sell_mock = MagicMock(side_effect=[
|
||||||
SellCheckTuple(sell_type=SellType.NONE),
|
SellCheckTuple(sell_type=SellType.NONE),
|
||||||
|
@ -90,7 +90,7 @@ def test_enter_exit_side(fee):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test__set_stop_loss_isolated_liq(fee):
|
def test_set_stop_loss_isolated_liq(fee):
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
id=2,
|
id=2,
|
||||||
pair='ADA/USDT',
|
pair='ADA/USDT',
|
||||||
|
Loading…
Reference in New Issue
Block a user