Merge pull request #4004 from freqtrade/new_release
New release 2020.11
This commit is contained in:
commit
ab7807cee5
@ -7,8 +7,8 @@ services:
|
||||
dockerfile: ".devcontainer/Dockerfile"
|
||||
volumes:
|
||||
# Allow git usage within container
|
||||
- "/home/${USER}/.ssh:/home/ftuser/.ssh:ro"
|
||||
- "/home/${USER}/.gitconfig:/home/ftuser/.gitconfig:ro"
|
||||
- "${HOME}/.ssh:/home/ftuser/.ssh:ro"
|
||||
- "${HOME}/.gitconfig:/home/ftuser/.gitconfig:ro"
|
||||
- ..:/freqtrade:cached
|
||||
# Persist bash-history
|
||||
- freqtrade-vscode-server:/home/ftuser/.vscode-server
|
||||
|
@ -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
|
||||
|
||||
|
12
README.md
12
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`.
|
||||
|
||||
|
@ -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",
|
||||
|
@ -162,6 +162,8 @@ 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 |
|
||||
@ -233,6 +235,8 @@ 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 |
|
||||
@ -251,16 +255,17 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||
|
||||
```
|
||||
|
||||
- `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.
|
||||
- `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).
|
||||
- `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 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
|
||||
|
@ -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) <br> **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) <br> **Datatype:** Dict
|
||||
| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer
|
||||
| `exchange.skip_pair_validation` | Skip pairlist validation on startup.<br>*Defaults to `false`<br> **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. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||
| `pairlists` | Define one or more pairlists to be used. [More information below](#pairlists-and-pairlist-handlers). <br>*Defaults to `StaticPairList`.* <br> **Datatype:** List of Dicts
|
||||
@ -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 <yourexchange> does not support market orders."`
|
||||
`"Exchange <yourexchange> 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.
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
```
|
||||
|
@ -145,7 +145,7 @@ freqtrade hyperopt --hyperop SampleHyperopt --hyperopt-loss SharpeHyperOptLossDa
|
||||
|
||||
### 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:
|
||||
|
||||
|
@ -19,6 +19,7 @@ Inactive markets are always removed from the resulting pairlist. Explicitly blac
|
||||
* [`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 +36,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 +60,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
|
||||
}],
|
||||
```
|
||||
|
||||
@ -113,6 +119,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 +159,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}
|
||||
],
|
||||
```
|
||||
|
@ -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).
|
||||
|
@ -1,3 +1,3 @@
|
||||
mkdocs-material==6.1.0
|
||||
mkdocs-material==6.1.6
|
||||
mdx_truly_sane_lists==1.2
|
||||
pymdown-extensions==8.0.1
|
||||
|
@ -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.
|
||||
<ins>Do not set too low stoploss value if using stop loss on exchange!</ins>
|
||||
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.
|
||||
<ins>Do not set too low/tight stoploss value if using stop loss on exchange!</ins>
|
||||
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.
|
||||
|
@ -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.
|
||||
|
@ -35,12 +35,30 @@ Copy the API Token (`22222222:APITOKEN` in the above example) and keep use it fo
|
||||
|
||||
Don't forget to start the conversation with your bot, by clicking `/START` button
|
||||
|
||||
### 2. Get your user id
|
||||
### 2. Telegram user_id
|
||||
|
||||
#### Get your user id
|
||||
|
||||
Talk to the [userinfobot](https://telegram.me/userinfobot)
|
||||
|
||||
Get your "Id", you will use it for the config parameter `chat_id`.
|
||||
|
||||
#### Use Group id
|
||||
|
||||
You can use bots in telegram groups by just adding them to the group. You can find the group id by first adding a [RawDataBot](https://telegram.me/rawdatabot) to your group. The Group id is shown as id in the `"chat"` section, which the RawDataBot will send to you:
|
||||
|
||||
``` json
|
||||
"chat":{
|
||||
"id":-1001332619709
|
||||
}
|
||||
```
|
||||
|
||||
For the Freqtrade configuration, you can then use the the full value (including `-` if it's there) as string:
|
||||
|
||||
```json
|
||||
"chat_id": "-1001332619709"
|
||||
```
|
||||
|
||||
## Control telegram noise
|
||||
|
||||
Freqtrade provides means to control the verbosity of your telegram bot.
|
||||
|
@ -32,7 +32,7 @@ python -m venv .env
|
||||
.env\Scripts\activate.ps1
|
||||
# optionally install ta-lib from wheel
|
||||
# Eventually adjust the below filename to match the downloaded wheel
|
||||
pip install build_helpes/TA_Lib‑0.4.19‑cp38‑cp38‑win_amd64.whl
|
||||
pip install build_helpers/TA_Lib-0.4.19-cp38-cp38-win_amd64.whl
|
||||
pip install -r requirements.txt
|
||||
pip install -e .
|
||||
freqtrade
|
||||
@ -50,8 +50,8 @@ freqtrade
|
||||
error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools
|
||||
```
|
||||
|
||||
Unfortunately, many packages requiring compilation don't provide a pre-build wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use.
|
||||
Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use.
|
||||
|
||||
The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building c code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker](docker.md) first.
|
||||
The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building C code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker](docker.md) first.
|
||||
|
||||
---
|
||||
|
@ -1,5 +1,5 @@
|
||||
""" Freqtrade bot """
|
||||
__version__ = '2020.10'
|
||||
__version__ = '2020.11'
|
||||
|
||||
if __version__ == 'develop':
|
||||
|
||||
|
@ -354,13 +354,11 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
'--data-format-ohlcv',
|
||||
help='Storage format for downloaded candle (OHLCV) data. (default: `%(default)s`).',
|
||||
choices=constants.AVAILABLE_DATAHANDLERS,
|
||||
default='json'
|
||||
),
|
||||
"dataformat_trades": Arg(
|
||||
'--data-format-trades',
|
||||
help='Storage format for downloaded trades data. (default: `%(default)s`).',
|
||||
choices=constants.AVAILABLE_DATAHANDLERS,
|
||||
default='jsongz'
|
||||
),
|
||||
"exchange": Arg(
|
||||
'--exchange',
|
||||
|
@ -1,10 +1,9 @@
|
||||
import logging
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import arrow
|
||||
|
||||
from freqtrade.configuration import TimeRange, setup_utils_configuration
|
||||
from freqtrade.data.converter import convert_ohlcv_format, convert_trades_format
|
||||
from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data,
|
||||
@ -29,7 +28,7 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
||||
"You can only specify one or the other.")
|
||||
timerange = TimeRange()
|
||||
if 'days' in config:
|
||||
time_since = arrow.utcnow().shift(days=-config['days']).strftime("%Y%m%d")
|
||||
time_since = (datetime.now() - timedelta(days=config['days'])).strftime("%Y%m%d")
|
||||
timerange = TimeRange.parse_timerange(f'{time_since}-')
|
||||
|
||||
if 'timerange' in config:
|
||||
|
@ -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:
|
||||
|
@ -52,11 +52,11 @@ class TimeRange:
|
||||
:return: None (Modifies the object in place)
|
||||
"""
|
||||
if (not self.starttype or (startup_candles
|
||||
and min_date.timestamp >= self.startts)):
|
||||
and min_date.int_timestamp >= self.startts)):
|
||||
# If no startts was defined, or backtest-data starts at the defined backtest-date
|
||||
logger.warning("Moving start-date by %s candles to account for startup time.",
|
||||
startup_candles)
|
||||
self.startts = (min_date.timestamp + timeframe_secs * startup_candles)
|
||||
self.startts = (min_date.int_timestamp + timeframe_secs * startup_candles)
|
||||
self.starttype = 'date'
|
||||
|
||||
@staticmethod
|
||||
@ -89,7 +89,7 @@ class TimeRange:
|
||||
if stype[0]:
|
||||
starts = rvals[index]
|
||||
if stype[0] == 'date' and len(starts) == 8:
|
||||
start = arrow.get(starts, 'YYYYMMDD').timestamp
|
||||
start = arrow.get(starts, 'YYYYMMDD').int_timestamp
|
||||
elif len(starts) == 13:
|
||||
start = int(starts) // 1000
|
||||
else:
|
||||
@ -98,7 +98,7 @@ class TimeRange:
|
||||
if stype[1]:
|
||||
stops = rvals[index]
|
||||
if stype[1] == 'date' and len(stops) == 8:
|
||||
stop = arrow.get(stops, 'YYYYMMDD').timestamp
|
||||
stop = arrow.get(stops, 'YYYYMMDD').int_timestamp
|
||||
elif len(stops) == 13:
|
||||
stop = int(stops) // 1000
|
||||
else:
|
||||
|
@ -25,7 +25,7 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
|
||||
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily']
|
||||
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
|
||||
'AgeFilter', 'PrecisionFilter', 'PriceFilter',
|
||||
'ShuffleFilter', 'SpreadFilter']
|
||||
'RangeStabilityFilter', 'ShuffleFilter', 'SpreadFilter']
|
||||
AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5']
|
||||
DRY_RUN_WALLET = 1000
|
||||
DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S'
|
||||
@ -365,3 +365,6 @@ CANCEL_REASON = {
|
||||
# List of pairs with their timeframes
|
||||
PairWithTimeframe = Tuple[str, str]
|
||||
ListPairsWithTimeframes = List[PairWithTimeframe]
|
||||
|
||||
# Type for trades list
|
||||
TradeList = List[List]
|
||||
|
@ -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,)
|
||||
|
@ -8,7 +8,6 @@ import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from arrow import Arrow
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe
|
||||
@ -38,7 +37,7 @@ class DataProvider:
|
||||
:param timeframe: Timeframe to get data for
|
||||
:param dataframe: analyzed dataframe
|
||||
"""
|
||||
self.__cached_pairs[(pair, timeframe)] = (dataframe, Arrow.utcnow().datetime)
|
||||
self.__cached_pairs[(pair, timeframe)] = (dataframe, datetime.now(timezone.utc))
|
||||
|
||||
def add_pairlisthandler(self, pairlists) -> None:
|
||||
"""
|
||||
@ -88,7 +87,8 @@ class DataProvider:
|
||||
"""
|
||||
return load_pair_history(pair=pair,
|
||||
timeframe=timeframe or self._config['timeframe'],
|
||||
datadir=self._config['datadir']
|
||||
datadir=self._config['datadir'],
|
||||
data_format=self._config.get('dataformat_ohlcv', 'json')
|
||||
)
|
||||
|
||||
def get_pair_dataframe(self, pair: str, timeframe: str = None) -> DataFrame:
|
||||
|
@ -3,14 +3,15 @@ import re
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
import numpy as np
|
||||
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__)
|
||||
@ -175,7 +176,8 @@ class HDF5DataHandler(IDataHandler):
|
||||
if timerange.stoptype == 'date':
|
||||
where.append(f"timestamp < {timerange.stopts * 1e3}")
|
||||
|
||||
trades = pd.read_hdf(filename, key=key, mode="r", where=where)
|
||||
trades: pd.DataFrame = pd.read_hdf(filename, key=key, mode="r", where=where)
|
||||
trades[['id', 'type']] = trades[['id', 'type']].replace({np.nan: None})
|
||||
return trades.values.tolist()
|
||||
|
||||
def trades_purge(self, pair: str) -> bool:
|
||||
|
@ -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]:
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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__)
|
||||
|
@ -87,7 +87,7 @@ class Edge:
|
||||
heartbeat = self.edge_config.get('process_throttle_secs')
|
||||
|
||||
if (self._last_updated > 0) and (
|
||||
self._last_updated + heartbeat > arrow.utcnow().timestamp):
|
||||
self._last_updated + heartbeat > arrow.utcnow().int_timestamp):
|
||||
return False
|
||||
|
||||
data: Dict[str, Any] = {}
|
||||
@ -146,7 +146,7 @@ class Edge:
|
||||
# Fill missing, calculable columns, profit, duration , abs etc.
|
||||
trades_df = self._fill_calculable_fields(DataFrame(trades))
|
||||
self._cached_pairs = self._process_expectancy(trades_df)
|
||||
self._last_updated = arrow.utcnow().timestamp
|
||||
self._last_updated = arrow.utcnow().int_timestamp
|
||||
|
||||
return True
|
||||
|
||||
|
@ -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))
|
||||
@ -282,7 +283,7 @@ class Exchange:
|
||||
asyncio.get_event_loop().run_until_complete(
|
||||
self._api_async.load_markets(reload=reload))
|
||||
|
||||
except ccxt.BaseError as e:
|
||||
except (asyncio.TimeoutError, ccxt.BaseError) as e:
|
||||
logger.warning('Could not load async markets. Reason: %s', e)
|
||||
return
|
||||
|
||||
@ -291,7 +292,7 @@ class Exchange:
|
||||
try:
|
||||
self._api.load_markets()
|
||||
self._load_async_markets()
|
||||
self._last_markets_refresh = arrow.utcnow().timestamp
|
||||
self._last_markets_refresh = arrow.utcnow().int_timestamp
|
||||
except ccxt.BaseError as e:
|
||||
logger.warning('Unable to initialize markets. Reason: %s', e)
|
||||
|
||||
@ -300,14 +301,14 @@ class Exchange:
|
||||
# Check whether markets have to be reloaded
|
||||
if (self._last_markets_refresh > 0) and (
|
||||
self._last_markets_refresh + self.markets_refresh_interval
|
||||
> arrow.utcnow().timestamp):
|
||||
> arrow.utcnow().int_timestamp):
|
||||
return None
|
||||
logger.debug("Performing scheduled market reload..")
|
||||
try:
|
||||
self._api.load_markets(reload=True)
|
||||
# Also reload async markets to avoid issues with newly listed pairs
|
||||
self._load_async_markets(reload=True)
|
||||
self._last_markets_refresh = arrow.utcnow().timestamp
|
||||
self._last_markets_refresh = arrow.utcnow().int_timestamp
|
||||
except ccxt.BaseError:
|
||||
logger.exception("Could not reload markets.")
|
||||
|
||||
@ -501,7 +502,7 @@ class Exchange:
|
||||
'side': side,
|
||||
'remaining': _amount,
|
||||
'datetime': arrow.utcnow().isoformat(),
|
||||
'timestamp': int(arrow.utcnow().timestamp * 1000),
|
||||
'timestamp': int(arrow.utcnow().int_timestamp * 1000),
|
||||
'status': "closed" if ordertype == "market" else "open",
|
||||
'fee': None,
|
||||
'info': {}
|
||||
@ -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:
|
||||
@ -699,7 +713,7 @@ class Exchange:
|
||||
)
|
||||
input_coroutines = [self._async_get_candle_history(
|
||||
pair, timeframe, since) for since in
|
||||
range(since_ms, arrow.utcnow().timestamp * 1000, one_call)]
|
||||
range(since_ms, arrow.utcnow().int_timestamp * 1000, one_call)]
|
||||
|
||||
results = await asyncio.gather(*input_coroutines, return_exceptions=True)
|
||||
|
||||
@ -766,7 +780,7 @@ class Exchange:
|
||||
interval_in_sec = timeframe_to_seconds(timeframe)
|
||||
|
||||
return not ((self._pairs_last_refresh_time.get((pair, timeframe), 0)
|
||||
+ interval_in_sec) >= arrow.utcnow().timestamp)
|
||||
+ interval_in_sec) >= arrow.utcnow().int_timestamp)
|
||||
|
||||
@retrier_async
|
||||
async def _async_get_candle_history(self, pair: str, timeframe: str,
|
||||
|
@ -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',
|
||||
|
@ -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)
|
||||
|
@ -340,7 +340,7 @@ class Backtesting:
|
||||
# max_open_trades must be respected
|
||||
# don't open on the last row
|
||||
if ((position_stacking or len(open_trades[pair]) == 0)
|
||||
and max_open_trades > 0 and open_trade_count_start < max_open_trades
|
||||
and (max_open_trades <= 0 or open_trade_count_start < max_open_trades)
|
||||
and tmp != end_date
|
||||
and row[BUY_IDX] == 1 and row[SELL_IDX] != 1):
|
||||
# Enter trade
|
||||
|
@ -268,9 +268,9 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
|
||||
'profit_total': results['profit_percent'].sum(),
|
||||
'profit_total_abs': results['profit_abs'].sum(),
|
||||
'backtest_start': min_date.datetime,
|
||||
'backtest_start_ts': min_date.timestamp * 1000,
|
||||
'backtest_start_ts': min_date.int_timestamp * 1000,
|
||||
'backtest_end': max_date.datetime,
|
||||
'backtest_end_ts': max_date.timestamp * 1000,
|
||||
'backtest_end_ts': max_date.int_timestamp * 1000,
|
||||
'backtest_days': backtest_days,
|
||||
|
||||
'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else 0,
|
||||
@ -396,6 +396,8 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
||||
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']),
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
"""
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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]:
|
||||
"""
|
||||
|
@ -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
|
||||
|
89
freqtrade/pairlist/rangestabilityfilter.py
Normal file
89
freqtrade/pairlist/rangestabilityfilter.py
Normal file
@ -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
|
@ -270,7 +270,6 @@ class Trade(_DECL_BASE):
|
||||
'amount_requested': round(self.amount_requested, 8) if self.amount_requested else None,
|
||||
'stake_amount': round(self.stake_amount, 8),
|
||||
'strategy': self.strategy,
|
||||
'ticker_interval': self.timeframe, # DEPRECATED
|
||||
'timeframe': self.timeframe,
|
||||
|
||||
'fee_open': self.fee_open,
|
||||
@ -295,12 +294,16 @@ class Trade(_DECL_BASE):
|
||||
tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None,
|
||||
'close_rate': self.close_rate,
|
||||
'close_rate_requested': self.close_rate_requested,
|
||||
'close_profit': self.close_profit,
|
||||
'close_profit_abs': self.close_profit_abs,
|
||||
'close_profit': self.close_profit, # Deprecated
|
||||
'close_profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
|
||||
'close_profit_abs': self.close_profit_abs, # Deprecated
|
||||
|
||||
'profit_ratio': self.close_profit,
|
||||
'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
|
||||
'profit_abs': self.close_profit_abs,
|
||||
|
||||
'sell_reason': self.sell_reason,
|
||||
'sell_order_status': self.sell_order_status,
|
||||
'stop_loss': self.stop_loss, # Deprecated - should not be used
|
||||
'stop_loss_abs': self.stop_loss,
|
||||
'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None,
|
||||
'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None,
|
||||
@ -309,7 +312,6 @@ class Trade(_DECL_BASE):
|
||||
if self.stoploss_last_update else None),
|
||||
'stoploss_last_update_timestamp': int(self.stoploss_last_update.replace(
|
||||
tzinfo=timezone.utc).timestamp() * 1000) if self.stoploss_last_update else None,
|
||||
'initial_stop_loss': self.initial_stop_loss, # Deprecated - should not be used
|
||||
'initial_stop_loss_abs': self.initial_stop_loss,
|
||||
'initial_stop_loss_ratio': (self.initial_stop_loss_pct
|
||||
if self.initial_stop_loss_pct else None),
|
||||
@ -395,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:
|
||||
|
@ -9,9 +9,9 @@ from freqtrade.data.btanalysis import (calculate_max_drawdown, combine_dataframe
|
||||
create_cum_profit, extract_trades_of_period, load_trades)
|
||||
from freqtrade.data.converter import trim_dataframe
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.data.history import load_data
|
||||
from freqtrade.data.history import get_timerange, load_data
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_prev_date
|
||||
from freqtrade.exchange import timeframe_to_prev_date, timeframe_to_seconds
|
||||
from freqtrade.misc import pair_to_filename
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
from freqtrade.strategy import IStrategy
|
||||
@ -29,7 +29,7 @@ except ImportError:
|
||||
exit(1)
|
||||
|
||||
|
||||
def init_plotscript(config):
|
||||
def init_plotscript(config, startup_candles: int = 0):
|
||||
"""
|
||||
Initialize objects needed for plotting
|
||||
:return: Dict with candle (OHLCV) data, trades and pairs
|
||||
@ -48,9 +48,16 @@ def init_plotscript(config):
|
||||
pairs=pairs,
|
||||
timeframe=config.get('timeframe', '5m'),
|
||||
timerange=timerange,
|
||||
startup_candles=startup_candles,
|
||||
data_format=config.get('dataformat_ohlcv', 'json'),
|
||||
)
|
||||
|
||||
if startup_candles:
|
||||
min_date, max_date = get_timerange(data)
|
||||
logger.info(f"Loading data from {min_date} to {max_date}")
|
||||
timerange.adjust_start_if_necessary(timeframe_to_seconds(config.get('timeframe', '5m')),
|
||||
startup_candles, min_date)
|
||||
|
||||
no_trades = False
|
||||
filename = config.get('exportfilename')
|
||||
if config.get('no_trades', False):
|
||||
@ -72,6 +79,7 @@ def init_plotscript(config):
|
||||
return {"ohlcv": data,
|
||||
"trades": trades,
|
||||
"pairs": pairs,
|
||||
"timerange": timerange,
|
||||
}
|
||||
|
||||
|
||||
@ -474,7 +482,8 @@ def load_and_plot_trades(config: Dict[str, Any]):
|
||||
|
||||
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
|
||||
IStrategy.dp = DataProvider(config, exchange)
|
||||
plot_elements = init_plotscript(config)
|
||||
plot_elements = init_plotscript(config, strategy.startup_candle_count)
|
||||
timerange = plot_elements['timerange']
|
||||
trades = plot_elements['trades']
|
||||
pair_counter = 0
|
||||
for pair, data in plot_elements["ohlcv"].items():
|
||||
@ -482,6 +491,7 @@ def load_and_plot_trades(config: Dict[str, Any]):
|
||||
logger.info("analyse pair %s", pair)
|
||||
|
||||
df_analyzed = strategy.analyze_ticker(data, {'pair': pair})
|
||||
df_analyzed = trim_dataframe(df_analyzed, timerange)
|
||||
trades_pair = trades.loc[trades['pair'] == pair]
|
||||
trades_pair = extract_trades_of_period(df_analyzed, trades_pair)
|
||||
|
||||
|
@ -329,7 +329,7 @@ class ApiServer(RPC):
|
||||
"""
|
||||
Prints the bot's version
|
||||
"""
|
||||
return jsonify(self._rpc_show_config(self._config))
|
||||
return jsonify(RPC._rpc_show_config(self._config, self._freqtrade.state))
|
||||
|
||||
@require_login
|
||||
@rpc_catch_errors
|
||||
@ -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())
|
||||
|
@ -93,7 +93,8 @@ class RPC:
|
||||
def send_msg(self, msg: Dict[str, str]) -> None:
|
||||
""" Sends a message to all registered rpc modules """
|
||||
|
||||
def _rpc_show_config(self, config) -> Dict[str, Any]:
|
||||
@staticmethod
|
||||
def _rpc_show_config(config, botstate: State) -> Dict[str, Any]:
|
||||
"""
|
||||
Return a dict of config options.
|
||||
Explicitly does NOT return the full config to avoid leakage of sensitive
|
||||
@ -104,22 +105,24 @@ class RPC:
|
||||
'stake_currency': config['stake_currency'],
|
||||
'stake_amount': config['stake_amount'],
|
||||
'max_open_trades': config['max_open_trades'],
|
||||
'minimal_roi': config['minimal_roi'].copy(),
|
||||
'stoploss': config['stoploss'],
|
||||
'trailing_stop': config['trailing_stop'],
|
||||
'minimal_roi': config['minimal_roi'].copy() if 'minimal_roi' in config else {},
|
||||
'stoploss': config.get('stoploss'),
|
||||
'trailing_stop': config.get('trailing_stop'),
|
||||
'trailing_stop_positive': config.get('trailing_stop_positive'),
|
||||
'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'),
|
||||
'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'),
|
||||
'ticker_interval': config['timeframe'], # DEPRECATED
|
||||
'timeframe': config['timeframe'],
|
||||
'timeframe_ms': timeframe_to_msecs(config['timeframe']),
|
||||
'timeframe_min': timeframe_to_minutes(config['timeframe']),
|
||||
'timeframe': config.get('timeframe'),
|
||||
'timeframe_ms': timeframe_to_msecs(config['timeframe']
|
||||
) if 'timeframe' in config else '',
|
||||
'timeframe_min': timeframe_to_minutes(config['timeframe']
|
||||
) if 'timeframe' in config else '',
|
||||
'exchange': config['exchange']['name'],
|
||||
'strategy': config['strategy'],
|
||||
'forcebuy_enabled': config.get('forcebuy_enable', False),
|
||||
'ask_strategy': config.get('ask_strategy', {}),
|
||||
'bid_strategy': config.get('bid_strategy', {}),
|
||||
'state': str(self._freqtrade.state) if self._freqtrade else '',
|
||||
'state': str(botstate),
|
||||
'runmode': config['runmode'].value
|
||||
}
|
||||
return val
|
||||
|
||||
@ -152,17 +155,18 @@ class RPC:
|
||||
stoploss_current_dist = trade.stop_loss - current_rate
|
||||
stoploss_current_dist_ratio = stoploss_current_dist / current_rate
|
||||
|
||||
fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%'
|
||||
if trade.close_profit is not None else None)
|
||||
trade_dict = trade.to_json()
|
||||
trade_dict.update(dict(
|
||||
base_currency=self._freqtrade.config['stake_currency'],
|
||||
close_profit=trade.close_profit if trade.close_profit is not None else None,
|
||||
close_profit_pct=fmt_close_profit,
|
||||
current_rate=current_rate,
|
||||
current_profit=current_profit,
|
||||
current_profit_pct=round(current_profit * 100, 2),
|
||||
current_profit_abs=current_profit_abs,
|
||||
current_profit=current_profit, # Deprectated
|
||||
current_profit_pct=round(current_profit * 100, 2), # Deprectated
|
||||
current_profit_abs=current_profit_abs, # Deprectated
|
||||
profit_ratio=current_profit,
|
||||
profit_pct=round(current_profit * 100, 2),
|
||||
profit_abs=current_profit_abs,
|
||||
|
||||
stoploss_current_dist=stoploss_current_dist,
|
||||
stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8),
|
||||
stoploss_current_dist_pct=round(stoploss_current_dist_ratio * 100, 2),
|
||||
@ -520,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
|
||||
@ -601,8 +605,6 @@ class RPC:
|
||||
|
||||
def _rpc_locks(self) -> Dict[str, Any]:
|
||||
""" Returns the current locks"""
|
||||
if self._freqtrade.state != State.RUNNING:
|
||||
raise RPCException('trader is not running')
|
||||
|
||||
locks = PairLocks.get_pair_locks(None)
|
||||
return {
|
||||
|
@ -247,18 +247,17 @@ class Telegram(RPC):
|
||||
"*Open Rate:* `{open_rate:.8f}`",
|
||||
"*Close Rate:* `{close_rate}`" if r['close_rate'] else "",
|
||||
"*Current Rate:* `{current_rate:.8f}`",
|
||||
("*Close Profit:* `{close_profit_pct}`"
|
||||
if r['close_profit_pct'] is not None else ""),
|
||||
"*Current Profit:* `{current_profit_pct:.2f}%`",
|
||||
("*Current Profit:* " if r['is_open'] else "*Close Profit: *")
|
||||
+ "`{profit_pct:.2f}%`",
|
||||
]
|
||||
if (r['stop_loss'] != r['initial_stop_loss']
|
||||
if (r['stop_loss_abs'] != r['initial_stop_loss_abs']
|
||||
and r['initial_stop_loss_pct'] is not None):
|
||||
# Adding initial stoploss only if it is different from stoploss
|
||||
lines.append("*Initial Stoploss:* `{initial_stop_loss:.8f}` "
|
||||
lines.append("*Initial Stoploss:* `{initial_stop_loss_abs:.8f}` "
|
||||
"`({initial_stop_loss_pct:.2f}%)`")
|
||||
|
||||
# Adding stoploss and stoploss percentage only if it is not None
|
||||
lines.append("*Stoploss:* `{stop_loss:.8f}` " +
|
||||
lines.append("*Stoploss:* `{stop_loss_abs:.8f}` " +
|
||||
("`({stop_loss_pct:.2f}%)`" if r['stop_loss_pct'] else ""))
|
||||
lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` "
|
||||
"`({stoploss_current_dist_pct:.2f}%)`")
|
||||
@ -776,7 +775,8 @@ class Telegram(RPC):
|
||||
:param update: message update
|
||||
:return: None
|
||||
"""
|
||||
val = self._rpc_show_config(self._freqtrade.config)
|
||||
val = RPC._rpc_show_config(self._freqtrade.config, self._freqtrade.state)
|
||||
|
||||
if val['trailing_stop']:
|
||||
sl_info = (
|
||||
f"*Initial Stoploss:* `{val['stoploss']}`\n"
|
||||
|
@ -63,7 +63,7 @@ class {{ strategy }}(IStrategy):
|
||||
ignore_roi_if_buy_signal = False
|
||||
|
||||
# Number of candles the strategy requires before producing valid signals
|
||||
startup_candle_count: int = 20
|
||||
startup_candle_count: int = 30
|
||||
|
||||
# Optional order type mapping.
|
||||
order_types = {
|
||||
|
@ -64,7 +64,7 @@ class SampleStrategy(IStrategy):
|
||||
ignore_roi_if_buy_signal = False
|
||||
|
||||
# Number of candles the strategy requires before producing valid signals
|
||||
startup_candle_count: int = 20
|
||||
startup_candle_count: int = 30
|
||||
|
||||
# Optional order type mapping.
|
||||
order_types = {
|
||||
@ -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']
|
||||
|
@ -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']
|
||||
|
@ -108,13 +108,13 @@ class Wallets:
|
||||
for trading operations, the latest balance is needed.
|
||||
:param require_update: Allow skipping an update if balances were recently refreshed
|
||||
"""
|
||||
if (require_update or (self._last_wallet_refresh + 3600 < arrow.utcnow().timestamp)):
|
||||
if (require_update or (self._last_wallet_refresh + 3600 < arrow.utcnow().int_timestamp)):
|
||||
if self._config['dry_run']:
|
||||
self._update_dry()
|
||||
else:
|
||||
self._update_live()
|
||||
logger.info('Wallets synced.')
|
||||
self._last_wallet_refresh = arrow.utcnow().timestamp
|
||||
self._last_wallet_refresh = arrow.utcnow().int_timestamp
|
||||
|
||||
def get_all_balances(self) -> Dict[str, Any]:
|
||||
return self._wallets
|
||||
|
@ -20,13 +20,13 @@ 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
|
||||
|
@ -3,12 +3,12 @@
|
||||
-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
|
||||
mypy==0.790
|
||||
pytest==6.1.1
|
||||
pytest==6.1.2
|
||||
pytest-asyncio==0.14.0
|
||||
pytest-cov==2.10.1
|
||||
pytest-mock==3.3.1
|
||||
|
@ -2,7 +2,7 @@
|
||||
-r requirements.txt
|
||||
|
||||
# Required for hyperopt
|
||||
scipy==1.5.3
|
||||
scipy==1.5.4
|
||||
scikit-learn==0.23.2
|
||||
scikit-optimize==0.8.1
|
||||
filelock==3.0.12
|
||||
|
@ -1,14 +1,14 @@
|
||||
numpy==1.19.2
|
||||
pandas==1.1.3
|
||||
numpy==1.19.4
|
||||
pandas==1.1.4
|
||||
|
||||
ccxt==1.36.85
|
||||
aiohttp==3.7.1
|
||||
ccxt==1.38.13
|
||||
aiohttp==3.7.3
|
||||
SQLAlchemy==1.3.20
|
||||
python-telegram-bot==13.0
|
||||
arrow==0.17.0
|
||||
cachetools==4.1.1
|
||||
requests==2.24.0
|
||||
urllib3==1.25.11
|
||||
requests==2.25.0
|
||||
urllib3==1.26.2
|
||||
wrapt==1.12.1
|
||||
jsonschema==3.2.0
|
||||
TA-Lib==0.4.19
|
||||
@ -22,18 +22,18 @@ 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
|
||||
|
||||
# Api server
|
||||
flask==1.1.2
|
||||
flask-jwt-extended==3.24.1
|
||||
flask-jwt-extended==3.25.0
|
||||
flask-cors==3.0.9
|
||||
|
||||
# Support for colorized terminal output
|
||||
colorama==0.4.4
|
||||
# Building config files interactively
|
||||
questionary==1.7.0
|
||||
questionary==1.8.1
|
||||
prompt-toolkit==3.0.8
|
||||
|
2
setup.py
2
setup.py
@ -69,7 +69,7 @@ setup(name='freqtrade',
|
||||
'ccxt>=1.24.96',
|
||||
'SQLAlchemy',
|
||||
'python-telegram-bot',
|
||||
'arrow',
|
||||
'arrow>=0.17.0',
|
||||
'cachetools',
|
||||
'requests',
|
||||
'urllib3',
|
||||
|
@ -601,7 +601,7 @@ def test_download_data_timerange(mocker, caplog, markets):
|
||||
start_download_data(get_args(args))
|
||||
assert dl_mock.call_count == 1
|
||||
# 20days ago
|
||||
days_ago = arrow.get(arrow.utcnow().shift(days=-20).date()).timestamp
|
||||
days_ago = arrow.get(arrow.utcnow().shift(days=-20).date()).int_timestamp
|
||||
assert dl_mock.call_args_list[0][1]['timerange'].startts == days_ago
|
||||
|
||||
dl_mock.reset_mock()
|
||||
@ -614,7 +614,8 @@ def test_download_data_timerange(mocker, caplog, markets):
|
||||
start_download_data(get_args(args))
|
||||
assert dl_mock.call_count == 1
|
||||
|
||||
assert dl_mock.call_args_list[0][1]['timerange'].startts == arrow.Arrow(2020, 1, 1).timestamp
|
||||
assert dl_mock.call_args_list[0][1]['timerange'].startts == arrow.Arrow(
|
||||
2020, 1, 1).int_timestamp
|
||||
|
||||
|
||||
def test_download_data_no_markets(mocker, caplog):
|
||||
|
@ -792,7 +792,7 @@ def limit_buy_order_open():
|
||||
'side': 'buy',
|
||||
'symbol': 'mocked',
|
||||
'datetime': arrow.utcnow().isoformat(),
|
||||
'timestamp': arrow.utcnow().timestamp,
|
||||
'timestamp': arrow.utcnow().int_timestamp,
|
||||
'price': 0.00001099,
|
||||
'amount': 90.99181073,
|
||||
'filled': 0.0,
|
||||
@ -911,7 +911,7 @@ def limit_buy_order_canceled_empty(request):
|
||||
'info': {},
|
||||
'id': '1234512345',
|
||||
'clientOrderId': None,
|
||||
'timestamp': arrow.utcnow().shift(minutes=-601).timestamp,
|
||||
'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp,
|
||||
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
||||
'lastTradeTimestamp': None,
|
||||
'symbol': 'LTC/USDT',
|
||||
@ -932,7 +932,7 @@ def limit_buy_order_canceled_empty(request):
|
||||
'info': {},
|
||||
'id': 'AZNPFF-4AC4N-7MKTAT',
|
||||
'clientOrderId': None,
|
||||
'timestamp': arrow.utcnow().shift(minutes=-601).timestamp,
|
||||
'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp,
|
||||
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
||||
'lastTradeTimestamp': None,
|
||||
'status': 'canceled',
|
||||
@ -953,7 +953,7 @@ def limit_buy_order_canceled_empty(request):
|
||||
'info': {},
|
||||
'id': '1234512345',
|
||||
'clientOrderId': 'alb1234123',
|
||||
'timestamp': arrow.utcnow().shift(minutes=-601).timestamp,
|
||||
'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp,
|
||||
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
||||
'lastTradeTimestamp': None,
|
||||
'symbol': 'LTC/USDT',
|
||||
@ -974,7 +974,7 @@ def limit_buy_order_canceled_empty(request):
|
||||
'info': {},
|
||||
'id': '1234512345',
|
||||
'clientOrderId': 'alb1234123',
|
||||
'timestamp': arrow.utcnow().shift(minutes=-601).timestamp,
|
||||
'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp,
|
||||
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
||||
'lastTradeTimestamp': None,
|
||||
'symbol': 'LTC/USDT',
|
||||
@ -1000,7 +1000,7 @@ def limit_sell_order_open():
|
||||
'side': 'sell',
|
||||
'pair': 'mocked',
|
||||
'datetime': arrow.utcnow().isoformat(),
|
||||
'timestamp': arrow.utcnow().timestamp,
|
||||
'timestamp': arrow.utcnow().int_timestamp,
|
||||
'price': 0.00001173,
|
||||
'amount': 90.99181073,
|
||||
'filled': 0.0,
|
||||
|
@ -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',
|
||||
|
@ -52,6 +52,31 @@ def test_historic_ohlcv(mocker, default_conf, ohlcv_history):
|
||||
assert historymock.call_args_list[0][1]["timeframe"] == "5m"
|
||||
|
||||
|
||||
def test_historic_ohlcv_dataformat(mocker, default_conf, ohlcv_history):
|
||||
hdf5loadmock = MagicMock(return_value=ohlcv_history)
|
||||
jsonloadmock = MagicMock(return_value=ohlcv_history)
|
||||
mocker.patch("freqtrade.data.history.hdf5datahandler.HDF5DataHandler._ohlcv_load", hdf5loadmock)
|
||||
mocker.patch("freqtrade.data.history.jsondatahandler.JsonDataHandler._ohlcv_load", jsonloadmock)
|
||||
|
||||
default_conf["runmode"] = RunMode.BACKTEST
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
dp = DataProvider(default_conf, exchange)
|
||||
data = dp.historic_ohlcv("UNITTEST/BTC", "5m")
|
||||
assert isinstance(data, DataFrame)
|
||||
hdf5loadmock.assert_not_called()
|
||||
jsonloadmock.assert_called_once()
|
||||
|
||||
# Swiching to dataformat hdf5
|
||||
hdf5loadmock.reset_mock()
|
||||
jsonloadmock.reset_mock()
|
||||
default_conf["dataformat_ohlcv"] = "hdf5"
|
||||
dp = DataProvider(default_conf, exchange)
|
||||
data = dp.historic_ohlcv("UNITTEST/BTC", "5m")
|
||||
assert isinstance(data, DataFrame)
|
||||
hdf5loadmock.assert_called_once()
|
||||
jsonloadmock.assert_not_called()
|
||||
|
||||
|
||||
def test_get_pair_dataframe(mocker, default_conf, ohlcv_history):
|
||||
default_conf["runmode"] = RunMode.DRY_RUN
|
||||
timeframe = default_conf["timeframe"]
|
||||
|
@ -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:
|
||||
@ -323,7 +320,7 @@ def test_load_partial_missing(testdatadir, caplog) -> None:
|
||||
start = arrow.get('2018-01-01T00:00:00')
|
||||
end = arrow.get('2018-01-11T00:00:00')
|
||||
data = load_data(testdatadir, '5m', ['UNITTEST/BTC'], startup_candles=20,
|
||||
timerange=TimeRange('date', 'date', start.timestamp, end.timestamp))
|
||||
timerange=TimeRange('date', 'date', start.int_timestamp, end.int_timestamp))
|
||||
assert log_has(
|
||||
'Using indicator startup period: 20 ...', caplog
|
||||
)
|
||||
@ -339,7 +336,7 @@ def test_load_partial_missing(testdatadir, caplog) -> None:
|
||||
start = arrow.get('2018-01-10T00:00:00')
|
||||
end = arrow.get('2018-02-20T00:00:00')
|
||||
data = load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'],
|
||||
timerange=TimeRange('date', 'date', start.timestamp, end.timestamp))
|
||||
timerange=TimeRange('date', 'date', start.int_timestamp, end.int_timestamp))
|
||||
# timedifference in 5 minutes
|
||||
td = ((end - start).total_seconds() // 60 // 5) + 1
|
||||
assert td != len(data['UNITTEST/BTC'])
|
||||
@ -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')
|
||||
@ -724,6 +727,8 @@ def test_hdf5datahandler_trades_load(testdatadir):
|
||||
|
||||
trades2 = dh._trades_load('XRP/ETH', timerange)
|
||||
assert len(trades) > len(trades2)
|
||||
# Check that ID is None (If it's nan, it's wrong)
|
||||
assert trades2[0][2] is None
|
||||
|
||||
# unfiltered load has trades before starttime
|
||||
assert len([t for t in trades if t[0] < timerange.startts * 1000]) >= 0
|
||||
|
@ -50,7 +50,7 @@ def _build_dataframe(buy_ohlc_sell_matrice):
|
||||
'date': tests_start_time.shift(
|
||||
minutes=(
|
||||
ohlc[0] *
|
||||
timeframe_in_minute)).timestamp *
|
||||
timeframe_in_minute)).int_timestamp *
|
||||
1000,
|
||||
'buy': ohlc[1],
|
||||
'open': ohlc[2],
|
||||
@ -71,7 +71,7 @@ def _build_dataframe(buy_ohlc_sell_matrice):
|
||||
|
||||
def _time_on_candle(number):
|
||||
return np.datetime64(tests_start_time.shift(
|
||||
minutes=(number * timeframe_in_minute)).timestamp * 1000, 'ms')
|
||||
minutes=(number * timeframe_in_minute)).int_timestamp * 1000, 'ms')
|
||||
|
||||
|
||||
# End helper functions
|
||||
@ -251,7 +251,7 @@ def test_edge_heartbeat_calculate(mocker, edge_conf):
|
||||
heartbeat = edge_conf['edge']['process_throttle_secs']
|
||||
|
||||
# should not recalculate if heartbeat not reached
|
||||
edge._last_updated = arrow.utcnow().timestamp - heartbeat + 1
|
||||
edge._last_updated = arrow.utcnow().int_timestamp - heartbeat + 1
|
||||
|
||||
assert edge.calculate() is False
|
||||
|
||||
@ -263,7 +263,7 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m',
|
||||
|
||||
NEOBTC = [
|
||||
[
|
||||
tests_start_time.shift(minutes=(x * timeframe_in_minute)).timestamp * 1000,
|
||||
tests_start_time.shift(minutes=(x * timeframe_in_minute)).int_timestamp * 1000,
|
||||
math.sin(x * hz) / 1000 + base,
|
||||
math.sin(x * hz) / 1000 + base + 0.0001,
|
||||
math.sin(x * hz) / 1000 + base - 0.0001,
|
||||
@ -275,7 +275,7 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m',
|
||||
base = 0.002
|
||||
LTCBTC = [
|
||||
[
|
||||
tests_start_time.shift(minutes=(x * timeframe_in_minute)).timestamp * 1000,
|
||||
tests_start_time.shift(minutes=(x * timeframe_in_minute)).int_timestamp * 1000,
|
||||
math.sin(x * hz) / 1000 + base,
|
||||
math.sin(x * hz) / 1000 + base + 0.0001,
|
||||
math.sin(x * hz) / 1000 + base - 0.0001,
|
||||
@ -299,7 +299,7 @@ def test_edge_process_downloaded_data(mocker, edge_conf):
|
||||
|
||||
assert edge.calculate()
|
||||
assert len(edge._cached_pairs) == 2
|
||||
assert edge._last_updated <= arrow.utcnow().timestamp + 2
|
||||
assert edge._last_updated <= arrow.utcnow().int_timestamp + 2
|
||||
|
||||
|
||||
def test_edge_process_no_data(mocker, edge_conf, caplog):
|
||||
|
@ -393,7 +393,7 @@ def test_reload_markets(default_conf, mocker, caplog):
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance",
|
||||
mock_markets=False)
|
||||
exchange._load_async_markets = MagicMock()
|
||||
exchange._last_markets_refresh = arrow.utcnow().timestamp
|
||||
exchange._last_markets_refresh = arrow.utcnow().int_timestamp
|
||||
updated_markets = {'ETH/BTC': {}, "LTC/BTC": {}}
|
||||
|
||||
assert exchange.markets == initial_markets
|
||||
@ -404,7 +404,7 @@ def test_reload_markets(default_conf, mocker, caplog):
|
||||
assert exchange._load_async_markets.call_count == 0
|
||||
|
||||
# more than 10 minutes have passed, reload is executed
|
||||
exchange._last_markets_refresh = arrow.utcnow().timestamp - 15 * 60
|
||||
exchange._last_markets_refresh = arrow.utcnow().int_timestamp - 15 * 60
|
||||
exchange.reload_markets()
|
||||
assert exchange.markets == updated_markets
|
||||
assert exchange._load_async_markets.call_count == 1
|
||||
@ -1272,7 +1272,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name):
|
||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||
ohlcv = [
|
||||
[
|
||||
arrow.utcnow().timestamp * 1000, # unix timestamp ms
|
||||
arrow.utcnow().int_timestamp * 1000, # unix timestamp ms
|
||||
1, # open
|
||||
2, # high
|
||||
3, # low
|
||||
@ -1289,7 +1289,8 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name):
|
||||
# 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(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000))
|
||||
ret = exchange.get_historic_ohlcv(pair, "5m", int((
|
||||
arrow.utcnow().int_timestamp - since) * 1000))
|
||||
|
||||
assert exchange._async_get_candle_history.call_count == 2
|
||||
# Returns twice the above OHLCV data
|
||||
@ -1301,14 +1302,17 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name):
|
||||
raise TimeoutError()
|
||||
|
||||
exchange._async_get_candle_history = MagicMock(side_effect=mock_get_candle_hist_error)
|
||||
ret = exchange.get_historic_ohlcv(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000))
|
||||
ret = exchange.get_historic_ohlcv(pair, "5m", int(
|
||||
(arrow.utcnow().int_timestamp - since) * 1000))
|
||||
assert log_has_re(r"Async code raised an exception: .*", caplog)
|
||||
|
||||
|
||||
def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
|
||||
@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().timestamp - 1) * 1000, # unix timestamp ms
|
||||
arrow.utcnow().int_timestamp * 1000, # unix timestamp ms
|
||||
1, # open
|
||||
2, # high
|
||||
3, # low
|
||||
@ -1316,7 +1320,56 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
|
||||
5, # volume (in quote currency)
|
||||
],
|
||||
[
|
||||
arrow.utcnow().timestamp * 1000, # unix timestamp ms
|
||||
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 = [
|
||||
[
|
||||
(arrow.utcnow().int_timestamp - 1) * 1000, # unix timestamp ms
|
||||
1, # open
|
||||
2, # high
|
||||
3, # low
|
||||
4, # close
|
||||
5, # volume (in quote currency)
|
||||
],
|
||||
[
|
||||
arrow.utcnow().int_timestamp * 1000, # unix timestamp ms
|
||||
3, # open
|
||||
1, # high
|
||||
4, # low
|
||||
@ -1362,7 +1415,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
|
||||
async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_name):
|
||||
ohlcv = [
|
||||
[
|
||||
arrow.utcnow().timestamp * 1000, # unix timestamp ms
|
||||
arrow.utcnow().int_timestamp * 1000, # unix timestamp ms
|
||||
1, # open
|
||||
2, # high
|
||||
3, # low
|
||||
@ -1397,14 +1450,14 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_
|
||||
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError("Unknown error"))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
await exchange._async_get_candle_history(pair, "5m",
|
||||
(arrow.utcnow().timestamp - 2000) * 1000)
|
||||
(arrow.utcnow().int_timestamp - 2000) * 1000)
|
||||
|
||||
with pytest.raises(OperationalException, match=r'Exchange.* does not support fetching '
|
||||
r'historical candle \(OHLCV\) data\..*'):
|
||||
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported("Not supported"))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
await exchange._async_get_candle_history(pair, "5m",
|
||||
(arrow.utcnow().timestamp - 2000) * 1000)
|
||||
(arrow.utcnow().int_timestamp - 2000) * 1000)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@ -1650,13 +1703,13 @@ async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name,
|
||||
with pytest.raises(OperationalException, match=r'Could not fetch trade data*'):
|
||||
api_mock.fetch_trades = MagicMock(side_effect=ccxt.BaseError("Unknown error"))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
await exchange._async_fetch_trades(pair, since=(arrow.utcnow().timestamp - 2000) * 1000)
|
||||
await exchange._async_fetch_trades(pair, since=(arrow.utcnow().int_timestamp - 2000) * 1000)
|
||||
|
||||
with pytest.raises(OperationalException, match=r'Exchange.* does not support fetching '
|
||||
r'historical trade data\..*'):
|
||||
api_mock.fetch_trades = MagicMock(side_effect=ccxt.NotSupported("Not supported"))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
await exchange._async_fetch_trades(pair, since=(arrow.utcnow().timestamp - 2000) * 1000)
|
||||
await exchange._async_fetch_trades(pair, since=(arrow.utcnow().int_timestamp - 2000) * 1000)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
@ -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):
|
||||
|
@ -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',
|
||||
@ -340,6 +340,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,
|
||||
@ -528,7 +532,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 +547,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 +563,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 +575,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 +586,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 +677,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):
|
||||
|
@ -69,8 +69,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
'min_rate': ANY,
|
||||
'max_rate': ANY,
|
||||
'strategy': ANY,
|
||||
'ticker_interval': ANY,
|
||||
'timeframe': ANY,
|
||||
'timeframe': 5,
|
||||
'open_order_id': ANY,
|
||||
'close_date': None,
|
||||
'close_date_hum': None,
|
||||
@ -87,14 +86,15 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
'current_profit': -0.00408133,
|
||||
'current_profit_pct': -0.41,
|
||||
'current_profit_abs': -4.09e-06,
|
||||
'stop_loss': 9.882e-06,
|
||||
'profit_ratio': -0.00408133,
|
||||
'profit_pct': -0.41,
|
||||
'profit_abs': -4.09e-06,
|
||||
'stop_loss_abs': 9.882e-06,
|
||||
'stop_loss_pct': -10.0,
|
||||
'stop_loss_ratio': -0.1,
|
||||
'stoploss_order_id': None,
|
||||
'stoploss_last_update': ANY,
|
||||
'stoploss_last_update_timestamp': ANY,
|
||||
'initial_stop_loss': 9.882e-06,
|
||||
'initial_stop_loss_abs': 9.882e-06,
|
||||
'initial_stop_loss_pct': -10.0,
|
||||
'initial_stop_loss_ratio': -0.1,
|
||||
@ -134,7 +134,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
'min_rate': ANY,
|
||||
'max_rate': ANY,
|
||||
'strategy': ANY,
|
||||
'ticker_interval': ANY,
|
||||
'timeframe': ANY,
|
||||
'open_order_id': ANY,
|
||||
'close_date': None,
|
||||
@ -152,14 +151,15 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
'current_profit': ANY,
|
||||
'current_profit_pct': ANY,
|
||||
'current_profit_abs': ANY,
|
||||
'stop_loss': 9.882e-06,
|
||||
'profit_ratio': ANY,
|
||||
'profit_pct': ANY,
|
||||
'profit_abs': ANY,
|
||||
'stop_loss_abs': 9.882e-06,
|
||||
'stop_loss_pct': -10.0,
|
||||
'stop_loss_ratio': -0.1,
|
||||
'stoploss_order_id': None,
|
||||
'stoploss_last_update': ANY,
|
||||
'stoploss_last_update_timestamp': ANY,
|
||||
'initial_stop_loss': 9.882e-06,
|
||||
'initial_stop_loss_abs': 9.882e-06,
|
||||
'initial_stop_loss_pct': -10.0,
|
||||
'initial_stop_loss_ratio': -0.1,
|
||||
@ -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'
|
||||
|
||||
|
@ -14,7 +14,7 @@ from freqtrade.__init__ import __version__
|
||||
from freqtrade.loggers import setup_logging, setup_logging_pre
|
||||
from freqtrade.persistence import PairLocks, Trade
|
||||
from freqtrade.rpc.api_server import BASE_URI, ApiServer
|
||||
from freqtrade.state import State
|
||||
from freqtrade.state import RunMode, State
|
||||
from tests.conftest import create_mock_trades, get_patched_freqtradebot, log_has, patch_get_signal
|
||||
|
||||
|
||||
@ -26,7 +26,7 @@ _TEST_PASS = "SuperSecurePassword1!"
|
||||
def botclient(default_conf, mocker):
|
||||
setup_logging_pre()
|
||||
setup_logging(default_conf)
|
||||
|
||||
default_conf['runmode'] = RunMode.DRY_RUN
|
||||
default_conf.update({"api_server": {"enabled": True,
|
||||
"listen_ip_address": "127.0.0.1",
|
||||
"listen_port": 8080,
|
||||
@ -360,7 +360,6 @@ def test_api_show_config(botclient, mocker):
|
||||
assert_response(rc)
|
||||
assert 'dry_run' in rc.json
|
||||
assert rc.json['exchange'] == 'bittrex'
|
||||
assert rc.json['ticker_interval'] == '5m'
|
||||
assert rc.json['timeframe'] == '5m'
|
||||
assert rc.json['timeframe_ms'] == 300000
|
||||
assert rc.json['timeframe_min'] == 5
|
||||
@ -639,6 +638,9 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
|
||||
'current_profit': -0.00408133,
|
||||
'current_profit_pct': -0.41,
|
||||
'current_profit_abs': -4.09e-06,
|
||||
'profit_ratio': -0.00408133,
|
||||
'profit_pct': -0.41,
|
||||
'profit_abs': -4.09e-06,
|
||||
'current_rate': 1.099e-05,
|
||||
'open_date': ANY,
|
||||
'open_date_hum': 'just now',
|
||||
@ -647,14 +649,12 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
|
||||
'open_rate': 1.098e-05,
|
||||
'pair': 'ETH/BTC',
|
||||
'stake_amount': 0.001,
|
||||
'stop_loss': 9.882e-06,
|
||||
'stop_loss_abs': 9.882e-06,
|
||||
'stop_loss_pct': -10.0,
|
||||
'stop_loss_ratio': -0.1,
|
||||
'stoploss_order_id': None,
|
||||
'stoploss_last_update': ANY,
|
||||
'stoploss_last_update_timestamp': ANY,
|
||||
'initial_stop_loss': 9.882e-06,
|
||||
'initial_stop_loss_abs': 9.882e-06,
|
||||
'initial_stop_loss_pct': -10.0,
|
||||
'initial_stop_loss_ratio': -0.1,
|
||||
@ -682,7 +682,6 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
|
||||
'sell_reason': None,
|
||||
'sell_order_status': None,
|
||||
'strategy': 'DefaultStrategy',
|
||||
'ticker_interval': 5,
|
||||
'timeframe': 5,
|
||||
'exchange': 'bittrex',
|
||||
}]
|
||||
@ -779,20 +778,22 @@ def test_api_forcebuy(botclient, mocker, fee):
|
||||
'open_rate': 0.245441,
|
||||
'pair': 'ETH/ETH',
|
||||
'stake_amount': 1,
|
||||
'stop_loss': None,
|
||||
'stop_loss_abs': None,
|
||||
'stop_loss_pct': None,
|
||||
'stop_loss_ratio': None,
|
||||
'stoploss_order_id': None,
|
||||
'stoploss_last_update': None,
|
||||
'stoploss_last_update_timestamp': None,
|
||||
'initial_stop_loss': None,
|
||||
'initial_stop_loss_abs': None,
|
||||
'initial_stop_loss_pct': None,
|
||||
'initial_stop_loss_ratio': None,
|
||||
'close_profit': None,
|
||||
'close_profit_pct': None,
|
||||
'close_profit_abs': None,
|
||||
'close_rate_requested': None,
|
||||
'profit_ratio': None,
|
||||
'profit_pct': None,
|
||||
'profit_abs': None,
|
||||
'fee_close': 0.0025,
|
||||
'fee_close_cost': None,
|
||||
'fee_close_currency': None,
|
||||
@ -808,7 +809,6 @@ def test_api_forcebuy(botclient, mocker, fee):
|
||||
'sell_reason': None,
|
||||
'sell_order_status': None,
|
||||
'strategy': None,
|
||||
'ticker_interval': None,
|
||||
'timeframe': None,
|
||||
'exchange': 'bittrex',
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ from freqtrade.loggers import setup_logging
|
||||
from freqtrade.persistence import PairLocks, Trade
|
||||
from freqtrade.rpc import RPCMessageType
|
||||
from freqtrade.rpc.telegram import Telegram, authorized_only
|
||||
from freqtrade.state import State
|
||||
from freqtrade.state import RunMode, State
|
||||
from freqtrade.strategy.interface import SellType
|
||||
from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, patch_exchange,
|
||||
patch_get_signal, patch_whitelist)
|
||||
@ -163,16 +163,17 @@ def test_telegram_status(default_conf, update, mocker) -> None:
|
||||
'amount': 90.99181074,
|
||||
'stake_amount': 90.99181074,
|
||||
'close_profit_pct': None,
|
||||
'current_profit': -0.0059,
|
||||
'current_profit_pct': -0.59,
|
||||
'initial_stop_loss': 1.098e-05,
|
||||
'stop_loss': 1.099e-05,
|
||||
'profit': -0.0059,
|
||||
'profit_pct': -0.59,
|
||||
'initial_stop_loss_abs': 1.098e-05,
|
||||
'stop_loss_abs': 1.099e-05,
|
||||
'sell_order_status': None,
|
||||
'initial_stop_loss_pct': -0.05,
|
||||
'stoploss_current_dist': 1e-08,
|
||||
'stoploss_current_dist_pct': -0.02,
|
||||
'stop_loss_pct': -0.01,
|
||||
'open_order': '(limit buy rem=0.00000000)'
|
||||
'open_order': '(limit buy rem=0.00000000)',
|
||||
'is_open': True
|
||||
}]),
|
||||
_status_table=status_table,
|
||||
_send_msg=msg_mock
|
||||
@ -1040,13 +1041,6 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
freqtradebot.state = State.STOPPED
|
||||
telegram._locks(update=update, context=MagicMock())
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'not running' in msg_mock.call_args_list[0][0][0]
|
||||
msg_mock.reset_mock()
|
||||
freqtradebot.state = State.RUNNING
|
||||
|
||||
PairLocks.lock_pair('ETH/BTC', arrow.utcnow().shift(minutes=4).datetime, 'randreason')
|
||||
PairLocks.lock_pair('XRP/BTC', arrow.utcnow().shift(minutes=20).datetime, 'deadbeef')
|
||||
|
||||
@ -1308,6 +1302,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None:
|
||||
_init=MagicMock(),
|
||||
_send_msg=msg_mock
|
||||
)
|
||||
default_conf['runmode'] = RunMode.DRY_RUN
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
|
@ -663,7 +663,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 +678,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 +840,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
|
||||
|
@ -816,24 +816,25 @@ def test_to_json(default_conf, fee):
|
||||
'amount_requested': 123.0,
|
||||
'stake_amount': 0.001,
|
||||
'close_profit': None,
|
||||
'close_profit_pct': None,
|
||||
'close_profit_abs': None,
|
||||
'profit_ratio': None,
|
||||
'profit_pct': None,
|
||||
'profit_abs': None,
|
||||
'sell_reason': None,
|
||||
'sell_order_status': None,
|
||||
'stop_loss': None,
|
||||
'stop_loss_abs': None,
|
||||
'stop_loss_ratio': None,
|
||||
'stop_loss_pct': None,
|
||||
'stoploss_order_id': None,
|
||||
'stoploss_last_update': None,
|
||||
'stoploss_last_update_timestamp': None,
|
||||
'initial_stop_loss': None,
|
||||
'initial_stop_loss_abs': None,
|
||||
'initial_stop_loss_pct': None,
|
||||
'initial_stop_loss_ratio': None,
|
||||
'min_rate': None,
|
||||
'max_rate': None,
|
||||
'strategy': None,
|
||||
'ticker_interval': None,
|
||||
'timeframe': None,
|
||||
'exchange': 'bittrex',
|
||||
}
|
||||
@ -868,19 +869,21 @@ def test_to_json(default_conf, fee):
|
||||
'amount': 100.0,
|
||||
'amount_requested': 101.0,
|
||||
'stake_amount': 0.001,
|
||||
'stop_loss': None,
|
||||
'stop_loss_abs': None,
|
||||
'stop_loss_pct': None,
|
||||
'stop_loss_ratio': None,
|
||||
'stoploss_order_id': None,
|
||||
'stoploss_last_update': None,
|
||||
'stoploss_last_update_timestamp': None,
|
||||
'initial_stop_loss': None,
|
||||
'initial_stop_loss_abs': None,
|
||||
'initial_stop_loss_pct': None,
|
||||
'initial_stop_loss_ratio': None,
|
||||
'close_profit': None,
|
||||
'close_profit_pct': None,
|
||||
'close_profit_abs': None,
|
||||
'profit_ratio': None,
|
||||
'profit_pct': None,
|
||||
'profit_abs': None,
|
||||
'close_rate_requested': None,
|
||||
'fee_close': 0.0025,
|
||||
'fee_close_cost': None,
|
||||
@ -897,7 +900,6 @@ def test_to_json(default_conf, fee):
|
||||
'sell_reason': None,
|
||||
'sell_order_status': None,
|
||||
'strategy': None,
|
||||
'ticker_interval': None,
|
||||
'timeframe': None,
|
||||
'exchange': 'bittrex',
|
||||
}
|
||||
|
@ -51,9 +51,10 @@ def test_init_plotscript(default_conf, mocker, testdatadir):
|
||||
assert "ohlcv" in ret
|
||||
assert "trades" in ret
|
||||
assert "pairs" in ret
|
||||
assert 'timerange' in ret
|
||||
|
||||
default_conf['pairs'] = ["TRX/BTC", "ADA/BTC"]
|
||||
ret = init_plotscript(default_conf)
|
||||
ret = init_plotscript(default_conf, 20)
|
||||
assert "ohlcv" in ret
|
||||
assert "TRX/BTC" in ret["ohlcv"]
|
||||
assert "ADA/BTC" in ret["ohlcv"]
|
||||
|
Loading…
Reference in New Issue
Block a user