Merge branch 'margin-db' into margin-commands

This commit is contained in:
Sam Germain 2021-07-10 21:08:19 -06:00
commit 681c15f021
101 changed files with 3907 additions and 993 deletions

View File

@ -1,4 +1,4 @@
FROM python:3.9.5-slim-buster as base
FROM python:3.9.6-slim-buster as base
# Setup env
ENV LANG C.UTF-8

View File

@ -37,6 +37,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even
Exchanges confirmed working by the community:
- [X] [Bitvavo](https://bitvavo.com/)
- [X] [Kukoin](https://www.kucoin.com/)
## Documentation

61
TODO
View File

@ -1,61 +0,0 @@
List of things TODO to add margin trading
Files to edit
freqtrade/freqtradebot.py
freqtrade/wallets.py
freqtrade/data/btanalysis.py
configuration
freqtrade/commands/deploy_commands.py
freqtrade/commands/arguments.py
freqtrade/strategy
freqtrade/constants.py
Tests
tests/test_persistence.pys
init with
lev & bor
lev
bor
neither lev nor bor
adjust_stop_loss
short
leverage
is_opening_trade
short
long
shortBuy
longSell
is_closing_trade
short
long
shortBuy
longSell
update, close, update fee
possible to test?
calc_profit
* * create a few shorts, a few leveraged longs test correct ratio
calc_profit_ratio
* create a few shorts, a few leveraged longs test correct ratio
get_open_trades
* create a short, check if exists
tests/test_freqtradebot.py
later
freqtrade/commands/build_config_commands.py
freqtrade/commands/cli_options.py
freqtrade/commands/list_commands.py
freqtrade/commands/hyperopt_commands.py
config_binance.json.example
config_kraken.json.example
freqtrade/enums/selltype.py
Did not look at these files
freqtrade/plot/plotting.py
freqtrade/plugins
freqtrade/resolvers/strategy_resolver.py
freqtrade/rpc
Already Edited
freqtrade/persistence/migrations.py
freqtrade/persistence/models.py

View File

@ -13,7 +13,7 @@
},
"bid_strategy": {
"ask_last_balance": 0.0,
"use_order_book": false,
"use_order_book": true,
"order_book_top": 1,
"check_depth_of_market": {
"enabled": false,
@ -21,12 +21,8 @@
}
},
"ask_strategy": {
"use_order_book": false,
"order_book_min": 1,
"order_book_max": 1,
"use_sell_signal": true,
"sell_profit_only": false,
"ignore_roi_if_buy_signal": false
"use_order_book": true,
"order_book_top": 1
},
"exchange": {
"name": "binance",

View File

@ -12,7 +12,7 @@
"sell": 30
},
"bid_strategy": {
"use_order_book": false,
"use_order_book": true,
"ask_last_balance": 0.0,
"order_book_top": 1,
"check_depth_of_market": {
@ -21,12 +21,8 @@
}
},
"ask_strategy":{
"use_order_book": false,
"order_book_min": 1,
"order_book_max": 1,
"use_sell_signal": true,
"sell_profit_only": false,
"ignore_roi_if_buy_signal": false
"use_order_book": true,
"order_book_top": 1
},
"exchange": {
"name": "bittrex",

View File

@ -13,7 +13,7 @@
},
"bid_strategy": {
"ask_last_balance": 0.0,
"use_order_book": false,
"use_order_book": true,
"order_book_top": 1,
"check_depth_of_market": {
"enabled": false,
@ -21,12 +21,8 @@
}
},
"ask_strategy": {
"use_order_book": false,
"order_book_min": 1,
"order_book_max": 1,
"use_sell_signal": true,
"sell_profit_only": false,
"ignore_roi_if_buy_signal": false
"use_order_book": true,
"order_book_top": 1
},
"exchange": {
"name": "ftx",

View File

@ -14,6 +14,10 @@
"trailing_stop_positive": 0.005,
"trailing_stop_positive_offset": 0.0051,
"trailing_only_offset_is_reached": false,
"use_sell_signal": true,
"sell_profit_only": false,
"sell_profit_offset": 0.0,
"ignore_roi_if_buy_signal": false,
"minimal_roi": {
"40": 0.0,
"30": 0.01,
@ -28,7 +32,7 @@
},
"bid_strategy": {
"price_side": "bid",
"use_order_book": false,
"use_order_book": true,
"ask_last_balance": 0.0,
"order_book_top": 1,
"check_depth_of_market": {
@ -38,13 +42,8 @@
},
"ask_strategy":{
"price_side": "ask",
"use_order_book": false,
"order_book_min": 1,
"order_book_max": 1,
"use_sell_signal": true,
"sell_profit_only": false,
"sell_profit_offset": 0.0,
"ignore_roi_if_buy_signal": false
"use_order_book": true,
"order_book_top": 1
},
"order_types": {
"buy": "limit",

View File

@ -12,7 +12,7 @@
"sell": 30
},
"bid_strategy": {
"use_order_book": false,
"use_order_book": true,
"ask_last_balance": 0.0,
"order_book_top": 1,
"check_depth_of_market": {
@ -21,12 +21,8 @@
}
},
"ask_strategy":{
"use_order_book": false,
"order_book_min": 1,
"order_book_max": 1,
"use_sell_signal": true,
"sell_profit_only": false,
"ignore_roi_if_buy_signal": false
"use_order_book": true,
"order_book_top": 1
},
"exchange": {
"name": "kraken",

View File

@ -32,6 +32,7 @@ class SuperDuperHyperOptLoss(IHyperOptLoss):
def hyperopt_loss_function(results: DataFrame, trade_count: int,
min_date: datetime, max_date: datetime,
config: Dict, processed: Dict[str, DataFrame],
backtest_stats: Dict[str, Any],
*args, **kwargs) -> float:
"""
Objective function, returns smaller number for better results
@ -53,7 +54,7 @@ class SuperDuperHyperOptLoss(IHyperOptLoss):
Currently, the arguments are:
* `results`: DataFrame containing the result
* `results`: DataFrame containing the resulting trades.
The following columns are available in results (corresponds to the output-file of backtesting when used with `--export trades`):
`pair, profit_ratio, profit_abs, open_date, open_rate, fee_open, close_date, close_rate, fee_close, amount, trade_duration, is_open, sell_reason, stake_amount, min_rate, max_rate, stop_loss_ratio, stop_loss_abs`
* `trade_count`: Amount of trades (identical to `len(results)`)
@ -61,6 +62,7 @@ Currently, the arguments are:
* `min_date`: End date of the timerange used
* `config`: Config object used (Note: Not all strategy-related parameters will be updated here if they are part of a hyperopt space).
* `processed`: Dict of Dataframes with the pair as keys containing the data used for backtesting.
* `backtest_stats`: Backtesting statistics using the same format as the backtesting file "strategy" substructure. Available fields can be seen in `generate_strategy_stats()` in `optimize_reports.py`.
This function needs to return a floating point number (`float`). Smaller numbers will be interpreted as better results. The parameters and balancing for this is up to you.

View File

@ -302,7 +302,6 @@ A backtesting result will look like that:
| Days win/draw/lose | 12 / 82 / 25 |
| Avg. Duration Winners | 4:23:00 |
| Avg. Duration Loser | 6:55:00 |
| Zero Duration Trades | 4.6% (20) |
| Rejected Buy signals | 3089 |
| | |
| Min balance | 0.00945123 BTC |
@ -390,7 +389,6 @@ It contains some useful key metrics about performance of your strategy on backte
| Days win/draw/lose | 12 / 82 / 25 |
| Avg. Duration Winners | 4:23:00 |
| Avg. Duration Loser | 6:55:00 |
| Zero Duration Trades | 4.6% (20) |
| Rejected Buy signals | 3089 |
| | |
| Min balance | 0.00945123 BTC |
@ -420,7 +418,6 @@ It contains some useful key metrics about performance of your strategy on backte
- `Best day` / `Worst day`: Best and worst day based on daily profit.
- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade).
- `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades.
- `Zero Duration Trades`: A number of trades that completed within same candle as they opened and had `trailing_stop_loss` sell reason. A significant amount of such trades may indicate that strategy is exploiting trailing stoploss behavior in backtesting and produces unrealistic results.
- `Rejected Buy signals`: Buy signals that could not be acted upon due to max_open_trades being reached.
- `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period.
- `Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced).

View File

@ -74,19 +74,18 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).<br> *Defaults to `bid`.* <br> **Datatype:** String (either `ask` or `bid`).
| `bid_strategy.ask_last_balance` | **Required.** Interpolate the bidding price. More information [below](#buy-price-without-orderbook-enabled).
| `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled). <br> **Datatype:** Boolean
| `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book Bids to buy. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in [Order Book Bids](#buy-price-with-orderbook-enabled). <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
| `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book "price_side" to buy. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in [Order Book Bids](#buy-price-with-orderbook-enabled). <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
| `bid_strategy. check_depth_of_market.enabled` | Do not buy if the difference of buy orders and sell orders is met in Order Book. [Check market depth](#check-depth-of-market). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `bid_strategy. check_depth_of_market.bids_to_ask_delta` | The difference ratio of buy orders and sell orders found in Order Book. A value below 1 means sell order size is greater, while value greater than 1 means buy order size is higher. [Check market depth](#check-depth-of-market) <br> *Defaults to `0`.* <br> **Datatype:** Float (as ratio)
| `ask_strategy.price_side` | Select the side of the spread the bot should look at to get the sell rate. [More information below](#sell-price-side).<br> *Defaults to `ask`.* <br> **Datatype:** String (either `ask` or `bid`).
| `ask_strategy.bid_last_balance` | Interpolate the selling price. More information [below](#sell-price-without-orderbook-enabled).
| `ask_strategy.use_order_book` | Enable selling of open trades using [Order Book Asks](#sell-price-with-orderbook-enabled). <br> **Datatype:** Boolean
| `ask_strategy.order_book_min` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
| `ask_strategy.order_book_max` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
| `ask_strategy.use_sell_signal` | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `true`.* <br> **Datatype:** Boolean
| `ask_strategy.sell_profit_only` | Wait until the bot reaches `ask_strategy.sell_profit_offset` before taking a sell decision. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `ask_strategy.sell_profit_offset` | Sell-signal is only active above this value. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0`.* <br> **Datatype:** Float (as ratio)
| `ask_strategy.ignore_roi_if_buy_signal` | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `ask_strategy.ignore_buying_expired_candle_after` | Specifies the number of seconds until a buy signal is no longer used. <br> **Datatype:** Integer
| `ask_strategy.order_book_top` | Bot will use the top N rate in Order Book "price_side" to sell. I.e. a value of 2 will allow the bot to pick the 2nd ask rate in [Order Book Asks](#sell-price-with-orderbook-enabled)<br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
| `use_sell_signal` | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `true`.* <br> **Datatype:** Boolean
| `sell_profit_only` | Wait until the bot reaches `sell_profit_offset` before taking a sell decision. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `sell_profit_offset` | Sell-signal is only active above this value. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0`.* <br> **Datatype:** Float (as ratio)
| `ignore_roi_if_buy_signal` | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `ignore_buying_expired_candle_after` | Specifies the number of seconds until a buy signal is no longer used. <br> **Datatype:** Integer
| `order_types` | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Dict
| `order_time_in_force` | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict
| `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). <br> **Datatype:** String
@ -141,7 +140,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
### Parameters in the strategy
The following parameters can be set in either configuration file or strategy.
The following parameters can be set in configuration file or strategy.
Values set in the configuration file always overwrite values set in the strategy.
* `minimal_roi`
@ -157,11 +156,11 @@ Values set in the configuration file always overwrite values set in the strategy
* `order_time_in_force`
* `unfilledtimeout`
* `disable_dataframe_checks`
* `use_sell_signal` (ask_strategy)
* `sell_profit_only` (ask_strategy)
* `sell_profit_offset` (ask_strategy)
* `ignore_roi_if_buy_signal` (ask_strategy)
* `ignore_buying_expired_candle_after` (ask_strategy)
* `use_sell_signal`
* `sell_profit_only`
* `sell_profit_offset`
* `ignore_roi_if_buy_signal`
* `ignore_buying_expired_candle_after`
### Configuring amount per trade
@ -170,7 +169,7 @@ There are several methods to configure how much of the stake currency the bot wi
#### Minimum trade stake
The minimum stake amount will depend by exchange and pair, and is usually listed in the exchange support pages.
Assuming the minimum tradable amount for XRP/USD is 20 XRP (given by the exchange), and the price is 0.4$.
Assuming the minimum tradable amount for XRP/USD is 20 XRP (given by the exchange), and the price is 0.6$.
The minimum stake amount to buy this pair is therefore `20 * 0.6 ~= 12`.
This exchange has also a limit on USD - where all orders must be > 10$ - which however does not apply in this case.
@ -292,16 +291,16 @@ See [the telegram documentation](telegram-usage.md) for details on usage.
When working with larger timeframes (for example 1h or more) and using a low `max_open_trades` value, the last candle can be processed as soon as a trade slot becomes available. When processing the last candle, this can lead to a situation where it may not be desirable to use the buy signal on that candle. For example, when using a condition in your strategy where you use a cross-over, that point may have passed too long ago for you to start a trade on it.
In these situations, you can enable the functionality to ignore candles that are beyond a specified period by setting `ask_strategy.ignore_buying_expired_candle_after` to a positive number, indicating the number of seconds after which the buy signal becomes expired.
In these situations, you can enable the functionality to ignore candles that are beyond a specified period by setting `ignore_buying_expired_candle_after` to a positive number, indicating the number of seconds after which the buy signal becomes expired.
For example, if your strategy is using a 1h timeframe, and you only want to buy within the first 5 minutes when a new candle comes in, you can add the following configuration to your strategy:
``` json
"ask_strategy":{
{
//...
"ignore_buying_expired_candle_after": 300,
"price_side": "bid",
// ...
},
}
```
!!! Note
@ -503,7 +502,8 @@ Once you will be happy with your bot performance running in the Dry-run mode, yo
* API-keys may or may not be provided. Only Read-Only operations (i.e. operations that do not alter account state) on the exchange are performed in dry-run mode.
* Wallets (`/balance`) are simulated based on `dry_run_wallet`.
* Orders are simulated, and will not be posted to the exchange.
* Orders are assumed to fill immediately, and will never time out.
* Market orders fill based on orderbook volume the moment the order is placed.
* Limit orders fill once price reaches the defined level - or time out based on `unfilledtimeout` settings.
* In combination with `stoploss_on_exchange`, the stop_loss price is assumed to be filled.
* Open orders (not trades, which are stored in the database) are reset on bot restart.

View File

@ -271,7 +271,7 @@ mkdir -p user_data/data/binance
cp tests/testdata/pairs.json user_data/data/binance
```
If you your configuration directory `user_data` was made by docker, you may get the following error:
If your configuration directory `user_data` was made by docker, you may get the following error:
```
cp: cannot create regular file 'user_data/data/binance/pairs.json': Permission denied

View File

@ -33,3 +33,8 @@ The old section of configuration parameters (`"pairlist"`) has been deprecated i
### deprecation of bidVolume and askVolume from volume-pairlist
Since only quoteVolume can be compared between assets, the other options (bidVolume, askVolume) have been deprecated in 2020.4, and have been removed in 2020.9.
### Using order book steps for sell price
Using `order_book_min` and `order_book_max` used to allow stepping the orderbook and trying to find the next ROI slot - trying to place sell-orders early.
As this does however increase risk and provides no benefit, it's been removed for maintainability purposes in 2021.7.

View File

@ -51,7 +51,7 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]]
[--print-all] [--no-color] [--print-json] [-j JOBS]
[--random-state INT] [--min-trades INT]
[--hyperopt-loss NAME]
[--hyperopt-loss NAME] [--disable-param-export]
optional arguments:
-h, --help show this help message and exit
@ -118,6 +118,8 @@ optional arguments:
ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss,
SharpeHyperOptLoss, SharpeHyperOptLossDaily,
SortinoHyperOptLoss, SortinoHyperOptLossDaily
--disable-param-export
Disable automatic hyperopt parameter export.
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
@ -403,6 +405,9 @@ While this strategy is most likely too simple to provide consistent profit, it s
!!! Note
`self.buy_ema_short.range` will act differently between hyperopt and other modes. For hyperopt, the above example may generate 48 new columns, however for all other modes (backtesting, dry/live), it will only generate the column for the selected value. You should therefore avoid using the resulting column with explicit values (values other than `self.buy_ema_short.value`).
!!! Note
`range` property may also be used with `DecimalParameter` and `CategoricalParameter`. `RealParameter` does not provide this property due to infinite search space.
??? Hint "Performance tip"
By doing the calculation of all possible indicators in `populate_indicators()`, the calculation of the indicator happens only once for every parameter.
While this may slow down the hyperopt startup speed, the overall performance will increase as the Hyperopt execution itself may pick the same value for multiple epochs (changing other values).
@ -509,7 +514,13 @@ You should understand this result like:
* You should not use ADX because `'buy_adx_enabled': False`.
* You should **consider** using the RSI indicator (`'buy_rsi_enabled': True`) and the best value is `29.0` (`'buy_rsi': 29.0`)
Your strategy class can immediately take advantage of these results. Simply copy hyperopt results block and paste them at class level, replacing old parameters (if any). New parameters will automatically be loaded next time strategy is executed.
### Automatic parameter application to the strategy
When using Hyperoptable parameters, the result of your hyperopt-run will be written to a json file next to your strategy (so for `MyAwesomeStrategy.py`, the file would be `MyAwesomeStrategy.json`).
This file is also updated when using the `hyperopt-show` sub-command, unless `--disable-param-export` is provided to either of the 2 commands.
Your strategy class can also contain these results explicitly. Simply copy hyperopt results block and paste them at class level, replacing old parameters (if any). New parameters will automatically be loaded next time strategy is executed.
Transferring your whole hyperopt result to your strategy would then look like:
@ -525,6 +536,10 @@ class MyAwesomeStrategy(IStrategy):
}
```
!!! Note
Values in the configuration file will overwrite Parameter-file level parameters - and both will overwrite parameters within the strategy.
The prevalence is therefore: config > parameter file > strategy
### Understand Hyperopt ROI results
If you are optimizing ROI (i.e. if optimization search-space contains 'all', 'default' or 'roi'), your result will look as follows and include a ROI table:

View File

@ -23,6 +23,7 @@ You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged
* [`StaticPairList`](#static-pair-list) (default, if not configured differently)
* [`VolumePairList`](#volume-pair-list)
* [`AgeFilter`](#agefilter)
* [`OffsetFilter`](#offsetfilter)
* [`PerformanceFilter`](#performancefilter)
* [`PrecisionFilter`](#precisionfilter)
* [`PriceFilter`](#pricefilter)
@ -63,17 +64,56 @@ The `refresh_period` setting allows to define the period (in seconds), at which
The pairlist cache (`refresh_period`) on `VolumePairList` is only applicable to generating pairlists.
Filtering instances (not the first position in the list) will not apply any cache and will always use up-to-date data.
`VolumePairList` is based on the ticker data from exchange, as reported by the ccxt library:
`VolumePairList` is per default based on the ticker data from exchange, as reported by the ccxt library:
* The `quoteVolume` is the amount of quote (stake) currency traded (bought or sold) in last 24 hours.
```json
"pairlists": [{
"pairlists": [
{
"method": "VolumePairList",
"number_assets": 20,
"sort_key": "quoteVolume",
"refresh_period": 1800
}],
}
],
```
`VolumePairList` can also operate in an advanced mode to build volume over a given timerange of specified candle size. It utilizes exchange historical candle data, builds a typical price (calculated by (open+high+low)/3) and multiplies the typical price with every candle's volume. The sum is the `quoteVolume` over the given range. This allows different scenarios, for a more smoothened volume, when using longer ranges with larger candle sizes, or the opposite when using a short range with small candles.
For convenience `lookback_days` can be specified, which will imply that 1d candles will be used for the lookback. In the example below the pairlist would be created based on the last 7 days:
```json
"pairlists": [
{
"method": "VolumePairList",
"number_assets": 20,
"sort_key": "quoteVolume",
"refresh_period": 86400,
"lookback_days": 7
}
],
```
!!! Warning "Range look back and refresh period"
When used in conjunction with `lookback_days` and `lookback_timeframe` the `refresh_period` can not be smaller than the candle size in seconds. As this will result in unnecessary requests to the exchanges API.
!!! Warning "Performance implications when using lookback range"
If used in first position in combination with lookback, the computation of the range based volume can be time and resource consuming, as it downloads candles for all tradable pairs. Hence it's highly advised to use the standard approach with `VolumeFilter` to narrow the pairlist down for further range volume calculation.
More sophisticated approach can be used, by using `lookback_timeframe` for candle size and `lookback_period` which specifies the amount of candles. This example will build the volume pairs based on a rolling period of 3 days of 1h candles:
```json
"pairlists": [
{
"method": "VolumePairList",
"number_assets": 20,
"sort_key": "quoteVolume",
"refresh_period": 3600,
"lookback_timeframe": "1h",
"lookback_period": 72
}
],
```
!!! Note
@ -81,13 +121,39 @@ Filtering instances (not the first position in the list) will not apply any cach
#### AgeFilter
Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`).
Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`) or more than `max_days_listed` days (defaults `None` mean infinity).
When pairs are first listed on an exchange they can suffer huge price drops and volatility
in the first few days while the pair goes through its price-discovery period. Bots can often
be caught out buying before the pair has finished dropping in price.
This filter allows freqtrade to ignore pairs until they have been listed for at least `min_days_listed` days.
This filter allows freqtrade to ignore pairs until they have been listed for at least `min_days_listed` days and listed before `max_days_listed`.
#### OffsetFilter
Offsets an incoming pairlist by a given `offset` value.
As an example it can be used in conjunction with `VolumeFilter` to remove the top X volume pairs. Or to split
a larger pairlist on two bot instances.
Example to remove the first 10 pairs from the pairlist:
```json
"pairlists": [
{
"method": "OffsetFilter",
"offset": 10
}
],
```
!!! Warning
When `OffsetFilter` is used to split a larger pairlist among multiple bots in combination with `VolumeFilter`
it can not be guaranteed that pairs won't overlap due to slightly different refresh intervals for the
`VolumeFilter`.
!!! Note
An offset larger then the total length of the incoming pairlist will result in an empty pairlist.
#### PerformanceFilter

View File

@ -47,7 +47,7 @@ Also, prices at the "ask" side of the spread are higher than prices at the "bid"
#### Buy price with Orderbook enabled
When buying with the orderbook enabled (`bid_strategy.use_order_book=True`), Freqtrade fetches the `bid_strategy.order_book_top` entries from the orderbook and then uses the entry specified as `bid_strategy.order_book_top` on the configured side (`bid_strategy.price_side`) of the orderbook. 1 specifies the topmost entry in the orderbook, while 2 would use the 2nd entry in the orderbook, and so on.
When buying with the orderbook enabled (`bid_strategy.use_order_book=True`), Freqtrade fetches the `bid_strategy.order_book_top` entries from the orderbook and uses the entry specified as `bid_strategy.order_book_top` on the configured side (`bid_strategy.price_side`) of the orderbook. 1 specifies the topmost entry in the orderbook, while 2 would use the 2nd entry in the orderbook, and so on.
#### Buy price without Orderbook enabled
@ -82,22 +82,9 @@ In line with that, if `ask_strategy.price_side` is set to `"bid"`, then the bot
#### Sell price with Orderbook enabled
When selling with the orderbook enabled (`ask_strategy.use_order_book=True`), Freqtrade fetches the `ask_strategy.order_book_max` entries in the orderbook. Then each of the orderbook steps between `ask_strategy.order_book_min` and `ask_strategy.order_book_max` on the configured orderbook side are validated for a profitable sell-possibility based on the strategy configuration (`minimal_roi` conditions) and the sell order is placed at the first profitable spot.
When selling with the orderbook enabled (`ask_strategy.use_order_book=True`), Freqtrade fetches the `ask_strategy.order_book_top` entries in the orderbook and uses the entry specified as `ask_strategy.order_book_top` from the configured side (`ask_strategy.price_side`) as selling price.
!!! Note
Using `order_book_max` higher than `order_book_min` only makes sense when ask_strategy.price_side is set to `"ask"`.
The idea here is to place the sell order early, to be ahead in the queue.
A fixed slot (mirroring `bid_strategy.order_book_top`) can be defined by setting `ask_strategy.order_book_min` and `ask_strategy.order_book_max` to the same number.
!!! Warning "Order_book_max > 1 - increased risks for stoplosses!"
Using `ask_strategy.order_book_max` higher than 1 will increase the risk the stoploss on exchange is cancelled too early, since an eventual [stoploss on exchange](#understand-order_types) will be cancelled as soon as the order is placed.
Also, the sell order will remain on the exchange for `unfilledtimeout.sell` (or until it's filled) - which can lead to missed stoplosses (with or without using stoploss on exchange).
!!! Warning "Order_book_max > 1 in dry-run"
Using `ask_strategy.order_book_max` higher than 1 will result in improper dry-run results (significantly better than real orders executed on exchange), since dry-run assumes orders to be filled almost instantly.
It is therefore advised to not use this setting for dry-runs.
1 specifies the topmost entry in the orderbook, while 2 would use the 2nd entry in the orderbook, and so on.
#### Sell price without Orderbook enabled

View File

@ -47,6 +47,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual,
Exchanges confirmed working by the community:
- [X] [Bitvavo](https://bitvavo.com/)
- [X] [Kukoin](https://www.kucoin.com/)
## Requirements

View File

@ -203,6 +203,8 @@ sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h
./configure --prefix=/usr/local
make
sudo make install
# On debian based systems (debian, ubuntu, ...) - updating ldconfig might be necessary.
sudo ldconfig
cd ..
rm -rf ./ta-lib*
```

3
docs/leverage.md Normal file
View File

@ -0,0 +1,3 @@
For shorts, the currency which pays the interest fee for the `borrowed` currency is purchased at the same time of the closing trade (This means that the amount purchased in short closing trades is greater than the amount sold in short opening trades).
For longs, the currency which pays the interest fee for the `borrowed` will already be owned by the user and does not need to be purchased. The interest is subtracted from the close_value of the trade.

View File

@ -1,5 +1,41 @@
{% extends "base.html" %}
<!-- Navigation -->
{% block site_nav %}
<!-- Main navigation -->
{% if nav %}
{% if page and page.meta and page.meta.hide %}
{% set hidden = "hidden" if "navigation" in page.meta.hide %}
{% endif %}
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" {{ hidden }}>
<div class="md-sidebar__scrollwrap">
<div id="widget-wrapper">
</div>
<div class="md-sidebar__inner">
{% include "partials/nav.html" %}
</div>
</div>
</div>
{% endif %}
<!-- Table of contents -->
{% if page.toc and not "toc.integrate" in features %}
{% if page and page.meta and page.meta.hide %}
{% set hidden = "hidden" if "toc" in page.meta.hide %}
{% endif %}
<div class="md-sidebar md-sidebar--secondary" data-md-component="sidebar" data-md-type="toc" {{ hidden }}>
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
{% include "partials/toc.html" %}
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block footer %}
{{ super() }}
@ -7,4 +43,26 @@
<script async defer src="https://buttons.github.io/buttons.js"></script>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
<!-- Load binance SDK -->
<script async defer src="https://public.bnbstatic.com/static/js/broker-sdk/broker-sdk@1.0.0.min.js"></script>
<script>
window.onload = function () {
var sidebar = document.getElementById('widget-wrapper')
var newDiv = document.createElement("div");
newDiv.id = "widget";
try {
sidebar.prepend(newDiv);
window.binanceBrokerPortalSdk.initBrokerSDK('#widget', {
apiHost: 'https://www.binance.com',
brokerId: 'R4BD3S82',
slideTime: 4e4,
});
} catch(err) {
console.log(err)
}
}
</script>
{% endblock %}

View File

@ -1,4 +1,4 @@
mkdocs==1.2.1
mkdocs-material==7.1.8
mkdocs-material==7.1.9
mdx_truly_sane_lists==1.2
pymdown-extensions==8.2

View File

@ -55,7 +55,7 @@ class AwesomeStrategy(IStrategy):
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
# Obtain last available candle. Do not use current_time to look up latest candle, because
# current_time points to curret incomplete candle whose data is not available.
# current_time points to current incomplete candle whose data is not available.
last_candle = dataframe.iloc[-1].squeeze()
# <...>
@ -83,7 +83,7 @@ It is possible to define custom sell signals, indicating that specified position
For example you could implement a 1:2 risk-reward ROI with `custom_sell()`.
Using custom_sell() signals in place of stoplosses though *is not recommended*. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange.
Using custom_sell() signals in place of stoploss though *is not recommended*. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange.
!!! Note
Returning a `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set already, or if sell signals are disabled (`use_sell_signal=False` or `sell_profit_only=True` while profit is below `sell_profit_offset`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters.
@ -243,7 +243,7 @@ class AwesomeStrategy(IStrategy):
current_rate: float, current_profit: float, **kwargs) -> float:
if current_profit < 0.04:
return -1 # return a value bigger than the inital stoploss to keep using the inital stoploss
return -1 # return a value bigger than the initial stoploss to keep using the initial stoploss
# After reaching the desired offset, allow the stoploss to trail by half the profit
desired_stoploss = current_profit / 2

View File

@ -130,6 +130,44 @@ trades = load_backtest_data(backtest_dir)
trades.groupby("pair")["sell_reason"].value_counts()
```
## Plotting daily profit / equity line
```python
# Plotting equity line (starting with 0 on day 1 and adding daily profit for each backtested day)
from freqtrade.configuration import Configuration
from freqtrade.data.btanalysis import load_backtest_data, load_backtest_stats
import plotly.express as px
import pandas as pd
# strategy = 'SampleStrategy'
# config = Configuration.from_files(["user_data/config.json"])
# backtest_dir = config["user_data_dir"] / "backtest_results"
stats = load_backtest_stats(backtest_dir)
strategy_stats = stats['strategy'][strategy]
dates = []
profits = []
for date_profit in strategy_stats['daily_profit']:
dates.append(date_profit[0])
profits.append(date_profit[1])
equity = 0
equity_daily = []
for daily_profit in profits:
equity_daily.append(equity)
equity += float(daily_profit)
df = pd.DataFrame({'dates': dates,'equity_daily': equity_daily})
fig = px.line(df, x="dates", y="equity_daily")
fig.show()
```
### Load live trading results into a pandas dataframe
In case you did already some trading and want to analyze your performance

View File

@ -11,3 +11,18 @@
.rst-versions .rst-other-versions {
color: white;
}
#widget-wrapper {
height: calc(220px * 0.5625 + 18px);
width: 220px;
margin: 0 auto 16px auto;
border-style: solid;
border-color: var(--md-code-bg-color);
border-width: 1px;
border-radius: 5px;
}
@media screen and (max-width: calc(76.25em - 1px)) {
#widget-wrapper { display: none; }
}

View File

@ -702,7 +702,8 @@ You can show the details of any hyperoptimization epoch previously evaluated by
usage: freqtrade hyperopt-show [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[-d PATH] [--userdir PATH] [--best]
[--profitable] [-n INT] [--print-json]
[--hyperopt-filename PATH] [--no-header]
[--hyperopt-filename FILENAME] [--no-header]
[--disable-param-export]
optional arguments:
-h, --help show this help message and exit
@ -714,6 +715,8 @@ optional arguments:
Hyperopt result filename.Example: `--hyperopt-
filename=hyperopt_results_2020-09-27_16-20-48.pickle`
--no-header Do not print epoch details header.
--disable-param-export
Disable automatic hyperopt parameter export.
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).

View File

@ -29,7 +29,7 @@ ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
"epochs", "spaces", "print_all",
"print_colorized", "print_json", "hyperopt_jobs",
"hyperopt_random_state", "hyperopt_min_trades",
"hyperopt_loss"]
"hyperopt_loss", "disableparamexport"]
ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
@ -85,7 +85,8 @@ ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable",
"hyperoptexportfilename", "export_csv"]
ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperopt_show_index",
"print_json", "hyperoptexportfilename", "hyperopt_show_no_header"]
"print_json", "hyperoptexportfilename", "hyperopt_show_no_header",
"disableparamexport"]
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
"list-markets", "list-pairs", "list-strategies", "list-data",

View File

@ -184,7 +184,7 @@ def deploy_new_config(config_path: Path, selections: Dict[str, Any]) -> None:
"""
Applies selections to the template and writes the result to config_path
:param config_path: Path object for new config file. Should not exist yet
:param selecions: Dict containing selections taken by the user.
:param selections: Dict containing selections taken by the user.
"""
from jinja2.exceptions import TemplateNotFound
try:
@ -214,7 +214,7 @@ def deploy_new_config(config_path: Path, selections: Dict[str, Any]) -> None:
def start_new_config(args: Dict[str, Any]) -> None:
"""
Create a new strategy from a template
Asking the user questions to fill out the templateaccordingly.
Asking the user questions to fill out the template accordingly.
"""
config_path = Path(args['config'][0])

View File

@ -178,6 +178,11 @@ AVAILABLE_CLI_OPTIONS = {
'Example: `--export-filename=user_data/backtest_results/backtest_today.json`',
metavar='PATH',
),
"disableparamexport": Arg(
'--disable-param-export',
help="Disable automatic hyperopt parameter export.",
action='store_true',
),
"fee": Arg(
'--fee',
help='Specify fee ratio. Will be applied twice (on trade entry and exit).',

View File

@ -129,9 +129,12 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
metrics = val['results_metrics']
if 'strategy_name' in metrics:
show_backtest_result(metrics['strategy_name'], metrics,
strategy_name = metrics['strategy_name']
show_backtest_result(strategy_name, metrics,
metrics['stake_currency'])
HyperoptTools.try_export_params(config, strategy_name, val)
HyperoptTools.show_epoch_details(val, total_epochs, print_json, no_header,
header_str="Epoch details")

View File

@ -15,6 +15,7 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[
"""
Prepare the configuration for the Hyperopt module
:param args: Cli args from Arguments()
:param method: Bot running mode
:return: Configuration
"""
config = setup_utils_configuration(args, method)

View File

@ -15,6 +15,7 @@ def setup_utils_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str
"""
Prepare the configuration for utils subcommands
:param args: Cli args from Arguments()
:param method: Bot running mode
:return: Configuration
"""
configuration = Configuration(args, method)

View File

@ -79,6 +79,7 @@ def validate_config_consistency(conf: Dict[str, Any]) -> None:
_validate_whitelist(conf)
_validate_protections(conf)
_validate_unlimited_amount(conf)
_validate_ask_orderbook(conf)
# validate configuration before returning
logger.info('Validating configuration ...')
@ -149,7 +150,7 @@ def _validate_edge(conf: Dict[str, Any]) -> None:
if not conf.get('edge', {}).get('enabled'):
return
if not conf.get('ask_strategy', {}).get('use_sell_signal', True):
if not conf.get('use_sell_signal', True):
raise OperationalException(
"Edge requires `use_sell_signal` to be True, otherwise no sells will happen."
)
@ -186,3 +187,23 @@ def _validate_protections(conf: Dict[str, Any]) -> None:
"Protections must specify either `lookback_period` or `lookback_period_candles`.\n"
f"Please fix the protection {prot.get('method')}"
)
def _validate_ask_orderbook(conf: Dict[str, Any]) -> None:
ask_strategy = conf.get('ask_strategy', {})
ob_min = ask_strategy.get('order_book_min')
ob_max = ask_strategy.get('order_book_max')
if ob_min is not None and ob_max is not None and ask_strategy.get('use_order_book'):
if ob_min != ob_max:
raise OperationalException(
"Using order_book_max != order_book_min in ask_strategy is no longer supported."
"Please pick one value and use `order_book_top` in the future."
)
else:
# Move value to order_book_top
ask_strategy['order_book_top'] = ob_min
logger.warning(
"DEPRECATED: "
"Please use `order_book_top` instead of `order_book_min` and `order_book_max` "
"for your `ask_strategy` configuration."
)

View File

@ -260,6 +260,8 @@ class Configuration:
self._args_to_config(config, argname='export',
logstring='Parameter --export detected: {} ...')
self._args_to_config(config, argname='disableparamexport',
logstring='Parameter --disableparamexport detected: {} ...')
# Edge section:
if 'stoploss_range' in self.args and self.args["stoploss_range"]:
txt_range = eval(self.args["stoploss_range"])
@ -460,7 +462,7 @@ class Configuration:
pairs_file = Path(self.args["pairs_file"])
logger.info(f'Reading pairs file "{pairs_file}".')
# Download pairs from the pairs file if no config is specified
# or if pairs file is specified explicitely
# or if pairs file is specified explicitly
if not pairs_file.exists():
raise OperationalException(f'No pairs file found with path "{pairs_file}".')
config['pairs'] = load_file(pairs_file)

View File

@ -3,7 +3,7 @@ Functions to handle deprecated settings
"""
import logging
from typing import Any, Dict
from typing import Any, Dict, Optional
from freqtrade.exceptions import OperationalException
@ -12,23 +12,24 @@ logger = logging.getLogger(__name__)
def check_conflicting_settings(config: Dict[str, Any],
section1: str, name1: str,
section2: str, name2: str) -> None:
section1_config = config.get(section1, {})
section2_config = config.get(section2, {})
if name1 in section1_config and name2 in section2_config:
section_old: str, name_old: str,
section_new: Optional[str], name_new: str) -> None:
section_new_config = config.get(section_new, {}) if section_new else config
section_old_config = config.get(section_old, {})
if name_new in section_new_config and name_old in section_old_config:
new_name = f"{section_new}.{name_new}" if section_new else f"{name_new}"
raise OperationalException(
f"Conflicting settings `{section1}.{name1}` and `{section2}.{name2}` "
f"Conflicting settings `{new_name}` and `{section_old}.{name_old}` "
"(DEPRECATED) detected in the configuration file. "
"This deprecated setting will be removed in the next versions of Freqtrade. "
f"Please delete it from your configuration and use the `{section1}.{name1}` "
f"Please delete it from your configuration and use the `{new_name}` "
"setting instead."
)
def process_removed_setting(config: Dict[str, Any],
section1: str, name1: str,
section2: str, name2: str) -> None:
section2: Optional[str], name2: str) -> None:
"""
:param section1: Removed section
:param name1: Removed setting name
@ -37,27 +38,32 @@ def process_removed_setting(config: Dict[str, Any],
"""
section1_config = config.get(section1, {})
if name1 in section1_config:
section_2 = f"{section2}.{name2}" if section2 else f"{name2}"
raise OperationalException(
f"Setting `{section1}.{name1}` has been moved to `{section2}.{name2}. "
f"Please delete it from your configuration and use the `{section2}.{name2}` "
f"Setting `{section1}.{name1}` has been moved to `{section_2}. "
f"Please delete it from your configuration and use the `{section_2}` "
"setting instead."
)
def process_deprecated_setting(config: Dict[str, Any],
section1: str, name1: str,
section2: str, name2: str) -> None:
section2_config = config.get(section2, {})
section_old: str, name_old: str,
section_new: Optional[str], name_new: str
) -> None:
check_conflicting_settings(config, section_old, name_old, section_new, name_new)
section_old_config = config.get(section_old, {})
if name2 in section2_config:
if name_old in section_old_config:
section_2 = f"{section_new}.{name_new}" if section_new else f"{name_new}"
logger.warning(
"DEPRECATED: "
f"The `{section2}.{name2}` setting is deprecated and "
f"The `{section_old}.{name_old}` setting is deprecated and "
"will be removed in the next versions of Freqtrade. "
f"Please use the `{section1}.{name1}` setting in your configuration instead."
f"Please use the `{section_2}` setting in your configuration instead."
)
section1_config = config.get(section1, {})
section1_config[name1] = section2_config[name2]
section_new_config = config.get(section_new, {}) if section_new else config
section_new_config[name_new] = section_old_config[name_old]
def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
@ -65,15 +71,24 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
# Kept for future deprecated / moved settings
# check_conflicting_settings(config, 'ask_strategy', 'use_sell_signal',
# 'experimental', 'use_sell_signal')
# process_deprecated_setting(config, 'ask_strategy', 'use_sell_signal',
# 'experimental', 'use_sell_signal')
process_deprecated_setting(config, 'ask_strategy', 'use_sell_signal',
None, 'use_sell_signal')
process_deprecated_setting(config, 'ask_strategy', 'sell_profit_only',
None, 'sell_profit_only')
process_deprecated_setting(config, 'ask_strategy', 'sell_profit_offset',
None, 'sell_profit_offset')
process_deprecated_setting(config, 'ask_strategy', 'ignore_roi_if_buy_signal',
None, 'ignore_roi_if_buy_signal')
process_deprecated_setting(config, 'ask_strategy', 'ignore_buying_expired_candle_after',
None, 'ignore_buying_expired_candle_after')
# Legacy way - having them in experimental ...
process_removed_setting(config, 'experimental', 'use_sell_signal',
'ask_strategy', 'use_sell_signal')
None, 'use_sell_signal')
process_removed_setting(config, 'experimental', 'sell_profit_only',
'ask_strategy', 'sell_profit_only')
None, 'sell_profit_only')
process_removed_setting(config, 'experimental', 'ignore_roi_if_buy_signal',
'ask_strategy', 'ignore_roi_if_buy_signal')
None, 'ignore_roi_if_buy_signal')
if (config.get('edge', {}).get('enabled', False)
and 'capital_available_percentage' in config.get('edge', {})):

