mirror of
https://github.com/freqtrade/freqtrade.git
synced 2025-11-29 00:23:07 +00:00
Merge pull request #11867 from freqtrade/maint/remove_edge
Remove Edge from Freqtrade
This commit is contained in:
@@ -70,7 +70,6 @@ Please find the complete documentation on the [freqtrade website](https://www.fr
|
||||
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
|
||||
- [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data.
|
||||
- [X] **Adaptive prediction modeling**: Build a smart strategy with FreqAI that self-trains to the market via adaptive machine learning methods. [Learn more](https://www.freqtrade.io/en/stable/freqai/)
|
||||
- [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/stable/edge/).
|
||||
- [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade or use dynamic whitelists.
|
||||
- [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid.
|
||||
- [x] **Builtin WebUI**: Builtin web UI to manage your bot.
|
||||
@@ -112,7 +111,6 @@ positional arguments:
|
||||
backtesting-show Show past Backtest results
|
||||
backtesting-analysis
|
||||
Backtest Analysis module.
|
||||
edge Edge module.
|
||||
hyperopt Hyperopt module.
|
||||
hyperopt-list List Hyperopt results
|
||||
hyperopt-show Show details of Hyperopt results
|
||||
|
||||
@@ -538,10 +538,6 @@
|
||||
"description": "Exchange configuration.",
|
||||
"$ref": "#/definitions/exchange"
|
||||
},
|
||||
"edge": {
|
||||
"description": "Edge configuration.",
|
||||
"$ref": "#/definitions/edge"
|
||||
},
|
||||
"log_config": {
|
||||
"description": "Logging configuration.",
|
||||
"$ref": "#/definitions/logging"
|
||||
@@ -1259,52 +1255,6 @@
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"edge": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"process_throttle_secs": {
|
||||
"type": "integer",
|
||||
"minimum": 600
|
||||
},
|
||||
"calculate_since_number_of_days": {
|
||||
"type": "integer"
|
||||
},
|
||||
"allowed_risk": {
|
||||
"type": "number"
|
||||
},
|
||||
"stoploss_range_min": {
|
||||
"type": "number"
|
||||
},
|
||||
"stoploss_range_max": {
|
||||
"type": "number"
|
||||
},
|
||||
"stoploss_range_step": {
|
||||
"type": "number"
|
||||
},
|
||||
"minimum_winrate": {
|
||||
"type": "number"
|
||||
},
|
||||
"minimum_expectancy": {
|
||||
"type": "number"
|
||||
},
|
||||
"min_trade_number": {
|
||||
"type": "number"
|
||||
},
|
||||
"max_trade_duration_minute": {
|
||||
"type": "integer"
|
||||
},
|
||||
"remove_pumps": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"process_throttle_secs",
|
||||
"allowed_risk"
|
||||
]
|
||||
},
|
||||
"logging": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -121,20 +121,6 @@
|
||||
"outdated_offset": 5,
|
||||
"markets_refresh_interval": 60
|
||||
},
|
||||
"edge": {
|
||||
"enabled": false,
|
||||
"process_throttle_secs": 3600,
|
||||
"calculate_since_number_of_days": 7,
|
||||
"allowed_risk": 0.01,
|
||||
"stoploss_range_min": -0.01,
|
||||
"stoploss_range_max": -0.1,
|
||||
"stoploss_range_step": -0.01,
|
||||
"minimum_winrate": 0.60,
|
||||
"minimum_expectancy": 0.20,
|
||||
"min_trade_number": 10,
|
||||
"max_trade_duration_minute": 1440,
|
||||
"remove_pumps": false
|
||||
},
|
||||
"telegram": {
|
||||
"enabled": false,
|
||||
"token": "your_telegram_token",
|
||||
|
||||
@@ -7,7 +7,6 @@ usage: freqtrade edge [-h] [-v] [--no-color] [--logfile FILE] [-V] [-c PATH]
|
||||
[--data-format-ohlcv {json,jsongz,feather,parquet}]
|
||||
[--max-open-trades INT] [--stake-amount STAKE_AMOUNT]
|
||||
[--fee FLOAT] [-p PAIRS [PAIRS ...]]
|
||||
[--stoplosses STOPLOSS_RANGE]
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
@@ -29,11 +28,6 @@ options:
|
||||
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
||||
Limit command to these pairs. Pairs are space-
|
||||
separated.
|
||||
--stoplosses STOPLOSS_RANGE
|
||||
Defines a range of stoploss values against which edge
|
||||
will assess the strategy. The format is "min,max,step"
|
||||
(without any space). Example:
|
||||
`--stoplosses=-0.01,-0.1,-0.001`
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
|
||||
@@ -22,7 +22,7 @@ positional arguments:
|
||||
backtesting-show Show past Backtest results
|
||||
backtesting-analysis
|
||||
Backtest Analysis module.
|
||||
edge Edge module.
|
||||
edge Edge module. No longer part of Freqtrade
|
||||
hyperopt Hyperopt module.
|
||||
hyperopt-list List Hyperopt results
|
||||
hyperopt-show Show details of Hyperopt results
|
||||
|
||||
@@ -234,7 +234,6 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `exchange.only_from_ccxt` | Prevent data-download from data.binance.vision. Leaving this as false can greatly speed up downloads, but may be problematic if the site is not available.<br>*Defaults to `false`*<br> **Datatype:** Boolean
|
||||
| `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||
| | **Plugins**
|
||||
| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation of all possible configuration options.
|
||||
| `pairlists` | Define one or more pairlists to be used. [More information](plugins.md#pairlists-and-pairlist-handlers). <br>*Defaults to `StaticPairList`.* <br> **Datatype:** List of Dicts
|
||||
| | **Telegram**
|
||||
| `telegram.enabled` | Enable the usage of Telegram. <br> **Datatype:** Boolean
|
||||
|
||||
@@ -93,3 +93,8 @@ Please use the [`convert-data` subcommand](data-download.md#sub-command-convert-
|
||||
|
||||
Configuring syslog and journald via `--logfile systemd` and `--logfile journald` respectively has been deprecated in 2025.3.
|
||||
Please use configuration based [log setup](advanced-setup.md#advanced-logging) instead.
|
||||
|
||||
## Removal of the edge module
|
||||
|
||||
The edge module has been deprecated in 2023.9 and removed in 2025.6.
|
||||
All functionalities of edge have been removed, and having edge configured will result in an error.
|
||||
|
||||
300
docs/edge.md
300
docs/edge.md
@@ -1,300 +0,0 @@
|
||||
# Edge positioning
|
||||
|
||||
The `Edge Positioning` module uses probability to calculate your win rate and risk reward ratio. It will use these statistics to control your strategy trade entry points, position size and, stoploss.
|
||||
|
||||
!!! Danger "Deprecated functionality"
|
||||
`Edge positioning` (or short Edge) is currently in maintenance mode only (we keep existing functionality alive) and should be considered as deprecated.
|
||||
It will currently not receive new features until either someone stepped forward to take up ownership of that module - or we'll decide to remove edge from freqtrade.
|
||||
|
||||
!!! Warning
|
||||
When using `Edge positioning` with a dynamic whitelist (VolumePairList), make sure to also use `AgeFilter` and set it to at least `calculate_since_number_of_days` to avoid problems with missing data.
|
||||
|
||||
!!! Note
|
||||
`Edge Positioning` only considers *its own* buy/sell/stoploss signals. It ignores the stoploss, trailing stoploss, and ROI settings in the strategy configuration file.
|
||||
`Edge Positioning` improves the performance of some trading strategies and *decreases* the performance of others.
|
||||
|
||||
|
||||
## Introduction
|
||||
|
||||
Trading strategies are not perfect. They are frameworks that are susceptible to the market and its indicators. Because the market is not at all predictable, sometimes a strategy will win and sometimes the same strategy will lose.
|
||||
|
||||
To obtain an edge in the market, a strategy has to make more money than it loses. Making money in trading is not only about *how often* the strategy makes or loses money.
|
||||
|
||||
!!! tip "It doesn't matter how often, but how much!"
|
||||
A bad strategy might make 1 penny in *ten* transactions but lose 1 dollar in *one* transaction. If one only checks the number of winning trades, it would be misleading to think that the strategy is actually making a profit.
|
||||
|
||||
The Edge Positioning module seeks to improve a strategy's winning probability and the money that the strategy will make *on the long run*.
|
||||
|
||||
We raise the following question[^1]:
|
||||
|
||||
!!! Question "Which trade is a better option?"
|
||||
a) A trade with 80% of chance of losing 100\$ and 20% chance of winning 200\$<br/>
|
||||
b) A trade with 100% of chance of losing 30\$
|
||||
|
||||
???+ Info "Answer"
|
||||
The expected value of *a)* is smaller than the expected value of *b)*.<br/>
|
||||
Hence, *b*) represents a smaller loss in the long run.<br/>
|
||||
However, the answer is: *it depends*
|
||||
|
||||
Another way to look at it is to ask a similar question:
|
||||
|
||||
!!! Question "Which trade is a better option?"
|
||||
a) A trade with 80% of chance of winning 100\$ and 20% chance of losing 200\$<br/>
|
||||
b) A trade with 100% of chance of winning 30\$
|
||||
|
||||
Edge positioning tries to answer the hard questions about risk/reward and position size automatically, seeking to minimizes the chances of losing of a given strategy.
|
||||
|
||||
### Trading, winning and losing
|
||||
|
||||
Let's call $o$ the return of a single transaction $o$ where $o \in \mathbb{R}$. The collection $O = \{o_1, o_2, ..., o_N\}$ is the set of all returns of transactions made during a trading session. We say that $N$ is the cardinality of $O$, or, in lay terms, it is the number of transactions made in a trading session.
|
||||
|
||||
!!! Example
|
||||
In a session where a strategy made three transactions we can say that $O = \{3.5, -1, 15\}$. That means that $N = 3$ and $o_1 = 3.5$, $o_2 = -1$, $o_3 = 15$.
|
||||
|
||||
A winning trade is a trade where a strategy *made* money. Making money means that the strategy closed the position in a value that returned a profit, after all deducted fees. Formally, a winning trade will have a return $o_i > 0$. Similarly, a losing trade will have a return $o_j \leq 0$. With that, we can discover the set of all winning trades, $T_{win}$, as follows:
|
||||
|
||||
$$ T_{win} = \{ o \in O | o > 0 \} $$
|
||||
|
||||
Similarly, we can discover the set of losing trades $T_{lose}$ as follows:
|
||||
|
||||
$$ T_{lose} = \{o \in O | o \leq 0\} $$
|
||||
|
||||
!!! Example
|
||||
In a section where a strategy made four transactions $O = \{3.5, -1, 15, 0\}$:<br>
|
||||
$T_{win} = \{3.5, 15\}$<br>
|
||||
$T_{lose} = \{-1, 0\}$<br>
|
||||
|
||||
### Win Rate and Lose Rate
|
||||
|
||||
The win rate $W$ is the proportion of winning trades with respect to all the trades made by a strategy. We use the following function to compute the win rate:
|
||||
|
||||
$$W = \frac{|T_{win}|}{N}$$
|
||||
|
||||
Where $W$ is the win rate, $N$ is the number of trades and, $T_{win}$ is the set of all trades where the strategy made money.
|
||||
|
||||
Similarly, we can compute the rate of losing trades:
|
||||
|
||||
$$
|
||||
L = \frac{|T_{lose}|}{N}
|
||||
$$
|
||||
|
||||
Where $L$ is the lose rate, $N$ is the amount of trades made and, $T_{lose}$ is the set of all trades where the strategy lost money. Note that the above formula is the same as calculating $L = 1 – W$ or $W = 1 – L$
|
||||
|
||||
### Risk Reward Ratio
|
||||
|
||||
Risk Reward Ratio ($R$) is a formula used to measure the expected gains of a given investment against the risk of loss. It is basically what you potentially win divided by what you potentially lose. Formally:
|
||||
|
||||
$$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$
|
||||
|
||||
???+ Example "Worked example of $R$ calculation"
|
||||
Let's say that you think that the price of *stonecoin* today is 10.0\$. You believe that, because they will start mining stonecoin, it will go up to 15.0\$ tomorrow. There is the risk that the stone is too hard, and the GPUs can't mine it, so the price might go to 0\$ tomorrow. You are planning to invest 100\$, which will give you 10 shares (100 / 10).
|
||||
|
||||
Your potential profit is calculated as:
|
||||
|
||||
$\begin{aligned}
|
||||
\text{potential_profit} &= (\text{potential_price} - \text{entry_price}) * \frac{\text{investment}}{\text{entry_price}} \\
|
||||
&= (15 - 10) * (100 / 10) \\
|
||||
&= 50
|
||||
\end{aligned}$
|
||||
|
||||
Since the price might go to 0\$, the 100\$ dollars invested could turn into 0.
|
||||
|
||||
We do however use a stoploss of 15% - so in the worst case, we'll sell 15% below entry price (or at 8.5$\).
|
||||
|
||||
$\begin{aligned}
|
||||
\text{potential_loss} &= (\text{entry_price} - \text{stoploss}) * \frac{\text{investment}}{\text{entry_price}} \\
|
||||
&= (10 - 8.5) * (100 / 10)\\
|
||||
&= 15
|
||||
\end{aligned}$
|
||||
|
||||
We can compute the Risk Reward Ratio as follows:
|
||||
|
||||
$\begin{aligned}
|
||||
R &= \frac{\text{potential_profit}}{\text{potential_loss}}\\
|
||||
&= \frac{50}{15}\\
|
||||
&= 3.33
|
||||
\end{aligned}$<br>
|
||||
What it effectively means is that the strategy have the potential to make 3.33\$ for each 1\$ invested.
|
||||
|
||||
On a long horizon, that is, on many trades, we can calculate the risk reward by dividing the strategy' average profit on winning trades by the strategy' average loss on losing trades. We can calculate the average profit, $\mu_{win}$, as follows:
|
||||
|
||||
$$ \text{average_profit} = \mu_{win} = \frac{\text{sum_of_profits}}{\text{count_winning_trades}} = \frac{\sum^{o \in T_{win}} o}{|T_{win}|} $$
|
||||
|
||||
Similarly, we can calculate the average loss, $\mu_{lose}$, as follows:
|
||||
|
||||
$$ \text{average_loss} = \mu_{lose} = \frac{\text{sum_of_losses}}{\text{count_losing_trades}} = \frac{\sum^{o \in T_{lose}} o}{|T_{lose}|} $$
|
||||
|
||||
Finally, we can calculate the Risk Reward ratio, $R$, as follows:
|
||||
|
||||
$$ R = \frac{\text{average_profit}}{\text{average_loss}} = \frac{\mu_{win}}{\mu_{lose}}\\ $$
|
||||
|
||||
|
||||
???+ Example "Worked example of $R$ calculation using mean profit/loss"
|
||||
Let's say the strategy that we are using makes an average win $\mu_{win} = 2.06$ and an average loss $\mu_{loss} = 4.11$.<br>
|
||||
We calculate the risk reward ratio as follows:<br>
|
||||
$R = \frac{\mu_{win}}{\mu_{loss}} = \frac{2.06}{4.11} = 0.5012...$
|
||||
|
||||
|
||||
### Expectancy
|
||||
|
||||
By combining the Win Rate $W$ and the Risk Reward ratio $R$ to create an expectancy ratio $E$. A expectance ratio is the expected return of the investment made in a trade. We can compute the value of $E$ as follows:
|
||||
|
||||
$$E = R * W - L$$
|
||||
|
||||
!!! Example "Calculating $E$"
|
||||
Let's say that a strategy has a win rate $W = 0.28$ and a risk reward ratio $R = 5$. What this means is that the strategy is expected to make 5 times the investment around on 28% of the trades it makes. Working out the example:<br>
|
||||
$E = R * W - L = 5 * 0.28 - 0.72 = 0.68$
|
||||
<br>
|
||||
|
||||
The expectancy worked out in the example above means that, on average, this strategy' trades will return 1.68 times the size of its losses. Said another way, the strategy makes 1.68\$ for every 1\$ it loses, on average.
|
||||
|
||||
This is important for two reasons: First, it may seem obvious, but you know right away that you have a positive return. Second, you now have a number you can compare to other candidate systems to make decisions about which ones you employ.
|
||||
|
||||
It is important to remember that any system with an expectancy greater than 0 is profitable using past data. The key is finding one that will be profitable in the future.
|
||||
|
||||
You can also use this value to evaluate the effectiveness of modifications to this system.
|
||||
|
||||
!!! Note
|
||||
It's important to keep in mind that Edge is testing your expectancy using historical data, there's no guarantee that you will have a similar edge in the future. It's still vital to do this testing in order to build confidence in your methodology but be wary of "curve-fitting" your approach to the historical data as things are unlikely to play out the exact same way for future trades.
|
||||
|
||||
## How does it work?
|
||||
|
||||
Edge combines dynamic stoploss, dynamic positions, and whitelist generation into one isolated module which is then applied to the trading strategy. If enabled in config, Edge will go through historical data with a range of stoplosses in order to find buy and sell/stoploss signals. It then calculates win rate and expectancy over *N* trades for each stoploss. Here is an example:
|
||||
|
||||
| Pair | Stoploss | Win Rate | Risk Reward Ratio | Expectancy |
|
||||
|----------|:-------------:|-------------:|------------------:|-----------:|
|
||||
| XZC/ETH | -0.01 | 0.50 |1.176384 | 0.088 |
|
||||
| XZC/ETH | -0.02 | 0.51 |1.115941 | 0.079 |
|
||||
| XZC/ETH | -0.03 | 0.52 |1.359670 | 0.228 |
|
||||
| XZC/ETH | -0.04 | 0.51 |1.234539 | 0.117 |
|
||||
|
||||
The goal here is to find the best stoploss for the strategy in order to have the maximum expectancy. In the above example stoploss at $3%$ leads to the maximum expectancy according to historical data.
|
||||
|
||||
Edge module then forces stoploss value it evaluated to your strategy dynamically.
|
||||
|
||||
### Position size
|
||||
|
||||
Edge dictates the amount at stake for each trade to the bot according to the following factors:
|
||||
|
||||
- Allowed capital at risk
|
||||
- Stoploss
|
||||
|
||||
Allowed capital at risk is calculated as follows:
|
||||
|
||||
```
|
||||
Allowed capital at risk = (Capital available_percentage) X (Allowed risk per trade)
|
||||
```
|
||||
|
||||
Stoploss is calculated as described above with respect to historical data.
|
||||
|
||||
The position size is calculated as follows:
|
||||
|
||||
```
|
||||
Position size = (Allowed capital at risk) / Stoploss
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
Let's say the stake currency is **ETH** and there is $10$ **ETH** on the wallet. The capital available percentage is $50%$ and the allowed risk per trade is $1\%$. Thus, the available capital for trading is $10 * 0.5 = 5$ **ETH** and the allowed capital at risk would be $5 * 0.01 = 0.05$ **ETH**.
|
||||
|
||||
- **Trade 1:** The strategy detects a new buy signal in the **XLM/ETH** market. `Edge Positioning` calculates a stoploss of $2\%$ and a position of $0.05 / 0.02 = 2.5$ **ETH**. The bot takes a position of $2.5$ **ETH** in the **XLM/ETH** market.
|
||||
|
||||
- **Trade 2:** The strategy detects a buy signal on the **BTC/ETH** market while **Trade 1** is still open. `Edge Positioning` calculates the stoploss of $4\%$ on this market. Thus, **Trade 2** position size is $0.05 / 0.04 = 1.25$ **ETH**.
|
||||
|
||||
!!! Tip "Available Capital $\neq$ Available in wallet"
|
||||
The available capital for trading didn't change in **Trade 2** even with **Trade 1** still open. The available capital **is not** the free amount in the wallet.
|
||||
|
||||
- **Trade 3:** The strategy detects a buy signal in the **ADA/ETH** market. `Edge Positioning` calculates a stoploss of $1\%$ and a position of $0.05 / 0.01 = 5$ **ETH**. Since **Trade 1** has $2.5$ **ETH** blocked and **Trade 2** has $1.25$ **ETH** blocked, there is only $5 - 1.25 - 2.5 = 1.25$ **ETH** available. Hence, the position size of **Trade 3** is $1.25$ **ETH**.
|
||||
|
||||
!!! Tip "Available Capital Updates"
|
||||
The available capital does not change before a position is sold. After a trade is closed the Available Capital goes up if the trade was profitable or goes down if the trade was a loss.
|
||||
|
||||
- The strategy detects a sell signal in the **XLM/ETH** market. The bot exits **Trade 1** for a profit of $1$ **ETH**. The total capital in the wallet becomes $11$ **ETH** and the available capital for trading becomes $5.5$ **ETH**.
|
||||
|
||||
- **Trade 4** The strategy detects a new buy signal int the **XLM/ETH** market. `Edge Positioning` calculates the stoploss of $2\%$, and the position size of $0.055 / 0.02 = 2.75$ **ETH**.
|
||||
|
||||
## Edge command reference
|
||||
|
||||
--8<-- "commands/edge.md"
|
||||
|
||||
## Configurations
|
||||
|
||||
Edge module has following configuration options:
|
||||
|
||||
| Parameter | Description |
|
||||
|------------|-------------|
|
||||
| `enabled` | If true, then Edge will run periodically. <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||
| `process_throttle_secs` | How often should Edge run in seconds. <br>*Defaults to `3600` (once per hour).* <br> **Datatype:** Integer
|
||||
| `calculate_since_number_of_days` | Number of days of data against which Edge calculates Win Rate, Risk Reward and Expectancy. <br> **Note** that it downloads historical data so increasing this number would lead to slowing down the bot. <br>*Defaults to `7`.* <br> **Datatype:** Integer
|
||||
| `allowed_risk` | Ratio of allowed risk per trade. <br>*Defaults to `0.01` (1%)).* <br> **Datatype:** Float
|
||||
| `stoploss_range_min` | Minimum stoploss. <br>*Defaults to `-0.01`.* <br> **Datatype:** Float
|
||||
| `stoploss_range_max` | Maximum stoploss. <br>*Defaults to `-0.10`.* <br> **Datatype:** Float
|
||||
| `stoploss_range_step` | As an example if this is set to -0.01 then Edge will test the strategy for `[-0.01, -0,02, -0,03 ..., -0.09, -0.10]` ranges. <br> **Note** than having a smaller step means having a bigger range which could lead to slow calculation. <br> If you set this parameter to -0.001, you then slow down the Edge calculation by a factor of 10. <br>*Defaults to `-0.001`.* <br> **Datatype:** Float
|
||||
| `minimum_winrate` | It filters out pairs which don't have at least minimum_winrate. <br>This comes handy if you want to be conservative and don't comprise win rate in favour of risk reward ratio. <br>*Defaults to `0.60`.* <br> **Datatype:** Float
|
||||
| `minimum_expectancy` | It filters out pairs which have the expectancy lower than this number. <br>Having an expectancy of 0.20 means if you put 10\$ on a trade you expect a 12\$ return. <br>*Defaults to `0.20`.* <br> **Datatype:** Float
|
||||
| `min_trade_number` | When calculating *W*, *R* and *E* (expectancy) against historical data, you always want to have a minimum number of trades. The more this number is the more Edge is reliable. <br>Having a win rate of 100% on a single trade doesn't mean anything at all. But having a win rate of 70% over past 100 trades means clearly something. <br>*Defaults to `10` (it is highly recommended not to decrease this number).* <br> **Datatype:** Integer
|
||||
| `max_trade_duration_minute` | Edge will filter out trades with long duration. If a trade is profitable after 1 month, it is hard to evaluate the strategy based on it. But if most of trades are profitable and they have maximum duration of 30 minutes, then it is clearly a good sign.<br>**NOTICE:** While configuring this value, you should take into consideration your timeframe. As an example filtering out trades having duration less than one day for a strategy which has 4h interval does not make sense. Default value is set assuming your strategy interval is relatively small (1m or 5m, etc.).<br>*Defaults to `1440` (one day).* <br> **Datatype:** Integer
|
||||
| `remove_pumps` | Edge will remove sudden pumps in a given market while going through historical data. However, given that pumps happen very often in crypto markets, we recommend you keep this off.<br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||
|
||||
## Running Edge independently
|
||||
|
||||
You can run Edge independently in order to see in details the result. Here is an example:
|
||||
|
||||
``` bash
|
||||
freqtrade edge
|
||||
```
|
||||
|
||||
An example of its output:
|
||||
|
||||
| **pair** | **stoploss** | **win rate** | **risk reward ratio** | **required risk reward** | **expectancy** | **total number of trades** | **average duration (min)** |
|
||||
|:----------|-----------:|-----------:|--------------------:|-----------------------:|-------------:|-----------------:|---------------:|
|
||||
| **AGI/BTC** | -0.02 | 0.64 | 5.86 | 0.56 | 3.41 | 14 | 54 |
|
||||
| **NXS/BTC** | -0.03 | 0.64 | 2.99 | 0.57 | 1.54 | 11 | 26 |
|
||||
| **LEND/BTC** | -0.02 | 0.82 | 2.05 | 0.22 | 1.50 | 11 | 36 |
|
||||
| **VIA/BTC** | -0.01 | 0.55 | 3.01 | 0.83 | 1.19 | 11 | 48 |
|
||||
| **MTH/BTC** | -0.09 | 0.56 | 2.82 | 0.80 | 1.12 | 18 | 52 |
|
||||
| **ARDR/BTC** | -0.04 | 0.42 | 3.14 | 1.40 | 0.73 | 12 | 42 |
|
||||
| **BCPT/BTC** | -0.01 | 0.71 | 1.34 | 0.40 | 0.67 | 14 | 30 |
|
||||
| **WINGS/BTC** | -0.02 | 0.56 | 1.97 | 0.80 | 0.65 | 27 | 42 |
|
||||
| **VIBE/BTC** | -0.02 | 0.83 | 0.91 | 0.20 | 0.59 | 12 | 35 |
|
||||
| **MCO/BTC** | -0.02 | 0.79 | 0.97 | 0.27 | 0.55 | 14 | 31 |
|
||||
| **GNT/BTC** | -0.02 | 0.50 | 2.06 | 1.00 | 0.53 | 18 | 24 |
|
||||
| **HOT/BTC** | -0.01 | 0.17 | 7.72 | 4.81 | 0.50 | 209 | 7 |
|
||||
| **SNM/BTC** | -0.03 | 0.71 | 1.06 | 0.42 | 0.45 | 17 | 38 |
|
||||
| **APPC/BTC** | -0.02 | 0.44 | 2.28 | 1.27 | 0.44 | 25 | 43 |
|
||||
| **NEBL/BTC** | -0.03 | 0.63 | 1.29 | 0.58 | 0.44 | 19 | 59 |
|
||||
|
||||
Edge produced the above table by comparing `calculate_since_number_of_days` to `minimum_expectancy` to find `min_trade_number` historical information based on the config file. The timerange Edge uses for its comparisons can be further limited by using the `--timerange` switch.
|
||||
|
||||
In live and dry-run modes, after the `process_throttle_secs` has passed, Edge will again process `calculate_since_number_of_days` against `minimum_expectancy` to find `min_trade_number`. If no `min_trade_number` is found, the bot will return "whitelist empty". Depending on the trade strategy being deployed, "whitelist empty" may be return much of the time - or *all* of the time. The use of Edge may also cause trading to occur in bursts, though this is rare.
|
||||
|
||||
If you encounter "whitelist empty" a lot, condsider tuning `calculate_since_number_of_days`, `minimum_expectancy` and `min_trade_number` to align to the trading frequency of your strategy.
|
||||
|
||||
### Update cached pairs with the latest data
|
||||
|
||||
Edge requires historic data the same way as backtesting does.
|
||||
Please refer to the [Data Downloading](data-download.md) section of the documentation for details.
|
||||
|
||||
### Precising stoploss range
|
||||
|
||||
```bash
|
||||
freqtrade edge --stoplosses=-0.01,-0.1,-0.001 #min,max,step
|
||||
```
|
||||
|
||||
### Advanced use of timerange
|
||||
|
||||
```bash
|
||||
freqtrade edge --timerange=20181110-20181113
|
||||
```
|
||||
|
||||
Doing `--timerange=-20190901` will get all available data until September 1st (excluding September 1st 2019).
|
||||
|
||||
The full timerange specification:
|
||||
|
||||
* Use tickframes till 2018/01/31: `--timerange=-20180131`
|
||||
* Use tickframes since 2018/01/31: `--timerange=20180131-`
|
||||
* Use tickframes since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301`
|
||||
* Use tickframes between POSIX timestamps 1527595200 1527618600: `--timerange=1527595200-1527618600`
|
||||
|
||||
|
||||
[^1]: Question extracted from MIT Opencourseware S096 - Mathematics with applications in Finance: https://ocw.mit.edu/courses/mathematics/18-s096-topics-in-mathematics-with-applications-in-finance-fall-2013/
|
||||
14
docs/faq.md
14
docs/faq.md
@@ -276,20 +276,6 @@ Example: 4% profit 650 times vs 0,3% profit a trade 10000 times in a year. If we
|
||||
Example:
|
||||
`freqtrade --config config.json --strategy SampleStrategy --hyperopt SampleHyperopt -e 1000 --timerange 20190601-20200601`
|
||||
|
||||
## Edge module
|
||||
|
||||
### Edge implements interesting approach for controlling position size, is there any theory behind it?
|
||||
|
||||
The Edge module is mostly a result of brainstorming of [@mishaker](https://github.com/mishaker) and [@creslinux](https://github.com/creslinux) freqtrade team members.
|
||||
|
||||
You can find further info on expectancy, win rate, risk management and position size in the following sources:
|
||||
|
||||
- https://www.tradeciety.com/ultimate-math-guide-for-traders/
|
||||
- https://samuraitradingacademy.com/trading-expectancy/
|
||||
- https://www.learningmarkets.com/determining-expectancy-in-your-trading/
|
||||
- https://www.lonestocktrader.com/make-money-trading-positive-expectancy/
|
||||
- https://www.babypips.com/trading/trade-expectancy-matter
|
||||
|
||||
## Official channels
|
||||
|
||||
Freqtrade is using exclusively the following official channels:
|
||||
|
||||
@@ -31,7 +31,6 @@ Freqtrade is a free and open source crypto trading bot written in Python. It is
|
||||
- Optimize: Find the best parameters for your strategy using hyperoptimization which employs machine learning methods. You can optimize buy, sell, take profit (ROI), stop-loss and trailing stop-loss parameters for your strategy.
|
||||
- Select markets: Create your static list or use an automatic one based on top traded volumes and/or prices (not available during backtesting). You can also explicitly blacklist markets you don't want to trade.
|
||||
- Run: Test your strategy with simulated money (Dry-Run mode) or deploy it with real money (Live-Trade mode).
|
||||
- Run using Edge (optional module): The concept is to find the best historical [trade expectancy](edge.md#expectancy) by markets based on variation of the stop-loss and then allow/reject markets to trade. The sizing of the trade is based on a risk of a percentage of your capital.
|
||||
- Control/Monitor: Use Telegram or a WebUI (start/stop the bot, show profit/loss, daily summary, current open trades results, etc.).
|
||||
- Analyze: Further analysis can be performed on either Backtesting data or Freqtrade trading history (SQL database), including automated standard plots, and methods to load the data into [interactive environments](data-analysis.md).
|
||||
|
||||
|
||||
@@ -190,9 +190,6 @@ delete_trade
|
||||
|
||||
:param trade_id: Deletes the trade with this ID from the database.
|
||||
|
||||
edge
|
||||
Return information about edge.
|
||||
|
||||
forcebuy
|
||||
Buy an asset.
|
||||
|
||||
@@ -368,7 +365,6 @@ All endpoints in the below table need to be prefixed with the base URL of the AP
|
||||
| `/blacklist` | GET | Show the current blacklist.
|
||||
| `/blacklist` | POST | Adds the specified pair to the blacklist.<br/>*Params:*<br/>- `pair` (`str`)
|
||||
| `/blacklist` | DELETE | Deletes the specified list of pairs from the blacklist.<br/>*Params:*<br/>- `[pair,pair]` (`list[str]`)
|
||||
| `/edge` | GET | Show validated pairs by Edge if it is enabled.
|
||||
| `/pair_candles` | GET | Returns dataframe for a pair / timeframe combination while the bot is running. **Alpha**
|
||||
| `/pair_candles` | POST | Returns dataframe for a pair / timeframe combination while the bot is running, filtered by a provided list of columns to return. **Alpha**<br/>*Params:*<br/>- `<column_list>` (`list[str]`)
|
||||
| `/pair_history` | GET | Returns an analyzed dataframe for a given timerange, analyzed by a given strategy. **Alpha**
|
||||
|
||||
@@ -256,4 +256,4 @@ The new stoploss value will be applied to open trades (and corresponding log-mes
|
||||
|
||||
### Limitations
|
||||
|
||||
Stoploss values cannot be changed if `trailing_stop` is enabled and the stoploss has already been adjusted, or if [Edge](edge.md) is enabled (since Edge would recalculate stoploss based on the current market situation).
|
||||
Stoploss values cannot be changed if `trailing_stop` is enabled and the stoploss has already been adjusted.
|
||||
|
||||
@@ -188,7 +188,7 @@ You can create your own keyboard in `config.json`:
|
||||
!!! Note "Supported Commands"
|
||||
Only the following commands are allowed. Command arguments are not supported!
|
||||
|
||||
`/start`, `/pause`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopentry`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version`, `/marketdir`
|
||||
`/start`, `/pause`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopentry`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/help`, `/version`, `/marketdir`
|
||||
|
||||
## Telegram commands
|
||||
|
||||
@@ -240,7 +240,6 @@ official commands. You can ask at any moment for help with `/help`.
|
||||
| `/entries` | Shows Wins / losses by Exit reason as well as Avg. holding durations for buys and sells
|
||||
| `/whitelist [sorted] [baseonly]` | Show the current whitelist. Optionally display in alphabetical order and/or with just the base currency of each pairing.
|
||||
| `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
|
||||
| `/edge` | Show validated pairs by Edge if it is enabled.
|
||||
|
||||
## Telegram commands in action
|
||||
|
||||
@@ -451,21 +450,6 @@ Use `/reload_config` to reset the blacklist.
|
||||
> Using blacklist `StaticPairList` with 2 pairs
|
||||
>`DODGE/BTC`, `HOT/BTC`.
|
||||
|
||||
### /edge
|
||||
|
||||
Shows pairs validated by Edge along with their corresponding win-rate, expectancy and stoploss values.
|
||||
|
||||
> **Edge only validated following pairs:**
|
||||
```
|
||||
Pair Winrate Expectancy Stoploss
|
||||
-------- --------- ------------ ----------
|
||||
DOCK/ETH 0.522727 0.881821 -0.03
|
||||
PHX/ETH 0.677419 0.560488 -0.03
|
||||
HOT/ETH 0.733333 0.490492 -0.03
|
||||
HC/ETH 0.588235 0.280988 -0.02
|
||||
ARDR/ETH 0.366667 0.143059 -0.01
|
||||
```
|
||||
|
||||
### /version
|
||||
|
||||
> **Version:** `0.14.3`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Utility Subcommands
|
||||
|
||||
Besides the Live-Trade and Dry-Run run modes, the `backtesting`, `edge` and `hyperopt` optimization subcommands, and the `download-data` subcommand which prepares historical data, the bot contains a number of utility subcommands. They are described in this section.
|
||||
Besides the Live-Trade and Dry-Run run modes, the `backtesting` and `hyperopt` optimization subcommands, and the `download-data` subcommand which prepares historical data, the bot contains a number of utility subcommands. They are described in this section.
|
||||
|
||||
## Create userdir
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ ARGS_HYPEROPT = [
|
||||
"early_stop",
|
||||
]
|
||||
|
||||
ARGS_EDGE = [*ARGS_COMMON_OPTIMIZE, "stoploss_range"]
|
||||
ARGS_EDGE = [*ARGS_COMMON_OPTIMIZE]
|
||||
|
||||
ARGS_LIST_STRATEGIES = [
|
||||
"strategy_path",
|
||||
@@ -506,7 +506,9 @@ class Arguments:
|
||||
|
||||
# Add edge subcommand
|
||||
edge_cmd = subparsers.add_parser(
|
||||
"edge", help="Edge module.", parents=[_common_parser, _strategy_parser]
|
||||
"edge",
|
||||
help="Edge module. No longer part of Freqtrade",
|
||||
parents=[_common_parser, _strategy_parser],
|
||||
)
|
||||
edge_cmd.set_defaults(func=start_edge)
|
||||
self._build_args(optionlist=ARGS_EDGE, parser=edge_cmd)
|
||||
|
||||
@@ -240,13 +240,6 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
default=constants.BACKTEST_CACHE_DEFAULT,
|
||||
choices=constants.BACKTEST_CACHE_AGE,
|
||||
),
|
||||
# Edge
|
||||
"stoploss_range": Arg(
|
||||
"--stoplosses",
|
||||
help="Defines a range of stoploss values against which edge will assess the strategy. "
|
||||
'The format is "min,max,step" (without any space). '
|
||||
"Example: `--stoplosses=-0.01,-0.1,-0.001`",
|
||||
),
|
||||
# Hyperopt
|
||||
"hyperopt": Arg(
|
||||
"--hyperopt",
|
||||
|
||||
@@ -129,15 +129,10 @@ def start_edge(args: dict[str, Any]) -> None:
|
||||
:param args: Cli args from Arguments()
|
||||
:return: None
|
||||
"""
|
||||
from freqtrade.optimize.edge_cli import EdgeCli
|
||||
|
||||
# Initialize configuration
|
||||
config = setup_optimize_configuration(args, RunMode.EDGE)
|
||||
logger.info("Starting freqtrade in Edge mode")
|
||||
|
||||
# Initialize Edge object
|
||||
edge_cli = EdgeCli(config)
|
||||
edge_cli.start()
|
||||
raise ConfigurationError(
|
||||
"The Edge module has been deprecated in 2023.9 and removed in 2025.6. "
|
||||
"All functionalities of edge have been removed."
|
||||
)
|
||||
|
||||
|
||||
def start_lookahead_analysis(args: dict[str, Any]) -> None:
|
||||
|
||||
@@ -423,10 +423,6 @@ CONF_SCHEMA = {
|
||||
"description": "Exchange configuration.",
|
||||
"$ref": "#/definitions/exchange",
|
||||
},
|
||||
"edge": {
|
||||
"description": "Edge configuration.",
|
||||
"$ref": "#/definitions/edge",
|
||||
},
|
||||
"log_config": {
|
||||
"description": "Logging configuration.",
|
||||
"$ref": "#/definitions/logging",
|
||||
@@ -929,24 +925,6 @@ CONF_SCHEMA = {
|
||||
},
|
||||
"required": ["name"],
|
||||
},
|
||||
"edge": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"process_throttle_secs": {"type": "integer", "minimum": 600},
|
||||
"calculate_since_number_of_days": {"type": "integer"},
|
||||
"allowed_risk": {"type": "number"},
|
||||
"stoploss_range_min": {"type": "number"},
|
||||
"stoploss_range_max": {"type": "number"},
|
||||
"stoploss_range_step": {"type": "number"},
|
||||
"minimum_winrate": {"type": "number"},
|
||||
"minimum_expectancy": {"type": "number"},
|
||||
"min_trade_number": {"type": "number"},
|
||||
"max_trade_duration_minute": {"type": "integer"},
|
||||
"remove_pumps": {"type": "boolean"},
|
||||
},
|
||||
"required": ["process_throttle_secs", "allowed_risk"],
|
||||
},
|
||||
"logging": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -99,14 +99,12 @@ def validate_config_consistency(conf: dict[str, Any], *, preliminary: bool = Fal
|
||||
|
||||
def _validate_unlimited_amount(conf: dict[str, Any]) -> None:
|
||||
"""
|
||||
If edge is disabled, either max_open_trades or stake_amount need to be set.
|
||||
Either max_open_trades or stake_amount need to be set.
|
||||
:raise: ConfigurationError if config validation failed
|
||||
"""
|
||||
if (
|
||||
not conf.get("edge", {}).get("enabled")
|
||||
and (conf.get("max_open_trades") == float("inf") or conf.get("max_open_trades") == -1)
|
||||
and conf.get("stake_amount") == UNLIMITED_STAKE_AMOUNT
|
||||
):
|
||||
conf.get("max_open_trades") == float("inf") or conf.get("max_open_trades") == -1
|
||||
) and conf.get("stake_amount") == UNLIMITED_STAKE_AMOUNT:
|
||||
raise ConfigurationError("`max_open_trades` and `stake_amount` cannot both be unlimited.")
|
||||
|
||||
|
||||
@@ -164,12 +162,9 @@ def _validate_edge(conf: dict[str, Any]) -> None:
|
||||
Edge and Dynamic whitelist should not both be enabled, since edge overrides dynamic whitelists.
|
||||
"""
|
||||
|
||||
if not conf.get("edge", {}).get("enabled"):
|
||||
return
|
||||
|
||||
if not conf.get("use_exit_signal", True):
|
||||
if conf.get("edge", {}).get("enabled"):
|
||||
raise ConfigurationError(
|
||||
"Edge requires `use_exit_signal` to be True, otherwise no sells will happen."
|
||||
"Edge is no longer supported and has been removed from Freqtrade with 2025.6."
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
This module contains the configuration class
|
||||
"""
|
||||
|
||||
import ast
|
||||
import logging
|
||||
import warnings
|
||||
from collections.abc import Callable
|
||||
@@ -314,14 +313,6 @@ class Configuration:
|
||||
]
|
||||
self._args_to_config_loop(config, configurations)
|
||||
|
||||
# Edge section:
|
||||
if self.args.get("stoploss_range"):
|
||||
txt_range = ast.literal_eval(self.args["stoploss_range"])
|
||||
config["edge"].update({"stoploss_range_min": txt_range[0]})
|
||||
config["edge"].update({"stoploss_range_max": txt_range[1]})
|
||||
config["edge"].update({"stoploss_range_step": txt_range[2]})
|
||||
logger.info("Parameter --stoplosses detected: %s ...", self.args["stoploss_range"])
|
||||
|
||||
# Hyperopt section
|
||||
|
||||
configurations = [
|
||||
|
||||
@@ -159,16 +159,6 @@ def process_temporary_deprecated_settings(config: Config) -> None:
|
||||
process_removed_setting(
|
||||
config, "ask_strategy", "ignore_roi_if_buy_signal", None, "ignore_roi_if_entry_signal"
|
||||
)
|
||||
if config.get("edge", {}).get(
|
||||
"enabled", False
|
||||
) and "capital_available_percentage" in config.get("edge", {}):
|
||||
raise ConfigurationError(
|
||||
"DEPRECATED: "
|
||||
"Using 'edge.capital_available_percentage' has been deprecated in favor of "
|
||||
"'tradable_balance_ratio'. Please migrate your configuration to "
|
||||
"'tradable_balance_ratio' and remove 'capital_available_percentage' "
|
||||
"from the edge configuration."
|
||||
)
|
||||
if "ticker_interval" in config:
|
||||
raise ConfigurationError(
|
||||
"DEPRECATED: 'ticker_interval' detected. "
|
||||
|
||||
@@ -405,7 +405,7 @@ class DataProvider:
|
||||
def runmode(self) -> RunMode:
|
||||
"""
|
||||
Get runmode of the bot
|
||||
can be "live", "dry-run", "backtest", "edgecli", "hyperopt" or "other".
|
||||
can be "live", "dry-run", "backtest", "hyperopt" or "other".
|
||||
"""
|
||||
return RunMode(self._config.get("runmode", RunMode.OTHER))
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
from .edge_positioning import Edge, PairInfo # noqa: F401
|
||||
@@ -1,524 +0,0 @@
|
||||
# pragma pylint: disable=W0603
|
||||
"""Edge positioning package"""
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from copy import deepcopy
|
||||
from datetime import timedelta
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
import numpy as np
|
||||
import utils_find_1st as utf1st
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, UNLIMITED_STAKE_AMOUNT, Config
|
||||
from freqtrade.data.history import get_timerange, load_data, refresh_data
|
||||
from freqtrade.enums import CandleType, ExitType, RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_seconds
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
from freqtrade.util import dt_now
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PairInfo(NamedTuple):
|
||||
stoploss: float
|
||||
winrate: float
|
||||
risk_reward_ratio: float
|
||||
required_risk_reward: float
|
||||
expectancy: float
|
||||
nb_trades: int
|
||||
avg_trade_duration: float
|
||||
|
||||
|
||||
class Edge:
|
||||
"""
|
||||
Calculates Win Rate, Risk Reward Ratio, Expectancy
|
||||
against historical data for a give set of markets and a strategy
|
||||
it then adjusts stoploss and position size accordingly
|
||||
and force it into the strategy
|
||||
Author: https://github.com/mishaker
|
||||
"""
|
||||
|
||||
_cached_pairs: dict[str, Any] = {} # Keeps a list of pairs
|
||||
|
||||
def __init__(self, config: Config, exchange, strategy) -> None:
|
||||
self.config = config
|
||||
self.exchange = exchange
|
||||
self.strategy: IStrategy = strategy
|
||||
|
||||
self.edge_config = self.config.get("edge", {})
|
||||
self._cached_pairs: dict[str, Any] = {} # Keeps a list of pairs
|
||||
self._final_pairs: list = []
|
||||
|
||||
# checking max_open_trades. it should be -1 as with Edge
|
||||
# the number of trades is determined by position size
|
||||
if self.config["max_open_trades"] != float("inf"):
|
||||
logger.critical("max_open_trades should be -1 in config !")
|
||||
|
||||
if self.config["stake_amount"] != UNLIMITED_STAKE_AMOUNT:
|
||||
raise OperationalException("Edge works only with unlimited stake amount")
|
||||
|
||||
self._capital_ratio: float = self.config["tradable_balance_ratio"]
|
||||
self._allowed_risk: float = self.edge_config.get("allowed_risk")
|
||||
self._since_number_of_days: int = self.edge_config.get("calculate_since_number_of_days", 14)
|
||||
self._last_updated: int = 0 # Timestamp of pairs last updated time
|
||||
self._refresh_pairs = True
|
||||
|
||||
self._stoploss_range_min = float(self.edge_config.get("stoploss_range_min", -0.01))
|
||||
self._stoploss_range_max = float(self.edge_config.get("stoploss_range_max", -0.05))
|
||||
self._stoploss_range_step = float(self.edge_config.get("stoploss_range_step", -0.001))
|
||||
|
||||
# calculating stoploss range
|
||||
self._stoploss_range = np.arange(
|
||||
self._stoploss_range_min, self._stoploss_range_max, self._stoploss_range_step
|
||||
)
|
||||
|
||||
self._timerange: TimeRange = TimeRange.parse_timerange(
|
||||
f"{(dt_now() - timedelta(days=self._since_number_of_days)).strftime('%Y%m%d')}-"
|
||||
)
|
||||
if config.get("fee"):
|
||||
self.fee = config["fee"]
|
||||
else:
|
||||
try:
|
||||
self.fee = self.exchange.get_fee(
|
||||
symbol=expand_pairlist(
|
||||
self.config["exchange"]["pair_whitelist"], list(self.exchange.markets)
|
||||
)[0]
|
||||
)
|
||||
except IndexError:
|
||||
self.fee = None
|
||||
|
||||
def calculate(self, pairs: list[str]) -> bool:
|
||||
if self.fee is None and pairs:
|
||||
self.fee = self.exchange.get_fee(pairs[0])
|
||||
|
||||
heartbeat = self.edge_config.get("process_throttle_secs")
|
||||
|
||||
if (self._last_updated > 0) and (
|
||||
self._last_updated + heartbeat > int(dt_now().timestamp())
|
||||
):
|
||||
return False
|
||||
|
||||
data: dict[str, Any] = {}
|
||||
logger.info("Using stake_currency: %s ...", self.config["stake_currency"])
|
||||
logger.info("Using local backtesting data (using whitelist in given config) ...")
|
||||
|
||||
if self._refresh_pairs:
|
||||
timerange_startup = deepcopy(self._timerange)
|
||||
timerange_startup.subtract_start(
|
||||
timeframe_to_seconds(self.strategy.timeframe) * self.strategy.startup_candle_count
|
||||
)
|
||||
refresh_data(
|
||||
datadir=self.config["datadir"],
|
||||
pairs=pairs,
|
||||
exchange=self.exchange,
|
||||
timeframe=self.strategy.timeframe,
|
||||
timerange=timerange_startup,
|
||||
data_format=self.config["dataformat_ohlcv"],
|
||||
candle_type=self.config.get("candle_type_def", CandleType.SPOT),
|
||||
)
|
||||
# Download informative pairs too
|
||||
res = defaultdict(list)
|
||||
for pair, timeframe, _ in self.strategy.gather_informative_pairs():
|
||||
res[timeframe].append(pair)
|
||||
for timeframe, inf_pairs in res.items():
|
||||
timerange_startup = deepcopy(self._timerange)
|
||||
timerange_startup.subtract_start(
|
||||
timeframe_to_seconds(timeframe) * self.strategy.startup_candle_count
|
||||
)
|
||||
refresh_data(
|
||||
datadir=self.config["datadir"],
|
||||
pairs=inf_pairs,
|
||||
exchange=self.exchange,
|
||||
timeframe=timeframe,
|
||||
timerange=timerange_startup,
|
||||
data_format=self.config["dataformat_ohlcv"],
|
||||
candle_type=self.config.get("candle_type_def", CandleType.SPOT),
|
||||
)
|
||||
|
||||
data = load_data(
|
||||
datadir=self.config["datadir"],
|
||||
pairs=pairs,
|
||||
timeframe=self.strategy.timeframe,
|
||||
timerange=self._timerange,
|
||||
startup_candles=self.strategy.startup_candle_count,
|
||||
data_format=self.config["dataformat_ohlcv"],
|
||||
candle_type=self.config.get("candle_type_def", CandleType.SPOT),
|
||||
)
|
||||
|
||||
if not data:
|
||||
# Reinitializing cached pairs
|
||||
self._cached_pairs = {}
|
||||
logger.critical("No data found. Edge is stopped ...")
|
||||
return False
|
||||
# Fake run-mode to Edge
|
||||
prior_rm = self.config["runmode"]
|
||||
self.config["runmode"] = RunMode.EDGE
|
||||
preprocessed = self.strategy.advise_all_indicators(data)
|
||||
self.config["runmode"] = prior_rm
|
||||
|
||||
# Print timeframe
|
||||
min_date, max_date = get_timerange(preprocessed)
|
||||
logger.info(
|
||||
f"Measuring data from {min_date.strftime(DATETIME_PRINT_FORMAT)} "
|
||||
f"up to {max_date.strftime(DATETIME_PRINT_FORMAT)} "
|
||||
f"({(max_date - min_date).days} days).."
|
||||
)
|
||||
# TODO: Should edge support shorts? needs to be investigated further
|
||||
# * (add enter_short exit_short)
|
||||
headers = ["date", "open", "high", "low", "close", "enter_long", "exit_long"]
|
||||
|
||||
trades: list = []
|
||||
for pair, pair_data in preprocessed.items():
|
||||
# Sorting dataframe by date and reset index
|
||||
pair_data = pair_data.sort_values(by=["date"])
|
||||
pair_data = pair_data.reset_index(drop=True)
|
||||
|
||||
df_analyzed = self.strategy.ft_advise_signals(pair_data, {"pair": pair})[headers].copy()
|
||||
|
||||
trades += self._find_trades_for_stoploss_range(df_analyzed, pair, self._stoploss_range)
|
||||
|
||||
# If no trade found then exit
|
||||
if len(trades) == 0:
|
||||
logger.info("No trades found.")
|
||||
return False
|
||||
|
||||
# Fill missing, calculable columns, profit, duration , abs etc.
|
||||
trades_df = self._fill_calculable_fields(DataFrame(trades))
|
||||
self._cached_pairs = self._process_expectancy(trades_df)
|
||||
self._last_updated = int(dt_now().timestamp())
|
||||
|
||||
return True
|
||||
|
||||
def stake_amount(
|
||||
self, pair: str, free_capital: float, total_capital: float, capital_in_trade: float
|
||||
) -> float:
|
||||
stoploss = self.get_stoploss(pair)
|
||||
available_capital = (total_capital + capital_in_trade) * self._capital_ratio
|
||||
allowed_capital_at_risk = available_capital * self._allowed_risk
|
||||
max_position_size = abs(allowed_capital_at_risk / stoploss)
|
||||
# Position size must be below available capital.
|
||||
position_size = min(min(max_position_size, free_capital), available_capital)
|
||||
if pair in self._cached_pairs:
|
||||
logger.info(
|
||||
"winrate: %s, expectancy: %s, position size: %s, pair: %s,"
|
||||
" capital in trade: %s, free capital: %s, total capital: %s,"
|
||||
" stoploss: %s, available capital: %s.",
|
||||
self._cached_pairs[pair].winrate,
|
||||
self._cached_pairs[pair].expectancy,
|
||||
position_size,
|
||||
pair,
|
||||
capital_in_trade,
|
||||
free_capital,
|
||||
total_capital,
|
||||
stoploss,
|
||||
available_capital,
|
||||
)
|
||||
return round(position_size, 15)
|
||||
|
||||
def get_stoploss(self, pair: str) -> float:
|
||||
if pair in self._cached_pairs:
|
||||
return self._cached_pairs[pair].stoploss
|
||||
else:
|
||||
logger.warning(
|
||||
f"Tried to access stoploss of non-existing pair {pair}, "
|
||||
"strategy stoploss is returned instead."
|
||||
)
|
||||
return self.strategy.stoploss
|
||||
|
||||
def adjust(self, pairs: list[str]) -> list:
|
||||
"""
|
||||
Filters out and sorts "pairs" according to Edge calculated pairs
|
||||
"""
|
||||
final = []
|
||||
for pair, info in self._cached_pairs.items():
|
||||
if (
|
||||
info.expectancy > float(self.edge_config.get("minimum_expectancy", 0.2))
|
||||
and info.winrate > float(self.edge_config.get("minimum_winrate", 0.60))
|
||||
and pair in pairs
|
||||
):
|
||||
final.append(pair)
|
||||
|
||||
if self._final_pairs != final:
|
||||
self._final_pairs = final
|
||||
if self._final_pairs:
|
||||
logger.info(
|
||||
"Minimum expectancy and minimum winrate are met only for %s,"
|
||||
" so other pairs are filtered out.",
|
||||
self._final_pairs,
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"Edge removed all pairs as no pair with minimum expectancy "
|
||||
"and minimum winrate was found !"
|
||||
)
|
||||
|
||||
return self._final_pairs
|
||||
|
||||
def accepted_pairs(self) -> list[dict[str, Any]]:
|
||||
"""
|
||||
return a list of accepted pairs along with their winrate, expectancy and stoploss
|
||||
"""
|
||||
final = []
|
||||
for pair, info in self._cached_pairs.items():
|
||||
if info.expectancy > float(
|
||||
self.edge_config.get("minimum_expectancy", 0.2)
|
||||
) and info.winrate > float(self.edge_config.get("minimum_winrate", 0.60)):
|
||||
final.append(
|
||||
{
|
||||
"Pair": pair,
|
||||
"Winrate": info.winrate,
|
||||
"Expectancy": info.expectancy,
|
||||
"Stoploss": info.stoploss,
|
||||
}
|
||||
)
|
||||
return final
|
||||
|
||||
def _fill_calculable_fields(self, result: DataFrame) -> DataFrame:
|
||||
"""
|
||||
The result frame contains a number of columns that are calculable
|
||||
from other columns. These are left blank till all rows are added,
|
||||
to be populated in single vector calls.
|
||||
|
||||
Columns to be populated are:
|
||||
- Profit
|
||||
- trade duration
|
||||
- profit abs
|
||||
:param result Dataframe
|
||||
:return: result Dataframe
|
||||
"""
|
||||
# We set stake amount to an arbitrary amount, as it doesn't change the calculation.
|
||||
# All returned values are relative, they are defined as ratios.
|
||||
stake = 0.015
|
||||
|
||||
result["trade_duration"] = result["close_date"] - result["open_date"]
|
||||
|
||||
result["trade_duration"] = result["trade_duration"].map(
|
||||
lambda x: int(x.total_seconds() / 60)
|
||||
)
|
||||
|
||||
# Spends, Takes, Profit, Absolute Profit
|
||||
|
||||
# Buy Price
|
||||
result["buy_vol"] = stake / result["open_rate"] # How many target are we buying
|
||||
result["buy_fee"] = stake * self.fee
|
||||
result["buy_spend"] = stake + result["buy_fee"] # How much we're spending
|
||||
|
||||
# Sell price
|
||||
result["sell_sum"] = result["buy_vol"] * result["close_rate"]
|
||||
result["sell_fee"] = result["sell_sum"] * self.fee
|
||||
result["sell_take"] = result["sell_sum"] - result["sell_fee"]
|
||||
|
||||
# profit_ratio
|
||||
result["profit_ratio"] = (result["sell_take"] - result["buy_spend"]) / result["buy_spend"]
|
||||
|
||||
# Absolute profit
|
||||
result["profit_abs"] = result["sell_take"] - result["buy_spend"]
|
||||
|
||||
return result
|
||||
|
||||
def _process_expectancy(self, results: DataFrame) -> dict[str, Any]:
|
||||
"""
|
||||
This calculates WinRate, Required Risk Reward, Risk Reward and Expectancy of all pairs
|
||||
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)
|
||||
results = results.groupby(["pair", "stoploss"]).filter(lambda x: len(x) > min_trades_number)
|
||||
###################################
|
||||
|
||||
# Removing outliers (Only Pumps) from the dataset
|
||||
# The method to detect outliers is to calculate standard deviation
|
||||
# Then every value more than (standard deviation + 2*average) is out (pump)
|
||||
#
|
||||
# Removing Pumps
|
||||
if self.edge_config.get("remove_pumps", False):
|
||||
results = results[
|
||||
results["profit_abs"]
|
||||
< 2 * results["profit_abs"].std() + results["profit_abs"].mean()
|
||||
]
|
||||
##########################################################################
|
||||
|
||||
# Removing trades having a duration more than X minutes (set in config)
|
||||
max_trade_duration = self.edge_config.get("max_trade_duration_minute", 1440)
|
||||
results = results[results.trade_duration < max_trade_duration]
|
||||
#######################################################################
|
||||
|
||||
if results.empty:
|
||||
return {}
|
||||
|
||||
groupby_aggregator = {
|
||||
"profit_abs": [
|
||||
("nb_trades", "count"), # number of all trades
|
||||
("profit_sum", lambda x: x[x > 0].sum()), # cumulative profit of all winning trades
|
||||
("loss_sum", lambda x: abs(x[x < 0].sum())), # cumulative loss of all losing trades
|
||||
("nb_win_trades", lambda x: x[x > 0].count()), # number of winning trades
|
||||
],
|
||||
"trade_duration": [("avg_trade_duration", "mean")],
|
||||
}
|
||||
|
||||
# Group by (pair and stoploss) by applying above aggregator
|
||||
df = (
|
||||
results.groupby(["pair", "stoploss"])[["profit_abs", "trade_duration"]]
|
||||
.agg(groupby_aggregator)
|
||||
.reset_index(col_level=1)
|
||||
)
|
||||
|
||||
# Dropping level 0 as we don't need it
|
||||
df.columns = df.columns.droplevel(0)
|
||||
|
||||
# Calculating number of losing trades, average win and average loss
|
||||
df["nb_loss_trades"] = df["nb_trades"] - df["nb_win_trades"]
|
||||
df["average_win"] = np.where(
|
||||
df["nb_win_trades"] == 0, 0.0, df["profit_sum"] / df["nb_win_trades"]
|
||||
)
|
||||
df["average_loss"] = np.where(
|
||||
df["nb_loss_trades"] == 0, 0.0, df["loss_sum"] / df["nb_loss_trades"]
|
||||
)
|
||||
|
||||
# Win rate = number of profitable trades / number of trades
|
||||
df["winrate"] = df["nb_win_trades"] / df["nb_trades"]
|
||||
|
||||
# risk_reward_ratio = average win / average loss
|
||||
df["risk_reward_ratio"] = df["average_win"] / df["average_loss"]
|
||||
|
||||
# required_risk_reward = (1 / winrate) - 1
|
||||
df["required_risk_reward"] = (1 / df["winrate"]) - 1
|
||||
|
||||
# expectancy = (risk_reward_ratio * winrate) - (lossrate)
|
||||
df["expectancy"] = (df["risk_reward_ratio"] * df["winrate"]) - (1 - df["winrate"])
|
||||
|
||||
# sort by expectancy and stoploss
|
||||
df = (
|
||||
df.sort_values(by=["expectancy", "stoploss"], ascending=False)
|
||||
.groupby("pair")
|
||||
.first()
|
||||
.sort_values(by=["expectancy"], ascending=False)
|
||||
.reset_index()
|
||||
)
|
||||
|
||||
final = {}
|
||||
for x in df.itertuples():
|
||||
final[x.pair] = PairInfo(
|
||||
x.stoploss,
|
||||
x.winrate,
|
||||
x.risk_reward_ratio,
|
||||
x.required_risk_reward,
|
||||
x.expectancy,
|
||||
x.nb_trades,
|
||||
x.avg_trade_duration,
|
||||
)
|
||||
|
||||
# Returning a list of pairs in order of "expectancy"
|
||||
return final
|
||||
|
||||
def _find_trades_for_stoploss_range(self, df, pair: str, stoploss_range) -> list:
|
||||
buy_column = df["enter_long"].values
|
||||
sell_column = df["exit_long"].values
|
||||
date_column = df["date"].values
|
||||
ohlc_columns = df[["open", "high", "low", "close"]].values
|
||||
|
||||
result: list = []
|
||||
for stoploss in stoploss_range:
|
||||
result += self._detect_next_stop_or_sell_point(
|
||||
buy_column, sell_column, date_column, ohlc_columns, round(stoploss, 6), pair
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def _detect_next_stop_or_sell_point(
|
||||
self, buy_column, sell_column, date_column, ohlc_columns, stoploss, pair: str
|
||||
):
|
||||
"""
|
||||
Iterate through ohlc_columns in order to find the next trade
|
||||
Next trade opens from the first buy signal noticed to
|
||||
The sell or stoploss signal after it.
|
||||
It then cuts OHLC, buy_column, sell_column and date_column.
|
||||
Cut from (the exit trade index) + 1.
|
||||
|
||||
Author: https://github.com/mishaker
|
||||
"""
|
||||
|
||||
result: list = []
|
||||
start_point = 0
|
||||
|
||||
while True:
|
||||
open_trade_index = utf1st.find_1st(buy_column, 1, utf1st.cmp_equal)
|
||||
|
||||
# Return empty if we don't find trade entry (i.e. buy==1) or
|
||||
# we find a buy but at the end of array
|
||||
if open_trade_index == -1 or open_trade_index == len(buy_column) - 1:
|
||||
break
|
||||
else:
|
||||
# When a buy signal is seen,
|
||||
# trade opens in reality on the next candle
|
||||
open_trade_index += 1
|
||||
|
||||
open_price = ohlc_columns[open_trade_index, 0]
|
||||
stop_price = open_price * (stoploss + 1)
|
||||
|
||||
# Searching for the index where stoploss is hit
|
||||
stop_index = utf1st.find_1st(
|
||||
ohlc_columns[open_trade_index:, 2], stop_price, utf1st.cmp_smaller
|
||||
)
|
||||
|
||||
# If we don't find it then we assume stop_index will be far in future (infinite number)
|
||||
if stop_index == -1:
|
||||
stop_index = float("inf")
|
||||
|
||||
# Searching for the index where sell is hit
|
||||
sell_index = utf1st.find_1st(sell_column[open_trade_index:], 1, utf1st.cmp_equal)
|
||||
|
||||
# If we don't find it then we assume sell_index will be far in future (infinite number)
|
||||
if sell_index == -1:
|
||||
sell_index = float("inf")
|
||||
|
||||
# Check if we don't find any stop or sell point (in that case trade remains open)
|
||||
# It is not interesting for Edge to consider it so we simply ignore the trade
|
||||
# And stop iterating there is no more entry
|
||||
if stop_index == sell_index == float("inf"):
|
||||
break
|
||||
|
||||
if stop_index <= sell_index:
|
||||
exit_index = open_trade_index + stop_index
|
||||
exit_type = ExitType.STOP_LOSS
|
||||
exit_price = stop_price
|
||||
elif stop_index > sell_index:
|
||||
# If exit is SELL then we exit at the next candle
|
||||
exit_index = open_trade_index + sell_index + 1
|
||||
|
||||
# Check if we have the next candle
|
||||
if len(ohlc_columns) - 1 < exit_index:
|
||||
break
|
||||
|
||||
exit_type = ExitType.EXIT_SIGNAL
|
||||
exit_price = ohlc_columns[exit_index, 0]
|
||||
|
||||
trade = {
|
||||
"pair": pair,
|
||||
"stoploss": stoploss,
|
||||
"profit_ratio": "",
|
||||
"profit_abs": "",
|
||||
"open_date": date_column[open_trade_index],
|
||||
"close_date": date_column[exit_index],
|
||||
"trade_duration": "",
|
||||
"open_rate": round(open_price, 15),
|
||||
"close_rate": round(exit_price, 15),
|
||||
"exit_type": exit_type,
|
||||
}
|
||||
|
||||
result.append(trade)
|
||||
|
||||
# Giving a view of exit_index till the end of array
|
||||
buy_column = buy_column[exit_index:]
|
||||
sell_column = sell_column[exit_index:]
|
||||
date_column = date_column[exit_index:]
|
||||
ohlc_columns = ohlc_columns[exit_index:]
|
||||
start_point += exit_index
|
||||
|
||||
return result
|
||||
@@ -4,13 +4,12 @@ from enum import Enum
|
||||
class RunMode(str, Enum):
|
||||
"""
|
||||
Bot running mode (backtest, hyperopt, ...)
|
||||
can be "live", "dry-run", "backtest", "edge", "hyperopt".
|
||||
can be "live", "dry-run", "backtest", "hyperopt".
|
||||
"""
|
||||
|
||||
LIVE = "live"
|
||||
DRY_RUN = "dry_run"
|
||||
BACKTEST = "backtest"
|
||||
EDGE = "edge"
|
||||
HYPEROPT = "hyperopt"
|
||||
UTIL_EXCHANGE = "util_exchange"
|
||||
UTIL_NO_EXCHANGE = "util_no_exchange"
|
||||
@@ -20,5 +19,5 @@ class RunMode(str, Enum):
|
||||
|
||||
|
||||
TRADE_MODES = [RunMode.LIVE, RunMode.DRY_RUN]
|
||||
OPTIMIZE_MODES = [RunMode.BACKTEST, RunMode.EDGE, RunMode.HYPEROPT]
|
||||
OPTIMIZE_MODES = [RunMode.BACKTEST, RunMode.HYPEROPT]
|
||||
NON_UTIL_MODES = TRADE_MODES + OPTIMIZE_MODES
|
||||
|
||||
@@ -107,7 +107,7 @@ EXCHANGE_HAS_OPTIONAL = [
|
||||
def remove_exchange_credentials(exchange_config: ExchangeConfig, dry_run: bool) -> None:
|
||||
"""
|
||||
Removes exchange keys from the configuration and specifies dry-run
|
||||
Used for backtesting / hyperopt / edge and utils.
|
||||
Used for backtesting / hyperopt and utils.
|
||||
Modifies the input dict!
|
||||
"""
|
||||
if dry_run:
|
||||
|
||||
@@ -18,7 +18,6 @@ from freqtrade.configuration import validate_config_consistency
|
||||
from freqtrade.constants import BuySell, Config, EntryExecuteMode, ExchangeConfig, LongShort
|
||||
from freqtrade.data.converter import order_book_to_dataframe
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.edge import Edge
|
||||
from freqtrade.enums import (
|
||||
ExitCheckTuple,
|
||||
ExitType,
|
||||
@@ -131,13 +130,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Attach Wallets to strategy instance
|
||||
self.strategy.wallets = self.wallets
|
||||
|
||||
# Initializing Edge only if enabled
|
||||
self.edge = (
|
||||
Edge(self.config, self.exchange, self.strategy)
|
||||
if self.config.get("edge", {}).get("enabled", False)
|
||||
else None
|
||||
)
|
||||
|
||||
# Init ExternalMessageConsumer if enabled
|
||||
self.emc = (
|
||||
ExternalMessageConsumer(self.config, self.dataprovider)
|
||||
@@ -242,9 +234,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.rpc.startup_messages(self.config, self.pairlists, self.protections)
|
||||
# Update older trades with precision and precision mode
|
||||
self.startup_backpopulate_precision()
|
||||
if not self.edge:
|
||||
# Adjust stoploss if it was changed
|
||||
Trade.stoploss_reinitialization(self.strategy.stoploss)
|
||||
# Adjust stoploss if it was changed
|
||||
Trade.stoploss_reinitialization(self.strategy.stoploss)
|
||||
|
||||
# Only update open orders on startup
|
||||
# This will update the database after the initial migration
|
||||
@@ -335,7 +326,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
def _refresh_active_whitelist(self, trades: list[Trade] | None = None) -> list[str]:
|
||||
"""
|
||||
Refresh active whitelist from pairlist or edge and extend it with
|
||||
Refresh active whitelist from pairlist and extend it with
|
||||
pairs that have open trades.
|
||||
"""
|
||||
# Refresh whitelist
|
||||
@@ -343,11 +334,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.pairlists.refresh_pairlist()
|
||||
_whitelist = self.pairlists.whitelist
|
||||
|
||||
# Calculating Edge positioning
|
||||
if self.edge:
|
||||
self.edge.calculate(_whitelist)
|
||||
_whitelist = self.edge.adjust(_whitelist)
|
||||
|
||||
if trades:
|
||||
# Extend active-pair whitelist with pairs of open trades
|
||||
# It ensures that candle (OHLCV) data are downloaded for open trades as well
|
||||
@@ -701,9 +687,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
else:
|
||||
self.log_once(f"Pair {pair} is currently locked.", logger.info)
|
||||
return False
|
||||
stake_amount = self.wallets.get_trade_stake_amount(
|
||||
pair, self.config["max_open_trades"], self.edge
|
||||
)
|
||||
stake_amount = self.wallets.get_trade_stake_amount(pair, self.config["max_open_trades"])
|
||||
|
||||
bid_check_dom = self.config.get("entry_pricing", {}).get("check_depth_of_market", {})
|
||||
if (bid_check_dom.get("enabled", False)) and (
|
||||
@@ -1042,7 +1026,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
precision_mode_price=self.exchange.precision_mode_price,
|
||||
contract_size=self.exchange.get_contract_size(pair),
|
||||
)
|
||||
stoploss = self.strategy.stoploss if not self.edge else self.edge.get_stoploss(pair)
|
||||
stoploss = self.strategy.stoploss
|
||||
trade.adjust_stop_loss(trade.open_rate, stoploss, initial=True)
|
||||
|
||||
else:
|
||||
@@ -1170,7 +1154,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
pair, enter_limit_requested, leverage
|
||||
)
|
||||
|
||||
if not self.edge and trade is None:
|
||||
if trade is None:
|
||||
stake_available = self.wallets.get_available_stake_amount()
|
||||
stake_amount = strategy_safe_wrapper(
|
||||
self.strategy.custom_stake_amount, default_retval=stake_amount
|
||||
@@ -1382,7 +1366,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
datetime.now(timezone.utc),
|
||||
enter=enter,
|
||||
exit_=exit_,
|
||||
force_stoploss=self.edge.get_stoploss(trade.pair) if self.edge else 0,
|
||||
force_stoploss=0,
|
||||
)
|
||||
for should_exit in exits:
|
||||
if should_exit.exit_flag:
|
||||
@@ -1487,13 +1471,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
# If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange
|
||||
if len(stoploss_orders) == 0:
|
||||
stop_price = trade.stoploss_or_liquidation
|
||||
if self.edge:
|
||||
stoploss = self.edge.get_stoploss(pair=trade.pair)
|
||||
stop_price = (
|
||||
trade.open_rate * (1 - stoploss)
|
||||
if trade.is_short
|
||||
else trade.open_rate * (1 + stoploss)
|
||||
)
|
||||
|
||||
if self.create_stoploss_order(trade=trade, stop_price=stop_price):
|
||||
# The above will return False if the placement failed and the trade was force-sold.
|
||||
@@ -2370,10 +2347,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
if send_msg:
|
||||
# Don't cancel stoploss in recovery modes immediately
|
||||
trade = self.cancel_stoploss_on_exchange(trade)
|
||||
if not self.edge:
|
||||
# TODO: should shorting/leverage be supported by Edge,
|
||||
# then this will need to be fixed.
|
||||
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
|
||||
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
|
||||
if (
|
||||
order.ft_order_side == trade.entry_side
|
||||
or (trade.amount > 0 and trade.is_open)
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
# pragma pylint: disable=missing-docstring, W0212, too-many-arguments
|
||||
|
||||
"""
|
||||
This module contains the edge backtesting interface
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from freqtrade import constants
|
||||
from freqtrade.configuration import TimeRange, validate_config_consistency
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.edge import Edge
|
||||
from freqtrade.optimize.optimize_reports import generate_edge_table
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EdgeCli:
|
||||
"""
|
||||
EdgeCli class, this class contains all the logic to run edge backtesting
|
||||
|
||||
To run a edge backtest:
|
||||
edge = EdgeCli(config)
|
||||
edge.start()
|
||||
"""
|
||||
|
||||
def __init__(self, config: Config) -> None:
|
||||
self.config = config
|
||||
|
||||
# Ensure using dry-run
|
||||
self.config["dry_run"] = True
|
||||
self.config["stake_amount"] = constants.UNLIMITED_STAKE_AMOUNT
|
||||
self.exchange = ExchangeResolver.load_exchange(self.config)
|
||||
self.strategy = StrategyResolver.load_strategy(self.config)
|
||||
self.strategy.dp = DataProvider(config, self.exchange)
|
||||
|
||||
validate_config_consistency(self.config)
|
||||
|
||||
self.edge = Edge(config, self.exchange, self.strategy)
|
||||
# Set refresh_pairs to false for edge-cli (it must be true for edge)
|
||||
self.edge._refresh_pairs = False
|
||||
|
||||
self.edge._timerange = TimeRange.parse_timerange(
|
||||
None if self.config.get("timerange") is None else str(self.config.get("timerange"))
|
||||
)
|
||||
self.strategy.ft_bot_start()
|
||||
|
||||
def start(self) -> None:
|
||||
result = self.edge.calculate(self.config["exchange"]["pair_whitelist"])
|
||||
if result:
|
||||
print("") # blank line for readability
|
||||
generate_edge_table(self.edge._cached_pairs)
|
||||
@@ -1,6 +1,5 @@
|
||||
# flake8: noqa: F401
|
||||
from freqtrade.optimize.optimize_reports.bt_output import (
|
||||
generate_edge_table,
|
||||
generate_wins_draws_losses,
|
||||
show_backtest_result,
|
||||
show_backtest_results,
|
||||
|
||||
@@ -499,33 +499,3 @@ def show_sorted_pairlist(config: Config, backtest_stats: BacktestResultType):
|
||||
if result["key"] != "TOTAL":
|
||||
print(f'"{result["key"]}", // {result["profit_mean"]:.2%}')
|
||||
print("]")
|
||||
|
||||
|
||||
def generate_edge_table(results: dict) -> None:
|
||||
tabular_data = []
|
||||
headers = [
|
||||
"Pair",
|
||||
"Stoploss",
|
||||
"Win Rate",
|
||||
"Risk Reward Ratio",
|
||||
"Required Risk Reward",
|
||||
"Expectancy",
|
||||
"Total Number of Trades",
|
||||
"Average Duration (min)",
|
||||
]
|
||||
|
||||
for result in results.items():
|
||||
if result[1].nb_trades > 0:
|
||||
tabular_data.append(
|
||||
[
|
||||
result[0],
|
||||
f"{result[1].stoploss:.10g}",
|
||||
f"{result[1].winrate:.2f}",
|
||||
f"{result[1].risk_reward_ratio:.2f}",
|
||||
f"{result[1].required_risk_reward:.2f}",
|
||||
f"{result[1].expectancy:.2f}",
|
||||
result[1].nb_trades,
|
||||
round(result[1].avg_trade_duration),
|
||||
]
|
||||
)
|
||||
print_rich_table(tabular_data, headers, summary="EDGE TABLE")
|
||||
|
||||
@@ -61,7 +61,7 @@ class PairListManager(LoggingMixin):
|
||||
LoggingMixin.__init__(self, logger, refresh_period)
|
||||
|
||||
def _check_backtest(self) -> None:
|
||||
if self._config["runmode"] not in (RunMode.BACKTEST, RunMode.EDGE, RunMode.HYPEROPT):
|
||||
if self._config["runmode"] not in (RunMode.BACKTEST, RunMode.HYPEROPT):
|
||||
return
|
||||
|
||||
pairlist_errors: list[str] = []
|
||||
|
||||
@@ -263,12 +263,6 @@ def list_custom_data(trade_id: int, key: str | None = Query(None), rpc: RPC = De
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
|
||||
# TODO: Missing response model
|
||||
@router.get("/edge", tags=["info"])
|
||||
def edge(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_edge()
|
||||
|
||||
|
||||
@router.get("/show_config", response_model=ShowConfig, tags=["info"])
|
||||
def show_config(rpc: RPC | None = Depends(get_rpc_optional), config=Depends(get_config)):
|
||||
state: State | str = ""
|
||||
|
||||
@@ -1345,12 +1345,6 @@ class RPC:
|
||||
|
||||
return {"log_count": len(records), "logs": records}
|
||||
|
||||
def _rpc_edge(self) -> list[dict[str, Any]]:
|
||||
"""Returns information related to Edge"""
|
||||
if not self._freqtrade.edge:
|
||||
raise RPCException("Edge is not enabled.")
|
||||
return self._freqtrade.edge.accepted_pairs()
|
||||
|
||||
@staticmethod
|
||||
def _convert_dataframe_to_dict(
|
||||
strategy: str,
|
||||
|
||||
@@ -215,7 +215,6 @@ class Telegram(RPCHandler):
|
||||
r"/forceshort$",
|
||||
r"/forcesell$",
|
||||
r"/forceexit$",
|
||||
r"/edge$",
|
||||
r"/health$",
|
||||
r"/help$",
|
||||
r"/version$",
|
||||
@@ -299,7 +298,6 @@ class Telegram(RPCHandler):
|
||||
CommandHandler("blacklist", self._blacklist),
|
||||
CommandHandler(["blacklist_delete", "bl_delete"], self._blacklist_delete),
|
||||
CommandHandler("logs", self._logs),
|
||||
CommandHandler("edge", self._edge),
|
||||
CommandHandler("health", self._health),
|
||||
CommandHandler("help", self._help),
|
||||
CommandHandler("version", self._version),
|
||||
@@ -1795,23 +1793,6 @@ class Telegram(RPCHandler):
|
||||
if msgs:
|
||||
await self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2)
|
||||
|
||||
@authorized_only
|
||||
async def _edge(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /edge
|
||||
Shows information related to Edge
|
||||
"""
|
||||
edge_pairs = self._rpc._rpc_edge()
|
||||
if not edge_pairs:
|
||||
message = "<b>Edge only validated following pairs:</b>"
|
||||
await self._send_msg(message, parse_mode=ParseMode.HTML)
|
||||
|
||||
for chunk in chunks(edge_pairs, 25):
|
||||
edge_pairs_tab = tabulate(chunk, headers="keys", tablefmt="simple")
|
||||
message = f"<b>Edge only validated following pairs:</b>\n<pre>{edge_pairs_tab}</pre>"
|
||||
|
||||
await self._send_msg(message, parse_mode=ParseMode.HTML)
|
||||
|
||||
@authorized_only
|
||||
async def _help(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
@@ -1864,7 +1845,6 @@ class Telegram(RPCHandler):
|
||||
"*/balance total:* `Show account balance per currency`\n"
|
||||
"*/logs [limit]:* `Show latest logs - defaults to 10` \n"
|
||||
"*/count:* `Show number of active trades compared to allowed number of trades`\n"
|
||||
"*/edge:* `Shows validated pairs by Edge if it is enabled` \n"
|
||||
"*/health* `Show latest process timestamp - defaults to 1970-01-01 00:00:00` \n"
|
||||
"*/marketdir [long | short | even | none]:* `Updates the user managed variable "
|
||||
"that represents the current market direction. If no direction is provided `"
|
||||
|
||||
@@ -352,7 +352,7 @@ class Wallets:
|
||||
return max(stake_amount, 0)
|
||||
|
||||
def get_trade_stake_amount(
|
||||
self, pair: str, max_open_trades: IntOrInf, edge=None, update: bool = True
|
||||
self, pair: str, max_open_trades: IntOrInf, update: bool = True
|
||||
) -> float:
|
||||
"""
|
||||
Calculate stake amount for the trade
|
||||
@@ -366,19 +366,11 @@ class Wallets:
|
||||
val_tied_up = Trade.total_open_trades_stakes()
|
||||
available_amount = self.get_available_stake_amount()
|
||||
|
||||
if edge:
|
||||
stake_amount = edge.stake_amount(
|
||||
pair,
|
||||
self.get_free(self._stake_currency),
|
||||
self.get_total(self._stake_currency),
|
||||
val_tied_up,
|
||||
stake_amount = self._config["stake_amount"]
|
||||
if stake_amount == UNLIMITED_STAKE_AMOUNT:
|
||||
stake_amount = self._calculate_unlimited_stake_amount(
|
||||
available_amount, val_tied_up, max_open_trades
|
||||
)
|
||||
else:
|
||||
stake_amount = self._config["stake_amount"]
|
||||
if stake_amount == UNLIMITED_STAKE_AMOUNT:
|
||||
stake_amount = self._calculate_unlimited_stake_amount(
|
||||
available_amount, val_tied_up, max_open_trades
|
||||
)
|
||||
|
||||
return self._check_available_stake_amount(stake_amount, available_amount)
|
||||
|
||||
|
||||
@@ -189,13 +189,6 @@ class FtRestClient:
|
||||
"""
|
||||
return self._get("monthly", params={"timescale": months} if months else None)
|
||||
|
||||
def edge(self):
|
||||
"""Return information about edge.
|
||||
|
||||
:return: json object
|
||||
"""
|
||||
return self._get("edge")
|
||||
|
||||
def profit(self):
|
||||
"""Return the profit summary.
|
||||
|
||||
|
||||
@@ -72,7 +72,6 @@ def test_FtRestClient_call_invalid(caplog):
|
||||
("weekly", [15], {}),
|
||||
("monthly", [], {}),
|
||||
("monthly", [12], {}),
|
||||
("edge", [], {}),
|
||||
("profit", [], {}),
|
||||
("stats", [], {}),
|
||||
("performance", [], {}),
|
||||
|
||||
@@ -52,7 +52,6 @@ nav:
|
||||
- Orderflow: advanced-orderflow.md
|
||||
- Producer/Consumer mode: producer-consumer.md
|
||||
- SQL Cheat-sheet: sql_cheatsheet.md
|
||||
- Edge Positioning: edge.md
|
||||
- FAQ: faq.md
|
||||
- Strategy migration: strategy_migration.md
|
||||
- Updating Freqtrade: updating.md
|
||||
|
||||
@@ -16,6 +16,7 @@ from freqtrade.commands import (
|
||||
start_convert_trades,
|
||||
start_create_userdir,
|
||||
start_download_data,
|
||||
start_edge,
|
||||
start_hyperopt_list,
|
||||
start_hyperopt_show,
|
||||
start_install_ui,
|
||||
@@ -1937,3 +1938,15 @@ def test_start_show_config(capsys, caplog):
|
||||
assert '"max_open_trades":' in captured.out
|
||||
assert '"secret": "REDACTED"' not in captured.out
|
||||
assert log_has_re(r"Sensitive information will be shown in the upcoming output.*", caplog)
|
||||
|
||||
|
||||
def test_start_edge():
|
||||
args = [
|
||||
"edge",
|
||||
"--config",
|
||||
"tests/testdata/testconfigs/main_test_config.json",
|
||||
]
|
||||
|
||||
pargs = get_args(args)
|
||||
with pytest.raises(OperationalException, match="The Edge module has been deprecated in 2023.9"):
|
||||
start_edge(pargs)
|
||||
|
||||
@@ -16,8 +16,7 @@ from xdist.scheduler.loadscope import LoadScopeScheduling
|
||||
from freqtrade import constants
|
||||
from freqtrade.commands import Arguments
|
||||
from freqtrade.data.converter import ohlcv_to_dataframe, trades_list_to_df
|
||||
from freqtrade.edge import PairInfo
|
||||
from freqtrade.enums import CandleType, MarginMode, RunMode, SignalDirection, TradingMode
|
||||
from freqtrade.enums import CandleType, MarginMode, SignalDirection, TradingMode
|
||||
from freqtrade.exchange import Exchange, timeframe_to_minutes, timeframe_to_seconds
|
||||
from freqtrade.freqtradebot import FreqtradeBot
|
||||
from freqtrade.persistence import LocalTrade, Order, Trade, init_db
|
||||
@@ -298,24 +297,6 @@ def patch_whitelist(mocker, conf) -> None:
|
||||
)
|
||||
|
||||
|
||||
def patch_edge(mocker) -> None:
|
||||
# "ETH/BTC",
|
||||
# "LTC/BTC",
|
||||
# "XRP/BTC",
|
||||
# "NEO/BTC"
|
||||
|
||||
mocker.patch(
|
||||
"freqtrade.edge.Edge._cached_pairs",
|
||||
mocker.PropertyMock(
|
||||
return_value={
|
||||
"NEO/BTC": PairInfo(-0.20, 0.66, 3.71, 0.50, 1.71, 10, 25),
|
||||
"LTC/BTC": PairInfo(-0.21, 0.66, 3.71, 0.50, 1.71, 11, 20),
|
||||
}
|
||||
),
|
||||
)
|
||||
mocker.patch("freqtrade.edge.Edge.calculate", MagicMock(return_value=True))
|
||||
|
||||
|
||||
# Functions for recurrent object patching
|
||||
|
||||
|
||||
@@ -2603,31 +2584,6 @@ def buy_order_fee():
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def edge_conf(default_conf):
|
||||
conf = deepcopy(default_conf)
|
||||
conf["runmode"] = RunMode.DRY_RUN
|
||||
conf["max_open_trades"] = -1
|
||||
conf["tradable_balance_ratio"] = 0.5
|
||||
conf["stake_amount"] = constants.UNLIMITED_STAKE_AMOUNT
|
||||
conf["edge"] = {
|
||||
"enabled": True,
|
||||
"process_throttle_secs": 1800,
|
||||
"calculate_since_number_of_days": 14,
|
||||
"allowed_risk": 0.01,
|
||||
"stoploss_range_min": -0.01,
|
||||
"stoploss_range_max": -0.1,
|
||||
"stoploss_range_step": -0.01,
|
||||
"maximum_winrate": 0.80,
|
||||
"minimum_expectancy": 0.20,
|
||||
"min_trade_number": 15,
|
||||
"max_trade_duration_minute": 1440,
|
||||
"remove_pumps": False,
|
||||
}
|
||||
|
||||
return conf
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rpc_balance():
|
||||
return {
|
||||
|
||||
@@ -1,606 +0,0 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103, C0330
|
||||
# pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments
|
||||
|
||||
import logging
|
||||
import math
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.data.converter import ohlcv_to_dataframe
|
||||
from freqtrade.edge import Edge, PairInfo
|
||||
from freqtrade.enums import ExitType
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.util.datetime_helpers import dt_ts, dt_utc
|
||||
from tests.conftest import EXMS, get_patched_freqtradebot, log_has
|
||||
from tests.optimize import (
|
||||
BTContainer,
|
||||
BTrade,
|
||||
_build_backtest_dataframe,
|
||||
_get_frame_time_from_offset,
|
||||
)
|
||||
|
||||
|
||||
# Cases to be tested:
|
||||
# 1) Open trade should be removed from the end
|
||||
# 2) Two complete trades within dataframe (with sell hit for all)
|
||||
# 3) Entered, sl 1%, candle drops 8% => Trade closed, 1% loss
|
||||
# 4) Entered, sl 3%, candle drops 4%, recovers to 1% => Trade closed, 3% loss
|
||||
# 5) Stoploss and sell are hit. should sell on stoploss
|
||||
####################################################################
|
||||
|
||||
tests_start_time = dt_utc(2018, 10, 3)
|
||||
timeframe_in_minute = 60
|
||||
|
||||
# End helper functions
|
||||
# Open trade should be removed from the end
|
||||
tc0 = BTContainer(
|
||||
data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 1],
|
||||
], # enter trade (signal on last candle)
|
||||
stop_loss=-0.99,
|
||||
roi={"0": float("inf")},
|
||||
profit_perc=0.00,
|
||||
trades=[],
|
||||
)
|
||||
|
||||
# Two complete trades within dataframe(with sell hit for all)
|
||||
tc1 = BTContainer(
|
||||
data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 1], # enter trade (signal on last candle)
|
||||
[2, 5000, 5025, 4975, 4987, 6172, 0, 0], # exit at open
|
||||
[3, 5000, 5025, 4975, 4987, 6172, 1, 0], # no action
|
||||
[4, 5000, 5025, 4975, 4987, 6172, 0, 0], # should enter the trade
|
||||
[5, 5000, 5025, 4975, 4987, 6172, 0, 1], # no action
|
||||
[6, 5000, 5025, 4975, 4987, 6172, 0, 0], # should sell
|
||||
],
|
||||
stop_loss=-0.99,
|
||||
roi={"0": float("inf")},
|
||||
profit_perc=0.00,
|
||||
trades=[
|
||||
BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=2),
|
||||
BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=4, close_tick=6),
|
||||
],
|
||||
)
|
||||
|
||||
# 3) Entered, sl 1%, candle drops 8% => Trade closed, 1% loss
|
||||
tc2 = BTContainer(
|
||||
data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4600, 4987, 6172, 0, 0], # enter trade, stoploss hit
|
||||
[2, 5000, 5025, 4975, 4987, 6172, 0, 0],
|
||||
],
|
||||
stop_loss=-0.01,
|
||||
roi={"0": float("inf")},
|
||||
profit_perc=-0.01,
|
||||
trades=[BTrade(exit_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=1)],
|
||||
)
|
||||
|
||||
# 4) Entered, sl 3 %, candle drops 4%, recovers to 1 % = > Trade closed, 3 % loss
|
||||
tc3 = BTContainer(
|
||||
data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4800, 4987, 6172, 0, 0], # enter trade, stoploss hit
|
||||
[2, 5000, 5025, 4975, 4987, 6172, 0, 0],
|
||||
],
|
||||
stop_loss=-0.03,
|
||||
roi={"0": float("inf")},
|
||||
profit_perc=-0.03,
|
||||
trades=[BTrade(exit_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=1)],
|
||||
)
|
||||
|
||||
# 5) Stoploss and sell are hit. should sell on stoploss
|
||||
tc4 = BTContainer(
|
||||
data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4800, 4987, 6172, 0, 1], # enter trade, stoploss hit, sell signal
|
||||
[2, 5000, 5025, 4975, 4987, 6172, 0, 0],
|
||||
],
|
||||
stop_loss=-0.03,
|
||||
roi={"0": float("inf")},
|
||||
profit_perc=-0.03,
|
||||
trades=[BTrade(exit_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=1)],
|
||||
)
|
||||
|
||||
TESTS = [tc0, tc1, tc2, tc3, tc4]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("data", TESTS)
|
||||
def test_edge_results(edge_conf, mocker, caplog, data) -> None:
|
||||
"""
|
||||
run functional tests
|
||||
"""
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||
frame = _build_backtest_dataframe(data.data)
|
||||
caplog.set_level(logging.DEBUG)
|
||||
edge.fee = 0
|
||||
|
||||
trades = edge._find_trades_for_stoploss_range(frame, "TEST/BTC", [data.stop_loss])
|
||||
results = edge._fill_calculable_fields(DataFrame(trades)) if trades else DataFrame()
|
||||
|
||||
assert len(trades) == len(data.trades)
|
||||
|
||||
if not results.empty:
|
||||
assert round(results["profit_ratio"].sum(), 3) == round(data.profit_perc, 3)
|
||||
|
||||
for c, trade in enumerate(data.trades):
|
||||
res = results.iloc[c]
|
||||
assert res.exit_type == trade.exit_reason
|
||||
assert res.open_date == _get_frame_time_from_offset(trade.open_tick).replace(tzinfo=None)
|
||||
assert res.close_date == _get_frame_time_from_offset(trade.close_tick).replace(tzinfo=None)
|
||||
|
||||
|
||||
def test_adjust(mocker, edge_conf):
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||
mocker.patch(
|
||||
"freqtrade.edge.Edge._cached_pairs",
|
||||
mocker.PropertyMock(
|
||||
return_value={
|
||||
"E/F": PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60),
|
||||
"C/D": PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60),
|
||||
"N/O": PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
pairs = ["A/B", "C/D", "E/F", "G/H"]
|
||||
assert edge.adjust(pairs) == ["E/F", "C/D"]
|
||||
|
||||
|
||||
def test_edge_get_stoploss(mocker, edge_conf):
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||
mocker.patch(
|
||||
"freqtrade.edge.Edge._cached_pairs",
|
||||
mocker.PropertyMock(
|
||||
return_value={
|
||||
"E/F": PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60),
|
||||
"C/D": PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60),
|
||||
"N/O": PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
assert edge.get_stoploss("E/F") == -0.01
|
||||
|
||||
|
||||
def test_nonexisting_get_stoploss(mocker, edge_conf):
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||
mocker.patch(
|
||||
"freqtrade.edge.Edge._cached_pairs",
|
||||
mocker.PropertyMock(
|
||||
return_value={
|
||||
"E/F": PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
assert edge.get_stoploss("N/O") == -0.1
|
||||
|
||||
|
||||
def test_edge_stake_amount(mocker, edge_conf):
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||
mocker.patch(
|
||||
"freqtrade.edge.Edge._cached_pairs",
|
||||
mocker.PropertyMock(
|
||||
return_value={
|
||||
"E/F": PairInfo(-0.02, 0.66, 3.71, 0.50, 1.71, 10, 60),
|
||||
}
|
||||
),
|
||||
)
|
||||
assert edge._capital_ratio == 0.5
|
||||
assert (
|
||||
edge.stake_amount("E/F", free_capital=100, total_capital=100, capital_in_trade=25) == 31.25
|
||||
)
|
||||
|
||||
assert edge.stake_amount("E/F", free_capital=20, total_capital=100, capital_in_trade=25) == 20
|
||||
|
||||
assert edge.stake_amount("E/F", free_capital=0, total_capital=100, capital_in_trade=25) == 0
|
||||
|
||||
# Test with increased allowed_risk
|
||||
# Result should be no more than allowed capital
|
||||
edge._allowed_risk = 0.4
|
||||
edge._capital_ratio = 0.5
|
||||
assert (
|
||||
edge.stake_amount("E/F", free_capital=100, total_capital=100, capital_in_trade=25) == 62.5
|
||||
)
|
||||
|
||||
assert edge.stake_amount("E/F", free_capital=100, total_capital=100, capital_in_trade=0) == 50
|
||||
|
||||
edge._capital_ratio = 1
|
||||
# Full capital is available
|
||||
assert edge.stake_amount("E/F", free_capital=100, total_capital=100, capital_in_trade=0) == 100
|
||||
# Full capital is available
|
||||
assert edge.stake_amount("E/F", free_capital=0, total_capital=100, capital_in_trade=0) == 0
|
||||
|
||||
|
||||
def test_nonexisting_stake_amount(mocker, edge_conf):
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||
mocker.patch(
|
||||
"freqtrade.edge.Edge._cached_pairs",
|
||||
mocker.PropertyMock(
|
||||
return_value={
|
||||
"E/F": PairInfo(-0.11, 0.66, 3.71, 0.50, 1.71, 10, 60),
|
||||
}
|
||||
),
|
||||
)
|
||||
# should use strategy stoploss
|
||||
assert edge.stake_amount("N/O", 1, 2, 1) == 0.15
|
||||
|
||||
|
||||
def test_edge_heartbeat_calculate(mocker, edge_conf):
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||
heartbeat = edge_conf["edge"]["process_throttle_secs"]
|
||||
|
||||
# should not recalculate if heartbeat not reached
|
||||
edge._last_updated = dt_ts() - heartbeat + 1
|
||||
|
||||
assert edge.calculate(edge_conf["exchange"]["pair_whitelist"]) is False
|
||||
|
||||
|
||||
def mocked_load_data(datadir, pairs=None, timeframe="0m", timerange=None, *args, **kwargs):
|
||||
if pairs is None:
|
||||
pairs = []
|
||||
hz = 0.1
|
||||
base = 0.001
|
||||
|
||||
NEOBTC = [
|
||||
[
|
||||
dt_ts(tests_start_time + timedelta(minutes=(x * timeframe_in_minute))),
|
||||
math.sin(x * hz) / 1000 + base,
|
||||
math.sin(x * hz) / 1000 + base + 0.0001,
|
||||
math.sin(x * hz) / 1000 + base - 0.0001,
|
||||
math.sin(x * hz) / 1000 + base,
|
||||
123.45,
|
||||
]
|
||||
for x in range(0, 500)
|
||||
]
|
||||
|
||||
hz = 0.2
|
||||
base = 0.002
|
||||
LTCBTC = [
|
||||
[
|
||||
dt_ts(tests_start_time + timedelta(minutes=(x * timeframe_in_minute))),
|
||||
math.sin(x * hz) / 1000 + base,
|
||||
math.sin(x * hz) / 1000 + base + 0.0001,
|
||||
math.sin(x * hz) / 1000 + base - 0.0001,
|
||||
math.sin(x * hz) / 1000 + base,
|
||||
123.45,
|
||||
]
|
||||
for x in range(0, 500)
|
||||
]
|
||||
|
||||
pairdata = {
|
||||
"NEO/BTC": ohlcv_to_dataframe(NEOBTC, "1h", pair="NEO/BTC", fill_missing=True),
|
||||
"LTC/BTC": ohlcv_to_dataframe(LTCBTC, "1h", pair="LTC/BTC", fill_missing=True),
|
||||
}
|
||||
return pairdata
|
||||
|
||||
|
||||
def test_edge_process_downloaded_data(mocker, edge_conf):
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
mocker.patch(f"{EXMS}.get_fee", MagicMock(return_value=0.001))
|
||||
mocker.patch("freqtrade.edge.edge_positioning.refresh_data", MagicMock())
|
||||
mocker.patch("freqtrade.edge.edge_positioning.load_data", mocked_load_data)
|
||||
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||
|
||||
assert edge.calculate(edge_conf["exchange"]["pair_whitelist"])
|
||||
assert len(edge._cached_pairs) == 2
|
||||
assert edge._last_updated <= dt_ts() + 2
|
||||
|
||||
|
||||
def test_edge_process_no_data(mocker, edge_conf, caplog):
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
mocker.patch(f"{EXMS}.get_fee", MagicMock(return_value=0.001))
|
||||
mocker.patch("freqtrade.edge.edge_positioning.refresh_data", MagicMock())
|
||||
mocker.patch("freqtrade.edge.edge_positioning.load_data", MagicMock(return_value={}))
|
||||
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||
|
||||
assert not edge.calculate(edge_conf["exchange"]["pair_whitelist"])
|
||||
assert len(edge._cached_pairs) == 0
|
||||
assert log_has("No data found. Edge is stopped ...", caplog)
|
||||
assert edge._last_updated == 0
|
||||
|
||||
|
||||
def test_edge_process_no_trades(mocker, edge_conf, caplog):
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
mocker.patch(f"{EXMS}.get_fee", return_value=0.001)
|
||||
mocker.patch(
|
||||
"freqtrade.edge.edge_positioning.refresh_data",
|
||||
)
|
||||
mocker.patch("freqtrade.edge.edge_positioning.load_data", mocked_load_data)
|
||||
# Return empty
|
||||
mocker.patch("freqtrade.edge.Edge._find_trades_for_stoploss_range", return_value=[])
|
||||
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||
|
||||
assert not edge.calculate(edge_conf["exchange"]["pair_whitelist"])
|
||||
assert len(edge._cached_pairs) == 0
|
||||
assert log_has("No trades found.", caplog)
|
||||
|
||||
|
||||
def test_edge_process_no_pairs(mocker, edge_conf, caplog):
|
||||
edge_conf["exchange"]["pair_whitelist"] = []
|
||||
mocker.patch("freqtrade.freqtradebot.validate_config_consistency")
|
||||
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
fee_mock = mocker.patch(f"{EXMS}.get_fee", return_value=0.001)
|
||||
mocker.patch("freqtrade.edge.edge_positioning.refresh_data")
|
||||
mocker.patch("freqtrade.edge.edge_positioning.load_data", mocked_load_data)
|
||||
# Return empty
|
||||
mocker.patch("freqtrade.edge.Edge._find_trades_for_stoploss_range", return_value=[])
|
||||
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||
assert fee_mock.call_count == 0
|
||||
assert edge.fee is None
|
||||
|
||||
assert not edge.calculate(["XRP/USDT"])
|
||||
assert fee_mock.call_count == 1
|
||||
assert edge.fee == 0.001
|
||||
|
||||
|
||||
def test_edge_init_error(mocker, edge_conf):
|
||||
edge_conf["stake_amount"] = 0.5
|
||||
mocker.patch(f"{EXMS}.get_fee", MagicMock(return_value=0.001))
|
||||
with pytest.raises(OperationalException, match="Edge works only with unlimited stake amount"):
|
||||
get_patched_freqtradebot(mocker, edge_conf)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"fee,risk_reward_ratio,expectancy",
|
||||
[
|
||||
(0.0005, 306.5384615384, 101.5128205128),
|
||||
(0.001, 152.6923076923, 50.2307692308),
|
||||
],
|
||||
)
|
||||
def test_process_expectancy(mocker, edge_conf, fee, risk_reward_ratio, expectancy):
|
||||
edge_conf["edge"]["min_trade_number"] = 2
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
|
||||
def get_fee(*args, **kwargs):
|
||||
return fee
|
||||
|
||||
freqtrade.exchange.get_fee = get_fee
|
||||
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||
|
||||
trades = [
|
||||
{
|
||||
"pair": "TEST/BTC",
|
||||
"stoploss": -0.9,
|
||||
"profit_percent": "",
|
||||
"profit_abs": "",
|
||||
"open_date": np.datetime64("2018-10-03T00:05:00.000000000"),
|
||||
"close_date": np.datetime64("2018-10-03T00:10:00.000000000"),
|
||||
"trade_duration": "",
|
||||
"open_rate": 17,
|
||||
"close_rate": 17,
|
||||
"exit_type": "exit_signal",
|
||||
},
|
||||
{
|
||||
"pair": "TEST/BTC",
|
||||
"stoploss": -0.9,
|
||||
"profit_percent": "",
|
||||
"profit_abs": "",
|
||||
"open_date": np.datetime64("2018-10-03T00:20:00.000000000"),
|
||||
"close_date": np.datetime64("2018-10-03T00:25:00.000000000"),
|
||||
"trade_duration": "",
|
||||
"open_rate": 20,
|
||||
"close_rate": 20,
|
||||
"exit_type": "exit_signal",
|
||||
},
|
||||
{
|
||||
"pair": "TEST/BTC",
|
||||
"stoploss": -0.9,
|
||||
"profit_percent": "",
|
||||
"profit_abs": "",
|
||||
"open_date": np.datetime64("2018-10-03T00:30:00.000000000"),
|
||||
"close_date": np.datetime64("2018-10-03T00:40:00.000000000"),
|
||||
"trade_duration": "",
|
||||
"open_rate": 26,
|
||||
"close_rate": 34,
|
||||
"exit_type": "exit_signal",
|
||||
},
|
||||
]
|
||||
|
||||
trades_df = DataFrame(trades)
|
||||
trades_df = edge._fill_calculable_fields(trades_df)
|
||||
final = edge._process_expectancy(trades_df)
|
||||
assert len(final) == 1
|
||||
|
||||
assert "TEST/BTC" in final
|
||||
assert final["TEST/BTC"].stoploss == -0.9
|
||||
assert round(final["TEST/BTC"].winrate, 10) == 0.3333333333
|
||||
assert round(final["TEST/BTC"].risk_reward_ratio, 10) == risk_reward_ratio
|
||||
assert round(final["TEST/BTC"].required_risk_reward, 10) == 2.0
|
||||
assert round(final["TEST/BTC"].expectancy, 10) == expectancy
|
||||
|
||||
# Pop last item so no trade is profitable
|
||||
trades.pop()
|
||||
trades_df = DataFrame(trades)
|
||||
trades_df = edge._fill_calculable_fields(trades_df)
|
||||
final = edge._process_expectancy(trades_df)
|
||||
assert len(final) == 0
|
||||
assert isinstance(final, dict)
|
||||
|
||||
|
||||
def test_process_expectancy_remove_pumps(mocker, edge_conf, fee):
|
||||
edge_conf["edge"]["min_trade_number"] = 2
|
||||
edge_conf["edge"]["remove_pumps"] = True
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
|
||||
freqtrade.exchange.get_fee = fee
|
||||
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||
|
||||
trades = [
|
||||
{
|
||||
"pair": "TEST/BTC",
|
||||
"stoploss": -0.9,
|
||||
"profit_percent": "",
|
||||
"profit_abs": "",
|
||||
"open_date": np.datetime64("2018-10-03T00:05:00.000000000"),
|
||||
"close_date": np.datetime64("2018-10-03T00:10:00.000000000"),
|
||||
"open_index": 1,
|
||||
"close_index": 1,
|
||||
"trade_duration": "",
|
||||
"open_rate": 17,
|
||||
"close_rate": 15,
|
||||
"exit_type": "sell_signal",
|
||||
},
|
||||
{
|
||||
"pair": "TEST/BTC",
|
||||
"stoploss": -0.9,
|
||||
"profit_percent": "",
|
||||
"profit_abs": "",
|
||||
"open_date": np.datetime64("2018-10-03T00:20:00.000000000"),
|
||||
"close_date": np.datetime64("2018-10-03T00:25:00.000000000"),
|
||||
"open_index": 4,
|
||||
"close_index": 4,
|
||||
"trade_duration": "",
|
||||
"open_rate": 20,
|
||||
"close_rate": 10,
|
||||
"exit_type": "sell_signal",
|
||||
},
|
||||
{
|
||||
"pair": "TEST/BTC",
|
||||
"stoploss": -0.9,
|
||||
"profit_percent": "",
|
||||
"profit_abs": "",
|
||||
"open_date": np.datetime64("2018-10-03T00:20:00.000000000"),
|
||||
"close_date": np.datetime64("2018-10-03T00:25:00.000000000"),
|
||||
"open_index": 4,
|
||||
"close_index": 4,
|
||||
"trade_duration": "",
|
||||
"open_rate": 20,
|
||||
"close_rate": 10,
|
||||
"exit_type": "sell_signal",
|
||||
},
|
||||
{
|
||||
"pair": "TEST/BTC",
|
||||
"stoploss": -0.9,
|
||||
"profit_percent": "",
|
||||
"profit_abs": "",
|
||||
"open_date": np.datetime64("2018-10-03T00:20:00.000000000"),
|
||||
"close_date": np.datetime64("2018-10-03T00:25:00.000000000"),
|
||||
"open_index": 4,
|
||||
"close_index": 4,
|
||||
"trade_duration": "",
|
||||
"open_rate": 20,
|
||||
"close_rate": 10,
|
||||
"exit_type": "sell_signal",
|
||||
},
|
||||
{
|
||||
"pair": "TEST/BTC",
|
||||
"stoploss": -0.9,
|
||||
"profit_percent": "",
|
||||
"profit_abs": "",
|
||||
"open_date": np.datetime64("2018-10-03T00:20:00.000000000"),
|
||||
"close_date": np.datetime64("2018-10-03T00:25:00.000000000"),
|
||||
"open_index": 4,
|
||||
"close_index": 4,
|
||||
"trade_duration": "",
|
||||
"open_rate": 20,
|
||||
"close_rate": 10,
|
||||
"exit_type": "sell_signal",
|
||||
},
|
||||
{
|
||||
"pair": "TEST/BTC",
|
||||
"stoploss": -0.9,
|
||||
"profit_percent": "",
|
||||
"profit_abs": "",
|
||||
"open_date": np.datetime64("2018-10-03T00:30:00.000000000"),
|
||||
"close_date": np.datetime64("2018-10-03T00:40:00.000000000"),
|
||||
"open_index": 6,
|
||||
"close_index": 7,
|
||||
"trade_duration": "",
|
||||
"open_rate": 26,
|
||||
"close_rate": 134,
|
||||
"exit_type": "sell_signal",
|
||||
},
|
||||
]
|
||||
|
||||
trades_df = DataFrame(trades)
|
||||
trades_df = edge._fill_calculable_fields(trades_df)
|
||||
final = edge._process_expectancy(trades_df)
|
||||
|
||||
assert "TEST/BTC" in final
|
||||
assert final["TEST/BTC"].stoploss == -0.9
|
||||
assert final["TEST/BTC"].nb_trades == len(trades_df) - 1
|
||||
assert round(final["TEST/BTC"].winrate, 10) == 0.0
|
||||
|
||||
|
||||
def test_process_expectancy_only_wins(mocker, edge_conf, fee):
|
||||
edge_conf["edge"]["min_trade_number"] = 2
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
|
||||
freqtrade.exchange.get_fee = fee
|
||||
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||
|
||||
trades = [
|
||||
{
|
||||
"pair": "TEST/BTC",
|
||||
"stoploss": -0.9,
|
||||
"profit_percent": "",
|
||||
"profit_abs": "",
|
||||
"open_date": np.datetime64("2018-10-03T00:05:00.000000000"),
|
||||
"close_date": np.datetime64("2018-10-03T00:10:00.000000000"),
|
||||
"open_index": 1,
|
||||
"close_index": 1,
|
||||
"trade_duration": "",
|
||||
"open_rate": 15,
|
||||
"close_rate": 17,
|
||||
"exit_type": "sell_signal",
|
||||
},
|
||||
{
|
||||
"pair": "TEST/BTC",
|
||||
"stoploss": -0.9,
|
||||
"profit_percent": "",
|
||||
"profit_abs": "",
|
||||
"open_date": np.datetime64("2018-10-03T00:20:00.000000000"),
|
||||
"close_date": np.datetime64("2018-10-03T00:25:00.000000000"),
|
||||
"open_index": 4,
|
||||
"close_index": 4,
|
||||
"trade_duration": "",
|
||||
"open_rate": 10,
|
||||
"close_rate": 20,
|
||||
"exit_type": "sell_signal",
|
||||
},
|
||||
{
|
||||
"pair": "TEST/BTC",
|
||||
"stoploss": -0.9,
|
||||
"profit_percent": "",
|
||||
"profit_abs": "",
|
||||
"open_date": np.datetime64("2018-10-03T00:30:00.000000000"),
|
||||
"close_date": np.datetime64("2018-10-03T00:40:00.000000000"),
|
||||
"open_index": 6,
|
||||
"close_index": 7,
|
||||
"trade_duration": "",
|
||||
"open_rate": 26,
|
||||
"close_rate": 134,
|
||||
"exit_type": "sell_signal",
|
||||
},
|
||||
]
|
||||
|
||||
trades_df = DataFrame(trades)
|
||||
trades_df = edge._fill_calculable_fields(trades_df)
|
||||
final = edge._process_expectancy(trades_df)
|
||||
|
||||
assert "TEST/BTC" in final
|
||||
assert final["TEST/BTC"].stoploss == -0.9
|
||||
assert final["TEST/BTC"].nb_trades == len(trades_df)
|
||||
assert round(final["TEST/BTC"].winrate, 10) == 1.0
|
||||
assert round(final["TEST/BTC"].risk_reward_ratio, 10) == float("inf")
|
||||
assert round(final["TEST/BTC"].expectancy, 10) == float("inf")
|
||||
@@ -43,7 +43,6 @@ from tests.conftest import (
|
||||
get_patched_worker,
|
||||
log_has,
|
||||
log_has_re,
|
||||
patch_edge,
|
||||
patch_exchange,
|
||||
patch_get_signal,
|
||||
patch_wallet,
|
||||
@@ -253,92 +252,6 @@ def test_check_available_stake_amount(
|
||||
freqtrade.wallets.get_trade_stake_amount("ETH/USDT", 1)
|
||||
|
||||
|
||||
def test_edge_called_in_process(mocker, edge_conf) -> None:
|
||||
patch_RPCManager(mocker)
|
||||
patch_edge(mocker)
|
||||
|
||||
patch_exchange(mocker)
|
||||
freqtrade = FreqtradeBot(edge_conf)
|
||||
patch_get_signal(freqtrade)
|
||||
freqtrade.process()
|
||||
assert freqtrade.active_pair_whitelist == ["NEO/BTC", "LTC/BTC"]
|
||||
|
||||
|
||||
def test_edge_overrides_stake_amount(mocker, edge_conf) -> None:
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
patch_edge(mocker)
|
||||
edge_conf["dry_run_wallet"] = 999.9
|
||||
freqtrade = FreqtradeBot(edge_conf)
|
||||
|
||||
assert (
|
||||
freqtrade.wallets.get_trade_stake_amount("NEO/BTC", 1, freqtrade.edge)
|
||||
== (999.9 * 0.5 * 0.01) / 0.20
|
||||
)
|
||||
assert (
|
||||
freqtrade.wallets.get_trade_stake_amount("LTC/BTC", 1, freqtrade.edge)
|
||||
== (999.9 * 0.5 * 0.01) / 0.21
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"buy_price_mult,ignore_strat_sl",
|
||||
[
|
||||
(0.79, False), # Override stoploss
|
||||
(0.85, True), # Override strategy stoploss
|
||||
],
|
||||
)
|
||||
def test_edge_overrides_stoploss(
|
||||
limit_order, fee, caplog, mocker, buy_price_mult, ignore_strat_sl, edge_conf
|
||||
) -> None:
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
patch_edge(mocker)
|
||||
edge_conf["max_open_trades"] = float("inf")
|
||||
|
||||
# Strategy stoploss is -0.1 but Edge imposes a stoploss at -0.2
|
||||
# Thus, if price falls 21%, stoploss should be triggered
|
||||
#
|
||||
# mocking the ticker: price is falling ...
|
||||
enter_price = limit_order["buy"]["price"]
|
||||
ticker_val = {
|
||||
"bid": enter_price,
|
||||
"ask": enter_price,
|
||||
"last": enter_price,
|
||||
}
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
fetch_ticker=MagicMock(return_value=ticker_val),
|
||||
get_fee=fee,
|
||||
)
|
||||
#############################################
|
||||
|
||||
# Create a trade with "limit_buy_order_usdt" price
|
||||
freqtrade = FreqtradeBot(edge_conf)
|
||||
freqtrade.active_pair_whitelist = ["NEO/BTC"]
|
||||
patch_get_signal(freqtrade)
|
||||
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
|
||||
freqtrade.enter_positions()
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
caplog.clear()
|
||||
#############################################
|
||||
ticker_val.update(
|
||||
{
|
||||
"bid": enter_price * buy_price_mult,
|
||||
"ask": enter_price * buy_price_mult,
|
||||
"last": enter_price * buy_price_mult,
|
||||
}
|
||||
)
|
||||
|
||||
# stoploss should be hit
|
||||
assert freqtrade.handle_trade(trade) is not ignore_strat_sl
|
||||
if not ignore_strat_sl:
|
||||
assert log_has_re("Exit for NEO/BTC detected. Reason: stop_loss.*", caplog)
|
||||
assert trade.exit_reason == ExitType.STOP_LOSS.value
|
||||
# Test compatibility ...
|
||||
assert trade.sell_reason == ExitType.STOP_LOSS.value
|
||||
|
||||
|
||||
def test_total_open_trades_stakes(mocker, default_conf_usdt, ticker_usdt, fee) -> None:
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
@@ -483,7 +396,7 @@ def test_create_trade_minimal_amount(
|
||||
if not max_open_trades:
|
||||
assert (
|
||||
freqtrade.wallets.get_trade_stake_amount(
|
||||
"ETH/USDT", default_conf_usdt["max_open_trades"], freqtrade.edge
|
||||
"ETH/USDT", default_conf_usdt["max_open_trades"]
|
||||
)
|
||||
== 0
|
||||
)
|
||||
@@ -4479,7 +4392,7 @@ def test_startup_state(default_conf_usdt, mocker):
|
||||
assert worker.freqtrade.state is State.RUNNING
|
||||
|
||||
|
||||
def test_startup_trade_reinit(default_conf_usdt, edge_conf, mocker):
|
||||
def test_startup_trade_reinit(default_conf_usdt, mocker):
|
||||
mocker.patch(f"{EXMS}.exchange_has", MagicMock(return_value=True))
|
||||
reinit_mock = MagicMock()
|
||||
mocker.patch("freqtrade.persistence.Trade.stoploss_reinitialization", reinit_mock)
|
||||
@@ -4488,12 +4401,6 @@ def test_startup_trade_reinit(default_conf_usdt, edge_conf, mocker):
|
||||
ftbot.startup()
|
||||
assert reinit_mock.call_count == 1
|
||||
|
||||
reinit_mock.reset_mock()
|
||||
|
||||
ftbot = get_patched_freqtradebot(mocker, edge_conf)
|
||||
ftbot.startup()
|
||||
assert reinit_mock.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_sync_wallet_dry_run(
|
||||
|
||||
@@ -16,7 +16,6 @@ from tests.conftest import (
|
||||
get_patched_freqtradebot,
|
||||
log_has,
|
||||
log_has_re,
|
||||
patch_edge,
|
||||
patch_exchange,
|
||||
patch_get_signal,
|
||||
patch_whitelist,
|
||||
@@ -971,128 +970,6 @@ def test_handle_stoploss_on_exchange_custom_stop(
|
||||
assert freqtrade.handle_trade(trade) is True
|
||||
|
||||
|
||||
def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_order) -> None:
|
||||
enter_order = limit_order["buy"]
|
||||
exit_order = limit_order["sell"]
|
||||
enter_order["average"] = 2.19
|
||||
# When trailing stoploss is set
|
||||
stoploss = MagicMock(return_value={"id": "13434334", "status": "open"})
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
patch_edge(mocker)
|
||||
edge_conf["max_open_trades"] = float("inf")
|
||||
edge_conf["dry_run_wallet"] = 999.9
|
||||
edge_conf["exchange"]["name"] = "binance"
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
fetch_ticker=MagicMock(return_value={"bid": 2.19, "ask": 2.2, "last": 2.19}),
|
||||
create_order=MagicMock(
|
||||
side_effect=[
|
||||
enter_order,
|
||||
exit_order,
|
||||
]
|
||||
),
|
||||
get_fee=fee,
|
||||
create_stoploss=stoploss,
|
||||
)
|
||||
|
||||
# enabling TSL
|
||||
edge_conf["trailing_stop"] = True
|
||||
edge_conf["trailing_stop_positive"] = 0.01
|
||||
edge_conf["trailing_stop_positive_offset"] = 0.011
|
||||
|
||||
# disabling ROI
|
||||
edge_conf["minimal_roi"]["0"] = 999999999
|
||||
|
||||
freqtrade = FreqtradeBot(edge_conf)
|
||||
|
||||
# enabling stoploss on exchange
|
||||
freqtrade.strategy.order_types["stoploss_on_exchange"] = True
|
||||
|
||||
# setting stoploss
|
||||
freqtrade.strategy.stoploss = -0.02
|
||||
|
||||
# setting stoploss_on_exchange_interval to 0 seconds
|
||||
freqtrade.strategy.order_types["stoploss_on_exchange_interval"] = 0
|
||||
|
||||
patch_get_signal(freqtrade)
|
||||
|
||||
freqtrade.active_pair_whitelist = freqtrade.edge.adjust(freqtrade.active_pair_whitelist)
|
||||
|
||||
freqtrade.enter_positions()
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
trade.is_open = True
|
||||
|
||||
trade.stoploss_last_update = dt_now()
|
||||
trade.orders.append(
|
||||
Order(
|
||||
ft_order_side="stoploss",
|
||||
ft_pair=trade.pair,
|
||||
ft_is_open=True,
|
||||
ft_amount=trade.amount,
|
||||
ft_price=trade.stop_loss,
|
||||
order_id="100",
|
||||
)
|
||||
)
|
||||
|
||||
stoploss_order_hanging = MagicMock(
|
||||
return_value={
|
||||
"id": "100",
|
||||
"status": "open",
|
||||
"type": "stop_loss_limit",
|
||||
"price": 3,
|
||||
"average": 2,
|
||||
"stopPrice": "2.178",
|
||||
}
|
||||
)
|
||||
|
||||
mocker.patch(f"{EXMS}.fetch_stoploss_order", stoploss_order_hanging)
|
||||
|
||||
# stoploss initially at 20% as edge dictated it.
|
||||
assert freqtrade.handle_trade(trade) is False
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
assert pytest.approx(trade.stop_loss) == 1.76
|
||||
|
||||
cancel_order_mock = MagicMock()
|
||||
stoploss_order_mock = MagicMock()
|
||||
mocker.patch(f"{EXMS}.cancel_stoploss_order", cancel_order_mock)
|
||||
mocker.patch(f"{EXMS}.create_stoploss", stoploss_order_mock)
|
||||
|
||||
# price goes down 5%
|
||||
mocker.patch(
|
||||
f"{EXMS}.fetch_ticker",
|
||||
MagicMock(return_value={"bid": 2.19 * 0.95, "ask": 2.2 * 0.95, "last": 2.19 * 0.95}),
|
||||
)
|
||||
assert freqtrade.handle_trade(trade) is False
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
|
||||
# stoploss should remain the same
|
||||
assert pytest.approx(trade.stop_loss) == 1.76
|
||||
|
||||
# stoploss on exchange should not be canceled
|
||||
cancel_order_mock.assert_not_called()
|
||||
|
||||
# price jumped 2x
|
||||
mocker.patch(
|
||||
f"{EXMS}.fetch_ticker", MagicMock(return_value={"bid": 4.38, "ask": 4.4, "last": 4.38})
|
||||
)
|
||||
|
||||
assert freqtrade.handle_trade(trade) is False
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
|
||||
# stoploss should be set to 1% as trailing is on
|
||||
assert trade.stop_loss == 4.4 * 0.99
|
||||
cancel_order_mock.assert_called_once_with("100", "NEO/BTC")
|
||||
stoploss_order_mock.assert_called_once_with(
|
||||
amount=30,
|
||||
pair="NEO/BTC",
|
||||
order_types=freqtrade.strategy.order_types,
|
||||
stop_price=4.4 * 0.99,
|
||||
side="sell",
|
||||
leverage=1.0,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(
|
||||
default_conf_usdt,
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103, C0330
|
||||
# pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_edge
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.optimize.edge_cli import EdgeCli
|
||||
from tests.conftest import (
|
||||
CURRENT_TEST_STRATEGY,
|
||||
EXMS,
|
||||
get_args,
|
||||
log_has,
|
||||
patch_exchange,
|
||||
patched_configuration_load_config_file,
|
||||
)
|
||||
|
||||
|
||||
def test_setup_optimize_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
args = [
|
||||
"edge",
|
||||
"--config",
|
||||
"config.json",
|
||||
"--strategy",
|
||||
CURRENT_TEST_STRATEGY,
|
||||
]
|
||||
|
||||
config = setup_optimize_configuration(get_args(args), RunMode.EDGE)
|
||||
assert config["runmode"] == RunMode.EDGE
|
||||
|
||||
assert "max_open_trades" in config
|
||||
assert "stake_currency" in config
|
||||
assert "stake_amount" in config
|
||||
assert "exchange" in config
|
||||
assert "pair_whitelist" in config["exchange"]
|
||||
assert "datadir" in config
|
||||
assert log_has("Using data directory: {} ...".format(config["datadir"]), caplog)
|
||||
assert "timeframe" in config
|
||||
|
||||
assert "timerange" not in config
|
||||
assert "stoploss_range" not in config
|
||||
|
||||
|
||||
def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> None:
|
||||
patched_configuration_load_config_file(mocker, edge_conf)
|
||||
mocker.patch("freqtrade.configuration.configuration.create_datadir", lambda c, x: x)
|
||||
|
||||
args = [
|
||||
"edge",
|
||||
"--config",
|
||||
"config.json",
|
||||
"--strategy",
|
||||
CURRENT_TEST_STRATEGY,
|
||||
"--datadir",
|
||||
"/foo/bar",
|
||||
"--timeframe",
|
||||
"1m",
|
||||
"--timerange",
|
||||
":100",
|
||||
"--stoplosses=-0.01,-0.10,-0.001",
|
||||
]
|
||||
|
||||
config = setup_optimize_configuration(get_args(args), RunMode.EDGE)
|
||||
assert "max_open_trades" in config
|
||||
assert "stake_currency" in config
|
||||
assert "stake_amount" in config
|
||||
assert "exchange" in config
|
||||
assert "pair_whitelist" in config["exchange"]
|
||||
assert "datadir" in config
|
||||
assert config["runmode"] == RunMode.EDGE
|
||||
assert log_has("Using data directory: {} ...".format(config["datadir"]), caplog)
|
||||
assert "timeframe" in config
|
||||
assert log_has("Parameter -i/--timeframe detected ... Using timeframe: 1m ...", caplog)
|
||||
|
||||
assert "timerange" in config
|
||||
assert log_has("Parameter --timerange detected: {} ...".format(config["timerange"]), caplog)
|
||||
|
||||
|
||||
def test_start(mocker, fee, edge_conf, caplog) -> None:
|
||||
start_mock = MagicMock()
|
||||
mocker.patch(f"{EXMS}.get_fee", fee)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch("freqtrade.optimize.edge_cli.EdgeCli.start", start_mock)
|
||||
patched_configuration_load_config_file(mocker, edge_conf)
|
||||
|
||||
args = [
|
||||
"edge",
|
||||
"--config",
|
||||
"config.json",
|
||||
"--strategy",
|
||||
CURRENT_TEST_STRATEGY,
|
||||
]
|
||||
pargs = get_args(args)
|
||||
start_edge(pargs)
|
||||
assert log_has("Starting freqtrade in Edge mode", caplog)
|
||||
assert start_mock.call_count == 1
|
||||
|
||||
|
||||
def test_edge_init(mocker, edge_conf) -> None:
|
||||
patch_exchange(mocker)
|
||||
edge_conf["stake_amount"] = 20
|
||||
edge_cli = EdgeCli(edge_conf)
|
||||
assert edge_cli.config == edge_conf
|
||||
assert edge_cli.config["stake_amount"] == "unlimited"
|
||||
assert callable(edge_cli.edge.calculate)
|
||||
assert edge_cli.strategy.bot_started is True
|
||||
|
||||
|
||||
def test_edge_init_fee(mocker, edge_conf) -> None:
|
||||
patch_exchange(mocker)
|
||||
edge_conf["fee"] = 0.01234
|
||||
edge_conf["stake_amount"] = 20
|
||||
fee_mock = mocker.patch(f"{EXMS}.get_fee", return_value=0.5)
|
||||
edge_cli = EdgeCli(edge_conf)
|
||||
assert edge_cli.edge.fee == 0.01234
|
||||
assert fee_mock.call_count == 0
|
||||
|
||||
|
||||
def test_edge_start(mocker, edge_conf) -> None:
|
||||
mock_calculate = mocker.patch(
|
||||
"freqtrade.edge.edge_positioning.Edge.calculate", return_value=True
|
||||
)
|
||||
table_mock = mocker.patch("freqtrade.optimize.edge_cli.generate_edge_table")
|
||||
|
||||
patch_exchange(mocker)
|
||||
edge_conf["stake_amount"] = 20
|
||||
|
||||
edge_cli = EdgeCli(edge_conf)
|
||||
edge_cli.start()
|
||||
assert mock_calculate.call_count == 1
|
||||
assert table_mock.call_count == 1
|
||||
@@ -18,12 +18,10 @@ from freqtrade.data.btanalysis import (
|
||||
load_backtest_data,
|
||||
load_backtest_stats,
|
||||
)
|
||||
from freqtrade.edge import PairInfo
|
||||
from freqtrade.enums import ExitType
|
||||
from freqtrade.optimize.optimize_reports import (
|
||||
generate_backtest_stats,
|
||||
generate_daily_stats,
|
||||
generate_edge_table,
|
||||
generate_pair_metrics,
|
||||
generate_periodic_breakdown_stats,
|
||||
generate_strategy_comparison,
|
||||
@@ -646,15 +644,6 @@ def test_text_table_strategy(testdatadir, capsys):
|
||||
)
|
||||
|
||||
|
||||
def test_generate_edge_table(capsys):
|
||||
results = {}
|
||||
results["ETH/BTC"] = PairInfo(-0.01, 0.60, 2, 1, 3, 10, 60)
|
||||
generate_edge_table(results)
|
||||
text = capsys.readouterr().out
|
||||
assert re.search(r".* ETH/BTC .*", text)
|
||||
assert re.search(r".* Risk Reward Ratio .* Required Risk Reward .* Expectancy .*", text)
|
||||
|
||||
|
||||
def test_generate_periodic_breakdown_stats(testdatadir):
|
||||
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||
bt_data = load_backtest_data(filename).to_dict(orient="records")
|
||||
|
||||
@@ -6,7 +6,6 @@ import pytest
|
||||
from numpy import isnan
|
||||
from sqlalchemy import select
|
||||
|
||||
from freqtrade.edge import PairInfo
|
||||
from freqtrade.enums import SignalDirection, State, TradingMode
|
||||
from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError
|
||||
from freqtrade.persistence import Order, Trade
|
||||
@@ -1393,36 +1392,6 @@ def test_rpc_blacklist(mocker, default_conf) -> None:
|
||||
assert isinstance(ret["errors"], dict)
|
||||
|
||||
|
||||
def test_rpc_edge_disabled(mocker, default_conf) -> None:
|
||||
mocker.patch("freqtrade.rpc.telegram.Telegram", MagicMock())
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
rpc = RPC(freqtradebot)
|
||||
with pytest.raises(RPCException, match=r"Edge is not enabled."):
|
||||
rpc._rpc_edge()
|
||||
|
||||
|
||||
def test_rpc_edge_enabled(mocker, edge_conf) -> None:
|
||||
mocker.patch("freqtrade.rpc.telegram.Telegram", MagicMock())
|
||||
mocker.patch(
|
||||
"freqtrade.edge.Edge._cached_pairs",
|
||||
mocker.PropertyMock(
|
||||
return_value={
|
||||
"E/F": PairInfo(-0.02, 0.66, 3.71, 0.50, 1.71, 10, 60),
|
||||
}
|
||||
),
|
||||
)
|
||||
freqtradebot = get_patched_freqtradebot(mocker, edge_conf)
|
||||
|
||||
rpc = RPC(freqtradebot)
|
||||
ret = rpc._rpc_edge()
|
||||
|
||||
assert len(ret) == 1
|
||||
assert ret[0]["Pair"] == "E/F"
|
||||
assert ret[0]["Winrate"] == 0.66
|
||||
assert ret[0]["Expectancy"] == 1.71
|
||||
assert ret[0]["Stoploss"] == -0.02
|
||||
|
||||
|
||||
def test_rpc_health(mocker, default_conf) -> None:
|
||||
mocker.patch("freqtrade.rpc.telegram.Telegram", MagicMock())
|
||||
|
||||
|
||||
@@ -1167,21 +1167,6 @@ def test_api_logs(botclient):
|
||||
assert len(rc1.json()["logs"]) == rc1.json()["log_count"]
|
||||
|
||||
|
||||
def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
|
||||
ftbot, client = botclient
|
||||
patch_get_signal(ftbot)
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
get_balances=MagicMock(return_value=ticker),
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
)
|
||||
rc = client_get(client, f"{BASE_URI}/edge")
|
||||
assert_response(rc, 502)
|
||||
assert rc.json() == {"error": "Error querying /api/v1/edge: Edge is not enabled."}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"is_short,expected",
|
||||
[
|
||||
|
||||
@@ -21,7 +21,6 @@ from telegram.error import BadRequest, NetworkError, TelegramError
|
||||
|
||||
from freqtrade import __version__
|
||||
from freqtrade.constants import CANCEL_REASON
|
||||
from freqtrade.edge import PairInfo
|
||||
from freqtrade.enums import (
|
||||
ExitType,
|
||||
MarketDirection,
|
||||
@@ -171,7 +170,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None:
|
||||
"['reload_conf', 'reload_config'], ['show_conf', 'show_config'], "
|
||||
"['pause', 'stopbuy', 'stopentry'], ['whitelist'], ['blacklist'], "
|
||||
"['bl_delete', 'blacklist_delete'], "
|
||||
"['logs'], ['edge'], ['health'], ['help'], ['version'], ['marketdir'], "
|
||||
"['logs'], ['health'], ['help'], ['version'], ['marketdir'], "
|
||||
"['order'], ['list_custom_data'], ['tg_info']]"
|
||||
)
|
||||
|
||||
@@ -1952,40 +1951,6 @@ async def test_telegram_logs(default_conf, update, mocker) -> None:
|
||||
assert msg_mock.call_count >= 2
|
||||
|
||||
|
||||
async def test_edge_disabled(default_conf, update, mocker) -> None:
|
||||
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
|
||||
await telegram._edge(update=update, context=MagicMock())
|
||||
assert msg_mock.call_count == 1
|
||||
assert "Edge is not enabled." in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
async def test_edge_enabled(edge_conf, update, mocker) -> None:
|
||||
mocker.patch(
|
||||
"freqtrade.edge.Edge._cached_pairs",
|
||||
mocker.PropertyMock(
|
||||
return_value={
|
||||
"E/F": PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
telegram, _, msg_mock = get_telegram_testobject(mocker, edge_conf)
|
||||
|
||||
await telegram._edge(update=update, context=MagicMock())
|
||||
assert msg_mock.call_count == 1
|
||||
assert "<b>Edge only validated following pairs:</b>\n<pre>" in msg_mock.call_args_list[0][0][0]
|
||||
assert "Pair Winrate Expectancy Stoploss" in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
msg_mock.reset_mock()
|
||||
|
||||
mocker.patch("freqtrade.edge.Edge._cached_pairs", mocker.PropertyMock(return_value={}))
|
||||
await telegram._edge(update=update, context=MagicMock())
|
||||
assert msg_mock.call_count == 1
|
||||
assert "<b>Edge only validated following pairs:</b>" in msg_mock.call_args_list[0][0][0]
|
||||
assert "Winrate" not in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"is_short,regex_pattern",
|
||||
[(True, r"now[ ]*XRP\/BTC \(#3\) -1.00% \("), (False, r"now[ ]*XRP\/BTC \(#3\) 1.00% \(")],
|
||||
|
||||
@@ -756,27 +756,6 @@ def test_validate_tsl(default_conf):
|
||||
validate_config_consistency(default_conf)
|
||||
|
||||
|
||||
def test_validate_edge2(edge_conf):
|
||||
edge_conf.update(
|
||||
{
|
||||
"use_exit_signal": True,
|
||||
}
|
||||
)
|
||||
# Passes test
|
||||
validate_config_consistency(edge_conf)
|
||||
|
||||
edge_conf.update(
|
||||
{
|
||||
"use_exit_signal": False,
|
||||
}
|
||||
)
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match="Edge requires `use_exit_signal` to be True, otherwise no sells will happen.",
|
||||
):
|
||||
validate_config_consistency(edge_conf)
|
||||
|
||||
|
||||
def test_validate_whitelist(default_conf):
|
||||
default_conf["runmode"] = RunMode.DRY_RUN
|
||||
# Test regular case - has whitelist and uses StaticPairlist
|
||||
@@ -1062,6 +1041,17 @@ def test__validate_orderflow(default_conf) -> None:
|
||||
validate_config_consistency(conf)
|
||||
|
||||
|
||||
def test_validate_edge_removal(default_conf):
|
||||
default_conf["edge"] = {
|
||||
"enabled": True,
|
||||
}
|
||||
with pytest.raises(
|
||||
ConfigurationError,
|
||||
match="Edge is no longer supported and has been removed from Freqtrade with 2025.6.",
|
||||
):
|
||||
validate_config_consistency(default_conf)
|
||||
|
||||
|
||||
def test_load_config_test_comments() -> None:
|
||||
"""
|
||||
Load config with comments
|
||||
@@ -1315,23 +1305,6 @@ def test_process_removed_settings(mocker, default_conf, setting):
|
||||
process_temporary_deprecated_settings(default_conf)
|
||||
|
||||
|
||||
def test_process_deprecated_setting_edge(mocker, edge_conf):
|
||||
patched_configuration_load_config_file(mocker, edge_conf)
|
||||
edge_conf.update(
|
||||
{
|
||||
"edge": {
|
||||
"enabled": True,
|
||||
"capital_available_percentage": 0.5,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
OperationalException, match=r"DEPRECATED.*Using 'edge.capital_available_percentage'*"
|
||||
):
|
||||
process_temporary_deprecated_settings(edge_conf)
|
||||
|
||||
|
||||
def test_check_conflicting_settings(mocker, default_conf, caplog):
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user