Compare commits

..

26 Commits

Author SHA1 Message Date
Matthias
c57db0a330 Version bump 2022.5.1 2022-06-01 06:34:28 +02:00
Matthias
f5087a82dc Merge branch 'stable' into new_release 2022-06-01 06:33:42 +02:00
Matthias
eed0d67005 Merge pull request #6893 from freqtrade/new_release
New release 2022.5
2022-05-28 13:46:24 +02:00
Matthias
a1d54f5ae0 Version bump 2022.5 2022-05-28 09:49:58 +02:00
Matthias
a4a7c6536d Merge branch 'stable' into new_release 2022-05-28 09:49:46 +02:00
Matthias
340a97d1df Merge pull request #6811 from DJCrashdummy/patch-1
corrected minor "typo" in formatting
2022-05-10 19:16:40 +02:00
DJCrashdummy
fab197edf2 corrected minor "typo" in formatting 2022-05-10 10:33:04 +00:00
Matthias
851c5dad30 Version bump 2022.4.2 2022-05-03 20:37:29 +02:00
Matthias
5b76ae452f Fix fee handling for futures trades 2022-05-03 20:35:30 +02:00
Matthias
2c750fdb09 Reduce no stake amount verbosity
closes #6768
2022-05-03 20:35:22 +02:00
Matthias
e7f5252074 Version bump 2022.4.1 2022-05-01 16:49:11 +02:00
Matthias
dfbd1c34c4 Merge pull request #6755 from freqtrade/new_release
New release 2022.4
2022-05-01 14:51:39 +02:00
Matthias
7615c4e904 Version bump 2022.4 2022-05-01 11:19:32 +02:00
Matthias
e9b78bf3ae Merge branch 'stable' into new_release 2022-05-01 11:19:17 +02:00
Matthias
2e397a88e1 Merge pull request #6592 from freqtrade/new_release
New release 2022.3
2022-03-27 15:51:58 +02:00
Matthias
fe6c62e144 Version bump 2022.3 2022-03-27 15:27:16 +02:00
Matthias
f0db721f05 Merge branch 'stable' into new_release 2022-03-27 15:09:06 +02:00
Matthias
4d8d30ea39 Version bump to 2022.2.2 2022-03-21 06:34:33 +01:00
Matthias
e90e3cead0 Map usdt fiat to correct coingecko fiat 2022-03-21 06:34:20 +01:00
Matthias
a568548192 Merge pull request #6464 from freqtrade/new_release
New release 2022.2.1
2022-02-26 08:57:42 +01:00
Matthias
f9d10a7fad Version bump 2022.2.1 2022-02-26 08:35:50 +01:00
Matthias
cbc2b00ee6 Merge branch 'stable' into new_release 2022-02-26 08:35:31 +01:00
Matthias
8f7b857ae9 Merge pull request #6459 from freqtrade/new_release
New release 2022.2
2022-02-25 15:14:27 +01:00
Matthias
e88b022cd4 Version bump 2022.2 2022-02-25 12:07:09 +01:00
Matthias
bfb738f69f Merge branch 'stable' into new_release 2022-02-25 12:06:11 +01:00
Matthias
00dd8e76ee Merge pull request #6416 from froggleston/patch-2
Update windows_installation.md
2022-02-25 11:44:40 +01:00
77 changed files with 1135 additions and 2227 deletions

View File

@@ -13,10 +13,6 @@ on:
schedule:
- cron: '0 5 * * 4'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build_linux:
@@ -30,7 +26,7 @@ jobs:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
@@ -66,12 +62,12 @@ jobs:
- name: Tests
run: |
pytest --random-order --cov=freqtrade --cov-config=.coveragerc
if: matrix.python-version != '3.9' || matrix.os != 'ubuntu-22.04'
if: matrix.python-version != '3.9'
- name: Tests incl. ccxt compatibility tests
run: |
pytest --random-order --cov=freqtrade --cov-config=.coveragerc --longrun
if: matrix.python-version == '3.9' && matrix.os == 'ubuntu-22.04'
if: matrix.python-version == '3.9'
- name: Coveralls
if: (runner.os == 'Linux' && matrix.python-version == '3.9')
@@ -127,7 +123,7 @@ jobs:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
@@ -211,7 +207,7 @@ jobs:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
@@ -263,7 +259,7 @@ jobs:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v3
with:
python-version: "3.10"
@@ -282,7 +278,7 @@ jobs:
./tests/test_docs.sh
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v3
with:
python-version: "3.10"
@@ -300,6 +296,18 @@ jobs:
details: Freqtrade doc test failed!
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
cleanup-prior-runs:
permissions:
actions: write # for rokroskar/workflow-run-cleanup-action to obtain workflow name & cancel it
contents: read # for rokroskar/workflow-run-cleanup-action to obtain branch
runs-on: ubuntu-20.04
steps:
- name: Cleanup previous runs on this branch
uses: rokroskar/workflow-run-cleanup-action@v0.3.3
if: "!startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/stable' && github.repository == 'freqtrade/freqtrade'"
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
# Notify only once - when CI completes (and after deploy) in case it's successfull
notify-complete:
needs: [ build_linux, build_macos, build_windows, docs_check, mypy_version_check ]
@@ -336,7 +344,7 @@ jobs:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v3
with:
python-version: "3.9"

View File

@@ -13,9 +13,9 @@ repos:
- id: mypy
exclude: build_helpers
additional_dependencies:
- types-cachetools==5.0.2
- types-filelock==3.2.7
- types-requests==2.27.30
- types-cachetools==5.0.1
- types-filelock==3.2.6
- types-requests==2.27.29
- types-tabulate==0.8.9
- types-python-dateutil==2.8.17
# stages: [push]

View File

@@ -1,4 +1,4 @@
FROM python:3.10.5-slim-bullseye as base
FROM python:3.10.4-slim-bullseye as base
# Setup env
ENV LANG C.UTF-8

View File

@@ -7,5 +7,4 @@ FROM freqtradeorg/freqtrade:develop
# The below dependency - pyti - serves as an example. Please use whatever you need!
RUN pip install --user pyti
# Switch back to user (only if you required root above)
# USER ftuser

View File

@@ -22,79 +22,50 @@ DataFrame of the candles that resulted in buy signals. Depending on how many buy
makes, this file may get quite large, so periodically check your `user_data/backtest_results`
folder to delete old exports.
To analyze the buy tags, we need to use the `buy_reasons.py` script from
[froggleston's repo](https://github.com/froggleston/freqtrade-buyreasons). Follow the instructions
in their README to copy the script into your `freqtrade/scripts/` folder.
Before running your next backtest, make sure you either delete your old backtest results or run
backtesting with the `--cache none` option to make sure no cached results are used.
If all goes well, you should now see a `backtest-result-{timestamp}_signals.pkl` file in the
`user_data/backtest_results` folder.
To analyze the entry/exit tags, we now need to use the `freqtrade backtesting-analysis` command
with `--analysis-groups` option provided with space-separated arguments (default `0 1 2`):
Now run the `buy_reasons.py` script, supplying a few options:
``` bash
freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 1 2 3 4
python3 scripts/buy_reasons.py -c <config.json> -s <strategy_name> -t <timerange> -g0,1,2,3,4
```
This command will read from the last backtesting results. The `--analysis-groups` option is
used to specify the various tabular outputs showing the profit fo each group or trade,
ranging from the simplest (0) to the most detailed per pair, per buy and per sell tag (4):
* 1: profit summaries grouped by enter_tag
* 2: profit summaries grouped by enter_tag and exit_tag
* 3: profit summaries grouped by pair and enter_tag
* 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large)
More options are available by running with the `-h` option.
### Using export-filename
Normally, `backtesting-analysis` uses the latest backtest results, but if you wanted to go
back to a previous backtest output, you need to supply the `--export-filename` option.
You can supply the same parameter to `backtest-analysis` with the name of the final backtest
output file. This allows you to keep historical versions of backtest results and re-analyse
them at a later date:
``` bash
freqtrade backtesting -c <config.json> --timeframe <tf> --strategy <strategy_name> --timerange=<timerange> --export=signals --export-filename=/tmp/mystrat_backtest.json
```
You should see some output similar to below in the logs with the name of the timestamped
filename that was exported:
```
2022-06-14 16:28:32,698 - freqtrade.misc - INFO - dumping json to "/tmp/mystrat_backtest-2022-06-14_16-28-32.json"
```
You can then use that filename in `backtesting-analysis`:
```
freqtrade backtesting-analysis -c <config.json> --export-filename=/tmp/mystrat_backtest-2022-06-14_16-28-32.json
```
The `-g` option is used to specify the various tabular outputs, ranging from the simplest (0)
to the most detailed per pair, per buy and per sell tag (4). More options are available by
running with the `-h` option.
### Tuning the buy tags and sell tags to display
To show only certain buy and sell tags in the displayed output, use the following two options:
```
--enter-reason-list : Space-separated list of enter signals to analyse. Default: "all"
--exit-reason-list : Space-separated list of exit signals to analyse. Default: "all"
--enter_reason_list : Comma separated list of enter signals to analyse. Default: "all"
--exit_reason_list : Comma separated list of exit signals to analyse. Default: "stop_loss,trailing_stop_loss"
```
For example:
```bash
freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 2 --enter-reason-list enter_tag_a enter_tag_b --exit-reason-list roi custom_exit_tag_a stop_loss
python3 scripts/buy_reasons.py -c <config.json> -s <strategy_name> -t <timerange> -g0,1,2,3,4 --enter_reason_list "enter_tag_a,enter_tag_b" --exit_reason_list "roi,custom_exit_tag_a,stop_loss"
```
### Outputting signal candle indicators
The real power of `freqtrade backtesting-analysis` comes from the ability to print out the indicator
The real power of the buy_reasons.py script comes from the ability to print out the indicator
values present on signal candles to allow fine-grained investigation and tuning of buy signal
indicators. To print out a column for a given set of indicators, use the `--indicator-list`
option:
```bash
freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 2 --enter-reason-list enter_tag_a enter_tag_b --exit-reason-list roi custom_exit_tag_a stop_loss --indicator-list rsi rsi_1h bb_lowerband ema_9 macd macdsignal
python3 scripts/buy_reasons.py -c <config.json> -s <strategy_name> -t <timerange> -g0,1,2,3,4 --enter_reason_list "enter_tag_a,enter_tag_b" --exit_reason_list "roi,custom_exit_tag_a,stop_loss" --indicator_list "rsi,rsi_1h,bb_lowerband,ema_9,macd,macdsignal"
```
The indicators have to be present in your strategy's main DataFrame (either for your main

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -300,7 +300,6 @@ A backtesting result will look like that:
| Absolute profit | 0.00762792 BTC |
| Total profit % | 76.2% |
| CAGR % | 460.87% |
| Profit factor | 1.11 |
| Avg. stake amount | 0.001 BTC |
| Total trade volume | 0.429 BTC |
| | |
@@ -400,7 +399,6 @@ It contains some useful key metrics about performance of your strategy on backte
| Absolute profit | 0.00762792 BTC |
| Total profit % | 76.2% |
| CAGR % | 460.87% |
| Profit factor | 1.11 |
| Avg. stake amount | 0.001 BTC |
| Total trade volume | 0.429 BTC |
| | |
@@ -446,8 +444,6 @@ It contains some useful key metrics about performance of your strategy on backte
- `Final balance`: Final balance - starting balance + absolute profit.
- `Absolute profit`: Profit made in stake currency.
- `Total profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital Starting capital) / Starting capital`.
- `CAGR %`: Compound annual growth rate.
- `Profit factor`: profit / loss.
- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount.
- `Total trade volume`: Volume generated on the exchange to reach the above profit.
- `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`.

View File