View File

@ -26,9 +26,9 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily',
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily']
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
'AgeFilter', 'PerformanceFilter', 'PrecisionFilter',
'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter',
'SpreadFilter', 'VolatilityFilter']
'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard']
AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5']
DRY_RUN_WALLET = 1000
@ -40,6 +40,7 @@ DEFAULT_DATAFRAME_COLUMNS = ['date', 'open', 'high', 'low', 'close', 'volume']
DEFAULT_TRADES_COLUMNS = ['timestamp', 'id', 'type', 'side', 'price', 'amount', 'cost']
LAST_BT_RESULT_FN = '.last_result.json'
FTHYPT_FILEVERSION = 'fthypt_fileversion'
USERPATH_HYPEROPTS = 'hyperopts'
USERPATH_STRATEGIES = 'strategies'
@ -62,7 +63,7 @@ DUST_PER_COIN = {
}
# Soure files with destination directories within user-directory
# Source files with destination directories within user-directory
USER_DATA_FILES = {
'sample_strategy.py': USERPATH_STRATEGIES,
'sample_hyperopt_advanced.py': USERPATH_HYPEROPTS,
@ -134,6 +135,11 @@ CONF_SCHEMA = {
'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1},
'trailing_stop_positive_offset': {'type': 'number', 'minimum': 0, 'maximum': 1},
'trailing_only_offset_is_reached': {'type': 'boolean'},
'use_sell_signal': {'type': 'boolean'},
'sell_profit_only': {'type': 'boolean'},
'sell_profit_offset': {'type': 'number'},
'ignore_roi_if_buy_signal': {'type': 'boolean'},
'ignore_buying_expired_candle_after': {'type': 'number'},
'bot_name': {'type': 'string'},
'unfilledtimeout': {
'type': 'object',
@ -154,7 +160,7 @@ CONF_SCHEMA = {
},
'price_side': {'type': 'string', 'enum': ORDERBOOK_SIDES, 'default': 'bid'},
'use_order_book': {'type': 'boolean'},
'order_book_top': {'type': 'integer', 'maximum': 20, 'minimum': 1},
'order_book_top': {'type': 'integer', 'minimum': 1, 'maximum': 50, },
'check_depth_of_market': {
'type': 'object',
'properties': {
@ -163,7 +169,7 @@ CONF_SCHEMA = {
}
},
},
'required': ['ask_last_balance']
'required': ['price_side']
},
'ask_strategy': {
'type': 'object',
@ -176,13 +182,9 @@ CONF_SCHEMA = {
'exclusiveMaximum': False,
},
'use_order_book': {'type': 'boolean'},
'order_book_min': {'type': 'integer', 'minimum': 1},
'order_book_max': {'type': 'integer', 'minimum': 1, 'maximum': 50},
'use_sell_signal': {'type': 'boolean'},
'sell_profit_only': {'type': 'boolean'},
'sell_profit_offset': {'type': 'number'},
'ignore_roi_if_buy_signal': {'type': 'boolean'}
}
'order_book_top': {'type': 'integer', 'minimum': 1, 'maximum': 50, },
},
'required': ['price_side']
},
'order_types': {
'type': 'object',
@ -311,6 +313,7 @@ CONF_SCHEMA = {
},
'db_url': {'type': 'string'},
'export': {'type': 'string', 'enum': EXPORT_OPTIONS, 'default': 'trades'},
'disableparamexport': {'type': 'boolean'},
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
'forcebuy_enable': {'type': 'boolean'},
'disable_dataframe_checks': {'type': 'boolean'},

View File

@ -49,7 +49,7 @@ def clean_ohlcv_dataframe(data: DataFrame, timeframe: str, pair: str, *,
fill_missing: bool = True,
drop_incomplete: bool = True) -> DataFrame:
"""
Clense a OHLCV dataframe by
Cleanse a OHLCV dataframe by
* Grouping it by date (removes duplicate tics)
* dropping last candles if requested
* Filling up missing data (if requested)

View File

@ -52,8 +52,8 @@ class HDF5DataHandler(IDataHandler):
"""
Store data in hdf5 file.
:param pair: Pair - used to generate filename
:timeframe: Timeframe - used to generate filename
:data: Dataframe containing OHLCV data
:param timeframe: Timeframe - used to generate filename
:param data: Dataframe containing OHLCV data
:return: None
"""
key = self._pair_ohlcv_key(pair, timeframe)

View File

@ -113,6 +113,7 @@ def refresh_data(datadir: Path,
:param timeframe: Timeframe (e.g. "5m")
:param pairs: List of pairs to load
:param exchange: Exchange object
:param data_format: dataformat to use
:param timerange: Limit data to be loaded to this timerange
"""
data_handler = get_datahandler(datadir, data_format)
@ -193,8 +194,8 @@ def _download_pair_history(datadir: Path,
new_data = exchange.get_historic_ohlcv(pair=pair,
timeframe=timeframe,
since_ms=since_ms if since_ms else
int(arrow.utcnow().shift(
days=-new_pairs_days).float_timestamp) * 1000
arrow.utcnow().shift(
days=-new_pairs_days).int_timestamp * 1000
)
# TODO: Maybe move parsing to exchange class (?)
new_dataframe = ohlcv_to_dataframe(new_data, timeframe, pair,
@ -271,7 +272,7 @@ def _download_trades_history(exchange: Exchange,
if timerange.stoptype == 'date':
until = timerange.stopts * 1000
else:
since = int(arrow.utcnow().shift(days=-new_pairs_days).float_timestamp) * 1000
since = arrow.utcnow().shift(days=-new_pairs_days).int_timestamp * 1000
trades = data_handler.trades_load(pair)

View File

@ -49,8 +49,8 @@ class IDataHandler(ABC):
"""
Store ohlcv data.
:param pair: Pair - used to generate filename
:timeframe: Timeframe - used to generate filename
:data: Dataframe containing OHLCV data
:param timeframe: Timeframe - used to generate filename
:param data: Dataframe containing OHLCV data
:return: None
"""
@ -245,8 +245,8 @@ def get_datahandler(datadir: Path, data_format: str = None,
data_handler: IDataHandler = None) -> IDataHandler:
"""
:param datadir: Folder to save data
:data_format: dataformat to use
:data_handler: returns this datahandler if it exists or initializes a new one
:param data_format: dataformat to use
:param data_handler: returns this datahandler if it exists or initializes a new one
"""
if not data_handler:

View File

@ -55,8 +55,8 @@ class JsonDataHandler(IDataHandler):
format looks as follows:
[[<date>,<open>,<high>,<low>,<close>]]
:param pair: Pair - used to generate filename
:timeframe: Timeframe - used to generate filename
:data: Dataframe containing OHLCV data
:param timeframe: Timeframe - used to generate filename
:param data: Dataframe containing OHLCV data
:return: None
"""
filename = self._pair_data_filename(self._datadir, pair, timeframe)

View File

@ -301,7 +301,7 @@ class Edge:
def _process_expectancy(self, results: DataFrame) -> Dict[str, Any]:
"""
This calculates WinRate, Required Risk Reward, Risk Reward and Expectancy of all pairs
The calulation will be done per pair and per strategy.
The calculation will be done per pair and per strategy.
"""
# Removing pairs having less than min_trades_number
min_trades_number = self.edge_config.get('min_trade_number', 10)

View File

@ -1,4 +1,5 @@
# flake8: noqa: F401
from freqtrade.enums.interestmode import InterestMode
from freqtrade.enums.rpcmessagetype import RPCMessageType
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
from freqtrade.enums.selltype import SellType

View File

@ -0,0 +1,30 @@
from decimal import Decimal
from enum import Enum
from math import ceil
from freqtrade.exceptions import OperationalException
one = Decimal(1.0)
four = Decimal(4.0)
twenty_four = Decimal(24.0)
class InterestMode(Enum):
"""Equations to calculate interest"""
HOURSPERDAY = "HOURSPERDAY"
HOURSPER4 = "HOURSPER4" # Hours per 4 hour segment
NONE = "NONE"
def __call__(self, *args, **kwargs):
borrowed, rate, hours = kwargs["borrowed"], kwargs["rate"], kwargs["hours"]
if self.name == "HOURSPERDAY":
return borrowed * rate * ceil(hours)/twenty_four
elif self.name == "HOURSPER4":
# Probably rounded based on https://kraken-fees-calculator.github.io/
return borrowed * rate * (1+ceil(hours/four))
else:
raise OperationalException("Leverage not available on this exchange with freqtrade")

View File

@ -47,7 +47,7 @@ class InvalidOrderException(ExchangeError):
class RetryableOrderError(InvalidOrderException):
"""
This is returned when the order is not found.
This Error will be repeated with increasing backof (in line with DDosError).
This Error will be repeated with increasing backoff (in line with DDosError).
"""
@ -75,6 +75,6 @@ class DDosProtection(TemporaryError):
class StrategyError(FreqtradeException):
"""
Errors with custom user-code deteced.
Errors with custom user-code detected.
Usually caused by errors in the strategy.
"""

View File

@ -235,7 +235,7 @@ class Exchange:
def ohlcv_candle_limit(self, timeframe: str) -> int:
"""
Exchange ohlcv candle limit
Uses ohlcv_candle_limit_per_timeframe if the exchange has different limts
Uses ohlcv_candle_limit_per_timeframe if the exchange has different limits
per timeframe (e.g. bittrex), otherwise falls back to ohlcv_candle_limit
:param timeframe: Timeframe to check
:return: Candle limit as integer
@ -475,11 +475,11 @@ class Exchange:
return endpoint in self._api.has and self._api.has[endpoint]
def amount_to_precision(self, pair: str, amount: float) -> float:
'''
"""
Returns the amount to buy or sell to a precision the Exchange accepts
Re-implementation of ccxt internal methods - ensuring we can test the result is correct
based on our definitions.
'''
"""
if self.markets[pair]['precision']['amount']:
amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE,
precision=self.markets[pair]['precision']['amount'],
@ -489,14 +489,14 @@ class Exchange:
return amount
def price_to_precision(self, pair: str, price: float) -> float:
'''
"""
Returns the price rounded up to the precision the Exchange accepts.
Partial Re-implementation of ccxt internal method decimal_to_precision(),
which does not support rounding up
TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and
align with amount_to_precision().
Rounds up
'''
"""
if self.markets[pair]['precision']['price']:
# price = float(decimal_to_precision(price, rounding_mode=ROUND,
# precision=self.markets[pair]['precision']['price'],
@ -567,7 +567,7 @@ class Exchange:
rate: float, params: Dict = {}) -> Dict[str, Any]:
order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
_amount = self.amount_to_precision(pair, amount)
dry_order = {
dry_order: Dict[str, Any] = {
'id': order_id,
'symbol': pair,
'price': rate,
@ -578,31 +578,99 @@ class Exchange:
'side': side,
'remaining': _amount,
'datetime': arrow.utcnow().isoformat(),
'timestamp': int(arrow.utcnow().int_timestamp * 1000),
'timestamp': arrow.utcnow().int_timestamp * 1000,
'status': "closed" if ordertype == "market" else "open",
'fee': None,
'info': {}
}
self._store_dry_order(dry_order, pair)
if dry_order["type"] in ["stop_loss_limit", "stop-loss-limit"]:
dry_order["info"] = {"stopPrice": dry_order["price"]}
if dry_order["type"] == "market":
# Update market order pricing
average = self.get_dry_market_fill_price(pair, side, amount, rate)
dry_order.update({
'average': average,
'cost': dry_order['amount'] * average,
})
dry_order = self.add_dry_order_fee(pair, dry_order)
dry_order = self.check_dry_limit_order_filled(dry_order)
self._dry_run_open_orders[dry_order["id"]] = dry_order
# Copy order and close it - so the returned order is open unless it's a market order
return dry_order
def _store_dry_order(self, dry_order: Dict, pair: str) -> None:
closed_order = dry_order.copy()
if closed_order['type'] in ["market", "limit"]:
closed_order.update({
'status': 'closed',
'filled': closed_order['amount'],
'remaining': 0,
def add_dry_order_fee(self, pair: str, dry_order: Dict[str, Any]) -> Dict[str, Any]:
dry_order.update({
'fee': {
'currency': self.get_pair_quote_currency(pair),
'cost': dry_order['cost'] * self.get_fee(pair),
'rate': self.get_fee(pair)
}
})
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
return dry_order
def get_dry_market_fill_price(self, pair: str, side: str, amount: float, rate: float) -> float:
"""
Get the market order fill price based on orderbook interpolation
"""
if self.exchange_has('fetchL2OrderBook'):
ob = self.fetch_l2_order_book(pair, 20)
ob_type = 'asks' if side == 'buy' else 'bids'
remaining_amount = amount
filled_amount = 0
for book_entry in ob[ob_type]:
book_entry_price = book_entry[0]
book_entry_coin_volume = book_entry[1]
if remaining_amount > 0:
if remaining_amount < book_entry_coin_volume:
filled_amount += remaining_amount * book_entry_price
else:
filled_amount += book_entry_coin_volume * book_entry_price
remaining_amount -= book_entry_coin_volume
else:
break
else:
# If remaining_amount wasn't consumed completely (break was not called)
filled_amount += remaining_amount * book_entry_price
forecast_avg_filled_price = filled_amount / amount
return self.price_to_precision(pair, forecast_avg_filled_price)
return rate
def _is_dry_limit_order_filled(self, pair: str, side: str, limit: float) -> bool:
if not self.exchange_has('fetchL2OrderBook'):
return True
ob = self.fetch_l2_order_book(pair, 1)
if side == 'buy':
price = ob['asks'][0][0]
logger.debug(f"{pair} checking dry buy-order: price={price}, limit={limit}")
if limit >= price:
return True
else:
price = ob['bids'][0][0]
logger.debug(f"{pair} checking dry sell-order: price={price}, limit={limit}")
if limit <= price:
return True
return False
def check_dry_limit_order_filled(self, order: Dict[str, Any]) -> Dict[str, Any]:
"""
Check dry-run limit order fill and update fee (if it filled).
"""
if order['status'] != "closed" and order['type'] in ["limit"]:
pair = order['symbol']
if self._is_dry_limit_order_filled(pair, order['side'], order['price']):
order.update({
'status': 'closed',
'filled': order['amount'],
'remaining': 0,
})
self.add_dry_order_fee(pair, order)
return order
def fetch_dry_run_order(self, order_id) -> Dict[str, Any]:
"""
@ -611,6 +679,7 @@ class Exchange:
"""
try:
order = self._dry_run_open_orders[order_id]
order = self.check_dry_limit_order_filled(order)
return order
except KeyError as e:
# Gracefully handle errors with dry-run orders.
@ -727,6 +796,8 @@ class Exchange:
"""
Simple wrapper calling either fetch_order or fetch_stoploss_order depending on
the stoploss_order parameter
:param order_id: OrderId to fetch order
:param pair: Pair corresponding to order_id
:param stoploss_order: If true, uses fetch_stoploss_order, otherwise fetch_order.
"""
if stoploss_order:
@ -928,15 +999,6 @@ class Exchange:
except ccxt.BaseError as e:
raise OperationalException(e) from e
def _order_book_gen(self, pair: str, side: str, order_book_max: int = 1,
order_book_min: int = 1):
"""
Helper generator to query orderbook in loop (used for early sell-order placing)
"""
order_book = self.fetch_l2_order_book(pair, order_book_max)
for i in range(order_book_min, order_book_max + 1):
yield order_book[side][i - 1][0]
def get_buy_rate(self, pair: str, refresh: bool) -> float:
"""
Calculates bid target between current ask price and last price
@ -1001,14 +1063,18 @@ class Exchange:
ask_strategy = self._config.get('ask_strategy', {})
if ask_strategy.get('use_order_book', False):
# This code is only used for notifications, selling uses the generator directly
logger.info(
logger.debug(
f"Getting price from order book {ask_strategy['price_side'].capitalize()} side."
)
order_book_top = ask_strategy.get('order_book_top', 1)
order_book = self.fetch_l2_order_book(pair, order_book_top)
try:
rate = next(self._order_book_gen(pair, f"{ask_strategy['price_side']}s"))
rate = order_book[f"{ask_strategy['price_side']}s"][order_book_top - 1][0]
except (IndexError, KeyError) as e:
logger.warning("Sell Price at location from orderbook could not be determined.")
logger.warning(
f"Sell Price at location {order_book_top} from orderbook could not be "
f"determined. Orderbook: {order_book}"
)
raise PricingError from e
else:
ticker = self.fetch_ticker(pair)

View File

@ -49,7 +49,7 @@ class Kraken(Exchange):
orders = self._api.fetch_open_orders()
order_list = [(x["symbol"].split("/")[0 if x["side"] == "sell" else 1],
x["remaining"] if x["side"] == "sell" else x["remaining"] * x["price"],
# Don't remove the below comment, this can be important for debuggung
# Don't remove the below comment, this can be important for debugging
# x["side"], x["amount"],
) for x in orders]
for bal in balances:

View File

@ -98,7 +98,7 @@ class FreqtradeBot(LoggingMixin):
initial_state = self.config.get('initial_state')
self.state = State[initial_state.upper()] if initial_state else State.STOPPED
# Protect sell-logic from forcesell and viceversa
# Protect sell-logic from forcesell and vice versa
self._sell_lock = Lock()
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
@ -472,6 +472,7 @@ class FreqtradeBot(LoggingMixin):
"""
Executes a limit buy for the given pair
:param pair: pair for which we want to create a LIMIT_BUY
:param stake_amount: amount of stake-currency for the pair
:return: True if a buy order is created, false if it fails.
"""
time_in_force = self.strategy.order_time_in_force['buy']
@ -683,42 +684,13 @@ class FreqtradeBot(LoggingMixin):
(buy, sell) = (False, False)
config_ask_strategy = self.config.get('ask_strategy', {})
if (config_ask_strategy.get('use_sell_signal', True) or
config_ask_strategy.get('ignore_roi_if_buy_signal', False)):
if (self.config.get('use_sell_signal', True) or
self.config.get('ignore_roi_if_buy_signal', False)):
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
self.strategy.timeframe)
(buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.timeframe, analyzed_df)
if config_ask_strategy.get('use_order_book', False):
order_book_min = config_ask_strategy.get('order_book_min', 1)
order_book_max = config_ask_strategy.get('order_book_max', 1)
logger.debug(f'Using order book between {order_book_min} and {order_book_max} '
f'for selling {trade.pair}...')
order_book = self.exchange._order_book_gen(
trade.pair, f"{config_ask_strategy['price_side']}s",
order_book_min=order_book_min, order_book_max=order_book_max)
for i in range(order_book_min, order_book_max + 1):
try:
sell_rate = next(order_book)
except (IndexError, KeyError) as e:
logger.warning(
f"Sell Price at location {i} from orderbook could not be determined."
)
raise PricingError from e
logger.debug(f" order book {config_ask_strategy['price_side']} top {i}: "
f"{sell_rate:0.8f}")
# Assign sell-rate to cache - otherwise sell-rate is never updated in the cache,
# resulting in outdated RPC messages
self.exchange._sell_rate_cache[trade.pair] = sell_rate
if self._check_and_execute_sell(trade, sell_rate, buy, sell):
return True
else:
logger.debug('checking sell')
sell_rate = self.exchange.get_sell_rate(trade.pair, True)
if self._check_and_execute_sell(trade, sell_rate, buy, sell):
@ -834,7 +806,7 @@ class FreqtradeBot(LoggingMixin):
"""
Check to see if stoploss on exchange should be updated
in case of trailing stoploss on exchange
:param Trade: Corresponding Trade
:param trade: Corresponding Trade
:param order: Current on exchange stoploss order
:return: None
"""

View File

@ -56,6 +56,7 @@ def file_dump_json(filename: Path, data: Any, is_zip: bool = False, log: bool =
"""
Dump JSON data into a file
:param filename: file to create
:param is_zip: if file should be zip
:param data: JSON Data to save
:return:
"""

View File

@ -116,6 +116,7 @@ class Backtesting:
# Get maximum required startup period
self.required_startup = max([strat.startup_candle_count for strat in self.strategylist])
self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe)
def __del__(self):
LoggingMixin.show_output = True
@ -228,16 +229,20 @@ class Backtesting:
# Special case: trailing triggers within same candle as trade opened. Assume most
# pessimistic price movement, which is moving just enough to arm stoploss and
# immediately going down to stop price.
if (sell.sell_type == SellType.TRAILING_STOP_LOSS and trade_dur == 0
and self.strategy.trailing_stop_positive):
if self.strategy.trailing_only_offset_is_reached:
if sell.sell_type == SellType.TRAILING_STOP_LOSS and trade_dur == 0:
if (
not self.strategy.use_custom_stoploss and self.strategy.trailing_stop
and self.strategy.trailing_only_offset_is_reached
and self.strategy.trailing_stop_positive_offset is not None
and self.strategy.trailing_stop_positive
):
# Worst case: price reaches stop_positive_offset and dives down.
stop_rate = (sell_row[OPEN_IDX] *
(1 + abs(self.strategy.trailing_stop_positive_offset) -
abs(self.strategy.trailing_stop_positive)))
else:
# Worst case: price ticks tiny bit above open and dives down.
stop_rate = sell_row[OPEN_IDX] * (1 - abs(self.strategy.trailing_stop_positive))
stop_rate = sell_row[OPEN_IDX] * (1 - abs(trade.stop_loss_pct))
assert stop_rate < sell_row[HIGH_IDX]
return stop_rate
@ -445,7 +450,7 @@ class Backtesting:
for trade in open_trades[pair]:
# also check the buying candle for sell conditions.
trade_entry = self._get_sell_trade_entry(trade, row)
# Sell occured
# Sell occurred
if trade_entry:
# logger.debug(f"{pair} - Backtesting sell {trade}")
open_trade_count -= 1

View File

@ -12,7 +12,6 @@ from math import ceil
from pathlib import Path
from typing import Any, Dict, List, Optional
import numpy as np
import progressbar
import rapidjson
from colorama import Fore, Style
@ -20,16 +19,16 @@ from colorama import init as colorama_init
from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects
from pandas import DataFrame
from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN
from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN
from freqtrade.data.converter import trim_dataframes
from freqtrade.data.history import get_timerange
from freqtrade.misc import file_dump_json, plural
from freqtrade.misc import deep_merge_dicts, file_dump_json, plural
from freqtrade.optimize.backtesting import Backtesting
# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules
from freqtrade.optimize.hyperopt_auto import HyperOptAuto
from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401
from freqtrade.optimize.hyperopt_tools import HyperoptTools
from freqtrade.optimize.hyperopt_tools import HyperoptTools, hyperopt_serializer
from freqtrade.optimize.optimize_reports import generate_strategy_stats
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver
@ -78,8 +77,11 @@ class Hyperopt:
if not self.config.get('hyperopt'):
self.custom_hyperopt = HyperOptAuto(self.config)
self.auto_hyperopt = True
else:
self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config)
self.auto_hyperopt = False
self.backtesting._set_strategy(self.backtesting.strategylist[0])
self.custom_hyperopt.strategy = self.backtesting.strategy
@ -163,13 +165,9 @@ class Hyperopt:
While not a valid json object - this allows appending easily.
:param epoch: result dictionary for this epoch.
"""
def default_parser(x):
if isinstance(x, np.integer):
return int(x)
return str(x)
epoch[FTHYPT_FILEVERSION] = 2
with self.results_file.open('a') as f:
rapidjson.dump(epoch, f, default=default_parser,
rapidjson.dump(epoch, f, default=hyperopt_serializer,
number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN)
f.write("\n")
@ -201,6 +199,25 @@ class Hyperopt:
return result
def _get_no_optimize_details(self) -> Dict[str, Any]:
"""
Get non-optimized parameters
"""
result: Dict[str, Any] = {}
strategy = self.backtesting.strategy
if not HyperoptTools.has_space(self.config, 'roi'):
result['roi'] = {str(k): v for k, v in strategy.minimal_roi.items()}
if not HyperoptTools.has_space(self.config, 'stoploss'):
result['stoploss'] = {'stoploss': strategy.stoploss}
if not HyperoptTools.has_space(self.config, 'trailing'):
result['trailing'] = {
'trailing_stop': strategy.trailing_stop,
'trailing_stop_positive': strategy.trailing_stop_positive,
'trailing_stop_positive_offset': strategy.trailing_stop_positive_offset,
'trailing_only_offset_is_reached': strategy.trailing_only_offset_is_reached,
}
return result
def print_results(self, results) -> None:
"""
Log results if it is better than any previous evaluation
@ -310,7 +327,8 @@ class Hyperopt:
results_explanation = HyperoptTools.format_results_explanation_string(
strat_stats, self.config['stake_currency'])
not_optimized = self.backtesting.strategy.get_params_dict()
not_optimized = self.backtesting.strategy.get_no_optimize_params()
not_optimized = deep_merge_dicts(not_optimized, self._get_no_optimize_details())
trade_count = strat_stats['total_trades']
total_profit = strat_stats['profit_total']
@ -324,7 +342,8 @@ class Hyperopt:
loss = self.calculate_loss(results=backtesting_results['results'],
trade_count=trade_count,
min_date=min_date, max_date=max_date,
config=self.config, processed=processed)
config=self.config, processed=processed,
backtest_stats=strat_stats)
return {
'loss': loss,
'params_dict': params_dict,
@ -469,6 +488,12 @@ class Hyperopt:
f"saved to '{self.results_file}'.")
if self.current_best_epoch:
if self.auto_hyperopt:
HyperoptTools.try_export_params(
self.config,
self.backtesting.strategy.get_strategy_name(),
self.current_best_epoch)
HyperoptTools.show_epoch_details(self.current_best_epoch, self.total_epochs,
self.print_json)
else:

View File

@ -5,7 +5,7 @@ This module defines the interface for the loss-function for hyperopt
from abc import ABC, abstractmethod
from datetime import datetime
from typing import Dict
from typing import Any, Dict
from pandas import DataFrame
@ -22,6 +22,7 @@ class IHyperOptLoss(ABC):
def hyperopt_loss_function(results: DataFrame, trade_count: int,
min_date: datetime, max_date: datetime,
config: Dict, processed: Dict[str, DataFrame],
backtest_stats: Dict[str, Any],
*args, **kwargs) -> float:
"""
Objective function, returns smaller number for better results

View File

@ -1,23 +1,82 @@
import io
import logging
from copy import deepcopy
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List
from typing import Any, Dict, List, Optional
import numpy as np
import rapidjson
import tabulate
from colorama import Fore, Style
from pandas import isna, json_normalize
from freqtrade.constants import FTHYPT_FILEVERSION, USERPATH_STRATEGIES
from freqtrade.exceptions import OperationalException
from freqtrade.misc import round_coin_value, round_dict
from freqtrade.misc import deep_merge_dicts, round_coin_value, round_dict, safe_value_fallback2
logger = logging.getLogger(__name__)
NON_OPT_PARAM_APPENDIX = " # value loaded from strategy"
def hyperopt_serializer(x):
if isinstance(x, np.integer):
return int(x)
if isinstance(x, np.bool_):
return bool(x)
return str(x)
class HyperoptTools():
@staticmethod
def get_strategy_filename(config: Dict, strategy_name: str) -> Optional[Path]:
"""
Get Strategy-location (filename) from strategy_name
"""
from freqtrade.resolvers.strategy_resolver import StrategyResolver
directory = Path(config.get('strategy_path', config['user_data_dir'] / USERPATH_STRATEGIES))
strategy_objs = StrategyResolver.search_all_objects(directory, False)
strategies = [s for s in strategy_objs if s['name'] == strategy_name]
if strategies:
strategy = strategies[0]
return Path(strategy['location'])
return None
@staticmethod
def export_params(params, strategy_name: str, filename: Path):
"""
Generate files
"""
final_params = deepcopy(params['params_not_optimized'])
final_params = deep_merge_dicts(params['params_details'], final_params)
final_params = {
'strategy_name': strategy_name,
'params': final_params,
'ft_stratparam_v': 1,
'export_time': datetime.now(timezone.utc),
}
logger.info(f"Dumping parameters to {filename}")
rapidjson.dump(final_params, filename.open('w'), indent=2,
default=hyperopt_serializer,
number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN
)
@staticmethod
def try_export_params(config: Dict[str, Any], strategy_name: str, params: Dict):
if params.get(FTHYPT_FILEVERSION, 1) >= 2 and not config.get('disableparamexport', False):
# Export parameters ...
fn = HyperoptTools.get_strategy_filename(config, strategy_name)
if fn:
HyperoptTools.export_params(params, strategy_name, fn.with_suffix('.json'))
else:
logger.warning("Strategy not found, not exporting parameter file.")
@staticmethod
def has_space(config: Dict[str, Any], space: str) -> bool:
"""
@ -99,9 +158,9 @@ class HyperoptTools():
non_optimized)
HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:",
non_optimized)
HyperoptTools._params_pretty_print(params, 'roi', "ROI table:")
HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:")
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:")
HyperoptTools._params_pretty_print(params, 'roi', "ROI table:", non_optimized)
HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:", non_optimized)
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:", non_optimized)
@staticmethod
def _params_update_for_json(result_dict, params, non_optimized, space: str) -> None:
@ -127,23 +186,34 @@ class HyperoptTools():
def _params_pretty_print(params, space: str, header: str, non_optimized={}) -> None:
if space in params or space in non_optimized:
space_params = HyperoptTools._space_params(params, space, 5)
no_params = HyperoptTools._space_params(non_optimized, space, 5)
appendix = ''
if not space_params and not no_params:
# No parameters - don't print
return
if not space_params:
# Not optimized parameters - append string
appendix = NON_OPT_PARAM_APPENDIX
result = f"\n# {header}\n"
if space == 'stoploss':
result += f"stoploss = {space_params.get('stoploss')}"
elif space == 'roi':
if space == "stoploss":
stoploss = safe_value_fallback2(space_params, no_params, space, space)
result += (f"stoploss = {stoploss}{appendix}")
elif space == "roi":
result = result[:-1] + f'{appendix}\n'
minimal_roi_result = rapidjson.dumps({
str(k): v for k, v in space_params.items()
str(k): v for k, v in (space_params or no_params).items()
}, default=str, indent=4, number_mode=rapidjson.NM_NATIVE)
result += f"minimal_roi = {minimal_roi_result}"
elif space == 'trailing':
for k, v in space_params.items():
result += f'{k} = {v}\n'
elif space == "trailing":
for k, v in (space_params or no_params).items():
result += f"{k} = {v}{appendix}\n"
else:
no_params = HyperoptTools._space_params(non_optimized, space, 5)
# Buy / sell parameters
result += f"{space}_params = {HyperoptTools._pprint(space_params, no_params)}"
result += f"{space}_params = {HyperoptTools._pprint_dict(space_params, no_params)}"
result = result.replace("\n", "\n ")
print(result)
@ -157,7 +227,7 @@ class HyperoptTools():
return {}
@staticmethod
def _pprint(params, non_optimized, indent: int = 4):
def _pprint_dict(params, non_optimized, indent: int = 4):
"""
Pretty-print hyperopt results (based on 2 dicts - with add. comment)
"""
@ -169,7 +239,7 @@ class HyperoptTools():
result += " " * indent + f'"{k}": '
result += f'"{param}",' if isinstance(param, str) else f'{param},'
if k in non_optimized:
result += " # value loaded from strategy"
result += NON_OPT_PARAM_APPENDIX
result += "\n"
result += '}'
return result

View File

@ -21,7 +21,7 @@ def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> N
Stores backtest results
:param recordfilename: Path object, which can either be a filename or a directory.
Filenames will be appended with a timestamp right before the suffix
while for diectories, <directory>/backtest-result-<datetime>.json will be used as filename
while for directories, <directory>/backtest-result-<datetime>.json will be used as filename
:param stats: Dataframe containing the backtesting statistics
"""
if recordfilename.is_dir():
@ -229,8 +229,6 @@ def generate_trading_stats(results: DataFrame) -> Dict[str, Any]:
winning_trades = results.loc[results['profit_ratio'] > 0]
draw_trades = results.loc[results['profit_ratio'] == 0]
losing_trades = results.loc[results['profit_ratio'] < 0]
zero_duration_trades = len(results.loc[(results['trade_duration'] == 0) &
(results['sell_reason'] == 'trailing_stop_loss')])
holding_avg = (timedelta(minutes=round(results['trade_duration'].mean()))
if not results.empty else timedelta())
@ -249,7 +247,6 @@ def generate_trading_stats(results: DataFrame) -> Dict[str, Any]:
'winner_holding_avg_s': winner_holding_avg.total_seconds(),
'loser_holding_avg': loser_holding_avg,
'loser_holding_avg_s': loser_holding_avg.total_seconds(),
'zero_duration_trades': zero_duration_trades,
}
@ -264,6 +261,7 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]:
'winning_days': 0,
'draw_days': 0,
'losing_days': 0,
'daily_profit_list': [],
}
daily_profit_rel = results.resample('1d', on='close_date')['profit_ratio'].sum()
daily_profit = results.resample('1d', on='close_date')['profit_abs'].sum().round(10)
@ -274,6 +272,7 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]:
winning_days = sum(daily_profit > 0)
draw_days = sum(daily_profit == 0)
losing_days = sum(daily_profit < 0)
daily_profit_list = [(str(idx.date()), val) for idx, val in daily_profit.iteritems()]
return {
'backtest_best_day': best_rel,
@ -283,6 +282,7 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]:
'winning_days': winning_days,
'draw_days': draw_days,
'losing_days': losing_days,
'daily_profit': daily_profit_list,
}
@ -300,7 +300,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
:param min_date: Backtest start date
:param max_date: Backtest end date
:param market_change: float indicating the market change
:return: Dictionary containing results per strategy and a stratgy summary.
:return: Dictionary containing results per strategy and a strategy summary.
"""
results: Dict[str, DataFrame] = content['results']
if not isinstance(results, DataFrame):
@ -325,8 +325,9 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'],
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
results['open_timestamp'] = results['open_date'].astype(int64) // 1e6
results['close_timestamp'] = results['close_date'].astype(int64) // 1e6
if not results.empty:
results['open_timestamp'] = results['open_date'].view(int64) // 1e6
results['close_timestamp'] = results['close_date'].view(int64) // 1e6
backtest_days = (max_date - min_date).days
strat_stats = {
@ -378,10 +379,10 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached', False),
'use_custom_stoploss': config.get('use_custom_stoploss', False),
'minimal_roi': config['minimal_roi'],
'use_sell_signal': config['ask_strategy']['use_sell_signal'],
'sell_profit_only': config['ask_strategy']['sell_profit_only'],
'sell_profit_offset': config['ask_strategy']['sell_profit_offset'],
'ignore_roi_if_buy_signal': config['ask_strategy']['ignore_roi_if_buy_signal'],
'use_sell_signal': config['use_sell_signal'],
'sell_profit_only': config['sell_profit_only'],
'sell_profit_offset': config['sell_profit_offset'],
'ignore_roi_if_buy_signal': config['ignore_roi_if_buy_signal'],
**daily_stats,
**trade_stats
}
@ -436,7 +437,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
{ Strategy: {'results: results, 'config: config}}.
:param min_date: Backtest start date
:param max_date: Backtest end date
:return: Dictionary containing results per strategy and a stratgy summary.
:return: Dictionary containing results per strategy and a strategy summary.
"""
result: Dict[str, Any] = {'strategy': {}}
market_change = calculate_market_change(btdata, 'close')
@ -507,9 +508,8 @@ def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren
def text_table_strategy(strategy_results, stake_currency: str) -> str:
"""
Generate summary table per strategy
:param strategy_results: Dict of <Strategyname: DataFrame> containing results for all strategies
:param stake_currency: stake-currency - used to correctly name headers
:param max_open_trades: Maximum allowed open trades used for backtest
:param all_results: Dict of <Strategyname: DataFrame> containing results for all strategies
:return: pretty printed table with tabulate as string
"""
floatfmt = _get_line_floatfmt(stake_currency)
@ -543,14 +543,6 @@ def text_table_add_metrics(strat_results: Dict) -> str:
# Newly added fields should be ignored if they are missing in strat_results. hyperopt-show
# command stores these results and newer version of freqtrade must be able to handle old
# results with missing new fields.
zero_duration_trades = '--'
if 'zero_duration_trades' in strat_results:
zero_duration_trades_per = \
100.0 / strat_results['total_trades'] * strat_results['zero_duration_trades']
zero_duration_trades = f'{zero_duration_trades_per:.2f}% ' \
f'({strat_results["zero_duration_trades"]})'
metrics = [
('Backtesting from', strat_results['backtest_start']),
('Backtesting to', strat_results['backtest_end']),
@ -586,7 +578,6 @@ def text_table_add_metrics(strat_results: Dict) -> str:
f"{strat_results['draw_days']} / {strat_results['losing_days']}"),
('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"),
('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"),
('Zero Duration Trades', zero_duration_trades),
('Rejected Buy signals', strat_results.get('rejected_signals', 'N/A')),
('', ''), # Empty line to improve readability
@ -664,6 +655,8 @@ def show_backtest_results(config: Dict, backtest_stats: Dict):
# Print Strategy summary table
table = text_table_strategy(backtest_stats['strategy_comparison'], stake_currency)
print(f"{results['backtest_start']} -> {results['backtest_end']} |"
f" Max open trades : {results['max_open_trades']}")
print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '='))
print(table)
print('=' * len(table.splitlines()[0]))

View File

@ -48,13 +48,11 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
sell_reason = get_column_def(cols, 'sell_reason', 'null')
strategy = get_column_def(cols, 'strategy', 'null')
leverage = get_column_def(cols, 'leverage', '0.0')
borrowed = get_column_def(cols, 'borrowed', '0.0')
borrowed_currency = get_column_def(cols, 'borrowed_currency', 'null')
collateral_currency = get_column_def(cols, 'collateral_currency', 'null')
leverage = get_column_def(cols, 'leverage', '1.0')
interest_rate = get_column_def(cols, 'interest_rate', '0.0')
liquidation_price = get_column_def(cols, 'liquidation_price', 'null')
is_short = get_column_def(cols, 'is_short', 'False')
interest_mode = get_column_def(cols, 'interest_mode', 'null')
# If ticker-interval existed use that, else null.
if has_column(cols, 'ticker_interval'):
timeframe = get_column_def(cols, 'timeframe', 'ticker_interval')
@ -91,7 +89,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
stoploss_order_id, stoploss_last_update,
max_rate, min_rate, sell_reason, sell_order_status, strategy,
timeframe, open_trade_value, close_profit_abs,
leverage, borrowed, borrowed_currency, collateral_currency, interest_rate, liquidation_price, is_short
leverage, interest_rate, liquidation_price, is_short, interest_mode
)
select id, lower(exchange),
case
@ -115,14 +113,12 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
{sell_order_status} sell_order_status,
{strategy} strategy, {timeframe} timeframe,
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs,
{leverage} leverage, {borrowed} borrowed, {borrowed_currency} borrowed_currency,
{collateral_currency} collateral_currency, {interest_rate} interest_rate,
{liquidation_price} liquidation_price, {is_short} is_short
{leverage} leverage, {interest_rate} interest_rate,
{liquidation_price} liquidation_price, {is_short} is_short,
{interest_mode} interest_mode
from {table_back_name}
"""))
# TODO: Does leverage go in here?
def migrate_open_orders_to_trades(engine):
with engine.begin() as connection:
@ -152,14 +148,18 @@ def migrate_orders_table(decl_base, inspector, engine, table_back_name: str, col
# let SQLAlchemy create the schema as required
decl_base.metadata.create_all(engine)
leverage = get_column_def(cols, 'leverage', '1.0')
is_short = get_column_def(cols, 'is_short', 'False')
# TODO-mg: Should liquidation price go in here?
with engine.begin() as connection:
connection.execute(text(f"""
insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
status, symbol, order_type, side, price, amount, filled, average, remaining, cost,
order_date, order_filled_date, order_update_date, leverage)
order_date, order_filled_date, order_update_date, leverage, is_short)
select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
status, symbol, order_type, side, price, amount, filled, null average, remaining, cost,
order_date, order_filled_date, order_update_date, leverage
order_date, order_filled_date, order_update_date,
{leverage} leverage, {is_short} is_short
from {table_back_name}
"""))
@ -188,9 +188,11 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
else:
cols_order = inspector.get_columns('orders')
if not has_column(cols_order, 'average'):
# Last added column of order table
# To determine if migrations need to run
if not has_column(cols_order, 'leverage'):
tabs = get_table_names_for_table(inspector, 'orders')
# Empty for now - as there is only one iteration of the orders table so far.
table_back_name = get_backup_name(tabs, 'orders_bak')
migrate_orders_table(decl_base, inspector, engine, table_back_name, cols)
migrate_orders_table(decl_base, inspector, engine, table_back_name, cols_order)

View File

@ -6,7 +6,7 @@ from datetime import datetime, timezone
from decimal import Decimal
from typing import Any, Dict, List, Optional
from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, String,
from sqlalchemy import (Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer, String,
create_engine, desc, func, inspect)
from sqlalchemy.exc import NoSuchModuleError
from sqlalchemy.orm import Query, declarative_base, relationship, scoped_session, sessionmaker
@ -14,7 +14,7 @@ from sqlalchemy.pool import StaticPool
from sqlalchemy.sql.schema import UniqueConstraint
from freqtrade.constants import DATETIME_PRINT_FORMAT
from freqtrade.enums import SellType
from freqtrade.enums import InterestMode, SellType
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.misc import safe_value_fallback
from freqtrade.persistence.migrations import check_migrate
@ -133,8 +133,10 @@ class Order(_DECL_BASE):
order_update_date = Column(DateTime, nullable=True)
leverage = Column(Float, nullable=True, default=1.0)
is_short = Column(Boolean, nullable=True, default=False)
def __repr__(self):
return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, '
f'side={self.side}, order_type={self.order_type}, status={self.status})')
@ -157,6 +159,7 @@ class Order(_DECL_BASE):
self.remaining = order.get('remaining', self.remaining)
self.cost = order.get('cost', self.cost)
self.leverage = order.get('leverage', self.leverage)
# TODO-mg: is_short?
if 'timestamp' in order and order['timestamp'] is not None:
self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc)
@ -228,7 +231,6 @@ class LocalTrade():
fee_close_currency: str = ''
open_rate: float = 0.0
open_rate_requested: Optional[float] = None
# open_trade_value - calculated via _calc_open_trade_value
open_trade_value: float = 0.0
close_rate: Optional[float] = None
@ -263,39 +265,30 @@ class LocalTrade():
timeframe: Optional[int] = None
# Margin trading properties
leverage: Optional[float] = 1.0
borrowed: float = 0.0
borrowed_currency: str = None
collateral_currency: str = None
interest_rate: float = 0.0
liquidation_price: float = None
liquidation_price: Optional[float] = None
is_short: bool = False
# End of margin trading properties
leverage: float = 1.0
interest_mode: InterestMode = InterestMode.NONE
def __init__(self, **kwargs):
lev = kwargs.get('leverage')
bor = kwargs.get('borrowed')
amount = kwargs.get('amount')
if lev and bor:
# TODO: should I raise an error?
raise OperationalException('Cannot pass both borrowed and leverage to Trade')
elif lev:
self.amount = amount * lev
self.borrowed = amount * (lev-1)
elif bor:
self.lev = (bor + amount)/amount
@property
def has_no_leverage(self) -> bool:
"""Returns true if this is a non-leverage, non-short trade"""
return (self.leverage == 1.0 and not self.is_short) or self.leverage is None
for key in kwargs:
setattr(self, key, kwargs[key])
if not self.is_short:
self.is_short = False
self.recalc_open_trade_value()
def __repr__(self):
open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed'
return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
f'open_rate={self.open_rate:.8f}, open_since={open_since})')
@property
def borrowed(self) -> float:
"""
The amount of currency borrowed from the exchange for leverage trades
If a long trade, the amount is in base currency
If a short trade, the amount is in the other currency being traded
"""
if self.has_no_leverage:
return 0.0
elif not self.is_short:
return self.stake_amount * (self.leverage-1)
else:
return self.amount
@property
def open_date_utc(self):
@ -305,6 +298,54 @@ class LocalTrade():
def close_date_utc(self):
return self.close_date.replace(tzinfo=timezone.utc)
def __init__(self, **kwargs):
for key in kwargs:
setattr(self, key, kwargs[key])
self.set_liquidation_price(self.liquidation_price)
self.recalc_open_trade_value()
def set_stop_loss_helper(self, stop_loss: Optional[float], liquidation_price: Optional[float]):
"""Helper function for set_liquidation_price and set_stop_loss"""
# Stoploss would be better as a computed variable,
# but that messes up the database so it might not be possible
if liquidation_price is not None:
if stop_loss is not None:
if self.is_short:
self.stop_loss = min(stop_loss, liquidation_price)
else:
self.stop_loss = max(stop_loss, liquidation_price)
else:
self.stop_loss = liquidation_price
self.initial_stop_loss = liquidation_price
self.liquidation_price = liquidation_price
else:
# programmming error check: 1 of liqudication_price or stop_loss must be set
assert stop_loss is not None
if not self.stop_loss:
self.initial_stop_loss = stop_loss
self.stop_loss = stop_loss
def set_stop_loss(self, stop_loss: float):
"""
Method you should use to set self.stop_loss.
Assures stop_loss is not passed the liquidation price
"""
self.set_stop_loss_helper(stop_loss=stop_loss, liquidation_price=self.liquidation_price)
def set_liquidation_price(self, liquidation_price: float):
"""
Method you should use to set self.liquidation price.
Assures stop_loss is not passed the liquidation price
"""
self.set_stop_loss_helper(stop_loss=self.stop_loss, liquidation_price=liquidation_price)
def __repr__(self):
open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed'
return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
f'open_rate={self.open_rate:.8f}, open_since={open_since})')
def to_json(self) -> Dict[str, Any]:
return {
'trade_id': self.id,
@ -368,9 +409,6 @@ class LocalTrade():
'max_rate': self.max_rate,
'leverage': self.leverage,
'borrowed': self.borrowed,
'borrowed_currency': self.borrowed_currency,
'collateral_currency': self.collateral_currency,
'interest_rate': self.interest_rate,
'liquidation_price': self.liquidation_price,
'is_short': self.is_short,
@ -396,7 +434,10 @@ class LocalTrade():
def _set_new_stoploss(self, new_loss: float, stoploss: float):
"""Assign new stop value"""
self.stop_loss = new_loss
self.set_stop_loss(new_loss)
if self.is_short:
self.stop_loss_pct = abs(stoploss)
else:
self.stop_loss_pct = -1 * abs(stoploss)
self.stoploss_last_update = datetime.utcnow()
@ -413,22 +454,37 @@ class LocalTrade():
# Don't modify if called with initial and nothing to do
return
new_loss = float(current_price * (1 - abs(stoploss)))
# TODO: Could maybe move this if into the new stoploss if branch
if (self.liquidation_price): # If trading on margin, don't set the stoploss below the liquidation price
if self.is_short:
new_loss = float(current_price * (1 + abs(stoploss)))
# If trading on margin, don't set the stoploss below the liquidation price
if self.liquidation_price:
new_loss = min(self.liquidation_price, new_loss)
else:
new_loss = float(current_price * (1 - abs(stoploss)))
# If trading on margin, don't set the stoploss below the liquidation price
if self.liquidation_price:
new_loss = max(self.liquidation_price, new_loss)
# no stop loss assigned yet
if not self.stop_loss:
logger.debug(f"{self.pair} - Assigning new stoploss...")
self._set_new_stoploss(new_loss, stoploss)
self.initial_stop_loss = new_loss
if self.is_short:
self.initial_stop_loss_pct = abs(stoploss)
else:
self.initial_stop_loss_pct = -1 * abs(stoploss)
# evaluate if the stop loss needs to be updated
else:
# stop losses only walk up, never down!, #TODO: But adding more to a margin account would create a lower liquidation price, decreasing the minimum stoploss
if (new_loss > self.stop_loss and not self.is_short) or (new_loss < self.stop_loss and self.is_short):
higher_stop = new_loss > self.stop_loss
lower_stop = new_loss < self.stop_loss
# stop losses only walk up, never down!,
# ? But adding more to a margin account would create a lower liquidation price,
# ? decreasing the minimum stoploss
if (higher_stop and not self.is_short) or (lower_stop and self.is_short):
logger.debug(f"{self.pair} - Adjusting stoploss...")
self._set_new_stoploss(new_loss, stoploss)
else:
@ -463,6 +519,11 @@ class LocalTrade():
:return: None
"""
order_type = order['type']
if 'is_short' in order and order['side'] == 'sell':
# Only set's is_short on opening trades, ignores non-shorts
self.is_short = order['is_short']
# Ignore open and cancelled orders
if order['status'] == 'open' or safe_value_fallback(order, 'average', 'price') is None:
return
@ -473,6 +534,8 @@ class LocalTrade():
# Update open rate and actual amount
self.open_rate = float(safe_value_fallback(order, 'average', 'price'))
self.amount = float(safe_value_fallback(order, 'filled', 'amount'))
if 'leverage' in order:
self.leverage = order['leverage']
self.recalc_open_trade_value()
if self.is_open:
payment = "SELL" if self.is_short else "BUY"
@ -481,8 +544,10 @@ class LocalTrade():
elif order_type in ('market', 'limit') and self.is_closing_trade(order['side']):
if self.is_open:
payment = "BUY" if self.is_short else "SELL"
# TODO-mg: On shorts, you buy a little bit more than the amount (amount + interest)
# This wll only print the original amount
logger.info(f'{order_type.upper()}_{payment} has been fulfilled for {self}.')
self.close(safe_value_fallback(order, 'average', 'price')) # TODO: Double check this
self.close(safe_value_fallback(order, 'average', 'price')) # TODO-mg: Double check this
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
@ -551,7 +616,7 @@ class LocalTrade():
"""
open_trade = Decimal(self.amount) * Decimal(self.open_rate)
fees = open_trade * Decimal(self.fee_open)
if (self.is_short):
if self.is_short:
return float(open_trade - fees)
else:
return float(open_trade + fees)
@ -563,77 +628,77 @@ class LocalTrade():
"""
self.open_trade_value = self._calc_open_trade_value()
def calculate_interest(self) -> Decimal:
# TODO-mg: Need to set other conditions because sometimes self.open_date is not defined, but why would it ever not be set
if not self.interest_rate or not (self.borrowed):
return Decimal(0.0)
def calculate_interest(self, interest_rate: Optional[float] = None) -> Decimal:
"""
: param interest_rate: interest_charge for borrowing this coin(optional).
If interest_rate is not set self.interest_rate will be used
"""
zero = Decimal(0.0)
# If nothing was borrowed
if self.has_no_leverage:
return zero
try:
open_date = self.open_date.replace(tzinfo=None)
now = datetime.now()
secPerDay = 86400
days = Decimal((now - open_date).total_seconds()/secPerDay) or 0.0
hours = days/24
except:
raise OperationalException("Time isn't calculated properly")
now = (self.close_date or datetime.now(timezone.utc)).replace(tzinfo=None)
sec_per_hour = Decimal(3600)
total_seconds = Decimal((now - open_date).total_seconds())
hours = total_seconds/sec_per_hour or zero
rate = Decimal(self.interest_rate)
rate = Decimal(interest_rate or self.interest_rate)
borrowed = Decimal(self.borrowed)
if self.exchange == 'binance':
# Rate is per day but accrued hourly or something
# binance: https://www.binance.com/en-AU/support/faq/360030157812
return borrowed * (rate/24) * max(hours, 1.0) # TODO-mg: Is hours rounded?
elif self.exchange == 'kraken':
# https://support.kraken.com/hc/en-us/articles/206161568-What-are-the-fees-for-margin-trading-
opening_fee = borrowed * rate
roll_over_fee = borrowed * rate * max(0, (hours-4)/4)
return opening_fee + roll_over_fee
elif self.exchange == 'binance_usdm_futures':
# ! TODO-mg: This is incorrect, I didn't look it up
return borrowed * (rate/24) * max(hours, 1.0)
elif self.exchange == 'binance_coinm_futures':
# ! TODO-mg: This is incorrect, I didn't look it up
return borrowed * (rate/24) * max(hours, 1.0)
else:
# TODO-mg: make sure this breaks and can't be squelched
raise OperationalException("Leverage not available on this exchange")
return self.interest_mode(borrowed=borrowed, rate=rate, hours=hours)
def calc_close_trade_value(self, rate: Optional[float] = None,
fee: Optional[float] = None) -> float:
fee: Optional[float] = None,
interest_rate: Optional[float] = None) -> float:
"""
Calculate the close_rate including fee
:param fee: fee to use on the close rate (optional).
If rate is not set self.fee will be used
:param rate: rate to compare with (optional).
If rate is not set self.close_rate will be used
:param interest_rate: interest_charge for borrowing this coin (optional).
If interest_rate is not set self.interest_rate will be used
:return: Price in BTC of the open trade
"""
if rate is None and not self.close_rate:
return 0.0
close_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) # type: ignore
fees = close_trade * Decimal(fee or self.fee_close)
interest = self.calculate_interest()
interest = self.calculate_interest(interest_rate)
if self.is_short:
amount = Decimal(self.amount) + Decimal(interest)
else:
# Currency already owned for longs, no need to purchase
amount = Decimal(self.amount)
if (self.is_short):
return float(close_trade + fees + interest)
close_trade = Decimal(amount) * Decimal(rate or self.close_rate) # type: ignore
fees = close_trade * Decimal(fee or self.fee_close)
if self.is_short:
return float(close_trade + fees)
else:
return float(close_trade - fees - interest)
def calc_profit(self, rate: Optional[float] = None,
fee: Optional[float] = None) -> float:
fee: Optional[float] = None,
interest_rate: Optional[float] = None) -> float:
"""
Calculate the absolute profit in stake currency between Close and Open trade
:param fee: fee to use on the close rate (optional).
If fee is not set self.fee will be used
:param rate: close rate to compare with (optional).
If rate is not set self.close_rate will be used
:param interest_rate: interest_charge for borrowing this coin (optional).
If interest_rate is not set self.interest_rate will be used
:return: profit in stake currency as float
"""
close_trade_value = self.calc_close_trade_value(
rate=(rate or self.close_rate),
fee=(fee or self.fee_close)
fee=(fee or self.fee_close),
interest_rate=(interest_rate or self.interest_rate)
)
if self.is_short:
@ -643,29 +708,36 @@ class LocalTrade():
return float(f"{profit:.8f}")
def calc_profit_ratio(self, rate: Optional[float] = None,
fee: Optional[float] = None) -> float:
fee: Optional[float] = None,
interest_rate: Optional[float] = None) -> float:
"""
Calculates the profit as ratio (including fee).
:param rate: rate to compare with (optional).
If rate is not set self.close_rate will be used
:param fee: fee to use on the close rate (optional).
:param interest_rate: interest_charge for borrowing this coin (optional).
If interest_rate is not set self.interest_rate will be used
:return: profit ratio as float
"""
close_trade_value = self.calc_close_trade_value(
rate=(rate or self.close_rate),
fee=(fee or self.fee_close)
fee=(fee or self.fee_close),
interest_rate=(interest_rate or self.interest_rate)
)
if self.is_short:
if close_trade_value == 0.0:
return 0.0
else:
profit_ratio = (self.open_trade_value / close_trade_value) - 1
else:
if self.open_trade_value == 0.0:
short_close_zero = (self.is_short and close_trade_value == 0.0)
long_close_zero = (not self.is_short and self.open_trade_value == 0.0)
leverage = self.leverage or 1.0
if (short_close_zero or long_close_zero):
return 0.0
else:
profit_ratio = (close_trade_value / self.open_trade_value) - 1
if self.is_short:
profit_ratio = (1 - (close_trade_value/self.open_trade_value)) * leverage
else:
profit_ratio = ((close_trade_value/self.open_trade_value) - 1) * leverage
return float(f"{profit_ratio:.8f}")
def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]:
@ -683,7 +755,7 @@ class LocalTrade():
else:
return None
@ staticmethod
@staticmethod
def get_trades_proxy(*, pair: str = None, is_open: bool = None,
open_date: datetime = None, close_date: datetime = None,
) -> List['LocalTrade']:
@ -717,27 +789,27 @@ class LocalTrade():
return sel_trades
@ staticmethod
@staticmethod
def close_bt_trade(trade):
LocalTrade.trades_open.remove(trade)
LocalTrade.trades.append(trade)
LocalTrade.total_profit += trade.close_profit_abs
@ staticmethod
@staticmethod
def add_bt_trade(trade):
if trade.is_open:
LocalTrade.trades_open.append(trade)
else:
LocalTrade.trades.append(trade)
@ staticmethod
@staticmethod
def get_open_trades() -> List[Any]:
"""
Query trades from persistence layer
"""
return Trade.get_trades_proxy(is_open=True)
@ staticmethod
@staticmethod
def stoploss_reinitialization(desired_stoploss):
"""
Adjust initial Stoploss to desired stoploss for all open trades.
@ -811,19 +883,17 @@ class Trade(_DECL_BASE, LocalTrade):
max_rate = Column(Float, nullable=True, default=0.0)
# Lowest price reached
min_rate = Column(Float, nullable=True)
sell_reason = Column(String(100), nullable=True) # TODO: Change to close_reason
sell_order_status = Column(String(100), nullable=True)
sell_reason = Column(String(100), nullable=True) # TODO-mg: Change to close_reason
sell_order_status = Column(String(100), nullable=True) # TODO-mg: Change to close_order_status
strategy = Column(String(100), nullable=True)
timeframe = Column(Integer, nullable=True)
# Margin trading properties
leverage = Column(Float, nullable=True, default=1.0)
borrowed = Column(Float, nullable=False, default=0.0)
borrowed_currency = Column(Float, nullable=True)
collateral_currency = Column(String(25), nullable=True)
interest_rate = Column(Float, nullable=False, default=0.0)
liquidation_price = Column(Float, nullable=True)
is_short = Column(Boolean, nullable=False, default=False)
interest_mode = Column(Enum(InterestMode), nullable=True)
# End of margin trading properties
def __init__(self, **kwargs):
@ -838,11 +908,11 @@ class Trade(_DECL_BASE, LocalTrade):
Trade.query.session.delete(self)
Trade.commit()
@ staticmethod
@staticmethod
def commit():
Trade.query.session.commit()
@ staticmethod
@staticmethod
def get_trades_proxy(*, pair: str = None, is_open: bool = None,
open_date: datetime = None, close_date: datetime = None,
) -> List['LocalTrade']:
@ -872,7 +942,7 @@ class Trade(_DECL_BASE, LocalTrade):
close_date=close_date
)
@ staticmethod
@staticmethod
def get_trades(trade_filter=None) -> Query:
"""
Helper function to query Trades using filters.
@ -892,7 +962,7 @@ class Trade(_DECL_BASE, LocalTrade):
else:
return Trade.query
@ staticmethod
@staticmethod
def get_open_order_trades():
"""
Returns all open trades
@ -900,7 +970,7 @@ class Trade(_DECL_BASE, LocalTrade):
"""
return Trade.get_trades(Trade.open_order_id.isnot(None)).all()
@ staticmethod
@staticmethod
def get_open_trades_without_assigned_fees():
"""
Returns all open trades which don't have open fees set correctly
@ -911,7 +981,7 @@ class Trade(_DECL_BASE, LocalTrade):
Trade.is_open.is_(True),
]).all()
@ staticmethod
@staticmethod
def get_closed_trades_without_assigned_fees():
"""
Returns all closed trades which don't have fees set correctly
@ -922,7 +992,7 @@ class Trade(_DECL_BASE, LocalTrade):
Trade.is_open.is_(False),
]).all()
@ staticmethod
@staticmethod
def total_open_trades_stakes() -> float:
"""
Calculates total invested amount in open trades
@ -936,7 +1006,7 @@ class Trade(_DECL_BASE, LocalTrade):
t.stake_amount for t in LocalTrade.get_trades_proxy(is_open=True))
return total_open_stake_amount or 0
@ staticmethod
@staticmethod
def get_overall_performance() -> List[Dict[str, Any]]:
"""
Returns List of dicts containing all Trades, including profit and trade count
@ -961,8 +1031,8 @@ class Trade(_DECL_BASE, LocalTrade):
for pair, profit, profit_abs, count in pair_rates
]
@ staticmethod
def get_best_pair():
@staticmethod
def get_best_pair(start_date: datetime = datetime.fromtimestamp(0)):
"""
Get best pair with closed trade.
NOTE: Not supported in Backtesting.
@ -970,7 +1040,7 @@ class Trade(_DECL_BASE, LocalTrade):
"""
best_pair = Trade.query.with_entities(
Trade.pair, func.sum(Trade.close_profit).label('profit_sum')
).filter(Trade.is_open.is_(False)) \
).filter(Trade.is_open.is_(False) & (Trade.close_date >= start_date)) \
.group_by(Trade.pair) \
.order_by(desc('profit_sum')).first()
return best_pair
@ -999,7 +1069,7 @@ class PairLock(_DECL_BASE):
return (f'PairLock(id={self.id}, pair={self.pair}, lock_time={lock_time}, '
f'lock_end_time={lock_end_time})')
@ staticmethod
@staticmethod
def query_pair_locks(pair: Optional[str], now: datetime) -> Query:
"""
Get all currently active locks for this pair

View File

@ -288,8 +288,8 @@ def plot_area(fig, row: int, data: pd.DataFrame, indicator_a: str,
:param fig: Plot figure to append to
:param row: row number for this plot
:param data: candlestick DataFrame
:param indicator_a: indicator name as populated in stragetie
:param indicator_b: indicator name as populated in stragetie
:param indicator_a: indicator name as populated in strategy
:param indicator_b: indicator name as populated in strategy
:param label: label for the filled area
:param fill_color: color to be used for the filled area
:return: fig with added filled_traces plot
@ -373,6 +373,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra
for i, name in enumerate(plot_config['subplots']):
fig['layout'][f'yaxis{3 + i}'].update(title=name)
fig['layout']['xaxis']['rangeslider'].update(visible=False)
fig.update_layout(modebar_add=["v1hovermode", "toggleSpikeLines"])
# Common information
candles = go.Candlestick(
@ -452,6 +453,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra
data=data)
# fill area between indicators ( 'fill_to': 'other_indicator')
fig = add_areas(fig, row, data, sub_config)
return fig
@ -484,6 +486,7 @@ def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame],
fig['layout']['yaxis2'].update(title=f'Profit {stake_currency}')
fig['layout']['yaxis3'].update(title=f'Profit {stake_currency}')
fig['layout']['xaxis']['rangeslider'].update(visible=False)
fig.update_layout(modebar_add=["v1hovermode", "toggleSpikeLines"])
fig.add_trace(avgclose, 1, 1)
fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit')
@ -497,7 +500,6 @@ def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame],
fig = add_profit(fig, 3, df_comb, profit_col, f"Profit {pair}")
except ValueError:
pass
return fig

View File

@ -27,6 +27,7 @@ class AgeFilter(IPairList):
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._min_days_listed = pairlistconfig.get('min_days_listed', 10)
self._max_days_listed = pairlistconfig.get('max_days_listed', None)
if self._min_days_listed < 1:
raise OperationalException("AgeFilter requires min_days_listed to be >= 1")
@ -34,6 +35,12 @@ class AgeFilter(IPairList):
raise OperationalException("AgeFilter requires min_days_listed to not exceed "
"exchange max request size "
f"({exchange.ohlcv_candle_limit('1d')})")
if self._max_days_listed and self._max_days_listed <= self._min_days_listed:
raise OperationalException("AgeFilter max_days_listed <= min_days_listed not permitted")
if self._max_days_listed and self._max_days_listed > exchange.ohlcv_candle_limit('1d'):
raise OperationalException("AgeFilter requires max_days_listed to not exceed "
"exchange max request size "
f"({exchange.ohlcv_candle_limit('1d')})")
@property
def needstickers(self) -> bool:
@ -48,8 +55,13 @@ class AgeFilter(IPairList):
"""
Short whitelist method description - used for startup-messages
"""
return (f"{self.name} - Filtering pairs with age less than "
f"{self._min_days_listed} {plural(self._min_days_listed, 'day')}.")
return (
f"{self.name} - Filtering pairs with age less than "
f"{self._min_days_listed} {plural(self._min_days_listed, 'day')}"
) + ((
" or more than "
f"{self._max_days_listed} {plural(self._max_days_listed, 'day')}"
) if self._max_days_listed else '')
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
"""
@ -61,9 +73,12 @@ class AgeFilter(IPairList):
if not needed_pairs:
return pairlist
since_days = -(
self._max_days_listed if self._max_days_listed else self._min_days_listed
) - 1
since_ms = int(arrow.utcnow()
.floor('day')
.shift(days=-self._min_days_listed - 1)
.shift(days=since_days)
.float_timestamp) * 1000
candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms, cache=False)
if self._enabled:
@ -86,14 +101,22 @@ class AgeFilter(IPairList):
return True
if daily_candles is not None:
if len(daily_candles) >= self._min_days_listed:
if (
len(daily_candles) >= self._min_days_listed
and (not self._max_days_listed or len(daily_candles) <= self._max_days_listed)
):
# We have fetched at least the minimum required number of daily candles
# Add to cache, store the time we last checked this symbol
self._symbolsChecked[pair] = int(arrow.utcnow().float_timestamp) * 1000
self._symbolsChecked[pair] = arrow.utcnow().int_timestamp * 1000
return True
else:
self.log_once(f"Removed {pair} from whitelist, because age "
self.log_once((
f"Removed {pair} from whitelist, because age "
f"{len(daily_candles)} is less than {self._min_days_listed} "
f"{plural(self._min_days_listed, 'day')}", logger.info)
f"{plural(self._min_days_listed, 'day')}"
) + ((
" or more than "
f"{self._max_days_listed} {plural(self._max_days_listed, 'day')}"
) if self._max_days_listed else ''), logger.info)
return False
return False

View File

@ -0,0 +1,54 @@
"""
Offset pair list filter
"""
import logging
from typing import Any, Dict, List
from freqtrade.exceptions import OperationalException
from freqtrade.plugins.pairlist.IPairList import IPairList
logger = logging.getLogger(__name__)
class OffsetFilter(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._offset = pairlistconfig.get('offset', 0)
if self._offset < 0:
raise OperationalException("OffsetFilter requires offset to be >= 0")
@property
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
If no Pairlist requires tickers, an empty Dict is passed
as tickers argument to filter_pairlist
"""
return False
def short_desc(self) -> str:
"""
Short whitelist method description - used for startup-messages
"""
return f"{self.name} - Offseting pairs by {self._offset}."
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
"""
Filters and sorts pairlist and returns the whitelist again.
Called on each bot iteration - please use internal caching if necessary
:param pairlist: pairlist to filter or sort
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
:return: new whitelist
"""
if self._offset > len(pairlist):
self.log_once(f"Offset of {self._offset} is larger than " +
f"pair count of {len(pairlist)}", logger.warning)
pairs = pairlist[self._offset:]
self.log_once(f"Searching {len(pairs)} pairs: {pairs}", logger.info)
return pairs

View File

@ -19,7 +19,7 @@ class PerformanceFilter(IPairList):
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
If no Pairlist requries tickers, an empty List is passed
If no Pairlist requires tickers, an empty List is passed
as tickers argument to filter_pairlist
"""
return False

View File

@ -20,9 +20,9 @@ logger = logging.getLogger(__name__)
class VolatilityFilter(IPairList):
'''
"""
Filters pairs by volatility
'''
"""
def __init__(self, exchange, pairlistmanager,
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
@ -69,10 +69,10 @@ class VolatilityFilter(IPairList):
"""
needed_pairs = [(p, '1d') for p in pairlist if p not in self._pair_cache]
since_ms = int(arrow.utcnow()
since_ms = (arrow.utcnow()
.floor('day')
.shift(days=-self._days - 1)
.float_timestamp) * 1000
.int_timestamp) * 1000
# Get all candles
candles = {}
if needed_pairs:

View File

@ -6,9 +6,12 @@ Provides dynamic pair list based on trade volumes
import logging
from typing import Any, Dict, List
import arrow
from cachetools.ttl import TTLCache
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes
from freqtrade.misc import format_ms_time
from freqtrade.plugins.pairlist.IPairList import IPairList
@ -36,6 +39,35 @@ class VolumePairList(IPairList):
self._min_value = self._pairlistconfig.get('min_value', 0)
self._refresh_period = self._pairlistconfig.get('refresh_period', 1800)
self._pair_cache: TTLCache = TTLCache(maxsize=1, ttl=self._refresh_period)
self._lookback_days = self._pairlistconfig.get('lookback_days', 0)
self._lookback_timeframe = self._pairlistconfig.get('lookback_timeframe', '1d')
self._lookback_period = self._pairlistconfig.get('lookback_period', 0)
if (self._lookback_days > 0) & (self._lookback_period > 0):
raise OperationalException(
'Ambigous configuration: lookback_days and lookback_period both set in pairlist '
'config. Please set lookback_days only or lookback_period and lookback_timeframe '
'and restart the bot.'
)
# overwrite lookback timeframe and days when lookback_days is set
if self._lookback_days > 0:
self._lookback_timeframe = '1d'
self._lookback_period = self._lookback_days
# get timeframe in minutes and seconds
self._tf_in_min = timeframe_to_minutes(self._lookback_timeframe)
self._tf_in_sec = self._tf_in_min * 60
# wether to use range lookback or not
self._use_range = (self._tf_in_min > 0) & (self._lookback_period > 0)
if self._use_range & (self._refresh_period < self._tf_in_sec):
raise OperationalException(
f'Refresh period of {self._refresh_period} seconds is smaller than one '
f'timeframe of {self._lookback_timeframe}. Please adjust refresh_period '
f'to at least {self._tf_in_sec} and restart the bot.'
)
if not self._exchange.exchange_has('fetchTickers'):
raise OperationalException(
@ -47,6 +79,13 @@ class VolumePairList(IPairList):
raise OperationalException(
f'key {self._sort_key} not in {SORT_VALUES}')
if self._lookback_period < 0:
raise OperationalException("VolumeFilter requires lookback_period to be >= 0")
if self._lookback_period > exchange.ohlcv_candle_limit(self._lookback_timeframe):
raise OperationalException("VolumeFilter requires lookback_period to not "
"exceed exchange max request size "
f"({exchange.ohlcv_candle_limit(self._lookback_timeframe)})")
@property
def needstickers(self) -> bool:
"""
@ -78,7 +117,6 @@ class VolumePairList(IPairList):
# Item found - no refresh necessary
return pairlist
else:
# Use fresh pairlist
# Check if pair quote currency equals to the stake currency.
filtered_tickers = [
@ -103,6 +141,60 @@ class VolumePairList(IPairList):
# Use the incoming pairlist.
filtered_tickers = [v for k, v in tickers.items() if k in pairlist]
# get lookback period in ms, for exchange ohlcv fetch
if self._use_range:
since_ms = int(arrow.utcnow()
.floor('minute')
.shift(minutes=-(self._lookback_period * self._tf_in_min)
- self._tf_in_min)
.int_timestamp) * 1000
to_ms = int(arrow.utcnow()
.floor('minute')
.shift(minutes=-self._tf_in_min)
.int_timestamp) * 1000
# todo: utc date output for starting date
self.log_once(f"Using volume range of {self._lookback_period} candles, timeframe: "
f"{self._lookback_timeframe}, starting from {format_ms_time(since_ms)} "
f"till {format_ms_time(to_ms)}", logger.info)
needed_pairs = [
(p, self._lookback_timeframe) for p in
[
s['symbol'] for s in filtered_tickers
] if p not in self._pair_cache
]
# Get all candles
candles = {}
if needed_pairs:
candles = self._exchange.refresh_latest_ohlcv(
needed_pairs, since_ms=since_ms, cache=False
)
for i, p in enumerate(filtered_tickers):
pair_candles = candles[
(p['symbol'], self._lookback_timeframe)
] if (p['symbol'], self._lookback_timeframe) in candles else None
# in case of candle data calculate typical price and quoteVolume for candle
if pair_candles is not None and not pair_candles.empty:
pair_candles['typical_price'] = (pair_candles['high'] + pair_candles['low']
+ pair_candles['close']) / 3
pair_candles['quoteVolume'] = (
pair_candles['volume'] * pair_candles['typical_price']
)
# ensure that a rolling sum over the lookback_period is built
# if pair_candles contains more candles than lookback_period
quoteVolume = (pair_candles['quoteVolume']
.rolling(self._lookback_period)
.sum()
.iloc[-1])
# replace quoteVolume with range quoteVolume sum calculated above
filtered_tickers[i]['quoteVolume'] = quoteVolume
else:
filtered_tickers[i]['quoteVolume'] = 0
if self._min_value > 0:
filtered_tickers = [
v for v in filtered_tickers if v[self._sort_key] > self._min_value]

View File

@ -62,10 +62,10 @@ class RangeStabilityFilter(IPairList):
"""
needed_pairs = [(p, '1d') for p in pairlist if p not in self._pair_cache]
since_ms = int(arrow.utcnow()
since_ms = (arrow.utcnow()
.floor('day')
.shift(days=-self._days - 1)
.float_timestamp) * 1000
.int_timestamp) * 1000
# Get all candles
candles = {}
if needed_pairs:

View File

@ -21,6 +21,7 @@ class ExchangeResolver(IResolver):
def load_exchange(exchange_name: str, config: dict, validate: bool = True) -> Exchange:
"""
Load the custom class from config parameter
:param exchange_name: name of the Exchange to load
:param config: configuration dictionary
"""
# Map exchange name to avoid duplicate classes for identical exchanges

View File

@ -135,7 +135,7 @@ class IResolver:
extra_dir: Optional[str] = None) -> Any:
"""
Search and loads the specified object as configured in hte child class.
:param objectname: name of the module to import
:param object_name: name of the module to import
:param config: configuration dictionary
:param extra_dir: additional directory to search for the given pairlist
:raises: OperationalException if the class is invalid or does not exist.
@ -163,7 +163,7 @@ class IResolver:
:param directory: Path to search
:param enum_failed: If True, will return None for modules which fail.
Otherwise, failing modules are skipped.
:return: List of dicts containing 'name', 'class' and 'location' entires
:return: List of dicts containing 'name', 'class' and 'location' entries
"""
logger.debug(f"Searching for {cls.object_type.__name__} '{directory}'")
objects = []

View File

@ -45,10 +45,6 @@ class StrategyResolver(IResolver):
strategy_name, config=config,
extra_dir=config.get('strategy_path'))
# make sure ask_strategy dict is available
if 'ask_strategy' not in config:
config['ask_strategy'] = {}
if hasattr(strategy, 'ticker_interval') and not hasattr(strategy, 'timeframe'):
# Assign ticker_interval to timeframe to keep compatibility
if 'timeframe' not in config:
@ -57,45 +53,54 @@ class StrategyResolver(IResolver):
)
strategy.timeframe = strategy.ticker_interval
if strategy._ft_params_from_file:
# Set parameters from Hyperopt results file
params = strategy._ft_params_from_file
strategy.minimal_roi = params.get('roi', strategy.minimal_roi)
strategy.stoploss = params.get('stoploss', {}).get('stoploss', strategy.stoploss)
trailing = params.get('trailing', {})
strategy.trailing_stop = trailing.get('trailing_stop', strategy.trailing_stop)
strategy.trailing_stop_positive = trailing.get('trailing_stop_positive',
strategy.trailing_stop_positive)
strategy.trailing_stop_positive_offset = trailing.get(
'trailing_stop_positive_offset', strategy.trailing_stop_positive_offset)
strategy.trailing_only_offset_is_reached = trailing.get(
'trailing_only_offset_is_reached', strategy.trailing_only_offset_is_reached)
# Set attributes
# Check if we need to override configuration
# (Attribute name, default, subkey)
attributes = [("minimal_roi", {"0": 10.0}, None),
("timeframe", None, None),
("stoploss", None, None),
("trailing_stop", None, None),
("trailing_stop_positive", None, None),
("trailing_stop_positive_offset", 0.0, None),
("trailing_only_offset_is_reached", None, None),
("use_custom_stoploss", None, None),
("process_only_new_candles", None, None),
("order_types", None, None),
("order_time_in_force", None, None),
("stake_currency", None, None),
("stake_amount", None, None),
("protections", None, None),
("startup_candle_count", None, None),
("unfilledtimeout", None, None),
("use_sell_signal", True, 'ask_strategy'),
("sell_profit_only", False, 'ask_strategy'),
("ignore_roi_if_buy_signal", False, 'ask_strategy'),
("sell_profit_offset", 0.0, 'ask_strategy'),
("disable_dataframe_checks", False, None),
("ignore_buying_expired_candle_after", 0, 'ask_strategy')
attributes = [("minimal_roi", {"0": 10.0}),
("timeframe", None),
("stoploss", None),
("trailing_stop", None),
("trailing_stop_positive", None),
("trailing_stop_positive_offset", 0.0),
("trailing_only_offset_is_reached", None),
("use_custom_stoploss", None),
("process_only_new_candles", None),
("order_types", None),
("order_time_in_force", None),
("stake_currency", None),
("stake_amount", None),
("protections", None),
("startup_candle_count", None),
("unfilledtimeout", None),
("use_sell_signal", True),
("sell_profit_only", False),
("ignore_roi_if_buy_signal", False),
("sell_profit_offset", 0.0),
("disable_dataframe_checks", False),
("ignore_buying_expired_candle_after", 0)
]
for attribute, default, subkey in attributes:
if subkey:
StrategyResolver._override_attribute_helper(strategy, config.get(subkey, {}),
attribute, default)
else:
for attribute, default in attributes:
StrategyResolver._override_attribute_helper(strategy, config,
attribute, default)
# Loop this list again to have output combined
for attribute, _, subkey in attributes:
if subkey and attribute in config[subkey]:
logger.info("Strategy using %s: %s", attribute, config[subkey][attribute])
elif attribute in config:
for attribute, _ in attributes:
if attribute in config:
logger.info("Strategy using %s: %s", attribute, config[attribute])
StrategyResolver._normalize_attributes(strategy)

View File

@ -115,6 +115,7 @@ class ShowConfig(BaseModel):
dry_run: bool
stake_currency: str
stake_amount: Union[float, str]
stake_currency_decimals: int
max_open_trades: int
minimal_roi: Dict[str, Any]
stoploss: float

View File

@ -162,8 +162,8 @@ def delete_lock_pair(payload: DeleteLockRequest, rpc: RPC = Depends(get_rpc)):
@router.get('/logs', response_model=Logs, tags=['info'])
def logs(limit: Optional[int] = None, rpc: RPC = Depends(get_rpc)):
return rpc._rpc_get_logs(limit)
def logs(limit: Optional[int] = None):
return RPC._rpc_get_logs(limit)
@router.post('/start', response_model=StatusMsg, tags=['botcontrol'])

View File

@ -18,6 +18,17 @@ async def fallback():
return FileResponse(str(Path(__file__).parent / 'ui/fallback_file.html'))
@router_ui.get('/ui_version', include_in_schema=False)
async def ui_version():
from freqtrade.commands.deploy_commands import read_ui_version
uibase = Path(__file__).parent / 'ui/installed/'
version = read_ui_version(uibase)
return {
"version": version if version else "not_installed",
}
@router_ui.get('/{rest_of_path:path}', include_in_schema=False)
async def index_html(rest_of_path: str):
"""

View File

@ -115,14 +115,12 @@ class ApiServer(RPCHandler):
logger.info('Starting Local Rest Server.')
verbosity = self._config['api_server'].get('verbosity', 'error')
log_config = uvicorn.config.LOGGING_CONFIG
# Change logging of access logs to stderr
log_config["handlers"]["access"]["stream"] = log_config["handlers"]["default"]["stream"]
uvconfig = uvicorn.Config(self.app,
port=rest_port,
host=rest_ip,
use_colors=False,
log_config=log_config,
log_config=None,
access_log=True if verbosity != 'error' else False,
)
try:

View File

@ -102,7 +102,7 @@ class CryptoToFiatConverter:
inverse = True
symbol = f"{crypto_symbol}/{fiat_symbol}"
# Check if the fiat convertion you want is supported
# Check if the fiat conversion you want is supported
if not self._is_supported_fiat(fiat=fiat_symbol):
raise ValueError(f'The fiat {fiat_symbol} is not supported.')
@ -135,7 +135,7 @@ class CryptoToFiatConverter:
:param fiat_symbol: FIAT currency you want to convert to (e.g usd)
:return: float, price of the crypto-currency in Fiat
"""
# Check if the fiat convertion you want is supported
# Check if the fiat conversion you want is supported
if not self._is_supported_fiat(fiat=fiat_symbol):
raise ValueError(f'The fiat {fiat_symbol} is not supported.')
@ -146,7 +146,7 @@ class CryptoToFiatConverter:
if self._cryptomap == {}:
if self._backoff <= datetime.datetime.now().timestamp():
self._load_cryptomap()
# return 0.0 if we still dont have data to check, no reason to proceed
# return 0.0 if we still don't have data to check, no reason to proceed
if self._cryptomap == {}:
return 0.0
else:

View File

@ -18,7 +18,7 @@ from freqtrade.enums import SellType, State
from freqtrade.exceptions import ExchangeError, PricingError
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
from freqtrade.loggers import bufferHandler
from freqtrade.misc import shorten_date
from freqtrade.misc import decimals_per_coin, shorten_date
from freqtrade.persistence import PairLocks, Trade
from freqtrade.persistence.models import PairLock
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
@ -104,6 +104,7 @@ class RPC:
val = {
'dry_run': config['dry_run'],
'stake_currency': config['stake_currency'],
'stake_currency_decimals': decimals_per_coin(config['stake_currency']),
'stake_amount': config['stake_amount'],
'max_open_trades': (config['max_open_trades']
if config['max_open_trades'] != float('inf') else -1),
@ -180,9 +181,9 @@ class RPC:
base_currency=self._freqtrade.config['stake_currency'],
close_profit=trade.close_profit if trade.close_profit is not None else None,
current_rate=current_rate,
current_profit=current_profit, # Deprectated
current_profit_pct=round(current_profit * 100, 2), # Deprectated
current_profit_abs=current_profit_abs, # Deprectated
current_profit=current_profit, # Deprecated
current_profit_pct=round(current_profit * 100, 2), # Deprecated
current_profit_abs=current_profit_abs, # Deprecated
profit_ratio=current_profit,
profit_pct=round(current_profit * 100, 2),
profit_abs=current_profit_abs,
@ -339,7 +340,9 @@ class RPC:
self, stake_currency: str, fiat_display_currency: str,
start_date: datetime = datetime.fromtimestamp(0)) -> Dict[str, Any]:
""" Returns cumulative profit statistics """
trades = Trade.get_trades([Trade.open_date >= start_date]).order_by(Trade.id).all()
trade_filter = ((Trade.is_open.is_(False) & (Trade.close_date >= start_date)) |
Trade.is_open.is_(True))
trades = Trade.get_trades(trade_filter).order_by(Trade.id).all()
profit_all_coin = []
profit_all_ratio = []
@ -378,7 +381,7 @@ class RPC:
)
profit_all_ratio.append(profit_ratio)
best_pair = Trade.get_best_pair()
best_pair = Trade.get_best_pair(start_date)
# Prepare data to display
profit_closed_coin_sum = round(sum(profit_closed_coin), 8)
@ -758,7 +761,7 @@ class RPC:
sell_signals = 0
if has_content:
dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].astype(int64) // 1000 // 1000
dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].view(int64) // 1000 // 1000
# Move open to seperate column when signal for easy plotting
if 'buy' in dataframe.columns:
buy_mask = (dataframe['buy'] == 1)
@ -822,8 +825,11 @@ class RPC:
)
if pair not in _data:
raise RPCException(f"No data for {pair}, {timeframe} in {timerange} found.")
from freqtrade.data.dataprovider import DataProvider
from freqtrade.resolvers.strategy_resolver import StrategyResolver
strategy = StrategyResolver.load_strategy(config)
strategy.dp = DataProvider(config, exchange=None, pairlists=None)
df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair})
return RPC._convert_dataframe_to_dict(strategy.get_strategy_name(), pair, timeframe,

View File

@ -24,7 +24,7 @@ from freqtrade.__init__ import __version__
from freqtrade.constants import DUST_PER_COIN
from freqtrade.enums import RPCMessageType
from freqtrade.exceptions import OperationalException
from freqtrade.misc import chunks, round_coin_value
from freqtrade.misc import chunks, plural, round_coin_value
from freqtrade.rpc import RPC, RPCException, RPCHandler
@ -103,7 +103,7 @@ class Telegram(RPCHandler):
# do not allow commands with mandatory arguments and critical cmds
# like /forcesell and /forcebuy
# TODO: DRY! - its not good to list all valid cmds here. But otherwise
# this needs refacoring of the whole telegram module (same
# this needs refactoring of the whole telegram module (same
# problem in _help()).
valid_keys: List[str] = [r'/start$', r'/stop$', r'/status$', r'/status table$',
r'/trades$', r'/performance$', r'/daily$', r'/daily \d+$',
@ -482,7 +482,7 @@ class Telegram(RPCHandler):
timescale = None
try:
if context.args:
timescale = int(context.args[0])
timescale = int(context.args[0]) - 1
today_start = datetime.combine(date.today(), datetime.min.time())
start_date = today_start - timedelta(days=timescale)
except (TypeError, ValueError, IndexError):
@ -598,7 +598,10 @@ class Telegram(RPCHandler):
"Starting capital: "
f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n"
)
total_dust_balance = 0
total_dust_currencies = 0
for curr in result['currencies']:
curr_output = ''
if curr['est_stake'] > balance_dust_level:
curr_output = (
f"*{curr['currency']}:*\n"
@ -607,17 +610,25 @@ class Telegram(RPCHandler):
f"\t`Pending: {curr['used']:.8f}`\n"
f"\t`Est. {curr['stake']}: "
f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n")
else:
curr_output = (f"*{curr['currency']}:* not showing <{balance_dust_level} "
f"{curr['stake']} amount \n")
elif curr['est_stake'] <= balance_dust_level:
total_dust_balance += curr['est_stake']
total_dust_currencies += 1
# Handle overflowing messsage length
# Handle overflowing message length
if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH:
self._send_msg(output)
output = curr_output
else:
output += curr_output
if total_dust_balance > 0:
output += (
f"*{total_dust_currencies} Other "
f"{plural(total_dust_currencies, 'Currency', 'Currencies')} "
f"(< {balance_dust_level} {result['stake']}):*\n"
f"\t`Est. {result['stake']}: "
f"{round_coin_value(total_dust_balance, result['stake'], False)}`\n")
output += ("\n*Estimated Value*:\n"
f"\t`{result['stake']}: {result['total']: .8f}`\n"
f"\t`{result['symbol']}: "

View File

@ -5,8 +5,10 @@ This module defines a base class for auto-hyperoptable strategies.
import logging
from abc import ABC, abstractmethod
from contextlib import suppress
from pathlib import Path
from typing import Any, Dict, Iterator, List, Optional, Sequence, Tuple, Union
from freqtrade.misc import deep_merge_dicts, json_load
from freqtrade.optimize.hyperopt_tools import HyperoptTools
@ -205,6 +207,21 @@ class DecimalParameter(NumericParameter):
return SKDecimal(low=self.low, high=self.high, decimals=self._decimals, name=name,
**self._space_params)
@property
def range(self):
"""
Get each value in this space as list.
Returns a List from low to high (inclusive) in Hyperopt mode.
Returns a List with 1 item (`value`) in "non-hyperopt" mode, to avoid
calculating 100ds of indicators.
"""
if self.in_space and self.optimize:
low = int(self.low * pow(10, self._decimals))
high = int(self.high * pow(10, self._decimals)) + 1
return [round(n * pow(0.1, self._decimals), self._decimals) for n in range(low, high)]
else:
return [self.value]
class CategoricalParameter(BaseParameter):
default: Any
@ -239,10 +256,23 @@ class CategoricalParameter(BaseParameter):
"""
return Categorical(self.opt_range, name=name, **self._space_params)
@property
def range(self):
"""
Get each value in this space as list.
Returns a List of categories in Hyperopt mode.
Returns a List with 1 item (`value`) in "non-hyperopt" mode, to avoid
calculating 100ds of indicators.
"""
if self.in_space and self.optimize:
return self.opt_range
else:
return [self.value]
class HyperStrategyMixin(object):
"""
A helper base class which allows HyperOptAuto class to reuse implementations of of buy/sell
A helper base class which allows HyperOptAuto class to reuse implementations of buy/sell
strategy logic.
"""
@ -258,7 +288,7 @@ class HyperStrategyMixin(object):
def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, BaseParameter]]:
"""
Find all optimizeable parameters and return (name, attr) iterator.
Find all optimizable parameters and return (name, attr) iterator.
:param category:
:return:
"""
@ -305,12 +335,38 @@ class HyperStrategyMixin(object):
"""
Load Hyperoptable parameters
"""
self._load_params(getattr(self, 'buy_params', None), 'buy', hyperopt)
self._load_params(getattr(self, 'sell_params', None), 'sell', hyperopt)
params = self.load_params_from_file()
params = params.get('params', {})
self._ft_params_from_file = params
buy_params = deep_merge_dicts(params.get('buy', {}), getattr(self, 'buy_params', None))
sell_params = deep_merge_dicts(params.get('sell', {}), getattr(self, 'sell_params', None))
def _load_params(self, params: dict, space: str, hyperopt: bool = False) -> None:
self._load_params(buy_params, 'buy', hyperopt)
self._load_params(sell_params, 'sell', hyperopt)
def load_params_from_file(self) -> Dict:
filename_str = getattr(self, '__file__', '')
if not filename_str:
return {}
filename = Path(filename_str).with_suffix('.json')
if filename.is_file():
logger.info(f"Loading parameters from file {filename}")
try:
params = json_load(filename.open('r'))
if params.get('strategy_name') != self.__class__.__name__:
raise OperationalException('Invalid parameter file provided.')
return params
except ValueError:
logger.warning("Invalid parameter file format.")
return {}
logger.info("Found no parameter file.")
return {}
def _load_params(self, params: Dict, space: str, hyperopt: bool = False) -> None:
"""
Set optimizeable parameter values.
Set optimizable parameter values.
:param params: Dictionary with new parameter values.
"""
if not params:
@ -335,7 +391,7 @@ class HyperStrategyMixin(object):
else:
logger.info(f'Strategy Parameter(default): {attr_name} = {attr.value}')
def get_params_dict(self):
def get_no_optimize_params(self):
"""
Returns list of Parameters that are not part of the current optimize job
"""

View File

@ -62,6 +62,7 @@ class IStrategy(ABC, HyperStrategyMixin):
_populate_fun_len: int = 0
_buy_fun_len: int = 0
_sell_fun_len: int = 0
_ft_params_from_file: Dict = {}
# associated minimal roi
minimal_roi: Dict
@ -97,6 +98,11 @@ class IStrategy(ABC, HyperStrategyMixin):
# run "populate_indicators" only for new candle
process_only_new_candles: bool = False
use_sell_signal: bool
sell_profit_only: bool
sell_profit_offset: float
ignore_roi_if_buy_signal: bool
# Number of seconds after which the candle will no longer result in a buy on expired candles
ignore_buying_expired_candle_after: int = 0
@ -270,7 +276,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
:param current_profit: Current profit (as ratio), calculated based on current_rate.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New stoploss value, relative to the currentrate
:return float: New stoploss value, relative to the current_rate
"""
return self.stoploss
@ -301,7 +307,7 @@ class IStrategy(ABC, HyperStrategyMixin):
def informative_pairs(self) -> ListPairsWithTimeframes:
"""
Define additional, informative pair/interval combinations to be cached from the exchange.
These pair/interval combinations are non-tradeable, unless they are part
These pair/interval combinations are non-tradable, unless they are part
of the whitelist as well.
For more information, please consult the documentation
:return: List of tuples in the format (pair, interval)
@ -349,7 +355,7 @@ class IStrategy(ABC, HyperStrategyMixin):
The 2nd, optional parameter ensures that locks are applied until the new candle arrives,
and not stop at 14:00:00 - while the next candle arrives at 14:00:02 leaving a gap
of 2 seconds for a buy to happen on an old signal.
:param: pair: "Pair to check"
:param pair: "Pair to check"
:param candle_date: Date of the last candle. Optional, defaults to current date
:returns: locking state of the pair in question.
"""
@ -543,10 +549,9 @@ class IStrategy(ABC, HyperStrategyMixin):
# Set current rate to high for backtesting sell
current_rate = high or rate
current_profit = trade.calc_profit_ratio(current_rate)
ask_strategy = self.config.get('ask_strategy', {})
# if buy signal and ignore_roi is set, we don't need to evaluate min_roi.
roi_reached = (not (buy and ask_strategy.get('ignore_roi_if_buy_signal', False))
roi_reached = (not (buy and self.ignore_roi_if_buy_signal)
and self.min_roi_reached(trade=trade, current_profit=current_profit,
current_time=date))
@ -556,11 +561,10 @@ class IStrategy(ABC, HyperStrategyMixin):
current_rate = rate
current_profit = trade.calc_profit_ratio(current_rate)
if (ask_strategy.get('sell_profit_only', False)
and current_profit <= ask_strategy.get('sell_profit_offset', 0)):
if (self.sell_profit_only and current_profit <= self.sell_profit_offset):
# sell_profit_only and profit doesn't reach the offset - ignore sell signal
pass
elif ask_strategy.get('use_sell_signal', True) and not buy:
elif self.use_sell_signal and not buy:
if sell:
sell_signal = SellType.SELL_SIGNAL
else:
@ -733,7 +737,8 @@ class IStrategy(ABC, HyperStrategyMixin):
Based on TA indicators, populates the buy signal for the given dataframe
This method should not be overridden.
:param dataframe: DataFrame
:param pair: Additional information, like the currently traded pair
:param metadata: Additional information dictionary, with details like the
currently traded pair
:return: DataFrame with buy column
"""
logger.debug(f"Populating buy signals for pair {metadata.get('pair')}.")
@ -750,7 +755,8 @@ class IStrategy(ABC, HyperStrategyMixin):
Based on TA indicators, populates the sell signal for the given dataframe
This method should not be overridden.
:param dataframe: DataFrame
:param pair: Additional information, like the currently traded pair
:param metadata: Additional information dictionary, with details like the
currently traded pair
:return: DataFrame with sell column
"""
logger.debug(f"Populating sell signals for pair {metadata.get('pair')}.")

View File

@ -15,7 +15,7 @@
"bid_strategy": {
"price_side": "bid",
"ask_last_balance": 0.0,
"use_order_book": false,
"use_order_book": true,
"order_book_top": 1,
"check_depth_of_market": {
"enabled": false,
@ -24,12 +24,8 @@
},
"ask_strategy": {
"price_side": "ask",
"use_order_book": false,
"order_book_min": 1,
"order_book_max": 1,
"use_sell_signal": true,
"sell_profit_only": false,
"ignore_roi_if_buy_signal": false
"use_order_book": true,
"order_book_top": 1,
},
{{ exchange | indent(4) }},
"pairlists": [

View File

@ -188,6 +188,52 @@
"trades.groupby(\"pair\")[\"sell_reason\"].value_counts()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Plotting daily profit / equity line"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Plotting equity line (starting with 0 on day 1 and adding daily profit for each backtested day)\n",
"\n",
"from freqtrade.configuration import Configuration\n",
"from freqtrade.data.btanalysis import load_backtest_data, load_backtest_stats\n",
"import plotly.express as px\n",
"import pandas as pd\n",
"\n",
"# strategy = 'SampleStrategy'\n",
"# config = Configuration.from_files([\"user_data/config.json\"])\n",
"# backtest_dir = config[\"user_data_dir\"] / \"backtest_results\"\n",
"\n",
"stats = load_backtest_stats(backtest_dir)\n",
"strategy_stats = stats['strategy'][strategy]\n",
"\n",
"dates = []\n",
"profits = []\n",
"for date_profit in strategy_stats['daily_profit']:\n",
" dates.append(date_profit[0])\n",
" profits.append(date_profit[1])\n",
"\n",
"equity = 0\n",
"equity_daily = []\n",
"for daily_profit in profits:\n",
" equity_daily.append(equity)\n",
" equity += float(daily_profit)\n",
"\n",
"\n",
"df = pd.DataFrame({'dates': dates,'equity_daily': equity_daily})\n",
"\n",
"fig = px.line(df, x=\"dates\", y=\"equity_daily\")\n",
"fig.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
@ -329,7 +375,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.4"
"version": "3.8.5"
},
"mimetype": "text/x-python",
"name": "python",

View File

@ -61,7 +61,7 @@ class Worker:
def _notify(self, message: str) -> None:
"""
Removes the need to verify in all occurances if sd_notify is enabled
Removes the need to verify in all occurrences if sd_notify is enabled
:param message: Message to send to systemd if it's enabled.
"""
if self._sd_notify:

View File

@ -44,8 +44,18 @@ theme:
favicon: 'images/logo.png'
custom_dir: 'docs/overrides'
palette:
- scheme: default
primary: 'blue grey'
accent: 'tear'
toggle:
icon: material/toggle-switch-off-outline
name: Switch to dark mode
- scheme: slate
primary: 'blue grey'
accent: 'tear'
toggle:
icon: material/toggle-switch-off-outline
name: Switch to dark mode
extra_css:
- 'stylesheets/ft.extra.css'
extra_javascript:

View File

@ -7,19 +7,19 @@ coveralls==3.1.0
flake8==3.9.2
flake8-type-annotations==0.1.0
flake8-tidy-imports==4.3.0
mypy==0.902
mypy==0.910
pytest==6.2.4
pytest-asyncio==0.15.1
pytest-cov==2.12.1
pytest-mock==3.6.1
pytest-random-order==1.0.4
isort==5.8.0
isort==5.9.1
# Convert jupyter notebooks to markdown documents
nbconvert==6.0.7
nbconvert==6.1.0
# mypy types
types-cachetools==0.1.8
types-cachetools==0.1.9
types-filelock==0.1.4
types-requests==0.1.13
types-requests==2.25.0
types-tabulate==0.1.1

View File

@ -1,5 +1,5 @@
# Include all requirements to run the bot.
-r requirements.txt
plotly==4.14.3
plotly==5.1.0

View File

@ -1,16 +1,16 @@
numpy==1.20.3
pandas==1.2.4
numpy==1.21.0
pandas==1.3.0
ccxt==1.51.77
ccxt==1.52.40
# Pin cryptography for now due to rust build errors with piwheels
cryptography==3.4.7
aiohttp==3.7.4.post0
SQLAlchemy==1.4.18
python-telegram-bot==13.6
arrow==1.1.0
SQLAlchemy==1.4.20
python-telegram-bot==13.7
arrow==1.1.1
cachetools==4.2.2
requests==2.25.1
urllib3==1.26.5
urllib3==1.26.6
wrapt==1.12.1
jsonschema==3.2.0
TA-Lib==0.4.20
@ -25,13 +25,13 @@ blosc==1.10.4
py_find_1st==1.1.5
# Load ticker files 30% faster
python-rapidjson==1.0
python-rapidjson==1.4
# Notify systemd
sdnotify==0.3.2
# API Server
fastapi==0.65.2
fastapi==0.66.0
uvicorn==0.14.0
pyjwt==2.1.0
aiofiles==0.7.0

View File

@ -1168,6 +1168,7 @@ def test_hyperopt_show(mocker, capsys, saved_hyperopt_results):
'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results',
MagicMock(return_value=saved_hyperopt_results)
)
mocker.patch('freqtrade.commands.hyperopt_commands.show_backtest_result')
args = [
"hyperopt-show",

View File

@ -23,8 +23,8 @@ from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import LocalTrade, Trade, init_db
from freqtrade.resolvers import ExchangeResolver
from freqtrade.worker import Worker
from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4,
mock_trade_5, mock_trade_6, short_trade, leverage_trade)
from tests.conftest_trades import (leverage_trade, mock_trade_1, mock_trade_2, mock_trade_3,
mock_trade_4, mock_trade_5, mock_trade_6, short_trade)
logging.getLogger('').setLevel(logging.INFO)
@ -205,7 +205,32 @@ def create_mock_trades(fee, use_db: bool = True):
# Simulate dry_run entries
trade = mock_trade_1(fee)
add_trade(trade)
trade = mock_trade_2(fee)
add_trade(trade)
trade = mock_trade_3(fee)
add_trade(trade)
trade = mock_trade_4(fee)
add_trade(trade)
trade = mock_trade_5(fee)
add_trade(trade)
trade = mock_trade_6(fee)
add_trade(trade)
if use_db:
Trade.query.session.flush()
def create_mock_trades_with_leverage(fee, use_db: bool = True):
"""
Create some fake trades ...
"""
def add_trade(trade):
if use_db:
Trade.query.session.add(trade)
else:
LocalTrade.add_bt_trade(trade)
# Simulate dry_run entries
trade = mock_trade_1(fee)
add_trade(trade)
trade = mock_trade_2(fee)
add_trade(trade)
@ -221,13 +246,10 @@ def create_mock_trades(fee, use_db: bool = True):
trade = mock_trade_6(fee)
add_trade(trade)
# TODO: margin trades
# trade = short_trade(fee)
# add_trade(trade)
# trade = leverage_trade(fee)
# add_trade(trade)
trade = short_trade(fee)
add_trade(trade)
trade = leverage_trade(fee)
add_trade(trade)
if use_db:
Trade.query.session.flush()
@ -257,7 +279,6 @@ def patch_coingekko(mocker) -> None:
@pytest.fixture(scope='function')
def init_persistence(default_conf):
init_db(default_conf['db_url'], default_conf['dry_run'])
# TODO-mg: trade with leverage and/or borrowed?
@pytest.fixture(scope="function")
@ -298,8 +319,7 @@ def get_default_conf(testdatadir):
},
"ask_strategy": {
"use_order_book": False,
"order_book_min": 1,
"order_book_max": 1
"order_book_top": 1,
},
"exchange": {
"name": "binance",
@ -333,6 +353,7 @@ def get_default_conf(testdatadir):
"verbosity": 3,
"strategy_path": str(Path(__file__).parent / "strategy" / "strats"),
"strategy": "DefaultStrategy",
"disableparamexport": True,
"internals": {},
"export": "none",
}
@ -922,7 +943,8 @@ def limit_sell_order_old():
@pytest.fixture
def limit_buy_order_old_partial():
return {'id': 'mocked_limit_buy_old_partial',
return {
'id': 'mocked_limit_buy_old_partial',
'type': 'limit',
'side': 'buy',
'symbol': 'ETH/BTC',
@ -1095,6 +1117,40 @@ def order_book_l2():
})
@pytest.fixture
def order_book_l2_usd():
return MagicMock(return_value={
'symbol': 'LTC/USDT',
'bids': [
[25.563, 49.269],
[25.562, 83.0],
[25.56, 106.0],
[25.559, 15.381],
[25.558, 29.299],
[25.557, 34.624],
[25.556, 10.0],
[25.555, 14.684],
[25.554, 45.91],
[25.553, 50.0]
],
'asks': [
[25.566, 14.27],
[25.567, 48.484],
[25.568, 92.349],
[25.572, 31.48],
[25.573, 23.0],
[25.574, 20.0],
[25.575, 89.606],
[25.576, 262.016],
[25.577, 178.557],
[25.578, 78.614]
],
'timestamp': None,
'datetime': None,
'nonce': 2372149736
})
@pytest.fixture
def ohlcv_history_list():
return [
@ -1742,7 +1798,6 @@ def rpc_balance():
'used': 0.0
},
}
# TODO-mg: Add shorts and leverage?
@pytest.fixture
@ -1928,12 +1983,13 @@ def saved_hyperopt_results():
'params_dict': {
'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1190, 'roi_t2': 541, 'roi_t3': 408, 'roi_p1': 0.026035863879169705, 'roi_p2': 0.12508730043628782, 'roi_p3': 0.27766427921605896, 'stoploss': -0.2562930402099556}, # noqa: E501
'params_details': {'buy': {'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4287874435315165, 408: 0.15112316431545753, 949: 0.026035863879169705, 2139: 0}, 'stoploss': {'stoploss': -0.2562930402099556}}, # noqa: E501
'results_metrics': {'total_trades': 2, 'wins': 0, 'draws': 0, 'losses': 2, 'profit_mean': -0.01254995, 'profit_median': -0.012222, 'profit_total': -0.00125625, 'profit_total_abs': -2.50999, 'holding_avg': timedelta(minutes=3930.0)}, # noqa: E501
'results_metrics': {'total_trades': 2, 'wins': 0, 'draws': 0, 'losses': 2, 'profit_mean': -0.01254995, 'profit_median': -0.012222, 'profit_total': -0.00125625, 'profit_total_abs': -2.50999, 'holding_avg': timedelta(minutes=3930.0), 'stake_currency': 'BTC', 'strategy_name': 'SampleStrategy'}, # noqa: E501
'results_explanation': ' 2 trades. Avg profit -1.25%. Total profit -0.00125625 BTC ( -2.51Σ%). Avg duration 3930.0 min.', # noqa: E501
'total_profit': -0.00125625,
'current_epoch': 1,
'is_initial_point': True,
'is_best': True
'is_best': True,
}, {
'loss': 20.0,
'params_dict': {
@ -2055,98 +2111,191 @@ def saved_hyperopt_results():
for res in hyperopt_res:
res['results_metrics']['holding_avg_s'] = res['results_metrics']['holding_avg'
].total_seconds()
return hyperopt_res
# * Margin Tests
# TODO-mg: fill in these tests with something useful
@pytest.fixture
def leveraged_fee():
return
@pytest.fixture
def short_fee():
return
@pytest.fixture
def ticker_short():
return
@pytest.fixture
def ticker_exit_short_up():
return
@pytest.fixture
def ticker_exit_short_down():
return
@pytest.fixture
def leveraged_markets():
return
@pytest.fixture(scope='function')
def limit_short_order_open():
return
@pytest.fixture(scope='function')
def limit_short_order(limit_short_order_open):
return
@pytest.fixture(scope='function')
def market_short_order():
return
@pytest.fixture
def market_short_exit_order():
return
@pytest.fixture
def limit_short_order_old():
return
@pytest.fixture
def limit_exit_short_order_old():
return
@pytest.fixture
def limit_short_order_old_partial():
return
@pytest.fixture
def limit_short_order_old_partial_canceled(limit_short_order_old_partial):
return
@pytest.fixture(scope='function')
def limit_short_order_canceled_empty(request):
return
return {
'id': 'mocked_limit_short',
'type': 'limit',
'side': 'sell',
'symbol': 'mocked',
'datetime': arrow.utcnow().isoformat(),
'timestamp': arrow.utcnow().int_timestamp,
'price': 0.00001173,
'amount': 90.99181073,
'leverage': 1.0,
'filled': 0.0,
'cost': 0.00106733393,
'remaining': 90.99181073,
'status': 'open',
'is_short': True
}
@pytest.fixture
def limit_exit_short_order_open():
return
return {
'id': 'mocked_limit_exit_short',
'type': 'limit',
'side': 'buy',
'pair': 'mocked',
'datetime': arrow.utcnow().isoformat(),
'timestamp': arrow.utcnow().int_timestamp,
'price': 0.00001099,
'amount': 90.99370639272354,
'filled': 0.0,
'remaining': 90.99370639272354,
'status': 'open',
'leverage': 1.0
}
@pytest.fixture(scope='function')
def limit_short_order(limit_short_order_open):
order = deepcopy(limit_short_order_open)
order['status'] = 'closed'
order['filled'] = order['amount']
order['remaining'] = 0.0
return order
@pytest.fixture
def limit_exit_short_order(limit_sell_order_open):
return
def limit_exit_short_order(limit_exit_short_order_open):
order = deepcopy(limit_exit_short_order_open)
order['remaining'] = 0.0
order['filled'] = order['amount']
order['status'] = 'closed'
return order
@pytest.fixture(scope='function')
def market_short_order():
return {
'id': 'mocked_market_short',
'type': 'market',
'side': 'sell',
'symbol': 'mocked',
'datetime': arrow.utcnow().isoformat(),
'price': 0.00004173,
'amount': 275.97543219,
'filled': 275.97543219,
'remaining': 0.0,
'status': 'closed',
'is_short': True,
'leverage': 3.0,
}
@pytest.fixture
def short_order_fee():
return
def market_exit_short_order():
return {
'id': 'mocked_limit_exit_short',
'type': 'market',
'side': 'buy',
'symbol': 'mocked',
'datetime': arrow.utcnow().isoformat(),
'price': 0.00004099,
'amount': 276.113419906095,
'filled': 276.113419906095,
'remaining': 0.0,
'status': 'closed',
'leverage': 3.0
}
# leverage 3x
@pytest.fixture(scope='function')
def limit_lev_buy_order_open():
return {
'id': 'mocked_limit_buy',
'type': 'limit',
'side': 'buy',
'symbol': 'mocked',
'datetime': arrow.utcnow().isoformat(),
'timestamp': arrow.utcnow().int_timestamp,
'price': 0.00001099,
'amount': 272.97543219,
'filled': 0.0,
'cost': 0.0009999999999226999,
'remaining': 272.97543219,
'leverage': 3.0,
'status': 'open',
'exchange': 'binance',
}
@pytest.fixture(scope='function')
def limit_lev_buy_order(limit_lev_buy_order_open):
order = deepcopy(limit_lev_buy_order_open)
order['status'] = 'closed'
order['filled'] = order['amount']
order['remaining'] = 0.0
return order
@pytest.fixture
def limit_lev_sell_order_open():
return {
'id': 'mocked_limit_sell',
'type': 'limit',
'side': 'sell',
'pair': 'mocked',
'datetime': arrow.utcnow().isoformat(),
'timestamp': arrow.utcnow().int_timestamp,
'price': 0.00001173,
'amount': 272.97543219,
'filled': 0.0,
'remaining': 272.97543219,
'leverage': 3.0,
'status': 'open',
'exchange': 'binance'
}
@pytest.fixture
def limit_lev_sell_order(limit_lev_sell_order_open):
order = deepcopy(limit_lev_sell_order_open)
order['remaining'] = 0.0
order['filled'] = order['amount']
order['status'] = 'closed'
return order
@pytest.fixture(scope='function')
def market_lev_buy_order():
return {
'id': 'mocked_market_buy',
'type': 'market',
'side': 'buy',
'symbol': 'mocked',
'datetime': arrow.utcnow().isoformat(),
'price': 0.00004099,
'amount': 275.97543219,
'filled': 275.97543219,
'remaining': 0.0,
'status': 'closed',
'exchange': 'kraken',
'leverage': 3.0
}
@pytest.fixture
def market_lev_sell_order():
return {
'id': 'mocked_limit_sell',
'type': 'market',
'side': 'sell',
'symbol': 'mocked',
'datetime': arrow.utcnow().isoformat(),
'price': 0.00004173,
'amount': 275.97543219,
'filled': 275.97543219,
'remaining': 0.0,
'status': 'closed',
'leverage': 3.0,
'exchange': 'kraken'
}

View File

@ -3,7 +3,7 @@ from datetime import datetime, timedelta, timezone
from freqtrade.persistence.models import Order, Trade
MOCK_TRADE_COUNT = 6 # TODO-mg: Increase for short and leverage
MOCK_TRADE_COUNT = 6
def mock_order_1():
@ -305,12 +305,9 @@ def mock_trade_6(fee):
return trade
#! TODO Currently the following short_trade test and leverage_trade test will fail
def short_order():
return {
'id': '1235',
'id': '1236',
'symbol': 'ETC/BTC',
'status': 'closed',
'side': 'sell',
@ -319,14 +316,12 @@ def short_order():
'amount': 123.0,
'filled': 123.0,
'remaining': 0.0,
'leverage': 5.0,
'isShort': True
}
def exit_short_order():
return {
'id': '12366',
'id': '12367',
'symbol': 'ETC/BTC',
'status': 'closed',
'side': 'buy',
@ -335,36 +330,60 @@ def exit_short_order():
'amount': 123.0,
'filled': 123.0,
'remaining': 0.0,
'leverage': 5.0,
'isShort': True
}
def short_trade(fee):
"""
Closed trade...
10 minute short limit trade on binance
Short trade
fee: 0.25% base
interest_rate: 0.05% per day
open_rate: 0.123 base
close_rate: 0.128 base
amount: 123.0 crypto
stake_amount: 15.129 base
borrowed: 123.0 crypto
time-periods: 10 minutes(rounds up to 1/24 time-period of 1 day)
interest: borrowed * interest_rate * time-periods
= 123.0 * 0.0005 * 1/24 = 0.0025625 crypto
open_value: (amount * open_rate) - (amount * open_rate * fee)
= (123 * 0.123) - (123 * 0.123 * 0.0025)
= 15.091177499999999
amount_closed: amount + interest = 123 + 0.0025625 = 123.0025625
close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee)
= (123.0025625 * 0.128) + (123.0025625 * 0.128 * 0.0025)
= 15.78368882
total_profit = open_value - close_value
= 15.091177499999999 - 15.78368882
= -0.6925113200000013
total_profit_percentage = total_profit / stake_amount
= -0.6925113200000013 / 15.129
= -0.04577376693766946
"""
trade = Trade(
pair='ETC/BTC',
stake_amount=0.001,
amount=123.0, # TODO-mg: In BTC?
stake_amount=15.129,
amount=123.0,
amount_requested=123.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.123,
close_rate=0.128,
close_profit=0.005, # TODO-mg: Would this be -0.005 or -0.025
close_profit_abs=0.000584127,
# close_rate=0.128,
# close_profit=-0.04577376693766946,
# close_profit_abs=-0.6925113200000013,
exchange='binance',
is_open=False,
is_open=True,
open_order_id='dry_run_exit_short_12345',
strategy='DefaultStrategy',
timeframe=5,
sell_reason='sell_signal', # TODO-mg: Update to exit/close reason
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
# close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
# borrowed=
isShort=True
is_short=True
)
o = Order.parse_from_ccxt_object(short_order(), 'ETC/BTC', 'sell')
trade.orders.append(o)
@ -375,8 +394,8 @@ def short_trade(fee):
def leverage_order():
return {
'id': '1235',
'symbol': 'ETC/BTC',
'id': '1237',
'symbol': 'DOGE/BTC',
'status': 'closed',
'side': 'buy',
'type': 'limit',
@ -390,8 +409,8 @@ def leverage_order():
def leverage_order_sell():
return {
'id': '12366',
'symbol': 'ETC/BTC',
'id': '12368',
'symbol': 'DOGE/BTC',
'status': 'closed',
'side': 'sell',
'type': 'limit',
@ -399,38 +418,63 @@ def leverage_order_sell():
'amount': 123.0,
'filled': 123.0,
'remaining': 0.0,
'leverage': 5.0,
'isShort': True
}
def leverage_trade(fee):
"""
Closed trade...
5 hour short limit trade on kraken
Short trade
fee: 0.25% base
interest_rate: 0.05% per day
open_rate: 0.123 base
close_rate: 0.128 base
amount: 615 crypto
stake_amount: 15.129 base
borrowed: 60.516 base
leverage: 5
hours: 5
interest: borrowed * interest_rate * ceil(1 + hours/4)
= 60.516 * 0.0005 * ceil(1 + 5/4) = 0.090774 base
open_value: (amount * open_rate) + (amount * open_rate * fee)
= (615.0 * 0.123) + (615.0 * 0.123 * 0.0025)
= 75.83411249999999
close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) - interest
= (615.0 * 0.128) - (615.0 * 0.128 * 0.0025) - 0.090774
= 78.432426
total_profit = close_value - open_value
= 78.432426 - 75.83411249999999
= 2.5983135000000175
total_profit_percentage = ((close_value/open_value)-1) * leverage
= ((78.432426/75.83411249999999)-1) * 5
= 0.1713156134055116
"""
trade = Trade(
pair='ETC/BTC',
stake_amount=0.001,
pair='DOGE/BTC',
stake_amount=15.129,
amount=615.0,
leverage=5.0,
amount_requested=615.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.123,
close_rate=0.128,
close_profit=0.005, # TODO-mg: Would this be -0.005 or -0.025
close_profit_abs=0.000584127,
exchange='binance',
close_profit=0.1713156134055116,
close_profit_abs=2.5983135000000175,
exchange='kraken',
is_open=False,
open_order_id='dry_run_leverage_sell_12345',
strategy='DefaultStrategy',
timeframe=5,
sell_reason='sell_signal', # TODO-mg: Update to exit/close reason
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
# borrowed=
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=300),
close_date=datetime.now(tz=timezone.utc),
interest_rate=0.0005
)
o = Order.parse_from_ccxt_object(leverage_order(), 'ETC/BTC', 'sell')
o = Order.parse_from_ccxt_object(leverage_order(), 'DOGE/BTC', 'sell')
trade.orders.append(o)
o = Order.parse_from_ccxt_object(leverage_order_sell(), 'ETC/BTC', 'sell')
o = Order.parse_from_ccxt_object(leverage_order_sell(), 'DOGE/BTC', 'sell')
trade.orders.append(o)
return trade

View File

@ -947,6 +947,72 @@ def test_create_dry_run_order(default_conf, mocker, side, exchange_name):
assert order["symbol"] == "ETH/BTC"
@pytest.mark.parametrize("side,startprice,endprice", [
("buy", 25.563, 25.566),
("sell", 25.566, 25.563)
])
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_create_dry_run_order_limit_fill(default_conf, mocker, side, startprice, endprice,
exchange_name, order_book_l2_usd):
default_conf['dry_run'] = True
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
mocker.patch.multiple('freqtrade.exchange.Exchange',
exchange_has=MagicMock(return_value=True),
fetch_l2_order_book=order_book_l2_usd,
)
order = exchange.create_dry_run_order(
pair='LTC/USDT', ordertype='limit', side=side, amount=1, rate=startprice)
assert order_book_l2_usd.call_count == 1
assert 'id' in order
assert f'dry_run_{side}_' in order["id"]
assert order["side"] == side
assert order["type"] == "limit"
assert order["symbol"] == "LTC/USDT"
order_book_l2_usd.reset_mock()
order_closed = exchange.fetch_dry_run_order(order['id'])
assert order_book_l2_usd.call_count == 1
assert order_closed['status'] == 'open'
assert not order['fee']
order_book_l2_usd.reset_mock()
order_closed['price'] = endprice
order_closed = exchange.fetch_dry_run_order(order['id'])
assert order_closed['status'] == 'closed'
assert order['fee']
@pytest.mark.parametrize("side,amount,endprice", [
("buy", 1, 25.566),
("buy", 100, 25.5672), # Requires interpolation
("buy", 1000, 25.575), # More than orderbook return
("sell", 1, 25.563),
("sell", 100, 25.5625), # Requires interpolation
("sell", 1000, 25.5555), # More than orderbook return
])
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_create_dry_run_order_market_fill(default_conf, mocker, side, amount, endprice,
exchange_name, order_book_l2_usd):
default_conf['dry_run'] = True
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
mocker.patch.multiple('freqtrade.exchange.Exchange',
exchange_has=MagicMock(return_value=True),
fetch_l2_order_book=order_book_l2_usd,
)
order = exchange.create_dry_run_order(
pair='LTC/USDT', ordertype='market', side=side, amount=amount, rate=25.5)
assert 'id' in order
assert f'dry_run_{side}_' in order["id"]
assert order["side"] == side
assert order["type"] == "market"
assert order["symbol"] == "LTC/USDT"
assert order['status'] == 'closed'
assert round(order["average"], 4) == round(endprice, 4)
@pytest.mark.parametrize("side", [
("buy"),
("sell")
@ -1778,8 +1844,7 @@ def test_get_sell_rate_orderbook(default_conf, mocker, caplog, side, expected, o
# Test orderbook mode
default_conf['ask_strategy']['price_side'] = side
default_conf['ask_strategy']['use_order_book'] = True
default_conf['ask_strategy']['order_book_min'] = 1
default_conf['ask_strategy']['order_book_max'] = 2
default_conf['ask_strategy']['order_book_top'] = 1
pair = "ETH/BTC"
mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book', order_book_l2)
exchange = get_patched_exchange(mocker, default_conf)
@ -1796,8 +1861,7 @@ def test_get_sell_rate_orderbook_exception(default_conf, mocker, caplog):
# Test orderbook mode
default_conf['ask_strategy']['price_side'] = 'ask'
default_conf['ask_strategy']['use_order_book'] = True
default_conf['ask_strategy']['order_book_min'] = 1
default_conf['ask_strategy']['order_book_max'] = 2
default_conf['ask_strategy']['order_book_top'] = 1
pair = "ETH/BTC"
# Test What happens if the exchange returns an empty orderbook.
mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book',
@ -1805,7 +1869,8 @@ def test_get_sell_rate_orderbook_exception(default_conf, mocker, caplog):
exchange = get_patched_exchange(mocker, default_conf)
with pytest.raises(PricingError):
exchange.get_sell_rate(pair, True)
assert log_has("Sell Price at location from orderbook could not be determined.", caplog)
assert log_has_re(r"Sell Price at location 1 from orderbook could not be determined\..*",
caplog)
def test_get_sell_rate_exception(default_conf, mocker, caplog):
@ -2117,6 +2182,7 @@ def test_get_historic_trades_notsupported(default_conf, mocker, caplog, exchange
def test_cancel_order_dry_run(default_conf, mocker, exchange_name):
default_conf['dry_run'] = True
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', return_value=True)
assert exchange.cancel_order(order_id='123', pair='TKN/BTC') == {}
assert exchange.cancel_stoploss_order(order_id='123', pair='TKN/BTC') == {}

View File

@ -34,6 +34,7 @@ class BTContainer(NamedTuple):
trailing_stop_positive: Optional[float] = None
trailing_stop_positive_offset: float = 0.0
use_sell_signal: bool = False
use_custom_stoploss: bool = False
def _get_frame_time_from_offset(offset):

View File

@ -501,6 +501,21 @@ tc31 = BTContainer(data=[
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=1)]
)
# Test 32: trailing_stop should be triggered immediately on trade open candle.
# stop-loss: 1%, ROI: 10% (should not apply)
tc32 = BTContainer(data=[
# D O H L C V B S
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
[1, 5000, 5500, 5000, 4900, 6172, 0, 0], # enter trade (signal on last candle) and stop
[2, 4900, 5250, 4500, 5100, 6172, 0, 0],
[3, 5100, 5100, 4650, 4750, 6172, 0, 0],
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.01, roi={"0": 0.10}, profit_perc=-0.01, trailing_stop=True,
trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.02,
trailing_stop_positive=0.01, use_custom_stoploss=True,
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=1)]
)
TESTS = [
tc0,
tc1,
@ -534,6 +549,7 @@ TESTS = [
tc29,
tc30,
tc31,
tc32,
]
@ -551,7 +567,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
if data.trailing_stop_positive is not None:
default_conf["trailing_stop_positive"] = data.trailing_stop_positive
default_conf["trailing_stop_positive_offset"] = data.trailing_stop_positive_offset
default_conf["ask_strategy"] = {"use_sell_signal": data.use_sell_signal}
default_conf["use_sell_signal"] = data.use_sell_signal
mocker.patch("freqtrade.exchange.Exchange.get_fee", return_value=0.0)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
@ -561,6 +577,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
backtesting._set_strategy(backtesting.strategylist[0])
backtesting.strategy.advise_buy = lambda a, m: frame
backtesting.strategy.advise_sell = lambda a, m: frame
backtesting.strategy.use_custom_stoploss = data.use_custom_stoploss
caplog.set_level(logging.DEBUG)
pair = "UNITTEST/BTC"

View File

@ -465,7 +465,7 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti
def test_backtest__enter_trade(default_conf, fee, mocker) -> None:
default_conf['ask_strategy']['use_sell_signal'] = False
default_conf['use_sell_signal'] = False
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
patch_exchange(mocker)
@ -511,7 +511,7 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None:
def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
default_conf['ask_strategy']['use_sell_signal'] = False
default_conf['use_sell_signal'] = False
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
patch_exchange(mocker)
@ -574,7 +574,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None:
default_conf['ask_strategy']['use_sell_signal'] = False
default_conf['use_sell_signal'] = False
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
patch_exchange(mocker)
@ -819,7 +819,7 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir):
@pytest.mark.filterwarnings("ignore:deprecated")
def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
default_conf['ask_strategy'].update({
default_conf.update({
"use_sell_signal": True,
"sell_profit_only": False,
"sell_profit_offset": 0.0,
@ -894,7 +894,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
@pytest.mark.filterwarnings("ignore:deprecated")
def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdatadir, capsys):
default_conf['ask_strategy'].update({
default_conf.update({
"use_sell_signal": True,
"sell_profit_only": False,
"sell_profit_offset": 0.0,
@ -993,4 +993,5 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
assert 'BACKTESTING REPORT' in captured.out
assert 'SELL REASON STATS' in captured.out
assert 'LEFT OPEN TRADES REPORT' in captured.out
assert '2017-11-14 21:17:00 -> 2017-11-14 22:58:00 | Max open trades : 1' in captured.out
assert 'STRATEGY SUMMARY' in captured.out

View File

@ -1,9 +1,6 @@
# pragma pylint: disable=missing-docstring,W0212,C0103
import logging
import re
from datetime import datetime
from pathlib import Path
from typing import Dict, List
from unittest.mock import ANY, MagicMock
import pandas as pd
@ -28,12 +25,6 @@ from tests.conftest import (get_args, log_has, log_has_re, patch_exchange,
from .hyperopts.default_hyperopt import DefaultHyperOpt
# Functions for recurrent object patching
def create_results() -> List[Dict]:
return [{'loss': 1, 'result': 'foo', 'params': {}, 'is_best': True}]
def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None:
patched_configuration_load_config_file(mocker, default_conf)
@ -303,52 +294,6 @@ def test_no_log_if_loss_does_not_improve(hyperopt, caplog) -> None:
assert caplog.record_tuples == []
def test_save_results_saves_epochs(mocker, hyperopt, tmpdir, caplog) -> None:
# Test writing to temp dir and reading again
epochs = create_results()
hyperopt.results_file = Path(tmpdir / 'ut_results.fthypt')
caplog.set_level(logging.DEBUG)
for epoch in epochs:
hyperopt._save_result(epoch)
assert log_has(f"1 epoch saved to '{hyperopt.results_file}'.", caplog)
hyperopt._save_result(epochs[0])
assert log_has(f"2 epochs saved to '{hyperopt.results_file}'.", caplog)
hyperopt_epochs = HyperoptTools.load_previous_results(hyperopt.results_file)
assert len(hyperopt_epochs) == 2
def test_load_previous_results(testdatadir, caplog) -> None:
results_file = testdatadir / 'hyperopt_results_SampleStrategy.pickle'
hyperopt_epochs = HyperoptTools.load_previous_results(results_file)
assert len(hyperopt_epochs) == 5
assert log_has_re(r"Reading pickled epochs from .*", caplog)
caplog.clear()
# Modern version
results_file = testdatadir / 'strategy_SampleStrategy.fthypt'
hyperopt_epochs = HyperoptTools.load_previous_results(results_file)
assert len(hyperopt_epochs) == 5
assert log_has_re(r"Reading epochs from .*", caplog)
def test_load_previous_results2(mocker, testdatadir, caplog) -> None:
mocker.patch('freqtrade.optimize.hyperopt_tools.HyperoptTools._read_results_pickle',
return_value=[{'asdf': '222'}])
results_file = testdatadir / 'hyperopt_results_SampleStrategy.pickle'
with pytest.raises(OperationalException, match=r"The file .* incompatible.*"):
HyperoptTools.load_previous_results(results_file)
def test_roi_table_generation(hyperopt) -> None:
params = {
'roi_t1': 5,
@ -362,6 +307,18 @@ def test_roi_table_generation(hyperopt) -> None:
assert hyperopt.custom_hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0}
def test_params_no_optimize_details(hyperopt) -> None:
hyperopt.config['spaces'] = ['buy']
res = hyperopt._get_no_optimize_details()
assert isinstance(res, dict)
assert "trailing" in res
assert res["trailing"]['trailing_stop'] is False
assert "roi" in res
assert res['roi']['0'] == 0.04
assert "stoploss" in res
assert res['stoploss']['stoploss'] == -0.1
def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None:
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
@ -467,40 +424,6 @@ def test_hyperopt_format_results(hyperopt):
assert '0:50:00 min' in result
@pytest.mark.parametrize("spaces, expected_results", [
(['buy'],
{'buy': True, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False}),
(['sell'],
{'buy': False, 'sell': True, 'roi': False, 'stoploss': False, 'trailing': False}),
(['roi'],
{'buy': False, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False}),
(['stoploss'],
{'buy': False, 'sell': False, 'roi': False, 'stoploss': True, 'trailing': False}),
(['trailing'],
{'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': True}),
(['buy', 'sell', 'roi', 'stoploss'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False}),
(['buy', 'sell', 'roi', 'stoploss', 'trailing'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True}),
(['buy', 'roi'],
{'buy': True, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False}),
(['all'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True}),
(['default'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False}),
(['default', 'trailing'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True}),
(['all', 'buy'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True}),
(['default', 'buy'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False}),
])
def test_has_space(hyperopt_conf, spaces, expected_results):
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']:
hyperopt_conf.update({'spaces': spaces})
assert HyperoptTools.has_space(hyperopt_conf, s) == expected_results[s]
def test_populate_indicators(hyperopt, testdatadir) -> None:
data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True)
dataframes = hyperopt.backtesting.strategy.ohlcvdata_to_dataframe(data)
@ -686,6 +609,8 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
def test_clean_hyperopt(mocker, hyperopt_conf, caplog):
patch_exchange(mocker)
mocker.patch("freqtrade.strategy.hyper.HyperStrategyMixin.load_params_from_file",
MagicMock(return_value={}))
mocker.patch("freqtrade.optimize.hyperopt.Path.is_file", MagicMock(return_value=True))
unlinkmock = mocker.patch("freqtrade.optimize.hyperopt.Path.unlink", MagicMock())
h = Hyperopt(hyperopt_conf)
@ -1068,42 +993,6 @@ def test_simplified_interface_failed(mocker, hyperopt_conf, method, space) -> No
hyperopt.start()
def test_show_epoch_details(capsys):
test_result = {
'params_details': {
'trailing': {
'trailing_stop': True,
'trailing_stop_positive': 0.02,
'trailing_stop_positive_offset': 0.04,
'trailing_only_offset_is_reached': True
},
'roi': {
0: 0.18,
90: 0.14,
225: 0.05,
430: 0},
},
'results_explanation': 'foo result',
'is_initial_point': False,
'total_profit': 0,
'current_epoch': 2, # This starts from 1 (in a human-friendly manner)
'is_best': True
}
HyperoptTools.show_epoch_details(test_result, 5, False, no_header=True)
captured = capsys.readouterr()
assert '# Trailing stop:' in captured.out
# re.match(r"Pairs for .*", captured.out)
assert re.search(r'^\s+trailing_stop = True$', captured.out, re.MULTILINE)
assert re.search(r'^\s+trailing_stop_positive = 0.02$', captured.out, re.MULTILINE)
assert re.search(r'^\s+trailing_stop_positive_offset = 0.04$', captured.out, re.MULTILINE)
assert re.search(r'^\s+trailing_only_offset_is_reached = True$', captured.out, re.MULTILINE)
assert '# ROI table:' in captured.out
assert re.search(r'^\s+minimal_roi = \{$', captured.out, re.MULTILINE)
assert re.search(r'^\s+\"90\"\:\s0.14,\s*$', captured.out, re.MULTILINE)
def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
@ -1143,17 +1032,3 @@ def test_SKDecimal():
assert space.transform([2.0]) == [200]
assert space.transform([1.0]) == [100]
assert space.transform([1.5, 1.6]) == [150, 160]
def test___pprint():
params = {'buy_std': 1.2, 'buy_rsi': 31, 'buy_enable': True, 'buy_what': 'asdf'}
non_params = {'buy_notoptimied': 55}
x = HyperoptTools._pprint(params, non_params)
assert x == """{
"buy_std": 1.2,
"buy_rsi": 31,
"buy_enable": True,
"buy_what": "asdf",
"buy_notoptimied": 55, # value loaded from strategy
}"""

View File

@ -0,0 +1,317 @@
import logging
import re
from pathlib import Path
from typing import Dict, List
import numpy as np
import pytest
import rapidjson
from freqtrade.constants import FTHYPT_FILEVERSION
from freqtrade.exceptions import OperationalException
from freqtrade.optimize.hyperopt_tools import HyperoptTools, hyperopt_serializer
from tests.conftest import log_has, log_has_re
# Functions for recurrent object patching
def create_results() -> List[Dict]:
return [{'loss': 1, 'result': 'foo', 'params': {}, 'is_best': True}]
def test_save_results_saves_epochs(hyperopt, tmpdir, caplog) -> None:
# Test writing to temp dir and reading again
epochs = create_results()
hyperopt.results_file = Path(tmpdir / 'ut_results.fthypt')
caplog.set_level(logging.DEBUG)
for epoch in epochs:
hyperopt._save_result(epoch)
assert log_has(f"1 epoch saved to '{hyperopt.results_file}'.", caplog)
hyperopt._save_result(epochs[0])
assert log_has(f"2 epochs saved to '{hyperopt.results_file}'.", caplog)
hyperopt_epochs = HyperoptTools.load_previous_results(hyperopt.results_file)
assert len(hyperopt_epochs) == 2
def test_load_previous_results(testdatadir, caplog) -> None:
results_file = testdatadir / 'hyperopt_results_SampleStrategy.pickle'
hyperopt_epochs = HyperoptTools.load_previous_results(results_file)
assert len(hyperopt_epochs) == 5
assert log_has_re(r"Reading pickled epochs from .*", caplog)
caplog.clear()
# Modern version
results_file = testdatadir / 'strategy_SampleStrategy.fthypt'
hyperopt_epochs = HyperoptTools.load_previous_results(results_file)
assert len(hyperopt_epochs) == 5
assert log_has_re(r"Reading epochs from .*", caplog)
def test_load_previous_results2(mocker, testdatadir, caplog) -> None:
mocker.patch('freqtrade.optimize.hyperopt_tools.HyperoptTools._read_results_pickle',
return_value=[{'asdf': '222'}])
results_file = testdatadir / 'hyperopt_results_SampleStrategy.pickle'
with pytest.raises(OperationalException, match=r"The file .* incompatible.*"):
HyperoptTools.load_previous_results(results_file)
@pytest.mark.parametrize("spaces, expected_results", [
(['buy'],
{'buy': True, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False}),
(['sell'],
{'buy': False, 'sell': True, 'roi': False, 'stoploss': False, 'trailing': False}),
(['roi'],
{'buy': False, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False}),
(['stoploss'],
{'buy': False, 'sell': False, 'roi': False, 'stoploss': True, 'trailing': False}),
(['trailing'],
{'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': True}),
(['buy', 'sell', 'roi', 'stoploss'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False}),
(['buy', 'sell', 'roi', 'stoploss', 'trailing'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True}),
(['buy', 'roi'],
{'buy': True, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False}),
(['all'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True}),
(['default'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False}),
(['default', 'trailing'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True}),
(['all', 'buy'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True}),
(['default', 'buy'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False}),
])
def test_has_space(hyperopt_conf, spaces, expected_results):
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']:
hyperopt_conf.update({'spaces': spaces})
assert HyperoptTools.has_space(hyperopt_conf, s) == expected_results[s]
def test_show_epoch_details(capsys):
test_result = {
'params_details': {
'trailing': {
'trailing_stop': True,
'trailing_stop_positive': 0.02,
'trailing_stop_positive_offset': 0.04,
'trailing_only_offset_is_reached': True
},
'roi': {
0: 0.18,
90: 0.14,
225: 0.05,
430: 0},
},
'results_explanation': 'foo result',
'is_initial_point': False,
'total_profit': 0,
'current_epoch': 2, # This starts from 1 (in a human-friendly manner)
'is_best': True
}
HyperoptTools.show_epoch_details(test_result, 5, False, no_header=True)
captured = capsys.readouterr()
assert '# Trailing stop:' in captured.out
# re.match(r"Pairs for .*", captured.out)
assert re.search(r'^\s+trailing_stop = True$', captured.out, re.MULTILINE)
assert re.search(r'^\s+trailing_stop_positive = 0.02$', captured.out, re.MULTILINE)
assert re.search(r'^\s+trailing_stop_positive_offset = 0.04$', captured.out, re.MULTILINE)
assert re.search(r'^\s+trailing_only_offset_is_reached = True$', captured.out, re.MULTILINE)
assert '# ROI table:' in captured.out
assert re.search(r'^\s+minimal_roi = \{$', captured.out, re.MULTILINE)
assert re.search(r'^\s+\"90\"\:\s0.14,\s*$', captured.out, re.MULTILINE)
def test__pprint_dict():
params = {'buy_std': 1.2, 'buy_rsi': 31, 'buy_enable': True, 'buy_what': 'asdf'}
non_params = {'buy_notoptimied': 55}
x = HyperoptTools._pprint_dict(params, non_params)
assert x == """{
"buy_std": 1.2,
"buy_rsi": 31,
"buy_enable": True,
"buy_what": "asdf",
"buy_notoptimied": 55, # value loaded from strategy
}"""
def test_get_strategy_filename(default_conf):
x = HyperoptTools.get_strategy_filename(default_conf, 'DefaultStrategy')
assert isinstance(x, Path)
assert x == Path(__file__).parents[1] / 'strategy/strats/default_strategy.py'
x = HyperoptTools.get_strategy_filename(default_conf, 'NonExistingStrategy')
assert x is None
def test_export_params(tmpdir):
filename = Path(tmpdir) / "DefaultStrategy.json"
assert not filename.is_file()
params = {
"params_details": {
"buy": {
"buy_rsi": 30
},
"sell": {
"sell_rsi": 70
},
"roi": {
"0": 0.528,
"346": 0.08499,
"507": 0.049,
"1595": 0
}
},
"params_not_optimized": {
"stoploss": -0.05,
"trailing": {
"trailing_stop": False,
"trailing_stop_positive": 0.05,
"trailing_stop_positive_offset": 0.1,
"trailing_only_offset_is_reached": True
},
}
}
HyperoptTools.export_params(params, "DefaultStrategy", filename)
assert filename.is_file()
content = rapidjson.load(filename.open('r'))
assert content['strategy_name'] == 'DefaultStrategy'
assert 'params' in content
assert "buy" in content["params"]
assert "sell" in content["params"]
assert "roi" in content["params"]
assert "stoploss" in content["params"]
assert "trailing" in content["params"]
def test_try_export_params(default_conf, tmpdir, caplog, mocker):
default_conf['disableparamexport'] = False
export_mock = mocker.patch("freqtrade.optimize.hyperopt_tools.HyperoptTools.export_params")
filename = Path(tmpdir) / "DefaultStrategy.json"
assert not filename.is_file()
params = {
"params_details": {
"buy": {
"buy_rsi": 30
},
"sell": {
"sell_rsi": 70
},
"roi": {
"0": 0.528,
"346": 0.08499,
"507": 0.049,
"1595": 0
}
},
"params_not_optimized": {
"stoploss": -0.05,
"trailing": {
"trailing_stop": False,
"trailing_stop_positive": 0.05,
"trailing_stop_positive_offset": 0.1,
"trailing_only_offset_is_reached": True
},
},
FTHYPT_FILEVERSION: 2,
}
HyperoptTools.try_export_params(default_conf, "DefaultStrategy22", params)
assert log_has("Strategy not found, not exporting parameter file.", caplog)
assert export_mock.call_count == 0
caplog.clear()
HyperoptTools.try_export_params(default_conf, "DefaultStrategy", params)
assert export_mock.call_count == 1
assert export_mock.call_args_list[0][0][1] == 'DefaultStrategy'
assert export_mock.call_args_list[0][0][2].name == 'default_strategy.json'
def test_params_print(capsys):
params = {
"buy": {
"buy_rsi": 30
},
"sell": {
"sell_rsi": 70
},
}
non_optimized = {
"buy": {
"buy_adx": 44
},
"sell": {
"sell_adx": 65
},
"stoploss": {
"stoploss": -0.05,
},
"roi": {
"0": 0.05,
"20": 0.01,
},
"trailing": {
"trailing_stop": False,
"trailing_stop_positive": 0.05,
"trailing_stop_positive_offset": 0.1,
"trailing_only_offset_is_reached": True
},
}
HyperoptTools._params_pretty_print(params, 'buy', 'No header', non_optimized)
captured = capsys.readouterr()
assert re.search("# No header", captured.out)
assert re.search('"buy_rsi": 30,\n', captured.out)
assert re.search('"buy_adx": 44, # value loaded.*\n', captured.out)
assert not re.search("sell", captured.out)
HyperoptTools._params_pretty_print(params, 'sell', 'Sell Header', non_optimized)
captured = capsys.readouterr()
assert re.search("# Sell Header", captured.out)
assert re.search('"sell_rsi": 70,\n', captured.out)
assert re.search('"sell_adx": 65, # value loaded.*\n', captured.out)
HyperoptTools._params_pretty_print(params, 'roi', 'ROI Table:', non_optimized)
captured = capsys.readouterr()
assert re.search("# ROI Table: # value loaded.*\n", captured.out)
assert re.search('minimal_roi = {\n', captured.out)
assert re.search('"20": 0.01\n', captured.out)
HyperoptTools._params_pretty_print(params, 'trailing', 'Trailing stop:', non_optimized)
captured = capsys.readouterr()
assert re.search("# Trailing stop:", captured.out)
assert re.search('trailing_stop = False # value loaded.*\n', captured.out)
assert re.search('trailing_stop_positive = 0.05 # value loaded.*\n', captured.out)
assert re.search('trailing_stop_positive_offset = 0.1 # value loaded.*\n', captured.out)
assert re.search('trailing_only_offset_is_reached = True # value loaded.*\n', captured.out)
def test_hyperopt_serializer():
assert isinstance(hyperopt_serializer(np.int_(5)), int)
assert isinstance(hyperopt_serializer(np.bool_(True)), bool)
assert isinstance(hyperopt_serializer(np.bool_(False)), bool)

View File

@ -63,6 +63,48 @@ def test_init_dryrun_db(default_conf, tmpdir):
assert Path(filename).is_file()
@pytest.mark.usefixtures("init_persistence")
def test_is_opening_closing_trade(fee):
trade = Trade(
id=2,
pair='ETH/BTC',
stake_amount=0.001,
open_rate=0.01,
amount=5,
is_open=True,
open_date=arrow.utcnow().datetime,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='binance',
is_short=False,
leverage=2.0
)
assert trade.is_opening_trade('buy') is True
assert trade.is_opening_trade('sell') is False
assert trade.is_closing_trade('buy') is False
assert trade.is_closing_trade('sell') is True
trade = Trade(
id=2,
pair='ETH/BTC',
stake_amount=0.001,
open_rate=0.01,
amount=5,
is_open=True,
open_date=arrow.utcnow().datetime,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='binance',
is_short=True,
leverage=2.0
)
assert trade.is_opening_trade('buy') is False
assert trade.is_opening_trade('sell') is True
assert trade.is_closing_trade('buy') is True
assert trade.is_closing_trade('sell') is False
@pytest.mark.usefixtures("init_persistence")
def test_update_with_binance(limit_buy_order, limit_sell_order, fee, caplog):
"""
@ -129,9 +171,6 @@ def test_update_with_binance(limit_buy_order, limit_sell_order, fee, caplog):
r"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=.*\).",
caplog)
# TODO-mg: create a short order
# TODO-mg: create a leveraged long order
@pytest.mark.usefixtures("init_persistence")
def test_update_market_order(market_buy_order, market_sell_order, fee, caplog):
@ -170,9 +209,6 @@ def test_update_market_order(market_buy_order, market_sell_order, fee, caplog):
r"pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=.*\).",
caplog)
# TODO-mg: market short
# TODO-mg: market leveraged long
@pytest.mark.usefixtures("init_persistence")
def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee):
@ -202,6 +238,7 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee):
@pytest.mark.usefixtures("init_persistence")
def test_trade_close(limit_buy_order, limit_sell_order, fee):
# TODO: limit_buy_order and limit_sell_order aren't used, remove them probably
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
@ -665,13 +702,11 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
order_date DATETIME,
order_filled_date DATETIME,
order_update_date DATETIME,
leverage FLOAT,
PRIMARY KEY (id),
CONSTRAINT _order_pair_order_id UNIQUE (ft_pair, order_id),
FOREIGN KEY(ft_trade_id) REFERENCES trades (id)
)
"""))
# TODO-mg: Had to add field leverage to this table, check that this is correct
connection.execute(text("""
insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status,
@ -695,6 +730,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
assert orders[1].order_id == 'stop_order_id222'
assert orders[1].ft_order_side == 'stoploss'
assert orders[0].is_short is False
def test_migrate_mid_state(mocker, default_conf, fee, caplog):
@ -920,11 +956,7 @@ def test_to_json(default_conf, fee):
'strategy': None,
'timeframe': None,
'exchange': 'binance',
'leverage': None,
'borrowed': None,
'borrowed_currency': None,
'collateral_currency': None,
'interest_rate': None,
'liquidation_price': None,
'is_short': None,
@ -993,11 +1025,7 @@ def test_to_json(default_conf, fee):
'strategy': None,
'timeframe': None,
'exchange': 'binance',
'leverage': None,
'borrowed': None,
'borrowed_currency': None,
'collateral_currency': None,
'interest_rate': None,
'liquidation_price': None,
'is_short': None,

