diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 399588f88..6b4e8adaf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,8 +12,7 @@ Few pointers for contributions: - New features need to contain unit tests, must conform to PEP8 (max-line-length = 100) and should be documented with the introduction PR. - PR's can be declared as `[WIP]` - which signify Work in Progress Pull Requests (which are not finished). -If you are unsure, discuss the feature on our [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) -or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR. +If you are unsure, discuss the feature on our [discord server](https://discord.gg/MA9v74M), on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR. ## Getting started diff --git a/README.md b/README.md index c9f4d0a52..8526b5c91 100644 --- a/README.md +++ b/README.md @@ -132,15 +132,13 @@ The project is currently setup in two main branches: ## Support -### Help / Slack / Discord +### Help / Discord / Slack -For any questions not covered by the documentation or for further information about the bot, we encourage you to join our slack channel. +For any questions not covered by the documentation or for further information about the bot, or to simply engage with like-minded individuals, we encourage you to join our slack channel. -- [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE). +Please check out our [discord server](https://discord.gg/MA9v74M). -Alternatively, check out the newly created [discord server](https://discord.gg/MA9v74M). - -*Note*: Since the discord server is relatively new, answers to questions might be slightly delayed as currently the user base quite small. +You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg). ### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) @@ -171,7 +169,7 @@ to understand the requirements before sending your pull-requests. Coding is not a necessity to contribute - maybe start with improving our documentation? Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase. -**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. +**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [discord](https://discord.gg/MA9v74M) or [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. **Important:** Always create your PR against the `develop` branch, not `stable`. diff --git a/config_full.json.example b/config_full.json.example index 45c5c695c..5ee2a1faf 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -67,7 +67,13 @@ {"method": "AgeFilter", "min_days_listed": 10}, {"method": "PrecisionFilter"}, {"method": "PriceFilter", "low_price_ratio": 0.01, "min_price": 0.00000010}, - {"method": "SpreadFilter", "max_spread_ratio": 0.005} + {"method": "SpreadFilter", "max_spread_ratio": 0.005}, + { + "method": "RangeStabilityFilter", + "lookback_days": 10, + "min_rate_of_change": 0.01, + "refresh_period": 1440 + } ], "exchange": { "name": "bittrex", diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index 59ebc16b5..1ace61769 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -77,7 +77,7 @@ Currently, the arguments are: * `results`: DataFrame containing the result The following columns are available in results (corresponds to the output-file of backtesting when used with `--export trades`): - `pair, profit_percent, profit_abs, open_time, close_time, open_index, close_index, trade_duration, open_at_end, open_rate, close_rate, sell_reason` + `pair, profit_percent, profit_abs, open_date, open_rate, open_fee, close_date, close_rate, close_fee, amount, trade_duration, open_at_end, sell_reason` * `trade_count`: Amount of trades (identical to `len(results)`) * `min_date`: Start date of the hyperopting TimeFrame * `min_date`: End date of the hyperopting TimeFrame diff --git a/docs/backtesting.md b/docs/backtesting.md index 84911568b..3058d1b57 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -162,11 +162,16 @@ A backtesting result will look like that: |-----------------------+---------------------| | Backtesting from | 2019-01-01 00:00:00 | | Backtesting to | 2019-05-01 00:00:00 | +| Max open trades | 3 | +| | | | Total trades | 429 | -| First trade | 2019-01-01 18:30:00 | -| First trade Pair | EOS/USDT | | Total Profit % | 152.41% | | Trades per day | 3.575 | +| | | +| Best Pair | LSK/BTC 26.26% | +| Worst Pair | ZEC/BTC -10.18% | +| Best Trade | LSK/BTC 4.25% | +| Worst Trade | ZEC/BTC -10.25% | | Best day | 25.27% | | Worst day | -30.67% | | Avg. Duration Winners | 4:23:00 | @@ -233,11 +238,16 @@ It contains some useful key metrics about performance of your strategy on backte |-----------------------+---------------------| | Backtesting from | 2019-01-01 00:00:00 | | Backtesting to | 2019-05-01 00:00:00 | +| Max open trades | 3 | +| | | | Total trades | 429 | -| First trade | 2019-01-01 18:30:00 | -| First trade Pair | EOS/USDT | | Total Profit % | 152.41% | | Trades per day | 3.575 | +| | | +| Best Pair | LSK/BTC 26.26% | +| Worst Pair | ZEC/BTC -10.18% | +| Best Trade | LSK/BTC 4.25% | +| Worst Trade | ZEC/BTC -10.25% | | Best day | 25.27% | | Worst day | -30.67% | | Avg. Duration Winners | 4:23:00 | @@ -251,16 +261,17 @@ It contains some useful key metrics about performance of your strategy on backte ``` -- `Total trades`: Identical to the total trades of the backtest output table. -- `First trade`: First trade entered. -- `First trade pair`: Which pair was part of the first trade. - `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option). +- `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - to clearly see settings for this. +- `Total trades`: Identical to the total trades of the backtest output table. - `Total Profit %`: Total profit per stake amount. Aligned to the TOTAL column of the first table. - `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy). +- `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`. +- `Best Trade` / `Worst Trade`: Biggest winning trade and biggest losing trade - `Best day` / `Worst day`: Best and worst day based on daily profit. - `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. - `Max Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced). -- `Drawdown Start` / `Drawdown End`: Start and end datetimes for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command). +- `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command). - `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column. ### Assumptions made by backtesting @@ -268,18 +279,24 @@ It contains some useful key metrics about performance of your strategy on backte Since backtesting lacks some detailed information about what happens within a candle, it needs to take a few assumptions: - Buys happen at open-price -- Sell signal sells happen at open-price of the following candle -- Low happens before high for stoploss, protecting capital first +- Sell-signal sells happen at open-price of the consecutive candle +- Sell-signal is favored over Stoploss, because sell-signals are assumed to trigger on candle's open - ROI - sells are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the sell will be at 2%) - sells are never "below the candle", so a ROI of 2% may result in a sell at 2.4% if low was at 2.4% profit - Forcesells caused by `=-1` ROI entries use low as sell value, unless N falls on the candle open (e.g. `120: -1` for 1h candles) - Stoploss sells happen exactly at stoploss price, even if low was lower +- Stoploss is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` sell reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes +- Low happens before high for stoploss, protecting capital first - Trailing stoploss - High happens first - adjusting stoploss - Low uses the adjusted stoploss (so sells with large high-low difference are backtested correctly) + - ROI applies before trailing-stop, ensuring profits are "top-capped" at ROI if both ROI and trailing stop applies - Sell-reason does not explain if a trade was positive or negative, just what triggered the sell (this can look odd if negative ROI values are used) -- Stoploss (and trailing stoploss) is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` and/or `trailing_stop` sell reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes. +- Evaluation sequence (if multiple signals happen on the same candle) + - ROI (if not stoploss) + - Sell-signal + - Stoploss Taking these assumptions, backtesting tries to mirror real trading as closely as possible. However, backtesting will **never** replace running a strategy in dry-run mode. Also, keep in mind that past results don't guarantee future success. diff --git a/docs/configuration.md b/docs/configuration.md index 47362e525..2e8f6555f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -87,6 +87,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict | `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict | `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded.
*Defaults to `60` minutes.*
**Datatype:** Positive Integer +| `exchange.skip_pair_validation` | Skip pairlist validation on startup.
*Defaults to `false`
**Datatype:** Boolean | `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation. | `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now.
*Defaults to `true`.*
**Datatype:** Boolean | `pairlists` | Define one or more pairlists to be used. [More information below](#pairlists-and-pairlist-handlers).
*Defaults to `StaticPairList`.*
**Datatype:** List of Dicts @@ -176,7 +177,7 @@ In the example above this would mean: This option only applies with [Static stake amount](#static-stake-amount) - since [Dynamic stake amount](#dynamic-stake-amount) divides the balances evenly. !!! Note - The minimum last stake amount can be configured using `amend_last_stake_amount` - which defaults to 0.5 (50%). This means that the minimum stake amount that's ever used is `stake_amount * 0.5`. This avoids very low stake amounts, that are close to the minimum tradable amount for the pair and can be refused by the exchange. + The minimum last stake amount can be configured using `last_stake_amount_min_ratio` - which defaults to 0.5 (50%). This means that the minimum stake amount that's ever used is `stake_amount * 0.5`. This avoids very low stake amounts, that are close to the minimum tradable amount for the pair and can be refused by the exchange. #### Static stake amount @@ -313,22 +314,21 @@ Configuration: } ``` -!!! Note +!!! Note "Market order support" Not all exchanges support "market" orders. The following message will be shown if your exchange does not support market orders: - `"Exchange does not support market orders."` + `"Exchange does not support market orders."` and the bot will refuse to start. -!!! Note - Stoploss on exchange interval is not mandatory. Do not change its value if you are +!!! Warning "Using market orders" + Please carefully read the section [Market order pricing](#market-order-pricing) section when using market orders. + +!!! Note "Stoploss on exchange" + `stoploss_on_exchange_interval` is not mandatory. Do not change its value if you are unsure of what you are doing. For more information about how stoploss works please refer to [the stoploss documentation](stoploss.md). -!!! Note If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new stoploss order. -!!! Warning "Using market orders" - Please read the section [Market order pricing](#market-order-pricing) section when using market orders. - !!! Warning "Warning: stoploss_on_exchange failures" If stoploss on exchange creation fails for some reason, then an "emergency sell" is initiated. By default, this will sell the asset using a market order. The order-type for the emergency-sell can be changed by setting the `emergencysell` value in the `order_types` dictionary - however this is not advised. diff --git a/docs/developer.md b/docs/developer.md index 8ef816d5d..c253f4460 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -2,7 +2,7 @@ This page is intended for developers of Freqtrade, people who want to contribute to the Freqtrade codebase or documentation, or people who want to understand the source code of the application they're running. -All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel in [slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) where you can ask questions. +All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel on [discord](https://discord.gg/MA9v74M) or [slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg) where you can ask questions. ## Documentation diff --git a/docs/edge.md b/docs/edge.md index 7442f1927..fd6d2cf7d 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -23,8 +23,8 @@ The Edge Positioning module seeks to improve a strategy's winning probability an We raise the following question[^1]: !!! Question "Which trade is a better option?" - a) A trade with 80% of chance of losing $100 and 20% chance of winning $200
- b) A trade with 100% of chance of losing $30 + a) A trade with 80% of chance of losing 100\$ and 20% chance of winning 200\$
+ b) A trade with 100% of chance of losing 30\$ ???+ Info "Answer" The expected value of *a)* is smaller than the expected value of *b)*.
@@ -34,8 +34,8 @@ We raise the following question[^1]: Another way to look at it is to ask a similar question: !!! Question "Which trade is a better option?" - a) A trade with 80% of chance of winning 100 and 20% chance of losing $200
- b) A trade with 100% of chance of winning $30 + a) A trade with 80% of chance of winning 100\$ and 20% chance of losing 200\$
+ b) A trade with 100% of chance of winning 30\$ Edge positioning tries to answer the hard questions about risk/reward and position size automatically, seeking to minimizes the chances of losing of a given strategy. @@ -82,7 +82,7 @@ Risk Reward Ratio ($R$) is a formula used to measure the expected gains of a giv $$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$ ???+ Example "Worked example of $R$ calculation" - Let's say that you think that the price of *stonecoin* today is $10.0. You believe that, because they will start mining stonecoin, it will go up to $15.0 tomorrow. There is the risk that the stone is too hard, and the GPUs can't mine it, so the price might go to $0 tomorrow. You are planning to invest $100, which will give you 10 shares (100 / 10). + Let's say that you think that the price of *stonecoin* today is 10.0\$. You believe that, because they will start mining stonecoin, it will go up to 15.0\$ tomorrow. There is the risk that the stone is too hard, and the GPUs can't mine it, so the price might go to 0\$ tomorrow. You are planning to invest 100\$, which will give you 10 shares (100 / 10). Your potential profit is calculated as: @@ -92,9 +92,9 @@ $$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$ &= 50 \end{aligned}$ - Since the price might go to $0, the $100 dollars invested could turn into 0. + Since the price might go to 0\$, the 100\$ dollars invested could turn into 0. - We do however use a stoploss of 15% - so in the worst case, we'll sell 15% below entry price (or at 8.5$). + We do however use a stoploss of 15% - so in the worst case, we'll sell 15% below entry price (or at 8.5$\). $\begin{aligned} \text{potential_loss} &= (\text{entry_price} - \text{stoploss}) * \frac{\text{investment}}{\text{entry_price}} \\ @@ -109,7 +109,7 @@ $$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$ &= \frac{50}{15}\\ &= 3.33 \end{aligned}$
- What it effectively means is that the strategy have the potential to make 3.33$ for each $1 invested. + What it effectively means is that the strategy have the potential to make 3.33\$ for each 1\$ invested. On a long horizon, that is, on many trades, we can calculate the risk reward by dividing the strategy' average profit on winning trades by the strategy' average loss on losing trades. We can calculate the average profit, $\mu_{win}$, as follows: @@ -141,7 +141,7 @@ $$E = R * W - L$$ $E = R * W - L = 5 * 0.28 - 0.72 = 0.68$
-The expectancy worked out in the example above means that, on average, this strategy' trades will return 1.68 times the size of its losses. Said another way, the strategy makes $1.68 for every $1 it loses, on average. +The expectancy worked out in the example above means that, on average, this strategy' trades will return 1.68 times the size of its losses. Said another way, the strategy makes 1.68\$ for every 1\$ it loses, on average. This is important for two reasons: First, it may seem obvious, but you know right away that you have a positive return. Second, you now have a number you can compare to other candidate systems to make decisions about which ones you employ. @@ -222,7 +222,7 @@ Edge module has following configuration options: | `stoploss_range_max` | Maximum stoploss.
*Defaults to `-0.10`.*
**Datatype:** Float | `stoploss_range_step` | As an example if this is set to -0.01 then Edge will test the strategy for `[-0.01, -0,02, -0,03 ..., -0.09, -0.10]` ranges.
**Note** than having a smaller step means having a bigger range which could lead to slow calculation.
If you set this parameter to -0.001, you then slow down the Edge calculation by a factor of 10.
*Defaults to `-0.001`.*
**Datatype:** Float | `minimum_winrate` | It filters out pairs which don't have at least minimum_winrate.
This comes handy if you want to be conservative and don't comprise win rate in favour of risk reward ratio.
*Defaults to `0.60`.*
**Datatype:** Float -| `minimum_expectancy` | It filters out pairs which have the expectancy lower than this number.
Having an expectancy of 0.20 means if you put 10$ on a trade you expect a 12$ return.
*Defaults to `0.20`.*
**Datatype:** Float +| `minimum_expectancy` | It filters out pairs which have the expectancy lower than this number.
Having an expectancy of 0.20 means if you put 10\$ on a trade you expect a 12\$ return.
*Defaults to `0.20`.*
**Datatype:** Float | `min_trade_number` | When calculating *W*, *R* and *E* (expectancy) against historical data, you always want to have a minimum number of trades. The more this number is the more Edge is reliable.
Having a win rate of 100% on a single trade doesn't mean anything at all. But having a win rate of 70% over past 100 trades means clearly something.
*Defaults to `10` (it is highly recommended not to decrease this number).*
**Datatype:** Integer | `max_trade_duration_minute` | Edge will filter out trades with long duration. If a trade is profitable after 1 month, it is hard to evaluate the strategy based on it. But if most of trades are profitable and they have maximum duration of 30 minutes, then it is clearly a good sign.
**NOTICE:** While configuring this value, you should take into consideration your timeframe. As an example filtering out trades having duration less than one day for a strategy which has 4h interval does not make sense. Default value is set assuming your strategy interval is relatively small (1m or 5m, etc.).
*Defaults to `1440` (one day).*
**Datatype:** Integer | `remove_pumps` | Edge will remove sudden pumps in a given market while going through historical data. However, given that pumps happen very often in crypto markets, we recommend you keep this off.
*Defaults to `false`.*
**Datatype:** Boolean diff --git a/docs/exchanges.md b/docs/exchanges.md index fcf7c1cad..d877e6da2 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -23,7 +23,8 @@ Binance has been split into 3, and users must use the correct ccxt exchange ID f ## Kraken !!! Tip "Stoploss on Exchange" - Kraken supports `stoploss_on_exchange` and uses stop-loss-market orders. It provides great advantages, so we recommend to benefit from it, however since the resulting order is a stoploss-market order, sell-rates are not guaranteed, which makes this feature less secure than on other exchanges. This limitation is based on kraken's policy [source](https://blog.kraken.com/post/1234/announcement-delisting-pairs-and-temporary-suspension-of-advanced-order-types/) and [source2](https://blog.kraken.com/post/1494/kraken-enables-advanced-orders-and-adds-10-currency-pairs/) - which has stoploss-limit orders disabled. + Kraken supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it. + You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use. ### Historic Kraken data @@ -75,8 +76,7 @@ print(res) !!! Tip "Stoploss on Exchange" FTX supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it. - You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide. - + You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type of stoploss shall be used. ### Using subaccounts @@ -99,10 +99,10 @@ To use subaccounts with FTX, you need to edit the configuration and add the foll Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys. - ## Random notes for other exchanges * The Ocean (exchange id: `theocean`) exchange uses Web3 functionality and requires `web3` python package to be installed: + ```shell $ pip3 install web3 ``` diff --git a/docs/faq.md b/docs/faq.md index a775060de..b424cd31d 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -2,30 +2,30 @@ ## Beginner Tips & Tricks -* When you work with your strategy & hyperopt file you should use a proper code editor like vscode or Pycharm. A good code editor will provide syntax highlighting as well as line numbers, making it easy to find syntax errors (most likely, pointed out by Freqtrade during startup). +* When you work with your strategy & hyperopt file you should use a proper code editor like VSCode or PyCharm. A good code editor will provide syntax highlighting as well as line numbers, making it easy to find syntax errors (most likely pointed out by Freqtrade during startup). ## Freqtrade common issues ### The bot does not start -Running the bot with `freqtrade trade --config config.json` does show the output `freqtrade: command not found`. +Running the bot with `freqtrade trade --config config.json` shows the output `freqtrade: command not found`. -This could have the following reasons: +This could be caused by the following reasons: -* The virtual environment is not active - * run `source .env/bin/activate` to activate the virtual environment +* The virtual environment is not active. + * Run `source .env/bin/activate` to activate the virtual environment. * The installation did not work correctly. * Please check the [Installation documentation](installation.md). -### I have waited 5 minutes, why hasn't the bot made any trades yet?! +### I have waited 5 minutes, why hasn't the bot made any trades yet? * Depending on the buy strategy, the amount of whitelisted coins, the -situation of the market etc, it can take up to hours to find good entry +situation of the market etc, it can take up to hours to find a good entry position for a trade. Be patient! -* Or it may because of a configuration error? Best check the logs, it's usually telling you if the bot is simply not getting buy signals (only heartbeat messages), or if there is something wrong (errors / exceptions in the log). +* It may be because of a configuration error. It's best to check the logs, they usually tell you if the bot is simply not getting buy signals (only heartbeat messages), or if there is something wrong (errors / exceptions in the log). -### I have made 12 trades already, why is my total profit negative?! +### I have made 12 trades already, why is my total profit negative? I understand your disappointment but unfortunately 12 trades is just not enough to say anything. If you run backtesting, you can see that our @@ -36,11 +36,9 @@ of course constantly aim to improve the bot but it will _always_ be a gamble, which should leave you with modest wins on monthly basis but you can't say much from few trades. -### I’d like to change the stake amount. Can I just stop the bot with /stop and then change the config.json and run it again? +### I’d like to make changes to the config. Can I do that without having to kill the bot? -Not quite. Trades are persisted to a database but the configuration is -currently only read when the bot is killed and restarted. `/stop` more -like pauses. You can stop your bot, adjust settings and start it again. +Yes. You can edit your config, use the `/stop` command in Telegram, followed by `/reload_config` and the bot will run with the new config. ### I want to improve the bot with a new strategy @@ -49,7 +47,7 @@ the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-c ### Is there a setting to only SELL the coins being held and not perform anymore BUYS? -You can use the `/forcesell all` command from Telegram. +You can use the `/stopbuy` command in Telegram to prevent future buys, followed by `/forcesell all` (sell all open trades). ### I want to run multiple bots on the same machine @@ -59,7 +57,7 @@ Please look at the [advanced setup documentation Page](advanced-setup.md#running This message is just a warning that the latest candles had missing candles in them. Depending on the exchange, this can indicate that the pair didn't have a trade for the timeframe you are using - and the exchange does only return candles with volume. -On low volume pairs, this is a rather common occurance. +On low volume pairs, this is a rather common occurrence. If this happens for all pairs in the pairlist, this might indicate a recent exchange downtime. Please check your exchange's public channels for details. @@ -73,7 +71,7 @@ Read [the Bittrex section about restricted markets](exchanges.md#restricted-mark ### I'm getting the "Exchange Bittrex does not support market orders." message and cannot run my strategy -As the message says, Bittrex does not support market orders and you have one of the [order types](configuration.md/#understand-order_types) set to "market". Probably your strategy was written with other exchanges in mind and sets "market" orders for "stoploss" orders, which is correct and preferable for most of the exchanges supporting market orders (but not for Bittrex). +As the message says, Bittrex does not support market orders and you have one of the [order types](configuration.md/#understand-order_types) set to "market". Your strategy was probably written with other exchanges in mind and sets "market" orders for "stoploss" orders, which is correct and preferable for most of the exchanges supporting market orders (but not for Bittrex). To fix it for Bittrex, redefine order types in the strategy to use "limit" instead of "market": @@ -85,7 +83,7 @@ To fix it for Bittrex, redefine order types in the strategy to use "limit" inste } ``` -Same fix should be done in the configuration file, if order types are defined in your custom config rather than in the strategy. +The same fix should be applied in the configuration file, if order types are defined in your custom config rather than in the strategy. ### How do I search the bot logs for something? @@ -127,10 +125,10 @@ On Windows, the `--logfile` option is also supported by Freqtrade and you can us ## Hyperopt module -### How many epoch do I need to get a good Hyperopt result? +### How many epochs do I need to get a good Hyperopt result? Per default Hyperopt called without the `-e`/`--epochs` command line option will only -run 100 epochs, means 100 evals of your triggers, guards, ... Too few +run 100 epochs, means 100 evaluations of your triggers, guards, ... Too few to find a great result (unless if you are very lucky), so you probably have to run it for 10.000 or more. But it will take an eternity to compute. @@ -140,32 +138,32 @@ Since hyperopt uses Bayesian search, running for too many epochs may not produce It's therefore recommended to run between 500-1000 epochs over and over until you hit at least 10.000 epochs in total (or are satisfied with the result). You can best judge by looking at the results - if the bot keeps discovering better strategies, it's best to keep on going. ```bash -freqtrade hyperopt --hyperop SampleHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy SampleStrategy -e 1000 +freqtrade hyperopt --hyperopt SampleHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy SampleStrategy -e 1000 ``` ### Why does it take a long time to run hyperopt? -* Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) - or the Freqtrade [discord community](https://discord.gg/X89cVG). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you. +* Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg) - or the Freqtrade [discord community](https://discord.gg/X89cVG). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you. * If you wonder why it can take from 20 minutes to days to do 1000 epochs here are some answers: This answer was written during the release 0.15.1, when we had: -- 8 triggers -- 9 guards: let's say we evaluate even 10 values from each -- 1 stoploss calculation: let's say we want 10 values from that too to be evaluated +* 8 triggers +* 9 guards: let's say we evaluate even 10 values from each +* 1 stoploss calculation: let's say we want 10 values from that too to be evaluated The following calculation is still very rough and not very precise but it will give the idea. With only these triggers and guards there is -already 8\*10^9\*10 evaluations. A roughly total of 80 billion evals. -Did you run 100 000 evals? Congrats, you've done roughly 1 / 100 000 th +already 8\*10^9\*10 evaluations. A roughly total of 80 billion evaluations. +Did you run 100 000 evaluations? Congrats, you've done roughly 1 / 100 000 th of the search space, assuming that the bot never tests the same parameters more than once. * The time it takes to run 1000 hyperopt epochs depends on things like: The available cpu, hard-disk, ram, timeframe, timerange, indicator settings, indicator count, amount of coins that hyperopt test strategies on and the resulting trade count - which can be 650 trades in a year or 10.0000 trades depending if the strategy aims for big profits by trading rarely or for many low profit trades. Example: 4% profit 650 times vs 0,3% profit a trade 10.000 times in a year. If we assume you set the --timerange to 365 days. -Example: +Example: `freqtrade --config config.json --strategy SampleStrategy --hyperopt SampleHyperopt -e 1000 --timerange 20190601-20200601` ## Edge module diff --git a/docs/hyperopt.md b/docs/hyperopt.md index fc7a0dd93..f88d9cd4f 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -64,9 +64,9 @@ Depending on the space you want to optimize, only some of the below are required Optional in hyperopt - can also be loaded from a strategy (recommended): -* copy `populate_indicators` from your strategy - otherwise default-strategy will be used -* copy `populate_buy_trend` from your strategy - otherwise default-strategy will be used -* copy `populate_sell_trend` from your strategy - otherwise default-strategy will be used +* `populate_indicators` - fallback to create indicators +* `populate_buy_trend` - fallback if not optimizing for buy space. should come from strategy +* `populate_sell_trend` - fallback if not optimizing for sell space. should come from strategy !!! Note You always have to provide a strategy to Hyperopt, even if your custom Hyperopt class contains all methods. @@ -104,7 +104,7 @@ This command will create a new hyperopt file from a template, allowing you to ge There are two places you need to change in your hyperopt file to add a new buy hyperopt for testing: * Inside `indicator_space()` - the parameters hyperopt shall be optimizing. -* Inside `populate_buy_trend()` - applying the parameters. +* Within `buy_strategy_generator()` - populate the nested `populate_buy_trend()` to apply the parameters. There you have two different types of indicators: 1. `guards` and 2. `triggers`. @@ -128,7 +128,7 @@ Similar to the buy-signal above, sell-signals can also be optimized. Place the corresponding settings into the following methods * Inside `sell_indicator_space()` - the parameters hyperopt shall be optimizing. -* Inside `populate_sell_trend()` - applying the parameters. +* Within `sell_strategy_generator()` - populate the nested method `populate_sell_trend()` to apply the parameters. The configuration and rules are the same than for buy signals. To avoid naming collisions in the search-space, please prefix all sell-spaces with `sell-`. @@ -173,6 +173,11 @@ one we call `trigger` and use it to decide which buy trigger we want to use. So let's write the buy strategy using these values: ```python + @staticmethod + def buy_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the buy strategy parameters to be used by Hyperopt. + """ def populate_buy_trend(dataframe: DataFrame) -> DataFrame: conditions = [] # GUARDS AND TRENDS diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index ae4ec818d..844f1d70a 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -15,10 +15,12 @@ Inactive markets are always removed from the resulting pairlist. Explicitly blac * [`StaticPairList`](#static-pair-list) (default, if not configured differently) * [`VolumePairList`](#volume-pair-list) * [`AgeFilter`](#agefilter) +* [`PerformanceFilter`](#performancefilter) * [`PrecisionFilter`](#precisionfilter) * [`PriceFilter`](#pricefilter) * [`ShuffleFilter`](#shufflefilter) * [`SpreadFilter`](#spreadfilter) +* [`RangeStabilityFilter`](#rangestabilityfilter) !!! Tip "Testing pairlists" Pairlist configurations can be quite tricky to get right. Best use the [`test-pairlist`](utils.md#test-pairlist) utility sub-command to test your configuration quickly. @@ -35,6 +37,11 @@ It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklis ], ``` +By default, only currently enabled pairs are allowed. +To skip pair validation against active markets, set `"allow_inactive": true` within the `StaticPairList` configuration. +This can be useful for backtesting expired pairs (like quarterly spot-markets). +This option must be configured along with `exchange.skip_pair_validation` in the exchange configuration. + #### Volume Pair List `VolumePairList` employs sorting/filtering of pairs by their trading volume. It selects `number_assets` top pairs with sorting based on the `sort_key` (which can only be `quoteVolume`). @@ -54,7 +61,7 @@ The `refresh_period` setting allows to define the period (in seconds), at which "method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume", - "refresh_period": 1800, + "refresh_period": 1800 }], ``` @@ -68,6 +75,15 @@ be caught out buying before the pair has finished dropping in price. This filter allows freqtrade to ignore pairs until they have been listed for at least `min_days_listed` days. +#### PerformanceFilter + +Sorts pairs by past trade performance, as follows: +1. Positive performance. +2. No closed trades yet. +3. Negative performance. + +Trade count is used as a tie breaker. + #### PrecisionFilter Filters low-value coins which would not allow setting stoplosses. @@ -113,6 +129,27 @@ Example: If `DOGE/BTC` maximum bid is 0.00000026 and minimum ask is 0.00000027, the ratio is calculated as: `1 - bid/ask ~= 0.037` which is `> 0.005` and this pair will be filtered out. +#### RangeStabilityFilter + +Removes pairs where the difference between lowest low and highest high over `lookback_days` days is below `min_rate_of_change`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. + +In the below example: +If the trading range over the last 10 days is <1%, remove the pair from the whitelist. + +```json +"pairlists": [ + { + "method": "RangeStabilityFilter", + "lookback_days": 10, + "min_rate_of_change": 0.01, + "refresh_period": 1440 + } +] +``` + +!!! Tip + This Filter can be used to automatically remove stable coin pairs, which have a very low trading range, and are therefore extremely difficult to trade with profit. + ### Full example of Pairlist Handlers The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies both [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#price-filter), filtering all assets where 1 price unit is > 1%. Then the `SpreadFilter` is applied and pairs are finally shuffled with the random seed set to some predefined value. @@ -132,6 +169,12 @@ The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, {"method": "PrecisionFilter"}, {"method": "PriceFilter", "low_price_ratio": 0.01}, {"method": "SpreadFilter", "max_spread_ratio": 0.005}, + { + "method": "RangeStabilityFilter", + "lookback_days": 10, + "min_rate_of_change": 0.01, + "refresh_period": 1440 + }, {"method": "ShuffleFilter", "seed": 42} ], ``` diff --git a/docs/index.md b/docs/index.md index 5608587db..f63aeb6b8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -59,17 +59,14 @@ Alternatively ## Support -### Help / Slack / Discord +### Help / Discord / Slack -For any questions not covered by the documentation or for further information about the bot, we encourage you to join our passionate Slack community. +For any questions not covered by the documentation or for further information about the bot, or to simply engage with like-minded individuals, we encourage you to join our slack channel. -Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) to join the Freqtrade Slack channel. +Please check out our [discord server](https://discord.gg/MA9v74M). -Alternatively, check out the newly created [discord server](https://discord.gg/MA9v74M). - -!!! Note - Since the discord server is relatively new, answers to questions might be slightly delayed as currently the user base quite small. +You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg). ## Ready to try? -Begin by reading our installation guide [for docker](docker.md) (recommended), or for [installation without docker](installation.md). +Begin by reading our installation guide [for docker](docker_quickstart.md) (recommended), or for [installation without docker](installation.md). diff --git a/docs/installation.md b/docs/installation.md index 9b15c9685..5cc0e03f4 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -90,13 +90,13 @@ Each time you open a new terminal, you must run `source .env/bin/activate`. ## Custom Installation -We've included/collected install instructions for Ubuntu 16.04, MacOS, and Windows. These are guidelines and your success may vary with other distros. +We've included/collected install instructions for Ubuntu, MacOS, and Windows. These are guidelines and your success may vary with other distros. OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems. !!! Note Python3.6 or higher and the corresponding pip are assumed to be available. -=== "Ubuntu 16.04" +=== "Ubuntu/Debian" #### Install necessary dependencies ```bash @@ -105,13 +105,17 @@ OS Specific steps are listed first, the [Common](#common) section below is neces ``` === "RaspberryPi/Raspbian" - The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/) from at least September 2019. + The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/). This image comes with python3.7 preinstalled, making it easy to get freqtrade up and running. Tested using a Raspberry Pi 3 with the Raspbian Buster lite image, all updates applied. + ``` bash sudo apt-get install python3-venv libatlas-base-dev + # Use pywheels.org to speed up installation + sudo echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > tee /etc/pip.conf + git clone https://github.com/freqtrade/freqtrade.git cd freqtrade @@ -120,6 +124,7 @@ OS Specific steps are listed first, the [Common](#common) section below is neces !!! Note "Installation duration" Depending on your internet speed and the Raspberry Pi version, installation can take multiple hours to complete. + Due to this, we recommend to use the prebuild docker-image for Raspberry, by following the [Docker quickstart documentation](docker_quickstart.md) !!! Note The above does not install hyperopt dependencies. To install these, please use `python3 -m pip install -e .[hyperopt]`. diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index cd294bef6..87bc6dfdd 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==6.1.5 +mkdocs-material==6.1.6 mdx_truly_sane_lists==1.2 pymdown-extensions==8.0.1 diff --git a/docs/stoploss.md b/docs/stoploss.md index fa888cd47..1e21fc50d 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -23,11 +23,12 @@ These modes can be configured with these values: ``` !!! Note - Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market) and FTX (stop limit and stop-market) as of now. - Do not set too low stoploss value if using stop loss on exchange! - If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work + Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market, stop-loss-limit) and FTX (stop limit and stop-market) as of now. + Do not set too low/tight stoploss value if using stop loss on exchange! + If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work. ### stoploss_on_exchange and stoploss_on_exchange_limit_ratio + Enable or Disable stop loss on exchange. If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order happens successfully. This will protect you against sudden crashes in market as the order will be in the queue immediately and if market goes down then the order has more chance of being fulfilled. @@ -35,18 +36,23 @@ If `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the st `stoploss` defines the stop-price where the limit order is placed - and limit should be slightly below this. If an exchange supports both limit and market stoploss orders, then the value of `stoploss` will be used to determine the stoploss type. -Calculation example: we bought the asset at 100$. -Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the limit order fill can happen between 95$ and 94.05$. +Calculation example: we bought the asset at 100\$. +Stop-price is 95\$, then limit would be `95 * 0.99 = 94.05$` - so the limit order fill can happen between 95$ and 94.05$. For example, assuming the stoploss is on exchange, and trailing stoploss is enabled, and the market is going up, then the bot automatically cancels the previous stoploss order and puts a new one with a stop value higher than the previous stoploss order. +!!! Note + If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new stoploss order. + ### stoploss_on_exchange_interval + In case of stoploss on exchange there is another parameter called `stoploss_on_exchange_interval`. This configures the interval in seconds at which the bot will check the stoploss and update it if necessary. The bot cannot do these every 5 seconds (at each iteration), otherwise it would get banned by the exchange. So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute). This same logic will reapply a stoploss order on the exchange should you cancel it accidentally. ### emergencysell + `emergencysell` is an optional value, which defaults to `market` and is used when creating stop loss on exchange orders fails. The below is the default which is used if not changed in strategy or configuration file. @@ -84,6 +90,7 @@ Example of stop loss: ``` For example, simplified math: + * the bot buys an asset at a price of 100$ * the stop loss is defined at -10% * the stop loss would get triggered once the asset drops below 90$ @@ -107,7 +114,7 @@ For example, simplified math: * the stop loss would get triggered once the asset drops below 90$ * assuming the asset now increases to 102$ * the stop loss will now be -10% of 102$ = 91.8$ -* now the asset drops in value to 101$, the stop loss will still be 91.8$ and would trigger at 91.8$. +* now the asset drops in value to 101\$, the stop loss will still be 91.8$ and would trigger at 91.8$. In summary: The stoploss will be adjusted to be always be -10% of the highest observed price. @@ -133,8 +140,8 @@ For example, simplified math: * the stop loss is defined at -10% * the stop loss would get triggered once the asset drops below 90$ * assuming the asset now increases to 102$ -* the stop loss will now be -2% of 102$ = 99.96$ (99.96$ stop loss will be locked in and will follow asset price increasements with -2%) -* now the asset drops in value to 101$, the stop loss will still be 99.96$ and would trigger at 99.96$ +* the stop loss will now be -2% of 102$ = 99.96$ (99.96$ stop loss will be locked in and will follow asset price increments with -2%) +* now the asset drops in value to 101\$, the stop loss will still be 99.96$ and would trigger at 99.96$ The 0.02 would translate to a -2% stop loss. Before this, `stoploss` is used for the trailing stoploss. @@ -151,7 +158,7 @@ This option can be used with or without `trailing_stop_positive`, but uses `trai trailing_only_offset_is_reached = True ``` -Configuration (offset is buyprice + 3%): +Configuration (offset is buy-price + 3%): ``` python stoploss = -0.10 @@ -169,7 +176,7 @@ For example, simplified math: * stoploss will remain at 90$ unless asset increases to or above our configured offset * assuming the asset now increases to 103$ (where we have the offset configured) * the stop loss will now be -2% of 103$ = 100.94$ -* now the asset drops in value to 101$, the stop loss will still be 100.94$ and would trigger at 100.94$ +* now the asset drops in value to 101\$, the stop loss will still be 100.94$ and would trigger at 100.94$ !!! Tip Make sure to have this value (`trailing_stop_positive_offset`) lower than minimal ROI, otherwise minimal ROI will apply first and sell the trade. diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 6c7d78864..db007985f 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -770,8 +770,6 @@ To get additional Ideas for strategies, head over to our [strategy repository](h Feel free to use any of them as inspiration for your own strategies. We're happy to accept Pull Requests containing new Strategies to that repo. -We also got a *strategy-sharing* channel in our [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) which is a great place to get and/or share ideas. - ## Next step Now you have a perfect strategy you probably want to backtest it. diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 09cf21223..f4bd0a12a 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -207,7 +207,7 @@ Return a summary of your profit/loss and performance. Note that for this to work, `forcebuy_enable` needs to be set to true. -[More details](configuration.md/#understand-forcebuy_enable) +[More details](configuration.md#understand-forcebuy_enable) ### /performance diff --git a/docs/updating.md b/docs/updating.md new file mode 100644 index 000000000..b23ce32dc --- /dev/null +++ b/docs/updating.md @@ -0,0 +1,31 @@ +# How to update + +To update your freqtrade installation, please use one of the below methods, corresponding to your installation method. + +## docker-compose + +!!! Note "Legacy installations using the `master` image" + We're switching from master to stable for the release Images - please adjust your docker-file and replace `freqtradeorg/freqtrade:master` with `freqtradeorg/freqtrade:stable` + +``` bash +docker-compose pull +docker-compose up -d +``` + +## Installation via setup script + +``` bash +./setup.sh --update +``` + +!!! Note + Make sure to run this command with your virtual environment disabled! + +## Plain native installation + +Please ensure that you're also updating dependencies - otherwise things might break without you noticing. + +``` bash +git pull +pip install -U -r requirements.txt +``` diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index d4612d8e0..ab21bc686 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -137,6 +137,10 @@ def _validate_edge(conf: Dict[str, Any]) -> None: "Edge and VolumePairList are incompatible, " "Edge will override whatever pairs VolumePairlist selects." ) + if not conf.get('ask_strategy', {}).get('use_sell_signal', True): + raise OperationalException( + "Edge requires `use_sell_signal` to be True, otherwise no sells will happen." + ) def _validate_whitelist(conf: Dict[str, Any]) -> None: diff --git a/freqtrade/configuration/deprecated_settings.py b/freqtrade/configuration/deprecated_settings.py index 03ed41ab8..6b2a20c8c 100644 --- a/freqtrade/configuration/deprecated_settings.py +++ b/freqtrade/configuration/deprecated_settings.py @@ -26,6 +26,24 @@ def check_conflicting_settings(config: Dict[str, Any], ) +def process_removed_setting(config: Dict[str, Any], + section1: str, name1: str, + section2: str, name2: str) -> None: + """ + :param section1: Removed section + :param name1: Removed setting name + :param section2: new section for this key + :param name2: new setting name + """ + section1_config = config.get(section1, {}) + if name1 in section1_config: + raise OperationalException( + f"Setting `{section1}.{name1}` has been moved to `{section2}.{name2}. " + f"Please delete it from your configuration and use the `{section2}.{name2}` " + "setting instead." + ) + + def process_deprecated_setting(config: Dict[str, Any], section1: str, name1: str, section2: str, name2: str) -> None: @@ -44,19 +62,18 @@ def process_deprecated_setting(config: Dict[str, Any], def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None: - check_conflicting_settings(config, 'ask_strategy', 'use_sell_signal', - 'experimental', 'use_sell_signal') - check_conflicting_settings(config, 'ask_strategy', 'sell_profit_only', - 'experimental', 'sell_profit_only') - check_conflicting_settings(config, 'ask_strategy', 'ignore_roi_if_buy_signal', - 'experimental', 'ignore_roi_if_buy_signal') + # Kept for future deprecated / moved settings + # check_conflicting_settings(config, 'ask_strategy', 'use_sell_signal', + # 'experimental', 'use_sell_signal') + # process_deprecated_setting(config, 'ask_strategy', 'use_sell_signal', + # 'experimental', 'use_sell_signal') - process_deprecated_setting(config, 'ask_strategy', 'use_sell_signal', - 'experimental', 'use_sell_signal') - process_deprecated_setting(config, 'ask_strategy', 'sell_profit_only', - 'experimental', 'sell_profit_only') - process_deprecated_setting(config, 'ask_strategy', 'ignore_roi_if_buy_signal', - 'experimental', 'ignore_roi_if_buy_signal') + process_removed_setting(config, 'experimental', 'use_sell_signal', + 'ask_strategy', 'use_sell_signal') + process_removed_setting(config, 'experimental', 'sell_profit_only', + 'ask_strategy', 'sell_profit_only') + process_removed_setting(config, 'experimental', 'ignore_roi_if_buy_signal', + 'ask_strategy', 'ignore_roi_if_buy_signal') if (config.get('edge', {}).get('enabled', False) and 'capital_available_percentage' in config.get('edge', {})): diff --git a/freqtrade/constants.py b/freqtrade/constants.py index dc5384f6f..601e525c1 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -24,8 +24,9 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', 'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily', 'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', - 'AgeFilter', 'PrecisionFilter', 'PriceFilter', - 'ShuffleFilter', 'SpreadFilter'] + 'AgeFilter', 'PerformanceFilter', 'PrecisionFilter', + 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', + 'SpreadFilter'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S' @@ -182,9 +183,6 @@ CONF_SCHEMA = { 'experimental': { 'type': 'object', 'properties': { - 'use_sell_signal': {'type': 'boolean'}, - 'sell_profit_only': {'type': 'boolean'}, - 'ignore_roi_if_buy_signal': {'type': 'boolean'}, 'block_bad_exchanges': {'type': 'boolean'} } }, @@ -365,3 +363,6 @@ CANCEL_REASON = { # List of pairs with their timeframes PairWithTimeframe = Tuple[str, str] ListPairsWithTimeframes = List[PairWithTimeframe] + +# Type for trades list +TradeList = List[List] diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index 38fa670e9..d4053abaa 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -10,7 +10,7 @@ from typing import Any, Dict, List import pandas as pd from pandas import DataFrame, to_datetime -from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS +from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList logger = logging.getLogger(__name__) @@ -168,7 +168,7 @@ def trades_remove_duplicates(trades: List[List]) -> List[List]: return [i for i, _ in itertools.groupby(sorted(trades, key=itemgetter(0)))] -def trades_dict_to_list(trades: List[Dict]) -> List[List]: +def trades_dict_to_list(trades: List[Dict]) -> TradeList: """ Convert fetch_trades result into a List (to be more memory efficient). :param trades: List of trades, as returned by ccxt.fetch_trades. @@ -177,16 +177,18 @@ def trades_dict_to_list(trades: List[Dict]) -> List[List]: return [[t[col] for col in DEFAULT_TRADES_COLUMNS] for t in trades] -def trades_to_ohlcv(trades: List, timeframe: str) -> DataFrame: +def trades_to_ohlcv(trades: TradeList, timeframe: str) -> DataFrame: """ Converts trades list to OHLCV list - TODO: This should get a dedicated test :param trades: List of trades, as returned by ccxt.fetch_trades. :param timeframe: Timeframe to resample data to :return: OHLCV Dataframe. + :raises: ValueError if no trades are provided """ from freqtrade.exchange import timeframe_to_minutes timeframe_minutes = timeframe_to_minutes(timeframe) + if not trades: + raise ValueError('Trade-list empty.') df = pd.DataFrame(trades, columns=DEFAULT_TRADES_COLUMNS) df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms', utc=True,) diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py index 00e41673d..d116637e7 100644 --- a/freqtrade/data/history/hdf5datahandler.py +++ b/freqtrade/data/history/hdf5datahandler.py @@ -9,9 +9,9 @@ import pandas as pd from freqtrade import misc from freqtrade.configuration import TimeRange from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, - ListPairsWithTimeframes) + ListPairsWithTimeframes, TradeList) -from .idatahandler import IDataHandler, TradeList +from .idatahandler import IDataHandler logger = logging.getLogger(__name__) diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index a420b9dcc..3b8b5a2f0 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -214,10 +214,9 @@ def _download_pair_history(datadir: Path, data_handler.ohlcv_store(pair, timeframe, data=data) return True - except Exception as e: - logger.error( - f'Failed to download history data for pair: "{pair}", timeframe: {timeframe}. ' - f'Error: {e}' + except Exception: + logger.exception( + f'Failed to download history data for pair: "{pair}", timeframe: {timeframe}.' ) return False @@ -304,10 +303,9 @@ def _download_trades_history(exchange: Exchange, logger.info(f"New Amount of trades: {len(trades)}") return True - except Exception as e: - logger.error( + except Exception: + logger.exception( f'Failed to download historic trades for pair: "{pair}". ' - f'Error: {e}' ) return False @@ -356,9 +354,12 @@ def convert_trades_to_ohlcv(pairs: List[str], timeframes: List[str], if erase: if data_handler_ohlcv.ohlcv_purge(pair, timeframe): logger.info(f'Deleting existing data for pair {pair}, interval {timeframe}.') - ohlcv = trades_to_ohlcv(trades, timeframe) - # Store ohlcv - data_handler_ohlcv.ohlcv_store(pair, timeframe, data=ohlcv) + try: + ohlcv = trades_to_ohlcv(trades, timeframe) + # Store ohlcv + data_handler_ohlcv.ohlcv_store(pair, timeframe, data=ohlcv) + except ValueError: + logger.exception(f'Could not convert {pair} to OHLCV.') def get_timerange(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py index a170a9dc5..070d9039d 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/idatahandler.py @@ -13,16 +13,13 @@ from typing import List, Optional, Type from pandas import DataFrame from freqtrade.configuration import TimeRange -from freqtrade.constants import ListPairsWithTimeframes +from freqtrade.constants import ListPairsWithTimeframes, TradeList from freqtrade.data.converter import clean_ohlcv_dataframe, trades_remove_duplicates, trim_dataframe from freqtrade.exchange import timeframe_to_seconds logger = logging.getLogger(__name__) -# Type for trades list -TradeList = List[List] - class IDataHandler(ABC): diff --git a/freqtrade/data/history/jsondatahandler.py b/freqtrade/data/history/jsondatahandler.py index 6436aa13d..9122170d5 100644 --- a/freqtrade/data/history/jsondatahandler.py +++ b/freqtrade/data/history/jsondatahandler.py @@ -8,10 +8,10 @@ from pandas import DataFrame, read_json, to_datetime from freqtrade import misc from freqtrade.configuration import TimeRange -from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, ListPairsWithTimeframes +from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, ListPairsWithTimeframes, TradeList from freqtrade.data.converter import trades_dict_to_list -from .idatahandler import IDataHandler, TradeList +from .idatahandler import IDataHandler logger = logging.getLogger(__name__) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e74f5668c..611ce4abd 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -124,7 +124,8 @@ class Exchange: # Check if all pairs are available self.validate_stakecurrency(config['stake_currency']) - self.validate_pairs(config['exchange']['pair_whitelist']) + if not exchange_config.get('skip_pair_validation'): + self.validate_pairs(config['exchange']['pair_whitelist']) self.validate_ordertypes(config.get('order_types', {})) self.validate_order_time_in_force(config.get('order_time_in_force', {})) self.validate_required_startup_candles(config.get('startup_candle_count', 0)) @@ -523,7 +524,7 @@ class Exchange: 'rate': self.get_fee(pair) } }) - if closed_order["type"] in ["stop_loss_limit"]: + if closed_order["type"] in ["stop_loss_limit", "stop-loss-limit"]: closed_order["info"].update({"stopPrice": closed_order["price"]}) self._dry_run_open_orders[closed_order["id"]] = closed_order @@ -678,12 +679,25 @@ class Exchange: :param pair: Pair to download :param timeframe: Timeframe to get data for :param since_ms: Timestamp in milliseconds to get history from - :returns List with candle (OHLCV) data + :return: List with candle (OHLCV) data """ return asyncio.get_event_loop().run_until_complete( self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe, since_ms=since_ms)) + def get_historic_ohlcv_as_df(self, pair: str, timeframe: str, + since_ms: int) -> DataFrame: + """ + Minimal wrapper around get_historic_ohlcv - converting the result into a dataframe + :param pair: Pair to download + :param timeframe: Timeframe to get data for + :param since_ms: Timestamp in milliseconds to get history from + :return: OHLCV DataFrame + """ + ticks = self.get_historic_ohlcv(pair, timeframe, since_ms=since_ms) + return ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True, + drop_incomplete=self._ohlcv_partial_candle) + async def _async_get_historic_ohlcv(self, pair: str, timeframe: str, since_ms: int) -> List: diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 5b7aa5c5b..4e4713052 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -69,7 +69,8 @@ class Kraken(Exchange): Verify stop_loss against stoploss-order value (limit or price) Returns True if adjustment is necessary. """ - return order['type'] == 'stop-loss' and stop_loss > float(order['price']) + return (order['type'] in ('stop-loss', 'stop-loss-limit') + and stop_loss > float(order['price'])) @retrier(retries=0) def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: @@ -77,8 +78,15 @@ class Kraken(Exchange): Creates a stoploss market order. Stoploss market orders is the only stoploss type supported by kraken. """ + params = self._params.copy() - ordertype = "stop-loss" + if order_types.get('stoploss', 'market') == 'limit': + ordertype = "stop-loss-limit" + limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) + limit_rate = stop_price * limit_price_pct + params['price2'] = self.price_to_precision(pair, limit_rate) + else: + ordertype = "stop-loss" stop_price = self.price_to_precision(pair, stop_price) @@ -88,8 +96,6 @@ class Kraken(Exchange): return dry_order try: - params = self._params.copy() - amount = self.amount_to_precision(pair, amount) order = self._api.create_order(symbol=pair, type=ordertype, side='sell', diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7416d8236..c8d281852 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -616,6 +616,9 @@ class FreqtradeBot: # Calculate price buy_limit_requested = self.get_buy_rate(pair, True) + if not buy_limit_requested: + raise PricingError('Could not determine buy price.') + min_stake_amount = self._get_min_pair_stake_amount(pair, buy_limit_requested) if min_stake_amount is not None and min_stake_amount > stake_amount: logger.warning( diff --git a/freqtrade/loggers.py b/freqtrade/loggers.py index 169cd2610..fbb05d879 100644 --- a/freqtrade/loggers.py +++ b/freqtrade/loggers.py @@ -37,6 +37,13 @@ def _set_loggers(verbosity: int = 0, api_verbosity: str = 'info') -> None: ) +def get_existing_handlers(handlertype): + """ + Returns Existing handler or None (if the handler has not yet been added to the root handlers). + """ + return next((h for h in logging.root.handlers if isinstance(h, handlertype)), None) + + def setup_logging_pre() -> None: """ Early setup for logging. @@ -71,18 +78,24 @@ def setup_logging(config: Dict[str, Any]) -> None: # config['logfilename']), which defaults to '/dev/log', applicable for most # of the systems. address = (s[1], int(s[2])) if len(s) > 2 else s[1] if len(s) > 1 else '/dev/log' - handler = SysLogHandler(address=address) + handler_sl = get_existing_handlers(SysLogHandler) + if handler_sl: + logging.root.removeHandler(handler_sl) + handler_sl = SysLogHandler(address=address) # No datetime field for logging into syslog, to allow syslog # to perform reduction of repeating messages if this is set in the # syslog config. The messages should be equal for this. - handler.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s')) - logging.root.addHandler(handler) + handler_sl.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s')) + logging.root.addHandler(handler_sl) elif s[0] == 'journald': try: from systemd.journal import JournaldLogHandler except ImportError: raise OperationalException("You need the systemd python package be installed in " "order to use logging to journald.") + handler_jd = get_existing_handlers(JournaldLogHandler) + if handler_jd: + logging.root.removeHandler(handler_jd) handler_jd = JournaldLogHandler() # No datetime field for logging into journald, to allow syslog # to perform reduction of repeating messages if this is set in the @@ -90,6 +103,9 @@ def setup_logging(config: Dict[str, Any]) -> None: handler_jd.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s')) logging.root.addHandler(handler_jd) else: + handler_rf = get_existing_handlers(RotatingFileHandler) + if handler_rf: + logging.root.removeHandler(handler_rf) handler_rf = RotatingFileHandler(logfile, maxBytes=1024 * 1024 * 10, # 10Mb backupCount=10) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index c977a991b..b3799856e 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -58,16 +58,19 @@ def _generate_result_line(result: DataFrame, max_open_trades: int, first_column: """ Generate one result dict, with "first_column" as key. """ + profit_sum = result['profit_percent'].sum() + profit_total = profit_sum / max_open_trades + return { 'key': first_column, 'trades': len(result), 'profit_mean': result['profit_percent'].mean() if len(result) > 0 else 0.0, 'profit_mean_pct': result['profit_percent'].mean() * 100.0 if len(result) > 0 else 0.0, - 'profit_sum': result['profit_percent'].sum(), - 'profit_sum_pct': result['profit_percent'].sum() * 100.0, + 'profit_sum': profit_sum, + 'profit_sum_pct': round(profit_sum * 100.0, 2), 'profit_total_abs': result['profit_abs'].sum(), - 'profit_total': result['profit_percent'].sum() / max_open_trades, - 'profit_total_pct': result['profit_percent'].sum() * 100.0 / max_open_trades, + 'profit_total': profit_total, + 'profit_total_pct': round(profit_total * 100.0, 2), 'duration_avg': str(timedelta( minutes=round(result['trade_duration'].mean())) ) if not result.empty else '0:00', @@ -122,8 +125,8 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List result = results.loc[results['sell_reason'] == reason] profit_mean = result['profit_percent'].mean() - profit_sum = result["profit_percent"].sum() - profit_percent_tot = result['profit_percent'].sum() / max_open_trades + profit_sum = result['profit_percent'].sum() + profit_total = profit_sum / max_open_trades tabular_data.append( { @@ -137,8 +140,8 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List 'profit_sum': profit_sum, 'profit_sum_pct': round(profit_sum * 100, 2), 'profit_total_abs': result['profit_abs'].sum(), - 'profit_total': profit_percent_tot, - 'profit_total_pct': round(profit_percent_tot * 100, 2), + 'profit_total': profit_total, + 'profit_total_pct': round(profit_total * 100, 2), } ) return tabular_data @@ -253,13 +256,18 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], results=results.loc[results['open_at_end']], skip_nan=True) daily_stats = generate_daily_stats(results) - + best_pair = max([pair for pair in pair_results if pair['key'] != 'TOTAL'], + key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None + worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'], + key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None results['open_timestamp'] = results['open_date'].astype(int64) // 1e6 results['close_timestamp'] = results['close_date'].astype(int64) // 1e6 backtest_days = (max_date - min_date).days strat_stats = { 'trades': results.to_dict(orient='records'), + 'best_pair': best_pair, + 'worst_pair': worst_pair, 'results_per_pair': pair_results, 'sell_reason_summary': sell_reason_stats, 'left_open_trades': left_open_results, @@ -392,15 +400,25 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str: def text_table_add_metrics(strat_results: Dict) -> str: if len(strat_results['trades']) > 0: - min_trade = min(strat_results['trades'], key=lambda x: x['open_date']) + best_trade = max(strat_results['trades'], key=lambda x: x['profit_percent']) + worst_trade = min(strat_results['trades'], key=lambda x: x['profit_percent']) metrics = [ ('Backtesting from', strat_results['backtest_start'].strftime(DATETIME_PRINT_FORMAT)), ('Backtesting to', strat_results['backtest_end'].strftime(DATETIME_PRINT_FORMAT)), + ('Max open trades', strat_results['max_open_trades']), + ('', ''), # Empty line to improve readability ('Total trades', strat_results['total_trades']), - ('First trade', min_trade['open_date'].strftime(DATETIME_PRINT_FORMAT)), - ('First trade Pair', min_trade['pair']), ('Total Profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"), ('Trades per day', strat_results['trades_per_day']), + ('', ''), # Empty line to improve readability + ('Best Pair', f"{strat_results['best_pair']['key']} " + f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"), + ('Worst Pair', f"{strat_results['worst_pair']['key']} " + f"{round(strat_results['worst_pair']['profit_sum_pct'], 2)}%"), + ('Best trade', f"{best_trade['pair']} {round(best_trade['profit_percent'] * 100, 2)}%"), + ('Worst trade', f"{worst_trade['pair']} " + f"{round(worst_trade['profit_percent'] * 100, 2)}%"), + ('Best day', f"{round(strat_results['backtest_best_day'] * 100, 2)}%"), ('Worst day', f"{round(strat_results['backtest_worst_day'] * 100, 2)}%"), ('Days win/draw/lose', f"{strat_results['winning_days']} / " diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index 19cf1c090..e2a13c20a 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -37,7 +37,7 @@ class AgeFilter(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed + If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ return True @@ -49,7 +49,7 @@ class AgeFilter(IPairList): return (f"{self.name} - Filtering pairs with age less than " f"{self._min_days_listed} {plural(self._min_days_listed, 'day')}.") - def _validate_pair(self, ticker: dict) -> bool: + def _validate_pair(self, ticker: Dict) -> bool: """ Validate age for the ticker :param ticker: ticker dict as returned from ccxt.load_markets() diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index 6b5bd11e7..c869e499b 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -68,7 +68,7 @@ class IPairList(ABC): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed + If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ diff --git a/freqtrade/pairlist/PerformanceFilter.py b/freqtrade/pairlist/PerformanceFilter.py new file mode 100644 index 000000000..92a97099e --- /dev/null +++ b/freqtrade/pairlist/PerformanceFilter.py @@ -0,0 +1,66 @@ +""" +Performance pair list filter +""" +import logging +from typing import Any, Dict, List + +import pandas as pd + +from freqtrade.pairlist.IPairList import IPairList +from freqtrade.persistence import Trade + + +logger = logging.getLogger(__name__) + + +class PerformanceFilter(IPairList): + + def __init__(self, exchange, pairlistmanager, + config: Dict[str, Any], pairlistconfig: Dict[str, Any], + pairlist_pos: int) -> None: + super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) + + @property + def needstickers(self) -> bool: + """ + Boolean property defining if tickers are necessary. + If no Pairlist requries tickers, an empty List is passed + as tickers argument to filter_pairlist + """ + return False + + def short_desc(self) -> str: + """ + Short allowlist method description - used for startup-messages + """ + return f"{self.name} - Sorting pairs by performance." + + def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: + """ + Filters and sorts pairlist and returns the allowlist again. + Called on each bot iteration - please use internal caching if necessary + :param pairlist: pairlist to filter or sort + :param tickers: Tickers (from exchange.get_tickers()). May be cached. + :return: new allowlist + """ + # Get the trading performance for pairs from database + performance = pd.DataFrame(Trade.get_overall_performance()) + + # Skip performance-based sorting if no performance data is available + if len(performance) == 0: + return pairlist + + # Get pairlist from performance dataframe values + list_df = pd.DataFrame({'pair': pairlist}) + + # Set initial value for pairs with no trades to 0 + # Sort the list using: + # - primarily performance (high to low) + # - then count (low to high, so as to favor same performance with fewer trades) + # - then pair name alphametically + sorted_df = list_df.merge(performance, on='pair', how='left')\ + .fillna(0).sort_values(by=['count', 'pair'], ascending=True)\ + .sort_values(by=['profit'], ascending=False) + pairlist = sorted_df['pair'].tolist() + + return pairlist diff --git a/freqtrade/pairlist/PrecisionFilter.py b/freqtrade/pairlist/PrecisionFilter.py index cf853397b..29e32fd44 100644 --- a/freqtrade/pairlist/PrecisionFilter.py +++ b/freqtrade/pairlist/PrecisionFilter.py @@ -32,7 +32,7 @@ class PrecisionFilter(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed + If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ return True diff --git a/freqtrade/pairlist/PriceFilter.py b/freqtrade/pairlist/PriceFilter.py index 8cd57ee1d..bef1c0a15 100644 --- a/freqtrade/pairlist/PriceFilter.py +++ b/freqtrade/pairlist/PriceFilter.py @@ -35,7 +35,7 @@ class PriceFilter(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed + If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ return True diff --git a/freqtrade/pairlist/ShuffleFilter.py b/freqtrade/pairlist/ShuffleFilter.py index eb4f6dcc3..28778db7b 100644 --- a/freqtrade/pairlist/ShuffleFilter.py +++ b/freqtrade/pairlist/ShuffleFilter.py @@ -25,7 +25,7 @@ class ShuffleFilter(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed + If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ return False diff --git a/freqtrade/pairlist/SpreadFilter.py b/freqtrade/pairlist/SpreadFilter.py index 2527a3131..a636b90bd 100644 --- a/freqtrade/pairlist/SpreadFilter.py +++ b/freqtrade/pairlist/SpreadFilter.py @@ -24,7 +24,7 @@ class SpreadFilter(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed + If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ return True diff --git a/freqtrade/pairlist/StaticPairList.py b/freqtrade/pairlist/StaticPairList.py index aa6268ba3..2879cb364 100644 --- a/freqtrade/pairlist/StaticPairList.py +++ b/freqtrade/pairlist/StaticPairList.py @@ -24,11 +24,13 @@ class StaticPairList(IPairList): raise OperationalException(f"{self.name} can only be used in the first position " "in the list of Pairlist Handlers.") + self._allow_inactive = self._pairlistconfig.get('allow_inactive', False) + @property def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed + If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ return False @@ -47,7 +49,10 @@ class StaticPairList(IPairList): :param tickers: Tickers (from exchange.get_tickers()). :return: List of pairs """ - return self._whitelist_for_active_markets(self._config['exchange']['pair_whitelist']) + if self._allow_inactive: + return self._config['exchange']['pair_whitelist'] + else: + return self._whitelist_for_active_markets(self._config['exchange']['pair_whitelist']) def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: """ diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index 44e5c52d7..7d3c2c653 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -49,7 +49,7 @@ class VolumePairList(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed + If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ return True diff --git a/freqtrade/pairlist/rangestabilityfilter.py b/freqtrade/pairlist/rangestabilityfilter.py new file mode 100644 index 000000000..b460ff477 --- /dev/null +++ b/freqtrade/pairlist/rangestabilityfilter.py @@ -0,0 +1,89 @@ +""" +Rate of change pairlist filter +""" +import logging +from typing import Any, Dict + +import arrow +from cachetools.ttl import TTLCache + +from freqtrade.exceptions import OperationalException +from freqtrade.misc import plural +from freqtrade.pairlist.IPairList import IPairList + + +logger = logging.getLogger(__name__) + + +class RangeStabilityFilter(IPairList): + + def __init__(self, exchange, pairlistmanager, + config: Dict[str, Any], pairlistconfig: Dict[str, Any], + pairlist_pos: int) -> None: + super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) + + self._days = pairlistconfig.get('lookback_days', 10) + self._min_rate_of_change = pairlistconfig.get('min_rate_of_change', 0.01) + self._refresh_period = pairlistconfig.get('refresh_period', 1440) + + self._pair_cache: TTLCache = TTLCache(maxsize=100, ttl=self._refresh_period) + + if self._days < 1: + raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1") + if self._days > exchange.ohlcv_candle_limit: + raise OperationalException("RangeStabilityFilter requires lookback_days to not " + "exceed exchange max request size " + f"({exchange.ohlcv_candle_limit})") + + @property + def needstickers(self) -> bool: + """ + Boolean property defining if tickers are necessary. + If no Pairlist requires tickers, an empty List is passed + as tickers argument to filter_pairlist + """ + return True + + def short_desc(self) -> str: + """ + Short whitelist method description - used for startup-messages + """ + return (f"{self.name} - Filtering pairs with rate of change below " + f"{self._min_rate_of_change} over the last {plural(self._days, 'day')}.") + + def _validate_pair(self, ticker: Dict) -> bool: + """ + Validate trading range + :param ticker: ticker dict as returned from ccxt.load_markets() + :return: True if the pair can stay, False if it should be removed + """ + pair = ticker['symbol'] + # Check symbol in cache + if pair in self._pair_cache: + return self._pair_cache[pair] + + since_ms = int(arrow.utcnow() + .floor('day') + .shift(days=-self._days) + .float_timestamp) * 1000 + + daily_candles = self._exchange.get_historic_ohlcv_as_df(pair=pair, + timeframe='1d', + since_ms=since_ms) + result = False + if daily_candles is not None and not daily_candles.empty: + highest_high = daily_candles['high'].max() + lowest_low = daily_candles['low'].min() + pct_change = ((highest_high - lowest_low) / lowest_low) if lowest_low > 0 else 0 + if pct_change >= self._min_rate_of_change: + result = True + else: + self.log_on_refresh(logger.info, + f"Removed {pair} from whitelist, " + f"because rate of change over {plural(self._days, 'day')} is " + f"{pct_change:.3f}, which is below the " + f"threshold of {self._min_rate_of_change}.") + result = False + self._pair_cache[pair] = result + + return result diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 8160ffbbf..6027908da 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -397,7 +397,7 @@ class Trade(_DECL_BASE): if self.is_open: logger.info(f'{order_type.upper()}_SELL has been fulfilled for {self}.') self.close(safe_value_fallback(order, 'average', 'price')) - elif order_type in ('stop_loss_limit', 'stop-loss', 'stop'): + elif order_type in ('stop_loss_limit', 'stop-loss', 'stop-loss-limit', 'stop'): self.stoploss_order_id = None self.close_rate_requested = self.stop_loss if self.is_open: diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 7f4773d57..8c2c203e6 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -470,7 +470,7 @@ class ApiServer(RPC): @require_login @rpc_catch_errors - def _trades_delete(self, tradeid): + def _trades_delete(self, tradeid: int): """ Handler for DELETE /trades/ endpoint. Removes the trade from the database (tries to cancel open orders first!) @@ -508,6 +508,8 @@ class ApiServer(RPC): """ asset = request.json.get("pair") price = request.json.get("price", None) + price = float(price) if price is not None else price + trade = self._rpc_forcebuy(asset, price) if trade: return jsonify(trade.to_json()) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 90564a19d..9ac271ba0 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -524,7 +524,7 @@ class RPC: stake_currency = self._freqtrade.config.get('stake_currency') if not self._freqtrade.exchange.get_pair_quote_currency(pair) == stake_currency: raise RPCException( - f'Wrong pair selected. Please pairs with stake {stake_currency} pairs only') + f'Wrong pair selected. Only pairs with stake-currency {stake_currency} allowed.') # check if valid pair # check if pair already has an open pair @@ -542,7 +542,7 @@ class RPC: else: return None - def _rpc_delete(self, trade_id: str) -> Dict[str, Union[str, int]]: + def _rpc_delete(self, trade_id: int) -> Dict[str, Union[str, int]]: """ Handler for delete . Delete the given trade and close eventually existing open orders. diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 7cd4a9bde..074a6367f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -5,11 +5,11 @@ This module manage Telegram communication """ import json import logging -from typing import Any, Callable, Dict, List +from typing import Any, Callable, Dict, List, Union import arrow from tabulate import tabulate -from telegram import ParseMode, ReplyKeyboardMarkup, Update +from telegram import KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update from telegram.error import NetworkError, TelegramError from telegram.ext import CallbackContext, CommandHandler, Updater from telegram.utils.helpers import escape_markdown @@ -71,7 +71,7 @@ class Telegram(RPC): """ super().__init__(freqtrade) - self._updater: Updater = None + self._updater: Updater self._config = freqtrade.config self._init() if self._config.get('fiat_display_currency', None): @@ -232,7 +232,7 @@ class Telegram(RPC): :return: None """ - if 'table' in context.args: + if context.args and 'table' in context.args: self._status_table(update, context) return @@ -306,7 +306,7 @@ class Telegram(RPC): stake_cur = self._config['stake_currency'] fiat_disp_cur = self._config.get('fiat_display_currency', '') try: - timescale = int(context.args[0]) + timescale = int(context.args[0]) if context.args else 7 except (TypeError, ValueError, IndexError): timescale = 7 try: @@ -486,7 +486,10 @@ class Telegram(RPC): :return: None """ - trade_id = context.args[0] if len(context.args) > 0 else None + trade_id = context.args[0] if context.args and len(context.args) > 0 else None + if not trade_id: + self._send_msg("You must specify a trade-id or 'all'.") + return try: msg = self._rpc_forcesell(trade_id) self._send_msg('Forcesell Result: `{result}`'.format(**msg)) @@ -503,13 +506,13 @@ class Telegram(RPC): :param update: message update :return: None """ - - pair = context.args[0] - price = float(context.args[1]) if len(context.args) > 1 else None - try: - self._rpc_forcebuy(pair, price) - except RPCException as e: - self._send_msg(str(e)) + if context.args: + pair = context.args[0] + price = float(context.args[1]) if len(context.args) > 1 else None + try: + self._rpc_forcebuy(pair, price) + except RPCException as e: + self._send_msg(str(e)) @authorized_only def _trades(self, update: Update, context: CallbackContext) -> None: @@ -522,7 +525,7 @@ class Telegram(RPC): """ stake_cur = self._config['stake_currency'] try: - nrecent = int(context.args[0]) + nrecent = int(context.args[0]) if context.args else 10 except (TypeError, ValueError, IndexError): nrecent = 10 try: @@ -555,9 +558,10 @@ class Telegram(RPC): :param update: message update :return: None """ - - trade_id = context.args[0] if len(context.args) > 0 else None try: + if not context.args or len(context.args) == 0: + raise RPCException("Trade-id not set.") + trade_id = int(context.args[0]) msg = self._rpc_delete(trade_id) self._send_msg(( '`{result_msg}`\n' @@ -677,7 +681,7 @@ class Telegram(RPC): """ try: try: - limit = int(context.args[0]) + limit = int(context.args[0]) if context.args else 10 except (TypeError, ValueError, IndexError): limit = 10 logs = self._rpc_get_logs(limit)['logs'] @@ -859,7 +863,7 @@ class Telegram(RPC): f"*Current state:* `{val['state']}`" ) - def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN, + def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, disable_notification: bool = False) -> None: """ Send given markdown message @@ -869,9 +873,11 @@ class Telegram(RPC): :return: None """ - keyboard = [['/daily', '/profit', '/balance'], - ['/status', '/status table', '/performance'], - ['/count', '/start', '/stop', '/help']] + keyboard: List[List[Union[str, KeyboardButton]]] = [ + ['/daily', '/profit', '/balance'], + ['/status', '/status table', '/performance'], + ['/count', '/start', '/stop', '/help'] + ] reply_markup = ReplyKeyboardMarkup(keyboard) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 1c6aa535d..81f4e7651 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -476,40 +476,44 @@ class IStrategy(ABC): current_time=date, current_profit=current_profit, force_stoploss=force_stoploss, high=high) - if stoplossflag.sell_flag: - logger.debug(f"{trade.pair} - Stoploss hit. sell_flag=True, " - f"sell_type={stoplossflag.sell_type}") - return stoplossflag - # Set current rate to high for backtesting sell current_rate = high or rate current_profit = trade.calc_profit_ratio(current_rate) config_ask_strategy = self.config.get('ask_strategy', {}) - if buy and config_ask_strategy.get('ignore_roi_if_buy_signal', False): - # This one is noisy, commented out - # logger.debug(f"{trade.pair} - Buy signal still active. sell_flag=False") - return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) + # if buy signal and ignore_roi is set, we don't need to evaluate min_roi. + roi_reached = (not (buy and config_ask_strategy.get('ignore_roi_if_buy_signal', False)) + and self.min_roi_reached(trade=trade, current_profit=current_profit, + current_time=date)) - # Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee) - if self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date): + if config_ask_strategy.get('sell_profit_only', False) and trade.calc_profit(rate=rate) <= 0: + # Negative profits and sell_profit_only - ignore sell signal + sell_signal = False + else: + sell_signal = sell and not buy and config_ask_strategy.get('use_sell_signal', True) + # TODO: return here if sell-signal should be favored over ROI + + # Start evaluations + # Sequence: + # ROI (if not stoploss) + # Sell-signal + # Stoploss + if roi_reached and stoplossflag.sell_type != SellType.STOP_LOSS: logger.debug(f"{trade.pair} - Required profit reached. sell_flag=True, " f"sell_type=SellType.ROI") return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI) - if config_ask_strategy.get('sell_profit_only', False): - # This one is noisy, commented out - # logger.debug(f"{trade.pair} - Checking if trade is profitable...") - if trade.calc_profit(rate=rate) <= 0: - # This one is noisy, commented out - # logger.debug(f"{trade.pair} - Trade is not profitable. sell_flag=False") - return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) - - if sell and not buy and config_ask_strategy.get('use_sell_signal', True): + if sell_signal: logger.debug(f"{trade.pair} - Sell signal received. sell_flag=True, " f"sell_type=SellType.SELL_SIGNAL") return SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL) + if stoplossflag.sell_flag: + + logger.debug(f"{trade.pair} - Stoploss hit. sell_flag=True, " + f"sell_type={stoplossflag.sell_type}") + return stoplossflag + # This one is noisy, commented out... # logger.debug(f"{trade.pair} - No sell signal. sell_flag=False") return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 44590dbbe..b3f9fef07 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -184,6 +184,8 @@ class SampleStrategy(IStrategy): 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'] diff --git a/freqtrade/templates/subtemplates/indicators_full.j2 b/freqtrade/templates/subtemplates/indicators_full.j2 index 60a358bec..57d2ca665 100644 --- a/freqtrade/templates/subtemplates/indicators_full.j2 +++ b/freqtrade/templates/subtemplates/indicators_full.j2 @@ -62,6 +62,8 @@ 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'] diff --git a/mkdocs.yml b/mkdocs.yml index 8d1ce1cfe..c791386ae 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -20,17 +20,18 @@ nav: - Hyperopt: hyperopt.md - Edge Positioning: edge.md - Utility Subcommands: utils.md - - Exchange-specific Notes: exchanges.md - FAQ: faq.md - Data Analysis: - Jupyter Notebooks: data-analysis.md - Strategy analysis: strategy_analysis_example.md - Plotting: plotting.md - SQL Cheatsheet: sql_cheatsheet.md + - Exchange-specific Notes: exchanges.md - Advanced Post-installation Tasks: advanced-setup.md - Advanced Strategy: strategy-advanced.md - Advanced Hyperopt: advanced-hyperopt.md - Sandbox Testing: sandbox-testing.md + - Updating Freqtrade: updating.md - Deprecated Features: deprecated.md - Contributors Guide: developer.md theme: diff --git a/requirements-dev.txt b/requirements-dev.txt index 1c96a880a..e681274c8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,7 +3,7 @@ -r requirements-plot.txt -r requirements-hyperopt.txt -coveralls==2.1.2 +coveralls==2.2.0 flake8==3.8.4 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.1.0 diff --git a/requirements-plot.txt b/requirements-plot.txt index bd40bc0b5..1c3b03133 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==4.12.0 +plotly==4.13.0 diff --git a/requirements.txt b/requirements.txt index 67b69a5dd..f59754f93 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ numpy==1.19.4 pandas==1.1.4 -ccxt==1.37.69 -aiohttp==3.7.2 +ccxt==1.38.55 +aiohttp==3.7.3 SQLAlchemy==1.3.20 -python-telegram-bot==13.0 +python-telegram-bot==13.1 arrow==0.17.0 cachetools==4.1.1 requests==2.25.0 @@ -22,7 +22,7 @@ blosc==1.9.2 py_find_1st==1.1.4 # Load ticker files 30% faster -python-rapidjson==0.9.3 +python-rapidjson==0.9.4 # Notify systemd sdnotify==0.3.2 @@ -35,5 +35,5 @@ flask-cors==3.0.9 # Support for colorized terminal output colorama==0.4.4 # Building config files interactively -questionary==1.8.0 +questionary==1.8.1 prompt-toolkit==3.0.8 diff --git a/setup.sh b/setup.sh index 049a6a77e..af5e70691 100755 --- a/setup.sh +++ b/setup.sh @@ -56,18 +56,45 @@ function updateenv() { exit 1 fi source .env/bin/activate + SYS_ARCH=$(uname -m) echo "pip install in-progress. Please wait..." ${PYTHON} -m pip install --upgrade pip read -p "Do you want to install dependencies for dev [y/N]? " if [[ $REPLY =~ ^[Yy]$ ]] then - ${PYTHON} -m pip install --upgrade -r requirements-dev.txt + REQUIREMENTS=requirements-dev.txt else - ${PYTHON} -m pip install --upgrade -r requirements.txt - echo "Dev dependencies ignored." + REQUIREMENTS=requirements.txt + fi + REQUIREMENTS_HYPEROPT="" + REQUIREMENTS_PLOT="" + read -p "Do you want to install plotting dependencies (plotly) [y/N]? " + if [[ $REPLY =~ ^[Yy]$ ]] + then + REQUIREMENTS_PLOT="-r requirements-plot.txt" + fi + if [ "${SYS_ARCH}" == "armv7l" ]; then + echo "Detected Raspberry, installing cython, skipping hyperopt installation." + ${PYTHON} -m pip install --upgrade cython + else + # Is not Raspberry + read -p "Do you want to install hyperopt dependencies [y/N]? " + if [[ $REPLY =~ ^[Yy]$ ]] + then + REQUIREMENTS_HYPEROPT="-r requirements-hyperopt.txt" + fi fi + ${PYTHON} -m pip install --upgrade -r ${REQUIREMENTS} ${REQUIREMENTS_HYPEROPT} ${REQUIREMENTS_PLOT} + if [ $? -ne 0 ]; then + echo "Failed installing dependencies" + exit 1 + fi ${PYTHON} -m pip install -e . + if [ $? -ne 0 ]; then + echo "Failed installing Freqtrade" + exit 1 + fi echo "pip install completed" echo } @@ -134,11 +161,11 @@ function reset() { git fetch -a - if [ "1" == $(git branch -vv |grep -c "* develop") ] + if [ "1" == $(git branch -vv | grep -c "* develop") ] then echo "- Hard resetting of 'develop' branch." git reset --hard origin/develop - elif [ "1" == $(git branch -vv |grep -c "* stable") ] + elif [ "1" == $(git branch -vv | grep -c "* stable") ] then echo "- Hard resetting of 'stable' branch." git reset --hard origin/stable @@ -149,7 +176,7 @@ function reset() { fi if [ -d ".env" ]; then - echo "- Delete your previous virtual env" + echo "- Deleting your previous virtual env" rm -rf .env fi echo @@ -253,7 +280,7 @@ function install() { echo "Run the bot !" echo "-------------------------" echo "You can now use the bot by executing 'source .env/bin/activate; freqtrade '." - echo "You can see the list of available bot subcommands by executing 'source .env/bin/activate; freqtrade --help'." + echo "You can see the list of available bot sub-commands by executing 'source .env/bin/activate; freqtrade --help'." echo "You verify that freqtrade is installed successfully by running 'source .env/bin/activate; freqtrade --version'." } diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index fdba7900f..4fdcce4d2 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -1,10 +1,13 @@ # pragma pylint: disable=missing-docstring, C0103 import logging +import pytest + from freqtrade.configuration.timerange import TimeRange from freqtrade.data.converter import (convert_ohlcv_format, convert_trades_format, ohlcv_fill_up_missing_data, ohlcv_to_dataframe, - trades_dict_to_list, trades_remove_duplicates, trim_dataframe) + trades_dict_to_list, trades_remove_duplicates, + trades_to_ohlcv, trim_dataframe) from freqtrade.data.history import (get_timerange, load_data, load_pair_history, validate_backtest_data) from tests.conftest import log_has @@ -26,6 +29,28 @@ def test_ohlcv_to_dataframe(ohlcv_history_list, caplog): assert log_has('Converting candle (OHLCV) data to dataframe for pair UNITTEST/BTC.', caplog) +def test_trades_to_ohlcv(ohlcv_history_list, caplog): + + caplog.set_level(logging.DEBUG) + with pytest.raises(ValueError, match="Trade-list empty."): + trades_to_ohlcv([], '1m') + + trades = [ + [1570752011620, "13519807", None, "sell", 0.00141342, 23.0, 0.03250866], + [1570752011620, "13519808", None, "sell", 0.00141266, 54.0, 0.07628364], + [1570752017964, "13519809", None, "sell", 0.00141266, 8.0, 0.01130128]] + + df = trades_to_ohlcv(trades, '1m') + assert not df.empty + assert len(df) == 1 + assert 'open' in df.columns + assert 'high' in df.columns + assert 'low' in df.columns + assert 'close' in df.columns + assert df.loc[:, 'high'][0] == 0.00141342 + assert df.loc[:, 'low'][0] == 0.00141266 + + def test_ohlcv_fill_up_missing_data(testdatadir, caplog): data = load_pair_history(datadir=testdatadir, timeframe='1m', diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 538a0840f..99b22adda 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -312,10 +312,7 @@ def test_download_backtesting_data_exception(ohlcv_history, mocker, caplog, # clean files freshly downloaded _clean_test_file(file1_1) _clean_test_file(file1_5) - assert log_has( - 'Failed to download history data for pair: "MEME/BTC", timeframe: 1m. ' - 'Error: File Error', caplog - ) + assert log_has('Failed to download history data for pair: "MEME/BTC", timeframe: 1m.', caplog) def test_load_partial_missing(testdatadir, caplog) -> None: @@ -620,6 +617,12 @@ def test_convert_trades_to_ohlcv(mocker, default_conf, testdatadir, caplog): _clean_test_file(file1) _clean_test_file(file5) + assert not log_has('Could not convert NoDatapair to OHLCV.', caplog) + + convert_trades_to_ohlcv(['NoDatapair'], timeframes=['1m', '5m'], + datadir=testdatadir, timerange=tr, erase=True) + assert log_has('Could not convert NoDatapair to OHLCV.', caplog) + def test_datahandler_ohlcv_get_pairs(testdatadir): pairs = JsonDataHandler.ohlcv_get_pairs(testdatadir, '5m') diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index e4452a83c..42681b367 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1307,6 +1307,57 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name): assert log_has_re(r"Async code raised an exception: .*", caplog) +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_historic_ohlcv_as_df(default_conf, mocker, exchange_name): + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + ohlcv = [ + [ + arrow.utcnow().int_timestamp * 1000, # unix timestamp ms + 1, # open + 2, # high + 3, # low + 4, # close + 5, # volume (in quote currency) + ], + [ + arrow.utcnow().shift(minutes=5).int_timestamp * 1000, # unix timestamp ms + 1, # open + 2, # high + 3, # low + 4, # close + 5, # volume (in quote currency) + ], + [ + arrow.utcnow().shift(minutes=10).int_timestamp * 1000, # unix timestamp ms + 1, # open + 2, # high + 3, # low + 4, # close + 5, # volume (in quote currency) + ] + ] + pair = 'ETH/BTC' + + async def mock_candle_hist(pair, timeframe, since_ms): + return pair, timeframe, ohlcv + + exchange._async_get_candle_history = Mock(wraps=mock_candle_hist) + # one_call calculation * 1.8 should do 2 calls + + since = 5 * 60 * exchange._ft_has['ohlcv_candle_limit'] * 1.8 + ret = exchange.get_historic_ohlcv_as_df(pair, "5m", int(( + arrow.utcnow().int_timestamp - since) * 1000)) + + assert exchange._async_get_candle_history.call_count == 2 + # Returns twice the above OHLCV data + assert len(ret) == 2 + assert isinstance(ret, DataFrame) + assert 'date' in ret.columns + assert 'open' in ret.columns + assert 'close' in ret.columns + assert 'high' in ret.columns + + def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: ohlcv = [ [ diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 31b79a202..3803658eb 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -10,6 +10,7 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers STOPLOSS_ORDERTYPE = 'stop-loss' +STOPLOSS_LIMIT_ORDERTYPE = 'stop-loss-limit' def test_buy_kraken_trading_agreement(default_conf, mocker): @@ -156,7 +157,8 @@ def test_get_balances_prod(default_conf, mocker): "get_balances", "fetch_balance") -def test_stoploss_order_kraken(default_conf, mocker): +@pytest.mark.parametrize('ordertype', ['market', 'limit']) +def test_stoploss_order_kraken(default_conf, mocker, ordertype): api_mock = MagicMock() order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) @@ -173,24 +175,26 @@ def test_stoploss_order_kraken(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - # stoploss_on_exchange_limit_ratio is irrelevant for kraken market orders - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, - order_types={'stoploss_on_exchange_limit_ratio': 1.05}) - assert api_mock.create_order.call_count == 1 - - api_mock.create_order.reset_mock() - - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, + order_types={'stoploss': ordertype, + 'stoploss_on_exchange_limit_ratio': 0.99 + }) assert 'id' in order assert 'info' in order assert order['id'] == order_id assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' - assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE + if ordertype == 'limit': + assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_LIMIT_ORDERTYPE + assert api_mock.create_order.call_args_list[0][1]['params'] == { + 'trading_agreement': 'agree', 'price2': 217.8} + else: + assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE + assert api_mock.create_order.call_args_list[0][1]['params'] == { + 'trading_agreement': 'agree'} assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 assert api_mock.create_order.call_args_list[0][1]['price'] == 220 - assert api_mock.create_order.call_args_list[0][1]['params'] == {'trading_agreement': 'agree'} # test exception handling with pytest.raises(DependencyException): diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index a5de64fe4..720ed8c13 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -328,6 +328,118 @@ tc20 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] ) +# Test 21: trailing_stop ROI collision. +# Roi should trigger before Trailing stop - otherwise Trailing stop profits can be > ROI +# which cannot happen in reality +# stop-loss: 10%, ROI: 4%, Trailing stop adjusted at the sell candle +tc21 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5050, 4950, 5100, 6172, 0, 0], + [2, 5100, 5251, 4650, 5100, 6172, 0, 0], + [3, 4850, 5050, 4650, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.04}, profit_perc=0.04, trailing_stop=True, + trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, + trailing_stop_positive=0.03, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)] +) + +# Test 22: trailing_stop Raises in candle 2 - but ROI applies at the same time. +# applying a positive trailing stop of 3% - ROI should apply before trailing stop. +# stop-loss: 10%, ROI: 4%, stoploss adjusted candle 2 +tc22 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5050, 4950, 5100, 6172, 0, 0], + [2, 5100, 5251, 5100, 5100, 6172, 0, 0], + [3, 4850, 5050, 4650, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.04}, profit_perc=0.04, trailing_stop=True, + trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, + trailing_stop_positive=0.03, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)] +) + +# Test 23: trailing_stop Raises in candle 2 (does not trigger) +# applying a positive trailing stop of 3% since stop_positive_offset is reached. +# ROI is changed after this to 4%, dropping ROI below trailing_stop_positive, causing a sell +# in the candle after the raised stoploss candle with ROI reason. +# Stoploss would trigger in this candle too, but it's no longer relevant. +# stop-loss: 10%, ROI: 4%, stoploss adjusted candle 2, ROI adjusted in candle 3 (causing the sell) +tc23 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5050, 4950, 5100, 6172, 0, 0], + [2, 5100, 5251, 5100, 5100, 6172, 0, 0], + [3, 4850, 5251, 4650, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.1, "119": 0.03}, profit_perc=0.03, trailing_stop=True, + trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, + trailing_stop_positive=0.03, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] +) + +# Test 24: Sell with signal sell in candle 3 (stoploss also triggers on this candle) +# Stoploss at 1%. +# Stoploss wins over Sell-signal (because sell-signal is acted on in the next candle) +tc24 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5025, 4975, 4987, 6172, 1, 0], + [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) + [2, 4987, 5012, 4986, 4600, 6172, 0, 0], + [3, 5010, 5000, 4855, 5010, 6172, 0, 1], # Triggers stoploss + sellsignal + [4, 5010, 4987, 4977, 4995, 6172, 0, 0], + [5, 4995, 4995, 4995, 4950, 6172, 0, 0]], + stop_loss=-0.01, roi={"0": 1}, profit_perc=-0.01, use_sell_signal=True, + trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=3)] +) + +# Test 25: Sell with signal sell in candle 3 (stoploss also triggers on this candle) +# Stoploss at 1%. +# Sell-signal wins over stoploss +tc25 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5025, 4975, 4987, 6172, 1, 0], + [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) + [2, 4987, 5012, 4986, 4600, 6172, 0, 0], + [3, 5010, 5000, 4986, 5010, 6172, 0, 1], + [4, 5010, 4987, 4855, 4995, 6172, 0, 0], # Triggers stoploss + sellsignal acted on + [5, 4995, 4995, 4995, 4950, 6172, 0, 0]], + stop_loss=-0.01, roi={"0": 1}, profit_perc=0.002, use_sell_signal=True, + trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)] +) + +# Test 26: Sell with signal sell in candle 3 (ROI at signal candle) +# Stoploss at 10% (irrelevant), ROI at 5% (will trigger) +# Sell-signal wins over stoploss +tc26 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5025, 4975, 4987, 6172, 1, 0], + [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) + [2, 4987, 5012, 4986, 4600, 6172, 0, 0], + [3, 5010, 5251, 4986, 5010, 6172, 0, 1], # Triggers ROI, sell-signal + [4, 5010, 4987, 4855, 4995, 6172, 0, 0], + [5, 4995, 4995, 4995, 4950, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.05}, profit_perc=0.05, use_sell_signal=True, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] +) + +# Test 27: Sell with signal sell in candle 3 (ROI at signal candle) +# Stoploss at 10% (irrelevant), ROI at 5% (will trigger) - Wins over Sell-signal +# TODO: figure out if sell-signal should win over ROI +# Sell-signal wins over stoploss +tc27 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5025, 4975, 4987, 6172, 1, 0], + [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) + [2, 4987, 5012, 4986, 4600, 6172, 0, 0], + [3, 5010, 5012, 4986, 5010, 6172, 0, 1], # sell-signal + [4, 5010, 5251, 4855, 4995, 6172, 0, 0], # Triggers ROI, sell-signal acted on + [5, 4995, 4995, 4995, 4950, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.05}, profit_perc=0.05, use_sell_signal=True, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=4)] +) TESTS = [ tc0, @@ -351,6 +463,13 @@ TESTS = [ tc18, tc19, tc20, + tc21, + tc22, + tc23, + tc24, + tc25, + tc26, + tc27, ] diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 1f05bef1e..1d2f16b45 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -58,7 +58,7 @@ def whitelist_conf_2(default_conf): @pytest.fixture(scope="function") -def whitelist_conf_3(default_conf): +def whitelist_conf_agefilter(default_conf): default_conf['stake_currency'] = 'BTC' default_conf['exchange']['pair_whitelist'] = [ 'ETH/BTC', 'TKN/BTC', 'BLK/BTC', 'LTC/BTC', @@ -246,7 +246,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): {"method": "PrecisionFilter"}, {"method": "PriceFilter", "low_price_ratio": 0.03}, {"method": "SpreadFilter", "max_spread_ratio": 0.005}, - {"method": "ShuffleFilter"}], + {"method": "ShuffleFilter"}, {"method": "PerformanceFilter"}], "ETH", []), # AgeFilter and VolumePairList (require 2 days only, all should pass age test) ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, @@ -326,6 +326,13 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): # ShuffleFilter only ([{"method": "ShuffleFilter", "seed": 42}], "BTC", 'filter_at_the_beginning'), # OperationalException expected + # PerformanceFilter after StaticPairList + ([{"method": "StaticPairList"}, + {"method": "PerformanceFilter"}], + "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), + # PerformanceFilter only + ([{"method": "PerformanceFilter"}], + "BTC", 'filter_at_the_beginning'), # OperationalException expected # SpreadFilter after StaticPairList ([{"method": "StaticPairList"}, {"method": "SpreadFilter", "max_spread_ratio": 0.005}], @@ -340,6 +347,10 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, {"method": "PriceFilter", "low_price_ratio": 0.02}], "USDT", ['ETH/USDT', 'NANO/USDT']), + ([{"method": "StaticPairList"}, + {"method": "RangeStabilityFilter", "lookback_days": 10, + "min_rate_of_change": 0.01, "refresh_period": 1440}], + "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), ]) def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers, ohlcv_history_list, pairlists, base_currency, @@ -366,6 +377,11 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), ) + # Provide for PerformanceFilter's dependency + mocker.patch.multiple('freqtrade.persistence.Trade', + get_overall_performance=MagicMock(return_value=[]) + ) + # Set whitelist_result to None if pairlist is invalid and should produce exception if whitelist_result == 'filter_at_the_beginning': with pytest.raises(OperationalException, @@ -409,7 +425,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t assert not log_has(logmsg, caplog) -def test_PrecisionFilter_error(mocker, whitelist_conf, tickers) -> None: +def test_PrecisionFilter_error(mocker, whitelist_conf) -> None: whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}] del whitelist_conf['stoploss'] @@ -482,7 +498,7 @@ def test__whitelist_for_active_markets(mocker, whitelist_conf, markets, pairlist @pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS) -def test__whitelist_for_active_markets_empty(mocker, whitelist_conf, markets, pairlist, tickers): +def test__whitelist_for_active_markets_empty(mocker, whitelist_conf, pairlist, tickers): whitelist_conf['pairlists'][0]['method'] = pairlist mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) @@ -498,7 +514,7 @@ def test__whitelist_for_active_markets_empty(mocker, whitelist_conf, markets, pa pairlist_handler._whitelist_for_active_markets(['ETH/BTC']) -def test_volumepairlist_invalid_sortvalue(mocker, markets, whitelist_conf): +def test_volumepairlist_invalid_sortvalue(mocker, whitelist_conf): whitelist_conf['pairlists'][0].update({"sort_key": "asdf"}) mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) @@ -528,7 +544,7 @@ def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers): assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh == lrf -def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tickers, caplog): +def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tickers): default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, {'method': 'AgeFilter', 'min_days_listed': -1}] @@ -543,7 +559,7 @@ def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tick get_patched_freqtradebot(mocker, default_conf) -def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tickers, caplog): +def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tickers): default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, {'method': 'AgeFilter', 'min_days_listed': 99999}] @@ -559,7 +575,7 @@ def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tick get_patched_freqtradebot(mocker, default_conf) -def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_history_list): +def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, ohlcv_history_list): mocker.patch.multiple('freqtrade.exchange.Exchange', markets=PropertyMock(return_value=markets), @@ -571,7 +587,7 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_his get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), ) - freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_3) + freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_agefilter) assert freqtrade.exchange.get_historic_ohlcv.call_count == 0 freqtrade.pairlists.refresh_pairlist() assert freqtrade.exchange.get_historic_ohlcv.call_count > 0 @@ -582,6 +598,62 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_his assert freqtrade.exchange.get_historic_ohlcv.call_count == previous_call_count +def test_rangestabilityfilter_checks(mocker, default_conf, markets, tickers): + default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, + {'method': 'RangeStabilityFilter', 'lookback_days': 99999}] + + mocker.patch.multiple('freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers + ) + + with pytest.raises(OperationalException, + match=r'RangeStabilityFilter requires lookback_days to not exceed ' + r'exchange max request size \([0-9]+\)'): + get_patched_freqtradebot(mocker, default_conf) + + default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, + {'method': 'RangeStabilityFilter', 'lookback_days': 0}] + + with pytest.raises(OperationalException, + match='RangeStabilityFilter requires lookback_days to be >= 1'): + get_patched_freqtradebot(mocker, default_conf) + + +@pytest.mark.parametrize('min_rate_of_change,expected_length', [ + (0.01, 5), + (0.05, 0), # Setting rate_of_change to 5% removes all pairs from the whitelist. +]) +def test_rangestabilityfilter_caching(mocker, markets, default_conf, tickers, ohlcv_history_list, + min_rate_of_change, expected_length): + default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, + {'method': 'RangeStabilityFilter', 'lookback_days': 2, + 'min_rate_of_change': min_rate_of_change}] + + mocker.patch.multiple('freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers + ) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), + ) + + freqtrade = get_patched_freqtradebot(mocker, default_conf) + assert freqtrade.exchange.get_historic_ohlcv.call_count == 0 + freqtrade.pairlists.refresh_pairlist() + assert len(freqtrade.pairlists.whitelist) == expected_length + assert freqtrade.exchange.get_historic_ohlcv.call_count > 0 + + previous_call_count = freqtrade.exchange.get_historic_ohlcv.call_count + freqtrade.pairlists.refresh_pairlist() + assert len(freqtrade.pairlists.whitelist) == expected_length + # Should not have increased since first call. + assert freqtrade.exchange.get_historic_ohlcv.call_count == previous_call_count + + @pytest.mark.parametrize("pairlistconfig,desc_expected,exception_expected", [ ({"method": "PriceFilter", "low_price_ratio": 0.001, "min_price": 0.00000010, "max_price": 1.0}, @@ -617,6 +689,11 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_his None, "PriceFilter requires max_price to be >= 0" ), # OperationalException expected + ({"method": "RangeStabilityFilter", "lookback_days": 10, "min_rate_of_change": 0.01}, + "[{'RangeStabilityFilter': 'RangeStabilityFilter - Filtering pairs with rate of change below " + "0.01 over the last days.'}]", + None + ), ]) def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig, desc_expected, exception_expected): @@ -636,7 +713,7 @@ def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig, freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) -def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog): +def test_pairlistmanager_no_pairlist(mocker, whitelist_conf): mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) whitelist_conf['pairlists'] = [] @@ -644,3 +721,63 @@ def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog): with pytest.raises(OperationalException, match=r"No Pairlist Handlers defined"): get_patched_freqtradebot(mocker, whitelist_conf) + + +@pytest.mark.parametrize("pairlists,pair_allowlist,overall_performance,allowlist_result", [ + # No trades yet + ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], + ['ETH/BTC', 'TKN/BTC', 'LTC/BTC'], [], ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']), + # Happy path: Descending order, all values filled + ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], + ['ETH/BTC', 'TKN/BTC'], + [{'pair': 'TKN/BTC', 'profit': 5, 'count': 3}, {'pair': 'ETH/BTC', 'profit': 4, 'count': 2}], + ['TKN/BTC', 'ETH/BTC']), + # Performance data outside allow list ignored + ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], + ['ETH/BTC', 'TKN/BTC'], + [{'pair': 'OTHER/BTC', 'profit': 5, 'count': 3}, + {'pair': 'ETH/BTC', 'profit': 4, 'count': 2}], + ['ETH/BTC', 'TKN/BTC']), + # Partial performance data missing and sorted between positive and negative profit + ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], + ['ETH/BTC', 'TKN/BTC', 'LTC/BTC'], + [{'pair': 'ETH/BTC', 'profit': -5, 'count': 100}, + {'pair': 'TKN/BTC', 'profit': 4, 'count': 2}], + ['TKN/BTC', 'LTC/BTC', 'ETH/BTC']), + # Tie in performance data broken by count (ascending) + ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], + ['ETH/BTC', 'TKN/BTC', 'LTC/BTC'], + [{'pair': 'LTC/BTC', 'profit': -5.01, 'count': 101}, + {'pair': 'TKN/BTC', 'profit': -5.01, 'count': 2}, + {'pair': 'ETH/BTC', 'profit': -5.01, 'count': 100}], + ['TKN/BTC', 'ETH/BTC', 'LTC/BTC']), + # Tie in performance and count, broken by alphabetical sort + ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], + ['ETH/BTC', 'TKN/BTC', 'LTC/BTC'], + [{'pair': 'LTC/BTC', 'profit': -5.01, 'count': 1}, + {'pair': 'TKN/BTC', 'profit': -5.01, 'count': 1}, + {'pair': 'ETH/BTC', 'profit': -5.01, 'count': 1}], + ['ETH/BTC', 'LTC/BTC', 'TKN/BTC']), +]) +def test_performance_filter(mocker, whitelist_conf, pairlists, pair_allowlist, overall_performance, + allowlist_result, tickers, markets, ohlcv_history_list): + allowlist_conf = whitelist_conf + allowlist_conf['pairlists'] = pairlists + allowlist_conf['exchange']['pair_whitelist'] = pair_allowlist + + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + + freqtrade = get_patched_freqtradebot(mocker, allowlist_conf) + mocker.patch.multiple('freqtrade.exchange.Exchange', + get_tickers=tickers, + markets=PropertyMock(return_value=markets) + ) + mocker.patch.multiple('freqtrade.exchange.Exchange', + get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), + ) + mocker.patch.multiple('freqtrade.persistence.Trade', + get_overall_performance=MagicMock(return_value=overall_performance), + ) + freqtrade.pairlists.refresh_pairlist() + allowlist = freqtrade.pairlists.whitelist + assert allowlist == allowlist_result diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 23ca53e53..47e0f763d 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -868,7 +868,8 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order_open) -> assert trade.open_rate == 0.0001 # Test buy pair not with stakes - with pytest.raises(RPCException, match=r'Wrong pair selected. Please pairs with stake.*'): + with pytest.raises(RPCException, + match=r'Wrong pair selected. Only pairs with stake-currency.*'): rpc._rpc_forcebuy('LTC/ETH', 0.0001) pair = 'XRP/BTC' diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index d4682e4a5..73a549860 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -58,7 +58,6 @@ def test__init__(default_conf, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) telegram = Telegram(get_patched_freqtradebot(mocker, default_conf)) - assert telegram._updater is None assert telegram._config == default_conf @@ -339,6 +338,18 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] assert str(' 0 trade') in msg_mock.call_args_list[0][0][0] + # Reset msg_mock + msg_mock.reset_mock() + context.args = [] + telegram._daily(update=update, context=context) + assert msg_mock.call_count == 1 + assert 'Daily' in msg_mock.call_args_list[0][0][0] + assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] + assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] + assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] + assert str(' 0 trade') in msg_mock.call_args_list[0][0][0] + # Reset msg_mock msg_mock.reset_mock() freqtradebot.config['max_open_trades'] = 2 @@ -882,7 +893,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: context.args = [] telegram._forcesell(update=update, context=context) assert msg_mock.call_count == 1 - assert 'invalid argument' in msg_mock.call_args_list[0][0][0] + assert "You must specify a trade-id or 'all'." in msg_mock.call_args_list[0][0][0] # Invalid argument msg_mock.reset_mock() @@ -1224,8 +1235,14 @@ def test_telegram_trades(mocker, update, default_conf, fee): telegram._trades(update=update, context=context) assert "0 recent trades:" in msg_mock.call_args_list[0][0][0] assert "
" not in msg_mock.call_args_list[0][0][0]
-
     msg_mock.reset_mock()