@@ -64,10 +64,7 @@ You will also have to pick a "margin mode" (explanation below) - with freqtrade
### Margin mode
On top of `trading_mode` - you will also have to configure your `margin_mode`.
While freqtrade currently only supports one margin mode, this will change, and by configuring it now you're all set for future updates.
The possible values are: `isolated`, or `cross`(*currently unavailable*).
The possible values are: `isolated`, or `cross`(*currently unavailable*)
#### Isolated margin mode
@@ -85,16 +82,6 @@ One account is used to share collateral between markets (trading pairs). Margin
"margin_mode": "cross"
```
## Set leverage to use
Different strategies and risk profiles will require different levels of leverage.
While you could configure one static leverage value - freqtrade offers you the flexibility to adjust this via [strategy leverage callback](strategy-callbacks.md#leverage-callback) - which allows you to use different leverages by pair, or based on some other factor benefitting your strategy result.
If not implemented, leverage defaults to 1x (no leverage).
!!! Warning
Higher leverage also equals higher risk - be sure you fully understand the implications of using leverage!
## Understand `liquidation_buffer`
*Defaults to `0.05`*

View File

@@ -1,5 +1,5 @@
mkdocs==1.3.0
mkdocs-material==8.3.6
mkdocs-material==8.2.16
mdx_truly_sane_lists==1.2
pymdown-extensions==9.5
pymdown-extensions==9.4
jinja2==3.1.2

View File

@@ -89,12 +89,11 @@ WHERE id=31;
If you'd still like to remove a trade from the database directly, you can use the below query.
!!! Danger
Some systems (Ubuntu) disable foreign keys in their sqlite3 packaging. When using sqlite - please ensure that foreign keys are on by running `PRAGMA foreign_keys = ON` before the above query.
```sql
DELETE FROM trades WHERE id = <tradeid>;
```
```sql
DELETE FROM trades WHERE id = 31;
```
@@ -103,20 +102,13 @@ DELETE FROM trades WHERE id = 31;
## Use a different database system
Freqtrade is using SQLAlchemy, which supports multiple different database systems. As such, a multitude of database systems should be supported.
Freqtrade does not depend or install any additional database driver. Please refer to the [SQLAlchemy docs](https://docs.sqlalchemy.org/en/14/core/engines.html#database-urls) on installation instructions for the respective database systems.
The following systems have been tested and are known to work with freqtrade:
* sqlite (default)
* PostgreSQL)
* MariaDB
!!! Warning
By using one of the below database systems, you acknowledge that you know how to manage such a system. The freqtrade team will not provide any support with setup or maintenance (or backups) of the below database systems.
By using one of the below database systems, you acknowledge that you know how to manage such a system. Freqtrade will not provide any support with setup or maintenance (or backups) of the below database systems.
### PostgreSQL
Freqtrade supports PostgreSQL by using SQLAlchemy, which supports multiple different database systems.
Installation:
`pip install psycopg2-binary`

View File

@@ -191,19 +191,6 @@ For example, simplified math:
!!! Tip
Make sure to have this value (`trailing_stop_positive_offset`) lower than minimal ROI, otherwise minimal ROI will apply first and sell the trade.
## Stoploss and Leverage
Stoploss should be thought of as "risk on this trade" - so a stoploss of 10% on a 100$ trade means you are willing to lose 10$ (10%) on this trade - which would trigger if the price moves 10% to the downside.
When using leverage, the same principle is applied - with stoploss defining the risk on the trade (the amount you are willing to lose).
Therefore, a stoploss of 10% on a 10x trade would trigger on a 1% price move.
If your stake amount (own capital) was 100$ - this trade would be 1000$ at 10x (after leverage).
If price moves 1% - you've lost 10$ of your own capital - therfore stoploss will trigger in this case.
Make sure to be aware of this, and avoid using too tight stoploss (at 10x leverage, 10% risk may be too little to allow the trade to "breath" a little).
## Changing stoploss on open trades
A stoploss on an open trade can be changed by changing the value in the configuration or strategy and use the `/reload_config` command (alternatively, completely stopping and restarting the bot also works).

View File

@@ -550,8 +550,7 @@ class AwesomeStrategy(IStrategy):
:param pair: Pair that's about to be bought/shorted.
:param order_type: Order type (as configured in order_types). usually limit or market.
:param amount: Amount in target (base) currency that's going to be traded.
:param rate: Rate that's going to be used when using limit orders
or current rate for market orders.
:param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param current_time: datetime object, containing the current datetime
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
@@ -601,7 +600,6 @@ class AwesomeStrategy(IStrategy):
:param order_type: Order type (as configured in order_types). usually limit or market.
:param amount: Amount in base currency.
:param rate: Rate that's going to be used when using limit orders
or current rate for market orders.
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param exit_reason: Exit reason.
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
@@ -806,23 +804,19 @@ For markets / exchanges that don't support leverage, this method is ignored.
``` python
class AwesomeStrategy(IStrategy):
def leverage(self, pair: str, current_time: datetime, current_rate: float,
proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str,
def leverage(self, pair: str, current_time: 'datetime', current_rate: float,
proposed_leverage: float, max_leverage: float, side: str,
**kwargs) -> float:
"""
Customize leverage for each new trade. This method is only called in futures mode.
Customize leverage for each new trade.
:param pair: Pair that's currently analyzed
:param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
:param proposed_leverage: A leverage proposed by the bot.
:param max_leverage: Max leverage allowed on this pair
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param side: 'long' or 'short' - indicating the direction of the proposed trade
:return: A leverage amount, which is between 1.0 and max_leverage.
"""
return 1.0
```
All profit calculations include leverage. Stoploss / ROI also include leverage in their calculation.
Defining a stoploss of 10% at 10x leverage would trigger the stoploss with a 1% move to the downside.

View File

@@ -171,8 +171,8 @@ official commands. You can ask at any moment for help with `/help`.
| `/locks` | Show currently locked pairs.
| `/unlock <pair or lock_id>` | Remove the lock for this pair (or for this lock id).
| `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default)
| `/forceexit <trade_id> | /fx <tradeid>` | Instantly exits the given trade (Ignoring `minimum_roi`).
| `/forceexit all | /fx all` | Instantly exits all open trades (Ignoring `minimum_roi`).
| `/forceexit <trade_id>` | Instantly exits the given trade (Ignoring `minimum_roi`).
| `/forceexit all` | Instantly exits all open trades (Ignoring `minimum_roi`).
| `/fx` | alias for `/forceexit`
| `/forcelong <pair> [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`force_entry_enable` must be set to True)
| `/forceshort <pair> [rate]` | Instantly shorts the given pair. Rate is optional and only applies to limit orders. This will only work on non-spot markets. (`force_entry_enable` must be set to True)
@@ -270,15 +270,10 @@ Return a summary of your profit/loss and performance.
> **Latest Trade opened:** `2 minutes ago`
> **Avg. Duration:** `2:33:45`
> **Best Performing:** `PAY/BTC: 50.23%`
> **Trading volume:** `0.5 BTC`
> **Profit factor:** `1.04`
> **Max Drawdown:** `9.23% (0.01255 BTC)`
The relative profit of `1.2%` is the average profit per trade.
The relative profit of `15.2 Σ%` is be based on the starting capital - so in this case, the starting capital was `0.00485701 * 1.152 = 0.00738 BTC`.
Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits.
Profit Factor is calculated as gross profits / gross losses - and should serve as an overall metric for the strategy.
Max drawdown corresponds to the backtesting metric `Absolute Drawdown (Account)` - calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`.
The relative profit of `15.2 Σ%` is be based on the starting capital - so in this case, the starting capital was `0.00485701 * 1.152 = 0.00738 BTC`.
Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits.
### /forceexit <trade_id>
@@ -286,7 +281,6 @@ Max drawdown corresponds to the backtesting metric `Absolute Drawdown (Account)`
!!! Tip
You can get a list of all open trades by calling `/forceexit` without parameter, which will show a list of buttons to simply exit a trade.
This command has an alias in `/fx` - which has the same capabilities, but is faster to type in "emergency" situations.
### /forcelong <pair> [rate] | /forceshort <pair> [rate]
@@ -334,11 +328,11 @@ Per default `/daily` will return the 7 last days. The example below if for `/dai
> **Daily Profit over the last 3 days:**
```
Day (count) USDT USD Profit %
-------------- ------------ ---------- ----------
2022-06-11 (1) -0.746 USDT -0.75 USD -0.08%
2022-06-10 (0) 0 USDT 0.00 USD 0.00%
2022-06-09 (5) 20 USDT 20.10 USD 5.00%
Day Profit BTC Profit USD
---------- -------------- ------------
2018-01-03 0.00224175 BTC 29,142 USD
2018-01-02 0.00033131 BTC 4,307 USD
2018-01-01 0.00269130 BTC 34.986 USD
```
### /weekly <n>
@@ -348,11 +342,11 @@ from Monday. The example below if for `/weekly 3`:
> **Weekly Profit over the last 3 weeks (starting from Monday):**
```
Monday (count) Profit BTC Profit USD Profit %
------------- -------------- ------------ ----------
2018-01-03 (5) 0.00224175 BTC 29,142 USD 4.98%
2017-12-27 (1) 0.00033131 BTC 4,307 USD 0.00%
2017-12-20 (4) 0.00269130 BTC 34.986 USD 5.12%
Monday Profit BTC Profit USD
---------- -------------- ------------
2018-01-03 0.00224175 BTC 29,142 USD
2017-12-27 0.00033131 BTC 4,307 USD
2017-12-20 0.00269130 BTC 34.986 USD
```
### /monthly <n>
@@ -362,11 +356,11 @@ if for `/monthly 3`:
> **Monthly Profit over the last 3 months:**
```
Month (count) Profit BTC Profit USD Profit %
------------- -------------- ------------ ----------
2018-01 (20) 0.00224175 BTC 29,142 USD 4.98%
2017-12 (5) 0.00033131 BTC 4,307 USD 0.00%
2017-11 (10) 0.00269130 BTC 34.986 USD 5.10%
Month Profit BTC Profit USD
---------- -------------- ------------
2018-01 0.00224175 BTC 29,142 USD
2017-12 0.00033131 BTC 4,307 USD
2017-11 0.00269130 BTC 34.986 USD
```
### /whitelist

View File

@@ -32,8 +32,4 @@ Please ensure that you're also updating dependencies - otherwise things might br
``` bash
git pull
pip install -U -r requirements.txt
pip install -e .
# Ensure freqUI is at the latest version
freqtrade install-ui
```

View File

@@ -651,61 +651,6 @@ Common arguments:
```
## Detailed backtest analysis
Advanced backtest result analysis.
More details in the [Backtesting analysis](advanced-backtesting.md#analyze-the-buyentry-and-sellexit-tags) Section.
```
usage: freqtrade backtesting-analysis [-h] [-v] [--logfile FILE] [-V]
[-c PATH] [-d PATH] [--userdir PATH]
[--export-filename PATH]
[--analysis-groups {0,1,2,3,4} [{0,1,2,3,4} ...]]
[--enter-reason-list ENTER_REASON_LIST [ENTER_REASON_LIST ...]]
[--exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...]]
[--indicator-list INDICATOR_LIST [INDICATOR_LIST ...]]
optional arguments:
-h, --help show this help message and exit
--export-filename PATH, --backtest-filename PATH
Use this filename for backtest results.Requires
`--export` to be set as well. Example: `--export-filen
ame=user_data/backtest_results/backtest_today.json`
--analysis-groups {0,1,2,3,4} [{0,1,2,3,4} ...]
grouping output - 0: simple wins/losses by enter tag,
1: by enter_tag, 2: by enter_tag and exit_tag, 3: by
pair and enter_tag, 4: by pair, enter_ and exit_tag
(this can get quite large)
--enter-reason-list ENTER_REASON_LIST [ENTER_REASON_LIST ...]
Comma separated list of entry signals to analyse.
Default: all. e.g. 'entry_tag_a,entry_tag_b'
--exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...]
Comma separated list of exit signals to analyse.
Default: all. e.g.
'exit_tag_a,roi,stop_loss,trailing_stop_loss'
--indicator-list INDICATOR_LIST [INDICATOR_LIST ...]
Comma separated list of indicators to analyse. e.g.
'close,rsi,bb_lowerband,profit_abs'
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified. Special values are:
'syslog', 'journald'. See the documentation for more
details.
-V, --version show program's version number and exit
-c PATH, --config PATH
Specify configuration file (default:
`userdir/config.json` or `config.json` whichever
exists). Multiple --config options may be used. Can be
set to `-` to read config from stdin.
-d PATH, --datadir PATH
Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
```
## List Hyperopt results
You can list the hyperoptimization epochs the Hyperopt module evaluated previously with the `hyperopt-list` sub-command.

View File

@@ -239,52 +239,3 @@ Possible parameters are:
The fields in `webhook.webhookstatus` are used for regular status messages (Started / Stopped / ...). Parameters are filled using string.format.
The only possible value here is `{status}`.
## Discord
A special form of webhooks is available for discord.
You can configure this as follows:
```json
"discord": {
"enabled": true,
"webhook_url": "https://discord.com/api/webhooks/<Your webhook URL ...>",
"exit_fill": [
{"Trade ID": "{trade_id}"},
{"Exchange": "{exchange}"},
{"Pair": "{pair}"},
{"Direction": "{direction}"},
{"Open rate": "{open_rate}"},
{"Close rate": "{close_rate}"},
{"Amount": "{amount}"},
{"Open date": "{open_date:%Y-%m-%d %H:%M:%S}"},
{"Close date": "{close_date:%Y-%m-%d %H:%M:%S}"},
{"Profit": "{profit_amount} {stake_currency}"},
{"Profitability": "{profit_ratio:.2%}"},
{"Enter tag": "{enter_tag}"},
{"Exit Reason": "{exit_reason}"},
{"Strategy": "{strategy}"},
{"Timeframe": "{timeframe}"},
],
"entry_fill": [
{"Trade ID": "{trade_id}"},
{"Exchange": "{exchange}"},
{"Pair": "{pair}"},
{"Direction": "{direction}"},
{"Open rate": "{open_rate}"},
{"Amount": "{amount}"},
{"Open date": "{open_date:%Y-%m-%d %H:%M:%S}"},
{"Enter tag": "{enter_tag}"},
{"Strategy": "{strategy} {timeframe}"},
]
}
```
The above represents the default (`exit_fill` and `entry_fill` are optional and will default to the above configuration) - modifications are obviously possible.
Available fields correspond to the fields for webhooks and are documented in the corresponding webhook sections.
The notifications will look as follows by default.
![discord-notification](assets/discord_notification.png)

View File

@@ -1,5 +1,5 @@
""" Freqtrade bot """
__version__ = 'develop'
__version__ = '2022.5.1'
if 'dev' in __version__:
try:

View File

@@ -6,7 +6,6 @@ Contains all start-commands, subcommands and CLI Interface creation.
Note: Be careful with file-scoped imports in these subfiles.
as they are parsed on startup, nothing containing optional modules should be loaded.
"""
from freqtrade.commands.analyze_commands import start_analysis_entries_exits
from freqtrade.commands.arguments import Arguments
from freqtrade.commands.build_config_commands import start_new_config
from freqtrade.commands.data_commands import (start_convert_data, start_convert_trades,

View File

@@ -1,69 +0,0 @@
import logging
from pathlib import Path
from typing import Any, Dict
from freqtrade.configuration import setup_utils_configuration
from freqtrade.enums import RunMode
from freqtrade.exceptions import OperationalException
logger = logging.getLogger(__name__)
def setup_analyze_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str, Any]:
"""
Prepare the configuration for the entry/exit reason analysis module
:param args: Cli args from Arguments()
:param method: Bot running mode
:return: Configuration
"""
config = setup_utils_configuration(args, method)
no_unlimited_runmodes = {
RunMode.BACKTEST: 'backtesting',
}
if method in no_unlimited_runmodes.keys():
from freqtrade.data.btanalysis import get_latest_backtest_filename
if 'exportfilename' in config:
if config['exportfilename'].is_dir():
btfile = Path(get_latest_backtest_filename(config['exportfilename']))
signals_file = f"{config['exportfilename']}/{btfile.stem}_signals.pkl"
else:
if config['exportfilename'].exists():
btfile = Path(config['exportfilename'])
signals_file = f"{btfile.parent}/{btfile.stem}_signals.pkl"
else:
raise OperationalException(f"{config['exportfilename']} does not exist.")
else:
raise OperationalException('exportfilename not in config.')
if (not Path(signals_file).exists()):
raise OperationalException(
(f"Cannot find latest backtest signals file: {signals_file}."
"Run backtesting with `--export signals`.")
)
return config
def start_analysis_entries_exits(args: Dict[str, Any]) -> None:
"""
Start analysis script
:param args: Cli args from Arguments()
:return: None
"""
from freqtrade.data.entryexitanalysis import process_entry_exit_reasons
# Initialize configuration
config = setup_analyze_configuration(args, RunMode.BACKTEST)
logger.info('Starting freqtrade in analysis mode')
process_entry_exit_reasons(config['exportfilename'],
config['exchange']['pair_whitelist'],
config['analysis_groups'],
config['enter_reason_list'],
config['exit_reason_list'],
config['indicator_list']
)

View File

@@ -101,9 +101,6 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop
"print_json", "hyperoptexportfilename", "hyperopt_show_no_header",
"disableparamexport", "backtest_breakdown"]
ARGS_ANALYZE_ENTRIES_EXITS = ["exportfilename", "analysis_groups", "enter_reason_list",
"exit_reason_list", "indicator_list"]
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
"list-markets", "list-pairs", "list-strategies", "list-data",
"hyperopt-list", "hyperopt-show", "backtest-filter",
@@ -185,9 +182,8 @@ class Arguments:
self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot')
self._build_args(optionlist=['version'], parser=self.parser)
from freqtrade.commands import (start_analysis_entries_exits, start_backtesting,
start_backtesting_show, start_convert_data,
start_convert_db, start_convert_trades,
from freqtrade.commands import (start_backtesting, start_backtesting_show,
start_convert_data, start_convert_db, start_convert_trades,
start_create_userdir, start_download_data, start_edge,
start_hyperopt, start_hyperopt_list, start_hyperopt_show,
start_install_ui, start_list_data, start_list_exchanges,
@@ -287,13 +283,6 @@ class Arguments:
backtesting_show_cmd.set_defaults(func=start_backtesting_show)
self._build_args(optionlist=ARGS_BACKTEST_SHOW, parser=backtesting_show_cmd)
# Add backtesting analysis subcommand
analysis_cmd = subparsers.add_parser('backtesting-analysis',
help='Backtest Analysis module.',
parents=[_common_parser])
analysis_cmd.set_defaults(func=start_analysis_entries_exits)
self._build_args(optionlist=ARGS_ANALYZE_ENTRIES_EXITS, parser=analysis_cmd)
# Add edge subcommand
edge_cmd = subparsers.add_parser('edge', help='Edge module.',
parents=[_common_parser, _strategy_parser])

View File

@@ -614,37 +614,4 @@ AVAILABLE_CLI_OPTIONS = {
"that do not contain any parameters."),
action="store_true",
),
"analysis_groups": Arg(
"--analysis-groups",
help=("grouping output - "
"0: simple wins/losses by enter tag, "
"1: by enter_tag, "
"2: by enter_tag and exit_tag, "
"3: by pair and enter_tag, "
"4: by pair, enter_ and exit_tag (this can get quite large)"),
nargs='+',
default=['0', '1', '2'],
choices=['0', '1', '2', '3', '4'],
),
"enter_reason_list": Arg(
"--enter-reason-list",
help=("Comma separated list of entry signals to analyse. Default: all. "
"e.g. 'entry_tag_a,entry_tag_b'"),
nargs='+',
default=['all'],
),
"exit_reason_list": Arg(
"--exit-reason-list",
help=("Comma separated list of exit signals to analyse. Default: all. "
"e.g. 'exit_tag_a,roi,stop_loss,trailing_stop_loss'"),
nargs='+',
default=['all'],
),
"indicator_list": Arg(
"--indicator-list",
help=("Comma separated list of indicators to analyse. "
"e.g. 'close,rsi,bb_lowerband,profit_abs'"),
nargs='+',
default=[],
),
}

View File

@@ -95,8 +95,6 @@ class Configuration:
self._process_data_options(config)
self._process_analyze_options(config)
# Check if the exchange set by the user is supported
check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True))
@@ -435,19 +433,6 @@ class Configuration:
self._args_to_config(config, argname='candle_types',
logstring='Detected --candle-types: {}')
def _process_analyze_options(self, config: Dict[str, Any]) -> None:
self._args_to_config(config, argname='analysis_groups',
logstring='Analysis reason groups: {}')
self._args_to_config(config, argname='enter_reason_list',
logstring='Analysis enter tag list: {}')
self._args_to_config(config, argname='exit_reason_list',
logstring='Analysis exit tag list: {}')
self._args_to_config(config, argname='indicator_list',
logstring='Analysis indicator list: {}')
def _process_runmode(self, config: Dict[str, Any]) -> None:
self._args_to_config(config, argname='dry_run',

View File

@@ -336,47 +336,6 @@ CONF_SCHEMA = {
'webhookstatus': {'type': 'object'},
},
},
'discord': {
'type': 'object',
'properties': {
'enabled': {'type': 'boolean'},
'webhook_url': {'type': 'string'},
"exit_fill": {
'type': 'array', 'items': {'type': 'object'},
'default': [
{"Trade ID": "{trade_id}"},
{"Exchange": "{exchange}"},
{"Pair": "{pair}"},
{"Direction": "{direction}"},
{"Open rate": "{open_rate}"},
{"Close rate": "{close_rate}"},
{"Amount": "{amount}"},
{"Open date": "{open_date:%Y-%m-%d %H:%M:%S}"},
{"Close date": "{close_date:%Y-%m-%d %H:%M:%S}"},
{"Profit": "{profit_amount} {stake_currency}"},
{"Profitability": "{profit_ratio:.2%}"},
{"Enter tag": "{enter_tag}"},
{"Exit Reason": "{exit_reason}"},
{"Strategy": "{strategy}"},
{"Timeframe": "{timeframe}"},
]
},
"entry_fill": {
'type': 'array', 'items': {'type': 'object'},
'default': [
{"Trade ID": "{trade_id}"},
{"Exchange": "{exchange}"},
{"Pair": "{pair}"},
{"Direction": "{direction}"},
{"Open rate": "{open_rate}"},
{"Amount": "{amount}"},
{"Open date": "{open_date:%Y-%m-%d %H:%M:%S}"},
{"Enter tag": "{enter_tag}"},
{"Strategy": "{strategy} {timeframe}"},
]
},
}
},
'api_server': {
'type': 'object',
'properties': {

View File

@@ -26,7 +26,7 @@ BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date',
'profit_ratio', 'profit_abs', 'exit_reason',
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs',
'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'enter_tag',
'is_short', 'open_timestamp', 'close_timestamp', 'orders'
'is_short'
]
@@ -283,8 +283,6 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non
if 'enter_tag' not in df.columns:
df['enter_tag'] = df['buy_tag']
df = df.drop(['buy_tag'], axis=1)
if 'orders' not in df.columns:
df.loc[:, 'orders'] = None
else:
# old format - only with lists.
@@ -339,7 +337,7 @@ def trade_list_to_dataframe(trades: List[LocalTrade]) -> pd.DataFrame:
:param trades: List of trade objects
:return: Dataframe with BT_DATA_COLUMNS
"""
df = pd.DataFrame.from_records([t.to_json(True) for t in trades], columns=BT_DATA_COLUMNS)
df = pd.DataFrame.from_records([t.to_json() for t in trades], columns=BT_DATA_COLUMNS)
if len(df) > 0:
df.loc[:, 'close_date'] = pd.to_datetime(df['close_date'], utc=True)
df.loc[:, 'open_date'] = pd.to_datetime(df['open_date'], utc=True)

View File

@@ -1,227 +0,0 @@
import logging
from pathlib import Path
from typing import List, Optional
import joblib
import pandas as pd
from tabulate import tabulate
from freqtrade.data.btanalysis import (get_latest_backtest_filename, load_backtest_data,
load_backtest_stats)
from freqtrade.exceptions import OperationalException
logger = logging.getLogger(__name__)
def _load_signal_candles(backtest_dir: Path):
if backtest_dir.is_dir():
scpf = Path(backtest_dir,
Path(get_latest_backtest_filename(backtest_dir)).stem + "_signals.pkl"
)
else:
scpf = Path(backtest_dir.parent / f"{backtest_dir.stem}_signals.pkl")
try:
scp = open(scpf, "rb")
signal_candles = joblib.load(scp)
logger.info(f"Loaded signal candles: {str(scpf)}")
except Exception as e:
logger.error("Cannot load signal candles from pickled results: ", e)
return signal_candles
def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_candles):
analysed_trades_dict = {}
analysed_trades_dict[strategy_name] = {}
try:
logger.info(f"Processing {strategy_name} : {len(pairlist)} pairs")
for pair in pairlist:
if pair in signal_candles[strategy_name]:
analysed_trades_dict[strategy_name][pair] = _analyze_candles_and_indicators(
pair,
trades,
signal_candles[strategy_name][pair])
except Exception as e:
print(f"Cannot process entry/exit reasons for {strategy_name}: ", e)
return analysed_trades_dict
def _analyze_candles_and_indicators(pair, trades, signal_candles):
buyf = signal_candles
if len(buyf) > 0:
buyf = buyf.set_index('date', drop=False)
trades_red = trades.loc[trades['pair'] == pair].copy()
trades_inds = pd.DataFrame()
if trades_red.shape[0] > 0 and buyf.shape[0] > 0:
for t, v in trades_red.open_date.items():
allinds = buyf.loc[(buyf['date'] < v)]
if allinds.shape[0] > 0:
tmp_inds = allinds.iloc[[-1]]
trades_red.loc[t, 'signal_date'] = tmp_inds['date'].values[0]
trades_red.loc[t, 'enter_reason'] = trades_red.loc[t, 'enter_tag']
tmp_inds.index.rename('signal_date', inplace=True)
trades_inds = pd.concat([trades_inds, tmp_inds])
if 'signal_date' in trades_red:
trades_red['signal_date'] = pd.to_datetime(trades_red['signal_date'], utc=True)
trades_red.set_index('signal_date', inplace=True)
try:
trades_red = pd.merge(trades_red, trades_inds, on='signal_date', how='outer')
except Exception as e:
raise e
return trades_red
else:
return pd.DataFrame()
def _do_group_table_output(bigdf, glist):
for g in glist:
# 0: summary wins/losses grouped by enter tag
if g == "0":
group_mask = ['enter_reason']
wins = bigdf.loc[bigdf['profit_abs'] >= 0] \
.groupby(group_mask) \
.agg({'profit_abs': ['sum']})
wins.columns = ['profit_abs_wins']
loss = bigdf.loc[bigdf['profit_abs'] < 0] \
.groupby(group_mask) \
.agg({'profit_abs': ['sum']})
loss.columns = ['profit_abs_loss']
new = bigdf.groupby(group_mask).agg({'profit_abs': [
'count',
lambda x: sum(x > 0),
lambda x: sum(x <= 0)]})
new = pd.concat([new, wins, loss], axis=1).fillna(0)
new['profit_tot'] = new['profit_abs_wins'] - abs(new['profit_abs_loss'])
new['wl_ratio_pct'] = (new.iloc[:, 1] / new.iloc[:, 0] * 100).fillna(0)
new['avg_win'] = (new['profit_abs_wins'] / new.iloc[:, 1]).fillna(0)
new['avg_loss'] = (new['profit_abs_loss'] / new.iloc[:, 2]).fillna(0)
new.columns = ['total_num_buys', 'wins', 'losses', 'profit_abs_wins', 'profit_abs_loss',
'profit_tot', 'wl_ratio_pct', 'avg_win', 'avg_loss']
sortcols = ['total_num_buys']
_print_table(new, sortcols, show_index=True)
else:
agg_mask = {'profit_abs': ['count', 'sum', 'median', 'mean'],
'profit_ratio': ['sum', 'median', 'mean']}
agg_cols = ['num_buys', 'profit_abs_sum', 'profit_abs_median',
'profit_abs_mean', 'median_profit_pct', 'mean_profit_pct',
'total_profit_pct']
sortcols = ['profit_abs_sum', 'enter_reason']
# 1: profit summaries grouped by enter_tag
if g == "1":
group_mask = ['enter_reason']
# 2: profit summaries grouped by enter_tag and exit_tag
if g == "2":
group_mask = ['enter_reason', 'exit_reason']
# 3: profit summaries grouped by pair and enter_tag
if g == "3":
group_mask = ['pair', 'enter_reason']
# 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large)
if g == "4":
group_mask = ['pair', 'enter_reason', 'exit_reason']
if group_mask:
new = bigdf.groupby(group_mask).agg(agg_mask).reset_index()
new.columns = group_mask + agg_cols
new['median_profit_pct'] = new['median_profit_pct'] * 100
new['mean_profit_pct'] = new['mean_profit_pct'] * 100
new['total_profit_pct'] = new['total_profit_pct'] * 100
_print_table(new, sortcols)
else:
logger.warning("Invalid group mask specified.")
def _print_results(analysed_trades, stratname, analysis_groups,
enter_reason_list, exit_reason_list,
indicator_list, columns=None):
if columns is None:
columns = ['pair', 'open_date', 'close_date', 'profit_abs', 'enter_reason', 'exit_reason']
bigdf = pd.DataFrame()
for pair, trades in analysed_trades[stratname].items():
bigdf = pd.concat([bigdf, trades], ignore_index=True)
if bigdf.shape[0] > 0 and ('enter_reason' in bigdf.columns):
if analysis_groups:
_do_group_table_output(bigdf, analysis_groups)
if enter_reason_list and "all" not in enter_reason_list:
bigdf = bigdf.loc[(bigdf['enter_reason'].isin(enter_reason_list))]
if exit_reason_list and "all" not in exit_reason_list:
bigdf = bigdf.loc[(bigdf['exit_reason'].isin(exit_reason_list))]
if "all" in indicator_list:
print(bigdf)
elif indicator_list is not None:
available_inds = []
for ind in indicator_list:
if ind in bigdf:
available_inds.append(ind)
ilist = ["pair", "enter_reason", "exit_reason"] + available_inds
_print_table(bigdf[ilist], sortcols=['exit_reason'], show_index=False)
else:
print("\\_ No trades to show")
def _print_table(df, sortcols=None, show_index=False):
if (sortcols is not None):
data = df.sort_values(sortcols)
else:
data = df
print(
tabulate(
data,
headers='keys',
tablefmt='psql',
showindex=show_index
)
)
def process_entry_exit_reasons(backtest_dir: Path,
pairlist: List[str],
analysis_groups: Optional[List[str]] = ["0", "1", "2"],
enter_reason_list: Optional[List[str]] = ["all"],
exit_reason_list: Optional[List[str]] = ["all"],
indicator_list: Optional[List[str]] = []):
try:
backtest_stats = load_backtest_stats(backtest_dir)
for strategy_name, results in backtest_stats['strategy'].items():
trades = load_backtest_data(backtest_dir, strategy_name)
if not trades.empty:
signal_candles = _load_signal_candles(backtest_dir)
analysed_trades_dict = _process_candles_and_indicators(pairlist, strategy_name,
trades, signal_candles)
_print_results(analysed_trades_dict,
strategy_name,
analysis_groups,
enter_reason_list,
exit_reason_list,
indicator_list)
except ValueError as e:
raise OperationalException(e) from e

View File

@@ -221,7 +221,7 @@ def _download_pair_history(pair: str, *,
prepend=prepend)
logger.info(f'({process}) - Download history data for "{pair}", {timeframe}, '
f'{candle_type} and store in {datadir}. '
f'{candle_type} and store in {datadir}.'
f'From {format_ms_time(since_ms) if since_ms else "start"} to '
f'{format_ms_time(until_ms) if until_ms else "now"}'
)

View File

@@ -52,15 +52,10 @@ class Binance(Exchange):
ordertype = 'stop' if self.trading_mode == TradingMode.FUTURES else 'stop_loss_limit'
return (
order.get('stopPrice', None) is None
or (
order['type'] == ordertype
and (
(side == "sell" and stop_loss > float(order['stopPrice'])) or
(side == "buy" and stop_loss < float(order['stopPrice']))
)
))
return order['type'] == ordertype and (
(side == "sell" and stop_loss > float(order['stopPrice'])) or
(side == "buy" and stop_loss < float(order['stopPrice']))
)
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Dict:
tickers = super().get_tickers(symbols=symbols, cached=cached)

View File

@@ -93,7 +93,7 @@ class Exchange:
:return: None
"""
self._api: ccxt.Exchange
self._api_async: ccxt_async.Exchange = None
self._api_async: ccxt_async.Exchange
self._markets: Dict = {}
self._trading_fees: Dict[str, Any] = {}
self._leverage_tiers: Dict[str, List[Dict]] = {}
@@ -2131,11 +2131,10 @@ class Exchange:
except ccxt.BaseError as e:
raise OperationalException(e) from e
@retrier_async
async def get_market_leverage_tiers(self, symbol: str) -> Tuple[str, List[Dict]]:
@retrier
def get_market_leverage_tiers(self, symbol) -> List[Dict]:
try:
tier = await self._api_async.fetch_market_leverage_tiers(symbol)
return symbol, tier
return self._api.fetch_market_leverage_tiers(symbol)
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
@@ -2169,14 +2168,8 @@ class Exchange:
f"Initializing leverage_tiers for {len(symbols)} markets. "
"This will take about a minute.")
coros = [self.get_market_leverage_tiers(symbol) for symbol in sorted(symbols)]
for input_coro in chunks(coros, 100):
results = self.loop.run_until_complete(
asyncio.gather(*input_coro, return_exceptions=True))
for symbol, res in results:
tiers[symbol] = res
for symbol in sorted(symbols):
tiers[symbol] = self.get_market_leverage_tiers(symbol)
logger.info(f"Done initializing {len(symbols)} markets.")

View File

@@ -3,7 +3,6 @@ import logging
from datetime import datetime
from typing import Dict, List, Optional, Tuple
from freqtrade.constants import BuySell
from freqtrade.enums import MarginMode, TradingMode
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import Exchange
@@ -25,8 +24,6 @@ class Gateio(Exchange):
_ft_has: Dict = {
"ohlcv_candle_limit": 1000,
"ohlcv_volume_currency": "quote",
"time_in_force_parameter": "timeInForce",
"order_time_in_force": ['gtc', 'ioc'],
"stoploss_order_types": {"limit": "limit"},
"stoploss_on_exchange": True,
}
@@ -43,33 +40,13 @@ class Gateio(Exchange):
]
def validate_ordertypes(self, order_types: Dict) -> None:
super().validate_ordertypes(order_types)
if self.trading_mode != TradingMode.FUTURES:
if any(v == 'market' for k, v in order_types.items()):
raise OperationalException(
f'Exchange {self.name} does not support market orders.')
def _get_params(
self,
side: BuySell,
ordertype: str,
leverage: float,
reduceOnly: bool,
time_in_force: str = 'gtc',
) -> Dict:
params = super()._get_params(
side=side,
ordertype=ordertype,
leverage=leverage,
reduceOnly=reduceOnly,
time_in_force=time_in_force,
)
if ordertype == 'market' and self.trading_mode == TradingMode.FUTURES:
params['type'] = 'market'
param = self._ft_has.get('time_in_force_parameter', '')
params.update({param: 'ioc'})
return params
def get_trades_for_order(self, order_id: str, pair: str, since: datetime,
params: Optional[Dict] = None) -> List:
trades = super().get_trades_for_order(order_id, pair, since, params)
@@ -84,8 +61,7 @@ class Gateio(Exchange):
pair_fees = self._trading_fees.get(pair, {})
if pair_fees:
for idx, trade in enumerate(trades):
fee = trade.get('fee', {})
if fee and fee.get('cost') is None:
if trade.get('fee', {}).get('cost') is None:
takerOrMaker = trade.get('takerOrMaker', 'taker')
if pair_fees.get(takerOrMaker) is not None:
trades[idx]['fee'] = {
@@ -114,7 +90,5 @@ class Gateio(Exchange):
Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary.
"""
return (order.get('stopPrice', None) is None or (
side == "sell" and stop_loss > float(order['stopPrice'])) or
(side == "buy" and stop_loss < float(order['stopPrice']))
)
return ((side == "sell" and stop_loss > float(order['stopPrice'])) or
(side == "buy" and stop_loss < float(order['stopPrice'])))

View File

@@ -27,13 +27,7 @@ class Huobi(Exchange):
Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary.
"""
return (
order.get('stopPrice', None) is None
or (
order['type'] == 'stop'
and stop_loss > float(order['stopPrice'])
)
)
return order['type'] == 'stop' and stop_loss > float(order['stopPrice'])
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:

View File

@@ -33,10 +33,7 @@ class Kucoin(Exchange):
Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary.
"""
return (
order.get('stopPrice', None) is None
or stop_loss > float(order['stopPrice'])
)
return order['info'].get('stop') is not None and stop_loss > float(order['stopPrice'])
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:

View File