View File

@ -0,0 +1,638 @@
from datetime import datetime, timedelta
from math import isclose
import pytest
from freqtrade.enums import InterestMode
from freqtrade.persistence import Trade
from tests.conftest import log_has_re
@pytest.mark.usefixtures("init_persistence")
def test_interest_kraken_lev(market_lev_buy_order, fee):
"""
Market trade on Kraken at 3x and 5x leverage
Short trade
interest_rate: 0.05%, 0.25% per 4 hrs
open_rate: 0.00004099 base
close_rate: 0.00004173 base
stake_amount: 0.0037707443218227
borrowed: 0.0075414886436454
amount:
275.97543219 crypto
459.95905365 crypto
borrowed:
0.0075414886436454 base
0.0150829772872908 base
time-periods: 10 minutes(rounds up to 1 time-period of 4hrs)
5 hours = 5/4
interest: borrowed * interest_rate * ceil(1 + time-periods)
= 0.0075414886436454 * 0.0005 * ceil(2) = 7.5414886436454e-06 base
= 0.0075414886436454 * 0.00025 * ceil(9/4) = 5.65611648273405e-06 base
= 0.0150829772872908 * 0.0005 * ceil(9/4) = 2.26244659309362e-05 base
= 0.0150829772872908 * 0.00025 * ceil(2) = 7.5414886436454e-06 base
"""
trade = Trade(
pair='ETH/BTC',
stake_amount=0.0037707443218227,
amount=275.97543219,
open_rate=0.00001099,
open_date=datetime.utcnow() - timedelta(hours=0, minutes=10),
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='kraken',
leverage=3.0,
interest_rate=0.0005,
interest_mode=InterestMode.HOURSPER4
)
assert float(trade.calculate_interest()) == 7.5414886436454e-06
trade.open_date = datetime.utcnow() - timedelta(hours=4, minutes=55)
assert float(round(trade.calculate_interest(interest_rate=0.00025), 11)
) == round(5.65611648273405e-06, 11)
trade = Trade(
pair='ETH/BTC',
stake_amount=0.0037707443218227,
amount=459.95905365,
open_rate=0.00001099,
open_date=datetime.utcnow() - timedelta(hours=4, minutes=55),
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='kraken',
leverage=5.0,
interest_rate=0.0005,
interest_mode=InterestMode.HOURSPER4
)
assert float(round(trade.calculate_interest(), 11)
) == round(2.26244659309362e-05, 11)
trade.open_date = datetime.utcnow() - timedelta(hours=0, minutes=10)
trade.interest_rate = 0.00025
assert float(trade.calculate_interest(interest_rate=0.00025)) == 7.5414886436454e-06
@pytest.mark.usefixtures("init_persistence")
def test_interest_binance_lev(market_lev_buy_order, fee):
"""
Market trade on Kraken at 3x and 5x leverage
Short trade
interest_rate: 0.05%, 0.25% per 4 hrs
open_rate: 0.00001099 base
close_rate: 0.00001173 base
stake_amount: 0.0009999999999226999
borrowed: 0.0019999999998453998
amount:
90.99181073 * leverage(3) = 272.97543219 crypto
90.99181073 * leverage(5) = 454.95905365 crypto
borrowed:
0.0019999999998453998 base
0.0039999999996907995 base
time-periods: 10 minutes(rounds up to 1/24 time-period of 24hrs)
5 hours = 5/24
interest: borrowed * interest_rate * time-periods
= 0.0019999999998453998 * 0.00050 * 1/24 = 4.166666666344583e-08 base
= 0.0019999999998453998 * 0.00025 * 5/24 = 1.0416666665861459e-07 base
= 0.0039999999996907995 * 0.00050 * 5/24 = 4.1666666663445834e-07 base
= 0.0039999999996907995 * 0.00025 * 1/24 = 4.166666666344583e-08 base
"""
trade = Trade(
pair='ETH/BTC',
stake_amount=0.0009999999999226999,
amount=272.97543219,
open_rate=0.00001099,
open_date=datetime.utcnow() - timedelta(hours=0, minutes=10),
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='binance',
leverage=3.0,
interest_rate=0.0005,
interest_mode=InterestMode.HOURSPERDAY
)
# 10 minutes round up to 4 hours evenly on kraken so we can predict the them more accurately
assert round(float(trade.calculate_interest()), 22) == round(4.166666666344583e-08, 22)
trade.open_date = datetime.utcnow() - timedelta(hours=4, minutes=55)
# All trade > 5 hours will vary slightly due to execution time and interest calculated
assert float(round(trade.calculate_interest(interest_rate=0.00025), 14)
) == round(1.0416666665861459e-07, 14)
trade = Trade(
pair='ETH/BTC',
stake_amount=0.0009999999999226999,
amount=459.95905365,
open_rate=0.00001099,
open_date=datetime.utcnow() - timedelta(hours=4, minutes=55),
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='binance',
leverage=5.0,
interest_rate=0.0005,
interest_mode=InterestMode.HOURSPERDAY
)
assert float(round(trade.calculate_interest(), 14)) == round(4.1666666663445834e-07, 14)
trade.open_date = datetime.utcnow() - timedelta(hours=0, minutes=10)
assert float(round(trade.calculate_interest(interest_rate=0.00025), 22)
) == round(4.166666666344583e-08, 22)
@pytest.mark.usefixtures("init_persistence")
def test_update_open_order_lev(limit_lev_buy_order):
trade = Trade(
pair='ETH/BTC',
stake_amount=1.00,
open_rate=0.01,
amount=5,
fee_open=0.1,
fee_close=0.1,
interest_rate=0.0005,
exchange='binance',
interest_mode=InterestMode.HOURSPERDAY
)
assert trade.open_order_id is None
assert trade.close_profit is None
assert trade.close_date is None
limit_lev_buy_order['status'] = 'open'
trade.update(limit_lev_buy_order)
assert trade.open_order_id is None
assert trade.close_profit is None
assert trade.close_date is None
@pytest.mark.usefixtures("init_persistence")
def test_calc_open_trade_value_lev(market_lev_buy_order, fee):
"""
10 minute leveraged market trade on Kraken at 3x leverage
Short trade
fee: 0.25% base
interest_rate: 0.05% per 4 hrs
open_rate: 0.00004099 base
close_rate: 0.00004173 base
amount: 91.99181073 * leverage(3) = 275.97543219 crypto
stake_amount: 0.0037707443218227
borrowed: 0.0075414886436454 base
time-periods: 10 minutes(rounds up to 1 time-period of 4hrs)
interest: borrowed * interest_rate * time-periods
= 0.0075414886436454 * 0.0005 * 1 = 7.5414886436454e-06 crypto
open_value: (amount * open_rate) + (amount * open_rate * fee)
= (275.97543219 * 0.00004099) + (275.97543219 * 0.00004099 * 0.0025)
= 0.01134051354788177
"""
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
amount=5,
open_rate=0.00004099,
open_date=datetime.utcnow() - timedelta(hours=0, minutes=10),
fee_open=fee.return_value,
fee_close=fee.return_value,
interest_rate=0.0005,
exchange='kraken',
leverage=3,
interest_mode=InterestMode.HOURSPER4
)
trade.open_order_id = 'open_trade'
trade.update(market_lev_buy_order) # Buy @ 0.00001099
# Get the open rate price with the standard fee rate
assert trade._calc_open_trade_value() == 0.01134051354788177
trade.fee_open = 0.003
# Get the open rate price with a custom fee rate
assert trade._calc_open_trade_value() == 0.011346169664364504
@pytest.mark.usefixtures("init_persistence")
def test_calc_open_close_trade_price_lev(limit_lev_buy_order, limit_lev_sell_order, fee):
"""
5 hour leveraged trade on Binance
fee: 0.25% base
interest_rate: 0.05% per day
open_rate: 0.00001099 base
close_rate: 0.00001173 base
amount: 272.97543219 crypto
stake_amount: 0.0009999999999226999 base
borrowed: 0.0019999999998453998 base
time-periods: 5 hours(rounds up to 5/24 time-period of 1 day)
interest: borrowed * interest_rate * time-periods
= 0.0019999999998453998 * 0.0005 * 5/24 = 2.0833333331722917e-07 base
open_value: (amount * open_rate) + (amount * open_rate * fee)
= (272.97543219 * 0.00001099) + (272.97543219 * 0.00001099 * 0.0025)
= 0.0030074999997675204
close_value: ((amount_closed * close_rate) - (amount_closed * close_rate * fee)) - interest
= (272.97543219 * 0.00001173)
- (272.97543219 * 0.00001173 * 0.0025)
- 2.0833333331722917e-07
= 0.003193788481706411
stake_value: (amount/lev * open_rate) + (amount/lev * open_rate * fee)
= (272.97543219/3 * 0.00001099) + (272.97543219/3 * 0.00001099 * 0.0025)
= 0.0010024999999225066
total_profit = close_value - open_value
= 0.003193788481706411 - 0.0030074999997675204
= 0.00018628848193889044
total_profit_percentage = total_profit / stake_value
= 0.00018628848193889054 / 0.0010024999999225066
= 0.18582392214792087
"""
trade = Trade(
pair='ETH/BTC',
stake_amount=0.0009999999999226999,
open_rate=0.01,
amount=5,
open_date=datetime.utcnow() - timedelta(hours=4, minutes=55),
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='binance',
interest_rate=0.0005,
interest_mode=InterestMode.HOURSPERDAY
)
trade.open_order_id = 'something'
trade.update(limit_lev_buy_order)
assert trade._calc_open_trade_value() == 0.00300749999976752
trade.update(limit_lev_sell_order)
# Is slightly different due to compilation time changes. Interest depends on time
assert round(trade.calc_close_trade_value(), 11) == round(0.003193788481706411, 11)
# Profit in BTC
assert round(trade.calc_profit(), 8) == round(0.00018628848193889054, 8)
# Profit in percent
assert round(trade.calc_profit_ratio(), 8) == round(0.18582392214792087, 8)
@pytest.mark.usefixtures("init_persistence")
def test_trade_close_lev(fee):
"""
5 hour leveraged market trade on Kraken at 3x leverage
fee: 0.25% base
interest_rate: 0.05% per 4 hrs
open_rate: 0.1 base
close_rate: 0.2 base
amount: 5 * leverage(3) = 15 crypto
stake_amount: 0.5
borrowed: 1 base
time-periods: 5/4 periods of 4hrs
interest: borrowed * interest_rate * ceil(1 + time-periods)
= 1 * 0.0005 * ceil(9/4) = 0.0015 crypto
open_value: (amount * open_rate) + (amount * open_rate * fee)
= (15 * 0.1) + (15 * 0.1 * 0.0025)
= 1.50375
close_value: (amount * close_rate) + (amount * close_rate * fee) - interest
= (15 * 0.2) - (15 * 0.2 * 0.0025) - 0.0015
= 2.991
total_profit = close_value - open_value
= 2.991 - 1.50375
= 1.4872500000000002
total_profit_ratio = ((close_value/open_value) - 1) * leverage
= ((2.991/1.50375) - 1) * 3
= 2.96708229426434
"""
trade = Trade(
pair='ETH/BTC',
stake_amount=0.5,
open_rate=0.1,
amount=15,
is_open=True,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_date=datetime.utcnow() - timedelta(hours=4, minutes=55),
exchange='kraken',
leverage=3.0,
interest_rate=0.0005,
interest_mode=InterestMode.HOURSPER4
)
assert trade.close_profit is None
assert trade.close_date is None
assert trade.is_open is True
trade.close(0.2)
assert trade.is_open is False
assert trade.close_profit == round(2.96708229426434, 8)
assert trade.close_date is not None
# TODO-mg: Remove these comments probably
# new_date = arrow.Arrow(2020, 2, 2, 15, 6, 1).datetime,
# assert trade.close_date != new_date
# # Close should NOT update close_date if the trade has been closed already
# assert trade.is_open is False
# trade.close_date = new_date
# trade.close(0.02)
# assert trade.close_date == new_date
@pytest.mark.usefixtures("init_persistence")
def test_calc_close_trade_price_lev(market_lev_buy_order, market_lev_sell_order, fee):
"""
10 minute leveraged market trade on Kraken at 3x leverage
Short trade
fee: 0.25% base
interest_rate: 0.05% per 4 hrs
open_rate: 0.00004099 base
close_rate: 0.00004173 base
amount: 91.99181073 * leverage(3) = 275.97543219 crypto
stake_amount: 0.0037707443218227
borrowed: 0.0075414886436454 base
time-periods: 10 minutes = 2
interest: borrowed * interest_rate * time-periods
= 0.0075414886436454 * 0.0005 * 2 = 7.5414886436454e-06 crypto
open_value: (amount * open_rate) + (amount * open_rate * fee)
= (275.97543219 * 0.00004099) + (275.97543219 * 0.00004099 * 0.0025)
= 0.01134051354788177
close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) - interest
= (275.97543219 * 0.00001234) - (275.97543219 * 0.00001234 * 0.0025) - 7.5414886436454e-06
= 0.0033894815024978933
= (275.97543219 * 0.00001234) - (275.97543219 * 0.00001234 * 0.003) - 7.5414886436454e-06
= 0.003387778734081281
= (275.97543219 * 0.00004173) - (275.97543219 * 0.00004173 * 0.005) - 7.5414886436454e-06
= 0.011451331022718612
"""
trade = Trade(
pair='ETH/BTC',
stake_amount=0.0037707443218227,
amount=5,
open_rate=0.00004099,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_date=datetime.utcnow() - timedelta(hours=0, minutes=10),
interest_rate=0.0005,
leverage=3.0,
exchange='kraken',
interest_mode=InterestMode.HOURSPER4
)
trade.open_order_id = 'close_trade'
trade.update(market_lev_buy_order) # Buy @ 0.00001099
# Get the close rate price with a custom close rate and a regular fee rate
assert isclose(trade.calc_close_trade_value(rate=0.00001234), 0.0033894815024978933)
# Get the close rate price with a custom close rate and a custom fee rate
assert isclose(trade.calc_close_trade_value(rate=0.00001234, fee=0.003), 0.003387778734081281)
# Test when we apply a Sell order, and ask price with a custom fee rate
trade.update(market_lev_sell_order)
assert isclose(trade.calc_close_trade_value(fee=0.005), 0.011451331022718612)
@pytest.mark.usefixtures("init_persistence")
def test_update_limit_order_lev(limit_lev_buy_order, limit_lev_sell_order, fee, caplog):
"""
10 minute leveraged limit trade on binance at 3x leverage
Leveraged trade
fee: 0.25% base
interest_rate: 0.05% per day
open_rate: 0.00001099 base
close_rate: 0.00001173 base
amount: 272.97543219 crypto
stake_amount: 0.0009999999999226999 base
borrowed: 0.0019999999998453998 base
time-periods: 10 minutes(rounds up to 1/24 time-period of 1 day)
interest: borrowed * interest_rate * time-periods
= 0.0019999999998453998 * 0.0005 * 1/24 = 4.166666666344583e-08 base
open_value: (amount * open_rate) + (amount * open_rate * fee)
= (272.97543219 * 0.00001099) + (272.97543219 * 0.00001099 * 0.0025)
= 0.0030074999997675204
stake_value = (amount/lev * open_rate) + (amount/lev * open_rate * fee)
= (272.97543219 * 0.00001099) + (272.97543219 * 0.00001099 * 0.0025)
= 0.0010024999999225066
close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee)
= (272.97543219 * 0.00001173) - (272.97543219 * 0.00001173 * 0.0025)
= 0.003193996815039728
stake_value: (amount/lev * open_rate) + (amount/lev * open_rate * fee)
= (272.97543219/3 * 0.00001099) + (272.97543219/3 * 0.00001099 * 0.0025)
= 0.0010024999999225066
total_profit = close_value - open_value - interest
= 0.003193996815039728 - 0.0030074999997675204 - 4.166666666344583e-08
= 0.00018645514860554435
total_profit_percentage = total_profit / stake_value
= 0.00018645514860554435 / 0.0010024999999225066
= 0.1859901731869899
"""
trade = Trade(
id=2,
pair='ETH/BTC',
stake_amount=0.0009999999999226999,
open_rate=0.01,
amount=5,
is_open=True,
open_date=datetime.utcnow() - timedelta(hours=0, minutes=10),
fee_open=fee.return_value,
fee_close=fee.return_value,
interest_rate=0.0005,
exchange='binance',
interest_mode=InterestMode.HOURSPERDAY
)
# assert trade.open_order_id is None
assert trade.close_profit is None
assert trade.close_date is None
# trade.open_order_id = 'something'
trade.update(limit_lev_buy_order)
# assert trade.open_order_id is None
assert trade.open_rate == 0.00001099
assert trade.close_profit is None
assert trade.close_date is None
assert trade.borrowed == 0.0019999999998453998
assert log_has_re(r"LIMIT_BUY has been fulfilled for Trade\(id=2, "
r"pair=ETH/BTC, amount=272.97543219, open_rate=0.00001099, open_since=.*\).",
caplog)
caplog.clear()
# trade.open_order_id = 'something'
trade.update(limit_lev_sell_order)
# assert trade.open_order_id is None
assert trade.close_rate == 0.00001173
assert trade.close_profit == round(0.1859901731869899, 8)
assert trade.close_date is not None
assert log_has_re(r"LIMIT_SELL has been fulfilled for Trade\(id=2, "
r"pair=ETH/BTC, amount=272.97543219, open_rate=0.00001099, open_since=.*\).",
caplog)
@pytest.mark.usefixtures("init_persistence")
def test_update_market_order_lev(market_lev_buy_order, market_lev_sell_order, fee, caplog):
"""
10 minute leveraged market trade on Kraken at 3x leverage
Short trade
fee: 0.25% base
interest_rate: 0.05% per 4 hrs
open_rate: 0.00004099 base
close_rate: 0.00004173 base
amount: = 275.97543219 crypto
stake_amount: 0.0037707443218227
borrowed: 0.0075414886436454 base
interest: borrowed * interest_rate * 1+ceil(hours)
= 0.0075414886436454 * 0.0005 * (1+ceil(1)) = 7.5414886436454e-06 crypto
open_value: (amount * open_rate) + (amount * open_rate * fee)
= (275.97543219 * 0.00004099) + (275.97543219 * 0.00004099 * 0.0025)
= 0.01134051354788177
close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) - interest
= (275.97543219 * 0.00004173) - (275.97543219 * 0.00004173 * 0.0025) - 7.5414886436454e-06
= 0.011480122159681833
stake_value: (amount/lev * open_rate) + (amount/lev * open_rate * fee)
= (275.97543219/3 * 0.00004099) + (275.97543219/3 * 0.00004099 * 0.0025)
= 0.0037801711826272568
total_profit = close_value - open_value
= 0.011480122159681833 - 0.01134051354788177
= 0.00013960861180006392
total_profit_percentage = ((close_value/open_value) - 1) * leverage
= ((0.011480122159681833 / 0.01134051354788177)-1) * 3
= 0.036931822675563275
"""
trade = Trade(
id=1,
pair='ETH/BTC',
stake_amount=0.0037707443218227,
amount=5,
open_rate=0.00004099,
is_open=True,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_date=datetime.utcnow() - timedelta(hours=0, minutes=10),
interest_rate=0.0005,
exchange='kraken',
interest_mode=InterestMode.HOURSPER4
)
trade.open_order_id = 'something'
trade.update(market_lev_buy_order)
assert trade.leverage == 3.0
assert trade.open_order_id is None
assert trade.open_rate == 0.00004099
assert trade.close_profit is None
assert trade.close_date is None
assert trade.interest_rate == 0.0005
# TODO: Uncomment the next assert and make it work.
# The logger also has the exact same but there's some spacing in there
assert log_has_re(r"MARKET_BUY has been fulfilled for Trade\(id=1, "
r"pair=ETH/BTC, amount=275.97543219, open_rate=0.00004099, open_since=.*\).",
caplog)
caplog.clear()
trade.is_open = True
trade.open_order_id = 'something'
trade.update(market_lev_sell_order)
assert trade.open_order_id is None
assert trade.close_rate == 0.00004173
assert trade.close_profit == round(0.036931822675563275, 8)
assert trade.close_date is not None
# TODO: The amount should maybe be the opening amount + the interest
# TODO: Uncomment the next assert and make it work.
# The logger also has the exact same but there's some spacing in there
assert log_has_re(r"MARKET_SELL has been fulfilled for Trade\(id=1, "
r"pair=ETH/BTC, amount=275.97543219, open_rate=0.00004099, open_since=.*\).",
caplog)
@pytest.mark.usefixtures("init_persistence")
def test_calc_close_trade_price_exception_lev(limit_lev_buy_order, fee):
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
open_rate=0.1,
amount=5,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='binance',
interest_rate=0.0005,
leverage=3.0,
interest_mode=InterestMode.HOURSPERDAY
)
trade.open_order_id = 'something'
trade.update(limit_lev_buy_order)
assert trade.calc_close_trade_value() == 0.0
@pytest.mark.usefixtures("init_persistence")
def test_calc_profit_lev(market_lev_buy_order, market_lev_sell_order, fee):
"""
Leveraged trade on Kraken at 3x leverage
fee: 0.25% base or 0.3%
interest_rate: 0.05%, 0.25% per 4 hrs
open_rate: 0.00004099 base
close_rate: 0.00004173 base
stake_amount: 0.0037707443218227
amount: 91.99181073 * leverage(3) = 275.97543219 crypto
borrowed: 0.0075414886436454 base
hours: 1/6, 5 hours
interest: borrowed * interest_rate * ceil(1+hours/4)
= 0.0075414886436454 * 0.0005 * ceil(1+((1/6)/4)) = 7.5414886436454e-06 crypto
= 0.0075414886436454 * 0.00025 * ceil(1+(5/4)) = 5.65611648273405e-06 crypto
= 0.0075414886436454 * 0.0005 * ceil(1+(5/4)) = 1.13122329654681e-05 crypto
= 0.0075414886436454 * 0.00025 * ceil(1+((1/6)/4)) = 3.7707443218227e-06 crypto
open_value: (amount * open_rate) + (amount * open_rate * fee)
= (275.97543219 * 0.00004099) + (275.97543219 * 0.00004099 * 0.0025)
= 0.01134051354788177
close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) - interest
(275.97543219 * 0.00005374) - (275.97543219 * 0.00005374 * 0.0025) - 7.5414886436454e-06
= 0.014786300937932227
(275.97543219 * 0.00000437) - (275.97543219 * 0.00000437 * 0.0025) - 5.65611648273405e-06
= 0.0011973414905908902
(275.97543219 * 0.00005374) - (275.97543219 * 0.00005374 * 0.003) - 1.13122329654681e-05
= 0.01477511473374746
(275.97543219 * 0.00000437) - (275.97543219 * 0.00000437 * 0.003) - 3.7707443218227e-06
= 0.0011986238564324662
stake_value: (amount/lev * open_rate) + (amount/lev * open_rate * fee)
= (275.97543219/3 * 0.00004099) + (275.97543219/3 * 0.00004099 * 0.0025)
= 0.0037801711826272568
total_profit = close_value - open_value
= 0.014786300937932227 - 0.01134051354788177 = 0.0034457873900504577
= 0.0011973414905908902 - 0.01134051354788177 = -0.01014317205729088
= 0.01477511473374746 - 0.01134051354788177 = 0.00343460118586569
= 0.0011986238564324662 - 0.01134051354788177 = -0.010141889691449303
total_profit_percentage = ((close_value/open_value) - 1) * leverage
((0.014786300937932227/0.01134051354788177) - 1) * 3 = 0.9115426851266561
((0.0011973414905908902/0.01134051354788177) - 1) * 3 = -2.683257336045103
((0.01477511473374746/0.01134051354788177) - 1) * 3 = 0.908583505860866
((0.0011986238564324662/0.01134051354788177) - 1) * 3 = -2.6829181011851926
"""
trade = Trade(
pair='ETH/BTC',
stake_amount=0.0037707443218227,
amount=5,
open_rate=0.00004099,
open_date=datetime.utcnow() - timedelta(hours=0, minutes=10),
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='kraken',
leverage=3.0,
interest_rate=0.0005,
interest_mode=InterestMode.HOURSPER4
)
trade.open_order_id = 'something'
trade.update(market_lev_buy_order) # Buy @ 0.00001099
# Custom closing rate and regular fee rate
# Higher than open rate
assert trade.calc_profit(rate=0.00005374, interest_rate=0.0005) == round(
0.0034457873900504577, 8)
assert trade.calc_profit_ratio(
rate=0.00005374, interest_rate=0.0005) == round(0.9115426851266561, 8)
# Lower than open rate
trade.open_date = datetime.utcnow() - timedelta(hours=4, minutes=55)
assert trade.calc_profit(
rate=0.00000437, interest_rate=0.00025) == round(-0.01014317205729088, 8)
assert trade.calc_profit_ratio(
rate=0.00000437, interest_rate=0.00025) == round(-2.683257336045103, 8)
# Custom closing rate and custom fee rate
# Higher than open rate
assert trade.calc_profit(rate=0.00005374, fee=0.003,
interest_rate=0.0005) == round(0.00343460118586569, 8)
assert trade.calc_profit_ratio(rate=0.00005374, fee=0.003,
interest_rate=0.0005) == round(0.908583505860866, 8)
# Lower than open rate
trade.open_date = datetime.utcnow() - timedelta(hours=0, minutes=10)
assert trade.calc_profit(rate=0.00000437, fee=0.003,
interest_rate=0.00025) == round(-0.010141889691449303, 8)
assert trade.calc_profit_ratio(rate=0.00000437, fee=0.003,
interest_rate=0.00025) == round(-2.6829181011851926, 8)
# Test when we apply a Sell order. Sell higher than open rate @ 0.00001173
trade.update(market_lev_sell_order)
assert trade.calc_profit() == round(0.00013960861180006392, 8)
assert trade.calc_profit_ratio() == round(0.036931822675563275, 8)
# Test with a custom fee rate on the close trade
# assert trade.calc_profit(fee=0.003) == 0.00006163
# assert trade.calc_profit_ratio(fee=0.003) == 0.06147824

