mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-10 18:23:55 +00:00
Merge branch 'freqtrade:feat/short' into feat/short
This commit is contained in:
commit
d961ddc2e2
|
@ -2,5 +2,6 @@ include LICENSE
|
||||||
include README.md
|
include README.md
|
||||||
recursive-include freqtrade *.py
|
recursive-include freqtrade *.py
|
||||||
recursive-include freqtrade/templates/ *.j2 *.ipynb
|
recursive-include freqtrade/templates/ *.j2 *.ipynb
|
||||||
|
include freqtrade/exchange/binance_leverage_tiers.json
|
||||||
include freqtrade/rpc/api_server/ui/fallback_file.html
|
include freqtrade/rpc/api_server/ui/fallback_file.html
|
||||||
include freqtrade/rpc/api_server/ui/favicon.ico
|
include freqtrade/rpc/api_server/ui/favicon.ico
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
"ignore_roi_if_buy_signal": false,
|
"ignore_roi_if_buy_signal": false,
|
||||||
"ignore_buying_expired_candle_after": 300,
|
"ignore_buying_expired_candle_after": 300,
|
||||||
"trading_mode": "spot",
|
"trading_mode": "spot",
|
||||||
// "margin_mode": "isolated",
|
"margin_mode": "",
|
||||||
"minimal_roi": {
|
"minimal_roi": {
|
||||||
"40": 0.0,
|
"40": 0.0,
|
||||||
"30": 0.01,
|
"30": 0.01,
|
||||||
|
|
|
@ -26,7 +26,7 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||||
--timerange TIMERANGE
|
--timerange TIMERANGE
|
||||||
Specify what timerange of data to use.
|
Specify what timerange of data to use.
|
||||||
|
@ -63,7 +63,7 @@ optional arguments:
|
||||||
`30m`, `1h`, `1d`).
|
`30m`, `1h`, `1d`).
|
||||||
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
||||||
Provide a space-separated list of strategies to
|
Provide a space-separated list of strategies to
|
||||||
backtest. Please note that ticker-interval needs to be
|
backtest. Please note that timeframe needs to be
|
||||||
set either in config or via command line. When using
|
set either in config or via command line. When using
|
||||||
this together with `--export trades`, the strategy-
|
this together with `--export trades`, the strategy-
|
||||||
name is injected into the filename (so `backtest-
|
name is injected into the filename (so `backtest-
|
||||||
|
@ -274,8 +274,8 @@ A backtesting result will look like that:
|
||||||
| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 |
|
| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 |
|
||||||
| ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 |
|
| ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 |
|
||||||
| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 |
|
| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 |
|
||||||
========================================================= SELL REASON STATS ==========================================================
|
========================================================= EXIT REASON STATS ==========================================================
|
||||||
| Sell Reason | Sells | Wins | Draws | Losses |
|
| Exit Reason | Sells | Wins | Draws | Losses |
|
||||||
|:-------------------|--------:|------:|-------:|--------:|
|
|:-------------------|--------:|------:|-------:|--------:|
|
||||||
| trailing_stop_loss | 205 | 150 | 0 | 55 |
|
| trailing_stop_loss | 205 | 150 | 0 | 55 |
|
||||||
| stop_loss | 166 | 0 | 0 | 166 |
|
| stop_loss | 166 | 0 | 0 | 166 |
|
||||||
|
@ -287,14 +287,14 @@ A backtesting result will look like that:
|
||||||
| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 |
|
| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 |
|
||||||
| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 |
|
| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 |
|
||||||
| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 |
|
| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 |
|
||||||
=============== SUMMARY METRICS ===============
|
================ SUMMARY METRICS ===============
|
||||||
| Metric | Value |
|
| Metric | Value |
|
||||||
|-----------------------+---------------------|
|
|------------------------+---------------------|
|
||||||
| Backtesting from | 2019-01-01 00:00:00 |
|
| Backtesting from | 2019-01-01 00:00:00 |
|
||||||
| Backtesting to | 2019-05-01 00:00:00 |
|
| Backtesting to | 2019-05-01 00:00:00 |
|
||||||
| Max open trades | 3 |
|
| Max open trades | 3 |
|
||||||
| | |
|
| | |
|
||||||
| Total/Daily Avg Trades| 429 / 3.575 |
|
| Total/Daily Avg Trades | 429 / 3.575 |
|
||||||
| Starting balance | 0.01000000 BTC |
|
| Starting balance | 0.01000000 BTC |
|
||||||
| Final balance | 0.01762792 BTC |
|
| Final balance | 0.01762792 BTC |
|
||||||
| Absolute profit | 0.00762792 BTC |
|
| Absolute profit | 0.00762792 BTC |
|
||||||
|
@ -312,7 +312,7 @@ A backtesting result will look like that:
|
||||||
| Days win/draw/lose | 12 / 82 / 25 |
|
| Days win/draw/lose | 12 / 82 / 25 |
|
||||||
| Avg. Duration Winners | 4:23:00 |
|
| Avg. Duration Winners | 4:23:00 |
|
||||||
| Avg. Duration Loser | 6:55:00 |
|
| Avg. Duration Loser | 6:55:00 |
|
||||||
| Rejected Buy signals | 3089 |
|
| Rejected Entry signals | 3089 |
|
||||||
| Entry/Exit Timeouts | 0 / 0 |
|
| Entry/Exit Timeouts | 0 / 0 |
|
||||||
| | |
|
| | |
|
||||||
| Min balance | 0.00945123 BTC |
|
| Min balance | 0.00945123 BTC |
|
||||||
|
@ -359,14 +359,14 @@ On the other hand, if you set a too high `minimal_roi` like `"0": 0.55`
|
||||||
(55%), there is almost no chance that the bot will ever reach this profit.
|
(55%), there is almost no chance that the bot will ever reach this profit.
|
||||||
Hence, keep in mind that your performance is an integral mix of all different elements of the strategy, your configuration, and the crypto-currency pairs you have set up.
|
Hence, keep in mind that your performance is an integral mix of all different elements of the strategy, your configuration, and the crypto-currency pairs you have set up.
|
||||||
|
|
||||||
### Sell reasons table
|
### Exit reasons table
|
||||||
|
|
||||||
The 2nd table contains a recap of sell reasons.
|
The 2nd table contains a recap of exit reasons.
|
||||||
This table can tell you which area needs some additional work (e.g. all or many of the `sell_signal` trades are losses, so you should work on improving the sell signal, or consider disabling it).
|
This table can tell you which area needs some additional work (e.g. all or many of the `sell_signal` trades are losses, so you should work on improving the sell signal, or consider disabling it).
|
||||||
|
|
||||||
### Left open trades table
|
### Left open trades table
|
||||||
|
|
||||||
The 3rd table contains all trades the bot had to `forcesell` at the end of the backtesting period to present you the full picture.
|
The 3rd table contains all trades the bot had to `forceexit` at the end of the backtesting period to present you the full picture.
|
||||||
This is necessary to simulate realistic behavior, since the backtest period has to end at some point, while realistically, you could leave the bot running forever.
|
This is necessary to simulate realistic behavior, since the backtest period has to end at some point, while realistically, you could leave the bot running forever.
|
||||||
These trades are also included in the first table, but are also shown separately in this table for clarity.
|
These trades are also included in the first table, but are also shown separately in this table for clarity.
|
||||||
|
|
||||||
|
@ -406,7 +406,7 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||||
| Days win/draw/lose | 12 / 82 / 25 |
|
| Days win/draw/lose | 12 / 82 / 25 |
|
||||||
| Avg. Duration Winners | 4:23:00 |
|
| Avg. Duration Winners | 4:23:00 |
|
||||||
| Avg. Duration Loser | 6:55:00 |
|
| Avg. Duration Loser | 6:55:00 |
|
||||||
| Rejected Buy signals | 3089 |
|
| Rejected Entry signals | 3089 |
|
||||||
| Entry/Exit Timeouts | 0 / 0 |
|
| Entry/Exit Timeouts | 0 / 0 |
|
||||||
| | |
|
| | |
|
||||||
| Min balance | 0.00945123 BTC |
|
| Min balance | 0.00945123 BTC |
|
||||||
|
@ -436,7 +436,7 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||||
- `Best day` / `Worst day`: Best and worst day based on daily profit.
|
- `Best day` / `Worst day`: Best and worst day based on daily profit.
|
||||||
- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade).
|
- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade).
|
||||||
- `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades.
|
- `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades.
|
||||||
- `Rejected Buy signals`: Buy signals that could not be acted upon due to max_open_trades being reached.
|
- `Rejected Entry signals`: Trade entry signals that could not be acted upon due to `max_open_trades` being reached.
|
||||||
- `Entry/Exit Timeouts`: Entry/exit orders which did not fill (only applicable if custom pricing is used).
|
- `Entry/Exit Timeouts`: Entry/exit orders which did not fill (only applicable if custom pricing is used).
|
||||||
- `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period.
|
- `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period.
|
||||||
- `Drawdown (Account)`: Maximum Account Drawdown experienced. Calculated as $(Absolute Drawdown) / (DrawdownHigh + startingBalance)$.
|
- `Drawdown (Account)`: Maximum Account Drawdown experienced. Calculated as $(Absolute Drawdown) / (DrawdownHigh + startingBalance)$.
|
||||||
|
|
|
@ -32,19 +32,19 @@ By default, loop runs every few seconds (`internals.process_throttle_secs`) and
|
||||||
* Call `populate_entry_trend()`
|
* Call `populate_entry_trend()`
|
||||||
* Call `populate_exit_trend()`
|
* Call `populate_exit_trend()`
|
||||||
* Check timeouts for open orders.
|
* Check timeouts for open orders.
|
||||||
* Calls `check_buy_timeout()` strategy callback for open buy orders.
|
* Calls `check_buy_timeout()` strategy callback for open entry orders.
|
||||||
* Calls `check_sell_timeout()` strategy callback for open sell orders.
|
* Calls `check_sell_timeout()` strategy callback for open exit orders.
|
||||||
* Verifies existing positions and eventually places sell orders.
|
* Verifies existing positions and eventually places exit orders.
|
||||||
* Considers stoploss, ROI and sell-signal, `custom_exit()` and `custom_stoploss()`.
|
* Considers stoploss, ROI and exit-signal, `custom_exit()` and `custom_stoploss()`.
|
||||||
* Determine sell-price based on `ask_strategy` configuration setting or by using the `custom_exit_price()` callback.
|
* Determine exit-price based on `ask_strategy` configuration setting or by using the `custom_exit_price()` callback.
|
||||||
* Before a sell order is placed, `confirm_trade_exit()` strategy callback is called.
|
* Before a exit order is placed, `confirm_trade_exit()` strategy callback is called.
|
||||||
* Check position adjustments for open trades if enabled by calling `adjust_trade_position()` and place additional order if required.
|
* Check position adjustments for open trades if enabled by calling `adjust_trade_position()` and place additional order if required.
|
||||||
* Check if trade-slots are still available (if `max_open_trades` is reached).
|
* Check if trade-slots are still available (if `max_open_trades` is reached).
|
||||||
* Verifies buy signal trying to enter new positions.
|
* Verifies entry signal trying to enter new positions.
|
||||||
* Determine buy-price based on `bid_strategy` configuration setting, or by using the `custom_entry_price()` callback.
|
* Determine entry-price based on `bid_strategy` configuration setting, or by using the `custom_entry_price()` callback.
|
||||||
* In Margin and Futures mode, `leverage()` strategy callback is called to determine the desired leverage.
|
* In Margin and Futures mode, `leverage()` strategy callback is called to determine the desired leverage.
|
||||||
* Determine stake size by calling the `custom_stake_amount()` callback.
|
* Determine stake size by calling the `custom_stake_amount()` callback.
|
||||||
* Before a buy order is placed, `confirm_trade_entry()` strategy callback is called.
|
* Before an entry order is placed, `confirm_trade_entry()` strategy callback is called.
|
||||||
|
|
||||||
This loop will be repeated again and again until the bot is stopped.
|
This loop will be repeated again and again until the bot is stopped.
|
||||||
|
|
||||||
|
@ -55,15 +55,15 @@ This loop will be repeated again and again until the bot is stopped.
|
||||||
* Load historic data for configured pairlist.
|
* Load historic data for configured pairlist.
|
||||||
* Calls `bot_loop_start()` once.
|
* Calls `bot_loop_start()` once.
|
||||||
* Calculate indicators (calls `populate_indicators()` once per pair).
|
* Calculate indicators (calls `populate_indicators()` once per pair).
|
||||||
* Calculate buy / sell signals (calls `populate_entry_trend()` and `populate_exit_trend()` once per pair).
|
* Calculate entry / exit signals (calls `populate_entry_trend()` and `populate_exit_trend()` once per pair).
|
||||||
* Loops per candle simulating entry and exit points.
|
* Loops per candle simulating entry and exit points.
|
||||||
* Confirm trade buy / sell (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy).
|
* Confirm trade entry / exits (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy).
|
||||||
* Call `custom_entry_price()` (if implemented in the strategy) to determine entry price (Prices are moved to be within the opening candle).
|
* Call `custom_entry_price()` (if implemented in the strategy) to determine entry price (Prices are moved to be within the opening candle).
|
||||||
* In Margin and Futures mode, `leverage()` strategy callback is called to determine the desired leverage.
|
* In Margin and Futures mode, `leverage()` strategy callback is called to determine the desired leverage.
|
||||||
* Determine stake size by calling the `custom_stake_amount()` callback.
|
* Determine stake size by calling the `custom_stake_amount()` callback.
|
||||||
* Check position adjustments for open trades if enabled and call `adjust_trade_position()` to determine if an additional order is requested.
|
* Check position adjustments for open trades if enabled and call `adjust_trade_position()` to determine if an additional order is requested.
|
||||||
* Call `custom_stoploss()` and `custom_exit()` to find custom exit points.
|
* Call `custom_stoploss()` and `custom_exit()` to find custom exit points.
|
||||||
* For sells based on sell-signal and custom-sell: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle).
|
* For exits based on exit-signal and custom-exit: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle).
|
||||||
* Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_buy_timeout()` / `check_sell_timeout()` strategy callbacks.
|
* Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_buy_timeout()` / `check_sell_timeout()` strategy callbacks.
|
||||||
* Generate backtest report output
|
* Generate backtest report output
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,10 @@ Please refer to [pairlists](plugins.md#pairlists-and-pairlist-handlers) instead.
|
||||||
Did only download the latest 500 candles, so was ineffective in getting good backtest data.
|
Did only download the latest 500 candles, so was ineffective in getting good backtest data.
|
||||||
Removed in 2019-7-dev (develop branch) and in freqtrade 2019.8.
|
Removed in 2019-7-dev (develop branch) and in freqtrade 2019.8.
|
||||||
|
|
||||||
|
### `ticker_interval` (now `timeframe`)
|
||||||
|
|
||||||
|
Support for `ticker_interval` terminology was deprecated in 2020.6 in favor of `timeframe` - and compatibility code was removed in 2022.3.
|
||||||
|
|
||||||
### Allow running multiple pairlists in sequence
|
### Allow running multiple pairlists in sequence
|
||||||
|
|
||||||
The former `"pairlist"` section in the configuration has been removed, and is replaced by `"pairlists"` - being a list to specify a sequence of pairlists.
|
The former `"pairlist"` section in the configuration has been removed, and is replaced by `"pairlists"` - being a list to specify a sequence of pairlists.
|
||||||
|
|
|
@ -222,7 +222,7 @@ usage: freqtrade edge [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||||
--timerange TIMERANGE
|
--timerange TIMERANGE
|
||||||
Specify what timerange of data to use.
|
Specify what timerange of data to use.
|
||||||
|
|
|
@ -55,7 +55,7 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||||
--timerange TIMERANGE
|
--timerange TIMERANGE
|
||||||
Specify what timerange of data to use.
|
Specify what timerange of data to use.
|
||||||
|
@ -200,7 +200,7 @@ There you have two different types of indicators: 1. `guards` and 2. `triggers`.
|
||||||
!!! Hint "Guards and Triggers"
|
!!! Hint "Guards and Triggers"
|
||||||
Technically, there is no difference between Guards and Triggers.
|
Technically, there is no difference between Guards and Triggers.
|
||||||
However, this guide will make this distinction to make it clear that signals should not be "sticking".
|
However, this guide will make this distinction to make it clear that signals should not be "sticking".
|
||||||
Sticking signals are signals that are active for multiple candles. This can lead into buying a signal late (right before the signal disappears - which means that the chance of success is a lot lower than right at the beginning).
|
Sticking signals are signals that are active for multiple candles. This can lead into entering a signal late (right before the signal disappears - which means that the chance of success is a lot lower than right at the beginning).
|
||||||
|
|
||||||
Hyper-optimization will, for each epoch round, pick one trigger and possibly multiple guards.
|
Hyper-optimization will, for each epoch round, pick one trigger and possibly multiple guards.
|
||||||
|
|
||||||
|
@ -216,8 +216,8 @@ The configuration and rules are the same than for buy signals.
|
||||||
|
|
||||||
## Solving a Mystery
|
## Solving a Mystery
|
||||||
|
|
||||||
Let's say you are curious: should you use MACD crossings or lower Bollinger Bands to trigger your buys.
|
Let's say you are curious: should you use MACD crossings or lower Bollinger Bands to trigger your long entries.
|
||||||
And you also wonder should you use RSI or ADX to help with those buy decisions.
|
And you also wonder should you use RSI or ADX to help with those decisions.
|
||||||
If you decide to use RSI or ADX, which values should I use for them?
|
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.
|
So let's use hyperparameter optimization to solve this mystery.
|
||||||
|
|
|
@ -65,7 +65,7 @@ optional arguments:
|
||||||
_today.json`
|
_today.json`
|
||||||
--timerange TIMERANGE
|
--timerange TIMERANGE
|
||||||
Specify what timerange of data to use.
|
Specify what timerange of data to use.
|
||||||
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||||
--no-trades Skip using trades from backtesting file and DB.
|
--no-trades Skip using trades from backtesting file and DB.
|
||||||
|
|
||||||
|
@ -334,7 +334,7 @@ optional arguments:
|
||||||
--trade-source {DB,file}
|
--trade-source {DB,file}
|
||||||
Specify the source for trades (Can be DB or file
|
Specify the source for trades (Can be DB or file
|
||||||
(backtest file)) Default: file
|
(backtest file)) Default: file
|
||||||
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||||
--auto-open Automatically open generated plot.
|
--auto-open Automatically open generated plot.
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
mkdocs==1.2.3
|
mkdocs==1.2.3
|
||||||
mkdocs-material==8.2.5
|
mkdocs-material==8.2.5
|
||||||
mdx_truly_sane_lists==1.2
|
mdx_truly_sane_lists==1.2
|
||||||
pymdown-extensions==9.2
|
pymdown-extensions==9.3
|
||||||
|
|
|
@ -146,7 +146,7 @@ def version(self) -> str:
|
||||||
|
|
||||||
The strategies can be derived from other strategies. This avoids duplication of your custom strategy code. You can use this technique to override small parts of your main strategy, leaving the rest untouched:
|
The strategies can be derived from other strategies. This avoids duplication of your custom strategy code. You can use this technique to override small parts of your main strategy, leaving the rest untouched:
|
||||||
|
|
||||||
``` python
|
``` python title="user_data/strategies/myawesomestrategy.py"
|
||||||
class MyAwesomeStrategy(IStrategy):
|
class MyAwesomeStrategy(IStrategy):
|
||||||
...
|
...
|
||||||
stoploss = 0.13
|
stoploss = 0.13
|
||||||
|
@ -155,6 +155,10 @@ class MyAwesomeStrategy(IStrategy):
|
||||||
# should be in any custom strategy...
|
# should be in any custom strategy...
|
||||||
...
|
...
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
``` python title="user_data/strategies/MyAwesomeStrategy2.py"
|
||||||
|
from myawesomestrategy import MyAwesomeStrategy
|
||||||
class MyAwesomeStrategy2(MyAwesomeStrategy):
|
class MyAwesomeStrategy2(MyAwesomeStrategy):
|
||||||
# Override something
|
# Override something
|
||||||
stoploss = 0.08
|
stoploss = 0.08
|
||||||
|
@ -163,16 +167,7 @@ class MyAwesomeStrategy2(MyAwesomeStrategy):
|
||||||
|
|
||||||
Both attributes and methods may be overridden, altering behavior of the original strategy in a way you need.
|
Both attributes and methods may be overridden, altering behavior of the original strategy in a way you need.
|
||||||
|
|
||||||
!!! Note "Parent-strategy in different files"
|
While keeping the subclass in the same file is technically possible, it can lead to some problems with hyperopt parameter files, we therefore recommend to use separate strategy files, and import the parent strategy as shown above.
|
||||||
If you have the parent-strategy in a different file, you'll need to add the following to the top of your "child"-file to ensure proper loading, otherwise freqtrade may not be able to load the parent strategy correctly.
|
|
||||||
|
|
||||||
``` python
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
sys.path.append(str(Path(__file__).parent))
|
|
||||||
|
|
||||||
from myawesomestrategy import MyAwesomeStrategy
|
|
||||||
```
|
|
||||||
|
|
||||||
## Embedding Strategies
|
## Embedding Strategies
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ Depending on the callback used, they may be called when entering / exiting a tra
|
||||||
Currently available callbacks:
|
Currently available callbacks:
|
||||||
|
|
||||||
* [`bot_loop_start()`](#bot-loop-start)
|
* [`bot_loop_start()`](#bot-loop-start)
|
||||||
* [`custom_stake_amount()`](#custom-stake-size)
|
* [`custom_stake_amount()`](#stake-size-management)
|
||||||
* [`custom_exit()`](#custom-exit-signal)
|
* [`custom_exit()`](#custom-exit-signal)
|
||||||
* [`custom_stoploss()`](#custom-stoploss)
|
* [`custom_stoploss()`](#custom-stoploss)
|
||||||
* [`custom_entry_price()` and `custom_exit_price()`](#custom-order-price-rules)
|
* [`custom_entry_price()` and `custom_exit_price()`](#custom-order-price-rules)
|
||||||
|
@ -16,6 +16,7 @@ Currently available callbacks:
|
||||||
* [`confirm_trade_entry()`](#trade-entry-buy-order-confirmation)
|
* [`confirm_trade_entry()`](#trade-entry-buy-order-confirmation)
|
||||||
* [`confirm_trade_exit()`](#trade-exit-sell-order-confirmation)
|
* [`confirm_trade_exit()`](#trade-exit-sell-order-confirmation)
|
||||||
* [`adjust_trade_position()`](#adjust-trade-position)
|
* [`adjust_trade_position()`](#adjust-trade-position)
|
||||||
|
* [`leverage()`](#leverage-callback)
|
||||||
|
|
||||||
!!! Tip "Callback calling sequence"
|
!!! Tip "Callback calling sequence"
|
||||||
You can find the callback calling sequence in [bot-basics](bot-basics.md#bot-execution-logic)
|
You can find the callback calling sequence in [bot-basics](bot-basics.md#bot-execution-logic)
|
||||||
|
|
|
@ -274,15 +274,16 @@ Starting capital is either taken from the `available_capital` setting, or calcul
|
||||||
|
|
||||||
### /forcesell <trade_id>
|
### /forcesell <trade_id>
|
||||||
|
|
||||||
> **BITTREX:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)`
|
> **BINANCE:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)`
|
||||||
|
|
||||||
### /forcelong <pair> [rate] | /forceshort <pair> [rate]
|
### /forcelong <pair> [rate] | /forceshort <pair> [rate]
|
||||||
|
|
||||||
`/forcebuy <pair> [rate]` is also supported for longs but should be considered deprecated.
|
`/forcebuy <pair> [rate]` is also supported for longs but should be considered deprecated.
|
||||||
|
|
||||||
> **BITTREX:** Long ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`)
|
> **BINANCE:** Long ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`)
|
||||||
|
|
||||||
Omitting the pair will open a query asking for the pair to trade (based on the current whitelist).
|
Omitting the pair will open a query asking for the pair to trade (based on the current whitelist).
|
||||||
|
Trades crated through `/forceentry` will have the buy-tag of `forceentry`.
|
||||||
|
|
||||||
![Telegram force-buy screenshot](assets/telegram_forcebuy.png)
|
![Telegram force-buy screenshot](assets/telegram_forcebuy.png)
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,14 @@
|
||||||
""" Freqtrade bot """
|
""" Freqtrade bot """
|
||||||
__version__ = 'develop'
|
__version__ = 'develop'
|
||||||
|
|
||||||
if __version__ == 'develop':
|
if 'dev' in __version__:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
__version__ = 'develop-' + subprocess.check_output(
|
__version__ = __version__ + '-' + 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('"')
|
||||||
|
|
||||||
# from datetime import datetime
|
|
||||||
# last_release = subprocess.check_output(
|
|
||||||
# ['git', 'tag']
|
|
||||||
# ).decode('utf-8').split()[-1].split(".")
|
|
||||||
# # Releases are in the format "2020.1" - we increment the latest version for dev.
|
|
||||||
# prefix = f"{last_release[0]}.{int(last_release[1]) + 1}"
|
|
||||||
# dev_version = int(datetime.now().timestamp() // 1000)
|
|
||||||
# __version__ = f"{prefix}.dev{dev_version}"
|
|
||||||
|
|
||||||
# subprocess.check_output(
|
|
||||||
# ['git', 'log', '--format="%h"', '-n 1'],
|
|
||||||
# stderr=subprocess.DEVNULL).decode("utf-8").rstrip().strip('"')
|
|
||||||
except Exception: # pragma: no cover
|
except Exception: # pragma: no cover
|
||||||
# git not available, ignore
|
# git not available, ignore
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -124,7 +124,7 @@ def ask_user_config() -> Dict[str, Any]:
|
||||||
"message": "Do you want to trade Perpetual Swaps (perpetual futures)?",
|
"message": "Do you want to trade Perpetual Swaps (perpetual futures)?",
|
||||||
"default": False,
|
"default": False,
|
||||||
"filter": lambda val: 'futures' if val else 'spot',
|
"filter": lambda val: 'futures' if val else 'spot',
|
||||||
"when": lambda x: x["exchange_name"] in ['binance', 'gateio'],
|
"when": lambda x: x["exchange_name"] in ['binance', 'gateio', 'okx'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "autocomplete",
|
"type": "autocomplete",
|
||||||
|
|
|
@ -118,7 +118,7 @@ AVAILABLE_CLI_OPTIONS = {
|
||||||
),
|
),
|
||||||
# Optimize common
|
# Optimize common
|
||||||
"timeframe": Arg(
|
"timeframe": Arg(
|
||||||
'-i', '--timeframe', '--ticker-interval',
|
'-i', '--timeframe',
|
||||||
help='Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).',
|
help='Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).',
|
||||||
),
|
),
|
||||||
"timerange": Arg(
|
"timerange": Arg(
|
||||||
|
@ -170,7 +170,7 @@ AVAILABLE_CLI_OPTIONS = {
|
||||||
"strategy_list": Arg(
|
"strategy_list": Arg(
|
||||||
'--strategy-list',
|
'--strategy-list',
|
||||||
help='Provide a space-separated list of strategies to backtest. '
|
help='Provide a space-separated list of strategies to backtest. '
|
||||||
'Please note that ticker-interval needs to be set either in config '
|
'Please note that timeframe needs to be set either in config '
|
||||||
'or via command line. When using this together with `--export trades`, '
|
'or via command line. When using this together with `--export trades`, '
|
||||||
'the strategy-name is injected into the filename '
|
'the strategy-name is injected into the filename '
|
||||||
'(so `backtest-data.json` becomes `backtest-data-SampleStrategy.json`',
|
'(so `backtest-data.json` becomes `backtest-data-SampleStrategy.json`',
|
||||||
|
|
|
@ -101,16 +101,11 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
|
||||||
"from the edge configuration."
|
"from the edge configuration."
|
||||||
)
|
)
|
||||||
if 'ticker_interval' in config:
|
if 'ticker_interval' in config:
|
||||||
logger.warning(
|
|
||||||
"DEPRECATED: "
|
raise OperationalException(
|
||||||
|
"DEPRECATED: 'ticker_interval' detected. "
|
||||||
"Please use 'timeframe' instead of 'ticker_interval."
|
"Please use 'timeframe' instead of 'ticker_interval."
|
||||||
)
|
)
|
||||||
if 'timeframe' in config:
|
|
||||||
raise OperationalException(
|
|
||||||
"Both 'timeframe' and 'ticker_interval' detected."
|
|
||||||
"Please remove 'ticker_interval' from your configuration to continue operating."
|
|
||||||
)
|
|
||||||
config['timeframe'] = config['ticker_interval']
|
|
||||||
|
|
||||||
if 'protections' in config:
|
if 'protections' in config:
|
||||||
logger.warning("DEPRECATED: Setting 'protections' in the configuration is deprecated.")
|
logger.warning("DEPRECATED: Setting 'protections' in the configuration is deprecated.")
|
||||||
|
|
|
@ -106,13 +106,13 @@ def load_data(datadir: Path,
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def refresh_data(datadir: Path,
|
def refresh_data(*, datadir: Path,
|
||||||
timeframe: str,
|
timeframe: str,
|
||||||
pairs: List[str],
|
pairs: List[str],
|
||||||
exchange: Exchange,
|
exchange: Exchange,
|
||||||
data_format: str = None,
|
data_format: str = None,
|
||||||
timerange: Optional[TimeRange] = None,
|
timerange: Optional[TimeRange] = None,
|
||||||
candle_type: CandleType = CandleType.SPOT
|
candle_type: CandleType,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Refresh ohlcv history data for a list of pairs.
|
Refresh ohlcv history data for a list of pairs.
|
||||||
|
@ -139,7 +139,7 @@ def _load_cached_data_for_updating(
|
||||||
timeframe: str,
|
timeframe: str,
|
||||||
timerange: Optional[TimeRange],
|
timerange: Optional[TimeRange],
|
||||||
data_handler: IDataHandler,
|
data_handler: IDataHandler,
|
||||||
candle_type: CandleType = CandleType.SPOT
|
candle_type: CandleType
|
||||||
) -> Tuple[DataFrame, Optional[int]]:
|
) -> Tuple[DataFrame, Optional[int]]:
|
||||||
"""
|
"""
|
||||||
Load cached data to download more data.
|
Load cached data to download more data.
|
||||||
|
@ -178,7 +178,7 @@ def _download_pair_history(pair: str, *,
|
||||||
new_pairs_days: int = 30,
|
new_pairs_days: int = 30,
|
||||||
data_handler: IDataHandler = None,
|
data_handler: IDataHandler = None,
|
||||||
timerange: Optional[TimeRange] = None,
|
timerange: Optional[TimeRange] = None,
|
||||||
candle_type: CandleType = CandleType.SPOT
|
candle_type: CandleType,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Download latest candles from the exchange for the pair and timeframe passed in parameters
|
Download latest candles from the exchange for the pair and timeframe passed in parameters
|
||||||
|
@ -202,7 +202,6 @@ def _download_pair_history(pair: str, *,
|
||||||
f'candle type: {candle_type} and store in {datadir}.'
|
f'candle type: {candle_type} and store in {datadir}.'
|
||||||
)
|
)
|
||||||
|
|
||||||
# data, since_ms = _load_cached_data_for_updating_old(datadir, pair, timeframe, timerange)
|
|
||||||
data, since_ms = _load_cached_data_for_updating(pair, timeframe, timerange,
|
data, since_ms = _load_cached_data_for_updating(pair, timeframe, timerange,
|
||||||
data_handler=data_handler,
|
data_handler=data_handler,
|
||||||
candle_type=candle_type)
|
candle_type=candle_type)
|
||||||
|
|
|
@ -14,6 +14,7 @@ from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, UNLIMITED_STAKE_AMOUNT
|
from freqtrade.constants import DATETIME_PRINT_FORMAT, UNLIMITED_STAKE_AMOUNT
|
||||||
from freqtrade.data.history import get_timerange, load_data, refresh_data
|
from freqtrade.data.history import get_timerange, load_data, refresh_data
|
||||||
from freqtrade.enums import RunMode, SellType
|
from freqtrade.enums import RunMode, SellType
|
||||||
|
from freqtrade.enums.candletype import CandleType
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange.exchange import timeframe_to_seconds
|
from freqtrade.exchange.exchange import timeframe_to_seconds
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||||
|
@ -116,6 +117,7 @@ class Edge:
|
||||||
timeframe=self.strategy.timeframe,
|
timeframe=self.strategy.timeframe,
|
||||||
timerange=timerange_startup,
|
timerange=timerange_startup,
|
||||||
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
||||||
|
candle_type=self.config.get('candle_type_def', CandleType.SPOT),
|
||||||
)
|
)
|
||||||
# Download informative pairs too
|
# Download informative pairs too
|
||||||
res = defaultdict(list)
|
res = defaultdict(list)
|
||||||
|
@ -132,6 +134,7 @@ class Edge:
|
||||||
timeframe=timeframe,
|
timeframe=timeframe,
|
||||||
timerange=timerange_startup,
|
timerange=timerange_startup,
|
||||||
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
||||||
|
candle_type=self.config.get('candle_type_def', CandleType.SPOT),
|
||||||
)
|
)
|
||||||
|
|
||||||
data = load_data(
|
data = load_data(
|
||||||
|
@ -141,6 +144,7 @@ class Edge:
|
||||||
timerange=self._timerange,
|
timerange=self._timerange,
|
||||||
startup_candles=self.strategy.startup_candle_count,
|
startup_candles=self.strategy.startup_candle_count,
|
||||||
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
||||||
|
candle_type=self.config.get('candle_type_def', CandleType.SPOT),
|
||||||
)
|
)
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
|
|
|
@ -39,6 +39,15 @@ MAP_EXCHANGE_CHILDCLASS = {
|
||||||
'okex': 'okx',
|
'okex': 'okx',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SUPPORTED_EXCHANGES = [
|
||||||
|
'binance',
|
||||||
|
'bittrex',
|
||||||
|
'ftx',
|
||||||
|
'gateio',
|
||||||
|
'huobi',
|
||||||
|
'kraken',
|
||||||
|
'okx',
|
||||||
|
]
|
||||||
|
|
||||||
EXCHANGE_HAS_REQUIRED = [
|
EXCHANGE_HAS_REQUIRED = [
|
||||||
# Required / private
|
# Required / private
|
||||||
|
@ -64,6 +73,9 @@ EXCHANGE_HAS_OPTIONAL = [
|
||||||
'fetchTickers', # For volumepairlist?
|
'fetchTickers', # For volumepairlist?
|
||||||
'fetchTrades', # Downloading trades data
|
'fetchTrades', # Downloading trades data
|
||||||
# 'fetchFundingRateHistory', # Futures trading
|
# 'fetchFundingRateHistory', # Futures trading
|
||||||
|
# 'fetchPositions', # Futures trading
|
||||||
|
# 'fetchLeverageTiers', # Futures initialization
|
||||||
|
# 'fetchMarketLeverageTiers', # Futures initialization
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,8 @@ from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFun
|
||||||
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,
|
EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED,
|
||||||
remove_credentials, retrier, retrier_async)
|
SUPPORTED_EXCHANGES, remove_credentials, retrier,
|
||||||
|
retrier_async)
|
||||||
from freqtrade.misc import chunks, 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
|
||||||
|
|
||||||
|
@ -309,8 +310,6 @@ class Exchange:
|
||||||
"""
|
"""
|
||||||
Return exchange ccxt markets, filtered out by base currency and quote currency
|
Return exchange ccxt markets, filtered out by base currency and quote currency
|
||||||
if this was requested in parameters.
|
if this was requested in parameters.
|
||||||
|
|
||||||
TODO: consider moving it to the Dataprovider
|
|
||||||
"""
|
"""
|
||||||
markets = self.markets
|
markets = self.markets
|
||||||
if not markets:
|
if not markets:
|
||||||
|
@ -2545,7 +2544,7 @@ def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = Non
|
||||||
|
|
||||||
|
|
||||||
def is_exchange_officially_supported(exchange_name: str) -> bool:
|
def is_exchange_officially_supported(exchange_name: str) -> bool:
|
||||||
return exchange_name in ['binance', 'bittrex', 'ftx', 'gateio', 'huobi', 'kraken', 'okx']
|
return exchange_name in SUPPORTED_EXCHANGES
|
||||||
|
|
||||||
|
|
||||||
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
|
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
|
||||||
|
|
|
@ -170,7 +170,12 @@ class Kraken(Exchange):
|
||||||
reduceOnly: bool,
|
reduceOnly: bool,
|
||||||
time_in_force: str = 'gtc'
|
time_in_force: str = 'gtc'
|
||||||
) -> Dict:
|
) -> Dict:
|
||||||
params = super()._get_params(ordertype, leverage, reduceOnly, time_in_force)
|
params = super()._get_params(
|
||||||
|
ordertype=ordertype,
|
||||||
|
leverage=leverage,
|
||||||
|
reduceOnly=reduceOnly,
|
||||||
|
time_in_force=time_in_force,
|
||||||
|
)
|
||||||
if leverage > 1.0:
|
if leverage > 1.0:
|
||||||
params['leverage'] = round(leverage)
|
params['leverage'] = round(leverage)
|
||||||
return params
|
return params
|
||||||
|
|
|
@ -58,11 +58,7 @@ class Okx(Exchange):
|
||||||
leverage: float,
|
leverage: float,
|
||||||
side: str # buy or sell
|
side: str # buy or sell
|
||||||
):
|
):
|
||||||
if self.trading_mode != TradingMode.SPOT:
|
if self.trading_mode != TradingMode.SPOT and self.margin_mode is not None:
|
||||||
if self.margin_mode is None:
|
|
||||||
raise OperationalException(
|
|
||||||
f"{self.name}.margin_mode must be set for {self.trading_mode.value}"
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
# TODO-lev: Test me properly (check mgnMode passed)
|
# TODO-lev: Test me properly (check mgnMode passed)
|
||||||
self._api.set_leverage(
|
self._api.set_leverage(
|
||||||
|
|
|
@ -16,8 +16,7 @@ from freqtrade.configuration import validate_config_consistency
|
||||||
from freqtrade.data.converter import order_book_to_dataframe
|
from freqtrade.data.converter import order_book_to_dataframe
|
||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.edge import Edge
|
from freqtrade.edge import Edge
|
||||||
from freqtrade.enums import (MarginMode, RPCMessageType, RunMode, SellType, SignalDirection, State,
|
from freqtrade.enums import RPCMessageType, RunMode, SellType, SignalDirection, State, TradingMode
|
||||||
TradingMode)
|
|
||||||
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
|
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
|
||||||
InvalidOrderException, PricingError)
|
InvalidOrderException, PricingError)
|
||||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
||||||
|
@ -104,9 +103,6 @@ class FreqtradeBot(LoggingMixin):
|
||||||
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
|
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
|
||||||
|
|
||||||
self.trading_mode: TradingMode = self.config.get('trading_mode', TradingMode.SPOT)
|
self.trading_mode: TradingMode = self.config.get('trading_mode', TradingMode.SPOT)
|
||||||
self.margin_mode_type: Optional[MarginMode] = None
|
|
||||||
if 'margin_mode' in self.config:
|
|
||||||
self.margin_mode = MarginMode(self.config['margin_mode'])
|
|
||||||
|
|
||||||
self._schedule = Scheduler()
|
self._schedule = Scheduler()
|
||||||
|
|
||||||
|
@ -1641,14 +1637,14 @@ class FreqtradeBot(LoggingMixin):
|
||||||
def handle_order_fee(self, trade: Trade, order_obj: Order, order: Dict[str, Any]) -> None:
|
def handle_order_fee(self, trade: Trade, order_obj: Order, order: Dict[str, Any]) -> None:
|
||||||
# Try update amount (binance-fix)
|
# Try update amount (binance-fix)
|
||||||
try:
|
try:
|
||||||
new_amount = self.get_real_amount(trade, order)
|
new_amount = self.get_real_amount(trade, order, order_obj)
|
||||||
if not isclose(safe_value_fallback(order, 'filled', 'amount'), new_amount,
|
if not isclose(safe_value_fallback(order, 'filled', 'amount'), new_amount,
|
||||||
abs_tol=constants.MATH_CLOSE_PREC):
|
abs_tol=constants.MATH_CLOSE_PREC):
|
||||||
order_obj.ft_fee_base = trade.amount - new_amount
|
order_obj.ft_fee_base = trade.amount - new_amount
|
||||||
except DependencyException as exception:
|
except DependencyException as exception:
|
||||||
logger.warning("Could not update trade amount: %s", exception)
|
logger.warning("Could not update trade amount: %s", exception)
|
||||||
|
|
||||||
def get_real_amount(self, trade: Trade, order: Dict) -> float:
|
def get_real_amount(self, trade: Trade, order: Dict, order_obj: Order) -> float:
|
||||||
"""
|
"""
|
||||||
Detect and update trade fee.
|
Detect and update trade fee.
|
||||||
Calls trade.update_fee() upon correct detection.
|
Calls trade.update_fee() upon correct detection.
|
||||||
|
@ -1666,7 +1662,7 @@ class FreqtradeBot(LoggingMixin):
|
||||||
# use fee from order-dict if possible
|
# use fee from order-dict if possible
|
||||||
if self.exchange.order_has_fee(order):
|
if self.exchange.order_has_fee(order):
|
||||||
fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(order)
|
fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(order)
|
||||||
logger.info(f"Fee for Trade {trade} [{order.get('side')}]: "
|
logger.info(f"Fee for Trade {trade} [{order_obj.ft_order_side}]: "
|
||||||
f"{fee_cost:.8g} {fee_currency} - rate: {fee_rate}")
|
f"{fee_cost:.8g} {fee_currency} - rate: {fee_rate}")
|
||||||
if fee_rate is None or fee_rate < 0.02:
|
if fee_rate is None or fee_rate < 0.02:
|
||||||
# Reject all fees that report as > 2%.
|
# Reject all fees that report as > 2%.
|
||||||
|
@ -1678,17 +1674,18 @@ class FreqtradeBot(LoggingMixin):
|
||||||
return self.apply_fee_conditional(trade, trade_base_currency,
|
return self.apply_fee_conditional(trade, trade_base_currency,
|
||||||
amount=order_amount, fee_abs=fee_cost)
|
amount=order_amount, fee_abs=fee_cost)
|
||||||
return order_amount
|
return order_amount
|
||||||
return self.fee_detection_from_trades(trade, order, order_amount, order.get('trades', []))
|
return self.fee_detection_from_trades(
|
||||||
|
trade, order, order_obj, order_amount, order.get('trades', []))
|
||||||
|
|
||||||
def fee_detection_from_trades(self, trade: Trade, order: Dict, order_amount: float,
|
def fee_detection_from_trades(self, trade: Trade, order: Dict, order_obj: Order,
|
||||||
trades: List) -> float:
|
order_amount: float, trades: List) -> float:
|
||||||
"""
|
"""
|
||||||
fee-detection fallback to Trades.
|
fee-detection fallback to Trades.
|
||||||
Either uses provided trades list or the result of fetch_my_trades to get correct fee.
|
Either uses provided trades list or the result of fetch_my_trades to get correct fee.
|
||||||
"""
|
"""
|
||||||
if not trades:
|
if not trades:
|
||||||
trades = self.exchange.get_trades_for_order(
|
trades = self.exchange.get_trades_for_order(
|
||||||
self.exchange.get_order_id_conditional(order), trade.pair, trade.open_date)
|
self.exchange.get_order_id_conditional(order), trade.pair, order_obj.order_date)
|
||||||
|
|
||||||
if len(trades) == 0:
|
if len(trades) == 0:
|
||||||
logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade)
|
logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade)
|
||||||
|
|
|
@ -90,7 +90,7 @@ class Backtesting:
|
||||||
validate_config_consistency(self.config)
|
validate_config_consistency(self.config)
|
||||||
|
|
||||||
if "timeframe" not in self.config:
|
if "timeframe" not in self.config:
|
||||||
raise OperationalException("Timeframe (ticker interval) needs to be set in either "
|
raise OperationalException("Timeframe needs to be set in either "
|
||||||
"configuration or as cli argument `--timeframe 5m`")
|
"configuration or as cli argument `--timeframe 5m`")
|
||||||
self.timeframe = str(self.config.get('timeframe'))
|
self.timeframe = str(self.config.get('timeframe'))
|
||||||
self.timeframe_min = timeframe_to_minutes(self.timeframe)
|
self.timeframe_min = timeframe_to_minutes(self.timeframe)
|
||||||
|
|
|
@ -29,15 +29,13 @@ class IHyperOpt(ABC):
|
||||||
Class attributes you can use:
|
Class attributes you can use:
|
||||||
timeframe -> int: value of the timeframe to use for the strategy
|
timeframe -> int: value of the timeframe to use for the strategy
|
||||||
"""
|
"""
|
||||||
ticker_interval: str # DEPRECATED
|
|
||||||
timeframe: str
|
timeframe: str
|
||||||
strategy: IStrategy
|
strategy: IStrategy
|
||||||
|
|
||||||
def __init__(self, config: dict) -> None:
|
def __init__(self, config: dict) -> None:
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
# Assign ticker_interval to be used in hyperopt
|
# Assign timeframe to be used in hyperopt
|
||||||
IHyperOpt.ticker_interval = str(config['timeframe']) # DEPRECATED
|
|
||||||
IHyperOpt.timeframe = str(config['timeframe'])
|
IHyperOpt.timeframe = str(config['timeframe'])
|
||||||
|
|
||||||
def generate_estimator(self, dimensions: List[Dimension], **kwargs) -> EstimatorType:
|
def generate_estimator(self, dimensions: List[Dimension], **kwargs) -> EstimatorType:
|
||||||
|
@ -192,7 +190,7 @@ class IHyperOpt(ABC):
|
||||||
Categorical([True, False], name='trailing_only_offset_is_reached'),
|
Categorical([True, False], name='trailing_only_offset_is_reached'),
|
||||||
]
|
]
|
||||||
|
|
||||||
# This is needed for proper unpickling the class attribute ticker_interval
|
# This is needed for proper unpickling the class attribute timeframe
|
||||||
# which is set to the actual value by the resolver.
|
# which is set to the actual value by the resolver.
|
||||||
# Why do I still need such shamanic mantras in modern python?
|
# Why do I still need such shamanic mantras in modern python?
|
||||||
def __getstate__(self):
|
def __getstate__(self):
|
||||||
|
@ -202,5 +200,4 @@ class IHyperOpt(ABC):
|
||||||
|
|
||||||
def __setstate__(self, state):
|
def __setstate__(self, state):
|
||||||
self.__dict__.update(state)
|
self.__dict__.update(state)
|
||||||
IHyperOpt.ticker_interval = state['timeframe']
|
|
||||||
IHyperOpt.timeframe = state['timeframe']
|
IHyperOpt.timeframe = state['timeframe']
|
||||||
|
|
|
@ -382,7 +382,7 @@ def generate_strategy_stats(pairlist: List[str],
|
||||||
enter_tag_results = generate_tag_metrics("enter_tag", starting_balance=start_balance,
|
enter_tag_results = generate_tag_metrics("enter_tag", starting_balance=start_balance,
|
||||||
results=results, skip_nan=False)
|
results=results, skip_nan=False)
|
||||||
|
|
||||||
sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades,
|
exit_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades,
|
||||||
results=results)
|
results=results)
|
||||||
left_open_results = generate_pair_metrics(pairlist, stake_currency=stake_currency,
|
left_open_results = generate_pair_metrics(pairlist, stake_currency=stake_currency,
|
||||||
starting_balance=start_balance,
|
starting_balance=start_balance,
|
||||||
|
@ -406,7 +406,7 @@ def generate_strategy_stats(pairlist: List[str],
|
||||||
'worst_pair': worst_pair,
|
'worst_pair': worst_pair,
|
||||||
'results_per_pair': pair_results,
|
'results_per_pair': pair_results,
|
||||||
'results_per_enter_tag': enter_tag_results,
|
'results_per_enter_tag': enter_tag_results,
|
||||||
'sell_reason_summary': sell_reason_stats,
|
'sell_reason_summary': exit_reason_stats,
|
||||||
'left_open_trades': left_open_results,
|
'left_open_trades': left_open_results,
|
||||||
# 'days_breakdown_stats': days_breakdown_stats,
|
# 'days_breakdown_stats': days_breakdown_stats,
|
||||||
|
|
||||||
|
@ -572,16 +572,16 @@ def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: st
|
||||||
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")
|
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")
|
||||||
|
|
||||||
|
|
||||||
def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_currency: str) -> str:
|
def text_table_exit_reason(sell_reason_stats: List[Dict[str, Any]], stake_currency: str) -> str:
|
||||||
"""
|
"""
|
||||||
Generate small table outlining Backtest results
|
Generate small table outlining Backtest results
|
||||||
:param sell_reason_stats: Sell reason metrics
|
:param sell_reason_stats: Exit reason metrics
|
||||||
:param stake_currency: Stakecurrency used
|
:param stake_currency: Stakecurrency used
|
||||||
:return: pretty printed table with tabulate as string
|
:return: pretty printed table with tabulate as string
|
||||||
"""
|
"""
|
||||||
headers = [
|
headers = [
|
||||||
'Sell Reason',
|
'Exit Reason',
|
||||||
'Sells',
|
'Exits',
|
||||||
'Win Draws Loss Win%',
|
'Win Draws Loss Win%',
|
||||||
'Avg Profit %',
|
'Avg Profit %',
|
||||||
'Cum Profit %',
|
'Cum Profit %',
|
||||||
|
@ -748,7 +748,7 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
||||||
f"{strat_results['draw_days']} / {strat_results['losing_days']}"),
|
f"{strat_results['draw_days']} / {strat_results['losing_days']}"),
|
||||||
('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"),
|
('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"),
|
||||||
('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"),
|
('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"),
|
||||||
('Rejected Buy signals', strat_results.get('rejected_signals', 'N/A')),
|
('Rejected Entry signals', strat_results.get('rejected_signals', 'N/A')),
|
||||||
('Entry/Exit Timeouts',
|
('Entry/Exit Timeouts',
|
||||||
f"{strat_results.get('timedout_entry_orders', 'N/A')} / "
|
f"{strat_results.get('timedout_entry_orders', 'N/A')} / "
|
||||||
f"{strat_results.get('timedout_exit_orders', 'N/A')}"),
|
f"{strat_results.get('timedout_exit_orders', 'N/A')}"),
|
||||||
|
@ -810,13 +810,13 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
|
||||||
stake_currency=stake_currency)
|
stake_currency=stake_currency)
|
||||||
|
|
||||||
if isinstance(table, str) and len(table) > 0:
|
if isinstance(table, str) and len(table) > 0:
|
||||||
print(' BUY TAG STATS '.center(len(table.splitlines()[0]), '='))
|
print(' ENTER TAG STATS '.center(len(table.splitlines()[0]), '='))
|
||||||
print(table)
|
print(table)
|
||||||
|
|
||||||
table = text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'],
|
table = text_table_exit_reason(sell_reason_stats=results['sell_reason_summary'],
|
||||||
stake_currency=stake_currency)
|
stake_currency=stake_currency)
|
||||||
if isinstance(table, str) and len(table) > 0:
|
if isinstance(table, str) and len(table) > 0:
|
||||||
print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '='))
|
print(' EXIT REASON STATS '.center(len(table.splitlines()[0]), '='))
|
||||||
print(table)
|
print(table)
|
||||||
|
|
||||||
table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency)
|
table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency)
|
||||||
|
|
|
@ -718,7 +718,7 @@ class LocalTrade():
|
||||||
|
|
||||||
zero = Decimal(0.0)
|
zero = Decimal(0.0)
|
||||||
# If nothing was borrowed
|
# If nothing was borrowed
|
||||||
if self.has_no_leverage or self.trading_mode != TradingMode.MARGIN:
|
if self.trading_mode != TradingMode.MARGIN or self.has_no_leverage:
|
||||||
return zero
|
return zero
|
||||||
|
|
||||||
open_date = self.open_date.replace(tzinfo=None)
|
open_date = self.open_date.replace(tzinfo=None)
|
||||||
|
|
|
@ -44,7 +44,6 @@ class HyperOptLossResolver(IResolver):
|
||||||
extra_dir=config.get('hyperopt_path'))
|
extra_dir=config.get('hyperopt_path'))
|
||||||
|
|
||||||
# Assign timeframe to be used in hyperopt
|
# Assign timeframe to be used in hyperopt
|
||||||
hyperoptloss.__class__.ticker_interval = str(config['timeframe'])
|
|
||||||
hyperoptloss.__class__.timeframe = str(config['timeframe'])
|
hyperoptloss.__class__.timeframe = str(config['timeframe'])
|
||||||
|
|
||||||
return hyperoptloss
|
return hyperoptloss
|
||||||
|
|
|
@ -6,6 +6,7 @@ This module load custom objects
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union
|
from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union
|
||||||
|
|
||||||
|
@ -15,6 +16,22 @@ from freqtrade.exceptions import OperationalException
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PathModifier:
|
||||||
|
def __init__(self, path: Path):
|
||||||
|
self.path = path
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
"""Inject path to allow importing with relative imports."""
|
||||||
|
sys.path.insert(0, str(self.path))
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
"""Undo insertion of local path."""
|
||||||
|
str_path = str(self.path)
|
||||||
|
if str_path in sys.path:
|
||||||
|
sys.path.remove(str_path)
|
||||||
|
|
||||||
|
|
||||||
class IResolver:
|
class IResolver:
|
||||||
"""
|
"""
|
||||||
This class contains all the logic to load custom classes
|
This class contains all the logic to load custom classes
|
||||||
|
@ -57,7 +74,9 @@ class IResolver:
|
||||||
|
|
||||||
# Generate spec based on absolute path
|
# Generate spec based on absolute path
|
||||||
# Pass object_name as first argument to have logging print a reasonable name.
|
# Pass object_name as first argument to have logging print a reasonable name.
|
||||||
spec = importlib.util.spec_from_file_location(object_name or "", str(module_path))
|
with PathModifier(module_path.parent):
|
||||||
|
module_name = module_path.stem or ""
|
||||||
|
spec = importlib.util.spec_from_file_location(module_name, str(module_path))
|
||||||
if not spec:
|
if not spec:
|
||||||
return iter([None])
|
return iter([None])
|
||||||
|
|
||||||
|
@ -75,8 +94,11 @@ class IResolver:
|
||||||
name, obj in inspect.getmembers(
|
name, obj in inspect.getmembers(
|
||||||
module, inspect.isclass) if ((object_name is None or object_name == name)
|
module, inspect.isclass) if ((object_name is None or object_name == name)
|
||||||
and issubclass(obj, cls.object_type)
|
and issubclass(obj, cls.object_type)
|
||||||
and obj is not cls.object_type)
|
and obj is not cls.object_type
|
||||||
|
and obj.__module__ == module_name
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
# The __module__ check ensures we only use strategies that are defined in this folder.
|
||||||
return valid_objects_gen
|
return valid_objects_gen
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -47,14 +47,6 @@ class StrategyResolver(IResolver):
|
||||||
strategy_name, config=config,
|
strategy_name, config=config,
|
||||||
extra_dir=config.get('strategy_path'))
|
extra_dir=config.get('strategy_path'))
|
||||||
|
|
||||||
if hasattr(strategy, 'ticker_interval') and not hasattr(strategy, 'timeframe'):
|
|
||||||
# Assign ticker_interval to timeframe to keep compatibility
|
|
||||||
if 'timeframe' not in config:
|
|
||||||
logger.warning(
|
|
||||||
"DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'."
|
|
||||||
)
|
|
||||||
strategy.timeframe = strategy.ticker_interval
|
|
||||||
|
|
||||||
if strategy._ft_params_from_file:
|
if strategy._ft_params_from_file:
|
||||||
# Set parameters from Hyperopt results file
|
# Set parameters from Hyperopt results file
|
||||||
params = strategy._ft_params_from_file
|
params = strategy._ft_params_from_file
|
||||||
|
@ -147,10 +139,6 @@ class StrategyResolver(IResolver):
|
||||||
"""
|
"""
|
||||||
Normalize attributes to have the correct type.
|
Normalize attributes to have the correct type.
|
||||||
"""
|
"""
|
||||||
# Assign deprecated variable - to not break users code relying on this.
|
|
||||||
if hasattr(strategy, 'timeframe'):
|
|
||||||
strategy.ticker_interval = strategy.timeframe
|
|
||||||
|
|
||||||
# Sort and apply type conversions
|
# Sort and apply type conversions
|
||||||
if hasattr(strategy, 'minimal_roi'):
|
if hasattr(strategy, 'minimal_roi'):
|
||||||
strategy.minimal_roi = dict(sorted(
|
strategy.minimal_roi = dict(sorted(
|
||||||
|
|
|
@ -141,7 +141,7 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g
|
||||||
def forceentry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)):
|
def forceentry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)):
|
||||||
ordertype = payload.ordertype.value if payload.ordertype else None
|
ordertype = payload.ordertype.value if payload.ordertype else None
|
||||||
stake_amount = payload.stakeamount if payload.stakeamount else None
|
stake_amount = payload.stakeamount if payload.stakeamount else None
|
||||||
entry_tag = payload.entry_tag if payload.entry_tag else None
|
entry_tag = payload.entry_tag if payload.entry_tag else 'forceentry'
|
||||||
|
|
||||||
trade = rpc._rpc_force_entry(payload.pair, payload.price, order_side=payload.side,
|
trade = rpc._rpc_force_entry(payload.pair, payload.price, order_side=payload.side,
|
||||||
order_type=ordertype, stake_amount=stake_amount,
|
order_type=ordertype, stake_amount=stake_amount,
|
||||||
|
|
|
@ -747,7 +747,7 @@ class RPC:
|
||||||
order_type: Optional[str] = None,
|
order_type: Optional[str] = None,
|
||||||
order_side: SignalDirection = SignalDirection.LONG,
|
order_side: SignalDirection = SignalDirection.LONG,
|
||||||
stake_amount: Optional[float] = None,
|
stake_amount: Optional[float] = None,
|
||||||
enter_tag: Optional[str] = None) -> Optional[Trade]:
|
enter_tag: Optional[str] = 'forceentry') -> Optional[Trade]:
|
||||||
"""
|
"""
|
||||||
Handler for forcebuy <asset> <price>
|
Handler for forcebuy <asset> <price>
|
||||||
Buys a pair trade at the given or current price
|
Buys a pair trade at the given or current price
|
||||||
|
|
|
@ -56,7 +56,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||||
Attributes you can use:
|
Attributes you can use:
|
||||||
minimal_roi -> Dict: Minimal ROI designed for the strategy
|
minimal_roi -> Dict: Minimal ROI designed for the strategy
|
||||||
stoploss -> float: optimal stoploss designed for the strategy
|
stoploss -> float: optimal stoploss designed for the strategy
|
||||||
timeframe -> str: value of the timeframe (ticker interval) to use with the strategy
|
timeframe -> str: value of the timeframe to use with the strategy
|
||||||
"""
|
"""
|
||||||
# Strategy interface version
|
# Strategy interface version
|
||||||
# Default to version 2
|
# Default to version 2
|
||||||
|
@ -86,7 +86,6 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||||
can_short: bool = False
|
can_short: bool = False
|
||||||
|
|
||||||
# associated timeframe
|
# associated timeframe
|
||||||
ticker_interval: str # DEPRECATED
|
|
||||||
timeframe: str
|
timeframe: str
|
||||||
|
|
||||||
# Optional order types
|
# Optional order types
|
||||||
|
|
|
@ -1,383 +0,0 @@
|
||||||
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
|
||||||
# flake8: noqa: F401
|
|
||||||
# isort: skip_file
|
|
||||||
# --- Do not remove these libs ---
|
|
||||||
import numpy as np # noqa
|
|
||||||
import pandas as pd # noqa
|
|
||||||
from pandas import DataFrame
|
|
||||||
|
|
||||||
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
|
||||||
IStrategy, IntParameter)
|
|
||||||
|
|
||||||
# --------------------------------
|
|
||||||
# Add your lib to import here
|
|
||||||
import talib.abstract as ta
|
|
||||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: Create a meaningfull short strategy (not just revresed signs).
|
|
||||||
# This class is a sample. Feel free to customize it.
|
|
||||||
class SampleShortStrategy(IStrategy):
|
|
||||||
"""
|
|
||||||
This is a sample strategy to inspire you.
|
|
||||||
More information in https://www.freqtrade.io/en/latest/strategy-customization/
|
|
||||||
|
|
||||||
You can:
|
|
||||||
:return: a Dataframe with all mandatory indicators for the strategies
|
|
||||||
- Rename the class name (Do not forget to update class_name)
|
|
||||||
- Add any methods you want to build your strategy
|
|
||||||
- Add any lib you need to build your strategy
|
|
||||||
|
|
||||||
You must keep:
|
|
||||||
- the lib in the section "Do not remove these libs"
|
|
||||||
- the methods: populate_indicators, populate_entry_trend, populate_exit_trend
|
|
||||||
You should keep:
|
|
||||||
- timeframe, minimal_roi, stoploss, trailing_*
|
|
||||||
"""
|
|
||||||
# Strategy interface version - allow new iterations of the strategy interface.
|
|
||||||
# Check the documentation or the Sample strategy to get the latest version.
|
|
||||||
INTERFACE_VERSION = 3
|
|
||||||
|
|
||||||
# Can this strategy go short?
|
|
||||||
can_short: bool = True
|
|
||||||
|
|
||||||
# Minimal ROI designed for the strategy.
|
|
||||||
# This attribute will be overridden if the config file contains "minimal_roi".
|
|
||||||
minimal_roi = {
|
|
||||||
"60": 0.01,
|
|
||||||
"30": 0.02,
|
|
||||||
"0": 0.04
|
|
||||||
}
|
|
||||||
|
|
||||||
# Optimal stoploss designed for the strategy.
|
|
||||||
# This attribute will be overridden if the config file contains "stoploss".
|
|
||||||
stoploss = -0.10
|
|
||||||
|
|
||||||
# Trailing stoploss
|
|
||||||
trailing_stop = False
|
|
||||||
# trailing_only_offset_is_reached = False
|
|
||||||
# trailing_stop_positive = 0.01
|
|
||||||
# trailing_stop_positive_offset = 0.0 # Disabled / not configured
|
|
||||||
|
|
||||||
# Hyperoptable parameters
|
|
||||||
short_rsi = IntParameter(low=51, high=100, default=70, space='sell', optimize=True, load=True)
|
|
||||||
exit_short_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True)
|
|
||||||
|
|
||||||
# Optimal timeframe for the strategy.
|
|
||||||
timeframe = '5m'
|
|
||||||
|
|
||||||
# Run "populate_indicators()" only for new candle.
|
|
||||||
process_only_new_candles = False
|
|
||||||
|
|
||||||
# These values can be overridden in the "ask_strategy" section in the config.
|
|
||||||
use_sell_signal = True
|
|
||||||
sell_profit_only = False
|
|
||||||
ignore_roi_if_buy_signal = False
|
|
||||||
|
|
||||||
# Number of candles the strategy requires before producing valid signals
|
|
||||||
startup_candle_count: int = 30
|
|
||||||
|
|
||||||
# Optional order type mapping.
|
|
||||||
order_types = {
|
|
||||||
'entry': 'limit',
|
|
||||||
'exit': 'limit',
|
|
||||||
'stoploss': 'market',
|
|
||||||
'stoploss_on_exchange': False
|
|
||||||
}
|
|
||||||
|
|
||||||
# Optional order time in force.
|
|
||||||
order_time_in_force = {
|
|
||||||
'entry': 'gtc',
|
|
||||||
'exit': 'gtc'
|
|
||||||
}
|
|
||||||
|
|
||||||
plot_config = {
|
|
||||||
'main_plot': {
|
|
||||||
'tema': {},
|
|
||||||
'sar': {'color': 'white'},
|
|
||||||
},
|
|
||||||
'subplots': {
|
|
||||||
"MACD": {
|
|
||||||
'macd': {'color': 'blue'},
|
|
||||||
'macdsignal': {'color': 'orange'},
|
|
||||||
},
|
|
||||||
"RSI": {
|
|
||||||
'rsi': {'color': 'red'},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def informative_pairs(self):
|
|
||||||
"""
|
|
||||||
Define additional, informative pair/interval combinations to be cached from the exchange.
|
|
||||||
These pair/interval combinations are non-tradeable, unless they are part
|
|
||||||
of the whitelist as well.
|
|
||||||
For more information, please consult the documentation
|
|
||||||
:return: List of tuples in the format (pair, interval)
|
|
||||||
Sample: return [("ETH/USDT", "5m"),
|
|
||||||
("BTC/USDT", "15m"),
|
|
||||||
]
|
|
||||||
"""
|
|
||||||
return []
|
|
||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Adds several different TA indicators to the given DataFrame
|
|
||||||
|
|
||||||
Performance Note: For the best performance be frugal on the number of indicators
|
|
||||||
you are using. Let uncomment only the indicator you are using in your strategies
|
|
||||||
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
|
||||||
:param dataframe: Dataframe with data from the exchange
|
|
||||||
:param metadata: Additional information, like the currently traded pair
|
|
||||||
:return: a Dataframe with all mandatory indicators for the strategies
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Momentum Indicators
|
|
||||||
# ------------------------------------
|
|
||||||
|
|
||||||
# ADX
|
|
||||||
dataframe['adx'] = ta.ADX(dataframe)
|
|
||||||
|
|
||||||
# # Plus Directional Indicator / Movement
|
|
||||||
# dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
|
|
||||||
# dataframe['plus_di'] = ta.PLUS_DI(dataframe)
|
|
||||||
|
|
||||||
# # Minus Directional Indicator / Movement
|
|
||||||
# dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
|
|
||||||
# dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
|
||||||
|
|
||||||
# # Aroon, Aroon Oscillator
|
|
||||||
# aroon = ta.AROON(dataframe)
|
|
||||||
# dataframe['aroonup'] = aroon['aroonup']
|
|
||||||
# dataframe['aroondown'] = aroon['aroondown']
|
|
||||||
# dataframe['aroonosc'] = ta.AROONOSC(dataframe)
|
|
||||||
|
|
||||||
# # Awesome Oscillator
|
|
||||||
# dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
|
|
||||||
|
|
||||||
# # Keltner Channel
|
|
||||||
# keltner = qtpylib.keltner_channel(dataframe)
|
|
||||||
# dataframe["kc_upperband"] = keltner["upper"]
|
|
||||||
# dataframe["kc_lowerband"] = keltner["lower"]
|
|
||||||
# dataframe["kc_middleband"] = keltner["mid"]
|
|
||||||
# dataframe["kc_percent"] = (
|
|
||||||
# (dataframe["close"] - dataframe["kc_lowerband"]) /
|
|
||||||
# (dataframe["kc_upperband"] - dataframe["kc_lowerband"])
|
|
||||||
# )
|
|
||||||
# dataframe["kc_width"] = (
|
|
||||||
# (dataframe["kc_upperband"] - dataframe["kc_lowerband"]) / dataframe["kc_middleband"]
|
|
||||||
# )
|
|
||||||
|
|
||||||
# # Ultimate Oscillator
|
|
||||||
# dataframe['uo'] = ta.ULTOSC(dataframe)
|
|
||||||
|
|
||||||
# # Commodity Channel Index: values [Oversold:-100, Overbought:100]
|
|
||||||
# dataframe['cci'] = ta.CCI(dataframe)
|
|
||||||
|
|
||||||
# RSI
|
|
||||||
dataframe['rsi'] = ta.RSI(dataframe)
|
|
||||||
|
|
||||||
# # Inverse Fisher transform on RSI: values [-1.0, 1.0] (https://goo.gl/2JGGoy)
|
|
||||||
# rsi = 0.1 * (dataframe['rsi'] - 50)
|
|
||||||
# dataframe['fisher_rsi'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1)
|
|
||||||
|
|
||||||
# # Inverse Fisher transform on RSI normalized: values [0.0, 100.0] (https://goo.gl/2JGGoy)
|
|
||||||
# dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
|
|
||||||
|
|
||||||
# # Stochastic Slow
|
|
||||||
# stoch = ta.STOCH(dataframe)
|
|
||||||
# dataframe['slowd'] = stoch['slowd']
|
|
||||||
# dataframe['slowk'] = stoch['slowk']
|
|
||||||
|
|
||||||
# Stochastic Fast
|
|
||||||
stoch_fast = ta.STOCHF(dataframe)
|
|
||||||
dataframe['fastd'] = stoch_fast['fastd']
|
|
||||||
dataframe['fastk'] = stoch_fast['fastk']
|
|
||||||
|
|
||||||
# # Stochastic RSI
|
|
||||||
# Please read https://github.com/freqtrade/freqtrade/issues/2961 before using this.
|
|
||||||
# STOCHRSI is NOT aligned with tradingview, which may result in non-expected results.
|
|
||||||
# stoch_rsi = ta.STOCHRSI(dataframe)
|
|
||||||
# dataframe['fastd_rsi'] = stoch_rsi['fastd']
|
|
||||||
# dataframe['fastk_rsi'] = stoch_rsi['fastk']
|
|
||||||
|
|
||||||
# MACD
|
|
||||||
macd = ta.MACD(dataframe)
|
|
||||||
dataframe['macd'] = macd['macd']
|
|
||||||
dataframe['macdsignal'] = macd['macdsignal']
|
|
||||||
dataframe['macdhist'] = macd['macdhist']
|
|
||||||
|
|
||||||
# MFI
|
|
||||||
dataframe['mfi'] = ta.MFI(dataframe)
|
|
||||||
|
|
||||||
# # ROC
|
|
||||||
# dataframe['roc'] = ta.ROC(dataframe)
|
|
||||||
|
|
||||||
# Overlap Studies
|
|
||||||
# ------------------------------------
|
|
||||||
|
|
||||||
# Bollinger Bands
|
|
||||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
|
||||||
dataframe['bb_lowerband'] = bollinger['lower']
|
|
||||||
dataframe['bb_middleband'] = bollinger['mid']
|
|
||||||
dataframe['bb_upperband'] = bollinger['upper']
|
|
||||||
dataframe["bb_percent"] = (
|
|
||||||
(dataframe["close"] - dataframe["bb_lowerband"]) /
|
|
||||||
(dataframe["bb_upperband"] - dataframe["bb_lowerband"])
|
|
||||||
)
|
|
||||||
dataframe["bb_width"] = (
|
|
||||||
(dataframe["bb_upperband"] - dataframe["bb_lowerband"]) / dataframe["bb_middleband"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Bollinger Bands - Weighted (EMA based instead of SMA)
|
|
||||||
# weighted_bollinger = qtpylib.weighted_bollinger_bands(
|
|
||||||
# qtpylib.typical_price(dataframe), window=20, stds=2
|
|
||||||
# )
|
|
||||||
# dataframe["wbb_upperband"] = weighted_bollinger["upper"]
|
|
||||||
# dataframe["wbb_lowerband"] = weighted_bollinger["lower"]
|
|
||||||
# dataframe["wbb_middleband"] = weighted_bollinger["mid"]
|
|
||||||
# dataframe["wbb_percent"] = (
|
|
||||||
# (dataframe["close"] - dataframe["wbb_lowerband"]) /
|
|
||||||
# (dataframe["wbb_upperband"] - dataframe["wbb_lowerband"])
|
|
||||||
# )
|
|
||||||
# dataframe["wbb_width"] = (
|
|
||||||
# (dataframe["wbb_upperband"] - dataframe["wbb_lowerband"]) /
|
|
||||||
# dataframe["wbb_middleband"]
|
|
||||||
# )
|
|
||||||
|
|
||||||
# # EMA - Exponential Moving Average
|
|
||||||
# dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3)
|
|
||||||
# dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
|
|
||||||
# dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
|
|
||||||
# dataframe['ema21'] = ta.EMA(dataframe, timeperiod=21)
|
|
||||||
# dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
|
|
||||||
# dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
|
|
||||||
|
|
||||||
# # SMA - Simple Moving Average
|
|
||||||
# dataframe['sma3'] = ta.SMA(dataframe, timeperiod=3)
|
|
||||||
# dataframe['sma5'] = ta.SMA(dataframe, timeperiod=5)
|
|
||||||
# dataframe['sma10'] = ta.SMA(dataframe, timeperiod=10)
|
|
||||||
# dataframe['sma21'] = ta.SMA(dataframe, timeperiod=21)
|
|
||||||
# dataframe['sma50'] = ta.SMA(dataframe, timeperiod=50)
|
|
||||||
# dataframe['sma100'] = ta.SMA(dataframe, timeperiod=100)
|
|
||||||
|
|
||||||
# Parabolic SAR
|
|
||||||
dataframe['sar'] = ta.SAR(dataframe)
|
|
||||||
|
|
||||||
# TEMA - Triple Exponential Moving Average
|
|
||||||
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
|
|
||||||
|
|
||||||
# Cycle Indicator
|
|
||||||
# ------------------------------------
|
|
||||||
# Hilbert Transform Indicator - SineWave
|
|
||||||
hilbert = ta.HT_SINE(dataframe)
|
|
||||||
dataframe['htsine'] = hilbert['sine']
|
|
||||||
dataframe['htleadsine'] = hilbert['leadsine']
|
|
||||||
|
|
||||||
# Pattern Recognition - Bullish candlestick patterns
|
|
||||||
# ------------------------------------
|
|
||||||
# # Hammer: values [0, 100]
|
|
||||||
# dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
|
|
||||||
# # Inverted Hammer: values [0, 100]
|
|
||||||
# dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
|
|
||||||
# # Dragonfly Doji: values [0, 100]
|
|
||||||
# dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
|
|
||||||
# # Piercing Line: values [0, 100]
|
|
||||||
# dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
|
|
||||||
# # Morningstar: values [0, 100]
|
|
||||||
# dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
|
|
||||||
# # Three White Soldiers: values [0, 100]
|
|
||||||
# dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
|
|
||||||
|
|
||||||
# Pattern Recognition - Bearish candlestick patterns
|
|
||||||
# ------------------------------------
|
|
||||||
# # Hanging Man: values [0, 100]
|
|
||||||
# dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
|
|
||||||
# # Shooting Star: values [0, 100]
|
|
||||||
# dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
|
|
||||||
# # Gravestone Doji: values [0, 100]
|
|
||||||
# dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
|
|
||||||
# # Dark Cloud Cover: values [0, 100]
|
|
||||||
# dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
|
|
||||||
# # Evening Doji Star: values [0, 100]
|
|
||||||
# dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
|
|
||||||
# # Evening Star: values [0, 100]
|
|
||||||
# dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
|
|
||||||
|
|
||||||
# Pattern Recognition - Bullish/Bearish candlestick patterns
|
|
||||||
# ------------------------------------
|
|
||||||
# # Three Line Strike: values [0, -100, 100]
|
|
||||||
# dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
|
|
||||||
# # Spinning Top: values [0, -100, 100]
|
|
||||||
# dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
|
|
||||||
# # Engulfing: values [0, -100, 100]
|
|
||||||
# dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
|
|
||||||
# # Harami: values [0, -100, 100]
|
|
||||||
# dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
|
|
||||||
# # Three Outside Up/Down: values [0, -100, 100]
|
|
||||||
# dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
|
|
||||||
# # Three Inside Up/Down: values [0, -100, 100]
|
|
||||||
# dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
|
|
||||||
|
|
||||||
# # Chart type
|
|
||||||
# # ------------------------------------
|
|
||||||
# # Heikin Ashi Strategy
|
|
||||||
# heikinashi = qtpylib.heikinashi(dataframe)
|
|
||||||
# dataframe['ha_open'] = heikinashi['open']
|
|
||||||
# dataframe['ha_close'] = heikinashi['close']
|
|
||||||
# dataframe['ha_high'] = heikinashi['high']
|
|
||||||
# dataframe['ha_low'] = heikinashi['low']
|
|
||||||
|
|
||||||
# Retrieve best bid and best ask from the orderbook
|
|
||||||
# ------------------------------------
|
|
||||||
"""
|
|
||||||
# first check if dataprovider is available
|
|
||||||
if self.dp:
|
|
||||||
if self.dp.runmode.value in ('live', 'dry_run'):
|
|
||||||
ob = self.dp.orderbook(metadata['pair'], 1)
|
|
||||||
dataframe['best_bid'] = ob['bids'][0][0]
|
|
||||||
dataframe['best_ask'] = ob['asks'][0][0]
|
|
||||||
"""
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Based on TA indicators, populates the buy signal for the given dataframe
|
|
||||||
:param dataframe: DataFrame populated with indicators
|
|
||||||
:param metadata: Additional information, like the currently traded pair
|
|
||||||
:return: DataFrame with buy column
|
|
||||||
"""
|
|
||||||
|
|
||||||
dataframe.loc[
|
|
||||||
(
|
|
||||||
# Signal: RSI crosses above 70
|
|
||||||
(qtpylib.crossed_above(dataframe['rsi'], self.short_rsi.value)) &
|
|
||||||
(dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle
|
|
||||||
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling
|
|
||||||
(dataframe['volume'] > 0) # Make sure Volume is not 0
|
|
||||||
),
|
|
||||||
'enter_short'] = 1
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Based on TA indicators, populates the sell signal for the given dataframe
|
|
||||||
:param dataframe: DataFrame populated with indicators
|
|
||||||
:param metadata: Additional information, like the currently traded pair
|
|
||||||
:return: DataFrame with sell column
|
|
||||||
"""
|
|
||||||
|
|
||||||
dataframe.loc[
|
|
||||||
(
|
|
||||||
# Signal: RSI crosses above 30
|
|
||||||
(qtpylib.crossed_above(dataframe['rsi'], self.exit_short_rsi.value)) &
|
|
||||||
# Guard: tema below BB middle
|
|
||||||
(dataframe['tema'] <= dataframe['bb_middleband']) &
|
|
||||||
(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising
|
|
||||||
(dataframe['volume'] > 0) # Make sure Volume is not 0
|
|
||||||
),
|
|
||||||
'exit_short'] = 1
|
|
||||||
|
|
||||||
return dataframe
|
|
|
@ -6,8 +6,8 @@
|
||||||
coveralls==3.3.1
|
coveralls==3.3.1
|
||||||
flake8==4.0.1
|
flake8==4.0.1
|
||||||
flake8-tidy-imports==4.6.0
|
flake8-tidy-imports==4.6.0
|
||||||
mypy==0.940
|
mypy==0.941
|
||||||
pytest==7.1.0
|
pytest==7.1.1
|
||||||
pytest-asyncio==0.18.2
|
pytest-asyncio==0.18.2
|
||||||
pytest-cov==3.0.0
|
pytest-cov==3.0.0
|
||||||
pytest-mock==3.7.0
|
pytest-mock==3.7.0
|
||||||
|
@ -22,8 +22,8 @@ nbconvert==6.4.4
|
||||||
# mypy types
|
# mypy types
|
||||||
types-cachetools==5.0.0
|
types-cachetools==5.0.0
|
||||||
types-filelock==3.2.5
|
types-filelock==3.2.5
|
||||||
types-requests==2.27.12
|
types-requests==2.27.14
|
||||||
types-tabulate==0.8.5
|
types-tabulate==0.8.6
|
||||||
|
|
||||||
# Extensions to datetime library
|
# Extensions to datetime library
|
||||||
types-python-dateutil==2.8.9
|
types-python-dateutil==2.8.10
|
|
@ -2,16 +2,16 @@ numpy==1.22.3
|
||||||
pandas==1.4.1
|
pandas==1.4.1
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==1.76.5
|
ccxt==1.76.65
|
||||||
# Pin cryptography for now due to rust build errors with piwheels
|
# Pin cryptography for now due to rust build errors with piwheels
|
||||||
cryptography==36.0.1
|
cryptography==36.0.2
|
||||||
aiohttp==3.8.1
|
aiohttp==3.8.1
|
||||||
SQLAlchemy==1.4.32
|
SQLAlchemy==1.4.32
|
||||||
python-telegram-bot==13.11
|
python-telegram-bot==13.11
|
||||||
arrow==1.2.2
|
arrow==1.2.2
|
||||||
cachetools==4.2.2
|
cachetools==4.2.2
|
||||||
requests==2.27.1
|
requests==2.27.1
|
||||||
urllib3==1.26.8
|
urllib3==1.26.9
|
||||||
jsonschema==4.4.0
|
jsonschema==4.4.0
|
||||||
TA-Lib==0.4.24
|
TA-Lib==0.4.24
|
||||||
technical==1.3.0
|
technical==1.3.0
|
||||||
|
|
|
@ -144,7 +144,8 @@ def test_load_data_with_new_pair_1min(ohlcv_history_list, mocker, caplog,
|
||||||
|
|
||||||
# download a new pair if refresh_pairs is set
|
# download a new pair if refresh_pairs is set
|
||||||
refresh_data(datadir=tmpdir1, timeframe='1m', pairs=['MEME/BTC'],
|
refresh_data(datadir=tmpdir1, timeframe='1m', pairs=['MEME/BTC'],
|
||||||
exchange=exchange)
|
exchange=exchange, candle_type=CandleType.SPOT
|
||||||
|
)
|
||||||
load_pair_history(datadir=tmpdir1, timeframe='1m', pair='MEME/BTC', candle_type=candle_type)
|
load_pair_history(datadir=tmpdir1, timeframe='1m', pair='MEME/BTC', candle_type=candle_type)
|
||||||
assert file.is_file()
|
assert file.is_file()
|
||||||
assert log_has_re(
|
assert log_has_re(
|
||||||
|
@ -222,14 +223,16 @@ def test_load_cached_data_for_updating(mocker, testdatadir) -> None:
|
||||||
# timeframe starts earlier than the cached data
|
# timeframe starts earlier than the cached data
|
||||||
# should fully update data
|
# should fully update data
|
||||||
timerange = TimeRange('date', None, test_data[0][0] / 1000 - 1, 0)
|
timerange = TimeRange('date', None, test_data[0][0] / 1000 - 1, 0)
|
||||||
data, start_ts = _load_cached_data_for_updating('UNITTEST/BTC', '1m', timerange, data_handler)
|
data, start_ts = _load_cached_data_for_updating(
|
||||||
|
'UNITTEST/BTC', '1m', timerange, data_handler, CandleType.SPOT)
|
||||||
assert data.empty
|
assert data.empty
|
||||||
assert start_ts == test_data[0][0] - 1000
|
assert start_ts == test_data[0][0] - 1000
|
||||||
|
|
||||||
# timeframe starts in the center of the cached data
|
# timeframe starts in the center of the cached data
|
||||||
# should return the cached data w/o the last item
|
# should return the cached data w/o the last item
|
||||||
timerange = TimeRange('date', None, test_data[0][0] / 1000 + 1, 0)
|
timerange = TimeRange('date', None, test_data[0][0] / 1000 + 1, 0)
|
||||||
data, start_ts = _load_cached_data_for_updating('UNITTEST/BTC', '1m', timerange, data_handler)
|
data, start_ts = _load_cached_data_for_updating(
|
||||||
|
'UNITTEST/BTC', '1m', timerange, data_handler, CandleType.SPOT)
|
||||||
|
|
||||||
assert_frame_equal(data, test_data_df.iloc[:-1])
|
assert_frame_equal(data, test_data_df.iloc[:-1])
|
||||||
assert test_data[-2][0] <= start_ts < test_data[-1][0]
|
assert test_data[-2][0] <= start_ts < test_data[-1][0]
|
||||||
|
@ -237,20 +240,23 @@ def test_load_cached_data_for_updating(mocker, testdatadir) -> None:
|
||||||
# timeframe starts after the cached data
|
# timeframe starts after the cached data
|
||||||
# should return the cached data w/o the last item
|
# should return the cached data w/o the last item
|
||||||
timerange = TimeRange('date', None, test_data[-1][0] / 1000 + 100, 0)
|
timerange = TimeRange('date', None, test_data[-1][0] / 1000 + 100, 0)
|
||||||
data, start_ts = _load_cached_data_for_updating('UNITTEST/BTC', '1m', timerange, data_handler)
|
data, start_ts = _load_cached_data_for_updating(
|
||||||
|
'UNITTEST/BTC', '1m', timerange, data_handler, CandleType.SPOT)
|
||||||
assert_frame_equal(data, test_data_df.iloc[:-1])
|
assert_frame_equal(data, test_data_df.iloc[:-1])
|
||||||
assert test_data[-2][0] <= start_ts < test_data[-1][0]
|
assert test_data[-2][0] <= start_ts < test_data[-1][0]
|
||||||
|
|
||||||
# no datafile exist
|
# no datafile exist
|
||||||
# should return timestamp start time
|
# should return timestamp start time
|
||||||
timerange = TimeRange('date', None, now_ts - 10000, 0)
|
timerange = TimeRange('date', None, now_ts - 10000, 0)
|
||||||
data, start_ts = _load_cached_data_for_updating('NONEXIST/BTC', '1m', timerange, data_handler)
|
data, start_ts = _load_cached_data_for_updating(
|
||||||
|
'NONEXIST/BTC', '1m', timerange, data_handler, CandleType.SPOT)
|
||||||
assert data.empty
|
assert data.empty
|
||||||
assert start_ts == (now_ts - 10000) * 1000
|
assert start_ts == (now_ts - 10000) * 1000
|
||||||
|
|
||||||
# no datafile exist, no timeframe is set
|
# no datafile exist, no timeframe is set
|
||||||
# should return an empty array and None
|
# should return an empty array and None
|
||||||
data, start_ts = _load_cached_data_for_updating('NONEXIST/BTC', '1m', None, data_handler)
|
data, start_ts = _load_cached_data_for_updating(
|
||||||
|
'NONEXIST/BTC', '1m', None, data_handler, CandleType.SPOT)
|
||||||
assert data.empty
|
assert data.empty
|
||||||
assert start_ts is None
|
assert start_ts is None
|
||||||
|
|
||||||
|
@ -322,9 +328,9 @@ def test_download_pair_history2(mocker, default_conf, testdatadir) -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=tick)
|
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=tick)
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
_download_pair_history(datadir=testdatadir, exchange=exchange, pair="UNITTEST/BTC",
|
_download_pair_history(datadir=testdatadir, exchange=exchange, pair="UNITTEST/BTC",
|
||||||
timeframe='1m')
|
timeframe='1m', candle_type='spot')
|
||||||
_download_pair_history(datadir=testdatadir, exchange=exchange, pair="UNITTEST/BTC",
|
_download_pair_history(datadir=testdatadir, exchange=exchange, pair="UNITTEST/BTC",
|
||||||
timeframe='3m')
|
timeframe='3m', candle_type='spot')
|
||||||
_download_pair_history(datadir=testdatadir, exchange=exchange, pair="UNITTEST/USDT",
|
_download_pair_history(datadir=testdatadir, exchange=exchange, pair="UNITTEST/USDT",
|
||||||
timeframe='1h', candle_type='mark')
|
timeframe='1h', candle_type='mark')
|
||||||
assert json_dump_mock.call_count == 3
|
assert json_dump_mock.call_count == 3
|
||||||
|
@ -338,7 +344,7 @@ def test_download_backtesting_data_exception(mocker, caplog, default_conf, tmpdi
|
||||||
|
|
||||||
assert not _download_pair_history(datadir=tmpdir1, exchange=exchange,
|
assert not _download_pair_history(datadir=tmpdir1, exchange=exchange,
|
||||||
pair='MEME/BTC',
|
pair='MEME/BTC',
|
||||||
timeframe='1m')
|
timeframe='1m', candle_type='spot')
|
||||||
assert log_has('Failed to download history data for pair: "MEME/BTC", timeframe: 1m.', caplog)
|
assert log_has('Failed to download history data for pair: "MEME/BTC", timeframe: 1m.', caplog)
|
||||||
|
|
||||||
|
|
||||||
|
@ -389,7 +395,8 @@ def test_init_with_refresh(default_conf, mocker) -> None:
|
||||||
datadir=Path(''),
|
datadir=Path(''),
|
||||||
pairs=[],
|
pairs=[],
|
||||||
timeframe=default_conf['timeframe'],
|
timeframe=default_conf['timeframe'],
|
||||||
exchange=exchange
|
exchange=exchange,
|
||||||
|
candle_type=CandleType.SPOT
|
||||||
)
|
)
|
||||||
assert {} == load_data(
|
assert {} == load_data(
|
||||||
datadir=Path(''),
|
datadir=Path(''),
|
||||||
|
|
|
@ -318,17 +318,15 @@ def test_backtesting_init_no_timeframe(mocker, default_conf, caplog) -> None:
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
del default_conf['timeframe']
|
del default_conf['timeframe']
|
||||||
default_conf['strategy_list'] = [CURRENT_TEST_STRATEGY,
|
default_conf['strategy_list'] = [CURRENT_TEST_STRATEGY,
|
||||||
'SampleStrategy']
|
'HyperoptableStrategy']
|
||||||
# TODO: This refers to the sampleStrategy in user_data if it exists...
|
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
|
||||||
with pytest.raises(OperationalException):
|
with pytest.raises(OperationalException,
|
||||||
|
match=r"Timeframe needs to be set in either configuration"):
|
||||||
Backtesting(default_conf)
|
Backtesting(default_conf)
|
||||||
log_has("Ticker-interval needs to be set in either configuration "
|
|
||||||
"or as cli argument `--ticker-interval 5m`", caplog)
|
|
||||||
|
|
||||||
|
|
||||||
def test_data_with_fee(default_conf, mocker, testdatadir) -> None:
|
def test_data_with_fee(default_conf, mocker) -> None:
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
default_conf['fee'] = 0.1234
|
default_conf['fee'] = 0.1234
|
||||||
|
|
||||||
|
@ -1304,7 +1302,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
|
||||||
|
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert 'BACKTESTING REPORT' in captured.out
|
assert 'BACKTESTING REPORT' in captured.out
|
||||||
assert 'SELL REASON STATS' in captured.out
|
assert 'EXIT REASON STATS' in captured.out
|
||||||
assert 'DAY BREAKDOWN' in captured.out
|
assert 'DAY BREAKDOWN' in captured.out
|
||||||
assert 'LEFT OPEN TRADES REPORT' in captured.out
|
assert 'LEFT OPEN TRADES REPORT' in captured.out
|
||||||
assert '2017-11-14 21:17:00 -> 2017-11-14 22:58:00 | Max open trades : 1' in captured.out
|
assert '2017-11-14 21:17:00 -> 2017-11-14 22:58:00 | Max open trades : 1' in captured.out
|
||||||
|
@ -1413,7 +1411,7 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker,
|
||||||
|
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert 'BACKTESTING REPORT' in captured.out
|
assert 'BACKTESTING REPORT' in captured.out
|
||||||
assert 'SELL REASON STATS' in captured.out
|
assert 'EXIT REASON STATS' in captured.out
|
||||||
assert 'LEFT OPEN TRADES REPORT' in captured.out
|
assert 'LEFT OPEN TRADES REPORT' in captured.out
|
||||||
|
|
||||||
|
|
||||||
|
@ -1518,7 +1516,7 @@ def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker,
|
||||||
|
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert 'BACKTESTING REPORT' in captured.out
|
assert 'BACKTESTING REPORT' in captured.out
|
||||||
assert 'SELL REASON STATS' in captured.out
|
assert 'EXIT REASON STATS' in captured.out
|
||||||
assert 'LEFT OPEN TRADES REPORT' in captured.out
|
assert 'LEFT OPEN TRADES REPORT' in captured.out
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ from unittest.mock import MagicMock
|
||||||
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_edge
|
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_edge
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
from freqtrade.optimize.edge_cli import EdgeCli
|
from freqtrade.optimize.edge_cli import EdgeCli
|
||||||
from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, log_has_re, patch_exchange,
|
from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, patch_exchange,
|
||||||
patched_configuration_load_config_file)
|
patched_configuration_load_config_file)
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,7 +30,6 @@ def test_setup_optimize_configuration_without_arguments(mocker, default_conf, ca
|
||||||
assert 'datadir' in config
|
assert 'datadir' in config
|
||||||
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
||||||
assert 'timeframe' in config
|
assert 'timeframe' in config
|
||||||
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog)
|
|
||||||
|
|
||||||
assert 'timerange' not in config
|
assert 'timerange' not in config
|
||||||
assert 'stoploss_range' not in config
|
assert 'stoploss_range' not in config
|
||||||
|
|
|
@ -63,7 +63,6 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca
|
||||||
assert 'datadir' in config
|
assert 'datadir' in config
|
||||||
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
||||||
assert 'timeframe' in config
|
assert 'timeframe' in config
|
||||||
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog)
|
|
||||||
|
|
||||||
assert 'position_stacking' not in config
|
assert 'position_stacking' not in config
|
||||||
assert not log_has('Parameter --enable-position-stacking detected ...', caplog)
|
assert not log_has('Parameter --enable-position-stacking detected ...', caplog)
|
||||||
|
|
|
@ -21,7 +21,7 @@ from freqtrade.optimize.optimize_reports import (_get_resample_from_period, gene
|
||||||
generate_strategy_comparison,
|
generate_strategy_comparison,
|
||||||
generate_trading_stats, show_sorted_pairlist,
|
generate_trading_stats, show_sorted_pairlist,
|
||||||
store_backtest_stats, text_table_bt_results,
|
store_backtest_stats, text_table_bt_results,
|
||||||
text_table_sell_reason, text_table_strategy)
|
text_table_exit_reason, text_table_strategy)
|
||||||
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||||
from tests.conftest import CURRENT_TEST_STRATEGY
|
from tests.conftest import CURRENT_TEST_STRATEGY
|
||||||
from tests.data.test_history import _backup_file, _clean_test_file
|
from tests.data.test_history import _backup_file, _clean_test_file
|
||||||
|
@ -281,7 +281,7 @@ def test_text_table_sell_reason():
|
||||||
)
|
)
|
||||||
|
|
||||||
result_str = (
|
result_str = (
|
||||||
'| Sell Reason | Sells | Win Draws Loss Win% | Avg Profit % | Cum Profit % |'
|
'| Exit Reason | Exits | Win Draws Loss Win% | Avg Profit % | Cum Profit % |'
|
||||||
' Tot Profit BTC | Tot Profit % |\n'
|
' Tot Profit BTC | Tot Profit % |\n'
|
||||||
'|---------------+---------+--------------------------+----------------+----------------+'
|
'|---------------+---------+--------------------------+----------------+----------------+'
|
||||||
'------------------+----------------|\n'
|
'------------------+----------------|\n'
|
||||||
|
@ -293,7 +293,7 @@ def test_text_table_sell_reason():
|
||||||
|
|
||||||
sell_reason_stats = generate_sell_reason_stats(max_open_trades=2,
|
sell_reason_stats = generate_sell_reason_stats(max_open_trades=2,
|
||||||
results=results)
|
results=results)
|
||||||
assert text_table_sell_reason(sell_reason_stats=sell_reason_stats,
|
assert text_table_exit_reason(sell_reason_stats=sell_reason_stats,
|
||||||
stake_currency='BTC') == result_str
|
stake_currency='BTC') == result_str
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1219,6 +1219,7 @@ def test_rpc_forceentry(mocker, default_conf, ticker, fee, limit_buy_order_open)
|
||||||
pair = 'LTC/BTC'
|
pair = 'LTC/BTC'
|
||||||
trade = rpc._rpc_force_entry(pair, 0.0001, order_type='limit', stake_amount=0.05)
|
trade = rpc._rpc_force_entry(pair, 0.0001, order_type='limit', stake_amount=0.05)
|
||||||
assert trade.stake_amount == 0.05
|
assert trade.stake_amount == 0.05
|
||||||
|
assert trade.buy_tag == 'forceentry'
|
||||||
|
|
||||||
# Test not buying
|
# Test not buying
|
||||||
pair = 'XRP/BTC'
|
pair = 'XRP/BTC'
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
||||||
|
|
||||||
import talib.abstract as ta
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
from strategy_test_v2 import StrategyTestV2
|
||||||
|
|
||||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
from freqtrade.strategy import (BooleanParameter, DecimalParameter, IntParameter, IStrategy,
|
from freqtrade.strategy import BooleanParameter, DecimalParameter, IntParameter, RealParameter
|
||||||
RealParameter)
|
|
||||||
|
|
||||||
|
|
||||||
class HyperoptableStrategy(IStrategy):
|
class HyperoptableStrategy(StrategyTestV2):
|
||||||
"""
|
"""
|
||||||
Default Strategy provided by freqtrade bot.
|
Default Strategy provided by freqtrade bot.
|
||||||
Please do not modify this strategy, it's intended for internal use only.
|
Please do not modify this strategy, it's intended for internal use only.
|
||||||
|
@ -16,38 +15,6 @@ class HyperoptableStrategy(IStrategy):
|
||||||
or strategy repository https://github.com/freqtrade/freqtrade-strategies
|
or strategy repository https://github.com/freqtrade/freqtrade-strategies
|
||||||
for samples and inspiration.
|
for samples and inspiration.
|
||||||
"""
|
"""
|
||||||
INTERFACE_VERSION = 2
|
|
||||||
|
|
||||||
# Minimal ROI designed for the strategy
|
|
||||||
minimal_roi = {
|
|
||||||
"40": 0.0,
|
|
||||||
"30": 0.01,
|
|
||||||
"20": 0.02,
|
|
||||||
"0": 0.04
|
|
||||||
}
|
|
||||||
|
|
||||||
# Optimal stoploss designed for the strategy
|
|
||||||
stoploss = -0.10
|
|
||||||
|
|
||||||
# Optimal ticker interval for the strategy
|
|
||||||
timeframe = '5m'
|
|
||||||
|
|
||||||
# Optional order type mapping
|
|
||||||
order_types = {
|
|
||||||
'entry': 'limit',
|
|
||||||
'exit': 'limit',
|
|
||||||
'stoploss': 'limit',
|
|
||||||
'stoploss_on_exchange': False
|
|
||||||
}
|
|
||||||
|
|
||||||
# Number of candles the strategy requires before producing valid signals
|
|
||||||
startup_candle_count: int = 20
|
|
||||||
|
|
||||||
# Optional time in force for orders
|
|
||||||
order_time_in_force = {
|
|
||||||
'entry': 'gtc',
|
|
||||||
'exit': 'gtc',
|
|
||||||
}
|
|
||||||
|
|
||||||
buy_params = {
|
buy_params = {
|
||||||
'buy_rsi': 35,
|
'buy_rsi': 35,
|
||||||
|
@ -91,55 +58,6 @@ class HyperoptableStrategy(IStrategy):
|
||||||
"""
|
"""
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Adds several different TA indicators to the given DataFrame
|
|
||||||
|
|
||||||
Performance Note: For the best performance be frugal on the number of indicators
|
|
||||||
you are using. Let uncomment only the indicator you are using in your strategies
|
|
||||||
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
|
||||||
:param dataframe: Dataframe with data from the exchange
|
|
||||||
:param metadata: Additional information, like the currently traded pair
|
|
||||||
:return: a Dataframe with all mandatory indicators for the strategies
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Momentum Indicator
|
|
||||||
# ------------------------------------
|
|
||||||
|
|
||||||
# ADX
|
|
||||||
dataframe['adx'] = ta.ADX(dataframe)
|
|
||||||
|
|
||||||
# MACD
|
|
||||||
macd = ta.MACD(dataframe)
|
|
||||||
dataframe['macd'] = macd['macd']
|
|
||||||
dataframe['macdsignal'] = macd['macdsignal']
|
|
||||||
dataframe['macdhist'] = macd['macdhist']
|
|
||||||
|
|
||||||
# Minus Directional Indicator / Movement
|
|
||||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
|
||||||
|
|
||||||
# Plus Directional Indicator / Movement
|
|
||||||
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
|
|
||||||
|
|
||||||
# RSI
|
|
||||||
dataframe['rsi'] = ta.RSI(dataframe)
|
|
||||||
|
|
||||||
# Stoch fast
|
|
||||||
stoch_fast = ta.STOCHF(dataframe)
|
|
||||||
dataframe['fastd'] = stoch_fast['fastd']
|
|
||||||
dataframe['fastk'] = stoch_fast['fastk']
|
|
||||||
|
|
||||||
# Bollinger bands
|
|
||||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
|
||||||
dataframe['bb_lowerband'] = bollinger['lower']
|
|
||||||
dataframe['bb_middleband'] = bollinger['mid']
|
|
||||||
dataframe['bb_upperband'] = bollinger['upper']
|
|
||||||
|
|
||||||
# EMA - Exponential Moving Average
|
|
||||||
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Based on TA indicators, populates the buy signal for the given dataframe
|
Based on TA indicators, populates the buy signal for the given dataframe
|
||||||
|
|
|
@ -31,9 +31,7 @@ class TestStrategyLegacyV1(IStrategy):
|
||||||
# This attribute will be overridden if the config file contains "stoploss"
|
# This attribute will be overridden if the config file contains "stoploss"
|
||||||
stoploss = -0.10
|
stoploss = -0.10
|
||||||
|
|
||||||
# Optimal timeframe for the strategy
|
timeframe = '5m'
|
||||||
# Keep the legacy value here to test compatibility
|
|
||||||
ticker_interval = '5m'
|
|
||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -113,7 +113,6 @@ def test_strategy_pre_v3(result, default_conf, strategy_name):
|
||||||
assert default_conf['stoploss'] == -0.10
|
assert default_conf['stoploss'] == -0.10
|
||||||
|
|
||||||
assert strategy.timeframe == '5m'
|
assert strategy.timeframe == '5m'
|
||||||
assert strategy.ticker_interval == '5m'
|
|
||||||
assert default_conf['timeframe'] == '5m'
|
assert default_conf['timeframe'] == '5m'
|
||||||
|
|
||||||
df_indicators = strategy.advise_indicators(result, metadata=metadata)
|
df_indicators = strategy.advise_indicators(result, metadata=metadata)
|
||||||
|
@ -440,7 +439,6 @@ def test_call_deprecated_function(result, default_conf, caplog):
|
||||||
assert strategy._sell_fun_len == 2
|
assert strategy._sell_fun_len == 2
|
||||||
assert strategy.INTERFACE_VERSION == 1
|
assert strategy.INTERFACE_VERSION == 1
|
||||||
assert strategy.timeframe == '5m'
|
assert strategy.timeframe == '5m'
|
||||||
assert strategy.ticker_interval == '5m'
|
|
||||||
|
|
||||||
indicator_df = strategy.advise_indicators(result, metadata=metadata)
|
indicator_df = strategy.advise_indicators(result, metadata=metadata)
|
||||||
assert isinstance(indicator_df, DataFrame)
|
assert isinstance(indicator_df, DataFrame)
|
||||||
|
@ -454,9 +452,6 @@ def test_call_deprecated_function(result, default_conf, caplog):
|
||||||
assert isinstance(exitdf, DataFrame)
|
assert isinstance(exitdf, DataFrame)
|
||||||
assert 'exit_long' in exitdf
|
assert 'exit_long' in exitdf
|
||||||
|
|
||||||
assert log_has("DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'.",
|
|
||||||
caplog)
|
|
||||||
|
|
||||||
|
|
||||||
def test_strategy_interface_versioning(result, default_conf):
|
def test_strategy_interface_versioning(result, default_conf):
|
||||||
default_conf.update({'strategy': 'StrategyTestV2'})
|
default_conf.update({'strategy': 'StrategyTestV2'})
|
||||||
|
|
|
@ -112,17 +112,17 @@ def test_parse_args_strategy_path_invalid() -> None:
|
||||||
|
|
||||||
def test_parse_args_backtesting_invalid() -> None:
|
def test_parse_args_backtesting_invalid() -> None:
|
||||||
with pytest.raises(SystemExit, match=r'2'):
|
with pytest.raises(SystemExit, match=r'2'):
|
||||||
Arguments(['backtesting --ticker-interval']).get_parsed_arg()
|
Arguments(['backtesting --timeframe']).get_parsed_arg()
|
||||||
|
|
||||||
with pytest.raises(SystemExit, match=r'2'):
|
with pytest.raises(SystemExit, match=r'2'):
|
||||||
Arguments(['backtesting --ticker-interval', 'abc']).get_parsed_arg()
|
Arguments(['backtesting --timeframe', 'abc']).get_parsed_arg()
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_backtesting_custom() -> None:
|
def test_parse_args_backtesting_custom() -> None:
|
||||||
args = [
|
args = [
|
||||||
'backtesting',
|
'backtesting',
|
||||||
'-c', 'test_conf.json',
|
'-c', 'test_conf.json',
|
||||||
'--ticker-interval', '1m',
|
'--timeframe', '1m',
|
||||||
'--strategy-list',
|
'--strategy-list',
|
||||||
CURRENT_TEST_STRATEGY,
|
CURRENT_TEST_STRATEGY,
|
||||||
'SampleStrategy'
|
'SampleStrategy'
|
||||||
|
|
|
@ -444,7 +444,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
||||||
'--strategy', CURRENT_TEST_STRATEGY,
|
'--strategy', CURRENT_TEST_STRATEGY,
|
||||||
'--datadir', '/foo/bar',
|
'--datadir', '/foo/bar',
|
||||||
'--userdir', "/tmp/freqtrade",
|
'--userdir', "/tmp/freqtrade",
|
||||||
'--ticker-interval', '1m',
|
'--timeframe', '1m',
|
||||||
'--enable-position-stacking',
|
'--enable-position-stacking',
|
||||||
'--disable-max-market-positions',
|
'--disable-max-market-positions',
|
||||||
'--timerange', ':100',
|
'--timerange', ':100',
|
||||||
|
@ -495,7 +495,7 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non
|
||||||
arglist = [
|
arglist = [
|
||||||
'backtesting',
|
'backtesting',
|
||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
'--ticker-interval', '1m',
|
'--timeframe', '1m',
|
||||||
'--export', 'trades',
|
'--export', 'trades',
|
||||||
'--strategy-list',
|
'--strategy-list',
|
||||||
CURRENT_TEST_STRATEGY,
|
CURRENT_TEST_STRATEGY,
|
||||||
|
@ -1381,22 +1381,14 @@ def test_process_removed_setting(mocker, default_conf, caplog):
|
||||||
def test_process_deprecated_ticker_interval(default_conf, caplog):
|
def test_process_deprecated_ticker_interval(default_conf, caplog):
|
||||||
message = "DEPRECATED: Please use 'timeframe' instead of 'ticker_interval."
|
message = "DEPRECATED: Please use 'timeframe' instead of 'ticker_interval."
|
||||||
config = deepcopy(default_conf)
|
config = deepcopy(default_conf)
|
||||||
|
|
||||||
process_temporary_deprecated_settings(config)
|
process_temporary_deprecated_settings(config)
|
||||||
assert not log_has(message, caplog)
|
assert not log_has(message, caplog)
|
||||||
|
|
||||||
del config['timeframe']
|
del config['timeframe']
|
||||||
config['ticker_interval'] = '15m'
|
config['ticker_interval'] = '15m'
|
||||||
process_temporary_deprecated_settings(config)
|
|
||||||
assert log_has(message, caplog)
|
|
||||||
assert config['ticker_interval'] == '15m'
|
|
||||||
|
|
||||||
config = deepcopy(default_conf)
|
|
||||||
# Have both timeframe and ticker interval in config
|
|
||||||
# Can also happen when using ticker_interval in configuration, and --timeframe as cli argument
|
|
||||||
config['timeframe'] = '5m'
|
|
||||||
config['ticker_interval'] = '4h'
|
|
||||||
with pytest.raises(OperationalException,
|
with pytest.raises(OperationalException,
|
||||||
match=r"Both 'timeframe' and 'ticker_interval' detected."):
|
match=r"DEPRECATED: 'ticker_interval' detected. Please use.*"):
|
||||||
process_temporary_deprecated_settings(config)
|
process_temporary_deprecated_settings(config)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4107,8 +4107,9 @@ def test_get_real_amount_quote(default_conf_usdt, trades_for_order, buy_order_fe
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
|
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
|
||||||
# Amount is reduced by "fee"
|
# Amount is reduced by "fee"
|
||||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.001)
|
assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) == amount - (amount * 0.001)
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, is_short=False,'
|
'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, is_short=False,'
|
||||||
' leverage=1.0, open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992).',
|
' leverage=1.0, open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992).',
|
||||||
|
@ -4134,8 +4135,9 @@ def test_get_real_amount_quote_dust(default_conf_usdt, trades_for_order, buy_ord
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
|
|
||||||
walletmock.reset_mock()
|
walletmock.reset_mock()
|
||||||
|
order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
|
||||||
# Amount is kept as is
|
# Amount is kept as is
|
||||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
|
assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) == amount
|
||||||
assert walletmock.call_count == 1
|
assert walletmock.call_count == 1
|
||||||
assert log_has_re(r'Fee amount for Trade.* was in base currency '
|
assert log_has_re(r'Fee amount for Trade.* was in base currency '
|
||||||
'- Eating Fee 0.008 into dust', caplog)
|
'- Eating Fee 0.008 into dust', caplog)
|
||||||
|
@ -4156,8 +4158,9 @@ def test_get_real_amount_no_trade(default_conf_usdt, buy_order_fee, caplog, mock
|
||||||
)
|
)
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
|
|
||||||
|
order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
|
||||||
# Amount is reduced by "fee"
|
# Amount is reduced by "fee"
|
||||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
|
assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) == amount
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
|
'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
|
||||||
'is_short=False, leverage=1.0, open_rate=0.24544100, open_since=closed) failed: '
|
'is_short=False, leverage=1.0, open_rate=0.24544100, open_since=closed) failed: '
|
||||||
|
@ -4213,7 +4216,8 @@ def test_get_real_amount(
|
||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', side_effect=ExchangeError)
|
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', side_effect=ExchangeError)
|
||||||
|
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
assert freqtrade.get_real_amount(trade, buy_order) == amount - fee_reduction_amount
|
order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
|
||||||
|
assert freqtrade.get_real_amount(trade, buy_order, order_obj) == amount - fee_reduction_amount
|
||||||
|
|
||||||
if expected_log:
|
if expected_log:
|
||||||
assert log_has(expected_log, caplog)
|
assert log_has(expected_log, caplog)
|
||||||
|
@ -4260,7 +4264,8 @@ def test_get_real_amount_multi(
|
||||||
|
|
||||||
# Amount is reduced by "fee"
|
# Amount is reduced by "fee"
|
||||||
expected_amount = amount - (amount * fee_reduction_amount)
|
expected_amount = amount - (amount * fee_reduction_amount)
|
||||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == expected_amount
|
order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
|
||||||
|
assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) == expected_amount
|
||||||
assert log_has(
|
assert log_has(
|
||||||
(
|
(
|
||||||
'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
|
'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
|
||||||
|
@ -4296,8 +4301,9 @@ def test_get_real_amount_invalid_order(default_conf_usdt, trades_for_order, buy_
|
||||||
)
|
)
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
|
|
||||||
|
order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
|
||||||
# Amount does not change
|
# Amount does not change
|
||||||
assert freqtrade.get_real_amount(trade, limit_buy_order_usdt) == amount
|
assert freqtrade.get_real_amount(trade, limit_buy_order_usdt, order_obj) == amount
|
||||||
|
|
||||||
|
|
||||||
def test_get_real_amount_fees_order(default_conf_usdt, market_buy_order_usdt_doublefee,
|
def test_get_real_amount_fees_order(default_conf_usdt, market_buy_order_usdt_doublefee,
|
||||||
|
@ -4319,7 +4325,8 @@ def test_get_real_amount_fees_order(default_conf_usdt, market_buy_order_usdt_dou
|
||||||
|
|
||||||
# Amount does not change
|
# Amount does not change
|
||||||
assert trade.fee_open == 0.0025
|
assert trade.fee_open == 0.0025
|
||||||
assert freqtrade.get_real_amount(trade, market_buy_order_usdt_doublefee) == 30.0
|
order_obj = Order.parse_from_ccxt_object(market_buy_order_usdt_doublefee, 'LTC/ETH', 'buy')
|
||||||
|
assert freqtrade.get_real_amount(trade, market_buy_order_usdt_doublefee, order_obj) == 30.0
|
||||||
assert tfo_mock.call_count == 0
|
assert tfo_mock.call_count == 0
|
||||||
# Fetch fees from trades dict if available to get "proper" values
|
# Fetch fees from trades dict if available to get "proper" values
|
||||||
assert round(trade.fee_open, 4) == 0.001
|
assert round(trade.fee_open, 4) == 0.001
|
||||||
|
@ -4343,9 +4350,10 @@ def test_get_real_amount_wrong_amount(default_conf_usdt, trades_for_order, buy_o
|
||||||
)
|
)
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
|
|
||||||
|
order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
|
||||||
# Amount does not change
|
# Amount does not change
|
||||||
with pytest.raises(DependencyException, match=r"Half bought\? Amounts don't match"):
|
with pytest.raises(DependencyException, match=r"Half bought\? Amounts don't match"):
|
||||||
freqtrade.get_real_amount(trade, limit_buy_order_usdt)
|
freqtrade.get_real_amount(trade, limit_buy_order_usdt, order_obj)
|
||||||
|
|
||||||
|
|
||||||
def test_get_real_amount_wrong_amount_rounding(default_conf_usdt, trades_for_order, buy_order_fee,
|
def test_get_real_amount_wrong_amount_rounding(default_conf_usdt, trades_for_order, buy_order_fee,
|
||||||
|
@ -4367,9 +4375,10 @@ def test_get_real_amount_wrong_amount_rounding(default_conf_usdt, trades_for_ord
|
||||||
)
|
)
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
|
|
||||||
|
order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
|
||||||
# Amount changes by fee amount.
|
# Amount changes by fee amount.
|
||||||
assert isclose(
|
assert isclose(
|
||||||
freqtrade.get_real_amount(trade, limit_buy_order_usdt),
|
freqtrade.get_real_amount(trade, limit_buy_order_usdt, order_obj),
|
||||||
amount - (amount * 0.001),
|
amount - (amount * 0.001),
|
||||||
abs_tol=MATH_CLOSE_PREC,
|
abs_tol=MATH_CLOSE_PREC,
|
||||||
)
|
)
|
||||||
|
@ -4393,7 +4402,8 @@ def test_get_real_amount_open_trade_usdt(default_conf_usdt, fee, mocker):
|
||||||
'side': 'buy',
|
'side': 'buy',
|
||||||
}
|
}
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
assert freqtrade.get_real_amount(trade, order) == amount
|
order_obj = Order.parse_from_ccxt_object(order, 'LTC/ETH', 'buy')
|
||||||
|
assert freqtrade.get_real_amount(trade, order, order_obj) == amount
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('amount,fee_abs,wallet,amount_exp', [
|
@pytest.mark.parametrize('amount,fee_abs,wallet,amount_exp', [
|
||||||
|
|
File diff suppressed because one or more lines are too long
2
tests/testdata/backtest-result_new.json
vendored
2
tests/testdata/backtest-result_new.json
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user