@@ -4,7 +4,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade()
import copy
import logging
import traceback
from datetime import datetime, time, timedelta, timezone
from datetime import datetime, time, timezone
from math import isclose
from threading import Lock
from typing import Any, Dict, List, Optional, Tuple
@@ -17,13 +17,12 @@ from freqtrade.constants import BuySell, 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, MarginMode, RPCMessageType, RunMode,
SignalDirection, State, TradingMode)
from freqtrade.enums import (ExitCheckTuple, ExitType, RPCMessageType, RunMode, SignalDirection,
State, TradingMode)
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
InvalidOrderException, PricingError)
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.exchange.exchange import timeframe_to_next_date
from freqtrade.maintenance_margin import MaintenanceMargin
from freqtrade.misc import safe_value_fallback, safe_value_fallback2
from freqtrade.mixins import LoggingMixin
from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db
@@ -74,6 +73,8 @@ class FreqtradeBot(LoggingMixin):
PairLocks.timeframe = self.config['timeframe']
self.protections = ProtectionManager(self.config, self.strategy.protections)
# RPC runs in separate threads, can start handling external commands just after
# initialization, even before Freqtradebot has a chance to start its throttling,
# so anything in the Freqtradebot instance should be ready (initialized), including
@@ -105,11 +106,7 @@ class FreqtradeBot(LoggingMixin):
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
self.trading_mode: TradingMode = self.config.get('trading_mode', TradingMode.SPOT)
self.margin_mode: MarginMode = (
MarginMode(config.get('margin_mode'))
if config.get('margin_mode')
else MarginMode.NONE
)
self._schedule = Scheduler()
if self.trading_mode == TradingMode.FUTURES:
@@ -127,18 +124,6 @@ class FreqtradeBot(LoggingMixin):
self.last_process = datetime(1970, 1, 1, tzinfo=timezone.utc)
self.strategy.ft_bot_start()
# Initialize protections AFTER bot start - otherwise parameters are not loaded.
self.protections = ProtectionManager(self.config, self.strategy.protections)
# Start calculating maintenance margin if on cross margin
# TODO-lev: finish the below...
if self.margin_mode == MarginMode.CROSS:
self.maintenance_margin = MaintenanceMargin(
exchange_name=self.exchange.name,
trading_mode=self.trading_mode)
self.maintenance_margin.run()
def notify_status(self, msg: str) -> None:
"""
@@ -242,7 +227,7 @@ class FreqtradeBot(LoggingMixin):
Notify the user when the bot is stopped (not reloaded)
and there are still open trades active.
"""
open_trades = Trade.get_open_trades()
open_trades = Trade.get_trades([Trade.is_open.is_(True)]).all()
if len(open_trades) != 0 and self.state != State.RELOAD_CONFIG:
msg = {
@@ -317,15 +302,6 @@ class FreqtradeBot(LoggingMixin):
self.update_trade_state(order.trade, order.order_id, fo,
stoploss_order=(order.ft_order_side == 'stoploss'))
except InvalidOrderException as e:
logger.warning(f"Error updating Order {order.order_id} due to {e}.")
if order.order_date_utc - timedelta(days=5) < datetime.now(timezone.utc):
logger.warning(
"Order is older than 5 days. Assuming order was fully cancelled.")
fo = order.to_ccxt_object()
fo['status'] = 'canceled'
self.handle_timedout_order(fo, order.trade)
except ExchangeError as e:
logger.warning(f"Error updating Order {order.order_id} due to {e}")
@@ -736,10 +712,6 @@ class FreqtradeBot(LoggingMixin):
trade.orders.append(order_obj)
trade.recalc_trade_from_orders()
if self.margin_mode == MarginMode.CROSS:
self.maintenance_margin.add_new_trade(trade)
Trade.query.session.add(trade)
Trade.commit()
@@ -809,7 +781,7 @@ class FreqtradeBot(LoggingMixin):
current_rate=enter_limit_requested,
proposed_leverage=1.0,
max_leverage=max_leverage,
side=trade_side, entry_tag=entry_tag,
side=trade_side,
) if self.trading_mode != TradingMode.SPOT else 1.0
# Cap leverage between 1.0 and max_leverage.
leverage = min(max(leverage, 1.0), max_leverage)
@@ -1520,21 +1492,10 @@ class FreqtradeBot(LoggingMixin):
# In case of market sell orders the order can be closed immediately
if order.get('status', 'unknown') in ('closed', 'expired'):
self.update_trade_state(trade, trade.open_order_id, order)
Trade.commit()
self._remove_maintenance_trade(trade)
return True
def _remove_maintenance_trade(self, trade: Trade):
"""
Removes a trade from the maintenance margin object
:param trade: The trade to remove from the maintenance margin
"""
if self.margin_mode == MarginMode.CROSS:
self.maintenance_margin.remove_trade(trade)
def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False) -> None:
"""
Sends rpc notification when a sell occurred.

View File

@@ -1,52 +0,0 @@
from typing import List
from freqtrade.enums import TradingMode
from freqtrade.leverage import liquidation_price
from freqtrade.persistence import Trade
class MaintenanceMargin:
trades: List[Trade]
exchange_name: str
trading_mode: TradingMode
@property
def margin_level(self):
# This is the current value of all assets,
# and if you pass below liq_level, you are liquidated
# TODO-lev: Add args to formula
return liquidation_price(
trading_mode=self.trading_mode,
exchange_name=self.exchange_name
)
@property
def liq_level(self): # This may be a constant value and may not need a function
# TODO-lev: The is the value that you are liquidated at
return # If constant, would need to be recalculated after each new trade
def __init__(self, exchange_name: str, trading_mode: TradingMode):
self.exchange_name = exchange_name
self.trading_mode = trading_mode
return
def add_new_trade(self, trade):
self.trades.append(trade)
def remove_trade(self, trade):
self.trades.remove(trade)
# ? def update_trade_pric(self):
def sell_all(self):
# TODO-lev
return
def run(self):
# TODO-lev: implement a thread that constantly updates with every price change,
# TODO-lev: must update at least every few seconds or so
# while true:
# if self.margin_level <= self.liq_level:
# self.sell_all()
return

View File

@@ -704,7 +704,7 @@ class Backtesting:
current_rate=row[OPEN_IDX],
proposed_leverage=1.0,
max_leverage=max_leverage,
side=direction, entry_tag=entry_tag,
side=direction,
) if self._can_short else 1.0
# Cap leverage between 1.0 and max_leverage.
leverage = min(max(leverage, 1.0), max_leverage)
@@ -966,7 +966,6 @@ class Backtesting:
return False
else:
del trade.orders[trade.orders.index(order)]
trade.open_order_id = None
self.canceled_entry_orders += 1
# place new order if result was not None
@@ -1055,7 +1054,6 @@ class Backtesting:
# Close trade
open_trade_count -= 1
open_trades[pair].remove(t)
LocalTrade.trades_open.remove(t)
self.wallets.update()
# 2. Process entries.
@@ -1079,8 +1077,6 @@ class Backtesting:
open_trade_count += 1
# logger.debug(f"{pair} - Emulate creation of new trade: {trade}.")
open_trades[pair].append(trade)
LocalTrade.add_bt_trade(trade)
self.wallets.update()
for trade in list(open_trades[pair]):
# 3. Process entry orders.
@@ -1088,6 +1084,7 @@ class Backtesting:
if order and self._get_order_filled(order.price, row):
order.close_bt_order(current_time, trade)
trade.open_order_id = None
LocalTrade.add_bt_trade(trade)
self.wallets.update()
# 4. Create exit orders (if any)
@@ -1097,7 +1094,6 @@ class Backtesting:
# 5. Process exit orders.
order = trade.select_order(trade.exit_side, is_open=True)
if order and self._get_order_filled(order.price, row):
order.close_bt_order(current_time, trade)
trade.open_order_id = None
trade.close_date = current_time
trade.close(order.price, show_msg=False)
@@ -1266,14 +1262,13 @@ class Backtesting:
self.results['strategy_comparison'].extend(results['strategy_comparison'])
else:
self.results = results
dt_appendix = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
if self.config.get('export', 'none') in ('trades', 'signals'):
store_backtest_stats(self.config['exportfilename'], self.results, dt_appendix)
store_backtest_stats(self.config['exportfilename'], self.results)
if (self.config.get('export', 'none') == 'signals' and
self.dataprovider.runmode == RunMode.BACKTEST):
store_backtest_signal_candles(
self.config['exportfilename'], self.processed_dfs, dt_appendix)
store_backtest_signal_candles(self.config['exportfilename'], self.processed_dfs)
# Results may be mixed up now. Sort them so they follow --strategy-list order.
if 'strategy_list' in self.config and len(self.results) > 0:

View File

@@ -429,7 +429,7 @@ class Hyperopt:
return new_list
i = 0
asked_non_tried: List[List[Any]] = []
is_random_non_tried: List[bool] = []
is_random: List[bool] = []
while i < 5 and len(asked_non_tried) < n_points:
if i < 3:
self.opt.cache_ = {}
@@ -438,9 +438,9 @@ class Hyperopt:
else:
asked = unique_list(self.opt.space.rvs(n_samples=n_points * 5))
is_random = [True for _ in range(len(asked))]
is_random_non_tried += [rand for x, rand in zip(asked, is_random)
if x not in self.opt.Xi
and x not in asked_non_tried]
is_random += [rand for x, rand in zip(asked, is_random)
if x not in self.opt.Xi
and x not in asked_non_tried]
asked_non_tried += [x for x in asked
if x not in self.opt.Xi
and x not in asked_non_tried]
@@ -449,7 +449,7 @@ class Hyperopt:
if asked_non_tried:
return (
asked_non_tried[:min(len(asked_non_tried), n_points)],
is_random_non_tried[:min(len(asked_non_tried), n_points)]
is_random[:min(len(asked_non_tried), n_points)]
)
else:
return self.opt.ask(n_points=n_points), [False for _ in range(n_points)]

View File

@@ -4,6 +4,7 @@ from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Dict, List, Union
from numpy import int64
from pandas import DataFrame, to_datetime
from tabulate import tabulate
@@ -17,21 +18,21 @@ from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
logger = logging.getLogger(__name__)
def store_backtest_stats(
recordfilename: Path, stats: Dict[str, DataFrame], dtappendix: str) -> None:
def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> None:
"""
Stores backtest results
:param recordfilename: Path object, which can either be a filename or a directory.
Filenames will be appended with a timestamp right before the suffix
while for directories, <directory>/backtest-result-<datetime>.json will be used as filename
:param stats: Dataframe containing the backtesting statistics
:param dtappendix: Datetime to use for the filename
"""
if recordfilename.is_dir():
filename = (recordfilename / f'backtest-result-{dtappendix}.json')
filename = (recordfilename /
f'backtest-result-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}.json')
else:
filename = Path.joinpath(
recordfilename.parent, f'{recordfilename.stem}-{dtappendix}'
recordfilename.parent,
f'{recordfilename.stem}-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}'
).with_suffix(recordfilename.suffix)
# Store metadata separately.
@@ -44,8 +45,7 @@ def store_backtest_stats(
file_dump_json(latest_filename, {'latest_backtest': str(filename.name)})
def store_backtest_signal_candles(
recordfilename: Path, candles: Dict[str, Dict], dtappendix: str) -> Path:
def store_backtest_signal_candles(recordfilename: Path, candles: Dict[str, Dict]) -> Path:
"""
Stores backtest trade signal candles
:param recordfilename: Path object, which can either be a filename or a directory.
@@ -53,13 +53,14 @@ def store_backtest_signal_candles(
while for directories, <directory>/backtest-result-<datetime>_signals.pkl will be used
as filename
:param stats: Dict containing the backtesting signal candles
:param dtappendix: Datetime to use for the filename
"""
if recordfilename.is_dir():
filename = (recordfilename / f'backtest-result-{dtappendix}_signals.pkl')
filename = (recordfilename /
f'backtest-result-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}_signals.pkl')
else:
filename = Path.joinpath(
recordfilename.parent, f'{recordfilename.stem}-{dtappendix}_signals.pkl'
recordfilename.parent,
f'{recordfilename.stem}-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}_signals.pkl'
)
file_dump_joblib(filename, candles)
@@ -416,9 +417,9 @@ def generate_strategy_stats(pairlist: List[str],
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'],
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
winning_profit = results.loc[results['profit_abs'] > 0, 'profit_abs'].sum()
losing_profit = results.loc[results['profit_abs'] < 0, 'profit_abs'].sum()
profit_factor = winning_profit / abs(losing_profit) if losing_profit else 0.0
if not results.empty:
results['open_timestamp'] = results['open_date'].view(int64) // 1e6
results['close_timestamp'] = results['close_date'].view(int64) // 1e6
backtest_days = (max_date - min_date).days or 1
strat_stats = {
@@ -446,7 +447,6 @@ def generate_strategy_stats(pairlist: List[str],
'profit_total_long_abs': results.loc[~results['is_short'], 'profit_abs'].sum(),
'profit_total_short_abs': results.loc[results['is_short'], 'profit_abs'].sum(),
'cagr': calculate_cagr(backtest_days, start_balance, content['final_balance']),
'profit_factor': profit_factor,
'backtest_start': min_date.strftime(DATETIME_PRINT_FORMAT),
'backtest_start_ts': int(min_date.timestamp() * 1000),
'backtest_end': max_date.strftime(DATETIME_PRINT_FORMAT),
@@ -501,10 +501,8 @@ def generate_strategy_stats(pairlist: List[str],
(drawdown_abs, drawdown_start, drawdown_end, high_val, low_val,
max_drawdown) = calculate_max_drawdown(
results, value_col='profit_abs', starting_balance=start_balance)
# max_relative_drawdown = Underwater
(_, _, _, _, _, max_relative_drawdown) = calculate_max_drawdown(
results, value_col='profit_abs', starting_balance=start_balance, relative=True)
strat_stats.update({
'max_drawdown': max_drawdown_legacy, # Deprecated - do not use
'max_drawdown_account': max_drawdown,
@@ -783,8 +781,6 @@ def text_table_add_metrics(strat_results: Dict) -> str:
strat_results['stake_currency'])),
('Total profit %', f"{strat_results['profit_total']:.2%}"),
('CAGR %', f"{strat_results['cagr']:.2%}" if 'cagr' in strat_results else 'N/A'),
('Profit factor', f'{strat_results["profit_factor"]:.2f}' if 'profit_factor'
in strat_results else 'N/A'),
('Trades per day', strat_results['trades_per_day']),
('Avg. daily profit %',
f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"),

View File

@@ -201,18 +201,16 @@ def migrate_orders_table(engine, table_back_name: str, cols_order: List):
ft_fee_base = get_column_def(cols_order, 'ft_fee_base', 'null')
average = get_column_def(cols_order, 'average', 'null')
stop_price = get_column_def(cols_order, 'stop_price', 'null')
# sqlite does not support literals for booleans
with engine.begin() as connection:
connection.execute(text(f"""
insert into orders (id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
status, symbol, order_type, side, price, amount, filled, average, remaining, cost,
stop_price, order_date, order_filled_date, order_update_date, ft_fee_base)
order_date, order_filled_date, order_update_date, ft_fee_base)
select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
status, symbol, order_type, side, price, amount, filled, {average} average, remaining,
cost, {stop_price} stop_price, order_date, order_filled_date,
order_update_date, {ft_fee_base} ft_fee_base
cost, order_date, order_filled_date, order_update_date, {ft_fee_base} ft_fee_base
from {table_back_name}
"""))
@@ -249,35 +247,6 @@ def set_sqlite_to_wal(engine):
connection.execute(text("PRAGMA journal_mode=wal"))
def fix_old_dry_orders(engine):
with engine.begin() as connection:
connection.execute(
text(
"""
update orders
set ft_is_open = 0
where ft_is_open = 1 and (ft_trade_id, order_id) not in (
select id, stoploss_order_id from trades where stoploss_order_id is not null
) and ft_order_side = 'stoploss'
and order_id like 'dry_%'
"""
)
)
connection.execute(
text(
"""
update orders
set ft_is_open = 0
where ft_is_open = 1
and (ft_trade_id, order_id) not in (
select id, open_order_id from trades where open_order_id is not null
) and ft_order_side != 'stoploss'
and order_id like 'dry_%'
"""
)
)
def check_migrate(engine, decl_base, previous_tables) -> None:
"""
Checks if migration is necessary and migrates if necessary
@@ -296,8 +265,9 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
# Check if migration necessary
# Migrates both trades and orders table!
if not has_column(cols_orders, 'stop_price'):
# if not has_column(cols_trades, 'base_currency'):
# if ('orders' not in previous_tables
# or not has_column(cols_orders, 'leverage')):
if not has_column(cols_trades, 'base_currency'):
logger.info(f"Running database migration for trades - "
f"backup: {table_back_name}, {order_table_bak_name}")
migrate_trades_and_orders_table(
@@ -318,4 +288,3 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
"start with a fresh database.")
set_sqlite_to_wal(engine)
fix_old_dry_orders(engine)

View File