View File

@ -0,0 +1,780 @@
from datetime import datetime, timedelta
from math import isclose
import arrow
import pytest
from freqtrade.enums import InterestMode
from freqtrade.persistence import Trade, init_db
from tests.conftest import create_mock_trades_with_leverage, log_has_re
@pytest.mark.usefixtures("init_persistence")
def test_interest_kraken_short(market_short_order, fee):
"""
Market trade on Kraken at 3x and 8x leverage
Short trade
interest_rate: 0.05%, 0.25% per 4 hrs
open_rate: 0.00004173 base
close_rate: 0.00004099 base
amount:
275.97543219 crypto
459.95905365 crypto
borrowed:
275.97543219 crypto
459.95905365 crypto
time-periods: 10 minutes(rounds up to 1 time-period of 4hrs)
5 hours = 5/4
interest: borrowed * interest_rate * ceil(1 + time-periods)
= 275.97543219 * 0.0005 * ceil(1+1) = 0.27597543219 crypto
= 275.97543219 * 0.00025 * ceil(9/4) = 0.20698157414249999 crypto
= 459.95905365 * 0.0005 * ceil(9/4) = 0.689938580475 crypto
= 459.95905365 * 0.00025 * ceil(1+1) = 0.229979526825 crypto
"""
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
amount=275.97543219,
open_rate=0.00001099,
open_date=datetime.utcnow() - timedelta(hours=0, minutes=10),
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='kraken',
is_short=True,
leverage=3.0,
interest_rate=0.0005,
interest_mode=InterestMode.HOURSPER4
)
assert float(round(trade.calculate_interest(), 8)) == round(0.27597543219, 8)
trade.open_date = datetime.utcnow() - timedelta(hours=4, minutes=55)
assert float(round(trade.calculate_interest(interest_rate=0.00025), 8)
) == round(0.20698157414249999, 8)
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
amount=459.95905365,
open_rate=0.00001099,
open_date=datetime.utcnow() - timedelta(hours=4, minutes=55),
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='kraken',
is_short=True,
leverage=5.0,
interest_rate=0.0005,
interest_mode=InterestMode.HOURSPER4
)
assert float(round(trade.calculate_interest(), 8)) == round(0.689938580475, 8)
trade.open_date = datetime.utcnow() - timedelta(hours=0, minutes=10)
assert float(round(trade.calculate_interest(interest_rate=0.00025), 8)
) == round(0.229979526825, 8)
@ pytest.mark.usefixtures("init_persistence")
def test_interest_binance_short(market_short_order, fee):
"""
Market trade on Binance at 3x and 5x leverage
Short trade
interest_rate: 0.05%, 0.25% per 1 day
open_rate: 0.00004173 base
close_rate: 0.00004099 base
amount:
91.99181073 * leverage(3) = 275.97543219 crypto
91.99181073 * leverage(5) = 459.95905365 crypto
borrowed:
275.97543219 crypto
459.95905365 crypto
time-periods: 10 minutes(rounds up to 1/24 time-period of 1 day)
5 hours = 5/24
interest: borrowed * interest_rate * time-periods
= 275.97543219 * 0.0005 * 1/24 = 0.005749488170625 crypto
= 275.97543219 * 0.00025 * 5/24 = 0.0143737204265625 crypto
= 459.95905365 * 0.0005 * 5/24 = 0.047912401421875 crypto
= 459.95905365 * 0.00025 * 1/24 = 0.0047912401421875 crypto
"""
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
amount=275.97543219,
open_rate=0.00001099,
open_date=datetime.utcnow() - timedelta(hours=0, minutes=10),
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='binance',
is_short=True,
leverage=3.0,
interest_rate=0.0005,
interest_mode=InterestMode.HOURSPERDAY
)
assert float(round(trade.calculate_interest(), 8)) == 0.00574949
trade.open_date = datetime.utcnow() - timedelta(hours=4, minutes=55)
assert float(round(trade.calculate_interest(interest_rate=0.00025), 8)) == 0.01437372
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
amount=459.95905365,
open_rate=0.00001099,
open_date=datetime.utcnow() - timedelta(hours=4, minutes=55),
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='binance',
is_short=True,
leverage=5.0,
interest_rate=0.0005,
interest_mode=InterestMode.HOURSPERDAY
)
assert float(round(trade.calculate_interest(), 8)) == 0.04791240
trade.open_date = datetime.utcnow() - timedelta(hours=0, minutes=10)
assert float(round(trade.calculate_interest(interest_rate=0.00025), 8)) == 0.00479124
@ pytest.mark.usefixtures("init_persistence")
def test_calc_open_trade_value_short(market_short_order, fee):
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
amount=5,
open_rate=0.00004173,
open_date=datetime.utcnow() - timedelta(hours=0, minutes=10),
fee_open=fee.return_value,
fee_close=fee.return_value,
interest_rate=0.0005,
is_short=True,
leverage=3.0,
exchange='kraken',
interest_mode=InterestMode.HOURSPER4
)
trade.open_order_id = 'open_trade'
trade.update(market_short_order) # Buy @ 0.00001099
# Get the open rate price with the standard fee rate
assert trade._calc_open_trade_value() == 0.011487663648325479
trade.fee_open = 0.003
# Get the open rate price with a custom fee rate
assert trade._calc_open_trade_value() == 0.011481905420932834
@ pytest.mark.usefixtures("init_persistence")
def test_update_open_order_short(limit_short_order):
trade = Trade(
pair='ETH/BTC',
stake_amount=1.00,
open_rate=0.01,
amount=5,
leverage=3.0,
fee_open=0.1,
fee_close=0.1,
interest_rate=0.0005,
is_short=True,
exchange='binance',
interest_mode=InterestMode.HOURSPERDAY
)
assert trade.open_order_id is None
assert trade.close_profit is None
assert trade.close_date is None
limit_short_order['status'] = 'open'
trade.update(limit_short_order)
assert trade.open_order_id is None
assert trade.close_profit is None
assert trade.close_date is None
@ pytest.mark.usefixtures("init_persistence")
def test_calc_close_trade_price_exception_short(limit_short_order, fee):
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
open_rate=0.1,
amount=15.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='binance',
interest_rate=0.0005,
leverage=3.0,
is_short=True,
interest_mode=InterestMode.HOURSPERDAY
)
trade.open_order_id = 'something'
trade.update(limit_short_order)
assert trade.calc_close_trade_value() == 0.0
@ pytest.mark.usefixtures("init_persistence")
def test_calc_close_trade_price_short(market_short_order, market_exit_short_order, fee):
"""
10 minute short market trade on Kraken at 3x leverage
Short trade
fee: 0.25% base
interest_rate: 0.05% per 4 hrs
open_rate: 0.00004173 base
close_rate: 0.00001234 base
amount: = 275.97543219 crypto
borrowed: 275.97543219 crypto
hours: 10 minutes = 1/6
interest: borrowed * interest_rate * ceil(1 + hours/4)
= 275.97543219 * 0.0005 * ceil(1 + ((1/6)/4)) = 0.27597543219 crypto
amount_closed: amount + interest = 275.97543219 + 0.27597543219 = 276.25140762219
close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee)
= (276.25140762219 * 0.00004099) + (276.25140762219 * 0.00004099 * 0.005)
= 0.011380162924425737
"""
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
amount=5,
open_rate=0.00001099,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_date=datetime.utcnow() - timedelta(hours=0, minutes=10),
interest_rate=0.0005,
is_short=True,
leverage=3.0,
exchange='kraken',
interest_mode=InterestMode.HOURSPER4
)
trade.open_order_id = 'close_trade'
trade.update(market_short_order) # Buy @ 0.00001099
# Get the close rate price with a custom close rate and a regular fee rate
assert isclose(trade.calc_close_trade_value(rate=0.00001234), 0.0034174647259)
# Get the close rate price with a custom close rate and a custom fee rate
assert isclose(trade.calc_close_trade_value(rate=0.00001234, fee=0.003), 0.0034191691971679986)
# Test when we apply a Sell order, and ask price with a custom fee rate
trade.update(market_exit_short_order)
assert isclose(trade.calc_close_trade_value(fee=0.005), 0.011380162924425737)
@ pytest.mark.usefixtures("init_persistence")
def test_calc_open_close_trade_price_short(limit_short_order, limit_exit_short_order, fee):
"""
5 hour short trade on Binance
Short trade
fee: 0.25% base
interest_rate: 0.05% per day
open_rate: 0.00001173 base
close_rate: 0.00001099 base
amount: 90.99181073 crypto
borrowed: 90.99181073 crypto
stake_amount: 0.0010673339398629
time-periods: 5 hours = 5/24
interest: borrowed * interest_rate * time-periods
= 90.99181073 * 0.0005 * 5/24 = 0.009478313617708333 crypto
open_value: (amount * open_rate) - (amount * open_rate * fee)
= (90.99181073 * 0.00001173) - (90.99181073 * 0.00001173 * 0.0025)
= 0.0010646656050132426
amount_closed: amount + interest = 90.99181073 + 0.009478313617708333 = 91.0012890436177
close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee)
= (91.0012890436177 * 0.00001099) + (91.0012890436177 * 0.00001099 * 0.0025)
= 0.001002604427005832
stake_value = (amount/lev * open_rate) - (amount/lev * open_rate * fee)
= 0.0010646656050132426
total_profit = open_value - close_value
= 0.0010646656050132426 - 0.001002604427005832
= 0.00006206117800741065
total_profit_percentage = (close_value - open_value) / stake_value
= (0.0010646656050132426 - 0.001002604427005832)/0.0010646656050132426
= 0.05829170935473088
"""
trade = Trade(
pair='ETH/BTC',
stake_amount=0.0010673339398629,
open_rate=0.01,
amount=5,
open_date=datetime.utcnow() - timedelta(hours=4, minutes=55),
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='binance',
interest_rate=0.0005,
interest_mode=InterestMode.HOURSPERDAY
)
trade.open_order_id = 'something'
trade.update(limit_short_order)
assert trade._calc_open_trade_value() == 0.0010646656050132426
trade.update(limit_exit_short_order)
# Is slightly different due to compilation time. Interest depends on time
assert round(trade.calc_close_trade_value(), 11) == round(0.001002604427005832, 11)
# Profit in BTC
assert round(trade.calc_profit(), 8) == round(0.00006206117800741065, 8)
# Profit in percent
assert round(trade.calc_profit_ratio(), 8) == round(0.05829170935473088, 8)
@ pytest.mark.usefixtures("init_persistence")
def test_trade_close_short(fee):
"""
Five hour short trade on Kraken at 3x leverage
Short trade
Exchange: Kraken
fee: 0.25% base
interest_rate: 0.05% per 4 hours
open_rate: 0.02 base
close_rate: 0.01 base
leverage: 3.0
amount: 15 crypto
borrowed: 15 crypto
time-periods: 5 hours = 5/4
interest: borrowed * interest_rate * time-periods
= 15 * 0.0005 * ceil(1 + 5/4) = 0.0225 crypto
open_value: (amount * open_rate) - (amount * open_rate * fee)
= (15 * 0.02) - (15 * 0.02 * 0.0025)
= 0.29925
amount_closed: amount + interest = 15 + 0.009375 = 15.0225
close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee)
= (15.0225 * 0.01) + (15.0225 * 0.01 * 0.0025)
= 0.15060056250000003
total_profit = open_value - close_value
= 0.29925 - 0.15060056250000003
= 0.14864943749999998
total_profit_percentage = (1-(close_value/open_value)) * leverage
= (1 - (0.15060056250000003/0.29925)) * 3
= 1.4902199248120298
"""
trade = Trade(
pair='ETH/BTC',
stake_amount=0.1,
open_rate=0.02,
amount=15,
is_open=True,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_date=datetime.utcnow() - timedelta(hours=4, minutes=55),
exchange='kraken',
is_short=True,
leverage=3.0,
interest_rate=0.0005,
interest_mode=InterestMode.HOURSPER4
)
assert trade.close_profit is None
assert trade.close_date is None
assert trade.is_open is True
trade.close(0.01)
assert trade.is_open is False
assert trade.close_profit == round(1.4902199248120298, 8)
assert trade.close_date is not None
# TODO-mg: Remove these comments probably
# new_date = arrow.Arrow(2020, 2, 2, 15, 6, 1).datetime,
# assert trade.close_date != new_date
# # Close should NOT update close_date if the trade has been closed already
# assert trade.is_open is False
# trade.close_date = new_date
# trade.close(0.02)
# assert trade.close_date == new_date
@ pytest.mark.usefixtures("init_persistence")
def test_update_with_binance_short(limit_short_order, limit_exit_short_order, fee, caplog):
"""
10 minute short limit trade on binance
Short trade
fee: 0.25% base
interest_rate: 0.05% per day
open_rate: 0.00001173 base
close_rate: 0.00001099 base
amount: 90.99181073 crypto
stake_amount: 0.0010673339398629 base
borrowed: 90.99181073 crypto
time-periods: 10 minutes(rounds up to 1/24 time-period of 1 day)
interest: borrowed * interest_rate * time-periods
= 90.99181073 * 0.0005 * 1/24 = 0.0018956627235416667 crypto
open_value: (amount * open_rate) - (amount * open_rate * fee)
= 90.99181073 * 0.00001173 - 90.99181073 * 0.00001173 * 0.0025
= 0.0010646656050132426
amount_closed: amount + interest = 90.99181073 + 0.0018956627235416667 = 90.99370639272354
close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee)
= (90.99370639272354 * 0.00001099) + (90.99370639272354 * 0.00001099 * 0.0025)
= 0.0010025208853391716
total_profit = open_value - close_value
= 0.0010646656050132426 - 0.0010025208853391716
= 0.00006214471967407108
total_profit_percentage = (1 - (close_value/open_value)) * leverage
= (1 - (0.0010025208853391716/0.0010646656050132426)) * 1
= 0.05837017687191848
"""
trade = Trade(
id=2,
pair='ETH/BTC',
stake_amount=0.0010673339398629,
open_rate=0.01,
amount=5,
is_open=True,
open_date=datetime.utcnow() - timedelta(hours=0, minutes=10),
fee_open=fee.return_value,
fee_close=fee.return_value,
# borrowed=90.99181073,
interest_rate=0.0005,
exchange='binance',
interest_mode=InterestMode.HOURSPERDAY
)
# assert trade.open_order_id is None
assert trade.close_profit is None
assert trade.close_date is None
assert trade.borrowed == 0.0
assert trade.is_short is None
# trade.open_order_id = 'something'
trade.update(limit_short_order)
# assert trade.open_order_id is None
assert trade.open_rate == 0.00001173
assert trade.close_profit is None
assert trade.close_date is None
assert trade.borrowed == 90.99181073
assert trade.is_short is True
assert log_has_re(r"LIMIT_SELL has been fulfilled for Trade\(id=2, "
r"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001173, open_since=.*\).",
caplog)
caplog.clear()
# trade.open_order_id = 'something'
trade.update(limit_exit_short_order)
# assert trade.open_order_id is None
assert trade.close_rate == 0.00001099
assert trade.close_profit == round(0.05837017687191848, 8)
assert trade.close_date is not None
assert log_has_re(r"LIMIT_BUY has been fulfilled for Trade\(id=2, "
r"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001173, open_since=.*\).",
caplog)
@ pytest.mark.usefixtures("init_persistence")
def test_update_market_order_short(
market_short_order,
market_exit_short_order,
fee,
caplog
):
"""
10 minute short market trade on Kraken at 3x leverage
Short trade
fee: 0.25% base
interest_rate: 0.05% per 4 hrs
open_rate: 0.00004173 base
close_rate: 0.00004099 base
amount: = 275.97543219 crypto
stake_amount: 0.0038388182617629
borrowed: 275.97543219 crypto
time-periods: 10 minutes(rounds up to 1 time-period of 4hrs)
interest: borrowed * interest_rate * time-periods
= 275.97543219 * 0.0005 * 2 = 0.27597543219 crypto
open_value: (amount * open_rate) - (amount * open_rate * fee)
= 275.97543219 * 0.00004173 - 275.97543219 * 0.00004173 * 0.0025
= 0.011487663648325479
amount_closed: amount + interest = 275.97543219 + 0.27597543219 = 276.25140762219
close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee)
= (276.25140762219 * 0.00004099) + (276.25140762219 * 0.00004099 * 0.0025)
= 0.0034174647259
total_profit = open_value - close_value
= 0.011487663648325479 - 0.0034174647259
= 0.00013580958689582596
total_profit_percentage = total_profit / stake_amount
= (1 - (close_value/open_value)) * leverage
= (1 - (0.0034174647259/0.011487663648325479)) * 3
= 0.03546663387440563
"""
trade = Trade(
id=1,
pair='ETH/BTC',
stake_amount=0.0038388182617629,
amount=5,
open_rate=0.01,
is_open=True,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_date=datetime.utcnow() - timedelta(hours=0, minutes=10),
interest_rate=0.0005,
exchange='kraken',
interest_mode=InterestMode.HOURSPER4
)
trade.open_order_id = 'something'
trade.update(market_short_order)
assert trade.leverage == 3.0
assert trade.is_short is True
assert trade.open_order_id is None
assert trade.open_rate == 0.00004173
assert trade.close_profit is None
assert trade.close_date is None
assert trade.interest_rate == 0.0005
# The logger also has the exact same but there's some spacing in there
assert log_has_re(r"MARKET_SELL has been fulfilled for Trade\(id=1, "
r"pair=ETH/BTC, amount=275.97543219, open_rate=0.00004173, open_since=.*\).",
caplog)
caplog.clear()
trade.is_open = True
trade.open_order_id = 'something'
trade.update(market_exit_short_order)
assert trade.open_order_id is None
assert trade.close_rate == 0.00004099
assert trade.close_profit == round(0.03546663387440563, 8)
assert trade.close_date is not None
# TODO-mg: The amount should maybe be the opening amount + the interest
# TODO-mg: Uncomment the next assert and make it work.
# The logger also has the exact same but there's some spacing in there
assert log_has_re(r"MARKET_BUY has been fulfilled for Trade\(id=1, "
r"pair=ETH/BTC, amount=275.97543219, open_rate=0.00004173, open_since=.*\).",
caplog)
@ pytest.mark.usefixtures("init_persistence")
def test_calc_profit_short(market_short_order, market_exit_short_order, fee):
"""
Market trade on Kraken at 3x leverage
Short trade
fee: 0.25% base or 0.3%
interest_rate: 0.05%, 0.025% per 4 hrs
open_rate: 0.00004173 base
close_rate: 0.00004099 base
stake_amount: 0.0038388182617629
amount: = 275.97543219 crypto
borrowed: 275.97543219 crypto
time-periods: 10 minutes(rounds up to 1 time-period of 4hrs)
5 hours = 5/4
interest: borrowed * interest_rate * time-periods
= 275.97543219 * 0.0005 * ceil(1+1) = 0.27597543219 crypto
= 275.97543219 * 0.00025 * ceil(1+5/4) = 0.20698157414249999 crypto
= 275.97543219 * 0.0005 * ceil(1+5/4) = 0.41396314828499997 crypto
= 275.97543219 * 0.00025 * ceil(1+1) = 0.27597543219 crypto
= 275.97543219 * 0.00025 * ceil(1+1) = 0.27597543219 crypto
open_value: (amount * open_rate) - (amount * open_rate * fee)
= (275.97543219 * 0.00004173) - (275.97543219 * 0.00004173 * 0.0025)
= 0.011487663648325479
amount_closed: amount + interest
= 275.97543219 + 0.27597543219 = 276.25140762219
= 275.97543219 + 0.20698157414249999 = 276.1824137641425
= 275.97543219 + 0.41396314828499997 = 276.389395338285
= 275.97543219 + 0.27597543219 = 276.25140762219
close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee)
(276.25140762219 * 0.00004374) + (276.25140762219 * 0.00004374 * 0.0025)
= 0.012113444660818078
(276.1824137641425 * 0.00000437) + (276.1824137641425 * 0.00000437 * 0.0025)
= 0.0012099344410196758
(276.389395338285 * 0.00004374) + (276.389395338285 * 0.00004374 * 0.003)
= 0.012125539968552874
(276.25140762219 * 0.00000437) + (276.25140762219 * 0.00000437 * 0.003)
= 0.0012102354919246037
(276.25140762219 * 0.00004099) + (276.25140762219 * 0.00004099 * 0.0025)
= 0.011351854061429653
total_profit = open_value - close_value
= 0.011487663648325479 - 0.012113444660818078 = -0.0006257810124925996
= 0.011487663648325479 - 0.0012099344410196758 = 0.010277729207305804
= 0.011487663648325479 - 0.012125539968552874 = -0.0006378763202273957
= 0.011487663648325479 - 0.0012102354919246037 = 0.010277428156400875
= 0.011487663648325479 - 0.011351854061429653 = 0.00013580958689582596
total_profit_percentage = (1-(close_value/open_value)) * leverage
(1-(0.012113444660818078 /0.011487663648325479))*3 = -0.16342252828332549
(1-(0.0012099344410196758/0.011487663648325479))*3 = 2.6840259748040123
(1-(0.012125539968552874 /0.011487663648325479))*3 = -0.16658121435868578
(1-(0.0012102354919246037/0.011487663648325479))*3 = 2.68394735544829
(1-(0.011351854061429653/0.011487663648325479))*3 = 0.03546663387440563
"""
trade = Trade(
pair='ETH/BTC',
stake_amount=0.0038388182617629,
amount=5,
open_rate=0.00001099,
open_date=datetime.utcnow() - timedelta(hours=0, minutes=10),
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='kraken',
is_short=True,
interest_rate=0.0005,
interest_mode=InterestMode.HOURSPER4
)
trade.open_order_id = 'something'
trade.update(market_short_order) # Buy @ 0.00001099
# Custom closing rate and regular fee rate
# Higher than open rate
assert trade.calc_profit(
rate=0.00004374, interest_rate=0.0005) == round(-0.0006257810124925996, 8)
assert trade.calc_profit_ratio(
rate=0.00004374, interest_rate=0.0005) == round(-0.16342252828332549, 8)
# Lower than open rate
trade.open_date = datetime.utcnow() - timedelta(hours=4, minutes=55)
assert trade.calc_profit(rate=0.00000437, interest_rate=0.00025) == round(
0.010277729207305804, 8)
assert trade.calc_profit_ratio(
rate=0.00000437, interest_rate=0.00025) == round(2.6840259748040123, 8)
# Custom closing rate and custom fee rate
# Higher than open rate
assert trade.calc_profit(rate=0.00004374, fee=0.003,
interest_rate=0.0005) == round(-0.0006378763202273957, 8)
assert trade.calc_profit_ratio(rate=0.00004374, fee=0.003,
interest_rate=0.0005) == round(-0.16658121435868578, 8)
# Lower than open rate
trade.open_date = datetime.utcnow() - timedelta(hours=0, minutes=10)
assert trade.calc_profit(rate=0.00000437, fee=0.003,
interest_rate=0.00025) == round(0.010277428156400875, 8)
assert trade.calc_profit_ratio(rate=0.00000437, fee=0.003,
interest_rate=0.00025) == round(2.68394735544829, 8)
# Test when we apply a exit short order.
trade.update(market_exit_short_order)
assert trade.calc_profit(rate=0.00004099) == round(0.00013580958689582596, 8)
assert trade.calc_profit_ratio() == round(0.03546663387440563, 8)
# Test with a custom fee rate on the close trade
# assert trade.calc_profit(fee=0.003) == 0.00006163
# assert trade.calc_profit_ratio(fee=0.003) == 0.06147824
def test_adjust_stop_loss_short(fee):
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
amount=5,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='binance',
open_rate=1,
max_rate=1,
is_short=True,
interest_mode=InterestMode.HOURSPERDAY
)
trade.adjust_stop_loss(trade.open_rate, 0.05, True)
assert trade.stop_loss == 1.05
assert trade.stop_loss_pct == 0.05
assert trade.initial_stop_loss == 1.05
assert trade.initial_stop_loss_pct == 0.05
# Get percent of profit with a lower rate
trade.adjust_stop_loss(1.04, 0.05)
assert trade.stop_loss == 1.05
assert trade.stop_loss_pct == 0.05
assert trade.initial_stop_loss == 1.05
assert trade.initial_stop_loss_pct == 0.05
# Get percent of profit with a custom rate (Higher than open rate)
trade.adjust_stop_loss(0.7, 0.1)
# If the price goes down to 0.7, with a trailing stop of 0.1,
# the new stoploss at 0.1 above 0.7 would be 0.7*0.1 higher
assert round(trade.stop_loss, 8) == 0.77
assert trade.stop_loss_pct == 0.1
assert trade.initial_stop_loss == 1.05
assert trade.initial_stop_loss_pct == 0.05
# current rate lower again ... should not change
trade.adjust_stop_loss(0.8, -0.1)
assert round(trade.stop_loss, 8) == 0.77
assert trade.initial_stop_loss == 1.05
assert trade.initial_stop_loss_pct == 0.05
# current rate higher... should raise stoploss
trade.adjust_stop_loss(0.6, -0.1)
assert round(trade.stop_loss, 8) == 0.66
assert trade.initial_stop_loss == 1.05
assert trade.initial_stop_loss_pct == 0.05
# Initial is true but stop_loss set - so doesn't do anything
trade.adjust_stop_loss(0.3, -0.1, True)
assert round(trade.stop_loss, 8) == 0.66 # TODO-mg: What is this test?
assert trade.initial_stop_loss == 1.05
assert trade.initial_stop_loss_pct == 0.05
assert trade.stop_loss_pct == 0.1
trade.set_liquidation_price(0.63)
trade.adjust_stop_loss(0.59, -0.1)
assert trade.stop_loss == 0.63
assert trade.liquidation_price == 0.63
# TODO-mg: Do a test with a trade that has a liquidation price
@ pytest.mark.usefixtures("init_persistence")
@ pytest.mark.parametrize('use_db', [True, False])
def test_get_open_short(fee, use_db):
Trade.use_db = use_db
Trade.reset_trades()
create_mock_trades_with_leverage(fee, use_db)
assert len(Trade.get_open_trades()) == 5
Trade.use_db = True
def test_stoploss_reinitialization_short(default_conf, fee):
init_db(default_conf['db_url'])
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
fee_open=fee.return_value,
open_date=arrow.utcnow().shift(hours=-2).datetime,
amount=10,
fee_close=fee.return_value,
exchange='binance',
open_rate=1,
max_rate=1,
is_short=True,
leverage=3.0,
interest_mode=InterestMode.HOURSPERDAY
)
trade.adjust_stop_loss(trade.open_rate, -0.05, True)
assert trade.stop_loss == 1.05
assert trade.stop_loss_pct == 0.05
assert trade.initial_stop_loss == 1.05
assert trade.initial_stop_loss_pct == 0.05
Trade.query.session.add(trade)
# Lower stoploss
Trade.stoploss_reinitialization(-0.06)
trades = Trade.get_open_trades()
assert len(trades) == 1
trade_adj = trades[0]
assert trade_adj.stop_loss == 1.06
assert trade_adj.stop_loss_pct == 0.06
assert trade_adj.initial_stop_loss == 1.06
assert trade_adj.initial_stop_loss_pct == 0.06
# Raise stoploss
Trade.stoploss_reinitialization(-0.04)
trades = Trade.get_open_trades()
assert len(trades) == 1
trade_adj = trades[0]
assert trade_adj.stop_loss == 1.04
assert trade_adj.stop_loss_pct == 0.04
assert trade_adj.initial_stop_loss == 1.04
assert trade_adj.initial_stop_loss_pct == 0.04
# Trailing stoploss
trade.adjust_stop_loss(0.98, -0.04)
assert trade_adj.stop_loss == 1.0192
assert trade_adj.initial_stop_loss == 1.04
Trade.stoploss_reinitialization(-0.04)
trades = Trade.get_open_trades()
assert len(trades) == 1
trade_adj = trades[0]
# Stoploss should not change in this case.
assert trade_adj.stop_loss == 1.0192
assert trade_adj.stop_loss_pct == 0.04
assert trade_adj.initial_stop_loss == 1.04
assert trade_adj.initial_stop_loss_pct == 0.04
# Stoploss can't go above liquidation price
trade_adj.set_liquidation_price(1.0)
trade.adjust_stop_loss(0.97, -0.04)
assert trade_adj.stop_loss == 1.0
assert trade_adj.stop_loss == 1.0
@ pytest.mark.usefixtures("init_persistence")
@ pytest.mark.parametrize('use_db', [True, False])
def test_total_open_trades_stakes_short(fee, use_db):
Trade.use_db = use_db
Trade.reset_trades()
res = Trade.total_open_trades_stakes()
assert res == 0
create_mock_trades_with_leverage(fee, use_db)
res = Trade.total_open_trades_stakes()
assert res == 15.133
Trade.use_db = True
@ pytest.mark.usefixtures("init_persistence")
def test_get_best_pair_short(fee):
res = Trade.get_best_pair()
assert res is None
create_mock_trades_with_leverage(fee)
res = Trade.get_best_pair()
assert len(res) == 2
assert res[0] == 'DOGE/BTC'
assert res[1] == 0.1713156134055116