+
+    context.args = ['hello']
+    telegram._trades(update=update, context=context)
+    assert "0 recent trades:" in msg_mock.call_args_list[0][0][0]
+    assert "
" not in msg_mock.call_args_list[0][0][0]
+    msg_mock.reset_mock()
+
     create_mock_trades(fee)
 
     context = MagicMock()
@@ -1252,7 +1269,7 @@ def test_telegram_delete_trade(mocker, update, default_conf, fee):
     context.args = []
 
     telegram._delete_trade(update=update, context=context)
-    assert "invalid argument" in msg_mock.call_args_list[0][0][0]
+    assert "Trade-id not set." in msg_mock.call_args_list[0][0][0]
 
     msg_mock.reset_mock()
     create_mock_trades(fee)
diff --git a/tests/test_configuration.py b/tests/test_configuration.py
index 7d6c81f74..167215f29 100644
--- a/tests/test_configuration.py
+++ b/tests/test_configuration.py
@@ -16,6 +16,7 @@ from freqtrade.configuration import (Configuration, check_exchange, remove_crede
 from freqtrade.configuration.config_validation import validate_config_schema
 from freqtrade.configuration.deprecated_settings import (check_conflicting_settings,
                                                          process_deprecated_setting,
+                                                         process_removed_setting,
                                                          process_temporary_deprecated_settings)
 from freqtrade.configuration.load_config import load_config_file, log_config_error_range
 from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL
@@ -663,7 +664,7 @@ def test_set_loggers() -> None:
 
 
 @pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
-def test_set_loggers_syslog(mocker):
+def test_set_loggers_syslog():
     logger = logging.getLogger()
     orig_handlers = logger.handlers
     logger.handlers = []
@@ -678,10 +679,38 @@ def test_set_loggers_syslog(mocker):
     assert [x for x in logger.handlers if type(x) == logging.handlers.SysLogHandler]
     assert [x for x in logger.handlers if type(x) == logging.StreamHandler]
     assert [x for x in logger.handlers if type(x) == logging.handlers.BufferingHandler]
+    # setting up logging again should NOT cause the loggers to be added a second time.
+    setup_logging(config)
+    assert len(logger.handlers) == 3
     # reset handlers to not break pytest
     logger.handlers = orig_handlers
 
 
+@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
+def test_set_loggers_Filehandler(tmpdir):
+    logger = logging.getLogger()
+    orig_handlers = logger.handlers
+    logger.handlers = []
+    logfile = Path(tmpdir) / 'ft_logfile.log'
+    config = {'verbosity': 2,
+              'logfile': str(logfile),
+              }
+
+    setup_logging_pre()
+    setup_logging(config)
+    assert len(logger.handlers) == 3
+    assert [x for x in logger.handlers if type(x) == logging.handlers.RotatingFileHandler]
+    assert [x for x in logger.handlers if type(x) == logging.StreamHandler]
+    assert [x for x in logger.handlers if type(x) == logging.handlers.BufferingHandler]
+    # setting up logging again should NOT cause the loggers to be added a second time.
+    setup_logging(config)
+    assert len(logger.handlers) == 3
+    # reset handlers to not break pytest
+    if logfile.exists:
+        logfile.unlink()
+    logger.handlers = orig_handlers
+
+
 @pytest.mark.skip(reason="systemd is not installed on every system, so we're not testing this.")
 def test_set_loggers_journald(mocker):
     logger = logging.getLogger()
@@ -812,6 +841,21 @@ def test_validate_edge(edge_conf):
     validate_config_consistency(edge_conf)
 
 
+def test_validate_edge2(edge_conf):
+    edge_conf.update({"ask_strategy": {
+        "use_sell_signal": True,
+    }})
+    # Passes test
+    validate_config_consistency(edge_conf)
+
+    edge_conf.update({"ask_strategy": {
+        "use_sell_signal": False,
+    }})
+    with pytest.raises(OperationalException, match="Edge requires `use_sell_signal` to be True, "
+                       "otherwise no sells will happen."):
+        validate_config_consistency(edge_conf)
+
+
 def test_validate_whitelist(default_conf):
     default_conf['runmode'] = RunMode.DRY_RUN
     # Test regular case - has whitelist and uses StaticPairlist
@@ -1018,13 +1062,11 @@ def test_pairlist_resolving_fallback(mocker):
     assert config['datadir'] == Path.cwd() / "user_data/data/binance"
 
 
+@pytest.mark.skip(reason='Currently no deprecated / moved sections')
+# The below is kept as a sample for the future.
 @pytest.mark.parametrize("setting", [
         ("ask_strategy", "use_sell_signal", True,
          "experimental", "use_sell_signal", False),
-        ("ask_strategy", "sell_profit_only", False,
-         "experimental", "sell_profit_only", True),
-        ("ask_strategy", "ignore_roi_if_buy_signal", False,
-         "experimental", "ignore_roi_if_buy_signal", True),
     ])
 def test_process_temporary_deprecated_settings(mocker, default_conf, setting, caplog):
     patched_configuration_load_config_file(mocker, default_conf)
@@ -1054,7 +1096,27 @@ def test_process_temporary_deprecated_settings(mocker, default_conf, setting, ca
     assert default_conf[setting[0]][setting[1]] == setting[5]
 
 
-def test_process_deprecated_setting_edge(mocker, edge_conf, caplog):
+@pytest.mark.parametrize("setting", [
+        ("experimental", "use_sell_signal", False),
+        ("experimental", "sell_profit_only", True),
+        ("experimental", "ignore_roi_if_buy_signal", True),
+    ])
+def test_process_removed_settings(mocker, default_conf, setting):
+    patched_configuration_load_config_file(mocker, default_conf)
+
+    # Create sections for new and deprecated settings
+    # (they may not exist in the config)
+    default_conf[setting[0]] = {}
+    # Assign removed setting
+    default_conf[setting[0]][setting[1]] = setting[2]
+
+    # New and deprecated settings are conflicting ones
+    with pytest.raises(OperationalException,
+                       match=r'Setting .* has been moved'):
+        process_temporary_deprecated_settings(default_conf)
+
+
+def test_process_deprecated_setting_edge(mocker, edge_conf):
     patched_configuration_load_config_file(mocker, edge_conf)
     edge_conf.update({'edge': {
         'enabled': True,
@@ -1153,6 +1215,30 @@ def test_process_deprecated_setting(mocker, default_conf, caplog):
     assert default_conf['sectionA']['new_setting'] == 'valA'
 
 
+def test_process_removed_setting(mocker, default_conf, caplog):
+    patched_configuration_load_config_file(mocker, default_conf)
+
+    # Create sections for new and deprecated settings
+    # (they may not exist in the config)
+    default_conf['sectionA'] = {}
+    default_conf['sectionB'] = {}
+    # Assign new setting
+    default_conf['sectionB']['somesetting'] = 'valA'
+
+    # Only new setting exists (nothing should happen)
+    process_removed_setting(default_conf,
+                            'sectionA', 'somesetting',
+                            'sectionB', 'somesetting')
+    # Assign removed setting
+    default_conf['sectionA']['somesetting'] = 'valB'
+
+    with pytest.raises(OperationalException,
+                       match=r"Setting .* has been moved"):
+        process_removed_setting(default_conf,
+                                'sectionA', 'somesetting',
+                                'sectionB', 'somesetting')
+
+
 def test_process_deprecated_ticker_interval(mocker, default_conf, caplog):
     message = "DEPRECATED: Please use 'timeframe' instead of 'ticker_interval."
     config = deepcopy(default_conf)
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 1f5b3ecaa..6adef510f 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -1074,6 +1074,12 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
     mocker.patch('freqtrade.exchange.Exchange.buy', MagicMock(return_value=limit_buy_order))
     assert not freqtrade.execute_buy(pair, stake_amount)
 
+    # Fail to get price...
+    mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_buy_rate', MagicMock(return_value=0.0))
+
+    with pytest.raises(PricingError, match="Could not determine buy price."):
+        freqtrade.execute_buy(pair, stake_amount)
+
 
 def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) -> None:
     freqtrade = get_patched_freqtradebot(mocker, default_conf)
@@ -3556,7 +3562,7 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_b
     # Test if buy-signal is absent
     patch_get_signal(freqtrade, value=(False, True))
     assert freqtrade.handle_trade(trade) is True
-    assert trade.sell_reason == SellType.STOP_LOSS.value
+    assert trade.sell_reason == SellType.SELL_SIGNAL.value
 
 
 def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, fee, caplog, mocker):