@@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional
from sqlalchemy import (Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer, String,
UniqueConstraint, desc, func)
from sqlalchemy.orm import Query, lazyload, relationship
from sqlalchemy.orm import Query, relationship
from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES, BuySell, LongShort
from freqtrade.enums import ExitType, TradingMode
@@ -57,7 +57,6 @@ class Order(_DECL_BASE):
filled = Column(Float, nullable=True)
remaining = Column(Float, nullable=True)
cost = Column(Float, nullable=True)
stop_price = Column(Float, nullable=True)
order_date = Column(DateTime, nullable=True, default=datetime.utcnow)
order_filled_date = Column(DateTime, nullable=True)
order_update_date = Column(DateTime, nullable=True)
@@ -75,7 +74,7 @@ class Order(_DECL_BASE):
@property
def safe_filled(self) -> float:
return self.filled if self.filled is not None else self.amount or 0.0
return self.filled or self.amount or 0.0
@property
def safe_fee_base(self) -> float:
@@ -108,7 +107,6 @@ class Order(_DECL_BASE):
self.average = order.get('average', self.average)
self.remaining = order.get('remaining', self.remaining)
self.cost = order.get('cost', self.cost)
self.stop_price = order.get('stopPrice', self.stop_price)
if 'timestamp' in order and order['timestamp'] is not None:
self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc)
@@ -132,7 +130,6 @@ class Order(_DECL_BASE):
'side': self.ft_order_side,
'filled': self.filled,
'remaining': self.remaining,
'stopPrice': self.stop_price,
'datetime': self.order_date_utc.strftime('%Y-%m-%dT%H:%M:%S.%f'),
'timestamp': int(self.order_date_utc.timestamp() * 1000),
'status': self.status,
@@ -140,40 +137,35 @@ class Order(_DECL_BASE):
'info': {},
}
def to_json(self, entry_side: str, minified: bool = False) -> Dict[str, Any]:
resp = {
def to_json(self, entry_side: str) -> Dict[str, Any]:
return {
'pair': self.ft_pair,
'order_id': self.order_id,
'status': self.status,
'amount': self.amount,
'average': round(self.average, 8) if self.average else 0,
'safe_price': self.safe_price,
'cost': self.cost if self.cost else 0,
'filled': self.filled,
'ft_order_side': self.ft_order_side,
'is_open': self.ft_is_open,
'order_date': self.order_date.strftime(DATETIME_PRINT_FORMAT)
if self.order_date else None,
'order_timestamp': int(self.order_date.replace(
tzinfo=timezone.utc).timestamp() * 1000) if self.order_date else None,
'order_filled_date': self.order_filled_date.strftime(DATETIME_PRINT_FORMAT)
if self.order_filled_date else None,
'order_filled_timestamp': int(self.order_filled_date.replace(
tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None,
'order_type': self.order_type,
'price': self.price,
'ft_is_entry': self.ft_order_side == entry_side,
'remaining': self.remaining,
}
if not minified:
resp.update({
'pair': self.ft_pair,
'order_id': self.order_id,
'status': self.status,
'average': round(self.average, 8) if self.average else 0,
'cost': self.cost if self.cost else 0,
'filled': self.filled,
'is_open': self.ft_is_open,
'order_date': self.order_date.strftime(DATETIME_PRINT_FORMAT)
if self.order_date else None,
'order_timestamp': int(self.order_date.replace(
tzinfo=timezone.utc).timestamp() * 1000) if self.order_date else None,
'order_filled_date': self.order_filled_date.strftime(DATETIME_PRINT_FORMAT)
if self.order_filled_date else None,
'order_type': self.order_type,
'price': self.price,
'remaining': self.remaining,
})
return resp
def close_bt_order(self, close_date: datetime, trade: 'LocalTrade'):
self.order_filled_date = close_date
self.filled = self.amount
self.remaining = 0
self.status = 'closed'
self.ft_is_open = False
if (self.ft_order_side == trade.entry_side
@@ -401,9 +393,9 @@ class LocalTrade():
f'open_rate={self.open_rate:.8f}, open_since={open_since})'
)
def to_json(self, minified: bool = False) -> Dict[str, Any]:
filled_orders = self.select_filled_or_open_orders()
orders = [order.to_json(self.entry_side, minified) for order in filled_orders]
def to_json(self) -> Dict[str, Any]:
filled_orders = self.select_filled_orders()
orders = [order.to_json(self.entry_side) for order in filled_orders]
return {
'trade_id': self.id,
@@ -627,8 +619,8 @@ class LocalTrade():
"""
self.close_rate = rate
self.close_date = self.close_date or datetime.utcnow()
self.close_profit = self.calc_profit_ratio(rate)
self.close_profit_abs = self.calc_profit(rate)
self.close_profit = self.calc_profit_ratio()
self.close_profit_abs = self.calc_profit()
self.is_open = False
self.exit_order_status = 'closed'
self.open_order_id = None
@@ -696,9 +688,10 @@ class LocalTrade():
"""
self.open_trade_value = self._calc_open_trade_value()
def calculate_interest(self) -> Decimal:
def calculate_interest(self, interest_rate: Optional[float] = None) -> Decimal:
"""
Calculate interest for this trade. Only applicable for Margin trading.
:param interest_rate: interest_charge for borrowing this coin(optional).
If interest_rate is not set self.interest_rate will be used
"""
zero = Decimal(0.0)
# If nothing was borrowed
@@ -711,26 +704,34 @@ class LocalTrade():
total_seconds = Decimal((now - open_date).total_seconds())
hours = total_seconds / sec_per_hour or zero
rate = Decimal(self.interest_rate)
rate = Decimal(interest_rate or self.interest_rate)
borrowed = Decimal(self.borrowed)
return interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours)
def _calc_base_close(self, amount: Decimal, rate: float, fee: float) -> Decimal:
def _calc_base_close(self, amount: Decimal, rate: Optional[float] = None,
fee: Optional[float] = None) -> Decimal:
close_trade = amount * Decimal(rate)
fees = close_trade * Decimal(fee)
close_trade = Decimal(amount) * Decimal(rate or self.close_rate) # type: ignore
fees = close_trade * Decimal(fee or self.fee_close)
if self.is_short:
return close_trade + fees
else:
return close_trade - fees
def calc_close_trade_value(self, rate: float) -> float:
def calc_close_trade_value(self, rate: Optional[float] = None,
fee: Optional[float] = None,
interest_rate: Optional[float] = None) -> float:
"""
Calculate the Trade's close value including fees
:param rate: rate to compare with.
:return: value in stake currency of the open trade
Calculate the close_rate including fee
:param fee: fee to use on the close rate (optional).
If rate is not set self.fee will be used
:param rate: rate to compare with (optional).
If rate is not set self.close_rate will be used
:param interest_rate: interest_charge for borrowing this coin (optional).
If interest_rate is not set self.interest_rate will be used
:return: Price in BTC of the open trade
"""
if rate is None and not self.close_rate:
return 0.0
@@ -739,38 +740,49 @@ class LocalTrade():
trading_mode = self.trading_mode or TradingMode.SPOT
if trading_mode == TradingMode.SPOT:
return float(self._calc_base_close(amount, rate, self.fee_close))
return float(self._calc_base_close(amount, rate, fee))
elif (trading_mode == TradingMode.MARGIN):
total_interest = self.calculate_interest()
total_interest = self.calculate_interest(interest_rate)
if self.is_short:
amount = amount + total_interest
return float(self._calc_base_close(amount, rate, self.fee_close))
return float(self._calc_base_close(amount, rate, fee))
else:
# Currency already owned for longs, no need to purchase
return float(self._calc_base_close(amount, rate, self.fee_close) - total_interest)
return float(self._calc_base_close(amount, rate, fee) - total_interest)
elif (trading_mode == TradingMode.FUTURES):
funding_fees = self.funding_fees or 0.0
# Positive funding_fees -> Trade has gained from fees.
# Negative funding_fees -> Trade had to pay the fees.
if self.is_short:
return float(self._calc_base_close(amount, rate, self.fee_close)) - funding_fees
return float(self._calc_base_close(amount, rate, fee)) - funding_fees
else:
return float(self._calc_base_close(amount, rate, self.fee_close)) + funding_fees
return float(self._calc_base_close(amount, rate, fee)) + funding_fees
else:
raise OperationalException(
f"{self.trading_mode.value} trading is not yet available using freqtrade")
def calc_profit(self, rate: float) -> float:
def calc_profit(self, rate: Optional[float] = None,
fee: Optional[float] = None,
interest_rate: Optional[float] = None) -> float:
"""
Calculate the absolute profit in stake currency between Close and Open trade
:param rate: close rate to compare with.
:return: profit in stake currency as float
:param fee: fee to use on the close rate (optional).
If fee is not set self.fee will be used
:param rate: close rate to compare with (optional).
If rate is not set self.close_rate will be used
:param interest_rate: interest_charge for borrowing this coin (optional).
If interest_rate is not set self.interest_rate will be used
:return: profit in stake currency as float
"""
close_trade_value = self.calc_close_trade_value(rate)
close_trade_value = self.calc_close_trade_value(
rate=(rate or self.close_rate),
fee=(fee or self.fee_close),
interest_rate=(interest_rate or self.interest_rate)
)
if self.is_short:
profit = self.open_trade_value - close_trade_value
@@ -778,13 +790,23 @@ class LocalTrade():
profit = close_trade_value - self.open_trade_value
return float(f"{profit:.8f}")
def calc_profit_ratio(self, rate: float) -> float:
def calc_profit_ratio(self, rate: Optional[float] = None,
fee: Optional[float] = None,
interest_rate: Optional[float] = None) -> float:
"""
Calculates the profit as ratio (including fee).
:param rate: rate to compare with.
:param rate: rate to compare with (optional).
If rate is not set self.close_rate will be used
:param fee: fee to use on the close rate (optional).
:param interest_rate: interest_charge for borrowing this coin (optional).
If interest_rate is not set self.interest_rate will be used
:return: profit ratio as float
"""
close_trade_value = self.calc_close_trade_value(rate)
close_trade_value = self.calc_close_trade_value(
rate=(rate or self.close_rate),
fee=(fee or self.fee_close),
interest_rate=(interest_rate or self.interest_rate)
)
short_close_zero = (self.is_short and close_trade_value == 0.0)
long_close_zero = (not self.is_short and self.open_trade_value == 0.0)
@@ -801,6 +823,14 @@ class LocalTrade():
return float(f"{profit_ratio:.8f}")
def recalc_trade_from_orders(self):
# We need at least 2 entry orders for averaging amounts and rates.
# TODO: this condition could probably be removed
if len(self.select_filled_orders(self.entry_side)) < 2:
self.stake_amount = self.amount * self.open_rate / self.leverage
# Just in case, still recalc open trade value
self.recalc_open_trade_value()
return
total_amount = 0.0
total_stake = 0.0
@@ -812,6 +842,8 @@ class LocalTrade():
tmp_amount = o.safe_amount_after_fee
tmp_price = o.average or o.price
if o.filled is not None:
tmp_amount = o.filled
if tmp_amount > 0.0 and tmp_price is not None:
total_amount += tmp_amount
total_stake += tmp_price * tmp_amount
@@ -865,21 +897,6 @@ class LocalTrade():
(o.filled or 0) > 0 and
o.status in NON_OPEN_EXCHANGE_STATES]
def select_filled_or_open_orders(self) -> List['Order']:
"""
Finds filled or open orders
:param order_side: Side of the order (either 'buy', 'sell', or None)
:return: array of Order objects
"""
return [o for o in self.orders if
(
o.ft_is_open is False
and (o.filled or 0) > 0
and o.status in NON_OPEN_EXCHANGE_STATES
)
or (o.ft_is_open is True and o.status is not None)
]
@property
def nr_of_successful_entries(self) -> int:
"""
@@ -1118,7 +1135,7 @@ class Trade(_DECL_BASE, LocalTrade):
)
@staticmethod
def get_trades(trade_filter=None, include_orders: bool = True) -> Query:
def get_trades(trade_filter=None) -> Query:
"""
Helper function to query Trades using filters.
NOTE: Not supported in Backtesting.
@@ -1133,14 +1150,9 @@ class Trade(_DECL_BASE, LocalTrade):
if trade_filter is not None:
if not isinstance(trade_filter, list):
trade_filter = [trade_filter]
this_query = Trade.query.filter(*trade_filter)
return Trade.query.filter(*trade_filter)
else:
this_query = Trade.query
if not include_orders:
# Don't load order relations
# Consider using noload or raiseload instead of lazyload
this_query = this_query.options(lazyload(Trade.orders))
return this_query
return Trade.query
@staticmethod
def get_open_order_trades() -> List['Trade']:
@@ -1360,18 +1372,3 @@ class Trade(_DECL_BASE, LocalTrade):
.group_by(Trade.pair) \
.order_by(desc('profit_sum')).first()
return best_pair
@staticmethod
def get_trading_volume(start_date: datetime = datetime.fromtimestamp(0)) -> float:
"""
Get Trade volume based on Orders
NOTE: Not supported in Backtesting.
:returns: Tuple containing (pair, profit_sum)
"""
trading_volume = Order.query.with_entities(
func.sum(Order.cost).label('volume')
).filter(
Order.order_filled_date >= start_date,
Order.status == 'closed'
).scalar()
return trading_volume

View File

@@ -1,7 +1,6 @@
import asyncio
import logging
from copy import deepcopy
from datetime import datetime
from typing import Any, Dict, List
from fastapi import APIRouter, BackgroundTasks, Depends
@@ -103,10 +102,7 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
min_date=min_date, max_date=max_date)
if btconfig.get('export', 'none') == 'trades':
store_backtest_stats(
btconfig['exportfilename'], ApiServer._bt.results,
datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
)
store_backtest_stats(btconfig['exportfilename'], ApiServer._bt.results)
logger.info("Backtest finished.")

View File

@@ -104,10 +104,6 @@ class Profit(BaseModel):
best_pair_profit_ratio: float
winning_trades: int
losing_trades: int
profit_factor: float
max_drawdown: float
max_drawdown_abs: float
trading_volume: Optional[float]
class SellReason(BaseModel):
@@ -124,8 +120,6 @@ class Stats(BaseModel):
class DailyRecord(BaseModel):
date: date
abs_profit: float
rel_profit: float
starting_balance: float
fiat_value: float
trade_count: int
@@ -172,7 +166,7 @@ class ShowConfig(BaseModel):
trailing_stop_positive: Optional[float]
trailing_stop_positive_offset: Optional[float]
trailing_only_offset_is_reached: Optional[bool]
unfilledtimeout: Optional[UnfilledTimeout] # Empty in webserver mode
unfilledtimeout: UnfilledTimeout
order_types: Optional[OrderTypes]
use_custom_stoploss: Optional[bool]
timeframe: Optional[str]
@@ -283,7 +277,6 @@ class OpenTradeSchema(TradeSchema):
class TradeResponse(BaseModel):
trades: List[TradeSchema]
trades_count: int
offset: int
total_trades: int

View File

@@ -36,8 +36,7 @@ logger = logging.getLogger(__name__)
# versions 2.xx -> futures/short branch
# 2.14: Add entry/exit orders to trade response
# 2.15: Add backtest history endpoints
# 2.16: Additional daily metrics
API_VERSION = 2.16
API_VERSION = 2.15
# Public API, requires no auth.
router_public = APIRouter()
@@ -87,8 +86,8 @@ def stats(rpc: RPC = Depends(get_rpc)):
@router.get('/daily', response_model=Daily, tags=['info'])
def daily(timescale: int = 7, rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
return rpc._rpc_timeunit_profit(timescale, config['stake_currency'],
config.get('fiat_display_currency', ''))
return rpc._rpc_daily_profit(timescale, config['stake_currency'],
config.get('fiat_display_currency', ''))
@router.get('/status', response_model=List[OpenTradeSchema], tags=['info'])

View File

@@ -1,59 +0,0 @@
import logging
from typing import Any, Dict
from freqtrade.enums.rpcmessagetype import RPCMessageType
from freqtrade.rpc import RPC
from freqtrade.rpc.webhook import Webhook
logger = logging.getLogger(__name__)
class Discord(Webhook):
def __init__(self, rpc: 'RPC', config: Dict[str, Any]):
# super().__init__(rpc, config)
self.rpc = rpc
self.config = config
self.strategy = config.get('strategy', '')
self.timeframe = config.get('timeframe', '')
self._url = self.config['discord']['webhook_url']
self._format = 'json'
self._retries = 1
self._retry_delay = 0.1
def cleanup(self) -> None:
"""
Cleanup pending module resources.
This will do nothing for webhooks, they will simply not be called anymore
"""
pass
def send_msg(self, msg) -> None:
logger.info(f"Sending discord message: {msg}")
if msg['type'].value in self.config['discord']:
msg['strategy'] = self.strategy
msg['timeframe'] = self.timeframe
fields = self.config['discord'].get(msg['type'].value)
color = 0x0000FF
if msg['type'] in (RPCMessageType.EXIT, RPCMessageType.EXIT_FILL):
profit_ratio = msg.get('profit_ratio')
color = (0x00FF00 if profit_ratio > 0 else 0xFF0000)
embeds = [{
'title': f"Trade: {msg['pair']} {msg['type'].value}",
'color': color,
'fields': [],
}]
for f in fields:
for k, v in f.items():
v = v.format(**msg)
embeds[0]['fields'].append( # type: ignore
{'name': k, 'value': v, 'inline': True})
# Send the message to discord channel
payload = {'embeds': embeds}
self._send_msg(payload)

View File

@@ -18,7 +18,6 @@ from freqtrade import __version__
from freqtrade.configuration.timerange import TimeRange
from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT
from freqtrade.data.history import load_data
from freqtrade.data.metrics import calculate_max_drawdown
from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, SignalDirection, State,
TradingMode)
from freqtrade.exceptions import ExchangeError, PricingError
@@ -284,57 +283,33 @@ class RPC:
columns.append('# Entries')
return trades_list, columns, fiat_profit_sum
def _rpc_timeunit_profit(
def _rpc_daily_profit(
self, timescale: int,
stake_currency: str, fiat_display_currency: str,
timeunit: str = 'days') -> Dict[str, Any]:
"""
:param timeunit: Valid entries are 'days', 'weeks', 'months'
"""
start_date = datetime.now(timezone.utc).date()
if timeunit == 'weeks':
# weekly
start_date = start_date - timedelta(days=start_date.weekday()) # Monday
if timeunit == 'months':
start_date = start_date.replace(day=1)
def time_offset(step: int):
if timeunit == 'months':
return relativedelta(months=step)
return timedelta(**{timeunit: step})
stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
today = datetime.now(timezone.utc).date()
profit_days: Dict[date, Dict] = {}
if not (isinstance(timescale, int) and timescale > 0):
raise RPCException('timescale must be an integer greater than 0')
profit_units: Dict[date, Dict] = {}
daily_stake = self._freqtrade.wallets.get_total_stake_amount()
for day in range(0, timescale):
profitday = start_date - time_offset(day)
# Only query for necessary columns for performance reasons.
trades = Trade.query.session.query(Trade.close_profit_abs).filter(
profitday = today - timedelta(days=day)
trades = Trade.get_trades(trade_filter=[
Trade.is_open.is_(False),
Trade.close_date >= profitday,
Trade.close_date < (profitday + time_offset(1))
).order_by(Trade.close_date).all()
Trade.close_date < (profitday + timedelta(days=1))
]).order_by(Trade.close_date).all()
curdayprofit = sum(
trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None)
# Calculate this periods starting balance
daily_stake = daily_stake - curdayprofit
profit_units[profitday] = {
profit_days[profitday] = {
'amount': curdayprofit,
'daily_stake': daily_stake,
'rel_profit': round(curdayprofit / daily_stake, 8) if daily_stake > 0 else 0,
'trades': len(trades),
'trades': len(trades)
}
data = [
{
'date': f"{key.year}-{key.month:02d}" if timeunit == 'months' else key,
'date': key,
'abs_profit': value["amount"],
'starting_balance': value["daily_stake"],
'rel_profit': value["rel_profit"],
'fiat_value': self._fiat_converter.convert_amount(
value['amount'],
stake_currency,
@@ -342,7 +317,92 @@ class RPC:
) if self._fiat_converter else 0,
'trade_count': value["trades"],
}
for key, value in profit_units.items()
for key, value in profit_days.items()
]
return {
'stake_currency': stake_currency,
'fiat_display_currency': fiat_display_currency,
'data': data
}
def _rpc_weekly_profit(
self, timescale: int,
stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
today = datetime.now(timezone.utc).date()
first_iso_day_of_week = today - timedelta(days=today.weekday()) # Monday
profit_weeks: Dict[date, Dict] = {}
if not (isinstance(timescale, int) and timescale > 0):
raise RPCException('timescale must be an integer greater than 0')
for week in range(0, timescale):
profitweek = first_iso_day_of_week - timedelta(weeks=week)
trades = Trade.get_trades(trade_filter=[
Trade.is_open.is_(False),
Trade.close_date >= profitweek,
Trade.close_date < (profitweek + timedelta(weeks=1))
]).order_by(Trade.close_date).all()
curweekprofit = sum(
trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None)
profit_weeks[profitweek] = {
'amount': curweekprofit,
'trades': len(trades)
}
data = [
{
'date': key,
'abs_profit': value["amount"],
'fiat_value': self._fiat_converter.convert_amount(
value['amount'],
stake_currency,
fiat_display_currency
) if self._fiat_converter else 0,
'trade_count': value["trades"],
}
for key, value in profit_weeks.items()
]
return {
'stake_currency': stake_currency,
'fiat_display_currency': fiat_display_currency,
'data': data
}
def _rpc_monthly_profit(
self, timescale: int,
stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
first_day_of_month = datetime.now(timezone.utc).date().replace(day=1)
profit_months: Dict[date, Dict] = {}
if not (isinstance(timescale, int) and timescale > 0):
raise RPCException('timescale must be an integer greater than 0')
for month in range(0, timescale):
profitmonth = first_day_of_month - relativedelta(months=month)
trades = Trade.get_trades(trade_filter=[
Trade.is_open.is_(False),
Trade.close_date >= profitmonth,
Trade.close_date < (profitmonth + relativedelta(months=1))
]).order_by(Trade.close_date).all()
curmonthprofit = sum(
trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None)
profit_months[profitmonth] = {
'amount': curmonthprofit,
'trades': len(trades)
}
data = [
{
'date': f"{key.year}-{key.month:02d}",
'abs_profit': value["amount"],
'fiat_value': self._fiat_converter.convert_amount(
value['amount'],
stake_currency,
fiat_display_currency
) if self._fiat_converter else 0,
'trade_count': value["trades"],
}
for key, value in profit_months.items()
]
return {
'stake_currency': stake_currency,
@@ -365,7 +425,6 @@ class RPC:
return {
"trades": output,
"trades_count": len(output),
"offset": offset,
"total_trades": Trade.get_trades([Trade.is_open.is_(False)]).count(),
}
@@ -380,7 +439,7 @@ class RPC:
return 'losses'
else:
return 'draws'
trades: List[Trade] = Trade.get_trades([Trade.is_open.is_(False)], include_orders=False)
trades: List[Trade] = Trade.get_trades([Trade.is_open.is_(False)])
# Sell reason
exit_reasons = {}
for trade in trades:
@@ -408,8 +467,7 @@ class RPC:
""" Returns cumulative profit statistics """
trade_filter = ((Trade.is_open.is_(False) & (Trade.close_date >= start_date)) |
Trade.is_open.is_(True))
trades: List[Trade] = Trade.get_trades(
trade_filter, include_orders=False).order_by(Trade.id).all()
trades: List[Trade] = Trade.get_trades(trade_filter).order_by(Trade.id).all()
profit_all_coin = []
profit_all_ratio = []
@@ -418,8 +476,6 @@ class RPC:
durations = []
winning_trades = 0
losing_trades = 0
winning_profit = 0.0
losing_profit = 0.0
for trade in trades:
current_rate: float = 0.0
@@ -435,10 +491,8 @@ class RPC:
profit_closed_ratio.append(profit_ratio)
if trade.close_profit >= 0:
winning_trades += 1
winning_profit += trade.close_profit_abs
else:
losing_trades += 1
losing_profit += trade.close_profit_abs
else:
# Get current rate
try:
@@ -454,7 +508,6 @@ class RPC:
profit_all_ratio.append(profit_ratio)
best_pair = Trade.get_best_pair(start_date)
trading_volume = Trade.get_trading_volume(start_date)
# Prepare data to display
profit_closed_coin_sum = round(sum(profit_closed_coin), 8)
@@ -478,21 +531,6 @@ class RPC:
profit_closed_ratio_fromstart = profit_closed_coin_sum / starting_balance
profit_all_ratio_fromstart = profit_all_coin_sum / starting_balance
profit_factor = winning_profit / abs(losing_profit) if losing_profit else float('inf')
trades_df = DataFrame([{'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT),
'profit_abs': trade.close_profit_abs}
for trade in trades if not trade.is_open])
max_drawdown_abs = 0.0
max_drawdown = 0.0
if len(trades_df) > 0:
try:
(max_drawdown_abs, _, _, _, _, max_drawdown) = calculate_max_drawdown(
trades_df, value_col='profit_abs', starting_balance=starting_balance)
except ValueError:
# ValueError if no losing trade.
pass
profit_all_fiat = self._fiat_converter.convert_amount(
profit_all_coin_sum,
stake_currency,
@@ -531,15 +569,11 @@ class RPC:
'best_pair_profit_ratio': best_pair[1] if best_pair else 0,
'winning_trades': winning_trades,
'losing_trades': losing_trades,
'profit_factor': profit_factor,
'max_drawdown': max_drawdown,
'max_drawdown_abs': max_drawdown_abs,
'trading_volume': trading_volume,
}
def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict:
""" Returns current account balance per crypto """
currencies: List[Dict] = []
currencies = []
total = 0.0
try:
tickers = self._freqtrade.exchange.get_tickers(cached=True)
@@ -574,12 +608,13 @@ class RPC:
except (ExchangeError):
logger.warning(f" Could not get rate for pair {coin}.")
continue
total = total + est_stake
total = total + (est_stake or 0)
currencies.append({
'currency': coin,
'free': balance.free,
'balance': balance.total,
'used': balance.used,
# TODO: The below can be simplified if we don't assign None to values.
'free': balance.free if balance.free is not None else 0,
'balance': balance.total if balance.total is not None else 0,
'used': balance.used if balance.used is not None else 0,
'est_stake': est_stake or 0,
'stake': stake_currency,
'side': 'long',
@@ -609,6 +644,7 @@ class RPC:
total, stake_currency, fiat_display_currency) if self._fiat_converter else 0
trade_count = len(Trade.get_trades_proxy())
starting_capital_ratio = 0.0
starting_capital_ratio = (total / starting_capital) - 1 if starting_capital else 0.0
starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0
@@ -896,7 +932,7 @@ class RPC:
else:
errors[pair] = {
'error_msg': f"Pair {pair} is not in the current blacklist."
}
}
resp = self._rpc_blacklist()
resp['errors'] = errors
return resp

View File

@@ -27,12 +27,6 @@ class RPCManager:
from freqtrade.rpc.telegram import Telegram
self.registered_modules.append(Telegram(self._rpc, config))
# Enable discord
if config.get('discord', {}).get('enabled', False):
logger.info('Enabling rpc.discord ...')
from freqtrade.rpc.discord import Discord
self.registered_modules.append(Discord(self._rpc, config))
# Enable Webhook
if config.get('webhook', {}).get('enabled', False):
logger.info('Enabling rpc.webhook ...')

View File

@@ -6,7 +6,6 @@ This module manage Telegram communication
import json
import logging
import re
from dataclasses import dataclass
from datetime import date, datetime, timedelta
from functools import partial
from html import escape
@@ -38,15 +37,6 @@ logger.debug('Included module rpc.telegram ...')
MAX_TELEGRAM_MESSAGE_LENGTH = 4096
@dataclass
class TimeunitMappings:
header: str
message: str
message2: str
callback: str
default: int
def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
"""
Decorator to check if the message comes from the correct chat_id
@@ -235,14 +225,6 @@ class Telegram(RPCHandler):
# This can take up to `timeout` from the call to `start_polling`.
self._updater.stop()
def _exchange_from_msg(self, msg: Dict[str, Any]) -> str:
"""
Extracts the exchange name from the given message.
:param msg: The message to extract the exchange name from.
:return: The exchange name.
"""
return f"{msg['exchange']}{' (dry)' if self._config['dry_run'] else ''}"
def _format_entry_msg(self, msg: Dict[str, Any]) -> str:
if self._rpc._fiat_converter:
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
@@ -255,7 +237,7 @@ class Telegram(RPCHandler):
entry_side = ({'enter': 'Long', 'entered': 'Longed'} if msg['direction'] == 'Long'
else {'enter': 'Short', 'entered': 'Shorted'})
message = (
f"{emoji} *{self._exchange_from_msg(msg)}:*"
f"{emoji} *{msg['exchange']}:*"
f" {entry_side['entered'] if is_fill else entry_side['enter']} {msg['pair']}"
f" (#{msg['trade_id']})\n"
)
@@ -304,7 +286,7 @@ class Telegram(RPCHandler):
msg['profit_extra'] = ''
is_fill = msg['type'] == RPCMessageType.EXIT_FILL
message = (
f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* "
f"{msg['emoji']} *{msg['exchange']}:* "
f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n"
f"*{'Profit' if is_fill else 'Unrealized Profit'}:* "
f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
@@ -334,33 +316,33 @@ class Telegram(RPCHandler):
elif msg_type in (RPCMessageType.ENTRY_CANCEL, RPCMessageType.EXIT_CANCEL):
msg['message_side'] = 'enter' if msg_type in [RPCMessageType.ENTRY_CANCEL] else 'exit'
message = (f"\N{WARNING SIGN} *{self._exchange_from_msg(msg)}:* "
f"Cancelling {msg['message_side']} Order for {msg['pair']} "
f"(#{msg['trade_id']}). Reason: {msg['reason']}.")
message = ("\N{WARNING SIGN} *{exchange}:* "
"Cancelling {message_side} Order for {pair} (#{trade_id}). "
"Reason: {reason}.".format(**msg))
elif msg_type == RPCMessageType.PROTECTION_TRIGGER:
message = (
f"*Protection* triggered due to {msg['reason']}. "
f"`{msg['pair']}` will be locked until `{msg['lock_end_time']}`."
)
"*Protection* triggered due to {reason}. "
"`{pair}` will be locked until `{lock_end_time}`."
).format(**msg)
elif msg_type == RPCMessageType.PROTECTION_TRIGGER_GLOBAL:
message = (
f"*Protection* triggered due to {msg['reason']}. "
f"*All pairs* will be locked until `{msg['lock_end_time']}`."
)
"*Protection* triggered due to {reason}. "
"*All pairs* will be locked until `{lock_end_time}`."
).format(**msg)
elif msg_type == RPCMessageType.STATUS:
message = f"*Status:* `{msg['status']}`"
message = '*Status:* `{status}`'.format(**msg)
elif msg_type == RPCMessageType.WARNING:
message = f"\N{WARNING SIGN} *Warning:* `{msg['status']}`"
message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg)
elif msg_type == RPCMessageType.STARTUP:
message = f"{msg['status']}"
message = '{status}'.format(**msg)
else:
raise NotImplementedError(f"Unknown message type: {msg_type}")
raise NotImplementedError('Unknown message type: {}'.format(msg_type))
return message
def send_msg(self, msg: Dict[str, Any]) -> None:
@@ -414,7 +396,7 @@ class Telegram(RPCHandler):
first_avg = filled_orders[0]["safe_price"]
for x, order in enumerate(filled_orders):
if not order['ft_is_entry'] or order['is_open'] is True:
if not order['ft_is_entry']:
continue
cur_entry_datetime = arrow.get(order["order_filled_date"])
cur_entry_amount = order["amount"]
@@ -581,60 +563,6 @@ class Telegram(RPCHandler):
except RPCException as e:
self._send_msg(str(e))
@authorized_only
def _timeunit_stats(self, update: Update, context: CallbackContext, unit: str) -> None:
"""
Handler for /daily <n>
Returns a daily profit (in BTC) over the last n days.
:param bot: telegram bot
:param update: message update
:return: None
"""
vals = {
'days': TimeunitMappings('Day', 'Daily', 'days', 'update_daily', 7),
'weeks': TimeunitMappings('Monday', 'Weekly', 'weeks (starting from Monday)',
'update_weekly', 8),
'months': TimeunitMappings('Month', 'Monthly', 'months', 'update_monthly', 6),
}
val = vals[unit]
stake_cur = self._config['stake_currency']
fiat_disp_cur = self._config.get('fiat_display_currency', '')
try:
timescale = int(context.args[0]) if context.args else val.default
except (TypeError, ValueError, IndexError):
timescale = val.default
try:
stats = self._rpc._rpc_timeunit_profit(
timescale,
stake_cur,
fiat_disp_cur,
unit
)
stats_tab = tabulate(
[[f"{period['date']} ({period['trade_count']})",
f"{round_coin_value(period['abs_profit'], stats['stake_currency'])}",
f"{period['fiat_value']:.2f} {stats['fiat_display_currency']}",
f"{period['rel_profit']:.2%}",
] for period in stats['data']],
headers=[
f"{val.header} (count)",
f'{stake_cur}',
f'{fiat_disp_cur}',
'Profit %',
'Trades',
],
tablefmt='simple')
message = (
f'<b>{val.message} Profit over the last {timescale} {val.message2}</b>:\n'
f'<pre>{stats_tab}</pre>'
)
self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
callback_path=val.callback, query=update.callback_query)
except RPCException as e:
self._send_msg(str(e))
@authorized_only
def _daily(self, update: Update, context: CallbackContext) -> None:
"""
@@ -644,7 +572,35 @@ class Telegram(RPCHandler):
:param update: message update
:return: None
"""
self._timeunit_stats(update, context, 'days')
stake_cur = self._config['stake_currency']
fiat_disp_cur = self._config.get('fiat_display_currency', '')
try:
timescale = int(context.args[0]) if context.args else 7
except (TypeError, ValueError, IndexError):
timescale = 7
try:
stats = self._rpc._rpc_daily_profit(
timescale,
stake_cur,
fiat_disp_cur
)
stats_tab = tabulate(
[[day['date'],
f"{round_coin_value(day['abs_profit'], stats['stake_currency'])}",
f"{day['fiat_value']:.3f} {stats['fiat_display_currency']}",
f"{day['trade_count']} trades"] for day in stats['data']],
headers=[
'Day',
f'Profit {stake_cur}',
f'Profit {fiat_disp_cur}',
'Trades',
],
tablefmt='simple')
message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats_tab}</pre>'
self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
callback_path="update_daily", query=update.callback_query)
except RPCException as e:
self._send_msg(str(e))
@authorized_only
def _weekly(self, update: Update, context: CallbackContext) -> None:
@@ -655,7 +611,36 @@ class Telegram(RPCHandler):
:param update: message update
:return: None
"""
self._timeunit_stats(update, context, 'weeks')
stake_cur = self._config['stake_currency']
fiat_disp_cur = self._config.get('fiat_display_currency', '')
try:
timescale = int(context.args[0]) if context.args else 8
except (TypeError, ValueError, IndexError):
timescale = 8
try:
stats = self._rpc._rpc_weekly_profit(
timescale,
stake_cur,
fiat_disp_cur
)
stats_tab = tabulate(
[[week['date'],
f"{round_coin_value(week['abs_profit'], stats['stake_currency'])}",
f"{week['fiat_value']:.3f} {stats['fiat_display_currency']}",
f"{week['trade_count']} trades"] for week in stats['data']],
headers=[
'Monday',
f'Profit {stake_cur}',
f'Profit {fiat_disp_cur}',
'Trades',
],
tablefmt='simple')
message = f'<b>Weekly Profit over the last {timescale} weeks ' \
f'(starting from Monday)</b>:\n<pre>{stats_tab}</pre> '
self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
callback_path="update_weekly", query=update.callback_query)
except RPCException as e:
self._send_msg(str(e))
@authorized_only
def _monthly(self, update: Update, context: CallbackContext) -> None:
@@ -666,7 +651,36 @@ class Telegram(RPCHandler):
:param update: message update
:return: None
"""
self._timeunit_stats(update, context, 'months')
stake_cur = self._config['stake_currency']
fiat_disp_cur = self._config.get('fiat_display_currency', '')
try:
timescale = int(context.args[0]) if context.args else 6
except (TypeError, ValueError, IndexError):
timescale = 6
try:
stats = self._rpc._rpc_monthly_profit(
timescale,
stake_cur,
fiat_disp_cur
)
stats_tab = tabulate(
[[month['date'],
f"{round_coin_value(month['abs_profit'], stats['stake_currency'])}",
f"{month['fiat_value']:.3f} {stats['fiat_display_currency']}",
f"{month['trade_count']} trades"] for month in stats['data']],
headers=[
'Month',
f'Profit {stake_cur}',
f'Profit {fiat_disp_cur}',
'Trades',
],
tablefmt='simple')
message = f'<b>Monthly Profit over the last {timescale} months' \
f'</b>:\n<pre>{stats_tab}</pre> '
self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
callback_path="update_monthly", query=update.callback_query)
except RPCException as e:
self._send_msg(str(e))
@authorized_only
def _profit(self, update: Update, context: CallbackContext) -> None:
@@ -730,18 +744,12 @@ class Telegram(RPCHandler):
f"*Total Trade Count:* `{trade_count}`\n"
f"*{'First Trade opened' if not timescale else 'Showing Profit since'}:* "
f"`{first_trade_date}`\n"
f"*Latest Trade opened:* `{latest_trade_date}`\n"
f"*Latest Trade opened:* `{latest_trade_date}\n`"
f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`"
)
if stats['closed_trade_count'] > 0:
markdown_msg += (
f"\n*Avg. Duration:* `{avg_duration}`\n"
f"*Best Performing:* `{best_pair}: {best_pair_profit_ratio:.2%}`\n"
f"*Trading volume:* `{round_coin_value(stats['trading_volume'], stake_cur)}`\n"
f"*Profit factor:* `{stats['profit_factor']:.2f}`\n"
f"*Max Drawdown:* `{stats['max_drawdown']:.2%} "
f"({round_coin_value(stats['max_drawdown_abs'], stake_cur)})`"
)
markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n"
f"*Best Performing:* `{best_pair}: {best_pair_profit_ratio:.2%}`")
self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit",
query=update.callback_query)
@@ -881,7 +889,7 @@ class Telegram(RPCHandler):
:return: None
"""
msg = self._rpc._rpc_start()
self._send_msg(f"Status: `{msg['status']}`")
self._send_msg('Status: `{status}`'.format(**msg))
@authorized_only
def _stop(self, update: Update, context: CallbackContext) -> None:
@@ -893,7 +901,7 @@ class Telegram(RPCHandler):
:return: None
"""
msg = self._rpc._rpc_stop()
self._send_msg(f"Status: `{msg['status']}`")
self._send_msg('Status: `{status}`'.format(**msg))
@authorized_only
def _reload_config(self, update: Update, context: CallbackContext) -> None:
@@ -905,7 +913,7 @@ class Telegram(RPCHandler):
:return: None
"""
msg = self._rpc._rpc_reload_config()
self._send_msg(f"Status: `{msg['status']}`")
self._send_msg('Status: `{status}`'.format(**msg))
@authorized_only
def _stopbuy(self, update: Update, context: CallbackContext) -> None:
@@ -917,7 +925,7 @@ class Telegram(RPCHandler):
:return: None
"""
msg = self._rpc._rpc_stopbuy()
self._send_msg(f"Status: `{msg['status']}`")
self._send_msg('Status: `{status}`'.format(**msg))
@authorized_only
def _force_exit(self, update: Update, context: CallbackContext) -> None:
@@ -1079,9 +1087,9 @@ class Telegram(RPCHandler):
trade_id = int(context.args[0])
msg = self._rpc._rpc_delete(trade_id)
self._send_msg((
f"`{msg['result_msg']}`\n"
'`{result_msg}`\n'
'Please make sure to take care of this asset on the exchange manually.'
))
).format(**msg))
except RPCException as e:
self._send_msg(str(e))
@@ -1409,7 +1417,7 @@ class Telegram(RPCHandler):
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
"*/forceexit <trade_id>|all:* `Instantly exits the given trade or all trades, "
"regardless of profit`\n"
"*/fx <trade_id>|all:* `Alias to /forceexit`\n"
"*/fe <trade_id>|all:* `Alias to /forceexit`\n"
f"{force_enter_text if self._config.get('force_entry_enable', False) else ''}"
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
"*/whitelist:* `Show current whitelist` \n"

View File

@@ -289,7 +289,6 @@ class IStrategy(ABC, HyperStrategyMixin):
:param order_type: Order type (as configured in order_types). usually limit or market.
:param amount: Amount in target (base) currency that's going to be traded.
:param rate: Rate that's going to be used when using limit orders
or current rate for market orders.
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param current_time: datetime object, containing the current datetime
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
@@ -317,7 +316,6 @@ class IStrategy(ABC, HyperStrategyMixin):
:param order_type: Order type (as configured in order_types). usually limit or market.
:param amount: Amount in base currency.
:param rate: Rate that's going to be used when using limit orders
or current rate for market orders.
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param exit_reason: Exit reason.
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
@@ -511,8 +509,8 @@ class IStrategy(ABC, HyperStrategyMixin):
return current_order_rate
def leverage(self, pair: str, current_time: datetime, current_rate: float,
proposed_leverage: float, max_leverage: float, entry_tag: Optional[str],
side: str, **kwargs) -> float:
proposed_leverage: float, max_leverage: float, side: str,
**kwargs) -> float:
"""
Customize leverage for each new trade. This method is only called in futures mode.
@@ -521,7 +519,6 @@ class IStrategy(ABC, HyperStrategyMixin):
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
:param proposed_leverage: A leverage proposed by the bot.
:param max_leverage: Max leverage allowed on this pair
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param side: 'long' or 'short' - indicating the direction of the proposed trade
:return: A leverage amount, which is between 1.0 and max_leverage.
"""

View File

@@ -161,7 +161,6 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f
:param order_type: Order type (as configured in order_types). usually limit or market.
:param amount: Amount in target (base) currency that's going to be traded.
:param rate: Rate that's going to be used when using limit orders
or current rate for market orders.
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param current_time: datetime object, containing the current datetime
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
@@ -189,7 +188,6 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount:
:param order_type: Order type (as configured in order_types). usually limit or market.
:param amount: Amount in base currency.
:param rate: Rate that's going to be used when using limit orders
or current rate for market orders.
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param exit_reason: Exit reason.
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
@@ -269,8 +267,8 @@ def adjust_trade_position(self, trade: 'Trade', current_time: 'datetime',
return None
def leverage(self, pair: str, current_time: datetime, current_rate: float,
proposed_leverage: float, max_leverage: float, entry_tag: Optional[str],
side: str, **kwargs) -> float:
proposed_leverage: float, max_leverage: float, side: str,
**kwargs) -> float:
"""
Customize leverage for each new trade. This method is only called in futures mode.
@@ -279,7 +277,6 @@ def leverage(self, pair: str, current_time: datetime, current_rate: float,
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
:param proposed_leverage: A leverage proposed by the bot.
:param max_leverage: Max leverage allowed on this pair
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param side: 'long' or 'short' - indicating the direction of the proposed trade
:return: A leverage amount, which is between 1.0 and max_leverage.
"""

View File

@@ -7,7 +7,7 @@
coveralls==3.3.1
flake8==4.0.1
flake8-tidy-imports==4.8.0
mypy==0.961
mypy==0.960
pre-commit==2.19.0
pytest==7.1.2
pytest-asyncio==0.18.3
@@ -22,8 +22,8 @@ time-machine==2.7.0
nbconvert==6.5.0
# mypy types
types-cachetools==5.0.2
types-filelock==3.2.7
types-requests==2.27.30
types-cachetools==5.0.1
types-filelock==3.2.6
types-requests==2.27.29
types-tabulate==0.8.9
types-python-dateutil==2.8.17

View File

@@ -5,5 +5,5 @@
scipy==1.8.1
scikit-learn==1.1.1
scikit-optimize==0.9.0
filelock==3.7.1
filelock==3.7.0
progressbar2==4.0.0

View File

@@ -1,4 +1,4 @@
# Include all requirements to run the bot.
-r requirements.txt
plotly==5.8.2
plotly==5.8.0

View File

@@ -1,18 +1,18 @@
numpy==1.23.0
numpy==1.22.4
pandas==1.4.2
pandas-ta==0.3.14b
ccxt==1.88.15
ccxt==1.84.39
# Pin cryptography for now due to rust build errors with piwheels
cryptography==37.0.2
aiohttp==3.8.1
SQLAlchemy==1.4.37
SQLAlchemy==1.4.36
python-telegram-bot==13.12
arrow==1.2.2
cachetools==4.2.2
requests==2.28.0
requests==2.27.1
urllib3==1.26.9
jsonschema==4.6.0
jsonschema==4.5.1
TA-Lib==0.4.24
technical==1.3.0
tabulate==0.8.9
@@ -28,7 +28,7 @@ py_find_1st==1.1.5
# Load ticker files 30% faster
python-rapidjson==1.6
# Properly format api responses
orjson==3.7.2
orjson==3.6.8
# Notify systemd
sdnotify==0.3.2
@@ -41,7 +41,7 @@ aiofiles==0.8.0
psutil==5.9.1
# Support for colorized terminal output
colorama==0.4.5
colorama==0.4.4
# Building config files interactively
questionary==1.10.0
prompt-toolkit==3.0.29

View File

@@ -261,7 +261,7 @@ class FtRestClient():
}
return self._post("forcebuy", data=data)
def forceenter(self, pair, side, price=None):
def force_enter(self, pair, side, price=None):
"""Force entering a trade
:param pair: Pair to buy (ETH/BTC)
@@ -273,7 +273,7 @@ class FtRestClient():
"side": side,
"price": price,
}
return self._post("forceenter", data=data)
return self._post("force_enter", data=data)
def forceexit(self, tradeid):
"""Force-exit a trade.

View File

@@ -87,10 +87,6 @@ function updateenv() {
echo "Failed installing Freqtrade"
exit 1
fi
echo "Installing freqUI"
freqtrade install-ui
echo "pip install completed"
echo
if [[ $dev =~ ^[Yy]$ ]]; then

View File

@@ -78,21 +78,9 @@ def get_args(args):
# Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines
# TODO: This should be replaced with AsyncMock once support for python 3.7 is dropped.
def get_mock_coro(return_value=None, side_effect=None):
def get_mock_coro(return_value):
async def mock_coro(*args, **kwargs):
if side_effect:
if isinstance(side_effect, list):
effect = side_effect.pop(0)
else:
effect = side_effect
if isinstance(effect, Exception):
raise effect
if callable(effect):
return effect(*args, **kwargs)
return effect
else:
return return_value
return return_value
return Mock(wraps=mock_coro)
@@ -337,7 +325,7 @@ def create_mock_trades_with_leverage(fee, use_db: bool = True):
Trade.query.session.flush()
def create_mock_trades_usdt(fee, is_short: Optional[bool] = False, use_db: bool = True):
def create_mock_trades_usdt(fee, use_db: bool = True):
"""
Create some fake trades ...
"""
@@ -347,29 +335,26 @@ def create_mock_trades_usdt(fee, is_short: Optional[bool] = False, use_db: bool
else:
LocalTrade.add_bt_trade(trade)
is_short1 = is_short if is_short is not None else True
is_short2 = is_short if is_short is not None else False
# Simulate dry_run entries
trade = mock_trade_usdt_1(fee, is_short1)
trade = mock_trade_usdt_1(fee)
add_trade(trade)
trade = mock_trade_usdt_2(fee, is_short1)
trade = mock_trade_usdt_2(fee)
add_trade(trade)
trade = mock_trade_usdt_3(fee, is_short1)
trade = mock_trade_usdt_3(fee)
add_trade(trade)
trade = mock_trade_usdt_4(fee, is_short2)
trade = mock_trade_usdt_4(fee)
add_trade(trade)
trade = mock_trade_usdt_5(fee, is_short2)
trade = mock_trade_usdt_5(fee)
add_trade(trade)
trade = mock_trade_usdt_6(fee, is_short1)
trade = mock_trade_usdt_6(fee)
add_trade(trade)
trade = mock_trade_usdt_7(fee, is_short1)
trade = mock_trade_usdt_7(fee)
add_trade(trade)
if use_db:
Trade.commit()

View File

@@ -29,7 +29,6 @@ def mock_order_1(is_short: bool):
'average': 0.123,
'amount': 123.0,
'filled': 123.0,
'cost': 15.129,
'remaining': 0.0,
}
@@ -66,7 +65,6 @@ def mock_order_2(is_short: bool):
'price': 0.123,
'amount': 123.0,
'filled': 123.0,
'cost': 15.129,
'remaining': 0.0,
}
@@ -81,7 +79,6 @@ def mock_order_2_sell(is_short: bool):
'price': 0.128,
'amount': 123.0,
'filled': 123.0,
'cost': 15.129,
'remaining': 0.0,
}
@@ -129,7 +126,6 @@ def mock_order_3(is_short: bool):
'price': 0.05,
'amount': 123.0,
'filled': 123.0,
'cost': 15.129,
'remaining': 0.0,
}
@@ -145,7 +141,6 @@ def mock_order_3_sell(is_short: bool):
'average': 0.06,
'amount': 123.0,
'filled': 123.0,
'cost': 15.129,
'remaining': 0.0,
}
@@ -191,7 +186,6 @@ def mock_order_4(is_short: bool):
'price': 0.123,
'amount': 123.0,
'filled': 0.0,
'cost': 15.129,
'remaining': 123.0,
}
@@ -231,7 +225,6 @@ def mock_order_5(is_short: bool):
'price': 0.123,
'amount': 123.0,
'filled': 123.0,
'cost': 15.129,
'remaining': 0.0,
}
@@ -246,7 +239,6 @@ def mock_order_5_stoploss(is_short: bool):
'price': 0.123,
'amount': 123.0,
'filled': 0.0,
'cost': 0.0,
'remaining': 123.0,
}
@@ -289,7 +281,6 @@ def mock_order_6(is_short: bool):
'price': 0.15,
'amount': 2.0,
'filled': 2.0,
'cost': 0.3,
'remaining': 0.0,
}
@@ -304,7 +295,6 @@ def mock_order_6_sell(is_short: bool):
'price': 0.15 if is_short else 0.20,
'amount': 2.0,
'filled': 0.0,
'cost': 0.0,
'remaining': 2.0,
}
@@ -347,7 +337,6 @@ def short_order():
'price': 0.123,
'amount': 123.0,
'filled': 123.0,
'cost': 15.129,
'remaining': 0.0,
}
@@ -362,7 +351,6 @@ def exit_short_order():
'price': 0.128,
'amount': 123.0,
'filled': 123.0,
'cost': 15.744,
'remaining': 0.0,
}
@@ -436,7 +424,6 @@ def leverage_order():
'amount': 123.0,
'filled': 123.0,
'remaining': 0.0,
'cost': 15.129,
'leverage': 5.0
}
@@ -452,7 +439,6 @@ def leverage_order_sell():
'amount': 123.0,
'filled': 123.0,
'remaining': 0.0,
'cost': 15.744,
'leverage': 5.0
}

View File

@@ -6,84 +6,47 @@ from freqtrade.persistence.models import Order, Trade
MOCK_TRADE_COUNT = 6
def entry_side(is_short: bool):
return "sell" if is_short else "buy"
def exit_side(is_short: bool):
return "buy" if is_short else "sell"
def direc(is_short: bool):
return "short" if is_short else "long"
def mock_order_usdt_1(is_short: bool):
def mock_order_usdt_1():
return {
'id': f'prod_entry_1_{direc(is_short)}',
'symbol': 'LTC/USDT',
'id': '1234',
'symbol': 'ADA/USDT',
'status': 'closed',
'side': entry_side(is_short),
'side': 'buy',
'type': 'limit',
'price': 10.0,
'amount': 2.0,
'filled': 2.0,
'price': 2.0,
'amount': 10.0,
'filled': 10.0,
'remaining': 0.0,
}
def mock_order_usdt_1_exit(is_short: bool):
return {
'id': f'prod_exit_1_{direc(is_short)}',
'symbol': 'LTC/USDT',
'status': 'closed',
'side': exit_side(is_short),
'type': 'limit',
'price': 8.0,
'amount': 2.0,
'filled': 2.0,
'remaining': 0.0,
}
def mock_trade_usdt_1(fee, is_short: bool):
"""
Simulate prod entry with open sell order
"""
def mock_trade_usdt_1(fee):
trade = Trade(
pair='LTC/USDT',
pair='ADA/USDT',
stake_amount=20.0,
amount=2.0,
amount_requested=2.0,
open_date=datetime.now(tz=timezone.utc) - timedelta(days=2, minutes=20),
close_date=datetime.now(tz=timezone.utc) - timedelta(days=2, minutes=5),
amount=10.0,
amount_requested=10.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
is_open=False,
open_rate=10.0,
close_rate=8.0,
close_profit=-0.2,
close_profit_abs=-4.0,
is_open=True,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17),
open_rate=2.0,
exchange='binance',
strategy='SampleStrategy',
open_order_id=f'prod_exit_1_{direc(is_short)}',
open_order_id='dry_run_buy_12345',
strategy='StrategyTestV2',
timeframe=5,
is_short=is_short,
)
o = Order.parse_from_ccxt_object(mock_order_usdt_1(is_short), 'LTC/USDT', entry_side(is_short))
trade.orders.append(o)
o = Order.parse_from_ccxt_object(mock_order_usdt_1_exit(is_short),
'LTC/USDT', exit_side(is_short))
o = Order.parse_from_ccxt_object(mock_order_usdt_1(), 'ADA/USDT', 'buy')
trade.orders.append(o)
return trade
def mock_order_usdt_2(is_short: bool):
def mock_order_usdt_2():
return {
'id': f'1235_{direc(is_short)}',
'id': '1235',
'symbol': 'ETC/USDT',
'status': 'closed',
'side': entry_side(is_short),
'side': 'buy',
'type': 'limit',
'price': 2.0,
'amount': 100.0,
@@ -92,12 +55,12 @@ def mock_order_usdt_2(is_short: bool):
}
def mock_order_usdt_2_exit(is_short: bool):
def mock_order_usdt_2_sell():
return {
'id': f'12366_{direc(is_short)}',
'id': '12366',
'symbol': 'ETC/USDT',
'status': 'closed',
'side': exit_side(is_short),
'side': 'sell',
'type': 'limit',
'price': 2.05,
'amount': 100.0,
@@ -106,7 +69,7 @@ def mock_order_usdt_2_exit(is_short: bool):
}
def mock_trade_usdt_2(fee, is_short: bool):
def mock_trade_usdt_2(fee):
"""
Closed trade...
"""
@@ -119,33 +82,30 @@ def mock_trade_usdt_2(fee, is_short: bool):
fee_close=fee.return_value,
open_rate=2.0,
close_rate=2.05,
close_profit=0.05,
close_profit=5.0,
close_profit_abs=3.9875,
exchange='binance',
is_open=False,
open_order_id=f'12366_{direc(is_short)}',
open_order_id='dry_run_sell_12345',
strategy='StrategyTestV2',
timeframe=5,
enter_tag='TEST1',
exit_reason='exit_signal',
exit_reason='sell_signal',
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
is_short=is_short,
)
o = Order.parse_from_ccxt_object(mock_order_usdt_2(is_short), 'ETC/USDT', entry_side(is_short))
o = Order.parse_from_ccxt_object(mock_order_usdt_2(), 'ETC/USDT', 'buy')
trade.orders.append(o)
o = Order.parse_from_ccxt_object(
mock_order_usdt_2_exit(is_short), 'ETC/USDT', exit_side(is_short))
o = Order.parse_from_ccxt_object(mock_order_usdt_2_sell(), 'ETC/USDT', 'sell')
trade.orders.append(o)
return trade
def mock_order_usdt_3(is_short: bool):
def mock_order_usdt_3():
return {
'id': f'41231a12a_{direc(is_short)}',
'id': '41231a12a',
'symbol': 'XRP/USDT',
'status': 'closed',
'side': entry_side(is_short),
'side': 'buy',
'type': 'limit',
'price': 1.0,
'amount': 30.0,
@@ -154,12 +114,12 @@ def mock_order_usdt_3(is_short: bool):
}
def mock_order_usdt_3_exit(is_short: bool):
def mock_order_usdt_3_sell():
return {
'id': f'41231a666a_{direc(is_short)}',
'id': '41231a666a',
'symbol': 'XRP/USDT',
'status': 'closed',
'side': exit_side(is_short),
'side': 'sell',
'type': 'stop_loss_limit',
'price': 1.1,
'average': 1.1,
@@ -169,7 +129,7 @@ def mock_order_usdt_3_exit(is_short: bool):
}
def mock_trade_usdt_3(fee, is_short: bool):
def mock_trade_usdt_3(fee):
"""
Closed trade
"""
@@ -182,32 +142,29 @@ def mock_trade_usdt_3(fee, is_short: bool):
fee_close=fee.return_value,
open_rate=1.0,
close_rate=1.1,
close_profit=0.1,
close_profit=10.0,
close_profit_abs=9.8425,
exchange='binance',
is_open=False,
strategy='StrategyTestV2',
timeframe=5,
enter_tag='TEST3',
exit_reason='roi',
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc),
is_short=is_short,
)
o = Order.parse_from_ccxt_object(mock_order_usdt_3(is_short), 'XRP/USDT', entry_side(is_short))
o = Order.parse_from_ccxt_object(mock_order_usdt_3(), 'XRP/USDT', 'buy')
trade.orders.append(o)
o = Order.parse_from_ccxt_object(mock_order_usdt_3_exit(is_short),
'XRP/USDT', exit_side(is_short))
o = Order.parse_from_ccxt_object(mock_order_usdt_3_sell(), 'XRP/USDT', 'sell')
trade.orders.append(o)
return trade
def mock_order_usdt_4(is_short: bool):
def mock_order_usdt_4():
return {
'id': f'prod_buy_12345_{direc(is_short)}',
'id': 'prod_buy_12345',
'symbol': 'ETC/USDT',
'status': 'open',
'side': entry_side(is_short),
'side': 'buy',
'type': 'limit',
'price': 2.0,
'amount': 10.0,
@@ -216,7 +173,7 @@ def mock_order_usdt_4(is_short: bool):
}
def mock_trade_usdt_4(fee, is_short: bool):
def mock_trade_usdt_4(fee):
"""
Simulate prod entry
"""
@@ -231,22 +188,21 @@ def mock_trade_usdt_4(fee, is_short: bool):
is_open=True,
open_rate=2.0,
exchange='binance',
open_order_id=f'prod_buy_12345_{direc(is_short)}',
open_order_id='prod_buy_12345',
strategy='StrategyTestV2',
timeframe=5,
is_short=is_short,
)
o = Order.parse_from_ccxt_object(mock_order_usdt_4(is_short), 'ETC/USDT', entry_side(is_short))
o = Order.parse_from_ccxt_object(mock_order_usdt_4(), 'ETC/USDT', 'buy')
trade.orders.append(o)
return trade
def mock_order_usdt_5(is_short: bool):
def mock_order_usdt_5():
return {
'id': f'prod_buy_3455_{direc(is_short)}',
'id': 'prod_buy_3455',
'symbol': 'XRP/USDT',
'status': 'closed',
'side': entry_side(is_short),
'side': 'buy',
'type': 'limit',
'price': 2.0,
'amount': 10.0,
@@ -255,12 +211,12 @@ def mock_order_usdt_5(is_short: bool):
}
def mock_order_usdt_5_stoploss(is_short: bool):
def mock_order_usdt_5_stoploss():
return {
'id': f'prod_stoploss_3455_{direc(is_short)}',
'id': 'prod_stoploss_3455',
'symbol': 'XRP/USDT',
'status': 'open',
'side': exit_side(is_short),
'side': 'sell',
'type': 'stop_loss_limit',
'price': 2.0,
'amount': 10.0,
@@ -269,7 +225,7 @@ def mock_order_usdt_5_stoploss(is_short: bool):
}
def mock_trade_usdt_5(fee, is_short: bool):
def mock_trade_usdt_5(fee):
"""
Simulate prod entry with stoploss
"""
@@ -285,23 +241,22 @@ def mock_trade_usdt_5(fee, is_short: bool):
open_rate=2.0,
exchange='binance',
strategy='SampleStrategy',
stoploss_order_id=f'prod_stoploss_3455_{direc(is_short)}',
stoploss_order_id='prod_stoploss_3455',
timeframe=5,
is_short=is_short,
)
o = Order.parse_from_ccxt_object(mock_order_usdt_5(is_short), 'XRP/USDT', entry_side(is_short))
o = Order.parse_from_ccxt_object(mock_order_usdt_5(), 'XRP/USDT', 'buy')
trade.orders.append(o)
o = Order.parse_from_ccxt_object(mock_order_usdt_5_stoploss(is_short), 'XRP/USDT', 'stoploss')
o = Order.parse_from_ccxt_object(mock_order_usdt_5_stoploss(), 'XRP/USDT', 'stoploss')
trade.orders.append(o)
return trade
def mock_order_usdt_6(is_short: bool):
def mock_order_usdt_6():
return {
'id': f'prod_entry_6_{direc(is_short)}',
'id': 'prod_buy_6',
'symbol': 'LTC/USDT',
'status': 'closed',
'side': entry_side(is_short),
'side': 'buy',
'type': 'limit',
'price': 10.0,
'amount': 2.0,
@@ -310,12 +265,12 @@ def mock_order_usdt_6(is_short: bool):
}
def mock_order_usdt_6_exit(is_short: bool):
def mock_order_usdt_6_sell():
return {
'id': f'prod_exit_6_{direc(is_short)}',
'id': 'prod_sell_6',
'symbol': 'LTC/USDT',
'status': 'open',
'side': exit_side(is_short),
'side': 'sell',
'type': 'limit',
'price': 12.0,
'amount': 2.0,
@@ -324,7 +279,7 @@ def mock_order_usdt_6_exit(is_short: bool):
}
def mock_trade_usdt_6(fee, is_short: bool):
def mock_trade_usdt_6(fee):
"""
Simulate prod entry with open sell order
"""
@@ -340,49 +295,69 @@ def mock_trade_usdt_6(fee, is_short: bool):
open_rate=10.0,
exchange='binance',
strategy='SampleStrategy',
open_order_id=f'prod_exit_6_{direc(is_short)}',
open_order_id="prod_sell_6",
timeframe=5,
is_short=is_short,
)
o = Order.parse_from_ccxt_object(mock_order_usdt_6(is_short), 'LTC/USDT', entry_side(is_short))
o = Order.parse_from_ccxt_object(mock_order_usdt_6(), 'LTC/USDT', 'buy')
trade.orders.append(o)
o = Order.parse_from_ccxt_object(mock_order_usdt_6_exit(is_short),
'LTC/USDT', exit_side(is_short))
o = Order.parse_from_ccxt_object(mock_order_usdt_6_sell(), 'LTC/USDT', 'sell')
trade.orders.append(o)
return trade
def mock_order_usdt_7(is_short: bool):
def mock_order_usdt_7():
return {
'id': f'1234_{direc(is_short)}',
'symbol': 'ADA/USDT',
'id': 'prod_buy_7',
'symbol': 'LTC/USDT',
'status': 'closed',
'side': entry_side(is_short),
'side': 'buy',
'type': 'limit',
'price': 2.0,
'amount': 10.0,
'filled': 10.0,
'price': 10.0,
'amount': 2.0,
'filled': 2.0,
'remaining': 0.0,
}
def mock_trade_usdt_7(fee, is_short: bool):
def mock_order_usdt_7_sell():
return {
'id': 'prod_sell_7',
'symbol': 'LTC/USDT',
'status': 'closed',
'side': 'sell',
'type': 'limit',
'price': 8.0,
'amount': 2.0,
'filled': 2.0,
'remaining': 0.0,
}
def mock_trade_usdt_7(fee):
"""
Simulate prod entry with open sell order
"""
trade = Trade(
pair='ADA/USDT',
pair='LTC/USDT',
stake_amount=20.0,
amount=10.0,
amount_requested=10.0,
amount=2.0,
amount_requested=2.0,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=5),
fee_open=fee.return_value,
fee_close=fee.return_value,
is_open=True,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17),
open_rate=2.0,
is_open=False,
open_rate=10.0,
close_rate=8.0,
close_profit=-0.2,
close_profit_abs=-4.0,
exchange='binance',
open_order_id=f'1234_{direc(is_short)}',
strategy='StrategyTestV2',
strategy='SampleStrategy',
open_order_id="prod_sell_6",
timeframe=5,
is_short=is_short,
)
o = Order.parse_from_ccxt_object(mock_order_usdt_7(is_short), 'ADA/USDT', entry_side(is_short))
o = Order.parse_from_ccxt_object(mock_order_usdt_7(), 'LTC/USDT', 'buy')
trade.orders.append(o)
o = Order.parse_from_ccxt_object(mock_order_usdt_7_sell(), 'LTC/USDT', 'sell')
trade.orders.append(o)
return trade

View File

@@ -85,7 +85,7 @@ def test_load_backtest_data_new_format(testdatadir):
filename = testdatadir / "backtest_results/backtest-result_new.json"
bt_data = load_backtest_data(filename)
assert isinstance(bt_data, DataFrame)
assert set(bt_data.columns) == set(BT_DATA_COLUMNS)
assert set(bt_data.columns) == set(BT_DATA_COLUMNS + ['close_timestamp', 'open_timestamp'])
assert len(bt_data) == 179
# Test loading from string (must yield same result)
@@ -110,7 +110,7 @@ def test_load_backtest_data_multi(testdatadir):
bt_data = load_backtest_data(filename, strategy=strategy)
assert isinstance(bt_data, DataFrame)
assert set(bt_data.columns) == set(
BT_DATA_COLUMNS)
BT_DATA_COLUMNS + ['close_timestamp', 'open_timestamp'])
assert len(bt_data) == 179
# Test loading from string (must yield same result)

View File

@@ -1,191 +0,0 @@
import logging
from unittest.mock import MagicMock, PropertyMock
import pandas as pd
import pytest
from freqtrade.commands.analyze_commands import start_analysis_entries_exits
from freqtrade.commands.optimize_commands import start_backtesting
from freqtrade.enums import ExitType
from freqtrade.optimize.backtesting import Backtesting
from tests.conftest import get_args, patch_exchange, patched_configuration_load_config_file
@pytest.fixture(autouse=True)
def entryexitanalysis_cleanup() -> None:
yield None
Backtesting.cleanup()
def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, tmpdir, capsys):
caplog.set_level(logging.INFO)
default_conf.update({
"use_exit_signal": True,
"exit_profit_only": False,
"exit_profit_offset": 0.0,
"ignore_roi_if_entry_signal": False,
})
patch_exchange(mocker)
result1 = pd.DataFrame({'pair': ['ETH/BTC', 'LTC/BTC', 'ETH/BTC', 'LTC/BTC'],
'profit_ratio': [0.025, 0.05, -0.1, -0.05],
'profit_abs': [0.5, 2.0, -4.0, -2.0],
'open_date': pd.to_datetime(['2018-01-29 18:40:00',
'2018-01-30 03:30:00',
'2018-01-30 08:10:00',
'2018-01-31 13:30:00', ], utc=True
),
'close_date': pd.to_datetime(['2018-01-29 20:45:00',
'2018-01-30 05:35:00',
'2018-01-30 09:10:00',
'2018-01-31 15:00:00', ], utc=True),
'trade_duration': [235, 40, 60, 90],
'is_open': [False, False, False, False],
'stake_amount': [0.01, 0.01, 0.01, 0.01],
'open_rate': [0.104445, 0.10302485, 0.10302485, 0.10302485],
'close_rate': [0.104969, 0.103541, 0.102041, 0.102541],
"is_short": [False, False, False, False],
'enter_tag': ["enter_tag_long_a",
"enter_tag_long_b",
"enter_tag_long_a",
"enter_tag_long_b"],
'exit_reason': [ExitType.ROI,
ExitType.EXIT_SIGNAL,
ExitType.STOP_LOSS,
ExitType.TRAILING_STOP_LOSS]
})
backtestmock = MagicMock(side_effect=[
{
'results': result1,
'config': default_conf,
'locks': [],
'rejected_signals': 20,
'timedout_entry_orders': 0,
'timedout_exit_orders': 0,
'canceled_trade_entries': 0,
'canceled_entry_orders': 0,
'replaced_entry_orders': 0,
'final_balance': 1000,
}
])
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
PropertyMock(return_value=['ETH/BTC', 'LTC/BTC', 'DASH/BTC']))
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
patched_configuration_load_config_file(mocker, default_conf)
args = [
'backtesting',
'--config', 'config.json',
'--datadir', str(testdatadir),
'--user-data-dir', str(tmpdir),
'--timeframe', '5m',
'--timerange', '1515560100-1517287800',
'--export', 'signals',
'--cache', 'none',
]
args = get_args(args)
start_backtesting(args)
captured = capsys.readouterr()
assert 'BACKTESTING REPORT' in captured.out
assert 'EXIT REASON STATS' in captured.out
assert 'LEFT OPEN TRADES REPORT' in captured.out
base_args = [
'backtesting-analysis',
'--config', 'config.json',
'--datadir', str(testdatadir),
'--user-data-dir', str(tmpdir),
]
# test group 0 and indicator list
args = get_args(base_args +
['--analysis-groups', "0",
'--indicator-list', "close", "rsi", "profit_abs"]
)
start_analysis_entries_exits(args)
captured = capsys.readouterr()
assert 'LTC/BTC' in captured.out
assert 'ETH/BTC' in captured.out
assert 'enter_tag_long_a' in captured.out
assert 'enter_tag_long_b' in captured.out
assert 'exit_signal' in captured.out
assert 'roi' in captured.out
assert 'stop_loss' in captured.out
assert 'trailing_stop_loss' in captured.out
assert '0.5' in captured.out
assert '-4' in captured.out
assert '-2' in captured.out
assert '-3.5' in captured.out
assert '50' in captured.out
assert '0' in captured.out
assert '0.01616' in captured.out
assert '34.049' in captured.out
assert '0.104104' in captured.out
assert '47.0996' in captured.out
# test group 1
args = get_args(base_args + ['--analysis-groups', "1"])
start_analysis_entries_exits(args)
captured = capsys.readouterr()
assert 'enter_tag_long_a' in captured.out
assert 'enter_tag_long_b' in captured.out
assert 'total_profit_pct' in captured.out
assert '-3.5' in captured.out
assert '-1.75' in captured.out
assert '-7.5' in captured.out
assert '-3.75' in captured.out
assert '0' in captured.out
# test group 2
args = get_args(base_args + ['--analysis-groups', "2"])
start_analysis_entries_exits(args)
captured = capsys.readouterr()
assert 'enter_tag_long_a' in captured.out
assert 'enter_tag_long_b' in captured.out
assert 'exit_signal' in captured.out
assert 'roi' in captured.out
assert 'stop_loss' in captured.out
assert 'trailing_stop_loss' in captured.out
assert 'total_profit_pct' in captured.out
assert '-10' in captured.out
assert '-5' in captured.out
assert '2.5' in captured.out
# test group 3
args = get_args(base_args + ['--analysis-groups', "3"])
start_analysis_entries_exits(args)
captured = capsys.readouterr()
assert 'LTC/BTC' in captured.out
assert 'ETH/BTC' in captured.out
assert 'enter_tag_long_a' in captured.out
assert 'enter_tag_long_b' in captured.out
assert 'total_profit_pct' in captured.out
assert '-7.5' in captured.out
assert '-3.75' in captured.out
assert '-1.75' in captured.out
assert '0' in captured.out
assert '2' in captured.out
# test group 4
args = get_args(base_args + ['--analysis-groups', "4"])
start_analysis_entries_exits(args)
captured = capsys.readouterr()
assert 'LTC/BTC' in captured.out
assert 'ETH/BTC' in captured.out
assert 'enter_tag_long_a' in captured.out
assert 'enter_tag_long_b' in captured.out
assert 'exit_signal' in captured.out
assert 'roi' in captured.out
assert 'stop_loss' in captured.out
assert 'trailing_stop_loss' in captured.out
assert 'total_profit_pct' in captured.out
assert '-10' in captured.out
assert '-5' in captured.out
assert '-4' in captured.out
assert '0.5' in captured.out
assert '1' in captured.out
assert '2.5' in captured.out

View File

@@ -199,13 +199,8 @@ class TestCCXTExchange():
l2 = exchange.fetch_l2_order_book(pair)
assert 'asks' in l2
assert 'bids' in l2
assert len(l2['asks']) >= 1
assert len(l2['bids']) >= 1
l2_limit_range = exchange._ft_has['l2_limit_range']
l2_limit_range_required = exchange._ft_has['l2_limit_range_required']
if exchangename == 'gateio':
# TODO: Gateio is unstable here at the moment, ignoring the limit partially.
return
for val in [1, 2, 5, 25, 100]:
l2 = exchange.fetch_l2_order_book(pair, val)
if not l2_limit_range or val in l2_limit_range:

View File

@@ -33,12 +33,6 @@ def test_validate_order_types_gateio(default_conf, mocker):
match=r'Exchange .* does not support market orders.'):
ExchangeResolver.load_exchange('gateio', default_conf, True)
# market-orders supported on futures markets.
default_conf['trading_mode'] = 'futures'
default_conf['margin_mode'] = 'isolated'
ex = ExchangeResolver.load_exchange('gateio', default_conf, True)
assert ex
@pytest.mark.usefixtures("init_persistence")
def test_fetch_stoploss_order_gateio(default_conf, mocker):

View File

@@ -123,5 +123,5 @@ def test_stoploss_adjust_kucoin(mocker, default_conf):
assert exchange.stoploss_adjust(1501, order, 'sell')
assert not exchange.stoploss_adjust(1499, order, 'sell')
# Test with invalid order case
order['stopPrice'] = None
assert exchange.stoploss_adjust(1501, order, 'sell')
order['info']['stop'] = None
assert not exchange.stoploss_adjust(1501, order, 'sell')

View File

@@ -6,7 +6,7 @@ import pytest
from freqtrade.enums import MarginMode, TradingMode
from freqtrade.enums.candletype import CandleType
from freqtrade.exchange.exchange import timeframe_to_minutes
from tests.conftest import get_mock_coro, get_patched_exchange
from tests.conftest import get_patched_exchange
from tests.exchange.test_exchange import ccxt_exceptionhandlers
@@ -273,7 +273,7 @@ def test_load_leverage_tiers_okx(default_conf, mocker, markets):
'fetchLeverageTiers': False,
'fetchMarketLeverageTiers': True,
})
api_mock.fetch_market_leverage_tiers = get_mock_coro(side_effect=[
api_mock.fetch_market_leverage_tiers = MagicMock(side_effect=[
[
{
'tier': 1,

View File

@@ -7,7 +7,6 @@ import pytest
from freqtrade.data.history import get_timerange
from freqtrade.enums import ExitType
from freqtrade.optimize.backtesting import Backtesting
from freqtrade.persistence.trade_model import LocalTrade
from tests.conftest import patch_exchange
from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe,
_get_frame_time_from_offset, tests_timeframe)
@@ -965,7 +964,5 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer)
assert res.open_date == _get_frame_time_from_offset(trade.open_tick)
assert res.close_date == _get_frame_time_from_offset(trade.close_tick)
assert res.is_short == trade.is_short
assert len(LocalTrade.trades) == len(data.trades)
assert len(LocalTrade.trades_open) == 0
backtesting.cleanup()
del backtesting

View File

@@ -795,27 +795,10 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
'is_open': [False, False],
'enter_tag': [None, None],
"is_short": [False, False],
'open_timestamp': [1517251200000, 1517283000000],
'close_timestamp': [1517265300000, 1517285400000],
'orders': [
[
{'amount': 0.00957442, 'safe_price': 0.104445, 'ft_order_side': 'buy',
'order_filled_timestamp': 1517251200000, 'ft_is_entry': True},
{'amount': 0.00957442, 'safe_price': 0.10496853383458644, 'ft_order_side': 'sell',
'order_filled_timestamp': 1517265300000, 'ft_is_entry': False}
], [
{'amount': 0.0097064, 'safe_price': 0.10302485, 'ft_order_side': 'buy',
'order_filled_timestamp': 1517283000000, 'ft_is_entry': True},
{'amount': 0.0097064, 'safe_price': 0.10354126528822055, 'ft_order_side': 'sell',
'order_filled_timestamp': 1517285400000, 'ft_is_entry': False}
]
]
})
pd.testing.assert_frame_equal(results, expected)
assert 'orders' in results.columns
data_pair = processed[pair]
for _, t in results.iterrows():
assert len(t['orders']) == 2
ln = data_pair.loc[data_pair["date"] == t["open_date"]]
# Check open trade rate alignes to open rate
assert ln is not None

View File

@@ -70,14 +70,9 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) ->
'is_open': [False, False],
'enter_tag': [None, None],
'is_short': [False, False],
'open_timestamp': [1517251200000, 1517283000000],
'close_timestamp': [1517265300000, 1517285400000],
})
pd.testing.assert_frame_equal(results.drop(columns=['orders']), expected)
pd.testing.assert_frame_equal(results, expected)
data_pair = processed[pair]
assert len(results.iloc[0]['orders']) == 6
assert len(results.iloc[1]['orders']) == 2
for _, t in results.iterrows():
ln = data_pair.loc[data_pair["date"] == t["open_date"]]
# Check open trade rate alignes to open rate

View File

@@ -171,7 +171,7 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir):
_backup_file(filename_last, copy_file=True)
assert not filename.is_file()
store_backtest_stats(filename, stats, '2022_01_01_15_05_13')
store_backtest_stats(filename, stats)
# get real Filename (it's btresult-<date>.json)
last_fn = get_latest_backtest_filename(filename_last.parent)
@@ -194,7 +194,7 @@ def test_store_backtest_stats(testdatadir, mocker):
dump_mock = mocker.patch('freqtrade.optimize.optimize_reports.file_dump_json')
store_backtest_stats(testdatadir, {'metadata': {}}, '2022_01_01_15_05_13')
store_backtest_stats(testdatadir, {'metadata': {}})
assert dump_mock.call_count == 3
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
@@ -202,7 +202,7 @@ def test_store_backtest_stats(testdatadir, mocker):
dump_mock.reset_mock()
filename = testdatadir / 'testresult.json'
store_backtest_stats(filename, {'metadata': {}}, '2022_01_01_15_05_13')
store_backtest_stats(filename, {'metadata': {}})
assert dump_mock.call_count == 3
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
# result will be testdatadir / testresult-<timestamp>.json
@@ -216,7 +216,7 @@ def test_store_backtest_candles(testdatadir, mocker):
candle_dict = {'DefStrat': {'UNITTEST/BTC': pd.DataFrame()}}
# mock directory exporting
store_backtest_signal_candles(testdatadir, candle_dict, '2022_01_01_15_05_13')
store_backtest_signal_candles(testdatadir, candle_dict)
assert dump_mock.call_count == 1
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
@@ -225,7 +225,7 @@ def test_store_backtest_candles(testdatadir, mocker):
dump_mock.reset_mock()
# mock file exporting
filename = Path(testdatadir / 'testresult')
store_backtest_signal_candles(filename, candle_dict, '2022_01_01_15_05_13')
store_backtest_signal_candles(filename, candle_dict)
assert dump_mock.call_count == 1
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
# result will be testdatadir / testresult-<timestamp>_signals.pkl
@@ -238,7 +238,7 @@ def test_write_read_backtest_candles(tmpdir):
candle_dict = {'DefStrat': {'UNITTEST/BTC': pd.DataFrame()}}
# test directory exporting
stored_file = store_backtest_signal_candles(Path(tmpdir), candle_dict, '2022_01_01_15_05_13')
stored_file = store_backtest_signal_candles(Path(tmpdir), candle_dict)
scp = open(stored_file, "rb")
pickled_signal_candles = joblib.load(scp)
scp.close()
@@ -252,7 +252,7 @@ def test_write_read_backtest_candles(tmpdir):
# test file exporting
filename = Path(tmpdir / 'testresult')
stored_file = store_backtest_signal_candles(filename, candle_dict, '2022_01_01_15_05_13')
stored_file = store_backtest_signal_candles(filename, candle_dict)
scp = open(stored_file, "rb")
pickled_signal_candles = joblib.load(scp)
scp.close()

View File

@@ -762,8 +762,8 @@ def test_PerformanceFilter_keep_mid_order(mocker, default_conf_usdt, fee, caplog
with time_machine.travel("2021-09-01 05:00:00 +00:00") as t:
create_mock_trades_usdt(fee)
pm.refresh_pairlist()
assert pm.whitelist == ['XRP/USDT', 'ETC/USDT', 'ETH/USDT', 'LTC/USDT',
'NEO/USDT', 'TKN/USDT', 'ADA/USDT', ]
assert pm.whitelist == ['XRP/USDT', 'ETC/USDT', 'ETH/USDT',
'NEO/USDT', 'TKN/USDT', 'ADA/USDT', 'LTC/USDT']
# assert log_has_re(r'Removing pair .* since .* is below .*', caplog)
# Move to "outside" of lookback window, so original sorting is restored.

View File

@@ -11,11 +11,11 @@ from freqtrade.edge import PairInfo
from freqtrade.enums import SignalDirection, State, TradingMode
from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError
from freqtrade.persistence import Trade
from freqtrade.persistence.models import Order
from freqtrade.persistence.pairlock_middleware import PairLocks
from freqtrade.rpc import RPC, RPCException
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
from tests.conftest import (create_mock_trades, create_mock_trades_usdt, get_patched_freqtradebot,
patch_get_signal)
from tests.conftest import create_mock_trades, get_patched_freqtradebot, patch_get_signal
# Functions for recurrent object patching
@@ -284,8 +284,8 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
assert isnan(fiat_profit_sum)
def test__rpc_timeunit_profit(default_conf_usdt, ticker, fee,
limit_buy_order, limit_sell_order, markets, mocker) -> None:
def test_rpc_daily_profit(default_conf, update, ticker, fee,
limit_buy_order, limit_sell_order, markets, mocker) -> None:
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
@@ -294,35 +294,45 @@ def test__rpc_timeunit_profit(default_conf_usdt, ticker, fee,
markets=PropertyMock(return_value=markets)
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt)
create_mock_trades_usdt(fee)
stake_currency = default_conf_usdt['stake_currency']
fiat_display_currency = default_conf_usdt['fiat_display_currency']
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot)
stake_currency = default_conf['stake_currency']
fiat_display_currency = default_conf['fiat_display_currency']
rpc = RPC(freqtradebot)
rpc._fiat_converter = CryptoToFiatConverter()
# Create some test data
freqtradebot.enter_positions()
trade = Trade.query.first()
assert trade
# Simulate buy & sell
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow()
trade.is_open = False
# Try valid data
days = rpc._rpc_timeunit_profit(7, stake_currency, fiat_display_currency)
update.message.text = '/daily 2'
days = rpc._rpc_daily_profit(7, stake_currency, fiat_display_currency)
assert len(days['data']) == 7
assert days['stake_currency'] == default_conf_usdt['stake_currency']
assert days['fiat_display_currency'] == default_conf_usdt['fiat_display_currency']
assert days['stake_currency'] == default_conf['stake_currency']
assert days['fiat_display_currency'] == default_conf['fiat_display_currency']
for day in days['data']:
# {'date': datetime.date(2022, 6, 11), 'abs_profit': 13.8299999,
# 'starting_balance': 1055.37, 'rel_profit': 0.0131044,
# 'fiat_value': 0.0, 'trade_count': 2}
assert day['abs_profit'] in (0.0, pytest.approx(13.8299999), pytest.approx(-4.0))
assert day['rel_profit'] in (0.0, pytest.approx(0.01310441), pytest.approx(-0.00377583))
assert day['trade_count'] in (0, 1, 2)
assert day['starting_balance'] in (pytest.approx(1059.37), pytest.approx(1055.37))
assert day['fiat_value'] in (0.0, )
# [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD']
assert (day['abs_profit'] == 0.0 or
day['abs_profit'] == 0.00006217)
assert (day['fiat_value'] == 0.0 or
day['fiat_value'] == 0.76748865)
# ensure first day is current date
assert str(days['data'][0]['date']) == str(datetime.utcnow().date())
# Try invalid data
with pytest.raises(RPCException, match=r'.*must be an integer greater than 0*'):
rpc._rpc_timeunit_profit(0, stake_currency, fiat_display_currency)
rpc._rpc_daily_profit(0, stake_currency, fiat_display_currency)
@pytest.mark.parametrize('is_short', [True, False])
@@ -406,8 +416,13 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short):
assert stoploss_mock.call_count == 0
def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None:
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1)
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
limit_buy_order, limit_sell_order, mocker) -> None:
mocker.patch.multiple(
'freqtrade.rpc.fiat_convert.CoinGeckoAPI',
get_price=MagicMock(return_value={'bitcoin': {'usd': 15000.0}}),
)
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
@@ -415,9 +430,10 @@ def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None:
get_fee=fee,
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt)
stake_currency = default_conf_usdt['stake_currency']
fiat_display_currency = default_conf_usdt['fiat_display_currency']
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot)
stake_currency = default_conf['stake_currency']
fiat_display_currency = default_conf['fiat_display_currency']
rpc = RPC(freqtradebot)
rpc._fiat_converter = CryptoToFiatConverter()
@@ -430,40 +446,75 @@ def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None:
assert res['latest_trade_timestamp'] == 0
# Create some test data
create_mock_trades_usdt(fee)
freqtradebot.enter_positions()
trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'sell')
trade.update_trade(oobj)
# Update the ticker with a market going up
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker_sell_up
)
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow()
trade.is_open = False
freqtradebot.enter_positions()
trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Update the ticker with a market going up
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker_sell_up
)
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow()
trade.is_open = False
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
assert pytest.approx(stats['profit_closed_coin']) == 9.83
assert pytest.approx(stats['profit_closed_percent_mean']) == -1.67
assert pytest.approx(stats['profit_closed_fiat']) == 10.813
assert pytest.approx(stats['profit_all_coin']) == -77.45964918
assert pytest.approx(stats['profit_all_percent_mean']) == -57.86
assert pytest.approx(stats['profit_all_fiat']) == -85.205614098
assert stats['trade_count'] == 7
assert stats['first_trade_date'] == '2 days ago'
assert stats['latest_trade_date'] == '17 minutes ago'
assert stats['avg_duration'] in ('0:17:40')
assert stats['best_pair'] == 'XRP/USDT'
assert stats['best_rate'] == 10.0
assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05)
assert prec_satoshi(stats['profit_closed_percent_mean'], 6.2)
assert prec_satoshi(stats['profit_closed_fiat'], 0.93255)
assert prec_satoshi(stats['profit_all_coin'], 5.802e-05)
assert prec_satoshi(stats['profit_all_percent_mean'], 2.89)
assert prec_satoshi(stats['profit_all_fiat'], 0.8703)
assert stats['trade_count'] == 2
assert stats['first_trade_date'] == 'just now'
assert stats['latest_trade_date'] == 'just now'
assert stats['avg_duration'] in ('0:00:00', '0:00:01', '0:00:02')
assert stats['best_pair'] == 'ETH/BTC'
assert prec_satoshi(stats['best_rate'], 6.2)
# Test non-available pair
mocker.patch('freqtrade.exchange.Exchange.get_rate',
MagicMock(side_effect=ExchangeError("Pair 'XRP/USDT' not available")))
MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available")))
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
assert stats['trade_count'] == 7
assert stats['first_trade_date'] == '2 days ago'
assert stats['latest_trade_date'] == '17 minutes ago'
assert stats['avg_duration'] in ('0:17:40')
assert stats['best_pair'] == 'XRP/USDT'
assert stats['best_rate'] == 10.0
assert stats['trade_count'] == 2
assert stats['first_trade_date'] == 'just now'
assert stats['latest_trade_date'] == 'just now'
assert stats['avg_duration'] in ('0:00:00', '0:00:01', '0:00:02')
assert stats['best_pair'] == 'ETH/BTC'
assert prec_satoshi(stats['best_rate'], 6.2)
assert isnan(stats['profit_all_coin'])
# Test that rpc_trade_statistics can handle trades that lacks
# trade.open_rate (it is set to None)
def test_rpc_trade_statistics_closed(mocker, default_conf_usdt, ticker, fee):
def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee,
ticker_sell_up, limit_buy_order, limit_sell_order):
mocker.patch.multiple(
'freqtrade.rpc.fiat_convert.CoinGeckoAPI',
get_price=MagicMock(return_value={'bitcoin': {'usd': 15000.0}}),
)
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price',
return_value=1.1)
return_value=15000.0)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
@@ -471,32 +522,46 @@ def test_rpc_trade_statistics_closed(mocker, default_conf_usdt, ticker, fee):
get_fee=fee,
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot)
stake_currency = default_conf_usdt['stake_currency']
fiat_display_currency = default_conf_usdt['fiat_display_currency']
stake_currency = default_conf['stake_currency']
fiat_display_currency = default_conf['fiat_display_currency']
rpc = RPC(freqtradebot)
# Create some test data
create_mock_trades_usdt(fee)
freqtradebot.enter_positions()
trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Update the ticker with a market going up
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker_sell_up,
get_fee=fee
)
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow()
trade.is_open = False
for trade in Trade.query.order_by(Trade.id).all():
trade.open_rate = None
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
assert stats['profit_closed_coin'] == 0
assert stats['profit_closed_percent_mean'] == 0
assert stats['profit_closed_fiat'] == 0
assert stats['profit_all_coin'] == 0
assert stats['profit_all_percent_mean'] == 0
assert stats['profit_all_fiat'] == 0
assert stats['trade_count'] == 7
assert stats['first_trade_date'] == '2 days ago'
assert stats['latest_trade_date'] == '17 minutes ago'
assert prec_satoshi(stats['profit_closed_coin'], 0)
assert prec_satoshi(stats['profit_closed_percent_mean'], 0)
assert prec_satoshi(stats['profit_closed_fiat'], 0)
assert prec_satoshi(stats['profit_all_coin'], 0)
assert prec_satoshi(stats['profit_all_percent_mean'], 0)
assert prec_satoshi(stats['profit_all_fiat'], 0)
assert stats['trade_count'] == 1
assert stats['first_trade_date'] == 'just now'
assert stats['latest_trade_date'] == 'just now'
assert stats['avg_duration'] == '0:00:00'
assert stats['best_pair'] == 'XRP/USDT'
assert stats['best_rate'] == 10.0
assert stats['best_pair'] == 'ETH/BTC'
assert prec_satoshi(stats['best_rate'], 6.2)
def test_rpc_balance_handle_error(default_conf, mocker):
@@ -848,30 +913,8 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
assert cancel_order_mock.call_count == 3
def test_performance_handle(default_conf_usdt, ticker, fee, mocker) -> None:
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_balances=MagicMock(return_value=ticker),
fetch_ticker=ticker,
get_fee=fee,
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt)
patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot)
# Create some test data
create_mock_trades_usdt(fee)
res = rpc._rpc_performance()
assert len(res) == 3
assert res[0]['pair'] == 'XRP/USDT'
assert res[0]['count'] == 1
assert res[0]['profit_pct'] == 10.0
def test_enter_tag_performance_handle(default_conf, ticker, fee, mocker) -> None:
def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
limit_sell_order, mocker) -> None:
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
@@ -885,22 +928,70 @@ def test_enter_tag_performance_handle(default_conf, ticker, fee, mocker) -> None
rpc = RPC(freqtradebot)
# Create some test data
create_mock_trades_usdt(fee)
freqtradebot.enter_positions()
trade = Trade.query.first()
assert trade
# Simulate fulfilled LIMIT_BUY order for trade
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow()
trade.is_open = False
res = rpc._rpc_performance()
assert len(res) == 1
assert res[0]['pair'] == 'ETH/BTC'
assert res[0]['count'] == 1
assert prec_satoshi(res[0]['profit_pct'], 6.2)
def test_enter_tag_performance_handle(default_conf, ticker, limit_buy_order, fee,
limit_sell_order, mocker) -> None:
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_balances=MagicMock(return_value=ticker),
fetch_ticker=ticker,
get_fee=fee,
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot)
# Create some test data
freqtradebot.enter_positions()
trade = Trade.query.first()
assert trade
# Simulate fulfilled LIMIT_BUY order for trade
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow()
trade.is_open = False
res = rpc._rpc_enter_tag_performance(None)
assert len(res) == 3
assert res[0]['enter_tag'] == 'TEST3'
assert len(res) == 1
assert res[0]['enter_tag'] == 'Other'
assert res[0]['count'] == 1
assert res[0]['profit_pct'] == 10.0
assert prec_satoshi(res[0]['profit_pct'], 6.2)
trade.enter_tag = "TEST_TAG"
res = rpc._rpc_enter_tag_performance(None)
assert len(res) == 3
assert res[0]['enter_tag'] == 'TEST3'
assert len(res) == 1
assert res[0]['enter_tag'] == 'TEST_TAG'
assert res[0]['count'] == 1
assert res[0]['profit_pct'] == 10.0
assert prec_satoshi(res[0]['profit_pct'], 6.2)
def test_enter_tag_performance_handle_2(mocker, default_conf, markets, fee):
@@ -932,7 +1023,8 @@ def test_enter_tag_performance_handle_2(mocker, default_conf, markets, fee):
assert prec_satoshi(res[0]['profit_pct'], 0.5)
def test_exit_reason_performance_handle(default_conf_usdt, ticker, fee, mocker) -> None:
def test_exit_reason_performance_handle(default_conf, ticker, limit_buy_order, fee,
limit_sell_order, mocker) -> None:
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
@@ -941,22 +1033,39 @@ def test_exit_reason_performance_handle(default_conf_usdt, ticker, fee, mocker)
get_fee=fee,
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot)
# Create some test data
create_mock_trades_usdt(fee)
freqtradebot.enter_positions()
trade = Trade.query.first()
assert trade
# Simulate fulfilled LIMIT_BUY order for trade
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow()
trade.is_open = False
res = rpc._rpc_exit_reason_performance(None)
assert len(res) == 3
assert res[0]['exit_reason'] == 'roi'
assert len(res) == 1
assert res[0]['exit_reason'] == 'Other'
assert res[0]['count'] == 1
assert res[0]['profit_pct'] == 10.0
assert prec_satoshi(res[0]['profit_pct'], 6.2)
assert res[1]['exit_reason'] == 'exit_signal'
assert res[2]['exit_reason'] == 'Other'
trade.exit_reason = "TEST1"
res = rpc._rpc_exit_reason_performance(None)
assert len(res) == 1
assert res[0]['exit_reason'] == 'TEST1'
assert res[0]['count'] == 1
assert prec_satoshi(res[0]['profit_pct'], 6.2)
def test_exit_reason_performance_handle_2(mocker, default_conf, markets, fee):
@@ -988,7 +1097,8 @@ def test_exit_reason_performance_handle_2(mocker, default_conf, markets, fee):
assert prec_satoshi(res[0]['profit_pct'], 0.5)
def test_mix_tag_performance_handle(default_conf, ticker, fee, mocker) -> None:
def test_mix_tag_performance_handle(default_conf, ticker, limit_buy_order, fee,
limit_sell_order, mocker) -> None:
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
@@ -1002,14 +1112,35 @@ def test_mix_tag_performance_handle(default_conf, ticker, fee, mocker) -> None:
rpc = RPC(freqtradebot)
# Create some test data
create_mock_trades_usdt(fee)
freqtradebot.enter_positions()
trade = Trade.query.first()
assert trade
# Simulate fulfilled LIMIT_BUY order for trade
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow()
trade.is_open = False
res = rpc._rpc_mix_tag_performance(None)
assert len(res) == 3
assert res[0]['mix_tag'] == 'TEST3 roi'
assert len(res) == 1
assert res[0]['mix_tag'] == 'Other Other'
assert res[0]['count'] == 1
assert res[0]['profit_pct'] == 10.0
assert prec_satoshi(res[0]['profit_pct'], 6.2)
trade.enter_tag = "TESTBUY"
trade.exit_reason = "TESTSELL"
res = rpc._rpc_mix_tag_performance(None)
assert len(res) == 1
assert res[0]['mix_tag'] == 'TESTBUY TESTSELL'
assert res[0]['count'] == 1
assert prec_satoshi(res[0]['profit_pct'], 6.2)
def test_mix_tag_performance_handle_2(mocker, default_conf, markets, fee):

View File

@@ -578,10 +578,9 @@ def test_api_trades(botclient, mocker, fee, markets, is_short):
)
rc = client_get(client, f"{BASE_URI}/trades")
assert_response(rc)
assert len(rc.json()) == 4
assert len(rc.json()) == 3
assert rc.json()['trades_count'] == 0
assert rc.json()['total_trades'] == 0
assert rc.json()['offset'] == 0
create_mock_trades(fee, is_short=is_short)
Trade.query.session.flush()
@@ -725,9 +724,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
'profit_closed_fiat': -83.19455985, 'profit_closed_ratio_mean': -0.0075,
'profit_closed_percent_mean': -0.75, 'profit_closed_ratio_sum': -0.015,
'profit_closed_percent_sum': -1.5, 'profit_closed_ratio': -6.739057628404269e-06,
'profit_closed_percent': -0.0, 'winning_trades': 0, 'losing_trades': 2,
'profit_factor': 0.0, 'trading_volume': 91.074,
}
'profit_closed_percent': -0.0, 'winning_trades': 0, 'losing_trades': 2}
),
(
False,
@@ -740,9 +737,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
'profit_closed_fiat': 9.124559849999999, 'profit_closed_ratio_mean': 0.0075,
'profit_closed_percent_mean': 0.75, 'profit_closed_ratio_sum': 0.015,
'profit_closed_percent_sum': 1.5, 'profit_closed_ratio': 7.391275897987988e-07,
'profit_closed_percent': 0.0, 'winning_trades': 2, 'losing_trades': 0,
'profit_factor': None, 'trading_volume': 91.074,
}
'profit_closed_percent': 0.0, 'winning_trades': 2, 'losing_trades': 0}
),
(
None,
@@ -755,9 +750,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
'profit_closed_fiat': -67.02260985, 'profit_closed_ratio_mean': 0.0025,
'profit_closed_percent_mean': 0.25, 'profit_closed_ratio_sum': 0.005,
'profit_closed_percent_sum': 0.5, 'profit_closed_ratio': -5.429078808526421e-06,
'profit_closed_percent': -0.0, 'winning_trades': 1, 'losing_trades': 1,
'profit_factor': 0.02775724835771106, 'trading_volume': 91.074,
}
'profit_closed_percent': -0.0, 'winning_trades': 1, 'losing_trades': 1}
)
])
def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected):
@@ -810,10 +803,6 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected)
'closed_trade_count': 2,
'winning_trades': expected['winning_trades'],
'losing_trades': expected['losing_trades'],
'profit_factor': expected['profit_factor'],
'max_drawdown': ANY,
'max_drawdown_abs': ANY,
'trading_volume': expected['trading_volume'],
}
@@ -863,8 +852,8 @@ def test_api_performance(botclient, fee):
close_rate=0.265441,
)
trade.close_profit = trade.calc_profit_ratio(trade.close_rate)
trade.close_profit_abs = trade.calc_profit(trade.close_rate)
trade.close_profit = trade.calc_profit_ratio()
trade.close_profit_abs = trade.calc_profit()
Trade.query.session.add(trade)
trade = Trade(
@@ -879,8 +868,8 @@ def test_api_performance(botclient, fee):
fee_open=fee.return_value,
close_rate=0.391
)
trade.close_profit = trade.calc_profit_ratio(trade.close_rate)
trade.close_profit_abs = trade.calc_profit(trade.close_rate)
trade.close_profit = trade.calc_profit_ratio()
trade.close_profit_abs = trade.calc_profit()
Trade.query.session.add(trade)
Trade.commit()
@@ -1395,14 +1384,12 @@ def test_api_strategies(botclient):
rc = client_get(client, f"{BASE_URI}/strategies")
assert_response(rc)
assert rc.json() == {'strategies': [
'HyperoptableStrategy',
'InformativeDecoratorTest',
'StrategyTestV2',
'StrategyTestV3',
'StrategyTestV3Analysis',
'StrategyTestV3Futures'
'StrategyTestV3Futures',
]}

View File

@@ -27,9 +27,8 @@ from freqtrade.persistence.models import Order
from freqtrade.rpc import RPC
from freqtrade.rpc.rpc import RPCException
from freqtrade.rpc.telegram import Telegram, authorized_only
from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, create_mock_trades_usdt,
get_patched_freqtradebot, log_has, log_has_re, patch_exchange,
patch_get_signal, patch_whitelist)
from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, get_patched_freqtradebot,
log_has, log_has_re, patch_exchange, patch_get_signal, patch_whitelist)
class DummyCls(Telegram):
@@ -405,10 +404,12 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None:
assert msg_mock.call_count == 1
def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None:
def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
limit_sell_order, mocker) -> None:
default_conf['max_open_trades'] = 1
mocker.patch(
'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
return_value=1.1
return_value=15000.0
)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
@@ -416,12 +417,25 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machi
get_fee=fee,
)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot)
# Move date to within day
time_machine.move_to('2022-06-11 08:00:00+00:00')
# Create some test data
create_mock_trades_usdt(fee)
freqtradebot.enter_positions()
trade = Trade.query.first()
assert trade
# Simulate fulfilled LIMIT_BUY order for trade
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade
oobjs = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobjs)
trade.close_date = datetime.utcnow()
trade.is_open = False
# Try valid data
# /daily 2
@@ -432,11 +446,10 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machi
assert "Daily Profit over the last 2 days</b>:" in msg_mock.call_args_list[0][0][0]
assert 'Day ' in msg_mock.call_args_list[0][0][0]
assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0]
assert ' 13.83 USDT' in msg_mock.call_args_list[0][0][0]
assert ' 15.21 USD' in msg_mock.call_args_list[0][0][0]
assert '(2)' in msg_mock.call_args_list[0][0][0]
assert '(2) 13.83 USDT 15.21 USD 1.31%' in msg_mock.call_args_list[0][0][0]
assert '(0)' in msg_mock.call_args_list[0][0][0]
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
assert str(' 0 trade') in msg_mock.call_args_list[0][0][0]
# Reset msg_mock
msg_mock.reset_mock()
@@ -445,23 +458,32 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machi
assert msg_mock.call_count == 1
assert "Daily Profit over the last 7 days</b>:" in msg_mock.call_args_list[0][0][0]
assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0]
assert str((datetime.utcnow() - timedelta(days=5)).date()) in msg_mock.call_args_list[0][0][0]
assert ' 13.83 USDT' in msg_mock.call_args_list[0][0][0]
assert ' 15.21 USD' in msg_mock.call_args_list[0][0][0]
assert '(2)' in msg_mock.call_args_list[0][0][0]
assert '(1)' in msg_mock.call_args_list[0][0][0]
assert '(0)' in msg_mock.call_args_list[0][0][0]
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
assert str(' 0 trade') in msg_mock.call_args_list[0][0][0]
# Reset msg_mock
msg_mock.reset_mock()
freqtradebot.config['max_open_trades'] = 2
# Add two other trades
n = freqtradebot.enter_positions()
assert n == 2
trades = Trade.query.all()
for trade in trades:
trade.update_trade(oobj)
trade.update_trade(oobjs)
trade.close_date = datetime.utcnow()
trade.is_open = False
# /daily 1
context = MagicMock()
context.args = ["1"]
telegram._daily(update=update, context=context)
assert ' 13.83 USDT' in msg_mock.call_args_list[0][0][0]
assert ' 15.21 USD' in msg_mock.call_args_list[0][0][0]
assert '(2)' in msg_mock.call_args_list[0][0][0]
assert str(' 0.00018651 BTC') in msg_mock.call_args_list[0][0][0]
assert str(' 2.798 USD') in msg_mock.call_args_list[0][0][0]
assert str(' 3 trades') in msg_mock.call_args_list[0][0][0]
def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
@@ -490,14 +512,15 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
context = MagicMock()
context.args = ["today"]
telegram._daily(update=update, context=context)
assert 'Daily Profit over the last 7 days</b>:' in msg_mock.call_args_list[0][0][0]
assert str('Daily Profit over the last 7 days</b>:') in msg_mock.call_args_list[0][0][0]
def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None:
default_conf_usdt['max_open_trades'] = 1
def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee,
limit_sell_order, mocker) -> None:
default_conf['max_open_trades'] = 1
mocker.patch(
'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
return_value=1.1
return_value=15000.0
)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
@@ -505,10 +528,25 @@ def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mach
get_fee=fee,
)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt)
# Move to saturday - so all trades are within that week
time_machine.move_to('2022-06-11')
create_mock_trades_usdt(fee)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot)
# Create some test data
freqtradebot.enter_positions()
trade = Trade.query.first()
assert trade
# Simulate fulfilled LIMIT_BUY order for trade
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade
oobjs = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobjs)
trade.close_date = datetime.utcnow()
trade.is_open = False
# Try valid data
# /weekly 2
@@ -522,10 +560,10 @@ def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mach
today = datetime.utcnow().date()
first_iso_day_of_current_week = today - timedelta(days=today.weekday())
assert str(first_iso_day_of_current_week) in msg_mock.call_args_list[0][0][0]
assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0]
assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0]
assert '(3)' in msg_mock.call_args_list[0][0][0]
assert '(0)' in msg_mock.call_args_list[0][0][0]
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
assert str(' 0 trade') in msg_mock.call_args_list[0][0][0]
# Reset msg_mock
msg_mock.reset_mock()
@@ -535,10 +573,44 @@ def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mach
assert "Weekly Profit over the last 8 weeks (starting from Monday)</b>:" \
in msg_mock.call_args_list[0][0][0]
assert 'Weekly' in msg_mock.call_args_list[0][0][0]
assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0]
assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0]
assert '(3)' in msg_mock.call_args_list[0][0][0]
assert '(0)' in msg_mock.call_args_list[0][0][0]
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
assert str(' 0 trade') in msg_mock.call_args_list[0][0][0]
# Reset msg_mock
msg_mock.reset_mock()
freqtradebot.config['max_open_trades'] = 2
# Add two other trades
n = freqtradebot.enter_positions()
assert n == 2
trades = Trade.query.all()
for trade in trades:
trade.update_trade(oobj)
trade.update_trade(oobjs)
trade.close_date = datetime.utcnow()
trade.is_open = False
# /weekly 1
# By default, the 8 previous weeks are shown
# So the previous modified trade should be excluded from the stats
context = MagicMock()
context.args = ["1"]
telegram._weekly(update=update, context=context)
assert str(' 0.00018651 BTC') in msg_mock.call_args_list[0][0][0]
assert str(' 2.798 USD') in msg_mock.call_args_list[0][0][0]
assert str(' 3 trades') in msg_mock.call_args_list[0][0][0]
def test_weekly_wrong_input(default_conf, update, ticker, mocker) -> None:
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker
)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot)
# Try invalid data
msg_mock.reset_mock()
@@ -557,17 +629,16 @@ def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mach
context = MagicMock()
context.args = ["this week"]
telegram._weekly(update=update, context=context)
assert (
'Weekly Profit over the last 8 weeks (starting from Monday)</b>:'
assert str('Weekly Profit over the last 8 weeks (starting from Monday)</b>:') \
in msg_mock.call_args_list[0][0][0]
)
def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None:
default_conf_usdt['max_open_trades'] = 1
def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee,
limit_sell_order, mocker) -> None:
default_conf['max_open_trades'] = 1
mocker.patch(
'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
return_value=1.1
return_value=15000.0
)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
@@ -575,10 +646,25 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac
get_fee=fee,
)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt)
# Move to day within the month so all mock trades fall into this week.
time_machine.move_to('2022-06-11')
create_mock_trades_usdt(fee)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot)
# Create some test data
freqtradebot.enter_positions()
trade = Trade.query.first()
assert trade
# Simulate fulfilled LIMIT_BUY order for trade
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade
oobjs = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobjs)
trade.close_date = datetime.utcnow()
trade.is_open = False
# Try valid data
# /monthly 2
@@ -591,10 +677,10 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac
today = datetime.utcnow().date()
current_month = f"{today.year}-{today.month:02} "
assert current_month in msg_mock.call_args_list[0][0][0]
assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0]
assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0]
assert '(3)' in msg_mock.call_args_list[0][0][0]
assert '(0)' in msg_mock.call_args_list[0][0][0]
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
assert str(' 0 trade') in msg_mock.call_args_list[0][0][0]
# Reset msg_mock
msg_mock.reset_mock()
@@ -605,13 +691,24 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac
assert 'Monthly Profit over the last 6 months</b>:' in msg_mock.call_args_list[0][0][0]
assert 'Month ' in msg_mock.call_args_list[0][0][0]
assert current_month in msg_mock.call_args_list[0][0][0]
assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0]
assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0]
assert '(3)' in msg_mock.call_args_list[0][0][0]
assert '(0)' in msg_mock.call_args_list[0][0][0]
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
assert str(' 0 trade') in msg_mock.call_args_list[0][0][0]
# Reset msg_mock
msg_mock.reset_mock()
freqtradebot.config['max_open_trades'] = 2
# Add two other trades
n = freqtradebot.enter_positions()
assert n == 2
trades = Trade.query.all()
for trade in trades:
trade.update_trade(oobj)
trade.update_trade(oobjs)
trade.close_date = datetime.utcnow()
trade.is_open = False
# /monthly 12
context = MagicMock()
@@ -619,14 +716,24 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac
telegram._monthly(update=update, context=context)
assert msg_mock.call_count == 1
assert 'Monthly Profit over the last 12 months</b>:' in msg_mock.call_args_list[0][0][0]
assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0]
assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0]
assert '(3)' in msg_mock.call_args_list[0][0][0]
assert str(' 0.00018651 BTC') in msg_mock.call_args_list[0][0][0]
assert str(' 2.798 USD') in msg_mock.call_args_list[0][0][0]
assert str(' 3 trades') in msg_mock.call_args_list[0][0][0]
# The one-digit months should contain a zero, Eg: September 2021 = "2021-09"
# Since we loaded the last 12 months, any month should appear
assert str('-09') in msg_mock.call_args_list[0][0][0]
def test_monthly_wrong_input(default_conf, update, ticker, mocker) -> None:
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker
)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot)
# Try invalid data
msg_mock.reset_mock()
freqtradebot.state = State.RUNNING
@@ -647,16 +754,16 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac
assert str('Monthly Profit over the last 6 months</b>:') in msg_mock.call_args_list[0][0][0]
def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, fee,
limit_sell_order_usdt, mocker) -> None:
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1)
def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
limit_buy_order, limit_sell_order, mocker) -> None:
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker_usdt,
fetch_ticker=ticker,
get_fee=fee,
)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot)
telegram._profit(update=update, context=MagicMock())
@@ -668,6 +775,10 @@ def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, f
freqtradebot.enter_positions()
trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
context = MagicMock()
# Test with invalid 2nd argument (should silently pass)
context.args = ["aaa"]
@@ -675,16 +786,15 @@ def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, f
assert msg_mock.call_count == 1
assert 'No closed trade' in msg_mock.call_args_list[-1][0][0]
assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0]
mocker.patch('freqtrade.wallets.Wallets.get_starting_balance', return_value=1000)
assert ('∙ `0.298 USDT (0.50%) (0.03 \N{GREEK CAPITAL LETTER SIGMA}%)`'
mocker.patch('freqtrade.wallets.Wallets.get_starting_balance', return_value=0.01)
assert ('∙ `-0.000005 BTC (-0.50%) (-0.0 \N{GREEK CAPITAL LETTER SIGMA}%)`'
in msg_mock.call_args_list[-1][0][0])
msg_mock.reset_mock()
# Update the ticker with a market going up
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', ticker_sell_up)
# Simulate fulfilled LIMIT_SELL order for trade
oobj = Order.parse_from_ccxt_object(
limit_sell_order_usdt, limit_sell_order_usdt['symbol'], 'sell')
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.now(timezone.utc)
@@ -695,22 +805,20 @@ def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, f
telegram._profit(update=update, context=context)
assert msg_mock.call_count == 1
assert '*ROI:* Closed trades' in msg_mock.call_args_list[-1][0][0]
assert ('∙ `5.685 USDT (9.45%) (0.57 \N{GREEK CAPITAL LETTER SIGMA}%)`'
assert ('∙ `0.00006217 BTC (6.20%) (0.62 \N{GREEK CAPITAL LETTER SIGMA}%)`'
in msg_mock.call_args_list[-1][0][0])
assert '∙ `6.253 USD`' in msg_mock.call_args_list[-1][0][0]
assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0]
assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0]
assert ('∙ `5.685 USDT (9.45%) (0.57 \N{GREEK CAPITAL LETTER SIGMA}%)`'
assert ('∙ `0.00006217 BTC (6.20%) (0.62 \N{GREEK CAPITAL LETTER SIGMA}%)`'
in msg_mock.call_args_list[-1][0][0])
assert '∙ `6.253 USD`' in msg_mock.call_args_list[-1][0][0]
assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0]
assert '*Best Performing:* `ETH/USDT: 9.45%`' in msg_mock.call_args_list[-1][0][0]
assert '*Max Drawdown:*' in msg_mock.call_args_list[-1][0][0]
assert '*Profit factor:*' in msg_mock.call_args_list[-1][0][0]
assert '*Trading volume:* `60 USDT`' in msg_mock.call_args_list[-1][0][0]
assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0]
@pytest.mark.parametrize('is_short', [True, False])
def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_short) -> None:
def test_telegram_stats(default_conf, update, ticker, ticker_sell_up, fee,
limit_buy_order, limit_sell_order, mocker, is_short) -> None:
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
@@ -1242,43 +1350,71 @@ def test_force_enter_no_pair(default_conf, update, mocker) -> None:
assert fbuy_mock.call_count == 1
def test_telegram_performance_handle(default_conf_usdt, update, ticker, fee, mocker) -> None:
def test_telegram_performance_handle(default_conf, update, ticker, fee,
limit_buy_order, limit_sell_order, mocker) -> None:
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
get_fee=fee,
)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot)
# Create some test data
create_mock_trades_usdt(fee)
freqtradebot.enter_positions()
trade = Trade.query.first()
assert trade
# Simulate fulfilled LIMIT_BUY order for trade
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow()
trade.is_open = False
telegram._performance(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert 'Performance' in msg_mock.call_args_list[0][0][0]
assert '<code>XRP/USDT\t9.842 USDT (10.00%) (1)</code>' in msg_mock.call_args_list[0][0][0]
assert '<code>ETH/BTC\t0.00006217 BTC (6.20%) (1)</code>' in msg_mock.call_args_list[0][0][0]
def test_telegram_entry_tag_performance_handle(
default_conf_usdt, update, ticker, fee, mocker) -> None:
default_conf, update, ticker, fee, limit_buy_order, limit_sell_order, mocker) -> None:
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
get_fee=fee,
)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot)
create_mock_trades_usdt(fee)
# Create some test data
freqtradebot.enter_positions()
trade = Trade.query.first()
assert trade
# Simulate fulfilled LIMIT_BUY order for trade
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
trade.enter_tag = "TESTBUY"
# Simulate fulfilled LIMIT_SELL order for trade
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow()
trade.is_open = False
context = MagicMock()
telegram._enter_tag_performance(update=update, context=context)
assert msg_mock.call_count == 1
assert 'Entry Tag Performance' in msg_mock.call_args_list[0][0][0]
assert '<code>TEST1\t3.987 USDT (5.00%) (1)</code>' in msg_mock.call_args_list[0][0][0]
assert '<code>TESTBUY\t0.00006217 BTC (6.20%) (1)</code>' in msg_mock.call_args_list[0][0][0]
context.args = ['XRP/USDT']
context.args = [trade.pair]
telegram._enter_tag_performance(update=update, context=context)
assert msg_mock.call_count == 2
@@ -1291,24 +1427,37 @@ def test_telegram_entry_tag_performance_handle(
assert "Error" in msg_mock.call_args_list[0][0][0]
def test_telegram_exit_reason_performance_handle(default_conf_usdt, update, ticker, fee,
mocker) -> None:
def test_telegram_exit_reason_performance_handle(default_conf, update, ticker, fee,
limit_buy_order, limit_sell_order, mocker) -> None:
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
get_fee=fee,
)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot)
create_mock_trades_usdt(fee)
# Create some test data
freqtradebot.enter_positions()
trade = Trade.query.first()
assert trade
trade.exit_reason = 'TESTSELL'
# Simulate fulfilled LIMIT_BUY order for trade
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow()
trade.is_open = False
context = MagicMock()
telegram._exit_reason_performance(update=update, context=context)
assert msg_mock.call_count == 1
assert 'Exit Reason Performance' in msg_mock.call_args_list[0][0][0]
assert '<code>roi\t9.842 USDT (10.00%) (1)</code>' in msg_mock.call_args_list[0][0][0]
context.args = ['XRP/USDT']
assert '<code>TESTSELL\t0.00006217 BTC (6.20%) (1)</code>' in msg_mock.call_args_list[0][0][0]
context.args = [trade.pair]
telegram._exit_reason_performance(update=update, context=context)
assert msg_mock.call_count == 2
@@ -1322,27 +1471,43 @@ def test_telegram_exit_reason_performance_handle(default_conf_usdt, update, tick
assert "Error" in msg_mock.call_args_list[0][0][0]
def test_telegram_mix_tag_performance_handle(default_conf_usdt, update, ticker, fee,
mocker) -> None:
def test_telegram_mix_tag_performance_handle(default_conf, update, ticker, fee,
limit_buy_order, limit_sell_order, mocker) -> None:
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
get_fee=fee,
)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot)
# Create some test data
create_mock_trades_usdt(fee)
freqtradebot.enter_positions()
trade = Trade.query.first()
assert trade
trade.enter_tag = "TESTBUY"
trade.exit_reason = "TESTSELL"
# Simulate fulfilled LIMIT_BUY order for trade
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow()
trade.is_open = False
context = MagicMock()
telegram._mix_tag_performance(update=update, context=context)
assert msg_mock.call_count == 1
assert 'Mix Tag Performance' in msg_mock.call_args_list[0][0][0]
assert ('<code>TEST3 roi\t9.842 USDT (10.00%) (1)</code>'
assert ('<code>TESTBUY TESTSELL\t0.00006217 BTC (6.20%) (1)</code>'
in msg_mock.call_args_list[0][0][0])
context.args = ['XRP/USDT']
context.args = [trade.pair]
telegram._mix_tag_performance(update=update, context=context)
assert msg_mock.call_count == 2
@@ -1682,7 +1847,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog, message_type,
leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else ''
assert msg_mock.call_args[0][0] == (
f'\N{LARGE BLUE CIRCLE} *Binance (dry):* {enter} ETH/BTC (#1)\n'
f'\N{LARGE BLUE CIRCLE} *Binance:* {enter} ETH/BTC (#1)\n'
f'*Enter Tag:* `{enter_signal}`\n'
'*Amount:* `1333.33333333`\n'
f'{leverage_text}'
@@ -1722,7 +1887,7 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker, message_type, en
'pair': 'ETH/BTC',
'reason': CANCEL_REASON['TIMEOUT']
})
assert (msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Binance (dry):* '
assert (msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Binance:* '
'Cancelling enter Order for ETH/BTC (#1). '
'Reason: cancelled due to timeout.')
@@ -1784,7 +1949,7 @@ def test_send_msg_entry_fill_notification(default_conf, mocker, message_type, en
})
leverage_text = f'*Leverage:* `{leverage}`\n' if leverage != 1.0 else ''
assert msg_mock.call_args[0][0] == (
f'\N{CHECK MARK} *Binance (dry):* {entered}ed ETH/BTC (#1)\n'
f'\N{CHECK MARK} *Binance:* {entered}ed ETH/BTC (#1)\n'
f'*Enter Tag:* `{enter_signal}`\n'
'*Amount:* `1333.33333333`\n'
f"{leverage_text}"
@@ -1822,7 +1987,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
'close_date': arrow.utcnow(),
})
assert msg_mock.call_args[0][0] == (
'\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n'
'\N{WARNING SIGN} *Binance:* Exiting KEY/ETH (#1)\n'
'*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n'
'*Enter Tag:* `buy_signal1`\n'
'*Exit Reason:* `stop_loss`\n'
@@ -1856,7 +2021,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
'close_date': arrow.utcnow(),
})
assert msg_mock.call_args[0][0] == (
'\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n'
'\N{WARNING SIGN} *Binance:* Exiting KEY/ETH (#1)\n'
'*Unrealized Profit:* `-57.41%`\n'
'*Enter Tag:* `buy_signal1`\n'
'*Exit Reason:* `stop_loss`\n'
@@ -1885,12 +2050,10 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
'reason': 'Cancelled on exchange'
})
assert msg_mock.call_args[0][0] == (
'\N{WARNING SIGN} *Binance (dry):* Cancelling exit Order for KEY/ETH (#1).'
'\N{WARNING SIGN} *Binance:* Cancelling exit Order for KEY/ETH (#1).'
' Reason: Cancelled on exchange.')
msg_mock.reset_mock()
# Test with live mode (no dry appendix)
telegram._config['dry_run'] = False
telegram.send_msg({
'type': RPCMessageType.EXIT_CANCEL,
'trade_id': 1,
@@ -1939,7 +2102,7 @@ def test_send_msg_sell_fill_notification(default_conf, mocker, direction,
leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else ''
assert msg_mock.call_args[0][0] == (
'\N{WARNING SIGN} *Binance (dry):* Exited KEY/ETH (#1)\n'
'\N{WARNING SIGN} *Binance:* Exited KEY/ETH (#1)\n'
'*Profit:* `-57.41%`\n'
f'*Enter Tag:* `{enter_signal}`\n'
'*Exit Reason:* `stop_loss`\n'
@@ -1995,7 +2158,6 @@ def test_send_msg_unknown_type(default_conf, mocker) -> None:
def test_send_msg_buy_notification_no_fiat(
default_conf, mocker, message_type, enter, enter_signal, leverage) -> None:
del default_conf['fiat_display_currency']
default_conf['dry_run'] = False
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
telegram.send_msg({
@@ -2065,7 +2227,7 @@ def test_send_msg_sell_notification_no_fiat(
leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else ''
assert msg_mock.call_args[0][0] == (
'\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n'
'\N{WARNING SIGN} *Binance:* Exiting KEY/ETH (#1)\n'
'*Unrealized Profit:* `-57.41%`\n'
f'*Enter Tag:* `{enter_signal}`\n'
'*Exit Reason:* `stop_loss`\n'

View File

@@ -1,6 +1,5 @@
# pragma pylint: disable=missing-docstring, C0103, protected-access
from datetime import datetime, timedelta
from unittest.mock import MagicMock
import pytest
@@ -8,7 +7,6 @@ from requests import RequestException
from freqtrade.enums import ExitType, RPCMessageType
from freqtrade.rpc import RPC
from freqtrade.rpc.discord import Discord
from freqtrade.rpc.webhook import Webhook
from tests.conftest import get_patched_freqtradebot, log_has
@@ -408,42 +406,3 @@ def test__send_msg_with_raw_format(default_conf, mocker, caplog):
webhook._send_msg(msg)
assert post.call_args[1] == {'data': msg['data'], 'headers': {'Content-Type': 'text/plain'}}
def test_send_msg_discord(default_conf, mocker):
default_conf["discord"] = {
'enabled': True,
'webhook_url': "https://webhookurl..."
}
msg_mock = MagicMock()
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
discord = Discord(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf)
msg = {
'type': RPCMessageType.EXIT_FILL,
'trade_id': 1,
'exchange': 'Binance',
'pair': 'ETH/BTC',
'direction': 'Long',
'gain': "profit",
'close_rate': 0.005,
'amount': 0.8,
'order_type': 'limit',
'open_date': datetime.now() - timedelta(days=1),
'close_date': datetime.now(),
'open_rate': 0.004,
'current_rate': 0.005,
'profit_amount': 0.001,
'profit_ratio': 0.20,
'stake_currency': 'BTC',
'enter_tag': 'enter_tagggg',
'exit_reason': ExitType.STOP_LOSS.value,
}
discord.send_msg(msg=msg)
assert msg_mock.call_count == 1
assert 'embeds' in msg_mock.call_args_list[0][0][0]
assert 'title' in msg_mock.call_args_list[0][0][0]['embeds'][0]
assert 'color' in msg_mock.call_args_list[0][0][0]['embeds'][0]
assert 'fields' in msg_mock.call_args_list[0][0][0]['embeds'][0]

View File

@@ -178,8 +178,8 @@ class StrategyTestV3(IStrategy):
return dataframe
def leverage(self, pair: str, current_time: datetime, current_rate: float,
proposed_leverage: float, max_leverage: float, entry_tag: Optional[str],
side: str, **kwargs) -> float:
proposed_leverage: float, max_leverage: float, side: str,
**kwargs) -> float:
# Return 3.0 in all cases.
# Bot-logic must make sure it's an allowed leverage and eventually adjust accordingly.

View File

@@ -1,175 +0,0 @@
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
import talib.abstract as ta
from pandas import DataFrame
import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.strategy import (BooleanParameter, DecimalParameter, IntParameter, IStrategy,
RealParameter)
class StrategyTestV3Analysis(IStrategy):
"""
Strategy used by tests freqtrade bot.
Please do not modify this strategy, it's intended for internal use only.
Please look at the SampleStrategy in the user_data/strategy directory
or strategy repository https://github.com/freqtrade/freqtrade-strategies
for samples and inspiration.
"""
INTERFACE_VERSION = 3
# Minimal ROI designed for the strategy
minimal_roi = {
"40": 0.0,
"30": 0.01,
"20": 0.02,
"0": 0.04
}
# Optimal stoploss designed for the strategy
stoploss = -0.10
# Optimal timeframe for the strategy
timeframe = '5m'
# Optional order type mapping
order_types = {
'entry': 'limit',
'exit': 'limit',
'stoploss': 'limit',
'stoploss_on_exchange': False
}
# Number of candles the strategy requires before producing valid signals
startup_candle_count: int = 20
# Optional time in force for orders
order_time_in_force = {
'entry': 'gtc',
'exit': 'gtc',
}
buy_params = {
'buy_rsi': 35,
# Intentionally not specified, so "default" is tested
# 'buy_plusdi': 0.4
}
sell_params = {
'sell_rsi': 74,
'sell_minusdi': 0.4
}
buy_rsi = IntParameter([0, 50], default=30, space='buy')
buy_plusdi = RealParameter(low=0, high=1, default=0.5, space='buy')
sell_rsi = IntParameter(low=50, high=100, default=70, space='sell')
sell_minusdi = DecimalParameter(low=0, high=1, default=0.5001, decimals=3, space='sell',
load=False)
protection_enabled = BooleanParameter(default=True)
protection_cooldown_lookback = IntParameter([0, 50], default=30)
# TODO: Can this work with protection tests? (replace HyperoptableStrategy implicitly ... )
# @property
# def protections(self):
# prot = []
# if self.protection_enabled.value:
# prot.append({
# "method": "CooldownPeriod",
# "stop_duration_candles": self.protection_cooldown_lookback.value
# })
# return prot
bot_started = False
def bot_start(self):
self.bot_started = True
def informative_pairs(self):
return []
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Momentum Indicator
# ------------------------------------
# ADX
dataframe['adx'] = ta.ADX(dataframe)
# MACD
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist']
# Minus Directional Indicator / Movement
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# Plus Directional Indicator / Movement
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
# RSI
dataframe['rsi'] = ta.RSI(dataframe)
# Stoch fast
stoch_fast = ta.STOCHF(dataframe)
dataframe['fastd'] = stoch_fast['fastd']
dataframe['fastk'] = stoch_fast['fastk']
# Bollinger bands
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe['bb_lowerband'] = bollinger['lower']
dataframe['bb_middleband'] = bollinger['mid']
dataframe['bb_upperband'] = bollinger['upper']
# EMA - Exponential Moving Average
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(dataframe['rsi'] < self.buy_rsi.value) &
(dataframe['fastd'] < 35) &
(dataframe['adx'] > 30) &
(dataframe['plus_di'] > self.buy_plusdi.value)
) |
(
(dataframe['adx'] > 65) &
(dataframe['plus_di'] > self.buy_plusdi.value)
),
['enter_long', 'enter_tag']] = 1, 'enter_tag_long'
dataframe.loc[
(
qtpylib.crossed_below(dataframe['rsi'], self.sell_rsi.value)
),
['enter_short', 'enter_tag']] = 1, 'enter_tag_short'
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(
(qtpylib.crossed_above(dataframe['rsi'], self.sell_rsi.value)) |
(qtpylib.crossed_above(dataframe['fastd'], 70))
) &
(dataframe['adx'] > 10) &
(dataframe['minus_di'] > 0)
) |
(
(dataframe['adx'] > 70) &
(dataframe['minus_di'] > self.sell_minusdi.value)
),
['exit_long', 'exit_tag']] = 1, 'exit_tag_long'
dataframe.loc[
(
qtpylib.crossed_above(dataframe['rsi'], self.buy_rsi.value)
),
['exit_long', 'exit_tag']] = 1, 'exit_tag_short'
return dataframe

View File

@@ -20,8 +20,7 @@ from freqtrade.strategy.hyper import detect_parameters
from freqtrade.strategy.parameters import (BaseParameter, BooleanParameter, CategoricalParameter,
DecimalParameter, IntParameter, RealParameter)
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from tests.conftest import (CURRENT_TEST_STRATEGY, TRADE_SIDES, create_mock_trades, log_has,
log_has_re)
from tests.conftest import CURRENT_TEST_STRATEGY, TRADE_SIDES, log_has, log_has_re
from .strats.strategy_test_v3 import StrategyTestV3
@@ -616,7 +615,6 @@ def test_leverage_callback(default_conf, side) -> None:
proposed_leverage=1.0,
max_leverage=5.0,
side=side,
entry_tag=None,
) == 1
default_conf['strategy'] = CURRENT_TEST_STRATEGY
@@ -628,7 +626,6 @@ def test_leverage_callback(default_conf, side) -> None:
proposed_leverage=1.0,
max_leverage=5.0,
side=side,
entry_tag='entry_tag_test',
) == 3
@@ -813,28 +810,6 @@ def test_strategy_safe_wrapper(value):
assert ret == value
@pytest.mark.usefixtures("init_persistence")
def test_strategy_safe_wrapper_trade_copy(fee):
create_mock_trades(fee)
def working_method(trade):
assert len(trade.orders) > 0
assert trade.orders
trade.orders = []
assert len(trade.orders) == 0
return trade
trade = Trade.get_open_trades()[0]
# Don't assert anything before strategy_wrapper.
# This ensures that relationship loading works correctly.
ret = strategy_safe_wrapper(working_method, message='DeadBeef')(trade=trade)
assert isinstance(ret, Trade)
assert id(trade) != id(ret)
# Did not modify the original order
assert len(trade.orders) > 0
assert len(ret.orders) == 0
def test_hyperopt_parameters():
from skopt.space import Categorical, Integer, Real
with pytest.raises(OperationalException, match=r"Name is determined.*"):

View File

@@ -34,7 +34,7 @@ def test_search_all_strategies_no_failed():
directory = Path(__file__).parent / "strats"
strategies = StrategyResolver.search_all_objects(directory, enum_failed=False)
assert isinstance(strategies, list)
assert len(strategies) == 6
assert len(strategies) == 5
assert isinstance(strategies[0], dict)
@@ -42,10 +42,10 @@ def test_search_all_strategies_with_failed():
directory = Path(__file__).parent / "strats"
strategies = StrategyResolver.search_all_objects(directory, enum_failed=True)
assert isinstance(strategies, list)
assert len(strategies) == 7
assert len(strategies) == 6
# with enum_failed=True search_all_objects() shall find 2 good strategies
# and 1 which fails to load
assert len([x for x in strategies if x['class'] is not None]) == 6
assert len([x for x in strategies if x['class'] is not None]) == 5
assert len([x for x in strategies if x['class'] is None]) == 1

View File

@@ -210,14 +210,13 @@ def test_edge_overrides_stoploss(limit_order, fee, caplog, mocker,
#
# 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(
'freqtrade.exchange.Exchange',
fetch_ticker=MagicMock(return_value=ticker_val),
fetch_ticker=MagicMock(return_value={
'bid': enter_price * buy_price_mult,
'ask': enter_price * buy_price_mult,
'last': enter_price * buy_price_mult,
}),
get_fee=fee,
)
#############################################
@@ -230,12 +229,9 @@ def test_edge_overrides_stoploss(limit_order, fee, caplog, mocker,
freqtrade.enter_positions()
trade = Trade.query.first()
caplog.clear()
oobj = Order.parse_from_ccxt_object(limit_order['buy'], 'ADA/USDT', 'buy')
trade.update_trade(oobj)
#############################################
ticker_val.update({
'bid': enter_price * buy_price_mult,
'ask': enter_price * buy_price_mult,
'last': enter_price * buy_price_mult,
})
# stoploss shoud be hit
assert freqtrade.handle_trade(trade) is not ignore_strat_sl
@@ -2151,7 +2147,7 @@ def test_handle_trade(
assert trade.close_rate == 2.0 if is_short else 2.2
assert trade.close_profit == close_profit
assert trade.calc_profit(trade.close_rate) == 5.685
assert trade.calc_profit() == 5.685
assert trade.close_date is not None
assert trade.exit_reason == 'sell_signal1'
@@ -3775,7 +3771,6 @@ def test_exit_profit_only(
trade = Trade.query.first()
assert trade.is_short == is_short
oobj = Order.parse_from_ccxt_object(limit_order[eside], limit_order[eside]['symbol'], eside)
trade.update_order(limit_order[eside])
trade.update_trade(oobj)
freqtrade.wallets.update()
if profit_only:
@@ -4064,7 +4059,6 @@ def test_trailing_stop_loss_positive(
trade = Trade.query.first()
assert trade.is_short == is_short
oobj = Order.parse_from_ccxt_object(limit_order[eside], limit_order[eside]['symbol'], eside)
trade.update_order(limit_order[eside])
trade.update_trade(oobj)
caplog.set_level(logging.DEBUG)
# stop-loss not reached
@@ -4808,19 +4802,10 @@ def test_startup_update_open_orders(mocker, default_conf_usdt, fee, caplog, is_s
assert len(Order.get_open_orders()) == 2
caplog.clear()
mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=ExchangeError)
mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=InvalidOrderException)
freqtrade.startup_update_open_orders()
assert log_has_re(r"Error updating Order .*", caplog)
mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=InvalidOrderException)
hto_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_timedout_order')
# Orders which are no longer found after X days should be assumed as canceled.
freqtrade.startup_update_open_orders()
assert log_has_re(r"Order is older than \d days.*", caplog)
assert hto_mock.call_count == 2
assert hto_mock.call_args_list[0][0][0]['status'] == 'canceled'
assert hto_mock.call_args_list[1][0][0]['status'] == 'canceled'
@pytest.mark.usefixtures("init_persistence")
@pytest.mark.parametrize("is_short", [False, True])

View File

@@ -606,9 +606,9 @@ def test_calc_open_close_trade_price(
trade.close_rate = 2.2
trade.recalc_open_trade_value()
assert isclose(trade._calc_open_trade_value(), open_value)
assert isclose(trade.calc_close_trade_value(trade.close_rate), close_value)
assert isclose(trade.calc_profit(trade.close_rate), round(profit, 8))
assert pytest.approx(trade.calc_profit_ratio(trade.close_rate)) == profit_ratio
assert isclose(trade.calc_close_trade_value(), close_value)
assert isclose(trade.calc_profit(), round(profit, 8))
assert pytest.approx(trade.calc_profit_ratio()) == profit_ratio
@pytest.mark.usefixtures("init_persistence")
@@ -660,7 +660,7 @@ def test_calc_close_trade_price_exception(limit_buy_order_usdt, fee):
trade.open_order_id = 'something'
oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
trade.update_trade(oobj)
assert trade.calc_close_trade_value(trade.close_rate) == 0.0
assert trade.calc_close_trade_value() == 0.0
@pytest.mark.usefixtures("init_persistence")
@@ -813,7 +813,7 @@ def test_calc_close_trade_price(
funding_fees=funding_fees
)
trade.open_order_id = 'close_trade'
assert round(trade.calc_close_trade_value(rate=close_rate), 8) == result
assert round(trade.calc_close_trade_value(rate=close_rate, fee=fee_rate), 8) == result
@pytest.mark.parametrize(
@@ -884,17 +884,6 @@ def test_calc_close_trade_price(
('binance', False, 3, 2.2, 0.0025, 4.684999, 0.23366583, futures, -1),
('binance', True, 1, 2.2, 0.0025, -7.315, -0.12222222, futures, -1),
('binance', True, 3, 2.2, 0.0025, -7.315, -0.36666666, futures, -1),
# FUTURES, funding_fee=0
('binance', False, 1, 2.1, 0.0025, 2.6925, 0.04476309, futures, 0),
('binance', False, 3, 2.1, 0.0025, 2.6925, 0.13428928, futures, 0),
('binance', True, 1, 2.1, 0.0025, -3.3074999, -0.05526316, futures, 0),
('binance', True, 3, 2.1, 0.0025, -3.3074999, -0.16578947, futures, 0),
('binance', False, 1, 1.9, 0.0025, -3.2925, -0.05473815, futures, 0),
('binance', False, 3, 1.9, 0.0025, -3.2925, -0.16421446, futures, 0),
('binance', True, 1, 1.9, 0.0025, 2.7075, 0.0452381, futures, 0),
('binance', True, 3, 1.9, 0.0025, 2.7075, 0.13571429, futures, 0),
])
@pytest.mark.usefixtures("init_persistence")
def test_calc_profit(
@@ -2075,24 +2064,6 @@ def test_get_trades_proxy(fee, use_db, is_short):
Trade.use_db = True
@pytest.mark.usefixtures("init_persistence")
@pytest.mark.parametrize('is_short', [True, False])
def test_get_trades__query(fee, is_short):
query = Trade.get_trades([])
# without orders there should be no join issued.
query1 = Trade.get_trades([], include_orders=False)
assert "JOIN orders" in str(query)
assert "JOIN orders" not in str(query1)
create_mock_trades(fee, is_short)
query = Trade.get_trades([])
query1 = Trade.get_trades([], include_orders=False)
assert "JOIN orders" in str(query)
assert "JOIN orders" not in str(query1)
def test_get_trades_backtest():
Trade.use_db = False
with pytest.raises(NotImplementedError, match=r"`Trade.get_trades\(\)` not .*"):
@@ -2287,7 +2258,6 @@ def test_Trade_object_idem():
'get_exit_reason_performance',
'get_enter_tag_performance',
'get_mix_tag_performance',
'get_trading_volume',
)
@@ -2717,7 +2687,5 @@ def test_order_to_ccxt(limit_buy_order_open):
del raw_order['fee']
del raw_order['datetime']
del raw_order['info']
assert raw_order['stopPrice'] is None
del raw_order['stopPrice']
del limit_buy_order_open['datetime']
assert raw_order == limit_buy_order_open