View File

@ -79,7 +79,8 @@ def whitelist_conf_agefilter(default_conf):
},
{
"method": "AgeFilter",
"min_days_listed": 2
"min_days_listed": 2,
"max_days_listed": 100
}
]
return default_conf
@ -302,7 +303,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
# No pair for ETH, all handlers
([{"method": "StaticPairList"},
{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "AgeFilter", "min_days_listed": 2},
{"method": "AgeFilter", "min_days_listed": 2, "max_days_listed": None},
{"method": "PrecisionFilter"},
{"method": "PriceFilter", "low_price_ratio": 0.03},
{"method": "SpreadFilter", "max_spread_ratio": 0.005},
@ -310,12 +311,24 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
"ETH", []),
# AgeFilter and VolumePairList (require 2 days only, all should pass age test)
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "AgeFilter", "min_days_listed": 2}],
{"method": "AgeFilter", "min_days_listed": 2, "max_days_listed": 100}],
"BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/BTC']),
# AgeFilter and VolumePairList (require 10 days, all should fail age test)
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "AgeFilter", "min_days_listed": 10}],
{"method": "AgeFilter", "min_days_listed": 10, "max_days_listed": None}],
"BTC", []),
# AgeFilter and VolumePairList (all pair listed > 2, all should fail age test)
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "AgeFilter", "min_days_listed": 1, "max_days_listed": 2}],
"BTC", []),
# AgeFilter and VolumePairList LTC/BTC has 6 candles - removes all
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "AgeFilter", "min_days_listed": 4, "max_days_listed": 5}],
"BTC", []),
# AgeFilter and VolumePairList LTC/BTC has 6 candles - passes
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "AgeFilter", "min_days_listed": 4, "max_days_listed": 10}],
"BTC", ["LTC/BTC"]),
# Precisionfilter and quote volume
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "PrecisionFilter"}],
@ -417,7 +430,19 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
([{"method": "StaticPairList"},
{"method": "VolatilityFilter", "lookback_days": 3,
"min_volatility": 0.002, "max_volatility": 0.004, "refresh_period": 1440}],
"BTC", ['ETH/BTC', 'TKN/BTC'])
"BTC", ['ETH/BTC', 'TKN/BTC']),
# VolumePairList with no offset = unchanged pairlist
([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"},
{"method": "OffsetFilter", "offset": 0}],
"USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT', 'ADADOUBLE/USDT']),
# VolumePairList with offset = 2
([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"},
{"method": "OffsetFilter", "offset": 2}],
"USDT", ['ADAHALF/USDT', 'ADADOUBLE/USDT']),
# VolumePairList with higher offset, than total pairlist
([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"},
{"method": "OffsetFilter", "offset": 100}],
"USDT", [])
])
def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers,
ohlcv_history, pairlists, base_currency,
@ -431,7 +456,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t
ohlcv_data = {
('ETH/BTC', '1d'): ohlcv_history,
('TKN/BTC', '1d'): ohlcv_history,
('LTC/BTC', '1d'): ohlcv_history,
('LTC/BTC', '1d'): ohlcv_history.append(ohlcv_history),
('XRP/BTC', '1d'): ohlcv_history,
('HOT/BTC', '1d'): ohlcv_history_high_vola,
}
@ -480,9 +505,13 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t
for pairlist in pairlists:
if pairlist['method'] == 'AgeFilter' and pairlist['min_days_listed'] and \
len(ohlcv_history) <= pairlist['min_days_listed']:
len(ohlcv_history) < pairlist['min_days_listed']:
assert log_has_re(r'^Removed .* from whitelist, because age .* is less than '
r'.* day.*', caplog)
if pairlist['method'] == 'AgeFilter' and pairlist['max_days_listed'] and \
len(ohlcv_history) > pairlist['max_days_listed']:
assert log_has_re(r'^Removed .* from whitelist, because age .* is less than '
r'.* day.* or more than .* day', caplog)
if pairlist['method'] == 'PrecisionFilter' and whitelist_result:
assert log_has_re(r'^Removed .* from whitelist, because stop price .* '
r'would be <= stop limit.*', caplog)
@ -507,6 +536,105 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t
assert log_has_re(r'^Removed .* from whitelist, because volatility.*$', caplog)
@pytest.mark.parametrize("pairlists,base_currency,volumefilter_result", [
# default refresh of 1800 to small for daily candle lookback
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume",
"lookback_days": 1}],
"BTC", "default_refresh_too_short"), # OperationalException expected
# ambigous configuration with lookback days and period
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume",
"lookback_days": 1, "lookback_period": 1}],
"BTC", "lookback_days_and_period"), # OperationalException expected
# negative lookback period
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume",
"lookback_timeframe": "1d", "lookback_period": -1}],
"BTC", "lookback_period_negative"), # OperationalException expected
# lookback range exceedes exchange limit
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume",
"lookback_timeframe": "1m", "lookback_period": 2000, "refresh_period": 3600}],
"BTC", 'lookback_exceeds_exchange_request_size'), # OperationalException expected
# expecing pairs as given
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume",
"lookback_timeframe": "1d", "lookback_period": 1, "refresh_period": 86400}],
"BTC", ['HOT/BTC', 'LTC/BTC', 'ETH/BTC', 'TKN/BTC', 'XRP/BTC']),
# expecting pairs from default tickers, because 1h candles are not available
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume",
"lookback_timeframe": "1h", "lookback_period": 2, "refresh_period": 3600}],
"BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'HOT/BTC', 'FUEL/BTC']),
])
def test_VolumePairList_range(mocker, whitelist_conf, shitcoinmarkets, tickers, ohlcv_history,
pairlists, base_currency, volumefilter_result, caplog) -> None:
whitelist_conf['pairlists'] = pairlists
whitelist_conf['stake_currency'] = base_currency
ohlcv_history_high_vola = ohlcv_history.copy()
ohlcv_history_high_vola.loc[ohlcv_history_high_vola.index == 1, 'close'] = 0.00090
# create candles for medium overall volume with last candle high volume
ohlcv_history_medium_volume = ohlcv_history.copy()
ohlcv_history_medium_volume.loc[ohlcv_history_medium_volume.index == 2, 'volume'] = 5
# create candles for high volume with all candles high volume
ohlcv_history_high_volume = ohlcv_history.copy()
ohlcv_history_high_volume.loc[:, 'volume'] = 10
ohlcv_data = {
('ETH/BTC', '1d'): ohlcv_history,
('TKN/BTC', '1d'): ohlcv_history,
('LTC/BTC', '1d'): ohlcv_history_medium_volume,
('XRP/BTC', '1d'): ohlcv_history_high_vola,
('HOT/BTC', '1d'): ohlcv_history_high_volume,
}
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
if volumefilter_result == 'default_refresh_too_short':
with pytest.raises(OperationalException,
match=r'Refresh period of [0-9]+ seconds is smaller than one timeframe '
r'of [0-9]+.*\. Please adjust refresh_period to at least [0-9]+ '
r'and restart the bot\.'):
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
return
elif volumefilter_result == 'lookback_days_and_period':
with pytest.raises(OperationalException,
match=r'Ambigous configuration: lookback_days and lookback_period both '
r'set in pairlist config\..*'):
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
elif volumefilter_result == 'lookback_period_negative':
with pytest.raises(OperationalException,
match=r'VolumeFilter requires lookback_period to be >= 0'):
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
elif volumefilter_result == 'lookback_exceeds_exchange_request_size':
with pytest.raises(OperationalException,
match=r'VolumeFilter requires lookback_period to not exceed '
r'exchange max request size \([0-9]+\)'):
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
else:
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_tickers=tickers,
markets=PropertyMock(return_value=shitcoinmarkets)
)
# remove ohlcv when looback_timeframe != 1d
# to enforce fallback to ticker data
if 'lookback_timeframe' in pairlists[0]:
if pairlists[0]['lookback_timeframe'] != '1d':
ohlcv_data = []
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data),
)
freqtrade.pairlists.refresh_pairlist()
whitelist = freqtrade.pairlists.whitelist
assert isinstance(whitelist, list)
assert whitelist == volumefilter_result
def test_PrecisionFilter_error(mocker, whitelist_conf) -> None:
whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}]
del whitelist_conf['stoploss']
@ -650,6 +778,22 @@ def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tick
get_patched_freqtradebot(mocker, default_conf)
def test_agefilter_max_days_lower_than_min_days(mocker, default_conf, markets, tickers):
default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10},
{'method': 'AgeFilter', 'min_days_listed': 3,
"max_days_listed": 2}]
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'AgeFilter max_days_listed <= min_days_listed not permitted'):
get_patched_freqtradebot(mocker, default_conf)
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}]
@ -695,6 +839,18 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, o
assert freqtrade.exchange.refresh_latest_ohlcv.call_count == previous_call_count + 1
def test_OffsetFilter_error(mocker, whitelist_conf) -> None:
whitelist_conf['pairlists'] = (
[{"method": "StaticPairList"}, {"method": "OffsetFilter", "offset": -1}]
)
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
with pytest.raises(OperationalException,
match=r'OffsetFilter requires offset to be >= 0'):
PairListManager(MagicMock, whitelist_conf)
def test_rangestabilityfilter_checks(mocker, default_conf, markets, tickers):
default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10},
{'method': 'RangeStabilityFilter', 'lookback_days': 99999}]

View File

@ -107,11 +107,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'stoploss_entry_dist_ratio': -0.10448878,
'open_order': None,
'exchange': 'binance',
'leverage': 1.0,
'borrowed': 0.0,
'borrowed_currency': None,
'collateral_currency': None,
'interest_rate': 0.0,
'liquidation_price': None,
'is_short': False,
@ -181,15 +177,10 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'stoploss_entry_dist_ratio': -0.10448878,
'open_order': None,
'exchange': 'binance',
'leverage': 1.0,
'borrowed': 0.0,
'borrowed_currency': None,
'collateral_currency': None,
'interest_rate': 0.0,
'liquidation_price': None,
'is_short': False,
}
@ -696,6 +687,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
'filled': 0.0,
}
),
_is_dry_limit_order_filled=MagicMock(return_value=True),
get_fee=fee,
)
mocker.patch('freqtrade.wallets.Wallets.get_free', return_value=1000)
@ -720,8 +712,8 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
assert msg == {'result': 'Created sell orders for all open trades.'}
freqtradebot.enter_positions()
msg = rpc._rpc_forcesell('1')
assert msg == {'result': 'Created sell order for trade 1.'}
msg = rpc._rpc_forcesell('2')
assert msg == {'result': 'Created sell order for trade 2.'}
freqtradebot.state = State.STOPPED
with pytest.raises(RPCException, match=r'.*trader is not running*'):
@ -732,9 +724,11 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
freqtradebot.state = State.RUNNING
assert cancel_order_mock.call_count == 0
mocker.patch(
'freqtrade.exchange.Exchange._is_dry_limit_order_filled', MagicMock(return_value=False))
freqtradebot.enter_positions()
# make an limit-buy open trade
trade = Trade.query.filter(Trade.id == '1').first()
trade = Trade.query.filter(Trade.id == '3').first()
filled_amount = trade.amount / 2
# Fetch order - it's open first, and closed after cancel_order is called.
mocker.patch(
@ -755,7 +749,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
)
# check that the trade is called, which is done by ensuring exchange.cancel_order is called
# and trade amount is updated
rpc._rpc_forcesell('1')
rpc._rpc_forcesell('3')
assert cancel_order_mock.call_count == 1
assert trade.amount == filled_amount
@ -783,8 +777,8 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
}
)
# check that the trade is called, which is done by ensuring exchange.cancel_order is called
msg = rpc._rpc_forcesell('2')
assert msg == {'result': 'Created sell order for trade 2.'}
msg = rpc._rpc_forcesell('4')
assert msg == {'result': 'Created sell order for trade 4.'}
assert cancel_order_mock.call_count == 2
assert trade.amount == amount

View File

@ -105,6 +105,15 @@ def test_api_ui_fallback(botclient):
assert rc.status_code == 200
def test_api_ui_version(botclient, mocker):
ftbot, client = botclient
mocker.patch('freqtrade.commands.deploy_commands.read_ui_version', return_value='0.1.2')
rc = client_get(client, "/ui_version")
assert rc.status_code == 200
assert rc.json()['version'] == '0.1.2'
def test_api_auth():
with pytest.raises(ValueError):
create_token({'identity': {'u': 'Freqtrade'}}, 'secret1234', token_type="NotATokenType")
@ -996,7 +1005,8 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets):
get_balances=MagicMock(return_value=ticker),
fetch_ticker=ticker,
get_fee=fee,
markets=PropertyMock(return_value=markets)
markets=PropertyMock(return_value=markets),
_is_dry_limit_order_filled=MagicMock(return_value=False),
)
patch_get_signal(ftbot, (True, False))

View File

@ -218,6 +218,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None:
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
get_fee=fee,
_is_dry_limit_order_filled=MagicMock(return_value=True),
)
status_table = MagicMock()
mocker.patch.multiple(
@ -518,12 +519,15 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick
assert msg_mock.call_count == 1
assert '*BTC:*' in result
assert '*ETH:*' not in result
assert '*USDT:*' in result
assert '*EUR:*' in result
assert '*USDT:*' not in result
assert '*EUR:*' not in result
assert '*LTC:*' in result
assert '*XRP:*' not in result
assert 'Balance:' in result
assert 'Est. BTC:' in result
assert 'BTC: 12.00000000' in result
assert '*XRP:* not showing <0.0001 BTC amount' in result
assert "*3 Other Currencies (< 0.0001 BTC):*" in result
assert 'BTC: 0.00000309' in result
def test_balance_handle_empty_response(default_conf, update, mocker) -> None:
@ -666,6 +670,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee,
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
get_fee=fee,
_is_dry_limit_order_filled=MagicMock(return_value=True),
)
freqtradebot = FreqtradeBot(default_conf)
@ -724,6 +729,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee,
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
get_fee=fee,
_is_dry_limit_order_filled=MagicMock(return_value=True),
)
freqtradebot = FreqtradeBot(default_conf)
@ -784,6 +790,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
get_fee=fee,
_is_dry_limit_order_filled=MagicMock(return_value=True),
)
default_conf['max_open_trades'] = 4
freqtradebot = FreqtradeBot(default_conf)
@ -800,9 +807,9 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
context.args = ["all"]
telegram._forcesell(update=update, context=context)
# Called for each trade 4 times
assert msg_mock.call_count == 12
msg = msg_mock.call_args_list[2][0][0]
# Called for each trade 2 times
assert msg_mock.call_count == 8
msg = msg_mock.call_args_list[1][0][0]
assert {
'type': RPCMessageType.SELL,
'trade_id': 1,

View File

@ -1,6 +1,7 @@
# pragma pylint: disable=missing-docstring, C0103
import logging
from datetime import datetime, timedelta, timezone
from pathlib import Path
from unittest.mock import MagicMock
import arrow
@ -12,6 +13,7 @@ from freqtrade.data.dataprovider import DataProvider
from freqtrade.data.history import load_data
from freqtrade.enums import SellType
from freqtrade.exceptions import OperationalException, StrategyError
from freqtrade.optimize.space import SKDecimal
from freqtrade.persistence import PairLocks, Trade
from freqtrade.resolvers import StrategyResolver
from freqtrade.strategy.hyper import (BaseParameter, CategoricalParameter, DecimalParameter,
@ -657,17 +659,31 @@ def test_hyperopt_parameters():
assert list(intpar.range) == [0, 1, 2, 3, 4, 5]
fltpar = RealParameter(low=0.0, high=5.5, default=1.0, space='buy')
assert fltpar.value == 1
assert isinstance(fltpar.get_space(''), Real)
assert fltpar.value == 1
fltpar = DecimalParameter(low=0.0, high=5.5, default=1.0004, decimals=3, space='buy')
assert isinstance(fltpar.get_space(''), Integer)
assert fltpar.value == 1
fltpar = DecimalParameter(low=0.0, high=0.5, default=0.14, decimals=1, space='buy')
assert fltpar.value == 0.1
assert isinstance(fltpar.get_space(''), SKDecimal)
assert isinstance(fltpar.range, list)
assert len(list(fltpar.range)) == 1
# Range contains ONLY the default / value.
assert list(fltpar.range) == [fltpar.value]
fltpar.in_space = True
assert len(list(fltpar.range)) == 6
assert list(fltpar.range) == [0.0, 0.1, 0.2, 0.3, 0.4, 0.5]
catpar = CategoricalParameter(['buy_rsi', 'buy_macd', 'buy_none'],
default='buy_macd', space='buy')
assert isinstance(catpar.get_space(''), Categorical)
assert catpar.value == 'buy_macd'
assert isinstance(catpar.get_space(''), Categorical)
assert isinstance(catpar.range, list)
assert len(list(catpar.range)) == 1
# Range contains ONLY the default / value.
assert list(catpar.range) == [catpar.value]
catpar.in_space = True
assert len(list(catpar.range)) == 3
assert list(catpar.range) == ['buy_rsi', 'buy_macd', 'buy_none']
def test_auto_hyperopt_interface(default_conf):
@ -692,3 +708,50 @@ def test_auto_hyperopt_interface(default_conf):
with pytest.raises(OperationalException, match=r"Inconclusive parameter.*"):
[x for x in strategy.detect_parameters('sell')]
def test_auto_hyperopt_interface_loadparams(default_conf, mocker, caplog):
default_conf.update({'strategy': 'HyperoptableStrategy'})
del default_conf['stoploss']
del default_conf['minimal_roi']
mocker.patch.object(Path, 'is_file', MagicMock(return_value=True))
mocker.patch.object(Path, 'open')
expected_result = {
"strategy_name": "HyperoptableStrategy",
"params": {
"stoploss": {
"stoploss": -0.05,
},
"roi": {
"0": 0.2,
"1200": 0.01
}
}
}
mocker.patch('freqtrade.strategy.hyper.json_load', return_value=expected_result)
PairLocks.timeframe = default_conf['timeframe']
strategy = StrategyResolver.load_strategy(default_conf)
assert strategy.stoploss == -0.05
assert strategy.minimal_roi == {0: 0.2, 1200: 0.01}
expected_result = {
"strategy_name": "HyperoptableStrategy_No",
"params": {
"stoploss": {
"stoploss": -0.05,
},
"roi": {
"0": 0.2,
"1200": 0.01
}
}
}
mocker.patch('freqtrade.strategy.hyper.json_load', return_value=expected_result)
with pytest.raises(OperationalException, match="Invalid parameter file provided."):
StrategyResolver.load_strategy(default_conf)
mocker.patch('freqtrade.strategy.hyper.json_load', MagicMock(side_effect=ValueError()))
StrategyResolver.load_strategy(default_conf)
assert log_has("Invalid parameter file format.", caplog)

View File

@ -290,14 +290,12 @@ def test_strategy_override_use_sell_signal(caplog, default_conf):
assert strategy.use_sell_signal
assert isinstance(strategy.use_sell_signal, bool)
# must be inserted to configuration
assert 'use_sell_signal' in default_conf['ask_strategy']
assert default_conf['ask_strategy']['use_sell_signal']
assert 'use_sell_signal' in default_conf
assert default_conf['use_sell_signal']
default_conf.update({
'strategy': 'DefaultStrategy',
'ask_strategy': {
'use_sell_signal': False,
},
})
strategy = StrategyResolver.load_strategy(default_conf)
@ -315,14 +313,12 @@ def test_strategy_override_use_sell_profit_only(caplog, default_conf):
assert not strategy.sell_profit_only
assert isinstance(strategy.sell_profit_only, bool)
# must be inserted to configuration
assert 'sell_profit_only' in default_conf['ask_strategy']
assert not default_conf['ask_strategy']['sell_profit_only']
assert 'sell_profit_only' in default_conf
assert not default_conf['sell_profit_only']
default_conf.update({
'strategy': 'DefaultStrategy',
'ask_strategy': {
'sell_profit_only': True,
},
})
strategy = StrategyResolver.load_strategy(default_conf)

View File

@ -878,15 +878,15 @@ def test_validate_tsl(default_conf):
def test_validate_edge2(edge_conf):
edge_conf.update({"ask_strategy": {
edge_conf.update({
"use_sell_signal": True,
}})
})
# Passes test
validate_config_consistency(edge_conf)
edge_conf.update({"ask_strategy": {
edge_conf.update({
"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)
@ -935,6 +935,23 @@ def test_validate_protections(default_conf, protconf, expected):
validate_config_consistency(conf)
def test_validate_ask_orderbook(default_conf, caplog) -> None:
conf = deepcopy(default_conf)
conf['ask_strategy']['use_order_book'] = True
conf['ask_strategy']['order_book_min'] = 2
conf['ask_strategy']['order_book_max'] = 2
validate_config_consistency(conf)
assert log_has_re(r"DEPRECATED: Please use `order_book_top` instead of.*", caplog)
assert conf['ask_strategy']['order_book_top'] == 2
conf['ask_strategy']['order_book_max'] = 5
with pytest.raises(OperationalException,
match=r"Using order_book_max != order_book_min in ask_strategy.*"):
validate_config_consistency(conf)
def test_load_config_test_comments() -> None:
"""
Load config with comments
@ -1111,11 +1128,17 @@ def test_pairlist_resolving_fallback(mocker):
assert config['datadir'] == Path.cwd() / "user_data/data/binance"
@pytest.mark.skip(reason='Currently no deprecated / moved sections')
# The below is kept as a sample for the future.
@pytest.mark.parametrize("setting", [
("ask_strategy", "use_sell_signal", True,
"experimental", "use_sell_signal", False),
None, "use_sell_signal", False),
("ask_strategy", "sell_profit_only", True,
None, "sell_profit_only", False),
("ask_strategy", "sell_profit_offset", 0.1,
None, "sell_profit_offset", 0.01),
("ask_strategy", "ignore_roi_if_buy_signal", True,
None, "ignore_roi_if_buy_signal", False),
("ask_strategy", "ignore_buying_expired_candle_after", 5,
None, "ignore_buying_expired_candle_after", 6),
])
def test_process_temporary_deprecated_settings(mocker, default_conf, setting, caplog):
patched_configuration_load_config_file(mocker, default_conf)
@ -1124,10 +1147,14 @@ def test_process_temporary_deprecated_settings(mocker, default_conf, setting, ca
# (they may not exist in the config)
default_conf[setting[0]] = {}
default_conf[setting[3]] = {}
# Assign new setting
default_conf[setting[0]][setting[1]] = setting[2]
# Assign deprecated setting
default_conf[setting[0]][setting[1]] = setting[2]
# Assign new setting
if setting[3]:
default_conf[setting[3]][setting[4]] = setting[5]
else:
default_conf[setting[4]] = setting[5]
# New and deprecated settings are conflicting ones
with pytest.raises(OperationalException, match=r'DEPRECATED'):
@ -1136,13 +1163,19 @@ def test_process_temporary_deprecated_settings(mocker, default_conf, setting, ca
caplog.clear()
# Delete new setting
del default_conf[setting[0]][setting[1]]
if setting[3]:
del default_conf[setting[3]][setting[4]]
else:
del default_conf[setting[4]]
process_temporary_deprecated_settings(default_conf)
assert log_has_re('DEPRECATED', caplog)
# The value of the new setting shall have been set to the
# value of the deprecated one
assert default_conf[setting[0]][setting[1]] == setting[5]
if setting[3]:
assert default_conf[setting[3]][setting[4]] == setting[2]
else:
assert default_conf[setting[4]] == setting[2]
@pytest.mark.parametrize("setting", [
@ -1192,16 +1225,16 @@ def test_check_conflicting_settings(mocker, default_conf, caplog):
# New and deprecated settings are conflicting ones
with pytest.raises(OperationalException, match=r'DEPRECATED'):
check_conflicting_settings(default_conf,
'sectionA', 'new_setting',
'sectionB', 'deprecated_setting')
'sectionB', 'deprecated_setting',
'sectionA', 'new_setting')
caplog.clear()
# Delete new setting (deprecated exists)
del default_conf['sectionA']['new_setting']
check_conflicting_settings(default_conf,
'sectionA', 'new_setting',
'sectionB', 'deprecated_setting')
'sectionB', 'deprecated_setting',
'sectionA', 'new_setting')
assert not log_has_re('DEPRECATED', caplog)
assert 'new_setting' not in default_conf['sectionA']
@ -1212,8 +1245,8 @@ def test_check_conflicting_settings(mocker, default_conf, caplog):
# Delete deprecated setting
del default_conf['sectionB']['deprecated_setting']
check_conflicting_settings(default_conf,
'sectionA', 'new_setting',
'sectionB', 'deprecated_setting')
'sectionB', 'deprecated_setting',
'sectionA', 'new_setting')
assert not log_has_re('DEPRECATED', caplog)
assert default_conf['sectionA']['new_setting'] == 'valA'
@ -1225,15 +1258,13 @@ def test_process_deprecated_setting(mocker, default_conf, caplog):
# (they may not exist in the config)
default_conf['sectionA'] = {}
default_conf['sectionB'] = {}
# Assign new setting
default_conf['sectionA']['new_setting'] = 'valA'
# Assign deprecated setting
default_conf['sectionB']['deprecated_setting'] = 'valB'
# Both new and deprecated settings exists
process_deprecated_setting(default_conf,
'sectionA', 'new_setting',
'sectionB', 'deprecated_setting')
'sectionB', 'deprecated_setting',
'sectionA', 'new_setting')
assert log_has_re('DEPRECATED', caplog)
# The value of the new setting shall have been set to the
# value of the deprecated one
@ -1244,8 +1275,8 @@ def test_process_deprecated_setting(mocker, default_conf, caplog):
# Delete new setting (deprecated exists)
del default_conf['sectionA']['new_setting']
process_deprecated_setting(default_conf,
'sectionA', 'new_setting',
'sectionB', 'deprecated_setting')
'sectionB', 'deprecated_setting',
'sectionA', 'new_setting')
assert log_has_re('DEPRECATED', caplog)
# The value of the new setting shall have been set to the
# value of the deprecated one
@ -1258,11 +1289,21 @@ def test_process_deprecated_setting(mocker, default_conf, caplog):
# Delete deprecated setting
del default_conf['sectionB']['deprecated_setting']
process_deprecated_setting(default_conf,
'sectionA', 'new_setting',
'sectionB', 'deprecated_setting')
'sectionB', 'deprecated_setting',
'sectionA', 'new_setting')
assert not log_has_re('DEPRECATED', caplog)
assert default_conf['sectionA']['new_setting'] == 'valA'
caplog.clear()
# Test moving to root
default_conf['sectionB']['deprecated_setting2'] = "DeadBeef"
process_deprecated_setting(default_conf,
'sectionB', 'deprecated_setting2',
None, 'new_setting')
assert log_has_re('DEPRECATED', caplog)
assert default_conf['new_setting']
def test_process_removed_setting(mocker, default_conf, caplog):
patched_configuration_load_config_file(mocker, default_conf)

Some files were not shown because too many files have changed in this diff Show More